memra_runtime/lib.rs
1//! memra inference runtime. Correctness-first: every GPU op is validated against a
2//! CPU reference before any sm_120 fast-path replaces it.
3
4use cudarc::cublaslt::{CudaBlasLT, Matmul, MatmulConfig};
5use cudarc::driver::{CudaContext, CudaStream};
6use std::sync::Arc;
7
8pub use memra_gguf;
9
10/// CPU reference matmul for a linear layer y = x @ W^T.
11/// Conventions (ggml/GGUF): a weight tensor with ne=[in, out] is stored row-major as
12/// `out` rows of `in` contiguous elements — i.e. W[o*in + i]. A linear layer computes
13/// y[o] = sum_i x[i] * W[o*in + i], for each of `out` outputs. Batched over `m` tokens:
14/// x: [m, in] row-major (x[t*in + i]); w: [out, in] row-major (w[o*in + i]); y: [m, out].
15pub fn cpu_linear(x: &[f32], w: &[f32], m: usize, in_f: usize, out_f: usize) -> Vec<f32> {
16 assert_eq!(x.len(), m * in_f);
17 assert_eq!(w.len(), out_f * in_f);
18 let mut y = vec![0f32; m * out_f];
19 for t in 0..m {
20 for o in 0..out_f {
21 let mut acc = 0f32;
22 let xr = &x[t * in_f..t * in_f + in_f];
23 let wr = &w[o * in_f..o * in_f + in_f];
24 for i in 0..in_f {
25 acc += xr[i] * wr[i];
26 }
27 y[t * out_f + o] = acc;
28 }
29 }
30 y
31}
32
33/// GPU runtime handle: a context + stream + cuBLASLt.
34pub struct Gpu {
35 pub ctx: Arc<CudaContext>,
36 /// The MAIN compute stream. PRIVATE since M1 increment 2: every launch site reads
37 /// `stream()` so the pp2 per-stage stream override (below) is a single seam. Naked
38 /// paths (no override pushed) get exactly this stream back — behavior unchanged.
39 stream: Arc<CudaStream>,
40 pub blas: CudaBlasLT,
41}
42
43// ---------------------------------------------------------------------------------------
44// M1-PP2 increment 2: AMBIENT STREAM OVERRIDE (per-stage CUDA streams).
45//
46// The engine's entire launch surface reads `Gpu::stream()`. A pipeline stage redirects it
47// by pushing a per-stage stream onto this thread-local stack for the stage's host-issue
48// scope (RAII guard pops it). Decode is single-threaded host-issue, so thread-local is the
49// natural scope; cost when the stack is empty (every naked path) is one TLS lookup + a
50// branch + one Arc clone per launch — nanoseconds against a kernel launch.
51//
52// SAFETY CONTRACT (the multi-stream law): cudarc's per-arg event tracking stays DISABLED
53// (see memra-engine Engine::new) — cross-stream ordering is the OVERRIDER's job, via
54// explicit CudaEvents (pp2's boundary TX/RX choreography). The async mem pool is configured
55// below with opportunistic reuse OFF + internal dependencies ON, so a block freed on stream
56// A and re-allocated on stream B carries a driver-inserted dependency — alloc reuse cannot
57// race across stages. Buffers that one stream writes and another reads must be evented by
58// the caller; pp2 routes ALL cross-stage bytes through its persistent boundary slots.
59// ---------------------------------------------------------------------------------------
60thread_local! {
61 static STREAM_OVERRIDE: std::cell::RefCell<Vec<Arc<CudaStream>>> =
62 const { std::cell::RefCell::new(Vec::new()) };
63}
64
65/// RAII scope: while alive, `Gpu::stream()` on THIS thread returns the pushed stream.
66/// Nest freely (stack). Popping on Drop keeps panic paths consistent.
67pub struct StreamOverride(());
68
69/// Push `s` as the ambient stream for the current thread until the guard drops.
70pub fn push_stream_override(s: Arc<CudaStream>) -> StreamOverride {
71 STREAM_OVERRIDE.with(|o| o.borrow_mut().push(s));
72 StreamOverride(())
73}
74
75impl Drop for StreamOverride {
76 fn drop(&mut self) {
77 STREAM_OVERRIDE.with(|o| {
78 o.borrow_mut().pop();
79 });
80 }
81}
82
83impl Gpu {
84 /// The stream every engine op launches on: the thread's override if one is pushed
85 /// (pp2 stage scopes), else the main compute stream. By-value Arc so callers hold a
86 /// stable handle across the call regardless of later pushes/pops.
87 #[inline]
88 pub fn stream(&self) -> Arc<CudaStream> {
89 STREAM_OVERRIDE
90 .with(|o| o.borrow().last().cloned())
91 .unwrap_or_else(|| self.stream.clone())
92 }
93
94 /// The main compute stream, override-blind (graph capture pins itself here; the pp2
95 /// runtime uses it to fence stage streams against load-time state).
96 #[inline]
97 pub fn main_stream(&self) -> &Arc<CudaStream> {
98 &self.stream
99 }
100}
101
102impl Gpu {
103 pub fn new(ordinal: usize) -> Result<Self, Box<dyn std::error::Error>> {
104 let ctx = CudaContext::new(ordinal)?;
105 // A NON-BLOCKING created stream (NOT the legacy NULL/default stream): the NULL stream cannot be
106 // CUDA-graph captured (cuStreamBeginCapture -> CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED). All
107 // engine kernels launch on this stream, so making it capturable enables the decode CUDA-graph
108 // capture/replay path (CUDA-GRAPH-PLAN Phase 3). Behaviorally identical for the existing single-
109 // stream paths (just a real stream id instead of NULL).
110 let stream = ctx.new_stream()?;
111 // DETERMINISM FIX (decode bit-stability): the default stream-ordered async memory pool
112 // (cuMemAllocAsync/cuMemFreeAsync, used by cudarc's `alloc`/`alloc_zeros`) reuses freed
113 // blocks OPPORTUNISTICALLY — it hands a freed block to the next alloc as soon as the HOST
114 // observes the GPU has passed the free, WITHOUT inserting a stream dependency. Whether that
115 // reuse happens is a function of how far the async GPU has progressed at host-alloc time, so
116 // it is timing-dependent. Our decode path launches kernels through the raw launch builder
117 // (no cudarc read/write event tracking on the args), so the per-step scratch buffers are
118 // freed-and-reused inside the async window: under opportunistic reuse a buffer can be
119 // recycled and overwritten by a later kernel while an earlier kernel that still references
120 // the same physical block is in flight — a WAR/RAW hazard that produces RUN-TO-RUN
121 // nondeterministic results (two identical prompt primes diverge; per-step sync hides it).
122 // Disable opportunistic reuse and require the pool to insert INTERNAL stream dependencies
123 // before reusing a freed block. This makes every reuse stream-ordered and deterministic with
124 // negligible cost (one-time pool config; the dependency is the same ordering the single
125 // stream already implies, just made explicit). The release threshold is set to MAX so freed
126 // blocks stay in the pool (no give-back to the OS between steps -> stable reuse, no per-step
127 // cuMemMap churn).
128 // (A/B-verified perf-neutral: decode ~80 tok/s with this on, off, or absent — full-power noise
129 // band. The real determinism fix is the SSM ping-pong in decode.rs; this is cheap belt-and-
130 // suspenders against any other per-step async-pool reuse hazard.)
131 unsafe {
132 use cudarc::driver::sys;
133 let dev = ctx.cu_device();
134 let mut pool: sys::CUmemoryPool = std::ptr::null_mut();
135 if sys::cuDeviceGetDefaultMemPool(&mut pool, dev) == sys::CUresult::CUDA_SUCCESS
136 && !pool.is_null()
137 {
138 let off: std::os::raw::c_int = 0;
139 let _ = sys::cuMemPoolSetAttribute(
140 pool,
141 sys::CUmemPool_attribute::CU_MEMPOOL_ATTR_REUSE_ALLOW_OPPORTUNISTIC,
142 &off as *const _ as *mut std::os::raw::c_void,
143 );
144 let on: std::os::raw::c_int = 1;
145 let _ = sys::cuMemPoolSetAttribute(
146 pool,
147 sys::CUmemPool_attribute::CU_MEMPOOL_ATTR_REUSE_ALLOW_INTERNAL_DEPENDENCIES,
148 &on as *const _ as *mut std::os::raw::c_void,
149 );
150 let thresh: u64 = u64::MAX;
151 let _ = sys::cuMemPoolSetAttribute(
152 pool,
153 sys::CUmemPool_attribute::CU_MEMPOOL_ATTR_RELEASE_THRESHOLD,
154 &thresh as *const _ as *mut std::os::raw::c_void,
155 );
156 }
157 }
158 let blas = CudaBlasLT::new(stream.clone())?;
159 Ok(Self { ctx, stream, blas })
160 }
161
162 /// GPU linear y = x @ W^T using cuBLASLt (f32), matching `cpu_linear` exactly.
163 ///
164 /// Layout reasoning (cuBLASLt is column-major):
165 /// We want y[m,out] row-major = y^T[out,m] column-major. Treat:
166 /// - x[m,in] row-major == x^T[in,m] col-major (an in×m col-major matrix)
167 /// - w[out,in] row-major == w^T[in,out] col-major (an in×out col-major matrix)
168 /// Compute C[out,m] col-major = W_colmajor(out×in) * X_colmajor(in×m)
169 /// => set A = w (interpreted col-major as in×out, so transa to get out×in),
170 /// B = x (col-major in×m), C = y (col-major out×m == y[m,out] row-major).
171 /// cfg: m_=out, n_=m_tokens, k=in. A is in×out (lda=in, transa=true -> out×in),
172 /// B is in×m (ldb=in), C is out×m (ldc=out).
173 pub fn linear_f32(
174 &self,
175 x: &cudarc::driver::CudaSlice<f32>,
176 w: &cudarc::driver::CudaSlice<f32>,
177 m_tokens: usize,
178 in_f: usize,
179 out_f: usize,
180 ) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
181 let mut c = self.stream.alloc_zeros::<f32>(m_tokens * out_f)?;
182 let cfg = MatmulConfig {
183 transa: true, // A stored in×out col-major -> use as out×in
184 transb: false,
185 transc: false,
186 m: out_f as u64,
187 n: m_tokens as u64,
188 k: in_f as u64,
189 alpha: 1.0,
190 lda: in_f as i64, // A leading dim = in (col-major in×out)
191 ldb: in_f as i64, // B leading dim = in (col-major in×m)
192 beta: 0.0,
193 ldc: out_f as i64, // C leading dim = out (col-major out×m)
194 stride_a: None,
195 stride_b: None,
196 stride_c: None,
197 stride_bias: None,
198 batch_size: None,
199 };
200 unsafe {
201 self.blas.matmul(cfg, w, x, &mut c, None, None)?;
202 }
203 let y = self.stream.clone_dtoh(&c)?;
204 self.stream.synchronize()?;
205 Ok(y)
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn cpu_linear_tiny() {
215 // m=1, in=2, out=2; x=[1,2], W=[[1,0],[0,1]] (identity) -> y=[1,2]
216 let x = vec![1.0, 2.0];
217 let w = vec![1.0, 0.0, 0.0, 1.0]; // row0=[1,0], row1=[0,1]
218 let y = cpu_linear(&x, &w, 1, 2, 2);
219 assert_eq!(y, vec![1.0, 2.0]);
220 // W=[[1,1],[2,0]] -> y[0]=1*1+2*1=3, y[1]=1*2+2*0=2
221 let w2 = vec![1.0, 1.0, 2.0, 0.0];
222 let y2 = cpu_linear(&x, &w2, 1, 2, 2);
223 assert_eq!(y2, vec![3.0, 2.0]);
224 }
225}