Skip to main content

logicaffeine_compile/codegen_cuda/
mod.rs

1//! LOGOS → **CUDA** AOT codegen tier. Emits ONE self-contained CUDA program — a
2//! `__global__` kernel that runs a whole LOGOS program on a single GPU thread, plus a
3//! host orchestrator — for the integer/bool scalar fragment of the language.
4//!
5//! ## Why this tier tree-shakes, rather than wrapping the whole runtime
6//! A Tenstorrent baby core is a RISC-V CPU, so [`crate::codegen_tt`] reuses the entire
7//! glibc-backed C body verbatim. A GPU thread is not a CPU: device code has no
8//! `realloc`/`calloc`/`strdup`/`snprintf`/`strtod`, which the C runtime leans on for
9//! every string, map, and big integer. And `codegen_c` emits the WHOLE ~1900-line runtime
10//! for every program, so lifting it wholesale would need device bignum + `vsnprintf` even
11//! for a float one-liner. So the CUDA tier TREE-SHAKES the C oracle (via
12//! [`crate::codegen_c::cshake`]): it lifts only the runtime slice a program actually
13//! references (see `device_lift`), `__device__`-annotating each unit, applying device
14//! OVERRIDES for the irreducibly-host ones (`logos_fmt_f64` → a from-scratch Dragon4
15//! shortest-float formatter, `device_float`), and REFUSING (typed) any reachable unit
16//! that still needs a device-absent facility. A program is accepted iff BOTH pass:
17//!   1. every op is in the scalar fragment ([`CUDA_SCALAR_OPS`]); and
18//!   2. every reachable runtime unit is device-liftable (verbatim, shimmed, or overridden).
19//!
20//! ## What is verified, and when
21//! - **Pre-toolkit (no nvcc):** the artifact is structurally a well-formed CUDA program,
22//!   and `oracle_c` is byte-for-byte the C tier's proven ≡VM body.
23//! - **On-GPU (gated on `nvcc` + a device):** the emitted `.cu` compiles with `nvcc` and
24//!   its output equals the tree-walker over the corpus (`cuda_aot_lock` / `cuda_float_lock`,
25//!   skipped when the toolkit is absent — never a failure, mirroring the RISC-V/qemu gate).
26//!
27//! ## Totality discipline (mirrors the RISC-V / Tenstorrent tiers)
28//! Every refusal is [`CudaLowerError::Unsupported`] — a SOUND decline, never a
29//! miscompile. The `cuda_aot_lock` ratchet holds the supported-op floor and a shrink-only
30//! gap list so the fragment can only grow.
31
32pub mod chip;
33pub(crate) mod device_float;
34mod device_lift;
35mod device_string;
36pub mod forward_shim;
37pub mod fused_row;
38pub mod gdn;
39pub mod gemm_tile;
40pub mod knobs;
41pub mod lane;
42pub mod lane_link;
43pub mod lane_shim;
44pub mod matrix;
45pub mod provenance;
46pub mod qwen38;
47pub mod qwen38_batched;
48pub mod qwen_batched;
49pub mod qwen_emit;
50mod runtime;
51pub mod solbench;
52mod wrap;
53
54pub use chip::{CudaChip, GpuFamily};
55pub use device_float::LOGOS_DEVICE_FLOAT_FMT;
56pub use fused_row::{emit_fused_row, emit_swiglu};
57pub use knobs::{CachePrecision, CudaKnobs, KnobTradeoff, LoadWidth, MatmulPrecision, StatePrecision, STRICTLY_BETTER};
58pub use matrix::{
59    emit_int8_gemm, lower_elementwise_to_cuda, lower_matmul_to_cuda, lower_matmul_to_cuda_with,
60    lower_reduction_to_cuda, sample_gemm_data, CudaGemmArtifact,
61};
62pub use runtime::{CUDA_MAKEFILE_TEMPLATE, KERNEL_FILE_NAME};
63pub use solbench::{emit_fused_row_solution, SolSolution};
64
65use crate::vm::{CompiledProgram, Op};
66
67/// An op / value outside the CUDA scalar fragment. The `&str` names what is not lifted.
68/// A refusal — the backend declines rather than emit wrong code (mirrors
69/// [`crate::codegen_riscv::RiscVLowerError`] / [`crate::codegen_tt::TtLowerError`]).
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum CudaLowerError {
72    Unsupported(&'static str),
73    /// The C tier refuses the program: its C carries an `#error` naming what it does not lower, and the
74    /// device lowers the same C.
75    CRefused(String),
76}
77
78impl std::fmt::Display for CudaLowerError {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            CudaLowerError::Unsupported(what) => write!(f, "cuda AOT backend: unsupported {what}"),
82            CudaLowerError::CRefused(why) => write!(f, "cuda AOT backend: {why}"),
83        }
84    }
85}
86
87/// The first refusal the C tier wrote into `c` — the words of its `#error` line.
88fn c_refusal(c: &str) -> Option<String> {
89    c.lines().find_map(|line| {
90        let words = line.trim_start().strip_prefix("#error")?.trim();
91        Some(
92            words
93                .strip_prefix('"')
94                .and_then(|w| w.strip_suffix('"'))
95                .unwrap_or(words)
96                .to_string(),
97        )
98    })
99}
100
101impl std::error::Error for CudaLowerError {}
102
103/// A complete single-file CUDA program emitted from a LOGOS program.
104#[derive(Debug, Clone)]
105pub struct CudaArtifact {
106    /// The self-contained `.cu`: device preamble + the lifted `__device__` program +
107    /// the `__global__` entry + the host orchestrator. Built with `nvcc`.
108    pub kernel_cu: String,
109    /// Makefile glue (`nvcc -arch=sm_86 …`).
110    pub makefile: String,
111    /// The pre-GPU ≡VM correctness oracle: the self-contained C program from
112    /// [`crate::codegen_c`], whose integer/bool compute the kernel runs. Compiled with
113    /// gcc on the host, its output is byte-identical to the VM.
114    pub oracle_c: String,
115}
116
117/// The op names the CUDA scalar fragment lowers today — integer/bool arithmetic,
118/// comparisons, control flow, calls, globals, and `Show`. EXHAUSTIVELY enforced against
119/// the VM ISA by `cuda_aot_lock` (floor + shrink-only gaps), so this set can only grow.
120/// A scalar op whose value escapes to BigInt/Rational/Float/Text is still caught, by the
121/// device-cleanliness backstop — this list is necessary, not sufficient, for support.
122pub const CUDA_SCALAR_OPS: &[&str] = &[
123    "LoadConst",
124    "Move",
125    "RevSwap",
126    "RevXor",
127    "RevNeg",
128    "RevAdd",
129    "RevSub",
130    "Add",
131    "AddAssign",
132    "Sub",
133    "Mul",
134    "Div",
135    "ExactDiv",
136    "FloorDiv",
137    "Mod",
138    "DivPow2",
139    "MagicDivU",
140    "Lt",
141    "Gt",
142    "LtEq",
143    "GtEq",
144    "Eq",
145    "ApproxEq",
146    "NotEq",
147    "Not",
148    "Pow",
149    "BitXor",
150    "BitAnd",
151    "BitOr",
152    "Shl",
153    "Shr",
154    "Jump",
155    "JumpIfFalse",
156    "JumpIfTrue",
157    "Call",
158    "CallBuiltin",
159    "GlobalGet",
160    "GlobalSet",
161    "Return",
162    "ReturnNothing",
163    "Show",
164    "Halt",
165    // Phase 1 — the runtime-error path: a fail site (`FailWith`, a non-exhaustive `Inspect` fallthrough,
166    // a div-by-zero guard) becomes a device trap (CUDA_DEVICE_FAULT_SHIM) → nonzero host exit ≡ the VM's
167    // error. The stderr diagnostic is not part of the ≡VM comparison.
168    "FailWith",
169    // Phase 2 — collections / heap (device allocator shim). A program `Show`ing a whole collection
170    // still needs the device string runtime (Phase 3) and is refused at the value-slice level; these
171    // ops themselves run ≡VM whenever the reachable slice is device-clean.
172    "NewList",
173    "NewEmptyList",
174    "NewEmptyListWithCapacity",
175    "NewEmptyListFixed",
176    "NewEmptyListI32",
177    "NewEmptySet",
178    "NewEmptyMap",
179    "NewEmptyMapWithCapacity",
180    "NewRange",
181    "ListPush",
182    "ListPop",
183    "ListPushField",
184    "SetAdd",
185    "RemoveFrom",
186    "SetIndex",
187    "SetIndexUnchecked",
188    "Index",
189    "IndexUnchecked",
190    "RegionBoundsGuard",
191    "Length",
192    "Contains",
193    "SliceOp",
194    "UnionOp",
195    "IntersectOp",
196    "SeqConcat",
197    "DeepClone",
198    "EnsureOwned",
199    "IterPrepare",
200    "IterNext",
201    "IterPop",
202    // Phase 3 — text: string concat + interpolation/format specs (device vsnprintf/str runtime).
203    "Concat",
204    "FormatValue",
205    // Phase 4 — aggregates: struct + tuple literals (synthesized C types lift as device structs).
206    // `DestructureTuple` (Phase 4b) rides the same synthesized `LTuple_…` device structs: a
207    // `Repeat for (a, b) in pairs` over a Seq-of-tuple binds each pattern var to a positional field.
208    // Enum (NewInductive/TestArm/BindArm) is still Phase 4b (see cuda_aot_lock).
209    "NewStruct",
210    "StructInsert",
211    "GetField",
212    "NewTuple",
213    "DestructureTuple",
214    // Phase 4b — enums: `new <Variant>` + `Inspect`/`When` match over a device tagged-union.
215    "NewInductive",
216    "TestArm",
217    "BindArm",
218    // Phase 2b — closures: a non-capturing lambda lambda-lifts to a top-level `__device__` function;
219    // `CallValue` is an indirect call through a device function pointer (nvcc supports these).
220    "MakeClosure",
221    "CallValue",
222    // Phase 4c — policy PREDICATE check: `Check that <subj> is <pred>` lowers to an inline guard
223    // `if (!(<condition>)) trap` where the condition is the `## Policy` predicate emitted over the subject's
224    // device struct fields (`FieldEquals`→strcmp/`==`, `FieldBool`, nested `Predicate`, `And`/`Or`) —
225    // byte-identical to `evaluate_policy_condition`. A denied check traps (CUDA_DEVICE_FAULT_SHIM) → nonzero
226    // exit ≡ the VM's `Security Check Failed`. Capability (`can <action> the <object>`) checks resolve their
227    // object by TYPE and error in BOTH oracles through this path, so they are not ≡VM-exercisable; codegen_c
228    // refuses any not-yet-lowerable condition form LOUDLY (`#error`), never a silent skip.
229    "CheckPolicy",
230    // Phase 6 — temporal: `today`/`now` read the deterministic fixed clock the HOST injects into device
231    // globals before launch (CUDA_DEVICE_CLOCK); `Show` renders the date/moment via device calendar math.
232    "LoadToday",
233    "LoadNow",
234    // Phase 6 — argv: `args()` reads the device `_logos_argc/_logos_argv` globals the HOST marshals its own
235    // argv into (cudaMalloc/cudaMemcpy + cudaMemcpyToSymbol) before launch; `logos_args` builds the Seq.
236    "Args",
237    // Phase 6 — counter CRDT on the single device thread: `NewCrdt` default-fills a `ConvergentCount`/
238    // `Tally` field, `CrdtBump` is `Increase`/`Decrease`, `CrdtMerge` is the per-replica-MAX lattice join
239    // (idempotent). The `logos_crdt` runtime is a device-clean integer count-map (malloc shim + arithmetic),
240    // so the slice lifts verbatim. `CrdtAppend` (Append to a shared `Seq` field) + `CrdtResolve` (a LWW
241    // register — last write wins) are now codegen_c-faithful (both were `/* unsupported stmt */` stubs) and
242    // device-clean. `CrdtJoin` stays Deferred (untested).
243    "NewCrdt",
244    "CrdtBump",
245    "CrdtMerge",
246    "CrdtAppend",
247    "CrdtResolve",
248    // Phase 6a — CONCURRENCY: every task runs on the stepped scheduler — the C family's scheduler core under
249    // step functions over heap frames (`codegen_c::stepped`), placed on the device whole — so pipes, tasks,
250    // selects and the logical clock schedule on the GPU exactly as on the host C tier, at every seed.
251    "ChanNew",
252    "ChanSend",
253    "ChanRecv",
254    "ChanTrySend",
255    "ChanTryRecv",
256    "ChanClose",
257    "Spawn",
258    "SpawnHandle",
259    "TaskAwait",
260    "TaskAbort",
261    "Sleep",
262    "SelectArmRecv",
263    "SelectArmTimeout",
264    "SelectWait",
265    // Phase 6c — OFFLINE NETWORKING (the loopback transport). `Connect`/`Listen`/`Sync` no-op, `MakePeer` is
266    // identity, `Send`/`Stream` enqueue a 64-bit message into a per-address FIFO, `Await` dequeues it — the
267    // device-lifted `C_NET_RUNTIME` (strcmp/strdup shimmed). Single-node round-trip ≡ the VM's offline model.
268    "NetConnect",
269    "NetListen",
270    "NetSend",
271    "NetStream",
272    "NetAwait",
273    "NetMakePeer",
274    "NetSync",
275];
276
277/// The OFFLINE NETWORKING ops the device serves via the loopback transport (`codegen_c`'s `C_NET_RUNTIME`,
278/// device-lifted). `Connect`/`Listen`/`Sync` are no-ops, `MakePeer` is identity, `Send`/`Stream` enqueue a
279/// 64-bit message into a per-address FIFO, `Await` dequeues it — so a single-node `Send X to peer; Await …`
280/// round-trips ≡ the VM's offline `NetInbox`. Every op here is also in [`CUDA_SCALAR_OPS`]; excluded from the
281/// offload refusal below (they are now device-served, not host-only). Bound by `cuda_aot_lock`.
282pub const CUDA_NET_OPS: &[&str] = &[
283    "NetConnect",
284    "NetListen",
285    "NetSend",
286    "NetStream",
287    "NetAwait",
288    "NetMakePeer",
289    "NetSync",
290];
291
292/// `Some(reason)` if `program` uses an op outside the CUDA scalar fragment. Host-only IO and the
293/// DOM get the shared single-device reason ([`crate::aot_unsupported`]); everything else that is not
294/// a scalar op is named directly.
295pub fn unsupported_reason(program: &CompiledProgram) -> Option<&'static str> {
296    use crate::aot_unsupported::Offload;
297    // Of the offload categories, the device serves every channel / task / virtual-time op (the stepped
298    // scheduler) and the loopback networking ops (`CUDA_NET_OPS`); the first op it does not serve is refused.
299    if let Some(o) = program
300        .code
301        .iter()
302        .find_map(|op| match crate::aot_unsupported::classify(op) {
303            Some(o @ (Offload::Net | Offload::Dom)) if !CUDA_NET_OPS.contains(&op.name()) => {
304                Some(o)
305            }
306            _ => None,
307        })
308    {
309        return Some(match o {
310            Offload::Net => "networking op (host-only IO)",
311            _ => "DOM / browser op (a GPU kernel has no document to build)",
312        });
313    }
314    program
315        .code
316        .iter()
317        .find(|op| !CUDA_SCALAR_OPS.contains(&op.name()))
318        .map(op_refusal)
319}
320
321/// A stable reason string for a non-scalar op (so the message is meaningful without
322/// leaking the whole op zoo). Grouped by the runtime surface it would require.
323fn op_refusal(op: &Op) -> &'static str {
324    match op.name() {
325        "NewList"
326        | "NewEmptyList"
327        | "NewEmptyListWithCapacity"
328        | "NewEmptyListFixed"
329        | "NewEmptyListI32"
330        | "NewEmptySet"
331        | "NewEmptyMap"
332        | "NewEmptyMapWithCapacity"
333        | "NewRange"
334        | "ListPush"
335        | "ListPop"
336        | "ListPushField"
337        | "SetAdd"
338        | "RemoveFrom"
339        | "SetIndex"
340        | "SetIndexUnchecked"
341        | "Index"
342        | "IndexUnchecked"
343        | "RegionBoundsGuard"
344        | "Length"
345        | "Contains"
346        | "SliceOp"
347        | "UnionOp"
348        | "IntersectOp"
349        | "SeqConcat" => "collection op (needs the device heap runtime, not yet lifted)",
350        "Concat" | "FormatValue" => "text/format op (needs the device string runtime)",
351        "NewStruct" | "StructInsert" | "GetField" | "NewInductive" | "TestArm" | "BindArm"
352        | "NewTuple" | "DestructureTuple" => {
353            "struct/enum/tuple op (aggregate values not yet lifted)"
354        }
355        "CrdtBump" | "CrdtMerge" | "CrdtJoin" | "NewCrdt" | "CrdtAppend" | "CrdtResolve" => {
356            "CRDT op (replicated state, off a single kernel)"
357        }
358        "BigIntWide" => "wide-integer op (256-bit lane not yet lifted)",
359        "IterPrepare" | "IterNext" | "IterPop" => {
360            "snapshot iteration (needs the device heap runtime)"
361        }
362        "LoadToday" | "LoadNow" => "temporal op (host clock, off the datapath)",
363        "MakeClosure" | "CallValue" => "closure op (heap-allocated captures not yet lifted)",
364        "Obs" => "host-side op (observation, off the datapath)",
365        "FailWith" => "runtime-error path (host exit, not yet a device trap)",
366        "DeepClone" | "EnsureOwned" => "ownership op (implies a heap collection, not yet lifted)",
367        _ => "op outside the CUDA scalar fragment",
368    }
369}
370
371/// Emit `program` as a single-file CUDA program. `oracle_c` is the self-contained C
372/// translation of the SAME program (from [`crate::compile::compile_to_c_stepped`], which is
373/// [`crate::compile::compile_to_c`]'s C for a program with no tasks); its program-region
374/// compute is lifted onto the device. Returns [`CudaLowerError::Unsupported`]
375/// if the program leaves the scalar fragment (op-level or value-escape).
376pub fn codegen_program_cuda(
377    program: &CompiledProgram,
378    oracle_c: &str,
379) -> Result<CudaArtifact, CudaLowerError> {
380    if let Some(reason) = unsupported_reason(program) {
381        return Err(CudaLowerError::Unsupported(reason));
382    }
383    if let Some(why) = c_refusal(oracle_c) {
384        return Err(CudaLowerError::CRefused(why));
385    }
386    // Tree-shake the C oracle and lift only the reachable runtime slice onto the device. A reachable
387    // unit that still needs a device-absent facility (bignum, string builder, …) is a typed refusal —
388    // never a miscompile. This REPLACES the old whole-oracle string scan with a precise per-slice check.
389    let kernel_cu =
390        device_lift::lift_program_to_device(oracle_c).map_err(CudaLowerError::Unsupported)?;
391    Ok(CudaArtifact {
392        kernel_cu,
393        makefile: CUDA_MAKEFILE_TEMPLATE.replace("{KERNEL_FILE}", KERNEL_FILE_NAME),
394        oracle_c: oracle_c.to_string(),
395    })
396}