1use std::collections::{BTreeMap, BTreeSet};
14
15use serde::{Deserialize, Serialize};
16
17use super::{
18 require_one_plan, require_verified, Census, SpatialArtifact, SpatialBackend, SpatialLowerError,
19};
20use crate::codegen_sva::fabric_target::Precision;
21use crate::spatial::ir::*;
22use crate::spatial::passes::{LV_ITEM, LV_ITEM_X, LV_ITEM_Y, LV_SUB_H, LV_SUB_W};
23use crate::spatial::target::SpatialTarget;
24
25mod general;
26
27#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
28pub struct PersistentBudget {
29 pub smem_bytes: usize,
30 pub dynamic_opt_in: bool,
32 pub rings_dynamic: bool,
35 pub dyn_bytes: usize,
37 pub regs_est: usize,
38 pub blocks_per_sm: usize,
39 pub threads: usize,
40}
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct CudaArtifact {
44 pub program_cu: String,
45 pub bench_cu: String,
46 pub budget: PersistentBudget,
47 pub census: Census,
48 pub general: Option<Box<CudaArtifact>>,
51}
52
53pub struct CudaPersistentBackend {
54 pub target: SpatialTarget,
55}
56
57impl CudaPersistentBackend {
58 pub fn new(target: SpatialTarget) -> Self {
59 CudaPersistentBackend { target }
60 }
61}
62
63fn nwarp(target: &SpatialTarget) -> usize {
69 target.accumulator_lanes()
70}
71const WARP: usize = 32;
72
73fn align16(n: usize) -> usize {
75 (n + 15) & !15
76}
77
78fn wmma_types(operand: Precision) -> Option<(&'static str, &'static str, &'static str)> {
80 match operand {
81 Precision::Int8 => Some(("signed char", "int", "int")),
83 Precision::Bf16 => Some(("__nv_bfloat16", "float", "float")),
84 Precision::Fp16 => Some(("__half", "float", "float")),
85 _ => None,
86 }
87}
88
89struct Ctx<'a> {
90 p: &'a SpatialProgram,
91 te: usize,
92 tr: usize,
93 tc: usize,
94 grid_nx: usize,
95 rings: BTreeMap<BufferId, (usize, usize, &'static str)>,
97 operand: Precision,
98 acc_t: &'static str,
99 acc_shape: Option<(usize, usize)>,
102 fused_out: Option<BufferId>,
115 lane_grid: Option<(usize, usize)>,
118 lanes: usize,
120}
121
122fn ring(ctx: &Ctx, b: BufferId) -> BufferId {
123 ctx.p.buffer(b).origin.unwrap_or(b)
124}
125
126fn ring_of(p: &SpatialProgram, b: BufferId) -> BufferId {
128 p.buffer(b).origin.unwrap_or(b)
129}
130
131fn var_name(v: LoopVar) -> String {
132 match v {
133 x if x == LV_ITEM => "item".into(),
134 x if x == LV_SUB_H => "sh".into(),
135 x if x == LV_SUB_W => "sw".into(),
136 x => format!("lv{}", x.0),
137 }
138}
139
140fn addr_expr(at: &TileAddr, scope: &BTreeSet<LoopVar>) -> String {
142 let mut terms = vec![format!("{}", at.base)];
143 for (v, stride) in &at.steps {
144 if *stride == 0 {
145 continue;
146 }
147 let n = if *v == LV_ITEM_Y {
148 "oy".to_string()
149 } else if *v == LV_ITEM_X {
150 "ox".to_string()
151 } else if scope.contains(v) {
152 var_name(*v)
153 } else {
154 continue;
155 };
156 terms.push(format!("{n} * {stride}"));
157 }
158 terms.join(" + ")
159}
160
161struct Emit {
162 s: String,
163 ind: usize,
164}
165
166impl Emit {
167 fn l(&mut self, line: impl AsRef<str>) {
168 for _ in 0..self.ind {
169 self.s.push_str(" ");
170 }
171 self.s.push_str(line.as_ref());
172 self.s.push('\n');
173 }
174 fn open(&mut self, line: impl AsRef<str>) {
175 self.l(format!("{} {{", line.as_ref()));
176 self.ind += 1;
177 }
178 fn close(&mut self) {
179 self.ind -= 1;
180 self.l("}");
181 }
182}
183
184fn ring_op(ctx: &Ctx, op: Option<&TaggedOp>, pop: bool) -> bool {
187 match op.map(|t| &t.op) {
188 Some(ChoreoOp::Wait { buf, .. }) if !pop => ctx.fused_out != Some(ring(ctx, *buf)),
189 Some(ChoreoOp::Pop { buf, .. }) if pop => ctx.fused_out != Some(ring(ctx, *buf)),
190 _ => false,
191 }
192}
193
194fn emit_ops(
197 ctx: &Ctx,
198 e: &mut Emit,
199 ops: &[TaggedOp],
200 scope: &mut BTreeSet<LoopVar>,
201 in_flight: usize,
202) -> Result<(), SpatialLowerError> {
203 for (i, t) in ops.iter().enumerate() {
204 let next = ops.get(i + 1);
205 match &t.op {
206 ChoreoOp::Reserve { buf, .. } if ctx.fused_out == Some(ring(ctx, *buf)) => {}
209 ChoreoOp::Push { buf, .. } if ctx.fused_out == Some(ring(ctx, *buf)) => {}
210 ChoreoOp::Wait { buf, .. } if ctx.fused_out == Some(ring(ctx, *buf)) => {}
211 ChoreoOp::Pop { buf, .. } if ctx.fused_out == Some(ring(ctx, *buf)) => {}
212 ChoreoOp::Reserve { buf, .. } => e.l(format!("const int wslot_{0} = wctr_{0} % DEPTH_{0}; ++wctr_{0};", ring(ctx, *buf).idx())),
213 ChoreoOp::Push { .. } => e.l("__pipeline_commit();"),
214 ChoreoOp::Wait { buf, .. } => {
217 e.l(format!("__pipeline_wait_prior({in_flight}); const int rslot_{0} = rctr_{0} % DEPTH_{0};", ring(ctx, *buf).idx()));
218 if !ring_op(ctx, next, false) {
219 e.l("__syncthreads();");
220 }
221 }
222 ChoreoOp::Pop { buf, .. } => {
223 e.l(format!("++rctr_{};", ring(ctx, *buf).idx()));
224 if !ring_op(ctx, next, true) {
225 e.l("__syncthreads();");
226 }
227 }
228 ChoreoOp::ReadBarrier | ChoreoOp::WriteBarrier | ChoreoOp::Flush { .. } => {}
229 ChoreoOp::ReadTiles { tensor, at, block_h, block_w, stride_w, stride_h, into } => {
233 let r = ring(ctx, *into).idx();
234 let te = ctx.te;
235 let base = addr_expr(at, scope);
236 e.open("");
237 e.l(format!("constexpr int chunks = (int)(sizeof(*ring_{r}) * {te} / 16);"));
238 e.l(format!("static_assert(sizeof(*ring_{r}) * {te} % 16 == 0, \"a tile is a whole number of 16-byte chunks\");"));
239 e.open(format!("for (int c = tid; c < {block_h} * {block_w} * chunks; c += NTHREAD)"));
240 e.l(format!("const int tl = c / chunks, off = c % chunks, bh = tl / {block_w}, bw = tl % {block_w};"));
241 e.l(format!("const long long tile = ({base}) + (long long)bh * {stride_h} + (long long)bw * {stride_w};"));
242 e.l(format!("__pipeline_memcpy_async(reinterpret_cast<char*>(&ring_{r}[wslot_{r} * PAGE_{r} + tl * {te}]) + off * 16, reinterpret_cast<const char*>(&t{}[tile * {te}]) + off * 16, 16);", tensor.idx()));
243 e.close();
244 e.close();
245 }
246 ChoreoOp::WriteTiles { from, tensor, at, block_w, stride_w, stride_h, .. }
250 if ctx.fused_out == Some(ring(ctx, *from)) =>
251 {
252 let te = ctx.te;
253 let base = addr_expr(at, scope);
254 let (osh, osw) = ctx.acc_shape.expect("a fused epilogue knows its block shape");
255 let (wm, wn) = ctx.lane_grid.expect("a fused epilogue has an even lane grid");
256 let (rm, rn) = (osh / wm, osw / wn);
257 let acct = ctx.acc_t;
258 e.l(format!("const int ewi = warp / {wn}, ewj = warp % {wn};"));
261 e.l("const int lane = tid % 32;");
262 e.open(format!("for (int i = 0; i < {rm}; ++i)"));
263 e.open(format!("for (int j = 0; j < {rn}; ++j)"));
264 e.l(format!("const int bh = ewi * {rm} + i, bw = ewj * {rn} + j;"));
265 e.l(format!("const long long tile = ({base}) + (long long)bh * {stride_h} + (long long)bw * {stride_w};"));
266 e.l(format!("nvcuda::wmma::store_matrix_sync(&epi_scratch[warp * {te}], accf[i * {rn} + j], {}, nvcuda::wmma::mem_row_major);", ctx.tc));
267 e.l("__syncwarp();");
270 e.open(format!("for (int el = lane; el < {te}; el += 32)"));
271 e.l(format!("t{}[tile * {te} + el] = (long long) epi_scratch[warp * {te} + el];", tensor.idx()));
272 e.close();
273 e.l("__syncwarp();");
274 e.close();
275 e.close();
276 let _ = (block_w, acct);
277 }
278 ChoreoOp::WriteTiles { from, tensor, at, block_h, block_w, stride_w, stride_h } => {
279 let r = ring(ctx, *from).idx();
280 let te = ctx.te;
281 let base = addr_expr(at, scope);
282 e.open(format!("for (int bh = 0; bh < {block_h}; ++bh)"));
283 e.open(format!("for (int bw = 0; bw < {block_w}; ++bw)"));
284 e.l(format!("const long long tile = ({base}) + (long long)bh * {stride_h} + (long long)bw * {stride_w};"));
285 e.open(format!("for (int el = tid; el < {te}; el += NTHREAD)"));
286 e.l(format!("t{}[tile * {te} + el] = (long long)ring_{r}[rslot_{r} * PAGE_{r} + (bh * {block_w} + bw) * {te} + el];", tensor.idx()));
287 e.close();
288 e.close();
289 e.close();
290 }
291 ChoreoOp::AcquireAcc { .. } => {
292 e.open("for (int q = 0; q < PERW; ++q)");
293 e.l("nvcuda::wmma::fill_fragment(accf[q], 0);");
294 e.close();
295 }
296 ChoreoOp::ReleaseAcc => {}
297 ChoreoOp::Compute { op, ins, acc_reload } => match op {
298 ComputeOp::MatmulTiles { block, sub_blocks, .. } => {
299 if acc_reload.is_some() {
300 return Err(SpatialLowerError::Unsupported("an accumulator spill on the GPU (the fragments never leave the registers)"));
301 }
302 let BlockShape { osh, osw, ibw } = *block;
303 let per_site_n = sub_blocks.1 * osw;
304 let (ra, rb) = (ring(ctx, ins[0]).idx(), ring(ctx, ins[1]).idx());
305 let (te, tc) = (ctx.te, ctx.tc);
306 let sh = if scope.contains(&LV_SUB_H) { "sh" } else { "0" };
308 let sw = if scope.contains(&LV_SUB_W) { "sw" } else { "0" };
309 match ctx.lane_grid {
310 Some((wm, wn)) => {
313 let (rm, rn) = (osh / wm, osw / wn);
314 e.l(format!("const int wi = warp / {wn}, wj = warp % {wn};"));
315 e.open(format!("for (int kk = 0; kk < {ibw}; ++kk)"));
316 e.l(format!("nvcuda::wmma::fragment<nvcuda::wmma::matrix_a, 16, 16, 16, OPT, nvcuda::wmma::row_major> af[{rm}];"));
317 e.l(format!("nvcuda::wmma::fragment<nvcuda::wmma::matrix_b, 16, 16, 16, OPT, nvcuda::wmma::row_major> bf[{rn}];"));
318 e.open(format!("for (int i = 0; i < {rm}; ++i)"));
319 e.l(format!("nvcuda::wmma::load_matrix_sync(af[i], &ring_{ra}[rslot_{ra} * PAGE_{ra} + (({sh} * {osh} + wi * {rm} + i) * {ibw} + kk) * {te}], {tc});"));
320 e.close();
321 e.open(format!("for (int j = 0; j < {rn}; ++j)"));
322 e.l(format!("nvcuda::wmma::load_matrix_sync(bf[j], &ring_{rb}[rslot_{rb} * PAGE_{rb} + (kk * {per_site_n} + {sw} * {osw} + wj * {rn} + j) * {te}], {tc});"));
323 e.close();
324 e.open(format!("for (int i = 0; i < {rm}; ++i)"));
325 e.open(format!("for (int j = 0; j < {rn}; ++j)"));
326 e.l(format!("nvcuda::wmma::mma_sync(accf[i * {rn} + j], af[i], bf[j], accf[i * {rn} + j]);"));
327 e.close();
328 e.close();
329 e.close();
330 }
331 None => {
333 e.open("for (int q = 0; q < PERW; ++q)");
334 e.l(format!("const int ot = warp + q * {};", ctx.lanes));
335 e.l(format!("if (ot >= {}) break;", osh * osw));
336 e.l(format!("const int oi = ot / {osw}, oj = ot % {osw};"));
337 e.open(format!("for (int kk = 0; kk < {ibw}; ++kk)"));
338 e.l("nvcuda::wmma::fragment<nvcuda::wmma::matrix_a, 16, 16, 16, OPT, nvcuda::wmma::row_major> af;");
339 e.l("nvcuda::wmma::fragment<nvcuda::wmma::matrix_b, 16, 16, 16, OPT, nvcuda::wmma::row_major> bf;");
340 e.l(format!("nvcuda::wmma::load_matrix_sync(af, &ring_{ra}[rslot_{ra} * PAGE_{ra} + (({sh} * {osh} + oi) * {ibw} + kk) * {te}], {tc});"));
341 e.l(format!("nvcuda::wmma::load_matrix_sync(bf, &ring_{rb}[rslot_{rb} * PAGE_{rb} + (kk * {per_site_n} + {sw} * {osw} + oj) * {te}], {tc});"));
342 e.l("nvcuda::wmma::mma_sync(accf[q], af, bf, accf[q]);");
343 e.close();
344 e.close();
345 }
346 }
347 }
348 other => return Err(SpatialLowerError::Unsupported(compute_name(other))),
349 },
350 ChoreoOp::PackAcc { to, .. } if ctx.fused_out == Some(ring(ctx, *to)) => {}
351 ChoreoOp::PackAcc { to, tiles, accumulate, .. } => {
352 if *accumulate {
353 return Err(SpatialLowerError::Unsupported("an accumulating epilogue on the GPU (the fragments never leave the registers)"));
354 }
355 let r = ring(ctx, *to).idx();
356 let te = ctx.te;
357 match ctx.acc_shape.and_then(|(osh, osw)| ctx.lane_grid.map(|g| (osh, osw, g))) {
361 Some((_osh, osw, (wn_m, wn_n))) if *tiles == _osh * osw => {
362 let (rm, rn) = (_osh / wn_m, osw / wn_n);
363 e.l(format!("const int pwi = warp / {wn_n}, pwj = warp % {wn_n};"));
364 e.open(format!("for (int i = 0; i < {rm}; ++i)"));
365 e.open(format!("for (int j = 0; j < {rn}; ++j)"));
366 e.l(format!("nvcuda::wmma::store_matrix_sync(&ring_{r}[wslot_{r} * PAGE_{r} + ((pwi * {rm} + i) * {osw} + pwj * {rn} + j) * {te}], accf[i * {rn} + j], {}, nvcuda::wmma::mem_row_major);", ctx.tc));
367 e.close();
368 e.close();
369 }
370 _ => {
371 e.open("for (int q = 0; q < PERW; ++q)");
372 e.l(format!("const int ot = warp + q * {};", ctx.lanes));
373 e.l(format!("if (ot >= {tiles}) break;"));
374 e.l(format!("nvcuda::wmma::store_matrix_sync(&ring_{r}[wslot_{r} * PAGE_{r} + ot * {te}], accf[q], {}, nvcuda::wmma::mem_row_major);", ctx.tc));
375 e.close();
376 }
377 }
378 }
379 ChoreoOp::SemSet { .. } | ChoreoOp::SemInc { .. } | ChoreoOp::SemWait { .. } | ChoreoOp::McastSetValid { .. } | ChoreoOp::McastSend { .. } => {
380 return Err(SpatialLowerError::Unsupported("a multicast handshake on sm_86 (no cluster-wide multicast)"))
381 }
382 ChoreoOp::Barrier { .. } => return Err(SpatialLowerError::Unsupported("a group barrier on the GPU lowering")),
383 ChoreoOp::Loop { count: LoopCount::Trip { .. }, .. } => {
384 return Err(SpatialLowerError::Unsupported("a loop whose count the program reads from its data (a trip) — the persistent kernel's loop bounds are fixed when it is printed"))
385 }
386 ChoreoOp::Loop { var, count, body } => {
387 let v = var_name(*var);
388 let bound = if *var == LV_ITEM { "n_items".to_string() } else { count.bound().to_string() };
389 if let Some((prod, cons)) = rotation(ctx, body) {
408 let n = count.bound();
409 let commits = commits_of(ctx, &prod);
410 e.open("");
411 e.l(format!("const int {v} = 0;"));
412 emit_ops(ctx, e, &prod, &mut scope.clone(), 0)?;
413 e.close();
414 e.open(format!("for (int {v} = 0; {v} < {bound}; ++{v})"));
415 scope.insert(*var);
416 e.open(format!("if ({v} + 1 < {n})"));
417 e.l(format!("const int {v}_pf = {v} + 1;"));
418 e.open("");
419 e.l(format!("const int {v} = {v}_pf;"));
420 emit_ops(ctx, e, &prod, &mut scope.clone(), 0)?;
421 e.close();
422 e.close();
423 if commits > 0 {
424 e.open("else");
425 for _ in 0..commits {
426 e.l("__pipeline_commit();");
427 }
428 e.close();
429 }
430 emit_ops(ctx, e, &cons, scope, commits)?;
431 e.close();
432 scope.remove(var);
433 continue;
434 }
435 e.open(format!("for (int {v} = 0; {v} < {bound}; ++{v})"));
436 if *var == LV_ITEM {
437 e.l("const int item_id = ITEM_LIST[item_off + item];");
438 e.l(format!("const int oy = item_id / {}, ox = item_id % {};", ctx.grid_nx.max(1), ctx.grid_nx.max(1)));
439 }
440 scope.insert(*var);
441 emit_ops(ctx, e, body, scope, 0)?;
442 scope.remove(var);
443 e.close();
444 }
445 }
446 }
447 Ok(())
448}
449
450fn rotation(ctx: &Ctx, body: &[TaggedOp]) -> Option<(Vec<TaggedOp>, Vec<TaggedOp>)> {
454 let (prod, cons) = pipelined_halves(body)?;
455 let mut pages: BTreeMap<BufferId, usize> = BTreeMap::new();
456 for t in &prod {
457 if let ChoreoOp::Reserve { buf, .. } = &t.op {
458 *pages.entry(ring(ctx, *buf)).or_default() += 1;
459 }
460 }
461 let fits = pages.iter().all(|(r, n)| {
462 ctx.fused_out == Some(*r) || ctx.rings.get(r).is_some_and(|(depth, ..)| 2 * n <= *depth)
463 });
464 fits.then_some((prod, cons))
465}
466
467fn commits_of(ctx: &Ctx, ops: &[TaggedOp]) -> usize {
469 ops.iter().filter(|t| matches!(&t.op, ChoreoOp::Push { buf, .. } if ctx.fused_out != Some(ring(ctx, *buf)))).count()
470}
471
472fn ring_occupancy(
482 ctx: &Ctx,
483 ops: &[TaggedOp],
484 held: &mut BTreeMap<BufferId, usize>,
485) -> Result<(), SpatialLowerError> {
486 for t in ops {
487 match &t.op {
488 ChoreoOp::Reserve { buf, pages }
489 | ChoreoOp::Push { buf, pages }
490 | ChoreoOp::Wait { buf, pages }
491 | ChoreoOp::Pop { buf, pages }
492 if ctx.fused_out != Some(ring(ctx, *buf)) && *pages != 1 =>
493 {
494 return Err(SpatialLowerError::Unsupported("a FIFO op that moves more than one page at once (the persistent kernel's ring slots are one page each)"));
495 }
496 ChoreoOp::Reserve { buf, .. } if ctx.fused_out != Some(ring(ctx, *buf)) => {
497 let r = ring(ctx, *buf);
498 let depth = ctx.rings.get(&r).map(|(d, ..)| *d).unwrap_or(0);
499 let n = held.entry(r).or_default();
500 *n += 1;
501 if *n > depth {
502 return Err(SpatialLowerError::TooBig {
503 what: "pages of a ring held before its consumer drains them",
504 need: *n,
505 have: depth,
506 });
507 }
508 }
509 ChoreoOp::Pop { buf, .. } if ctx.fused_out != Some(ring(ctx, *buf)) => {
510 let r = ring(ctx, *buf);
511 if let Some(n) = held.get_mut(&r) {
512 *n -= 1;
513 if *n == 0 {
514 held.remove(&r);
515 }
516 }
517 }
518 ChoreoOp::Loop { count, body, .. } => {
519 let entry = held.clone();
520 let once = matches!(count, LoopCount::Const(1));
521 match rotation(ctx, body) {
522 Some((prod, cons)) if !once => {
523 ring_occupancy(ctx, &prod, held)?;
524 let steady = held.clone();
525 ring_occupancy(ctx, &prod, held)?;
526 ring_occupancy(ctx, &cons, held)?;
527 if *held != steady {
528 return Err(SpatialLowerError::Unsupported(
529 "a rotated loop whose iterations do not drain what they fill",
530 ));
531 }
532 ring_occupancy(ctx, &cons, held)?;
533 }
534 _ => ring_occupancy(ctx, body, held)?,
535 }
536 if !once && *held != entry {
537 return Err(SpatialLowerError::Unsupported("a loop whose iterations do not drain what they fill (a ring with no flow control would overrun)"));
538 }
539 }
540 _ => {}
541 }
542 }
543 Ok(())
544}
545
546fn compute_name(op: &ComputeOp) -> &'static str {
547 match op {
548 ComputeOp::Generic { .. } => {
549 "the general compute form on the GPU lowering (S2 gives it an arm)"
550 }
551 ComputeOp::Requant { .. } => "a requant stage on the GPU lowering",
552 ComputeOp::Bias { .. } => "a bias stage on the GPU lowering",
553 ComputeOp::Relu => "a relu stage on the GPU lowering",
554 ComputeOp::Scalar(_) => "a scalar stage on the GPU lowering",
555 ComputeOp::MatmulTiles { .. } => "a matmul stage",
556 }
557}
558
559fn merge_programs(progs: &[Vec<TaggedOp>], site: Site) -> Result<Vec<TaggedOp>, SpatialLowerError> {
565 fn next_loop(ops: &[TaggedOp]) -> Option<usize> {
567 ops.iter()
568 .position(|t| matches!(t.op, ChoreoOp::Loop { .. }))
569 }
570 let mut heads: Vec<&[TaggedOp]> = progs.iter().map(|v| v.as_slice()).collect();
571 let mut out = Vec::new();
572 loop {
573 let firsts: Vec<Option<(usize, LoopVar, LoopCount)>> = heads
583 .iter()
584 .map(|h| {
585 next_loop(h).and_then(|i| match &h[i].op {
586 ChoreoOp::Loop { var, count, .. } => Some((i, *var, *count)),
587 _ => None,
588 })
589 })
590 .collect();
591 let shared: std::collections::BTreeSet<LoopVar> = firsts
592 .iter()
593 .flatten()
594 .filter(|(_, v, _)| firsts.iter().flatten().filter(|(_, w, _)| w == v).count() > 1)
595 .map(|(_, v, _)| *v)
596 .collect();
597 let cuts: Vec<usize> = heads
598 .iter()
599 .zip(&firsts)
600 .map(|(h, f)| match f {
601 Some((i, v, c)) if shared.contains(v) || !matches!(c, LoopCount::Const(1)) => *i,
602 Some((i, _, _)) => next_loop(&h[i + 1..]).map(|j| i + 1 + j).unwrap_or(h.len()),
603 None => h.len(),
604 })
605 .collect();
606 let segs: Vec<Vec<TaggedOp>> = heads
607 .iter()
608 .zip(&cuts)
609 .map(|(h, c)| h[..*c].to_vec())
610 .collect();
611 let tails: Vec<&[TaggedOp]> = heads.iter().zip(&cuts).map(|(h, c)| &h[*c..]).collect();
614 out.extend(order_segment(&segs, &tails, site)?);
615 let loops: Vec<Option<(LoopVar, LoopCount, Vec<TaggedOp>, StageId)>> = heads
617 .iter()
618 .zip(&cuts)
619 .map(|(h, c)| match h.get(*c) {
620 Some(TaggedOp {
621 stage,
622 op: ChoreoOp::Loop { var, count, body },
623 }) => Some((*var, *count, body.clone(), *stage)),
624 _ => None,
625 })
626 .collect();
627 if loops.iter().all(|l| l.is_none()) {
628 return Ok(out);
629 }
630 let var = loops.iter().flatten().map(|(v, ..)| *v).next().unwrap();
631 if loops.iter().flatten().any(|(v, ..)| *v != var) {
632 return Err(SpatialLowerError::Unsupported(
633 "engine programs whose loop nests do not align (the GPU runs one block per site)",
634 ));
635 }
636 let (count, stage) = loops
637 .iter()
638 .flatten()
639 .map(|(_, c, _, s)| (*c, *s))
640 .next()
641 .unwrap();
642 let bodies: Vec<Vec<TaggedOp>> = loops
643 .iter()
644 .flatten()
645 .map(|(_, _, b, _)| b.clone())
646 .collect();
647 let body = merge_programs(&bodies, site)?;
648 out.push(TaggedOp {
649 stage,
650 op: ChoreoOp::Loop { var, count, body },
651 });
652 heads = heads
653 .iter()
654 .zip(&cuts)
655 .zip(&loops)
656 .map(|((h, c), l)| {
657 if l.is_some() {
658 &h[*c + 1..]
659 } else {
660 &h[h.len()..]
661 }
662 })
663 .collect();
664 }
665}
666
667fn pushes_buf(ops: &[TaggedOp], buf: BufferId) -> bool {
676 ops.iter().any(|t| match &t.op {
677 ChoreoOp::Push { buf: b, .. } => *b == buf,
678 ChoreoOp::Loop { body, .. } => pushes_buf(body, buf),
679 _ => false,
680 })
681}
682
683fn raises_sem(ops: &[TaggedOp], sem: SemId, site: Site) -> bool {
685 ops.iter().any(|t| match &t.op {
686 ChoreoOp::SemInc { sem: s, at, .. } => *s == sem && *at == site,
687 ChoreoOp::SemSet { sem: s, .. } => *s == sem,
688 ChoreoOp::Loop { body, .. } => raises_sem(body, sem, site),
689 _ => false,
690 })
691}
692
693fn collect_pushes(ops: &[TaggedOp], out: &mut std::collections::BTreeSet<usize>) {
695 for t in ops {
696 match &t.op {
697 ChoreoOp::Push { buf, .. } => {
698 out.insert(buf.idx());
699 }
700 ChoreoOp::Loop { body, .. } => collect_pushes(body, out),
701 _ => {}
702 }
703 }
704}
705
706fn collect_waits(ops: &[TaggedOp], out: &mut std::collections::BTreeSet<usize>) {
708 for t in ops {
709 match &t.op {
710 ChoreoOp::Wait { buf, .. } => {
711 out.insert(buf.idx());
712 }
713 ChoreoOp::Loop { body, .. } => collect_waits(body, out),
714 _ => {}
715 }
716 }
717}
718
719fn order_segment(
720 segs: &[Vec<TaggedOp>],
721 tails: &[&[TaggedOp]],
722 site: Site,
723) -> Result<Vec<TaggedOp>, SpatialLowerError> {
724 let mut idx = vec![0usize; segs.len()];
725 let mut out = Vec::new();
726 let pending_push =
727 |segs: &[Vec<TaggedOp>], idx: &[usize], buf: BufferId, skip: usize| -> bool {
728 segs.iter().enumerate().any(|(i, s)| {
729 i != skip
730 && (pushes_buf(&s[idx[i]..], buf)
731 || tails.get(i).map(|t| pushes_buf(t, buf)).unwrap_or(false))
732 })
733 };
734 let pending_raise = |segs: &[Vec<TaggedOp>], idx: &[usize], sem: SemId, skip: usize| -> bool {
737 segs.iter().enumerate().any(|(i, s)| {
738 i != skip
739 && (raises_sem(&s[idx[i]..], sem, site) || tails.get(i).map(|t| raises_sem(t, sem, site)).unwrap_or(false))
740 })
741 };
742 loop {
743 let mut progressed = false;
744 for i in 0..segs.len() {
745 while idx[i] < segs[i].len() {
746 let blocked = match &segs[i][idx[i]].op {
747 ChoreoOp::Wait { buf, .. } => pending_push(segs, &idx, *buf, i),
748 ChoreoOp::SemWait { sem, .. } => pending_raise(segs, &idx, *sem, i),
749 _ => false,
750 };
751 if blocked {
752 break;
753 }
754 out.push(segs[i][idx[i]].clone());
755 idx[i] += 1;
756 progressed = true;
757 }
758 }
759 if idx.iter().zip(segs).all(|(k, s)| *k == s.len()) {
760 return Ok(out);
761 }
762 if !progressed {
763 return Err(SpatialLowerError::Unsupported(
764 "engine programs the block cannot serialize without deadlock",
765 ));
766 }
767 }
768}
769
770fn pipelined_halves(body: &[TaggedOp]) -> Option<(Vec<TaggedOp>, Vec<TaggedOp>)> {
793 let is_producer = |op: &ChoreoOp| {
794 matches!(
795 op,
796 ChoreoOp::Reserve { .. }
797 | ChoreoOp::Push { .. }
798 | ChoreoOp::ReadTiles { .. }
799 | ChoreoOp::ReadBarrier { .. }
800 )
801 };
802 let is_consumer = |op: &ChoreoOp| {
803 matches!(
804 op,
805 ChoreoOp::Wait { .. } | ChoreoOp::Pop { .. } | ChoreoOp::Compute { .. }
806 )
807 };
808 let split = body.iter().position(|t| !is_producer(&t.op))?;
809 if split == 0 || split == body.len() {
810 return None;
811 }
812 if !body[split..].iter().all(|t| is_consumer(&t.op)) {
813 return None;
814 }
815 Some((body[..split].to_vec(), body[split..].to_vec()))
816}
817
818fn per_warp_tiles(p: &SpatialProgram, lanes: usize) -> usize {
819 let mut max = 0usize;
820 if let Some(sch) = &p.schedule {
821 for prog in &sch.programs {
822 walk_ops(
823 &prog.ops.iter().map(|t| t.op.clone()).collect::<Vec<_>>(),
824 1,
825 &mut |op, _| {
826 if let ChoreoOp::Compute {
827 op: ComputeOp::MatmulTiles { block, .. },
828 ..
829 } = op
830 {
831 max = max.max(block.osh * block.osw);
832 }
833 if let ChoreoOp::PackAcc { tiles, .. } = op {
834 max = max.max(*tiles);
835 }
836 },
837 );
838 }
839 }
840 max.div_ceil(lanes).max(1)
841}
842
843impl SpatialBackend for CudaPersistentBackend {
844 fn name(&self) -> &str {
845 self.target.name
846 }
847 fn target(&self) -> &SpatialTarget {
848 &self.target
849 }
850 fn lower(&self, p: &SpatialProgram) -> Result<SpatialArtifact, SpatialLowerError> {
851 require_verified(p)?;
852 if general::wants_general(p) {
855 return general::lower(p, &self.target).map(SpatialArtifact::Cuda);
856 }
857 require_one_plan(p)?;
858 if p.has_indirect_access() {
859 return Err(SpatialLowerError::Unsupported("an indirect (data-dependent) tile or element access on the GPU lowering (gathers are B-P5)"));
860 }
861 let pl = p
862 .placement
863 .as_ref()
864 .ok_or(SpatialLowerError::Unsupported("a placed program"))?;
865 if let Some(g) = pl.groups.first() {
866 return Err(SpatialLowerError::GroupSpansSites { group: g.id });
867 }
868 if !p.semaphores.is_empty() {
869 return Err(SpatialLowerError::Unsupported(
870 "a semaphore handshake on sm_86 (no cluster-wide multicast)",
871 ));
872 }
873 let sched = p
874 .schedule
875 .as_ref()
876 .ok_or(SpatialLowerError::Unsupported("a scheduled program"))?;
877 let plan = p
878 .work
879 .plan
880 .ok_or(SpatialLowerError::Unsupported("a tiled program"))?;
881 if p.tile.rows != 16 || p.tile.cols != 16 {
882 return Err(SpatialLowerError::Unsupported(
883 "a tile shape other than WMMA's 16×16",
884 ));
885 }
886 let operand = p
888 .stages
889 .iter()
890 .find_map(|s| match &s.kind {
891 StageKind::Compute(ComputeOp::MatmulTiles { operand, .. }) => Some(*operand),
892 _ => None,
893 })
894 .ok_or(SpatialLowerError::Unsupported(
895 "a program without a tile matmul",
896 ))?;
897 let (opt, acct, _) =
898 wmma_types(operand).ok_or(SpatialLowerError::Precision { needed: operand })?;
899 if operand == Precision::Int8 {
902 let k = p.work.k_tiles as i128 * p.tile.cols as i128;
903 if k * 127 * 127 >= i32::MAX as i128 {
904 return Err(SpatialLowerError::TooBig {
905 what: "an int8 reduction that overflows the int32 fragment accumulator",
906 need: (k * 127 * 127) as usize,
907 have: i32::MAX as usize,
908 });
909 }
910 }
911 let mut rings: BTreeMap<BufferId, (usize, usize, &'static str)> = BTreeMap::new();
912 for b in &p.buffers {
913 let (depth, page) = match b.kind {
914 BufferKind::Fifo {
915 depth_pages,
916 page_tiles,
917 }
918 | BufferKind::Interm {
919 depth_pages,
920 page_tiles,
921 } => (depth_pages, page_tiles * p.tile.elems()),
922 BufferKind::Accumulator { .. } => continue,
923 };
924 let ty = if p
926 .stages
927 .iter()
928 .any(|s| s.outs.contains(&b.id) && matches!(s.kind, StageKind::Compute(_)))
929 {
930 acct
931 } else {
932 opt
933 };
934 rings.insert(b.origin.unwrap_or(b.id), (depth, page, ty));
935 }
936 let acc_shape = Some((plan.block.osh, plan.block.osw));
937 let lane_grid = self
941 .target
942 .accumulator_lane_grid(plan.block.osh, plan.block.osw);
943 let fused_out = if self.target.accumulator_in_registers() && lane_grid.is_some() {
944 p.output_tensor()
945 .and_then(|_| {
946 p.stages.iter().find(|s| {
947 matches!(s.kind, StageKind::Compute(ComputeOp::MatmulTiles { .. }))
948 })
949 })
950 .and_then(|s| s.outs.first().copied())
951 .map(|b| ring_of(p, b))
952 } else {
953 None
954 };
955 let ctx = Ctx {
956 p,
957 te: p.tile.elems(),
958 tr: p.tile.rows,
959 tc: p.tile.cols,
960 grid_nx: plan.grid.1,
961 rings,
962 operand,
963 acc_t: acct,
964 acc_shape,
965 fused_out,
966 lane_grid,
967 lanes: nwarp(&self.target),
968 };
969 let sites: Vec<Site> = pl.items_of_site.iter().map(|(s, _)| *s).collect();
971 let mut site_progs: Vec<Vec<TaggedOp>> = sched
976 .programs
977 .iter()
978 .filter(|pr| pr.site == sites[0])
979 .map(|pr| pr.ops.clone())
980 .collect();
981 let fills: Vec<std::collections::BTreeSet<usize>> = site_progs
982 .iter()
983 .map(|ops| {
984 let mut f = Default::default();
985 collect_pushes(ops, &mut f);
986 f
987 })
988 .collect();
989 let drains: Vec<std::collections::BTreeSet<usize>> = site_progs
990 .iter()
991 .map(|ops| {
992 let mut d = Default::default();
993 collect_waits(ops, &mut d);
994 d
995 })
996 .collect();
997 let mut order: Vec<usize> = (0..site_progs.len()).collect();
998 order.sort_by(|a, b| {
999 let a_feeds_b = !fills[*a].is_disjoint(&drains[*b]);
1000 let b_feeds_a = !fills[*b].is_disjoint(&drains[*a]);
1001 match (a_feeds_b, b_feeds_a) {
1002 (true, false) => std::cmp::Ordering::Less,
1003 (false, true) => std::cmp::Ordering::Greater,
1004 _ => a.cmp(b),
1005 }
1006 });
1007 site_progs = order.into_iter().map(|i| site_progs[i].clone()).collect();
1008 let ops = merge_programs(&site_progs, sites[0])?;
1009 ring_occupancy(&ctx, &ops, &mut BTreeMap::new())?;
1010 let perw = per_warp_tiles(p, nwarp(&self.target));
1011 let smem: usize = ctx
1012 .rings
1013 .values()
1014 .map(|(d, pg, ty)| d * pg * c_type_bytes(ty))
1015 .sum();
1016 let epi_bytes = if ctx.fused_out.is_some() {
1024 ctx.lanes * ctx.te * c_type_bytes(acct)
1025 } else {
1026 0
1027 };
1028 let rings_dynamic = smem > 48 * 1024;
1029 let dyn_bytes = align16(epi_bytes) + if rings_dynamic { align16(smem) } else { 0 };
1030 let dynamic = dyn_bytes > 48 * 1024;
1031 let threads = nwarp(&self.target) * WARP;
1032 let regs_est = perw * 8 + 48;
1033 let blocks_per_sm =
1036 self.target
1037 .resident_sites_per_unit(smem + epi_bytes, plan.block.osh, plan.block.osw);
1038 let budget = PersistentBudget {
1039 smem_bytes: smem,
1040 dynamic_opt_in: dynamic,
1041 rings_dynamic,
1042 dyn_bytes,
1043 regs_est,
1044 blocks_per_sm,
1045 threads,
1046 };
1047 let program_cu = print_program(&ctx, &ops, &sites, perw, &budget, false)?;
1048 let bench_cu = print_program(&ctx, &ops, &sites, perw, &budget, true)?;
1049 Ok(SpatialArtifact::Cuda(CudaArtifact {
1050 program_cu,
1051 bench_cu,
1052 budget,
1053 census: Census::of(p),
1054 general: None,
1055 }))
1056 }
1057}
1058
1059fn c_type_bytes(ty: &str) -> usize {
1060 match ty {
1061 "signed char" => 1,
1062 "__nv_bfloat16" | "__half" => 2,
1063 _ => 4,
1064 }
1065}
1066
1067fn print_program(
1068 ctx: &Ctx,
1069 ops: &[TaggedOp],
1070 sites: &[Site],
1071 perw: usize,
1072 budget: &PersistentBudget,
1073 bench: bool,
1074) -> Result<String, SpatialLowerError> {
1075 let p = ctx.p;
1076 let pl = p.placement.as_ref().unwrap();
1077 let mut e = Emit {
1078 s: String::new(),
1079 ind: 0,
1080 };
1081 e.l(format!(
1082 "// {} — a persistent CUDA kernel printed from the spatial IR; each block is one IR site.",
1083 p.name
1084 ));
1085 e.l("#include <cuda_pipeline.h>");
1086 e.l("#include <mma.h>");
1087 e.l("#include <cuda_bf16.h>");
1088 e.l("#include <cuda_fp16.h>");
1089 e.l("#include <cstdio>");
1090 e.l("#include <cstdlib>");
1091 e.l("#include <vector>");
1092 e.l("#include <cstring>");
1093 e.l("");
1094 e.l(format!(
1095 "typedef {} OPT;",
1096 wmma_types(ctx.operand).unwrap().0
1097 ));
1098 e.l(format!("typedef {} ACCT;", ctx.acc_t));
1099 e.l(format!("#define NTHREAD {}", ctx.lanes * WARP));
1100 e.l(format!("#define PERW {perw}"));
1101 let mut dyn_off = 0usize;
1104 if ctx.fused_out.is_some() {
1105 e.l(format!("#define SMEM_OFF_EPI {dyn_off}"));
1106 dyn_off += align16(ctx.lanes * ctx.te * c_type_bytes(ctx.acc_t));
1107 }
1108 for (b, (depth, page, ty)) in &ctx.rings {
1109 e.l(format!("#define DEPTH_{} {depth}", b.idx()));
1110 e.l(format!("#define PAGE_{} {page}", b.idx()));
1111 if budget.rings_dynamic {
1112 dyn_off = (dyn_off + 15) & !15;
1113 e.l(format!("#define SMEM_OFF_{} {dyn_off}", b.idx()));
1114 dyn_off += depth * page * c_type_bytes(ty);
1115 }
1116 }
1117 if dyn_off > 0 {
1118 e.l(format!("#define DYN_SMEM {}", (dyn_off + 15) & !15));
1119 }
1120 let mut flat: Vec<String> = Vec::new();
1133 let mut offs: Vec<String> = Vec::new();
1134 let mut counts: Vec<String> = Vec::new();
1135 for s in sites.iter() {
1136 offs.push(flat.len().to_string());
1137 let items = pl.items_at(*s);
1138 counts.push(items.len().to_string());
1139 flat.extend(items.iter().map(|i| i.idx().to_string()));
1140 }
1141 if flat.is_empty() {
1142 flat.push("0".to_string());
1143 }
1144 e.l(format!(
1145 "__device__ __constant__ int ITEM_LIST[{}] = {{{}}};",
1146 flat.len(),
1147 flat.join(", ")
1148 ));
1149 e.l(format!(
1150 "__device__ __constant__ int ITEM_OFF[{}] = {{{}}};",
1151 offs.len(),
1152 offs.join(", ")
1153 ));
1154 e.l(format!(
1155 "__device__ __constant__ int ITEM_COUNT[{}] = {{{}}};",
1156 sites.len(),
1157 counts.join(", ")
1158 ));
1159 e.l("");
1160 let mut params: Vec<String> = Vec::new();
1162 for t in &p.tensors {
1163 let ty = if t.role == TensorRole::Output {
1164 "long long".to_string()
1165 } else {
1166 wmma_types(ctx.operand).unwrap().0.to_string()
1167 };
1168 let cst = if t.role == TensorRole::Output {
1169 ""
1170 } else {
1171 "const "
1172 };
1173 params.push(format!("{cst}{ty}* __restrict__ t{}", t.id.idx()));
1174 }
1175 e.l(format!(
1176 "__global__ __launch_bounds__(NTHREAD, {}) void logos_spatial_persistent({})",
1177 budget.blocks_per_sm,
1178 params.join(", ")
1179 ));
1180 e.open("");
1181 e.l("const int tid = threadIdx.x;");
1182 e.l("const int warp = tid / 32;");
1183 e.l("const int site = blockIdx.x;");
1184 e.l("const int item_off = ITEM_OFF[site];");
1185 e.l("const int n_items = ITEM_COUNT[site];");
1186 if budget.dyn_bytes > 0 {
1187 e.l("extern __shared__ __align__(16) char logos_dyn_smem[];");
1191 }
1192 for (b, (_, _, ty)) in &ctx.rings {
1193 let k = b.idx();
1194 if budget.rings_dynamic {
1195 e.l(format!(
1196 "{ty}* ring_{k} = reinterpret_cast<{ty}*>(logos_dyn_smem + SMEM_OFF_{k});"
1197 ));
1198 } else {
1199 e.l(format!(
1200 "__shared__ __align__(16) {ty} ring_{k}[DEPTH_{k} * PAGE_{k}];"
1201 ));
1202 }
1203 e.l(format!(
1204 "int wctr_{k} = 0, rctr_{k} = 0; (void)wctr_{k}; (void)rctr_{k};"
1205 ));
1206 }
1207 e.l("nvcuda::wmma::fragment<nvcuda::wmma::accumulator, 16, 16, 16, ACCT> accf[PERW];");
1208 if ctx.fused_out.is_some() {
1209 e.l("ACCT* epi_scratch = reinterpret_cast<ACCT*>(logos_dyn_smem + SMEM_OFF_EPI);");
1211 }
1212 let has_item_loop = ops
1214 .iter()
1215 .any(|t| matches!(t.op, ChoreoOp::Loop { var, .. } if var == LV_ITEM));
1216 if !has_item_loop {
1217 e.l("if (n_items == 0) { return; }");
1223 e.l("const int item_id = ITEM_LIST[item_off];");
1224 e.l(format!(
1225 "const int oy = item_id / {}, ox = item_id % {};",
1226 ctx.grid_nx.max(1),
1227 ctx.grid_nx.max(1)
1228 ));
1229 e.l("(void)oy; (void)ox;");
1230 }
1231 let mut scope = BTreeSet::new();
1232 emit_ops(ctx, &mut e, ops, &mut scope, 0)?;
1233 e.close();
1234 e.l("");
1235 let a = &p.tensors[0];
1237 let b = &p.tensors[1];
1238 let out = p
1239 .output_tensor()
1240 .ok_or(SpatialLowerError::Unsupported("an output tensor"))?;
1241 let (m, k, n) = (a.elems.rows, a.elems.cols, b.elems.cols);
1242 let (tr, tc, te) = (ctx.tr, ctx.tc, ctx.te);
1243 e.open("int main()");
1244 e.l(format!("const int M = {m}, K = {k}, N = {n};"));
1245 e.l(format!(
1246 "const long long AW = {}LL * {}LL * {te};",
1247 a.tiles.rows_t, a.tiles.cols_t
1248 ));
1249 e.l(format!(
1250 "const long long BW = {}LL * {}LL * {te};",
1251 b.tiles.rows_t, b.tiles.cols_t
1252 ));
1253 e.l(format!(
1254 "const long long CW = {}LL * {}LL * {te};",
1255 out.tiles.rows_t, out.tiles.cols_t
1256 ));
1257 e.l("std::vector<double> ah((size_t)M * K, 0.0), bh((size_t)K * N, 0.0);");
1258 if bench {
1259 e.l("for (size_t i = 0; i < ah.size(); ++i) ah[i] = (double)((int)(i % 7) - 3);");
1260 e.l("for (size_t i = 0; i < bh.size(); ++i) bh[i] = (double)((int)(i % 11) - 5);");
1261 } else {
1262 e.l("{ FILE* f = fopen(\"a.txt\", \"r\"); if (!f) { fprintf(stderr, \"a.txt\\n\"); return 2; } for (size_t i = 0; i < ah.size(); ++i) { double v; if (fscanf(f, \"%lf\", &v) != 1) { fprintf(stderr, \"a.txt short\\n\"); return 2; } ah[i] = v; } fclose(f); }");
1263 e.l("{ FILE* f = fopen(\"b.txt\", \"r\"); if (!f) { fprintf(stderr, \"b.txt\\n\"); return 2; } for (size_t i = 0; i < bh.size(); ++i) { double v; if (fscanf(f, \"%lf\", &v) != 1) { fprintf(stderr, \"b.txt short\\n\"); return 2; } bh[i] = v; } fclose(f); }");
1264 }
1265 e.l("std::vector<OPT> at(AW), bt(BW);");
1266 e.l("memset(at.data(), 0, at.size() * sizeof(OPT)); memset(bt.data(), 0, bt.size() * sizeof(OPT));");
1267 e.l(format!("for (int i = 0; i < M; ++i) for (int j = 0; j < K; ++j) at[((long long)(i / {tr}) * {} + j / {tc}) * {te} + (i % {tr}) * {tc} + j % {tc}] = (OPT)ah[(size_t)i * K + j];", a.tiles.cols_t));
1268 e.l(format!("for (int i = 0; i < K; ++i) for (int j = 0; j < N; ++j) bt[((long long)(i / {tr}) * {} + j / {tc}) * {te} + (i % {tr}) * {tc} + j % {tc}] = (OPT)bh[(size_t)i * N + j];", b.tiles.cols_t));
1269 e.l("OPT *da, *db; long long* dc;");
1270 e.l("cudaMalloc(&da, AW * sizeof(OPT)); cudaMalloc(&db, BW * sizeof(OPT)); cudaMalloc(&dc, CW * sizeof(long long));");
1271 e.l("cudaMemcpy(da, at.data(), AW * sizeof(OPT), cudaMemcpyHostToDevice);");
1272 e.l("cudaMemcpy(db, bt.data(), BW * sizeof(OPT), cudaMemcpyHostToDevice);");
1273 e.l("cudaMemset(dc, 0, CW * sizeof(long long));");
1274 let smem_arg = if budget.dyn_bytes > 0 {
1275 ", DYN_SMEM"
1276 } else {
1277 ""
1278 };
1279 if budget.dynamic_opt_in {
1280 e.l("cudaFuncSetAttribute(logos_spatial_persistent, cudaFuncAttributeMaxDynamicSharedMemorySize, DYN_SMEM);");
1281 }
1282 e.l(format!("const int GRID = {};", sites.len()));
1283 if bench {
1284 e.l("cudaEvent_t e0, e1; cudaEventCreate(&e0); cudaEventCreate(&e1);");
1285 e.l(format!(
1286 "logos_spatial_persistent<<<GRID, NTHREAD{}>>>(da, db, dc); cudaDeviceSynchronize();",
1287 smem_arg
1288 ));
1289 e.l("const int ITERS = 20;");
1290 e.l("cudaEventRecord(e0);");
1291 e.l(format!("for (int it = 0; it < ITERS; ++it) logos_spatial_persistent<<<GRID, NTHREAD{}>>>(da, db, dc);", smem_arg));
1292 e.l("cudaEventRecord(e1); cudaEventSynchronize(e1);");
1293 e.l("{ cudaError_t err = cudaGetLastError(); if (err != cudaSuccess) { fprintf(stderr, \"%s\\n\", cudaGetErrorString(err)); return 1; } }");
1295 e.l("float ms = 0; cudaEventElapsedTime(&ms, e0, e1);");
1296 e.l("const double ops = 2.0 * (double)M * (double)N * (double)K * (double)ITERS;");
1297 e.l("printf(\"GOPS %.1f\\n\", ops / (ms * 1e6));");
1298 } else {
1299 e.l(format!(
1300 "logos_spatial_persistent<<<GRID, NTHREAD{}>>>(da, db, dc);",
1301 smem_arg
1302 ));
1303 e.l("cudaError_t err = cudaDeviceSynchronize();");
1304 e.l("if (err != cudaSuccess) { fprintf(stderr, \"%s\\n\", cudaGetErrorString(err)); return 1; }");
1305 e.l("std::vector<long long> ct(CW);");
1306 e.l("cudaMemcpy(ct.data(), dc, CW * sizeof(long long), cudaMemcpyDeviceToHost);");
1307 e.l(format!("for (int i = 0; i < M; ++i) for (int j = 0; j < N; ++j) printf(\"%lld\\n\", ct[((long long)(i / {tr}) * {} + j / {tc}) * {te} + (i % {tr}) * {tc} + j % {tc}]);", out.tiles.cols_t));
1308 }
1309 e.l("cudaFree(da); cudaFree(db); cudaFree(dc);");
1310 e.l("return 0;");
1311 e.close();
1312 Ok(e.s)
1313}