1use ferrox_core::attention::{apply_rope_interleaved, causal_mla_attention};
63use ferrox_core::matmul::rms_norm;
64use ferrox_core::weight_matrix::WeightMatrix;
65
66use crate::config::MlaConfig;
67
68pub struct MlaAttnWeights {
69 pub q_a_proj: WeightMatrix, pub q_a_layernorm: Vec<f32>, pub q_b_proj: WeightMatrix, pub kv_a_proj_with_mqa: WeightMatrix, pub kv_a_layernorm: Vec<f32>, pub kv_b_proj: WeightMatrix, pub o_proj: WeightMatrix, pub g_proj: Option<WeightMatrix>, }
79
80#[allow(clippy::too_many_arguments)]
87pub fn mla_forward_token(
88 weights: &MlaAttnWeights,
89 cfg: &MlaConfig,
90 hidden: &[f32],
91 rms_norm_eps: f32,
92 k_cache: &mut Vec<f32>,
93 v_cache: &mut Vec<f32>,
94) -> Vec<f32> {
95 let q_head_dim = cfg.qk_nope_head_dim + cfg.qk_rope_head_dim;
96 let pos = k_cache.len() / (cfg.num_heads * q_head_dim);
102
103 let q_a = weights.q_a_proj.apply(hidden);
104 let q_a_normed = rms_norm(&q_a, &weights.q_a_layernorm, rms_norm_eps);
105 let mut query = weights.q_b_proj.apply(&q_a_normed); if let Some(rope) = &cfg.rope {
111 for h in 0..cfg.num_heads {
112 let q_rot_h = &mut query[h * q_head_dim + cfg.qk_nope_head_dim..(h + 1) * q_head_dim];
113 apply_rope_interleaved(q_rot_h, pos, rope.theta);
114 }
115 }
116
117 let compressed_kv = weights.kv_a_proj_with_mqa.apply(hidden);
118 let (k_pass_c, k_rot_raw) = compressed_kv.split_at(cfg.kv_lora_rank);
119 let mut k_rot = k_rot_raw.to_vec();
126 if let Some(rope) = &cfg.rope {
127 apply_rope_interleaved(&mut k_rot, pos, rope.theta);
128 }
129 let k_pass_c_normed = rms_norm(k_pass_c, &weights.kv_a_layernorm, rms_norm_eps);
130 let k_pass_full = weights.kv_b_proj.apply(&k_pass_c_normed); let mut key_step = vec![0f32; cfg.num_heads * q_head_dim];
133 let mut value_step = vec![0f32; cfg.num_heads * cfg.v_head_dim];
134 let kpf_stride = cfg.qk_nope_head_dim + cfg.v_head_dim;
135 for h in 0..cfg.num_heads {
136 let k_pass = &k_pass_full[h * kpf_stride..h * kpf_stride + cfg.qk_nope_head_dim];
137 let v_h = &k_pass_full[h * kpf_stride + cfg.qk_nope_head_dim..(h + 1) * kpf_stride];
138
139 let key_h = &mut key_step[h * q_head_dim..(h + 1) * q_head_dim];
140 key_h[..cfg.qk_nope_head_dim].copy_from_slice(k_pass);
141 key_h[cfg.qk_nope_head_dim..].copy_from_slice(&k_rot);
142
143 value_step[h * cfg.v_head_dim..(h + 1) * cfg.v_head_dim].copy_from_slice(v_h);
144 }
145
146 k_cache.extend_from_slice(&key_step);
147 v_cache.extend_from_slice(&value_step);
148 let seq_len = k_cache.len() / (cfg.num_heads * q_head_dim);
149
150 let attn_out = causal_mla_attention(
151 &query,
152 k_cache,
153 v_cache,
154 cfg.num_heads,
155 q_head_dim,
156 cfg.v_head_dim,
157 seq_len,
158 );
159
160 let gated = match &weights.g_proj {
161 Some(g_proj) => {
162 let g = g_proj.apply(hidden);
163 attn_out
164 .iter()
165 .zip(g.iter())
166 .map(|(a, g)| a * (1.0 / (1.0 + (-g).exp())))
167 .collect::<Vec<f32>>()
168 }
169 None => attn_out,
170 };
171
172 weights.o_proj.apply(&gated)
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::config::MlaRopeConfig;
179 use ferrox_core::tensor::Tensor;
180
181 const HIDDEN_SIZE: usize = 8;
182 const NUM_HEADS: usize = 2;
183 const QK_NOPE_HEAD_DIM: usize = 3;
184 const QK_ROPE_HEAD_DIM: usize = 2;
185 const KV_LORA_RANK: usize = 4;
186 const Q_LORA_RANK: usize = 6;
187 const V_HEAD_DIM: usize = 3;
188 const EPS: f32 = 1e-5;
189
190 fn wm(data: &[f32], rows: usize, cols: usize) -> WeightMatrix {
191 assert_eq!(data.len(), rows * cols);
192 WeightMatrix::F32(Tensor::new(data.to_vec(), vec![rows, cols]))
193 }
194
195 fn cfg() -> MlaConfig {
196 MlaConfig {
197 num_heads: NUM_HEADS,
198 q_lora_rank: Q_LORA_RANK,
199 kv_lora_rank: KV_LORA_RANK,
200 qk_nope_head_dim: QK_NOPE_HEAD_DIM,
201 qk_rope_head_dim: QK_ROPE_HEAD_DIM,
202 v_head_dim: V_HEAD_DIM,
203 use_output_gate: true,
204 rope: None,
205 }
206 }
207
208 const MLA_Q_A_PROJ: [f32; 48] = [
210 -0.237937, 0.0721714, -0.568898, 0.418732, 0.191488, -0.0876142, -0.0935848, 0.0911506,
211 -0.0802981, -0.0677727, 0.21602, 0.154412, -0.0192384, -0.025643, 0.0482749, -0.184206,
212 -0.121125, 0.164478, -0.0391448, -0.412328, -0.143184, 0.196986, -0.0696848, -0.0446198,
213 0.192551, 0.547383, -0.213957, 0.404462, -0.369004, 0.0524933, -0.350859, 0.405437,
214 0.250177, 0.341315, -0.26566, 0.205367, -0.155704, -0.137216, 0.151961, 0.263015,
215 0.0613261, -0.188396, -0.247745, 0.433295, 0.178184, 0.215918, 0.655046, -0.244759,
216 ];
217 const MLA_Q_A_LAYERNORM_W: [f32; 6] = [1.25595, 1.31509, 1.16184, 1.08271, 0.933618, 1.09945];
218 const MLA_Q_B_PROJ: [f32; 60] = [
219 -0.132808,
220 -0.00649524,
221 -0.08713,
222 0.085149,
223 0.386423,
224 -0.166675,
225 -0.295621,
226 -0.300887,
227 -0.290483,
228 -0.429331,
229 -0.27388,
230 0.38798,
231 -0.177994,
232 0.0771323,
233 -0.365068,
234 0.0508952,
235 -0.522234,
236 -0.209627,
237 0.676362,
238 -0.174891,
239 0.335993,
240 0.136516,
241 -0.0458957,
242 -0.195632,
243 0.386073,
244 -0.053215,
245 0.458227,
246 -0.215724,
247 0.0172017,
248 0.13965,
249 0.111948,
250 -0.37014,
251 -0.199219,
252 -0.0587941,
253 -0.25611,
254 0.203198,
255 0.176406,
256 -0.587125,
257 -0.541576,
258 -0.384469,
259 0.0351779,
260 0.609952,
261 -0.114707,
262 0.0751952,
263 -0.318934,
264 -0.314051,
265 -0.587168,
266 -0.00850327,
267 0.284165,
268 -0.107014,
269 0.418936,
270 0.0593565,
271 -0.0109237,
272 0.155176,
273 0.146213,
274 0.344342,
275 -0.240586,
276 -0.686415,
277 0.034424,
278 -0.183503,
279 ];
280 const MLA_KV_A_PROJ: [f32; 48] = [
281 -0.00815316,
282 0.49929,
283 -0.330853,
284 0.229439,
285 0.28376,
286 0.138221,
287 0.335552,
288 -0.137509,
289 -0.204453,
290 0.311695,
291 0.21609,
292 0.417113,
293 0.0636869,
294 0.487069,
295 -0.0846241,
296 -0.318099,
297 -0.611127,
298 -0.33042,
299 0.245551,
300 -0.439479,
301 -0.135709,
302 0.631975,
303 0.254039,
304 0.537295,
305 -0.297469,
306 -0.737993,
307 0.454914,
308 -0.425083,
309 0.0272771,
310 0.065128,
311 -0.284669,
312 0.46579,
313 0.464801,
314 0.165433,
315 -0.0088472,
316 0.057431,
317 -0.328772,
318 -0.0717191,
319 -0.0314799,
320 -0.25567,
321 0.254764,
322 -0.427944,
323 -0.136026,
324 -0.675863,
325 0.149046,
326 0.222782,
327 0.186388,
328 0.875426,
329 ];
330 const MLA_KV_A_LAYERNORM_W: [f32; 4] = [1.09398, 0.905801, 1.26635, 0.907968];
331 const MLA_KV_B_PROJ: [f32; 48] = [
332 0.236622, 0.222938, -0.263917, 0.418605, 0.106378, 0.171286, -0.20272, 0.39079, -0.258281,
333 -0.533748, 0.427538, -0.128019, 0.013011, 0.576827, 0.340947, 0.189813, 0.320974,
334 -0.218253, 0.225927, 0.332981, 0.226627, -0.0612404, -1.12422, 0.167191, 0.0344629,
335 -0.110866, 0.23343, 0.0797109, 0.0611867, -0.445077, -0.437797, 0.111161, 0.0228171,
336 0.0416223, 0.0498789, 0.123437, 0.0108197, 0.0473313, 0.0768343, 0.278823, -0.154407,
337 0.235919, 0.190915, 0.0316206, -0.142534, 0.110742, 0.259192, -0.322493,
338 ];
339 const MLA_O_PROJ: [f32; 48] = [
340 0.0186437, 0.0194232, -0.198361, -0.185786, -0.465344, -0.330938, -0.461243, -0.184972,
341 -0.167996, -0.162386, 0.111553, -0.05627, -0.218674, -0.216565, -0.339453, -0.0588504,
342 -0.0642164, 0.486887, 0.460246, 0.443371, 0.623773, -0.36512, 0.201895, 0.228316, 0.142865,
343 0.638001, 0.761363, 0.29381, 0.195944, -0.336719, -0.336108, 0.210067, -0.0226898,
344 -0.399168, -0.0372073, 0.108002, 0.280778, -0.0323038, -0.0807652, 0.00186235, 0.051895,
345 -0.34894, 0.249993, 0.657098, 0.24426, -0.434459, 0.0283526, -0.529688,
346 ];
347 const MLA_G_PROJ: [f32; 48] = [
348 -0.548764, 0.197775, 0.0232947, -0.206133, -0.626234, -0.38068, 0.0486588, 0.00956812,
349 0.20691, 0.457018, -0.101777, 0.14069, -0.0413395, -0.148962, -0.0734372, 0.288226,
350 -0.308526, 0.342312, 0.443186, 0.0107199, 0.0764755, -0.42354, 0.70816, 0.293666,
351 0.0516828, 0.0172313, 0.135292, -0.195371, 0.0849785, -0.277073, 0.149196, -0.203464,
352 0.268656, -0.0361107, 0.0806238, 0.888267, 0.24127, 0.0803401, 0.0133166, -0.311749,
353 0.325266, 0.480702, 0.0193065, 0.503025, -0.0336833, -0.531916, -0.195972, -0.317098,
354 ];
355
356 const MLA_HIDDEN_0: [f32; 8] = [
357 -0.13727, 0.31349, -0.464487, -0.0401226, 0.0996761, -0.366514, -0.45466, 0.0435914,
358 ];
359 const MLA_HIDDEN_1: [f32; 8] = [
360 0.436185, -0.301614, -0.375696, -0.0706267, 0.144974, -0.271508, -0.167074, -0.115634,
361 ];
362 const MLA_HIDDEN_2: [f32; 8] = [
363 -0.542896, -0.0887686, -0.280024, 0.791695, -0.258661, 0.228556, 0.189759, -0.559016,
364 ];
365
366 const MLA_GOLDEN_OUT_0: [f32; 8] = [
367 0.01809, 0.174022, -0.240674, 0.212529, 0.493314, 0.191567, -0.184672, 0.118346,
368 ];
369 const MLA_GOLDEN_OUT_1: [f32; 8] = [
370 0.0683158, 0.13817, -0.241981, 0.162395, 0.452095, 0.159199, -0.139237, 0.145646,
371 ];
372 const MLA_GOLDEN_OUT_2: [f32; 8] = [
373 0.074717, 0.0909497, -0.208236, 0.166908, 0.390386, 0.129038, -0.124391, 0.12221,
374 ];
375
376 fn make_weights() -> MlaAttnWeights {
377 MlaAttnWeights {
378 q_a_proj: wm(&MLA_Q_A_PROJ, Q_LORA_RANK, HIDDEN_SIZE),
379 q_a_layernorm: MLA_Q_A_LAYERNORM_W.to_vec(),
380 q_b_proj: wm(
381 &MLA_Q_B_PROJ,
382 NUM_HEADS * (QK_NOPE_HEAD_DIM + QK_ROPE_HEAD_DIM),
383 Q_LORA_RANK,
384 ),
385 kv_a_proj_with_mqa: wm(&MLA_KV_A_PROJ, KV_LORA_RANK + QK_ROPE_HEAD_DIM, HIDDEN_SIZE),
386 kv_a_layernorm: MLA_KV_A_LAYERNORM_W.to_vec(),
387 kv_b_proj: wm(
388 &MLA_KV_B_PROJ,
389 NUM_HEADS * (QK_NOPE_HEAD_DIM + V_HEAD_DIM),
390 KV_LORA_RANK,
391 ),
392 o_proj: wm(&MLA_O_PROJ, HIDDEN_SIZE, NUM_HEADS * V_HEAD_DIM),
393 g_proj: Some(wm(&MLA_G_PROJ, NUM_HEADS * V_HEAD_DIM, HIDDEN_SIZE)),
394 }
395 }
396
397 #[test]
398 fn matches_independent_python_reference_across_three_decode_steps() {
399 let weights = make_weights();
406 let cfg = cfg();
407 assert!(
408 cfg.rope.is_none(),
409 "this test's whole point is pinning the rope-less path"
410 );
411 let mut k_cache = Vec::new();
412 let mut v_cache = Vec::new();
413
414 let hiddens = [&MLA_HIDDEN_0[..], &MLA_HIDDEN_1[..], &MLA_HIDDEN_2[..]];
415 let goldens = [
416 &MLA_GOLDEN_OUT_0[..],
417 &MLA_GOLDEN_OUT_1[..],
418 &MLA_GOLDEN_OUT_2[..],
419 ];
420
421 for (pos, (hidden, golden)) in hiddens.iter().zip(goldens.iter()).enumerate() {
422 let out = mla_forward_token(&weights, &cfg, hidden, EPS, &mut k_cache, &mut v_cache);
423 assert_eq!(out.len(), golden.len());
424 for (i, (a, b)) in out.iter().zip(golden.iter()).enumerate() {
425 assert!(
426 (a - b).abs() < 1e-3,
427 "position {pos} element {i}: rust={a} python={b}"
428 );
429 }
430 }
431 }
432
433 #[test]
434 fn without_output_gate_skips_the_sigmoid_multiply() {
435 let mut weights = make_weights();
436 weights.g_proj = None;
437 let mut cfg = cfg();
438 cfg.use_output_gate = false;
439 let mut k_cache = Vec::new();
440 let mut v_cache = Vec::new();
441
442 let out = mla_forward_token(
443 &weights,
444 &cfg,
445 &MLA_HIDDEN_0,
446 EPS,
447 &mut k_cache,
448 &mut v_cache,
449 );
450 assert_eq!(out.len(), HIDDEN_SIZE);
451 assert!(out.iter().all(|v| v.is_finite()));
452 assert!((out[0] - MLA_GOLDEN_OUT_0[0]).abs() > 1e-6);
454 }
455
456 const ROPE_HIDDEN_SIZE: usize = 8;
462 const ROPE_NUM_HEADS: usize = 2;
463 const ROPE_QK_NOPE_HEAD_DIM: usize = 4;
464 const ROPE_QK_ROPE_HEAD_DIM: usize = 4;
465 const ROPE_KV_LORA_RANK: usize = 4;
466 const ROPE_Q_LORA_RANK: usize = 6;
467 const ROPE_V_HEAD_DIM: usize = 3;
468 const ROPE_THETA: f32 = 10000.0;
469
470 const MLA_ROPE_Q_A_PROJ: [f32; 48] = [
472 -0.0282433, -0.200786, 0.229784, 0.162609, 0.161474, 0.190923, -0.28271, -0.259493,
473 -0.299452, -0.445281, 0.438341, 0.223402, -0.114472, 0.108293, 0.138669, -0.0983588,
474 -0.139929, -0.287077, 0.233259, -0.0924404, -0.48316, 0.236404, 0.487659, -0.22182,
475 -0.425881, 0.474637, 0.224366, 0.227792, 0.155548, 0.139447, -0.0826791, -0.435336,
476 0.438776, -0.328727, 0.415205, -0.280294, 0.25502, -0.264693, -0.374199, 0.626933,
477 0.175746, 0.198436, -0.0882081, -0.131098, 0.416386, 0.0938253, -0.086288, -0.173465,
478 ];
479 const MLA_ROPE_Q_A_LAYERNORM_W: [f32; 6] =
480 [0.983826, 0.879749, 1.11838, 1.05149, 1.1818, 0.834347];
481 const MLA_ROPE_Q_B_PROJ: [f32; 96] = [
482 0.291994,
483 -0.728247,
484 -0.316577,
485 0.0813452,
486 -0.266261,
487 0.389674,
488 0.0833719,
489 0.351552,
490 0.356938,
491 0.350334,
492 -0.0473987,
493 -0.0266404,
494 -0.169264,
495 0.0701104,
496 0.0207743,
497 0.44759,
498 0.372409,
499 0.283663,
500 0.161893,
501 0.0691206,
502 0.164776,
503 -0.159844,
504 0.244357,
505 0.254148,
506 0.781266,
507 -0.0410461,
508 0.00448047,
509 0.167438,
510 0.134256,
511 -0.117364,
512 0.613949,
513 -0.207111,
514 0.42746,
515 0.45351,
516 0.237126,
517 -0.50974,
518 0.328859,
519 -0.250491,
520 -0.356138,
521 0.122879,
522 0.254109,
523 0.120117,
524 -0.244618,
525 0.090442,
526 0.572282,
527 -0.175117,
528 0.150304,
529 0.127176,
530 -0.230927,
531 -0.181049,
532 0.0503238,
533 -0.252932,
534 -0.00813607,
535 -0.169141,
536 0.178562,
537 -0.172518,
538 -0.163208,
539 -0.286795,
540 0.358209,
541 0.355661,
542 -0.0321808,
543 0.025399,
544 -0.227651,
545 -0.0153813,
546 -0.0254572,
547 -0.364581,
548 -0.450488,
549 0.155816,
550 0.0033226,
551 0.481021,
552 -0.000260049,
553 -0.230117,
554 -0.0422523,
555 0.269254,
556 -0.225551,
557 -0.265757,
558 -0.192519,
559 -0.300859,
560 -0.152023,
561 0.31445,
562 -0.229592,
563 -0.417754,
564 -0.219984,
565 0.0230321,
566 0.162062,
567 -0.162489,
568 -0.504785,
569 0.117479,
570 -0.152083,
571 0.203557,
572 -0.232979,
573 -0.537171,
574 -0.131909,
575 -0.0782392,
576 0.187798,
577 -0.364894,
578 ];
579 const MLA_ROPE_KV_A_PROJ: [f32; 64] = [
580 -0.0108888,
581 0.122853,
582 -0.388147,
583 -0.320502,
584 0.288834,
585 0.0587081,
586 0.0027565,
587 -0.0303023,
588 0.252204,
589 0.103756,
590 -0.563955,
591 0.539224,
592 -0.515732,
593 0.475067,
594 -0.179422,
595 0.512039,
596 0.564391,
597 -0.309453,
598 0.178157,
599 0.4829,
600 0.304922,
601 -0.327155,
602 0.235531,
603 -0.223351,
604 0.113775,
605 -0.326219,
606 0.129363,
607 0.343847,
608 -0.555633,
609 -0.00371874,
610 -0.480022,
611 0.0622793,
612 0.121396,
613 0.902273,
614 -0.271857,
615 -0.0787809,
616 -0.148056,
617 -0.246381,
618 -0.388923,
619 -0.326308,
620 0.754771,
621 -0.188557,
622 0.157124,
623 -0.242718,
624 -0.196856,
625 0.168396,
626 0.116464,
627 0.406121,
628 -0.0524445,
629 -0.226537,
630 -0.220791,
631 -0.42747,
632 -0.109609,
633 0.327875,
634 0.238249,
635 0.262922,
636 0.0603609,
637 0.259383,
638 -0.125942,
639 0.0253563,
640 -0.672037,
641 -0.0822506,
642 -0.313883,
643 -0.079927,
644 ];
645 const MLA_ROPE_KV_A_LAYERNORM_W: [f32; 4] = [1.11964, 1.00794, 0.927425, 0.974059];
646 const MLA_ROPE_KV_B_PROJ: [f32; 56] = [
647 0.152348, 0.223828, 0.104805, 0.146991, 0.167048, 0.0596911, 0.13428, -0.165432, 0.185469,
648 -0.438659, 0.0505363, -0.378193, -0.367254, 0.291605, 0.25605, -0.278039, 0.32183,
649 0.077576, 0.657456, -0.210592, -0.241762, -0.113545, 0.305935, -0.142537, -0.131723,
650 -0.0698308, -0.231153, 0.0832406, -0.184562, -0.395013, -0.434206, 0.643208, -0.0451786,
651 0.016892, -0.503596, 0.38556, 0.0672434, -0.345241, 0.261026, 0.113356, 0.195666, 0.124068,
652 0.169155, 0.0241996, 0.0460725, -0.199187, 0.525018, 0.704311, 0.214609, 0.155865,
653 -0.13945, -0.361349, 0.200727, 0.669041, -0.35694, 0.405635,
654 ];
655 const MLA_ROPE_O_PROJ: [f32; 48] = [
656 0.156636,
657 0.435755,
658 0.254836,
659 -0.28038,
660 -0.00686566,
661 0.254093,
662 0.13879,
663 0.298608,
664 -0.654407,
665 0.544604,
666 -0.40823,
667 0.557235,
668 -0.401607,
669 0.0393622,
670 -0.0108063,
671 -0.425778,
672 -0.0790213,
673 0.183181,
674 0.770074,
675 0.431033,
676 -0.191665,
677 -0.321149,
678 -0.243943,
679 -0.0704616,
680 0.180775,
681 -0.216385,
682 0.0824125,
683 -0.320591,
684 -0.182163,
685 -0.0257085,
686 -0.0184709,
687 0.292862,
688 -0.215734,
689 0.652291,
690 -0.0461593,
691 0.249014,
692 -0.205017,
693 0.0634068,
694 0.087137,
695 0.529326,
696 0.477227,
697 0.171185,
698 0.0539693,
699 0.0189488,
700 -0.138254,
701 -0.173556,
702 0.65771,
703 -0.0616593,
704 ];
705
706 const MLA_ROPE_HIDDEN_0: [f32; 8] = [
707 -0.145978, -0.0867699, 0.281822, -0.765789, -0.590168, -0.252274, -0.397543, 1.30624,
708 ];
709 const MLA_ROPE_HIDDEN_1: [f32; 8] = [
710 0.0788124, -0.477519, -0.142939, 0.0694206, 0.639385, 0.515165, -0.118041, -0.755483,
711 ];
712 const MLA_ROPE_HIDDEN_2: [f32; 8] = [
713 -0.0406029, 0.440107, 0.194938, 0.0549023, 0.270816, 0.624518, 0.0925645, 0.0192617,
714 ];
715
716 const MLA_ROPE_GOLDEN_OUT_0: [f32; 8] = [
717 0.0871067, -0.172861, 0.884245, -1.2416, 0.0539017, -0.224481, 0.230219, -0.137034,
718 ];
719 const MLA_ROPE_GOLDEN_OUT_1: [f32; 8] = [
720 -0.24112, -0.298445, -0.0592754, -0.296603, -0.0395015, -0.0695785, 0.0639684, -0.0166364,
721 ];
722 const MLA_ROPE_GOLDEN_OUT_2: [f32; 8] = [
723 0.0310026, -0.604048, 0.119056, 0.10382, 0.137893, -0.456617, -0.176747, 0.347884,
724 ];
725
726 fn rope_cfg() -> MlaConfig {
727 MlaConfig {
728 num_heads: ROPE_NUM_HEADS,
729 q_lora_rank: ROPE_Q_LORA_RANK,
730 kv_lora_rank: ROPE_KV_LORA_RANK,
731 qk_nope_head_dim: ROPE_QK_NOPE_HEAD_DIM,
732 qk_rope_head_dim: ROPE_QK_ROPE_HEAD_DIM,
733 v_head_dim: ROPE_V_HEAD_DIM,
734 use_output_gate: false,
735 rope: Some(MlaRopeConfig { theta: ROPE_THETA }),
736 }
737 }
738
739 fn make_rope_weights() -> MlaAttnWeights {
740 MlaAttnWeights {
741 q_a_proj: wm(&MLA_ROPE_Q_A_PROJ, ROPE_Q_LORA_RANK, ROPE_HIDDEN_SIZE),
742 q_a_layernorm: MLA_ROPE_Q_A_LAYERNORM_W.to_vec(),
743 q_b_proj: wm(
744 &MLA_ROPE_Q_B_PROJ,
745 ROPE_NUM_HEADS * (ROPE_QK_NOPE_HEAD_DIM + ROPE_QK_ROPE_HEAD_DIM),
746 ROPE_Q_LORA_RANK,
747 ),
748 kv_a_proj_with_mqa: wm(
749 &MLA_ROPE_KV_A_PROJ,
750 ROPE_KV_LORA_RANK + ROPE_QK_ROPE_HEAD_DIM,
751 ROPE_HIDDEN_SIZE,
752 ),
753 kv_a_layernorm: MLA_ROPE_KV_A_LAYERNORM_W.to_vec(),
754 kv_b_proj: wm(
755 &MLA_ROPE_KV_B_PROJ,
756 ROPE_NUM_HEADS * (ROPE_QK_NOPE_HEAD_DIM + ROPE_V_HEAD_DIM),
757 ROPE_KV_LORA_RANK,
758 ),
759 o_proj: wm(
760 &MLA_ROPE_O_PROJ,
761 ROPE_HIDDEN_SIZE,
762 ROPE_NUM_HEADS * ROPE_V_HEAD_DIM,
763 ),
764 g_proj: None,
765 }
766 }
767
768 #[test]
769 fn rope_enabled_matches_independent_python_reference_across_three_decode_steps() {
770 let weights = make_rope_weights();
771 let cfg = rope_cfg();
772 let mut k_cache = Vec::new();
773 let mut v_cache = Vec::new();
774
775 let hiddens = [
776 &MLA_ROPE_HIDDEN_0[..],
777 &MLA_ROPE_HIDDEN_1[..],
778 &MLA_ROPE_HIDDEN_2[..],
779 ];
780 let goldens = [
781 &MLA_ROPE_GOLDEN_OUT_0[..],
782 &MLA_ROPE_GOLDEN_OUT_1[..],
783 &MLA_ROPE_GOLDEN_OUT_2[..],
784 ];
785
786 for (pos, (hidden, golden)) in hiddens.iter().zip(goldens.iter()).enumerate() {
787 let out = mla_forward_token(&weights, &cfg, hidden, EPS, &mut k_cache, &mut v_cache);
788 assert_eq!(out.len(), golden.len());
789 for (i, (a, b)) in out.iter().zip(golden.iter()).enumerate() {
790 assert!(
791 (a - b).abs() < 1e-3,
792 "position {pos} element {i}: rust={a} python={b}"
793 );
794 }
795 }
796 }
797
798 #[test]
799 fn rope_enabled_output_changes_with_position() {
800 let weights = make_rope_weights();
808 let cfg = rope_cfg();
809
810 let mut k_cache_pos0 = Vec::new();
811 let mut v_cache_pos0 = Vec::new();
812 let out_pos0 = mla_forward_token(
813 &weights,
814 &cfg,
815 &MLA_ROPE_HIDDEN_0,
816 EPS,
817 &mut k_cache_pos0,
818 &mut v_cache_pos0,
819 );
820
821 let mut k_cache_pos1 = Vec::new();
825 let mut v_cache_pos1 = Vec::new();
826 mla_forward_token(
827 &weights,
828 &cfg,
829 &MLA_ROPE_HIDDEN_1,
830 EPS,
831 &mut k_cache_pos1,
832 &mut v_cache_pos1,
833 );
834 let out_pos1 = mla_forward_token(
835 &weights,
836 &cfg,
837 &MLA_ROPE_HIDDEN_0,
838 EPS,
839 &mut k_cache_pos1,
840 &mut v_cache_pos1,
841 );
842
843 assert_eq!(out_pos0.len(), out_pos1.len());
844 let differs = out_pos0
845 .iter()
846 .zip(out_pos1.iter())
847 .any(|(a, b)| (a - b).abs() > 1e-4);
848 assert!(
849 differs,
850 "identical hidden state at two different positions must produce \
851 different output when RoPE is enabled"
852 );
853 }
854
855 #[test]
856 fn rope_disabled_config_is_unaffected_by_position_change() {
857 let weights = make_weights();
865 let cfg = cfg();
866 assert!(cfg.rope.is_none());
867
868 let mut k_cache_a = Vec::new();
869 let mut v_cache_a = Vec::new();
870 let out_a = mla_forward_token(
871 &weights,
872 &cfg,
873 &MLA_HIDDEN_0,
874 EPS,
875 &mut k_cache_a,
876 &mut v_cache_a,
877 );
878
879 let mut k_cache_b = Vec::new();
880 let mut v_cache_b = Vec::new();
881 let out_b = mla_forward_token(
882 &weights,
883 &cfg,
884 &MLA_HIDDEN_0,
885 EPS,
886 &mut k_cache_b,
887 &mut v_cache_b,
888 );
889
890 for (a, b) in out_a.iter().zip(out_b.iter()) {
891 assert_eq!(a.to_bits(), b.to_bits());
892 }
893 }
894}