spark_model/layers/deepseek_v41_ref/
engram.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// provenance-id: 526f6e616c6420522e205374657369616b
3
4//! DeepSeek-V4.1 **engram**, CPU reference, checked sub-op by sub-op against the golden.
5//!
6//! Four stages, each independently testable against its own capture:
7//!
8//! 1. **Hash** (`NgramHashState`): compressed token map, n-gram window with pad and a cache that
9//!    carries look-back across the prefill/decode split, per-lookback multiply, XOR-rolled, then
10//!    `% prime` per (n-gram size, head) plus that slot's offset. Integer-exact.
11//! 2. **Table rows** (`embed_rows`): the fp8 e4m3 table, dequantised per 32-block by an e8m0
12//!    scale, gathered by hash id. e4m3 values are exactly representable in bf16, so with the
13//!    fixture's unit scales this stage is exact too.
14//! 3. **Projection** (`wkv`): one bf16 GEMM, `[n_hash_cols * head_dim] -> [dim * (hc_mult + 1)]`,
15//!    split into one key per hyper-connection copy plus one shared value.
16//! 4. **Gate** (`gate_and_add`): normalised dot product of stream against key, signed-sqrt
17//!    sigmoid, `h + gate * value`, back to bf16.
18//!
19//! The tables (`token_map`, `multipliers`, `primes`, `offsets`) are consumed as GIVEN, never
20//! recomputed: in production they come from the GGUF's `deepseek41.engram.*` metadata, which
21//! `gen_ds41_engram_hash_check.py` proved identical to the reference's own computation.
22
23use super::{Golden, fixed_value, to_bf16_rne};
24
25/// The hash tables and geometry, exactly as the loader will read them from the GGUF.
26pub struct EngramTables {
27    pub layer_ids: Vec<usize>,
28    pub max_ngram: usize,
29    pub n_heads: usize,
30    pub head_dim: usize,
31    pub num_embeddings: Vec<usize>,
32    /// `token_map[engram_pad_id]`: the pad in the COMPRESSED id space.
33    pub pad_id: i64,
34    pub token_map: Vec<i64>,
35    /// `[layer][lookback]`
36    pub multipliers: Vec<Vec<i64>>,
37    /// `[layer][ngram_size - 2][head]`
38    pub primes: Vec<Vec<Vec<i64>>>,
39    /// `[layer][(ngram_size - 2) * n_heads + head]`
40    pub offsets: Vec<Vec<i64>>,
41    /// Elements per e8m0 scale block along `head_dim`.
42    pub scale_block: usize,
43    /// `[layer][row * (head_dim / scale_block) + block]`, each a power of two.
44    pub scales: Vec<Vec<f32>>,
45}
46
47impl EngramTables {
48    pub fn n_hash_cols(&self) -> usize {
49        (self.max_ngram - 1) * self.n_heads
50    }
51
52    pub fn from_golden(g: &Golden) -> Self {
53        let e = &g.0["fixture"]["engram"];
54        let ints = |v: &serde_json::Value| -> Vec<i64> {
55            v.as_array()
56                .expect("array")
57                .iter()
58                .map(|x| x.as_i64().expect("int"))
59                .collect()
60        };
61        EngramTables {
62            layer_ids: ints(&e["layer_ids"])
63                .into_iter()
64                .map(|x| x as usize)
65                .collect(),
66            max_ngram: e["max_ngram_size"].as_u64().unwrap() as usize,
67            n_heads: e["n_heads"].as_u64().unwrap() as usize,
68            head_dim: e["head_dim"].as_u64().unwrap() as usize,
69            num_embeddings: ints(&e["num_embeddings"])
70                .into_iter()
71                .map(|x| x as usize)
72                .collect(),
73            pad_id: e["pad_id_compressed"].as_i64().unwrap(),
74            token_map: ints(&e["token_map"]),
75            multipliers: e["multipliers"]
76                .as_array()
77                .unwrap()
78                .iter()
79                .map(ints)
80                .collect(),
81            primes: e["primes"]
82                .as_array()
83                .unwrap()
84                .iter()
85                .map(|l| l.as_array().unwrap().iter().map(ints).collect())
86                .collect(),
87            offsets: e["offsets"].as_array().unwrap().iter().map(ints).collect(),
88            scale_block: e["scale_block"].as_u64().unwrap() as usize,
89            scales: e["scales"]
90                .as_array()
91                .unwrap()
92                .iter()
93                .map(|l| {
94                    l.as_array()
95                        .unwrap()
96                        .iter()
97                        .map(|x| x.as_f64().unwrap() as f32)
98                        .collect()
99                })
100                .collect(),
101        }
102    }
103}
104
105/// `engram.py::NgramHashState`. The cache holds compressed ids for every position seen, so a
106/// decode step's look-back reaches into the prefill.
107pub struct NgramHashState<'a> {
108    pub t: &'a EngramTables,
109    pub batch: usize,
110    pub max_seq: usize,
111    cache: Vec<i64>,
112}
113
114impl<'a> NgramHashState<'a> {
115    pub fn new(t: &'a EngramTables, batch: usize, max_seq: usize) -> Self {
116        NgramHashState {
117            t,
118            batch,
119            max_seq,
120            cache: vec![0; batch * max_seq],
121        }
122    }
123
124    /// `input_ids`: `[batch, seqlen]` row-major. Returns `[batch, seqlen, n_layers, n_hash_cols]`.
125    pub fn forward(&mut self, input_ids: &[i64], seqlen: usize, start_pos: usize) -> Vec<i64> {
126        let t = self.t;
127        let (b_n, ng, nl, cols) = (self.batch, t.max_ngram, t.layer_ids.len(), t.n_hash_cols());
128        assert_eq!(input_ids.len(), b_n * seqlen);
129        assert!(start_pos + seqlen <= self.max_seq);
130        for b in 0..b_n {
131            for s in 0..seqlen {
132                self.cache[b * self.max_seq + start_pos + s] =
133                    t.token_map[input_ids[b * seqlen + s] as usize];
134            }
135        }
136        let mut out = vec![0i64; b_n * seqlen * nl * cols];
137        let mut tokens = vec![0i64; ng];
138        for b in 0..b_n {
139            for s in 0..seqlen {
140                let pos = start_pos + s;
141                // `blocked` accumulates over shifts, as the reference's running OR does
142                let mut blocked = false;
143                for (shift, tok) in tokens.iter_mut().enumerate() {
144                    let src = self.cache[b * self.max_seq + pos.saturating_sub(shift)];
145                    blocked = blocked || pos < shift || src == -1;
146                    *tok = if blocked { t.pad_id } else { src };
147                }
148                for (li, mults) in t.multipliers.iter().enumerate() {
149                    let mut rolling = tokens[0].wrapping_mul(mults[0]);
150                    for i in 1..ng {
151                        rolling ^= tokens[i].wrapping_mul(mults[i]);
152                        for h in 0..t.n_heads {
153                            let col = (i - 1) * t.n_heads + h;
154                            out[((b * seqlen + s) * nl + li) * cols + col] =
155                                rolling.rem_euclid(t.primes[li][i - 1][h]) + t.offsets[li][col];
156                        }
157                    }
158                }
159            }
160        }
161        out
162    }
163}
164
165// ── fp8 e4m3fn ───────────────────────────────────────────────────────────────
166
167/// Encode with round-to-nearest-even, as torch's `.to(float8_e4m3fn)` does. Saturates above
168/// 448 (callers clamp first; the fixture never gets there).
169pub fn f32_to_e4m3_rne(x: f32) -> u8 {
170    if x.is_nan() {
171        return 0x7F;
172    }
173    let sign: u8 = if x.is_sign_negative() { 0x80 } else { 0 };
174    let bits = x.abs().to_bits();
175    let exp_field = ((bits >> 23) & 0xFF) as i32;
176    if exp_field == 0 {
177        return sign; // f32 subnormal: far below e4m3's 2^-9 minimum
178    }
179    let a = x.abs() as f64;
180    if a > 448.0 {
181        return sign | 0x7E;
182    }
183    let e = exp_field - 127;
184    if e < -6 {
185        // e4m3 subnormal: value = m/8 * 2^-6, m in 0..8; m == 8 carries into the first normal
186        let r = (a * 512.0).round_ties_even() as u8;
187        return sign | r; // r == 8 encodes as exp_field 1, mantissa 0, which is 0x08
188    }
189    let mant = ((a * 2f64.powi(-e) - 1.0) * 8.0).round_ties_even() as u8;
190    let (e, mant) = if mant == 8 { (e + 1, 0u8) } else { (e, mant) };
191    sign | (((e + 7) as u8) << 3) | mant
192}
193
194pub fn e4m3_to_f32(b: u8) -> f32 {
195    let sign = if b & 0x80 != 0 { -1.0f32 } else { 1.0 };
196    let exp_field = ((b >> 3) & 0x0F) as i32;
197    let mant = (b & 0x07) as f32;
198    if exp_field == 15 && mant == 7.0 {
199        return f32::NAN;
200    }
201    if exp_field == 0 {
202        return sign * mant / 8.0 * 2f32.powi(-6);
203    }
204    sign * (1.0 + mant / 8.0) * 2f32.powi(exp_field - 7)
205}
206
207/// Torch's f32 -> e8m0 cast, as a power of two: nearest exponent, ties (exactly 1.5 * 2^e) to
208/// the even exponent. Pinned against the fixture's shipped scales by
209/// `e8m0_rounding_rule_matches_torch`.
210pub fn f32_to_e8m0_rne(x: f32) -> f32 {
211    let bits = x.to_bits();
212    let mut e = ((bits >> 23) & 0xFF) as i32;
213    let m = bits & 0x7F_FFFF;
214    if m > 0x40_0000 || (m == 0x40_0000 && (e & 1) == 1) {
215        e += 1;
216    }
217    f32::from_bits((e as u32) << 23)
218}
219
220/// The fixture's fp8 table for `layer_id`, regenerated and dequantised with the SHIPPED per-block
221/// e8m0 scales (`ParallelEngramEmbedding.forward`: `values.float() * scales.float()` per block,
222/// then bf16). `[rows * head_dim]` f32; exact, since e4m3 times a power of two is bf16-exact.
223pub fn regen_table(t: &EngramTables, layer_idx: usize) -> Vec<f32> {
224    let lid = t.layer_ids[layer_idx];
225    let (rows, hd, blk) = (t.num_embeddings[layer_idx], t.head_dim, t.scale_block);
226    let name = format!("layers.{lid}.engram.embed.weight");
227    let scales = &t.scales[layer_idx];
228    (0..(rows * hd) as u64)
229        .map(|i| {
230            let v = e4m3_to_f32(f32_to_e4m3_rne(fixed_value(&name, i, 0.5, 0.0)));
231            let (r, d) = ((i as usize) / hd, (i as usize) % hd);
232            to_bf16_rne(v * scales[r * (hd / blk) + d / blk])
233        })
234        .collect()
235}
236
237/// `ParallelEngramEmbedding.forward` with unit scales: gather rows by id. Output
238/// `[n_ids, head_dim]`, bf16-exact because e4m3 ⊂ bf16.
239pub fn embed_rows(table: &[f32], head_dim: usize, hash_ids: &[i64]) -> Vec<f32> {
240    let mut out = Vec::with_capacity(hash_ids.len() * head_dim);
241    for &id in hash_ids {
242        let r = id as usize * head_dim;
243        out.extend_from_slice(&table[r..r + head_dim]);
244    }
245    out
246}
247
248/// A bf16-stored `[out, in]` weight regenerated by the fixture rule `scale = in^-0.5`.
249pub fn regen_bf16_matrix(name: &str, out_f: usize, in_f: usize) -> Vec<f32> {
250    let scale = (in_f as f64).powf(-0.5);
251    (0..(out_f * in_f) as u64)
252        .map(|i| to_bf16_rne(fixed_value(name, i, scale, 0.0)))
253        .collect()
254}
255
256/// A bf16-stored `[hc, dim]` engram q/k weight regenerated by the fixture rule (offset 1, ±0.05).
257pub fn regen_bf16_qk(name: &str, n: usize) -> Vec<f32> {
258    (0..n as u64)
259        .map(|i| to_bf16_rne(fixed_value(name, i, 0.05, 1.0)))
260        .collect()
261}
262
263/// `F.linear` on bf16: f32 accumulate, bf16 output. `x`: `[rows, in]`, `w`: `[out, in]`.
264pub fn linear_bf16(x: &[f32], rows: usize, in_f: usize, w: &[f32], out_f: usize) -> Vec<f32> {
265    let mut y = vec![0f32; rows * out_f];
266    for r in 0..rows {
267        let xr = &x[r * in_f..(r + 1) * in_f];
268        for o in 0..out_f {
269            let wr = &w[o * in_f..(o + 1) * in_f];
270            let acc: f32 = xr.iter().zip(wr).map(|(a, b)| a * b).sum();
271            y[r * out_f + o] = to_bf16_rne(acc);
272        }
273    }
274    y
275}
276
277/// `Engram.forward` after the projection: `x` `[tokens, hc, dim]` (bf16 values), `kv`
278/// `[tokens, dim*(hc+1)]`, q/k weights `[hc, dim]`. Returns bf16-rounded `[tokens, hc, dim]`.
279pub fn gate_and_add(
280    x: &[f32],
281    kv: &[f32],
282    q_w: &[f32],
283    k_w: &[f32],
284    tokens: usize,
285    hc: usize,
286    dim: usize,
287    eps: f32,
288) -> Vec<f32> {
289    let kv_w = dim * (hc + 1);
290    let inv_sqrt_dim = (dim as f32).powf(-0.5);
291    let mut out = vec![0f32; tokens * hc * dim];
292    for t in 0..tokens {
293        let value = &kv[t * kv_w + hc * dim..(t + 1) * kv_w];
294        for c in 0..hc {
295            let h = &x[(t * hc + c) * dim..(t * hc + c + 1) * dim];
296            let key = &kv[t * kv_w + c * dim..t * kv_w + (c + 1) * dim];
297            let (mut hh, mut kk, mut dot) = (0f32, 0f32, 0f32);
298            for d in 0..dim {
299                hh += h[d] * h[d];
300                kk += key[d] * key[d];
301                // q_weight * k_weight is only ever used as a product
302                dot += h[d] * (q_w[c * dim + d] * k_w[c * dim + d]) * key[d];
303            }
304            let rstd =
305                (hh / dim as f32 + eps).sqrt().recip() * (kk / dim as f32 + eps).sqrt().recip();
306            let dot = dot * rstd * inv_sqrt_dim;
307            // signed sqrt before the sigmoid, matching the training kernel
308            let g = dot.abs().max(1e-6).sqrt().copysign(dot);
309            let gate = 1.0 / (1.0 + (-g).exp());
310            for d in 0..dim {
311                out[(t * hc + c) * dim + d] = to_bf16_rne(h[d] + gate * value[d]);
312            }
313        }
314    }
315    out
316}
317
318#[cfg(test)]
319#[path = "engram_tests.rs"]
320mod tests;