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, sys as cu};
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].
15#[allow(clippy::type_complexity)] // allow: one-shot composite type; naming it would hide the shape that matters at the call site
16pub fn cpu_linear(x: &[f32], w: &[f32], m: usize, in_f: usize, out_f: usize) -> Vec<f32> {
17    assert_eq!(x.len(), m * in_f);
18    assert_eq!(w.len(), out_f * in_f);
19    let mut y = vec![0f32; m * out_f];
20    for t in 0..m {
21        for o in 0..out_f {
22            let mut acc = 0f32;
23            let xr = &x[t * in_f..t * in_f + in_f];
24            let wr = &w[o * in_f..o * in_f + in_f];
25            for i in 0..in_f {
26                acc += xr[i] * wr[i];
27            }
28            y[t * out_f + o] = acc;
29        }
30    }
31    y
32}
33
34/// GPU runtime handle: a context + stream + cuBLASLt.
35pub struct Gpu {
36    pub ctx: Arc<CudaContext>,
37    /// The MAIN compute stream. PRIVATE since M1 increment 2: every launch site reads
38    /// `stream()` so the pp2 per-stage stream override (below) is a single seam. Naked
39    /// paths (no override pushed) get exactly this stream back — behavior unchanged.
40    stream: Arc<CudaStream>,
41    blas: Arc<CudaBlasLT>,
42    /// TOKEN-PIPELINE phase streams (step37 chain): two extra stream/cuBLASLt pairs so
43    /// alternate tokens' host-issue rides disjoint streams. Lazily built (first
44    /// enter_main under an active decode phase); None everywhere else — zero cost.
45    #[allow(clippy::type_complexity)]
46    // allow: one-shot composite type; naming it would hide the shape that matters at the call site
47    phase: std::sync::Mutex<Option<[(Arc<CudaStream>, Arc<CudaBlasLT>); 2]>>,
48}
49
50// ---------------------------------------------------------------------------------------
51// M1-PP2 increment 2: AMBIENT STREAM OVERRIDE (per-stage CUDA streams).
52//
53// The engine's entire launch surface reads `Gpu::stream()`. A pipeline stage redirects it
54// by pushing a per-stage stream onto this thread-local stack for the stage's host-issue
55// scope (RAII guard pops it). Decode is single-threaded host-issue, so thread-local is the
56// natural scope; cost when the stack is empty (every naked path) is one TLS lookup + a
57// branch + one Arc clone per launch — nanoseconds against a kernel launch.
58//
59// SAFETY CONTRACT (the multi-stream law): cudarc's per-arg event tracking stays DISABLED
60// (see memra-engine Engine::new) — cross-stream ordering is the OVERRIDER's job, via
61// explicit CudaEvents (pp2's boundary TX/RX choreography). The async mem pool is configured
62// below with opportunistic reuse OFF + internal dependencies ON, so a block freed on stream
63// A and re-allocated on stream B carries a driver-inserted dependency — alloc reuse cannot
64// race across stages. Buffers that one stream writes and another reads must be evented by
65// the caller; pp2 routes ALL cross-stage bytes through its persistent boundary slots.
66// ---------------------------------------------------------------------------------------
67struct StreamBinding {
68    stream: Arc<CudaStream>,
69    blas: Arc<CudaBlasLT>,
70}
71
72thread_local! {
73    static STREAM_OVERRIDE: std::cell::RefCell<Vec<StreamBinding>> =
74        const { std::cell::RefCell::new(Vec::new()) };
75}
76
77thread_local! {
78    static DECODE_PHASE: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
79}
80
81thread_local! {
82    /// RANK0 STREAM MERGE (step37): while set to a (ctx, stream, blas) binding, enter_main
83    /// on the MATCHING context binds THIS stream instead of the gpu's own main stream —
84    /// the same-device rank's work then rides the model engine's stream and every
85    /// e<->rank0 event hop becomes same-stream program order.
86    #[allow(clippy::type_complexity)] // allow: one-shot composite type; naming it would hide the shape that matters at the call site
87    static RANK0_REDIRECT: std::cell::RefCell<Option<(usize, Arc<CudaStream>, Arc<CudaBlasLT>)>> =
88        const { std::cell::RefCell::new(None) };
89}
90
91/// Install/clear the rank0 redirect (ctx ordinal + stream/blas of the model engine).
92pub fn set_rank0_redirect(binding: Option<(usize, Arc<CudaStream>, Arc<CudaBlasLT>)>) {
93    RANK0_REDIRECT.with(|c| *c.borrow_mut() = binding);
94}
95
96/// RAII scope for the rank0 redirect: clears on drop (panic-safe).
97pub struct Rank0RedirectGuard(());
98pub fn rank0_redirect_scope(
99    ordinal: usize,
100    stream: Arc<CudaStream>,
101    blas: Arc<CudaBlasLT>,
102) -> Rank0RedirectGuard {
103    set_rank0_redirect(Some((ordinal, stream, blas)));
104    Rank0RedirectGuard(())
105}
106impl Drop for Rank0RedirectGuard {
107    fn drop(&mut self) {
108        set_rank0_redirect(None);
109    }
110}
111fn rank0_redirect_for(ordinal: usize) -> Option<(Arc<CudaStream>, Arc<CudaBlasLT>)> {
112    RANK0_REDIRECT.with(|c| {
113        c.borrow()
114            .as_ref()
115            .filter(|(o, ..)| *o == ordinal)
116            .map(|(_, s, b)| (s.clone(), b.clone()))
117    })
118}
119
120/// TOKEN-PIPELINE phase (step37 chain): while `Some(p)`, `enter_main` binds each gpu's
121/// phase-p stream instead of its main stream, so alternate tokens' rank-local work rides
122/// disjoint streams. Cross-stream ordering is the SETTER's job (the multi-stream law
123/// above): the chain wires per-layer KV events between phases.
124pub fn set_decode_phase(p: Option<usize>) {
125    DECODE_PHASE.with(|c| c.set(p));
126}
127pub fn decode_phase() -> Option<usize> {
128    DECODE_PHASE.with(|c| c.get())
129}
130
131/// RAII scope: while alive, `Gpu::stream()` on THIS thread returns the pushed stream.
132/// Nest freely (stack). Popping on Drop keeps panic paths consistent.
133pub struct StreamOverride(());
134
135/// A rank-local CUDA scope nested inside another engine's PP stage scope.
136///
137/// CUDA contexts are a per-thread stack. The stream override alone is not enough: every rank-local
138/// allocation and launch must make that rank's context current, then restore the caller's context
139/// before the PP owner resumes issuing work.
140pub struct GpuMainOverride {
141    stream: Option<StreamOverride>,
142    expected_ctx: cu::CUcontext,
143}
144
145/// Push a matched stream/cuBLASLt binding for the current thread until the guard drops.
146pub fn push_stream_override(stream: Arc<CudaStream>, blas: Arc<CudaBlasLT>) -> StreamOverride {
147    STREAM_OVERRIDE.with(|o| o.borrow_mut().push(StreamBinding { stream, blas }));
148    StreamOverride(())
149}
150
151impl Drop for StreamOverride {
152    fn drop(&mut self) {
153        STREAM_OVERRIDE.with(|o| {
154            o.borrow_mut().pop();
155        });
156    }
157}
158
159impl Drop for GpuMainOverride {
160    fn drop(&mut self) {
161        // Restore the ambient PP stream/cuBLAS binding before restoring its CUDA context.
162        drop(self.stream.take());
163        // cudarc binds the context of every stream operation with cuCtxSetCurrent. NVIDIA defines
164        // that call as replacing the top entry of an existing context stack, so a cross-context
165        // operation may replace the slot that enter_main pushed. Put the owning context back in
166        // that slot before popping it; the untouched PP context beneath it then becomes current.
167        let set_rc = unsafe { cu::cuCtxSetCurrent(self.expected_ctx) };
168        let mut popped = std::ptr::null_mut();
169        let pop_rc = unsafe { cu::cuCtxPopCurrent_v2(&mut popped) };
170        if set_rc != cu::CUresult::CUDA_SUCCESS
171            || pop_rc != cu::CUresult::CUDA_SUCCESS
172            || popped != self.expected_ctx
173        {
174            let message = format!(
175                "rank-local CUDA context restore failed: set_rc={set_rc:?} pop_rc={pop_rc:?} \
176                 expected={:?} popped={popped:?}",
177                self.expected_ctx,
178            );
179            if std::thread::panicking() {
180                eprintln!("{message}");
181            } else {
182                panic!("{message}");
183            }
184        }
185    }
186}
187
188impl Gpu {
189    /// The stream every engine op launches on: the thread's override if one is pushed
190    /// (pp2 stage scopes), else the main compute stream. By-value Arc so callers hold a
191    /// stable handle across the call regardless of later pushes/pops.
192    #[inline]
193    pub fn stream(&self) -> Arc<CudaStream> {
194        STREAM_OVERRIDE
195            .with(|o| o.borrow().last().map(|binding| binding.stream.clone()))
196            .unwrap_or_else(|| self.stream.clone())
197    }
198
199    /// The cuBLASLt handle bound to the same stream returned by `stream()`.
200    #[inline]
201    pub fn blas(&self) -> Arc<CudaBlasLT> {
202        STREAM_OVERRIDE
203            .with(|o| o.borrow().last().map(|binding| binding.blas.clone()))
204            .unwrap_or_else(|| self.blas.clone())
205    }
206
207    /// The main compute stream, override-blind (graph capture pins itself here; the pp2
208    /// runtime uses it to fence stage streams against load-time state).
209    #[inline]
210    pub fn main_stream(&self) -> &Arc<CudaStream> {
211        &self.stream
212    }
213
214    /// Enter this GPU's own context and matched main stream/cuBLASLt binding, even when the
215    /// calling thread currently carries a pipeline-stage stream override for another engine.
216    ///
217    /// Multi-context TP/EP helpers invoke rank-local engines from inside a PP stage scope. Without
218    /// this nested binding, `stream()` would inherit the PP owner's stream and launch rank-local
219    /// pointers through the wrong CUDA context.
220    pub fn enter_main(&self) -> Result<GpuMainOverride, Box<dyn std::error::Error>> {
221        let rc = unsafe { cu::cuCtxPushCurrent_v2(self.ctx.cu_ctx()) };
222        if rc != cu::CUresult::CUDA_SUCCESS {
223            return Err(format!("rank-local CUDA context push failed: {rc:?}").into());
224        }
225        // Token-pipeline phase / rank0 redirect: bind the override stream when armed.
226        let (stream, blas) = if let Some(pair) = rank0_redirect_for(self.ctx.ordinal()) {
227            pair
228        } else {
229            match decode_phase() {
230                Some(p) => self.phase_pair(p)?,
231                None => (self.stream.clone(), self.blas.clone()),
232            }
233        };
234        Ok(GpuMainOverride {
235            stream: Some(push_stream_override(stream, blas)),
236            expected_ctx: self.ctx.cu_ctx(),
237        })
238    }
239
240    /// The phase-`p` stream/cuBLASLt pair, lazily created. Caller must have this gpu's
241    /// context current (enter_main does; external callers use enter_main first).
242    pub fn phase_pair(
243        &self,
244        p: usize,
245    ) -> Result<(Arc<CudaStream>, Arc<CudaBlasLT>), Box<dyn std::error::Error>> {
246        let mut guard = self
247            .phase
248            .lock()
249            .map_err(|_| "gpu phase lock is poisoned")?;
250        if guard.is_none() {
251            let s0 = self.ctx.new_stream()?;
252            let s1 = self.ctx.new_stream()?;
253            let b0 = Arc::new(CudaBlasLT::new(s0.clone())?);
254            let b1 = Arc::new(CudaBlasLT::new(s1.clone())?);
255            *guard = Some([(s0, b0), (s1, b1)]);
256        }
257        let arr = guard.as_ref().expect("armed above");
258        Ok((arr[p & 1].0.clone(), arr[p & 1].1.clone()))
259    }
260}
261
262impl Gpu {
263    pub fn new(ordinal: usize) -> Result<Self, Box<dyn std::error::Error>> {
264        let ctx = CudaContext::new(ordinal)?;
265        // A NON-BLOCKING created stream (NOT the legacy NULL/default stream): the NULL stream cannot be
266        // CUDA-graph captured (cuStreamBeginCapture -> CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED). All
267        // engine kernels launch on this stream, so making it capturable enables the decode CUDA-graph
268        // capture/replay path (CUDA-GRAPH-PLAN Phase 3). Behaviorally identical for the existing single-
269        // stream paths (just a real stream id instead of NULL).
270        let stream = ctx.new_stream()?;
271        // DETERMINISM FIX (decode bit-stability): the default stream-ordered async memory pool
272        // (cuMemAllocAsync/cuMemFreeAsync, used by cudarc's `alloc`/`alloc_zeros`) reuses freed
273        // blocks OPPORTUNISTICALLY — it hands a freed block to the next alloc as soon as the HOST
274        // observes the GPU has passed the free, WITHOUT inserting a stream dependency. Whether that
275        // reuse happens is a function of how far the async GPU has progressed at host-alloc time, so
276        // it is timing-dependent. Our decode path launches kernels through the raw launch builder
277        // (no cudarc read/write event tracking on the args), so the per-step scratch buffers are
278        // freed-and-reused inside the async window: under opportunistic reuse a buffer can be
279        // recycled and overwritten by a later kernel while an earlier kernel that still references
280        // the same physical block is in flight — a WAR/RAW hazard that produces RUN-TO-RUN
281        // nondeterministic results (two identical prompt primes diverge; per-step sync hides it).
282        // Disable opportunistic reuse and require the pool to insert INTERNAL stream dependencies
283        // before reusing a freed block. This makes every reuse stream-ordered and deterministic with
284        // negligible cost (one-time pool config; the dependency is the same ordering the single
285        // stream already implies, just made explicit). The release threshold is set to MAX so freed
286        // blocks stay in the pool (no give-back to the OS between steps -> stable reuse, no per-step
287        // cuMemMap churn).
288        // (A/B-verified perf-neutral: decode ~80 tok/s with this on, off, or absent — full-power noise
289        // band. The real determinism fix is the SSM ping-pong in decode.rs; this is cheap belt-and-
290        // suspenders against any other per-step async-pool reuse hazard.)
291        unsafe {
292            use cudarc::driver::sys;
293            let dev = ctx.cu_device();
294            let mut pool: sys::CUmemoryPool = std::ptr::null_mut();
295            if sys::cuDeviceGetDefaultMemPool(&mut pool, dev) == sys::CUresult::CUDA_SUCCESS
296                && !pool.is_null()
297            {
298                let off: std::os::raw::c_int = 0;
299                let _ = sys::cuMemPoolSetAttribute(
300                    pool,
301                    sys::CUmemPool_attribute::CU_MEMPOOL_ATTR_REUSE_ALLOW_OPPORTUNISTIC,
302                    &off as *const _ as *mut std::os::raw::c_void,
303                );
304                let on: std::os::raw::c_int = 1;
305                let _ = sys::cuMemPoolSetAttribute(
306                    pool,
307                    sys::CUmemPool_attribute::CU_MEMPOOL_ATTR_REUSE_ALLOW_INTERNAL_DEPENDENCIES,
308                    &on as *const _ as *mut std::os::raw::c_void,
309                );
310                let thresh: u64 = u64::MAX;
311                let _ = sys::cuMemPoolSetAttribute(
312                    pool,
313                    sys::CUmemPool_attribute::CU_MEMPOOL_ATTR_RELEASE_THRESHOLD,
314                    &thresh as *const _ as *mut std::os::raw::c_void,
315                );
316            }
317        }
318        let blas = Arc::new(CudaBlasLT::new(stream.clone())?);
319        Ok(Self {
320            ctx,
321            stream,
322            blas,
323            phase: std::sync::Mutex::new(None),
324        })
325    }
326
327    /// GPU linear y = x @ W^T using cuBLASLt (f32), matching `cpu_linear` exactly.
328    ///
329    /// Layout reasoning (cuBLASLt is column-major):
330    /// We want y[m,out] row-major = y^T[out,m] column-major. Treat:
331    ///   - x[m,in] row-major == x^T[in,m] col-major  (an in×m col-major matrix)
332    ///   - w[out,in] row-major == w^T[in,out] col-major (an in×out col-major matrix)
333    ///     Compute C[out,m] col-major = W_colmajor(out×in) * X_colmajor(in×m)
334    ///     => set A = w (interpreted col-major as in×out, so transa to get out×in),
335    ///     B = x (col-major in×m), C = y (col-major out×m == y[m,out] row-major).
336    ///     cfg: m_=out, n_=m_tokens, k=in. A is in×out (lda=in, transa=true -> out×in),
337    ///     B is in×m (ldb=in), C is out×m (ldc=out).
338    pub fn linear_f32(
339        &self,
340        x: &cudarc::driver::CudaSlice<f32>,
341        w: &cudarc::driver::CudaSlice<f32>,
342        m_tokens: usize,
343        in_f: usize,
344        out_f: usize,
345    ) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
346        let stream = self.stream();
347        let mut c = stream.alloc_zeros::<f32>(m_tokens * out_f)?;
348        let cfg = MatmulConfig {
349            transa: true, // A stored in×out col-major -> use as out×in
350            transb: false,
351            transc: false,
352            m: out_f as u64,
353            n: m_tokens as u64,
354            k: in_f as u64,
355            alpha: 1.0,
356            lda: in_f as i64, // A leading dim = in (col-major in×out)
357            ldb: in_f as i64, // B leading dim = in (col-major in×m)
358            beta: 0.0,
359            ldc: out_f as i64, // C leading dim = out (col-major out×m)
360            stride_a: None,
361            stride_b: None,
362            stride_c: None,
363            stride_bias: None,
364            batch_size: None,
365        };
366        let blas = self.blas();
367        unsafe {
368            blas.matmul(cfg, w, x, &mut c, None, None)?;
369        }
370        let y = stream.clone_dtoh(&c)?;
371        stream.synchronize()?;
372        Ok(y)
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn cpu_linear_tiny() {
382        // m=1, in=2, out=2; x=[1,2], W=[[1,0],[0,1]] (identity) -> y=[1,2]
383        let x = vec![1.0, 2.0];
384        let w = vec![1.0, 0.0, 0.0, 1.0]; // row0=[1,0], row1=[0,1]
385        let y = cpu_linear(&x, &w, 1, 2, 2);
386        assert_eq!(y, vec![1.0, 2.0]);
387        // W=[[1,1],[2,0]] -> y[0]=1*1+2*1=3, y[1]=1*2+2*0=2
388        let w2 = vec![1.0, 1.0, 2.0, 0.0];
389        let y2 = cpu_linear(&x, &w2, 1, 2, 2);
390        assert_eq!(y2, vec![3.0, 2.0]);
391    }
392}