Skip to main content

needle_core/v3/
heads.rs

1//! Needle 3 probe heads.
2//!
3//! v3 exports one: confidence. The contrastive head v2 carried is not in this
4//! container (`extras.heads = ["confidence"]`), so `retrieve_tools` has no v3
5//! equivalent — the same situation as the published v1 weights.
6//!
7//! A probe head reads the per-layer cells rather than the final hidden state,
8//! and pools them twice: once over positions, per (layer, probe), and once
9//! over (layer, probe), per query. Both poolings are softmaxes, so the head
10//! sees the whole sequence rather than just the last token.
11
12extern crate alloc;
13use alloc::vec;
14use alloc::vec::Vec;
15
16use crate::kernels::rms_unit;
17use crate::math::{exp, sqrt};
18
19/// Head codes in `heads.manifest`, in upstream's canonical order.
20pub const HEAD_EMBEDDING: u8 = 1;
21pub const HEAD_CONFIDENCE: u8 = 2;
22pub const HEAD_ROUTER: u8 = 3;
23
24/// One probe head's weights, already dequantised.
25///
26/// Small enough that packing buys nothing: on the shipped model this is
27/// 84x768 + 4x768 + a few vectors.
28pub struct ProbeHead {
29    /// `(cells * probes, d_model)`.
30    pub probes: Vec<f32>,
31    /// `(cells, probes)`.
32    pub gain: Vec<f32>,
33    /// `(queries, d_model)`.
34    pub query: Vec<f32>,
35    /// `(queries, cells, probes)`.
36    pub row_bias: Vec<f32>,
37    /// `(out_dim, queries * d_model)`.
38    pub proj: Vec<f32>,
39    /// `(out_dim)`.
40    pub bias: Vec<f32>,
41    pub cells: usize,
42    pub probes_per_cell: usize,
43    pub queries: usize,
44    pub out_dim: usize,
45}
46
47impl ProbeHead {
48    /// Run the head over `cells`, shaped `(seq, self.cells, d_model)`.
49    ///
50    /// Returns `out_dim` logits. Mirrors upstream `probe_pool` followed by
51    /// `HeadProjection`.
52    pub fn forward(&self, cells: &[f32], seq: usize, d: usize) -> Vec<f32> {
53        debug_assert_eq!(cells.len(), seq * self.cells * d);
54        let (l1, k, q) = (self.cells, self.probes_per_cell, self.queries);
55        let scale = 1.0 / sqrt(d as f32);
56
57        // r[l][k] — each (layer, probe) pair's softmax-weighted summary of the
58        // sequence.
59        let mut r = vec![0.0f32; l1 * k * d];
60        let mut scores = vec![0.0f32; seq];
61        for l in 0..l1 {
62            for kk in 0..k {
63                let pr = &self.probes[(l * k + kk) * d..(l * k + kk + 1) * d];
64                let mut max = f32::NEG_INFINITY;
65                for (t, sc) in scores.iter_mut().enumerate() {
66                    let cell = &cells[(t * l1 + l) * d..(t * l1 + l + 1) * d];
67                    let mut acc = 0.0f32;
68                    for (a, b) in cell.iter().zip(pr) {
69                        acc += a * b;
70                    }
71                    *sc = acc * scale;
72                    if *sc > max {
73                        max = *sc;
74                    }
75                }
76                let mut sum = 0.0f32;
77                for sc in scores.iter_mut() {
78                    *sc = exp(*sc - max);
79                    sum += *sc;
80                }
81                let inv = 1.0 / sum;
82
83                let dst = &mut r[(l * k + kk) * d..(l * k + kk + 1) * d];
84                dst.fill(0.0);
85                for (t, &sc) in scores.iter().enumerate() {
86                    let w = sc * inv;
87                    let cell = &cells[(t * l1 + l) * d..(t * l1 + l + 1) * d];
88                    for (o, &c) in dst.iter_mut().zip(cell) {
89                        *o += w * c;
90                    }
91                }
92                // Normalise, then scale by this pair's learned gain.
93                rms_unit(dst);
94                let g = self.gain[l * k + kk];
95                for o in dst.iter_mut() {
96                    *o *= g;
97                }
98            }
99        }
100
101        self.pool_and_project(&r, l1, k, q, d)
102    }
103
104    /// The second softmax — over `(layer, probe)` pairs, per query — and the
105    /// final projection. Shared by the two-pass and streaming paths so they
106    /// cannot drift apart.
107    fn pool_and_project(&self, r: &[f32], l1: usize, k: usize, q: usize, d: usize) -> Vec<f32> {
108        let scale = 1.0 / sqrt(d as f32);
109        // For each query, a softmax over all (layer, probe) pairs.
110        let m = l1 * k;
111        let mut pooled = vec![0.0f32; q * d];
112        let mut u = vec![0.0f32; m];
113        for qi in 0..q {
114            let qv = &self.query[qi * d..(qi + 1) * d];
115            let mut max = f32::NEG_INFINITY;
116            for (idx, uu) in u.iter_mut().enumerate() {
117                let rv = &r[idx * d..(idx + 1) * d];
118                let mut acc = 0.0f32;
119                for (a, b) in rv.iter().zip(qv) {
120                    acc += a * b;
121                }
122                *uu = acc * scale + self.row_bias[qi * m + idx];
123                if *uu > max {
124                    max = *uu;
125                }
126            }
127            let mut sum = 0.0f32;
128            for uu in u.iter_mut() {
129                *uu = exp(*uu - max);
130                sum += *uu;
131            }
132            let inv = 1.0 / sum;
133
134            let dst = &mut pooled[qi * d..(qi + 1) * d];
135            dst.fill(0.0);
136            for (idx, &uu) in u.iter().enumerate() {
137                let w = uu * inv;
138                let rv = &r[idx * d..(idx + 1) * d];
139                for (o, &v) in dst.iter_mut().zip(rv) {
140                    *o += w * v;
141                }
142            }
143        }
144
145        // Final projection over the flattened (queries * d_model) vector.
146        let mut out = vec![0.0f32; self.out_dim];
147        for (o, dst) in out.iter_mut().enumerate() {
148            let row = &self.proj[o * q * d..(o + 1) * q * d];
149            let mut acc = 0.0f32;
150            for (a, b) in row.iter().zip(&pooled) {
151                acc += a * b;
152            }
153            *dst = acc + self.bias.get(o).copied().unwrap_or(0.0);
154        }
155        out
156    }
157}
158
159/// Pools a probe head's first softmax **as positions arrive**, so the cells
160/// are never all held at once.
161///
162/// Materialising them costs `seq * cells * d_model` floats — 504 MB at full
163/// context on the shipped model, which is not survivable in a browser tab.
164/// This holds `cells * probes * d_model` regardless of sequence length: 252 KB.
165///
166/// The trick is the online softmax. `r = Σ_t softmax(s)_t · cell_t` is computed
167/// by carrying a running maximum, denominator and numerator, and rescaling both
168/// whenever a larger score appears — algebraically identical to the two-pass
169/// form, which [`ProbeHead::forward`] still does and the tests compare against.
170pub struct ProbePool<'a> {
171    head: &'a ProbeHead,
172    d: usize,
173    /// Running max per (cell, probe).
174    max: Vec<f32>,
175    /// Running denominator per (cell, probe).
176    den: Vec<f32>,
177    /// Running numerator, `(cells * probes, d_model)`.
178    num: Vec<f32>,
179}
180
181impl<'a> ProbePool<'a> {
182    pub fn new(head: &'a ProbeHead, d_model: usize) -> Self {
183        let m = head.cells * head.probes_per_cell;
184        Self {
185            head,
186            d: d_model,
187            max: vec![f32::NEG_INFINITY; m],
188            den: vec![0.0; m],
189            num: vec![0.0; m * d_model],
190        }
191    }
192
193    /// Feed one position's cell for one cell index (0 is the input embedding).
194    ///
195    /// Order does not matter — the pooling is a sum over positions — so a
196    /// caller may stream all positions for one cell before moving to the next,
197    /// which is how the forward pass is laid out.
198    pub fn observe(&mut self, cell_index: usize, cell: &[f32]) {
199        debug_assert_eq!(cell.len(), self.d);
200        let (k, d) = (self.head.probes_per_cell, self.d);
201        let scale = 1.0 / sqrt(d as f32);
202        for kk in 0..k {
203            let idx = cell_index * k + kk;
204            let pr = &self.head.probes[idx * d..(idx + 1) * d];
205            let mut dot = 0.0f32;
206            for (a, b) in cell.iter().zip(pr) {
207                dot += a * b;
208            }
209            let s = dot * scale;
210
211            let m0 = self.max[idx];
212            let m1 = if s > m0 { s } else { m0 };
213            // Rescale what is already accumulated onto the new maximum.
214            let shift = if m0.is_finite() { exp(m0 - m1) } else { 0.0 };
215            let w = exp(s - m1);
216            self.den[idx] = self.den[idx] * shift + w;
217            let dst = &mut self.num[idx * d..(idx + 1) * d];
218            for (o, &c) in dst.iter_mut().zip(cell) {
219                *o = *o * shift + w * c;
220            }
221            self.max[idx] = m1;
222        }
223    }
224
225    /// Finish the pooling and run the head's second softmax and projection.
226    pub fn finish(self) -> Vec<f32> {
227        let (l1, k, q, d) = (
228            self.head.cells,
229            self.head.probes_per_cell,
230            self.head.queries,
231            self.d,
232        );
233        let mut r = self.num;
234        for idx in 0..l1 * k {
235            let inv = 1.0 / self.den[idx];
236            let dst = &mut r[idx * d..(idx + 1) * d];
237            for o in dst.iter_mut() {
238                *o *= inv;
239            }
240            rms_unit(dst);
241            let g = self.head.gain[idx];
242            for o in dst.iter_mut() {
243                *o *= g;
244            }
245        }
246        self.head.pool_and_project(&r, l1, k, q, d)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    /// A head with one cell, one probe and one query, so the pooling collapses
255    /// to something checkable by hand.
256    fn trivial(d: usize) -> ProbeHead {
257        ProbeHead {
258            probes: vec![0.0; d],
259            gain: vec![1.0],
260            query: vec![0.0; d],
261            row_bias: vec![0.0],
262            proj: vec![1.0; d],
263            bias: vec![0.5],
264            cells: 1,
265            probes_per_cell: 1,
266            queries: 1,
267            out_dim: 1,
268        }
269    }
270
271    #[test]
272    fn a_zero_probe_averages_the_sequence() {
273        // With a zero probe every position scores 0, so the softmax is uniform
274        // and r is the mean cell — then rms_unit normalises it.
275        let d = 4;
276        let h = trivial(d);
277        let cells = vec![
278            1.0f32, 0.0, 0.0, 0.0, // t=0
279            3.0, 0.0, 0.0, 0.0, // t=1
280        ];
281        let out = h.forward(&cells, 2, d);
282        // mean = [2,0,0,0]; rms over 4 dims = sqrt(4/4) = 1 → unit = [2,0,0,0]/1
283        // proj is all ones, so the logit is the sum plus the bias.
284        assert!(
285            (out[0] - (2.0 / (4.0f32 / 4.0).sqrt() + 0.5)).abs() < 1e-5,
286            "{out:?}"
287        );
288    }
289
290    #[test]
291    fn the_gain_scales_the_summary() {
292        let d = 4;
293        let mut h = trivial(d);
294        let cells = vec![2.0f32, 0.0, 0.0, 0.0];
295        let base = h.forward(&cells, 1, d)[0] - 0.5;
296        h.gain[0] = 3.0;
297        let scaled = h.forward(&cells, 1, d)[0] - 0.5;
298        assert!((scaled - 3.0 * base).abs() < 1e-4, "{scaled} vs 3 * {base}");
299    }
300}