Skip to main content

logicaffeine_compile/
codegen_sass.rs

1//! codegen_sass — the foundation of our OWN sm_86 SASS assembler (the MaxAs / turingas lever).
2//!
3//! `ptxas` is the only PTX→SASS path and it is closed-source: it fixes our register allocation and, crucially,
4//! the per-instruction CONTROL CODES (stall counts, yields, read/write barriers, the operand reuse cache) that
5//! decide how well a hot loop hides latency. That last 5-20% cuBLAS keeps lives there. To take it we must read
6//! AND write SASS ourselves. This module is that capability: locate the kernel's machine code in the cubin ELF,
7//! decode every 128-bit instruction's control field (verified bit-exact against `nvdisasm` on sm_86), re-encode
8//! it, and patch the bytes back. A scheduler/optimizer on top is the follow-on; this is the assembler core.
9//!
10//! Ampere (sm_80/86) 128-bit instruction = (lo:u64, hi:u64). The control field sits in the high word:
11//!
12//! ```text
13//! stall   bits [41:44]  yield bit [45]  wr_barrier [46:48]  rd_barrier [49:51]  wait_mask [52:57]  reuse [58:61]
14//! ```
15//!
16//! (barrier index 7 = "none".) Verified: S2R/S2UR/memory ops set a write barrier; pure ALU ops do not.
17
18use std::path::Path;
19use std::process::Command;
20
21/// One decoded SASS instruction: its byte offset within the .text section and the raw 128-bit encoding.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct SassInsn {
24    pub offset: usize,
25    pub lo: u64,
26    pub hi: u64,
27}
28
29/// The per-instruction control codes — where the GPU's static scheduling lives.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct Control {
32    /// Fixed stall before issuing the next instruction (0–15 cycles).
33    pub stall: u8,
34    /// Yield hint (let another warp issue).
35    pub yield_: bool,
36    /// Write-barrier scoreboard this instruction sets (7 = none) — for variable-latency results.
37    pub wr_barrier: u8,
38    /// Read-barrier scoreboard this instruction sets (7 = none) — for operand-still-in-use.
39    pub rd_barrier: u8,
40    /// Which barrier scoreboards this instruction waits on (6-bit mask).
41    pub wait_mask: u8,
42    /// Operand reuse-cache flags (4 bits) — avoids re-reading the register file.
43    pub reuse: u8,
44}
45
46impl Control {
47    /// Decode the control field out of the high 64-bit word.
48    pub fn decode(hi: u64) -> Control {
49        Control {
50            stall: ((hi >> 41) & 0xF) as u8,
51            yield_: ((hi >> 45) & 0x1) != 0,
52            wr_barrier: ((hi >> 46) & 0x7) as u8,
53            rd_barrier: ((hi >> 49) & 0x7) as u8,
54            wait_mask: ((hi >> 52) & 0x3F) as u8,
55            reuse: ((hi >> 58) & 0xF) as u8,
56        }
57    }
58
59    /// Write this control field back into `hi`, replacing exactly the control bits and leaving the rest intact.
60    pub fn apply(&self, hi: u64) -> u64 {
61        // Clear bits [41:61] then set them from the fields.
62        let clear_mask: u64 = ((1u64 << (61 - 41 + 1)) - 1) << 41;
63        let mut h = hi & !clear_mask;
64        h |= ((self.stall & 0xF) as u64) << 41;
65        h |= ((self.yield_ as u64) & 0x1) << 45;
66        h |= ((self.wr_barrier & 0x7) as u64) << 46;
67        h |= ((self.rd_barrier & 0x7) as u64) << 49;
68        h |= ((self.wait_mask & 0x3F) as u64) << 52;
69        h |= ((self.reuse & 0xF) as u64) << 58;
70        h
71    }
72}
73
74/// Locate the `.text.<kernel>` section in a cubin ELF64: returns (file_offset, size_in_bytes).
75pub fn find_text_section(cubin: &[u8]) -> Option<(usize, usize)> {
76    if cubin.len() < 64 || &cubin[0..4] != b"\x7fELF" {
77        return None;
78    }
79    let rd_u64 = |o: usize| u64::from_le_bytes(cubin[o..o + 8].try_into().unwrap());
80    let rd_u16 = |o: usize| u16::from_le_bytes(cubin[o..o + 2].try_into().unwrap());
81    let e_shoff = rd_u64(0x28) as usize;
82    let e_shentsize = rd_u16(0x3a) as usize;
83    let e_shnum = rd_u16(0x3c) as usize;
84    let e_shstrndx = rd_u16(0x3e) as usize;
85    // section-header string table
86    let shstr_hdr = e_shoff + e_shstrndx * e_shentsize;
87    let shstr_off = rd_u64(shstr_hdr + 0x18) as usize;
88    for i in 0..e_shnum {
89        let sh = e_shoff + i * e_shentsize;
90        let name_off = u32::from_le_bytes(cubin[sh..sh + 4].try_into().unwrap()) as usize;
91        let name_start = shstr_off + name_off;
92        let name_end = cubin[name_start..]
93            .iter()
94            .position(|&b| b == 0)
95            .map(|p| name_start + p)?;
96        let name = std::str::from_utf8(&cubin[name_start..name_end]).ok()?;
97        if name.starts_with(".text.") {
98            let off = rd_u64(sh + 0x18) as usize;
99            let size = rd_u64(sh + 0x20) as usize;
100            return Some((off, size));
101        }
102    }
103    None
104}
105
106/// Parse every 128-bit instruction out of the kernel's .text section of a cubin.
107pub fn instructions(cubin: &[u8]) -> Vec<SassInsn> {
108    let Some((off, size)) = find_text_section(cubin) else {
109        return Vec::new();
110    };
111    let mut out = Vec::new();
112    let mut o = off;
113    while o + 16 <= off + size {
114        let lo = u64::from_le_bytes(cubin[o..o + 8].try_into().unwrap());
115        let hi = u64::from_le_bytes(cubin[o + 8..o + 16].try_into().unwrap());
116        out.push(SassInsn {
117            offset: o - off,
118            lo,
119            hi,
120        });
121        o += 16;
122    }
123    out
124}
125
126/// Patch instruction `idx`'s high word (control codes live here) in-place in the cubin bytes.
127pub fn patch_hi(cubin: &mut [u8], idx: usize, new_hi: u64) {
128    if let Some((off, _)) = find_text_section(cubin) {
129        let o = off + idx * 16 + 8;
130        cubin[o..o + 8].copy_from_slice(&new_hi.to_le_bytes());
131    }
132}
133
134impl SassInsn {
135    /// Source operand register Ra (Ampere: low word bits `[24:31]`). 255 = RZ (the zero register).
136    pub fn ra(&self) -> u8 {
137        ((self.lo >> 24) & 0xFF) as u8
138    }
139    /// Source operand register Rb (low word bits `[32:39]`) — only a real register for register-form opcodes.
140    pub fn rb(&self) -> u8 {
141        ((self.lo >> 32) & 0xFF) as u8
142    }
143    /// Destination register Rd (low word bits `[16:23]`).
144    pub fn rd(&self) -> u8 {
145        ((self.lo >> 16) & 0xFF) as u8
146    }
147}
148
149/// Execution-unit class of a SASS opcode — what a scheduler must know to place it and set control codes.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
151pub enum SassClass {
152    /// Fixed-latency integer/logic ALU (IMAD, IADD3, LEA, SHF, LOP3, ISETP, SEL, MOV, PRMT, …).
153    Alu,
154    /// Fixed-latency FP32 (FADD, FFMA, FMUL, FSETP, …) and FP16x2 (HADD2, HFMA2, …).
155    Fp,
156    /// Fixed-latency FP64 (DADD, DFMA, DMUL, …) — longer than FP32.
157    Fp64,
158    /// Multi-function unit (MUFU: rcp/sqrt/sin) — long fixed latency.
159    Mufu,
160    /// Tensor core matrix-multiply-accumulate (IMMA/HMMA) — fixed throughput (~8 cyc/int8 IMMA).
161    Tensor,
162    /// Variable-latency memory that SETS A WRITE BARRIER (LDG/LDS/LDSM/LDL/LD/ATOMG/CS2R/S2R/…).
163    Memory,
164    /// Asynchronous copy global->shared (LDGSTS = cp.async) — completion tracked by the pipeline, not a barrier.
165    AsyncCopy,
166    /// Store (STG/STS/STL/ST) — no result register.
167    Store,
168    /// Control-flow / barriers / sync (BRA/BSSY/BSYNC/BAR/WARPSYNC/EXIT/CALL/RET/YIELD/MEMBAR/…).
169    Control,
170    /// Uniform-datapath (scalar per-warp) ops (ULDC/UIADD3/UMOV/USHF/…).
171    Uniform,
172    /// Anything not yet classified.
173    Other,
174}
175
176/// Classify an sm_86 SASS base mnemonic (the token before the first `.`). Covers the full 81-opcode inventory
177/// extracted from real machine code (our GEMM + cuBLAS) — this is the ISA a SASS scheduler operates over.
178pub fn classify_opcode(mnemonic: &str) -> SassClass {
179    use SassClass::*;
180    let base = mnemonic.split('.').next().unwrap_or(mnemonic);
181    match base {
182        // tensor cores
183        "IMMA" | "HMMA" | "BMMA" => Tensor,
184        // async copy (cp.async)
185        "LDGSTS" => AsyncCopy,
186        // variable-latency loads (set a write barrier) + special-register reads
187        "LDG" | "LDS" | "LDSM" | "LDL" | "LD" | "LDGDEPBAR" | "ATOMG" | "ATOMS" | "RED" | "S2R"
188        | "S2UR" | "CS2R" | "CCTL" | "SHFL" | "VOTEU" => Memory,
189        // stores
190        "STG" | "STS" | "STL" | "ST" => Store,
191        // fp64
192        "DADD" | "DMUL" | "DFMA" | "DSETP" => Fp64,
193        // fp32 + fp16
194        "FADD" | "FMUL" | "FFMA" | "FSETP" | "FSEL" | "FMNMX" | "FCHK" | "HADD2" | "HMUL2"
195        | "HFMA2" | "HSETP2" | "F2F" | "F2I" | "I2F" | "I2FP" | "F2FP" | "FRND" | "F2IP" => Fp,
196        "MUFU" => Mufu,
197        // integer/logic ALU
198        "IMAD" | "IADD3" | "LEA" | "SHF" | "LOP3" | "PLOP3" | "ISETP" | "SEL" | "MOV" | "PRMT"
199        | "IMNMX" | "FLO" | "POPC" | "BFE" | "BFI" | "P2R" | "R2P" | "B2R" | "R2UR" | "NOP" => Alu,
200        // uniform datapath
201        "ULDC" | "UIADD3" | "UIMAD" | "USHF" | "ULEA" | "UMOV" | "UISETP" | "ULOP3" | "USEL"
202        | "UPLOP3" => Uniform,
203        // control / barriers
204        "BRA" | "BSSY" | "BSYNC" | "BAR" | "WARPSYNC" | "EXIT" | "CALL" | "RET" | "YIELD"
205        | "MEMBAR" | "ERRBAR" | "BREAK" | "BMOV" | "NANOSLEEP" | "DEPBAR" | "PBK" | "SSY" => {
206            Control
207        }
208        _ => Other,
209    }
210}
211
212impl SassClass {
213    /// Fixed issue-to-RESULT latency in cycles — the delay before a dependent consumer may read the result.
214    /// CALIBRATED by microbenchmark on this sm_86 GPU (clock64 dependent chains), NOT guessed: ALU/FP ~4.2-4.5
215    /// (round to 5), int8 IMMA = 24 (MEASURED — a guessed 8 under-stalls the cross-iteration accumulate and
216    /// hangs). Variable-latency classes (Memory/AsyncCopy) use write barriers instead of a fixed stall.
217    pub fn fixed_latency(&self) -> u8 {
218        match self {
219            SassClass::Alu | SassClass::Uniform => 5, // measured IMAD 4.48, SHF/LOP3 4.21
220            SassClass::Fp => 5,                       // measured FFMA 4.21
221            SassClass::Fp64 => 8,
222            SassClass::Mufu => 10,
223            SassClass::Tensor => 24, // MEASURED int8 m16n8k32 dependent-accumulate latency (was 8 => hung)
224            SassClass::Store | SassClass::Control => 1,
225            SassClass::Memory | SassClass::AsyncCopy | SassClass::Other => 2,
226        }
227    }
228
229    /// Functional-unit THROUGHPUT — minimum cycles between consecutive ops on the same pipe even with no
230    /// register dependency. CALIBRATED on sm_86: ALU ~1.2, FP32 ~2.7, int8 IMMA ~16 for a single warp (ptxas
231    /// uses ~8 assuming >=2 warps interleave the tensor pipe). Memory issue ~2. This is the constraint that
232    /// serializes independent IMMAs and independent FFMAs — the register-only model misses it.
233    pub fn throughput(&self) -> u8 {
234        match self {
235            SassClass::Tensor => 16, // single-warp measured; scheduler halves this per extra interleaved warp
236            SassClass::Fp => 3,      // measured FFMA throughput 2.65
237            SassClass::Fp64 => 4,
238            SassClass::Mufu => 4,
239            SassClass::Memory | SassClass::AsyncCopy | SassClass::Store => 2,
240            _ => 1, // measured IMAD throughput 1.21
241        }
242    }
243    /// Measured VARIABLE-latency result delay in cycles for barrier-tracked ops (L1/shared hit): LDG/LDSM ~45
244    /// on sm_86 (pointer-chasing microbenchmark). The scheduler doesn't stall for this (a barrier does), but it
245    /// uses it to know these results are FAR from ready — a consumer MUST wait on the scoreboard, never a stall.
246    pub fn variable_result_latency(&self) -> u32 {
247        match self {
248            SassClass::Memory | SassClass::AsyncCopy => 45,
249            _ => 0,
250        }
251    }
252
253    /// Whether this op's result is produced with VARIABLE latency and therefore must set a write barrier
254    /// (scoreboard) that dependent consumers wait on — the key fact a scheduler needs for correctness.
255    pub fn is_variable_latency(&self) -> bool {
256        matches!(self, SassClass::Memory | SassClass::AsyncCopy)
257    }
258}
259
260impl SassInsn {
261    /// The registers this instruction WRITES, given its execution `class` (from the nvdisasm mnemonic), SOUNDLY
262    /// over-approximated — never under-count, or the scheduler would drop a real hazard. Multi-register
263    /// producers (IMMA's 4-reg accumulator, wide LDSM/LDG) claim a conservative range from Rd; stores/control
264    /// write nothing.
265    pub fn writes_for(&self, class: SassClass) -> Vec<u8> {
266        let rd = self.rd();
267        if rd == 255 {
268            return Vec::new();
269        }
270        let width: u8 = match class {
271            SassClass::Tensor => 4,
272            SassClass::Memory | SassClass::AsyncCopy => 4,
273            SassClass::Store | SassClass::Control => 0,
274            _ => 1,
275        };
276        (0..width)
277            .map(|k| rd.wrapping_add(k))
278            .filter(|&r| r != 255)
279            .collect()
280    }
281
282    /// The registers this instruction READS, given its `class`, soundly over-approximated. ALU/FP read Ra,Rb;
283    /// tensor ops also read accumulator (Rd..) + A-frag (Ra..) + B-frag (Rb..); stores read data Rd + addr Ra.
284    pub fn reads_for(&self, class: SassClass) -> Vec<u8> {
285        let (ra, rb, rd) = (self.ra(), self.rb(), self.rd());
286        let mut r = Vec::new();
287        let mut push = |x: u8| {
288            if x != 255 && !r.contains(&x) {
289                r.push(x);
290            }
291        };
292        match class {
293            SassClass::Tensor => {
294                for k in 0..4 {
295                    push(rd.wrapping_add(k));
296                    push(ra.wrapping_add(k));
297                }
298                push(rb);
299                push(rb.wrapping_add(1));
300            }
301            SassClass::Store => {
302                push(rd);
303                push(ra);
304                push(ra.wrapping_add(1));
305            }
306            SassClass::Memory | SassClass::AsyncCopy => {
307                push(ra);
308                push(ra.wrapping_add(1));
309            }
310            SassClass::Control => {}
311            _ => {
312                push(ra);
313                push(rb);
314            }
315        }
316        r
317    }
318}
319
320/// PRECISE operand decode from the nvdisasm TEXT of one instruction (everything after the mnemonic, before the
321/// `;`). nvdisasm already prints the exact registers, so we parse them rather than reverse-engineer the binary
322/// operand fields. Returns (writes, reads) as register numbers (RZ/255 dropped). Rules, verified against real
323/// SASS: operand 0 is the WRITE destination UNLESS it is a memory ref `[...]` (a store — then it's a read
324/// address and nothing is written); every other operand is a READ; registers inside `[...]` are address reads;
325/// a `.64`/`.128` width on an address adds the consecutive address registers; accumulate ops (IMMA/FFMA)
326/// naturally read their destination because it reappears as a later operand. Groups `{R8,R9,...}` expand.
327pub fn precise_operands(operand_text: &str) -> (Vec<u8>, Vec<u8>) {
328    // split the operand list on commas at brace/bracket depth 0.
329    let mut parts: Vec<String> = Vec::new();
330    let (mut depth, mut cur) = (0i32, String::new());
331    for ch in operand_text.chars() {
332        match ch {
333            '{' | '[' => {
334                depth += 1;
335                cur.push(ch);
336            }
337            '}' | ']' => {
338                depth -= 1;
339                cur.push(ch);
340            }
341            ',' if depth == 0 => {
342                parts.push(cur.trim().to_string());
343                cur.clear();
344            }
345            _ => cur.push(ch),
346        }
347    }
348    if !cur.trim().is_empty() {
349        parts.push(cur.trim().to_string());
350    }
351
352    // extract register numbers from a token (handles R12, R12.reuse, {R8, R9}, [R16.64+0x10], etc.).
353    fn regs_in(tok: &str) -> Vec<u8> {
354        let bytes = tok.as_bytes();
355        let mut out = Vec::new();
356        let mut i = 0;
357        while i < bytes.len() {
358            // a register is 'R' followed by digits, not preceded by a letter (avoid 'UR', 'PR', 'SR').
359            if bytes[i] == b'R'
360                && (i == 0 || !bytes[i - 1].is_ascii_alphabetic())
361                && i + 1 < bytes.len()
362                && bytes[i + 1].is_ascii_digit()
363            {
364                let mut j = i + 1;
365                let mut num = 0u32;
366                while j < bytes.len() && bytes[j].is_ascii_digit() {
367                    num = num * 10 + (bytes[j] - b'0') as u32;
368                    j += 1;
369                }
370                if num < 255 {
371                    out.push(num as u8);
372                }
373                i = j;
374            } else {
375                i += 1;
376            }
377        }
378        out
379    }
380    // widen an address register by .64/.128 (adds the consecutive pair/quad).
381    fn addr_regs(tok: &str) -> Vec<u8> {
382        let mut base = regs_in(tok);
383        let extra = if tok.contains(".128") {
384            3
385        } else if tok.contains(".64") {
386            1
387        } else {
388            0
389        };
390        if let Some(&b0) = base.first() {
391            for k in 1..=extra {
392                let r = b0.wrapping_add(k as u8);
393                if r != 255 {
394                    base.push(r);
395                }
396            }
397        }
398        base
399    }
400
401    let mut writes = Vec::new();
402    let mut reads = Vec::new();
403    for (idx, p) in parts.iter().enumerate() {
404        let is_mem = p.starts_with('[') || (p.starts_with("desc") && p.contains('['));
405        if idx == 0 && !is_mem {
406            // destination register(s).
407            for r in regs_in(p) {
408                if !writes.contains(&r) {
409                    writes.push(r);
410                }
411            }
412        } else if is_mem {
413            for r in addr_regs(p) {
414                if !reads.contains(&r) {
415                    reads.push(r);
416                }
417            }
418        } else {
419            for r in regs_in(p) {
420                if !reads.contains(&r) {
421                    reads.push(r);
422                }
423            }
424        }
425    }
426    (writes, reads)
427}
428
429/// Build a PRECISE dependency DAG from a cubin by parsing nvdisasm's exact operands per instruction (not the
430/// conservative range approximation). This gives the scheduler maximal legal freedom — the graph has only the
431/// TRUE hazards, so a reschedule can hide latency ptxas left exposed. Returns (insns, classes, mnemonics, DAG).
432pub fn precise_dependency_graph(
433    cubin_path: &Path,
434) -> Option<(Vec<SassInsn>, Vec<SassClass>, DepGraph)> {
435    let bytes = std::fs::read(cubin_path).ok()?;
436    let insns = instructions(&bytes);
437    if insns.is_empty() {
438        return None;
439    }
440    let dis = nvdisasm(cubin_path)?;
441    // per-instruction (mnemonic, operand-text) from the disassembly lines, in order.
442    let mut ops: Vec<(String, String)> = Vec::new();
443    for l in dis.lines() {
444        let s = l.trim();
445        if s.starts_with("/*") {
446            if let Some(rest) = s.split("*/").nth(1) {
447                let body = rest.split(';').next().unwrap_or("").trim();
448                // strip a leading predicate '@P0' / '@!P0'
449                let body = body
450                    .strip_prefix('@')
451                    .map(|b| b.split_whitespace().skip(1).collect::<Vec<_>>().join(" "))
452                    .unwrap_or_else(|| body.to_string());
453                let mut it = body.splitn(2, char::is_whitespace);
454                let mnem = it.next().unwrap_or("").to_string();
455                let operands = it.next().unwrap_or("").to_string();
456                if !mnem.is_empty() {
457                    ops.push((mnem, operands));
458                }
459            }
460        }
461    }
462    let n = insns.len().min(ops.len());
463    let classes: Vec<SassClass> = (0..n).map(|i| classify_opcode(&ops[i].0)).collect();
464    let mut reads: Vec<Vec<u8>> = Vec::with_capacity(n);
465    let mut writes: Vec<Vec<u8>> = Vec::with_capacity(n);
466    for i in 0..n {
467        let (w, r) = precise_operands(&ops[i].1);
468        writes.push(w);
469        reads.push(r);
470    }
471    let is_barrier: Vec<bool> = classes.iter().map(|c| *c == SassClass::Control).collect();
472    let g = build_dependency_graph(&reads, &writes, &is_barrier);
473    Some((insns[..n].to_vec(), classes, g))
474}
475
476/// Build the dependency DAG directly from a cubin, using nvdisasm to name each opcode (so classification —
477/// hence the read/write sets — is precise). Returns the graph paired with the parsed instructions. This is the
478/// mnemonic-aware, SOUND path the scheduler uses.
479pub fn dependency_graph_from_cubin(
480    cubin_path: &Path,
481) -> Option<(Vec<SassInsn>, Vec<SassClass>, DepGraph)> {
482    let bytes = std::fs::read(cubin_path).ok()?;
483    let insns = instructions(&bytes);
484    if insns.is_empty() {
485        return None;
486    }
487    let dis = nvdisasm(cubin_path)?;
488    let mnems: Vec<String> = dis
489        .lines()
490        .filter_map(|l| {
491            let s = l.trim();
492            if s.starts_with("/*") {
493                s.split("*/")
494                    .nth(1)
495                    .and_then(|r| r.trim().split_whitespace().next())
496                    .map(|m| m.to_string())
497            } else {
498                None
499            }
500        })
501        .collect();
502    let classes: Vec<SassClass> = (0..insns.len())
503        .map(|i| classify_opcode(mnems.get(i).map(|s| s.as_str()).unwrap_or("IMAD")))
504        .collect();
505    let reads: Vec<Vec<u8>> = insns
506        .iter()
507        .zip(&classes)
508        .map(|(ins, &c)| ins.reads_for(c))
509        .collect();
510    let writes: Vec<Vec<u8>> = insns
511        .iter()
512        .zip(&classes)
513        .map(|(ins, &c)| ins.writes_for(c))
514        .collect();
515    let is_barrier: Vec<bool> = classes.iter().map(|c| *c == SassClass::Control).collect();
516    let g = build_dependency_graph(&reads, &writes, &is_barrier);
517    Some((insns, classes, g))
518}
519
520/// A register-dependency DAG over a straight-line SASS region: `deps[i]` lists earlier instructions that `i`
521/// must stay after (RAW/WAR/WAW hazards + barrier ordering). Any reordering that respects these edges preserves
522/// semantics — this is what the scheduler is free to permute to hide latency.
523#[derive(Debug, Clone)]
524pub struct DepGraph {
525    pub deps: Vec<Vec<usize>>,
526}
527
528/// Build the dependency DAG from per-instruction (reads, writes, is_barrier) triples. Barriers (BAR/BSYNC/
529/// control) force a total order across themselves: nothing moves past a barrier in either direction.
530pub fn build_dependency_graph(
531    reads: &[Vec<u8>],
532    writes: &[Vec<u8>],
533    is_barrier: &[bool],
534) -> DepGraph {
535    let n = reads.len();
536    let mut deps: Vec<Vec<usize>> = vec![Vec::new(); n];
537    let mut last_writer: [Option<usize>; 256] = [None; 256];
538    let mut readers: Vec<Vec<usize>> = vec![Vec::new(); 256];
539    let mut last_barrier: Option<usize> = None;
540    let add = |deps: &mut Vec<Vec<usize>>, i: usize, j: usize| {
541        if i != j && !deps[i].contains(&j) {
542            deps[i].push(j);
543        }
544    };
545    for i in 0..n {
546        // barrier ordering: this instruction stays after the previous barrier.
547        if let Some(b) = last_barrier {
548            add(&mut deps, i, b);
549        }
550        // RAW: read-after-write on each source register.
551        for &r in &reads[i] {
552            if let Some(w) = last_writer[r as usize] {
553                add(&mut deps, i, w);
554            }
555        }
556        // WAW + WAR on each destination register.
557        for &w in &writes[i] {
558            if let Some(pw) = last_writer[w as usize] {
559                add(&mut deps, i, pw); // WAW
560            }
561            let rs = std::mem::take(&mut readers[w as usize]);
562            for reader in rs {
563                add(&mut deps, i, reader); // WAR
564            }
565        }
566        // update state.
567        for &r in &reads[i] {
568            readers[r as usize].push(i);
569        }
570        for &w in &writes[i] {
571            last_writer[w as usize] = Some(i);
572            readers[w as usize].clear();
573        }
574        if is_barrier[i] {
575            // a barrier depends on everything before it, and becomes the new fence.
576            for j in 0..i {
577                add(&mut deps, i, j);
578            }
579            last_barrier = Some(i);
580        }
581    }
582    DepGraph { deps }
583}
584
585/// CONTROL-CODE ASSIGNER (correctness-first, stalls). Recompute every instruction's STALL count from our own
586/// latency model — `stall = fixed_latency(class)` — which is provably >= the cycles any fixed-latency consumer
587/// needs (the immediate next instruction already issues `stall` cycles later, so later consumers have even
588/// more). We KEEP ptxas's write/read barriers + wait masks (the variable-latency memory / cp.async tracking,
589/// the fragile part) untouched. If the kernel stays numerically correct with OUR stalls, the stall model is
590/// validated on-device — the first half of a working control-code assigner. Returns the patched cubin bytes.
591pub fn assign_stalls_by_latency(cubin_path: &Path) -> Option<Vec<u8>> {
592    let bytes = std::fs::read(cubin_path).ok()?;
593    let insns = instructions(&bytes);
594    if insns.is_empty() {
595        return None;
596    }
597    let dis = nvdisasm(cubin_path)?;
598    let mnems: Vec<String> = dis
599        .lines()
600        .filter_map(|l| {
601            let s = l.trim();
602            if s.starts_with("/*") {
603                s.split("*/")
604                    .nth(1)
605                    .and_then(|r| r.trim().split_whitespace().next())
606                    .map(|m| m.to_string())
607            } else {
608                None
609            }
610        })
611        .collect();
612    let mut out = bytes.clone();
613    for (i, ins) in insns.iter().enumerate() {
614        let class = classify_opcode(mnems.get(i).map(|s| s.as_str()).unwrap_or("IMAD"));
615        let mut ctl = Control::decode(ins.hi); // preserve barriers/waits/reuse/yield
616                                               // our own stall from the latency model, but never LOWER than what ptxas chose (ptxas may know a
617                                               // throughput/dual-issue constraint we don't model — staying >= its stall keeps us sound).
618        let ours = class.fixed_latency().min(15);
619        ctl.stall = ctl.stall.max(ours);
620        patch_hi(&mut out, i, ctl.apply(ins.hi));
621    }
622    Some(out)
623}
624
625/// CONTROL-CODE ASSIGNER (perf): compute the MINIMAL stall per instruction from the fixed-latency dependency
626/// timing, keeping ptxas's memory barriers. We walk the instruction order tracking the cycle each register's
627/// fixed-latency result becomes ready; each instruction issues at the max of (prev issue + 1) and its
628/// producers' ready cycles; the stall between two instructions is exactly that gap (clamped to `[1,15]`). This is
629/// the tightest schedule that still satisfies every fixed-latency hazard — if ptxas over-stalled anywhere, ours
630/// is faster; if it was already minimal, ours matches. Correctness is guarded on-device (a too-tight stall that
631/// violates a hazard we don't model shows up as a wrong result). Returns the patched cubin bytes.
632pub fn assign_minimal_stalls(cubin_path: &Path) -> Option<Vec<u8>> {
633    assign_minimal_stalls_opt(cubin_path, true)
634}
635
636/// As above, but `floor_ptxas` selects whether to floor our computed stall at ptxas's (sound) or trust our
637/// CALIBRATED model outright (`false` — the true test of whether the measured latencies are complete).
638pub fn assign_minimal_stalls_opt(cubin_path: &Path, floor_ptxas: bool) -> Option<Vec<u8>> {
639    let (insns, classes, _g) = precise_dependency_graph(cubin_path)?;
640    // precise reads/writes again (the graph consumed them); recompute from the disassembly text.
641    let dis = nvdisasm(cubin_path)?;
642    let mut ops: Vec<String> = Vec::new();
643    for l in dis.lines() {
644        let s = l.trim();
645        if s.starts_with("/*") {
646            if let Some(rest) = s.split("*/").nth(1) {
647                let body = rest.split(';').next().unwrap_or("").trim();
648                let body = body
649                    .strip_prefix('@')
650                    .map(|b| b.split_whitespace().skip(1).collect::<Vec<_>>().join(" "))
651                    .unwrap_or_else(|| body.to_string());
652                let operands = body
653                    .splitn(2, char::is_whitespace)
654                    .nth(1)
655                    .unwrap_or("")
656                    .to_string();
657                ops.push(operands);
658            }
659        }
660    }
661    let n = insns.len().min(ops.len());
662    let lat: Vec<u32> = classes.iter().map(|c| c.fixed_latency() as u32).collect();
663
664    let mut ready = [0u32; 256]; // cycle each reg's FIXED-latency result is available
665    let mut issue = vec![0u32; n];
666    // functional-unit throughput: consecutive ops on the same unit cannot issue faster than the unit's rate,
667    // EVEN with no register dependency (e.g. IMMAs writing different accumulators still serialize on the tensor
668    // pipe). This is the constraint the register-only model missed (it caused a tensor-pipe overrun -> hang).
669    let mut last_unit_issue: std::collections::HashMap<SassClass, u32> =
670        std::collections::HashMap::new();
671    // multi-warp effective tensor issue rate (8) — a well-occupied kernel interleaves >=2 warps over the
672    // 16-cycle tensor pipe. Other units from the calibrated throughput() table.
673    let throughput = |c: SassClass| -> u32 {
674        match c {
675            SassClass::Tensor => 8,
676            other => other.throughput() as u32,
677        }
678    };
679    for p in 0..n {
680        let (w, r) = precise_operands(&ops[p]);
681        let mut earliest = if p > 0 { issue[p - 1] + 1 } else { 0 };
682        for &reg in &r {
683            earliest = earliest.max(ready[reg as usize]);
684        }
685        // throughput on this instruction's functional unit
686        if let Some(&last) = last_unit_issue.get(&classes[p]) {
687            earliest = earliest.max(last + throughput(classes[p]));
688        }
689        issue[p] = earliest;
690        last_unit_issue.insert(classes[p], issue[p]);
691        if !classes[p].is_variable_latency() {
692            for &reg in &w {
693                ready[reg as usize] = issue[p] + lat[p];
694            }
695        }
696        // variable-latency writes use ptxas's write barrier (kept) — consumers wait on it, not on `ready`.
697    }
698    // stall[p] = gap to the next instruction's issue (>=1, <=15). Keep barriers/waits/reuse from ptxas.
699    let bytes = std::fs::read(cubin_path).ok()?;
700    let mut out = bytes.clone();
701    for p in 0..n {
702        let mut ctl = Control::decode(insns[p].hi);
703        let gap = if p + 1 < n {
704            issue[p + 1].saturating_sub(issue[p])
705        } else {
706            lat[p]
707        };
708        // SOUND floor: never go below ptxas's stall. Our latency+throughput model captures the hazards we know;
709        // ptxas encodes additional ones we haven't fully reverse-engineered (barrier-wait timing, dual-issue,
710        // register-bank conflicts). Flooring at its value keeps the kernel correct while applying our (higher)
711        // stalls where our model demands them. A truly-minimal below-ptxas schedule needs the COMPLETE hazard
712        // model — that is the remaining reverse-engineering to actually beat ptxas.
713        ctl.stall = if floor_ptxas {
714            (gap.clamp(1, 15) as u8).max(ctl.stall)
715        } else {
716            gap.clamp(1, 15) as u8
717        };
718        patch_hi(&mut out, p, ctl.apply(insns[p].hi));
719    }
720    Some(out)
721}
722
723/// LIST SCHEDULER — the scheduler engine. Given the sound dependency DAG and per-instruction latencies, produce
724/// a new instruction order that respects every hazard while placing latency-critical work early so its result
725/// is ready by the time a consumer needs it (classic critical-path list scheduling). Returns the new order as
726/// original-instruction indices. This is what a re-emitter writes back to close the gap ptxas's schedule leaves.
727pub fn list_schedule(g: &DepGraph, latencies: &[u8]) -> Vec<usize> {
728    let n = g.deps.len();
729    // successors + in-degree
730    let mut succ: Vec<Vec<usize>> = vec![Vec::new(); n];
731    let mut indeg = vec![0usize; n];
732    for (i, ds) in g.deps.iter().enumerate() {
733        indeg[i] = ds.len();
734        for &j in ds {
735            succ[j].push(i);
736        }
737    }
738    // critical-path length (longest latency-weighted path to a sink), computed in reverse topo order.
739    let mut cpl = vec![0u32; n];
740    // process nodes in reverse index order — since edges point backward (j<i), successors have larger indices;
741    // a reverse pass gives each node its successors' cpl already computed.
742    for i in (0..n).rev() {
743        let mut best = 0u32;
744        for &s in &succ[i] {
745            best = best.max(cpl[s]);
746        }
747        cpl[i] = best + latencies.get(i).copied().unwrap_or(1) as u32;
748    }
749    // Kahn with a max-cpl tie-break (and stable original order as the final tie-break for determinism).
750    let mut ready: Vec<usize> = (0..n).filter(|&i| indeg[i] == 0).collect();
751    let mut order = Vec::with_capacity(n);
752    while !ready.is_empty() {
753        // pick the ready node with the highest critical-path length; tie-break to the earliest original index
754        // (keeps the schedule close to ptxas's where the DAG is indifferent, which is where it is already good).
755        let (mut bi, mut bpos) = (0usize, 0usize);
756        for (pos, &node) in ready.iter().enumerate() {
757            let better = cpl[node] > cpl[bi] || (cpl[node] == cpl[bi] && node < bi);
758            if pos == 0 || better {
759                bi = node;
760                bpos = pos;
761            }
762        }
763        ready.swap_remove(bpos);
764        order.push(bi);
765        for &s in &succ[bi] {
766            indeg[s] -= 1;
767            if indeg[s] == 0 {
768                ready.push(s);
769            }
770        }
771    }
772    order
773}
774
775/// The reuse-cache optimizer pass — our first SASS optimization ON TOP of ptxas. The operand collector can
776/// REUSE a source register read by the previous instruction instead of re-reading the register file, saving
777/// RF bandwidth (the bottleneck under our 222-register, 16.7%-occupancy kernel). ptxas already sets many reuse
778/// flags; this finds the ones it MISSED: consecutive instructions that read the same register into the same
779/// operand slot (Ra→reuse bit 0 = `hi[58]`, Rb→reuse bit 1 = `hi[59]`) but where the flag is unset. RZ is skipped
780/// (reusing the zero register is pointless). Returns the patched cubin bytes and the number of flags added.
781/// The result MUST be validated on-device (varied-fill oracle) — the operand→slot mapping is verified there,
782/// never trusted blind.
783pub fn optimize_reuse(cubin: &[u8]) -> (Vec<u8>, usize) {
784    let insns = instructions(cubin);
785    let mut out = cubin.to_vec();
786    let mut added = 0usize;
787    for i in 1..insns.len() {
788        let (prev, cur) = (&insns[i - 1], &insns[i]);
789        let mut ctl = Control::decode(cur.hi);
790        let mut changed = false;
791        // Ra reuse (slot 0): current Ra read the same non-RZ register the previous instruction read as Ra.
792        if cur.ra() != 255 && cur.ra() == prev.ra() && (ctl.reuse & 0b0001) == 0 {
793            ctl.reuse |= 0b0001;
794            changed = true;
795        }
796        // Rb reuse (slot 1): same for the Rb operand slot.
797        if cur.rb() != 255 && cur.rb() == prev.rb() && (ctl.reuse & 0b0010) == 0 {
798            ctl.reuse |= 0b0010;
799            changed = true;
800        }
801        if changed {
802            patch_hi(&mut out, i, ctl.apply(cur.hi));
803            added += 1;
804        }
805    }
806    (out, added)
807}
808
809/// Disassemble a cubin file to SASS text via nvdisasm (for inspection / labelling).
810pub fn nvdisasm(cubin_path: &Path) -> Option<String> {
811    let o = Command::new("nvdisasm")
812        .args(["-c", "-hex"])
813        .arg(cubin_path)
814        .output()
815        .ok()?;
816    if !o.status.success() {
817        return None;
818    }
819    Some(String::from_utf8_lossy(&o.stdout).into_owned())
820}
821
822/// What the COMPILER learns about the machine code `ptxas` produced for one of its kernels — the SASS-awareness
823/// the backend needs to reason about the ptxas ceiling (register pressure/occupancy, tensor-core utilisation,
824/// reuse-cache usage). Every field is read from the real cubin, not guessed.
825#[derive(Debug, Clone, PartialEq, Eq)]
826pub struct SassReport {
827    pub kernel: String,
828    pub instructions: usize,
829    pub imma: usize,         // tensor-core MACs
830    pub ldsm: usize,         // ldmatrix cooperative loads
831    pub reuse_insns: usize,  // instructions that use the operand reuse cache
832    pub registers: usize,    // registers/thread (SHI_REGISTERS) — the occupancy driver
833    pub avg_stall_x100: u64, // mean fixed stall ×100 (integer to stay Eq/Hash-friendly)
834}
835
836impl SassReport {
837    /// Estimate resident blocks/SM from the register count (Ampere: 65,536 regs/SM) at `threads` per block —
838    /// this is where a register-bound kernel loses to cuBLAS, and the compiler can now SEE it.
839    pub fn blocks_per_sm(&self, threads: usize) -> usize {
840        if self.registers == 0 || threads == 0 {
841            return 0;
842        }
843        (65536 / (self.registers * threads)).min(16)
844    }
845
846    /// Analyse the cubin a compiler backend just produced: parse its machine code + control codes and correlate
847    /// with the SASS mnemonics. The backend calls this to know how well ptxas scheduled its kernel.
848    pub fn analyze(cubin_path: &Path, kernel: &str) -> Option<SassReport> {
849        let bytes = std::fs::read(cubin_path).ok()?;
850        let insns = instructions(&bytes);
851        if insns.is_empty() {
852            return None;
853        }
854        let dis = nvdisasm(cubin_path)?;
855        // mnemonic per instruction (the token after the `/*addr*/` marker).
856        let mnems: Vec<String> = dis
857            .lines()
858            .filter_map(|l| {
859                let s = l.trim();
860                if s.starts_with("/*") {
861                    s.split("*/")
862                        .nth(1)
863                        .and_then(|r| r.trim().split_whitespace().next())
864                        .map(|m| m.to_string())
865                } else {
866                    None
867                }
868            })
869            .collect();
870        let count = |needle: &str| mnems.iter().filter(|m| m.contains(needle)).count();
871        let reuse_insns = insns
872            .iter()
873            .filter(|i| Control::decode(i.hi).reuse != 0)
874            .count();
875        let stall_sum: u64 = insns
876            .iter()
877            .map(|i| Control::decode(i.hi).stall as u64)
878            .sum();
879        // registers/thread from nvdisasm's `.sectioninfo @"SHI_REGISTERS=N"`.
880        let registers = dis
881            .lines()
882            .find_map(|l| l.split("SHI_REGISTERS=").nth(1))
883            .and_then(|r| {
884                r.trim_end_matches(|c: char| !c.is_ascii_digit())
885                    .parse::<usize>()
886                    .ok()
887            })
888            .unwrap_or(0);
889        Some(SassReport {
890            kernel: kernel.to_string(),
891            instructions: insns.len(),
892            imma: count("IMMA"),
893            ldsm: count("LDSM"),
894            reuse_insns,
895            registers,
896            avg_stall_x100: stall_sum * 100 / insns.len() as u64,
897        })
898    }
899}
900
901/// The compiler emits `cu_src`, compiles it with nvcc to a cubin, and returns the SASS report — the backend
902/// "knowing" the machine code its codegen ultimately becomes. Requires nvcc + nvdisasm; returns None otherwise.
903pub fn analyze_source(cu_src: &str, kernel: &str, arch: &str) -> Option<SassReport> {
904    let dir = std::env::temp_dir().join(format!("logos_sassrep_{}_{}", std::process::id(), kernel));
905    std::fs::create_dir_all(&dir).ok()?;
906    let src = dir.join("k.cu");
907    let cubin = dir.join("k.cubin");
908    std::fs::write(&src, cu_src).ok()?;
909    let o = Command::new("nvcc")
910        .args(["-O3", &format!("-arch={arch}"), "-cubin", "-o"])
911        .arg(&cubin)
912        .arg(&src)
913        .output()
914        .ok()?;
915    if !o.status.success() {
916        return None;
917    }
918    let rep = SassReport::analyze(&cubin, kernel);
919    let _ = std::fs::remove_dir_all(&dir);
920    rep
921}
922
923#[cfg(test)]
924#[path = "../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sass/tests.rs"]
925mod tests;