Skip to main content

logicaffeine_compile/codegen_cuda/
matrix.rs

1//! The **INT8 weight-stationary GEMM** path of the CUDA tier — the workload a GPU (and an
2//! RDU) actually exists for: `O(M×N) = A(M×K) · W(K×N)` in int8, the shape every transformer
3//! projection and attention dot reduces to.
4//!
5//! Unlike the scalar tier (`super::wrap`) this is NOT a lift of the C body — it is a
6//! purpose-built **DP4A** kernel (Ampere's 4×int8→int32 dot-product intrinsic), so there is no
7//! device-libc question. It is gated on the SAME certified capability the fabric lowering uses
8//! ([`FabricTarget::require_gemm`]) and — crucially — its numerics are anchored to the certified
9//! [`crate::codegen_sva::systolic::eval_systolic`] reference (itself ≡ VM ≡ SAT-placed PE ≡
10//! iverilog, `weight_stationary_gemm_lock`). Running the emitted kernel and comparing to
11//! `eval_systolic` adds a CUDA rung to that existing proof chain (`cuda_matrix_lock`).
12//!
13//! Accumulation is int32 (DP4A's native width); `eval_systolic` wraps in i64. For the int8
14//! operands and the small `K` the lock exercises, no int32 overflow occurs, so the two agree
15//! exactly — the lock keeps `K` in that safe range on purpose.
16
17use crate::codegen_sva::fabric_target::{Dataflow, FabricTarget, Precision};
18use crate::tensor_recognize::{
19    ElementwiseSpec, FusedRowSpec, MatmulSpec, ReductionKind, ReductionSpec, RowReduceKind,
20};
21
22use super::knobs::{CudaKnobs, ResolvedPrecision};
23use super::runtime::CUDA_MAKEFILE_TEMPLATE;
24use super::CudaLowerError;
25
26/// Lower a RECOGNIZED LOGOS matmul (`C(M×N) = A(M×K)·B(K×N)`, from [`crate::tensor_recognize::recognize_matmul`])
27/// to a self-contained CUDA program whose dims ARE the source program's — the compiler *generating* the
28/// kernel, not a template. The emitted program reproduces the LOGOS matmul's deterministic int8-range fill
29/// (`a[p]=(p%7)-3`, `b[p]=(p%5)-2`), computes `C=A·B` on the GPU, and prints the checksum `Σ C` — so its
30/// stdout equals the VM's `Show checksum` byte-for-byte. `M,N,K` are baked from the spec.
31///
32/// Uses the **best-general** knobs ([`CudaKnobs::default`]). Call [`lower_matmul_to_cuda_with`] to pin a
33/// point in the [knob lattice](super::knobs) — e.g. the `Int8Dp4a` precision fast path.
34pub fn lower_matmul_to_cuda(spec: &MatmulSpec) -> String {
35    lower_matmul_to_cuda_with(spec, &CudaKnobs::default())
36}
37
38/// Lower a recognized matmul under an explicit [`CudaKnobs`] point. The `precision` knob selects the kernel:
39/// [`ResolvedPrecision::Int64`] (always-correct i64 accumulation, the general default) or
40/// [`ResolvedPrecision::Int8Dp4a`] (`__dp4a` int8 fast path — valid only when operands fit int8, which is why
41/// it is opt-in). Both reproduce the LOGOS program's fill + checksum, so both equal the VM byte-for-byte on
42/// any int8-safe program; they differ only in *how* the contraction is computed on the GPU.
43pub fn lower_matmul_to_cuda_with(spec: &MatmulSpec, knobs: &CudaKnobs) -> String {
44    let int_template = |t: &str| {
45        t.replace("{M}", &spec.m.to_string())
46            .replace("{N}", &spec.n.to_string())
47            .replace("{K}", &spec.k.to_string())
48    };
49    match knobs.precision.resolved() {
50        ResolvedPrecision::Int8Dp4a => int_template(CUDA_MATMUL_DP4A_TEMPLATE),
51        ResolvedPrecision::Int64 => int_template(CUDA_MATMUL_TEMPLATE),
52        // Half-precision: `select_mma` gives the tensor-core kind IF the shape is 16-aligned and the chip is
53        // capable; otherwise it degrades to DP4A, in which case we fall back to the always-correct int64 kernel
54        // (still ≡ VM — just not on the tensor cores). Never emit a WMMA kernel a shape/GPU cannot run.
55        ResolvedPrecision::Bf16Hmma | ResolvedPrecision::Fp16Hmma => {
56            use super::gemm_tile::{select_hmma_kernel, select_mma, HmmaKernel, MmaKind};
57            let kind = select_mma(
58                knobs.fabric_precision(),
59                spec.m,
60                spec.k,
61                spec.n,
62                knobs.chip.sm_arch,
63            );
64            if !kind.is_tensor_core() || matches!(kind, MmaKind::WmmaI8) {
65                // Not a legal HMMA shape/arch (ragged / too old) → the always-correct int64 kernel.
66                int_template(CUDA_MATMUL_TEMPLATE)
67            } else {
68                // Legal HMMA: pick the FASTEST kernel — cp.async double-buffered (the 1.43× lever) when the tile
69                // divides the shape and fits static shared memory, else the basic one-warp kernel.
70                match select_hmma_kernel(
71                    kind,
72                    spec.m,
73                    spec.k,
74                    spec.n,
75                    knobs.chip.sm_arch,
76                    &super::gemm_tile::GpuLimits::ampere(),
77                ) {
78                    HmmaKernel::CpAsync { tile, stages } => {
79                        super::gemm_tile::emit_wmma_cpasync_program(
80                            kind, &tile, stages, spec.m, spec.k, spec.n,
81                        )
82                    }
83                    HmmaKernel::Basic => emit_wmma_matmul(kind, spec.m, spec.k, spec.n),
84                }
85            }
86        }
87    }
88}
89
90/// A checksum-matching **WMMA GEMM** program for a half-precision [`MmaKind`] — reuses the tensor-core kernel
91/// from [`super::gemm_tile::emit_wmma`] with the matmul emitter's fill (`a[p]=(p%7)-3`, `b[p]=(p%5)-2`) and the
92/// `Σ C` integer checksum, so on int8-range operands (exact in bf16/f16, wide f32 accumulate) its output equals
93/// the VM byte-for-byte — the same ≡-oracle contract as the int64 / DP4A templates.
94fn emit_wmma_matmul(kind: super::gemm_tile::MmaKind, m: usize, k: usize, n: usize) -> String {
95    let kernel = super::gemm_tile::emit_wmma(kind);
96    let op = kind.operand_ctype();
97    let acc = kind.acc_ctype();
98    let tag = kind.tag();
99    let conv_a = kind.host_convert("(p % 7) - 3");
100    let conv_b = kind.host_convert("(p % 5) - 2");
101    format!(
102        r#"{kernel}#include <cstdio>
103#include <cstdlib>
104#include <cmath>
105#define M {m}
106#define N {n}
107#define K {k}
108int main(void){{
109    const int MM=M, KK=K, NN=N;
110    {op}* hA=({op}*)malloc((size_t)MM*KK*sizeof({op}));
111    {op}* hB=({op}*)malloc((size_t)KK*NN*sizeof({op}));
112    for(long p=0;p<(long)MM*KK;p++) hA[p]={conv_a};
113    for(long p=0;p<(long)KK*NN;p++) hB[p]={conv_b};
114    {op} *dA,*dB; {acc}* dC;
115    if(cudaMalloc((void**)&dA,(size_t)MM*KK*sizeof({op}))||cudaMalloc((void**)&dB,(size_t)KK*NN*sizeof({op}))||cudaMalloc((void**)&dC,(size_t)MM*NN*sizeof({acc}))){{ fprintf(stderr,"malloc\n"); return 1; }}
116    cudaMemcpy(dA,hA,(size_t)MM*KK*sizeof({op}),cudaMemcpyHostToDevice);
117    cudaMemcpy(dB,hB,(size_t)KK*NN*sizeof({op}),cudaMemcpyHostToDevice);
118    dim3 grid(NN/16, MM/16);
119    gemm_wmma_{tag}<<<grid,32>>>(dA,dB,dC,MM,NN,KK);
120    if(cudaDeviceSynchronize()!=cudaSuccess){{ fprintf(stderr,"sync\n"); return 1; }}
121    {acc}* hC=({acc}*)malloc((size_t)MM*NN*sizeof({acc}));
122    cudaMemcpy(hC,dC,(size_t)MM*NN*sizeof({acc}),cudaMemcpyDeviceToHost);
123    double sum=0; for(long t=0;t<(long)MM*NN;t++) sum+=(double)hC[t];
124    printf("%lld\n",(long long)llround(sum));
125    return 0;
126}}
127"#,
128        kernel = kernel,
129        op = op,
130        acc = acc,
131        tag = tag,
132        m = m,
133        k = k,
134        n = n,
135        conv_a = conv_a,
136        conv_b = conv_b,
137    )
138}
139
140/// The emitted GEMM program. One thread per output element accumulates over the contraction in i64 (exactly
141/// the VM's semantics). `{M}/{N}/{K}` are the recognized dims. This is the always-correct general default
142/// (`MatmulPrecision::Auto`/`Int64`); the `Int8Dp4a` knob selects [`CUDA_MATMUL_DP4A_TEMPLATE`] instead.
143const CUDA_MATMUL_TEMPLATE: &str = r#"// Auto-generated by LOGOS codegen_cuda::matrix — a recognized LOGOS matmul lowered to CUDA.
144// C(M x N) = A(M x K) . B(K x N), row-major; dims are the source program's; checksum == the VM's Show.
145#include <cstdio>
146#include <cstdint>
147#define M {M}
148#define N {N}
149#define K {K}
150// Reproduce the LOGOS program's deterministic int8-range fill (0-based push order).
151__global__ void fill_ab(int64_t* a, int64_t* b){
152    long p = blockIdx.x*(long)blockDim.x + threadIdx.x;
153    if(p < (long)M*K) a[p] = (p % 7) - 3;
154    if(p < (long)K*N) b[p] = (p % 5) - 2;
155}
156__global__ void gemm(const int64_t* a, const int64_t* b, int64_t* c){
157    long j = blockIdx.x*(long)blockDim.x + threadIdx.x;
158    long i = blockIdx.y*(long)blockDim.y + threadIdx.y;
159    if(i >= M || j >= N) return;
160    int64_t acc = 0;
161    for(int k = 0; k < K; ++k) acc += a[i*(long)K + k] * b[(long)k*N + j];
162    c[i*(long)N + j] = acc;
163}
164int main(void){
165    int64_t *a,*b,*c;
166    if(cudaMalloc(&a,(size_t)M*K*8)||cudaMalloc(&b,(size_t)K*N*8)||cudaMalloc(&c,(size_t)M*N*8)){ fprintf(stderr,"malloc\n"); return 1; }
167    long filln = ((long)M*K > (long)K*N) ? (long)M*K : (long)K*N;
168    fill_ab<<<(filln+255)/256,256>>>(a,b);
169    dim3 blk(16,16), grd((N+15)/16,(M+15)/16);
170    gemm<<<grd,blk>>>(a,b,c);
171    if(cudaDeviceSynchronize()!=cudaSuccess){ fprintf(stderr,"sync\n"); return 1; }
172    int64_t* hc = (int64_t*)malloc((size_t)M*N*8);
173    cudaMemcpy(hc, c, (size_t)M*N*8, cudaMemcpyDeviceToHost);
174    int64_t sum = 0;
175    for(long t = 0; t < (long)M*N; ++t) sum += hc[t];
176    printf("%lld\n", (long long)sum);
177    return 0;
178}
179"#;
180
181/// Lower a recognized elementwise op (`C[p] = A[p] ⊕ B[p]`, from
182/// [`crate::tensor_recognize::recognize_elementwise`]) to a self-contained CUDA program — the
183/// elementwise analog of [`lower_matmul_to_cuda`]. One thread per element; the deterministic int8-range fill
184/// and the `Σ C` checksum match the matmul emitter's convention, so it runs and prints on the GPU. A `B` of one row
185/// holds that row's elements and is read at each element's column.
186pub fn lower_elementwise_to_cuda(spec: &ElementwiseSpec) -> String {
187    CUDA_ELEMENTWISE_TEMPLATE
188        .replace("{B_AT}", if spec.row.is_some() { "p % NB" } else { "p" })
189        .replace("{NB}", &spec.row.unwrap_or(spec.n).to_string())
190        .replace("{N}", &spec.n.to_string())
191        .replace("{OP}", spec.op.c_op())
192}
193
194/// The emitted elementwise program. `{N}` is the recognized element count, `{OP}` the recognized operator
195/// (`+` or `*`), `{NB}` and `{B_AT}` the elements of `B` and where element `p` reads it (`p`, or its column `p % NB`
196/// for a row). Same synthetic fill + checksum shape as the matmul kernel.
197const CUDA_ELEMENTWISE_TEMPLATE: &str = r#"// Auto-generated by LOGOS codegen_cuda::matrix — a recognized LOGOS elementwise op lowered to CUDA.
198// C[p] = A[p] {OP} B[{B_AT}] over N elements (B has NB: N, or a row's width); dims are the source program's; checksum ==
199// the VM's Show.
200#include <cstdio>
201#include <cstdint>
202#define N {N}
203#define NB {NB}
204__global__ void fill_ab(int64_t* a, int64_t* b){
205    long p = blockIdx.x*(long)blockDim.x + threadIdx.x;
206    if(p < N) a[p] = (p % 7) - 3;
207    if(p < NB) b[p] = (p % 5) - 2;
208}
209__global__ void ew(const int64_t* a, const int64_t* b, int64_t* c){
210    long p = blockIdx.x*(long)blockDim.x + threadIdx.x;
211    if(p < N) c[p] = a[p] {OP} b[{B_AT}];
212}
213int main(void){
214    int64_t *a,*b,*c;
215    if(cudaMalloc(&a,(size_t)N*8)||cudaMalloc(&b,(size_t)N*8)||cudaMalloc(&c,(size_t)N*8)){ fprintf(stderr,"malloc\n"); return 1; }
216    fill_ab<<<(N+255)/256,256>>>(a,b);
217    ew<<<(N+255)/256,256>>>(a,b,c);
218    if(cudaDeviceSynchronize()!=cudaSuccess){ fprintf(stderr,"sync\n"); return 1; }
219    int64_t* hc = (int64_t*)malloc((size_t)N*8);
220    cudaMemcpy(hc, c, (size_t)N*8, cudaMemcpyDeviceToHost);
221    int64_t sum = 0;
222    for(long t = 0; t < N; ++t) sum += hc[t];
223    printf("%lld\n", (long long)sum);
224    return 0;
225}
226"#;
227
228/// Lower a recognized reduction (`rmsnorm`/`softmax`, from
229/// [`crate::tensor_recognize::recognize_reduction`]) with the program's epsilon to a self-contained float CUDA
230/// program — the reduction analog of [`lower_matmul_to_cuda`]. Deterministic float fill, one thread per element, and
231/// a `Σ Y` checksum printed with fixed precision so it compiles + runs on the GPU.
232pub fn lower_reduction_to_cuda(spec: &ReductionSpec, eps: crate::tensor_recognize::Epsilon) -> String {
233    // Route the recognized flat reduction through the real one-block-per-row fused-row emitter (`rows == 1`,
234    // `cols == n`). The per-row / weighted / fused-add variants reuse the SAME emitter as their sugar lands.
235    let frs = FusedRowSpec {
236        rows: 1,
237        cols: spec.n,
238        reduce: match spec.kind {
239            ReductionKind::Rmsnorm => RowReduceKind::RmsNorm,
240            ReductionKind::Softmax => RowReduceKind::Softmax,
241        },
242        x: spec.x,
243        y: spec.y,
244        weight: None,
245        weight_rows: 1,
246        residual: None,
247        bias: None,
248        eps,
249    };
250    super::fused_row::emit_fused_row(&frs, &CudaKnobs::default())
251}
252
253/// The `Int8Dp4a` variant — same recognized matmul, same fill, same checksum, but the contraction runs on
254/// Ampere's `__dp4a` (4 signed-int8 MACs per instruction) with int8 operands and an int32 accumulator. Valid
255/// ONLY when operands fit int8 and the contraction fits int32 (the `precision` knob's documented trade-off);
256/// for such programs its stdout is identical to [`CUDA_MATMUL_TEMPLATE`] and to the VM. `K` is padded to a
257/// multiple of 4 with zero bytes, which contribute nothing to the dot product.
258const CUDA_MATMUL_DP4A_TEMPLATE: &str = r#"// Auto-generated by LOGOS codegen_cuda::matrix — recognized LOGOS matmul, __dp4a int8 fast path.
259// C(M x N) = A(M x K) . B(K x N); operands int8, __dp4a accumulation; checksum == the VM's Show (int8-safe).
260#include <cstdio>
261#include <cstdint>
262#define M {M}
263#define N {N}
264#define K {K}
265// Same deterministic fill as the general kernel, stored as signed int8 (values are in [-3,3]/[-2,2]).
266__global__ void fill_ab(signed char* a, signed char* b){
267    long p = blockIdx.x*(long)blockDim.x + threadIdx.x;
268    if(p < (long)M*K) a[p] = (signed char)((p % 7) - 3);
269    if(p < (long)K*N) b[p] = (signed char)((p % 5) - 2);
270}
271__global__ void gemm_dp4a(const signed char* a, const signed char* b, int64_t* c){
272    long j = blockIdx.x*(long)blockDim.x + threadIdx.x;
273    long i = blockIdx.y*(long)blockDim.y + threadIdx.y;
274    if(i >= M || j >= N) return;
275    int acc = 0;
276    for(int kk = 0; kk < K; kk += 4){
277        int a0 = a[i*(long)K + kk];
278        int a1 = (kk+1 < K) ? a[i*(long)K + kk+1] : 0;
279        int a2 = (kk+2 < K) ? a[i*(long)K + kk+2] : 0;
280        int a3 = (kk+3 < K) ? a[i*(long)K + kk+3] : 0;
281        int b0 = b[(long)kk*N + j];
282        int b1 = (kk+1 < K) ? b[(long)(kk+1)*N + j] : 0;
283        int b2 = (kk+2 < K) ? b[(long)(kk+2)*N + j] : 0;
284        int b3 = (kk+3 < K) ? b[(long)(kk+3)*N + j] : 0;
285        int aa = (a0 & 0xff) | ((a1 & 0xff)<<8) | ((a2 & 0xff)<<16) | ((a3 & 0xff)<<24);
286        int bb = (b0 & 0xff) | ((b1 & 0xff)<<8) | ((b2 & 0xff)<<16) | ((b3 & 0xff)<<24);
287        acc = __dp4a(aa, bb, acc);
288    }
289    c[i*(long)N + j] = acc;
290}
291int main(void){
292    signed char *a,*b; int64_t *c;
293    if(cudaMalloc(&a,(size_t)M*K)||cudaMalloc(&b,(size_t)K*N)||cudaMalloc(&c,(size_t)M*N*8)){ fprintf(stderr,"malloc\n"); return 1; }
294    long filln = ((long)M*K > (long)K*N) ? (long)M*K : (long)K*N;
295    fill_ab<<<(filln+255)/256,256>>>(a,b);
296    dim3 blk(16,16), grd((N+15)/16,(M+15)/16);
297    gemm_dp4a<<<grd,blk>>>(a,b,c);
298    if(cudaDeviceSynchronize()!=cudaSuccess){ fprintf(stderr,"sync\n"); return 1; }
299    int64_t* hc = (int64_t*)malloc((size_t)M*N*8);
300    cudaMemcpy(hc, c, (size_t)M*N*8, cudaMemcpyDeviceToHost);
301    int64_t sum = 0;
302    for(long t = 0; t < (long)M*N; ++t) sum += hc[t];
303    printf("%lld\n", (long long)sum);
304    return 0;
305}
306"#;
307
308/// A complete int8 weight-stationary GEMM CUDA program (weights + activations baked, computed
309/// with DP4A, printed row-major) plus its Makefile.
310#[derive(Debug, Clone)]
311pub struct CudaGemmArtifact {
312    /// The self-contained `.cu`: baked int8 `A`/`Wᵀ`, the DP4A kernel, and a host that runs it and
313    /// prints `O` row-major (one int32 per line) — parseable by the lock and comparable to `eval_systolic`.
314    pub kernel_cu: String,
315    /// Makefile glue (`nvcc -arch=sm_86 …`).
316    pub makefile: String,
317    pub m: usize,
318    pub k: usize,
319    pub n: usize,
320}
321
322/// A reproducible int8 `(weights K×N row-major, acts M×K row-major)` sample in `[-128, 127]`, shared by
323/// [`compile_to_cuda_gemm`] and the lock so the emitted kernel and the `eval_systolic` oracle read identical
324/// data (no drift). Deterministic in the indices — no RNG.
325///
326/// [`compile_to_cuda_gemm`]: crate::compile::compile_to_cuda_gemm
327pub fn sample_gemm_data(m: usize, k: usize, n: usize) -> (Vec<i32>, Vec<i32>) {
328    let weights: Vec<i32> = (0..k * n)
329        .map(|idx| ((idx as i32 * 7 + 3).rem_euclid(255)) - 128)
330        .collect();
331    let acts: Vec<i32> = (0..m * k)
332        .map(|idx| ((idx as i32 * 5 + 1).rem_euclid(255)) - 128)
333        .collect();
334    (weights, acts)
335}
336
337/// Emit a DP4A int8 GEMM CUDA program for `O(m×n) = A(m×k) · W(k×n)`, with `weights` (`k*n` row-major,
338/// `W[r][c] = weights[r*n+c]`) and `acts` (`m*k` row-major) baked in. Refuses (typed) if the RDU-shaped
339/// target does not advertise int8 weight-stationary GEMM — the exact certified gate the fabric uses.
340pub fn emit_int8_gemm(
341    m: usize,
342    k: usize,
343    n: usize,
344    weights: &[i32],
345    acts: &[i32],
346) -> Result<CudaGemmArtifact, CudaLowerError> {
347    // The certified capability gate: int8 + weight-stationary GEMM on the RDU-shaped fabric.
348    FabricTarget::rdu_shaped()
349        .require_gemm(Dataflow::WeightStationary, Precision::Int8)
350        .map_err(|_| {
351            CudaLowerError::Unsupported("rdu-shaped target lacks int8 weight-stationary GEMM")
352        })?;
353    assert_eq!(weights.len(), k * n, "weights must be k*n row-major");
354    assert_eq!(acts.len(), m * k, "acts must be m*k row-major");
355
356    // K padded to a multiple of 4 so DP4A consumes whole 4-lanes; the pad is zeros (0 products add 0),
357    // so the result is unchanged. A is stored row-major padded; W is stored TRANSPOSED+padded
358    // (`Wt[c*kp + r] = W[r][c]`) so the 4 weights for a fixed output column c are contiguous for DP4A.
359    let kp = k.div_ceil(4) * 4;
360
361    let mut a_pad = vec![0i32; m * kp];
362    for i in 0..m {
363        for r in 0..k {
364            a_pad[i * kp + r] = acts[i * k + r];
365        }
366    }
367    let mut wt = vec![0i32; n * kp];
368    for r in 0..k {
369        for c in 0..n {
370            wt[c * kp + r] = weights[r * n + c];
371        }
372    }
373
374    let a_lit = i8_array_literal(&a_pad);
375    let wt_lit = i8_array_literal(&wt);
376    let kernel_cu = CUDA_GEMM_TEMPLATE
377        .replace("{M}", &m.to_string())
378        .replace("{N}", &n.to_string())
379        .replace("{KP}", &kp.to_string())
380        .replace("{A_DATA}", &a_lit)
381        .replace("{WT_DATA}", &wt_lit);
382
383    Ok(CudaGemmArtifact {
384        kernel_cu,
385        makefile: CUDA_MAKEFILE_TEMPLATE.replace("{KERNEL_FILE}", super::KERNEL_FILE_NAME),
386        m,
387        k,
388        n,
389    })
390}
391
392// ───────────────────────── block-scaled Q8_0 GEMM (the real model operation) ─────────────────────────
393//
394// A linear layer `y = W·x` with Q8_0 weights: `W` is `[out][in]`, both W and the activations are int8 with
395// a separate f32 scale per run of 32 along the contraction (`in`) dim. `O(m×n) = A(m×k) · Wᵀ(k×n)`.
396// Layout: `wq[o*k + t]`, `ws[o*nb + t/32]`, `aq[i*k + t]`, `as_[i*nb + t/32]`, `nb = k/32`.
397
398/// GROUND TRUTH (f64): dequantize every element (`q * scale`) then multiply-accumulate. The unambiguous
399/// meaning of a Q8_0 matmul, order-independent.
400pub fn blockscaled_gemm_dequant(
401    m: usize,
402    k: usize,
403    n: usize,
404    wq: &[i32],
405    ws: &[f32],
406    aq: &[i32],
407    as_: &[f32],
408) -> Vec<f64> {
409    assert!(k % 32 == 0);
410    let nb = k / 32;
411    let mut out = vec![0f64; m * n];
412    for i in 0..m {
413        for o in 0..n {
414            let mut acc = 0f64;
415            for t in 0..k {
416                let a = aq[i * k + t] as f64 * as_[i * nb + t / 32] as f64;
417                let w = wq[o * k + t] as f64 * ws[o * nb + t / 32] as f64;
418                acc += a * w;
419            }
420            out[i * n + o] = acc;
421        }
422    }
423    out
424}
425
426/// WHAT THE DP4A KERNEL COMPUTES: an exact int32 dot within each 32-block, scaled once by the two block
427/// scales, accumulated in f64. Algebraically identical to [`blockscaled_gemm_dequant`] (the scale is
428/// constant across a block) — the lock proves the implementation matches, catching any scale-indexing bug.
429pub fn blockscaled_gemm_int8(
430    m: usize,
431    k: usize,
432    n: usize,
433    wq: &[i32],
434    ws: &[f32],
435    aq: &[i32],
436    as_: &[f32],
437) -> Vec<f64> {
438    assert!(k % 32 == 0);
439    let nb = k / 32;
440    let mut out = vec![0f64; m * n];
441    for i in 0..m {
442        for o in 0..n {
443            let mut acc = 0f64;
444            for b in 0..nb {
445                let mut dot: i64 = 0;
446                for j in 0..32 {
447                    let t = b * 32 + j;
448                    dot += aq[i * k + t] as i64 * wq[o * k + t] as i64;
449                }
450                acc += dot as f64 * as_[i * nb + b] as f64 * ws[o * nb + b] as f64;
451            }
452            out[i * n + o] = acc;
453        }
454    }
455    out
456}
457
458/// Emit a block-scaled Q8_0 GEMM CUDA program: DP4A int8 dot per 32-block, scaled by the baked f32 block
459/// scales, accumulated in f32. Prints `O(m×n)` row-major as `%.9g` (one per line). `k` must be a multiple
460/// of 32 (whole Q8_0 blocks). Refuses (typed) if the RDU-shaped target lacks int8 weight-stationary GEMM.
461pub fn emit_scaled_int8_gemm(
462    m: usize,
463    k: usize,
464    n: usize,
465    wq: &[i32],
466    ws: &[f32],
467    aq: &[i32],
468    as_: &[f32],
469) -> Result<CudaGemmArtifact, CudaLowerError> {
470    FabricTarget::rdu_shaped()
471        .require_gemm(Dataflow::WeightStationary, Precision::Int8)
472        .map_err(|_| {
473            CudaLowerError::Unsupported("rdu-shaped target lacks int8 weight-stationary GEMM")
474        })?;
475    assert!(k % 32 == 0, "k must be a multiple of the Q8_0 block (32)");
476    let nb = k / 32;
477    assert_eq!(wq.len(), n * k);
478    assert_eq!(ws.len(), n * nb);
479    assert_eq!(aq.len(), m * k);
480    assert_eq!(as_.len(), m * nb);
481
482    // A[i][t] row-major, Wt[o][t] row-major (already [out][in], the DP4A per-column contraction is
483    // contiguous). Scales baked as f32.
484    let kernel_cu = CUDA_SCALED_GEMM_TEMPLATE
485        .replace("{M}", &m.to_string())
486        .replace("{N}", &n.to_string())
487        .replace("{K}", &k.to_string())
488        .replace("{NB}", &nb.to_string())
489        .replace("{A_DATA}", &i8_array_literal(aq))
490        .replace("{W_DATA}", &i8_array_literal(wq))
491        .replace("{AS_DATA}", &f32_array_literal(as_))
492        .replace("{WS_DATA}", &f32_array_literal(ws));
493
494    Ok(CudaGemmArtifact {
495        kernel_cu,
496        makefile: CUDA_MAKEFILE_TEMPLATE.replace("{KERNEL_FILE}", super::KERNEL_FILE_NAME),
497        m,
498        k,
499        n,
500    })
501}
502
503/// Format an f32 slice as a C initializer body with full round-trip precision.
504fn f32_array_literal(xs: &[f32]) -> String {
505    let mut s = String::with_capacity(xs.len() * 14);
506    for (i, x) in xs.iter().enumerate() {
507        if i > 0 {
508            s.push_str(", ");
509        }
510        s.push_str(&format!("{:.9e}f", x));
511    }
512    s
513}
514
515/// Block-scaled DP4A GEMM. `A`/`W` are `[m|n][k]` row-major int8; `As`/`Ws` are `[m|n][nb]` f32 scales.
516const CUDA_SCALED_GEMM_TEMPLATE: &str = r#"// Auto-generated by LOGOS codegen_cuda::matrix — do not edit.
517// Block-scaled Q8_0 GEMM, O(M x N) = A(M x K) . W(N x K)^T, via Ampere DP4A with per-32-block f32 scales.
518#include <cstdio>
519#include <cstdint>
520
521#define M {M}
522#define N {N}
523#define K {K}
524#define NB {NB}   // K / 32 blocks
525
526__device__ static const signed char A[M * K] = { {A_DATA} };
527__device__ static const signed char W[N * K] = { {W_DATA} };  // W[o][t], [out][in]
528__device__ static const float As[M * NB]     = { {AS_DATA} };
529__device__ static const float Ws[N * NB]     = { {WS_DATA} };
530
531__device__ static inline int pack4(const signed char *p) {
532    return (p[0] & 0xff) | ((p[1] & 0xff) << 8) | ((p[2] & 0xff) << 16) | ((p[3] & 0xff) << 24);
533}
534
535__global__ void gemm_i8s(float *O) {
536    int o = blockIdx.x * blockDim.x + threadIdx.x;  // output feature
537    int i = blockIdx.y * blockDim.y + threadIdx.y;  // row
538    if (i >= M || o >= N) return;
539    float acc = 0.0f;
540    for (int b = 0; b < NB; ++b) {
541        int d = 0;
542        for (int j = 0; j < 32; j += 4)
543            d = __dp4a(pack4(&A[i * K + b * 32 + j]), pack4(&W[o * K + b * 32 + j]), d);
544        acc += (float)d * As[i * NB + b] * Ws[o * NB + b];
545    }
546    O[i * N + o] = acc;
547}
548
549int main(void) {
550    float *dO = nullptr;
551    if (cudaMalloc(&dO, (size_t)M * N * sizeof(float)) != cudaSuccess) { fprintf(stderr, "cudaMalloc\n"); return 1; }
552    dim3 block(16, 16);
553    dim3 grid((N + block.x - 1) / block.x, (M + block.y - 1) / block.y);
554    gemm_i8s<<<grid, block>>>(dO);
555    cudaError_t err = cudaGetLastError();
556    if (err != cudaSuccess) { fprintf(stderr, "launch: %s\n", cudaGetErrorString(err)); return 1; }
557    if (cudaDeviceSynchronize() != cudaSuccess) { fprintf(stderr, "sync\n"); return 1; }
558    float hO[M * N];
559    if (cudaMemcpy(hO, dO, (size_t)M * N * sizeof(float), cudaMemcpyDeviceToHost) != cudaSuccess) { fprintf(stderr, "memcpy\n"); return 1; }
560    for (int idx = 0; idx < M * N; ++idx) printf("%.9g\n", hO[idx]);
561    cudaFree(dO);
562    return 0;
563}
564"#;
565
566/// Format an int8-range slice as a C `signed char` initializer body (`-128, 4, …`).
567fn i8_array_literal(xs: &[i32]) -> String {
568    let mut s = String::with_capacity(xs.len() * 5);
569    for (i, x) in xs.iter().enumerate() {
570        if i > 0 {
571            s.push_str(", ");
572        }
573        s.push_str(&x.to_string());
574    }
575    s
576}
577
578/// The single-file DP4A int8 GEMM program. `{M}`/`{N}`/`{KP}` are the padded dims; `{A_DATA}`/`{WT_DATA}`
579/// are the baked int8 arrays. One thread per output element; `Wᵀ` layout makes each column's contraction
580/// contiguous so `__dp4a` reads 4 int8 lanes at a stride of 1.
581const CUDA_GEMM_TEMPLATE: &str = r#"// Auto-generated by LOGOS codegen_cuda::matrix — do not edit.
582// Weight-stationary int8 GEMM, O(M x N) = A(M x K) . W(K x N), via Ampere DP4A.
583// Prints O row-major, one int32 per line — byte-comparable to codegen_sva::eval_systolic.
584#include <cstdio>
585#include <cstdint>
586
587#define M {M}
588#define N {N}
589#define KP {KP}   // K padded up to a multiple of 4 (pad lanes are 0)
590
591__device__ static const signed char A[M * KP]  = { {A_DATA} };
592__device__ static const signed char Wt[N * KP] = { {WT_DATA} };  // Wt[c*KP + r] = W[r][c]
593
594// Pack 4 consecutive signed int8 into an int32 (little-endian lanes), the DP4A operand form.
595__device__ static inline int pack4(const signed char *p) {
596    return (p[0] & 0xff) | ((p[1] & 0xff) << 8) | ((p[2] & 0xff) << 16) | ((p[3] & 0xff) << 24);
597}
598
599__global__ void gemm_i8(int *O) {
600    int c = blockIdx.x * blockDim.x + threadIdx.x;  // output column
601    int i = blockIdx.y * blockDim.y + threadIdx.y;  // output row
602    if (i >= M || c >= N) return;
603    int acc = 0;
604    for (int r = 0; r < KP; r += 4) {
605        acc = __dp4a(pack4(&A[i * KP + r]), pack4(&Wt[c * KP + r]), acc);
606    }
607    O[i * N + c] = acc;
608}
609
610int main(void) {
611    int *dO = nullptr;
612    if (cudaMalloc(&dO, (size_t)M * N * sizeof(int)) != cudaSuccess) { fprintf(stderr, "cudaMalloc\n"); return 1; }
613    dim3 block(16, 16);
614    dim3 grid((N + block.x - 1) / block.x, (M + block.y - 1) / block.y);
615    gemm_i8<<<grid, block>>>(dO);
616    cudaError_t err = cudaGetLastError();
617    if (err != cudaSuccess) { fprintf(stderr, "launch: %s\n", cudaGetErrorString(err)); return 1; }
618    if (cudaDeviceSynchronize() != cudaSuccess) { fprintf(stderr, "sync\n"); return 1; }
619    int hO[M * N];
620    if (cudaMemcpy(hO, dO, (size_t)M * N * sizeof(int), cudaMemcpyDeviceToHost) != cudaSuccess) { fprintf(stderr, "memcpy\n"); return 1; }
621    for (int idx = 0; idx < M * N; ++idx) printf("%d\n", hO[idx]);
622    cudaFree(dO);
623    return 0;
624}
625"#;
626
627#[cfg(test)]
628#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_cuda/matrix/tests.rs"]
629mod tests;