Skip to main content

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}