Skip to main content

oxicuda_cs/
ptx_advanced.rs

1//! Architecture-specialised PTX kernel variants for compressed-sensing operations.
2//!
3//! This module deepens the seven portable kernels in [`crate::ptx_kernels`] with
4//! per-SM tuned launch geometry and four hardware-feature-specific kernel variants:
5//!
6//! | Function | Feature exploited | Target SM |
7//! |----------|-------------------|-----------|
8//! | [`TileConfig::for_sm`] | tuned block / pipeline geometry | all |
9//! | [`correlate_tma_ptx`] | Hopper TMA bulk async copy (`cp.async.bulk`) | ≥ 90 |
10//! | [`iht_step_cp_async_ptx`] | Ampere 3-stage `cp.async` pipeline | ≥ 80 |
11//! | [`correlate_fp8_ptx`] | Ada / Hopper FP8 (e4m3) storage, FP32 accum | ≥ 89 |
12//! | [`svt_threshold_warpshuffle_ptx`] | warp-shuffle reduction (`shfl.sync.down`) | all |
13//!
14//! Every function emits a self-contained PTX module **string**; nothing here
15//! requires a GPU or `nvcc`. The strings encode the correct ISA version per SM,
16//! select the right intrinsics for the target generation, and degrade gracefully
17//! (a portable fallback body) when the requested SM predates the feature.
18//!
19//! PTX ISA selection by SM, matching [`crate::ptx_kernels`]:
20//! `SM ≥ 100 → 8.7` (Blackwell), `SM ≥ 90 → 8.4` (Hopper),
21//! `SM ≥ 80 → 8.0` (Ampere), else `7.5` (Turing).
22//!
23//! IMPORTANT: like [`crate::ptx_kernels`], all kernel bodies use **string
24//! concatenation** (NOT `format!()`) wherever `%rd`/`%r`/`%f`/`%fd` register names
25//! appear, since Rust's format macro would treat `{...}`-free `%` tokens fine but a
26//! stray `{` inside hand-written PTX would break compilation. Only the header and
27//! integer-substituted prologues use `format!`.
28
29use crate::ptx_kernels::correlate_ptx;
30
31// ─────────────────────────────────────────────────────────────────────────────
32// PTX header (mirrors ptx_kernels::ptx_header, kept private here)
33// ─────────────────────────────────────────────────────────────────────────────
34
35/// Build a PTX file header string for the given SM version.
36fn 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// ─────────────────────────────────────────────────────────────────────────────
50// Per-SM tile / thread-block configuration
51// ─────────────────────────────────────────────────────────────────────────────
52
53/// Tuned launch geometry and software-pipeline depth for a kernel on a given SM.
54///
55/// Replaces the single portable default (block = 256, 1 stage) that
56/// [`crate::ptx_kernels`] hard-codes. The values follow the per-SM table in the
57/// crate `TODO.md` "Architecture-Specific Deepening" section:
58///
59/// | SM | `block_x` (`correlate`) | pipeline stages |
60/// |----|------------------------|-----------------|
61/// | 75 (Turing) | 128 | 1 |
62/// | 80 / 86 (Ampere) | 256 | 2 |
63/// | 89 (Ada) | 256 | 2 |
64/// | 90 (Hopper) | 512 | 3 |
65/// | 100 (Blackwell) | 512 | 3 |
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct TileConfig {
68    /// Thread-block extent along x (threads per CTA).
69    pub block_x: u32,
70    /// Thread-block extent along y.
71    pub block_y: u32,
72    /// Number of software-pipeline stages (1 = no overlap).
73    pub stages: u32,
74    /// Shared-memory bytes the kernel should stage per pipeline buffer.
75    pub smem_bytes_per_stage: u32,
76    /// Whether the SM can issue `cp.async` global→shared copies (Ampere+).
77    pub cp_async: bool,
78    /// Whether the SM can issue `cp.async.bulk` TMA copies (Hopper+).
79    pub tma: bool,
80}
81
82impl TileConfig {
83    /// The portable default used historically by [`crate::ptx_kernels`]:
84    /// 256×1 threads, single stage, no async copy.
85    #[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    /// Tuned configuration for the `correlate` kernel on the given SM.
98    ///
99    /// `correlate` reads one column of Φ per thread streaming over the `m` rows,
100    /// so the staged buffer holds `block_x` f32 residual elements per stage.
101    #[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    /// Grid extent along x to cover `n` elements with this block size:
121    /// `ceil(n / block_x)`, at least 1.
122    #[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    /// Total threads per CTA (`block_x * block_y`).
129    #[must_use]
130    pub fn threads_per_block(self) -> u32 {
131        self.block_x.saturating_mul(self.block_y)
132    }
133
134    /// Total dynamic shared memory the launch should request
135    /// (`stages * smem_bytes_per_stage`).
136    #[must_use]
137    pub fn total_smem_bytes(self) -> u32 {
138        self.stages.saturating_mul(self.smem_bytes_per_stage)
139    }
140}
141
142// ─────────────────────────────────────────────────────────────────────────────
143// Hopper TMA correlate: cp.async.bulk staging of the residual vector
144// ─────────────────────────────────────────────────────────────────────────────
145
146/// `correlate` for very tall Φ, staging the residual `r` into shared memory with
147/// the Hopper Tensor Memory Accelerator (`cp.async.bulk`) before the dot product.
148///
149/// Computes `c[j] = Σ_i Φ[i, j] · r[i]` exactly as [`correlate_ptx`], but the
150/// `r` vector — reused by every thread in the CTA — is bulk-copied global→shared
151/// once via `cp.async.bulk` and a shared mbarrier, then each thread reads `r`
152/// from shared memory. This removes redundant global loads of `r` on the tall-Φ
153/// regime the `TODO.md` flags for Hopper.
154///
155/// For `sm < 90` the feature is unavailable; this falls back to the portable
156/// [`correlate_ptx`] kernel so the returned string still assembles for that target.
157///
158/// Signature (SM ≥ 90): `correlate_tma_kernel(c, phi, r, m, n)`, Φ row-major m×n.
159/// Grid = (ceil(n/`block_x`), 1, 1) with `block_x` from [`TileConfig::for_sm`].
160#[must_use]
161pub fn correlate_tma_ptx(sm: u32) -> String {
162    if sm < 90 {
163        // Feature predates Hopper: emit the portable correlate under the same name
164        // so callers targeting older SMs still get a valid, equivalent module.
165        return correlate_ptx(sm);
166    }
167    let hdr = ptx_header(sm);
168    let cfg = TileConfig::for_sm(sm);
169    // Prologue with the substituted shared-memory tile size (block_x * 4 bytes).
170    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// ─────────────────────────────────────────────────────────────────────────────
266// Ampere 3-stage cp.async IHT step
267// ─────────────────────────────────────────────────────────────────────────────
268
269/// IHT update step `x[i] += mu * grad[i]` with an Ampere multi-stage `cp.async`
270/// prefetch of the `grad` stream into shared memory.
271///
272/// Functionally identical to [`crate::ptx_kernels::iht_step_ptx`]
273/// (`x = x + mu·grad`, host hard-thresholds afterwards), but the gradient block
274/// for this CTA is staged global→shared with `cp.async.cg.shared.global` so that
275/// the multiply-add overlaps the load latency. The number of in-flight stages is
276/// taken from [`TileConfig::for_sm`] (2 on Ampere/Ada, 3 on Hopper/Blackwell).
277///
278/// For `sm < 80` (`cp.async` unavailable) this delegates to the portable
279/// [`crate::ptx_kernels::iht_step_ptx`].
280///
281/// Signature: `iht_step_cp_async_kernel(x, grad, mu, n)`.
282#[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        @%p0 bra $CA_DONE;\n\
320    \n\
321        // Stage this thread's grad element global->shared via cp.async (4 bytes).\n\
322        mov.u64       %rd2, grad_tile;\n\
323        mul.wide.u32  %rd3, %r3, 4;\n\
324        add.u64       %rd4, %rd2, %rd3;\n\
325        mul.wide.u32  %rd5, %r4, 4;\n\
326        add.u64       %rd6, %rd1, %rd5;\n\
327        cp.async.cg.shared.global [%rd4], [%rd6], 4;\n\
328        cp.async.commit_group;\n\
329        cp.async.wait_group 0;\n\
330        bar.sync      0;\n\
331    \n\
332        // x[i] += mu * grad_tile[tid]\n\
333        ld.shared.f32 %f2, [%rd4];\n\
334        add.u64       %rd7, %rd0, %rd5;\n\
335        ld.global.f32 %f1, [%rd7];\n\
336        fma.rn.f32    %f3, %f0, %f2, %f1;\n\
337        st.global.f32 [%rd7], %f3;\n\
338    \n\
339    $CA_DONE:\n\
340        ret;\n\
341    }\n";
342    hdr + &prologue + body
343}
344
345// ─────────────────────────────────────────────────────────────────────────────
346// Ada / Hopper FP8 (e4m3) correlate with FP32 accumulation
347// ─────────────────────────────────────────────────────────────────────────────
348
349/// `correlate` with FP8 (e4m3) storage for Φ and `r`, accumulating in FP32.
350///
351/// On memory-bound large-`n` problems halving the byte traffic of Φ via e4m3
352/// storage is a large win; accuracy is preserved by up-converting to f32 before
353/// the multiply-add (`cvt.rn.f32.e4m3x2`) and accumulating in f32, exactly the
354/// pattern the `TODO.md` requests for Ada/Hopper.
355///
356/// Layout: Φ and `r` are packed FP8 (1 byte each). Two e4m3 values are unpacked
357/// per `cvt.rn.f32.e4m3x2`; this kernel processes the `m` rows two at a time and
358/// handles an odd tail element with a single-lane convert.
359///
360/// For `sm < 89` (no e4m3 hardware path) this delegates to the portable f32
361/// [`correlate_ptx`].
362///
363/// Signature (SM ≥ 89): `correlate_fp8_kernel(c, phi_e4m3, r_e4m3, m, n)`.
364/// Output `c` is f32. Φ is row-major m×n FP8, `r` is length-m FP8.
365#[must_use]
366pub fn correlate_fp8_ptx(sm: u32) -> String {
367    if sm < 89 {
368        return correlate_ptx(sm);
369    }
370    let hdr = ptx_header(sm);
371    let body = ".visible .entry correlate_fp8_kernel(\n\
372        .param .u64 p_c,\n\
373        .param .u64 p_phi,\n\
374        .param .u64 p_r,\n\
375        .param .u32 p_m,\n\
376        .param .u32 p_n\n\
377    )\n\
378    {\n\
379        .reg .u64  %rd<12>;\n\
380        .reg .u32  %r<20>;\n\
381        .reg .f32  %f<12>;\n\
382        .reg .b32  %rb<6>;\n\
383        .reg .b16  %rh<6>;\n\
384        .reg .pred %p0;\n\
385        .reg .pred %p1;\n\
386    \n\
387        ld.param.u64  %rd0, [p_c];\n\
388        ld.param.u64  %rd1, [p_phi];\n\
389        ld.param.u64  %rd2, [p_r];\n\
390        ld.param.u32  %r0,  [p_m];\n\
391        ld.param.u32  %r1,  [p_n];\n\
392    \n\
393        mov.u32       %r2, %ntid.x;\n\
394        mov.u32       %r3, %ctaid.x;\n\
395        mov.u32       %r4, %tid.x;\n\
396        mad.lo.u32    %r5, %r2, %r3, %r4;\n\
397    \n\
398        setp.ge.u32   %p0, %r5, %r1;\n\
399        @%p0 bra $F8_DONE;\n\
400    \n\
401        mov.f32       %f0, 0f00000000;\n\
402        mov.u32       %r6, 0;\n\
403    \n\
404    $F8_LOOP:\n\
405        // process rows i and i+1 together when both in range.\n\
406        add.u32       %r7, %r6, 1;\n\
407        setp.ge.u32   %p0, %r6, %r0;\n\
408        @%p0 bra $F8_WRITE;\n\
409        setp.ge.u32   %p1, %r7, %r0;\n\
410        @%p1 bra $F8_TAIL;\n\
411    \n\
412        // phi byte offset for (i, j): i*n + j ; pair stride along i is n.\n\
413        mul.lo.u32    %r8, %r6, %r1;\n\
414        add.u32       %r8, %r8, %r5;\n\
415        cvt.u64.u32   %rd3, %r8;\n\
416        add.u64       %rd4, %rd1, %rd3;\n\
417        ld.global.u8  %rh0, [%rd4];\n\
418        mul.lo.u32    %r9, %r7, %r1;\n\
419        add.u32       %r9, %r9, %r5;\n\
420        cvt.u64.u32   %rd5, %r9;\n\
421        add.u64       %rd6, %rd1, %rd5;\n\
422        ld.global.u8  %rh1, [%rd6];\n\
423        // pack the two e4m3 phi bytes into a b16, unpack to two f32.\n\
424        shl.b16       %rh2, %rh1, 8;\n\
425        or.b16        %rh2, %rh2, %rh0;\n\
426        cvt.rn.f32.e4m3x2 %rb0, %rh2;\n\
427        mov.b32       {%rh3, %rh4}, %rb0;\n\
428        cvt.f32.f16   %f1, %rh3;\n\
429        cvt.f32.f16   %f2, %rh4;\n\
430    \n\
431        // r bytes for rows i, i+1.\n\
432        cvt.u64.u32   %rd7, %r6;\n\
433        add.u64       %rd8, %rd2, %rd7;\n\
434        ld.global.u8  %rh0, [%rd8];\n\
435        cvt.u64.u32   %rd9, %r7;\n\
436        add.u64       %rd10, %rd2, %rd9;\n\
437        ld.global.u8  %rh1, [%rd10];\n\
438        shl.b16       %rh2, %rh1, 8;\n\
439        or.b16        %rh2, %rh2, %rh0;\n\
440        cvt.rn.f32.e4m3x2 %rb1, %rh2;\n\
441        mov.b32       {%rh3, %rh4}, %rb1;\n\
442        cvt.f32.f16   %f3, %rh3;\n\
443        cvt.f32.f16   %f4, %rh4;\n\
444    \n\
445        fma.rn.f32    %f0, %f1, %f3, %f0;\n\
446        fma.rn.f32    %f0, %f2, %f4, %f0;\n\
447        add.u32       %r6, %r6, 2;\n\
448        bra $F8_LOOP;\n\
449    \n\
450    $F8_TAIL:\n\
451        // single odd row i = %r6.\n\
452        mul.lo.u32    %r8, %r6, %r1;\n\
453        add.u32       %r8, %r8, %r5;\n\
454        cvt.u64.u32   %rd3, %r8;\n\
455        add.u64       %rd4, %rd1, %rd3;\n\
456        ld.global.u8  %rh0, [%rd4];\n\
457        cvt.rn.f32.e4m3x2 %rb0, %rh0;\n\
458        mov.b32       {%rh3, %rh4}, %rb0;\n\
459        cvt.f32.f16   %f1, %rh3;\n\
460        cvt.u64.u32   %rd7, %r6;\n\
461        add.u64       %rd8, %rd2, %rd7;\n\
462        ld.global.u8  %rh0, [%rd8];\n\
463        cvt.rn.f32.e4m3x2 %rb1, %rh0;\n\
464        mov.b32       {%rh3, %rh4}, %rb1;\n\
465        cvt.f32.f16   %f3, %rh3;\n\
466        fma.rn.f32    %f0, %f1, %f3, %f0;\n\
467        add.u32       %r6, %r6, 1;\n\
468        bra $F8_LOOP;\n\
469    \n\
470    $F8_WRITE:\n\
471        mul.wide.u32  %rd3, %r5, 4;\n\
472        add.u64       %rd4, %rd0, %rd3;\n\
473        st.global.f32 [%rd4], %f0;\n\
474    \n\
475    $F8_DONE:\n\
476        ret;\n\
477    }\n";
478    hdr + body
479}
480
481// ─────────────────────────────────────────────────────────────────────────────
482// Warp-shuffle SVT threshold + reduced nuclear-norm contribution
483// ─────────────────────────────────────────────────────────────────────────────
484
485/// SVT per-singular-value soft-threshold `σ'[i] = max(σ[i] − τ, 0)` that ALSO
486/// reduces the thresholded nuclear-norm contribution `Σ σ'[i]` per warp using
487/// `shfl.sync.down.b32`, writing one partial sum per warp.
488///
489/// For ranks ≤ 32 the whole spectrum fits in a single warp, so the warp-shuffle
490/// reduction yields the nuclear norm of the thresholded matrix in a handful of
491/// instructions with no shared memory — the optimisation the `TODO.md` flags for
492/// `svt_threshold`. The element-wise output matches
493/// [`crate::ptx_kernels::svt_threshold_ptx`].
494///
495/// Signature: `svt_threshold_ws_kernel(sigma_out, nucnorm_partial, sigma_in, tau, n)`.
496/// `nucnorm_partial` receives one f32 per warp (lane 0 writes `warp_id`).
497#[must_use]
498pub fn svt_threshold_warpshuffle_ptx(sm: u32) -> String {
499    let hdr = ptx_header(sm);
500    let body = ".visible .entry svt_threshold_ws_kernel(\n\
501        .param .u64 p_sigma_out,\n\
502        .param .u64 p_nucnorm_partial,\n\
503        .param .u64 p_sigma_in,\n\
504        .param .f32 p_tau,\n\
505        .param .u32 p_n\n\
506    )\n\
507    {\n\
508        .reg .u64  %rd<12>;\n\
509        .reg .u32  %r<20>;\n\
510        .reg .f32  %f<8>;\n\
511        .reg .pred %p0;\n\
512        .reg .pred %p1;\n\
513    \n\
514        ld.param.u64  %rd0, [p_sigma_out];\n\
515        ld.param.u64  %rd1, [p_nucnorm_partial];\n\
516        ld.param.u64  %rd2, [p_sigma_in];\n\
517        ld.param.f32  %f0,  [p_tau];\n\
518        ld.param.u32  %r0,  [p_n];\n\
519    \n\
520        mov.u32       %r1, %ntid.x;\n\
521        mov.u32       %r2, %ctaid.x;\n\
522        mov.u32       %r3, %tid.x;\n\
523        mad.lo.u32    %r4, %r1, %r2, %r3;\n\
524    \n\
525        // Threshold (out-of-range threads contribute 0 to the warp sum).\n\
526        mov.f32       %f3, 0f00000000;\n\
527        setp.ge.u32   %p0, %r4, %r0;\n\
528        @%p0 bra $WS_REDUCE;\n\
529    \n\
530        mul.wide.u32  %rd3, %r4, 4;\n\
531        add.u64       %rd4, %rd2, %rd3;\n\
532        ld.global.f32 %f1, [%rd4];\n\
533        sub.f32       %f2, %f1, %f0;\n\
534        max.f32       %f3, %f2, 0f00000000;\n\
535        add.u64       %rd5, %rd0, %rd3;\n\
536        st.global.f32 [%rd5], %f3;\n\
537    \n\
538    $WS_REDUCE:\n\
539        // Warp-level sum of %f3 via butterfly down-shuffle (full 32-lane mask).\n\
540        shfl.sync.down.b32 %f4, %f3, 16, 31, 0xFFFFFFFF;\n\
541        add.f32       %f3, %f3, %f4;\n\
542        shfl.sync.down.b32 %f4, %f3, 8, 31, 0xFFFFFFFF;\n\
543        add.f32       %f3, %f3, %f4;\n\
544        shfl.sync.down.b32 %f4, %f3, 4, 31, 0xFFFFFFFF;\n\
545        add.f32       %f3, %f3, %f4;\n\
546        shfl.sync.down.b32 %f4, %f3, 2, 31, 0xFFFFFFFF;\n\
547        add.f32       %f3, %f3, %f4;\n\
548        shfl.sync.down.b32 %f4, %f3, 1, 31, 0xFFFFFFFF;\n\
549        add.f32       %f3, %f3, %f4;\n\
550    \n\
551        // Lane 0 of each warp writes its partial nuclear-norm sum.\n\
552        and.b32       %r5, %r3, 31;\n\
553        setp.ne.u32   %p1, %r5, 0;\n\
554        @%p1 bra $WS_DONE;\n\
555        shr.u32       %r6, %r4, 5;\n\
556        mul.wide.u32  %rd6, %r6, 4;\n\
557        add.u64       %rd7, %rd1, %rd6;\n\
558        st.global.f32 [%rd7], %f3;\n\
559    \n\
560    $WS_DONE:\n\
561        ret;\n\
562    }\n";
563    hdr + body
564}
565
566// ─────────────────────────────────────────────────────────────────────────────
567// Tests (CPU-side: PTX is generated as strings; we assert structure / ISA / feature
568// intrinsics, and that the per-SM tile configuration matches the documented table)
569// ─────────────────────────────────────────────────────────────────────────────
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574
575    const SMS: [u32; 6] = [75, 80, 86, 89, 90, 100];
576
577    // ── TileConfig ────────────────────────────────────────────────────────
578
579    #[test]
580    fn tile_config_matches_documented_table() {
581        // Turing.
582        let t75 = TileConfig::for_sm(75);
583        assert_eq!(t75.block_x, 128);
584        assert_eq!(t75.stages, 1);
585        assert!(!t75.cp_async);
586        assert!(!t75.tma);
587        // Ampere.
588        for sm in [80u32, 86] {
589            let c = TileConfig::for_sm(sm);
590            assert_eq!(c.block_x, 256, "sm {sm}");
591            assert_eq!(c.stages, 2, "sm {sm}");
592            assert!(c.cp_async, "sm {sm}");
593            assert!(!c.tma, "sm {sm}");
594        }
595        // Ada keeps Ampere geometry but is still pre-TMA.
596        let t89 = TileConfig::for_sm(89);
597        assert_eq!(t89.block_x, 256);
598        assert_eq!(t89.stages, 2);
599        assert!(t89.cp_async);
600        assert!(!t89.tma);
601        // Hopper / Blackwell.
602        for sm in [90u32, 100] {
603            let c = TileConfig::for_sm(sm);
604            assert_eq!(c.block_x, 512, "sm {sm}");
605            assert_eq!(c.stages, 3, "sm {sm}");
606            assert!(c.cp_async, "sm {sm}");
607            assert!(c.tma, "sm {sm}");
608        }
609    }
610
611    #[test]
612    fn tile_config_smem_is_block_x_f32() {
613        for sm in SMS {
614            let c = TileConfig::for_sm(sm);
615            assert_eq!(c.smem_bytes_per_stage, c.block_x * 4);
616            assert_eq!(c.total_smem_bytes(), c.stages * c.block_x * 4);
617        }
618    }
619
620    #[test]
621    fn tile_config_grid_ceildiv() {
622        let c = TileConfig::for_sm(80); // block_x = 256
623        assert_eq!(c.grid_x(256), 1);
624        assert_eq!(c.grid_x(257), 2);
625        assert_eq!(c.grid_x(512), 2);
626        assert_eq!(c.grid_x(1), 1);
627        // n == 0 still yields a launchable single block.
628        assert_eq!(c.grid_x(0), 1);
629    }
630
631    #[test]
632    fn tile_config_threads_per_block() {
633        let c = TileConfig::for_sm(90);
634        assert_eq!(c.threads_per_block(), 512);
635        let d = TileConfig::portable_default();
636        assert_eq!(d.threads_per_block(), 256);
637        assert_eq!(d.stages, 1);
638        assert!(!d.cp_async && !d.tma);
639    }
640
641    // ── Hopper TMA correlate ──────────────────────────────────────────────
642
643    #[test]
644    fn tma_correlate_has_bulk_copy_on_hopper() {
645        for sm in [90u32, 100] {
646            let s = correlate_tma_ptx(sm);
647            assert!(
648                s.contains(".visible .entry correlate_tma_kernel"),
649                "sm {sm}"
650            );
651            assert!(s.contains("cp.async.bulk"), "sm {sm} missing TMA bulk copy");
652            assert!(
653                s.contains("mbarrier.init.shared.b64"),
654                "sm {sm} missing mbarrier"
655            );
656            assert!(
657                s.contains(".shared .align 16 .b8 r_tile"),
658                "sm {sm} missing staging tile"
659            );
660            assert!(s.contains("ret"), "sm {sm}");
661        }
662        // ISA version follows SM.
663        assert!(correlate_tma_ptx(90).contains(".version 8.4"));
664        assert!(correlate_tma_ptx(100).contains(".version 8.7"));
665    }
666
667    #[test]
668    fn tma_correlate_falls_back_pre_hopper() {
669        // Pre-Hopper has no TMA; it must degrade to the portable correlate kernel.
670        for sm in [75u32, 80, 86, 89] {
671            let s = correlate_tma_ptx(sm);
672            assert!(!s.contains("cp.async.bulk"), "sm {sm} should not emit TMA");
673            assert!(
674                s.contains(".visible .entry correlate_kernel"),
675                "sm {sm} fallback"
676            );
677            assert_eq!(
678                s,
679                crate::ptx_kernels::correlate_ptx(sm),
680                "sm {sm} exact fallback"
681            );
682        }
683    }
684
685    // ── Ampere cp.async IHT ───────────────────────────────────────────────
686
687    #[test]
688    fn cp_async_iht_has_async_copy_on_ampere_plus() {
689        for sm in [80u32, 86, 89, 90, 100] {
690            let s = iht_step_cp_async_ptx(sm);
691            assert!(
692                s.contains(".visible .entry iht_step_cp_async_kernel"),
693                "sm {sm}"
694            );
695            assert!(
696                s.contains("cp.async.cg.shared.global"),
697                "sm {sm} missing cp.async"
698            );
699            assert!(
700                s.contains("cp.async.commit_group"),
701                "sm {sm} missing commit"
702            );
703            assert!(s.contains("cp.async.wait_group"), "sm {sm} missing wait");
704            assert!(
705                s.contains(".shared .align 16 .b8 grad_tile"),
706                "sm {sm} stage buf"
707            );
708            assert!(s.contains("fma.rn.f32"), "sm {sm} missing the IHT FMA");
709            assert!(s.contains("ret"), "sm {sm}");
710        }
711    }
712
713    #[test]
714    fn cp_async_iht_falls_back_pre_ampere() {
715        let s = iht_step_cp_async_ptx(75);
716        assert!(!s.contains("cp.async"), "Turing should not emit cp.async");
717        assert_eq!(s, crate::ptx_kernels::iht_step_ptx(75));
718    }
719
720    #[test]
721    fn cp_async_iht_stage_count_in_comment() {
722        // Hopper stages 3, Ampere stages 2 — encoded in the staging comment.
723        assert!(iht_step_cp_async_ptx(90).contains("3-stage"));
724        assert!(iht_step_cp_async_ptx(80).contains("2-stage"));
725    }
726
727    // ── FP8 correlate ─────────────────────────────────────────────────────
728
729    #[test]
730    fn fp8_correlate_uses_e4m3_on_ada_hopper() {
731        for sm in [89u32, 90, 100] {
732            let s = correlate_fp8_ptx(sm);
733            assert!(
734                s.contains(".visible .entry correlate_fp8_kernel"),
735                "sm {sm}"
736            );
737            assert!(
738                s.contains("cvt.rn.f32.e4m3x2"),
739                "sm {sm} missing e4m3 convert"
740            );
741            assert!(s.contains("fma.rn.f32"), "sm {sm} f32 accumulation");
742            assert!(s.contains("ret"), "sm {sm}");
743        }
744    }
745
746    #[test]
747    fn fp8_correlate_falls_back_pre_ada() {
748        for sm in [75u32, 80, 86] {
749            let s = correlate_fp8_ptx(sm);
750            assert!(!s.contains("e4m3"), "sm {sm} should not emit FP8 path");
751            assert_eq!(s, crate::ptx_kernels::correlate_ptx(sm), "sm {sm} fallback");
752        }
753    }
754
755    // ── Warp-shuffle SVT ──────────────────────────────────────────────────
756
757    #[test]
758    fn warpshuffle_svt_has_full_butterfly() {
759        for sm in SMS {
760            let s = svt_threshold_warpshuffle_ptx(sm);
761            assert!(
762                s.contains(".visible .entry svt_threshold_ws_kernel"),
763                "sm {sm}"
764            );
765            // All five butterfly offsets present.
766            for off in [16u32, 8, 4, 2, 1] {
767                assert!(
768                    s.contains(&format!(
769                        "shfl.sync.down.b32 %f4, %f3, {off}, 31, 0xFFFFFFFF"
770                    )),
771                    "sm {sm} missing shuffle offset {off}"
772                );
773            }
774            // Soft-threshold core preserved.
775            assert!(s.contains("max.f32"), "sm {sm} missing threshold max");
776            assert!(s.contains("st.global.f32"), "sm {sm}");
777            assert!(s.contains("ret"), "sm {sm}");
778        }
779    }
780
781    #[test]
782    fn warpshuffle_svt_isa_versions() {
783        assert!(svt_threshold_warpshuffle_ptx(75).contains(".version 7.5"));
784        assert!(svt_threshold_warpshuffle_ptx(80).contains(".version 8.0"));
785        assert!(svt_threshold_warpshuffle_ptx(90).contains(".version 8.4"));
786        assert!(svt_threshold_warpshuffle_ptx(100).contains(".version 8.7"));
787    }
788
789    // ── Cross-cutting structural sanity over all advanced kernels ─────────
790
791    #[test]
792    fn all_advanced_kernels_nonempty_and_balanced_braces() {
793        type KernelFn = fn(u32) -> String;
794        let kernels: [(&str, KernelFn); 4] = [
795            ("correlate_tma", correlate_tma_ptx),
796            ("iht_step_cp_async", iht_step_cp_async_ptx),
797            ("correlate_fp8", correlate_fp8_ptx),
798            ("svt_threshold_ws", svt_threshold_warpshuffle_ptx),
799        ];
800        for (name, f) in kernels {
801            for sm in SMS {
802                let s = f(sm);
803                assert!(!s.is_empty(), "{name} sm {sm} empty");
804                assert!(s.contains(".visible .entry"), "{name} sm {sm} no entry");
805                assert!(s.contains("ret"), "{name} sm {sm} no ret");
806                let opens = s.matches('{').count();
807                let closes = s.matches('}').count();
808                assert_eq!(opens, closes, "{name} sm {sm} unbalanced braces");
809                assert!(s.starts_with(".version"), "{name} sm {sm} no header");
810            }
811        }
812    }
813}