Skip to main content

needle_core/
model.rs

1//! Top-level Needle SAN (Simple Attention Network) model.
2//!
3//! Encoder: N layers of self-attention only (no FFN by default).
4//! Decoder: M layers of self-attn + cross-attn (+ optional FFN).
5//! Tied embeddings: embedding matrix is shared with LM head (transposed multiply).
6//! Inference format:
7//!   Encoder input: [query_tokens..., tools_id(5), tools_tokens...]
8//!   Decoder start: [eos_id(1)]
9//!   Decoder output: <tool_call>(4) then JSON
10
11use crate::attn::{AttnConfig, KvCache};
12use crate::config::TransformerConfig;
13use crate::layers::{decoder_layer_forward, encoder_layer_forward, DecoderLayer, EncoderLayer};
14use crate::math;
15use crate::norm::zc_rms_norm_vec;
16use crate::rope::RopeCache;
17use alloc::vec;
18use alloc::vec::Vec;
19
20pub struct NeedleModel {
21    pub cfg: TransformerConfig,
22
23    /// Token embedding matrix [vocab_size, d_model] (also used as LM head transposed)
24    pub embedding: Vec<f32>,
25
26    pub encoder_layers: Vec<EncoderLayer>,
27    pub decoder_layers: Vec<DecoderLayer>,
28
29    /// Encoder final norm scale [d_model]
30    pub encoder_final_norm: Vec<f32>,
31    /// Decoder final norm scale [d_model]
32    pub decoder_final_norm: Vec<f32>,
33
34    /// RoPE cache (shared encoder + decoder)
35    rope: RopeCache,
36
37    attn_cfg: AttnConfig,
38
39    /// sqrt(d_model) — applied to embeddings before encoder/decoder
40    embed_scale: f32,
41}
42
43impl NeedleModel {
44    pub fn new(
45        cfg: TransformerConfig,
46        embedding: Vec<f32>,
47        encoder_layers: Vec<EncoderLayer>,
48        decoder_layers: Vec<DecoderLayer>,
49        encoder_final_norm: Vec<f32>,
50        decoder_final_norm: Vec<f32>,
51    ) -> Self {
52        let rope = RopeCache::new(
53            cfg.max_enc_len.max(cfg.max_dec_len),
54            cfg.head_dim(),
55            cfg.rope_theta,
56        );
57        let attn_cfg = AttnConfig {
58            num_heads: cfg.num_heads,
59            num_kv_heads: cfg.num_kv_heads,
60            head_dim: cfg.head_dim(),
61            d_model: cfg.d_model,
62        };
63        let embed_scale = math::sqrt(cfg.d_model as f32);
64        Self {
65            cfg,
66            embedding,
67            encoder_layers,
68            decoder_layers,
69            encoder_final_norm,
70            decoder_final_norm,
71            rope,
72            attn_cfg,
73            embed_scale,
74        }
75    }
76
77    /// Embed a token and apply embedding scale: returns owned Vec.
78    fn embed_scaled(&self, token_id: u32) -> Vec<f32> {
79        let d = self.cfg.d_model;
80        let idx = token_id as usize;
81        debug_assert!(idx < self.cfg.vocab_size);
82        let row = &self.embedding[idx * d..(idx + 1) * d];
83        row.iter().map(|v| v * self.embed_scale).collect()
84    }
85
86    /// Compute logits over vocabulary for a hidden vector.
87    /// Uses tied embedding (LM head = embedding^T).
88    /// Dispatches to AVX2 path at runtime via CPUID (matches quant.rs dispatch pattern).
89    fn lm_head(&self, hidden: &[f32], logits: &mut [f32]) {
90        debug_assert_eq!(hidden.len(), self.cfg.d_model);
91        debug_assert_eq!(logits.len(), self.cfg.vocab_size);
92
93        #[cfg(all(target_arch = "x86_64", feature = "simd"))]
94        if crate::quant::has_avx2() {
95            // Safety: has_avx2() confirmed AVX2 via CPUID before this call.
96            return unsafe { self.lm_head_avx2(hidden, logits) };
97        }
98
99        self.lm_head_scalar(hidden, logits);
100    }
101
102    #[allow(clippy::needless_range_loop)]
103    fn lm_head_scalar(&self, hidden: &[f32], logits: &mut [f32]) {
104        let d = self.cfg.d_model;
105        let v = self.cfg.vocab_size;
106        for tok in 0..v {
107            let emb = &self.embedding[tok * d..(tok + 1) * d];
108            let mut acc = 0.0f32;
109            for i in 0..d {
110                acc += hidden[i] * emb[i];
111            }
112            logits[tok] = acc;
113        }
114    }
115
116    #[cfg(all(target_arch = "x86_64", feature = "simd"))]
117    #[target_feature(enable = "avx2")]
118    #[allow(clippy::needless_range_loop)]
119    unsafe fn lm_head_avx2(&self, hidden: &[f32], logits: &mut [f32]) {
120        use core::arch::x86_64::*;
121        let d = self.cfg.d_model;
122        let v = self.cfg.vocab_size;
123        let chunks = d / 8;
124
125        for tok in 0..v {
126            let emb_ptr = self.embedding.as_ptr().add(tok * d);
127            let mut acc = _mm256_setzero_ps();
128            for c in 0..chunks {
129                let h = _mm256_loadu_ps(hidden.as_ptr().add(c * 8));
130                let e = _mm256_loadu_ps(emb_ptr.add(c * 8));
131                acc = _mm256_fmadd_ps(h, e, acc);
132            }
133            // Horizontal sum: reduce 8 lanes → 1 scalar
134            let hi128 = _mm256_extractf128_ps(acc, 1);
135            let lo128 = _mm256_castps256_ps128(acc);
136            let sum128 = _mm_add_ps(hi128, lo128);
137            let shuf = _mm_shuffle_ps(sum128, sum128, 0x4E);
138            let sum64 = _mm_add_ps(sum128, shuf);
139            let shuf2 = _mm_shuffle_ps(sum64, sum64, 0xB1);
140            let sum32 = _mm_add_ss(sum64, shuf2);
141            let mut result = _mm_cvtss_f32(sum32);
142            // Scalar tail for d not divisible by 8
143            for i in chunks * 8..d {
144                result += hidden[i] * *emb_ptr.add(i);
145            }
146            logits[tok] = result;
147        }
148    }
149
150    /// Run the encoder over the full input sequence.
151    /// Returns encoder hidden states [enc_len, d_model].
152    /// Also fills enc_kv_caches (one per decoder layer) with cross-attn K/V projections.
153    pub fn encode(&self, input_ids: &[u32], enc_kv_caches: &mut [KvCache]) -> Vec<f32> {
154        let seq_len = input_ids.len();
155        let d = self.cfg.d_model;
156
157        // Embed + scale
158        let mut x = vec![0.0f32; seq_len * d];
159        for (t, &tok) in input_ids.iter().enumerate() {
160            let row = &self.embedding[tok as usize * d..(tok as usize + 1) * d];
161            for (dst, &src) in x[t * d..(t + 1) * d].iter_mut().zip(row.iter()) {
162                *dst = src * self.embed_scale;
163            }
164        }
165
166        let mut tmp = Vec::with_capacity(seq_len * d);
167        let mut normed = vec![0.0f32; seq_len * d]; // pre-allocated scratch, reused across layers
168
169        // Run encoder layers (self-attn only, no KV cache written here)
170        for layer in self.encoder_layers.iter() {
171            encoder_layer_forward(
172                &mut x,
173                layer,
174                &self.attn_cfg,
175                &self.rope,
176                seq_len,
177                None,
178                &mut tmp,
179                &mut normed,
180            );
181        }
182
183        // Encoder final norm
184        for t in 0..seq_len {
185            zc_rms_norm_vec(&mut x[t * d..(t + 1) * d], &self.encoder_final_norm);
186        }
187
188        // Project encoder output into cross-attn K/V for each decoder layer (no RoPE)
189        self.fill_cross_kv(&x, seq_len, enc_kv_caches);
190
191        x
192    }
193
194    /// Project encoder hidden states into K/V for each decoder layer's cross-attn.
195    /// No RoPE applied (Python passes rope=None to cross-attention).
196    fn fill_cross_kv(&self, enc_hidden: &[f32], seq_len: usize, kv_caches: &mut [KvCache]) {
197        let kv_h = self.cfg.num_kv_heads;
198        let hd = self.cfg.head_dim();
199        let kv_dim = kv_h * hd;
200
201        for (li, layer) in self.decoder_layers.iter().enumerate() {
202            let kv = &mut kv_caches[li];
203            kv.reset();
204
205            let mut k_all = vec![0.0f32; seq_len * kv_dim];
206            let mut v_all = vec![0.0f32; seq_len * kv_dim];
207
208            layer.cross_attn.wk.matmul(enc_hidden, seq_len, &mut k_all);
209            layer.cross_attn.wv.matmul(enc_hidden, seq_len, &mut v_all);
210
211            // Apply k_norm (ZCRMSNorm on each KV head slice)
212            for t in 0..seq_len {
213                for ki in 0..kv_h {
214                    crate::norm::zc_rms_norm_vec(
215                        &mut k_all[(t * kv_h + ki) * hd..(t * kv_h + ki + 1) * hd],
216                        &layer.cross_attn.k_norm,
217                    );
218                }
219            }
220
221            // No RoPE on cross-attn keys (Python: rope=None)
222
223            let kv_stride = kv_h * hd;
224            for t in 0..seq_len {
225                kv.push_kv(
226                    &k_all[t * kv_stride..(t + 1) * kv_stride],
227                    &v_all[t * kv_stride..(t + 1) * kv_stride],
228                );
229            }
230        }
231    }
232
233    /// Autoregressive decode: generates one token given decoder input token and KV caches.
234    /// Returns logits over the full vocabulary.
235    pub fn decode_step(
236        &self,
237        token_id: u32,
238        enc_kv_caches: &[KvCache],
239        dec_kv_caches: &mut [KvCache],
240        logits: &mut [f32],
241    ) {
242        let d = self.cfg.d_model;
243
244        // Embed + scale
245        let mut x: Vec<f32> = self.embed_scaled(token_id);
246        let mut tmp = Vec::with_capacity(d);
247        let mut normed = vec![0.0f32; d]; // pre-allocated scratch, reused across layers
248
249        for (li, layer) in self.decoder_layers.iter().enumerate() {
250            decoder_layer_forward(
251                &mut x,
252                layer,
253                &self.attn_cfg,
254                &self.rope,
255                &enc_kv_caches[li],
256                &mut dec_kv_caches[li],
257                &mut tmp,
258                &mut normed,
259            );
260        }
261
262        // Decoder final norm + LM head
263        zc_rms_norm_vec(&mut x, &self.decoder_final_norm);
264        self.lm_head(&x, logits);
265    }
266
267    /// Allocate fresh KV caches for cross-attention (one per decoder layer, sized for enc input).
268    pub fn make_enc_kv_caches(&self, enc_len: usize) -> Vec<KvCache> {
269        (0..self.cfg.num_dec_layers)
270            .map(|_| KvCache::new(enc_len, self.cfg.num_kv_heads, self.cfg.head_dim()))
271            .collect()
272    }
273
274    /// Allocate fresh KV caches for decoder self-attention.
275    pub fn make_dec_kv_caches(&self) -> Vec<KvCache> {
276        (0..self.cfg.num_dec_layers)
277            .map(|_| {
278                KvCache::new(
279                    self.cfg.max_dec_len,
280                    self.cfg.num_kv_heads,
281                    self.cfg.head_dim(),
282                )
283            })
284            .collect()
285    }
286}