Skip to main content

memra_engine/
round_stream.rs

1//! ROUND-STREAM: the model-generic device machinery for pre-issued M-round speculative
2//! bursts with zero per-round host readbacks (extracted from the qwen spec loop 2026-07-12
3//! so the gemma/next-model loops reuse it instead of re-growing their own).
4//!
5//! The pieces that live here are pure device-buffer plumbing — every model-specific thing
6//! (the draft-chain graph, the verify trunk, commit semantics) stays in the caller:
7//!   - `StreamBufs`: the per-burst device buffers (verify tokens, break, pending, ring,
8//!     accept counters, device position) sized from (k, m_rounds).
9//!   - `kv_len_ptr_table`: the per-layer `kvl.len_d` pointer table the device rollback
10//!     kernel walks (pointers are stable for the cache's lifetime — cache.rs note).
11//!   - `drain_ring`: the one host sync per M rounds — reads the ring and appends tokens.
12//!
13//! The kernels these feed (`spec_accept_greedy_dc`, `spec_ring_commit`,
14//! `spec_rollback_stream`, `spec_seed_gather`, `spec_assemble_verify`) are already
15//! model-generic in lib.rs; this module is the buffer/lifecycle half.
16
17use crate::cache::Cache;
18use crate::Engine;
19use cudarc::driver::CudaSlice;
20
21pub struct StreamBufs {
22    /// assembled verify tokens [k+1]
23    pub vtok_d: CudaSlice<u32>,
24    /// p-min break markers [2]
25    pub brk_d: CudaSlice<u32>,
26    /// pending (bonus-fold) token [1]
27    pub pend_d: CudaSlice<u32>,
28    /// last verify prediction [1]
29    pub last_pred_d: CudaSlice<u32>,
30    /// device position counter (rope/append base)
31    pub pos_ctr: CudaSlice<i32>,
32    /// round-start position (rollback anchor)
33    pub pos_start_d: CudaSlice<i32>,
34    /// committed-token ring [m*(k+1)+1] (slot 0 = count)
35    pub ring_d: CudaSlice<u32>,
36    /// device accept counters [2]
37    pub acc_d: CudaSlice<u32>,
38    pub m_rounds: usize,
39    pub k: usize,
40}
41
42impl StreamBufs {
43    pub fn new(e: &Engine, k: usize, m_rounds: usize) -> Result<Self, Box<dyn std::error::Error>> {
44        Ok(StreamBufs {
45            vtok_d: e.alloc_u32_zeroed(k + 1)?,
46            brk_d: e.alloc_u32_zeroed(2)?,
47            pend_d: e.alloc_u32_zeroed(1)?,
48            last_pred_d: e.alloc_u32_zeroed(1)?,
49            pos_ctr: e.htod_i32(&[0])?,
50            pos_start_d: e.htod_i32(&[0])?,
51            ring_d: e.alloc_u32_zeroed(m_rounds * (k + 1) + 1)?,
52            acc_d: e.alloc_u32_zeroed(2)?,
53            m_rounds,
54            k,
55        })
56    }
57
58    /// Drain the ring after a burst (THE one host sync per M rounds): returns the committed
59    /// tokens in order and resets nothing — the caller zeroes the ring count for the next
60    /// burst via `e.set_u32_one(&mut ring_d, 0)`.
61    pub fn drain_ring(&self, e: &Engine) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
62        let h = e.dtoh_u32(&self.ring_d)?;
63        let cnt = (h[0] as usize).min(self.ring_d.len() - 1);
64        Ok(h[1..1 + cnt].to_vec())
65    }
66}
67
68/// Per-layer `kvl.len_d` device-pointer table (+ the position counter appended when
69/// `pos_ctr` is given) for `spec_rollback_stream`. Pointers are stable for the cache's
70/// lifetime; 0 marks layers without KV (linear-attention / KV-shared).
71pub fn kv_len_ptr_table(
72    e: &Engine,
73    cache: &Cache,
74    pos_ctr: Option<&CudaSlice<i32>>,
75) -> Result<CudaSlice<u64>, Box<dyn std::error::Error>> {
76    use cudarc::driver::DevicePtr;
77    let mut ptrs: Vec<u64> = cache
78        .kv
79        .iter()
80        .map(|kv| match kv.as_ref() {
81            Some(kvl) => {
82                let __s_g = e.stream();
83                let (p, _g) = kvl.len_d.device_ptr(&__s_g);
84                p as u64
85            }
86            None => 0u64,
87        })
88        .collect();
89    if let Some(pc) = pos_ctr {
90        let __s_g = e.stream();
91        let (p, _g) = pc.device_ptr(&__s_g);
92        ptrs.push(p as u64);
93    }
94    Ok(e.htod_u64(&ptrs)?)
95}