1use crate::{EngineError, SimdMode};
2use rayon::prelude::*;
3
4pub fn matmul(
7 a: &[f32],
8 a_rows: usize,
9 a_cols: usize,
10 b: &[f32],
11 b_rows: usize,
12 b_cols: usize,
13 _mode: SimdMode,
14) -> Result<Vec<f32>, EngineError> {
15 if a_cols != b_rows {
16 return Err(EngineError::ShapeMismatch(format!(
17 "matmul inner dim {a_cols} != {b_rows}"
18 )));
19 }
20 if a.len() != a_rows * a_cols || b.len() != b_rows * b_cols {
21 return Err(EngineError::ShapeMismatch(
22 "matmul buffer length does not match shape".into(),
23 ));
24 }
25 let mut out = vec![0.0f32; a_rows * b_cols];
26 for i in 0..a_rows {
27 for j in 0..b_cols {
28 let mut s = 0.0f32;
29 for k in 0..a_cols {
30 s += a[i * a_cols + k] * b[k * b_cols + j];
31 }
32 out[i * b_cols + j] = s;
33 }
34 }
35 Ok(out)
36}
37
38pub fn linear(x: &[f32], w: &[f32], out_f: usize, in_f: usize) -> Result<Vec<f32>, EngineError> {
40 if !x.len().is_multiple_of(in_f) {
41 return Err(EngineError::ShapeMismatch(format!(
42 "linear x len {} not divisible by in_f {in_f}",
43 x.len()
44 )));
45 }
46 if w.len() != out_f * in_f {
47 return Err(EngineError::ShapeMismatch(format!(
48 "linear weight length mismatch: got {} want out_f*in_f={out_f}*{in_f}={}",
49 w.len(),
50 out_f * in_f
51 )));
52 }
53 let batch = x.len() / in_f;
54 let mut out = vec![0.0f32; batch * out_f];
55 for b in 0..batch {
56 for o in 0..out_f {
57 let mut s = 0.0f32;
58 let wr = &w[o * in_f..(o + 1) * in_f];
59 let xr = &x[b * in_f..(b + 1) * in_f];
60 for i in 0..in_f {
61 s += xr[i] * wr[i];
62 }
63 out[b * out_f + o] = s;
64 }
65 }
66 Ok(out)
67}
68
69fn dot_f32(a: &[f32], b: &[f32]) -> f32 {
70 debug_assert_eq!(a.len(), b.len());
71 #[cfg(target_arch = "x86_64")]
72 {
73 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
74 return unsafe { dot_avx2(a, b) };
75 }
76 }
77 #[cfg(target_arch = "aarch64")]
78 {
79 unsafe { dot_neon(a, b) }
80 }
81 #[cfg(not(target_arch = "aarch64"))]
82 {
83 let mut s = 0.0f32;
84 for i in 0..a.len() {
85 s += a[i] * b[i];
86 }
87 s
88 }
89}
90
91#[cfg(target_arch = "x86_64")]
92#[target_feature(enable = "avx2,fma")]
93unsafe fn dot_avx2(a: &[f32], b: &[f32]) -> f32 {
94 use std::arch::x86_64::*;
95 let n = a.len();
96 let mut i = 0usize;
97 let mut acc = _mm256_setzero_ps();
98 while i + 8 <= n {
99 let va = _mm256_loadu_ps(a.as_ptr().add(i));
100 let vb = _mm256_loadu_ps(b.as_ptr().add(i));
101 acc = _mm256_fmadd_ps(va, vb, acc);
102 i += 8;
103 }
104 let mut tmp = [0.0f32; 8];
105 _mm256_storeu_ps(tmp.as_mut_ptr(), acc);
106 let mut s = tmp.iter().sum::<f32>();
107 while i < n {
108 s += a[i] * b[i];
109 i += 1;
110 }
111 s
112}
113
114#[cfg(target_arch = "aarch64")]
115unsafe fn dot_neon(a: &[f32], b: &[f32]) -> f32 {
116 use std::arch::aarch64::*;
117 let n = a.len();
118 let mut i = 0usize;
119 let mut acc = vdupq_n_f32(0.0);
120 while i + 4 <= n {
121 let va = vld1q_f32(a.as_ptr().add(i));
122 let vb = vld1q_f32(b.as_ptr().add(i));
123 acc = vfmaq_f32(acc, va, vb);
124 i += 4;
125 }
126 let mut tmp = [0.0f32; 4];
127 vst1q_f32(tmp.as_mut_ptr(), acc);
128 let mut s = tmp[0] + tmp[1] + tmp[2] + tmp[3];
129 while i < n {
130 s += a[i] * b[i];
131 i += 1;
132 }
133 s
134}
135
136pub fn linear_cpu(x: &[f32], w: &[f32], out_f: usize, in_f: usize) -> Result<Vec<f32>, EngineError> {
138 if !x.len().is_multiple_of(in_f) {
139 return Err(EngineError::ShapeMismatch(format!(
140 "linear x len {} not divisible by in_f {in_f}",
141 x.len()
142 )));
143 }
144 if w.len() != out_f * in_f {
145 return Err(EngineError::ShapeMismatch(format!(
146 "linear weight length mismatch: got {} want out_f*in_f={out_f}*{in_f}={}",
147 w.len(),
148 out_f * in_f
149 )));
150 }
151 let batch = x.len() / in_f;
152 let mut out = vec![0.0f32; batch * out_f];
153 if batch == 0 || out_f == 0 {
154 return Ok(out);
155 }
156 out.par_iter_mut().enumerate().for_each(|(idx, slot)| {
159 let b = idx / out_f;
160 let o = idx % out_f;
161 *slot = dot_f32(
162 &w[o * in_f..(o + 1) * in_f],
163 &x[b * in_f..(b + 1) * in_f],
164 );
165 });
166 Ok(out)
167}
168
169pub fn rms_norm(x: &[f32], weight: &[f32], eps: f32) -> Result<Vec<f32>, EngineError> {
170 let n = weight.len();
171 if n == 0 || !x.len().is_multiple_of(n) {
172 return Err(EngineError::ShapeMismatch(
173 "rms_norm: x length must be multiple of weight len".into(),
174 ));
175 }
176 let batch = x.len() / n;
177 let mut out = vec![0.0f32; x.len()];
178 for b in 0..batch {
179 let base = b * n;
180 let mut ms = 0.0f32;
181 for i in 0..n {
182 ms += x[base + i] * x[base + i];
183 }
184 let scale = (ms / n as f32 + eps).sqrt().recip();
185 for i in 0..n {
186 out[base + i] = x[base + i] * scale * weight[i];
187 }
188 }
189 Ok(out)
190}
191
192pub fn softmax_inplace(logits: &mut [f32]) {
193 if logits.is_empty() {
194 return;
195 }
196 let m = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
197 let mut sum = 0.0f32;
198 for v in logits.iter_mut() {
199 *v = (*v - m).exp();
200 sum += *v;
201 }
202 let inv = if sum > 0.0 { 1.0 / sum } else { 0.0 };
203 for v in logits.iter_mut() {
204 *v *= inv;
205 }
206}
207
208pub fn softmax(logits: &[f32]) -> Vec<f32> {
209 let mut o = logits.to_vec();
210 softmax_inplace(&mut o);
211 o
212}
213
214pub fn rope(x: &mut [f32], head_dim: usize, pos: usize, theta: f32) -> Result<(), EngineError> {
216 if head_dim == 0 || !head_dim.is_multiple_of(2) {
217 return Err(EngineError::ShapeMismatch(
218 "rope head_dim must be positive even".into(),
219 ));
220 }
221 if !x.len().is_multiple_of(head_dim) {
222 return Err(EngineError::ShapeMismatch(
223 "rope x len not divisible by head_dim".into(),
224 ));
225 }
226 let n_heads = x.len() / head_dim;
227 for h in 0..n_heads {
228 let base = h * head_dim;
229 for i in 0..(head_dim / 2) {
230 let freq = 1.0 / theta.powf((2 * i) as f32 / head_dim as f32);
231 let angle = pos as f32 * freq;
232 let (c, s) = (angle.cos(), angle.sin());
233 let u = x[base + 2 * i];
234 let v = x[base + 2 * i + 1];
235 x[base + 2 * i] = u * c - v * s;
236 x[base + 2 * i + 1] = u * s + v * c;
237 }
238 }
239 Ok(())
240}
241
242pub fn attention(
245 q: &[f32],
246 k_cache: &[f32],
247 v_cache: &[f32],
248 n_heads: usize,
249 n_kv_heads: usize,
250 head_dim: usize,
251) -> Result<Vec<f32>, EngineError> {
252 let scale = 1.0 / (head_dim as f32).sqrt();
253 attention_with_scale(q, k_cache, v_cache, n_heads, n_kv_heads, head_dim, scale)
254}
255
256fn sliding_kv_start(seq: usize, window: Option<usize>) -> usize {
257 match window {
258 Some(w) if w > 0 => seq.saturating_sub(w),
259 _ => 0,
260 }
261}
262
263pub fn kv_sliding_view<'a>(
265 k_cache: &'a [f32],
266 v_cache: &'a [f32],
267 kv_dim: usize,
268 window: Option<usize>,
269) -> Result<(&'a [f32], &'a [f32]), EngineError> {
270 if kv_dim == 0
271 || k_cache.len() != v_cache.len()
272 || !k_cache.len().is_multiple_of(kv_dim)
273 {
274 return Err(EngineError::ShapeMismatch(
275 "sliding kv view shape".into(),
276 ));
277 }
278 let seq = k_cache.len() / kv_dim;
279 let start = sliding_kv_start(seq, window);
280 Ok((
281 &k_cache[start * kv_dim..],
282 &v_cache[start * kv_dim..],
283 ))
284}
285
286pub fn attention_with_scale(
288 q: &[f32],
289 k_cache: &[f32],
290 v_cache: &[f32],
291 n_heads: usize,
292 n_kv_heads: usize,
293 head_dim: usize,
294 scale: f32,
295) -> Result<Vec<f32>, EngineError> {
296 if n_heads == 0 || head_dim == 0 || n_kv_heads == 0 || !n_heads.is_multiple_of(n_kv_heads) {
297 return Err(EngineError::ShapeMismatch(
298 "attention invalid head configuration".into(),
299 ));
300 }
301 let kv_dim = n_kv_heads * head_dim;
302 if q.len() != n_heads * head_dim {
303 return Err(EngineError::ShapeMismatch("attention q shape".into()));
304 }
305 if k_cache.len() != v_cache.len() || !k_cache.len().is_multiple_of(kv_dim) {
306 return Err(EngineError::ShapeMismatch(
307 "attention kv cache shape".into(),
308 ));
309 }
310 let seq = k_cache.len() / kv_dim;
311 let rep = n_heads / n_kv_heads;
312 let mut out = vec![0.0f32; n_heads * head_dim];
313 for h in 0..n_heads {
314 let kv_h = h / rep;
315 let qh = &q[h * head_dim..(h + 1) * head_dim];
316 let mut scores = vec![0.0f32; seq];
317 for t in 0..seq {
318 let kh = &k_cache[t * kv_dim + kv_h * head_dim..t * kv_dim + (kv_h + 1) * head_dim];
319 let mut dot = 0.0f32;
320 for i in 0..head_dim {
321 dot += qh[i] * kh[i];
322 }
323 scores[t] = dot * scale;
324 }
325 softmax_inplace(&mut scores);
326 let oh = &mut out[h * head_dim..(h + 1) * head_dim];
327 for t in 0..seq {
328 let vh = &v_cache[t * kv_dim + kv_h * head_dim..t * kv_dim + (kv_h + 1) * head_dim];
329 for i in 0..head_dim {
330 oh[i] += scores[t] * vh[i];
331 }
332 }
333 }
334 Ok(out)
335}
336
337pub fn attention_causal(
341 q: &[f32],
342 k_cache: &[f32],
343 v_cache: &[f32],
344 n_heads: usize,
345 n_kv_heads: usize,
346 head_dim: usize,
347) -> Result<Vec<f32>, EngineError> {
348 let q_dim = n_heads * head_dim;
349 if q_dim == 0 || !q.len().is_multiple_of(q_dim) {
350 return Err(EngineError::ShapeMismatch("attention_causal q shape".into()));
351 }
352 let seq_q = q.len() / q_dim;
353 if seq_q == 1 {
354 return attention(q, k_cache, v_cache, n_heads, n_kv_heads, head_dim);
355 }
356 let kv_dim = n_kv_heads * head_dim;
357 let seq_kv = k_cache.len() / kv_dim;
358 let causal = seq_q == seq_kv;
359 let mut out = vec![0.0f32; q.len()];
360 for tq in 0..seq_q {
361 let q_tok = &q[tq * q_dim..(tq + 1) * q_dim];
362 let k_end = if causal { tq + 1 } else { seq_kv };
363 let attn = attention(
364 q_tok,
365 &k_cache[..k_end * kv_dim],
366 &v_cache[..k_end * kv_dim],
367 n_heads,
368 n_kv_heads,
369 head_dim,
370 )?;
371 out[tq * q_dim..(tq + 1) * q_dim].copy_from_slice(&attn);
372 }
373 Ok(out)
374}
375
376#[allow(clippy::too_many_arguments)]
379pub fn attention_causal_with_scale(
380 q: &[f32],
381 k_cache: &[f32],
382 v_cache: &[f32],
383 n_heads: usize,
384 n_kv_heads: usize,
385 head_dim: usize,
386 scale: f32,
387 window: Option<usize>,
388) -> Result<Vec<f32>, EngineError> {
389 let q_dim = n_heads * head_dim;
390 if q_dim == 0 || !q.len().is_multiple_of(q_dim) {
391 return Err(EngineError::ShapeMismatch("attention_causal q shape".into()));
392 }
393 let seq_q = q.len() / q_dim;
394 let kv_dim = n_kv_heads * head_dim;
395 if seq_q == 1 {
396 let (k, v) = kv_sliding_view(k_cache, v_cache, kv_dim, window)?;
397 return attention_with_scale(q, k, v, n_heads, n_kv_heads, head_dim, scale);
398 }
399 let seq_kv = k_cache.len() / kv_dim;
400 let causal = seq_q == seq_kv;
401 let mut out = vec![0.0f32; q.len()];
402 for tq in 0..seq_q {
403 let q_tok = &q[tq * q_dim..(tq + 1) * q_dim];
404 let k_end = if causal { tq + 1 } else { seq_kv };
405 let (k, v) = kv_sliding_view(
406 &k_cache[..k_end * kv_dim],
407 &v_cache[..k_end * kv_dim],
408 kv_dim,
409 window,
410 )?;
411 let attn = attention_with_scale(q_tok, k, v, n_heads, n_kv_heads, head_dim, scale)?;
412 out[tq * q_dim..(tq + 1) * q_dim].copy_from_slice(&attn);
413 }
414 Ok(out)
415}
416
417pub fn swiglu(gate: &[f32], up: &[f32]) -> Result<Vec<f32>, EngineError> {
418 if gate.len() != up.len() {
419 return Err(EngineError::ShapeMismatch("swiglu length mismatch".into()));
420 }
421 Ok(gate
422 .iter()
423 .zip(up.iter())
424 .map(|(g, u)| {
425 let s = 1.0 / (1.0 + (-g).exp());
426 s * g * u
427 })
428 .collect())
429}
430
431pub fn short_conv_step(
436 x: &[f32],
437 w: &[f32],
438 state: &mut [f32],
439 hidden: usize,
440 kernel: usize,
441) -> Result<Vec<f32>, EngineError> {
442 if hidden == 0 || kernel == 0 {
443 return Err(EngineError::ShapeMismatch(
444 "short_conv_step: hidden and kernel must be > 0".into(),
445 ));
446 }
447 if x.len() != hidden {
448 return Err(EngineError::ShapeMismatch(
449 "short_conv_step: x len != hidden".into(),
450 ));
451 }
452 if w.len() != hidden * kernel {
453 return Err(EngineError::ShapeMismatch(
454 "short_conv_step: weight len != hidden*kernel".into(),
455 ));
456 }
457 let hist = kernel.saturating_sub(1);
458 if state.len() != hidden * hist {
459 return Err(EngineError::ShapeMismatch(
460 "short_conv_step: state len != hidden*(kernel-1)".into(),
461 ));
462 }
463 let mut out = vec![0.0f32; hidden];
464 for c in 0..hidden {
465 let mut acc = 0.0f32;
466 let wbase = c * kernel;
467 let sbase = c * hist;
468 for k in 0..hist {
469 acc += w[wbase + k] * state[sbase + k];
470 }
471 acc += w[wbase + hist] * x[c];
472 out[c] = acc;
473 if hist > 0 {
474 for k in 0..(hist - 1) {
475 state[sbase + k] = state[sbase + k + 1];
476 }
477 state[sbase + hist - 1] = x[c];
478 }
479 }
480 Ok(out)
481}
482
483pub fn moe_topk_route(
485 logits: &[f32],
486 top_k: usize,
487 use_sigmoid: bool,
488) -> Result<(Vec<usize>, Vec<f32>), EngineError> {
489 let n = logits.len();
490 if n == 0 || top_k == 0 {
491 return Err(EngineError::InvalidParam(
492 "moe_topk_route: num_experts and top_k must be > 0".into(),
493 ));
494 }
495 let k = top_k.min(n);
496 let scores: Vec<f32> = if use_sigmoid {
497 logits.iter().map(|x| 1.0 / (1.0 + (-x).exp())).collect()
498 } else {
499 let m = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
500 let mut exps: Vec<f32> = logits.iter().map(|x| (x - m).exp()).collect();
501 let s: f32 = exps.iter().sum();
502 if s > 0.0 {
503 for e in &mut exps {
504 *e /= s;
505 }
506 }
507 exps
508 };
509 let mut idx: Vec<usize> = (0..n).collect();
510 idx.sort_by(|&a, &b| {
511 scores[b]
512 .partial_cmp(&scores[a])
513 .unwrap_or(std::cmp::Ordering::Equal)
514 });
515 idx.truncate(k);
516 let mut weights: Vec<f32> = idx.iter().map(|&i| scores[i]).collect();
517 let sum: f32 = weights.iter().sum();
518 if sum > 0.0 {
519 for w in &mut weights {
520 *w /= sum;
521 }
522 }
523 Ok((idx, weights))
524}
525
526fn silu(x: f32) -> f32 {
527 x / (1.0 + (-x).exp())
528}
529
530pub fn silu_vec(x: &mut [f32]) {
532 for v in x.iter_mut() {
533 *v = silu(*v);
534 }
535}
536
537pub fn softplus(x: f32) -> f32 {
539 if x > 20.0 {
540 x
541 } else {
542 (1.0 + x.exp()).ln()
543 }
544}
545
546fn l2_normalize_inplace(x: &mut [f32]) {
547 let mut ss = 0.0f32;
548 for v in x.iter() {
549 ss += *v * *v;
550 }
551 let n = (ss + 1e-6).sqrt();
552 if n > 0.0 {
553 for v in x.iter_mut() {
554 *v /= n;
555 }
556 }
557}
558
559pub struct GatedDeltaStep<'a> {
561 pub q: &'a [f32],
562 pub k: &'a [f32],
563 pub v: &'a [f32],
564 pub g: &'a [f32],
565 pub beta: &'a [f32],
566 pub state: &'a mut [f32],
567 pub n_heads: usize,
568 pub dk: usize,
569 pub dv: usize,
570}
571
572pub fn gated_delta_step(p: GatedDeltaStep<'_>) -> Result<Vec<f32>, EngineError> {
577 let GatedDeltaStep {
578 q,
579 k,
580 v,
581 g,
582 beta,
583 state,
584 n_heads,
585 dk,
586 dv,
587 } = p;
588 if n_heads == 0 || dk == 0 || dv == 0 {
589 return Err(EngineError::ShapeMismatch(
590 "gated_delta_step: heads/dk/dv must be > 0".into(),
591 ));
592 }
593 if q.len() != n_heads * dk
594 || k.len() != n_heads * dk
595 || v.len() != n_heads * dv
596 || g.len() != n_heads
597 || beta.len() != n_heads
598 || state.len() != n_heads * dk * dv
599 {
600 return Err(EngineError::ShapeMismatch(
601 "gated_delta_step: q/k/v/g/beta/state shape mismatch".into(),
602 ));
603 }
604 let mut qq = q.to_vec();
605 let mut kk = k.to_vec();
606 for h in 0..n_heads {
607 l2_normalize_inplace(&mut qq[h * dk..(h + 1) * dk]);
608 l2_normalize_inplace(&mut kk[h * dk..(h + 1) * dk]);
609 }
610 let scale = (dk as f32).sqrt().recip();
611 let mut out = vec![0.0f32; n_heads * dv];
612 for h in 0..n_heads {
613 let sbase = h * dk * dv;
614 let gh = g[h];
615 for i in 0..dk * dv {
616 state[sbase + i] *= gh;
617 }
618 let mut kv_mem = vec![0.0f32; dv];
619 for i in 0..dk {
620 let kv = kk[h * dk + i];
621 for j in 0..dv {
622 kv_mem[j] += state[sbase + i * dv + j] * kv;
623 }
624 }
625 for j in 0..dv {
626 let delta = (v[h * dv + j] - kv_mem[j]) * beta[h];
627 for i in 0..dk {
628 state[sbase + i * dv + j] += kk[h * dk + i] * delta;
629 }
630 }
631 for j in 0..dv {
632 let mut o = 0.0f32;
633 for i in 0..dk {
634 o += state[sbase + i * dv + j] * qq[h * dk + i];
635 }
636 out[h * dv + j] = o * scale;
637 }
638 }
639 Ok(out)
640}
641
642pub fn geglu(gate: &[f32], up: &[f32]) -> Result<Vec<f32>, EngineError> {
644 if gate.len() != up.len() {
645 return Err(EngineError::ShapeMismatch("geglu length mismatch".into()));
646 }
647 Ok(gate
648 .iter()
649 .zip(up.iter())
650 .map(|(g, u)| gelu_pytorch_tanh(*g) * u)
651 .collect())
652}
653
654pub fn gelu_pytorch_tanh(x: f32) -> f32 {
656 const SQRT_2_OVER_PI: f32 = 0.797_884_6;
658 const COEFF: f32 = 0.044_715;
659 let inner = SQRT_2_OVER_PI * (x + COEFF * x * x * x);
660 0.5 * x * (1.0 + inner.tanh())
661}
662
663pub fn rms_norm_gemma(x: &[f32], weight: &[f32], eps: f32) -> Result<Vec<f32>, EngineError> {
665 let n = weight.len();
666 if n == 0 || !x.len().is_multiple_of(n) {
667 return Err(EngineError::ShapeMismatch(
668 "rms_norm_gemma: x length must be multiple of weight len".into(),
669 ));
670 }
671 let batch = x.len() / n;
672 let mut out = vec![0.0f32; x.len()];
673 for b in 0..batch {
674 let base = b * n;
675 let mut ms = 0.0f32;
676 for i in 0..n {
677 ms += x[base + i] * x[base + i];
678 }
679 let rrms = (ms / n as f32 + eps).sqrt().recip();
680 for i in 0..n {
681 out[base + i] = x[base + i] * rrms * (1.0 + weight[i]);
682 }
683 }
684 Ok(out)
685}
686
687pub fn rope_half(
689 x: &mut [f32],
690 head_dim: usize,
691 pos: usize,
692 theta: f32,
693) -> Result<(), EngineError> {
694 if head_dim == 0 || !head_dim.is_multiple_of(2) {
695 return Err(EngineError::ShapeMismatch(
696 "rope_half head_dim must be positive even".into(),
697 ));
698 }
699 if !x.len().is_multiple_of(head_dim) {
700 return Err(EngineError::ShapeMismatch(
701 "rope_half x len not divisible by head_dim".into(),
702 ));
703 }
704 let half = head_dim / 2;
705 let n_heads = x.len() / head_dim;
706 for h in 0..n_heads {
707 let base = h * head_dim;
708 for i in 0..half {
709 let freq = 1.0 / theta.powf((2 * i) as f32 / head_dim as f32);
710 let angle = pos as f32 * freq;
711 let (c, s) = (angle.cos(), angle.sin());
712 let u = x[base + i];
713 let v = x[base + i + half];
714 x[base + i] = u * c - v * s;
715 x[base + i + half] = u * s + v * c;
716 }
717 }
718 Ok(())
719}
720
721pub fn rope_half_partial(
723 x: &mut [f32],
724 head_dim: usize,
725 rotary_dim: usize,
726 pos: usize,
727 theta: f32,
728) -> Result<(), EngineError> {
729 if rotary_dim == 0 || rotary_dim > head_dim || !rotary_dim.is_multiple_of(2) {
730 return Err(EngineError::ShapeMismatch(
731 "rope_half_partial rotary_dim must be positive even and <= head_dim".into(),
732 ));
733 }
734 if rotary_dim == head_dim {
735 return rope_half(x, head_dim, pos, theta);
736 }
737 if !x.len().is_multiple_of(head_dim) {
738 return Err(EngineError::ShapeMismatch(
739 "rope_half_partial x len not divisible by head_dim".into(),
740 ));
741 }
742 let n_heads = x.len() / head_dim;
743 for h in 0..n_heads {
744 let sl = &mut x[h * head_dim..h * head_dim + rotary_dim];
745 rope_half(sl, rotary_dim, pos, theta)?;
746 }
747 Ok(())
748}
749
750pub fn rope_half_proportional(
754 x: &mut [f32],
755 head_dim: usize,
756 factor: f32,
757 pos: usize,
758 theta: f32,
759) -> Result<(), EngineError> {
760 if !(0.0..=1.0).contains(&factor) {
761 return Err(EngineError::ShapeMismatch(
762 "rope_half_proportional factor must be in [0, 1]".into(),
763 ));
764 }
765 if (factor - 1.0).abs() < 1e-6 {
766 return rope_half(x, head_dim, pos, theta);
767 }
768 if head_dim == 0 || !head_dim.is_multiple_of(2) {
769 return Err(EngineError::ShapeMismatch(
770 "rope_half_proportional head_dim must be positive even".into(),
771 ));
772 }
773 if !x.len().is_multiple_of(head_dim) {
774 return Err(EngineError::ShapeMismatch(
775 "rope_half_proportional x len not divisible by head_dim".into(),
776 ));
777 }
778 let half = head_dim / 2;
779 let rope_angles = (factor * head_dim as f32 / 2.0) as usize;
780 if rope_angles == 0 {
781 return Ok(());
782 }
783 let n_heads = x.len() / head_dim;
784 for h in 0..n_heads {
785 let base = h * head_dim;
786 for i in 0..rope_angles.min(half) {
787 let freq = 1.0 / theta.powf((2 * i) as f32 / head_dim as f32);
788 let angle = pos as f32 * freq;
789 let (c, s) = (angle.cos(), angle.sin());
790 let u = x[base + i];
791 let v = x[base + i + half];
792 x[base + i] = u * c - v * s;
793 x[base + i + half] = u * s + v * c;
794 }
795 }
796 Ok(())
797}
798
799pub fn hdm_linear(
805 x: &[f32],
806 w_rot: &[f32],
807 out_f: usize,
808 in_f: usize,
809 hadamard_seed: Option<i64>,
810) -> Result<Vec<f32>, EngineError> {
811 let mut y = linear(x, w_rot, out_f, in_f)?;
812 if out_f == 0 || !y.len().is_multiple_of(out_f) {
813 return Err(EngineError::ShapeMismatch(
814 "hdm_linear output not divisible by out_f".into(),
815 ));
816 }
817 let batch = y.len() / out_f;
818 for b in 0..batch {
819 let sl = b * out_f..(b + 1) * out_f;
820 hadamard_blocked_vec(&mut y[sl], hadamard_seed, true)?;
821 }
822 Ok(y)
823}
824
825pub fn fwht(x: &mut [f32]) -> Result<(), EngineError> {
827 let n = x.len();
828 if n == 0 || !n.is_power_of_two() {
829 return Err(EngineError::ShapeMismatch(
830 "fwht length must be power of two".into(),
831 ));
832 }
833 if n == 1 {
834 return Ok(());
835 }
836 let mut h = 1usize;
837 while h < n {
838 for i in (0..n).step_by(h * 2) {
839 for j in i..(i + h) {
840 let a = x[j];
841 let b = x[j + h];
842 x[j] = a + b;
843 x[j + h] = a - b;
844 }
845 }
846 h *= 2;
847 }
848 let scale = 1.0 / (n as f32).sqrt();
849 for v in x.iter_mut() {
850 *v *= scale;
851 }
852 Ok(())
853}
854
855pub fn pow2_tile_sizes(k: usize) -> Result<Vec<usize>, EngineError> {
857 if k == 0 {
858 return Err(EngineError::ShapeMismatch(
859 "pow2_tile_sizes expects k>=1".into(),
860 ));
861 }
862 let mut sizes = Vec::new();
863 let mut rem = k;
864 while rem > 0 {
865 let mut b = 1usize;
866 while (b << 1) <= rem {
867 b <<= 1;
868 }
869 sizes.push(b);
870 rem -= b;
871 }
872 Ok(sizes)
873}
874
875pub fn portable_block_signs(seed: i64, start: usize, size: usize) -> Vec<f32> {
877 let mut signs = vec![0.0f32; size];
878 let mut state = (seed as u64) ^ ((start as u64).wrapping_mul(0x9E3779B97F4A7C15));
879 for s in signs.iter_mut() {
880 state = state.wrapping_add(0x9E3779B97F4A7C15);
881 let mut z = state;
882 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
883 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
884 z ^= z >> 31;
885 *s = if (z & 1) == 0 { 1.0 } else { -1.0 };
886 }
887 signs
888}
889
890pub fn hadamard_blocked_rows(
895 data: &mut [f32],
896 rows: usize,
897 cols: usize,
898 seed: Option<i64>,
899 inverse: bool,
900) -> Result<(), EngineError> {
901 let sizes = pow2_tile_sizes(rows)?;
902 hadamard_blocked_rows_tiles(data, rows, cols, seed, inverse, &sizes)
903}
904
905pub fn hadamard_blocked_rows_tiles(
907 data: &mut [f32],
908 rows: usize,
909 cols: usize,
910 seed: Option<i64>,
911 inverse: bool,
912 sizes: &[usize],
913) -> Result<(), EngineError> {
914 if rows == 0 || cols == 0 || data.len() != rows * cols {
915 return Err(EngineError::ShapeMismatch(
916 "hadamard_blocked_rows shape mismatch".into(),
917 ));
918 }
919 let covered: usize = sizes.iter().copied().sum();
920 if covered != rows || sizes.iter().any(|&s| s == 0) {
921 return Err(EngineError::ShapeMismatch(format!(
922 "hadamard tiles {:?} cover {covered} != rows {rows}",
923 sizes
924 )));
925 }
926 let mut start = 0usize;
927 for &sz in sizes {
928 let signs = seed.map(|s| portable_block_signs(s, start, sz));
929 let mut work = vec![0.0f32; sz * cols];
931 for r in 0..sz {
932 let src = (start + r) * cols;
933 work[r * cols..(r + 1) * cols].copy_from_slice(&data[src..src + cols]);
934 }
935 let col_out: Result<Vec<Vec<f32>>, EngineError> = (0..cols)
936 .into_par_iter()
937 .map(|c| {
938 let mut colbuf = vec![0.0f32; sz];
939 for r in 0..sz {
940 colbuf[r] = work[r * cols + c];
941 }
942 if let Some(ref sg) = signs {
943 if !inverse {
944 for r in 0..sz {
945 colbuf[r] *= sg[r];
946 }
947 fwht(&mut colbuf)?;
948 } else {
949 fwht(&mut colbuf)?;
950 for r in 0..sz {
951 colbuf[r] *= sg[r];
952 }
953 }
954 } else if sz > 1 {
955 fwht(&mut colbuf)?;
956 }
957 Ok(colbuf)
958 })
959 .collect();
960 let col_out = col_out?;
961 for c in 0..cols {
962 for r in 0..sz {
963 work[r * cols + c] = col_out[c][r];
964 }
965 }
966 for r in 0..sz {
967 let dst = (start + r) * cols;
968 data[dst..dst + cols].copy_from_slice(&work[r * cols..(r + 1) * cols]);
969 }
970 start += sz;
971 }
972 Ok(())
973}
974
975pub fn hadamard_blocked_vec(
977 x: &mut [f32],
978 seed: Option<i64>,
979 inverse: bool,
980) -> Result<(), EngineError> {
981 let rows = x.len();
982 hadamard_blocked_rows(x, rows, 1, seed, inverse)
983}
984
985pub fn dequant_lookup_group(
987 indices: &[u8],
988 codebook: &[f32],
989 num_groups: usize,
990 group_size: usize,
991 n: usize,
992 kc: usize,
993 k0: usize,
994) -> Result<Vec<f32>, EngineError> {
995 let k_work = num_groups * group_size;
996 if indices.len() != k_work * n {
997 return Err(EngineError::ShapeMismatch(
998 "dequant indices length mismatch".into(),
999 ));
1000 }
1001 if codebook.len() != num_groups * kc {
1002 return Err(EngineError::Quant(
1003 "dequant codebook length mismatch".into(),
1004 ));
1005 }
1006 let mut out = vec![0.0f32; k_work * n];
1007 for g in 0..num_groups {
1008 let cb = &codebook[g * kc..(g + 1) * kc];
1009 for r in 0..group_size {
1010 let row = g * group_size + r;
1011 for j in 0..n {
1012 let idx = indices[row * n + j] as usize;
1013 if idx >= kc {
1014 return Err(EngineError::Quant(format!("index {idx} >= kc {kc}")));
1015 }
1016 out[row * n + j] = cb[idx];
1017 }
1018 }
1019 }
1020 out.truncate(k0 * n);
1021 Ok(out)
1022}
1023
1024pub fn matmul_blocked(
1026 a: &[f32],
1027 a_rows: usize,
1028 a_cols: usize,
1029 b: &[f32],
1030 b_rows: usize,
1031 b_cols: usize,
1032 block: usize,
1033) -> Result<Vec<f32>, EngineError> {
1034 if a_cols != b_rows {
1035 return Err(EngineError::ShapeMismatch(format!(
1036 "matmul inner dim {a_cols} != {b_rows}"
1037 )));
1038 }
1039 if a.len() != a_rows * a_cols || b.len() != b_rows * b_cols {
1040 return Err(EngineError::ShapeMismatch(
1041 "matmul buffer length does not match shape".into(),
1042 ));
1043 }
1044 let block = block.max(1);
1045 let mut out = vec![0.0f32; a_rows * b_cols];
1046 for i0 in (0..a_rows).step_by(block) {
1047 for j0 in (0..b_cols).step_by(block) {
1048 for k0 in (0..a_cols).step_by(block) {
1049 let i_max = (i0 + block).min(a_rows);
1050 let j_max = (j0 + block).min(b_cols);
1051 let k_max = (k0 + block).min(a_cols);
1052 for i in i0..i_max {
1053 for j in j0..j_max {
1054 let mut s = out[i * b_cols + j];
1055 for k in k0..k_max {
1056 s += a[i * a_cols + k] * b[k * b_cols + j];
1057 }
1058 out[i * b_cols + j] = s;
1059 }
1060 }
1061 }
1062 }
1063 }
1064 Ok(out)
1065}
1066
1067pub fn matmul_dispatch(
1070 a: &[f32],
1071 a_rows: usize,
1072 a_cols: usize,
1073 b: &[f32],
1074 b_rows: usize,
1075 b_cols: usize,
1076 mode: SimdMode,
1077) -> Result<Vec<f32>, EngineError> {
1078 match mode {
1079 SimdMode::Scalar => matmul(a, a_rows, a_cols, b, b_rows, b_cols, SimdMode::Scalar),
1080 SimdMode::Neon | SimdMode::Avx2 => matmul_blocked(a, a_rows, a_cols, b, b_rows, b_cols, 8),
1081 }
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086 use super::*;
1087
1088 #[test]
1089 fn matmul_ok() {
1090 let a = [1.0f32, 2.0, 3.0, 4.0]; let b = [1.0f32, 0.0, 0.0, 1.0];
1092 let c = matmul(&a, 2, 2, &b, 2, 2, SimdMode::Scalar).unwrap();
1093 assert_eq!(c, vec![1.0, 2.0, 3.0, 4.0]);
1094 }
1095
1096 #[test]
1097 fn matmul_shape_err() {
1098 let err = matmul(&[1.0], 1, 1, &[1.0, 2.0], 2, 1, SimdMode::Scalar).unwrap_err();
1099 assert!(matches!(err, EngineError::ShapeMismatch(_)));
1100 }
1101
1102 #[test]
1103 fn rms_and_softmax() {
1104 let w = [1.0f32, 1.0];
1105 let y = rms_norm(&[3.0, 4.0], &w, 1e-6).unwrap();
1106 let rms = (12.5f32).sqrt();
1108 assert!((y[0] - 3.0 / rms).abs() < 1e-4);
1109 let s = softmax(&[1.0, 2.0, 3.0]);
1110 let sum: f32 = s.iter().sum();
1111 assert!((sum - 1.0).abs() < 1e-5);
1112 }
1113
1114 #[test]
1115 fn fwht_roundtrip_ish() {
1116 let mut x = [1.0f32, 2.0, 3.0, 4.0];
1117 let orig = x;
1118 fwht(&mut x).unwrap();
1119 fwht(&mut x).unwrap();
1120 for (a, b) in x.iter().zip(orig.iter()) {
1121 assert!((a - b).abs() < 1e-4);
1122 }
1123 }
1124
1125 #[test]
1126 fn pow2_tiles() {
1127 assert_eq!(pow2_tile_sizes(10).unwrap(), vec![8, 2]);
1128 assert_eq!(pow2_tile_sizes(3072).unwrap(), vec![2048, 1024]);
1129 assert_eq!(pow2_tile_sizes(64).unwrap(), vec![64]);
1130 assert_eq!(pow2_tile_sizes(1).unwrap(), vec![1]);
1131 assert_eq!(pow2_tile_sizes(151936).unwrap()[0], 131072);
1132 assert!(matches!(
1133 pow2_tile_sizes(0),
1134 Err(EngineError::ShapeMismatch(_))
1135 ));
1136 }
1137
1138 #[test]
1139 fn blocked_roundtrip_non_pow2() {
1140 let rows = 10usize;
1141 let cols = 3usize;
1142 let mut w: Vec<f32> = (0..rows * cols).map(|i| (i as f32) * 0.1 - 0.5).collect();
1143 let orig = w.clone();
1144 hadamard_blocked_rows(&mut w, rows, cols, Some(7), false).unwrap();
1145 let mut twice = w.clone();
1147 hadamard_blocked_rows(&mut twice, rows, cols, Some(7), false).unwrap();
1148 let mut err_wrong = 0.0f32;
1149 for (a, b) in twice.iter().zip(orig.iter()) {
1150 err_wrong += (a - b).abs();
1151 }
1152 assert!(err_wrong > 1.0, "second forward unexpectedly near identity");
1153 hadamard_blocked_rows(&mut w, rows, cols, Some(7), true).unwrap();
1154 for (a, b) in w.iter().zip(orig.iter()) {
1155 assert!((a - b).abs() < 1e-4, "{a} vs {b}");
1156 }
1157 }
1158
1159 #[test]
1160 fn blocked_roundtrip_unsigned_and_pow2() {
1161 let rows = 16usize;
1162 let cols = 5usize;
1163 let mut w: Vec<f32> = (0..rows * cols).map(|i| (i as f32) * 0.03).collect();
1164 let orig = w.clone();
1165 hadamard_blocked_rows(&mut w, rows, cols, None, false).unwrap();
1166 hadamard_blocked_rows(&mut w, rows, cols, None, true).unwrap();
1167 for (a, b) in w.iter().zip(orig.iter()) {
1168 assert!((a - b).abs() < 1e-4);
1169 }
1170 }
1171
1172 #[test]
1173 fn blocked_matches_python_golden() {
1174 let rows = 10usize;
1176 let cols = 3usize;
1177 let mut w: Vec<f32> = (0..rows * cols).map(|i| (i as f32) * 0.1 - 0.5).collect();
1178 hadamard_blocked_rows(&mut w, rows, cols, Some(7), false).unwrap();
1179 let golden: [f32; 30] = [
1181 0.919239,
1182 0.989949,
1183 1.060_66,
1184 0.919239,
1185 0.989950,
1186 1.060_66,
1187 -0.919239,
1188 -0.989949,
1189 -1.060_66,
1190 0.777817,
1191 std::f32::consts::FRAC_1_SQRT_2,
1192 0.636396,
1193 -0.919239,
1194 -0.989949,
1195 -1.060_66,
1196 -0.070711,
1197 -0.141421,
1198 -0.212132,
1199 1.343503,
1200 std::f32::consts::SQRT_2,
1201 1.484924,
1202 -0.636396,
1203 -0.848528,
1204 -1.060_66,
1205 -2.899138,
1206 -3.040559,
1207 -3.181_98,
1208 0.212132,
1209 0.212132,
1210 0.212132,
1211 ];
1212 assert_eq!(w.len(), golden.len());
1213 for (a, b) in w.iter().zip(golden.iter()) {
1214 assert!((a - b).abs() < 1e-4, "{a} vs {b}");
1215 }
1216 assert_eq!(
1217 portable_block_signs(7, 0, 8),
1218 vec![-1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0]
1219 );
1220 assert_eq!(portable_block_signs(7, 8, 2), vec![-1.0, -1.0]);
1221 }
1222
1223 #[test]
1224 fn portable_signs_stable() {
1225 let a = portable_block_signs(0, 0, 8);
1226 let b = portable_block_signs(0, 0, 8);
1227 assert_eq!(a, b);
1228 let golden = [-1.0f32, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0];
1230 assert_eq!(a, golden);
1231 }
1232
1233 #[test]
1234 fn hadamard_blocked_shape_errors() {
1235 let mut w = [1.0f32, 2.0];
1236 assert!(matches!(
1237 hadamard_blocked_rows(&mut w, 2, 2, None, false),
1238 Err(EngineError::ShapeMismatch(_))
1239 ));
1240 assert!(matches!(
1241 hadamard_blocked_rows(&mut [], 0, 1, None, false),
1242 Err(EngineError::ShapeMismatch(_))
1243 ));
1244 }
1245
1246 #[test]
1247 fn hadamard_blocked_vec_roundtrip() {
1248 let mut x: Vec<f32> = (0..10).map(|i| (i as f32) * 0.2 - 1.0).collect();
1249 let orig = x.clone();
1250 hadamard_blocked_vec(&mut x, Some(11), false).unwrap();
1251 hadamard_blocked_vec(&mut x, Some(11), true).unwrap();
1252 for (a, b) in x.iter().zip(orig.iter()) {
1253 assert!((a - b).abs() < 1e-4);
1254 }
1255 }
1256
1257 #[test]
1258 fn dequant_group() {
1259 let indices = [0u8, 1, 1, 0];
1261 let codebook = [10.0f32, 20.0];
1262 let out = dequant_lookup_group(&indices, &codebook, 1, 2, 2, 2, 2).unwrap();
1263 assert_eq!(out, vec![10.0, 20.0, 20.0, 10.0]);
1264 }
1265
1266 #[test]
1267 fn neon_scalar_matmul_parity() {
1268 let a: Vec<f32> = (0..64).map(|i| (i as f32) * 0.01).collect();
1269 let b: Vec<f32> = (0..64).map(|i| (i as f32) * 0.02 - 0.5).collect();
1270 let s = matmul_dispatch(&a, 8, 8, &b, 8, 8, SimdMode::Scalar).unwrap();
1271 let n = matmul_dispatch(&a, 8, 8, &b, 8, 8, SimdMode::Neon).unwrap();
1272 assert_eq!(s.len(), n.len());
1273 for (x, y) in s.iter().zip(n.iter()) {
1274 assert!((x - y).abs() < 1e-5, "{x} vs {y}");
1275 }
1276 }
1277
1278 #[test]
1279 fn short_conv_step_and_moe_route() {
1280 let hidden = 2usize;
1281 let kernel = 3usize;
1282 let w = vec![0.0f32, 0.0, 1.0, 0.0, 0.0, 1.0];
1284 let mut state = vec![0.0f32; hidden * (kernel - 1)];
1285 let x = [3.0f32, 5.0];
1286 let y = short_conv_step(&x, &w, &mut state, hidden, kernel).unwrap();
1287 assert!((y[0] - 3.0).abs() < 1e-5);
1288 assert!((y[1] - 5.0).abs() < 1e-5);
1289 let w_old = vec![1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0];
1291 let y2 = short_conv_step(&[1.0, 2.0], &w_old, &mut state, hidden, kernel).unwrap();
1292 assert!((y2[0] - 0.0).abs() < 1e-5);
1293 assert!((y2[1] - 0.0).abs() < 1e-5);
1294
1295 let (ids, ws) = moe_topk_route(&[0.1, 2.0, 0.5, -1.0], 2, false).unwrap();
1296 assert_eq!(ids.len(), 2);
1297 assert_eq!(ids[0], 1);
1298 assert!((ws.iter().sum::<f32>() - 1.0).abs() < 1e-5);
1299 let (ids_s, _) = moe_topk_route(&[0.0, 10.0, 0.0], 1, true).unwrap();
1300 assert_eq!(ids_s, vec![1]);
1301 }
1302
1303 #[test]
1304 fn gated_delta_step_updates_state() {
1305 let n_heads = 1usize;
1306 let dk = 2usize;
1307 let dv = 2usize;
1308 let q = [1.0f32, 0.0];
1309 let k = [1.0f32, 0.0];
1310 let v = [0.5f32, -0.25];
1311 let g = [0.9f32];
1312 let beta = [1.0f32];
1313 let mut s = vec![0.0f32; n_heads * dk * dv];
1314 let o1 = gated_delta_step(GatedDeltaStep {
1315 q: &q,
1316 k: &k,
1317 v: &v,
1318 g: &g,
1319 beta: &beta,
1320 state: &mut s,
1321 n_heads,
1322 dk,
1323 dv,
1324 })
1325 .unwrap();
1326 assert_eq!(o1.len(), dv);
1327 let s_after = s.clone();
1328 let o2 = gated_delta_step(GatedDeltaStep {
1329 q: &q,
1330 k: &k,
1331 v: &v,
1332 g: &g,
1333 beta: &beta,
1334 state: &mut s,
1335 n_heads,
1336 dk,
1337 dv,
1338 })
1339 .unwrap();
1340 assert!(s != s_after || (o2[0] - o1[0]).abs() > 0.0);
1341 }
1342
1343 #[test]
1344 fn geglu_and_rms_norm_gemma() {
1345 let gate = [0.5f32, -1.0];
1346 let up = [2.0f32, 3.0];
1347 let y = geglu(&gate, &up).unwrap();
1348 assert!((y[0] - gelu_pytorch_tanh(0.5) * 2.0).abs() < 1e-5);
1349 assert!((y[1] - gelu_pytorch_tanh(-1.0) * 3.0).abs() < 1e-5);
1350 let x = [1.0f32, -1.0, 2.0, 0.0];
1351 let w = [0.1f32, 0.2];
1352 let n = rms_norm_gemma(&x, &w, 1e-6).unwrap();
1353 assert_eq!(n.len(), 4);
1354 let plain = rms_norm(&x, &w, 1e-6).unwrap();
1356 assert!((n[0] - plain[0]).abs() > 1e-4 || (n[1] - plain[1]).abs() > 1e-4);
1357 }
1358
1359 #[test]
1360 fn rope_half_layout() {
1361 let mut x = [1.0f32, 2.0, 3.0, 4.0];
1362 rope_half(&mut x, 4, 1, 10000.0).unwrap();
1363 let freq0 = 1.0 / 10000f32.powf(0.0);
1365 let (c0, s0) = (freq0.cos(), freq0.sin());
1366 let freq1 = 1.0 / 10000f32.powf(2.0 / 4.0);
1367 let (c1, s1) = (freq1.cos(), freq1.sin());
1368 assert!((x[0] - (1.0 * c0 - 3.0 * s0)).abs() < 1e-5);
1369 assert!((x[2] - (1.0 * s0 + 3.0 * c0)).abs() < 1e-5);
1370 assert!((x[1] - (2.0 * c1 - 4.0 * s1)).abs() < 1e-5);
1371 assert!((x[3] - (2.0 * s1 + 4.0 * c1)).abs() < 1e-5);
1372 assert!(matches!(
1373 rope_half(&mut [1.0, 2.0, 3.0], 3, 0, 10000.0),
1374 Err(EngineError::ShapeMismatch(_))
1375 ));
1376 }
1377
1378 #[test]
1379 fn rope_half_partial_full_matches_rope_half() {
1380 let mut a = [1.0f32, 2.0, 3.0, 4.0];
1381 let mut b = a;
1382 rope_half(&mut a, 4, 2, 10000.0).unwrap();
1383 rope_half_partial(&mut b, 4, 4, 2, 10000.0).unwrap();
1384 for (x, y) in a.iter().zip(b.iter()) {
1385 assert!((x - y).abs() < 1e-6);
1386 }
1387 let mut c = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1388 let tail = [5.0f32, 6.0, 7.0, 8.0];
1389 rope_half_partial(&mut c, 8, 4, 1, 10000.0).unwrap();
1390 assert_eq!(&c[4..], &tail);
1391 }
1392
1393 #[test]
1394 fn rope_half_proportional_identity_tail() {
1395 let mut full = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1396 let orig = full;
1397 rope_half(&mut full, 8, 3, 10000.0).unwrap();
1398 let mut prop = orig;
1399 rope_half_proportional(&mut prop, 8, 1.0, 3, 10000.0).unwrap();
1400 for (a, b) in full.iter().zip(prop.iter()) {
1401 assert!((a - b).abs() < 1e-6);
1402 }
1403 let mut half = orig;
1404 rope_half_proportional(&mut half, 8, 0.25, 3, 10000.0).unwrap();
1405 assert!((half[0] - orig[0]).abs() > 1e-6);
1407 assert!((half[4] - orig[4]).abs() > 1e-6);
1408 assert_eq!(&half[1..4], &orig[1..4]);
1409 assert_eq!(&half[5..], &orig[5..]);
1410 }
1411
1412 #[test]
1413 fn hdm_linear_matches_unrotated_weight() {
1414 let out_f = 8usize;
1415 let in_f = 4usize;
1416 let seed = Some(7i64);
1417 let mut w_orig: Vec<f32> = (0..out_f * in_f).map(|i| (i as f32) * 0.05 - 0.2).collect();
1418 let x: Vec<f32> = (0..in_f).map(|i| (i as f32) * 0.1).collect();
1419 let y_ref = linear(&x, &w_orig, out_f, in_f).unwrap();
1420 hadamard_blocked_rows(&mut w_orig, out_f, in_f, seed, false).unwrap();
1421 let y = hdm_linear(&x, &w_orig, out_f, in_f, seed).unwrap();
1422 for (a, b) in y.iter().zip(y_ref.iter()) {
1423 assert!((a - b).abs() < 1e-4, "{a} vs {b}");
1424 }
1425 }
1426
1427 #[test]
1428 fn linear_cpu_matches_scalar_linear() {
1429 let out_f = 7usize;
1430 let in_f = 5usize;
1431 let w: Vec<f32> = (0..out_f * in_f).map(|i| (i as f32) * 0.02 - 0.1).collect();
1432 let x: Vec<f32> = (0..in_f * 3).map(|i| (i as f32) * 0.03).collect();
1433 let a = linear(&x, &w, out_f, in_f).unwrap();
1434 let b = linear_cpu(&x, &w, out_f, in_f).unwrap();
1435 for (x, y) in a.iter().zip(b.iter()) {
1436 assert!((x - y).abs() < 1e-4, "{x} vs {y}");
1437 }
1438 }
1439
1440 #[test]
1441 fn attention_causal_matches_stepwise() {
1442 let n_heads = 2usize;
1443 let n_kv = 1usize;
1444 let head_dim = 4usize;
1445 let seq = 3usize;
1446 let q_dim = n_heads * head_dim;
1447 let kv_dim = n_kv * head_dim;
1448 let q: Vec<f32> = (0..seq * q_dim).map(|i| (i as f32) * 0.01).collect();
1449 let k: Vec<f32> = (0..seq * kv_dim).map(|i| (i as f32) * 0.02).collect();
1450 let v: Vec<f32> = (0..seq * kv_dim).map(|i| (i as f32) * 0.03).collect();
1451 let batched = attention_causal(&q, &k, &v, n_heads, n_kv, head_dim).unwrap();
1452 let mut step = Vec::new();
1453 for t in 0..seq {
1454 let qi = &q[t * q_dim..(t + 1) * q_dim];
1455 let a = attention(
1456 qi,
1457 &k[..(t + 1) * kv_dim],
1458 &v[..(t + 1) * kv_dim],
1459 n_heads,
1460 n_kv,
1461 head_dim,
1462 )
1463 .unwrap();
1464 step.extend_from_slice(&a);
1465 }
1466 for (a, b) in batched.iter().zip(step.iter()) {
1467 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
1468 }
1469 }
1470
1471 #[test]
1472 fn sliding_window_matches_truncated_kv() {
1473 let n_heads = 2usize;
1474 let n_kv = 1usize;
1475 let head_dim = 4usize;
1476 let seq = 6usize;
1477 let window = 2usize;
1478 let q_dim = n_heads * head_dim;
1479 let kv_dim = n_kv * head_dim;
1480 let scale = 1.0 / (head_dim as f32).sqrt();
1481 let q: Vec<f32> = (0..seq * q_dim).map(|i| (i as f32) * 0.01).collect();
1482 let k: Vec<f32> = (0..seq * kv_dim).map(|i| (i as f32) * 0.02).collect();
1483 let v: Vec<f32> = (0..seq * kv_dim).map(|i| (i as f32) * 0.03).collect();
1484
1485 let wide = attention_causal_with_scale(&q, &k, &v, n_heads, n_kv, head_dim, scale, None)
1486 .unwrap();
1487 let win = attention_causal_with_scale(
1488 &q,
1489 &k,
1490 &v,
1491 n_heads,
1492 n_kv,
1493 head_dim,
1494 scale,
1495 Some(window),
1496 )
1497 .unwrap();
1498 for i in 0..window * q_dim {
1500 assert!((wide[i] - win[i]).abs() < 1e-6, "prefix {i}");
1501 }
1502 assert!(
1503 wide.iter()
1504 .zip(win.iter())
1505 .any(|(a, b)| (a - b).abs() > 1e-5),
1506 "window must change scores once seq > window"
1507 );
1508
1509 let last_q = &q[(seq - 1) * q_dim..];
1510 let start = seq - window;
1511 let sliced = attention_with_scale(
1512 last_q,
1513 &k[start * kv_dim..],
1514 &v[start * kv_dim..],
1515 n_heads,
1516 n_kv,
1517 head_dim,
1518 scale,
1519 )
1520 .unwrap();
1521 let (k_win, v_win) = kv_sliding_view(&k, &v, kv_dim, Some(window)).unwrap();
1522 let via_window =
1523 attention_with_scale(last_q, k_win, v_win, n_heads, n_kv, head_dim, scale).unwrap();
1524 for (a, b) in sliced.iter().zip(via_window.iter()) {
1525 assert!((a - b).abs() < 1e-6, "{a} vs {b}");
1526 }
1527 let (k_noop, v_noop) = kv_sliding_view(&k, &v, kv_dim, Some(seq + 8)).unwrap();
1528 let noop =
1529 attention_with_scale(last_q, k_noop, v_noop, n_heads, n_kv, head_dim, scale).unwrap();
1530 let full = attention_with_scale(last_q, &k, &v, n_heads, n_kv, head_dim, scale).unwrap();
1531 for (a, b) in noop.iter().zip(full.iter()) {
1532 assert!((a - b).abs() < 1e-6);
1533 }
1534 }
1535
1536 #[test]
1537 fn embedding_row_gather_needs_full_matrix_unrotate() {
1538 let vocab = 8usize;
1539 let hidden = 4usize;
1540 let seed = Some(7i64);
1541 let tid = 3usize;
1542 let mut w: Vec<f32> = (0..vocab * hidden)
1543 .map(|i| (i as f32) * 0.05 - 0.2)
1544 .collect();
1545 let orig = w[tid * hidden..(tid + 1) * hidden].to_vec();
1546 hadamard_blocked_rows(&mut w, vocab, hidden, seed, false).unwrap();
1547 let rotated_row = &w[tid * hidden..(tid + 1) * hidden];
1548 let drift: f32 = orig
1549 .iter()
1550 .zip(rotated_row.iter())
1551 .map(|(a, b)| (a - b).abs())
1552 .sum();
1553 assert!(
1554 drift > 1e-3,
1555 "axis-0 Hadamard must mix vocab rows; gather of W_rot[token] is wrong"
1556 );
1557 hadamard_blocked_rows(&mut w, vocab, hidden, seed, true).unwrap();
1558 for (a, b) in orig.iter().zip(w[tid * hidden..(tid + 1) * hidden].iter()) {
1559 assert!((a - b).abs() < 1e-4, "{a} vs {b}");
1560 }
1561 }
1562
1563 #[test]
1564 fn vocab_table_non_pow2_row_gather_after_unrotate() {
1565 let vocab = 10usize;
1568 let hidden = 6usize;
1569 let seed = Some(0i64);
1570 let mut w: Vec<f32> = (0..vocab * hidden)
1571 .map(|i| (i as f32) * 0.02 - 0.1)
1572 .collect();
1573 let orig = w.clone();
1574 hadamard_blocked_rows(&mut w, vocab, hidden, seed, false).unwrap();
1575 hadamard_blocked_rows(&mut w, vocab, hidden, seed, true).unwrap();
1576 for tid in [0usize, 2, 9] {
1577 let a = &orig[tid * hidden..(tid + 1) * hidden];
1578 let b = &w[tid * hidden..(tid + 1) * hidden];
1579 for (x, y) in a.iter().zip(b.iter()) {
1580 assert!((x - y).abs() < 1e-4, "tid={tid} {x} vs {y}");
1581 }
1582 }
1583 let tiles = pow2_tile_sizes(vocab).unwrap();
1584 assert_eq!(tiles, vec![8, 2]);
1585 let mut w2 = orig.clone();
1586 hadamard_blocked_rows_tiles(&mut w2, vocab, hidden, seed, false, &tiles).unwrap();
1587 hadamard_blocked_rows_tiles(&mut w2, vocab, hidden, seed, true, &tiles).unwrap();
1588 for (a, b) in orig.iter().zip(w2.iter()) {
1589 assert!((a - b).abs() < 1e-4);
1590 }
1591 assert!(hadamard_blocked_rows_tiles(
1592 &mut orig.clone(),
1593 vocab,
1594 hidden,
1595 seed,
1596 true,
1597 &[4, 4],
1598 )
1599 .is_err());
1600 }
1601
1602 #[test]
1603 fn shape_errors() {
1604 assert!(matches!(
1605 rms_norm(&[1.0, 2.0], &[1.0, 1.0, 1.0], 1e-6),
1606 Err(EngineError::ShapeMismatch(_))
1607 ));
1608 assert!(matches!(
1609 linear(&[1.0], &[1.0, 2.0], 1, 1),
1610 Err(EngineError::ShapeMismatch(_))
1611 ));
1612 assert!(matches!(
1613 rope(&mut [1.0, 2.0, 3.0], 3, 0, 10000.0),
1614 Err(EngineError::ShapeMismatch(_))
1615 ));
1616 assert!(matches!(
1617 attention(&[1.0], &[1.0], &[1.0], 1, 1, 2),
1618 Err(EngineError::ShapeMismatch(_))
1619 ));
1620 assert!(matches!(
1621 fwht(&mut [1.0, 2.0, 3.0]),
1622 Err(EngineError::ShapeMismatch(_))
1623 ));
1624 }
1625}