Skip to main content

needle_core/v2/
config.rs

1//! Geometry for the Needle v2 architecture.
2//!
3//! Every field comes from the `.cact` header, so one build runs any
4//! configuration of the architecture. `needle-infer` converts its
5//! `CactGeometry` into this; `needle-core` stays `no_std` and container-agnostic.
6
7use alloc::vec::Vec;
8
9/// Engram (n-gram associative memory) geometry.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct EngramGeometry {
12    /// Rows per hash table.
13    pub slots: usize,
14    /// Width of one table's entry.
15    pub sub_dim: usize,
16    /// Number of hash tables — `orders.len() * heads`.
17    pub num_tables: usize,
18    /// Taps in the causal convolution over the value stream.
19    pub conv_taps: usize,
20    /// Stride between taps, `max(orders)`.
21    pub conv_dilation: usize,
22    /// n-gram orders, one group of `num_tables / orders.len()` tables each.
23    pub orders: Vec<usize>,
24    /// Layer indices carrying a site.
25    pub sites: Vec<usize>,
26}
27
28impl EngramGeometry {
29    /// Tables per order.
30    pub fn heads(&self) -> usize {
31        if self.orders.is_empty() {
32            0
33        } else {
34            self.num_tables / self.orders.len()
35        }
36    }
37
38    /// Flattened width of one position's fetched entries.
39    pub fn fetched_dim(&self) -> usize {
40        self.num_tables * self.sub_dim
41    }
42
43    /// Tokens of history the value convolution reaches back over.
44    /// Mirrors `decode._engram_window`.
45    pub fn window(&self) -> usize {
46        if self.sites.is_empty() {
47            0
48        } else {
49            self.conv_taps * self.conv_dilation
50        }
51    }
52
53    /// Site index for a layer, if it carries one.
54    pub fn site_of_layer(&self, layer: usize) -> Option<usize> {
55        self.sites.iter().position(|&l| l == layer)
56    }
57}
58
59/// Full v2 model geometry.
60#[derive(Debug, Clone, PartialEq)]
61pub struct V2Config {
62    pub vocab_size: usize,
63    pub d_model: usize,
64    pub num_heads: usize,
65    pub num_kv_heads: usize,
66    pub num_layers: usize,
67    pub head_dim: usize,
68    pub max_seq_len: usize,
69    /// Hadamard width in HadamardMLP: `d_model` rounded up to a power of two.
70    pub hada_n: usize,
71    pub mhc_lanes: usize,
72    pub rope_theta: f32,
73    /// Sliding attention window. `0` disables it (full causal).
74    pub kv_window: usize,
75    pub engram: EngramGeometry,
76}
77
78impl V2Config {
79    /// Attention width, `num_heads * head_dim`.
80    pub fn attn_dim(&self) -> usize {
81        self.num_heads * self.head_dim
82    }
83
84    /// Packed K/V projection width.
85    pub fn kv_dim(&self) -> usize {
86        self.num_kv_heads * self.head_dim
87    }
88
89    /// Query heads served by one KV head.
90    pub fn kv_repeat(&self) -> usize {
91        self.num_heads / self.num_kv_heads
92    }
93
94    /// Flattened mHC lane-stream width, `mhc_lanes * d_model`.
95    pub fn mhc_width(&self) -> usize {
96        self.mhc_lanes * self.d_model
97    }
98
99    /// The lane layer `i` is "assigned" to — `eye(n)[i % n]` in
100    /// `architecture.Stack`, which drives the `pre_off` / `post_off` biases.
101    pub fn active_lane(&self, layer: usize) -> usize {
102        layer % self.mhc_lanes
103    }
104
105    /// `pre_off[layer][lane]` — `8 * lane_onehot - 4`.
106    pub fn pre_off(&self, layer: usize, lane: usize) -> f32 {
107        if lane == self.active_lane(layer) {
108            4.0
109        } else {
110            -4.0
111        }
112    }
113
114    /// `post_off[layer][lane]` — `-4 * (1 - lane_onehot)`.
115    pub fn post_off(&self, layer: usize, lane: usize) -> f32 {
116        if lane == self.active_lane(layer) {
117            0.0
118        } else {
119            -4.0
120        }
121    }
122
123    pub fn validate(&self) -> Result<(), &'static str> {
124        if self.num_heads == 0 || self.num_kv_heads == 0 {
125            return Err("head counts must be non-zero");
126        }
127        if !self.num_heads.is_multiple_of(self.num_kv_heads) {
128            return Err("num_heads must be divisible by num_kv_heads");
129        }
130        if self.head_dim == 0 || !self.head_dim.is_multiple_of(2) {
131            return Err("head_dim must be even (RoPE rotates half-splits)");
132        }
133        if self.d_model == 0 || self.mhc_lanes == 0 || self.num_layers == 0 {
134            return Err("d_model, mhc_lanes and num_layers must be non-zero");
135        }
136        if !self.hada_n.is_power_of_two() || self.hada_n < self.d_model {
137            return Err("hada_n must be a power of two at or above d_model");
138        }
139        if self.vocab_size == 0 || self.max_seq_len == 0 {
140            return Err("vocab_size and max_seq_len must be non-zero");
141        }
142        if !self.engram.sites.is_empty() {
143            let e = &self.engram;
144            if e.orders.is_empty() || e.num_tables == 0 || e.sub_dim == 0 || e.slots == 0 {
145                return Err("engram geometry incomplete");
146            }
147            if !e.num_tables.is_multiple_of(e.orders.len()) {
148                return Err("num_tables must be divisible by the order count");
149            }
150            if e.sites.iter().any(|&l| l >= self.num_layers) {
151                return Err("engram site beyond num_layers");
152            }
153            if e.fetched_dim() != self.d_model {
154                // Not required by the maths, but every shipped configuration has
155                // it, and the projections' input width is derived from it.
156                return Err("engram fetched_dim must equal d_model");
157            }
158        }
159        Ok(())
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use alloc::vec;
167
168    fn cfg() -> V2Config {
169        V2Config {
170            vocab_size: 8192,
171            d_model: 512,
172            num_heads: 8,
173            num_kv_heads: 4,
174            num_layers: 27,
175            head_dim: 64,
176            max_seq_len: 2048,
177            hada_n: 512,
178            mhc_lanes: 4,
179            rope_theta: 100000.0,
180            kv_window: 256,
181            engram: EngramGeometry {
182                slots: 8192,
183                sub_dim: 128,
184                num_tables: 4,
185                conv_taps: 4,
186                conv_dilation: 3,
187                orders: vec![2, 3],
188                sites: vec![2, 15],
189            },
190        }
191    }
192
193    #[test]
194    fn shipped_geometry_validates() {
195        let c = cfg();
196        assert_eq!(c.validate(), Ok(()));
197        assert_eq!(c.attn_dim(), 512);
198        assert_eq!(c.kv_dim(), 256);
199        assert_eq!(c.kv_repeat(), 2);
200        assert_eq!(c.mhc_width(), 2048);
201        assert_eq!(c.engram.heads(), 2);
202        assert_eq!(c.engram.fetched_dim(), 512);
203        assert_eq!(c.engram.window(), 12);
204        assert_eq!(c.engram.site_of_layer(2), Some(0));
205        assert_eq!(c.engram.site_of_layer(15), Some(1));
206        assert_eq!(c.engram.site_of_layer(3), None);
207    }
208
209    /// `pre_off` / `post_off` must reproduce `8*lane - 4` and `-4*(1 - lane)`.
210    #[test]
211    fn mhc_lane_offsets_match_reference_formulas() {
212        let c = cfg();
213        for layer in 0..c.num_layers {
214            for lane in 0..c.mhc_lanes {
215                let onehot = f32::from(lane == layer % c.mhc_lanes);
216                assert_eq!(c.pre_off(layer, lane), 8.0 * onehot - 4.0);
217                assert_eq!(c.post_off(layer, lane), -4.0 * (1.0 - onehot));
218            }
219        }
220    }
221
222    #[test]
223    fn validate_rejects_broken_geometry() {
224        let mut c = cfg();
225        c.num_kv_heads = 3;
226        assert!(c.validate().is_err());
227
228        let mut c = cfg();
229        c.head_dim = 63;
230        assert!(c.validate().is_err());
231
232        let mut c = cfg();
233        c.hada_n = 256; // below d_model
234        assert!(c.validate().is_err());
235
236        let mut c = cfg();
237        c.engram.sites = vec![2, 99];
238        assert!(c.validate().is_err());
239
240        let mut c = cfg();
241        c.engram.sub_dim = 64; // fetched_dim 256 != d_model
242        assert!(c.validate().is_err());
243    }
244}