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    \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// ─────────────────────────────────────────────────────────────────────────────
361// Ada / Hopper FP8 (e4m3) correlate with FP32 accumulation
362// ─────────────────────────────────────────────────────────────────────────────
363
364/// `correlate` with FP8 (e4m3) storage for Φ and `r`, accumulating in FP32.
365///
366/// On memory-bound large-`n` problems halving the byte traffic of Φ via e4m3
367/// storage is a large win; accuracy is preserved by up-converting to f32 before
368/// the multiply-add (`cvt.rn.f32.e4m3x2`) and accumulating in f32, exactly the
369/// pattern the `TODO.md` requests for Ada/Hopper.
370///
371/// Layout: Φ and `r` are packed FP8 (1 byte each). Two e4m3 values are unpacked
372/// per `cvt.rn.f32.e4m3x2`; this kernel processes the `m` rows two at a time and
373/// handles an odd tail element with a single-lane convert.
374///
375/// For `sm < 89` (no e4m3 hardware path) this delegates to the portable f32
376/// [`correlate_ptx`].
377///
378/// Signature (SM ≥ 89): `correlate_fp8_kernel(c, phi_e4m3, r_e4m3, m, n)`.
379/// Output `c` is f32. Φ is row-major m×n FP8, `r` is length-m FP8.
380#[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// ─────────────────────────────────────────────────────────────────────────────
497// Warp-shuffle SVT threshold + reduced nuclear-norm contribution
498// ─────────────────────────────────────────────────────────────────────────────
499
500/// SVT per-singular-value soft-threshold `σ'[i] = max(σ[i] − τ, 0)` that ALSO
501/// reduces the thresholded nuclear-norm contribution `Σ σ'[i]` per warp using
502/// `shfl.sync.down.b32`, writing one partial sum per warp.
503///
504/// For ranks ≤ 32 the whole spectrum fits in a single warp, so the warp-shuffle
505/// reduction yields the nuclear norm of the thresholded matrix in a handful of
506/// instructions with no shared memory — the optimisation the `TODO.md` flags for
507/// `svt_threshold`. The element-wise output matches
508/// [`crate::ptx_kernels::svt_threshold_ptx`].
509///
510/// Signature: `svt_threshold_ws_kernel(sigma_out, nucnorm_partial, sigma_in, tau, n)`.
511/// `nucnorm_partial` receives one f32 per warp (lane 0 writes `warp_id`).
512#[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// ─────────────────────────────────────────────────────────────────────────────
582// Tests (CPU-side: PTX is generated as strings; we assert structure / ISA / feature
583// intrinsics, and that the per-SM tile configuration matches the documented table)
584// ─────────────────────────────────────────────────────────────────────────────
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589
590    const SMS: [u32; 6] = [75, 80, 86, 89, 90, 100];
591
592    // ── TileConfig ────────────────────────────────────────────────────────
593
594    #[test]
595    fn tile_config_matches_documented_table() {
596        // Turing.
597        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        // Ampere.
603        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        // Ada keeps Ampere geometry but is still pre-TMA.
611        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        // Hopper / Blackwell.
617        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); // block_x = 256
638        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        // n == 0 still yields a launchable single block.
643        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    // ── Hopper TMA correlate ──────────────────────────────────────────────
657
658    #[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        // ISA version follows SM.
678        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        // Pre-Hopper has no TMA; it must degrade to the portable correlate kernel.
685        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    // ── Ampere cp.async IHT ───────────────────────────────────────────────
701
702    #[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        // Hopper stages 3, Ampere stages 2 — encoded in the staging comment.
738        assert!(iht_step_cp_async_ptx(90).contains("3-stage"));
739        assert!(iht_step_cp_async_ptx(80).contains("2-stage"));
740    }
741
742    // ── FP8 correlate ─────────────────────────────────────────────────────
743
744    #[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    // ── Warp-shuffle SVT ──────────────────────────────────────────────────
771
772    #[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            // All five butterfly offsets present.
781            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            // Soft-threshold core preserved.
790            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    // ── Cross-cutting structural sanity over all advanced kernels ─────────
805
806    #[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}