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 std::sync::Arc;
5use cudarc::driver::{CudaContext, CudaStream};
6use cudarc::cublaslt::{CudaBlasLT, Matmul, MatmulConfig};
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, sys::CUmemPool_attribute::CU_MEMPOOL_ATTR_REUSE_ALLOW_OPPORTUNISTIC,
141 &off as *const _ as *mut std::os::raw::c_void);
142 let on: std::os::raw::c_int = 1;
143 let _ = sys::cuMemPoolSetAttribute(
144 pool, sys::CUmemPool_attribute::CU_MEMPOOL_ATTR_REUSE_ALLOW_INTERNAL_DEPENDENCIES,
145 &on as *const _ as *mut std::os::raw::c_void);
146 let thresh: u64 = u64::MAX;
147 let _ = sys::cuMemPoolSetAttribute(
148 pool, sys::CUmemPool_attribute::CU_MEMPOOL_ATTR_RELEASE_THRESHOLD,
149 &thresh as *const _ as *mut std::os::raw::c_void);
150 }
151 }
152 let blas = CudaBlasLT::new(stream.clone())?;
153 Ok(Self { ctx, stream, blas })
154 }
155
156 /// GPU linear y = x @ W^T using cuBLASLt (f32), matching `cpu_linear` exactly.
157 ///
158 /// Layout reasoning (cuBLASLt is column-major):
159 /// We want y[m,out] row-major = y^T[out,m] column-major. Treat:
160 /// - x[m,in] row-major == x^T[in,m] col-major (an in×m col-major matrix)
161 /// - w[out,in] row-major == w^T[in,out] col-major (an in×out col-major matrix)
162 /// Compute C[out,m] col-major = W_colmajor(out×in) * X_colmajor(in×m)
163 /// => set A = w (interpreted col-major as in×out, so transa to get out×in),
164 /// B = x (col-major in×m), C = y (col-major out×m == y[m,out] row-major).
165 /// cfg: m_=out, n_=m_tokens, k=in. A is in×out (lda=in, transa=true -> out×in),
166 /// B is in×m (ldb=in), C is out×m (ldc=out).
167 pub fn linear_f32(
168 &self,
169 x: &cudarc::driver::CudaSlice<f32>,
170 w: &cudarc::driver::CudaSlice<f32>,
171 m_tokens: usize,
172 in_f: usize,
173 out_f: usize,
174 ) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
175 let mut c = self.stream.alloc_zeros::<f32>(m_tokens * out_f)?;
176 let cfg = MatmulConfig {
177 transa: true, // A stored in×out col-major -> use as out×in
178 transb: false,
179 transc: false,
180 m: out_f as u64,
181 n: m_tokens as u64,
182 k: in_f as u64,
183 alpha: 1.0,
184 lda: in_f as i64, // A leading dim = in (col-major in×out)
185 ldb: in_f as i64, // B leading dim = in (col-major in×m)
186 beta: 0.0,
187 ldc: out_f as i64, // C leading dim = out (col-major out×m)
188 stride_a: None, stride_b: None, stride_c: None, stride_bias: None,
189 batch_size: None,
190 };
191 unsafe { self.blas.matmul(cfg, w, x, &mut c, None, None)?; }
192 let y = self.stream.clone_dtoh(&c)?;
193 self.stream.synchronize()?;
194 Ok(y)
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn cpu_linear_tiny() {
204 // m=1, in=2, out=2; x=[1,2], W=[[1,0],[0,1]] (identity) -> y=[1,2]
205 let x = vec![1.0, 2.0];
206 let w = vec![1.0, 0.0, 0.0, 1.0]; // row0=[1,0], row1=[0,1]
207 let y = cpu_linear(&x, &w, 1, 2, 2);
208 assert_eq!(y, vec![1.0, 2.0]);
209 // W=[[1,1],[2,0]] -> y[0]=1*1+2*1=3, y[1]=1*2+2*0=2
210 let w2 = vec![1.0, 1.0, 2.0, 0.0];
211 let y2 = cpu_linear(&x, &w2, 1, 2, 2);
212 assert_eq!(y2, vec![3.0, 2.0]);
213 }
214}