1use ferrox_core::attention::{
77 apply_rope_interleaved, causal_mla_attention_sparse, lightning_indexer_topk,
78};
79use ferrox_core::matmul::{layer_norm, rms_norm};
80use ferrox_core::weight_matrix::WeightMatrix;
81
82use crate::config::MlaRopeConfig;
83
84#[derive(Debug, Clone)]
92pub struct Glm52MlaConfig {
93 pub num_heads: usize,
94 pub q_lora_rank: usize,
95 pub kv_lora_rank: usize,
96 pub qk_nope_head_dim: usize,
97 pub qk_rope_head_dim: usize,
98 pub v_head_dim: usize,
99 pub rope: MlaRopeConfig,
100}
101
102#[derive(Debug, Clone)]
108pub struct IndexerConfig {
109 pub n_heads: usize,
110 pub head_dim: usize,
112 pub rope_dim: usize,
115 pub top_k: usize,
116 pub rope_theta: f32,
117}
118
119pub struct IndexerWeights {
123 pub k_norm_weight: Vec<f32>,
124 pub k_norm_bias: Vec<f32>,
125 pub proj: WeightMatrix,
127 pub attn_k: WeightMatrix,
132 pub attn_q_b: WeightMatrix,
134}
135
136pub struct Glm52AttnWeights {
138 pub q_a_proj: WeightMatrix,
139 pub q_a_layernorm: Vec<f32>,
140 pub q_b_proj: WeightMatrix,
141 pub kv_a_proj_with_mqa: WeightMatrix,
142 pub kv_a_layernorm: Vec<f32>,
143 pub wk_b: Vec<WeightMatrix>,
157 pub wv_b: Vec<WeightMatrix>,
163 pub o_proj: WeightMatrix,
164 pub indexer: Option<IndexerWeights>,
167}
168
169#[derive(Debug, Clone, Default)]
177pub struct Glm52AttnState {
178 pub k_cache: Vec<f32>,
179 pub v_cache: Vec<f32>,
180 pub indexer_k_cache: Vec<f32>,
181}
182
183impl Glm52AttnState {
184 pub fn new() -> Self {
185 Self::default()
186 }
187}
188
189#[allow(clippy::too_many_arguments)]
209pub fn glm52_attn_forward_token(
210 weights: &Glm52AttnWeights,
211 cfg: &Glm52MlaConfig,
212 indexer_cfg: &IndexerConfig,
213 hidden: &[f32],
214 rms_norm_eps: f32,
215 is_full_indexer_layer: bool,
216 state: &mut Glm52AttnState,
217 prev_top_k: &mut Option<Vec<usize>>,
218) -> Vec<f32> {
219 let q_head_dim = cfg.qk_nope_head_dim + cfg.qk_rope_head_dim;
220 let pos = state.k_cache.len() / (cfg.num_heads * q_head_dim);
221
222 let q_a = weights.q_a_proj.apply(hidden);
223 let qr = rms_norm(&q_a, &weights.q_a_layernorm, rms_norm_eps);
224
225 let visible: Vec<usize> = if is_full_indexer_layer {
226 let indexer = weights
227 .indexer
228 .as_ref()
229 .expect("a \"full\" indexer layer must carry indexer weights");
230 let idx_head_dim = indexer_cfg.head_dim;
231 let idx_rope_dim = indexer_cfg.rope_dim;
232
233 let indexer_q_full = indexer.attn_q_b.apply(&qr); let indexer_q: Vec<Vec<f32>> = (0..indexer_cfg.n_heads)
235 .map(|h| {
236 let head_slice = &indexer_q_full[h * idx_head_dim..(h + 1) * idx_head_dim];
237 let mut rope_part = head_slice[..idx_rope_dim].to_vec();
239 apply_rope_interleaved(&mut rope_part, pos, indexer_cfg.rope_theta);
240 rope_part.extend_from_slice(&head_slice[idx_rope_dim..]);
241 rope_part
242 })
243 .collect();
244
245 let indexer_k_raw = indexer.attn_k.apply(hidden); let indexer_k_normed = layer_norm(
247 &indexer_k_raw,
248 &indexer.k_norm_weight,
249 &indexer.k_norm_bias,
250 rms_norm_eps,
251 );
252 let mut idx_rope_part = indexer_k_normed[..idx_rope_dim].to_vec();
253 apply_rope_interleaved(&mut idx_rope_part, pos, indexer_cfg.rope_theta);
254 idx_rope_part.extend_from_slice(&indexer_k_normed[idx_rope_dim..]);
255 state.indexer_k_cache.extend_from_slice(&idx_rope_part);
256
257 let idx_seq_len = state.indexer_k_cache.len() / idx_head_dim;
258 let indexer_keys: Vec<Vec<f32>> = (0..idx_seq_len)
259 .map(|t| state.indexer_k_cache[t * idx_head_dim..(t + 1) * idx_head_dim].to_vec())
260 .collect();
261
262 let indexer_weights_vec = indexer.proj.apply(hidden); let top_k = lightning_indexer_topk(
264 &indexer_q,
265 &indexer_keys,
266 &indexer_weights_vec,
267 indexer_cfg.top_k,
268 );
269 *prev_top_k = Some(top_k.clone());
270 top_k
271 } else {
272 prev_top_k
273 .clone()
274 .expect("a \"shared\" indexer layer must follow a previous \"full\" layer's top-k within this token's forward pass")
275 };
276
277 let mut query = weights.q_b_proj.apply(&qr); for h in 0..cfg.num_heads {
279 let q_rot_h = &mut query[h * q_head_dim + cfg.qk_nope_head_dim..(h + 1) * q_head_dim];
280 apply_rope_interleaved(q_rot_h, pos, cfg.rope.theta);
281 }
282
283 let kv_cmpr_pe = weights.kv_a_proj_with_mqa.apply(hidden);
284 let (kv_cmpr_raw, k_pe_raw) = kv_cmpr_pe.split_at(cfg.kv_lora_rank);
285 let mut k_pe = k_pe_raw.to_vec();
286 apply_rope_interleaved(&mut k_pe, pos, cfg.rope.theta);
287 let kv_cmpr = rms_norm(kv_cmpr_raw, &weights.kv_a_layernorm, rms_norm_eps);
288
289 let mut key_step = vec![0f32; cfg.num_heads * q_head_dim];
290 let mut value_step = vec![0f32; cfg.num_heads * cfg.v_head_dim];
291 for h in 0..cfg.num_heads {
292 let k_pass = weights.wk_b[h].apply(&kv_cmpr); let v_h = weights.wv_b[h].apply(&kv_cmpr); let key_h = &mut key_step[h * q_head_dim..(h + 1) * q_head_dim];
296 key_h[..cfg.qk_nope_head_dim].copy_from_slice(&k_pass);
297 key_h[cfg.qk_nope_head_dim..].copy_from_slice(&k_pe);
298
299 value_step[h * cfg.v_head_dim..(h + 1) * cfg.v_head_dim].copy_from_slice(&v_h);
300 }
301
302 state.k_cache.extend_from_slice(&key_step);
303 state.v_cache.extend_from_slice(&value_step);
304 let seq_len = state.k_cache.len() / (cfg.num_heads * q_head_dim);
305
306 let attn_out = causal_mla_attention_sparse(
307 &query,
308 &state.k_cache,
309 &state.v_cache,
310 cfg.num_heads,
311 q_head_dim,
312 cfg.v_head_dim,
313 seq_len,
314 &visible,
315 );
316
317 weights.o_proj.apply(&attn_out)
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::config::MlaRopeConfig;
324 use ferrox_core::tensor::Tensor;
325
326 fn wm(data: &[f32], rows: usize, cols: usize) -> WeightMatrix {
327 assert_eq!(data.len(), rows * cols);
328 WeightMatrix::F32(Tensor::new(data.to_vec(), vec![rows, cols]))
329 }
330
331 const HIDDEN_SIZE: usize = 8;
332 const NUM_HEADS: usize = 2;
333 const QK_NOPE_HEAD_DIM: usize = 4;
334 const QK_ROPE_HEAD_DIM: usize = 4;
335 const KV_LORA_RANK: usize = 4;
336 const Q_LORA_RANK: usize = 6;
337 const V_HEAD_DIM: usize = 3;
338 const EPS: f32 = 1e-5;
339 const ROPE_THETA: f32 = 10000.0;
340
341 const IDX_N_HEADS: usize = 2;
342 const IDX_ROPE_DIM: usize = 2;
343 const IDX_NOPE_DIM: usize = 2;
344 const IDX_HEAD_DIM: usize = IDX_ROPE_DIM + IDX_NOPE_DIM;
345 const TOP_K: usize = 2;
346
347 const GLM_Q_A_PROJ: [f32; 48] = [
349 -0.0282433, -0.200786, 0.229784, 0.162609, 0.161474, 0.190923, -0.28271, -0.259493,
350 -0.299452, -0.445281, 0.438341, 0.223402, -0.114472, 0.108293, 0.138669, -0.0983588,
351 -0.139929, -0.287077, 0.233259, -0.0924404, -0.48316, 0.236404, 0.487659, -0.22182,
352 -0.425881, 0.474637, 0.224366, 0.227792, 0.155548, 0.139447, -0.0826791, -0.435336,
353 0.438776, -0.328727, 0.415205, -0.280294, 0.25502, -0.264693, -0.374199, 0.626933,
354 0.175746, 0.198436, -0.0882081, -0.131098, 0.416386, 0.0938253, -0.086288, -0.173465,
355 ];
356 const GLM_Q_A_LAYERNORM_W: [f32; 6] = [0.983826, 0.879749, 1.11838, 1.05149, 1.1818, 0.834347];
357 const GLM_Q_B_PROJ: [f32; 96] = [
358 0.291994,
359 -0.728247,
360 -0.316577,
361 0.0813452,
362 -0.266261,
363 0.389674,
364 0.0833719,
365 0.351552,
366 0.356938,
367 0.350334,
368 -0.0473987,
369 -0.0266404,
370 -0.169264,
371 0.0701104,
372 0.0207743,
373 0.44759,
374 0.372409,
375 0.283663,
376 0.161893,
377 0.0691206,
378 0.164776,
379 -0.159844,
380 0.244357,
381 0.254148,
382 0.781266,
383 -0.0410461,
384 0.00448047,
385 0.167438,
386 0.134256,
387 -0.117364,
388 0.613949,
389 -0.207111,
390 0.42746,
391 0.45351,
392 0.237126,
393 -0.50974,
394 0.328859,
395 -0.250491,
396 -0.356138,
397 0.122879,
398 0.254109,
399 0.120117,
400 -0.244618,
401 0.090442,
402 0.572282,
403 -0.175117,
404 0.150304,
405 0.127176,
406 -0.230927,
407 -0.181049,
408 0.0503238,
409 -0.252932,
410 -0.00813607,
411 -0.169141,
412 0.178562,
413 -0.172518,
414 -0.163208,
415 -0.286795,
416 0.358209,
417 0.355661,
418 -0.0321808,
419 0.025399,
420 -0.227651,
421 -0.0153813,
422 -0.0254572,
423 -0.364581,
424 -0.450488,
425 0.155816,
426 0.0033226,
427 0.481021,
428 -0.000260049,
429 -0.230117,
430 -0.0422523,
431 0.269254,
432 -0.225551,
433 -0.265757,
434 -0.192519,
435 -0.300859,
436 -0.152023,
437 0.31445,
438 -0.229592,
439 -0.417754,
440 -0.219984,
441 0.0230321,
442 0.162062,
443 -0.162489,
444 -0.504785,
445 0.117479,
446 -0.152083,
447 0.203557,
448 -0.232979,
449 -0.537171,
450 -0.131909,
451 -0.0782392,
452 0.187798,
453 -0.364894,
454 ];
455 const GLM_KV_A_PROJ: [f32; 64] = [
456 -0.0108888,
457 0.122853,
458 -0.388147,
459 -0.320502,
460 0.288834,
461 0.0587081,
462 0.0027565,
463 -0.0303023,
464 0.252204,
465 0.103756,
466 -0.563955,
467 0.539224,
468 -0.515732,
469 0.475067,
470 -0.179422,
471 0.512039,
472 0.564391,
473 -0.309453,
474 0.178157,
475 0.4829,
476 0.304922,
477 -0.327155,
478 0.235531,
479 -0.223351,
480 0.113775,
481 -0.326219,
482 0.129363,
483 0.343847,
484 -0.555633,
485 -0.00371874,
486 -0.480022,
487 0.0622793,
488 0.121396,
489 0.902273,
490 -0.271857,
491 -0.0787809,
492 -0.148056,
493 -0.246381,
494 -0.388923,
495 -0.326308,
496 0.754771,
497 -0.188557,
498 0.157124,
499 -0.242718,
500 -0.196856,
501 0.168396,
502 0.116464,
503 0.406121,
504 -0.0524445,
505 -0.226537,
506 -0.220791,
507 -0.42747,
508 -0.109609,
509 0.327875,
510 0.238249,
511 0.262922,
512 0.0603609,
513 0.259383,
514 -0.125942,
515 0.0253563,
516 -0.672037,
517 -0.0822506,
518 -0.313883,
519 -0.079927,
520 ];
521 const GLM_KV_A_LAYERNORM_W: [f32; 4] = [1.11964, 1.00794, 0.927425, 0.974059];
522 const GLM_WK_B_0: [f32; 16] = [
523 0.152348, 0.223828, 0.104805, 0.146991, 0.167048, 0.0596911, 0.13428, -0.165432, 0.185469,
524 -0.438659, 0.0505363, -0.378193, -0.367254, 0.291605, 0.25605, -0.278039,
525 ];
526 const GLM_WV_B_0: [f32; 12] = [
527 -0.0451786, 0.016892, -0.503596, 0.38556, 0.0672434, -0.345241, 0.261026, 0.113356,
528 0.195666, 0.124068, 0.169155, 0.0241996,
529 ];
530 const GLM_WK_B_1: [f32; 16] = [
531 0.32183, 0.077576, 0.657456, -0.210592, -0.241762, -0.113545, 0.305935, -0.142537,
532 -0.131723, -0.0698308, -0.231153, 0.0832406, -0.184562, -0.395013, -0.434206, 0.643208,
533 ];
534 const GLM_WV_B_1: [f32; 12] = [
535 0.0460725, -0.199187, 0.525018, 0.704311, 0.214609, 0.155865, -0.13945, -0.361349,
536 0.200727, 0.669041, -0.35694, 0.405635,
537 ];
538 const GLM_O_PROJ: [f32; 48] = [
539 0.156636,
540 0.435755,
541 0.254836,
542 -0.28038,
543 -0.00686566,
544 0.254093,
545 0.13879,
546 0.298608,
547 -0.654407,
548 0.544604,
549 -0.40823,
550 0.557235,
551 -0.401607,
552 0.0393622,
553 -0.0108063,
554 -0.425778,
555 -0.0790213,
556 0.183181,
557 0.770074,
558 0.431033,
559 -0.191665,
560 -0.321149,
561 -0.243943,
562 -0.0704616,
563 0.180775,
564 -0.216385,
565 0.0824125,
566 -0.320591,
567 -0.182163,
568 -0.0257085,
569 -0.0184709,
570 0.292862,
571 -0.215734,
572 0.652291,
573 -0.0461593,
574 0.249014,
575 -0.205017,
576 0.0634068,
577 0.087137,
578 0.529326,
579 0.477227,
580 0.171185,
581 0.0539693,
582 0.0189488,
583 -0.138254,
584 -0.173556,
585 0.65771,
586 -0.0616593,
587 ];
588 const GLM_INDEXER_PROJ: [f32; 16] = [
589 -0.087587, -0.0520619, 0.169093, -0.459473, -0.354101, -0.151364, -0.238526, 0.783742,
590 0.0472875, -0.286511, -0.0857637, 0.0416524, 0.383631, 0.309099, -0.0708246, -0.45329,
591 ];
592 const GLM_INDEXER_ATTN_K: [f32; 32] = [
593 -0.0243618, 0.264064, 0.116963, 0.0329414, 0.16249, 0.374711, 0.0555387, 0.011557,
594 0.720572, -0.288658, 0.660557, -0.0520769, -0.398689, 0.10919, -0.21608, -0.272101,
595 0.324867, -0.0406344, 0.436622, -0.353795, 0.30306, -0.256956, 0.381955, 0.346494,
596 0.579363, 0.0492111, 0.0489935, -0.110262, -0.359962, 0.0704509, 0.402797, -0.121248,
597 ];
598 const GLM_INDEXER_ATTN_Q_B: [f32; 48] = [
599 0.423031,
600 0.0939434,
601 0.165178,
602 -0.337296,
603 -0.0761678,
604 -0.0724417,
605 -0.485867,
606 0.693554,
607 0.201884,
608 0.544458,
609 -0.0574976,
610 0.2739,
611 0.174839,
612 0.602076,
613 -0.00910648,
614 0.12034,
615 -0.0199685,
616 0.238296,
617 0.0537028,
618 0.221498,
619 -0.0995168,
620 0.0551425,
621 0.126781,
622 0.14289,
623 0.0625852,
624 0.14437,
625 0.00611794,
626 0.166845,
627 -0.566204,
628 0.132155,
629 -0.292038,
630 -0.203268,
631 -0.226073,
632 0.2511,
633 -0.339645,
634 0.0101247,
635 0.0409607,
636 -0.369437,
637 0.101741,
638 -0.517995,
639 0.150119,
640 0.19386,
641 0.115472,
642 -0.196285,
643 -0.0137043,
644 0.291252,
645 -0.0213699,
646 0.00902874,
647 ];
648 const GLM_INDEXER_K_NORM_W: [f32; 4] = [1.01802, 0.864429, 0.781694, 1.0421];
649 const GLM_INDEXER_K_NORM_B: [f32; 4] = [-0.00865097, -0.0181871, -0.0141045, 0.0431985];
650
651 const GLM_HIDDEN_0: [f32; 8] = [
652 0.173677, 0.403207, -0.819441, 0.0385262, -0.382448, 0.0666326, 0.107605, -0.50578,
653 ];
654 const GLM_HIDDEN_1: [f32; 8] = [
655 0.0557989, -0.774157, 0.586028, -0.0179948, 0.00680277, 0.436953, 0.188515, -0.2977,
656 ];
657 const GLM_HIDDEN_2: [f32; 8] = [
658 -0.718918, 1.18182, -0.484152, -0.465392, 0.221702, -0.438492, -0.388645, -0.164228,
659 ];
660 const GLM_HIDDEN_3: [f32; 8] = [
661 0.650653, -0.256895, -0.94904, 0.929269, -0.585101, 0.0973952, 0.140374, 0.190254,
662 ];
663
664 const GLM_GOLDEN_OUT_0: [f32; 8] = [
665 0.361853, -0.250738, 0.433283, -0.278145, 0.27569, -0.414117, 0.121914, 0.401593,
666 ];
667 const GLM_GOLDEN_OUT_1: [f32; 8] = [
668 -0.013624, 0.187721, -0.050068, -0.116258, -0.0672519, 0.179838, 0.13011, -0.114857,
669 ];
670 const GLM_GOLDEN_OUT_2: [f32; 8] = [
671 0.28988, -0.593942, 0.388018, 0.0189887, 0.317219, -0.64959, -0.143766, 0.524578,
672 ];
673 const GLM_GOLDEN_OUT_3: [f32; 8] = [
674 0.212341, 0.238018, 0.206285, -0.35234, 0.114026, 0.0278299, 0.262389, 0.0396803,
675 ];
676
677 const GLM_GOLDEN_VISIBLE_0: [usize; 1] = [0];
678 const GLM_GOLDEN_VISIBLE_1: [usize; 2] = [0, 1];
679 const GLM_GOLDEN_VISIBLE_2: [usize; 2] = [0, 2];
680 const GLM_GOLDEN_VISIBLE_3: [usize; 2] = [0, 3];
681
682 fn cfg() -> Glm52MlaConfig {
683 Glm52MlaConfig {
684 num_heads: NUM_HEADS,
685 q_lora_rank: Q_LORA_RANK,
686 kv_lora_rank: KV_LORA_RANK,
687 qk_nope_head_dim: QK_NOPE_HEAD_DIM,
688 qk_rope_head_dim: QK_ROPE_HEAD_DIM,
689 v_head_dim: V_HEAD_DIM,
690 rope: MlaRopeConfig { theta: ROPE_THETA },
691 }
692 }
693
694 fn indexer_cfg() -> IndexerConfig {
695 IndexerConfig {
696 n_heads: IDX_N_HEADS,
697 head_dim: IDX_HEAD_DIM,
698 rope_dim: IDX_ROPE_DIM,
699 top_k: TOP_K,
700 rope_theta: ROPE_THETA,
701 }
702 }
703
704 fn make_weights() -> Glm52AttnWeights {
705 let q_head_dim = QK_NOPE_HEAD_DIM + QK_ROPE_HEAD_DIM;
706 Glm52AttnWeights {
707 q_a_proj: wm(&GLM_Q_A_PROJ, Q_LORA_RANK, HIDDEN_SIZE),
708 q_a_layernorm: GLM_Q_A_LAYERNORM_W.to_vec(),
709 q_b_proj: wm(&GLM_Q_B_PROJ, NUM_HEADS * q_head_dim, Q_LORA_RANK),
710 kv_a_proj_with_mqa: wm(&GLM_KV_A_PROJ, KV_LORA_RANK + QK_ROPE_HEAD_DIM, HIDDEN_SIZE),
711 kv_a_layernorm: GLM_KV_A_LAYERNORM_W.to_vec(),
712 wk_b: vec![
713 wm(&GLM_WK_B_0, QK_NOPE_HEAD_DIM, KV_LORA_RANK),
714 wm(&GLM_WK_B_1, QK_NOPE_HEAD_DIM, KV_LORA_RANK),
715 ],
716 wv_b: vec![
717 wm(&GLM_WV_B_0, V_HEAD_DIM, KV_LORA_RANK),
718 wm(&GLM_WV_B_1, V_HEAD_DIM, KV_LORA_RANK),
719 ],
720 o_proj: wm(&GLM_O_PROJ, HIDDEN_SIZE, NUM_HEADS * V_HEAD_DIM),
721 indexer: Some(IndexerWeights {
722 k_norm_weight: GLM_INDEXER_K_NORM_W.to_vec(),
723 k_norm_bias: GLM_INDEXER_K_NORM_B.to_vec(),
724 proj: wm(&GLM_INDEXER_PROJ, IDX_N_HEADS, HIDDEN_SIZE),
725 attn_k: wm(&GLM_INDEXER_ATTN_K, IDX_HEAD_DIM, HIDDEN_SIZE),
726 attn_q_b: wm(
727 &GLM_INDEXER_ATTN_Q_B,
728 IDX_N_HEADS * IDX_HEAD_DIM,
729 Q_LORA_RANK,
730 ),
731 }),
732 }
733 }
734
735 #[test]
736 fn full_layer_matches_independent_python_reference_across_four_decode_steps() {
737 let weights = make_weights();
738 let cfg = cfg();
739 let idx_cfg = indexer_cfg();
740 let mut state = Glm52AttnState::new();
741 let mut prev_top_k: Option<Vec<usize>> = None;
742
743 let hiddens = [
744 &GLM_HIDDEN_0[..],
745 &GLM_HIDDEN_1[..],
746 &GLM_HIDDEN_2[..],
747 &GLM_HIDDEN_3[..],
748 ];
749 let goldens = [
750 &GLM_GOLDEN_OUT_0[..],
751 &GLM_GOLDEN_OUT_1[..],
752 &GLM_GOLDEN_OUT_2[..],
753 &GLM_GOLDEN_OUT_3[..],
754 ];
755 let golden_visible: [&[usize]; 4] = [
756 &GLM_GOLDEN_VISIBLE_0,
757 &GLM_GOLDEN_VISIBLE_1,
758 &GLM_GOLDEN_VISIBLE_2,
759 &GLM_GOLDEN_VISIBLE_3,
760 ];
761
762 for (pos, ((hidden, golden), visible)) in hiddens
763 .iter()
764 .zip(goldens.iter())
765 .zip(golden_visible.iter())
766 .enumerate()
767 {
768 let out = glm52_attn_forward_token(
772 &weights,
773 &cfg,
774 &idx_cfg,
775 hidden,
776 EPS,
777 true,
778 &mut state,
779 &mut prev_top_k,
780 );
781 assert_eq!(out.len(), golden.len());
782 for (i, (a, b)) in out.iter().zip(golden.iter()).enumerate() {
783 assert!(
784 (a - b).abs() < 1e-3,
785 "position {pos} element {i}: rust={a} python={b}"
786 );
787 }
788 assert_eq!(
792 prev_top_k.as_deref(),
793 Some(*visible),
794 "position {pos}: indexer top-k selection mismatch"
795 );
796 }
797 }
798
799 #[test]
800 #[should_panic(expected = "must follow a previous")]
801 fn shared_layer_without_a_preceding_full_layer_panics() {
802 let weights = make_weights();
803 let cfg = cfg();
804 let idx_cfg = indexer_cfg();
805 let mut state = Glm52AttnState::new();
806 let mut prev_top_k: Option<Vec<usize>> = None;
807
808 glm52_attn_forward_token(
814 &weights,
815 &cfg,
816 &idx_cfg,
817 &GLM_HIDDEN_0,
818 EPS,
819 false,
820 &mut state,
821 &mut prev_top_k,
822 );
823 }
824
825 #[test]
826 fn shared_layer_reuses_the_nearest_preceding_full_layers_top_k_exactly() {
827 let mut weights = make_weights();
836 let cfg = cfg();
837 let idx_cfg = indexer_cfg();
838 let mut state = Glm52AttnState::new();
839 let mut prev_top_k: Option<Vec<usize>> = None;
840
841 glm52_attn_forward_token(
843 &weights,
844 &cfg,
845 &idx_cfg,
846 &GLM_HIDDEN_0,
847 EPS,
848 true,
849 &mut state,
850 &mut prev_top_k,
851 );
852 let top_k_after_full = prev_top_k.clone();
853 assert!(top_k_after_full.is_some());
854
855 weights.indexer = None;
864
865 glm52_attn_forward_token(
876 &weights,
877 &cfg,
878 &idx_cfg,
879 &GLM_HIDDEN_1,
880 EPS,
881 false,
882 &mut state,
883 &mut prev_top_k,
884 );
885 assert_eq!(
886 prev_top_k, top_k_after_full,
887 "shared layer B must not alter prev_top_k"
888 );
889
890 glm52_attn_forward_token(
892 &weights,
893 &cfg,
894 &idx_cfg,
895 &GLM_HIDDEN_2,
896 EPS,
897 false,
898 &mut state,
899 &mut prev_top_k,
900 );
901 assert_eq!(
902 prev_top_k, top_k_after_full,
903 "shared layer C must not alter prev_top_k"
904 );
905 }
906
907 #[test]
908 fn full_layer_grows_the_indexer_cache_but_shared_layer_does_not() {
909 let mut weights = make_weights();
910 let cfg = cfg();
911 let idx_cfg = indexer_cfg();
912 let mut state = Glm52AttnState::new();
913 let mut prev_top_k: Option<Vec<usize>> = None;
914
915 glm52_attn_forward_token(
916 &weights,
917 &cfg,
918 &idx_cfg,
919 &GLM_HIDDEN_0,
920 EPS,
921 true,
922 &mut state,
923 &mut prev_top_k,
924 );
925 let len_after_full = state.indexer_k_cache.len();
926 assert_eq!(len_after_full, IDX_HEAD_DIM);
927
928 weights.indexer = None;
929 glm52_attn_forward_token(
930 &weights,
931 &cfg,
932 &idx_cfg,
933 &GLM_HIDDEN_1,
934 EPS,
935 false,
936 &mut state,
937 &mut prev_top_k,
938 );
939 assert_eq!(
940 state.indexer_k_cache.len(),
941 len_after_full,
942 "a \"shared\" layer must not grow the indexer K cache"
943 );
944 }
945}