1use crate::ptx_kernels::correlate_ptx;
30
31fn ptx_header(sm: u32) -> String {
37 let ptx_ver = if sm >= 100 {
38 "8.7"
39 } else if sm >= 90 {
40 "8.4"
41 } else if sm >= 80 {
42 "8.0"
43 } else {
44 "7.5"
45 };
46 format!(".version {ptx_ver}\n.target sm_{sm}\n.address_size 64\n\n")
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct TileConfig {
68 pub block_x: u32,
70 pub block_y: u32,
72 pub stages: u32,
74 pub smem_bytes_per_stage: u32,
76 pub cp_async: bool,
78 pub tma: bool,
80}
81
82impl TileConfig {
83 #[must_use]
86 pub const fn portable_default() -> Self {
87 Self {
88 block_x: 256,
89 block_y: 1,
90 stages: 1,
91 smem_bytes_per_stage: 0,
92 cp_async: false,
93 tma: false,
94 }
95 }
96
97 #[must_use]
102 pub fn for_sm(sm: u32) -> Self {
103 let (block_x, stages) = if sm >= 90 {
104 (512, 3)
105 } else if sm >= 80 {
106 (256, 2)
107 } else {
108 (128, 1)
109 };
110 Self {
111 block_x,
112 block_y: 1,
113 stages,
114 smem_bytes_per_stage: block_x * 4,
115 cp_async: sm >= 80,
116 tma: sm >= 90,
117 }
118 }
119
120 #[must_use]
123 pub fn grid_x(self, n: usize) -> u32 {
124 let bx = self.block_x.max(1) as usize;
125 (n.div_ceil(bx)).max(1) as u32
126 }
127
128 #[must_use]
130 pub fn threads_per_block(self) -> u32 {
131 self.block_x.saturating_mul(self.block_y)
132 }
133
134 #[must_use]
137 pub fn total_smem_bytes(self) -> u32 {
138 self.stages.saturating_mul(self.smem_bytes_per_stage)
139 }
140}
141
142#[must_use]
161pub fn correlate_tma_ptx(sm: u32) -> String {
162 if sm < 90 {
163 return correlate_ptx(sm);
166 }
167 let hdr = ptx_header(sm);
168 let cfg = TileConfig::for_sm(sm);
169 let prologue = format!(
171 ".visible .entry correlate_tma_kernel(\n\
172 .param .u64 p_c,\n\
173 .param .u64 p_phi,\n\
174 .param .u64 p_r,\n\
175 .param .u32 p_m,\n\
176 .param .u32 p_n\n\
177 )\n\
178 {{\n\
179 // CTA-shared staging tile for the residual block ({tile} bytes).\n\
180 .shared .align 16 .b8 r_tile[{tile}];\n\
181 // mbarrier object for the bulk async copy completion.\n\
182 .shared .align 8 .b64 tma_bar[1];\n",
183 tile = cfg.smem_bytes_per_stage,
184 );
185 let body = "\
186 .reg .u64 %rd<20>;\n\
187 .reg .u32 %r<24>;\n\
188 .reg .f32 %f<8>;\n\
189 .reg .pred %p0;\n\
190 \n\
191 ld.param.u64 %rd0, [p_c];\n\
192 ld.param.u64 %rd1, [p_phi];\n\
193 ld.param.u64 %rd2, [p_r];\n\
194 ld.param.u32 %r0, [p_m];\n\
195 ld.param.u32 %r1, [p_n];\n\
196 \n\
197 mov.u32 %r2, %ntid.x;\n\
198 mov.u32 %r3, %ctaid.x;\n\
199 mov.u32 %r4, %tid.x;\n\
200 mad.lo.u32 %r5, %r2, %r3, %r4;\n\
201 \n\
202 // Thread 0 initialises the mbarrier and issues the bulk copy of r[0..m).\n\
203 mov.u64 %rd3, r_tile;\n\
204 mov.u64 %rd4, tma_bar;\n\
205 setp.ne.u32 %p0, %r4, 0;\n\
206 @%p0 bra $TMA_WAIT;\n\
207 \n\
208 // bytes = min(m, block_x) * 4 ; here we stage the leading block.\n\
209 mul.lo.u32 %r6, %r2, 4;\n\
210 mbarrier.init.shared.b64 [%rd4], 1;\n\
211 cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes \
212[%rd3], [%rd2], %r6, [%rd4];\n\
213 mbarrier.arrive.expect_tx.shared.b64 _, [%rd4], %r6;\n\
214 \n\
215 $TMA_WAIT:\n\
216 bar.sync 0;\n\
217 // All threads wait on the mbarrier phase before reading r_tile.\n\
218 mbarrier.try_wait.parity.shared.b64 %p0, [%rd4], 0;\n\
219 @!%p0 bra $TMA_WAIT;\n\
220 \n\
221 setp.ge.u32 %p0, %r5, %r1;\n\
222 @%p0 bra $TMA_DONE;\n\
223 \n\
224 mov.f32 %f0, 0f00000000;\n\
225 mov.u32 %r7, 0;\n\
226 \n\
227 $TMA_LOOP:\n\
228 setp.ge.u32 %p0, %r7, %r0;\n\
229 @%p0 bra $TMA_WRITE;\n\
230 \n\
231 // phi[i, j] = i * n + j\n\
232 mul.lo.u32 %r8, %r7, %r1;\n\
233 add.u32 %r8, %r8, %r5;\n\
234 mul.wide.u32 %rd5, %r8, 4;\n\
235 add.u64 %rd6, %rd1, %rd5;\n\
236 ld.global.f32 %f1, [%rd6];\n\
237 \n\
238 // r[i] from the shared staging tile when i < block_x, else global.\n\
239 setp.ge.u32 %p0, %r7, %r2;\n\
240 @%p0 bra $TMA_RGLOBAL;\n\
241 mul.wide.u32 %rd7, %r7, 4;\n\
242 add.u64 %rd8, %rd3, %rd7;\n\
243 ld.shared.f32 %f2, [%rd8];\n\
244 bra $TMA_HAVE_R;\n\
245 $TMA_RGLOBAL:\n\
246 mul.wide.u32 %rd7, %r7, 4;\n\
247 add.u64 %rd8, %rd2, %rd7;\n\
248 ld.global.f32 %f2, [%rd8];\n\
249 $TMA_HAVE_R:\n\
250 fma.rn.f32 %f0, %f1, %f2, %f0;\n\
251 add.u32 %r7, %r7, 1;\n\
252 bra $TMA_LOOP;\n\
253 \n\
254 $TMA_WRITE:\n\
255 mul.wide.u32 %rd9, %r5, 4;\n\
256 add.u64 %rd10, %rd0, %rd9;\n\
257 st.global.f32 [%rd10], %f0;\n\
258 \n\
259 $TMA_DONE:\n\
260 ret;\n\
261 }\n";
262 hdr + &prologue + body
263}
264
265#[must_use]
283pub fn iht_step_cp_async_ptx(sm: u32) -> String {
284 if sm < 80 {
285 return crate::ptx_kernels::iht_step_ptx(sm);
286 }
287 let hdr = ptx_header(sm);
288 let cfg = TileConfig::for_sm(sm);
289 let prologue = format!(
290 ".visible .entry iht_step_cp_async_kernel(\n\
291 .param .u64 p_x,\n\
292 .param .u64 p_grad,\n\
293 .param .f32 p_mu,\n\
294 .param .u32 p_n\n\
295 )\n\
296 {{\n\
297 // {stages}-stage shared staging buffer for grad ({tile} bytes/stage).\n\
298 .shared .align 16 .b8 grad_tile[{tile}];\n",
299 stages = cfg.stages,
300 tile = cfg.smem_bytes_per_stage,
301 );
302 let body = "\
303 .reg .u64 %rd<16>;\n\
304 .reg .u32 %r<16>;\n\
305 .reg .f32 %f<8>;\n\
306 .reg .pred %p0;\n\
307 \n\
308 ld.param.u64 %rd0, [p_x];\n\
309 ld.param.u64 %rd1, [p_grad];\n\
310 ld.param.f32 %f0, [p_mu];\n\
311 ld.param.u32 %r0, [p_n];\n\
312 \n\
313 mov.u32 %r1, %ntid.x;\n\
314 mov.u32 %r2, %ctaid.x;\n\
315 mov.u32 %r3, %tid.x;\n\
316 mad.lo.u32 %r4, %r1, %r2, %r3;\n\
317 \n\
318 setp.ge.u32 %p0, %r4, %r0;\n\
319 \n\
320 // Shared destination for this thread's grad element. `tid` is always in\n\
321 // range for a launched thread, so this shared address is valid even when\n\
322 // the global index is out of range; the cp.async itself is skipped for\n\
323 // those threads below.\n\
324 mov.u64 %rd2, grad_tile;\n\
325 mul.wide.u32 %rd3, %r3, 4;\n\
326 add.u64 %rd4, %rd2, %rd3;\n\
327 \n\
328 // Out-of-range global threads skip the async copy but STILL fall through\n\
329 // to the barrier, so a ragged launch (n % blockDim.x != 0) cannot\n\
330 // deadlock at `bar.sync` (the original kernel branched past the barrier).\n\
331 @%p0 bra $CA_NOLOAD;\n\
332 mul.wide.u32 %rd5, %r4, 4;\n\
333 add.u64 %rd6, %rd1, %rd5;\n\
334 // A 4-byte cp.async must use the .ca qualifier: the .cg variant only\n\
335 // accepts a 16-byte copy and is rejected outright by ptxas for size 4.\n\
336 cp.async.ca.shared.global [%rd4], [%rd6], 4;\n\
337 \n\
338 $CA_NOLOAD:\n\
339 cp.async.commit_group;\n\
340 cp.async.wait_group 0;\n\
341 bar.sync 0;\n\
342 \n\
343 // Out-of-range threads are finished once they have joined the barrier.\n\
344 @%p0 bra $CA_DONE;\n\
345 \n\
346 // x[i] += mu * grad_tile[tid]\n\
347 mul.wide.u32 %rd5, %r4, 4;\n\
348 ld.shared.f32 %f2, [%rd4];\n\
349 add.u64 %rd7, %rd0, %rd5;\n\
350 ld.global.f32 %f1, [%rd7];\n\
351 fma.rn.f32 %f3, %f0, %f2, %f1;\n\
352 st.global.f32 [%rd7], %f3;\n\
353 \n\
354 $CA_DONE:\n\
355 ret;\n\
356 }\n";
357 hdr + &prologue + body
358}
359
360#[must_use]
381pub fn correlate_fp8_ptx(sm: u32) -> String {
382 if sm < 89 {
383 return correlate_ptx(sm);
384 }
385 let hdr = ptx_header(sm);
386 let body = ".visible .entry correlate_fp8_kernel(\n\
387 .param .u64 p_c,\n\
388 .param .u64 p_phi,\n\
389 .param .u64 p_r,\n\
390 .param .u32 p_m,\n\
391 .param .u32 p_n\n\
392 )\n\
393 {\n\
394 .reg .u64 %rd<12>;\n\
395 .reg .u32 %r<20>;\n\
396 .reg .f32 %f<12>;\n\
397 .reg .b32 %rb<6>;\n\
398 .reg .b16 %rh<6>;\n\
399 .reg .pred %p0;\n\
400 .reg .pred %p1;\n\
401 \n\
402 ld.param.u64 %rd0, [p_c];\n\
403 ld.param.u64 %rd1, [p_phi];\n\
404 ld.param.u64 %rd2, [p_r];\n\
405 ld.param.u32 %r0, [p_m];\n\
406 ld.param.u32 %r1, [p_n];\n\
407 \n\
408 mov.u32 %r2, %ntid.x;\n\
409 mov.u32 %r3, %ctaid.x;\n\
410 mov.u32 %r4, %tid.x;\n\
411 mad.lo.u32 %r5, %r2, %r3, %r4;\n\
412 \n\
413 setp.ge.u32 %p0, %r5, %r1;\n\
414 @%p0 bra $F8_DONE;\n\
415 \n\
416 mov.f32 %f0, 0f00000000;\n\
417 mov.u32 %r6, 0;\n\
418 \n\
419 $F8_LOOP:\n\
420 // process rows i and i+1 together when both in range.\n\
421 add.u32 %r7, %r6, 1;\n\
422 setp.ge.u32 %p0, %r6, %r0;\n\
423 @%p0 bra $F8_WRITE;\n\
424 setp.ge.u32 %p1, %r7, %r0;\n\
425 @%p1 bra $F8_TAIL;\n\
426 \n\
427 // phi byte offset for (i, j): i*n + j ; pair stride along i is n.\n\
428 mul.lo.u32 %r8, %r6, %r1;\n\
429 add.u32 %r8, %r8, %r5;\n\
430 cvt.u64.u32 %rd3, %r8;\n\
431 add.u64 %rd4, %rd1, %rd3;\n\
432 ld.global.u8 %rh0, [%rd4];\n\
433 mul.lo.u32 %r9, %r7, %r1;\n\
434 add.u32 %r9, %r9, %r5;\n\
435 cvt.u64.u32 %rd5, %r9;\n\
436 add.u64 %rd6, %rd1, %rd5;\n\
437 ld.global.u8 %rh1, [%rd6];\n\
438 // pack the two e4m3 phi bytes into a b16, unpack to two f32.\n\
439 shl.b16 %rh2, %rh1, 8;\n\
440 or.b16 %rh2, %rh2, %rh0;\n\
441 cvt.rn.f32.e4m3x2 %rb0, %rh2;\n\
442 mov.b32 {%rh3, %rh4}, %rb0;\n\
443 cvt.f32.f16 %f1, %rh3;\n\
444 cvt.f32.f16 %f2, %rh4;\n\
445 \n\
446 // r bytes for rows i, i+1.\n\
447 cvt.u64.u32 %rd7, %r6;\n\
448 add.u64 %rd8, %rd2, %rd7;\n\
449 ld.global.u8 %rh0, [%rd8];\n\
450 cvt.u64.u32 %rd9, %r7;\n\
451 add.u64 %rd10, %rd2, %rd9;\n\
452 ld.global.u8 %rh1, [%rd10];\n\
453 shl.b16 %rh2, %rh1, 8;\n\
454 or.b16 %rh2, %rh2, %rh0;\n\
455 cvt.rn.f32.e4m3x2 %rb1, %rh2;\n\
456 mov.b32 {%rh3, %rh4}, %rb1;\n\
457 cvt.f32.f16 %f3, %rh3;\n\
458 cvt.f32.f16 %f4, %rh4;\n\
459 \n\
460 fma.rn.f32 %f0, %f1, %f3, %f0;\n\
461 fma.rn.f32 %f0, %f2, %f4, %f0;\n\
462 add.u32 %r6, %r6, 2;\n\
463 bra $F8_LOOP;\n\
464 \n\
465 $F8_TAIL:\n\
466 // single odd row i = %r6.\n\
467 mul.lo.u32 %r8, %r6, %r1;\n\
468 add.u32 %r8, %r8, %r5;\n\
469 cvt.u64.u32 %rd3, %r8;\n\
470 add.u64 %rd4, %rd1, %rd3;\n\
471 ld.global.u8 %rh0, [%rd4];\n\
472 cvt.rn.f32.e4m3x2 %rb0, %rh0;\n\
473 mov.b32 {%rh3, %rh4}, %rb0;\n\
474 cvt.f32.f16 %f1, %rh3;\n\
475 cvt.u64.u32 %rd7, %r6;\n\
476 add.u64 %rd8, %rd2, %rd7;\n\
477 ld.global.u8 %rh0, [%rd8];\n\
478 cvt.rn.f32.e4m3x2 %rb1, %rh0;\n\
479 mov.b32 {%rh3, %rh4}, %rb1;\n\
480 cvt.f32.f16 %f3, %rh3;\n\
481 fma.rn.f32 %f0, %f1, %f3, %f0;\n\
482 add.u32 %r6, %r6, 1;\n\
483 bra $F8_LOOP;\n\
484 \n\
485 $F8_WRITE:\n\
486 mul.wide.u32 %rd3, %r5, 4;\n\
487 add.u64 %rd4, %rd0, %rd3;\n\
488 st.global.f32 [%rd4], %f0;\n\
489 \n\
490 $F8_DONE:\n\
491 ret;\n\
492 }\n";
493 hdr + body
494}
495
496#[must_use]
513pub fn svt_threshold_warpshuffle_ptx(sm: u32) -> String {
514 let hdr = ptx_header(sm);
515 let body = ".visible .entry svt_threshold_ws_kernel(\n\
516 .param .u64 p_sigma_out,\n\
517 .param .u64 p_nucnorm_partial,\n\
518 .param .u64 p_sigma_in,\n\
519 .param .f32 p_tau,\n\
520 .param .u32 p_n\n\
521 )\n\
522 {\n\
523 .reg .u64 %rd<12>;\n\
524 .reg .u32 %r<20>;\n\
525 .reg .f32 %f<8>;\n\
526 .reg .pred %p0;\n\
527 .reg .pred %p1;\n\
528 \n\
529 ld.param.u64 %rd0, [p_sigma_out];\n\
530 ld.param.u64 %rd1, [p_nucnorm_partial];\n\
531 ld.param.u64 %rd2, [p_sigma_in];\n\
532 ld.param.f32 %f0, [p_tau];\n\
533 ld.param.u32 %r0, [p_n];\n\
534 \n\
535 mov.u32 %r1, %ntid.x;\n\
536 mov.u32 %r2, %ctaid.x;\n\
537 mov.u32 %r3, %tid.x;\n\
538 mad.lo.u32 %r4, %r1, %r2, %r3;\n\
539 \n\
540 // Threshold (out-of-range threads contribute 0 to the warp sum).\n\
541 mov.f32 %f3, 0f00000000;\n\
542 setp.ge.u32 %p0, %r4, %r0;\n\
543 @%p0 bra $WS_REDUCE;\n\
544 \n\
545 mul.wide.u32 %rd3, %r4, 4;\n\
546 add.u64 %rd4, %rd2, %rd3;\n\
547 ld.global.f32 %f1, [%rd4];\n\
548 sub.f32 %f2, %f1, %f0;\n\
549 max.f32 %f3, %f2, 0f00000000;\n\
550 add.u64 %rd5, %rd0, %rd3;\n\
551 st.global.f32 [%rd5], %f3;\n\
552 \n\
553 $WS_REDUCE:\n\
554 // Warp-level sum of %f3 via butterfly down-shuffle (full 32-lane mask).\n\
555 shfl.sync.down.b32 %f4, %f3, 16, 31, 0xFFFFFFFF;\n\
556 add.f32 %f3, %f3, %f4;\n\
557 shfl.sync.down.b32 %f4, %f3, 8, 31, 0xFFFFFFFF;\n\
558 add.f32 %f3, %f3, %f4;\n\
559 shfl.sync.down.b32 %f4, %f3, 4, 31, 0xFFFFFFFF;\n\
560 add.f32 %f3, %f3, %f4;\n\
561 shfl.sync.down.b32 %f4, %f3, 2, 31, 0xFFFFFFFF;\n\
562 add.f32 %f3, %f3, %f4;\n\
563 shfl.sync.down.b32 %f4, %f3, 1, 31, 0xFFFFFFFF;\n\
564 add.f32 %f3, %f3, %f4;\n\
565 \n\
566 // Lane 0 of each warp writes its partial nuclear-norm sum.\n\
567 and.b32 %r5, %r3, 31;\n\
568 setp.ne.u32 %p1, %r5, 0;\n\
569 @%p1 bra $WS_DONE;\n\
570 shr.u32 %r6, %r4, 5;\n\
571 mul.wide.u32 %rd6, %r6, 4;\n\
572 add.u64 %rd7, %rd1, %rd6;\n\
573 st.global.f32 [%rd7], %f3;\n\
574 \n\
575 $WS_DONE:\n\
576 ret;\n\
577 }\n";
578 hdr + body
579}
580
581#[cfg(test)]
587mod tests {
588 use super::*;
589
590 const SMS: [u32; 6] = [75, 80, 86, 89, 90, 100];
591
592 #[test]
595 fn tile_config_matches_documented_table() {
596 let t75 = TileConfig::for_sm(75);
598 assert_eq!(t75.block_x, 128);
599 assert_eq!(t75.stages, 1);
600 assert!(!t75.cp_async);
601 assert!(!t75.tma);
602 for sm in [80u32, 86] {
604 let c = TileConfig::for_sm(sm);
605 assert_eq!(c.block_x, 256, "sm {sm}");
606 assert_eq!(c.stages, 2, "sm {sm}");
607 assert!(c.cp_async, "sm {sm}");
608 assert!(!c.tma, "sm {sm}");
609 }
610 let t89 = TileConfig::for_sm(89);
612 assert_eq!(t89.block_x, 256);
613 assert_eq!(t89.stages, 2);
614 assert!(t89.cp_async);
615 assert!(!t89.tma);
616 for sm in [90u32, 100] {
618 let c = TileConfig::for_sm(sm);
619 assert_eq!(c.block_x, 512, "sm {sm}");
620 assert_eq!(c.stages, 3, "sm {sm}");
621 assert!(c.cp_async, "sm {sm}");
622 assert!(c.tma, "sm {sm}");
623 }
624 }
625
626 #[test]
627 fn tile_config_smem_is_block_x_f32() {
628 for sm in SMS {
629 let c = TileConfig::for_sm(sm);
630 assert_eq!(c.smem_bytes_per_stage, c.block_x * 4);
631 assert_eq!(c.total_smem_bytes(), c.stages * c.block_x * 4);
632 }
633 }
634
635 #[test]
636 fn tile_config_grid_ceildiv() {
637 let c = TileConfig::for_sm(80); assert_eq!(c.grid_x(256), 1);
639 assert_eq!(c.grid_x(257), 2);
640 assert_eq!(c.grid_x(512), 2);
641 assert_eq!(c.grid_x(1), 1);
642 assert_eq!(c.grid_x(0), 1);
644 }
645
646 #[test]
647 fn tile_config_threads_per_block() {
648 let c = TileConfig::for_sm(90);
649 assert_eq!(c.threads_per_block(), 512);
650 let d = TileConfig::portable_default();
651 assert_eq!(d.threads_per_block(), 256);
652 assert_eq!(d.stages, 1);
653 assert!(!d.cp_async && !d.tma);
654 }
655
656 #[test]
659 fn tma_correlate_has_bulk_copy_on_hopper() {
660 for sm in [90u32, 100] {
661 let s = correlate_tma_ptx(sm);
662 assert!(
663 s.contains(".visible .entry correlate_tma_kernel"),
664 "sm {sm}"
665 );
666 assert!(s.contains("cp.async.bulk"), "sm {sm} missing TMA bulk copy");
667 assert!(
668 s.contains("mbarrier.init.shared.b64"),
669 "sm {sm} missing mbarrier"
670 );
671 assert!(
672 s.contains(".shared .align 16 .b8 r_tile"),
673 "sm {sm} missing staging tile"
674 );
675 assert!(s.contains("ret"), "sm {sm}");
676 }
677 assert!(correlate_tma_ptx(90).contains(".version 8.4"));
679 assert!(correlate_tma_ptx(100).contains(".version 8.7"));
680 }
681
682 #[test]
683 fn tma_correlate_falls_back_pre_hopper() {
684 for sm in [75u32, 80, 86, 89] {
686 let s = correlate_tma_ptx(sm);
687 assert!(!s.contains("cp.async.bulk"), "sm {sm} should not emit TMA");
688 assert!(
689 s.contains(".visible .entry correlate_kernel"),
690 "sm {sm} fallback"
691 );
692 assert_eq!(
693 s,
694 crate::ptx_kernels::correlate_ptx(sm),
695 "sm {sm} exact fallback"
696 );
697 }
698 }
699
700 #[test]
703 fn cp_async_iht_has_async_copy_on_ampere_plus() {
704 for sm in [80u32, 86, 89, 90, 100] {
705 let s = iht_step_cp_async_ptx(sm);
706 assert!(
707 s.contains(".visible .entry iht_step_cp_async_kernel"),
708 "sm {sm}"
709 );
710 assert!(
711 s.contains("cp.async.ca.shared.global"),
712 "sm {sm} missing cp.async"
713 );
714 assert!(
715 s.contains("cp.async.commit_group"),
716 "sm {sm} missing commit"
717 );
718 assert!(s.contains("cp.async.wait_group"), "sm {sm} missing wait");
719 assert!(
720 s.contains(".shared .align 16 .b8 grad_tile"),
721 "sm {sm} stage buf"
722 );
723 assert!(s.contains("fma.rn.f32"), "sm {sm} missing the IHT FMA");
724 assert!(s.contains("ret"), "sm {sm}");
725 }
726 }
727
728 #[test]
729 fn cp_async_iht_falls_back_pre_ampere() {
730 let s = iht_step_cp_async_ptx(75);
731 assert!(!s.contains("cp.async"), "Turing should not emit cp.async");
732 assert_eq!(s, crate::ptx_kernels::iht_step_ptx(75));
733 }
734
735 #[test]
736 fn cp_async_iht_stage_count_in_comment() {
737 assert!(iht_step_cp_async_ptx(90).contains("3-stage"));
739 assert!(iht_step_cp_async_ptx(80).contains("2-stage"));
740 }
741
742 #[test]
745 fn fp8_correlate_uses_e4m3_on_ada_hopper() {
746 for sm in [89u32, 90, 100] {
747 let s = correlate_fp8_ptx(sm);
748 assert!(
749 s.contains(".visible .entry correlate_fp8_kernel"),
750 "sm {sm}"
751 );
752 assert!(
753 s.contains("cvt.rn.f32.e4m3x2"),
754 "sm {sm} missing e4m3 convert"
755 );
756 assert!(s.contains("fma.rn.f32"), "sm {sm} f32 accumulation");
757 assert!(s.contains("ret"), "sm {sm}");
758 }
759 }
760
761 #[test]
762 fn fp8_correlate_falls_back_pre_ada() {
763 for sm in [75u32, 80, 86] {
764 let s = correlate_fp8_ptx(sm);
765 assert!(!s.contains("e4m3"), "sm {sm} should not emit FP8 path");
766 assert_eq!(s, crate::ptx_kernels::correlate_ptx(sm), "sm {sm} fallback");
767 }
768 }
769
770 #[test]
773 fn warpshuffle_svt_has_full_butterfly() {
774 for sm in SMS {
775 let s = svt_threshold_warpshuffle_ptx(sm);
776 assert!(
777 s.contains(".visible .entry svt_threshold_ws_kernel"),
778 "sm {sm}"
779 );
780 for off in [16u32, 8, 4, 2, 1] {
782 assert!(
783 s.contains(&format!(
784 "shfl.sync.down.b32 %f4, %f3, {off}, 31, 0xFFFFFFFF"
785 )),
786 "sm {sm} missing shuffle offset {off}"
787 );
788 }
789 assert!(s.contains("max.f32"), "sm {sm} missing threshold max");
791 assert!(s.contains("st.global.f32"), "sm {sm}");
792 assert!(s.contains("ret"), "sm {sm}");
793 }
794 }
795
796 #[test]
797 fn warpshuffle_svt_isa_versions() {
798 assert!(svt_threshold_warpshuffle_ptx(75).contains(".version 7.5"));
799 assert!(svt_threshold_warpshuffle_ptx(80).contains(".version 8.0"));
800 assert!(svt_threshold_warpshuffle_ptx(90).contains(".version 8.4"));
801 assert!(svt_threshold_warpshuffle_ptx(100).contains(".version 8.7"));
802 }
803
804 #[test]
807 fn all_advanced_kernels_nonempty_and_balanced_braces() {
808 type KernelFn = fn(u32) -> String;
809 let kernels: [(&str, KernelFn); 4] = [
810 ("correlate_tma", correlate_tma_ptx),
811 ("iht_step_cp_async", iht_step_cp_async_ptx),
812 ("correlate_fp8", correlate_fp8_ptx),
813 ("svt_threshold_ws", svt_threshold_warpshuffle_ptx),
814 ];
815 for (name, f) in kernels {
816 for sm in SMS {
817 let s = f(sm);
818 assert!(!s.is_empty(), "{name} sm {sm} empty");
819 assert!(s.contains(".visible .entry"), "{name} sm {sm} no entry");
820 assert!(s.contains("ret"), "{name} sm {sm} no ret");
821 let opens = s.matches('{').count();
822 let closes = s.matches('}').count();
823 assert_eq!(opens, closes, "{name} sm {sm} unbalanced braces");
824 assert!(s.starts_with(".version"), "{name} sm {sm} no header");
825 }
826 }
827 }
828}