Skip to main content

logicaffeine_compile/codegen_cuda/
runtime.rs

1//! Fixed templates for the emitted CUDA program. Unlike the Tenstorrent tier — a
2//! baby RISC-V core that runs the whole glibc-backed C body — a GPU thread has no
3//! `realloc`/`snprintf`/`strdup`. So the CUDA tier does NOT reuse the C runtime:
4//! it lifts only the program's own (integer/bool) code onto the device and supplies
5//! a tiny device preamble for the one thing that code touches — `Show`. Everything
6//! that needs the dynamic runtime is refused upstream (see [`super::codegen_program_cuda`]).
7
8/// Prepended to the tree-shaken, device-lifted runtime slice. Supplies only the standard headers the
9/// lifted runtime needs (device `printf`, fixed-width ints, `PRId64`, `memcpy`, `isnan`/`isinf`,
10/// `malloc`). The `Show` primitives (`show_i64`/`show_bool`/`show_f64`/…) are now LIFTED from the C
11/// runtime itself (tree-shaken to only what the program uses), so output stays byte-identical to the
12/// VM and there is no hand-maintained duplicate to drift.
13pub const CUDA_DEVICE_PREAMBLE: &str = r#"// Auto-generated by LOGOS codegen_cuda — do not edit.
14// A whole LOGOS program lowered to ONE CUDA kernel (launched <<<1,1>>>). Only the runtime slice the
15// program references is lifted here as __device__ functions (tree-shaken from the C oracle), plus the
16// program's own code; `Show` rides CUDA's native device printf, so output is byte-identical to the VM.
17#include <cstdio>
18#include <cstdint>
19#include <cinttypes>
20#include <cstring>
21#include <cstdlib>
22#include <cmath>
23
24// --- lifted LOGOS runtime slice (device) ----------------------------------------
25"#;
26
27/// A device allocator shim, prepended after the includes. The C runtime grows every Seq/Map/Set with
28/// `realloc`, which device code lacks (and which needs the OLD size). So every lifted allocation is
29/// routed (`malloc`/`free`/`calloc`/`realloc`/`strdup` → `logos_*`) through a header-carrying malloc:
30/// each block stores its byte size 16 bytes before the returned pointer, so `logos_realloc` recovers it.
31/// All routed allocations share one header convention, so free/realloc are always consistent. The real
32/// `malloc`/`free` are used only INSIDE these shims (the lifted code never calls them directly).
33pub const CUDA_DEVICE_ALLOC_SHIM: &str = r#"// --- device allocator shim (header-carrying malloc so realloc works) ------------
34__device__ static void* logos_malloc(size_t n){ size_t* p=(size_t*)malloc(n+16); if(!p) return (void*)0; p[0]=n; return (void*)((char*)p+16); }
35__device__ static void logos_free(void* q){ if(q) free((void*)((char*)q-16)); }
36__device__ static void* logos_realloc(void* q, size_t n){ if(!q) return logos_malloc(n); size_t old=((size_t*)((char*)q-16))[0]; void* r=logos_malloc(n); if(r){ size_t m=old<n?old:n; memcpy(r,q,m); logos_free(q);} return r; }
37__device__ static void* logos_calloc(size_t c, size_t s){ size_t n=c*s; void* r=logos_malloc(n); if(r) memset(r,0,n); return r; }
38__device__ static char* logos_strdup(const char* s){ size_t n=0; while(s[n]) n++; char* r=(char*)logos_malloc(n+1); if(r){ memcpy(r,s,n); r[n]=0; } return r; }
39// --- lifted LOGOS runtime slice (device) ----------------------------------------
40"#;
41
42/// The checked-overflow builtins the big-integer fast path uses (`__builtin_{add,sub,mul}_overflow`),
43/// which nvcc treats as host-only. Reimplemented exactly via `__int128` (device-supported): compute in
44/// 128 bits, store the low 64, and report whether the value did not fit. Prepended so the lifted bignum
45/// lifts verbatim.
46pub const CUDA_DEVICE_INTRIN_SHIM: &str = r#"// --- device checked-overflow builtins (via __int128) ----------------------------
47__device__ static int logos_add_overflow(int64_t a, int64_t b, int64_t* r){ __int128 s=(__int128)a+(__int128)b; *r=(int64_t)s; return s != (__int128)(int64_t)s; }
48__device__ static int logos_sub_overflow(int64_t a, int64_t b, int64_t* r){ __int128 s=(__int128)a-(__int128)b; *r=(int64_t)s; return s != (__int128)(int64_t)s; }
49__device__ static int logos_mul_overflow(int64_t a, int64_t b, int64_t* r){ __int128 s=(__int128)a*(__int128)b; *r=(int64_t)s; return s != (__int128)(int64_t)s; }
50"#;
51
52/// `floor`/`ceil`/`round` of an INTEGER argument. The C tier emits `(int64_t)round(x)` whatever `x`'s type — C
53/// converts an integer to `double` through the prototype, so `round(1000000LL)` is `1000000.0`. CUDA C++ resolves
54/// the same call to the host-only integral template (`constexpr __host__`), which device code cannot call. These
55/// non-template exact matches win overload resolution and keep the C meaning: an integer rounds to itself.
56pub const CUDA_DEVICE_INTEGRAL_ROUNDING: &str = r#"// --- floor/ceil/round of an integer argument (C's double conversion) ------------
57__host__ __device__ static inline double floor(long long x){ return (double)x; }
58__host__ __device__ static inline double floor(long x){ return (double)x; }
59__host__ __device__ static inline double floor(int x){ return (double)x; }
60__host__ __device__ static inline double ceil(long long x){ return (double)x; }
61__host__ __device__ static inline double ceil(long x){ return (double)x; }
62__host__ __device__ static inline double ceil(int x){ return (double)x; }
63__host__ __device__ static inline double round(long long x){ return (double)x; }
64__host__ __device__ static inline double round(long x){ return (double)x; }
65__host__ __device__ static inline double round(int x){ return (double)x; }
66"#;
67
68/// The device RUNTIME-ERROR PATH. A fail site in the C runtime is `fprintf(stderr, msg); exit(1);` (a
69/// non-exhaustive `Inspect`, a div-by-zero guard, a bignum parse error, an explicit `FailWith`, …). On a
70/// GPU thread none of `fprintf`/`stderr`/`exit`/`abort` exist — but the stderr diagnostic is NOT part of
71/// the ≡VM comparison (stdout + exit status are), so the fail path lowers to: DROP the stderr message
72/// (`logos_fprintf` is a no-op) and turn `exit(nonzero)`/`abort()` into a device `trap` — the kernel
73/// faults, `cudaDeviceSynchronize` returns non-success, and the host orchestrator exits nonzero, exactly
74/// mirroring the VM's `error.is_some()`. These names are routed into the LIFTED `__device__` runtime only
75/// (`fprintf`→`logos_fprintf`, `stderr`→`logos_stderr_sink`, `exit`→`logos_exit`, `abort`→`logos_abort`,
76/// `fflush`→`logos_fflush`, `stdout`→`logos_stdout_sink`), so the HOST orchestrator keeps real libc. For a program whose fail path is UNREACHABLE (e.g. an
77/// `Inspect` whose match is exhaustive at runtime) the trap is dead code that merely has to compile.
78pub const CUDA_DEVICE_FAULT_SHIM: &str = r#"// --- device runtime-error path (stderr elided; nonzero exit / abort -> trap) ----
79__device__ static int logos_stderr_sink = 0;
80__device__ static int logos_stdout_sink = 0;
81__device__ static int logos_fprintf(int sink, const char *fmt, ...) { (void)sink; (void)fmt; return 0; }
82__device__ static int logos_fflush(int sink) { (void)sink; return 0; }
83__device__ static void logos_exit(int code) { if (code != 0) { asm("trap;"); } }
84__device__ static void logos_abort(void) { asm("trap;"); }
85"#;
86
87/// The device CLOCK globals for `today`/`now`. The C runtime reads the injected fixed clock via
88/// `getenv("LOGOS_FIXED_DAYS"/"LOGOS_FIXED_NANOS")` + `time()` — neither exists on a GPU thread. Instead
89/// the HOST orchestrator (a CPU) reads that same env (or `time()`) and `cudaMemcpyToSymbol`s it into these
90/// globals BEFORE the launch (see [`CUDA_HOST_TEMPLATE`]); the device `logos_today`/`logos_now` overrides
91/// (see [`super::device_lift::device_override`]) simply return them. Non-`static` so the host symbol
92/// reference resolves. Always emitted (0 for a non-temporal program, which never reads them).
93pub const CUDA_DEVICE_CLOCK: &str = r#"// --- device clock (host injects the fixed instant before launch) ----------------
94__device__ long long logos_dev_fixed_days = 0;
95__device__ long long logos_dev_fixed_nanos = 0;
96"#;
97
98/// The CUDA tier's prologue to the stepped scheduler ([`crate::codegen_c::runtime::STEPPED_SCHEDULER`]), under
99/// which every concurrent program runs on the GPU as the host C tier schedules it — the same sources, placed
100/// where the host C carries them: device linkage, the device allocator shim, no failure escape (a runtime
101/// helper's failure traps the run; a failing statement fails its task through the core), the run's seed from
102/// a device global the host fills from `LOGOS_SEED` before the launch, and a failed run's message in a device
103/// buffer the host prints after it, with exit status 1.
104pub const CUDA_DEVICE_STEPPED_PROLOGUE: &str = r#"// --- the stepped scheduler (device) ---------------------------------------------
105#define LOGOS_FN __device__ static
106#define LOGOS_VAR __device__ static
107#define LOGOS_API __device__ static
108#define logos_alloc(n) logos_calloc(1, (n))
109#define logos_release(p) logos_free(p)
110typedef int (*logos_step_fn)(void *);
111#define LOGOS_TASK_TIER logos_step_fn step; void *frame;
112#define LOGOS_STEP_NO_ESCAPE
113__device__ unsigned long long logos_dev_seed = 0;
114#define LOGOS_RUN_SEED logos_dev_seed
115__device__ int logos_dev_failed = 0;
116__device__ char logos_dev_failure[256];
117__device__ static void logos_dev_fail(const char *message) {
118    int i = 0;
119    for (; message[i] && i < 255; i++) logos_dev_failure[i] = message[i];
120    logos_dev_failure[i] = 0;
121    logos_dev_failed = 1;
122}
123#define LOGOS_RUN_FAILS(message) logos_dev_fail(message)
124"#;
125
126/// The host's side of a concurrent program's run, each spliced at its line of [`CUDA_HOST_TEMPLATE`] (and the
127/// line dropped for a program with no tasks): the seed reader, the seed copied into the device before the
128/// launch, and a failed run reported after it.
129pub const CUDA_HOST_SEED_READER: &str = include_str!("../codegen_c/host_seed.c");
130pub const CUDA_HOST_RUN_SEED: &str =
131    "    { unsigned long long seed = logos_env_seed(0); cudaMemcpyToSymbol(logos_dev_seed, &seed, sizeof seed); }\n";
132pub const CUDA_HOST_RUN_FAILED: &str = "    { int failed = 0; cudaMemcpyFromSymbol(&failed, logos_dev_failed, sizeof failed);\n      \
133     if (failed) { char message[256]; cudaMemcpyFromSymbol(message, logos_dev_failure, sizeof message); \
134     fprintf(stderr, \"%s\\n\", message); return 1; } }\n";
135
136/// The `__global__` entry that runs the whole program on one thread, plus the host
137/// orchestrator: launch, sync (flushes device printf), and map any device fault to a
138/// nonzero exit so error behaviour matches the VM. Substituted after the lifted code.
139pub const CUDA_HOST_TEMPLATE: &str = r#"
140__global__ void logos_kernel() { (void) logos_main(0, (char **)0); }
141
142// --- host orchestrator ----------------------------------------------------------
143#include <cstdlib>
144#include <ctime>
145#include <cstring>
146/*LOGOS_HOST_SEED_READER*/
147int main(int argc, char **argv) {
148    (void)argc; (void)argv;
149    // The device runtime uses recursion + a fixed-limb bignum on the stack (the shortest-float
150    // formatter) and device malloc (string/heap builders); raise both limits above their small
151    // defaults so a whole program runs on one thread.
152    cudaDeviceSetLimit(cudaLimitStackSize, (size_t)64 * 1024);
153    cudaDeviceSetLimit(cudaLimitMallocHeapSize, (size_t)256 * 1024 * 1024);
154    // Inject the deterministic fixed clock the device `today`/`now` read, mirroring codegen_c's
155    // getenv(LOGOS_FIXED_*)-or-time() shim on the HOST, then copy it into the device globals. Always set;
156    // a non-temporal program simply never reads them.
157    { const char *e; long long v;
158      e = getenv("LOGOS_FIXED_DAYS");  v = e ? atoll(e) : (long long)(time(NULL) / 86400);
159      cudaMemcpyToSymbol(logos_dev_fixed_days, &v, sizeof(v));
160      e = getenv("LOGOS_FIXED_NANOS"); v = e ? atoll(e) : (long long)time(NULL) * 1000000000LL;
161      cudaMemcpyToSymbol(logos_dev_fixed_nanos, &v, sizeof(v)); }
162/*LOGOS_ARGV_MARSHAL*/
163/*LOGOS_HOST_RUN_SEED*/
164    logos_kernel<<<1, 1>>>();
165    cudaError_t err = cudaGetLastError();
166    if (err != cudaSuccess) { fprintf(stderr, "kernel launch: %s\n", cudaGetErrorString(err)); return 1; }
167    err = cudaDeviceSynchronize();
168    if (err != cudaSuccess) { fprintf(stderr, "kernel exec: %s\n", cudaGetErrorString(err)); return 1; }
169/*LOGOS_HOST_RUN_FAILED*/
170    return 0;
171}
172"#;
173
174/// The single canonical name of the emitted CUDA source (referenced by the Makefile).
175pub const KERNEL_FILE_NAME: &str = "logos_cuda.cu";
176
177/// Makefile glue: builds the single-file CUDA program with `nvcc` for the Ampere
178/// (`sm_86`, the RTX 3060 class) target. `{KERNEL_FILE}` is substituted with the
179/// emitted source name.
180pub const CUDA_MAKEFILE_TEMPLATE: &str = r#"# Auto-generated by LOGOS codegen_cuda.
181# Builds the single-file CUDA program with nvcc. Override ARCH for other GPUs.
182ARCH ?= sm_86
183NVCC ?= nvcc
184logos_cuda: {KERNEL_FILE}
185	$(NVCC) -O2 -arch=$(ARCH) -fmad=false $< -o $@
186.PHONY: clean
187clean:
188	rm -f logos_cuda
189"#;