1#![allow(clippy::needless_range_loop)]
55
56use std::ops::Range;
57use std::sync::Arc;
58use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
59
60use cudarc::driver::CudaSlice;
61
62use crate::Engine;
63use crate::kda::{ConvArm, KdaAttnLayer};
64use crate::model::GpuTensor;
65use memra_kv::{Cache, LatentKvLayer, RecurLayer};
66
67pub const GLM5_TP_ALLOWED_RANKS: [usize; 2] = [2, 4];
78
79pub const GLM5_TP_TRANSPORT_TAG: &str = "glm5-tp-transport";
85
86pub type Glm5TpLayerSpec = crate::tp::StepEpLayerSpec;
87
88pub fn glm5_tp_env_raw() -> Option<String> {
94 std::env::var("MEMRA_GLM5_TP").ok()
95}
96
97pub fn glm5_tp_armed() -> bool {
101 matches!(glm5_tp_env_raw().as_deref(), Some(v) if !v.is_empty() && v != "0")
102}
103
104pub fn parse_glm5_tp_layer_specs(
108 value: Option<&str>,
109 trunk_layers: usize,
110) -> Result<Vec<Glm5TpLayerSpec>, String> {
111 crate::tp::parse_layer_specs_for_trunk("MEMRA_GLM5_TP", value, Some(trunk_layers))
112}
113
114pub fn gate_same_device() -> bool {
120 std::env::var("MEMRA_GLM5_TP_GATE_SAME_DEV").as_deref() == Ok("1")
121}
122
123#[derive(Clone, Copy, PartialEq, Eq, Debug)]
137pub enum GateRed {
138 SwapWo,
139 SwapEpGateUp,
140 SkipPeerCombine,
141 CorruptEpMap,
142}
143
144pub fn gate_red() -> Result<Option<GateRed>, String> {
145 match std::env::var("MEMRA_GLM5_TP_GATE_RED").ok().as_deref() {
146 None | Some("") => Ok(None),
147 Some("swap-wo") => Ok(Some(GateRed::SwapWo)),
148 Some("swap-ep-gateup") => Ok(Some(GateRed::SwapEpGateUp)),
149 Some("skip-peer-combine") => Ok(Some(GateRed::SkipPeerCombine)),
150 Some("corrupt-ep-map") => Ok(Some(GateRed::CorruptEpMap)),
151 Some(other) => Err(format!(
152 "MEMRA_GLM5_TP_GATE_RED={other:?} is not a known red arm \
153 (swap-wo | swap-ep-gateup | skip-peer-combine | corrupt-ep-map)"
154 )),
155 }
156}
157
158pub struct Glm5TpRt {
166 pub peers: Vec<Engine>,
167 pub root_dev: usize,
168 pub peer_devs: Vec<usize>,
169 pub same_device_gate: bool,
173 pub transport: crate::tp_transport::TpTransport,
177 link: Option<crate::tp_transport::PeerPullLink>,
180}
181
182impl Glm5TpRt {
183 pub fn new(devices: &[usize]) -> Result<Self, Box<dyn std::error::Error>> {
184 let root_dev = devices[0];
185 let peer_devs: Vec<usize> = devices[1..].to_vec();
186 for &d in &peer_devs {
187 if d == root_dev || peer_devs.iter().filter(|&&x| x == d).count() > 1 {
188 return Err(format!(
189 "MEMRA_GLM5_TP rank devices must be distinct in serving; got {devices:?} \
190 (the same-device form exists only for the rig gate binary)"
191 )
192 .into());
193 }
194 }
195 let mut peers = Vec::with_capacity(peer_devs.len());
196 for &d in &peer_devs {
197 peers.push(Engine::new(d)?);
198 }
199 Ok(Self {
200 peers,
201 root_dev,
202 peer_devs,
203 same_device_gate: false,
204 transport: crate::tp_transport::TpTransport::HostCanonical,
205 link: None,
206 })
207 }
208
209 pub fn new_gate_same_device(
215 root_dev: usize,
216 ranks: usize,
217 ) -> Result<Self, Box<dyn std::error::Error>> {
218 let mut peers = Vec::with_capacity(ranks - 1);
219 for _ in 1..ranks {
220 peers.push(Engine::new(root_dev)?);
221 }
222 Ok(Self {
223 peers,
224 root_dev,
225 peer_devs: vec![root_dev; ranks - 1],
226 same_device_gate: true,
227 transport: crate::tp_transport::TpTransport::HostCanonical,
228 link: None,
229 })
230 }
231
232 pub fn ranks(&self) -> usize {
234 self.peers.len() + 1
235 }
236
237 pub fn devices(&self) -> Vec<usize> {
239 let mut devs = Vec::with_capacity(1 + self.peer_devs.len());
240 devs.push(self.root_dev);
241 for &d in &self.peer_devs {
242 if !devs.contains(&d) {
243 devs.push(d);
244 }
245 }
246 devs
247 }
248
249 pub fn arm_transport(&mut self, root: &Engine) -> Result<(), Box<dyn std::error::Error>> {
254 let (transport, armed_flag) = crate::tp_transport::transport_env()?;
260 let engines: Vec<&Engine> = std::iter::once(root).chain(self.peers.iter()).collect();
261 let link = crate::tp_transport::arm_transport(
262 transport,
263 armed_flag,
264 GLM5_TP_TRANSPORT_TAG,
265 &engines,
266 self.same_device_gate,
267 )?;
268 self.transport = transport;
269 self.link = link;
270 Ok(())
271 }
272
273 pub fn hop<'a>(&'a self, root: &'a Engine) -> crate::tp_transport::Hop<'a> {
277 crate::tp_transport::Hop {
278 engines: std::iter::once(root).chain(self.peers.iter()).collect(),
279 transport: self.transport,
280 link: self.link.as_ref(),
281 }
282 }
283}
284
285pub struct Glm5TpModelView {
293 pub trunk_layers: usize,
294 pub layer_class: Vec<Glm5LayerClass>,
296 pub layer_is_moe: Vec<bool>,
298 pub kda_heads: usize,
299 pub kda_head_dim: usize,
300 pub mla_heads: usize,
301 pub n_routed_experts: usize,
302 pub top_k: usize,
303}
304
305#[derive(Clone, Copy, PartialEq, Eq, Debug)]
306pub enum Glm5LayerClass {
307 Kda,
308 Mla,
309}
310
311pub struct Glm5TpLoadPlan {
315 pub rt: Arc<Glm5TpRt>,
316 pub layers: std::collections::BTreeSet<usize>,
317 pub ep_map: Option<crate::ep_map::EpMap>,
318}
319
320fn load_glm5_ep_map(
327 view: &Glm5TpModelView,
328 layers: &std::collections::BTreeSet<usize>,
329 ranks: usize,
330) -> Result<Option<crate::ep_map::EpMap>, Box<dyn std::error::Error>> {
331 let Some((flag, path)) = crate::ep_map::ep_map_env()? else {
332 return Ok(None);
333 };
334 if path.is_empty() {
335 return Err(format!(
336 "{flag} is set but empty (fail-closed: unset the flag for \
337 the even split; an empty value never silently means default)"
338 )
339 .into());
340 }
341 let text = std::fs::read_to_string(&path)
342 .map_err(|e| format!("{flag}={path}: cannot read the map file ({e}) — refused by name"))?;
343 let map = crate::ep_map::EpMap::parse(&text).map_err(|e| format!("{flag}={path}: {e}"))?;
344 if map.ranks != ranks {
345 return Err(format!(
346 "{flag}={path}: map declares ranks={}, this load is TP-{ranks} \
347 (re-mint the map for the armed rank count)",
348 map.ranks
349 )
350 .into());
351 }
352 if map.n_experts != view.n_routed_experts {
353 return Err(format!(
354 "{flag}={path}: map declares expert_count={}, the model routes {}",
355 map.n_experts, view.n_routed_experts
356 )
357 .into());
358 }
359 if map.entry_rank != 0 {
360 return Err(format!(
361 "{flag}={path}: entry_rank={} but the glm5 TP first-hop card is \
362 rank 0 (root: router + combine + shared expert) — re-mint with \
363 --entry-rank 0 (refused rather than silently remapping ranks)",
364 map.entry_rank
365 )
366 .into());
367 }
368 let ep_layers: Vec<usize> = layers
369 .iter()
370 .copied()
371 .filter(|&il| view.layer_is_moe[il])
372 .collect();
373 map.validate_layer_cover(&ep_layers)
374 .map_err(|e| format!("{flag}={path}: {e}"))?;
375 let digest = {
377 use sha2::{Digest, Sha256};
378 let mut h = Sha256::new();
379 h.update(text.as_bytes());
380 let out = h.finalize();
381 out.iter().map(|b| format!("{b:02x}")).collect::<String>()
382 };
383 eprintln!(
384 "[glm5-tp-preflight] ep-map armed path={path} sha256={digest} layers={} \
385 experts={} ranks={} entry_rank={} performance_claim=false",
386 map.layers.len(),
387 map.n_experts,
388 map.ranks,
389 map.entry_rank,
390 );
391 Ok(Some(map))
392}
393
394pub const GLM5_TP_REFUSED_DOOR_FLAGS: [(&str, &str); 4] = [
404 (
405 "MEMRA_HC_FUSED_PRE",
406 "the fused mHC pre-chain is gated on the unsharded walk only",
407 ),
408 (
409 "MEMRA_HC_DECODE_WS",
410 "the workspace decode walk carries no TP mixer branches",
411 ),
412 (
413 "MEMRA_KDA_FUSED_PROJ",
414 "the fused six-projection door (either operand arm) is gated on full-width \
415 projections, never head shards",
416 ),
417 (
418 "MEMRA_MLA_DECODE_SPLIT",
419 "the absorb/decompress split is gated on the full-head geometry",
420 ),
421];
422
423pub fn refuse_glm5_tp_door_composition(armed: impl Fn(&str) -> bool) -> Result<(), String> {
430 crate::tp::refuse_door_composition("MEMRA_GLM5_TP", &GLM5_TP_REFUSED_DOOR_FLAGS, armed)
431}
432
433pub fn prepare_glm5_tp_load(
436 e: &Engine,
437 view: &Glm5TpModelView,
438) -> Result<Option<Glm5TpLoadPlan>, Box<dyn std::error::Error>> {
439 let raw = glm5_tp_env_raw();
440 let specs = parse_glm5_tp_layer_specs(raw.as_deref(), view.trunk_layers)?;
441 if specs.is_empty() {
442 return Ok(None);
443 }
444
445 if crate::pp::pp_cuts(view.trunk_layers).is_some() {
448 return Err(
449 "MEMRA_GLM5_TP + MEMRA_PP_STAGES>1: the TP x PP composition is unwired and \
450 refuses until its own gate exists (stage 5 of the tp2 lane names it)"
451 .into(),
452 );
453 }
454 if !crate::tp::step_tp_layer_specs()?.is_empty()
455 || !crate::tp::step_ep_layer_specs()?.is_empty()
456 {
457 return Err(
458 "MEMRA_GLM5_TP + MEMRA_STEP_TP/MEMRA_STEP_EP: the step and glm5 parallel \
459 contracts never co-arm"
460 .into(),
461 );
462 }
463 refuse_glm5_tp_door_composition(|flag| std::env::var(flag).as_deref() == Ok("1"))?;
464
465 let devices = specs[0].devices.clone();
468 let ranks = devices.len();
469 if !GLM5_TP_ALLOWED_RANKS.contains(&ranks) {
470 return Err(format!(
471 "MEMRA_GLM5_TP names {ranks} devices per layer; the qualified rank envelope is \
472 {GLM5_TP_ALLOWED_RANKS:?} (TP-3 is a head-padding question, not built — see the \
473 module doc)"
474 )
475 .into());
476 }
477
478 if view.layer_class.len() != view.trunk_layers || view.layer_is_moe.len() != view.trunk_layers {
480 return Err(format!(
481 "glm5-tp preflight: layer class map ({}/{}) does not cover the {}-layer trunk",
482 view.layer_class.len(),
483 view.layer_is_moe.len(),
484 view.trunk_layers
485 )
486 .into());
487 }
488 if !view.kda_heads.is_multiple_of(ranks) || view.kda_heads == 0 {
489 return Err(format!(
490 "glm5-tp: {} KDA heads do not shard across {ranks} ranks",
491 view.kda_heads
492 )
493 .into());
494 }
495 if view.kda_head_dim != crate::kda::KDA_HEAD_DIM {
496 return Err(format!(
497 "glm5-tp: KDA head_dim {} is not the {} the scan kernel is instantiated for",
498 view.kda_head_dim,
499 crate::kda::KDA_HEAD_DIM
500 )
501 .into());
502 }
503 if !view.mla_heads.is_multiple_of(ranks) || view.mla_heads == 0 {
504 return Err(format!(
505 "glm5-tp: {} MLA heads do not shard across {ranks} ranks",
506 view.mla_heads
507 )
508 .into());
509 }
510 if !view.n_routed_experts.is_multiple_of(ranks) || view.n_routed_experts == 0 {
511 return Err(format!(
512 "glm5-tp: {} routed experts do not partition across {ranks} ranks",
513 view.n_routed_experts
514 )
515 .into());
516 }
517 if view.top_k > view.n_routed_experts {
518 return Err("glm5-tp: top_k exceeds the routed expert count".into());
519 }
520
521 for s in &specs {
522 if s.devices != devices {
523 return Err(format!(
524 "MEMRA_GLM5_TP carries ONE runtime group: layer {} names devices {:?}, \
525 the first spec names {:?}",
526 s.layer, s.devices, devices
527 )
528 .into());
529 }
530 if s.layer >= view.trunk_layers {
531 return Err(format!(
532 "MEMRA_GLM5_TP layer {} outside the {}-layer trunk",
533 s.layer, view.trunk_layers
534 )
535 .into());
536 }
537 }
538 let root_dev = e.ctx().ordinal();
539 if devices[0] != root_dev {
540 return Err(format!(
541 "MEMRA_GLM5_TP rank list {:?} must start with the owning device {root_dev} \
542 (the owner-first rank law)",
543 devices
544 )
545 .into());
546 }
547
548 let red = gate_red()?;
550 let same_dev = gate_same_device();
551 if let Some(red) = red {
552 eprintln!("[glm5-tp-preflight] GATE RED ARM armed: {red:?} — outputs MUST diverge");
553 }
554 let mut rt = if same_dev {
555 eprintln!(
556 "[glm5-tp-preflight] GATE same-device emulation: {} peer ranks are additional \
557 contexts on device {root_dev} (spec devices {:?} are logical rank ids)",
558 ranks - 1,
559 &devices[1..],
560 );
561 Glm5TpRt::new_gate_same_device(root_dev, ranks)?
562 } else {
563 Glm5TpRt::new(&devices)?
564 };
565 rt.arm_transport(e)?;
568 let rt = Arc::new(rt);
569 let layers: std::collections::BTreeSet<usize> = specs.iter().map(|s| s.layer).collect();
570 let ep_map = load_glm5_ep_map(view, &layers, ranks)?;
571 let (mut kda_n, mut mla_n, mut moe_n) = (0usize, 0usize, 0usize);
572 for &il in &layers {
573 match view.layer_class[il] {
574 Glm5LayerClass::Kda => kda_n += 1,
575 Glm5LayerClass::Mla => mla_n += 1,
576 }
577 if view.layer_is_moe[il] {
578 moe_n += 1;
579 }
580 }
581 eprintln!(
582 "[glm5-tp-preflight] armed ranks={ranks} devices={devices:?} layers={} \
583 kda_shard={kda_n} mla_shard={mla_n} moe_ep={moe_n} kda_heads_per_rank={} \
584 mla_heads_per_rank={} experts_per_rank={} transport={} \
585 weights_loaded=false performance_claim=false",
586 layers.len(),
587 view.kda_heads / ranks,
588 view.mla_heads / ranks,
589 view.n_routed_experts / ranks,
590 rt.transport.name(),
591 );
592 Ok(Some(Glm5TpLoadPlan { rt, layers, ep_map }))
593}
594
595fn outer_rows(ne: &[u64]) -> (usize, usize) {
600 let outer = *ne.last().expect("tensor has at least one axis") as usize;
603 let inner: usize = ne[..ne.len() - 1].iter().map(|&d| d as usize).product();
604 (outer, inner.max(1))
605}
606
607fn shard_rows(
611 src_engine: &Engine,
612 dst: &Engine,
613 t: &GpuTensor,
614 rows: Range<usize>,
615) -> Result<GpuTensor, Box<dyn std::error::Error>> {
616 match t {
617 GpuTensor::Float { data, ne } => {
618 let (outer, inner) = outer_rows(ne);
619 if rows.end > outer {
620 return Err(format!("shard rows {rows:?} exceed outer axis {outer}").into());
621 }
622 let host = src_engine.dtoh(data)?;
623 let piece = &host[rows.start * inner..rows.end * inner];
624 let mut ne2 = ne.clone();
625 *ne2.last_mut().unwrap() = (rows.end - rows.start) as u64;
626 Ok(GpuTensor::Float {
627 data: dst.htod(piece)?,
628 ne: ne2,
629 })
630 }
631 GpuTensor::FloatBf16 { data, ne } => {
632 let (outer, inner) = outer_rows(ne);
633 if rows.end > outer {
634 return Err(format!("shard rows {rows:?} exceed outer axis {outer}").into());
635 }
636 let host = src_engine.dtoh_u8(data)?;
637 let piece = &host[rows.start * inner * 2..rows.end * inner * 2];
638 let mut ne2 = ne.clone();
639 *ne2.last_mut().unwrap() = (rows.end - rows.start) as u64;
640 Ok(GpuTensor::FloatBf16 {
641 data: dst.htod_bytes(piece)?,
642 ne: ne2,
643 })
644 }
645 GpuTensor::Quant {
646 bytes,
647 qtype,
648 row_bytes,
649 ne,
650 scale,
651 rp,
652 fp8,
653 rp4,
654 blk,
655 f16,
656 #[cfg(memra_cutlass)]
657 cutlass,
658 } => {
659 if *rp {
660 return Err(
661 "glm5-tp shard: rp split-plane mirror layout is unwired — load \
662 the TP-armed tensor with MEMRA_RP=0 (raw layout is bit-identical \
663 by the mirror's own contract)"
664 .into(),
665 );
666 }
667 if fp8.is_some() || rp4.is_some() || blk.is_some() || f16.is_some() {
668 return Err(
669 "glm5-tp shard: a decode/prefill mirror (fp8/rp4/blk/f16) is present on a \
670 TP-armed tensor — mirrors are unwired for shards in v1; disable the \
671 mirror door for this load"
672 .into(),
673 );
674 }
675 #[cfg(memra_cutlass)]
676 if cutlass.is_some() {
677 return Err("glm5-tp shard: cutlass prefill operand unwired for shards".into());
678 }
679 let (outer, inner) = outer_rows(ne);
680 if ne.len() != 2 {
681 return Err("glm5-tp shard: quantized shards are 2D-only in v1".into());
682 }
683 let _ = inner;
684 if rows.end > outer {
685 return Err(format!("shard rows {rows:?} exceed outer axis {outer}").into());
686 }
687 let host = src_engine.dtoh_u8(bytes)?;
688 let piece = &host[rows.start * row_bytes..rows.end * row_bytes];
689 let mut ne2 = ne.clone();
690 *ne2.last_mut().unwrap() = (rows.end - rows.start) as u64;
691 Ok(GpuTensor::Quant {
692 bytes: dst.htod_bytes(piece)?,
693 qtype: *qtype,
694 row_bytes: *row_bytes,
695 ne: ne2,
696 scale: *scale,
697 rp: false,
698 fp8: None,
699 rp4: None,
700 blk: None,
701 f16: None,
702 #[cfg(memra_cutlass)]
703 cutlass: None,
704 })
705 }
706 }
707}
708
709fn replicate(
711 src_engine: &Engine,
712 dst: &Engine,
713 t: &GpuTensor,
714) -> Result<GpuTensor, Box<dyn std::error::Error>> {
715 let (outer, _) = outer_rows(t.ne());
716 shard_rows(src_engine, dst, t, 0..outer)
717}
718
719pub(crate) fn rank_engine<'a>(e: &'a Engine, rt: &'a Glm5TpRt, r: usize) -> &'a Engine {
722 if r == 0 { e } else { &rt.peers[r - 1] }
723}
724
725pub struct Glm5TpKda {
733 pub rt: Arc<Glm5TpRt>,
734 pub peers: Vec<KdaAttnLayer>,
736 pub full_qkv: usize,
738 pub n_embd: usize,
740}
741
742impl Glm5TpKda {
743 pub fn ranks(&self) -> usize {
744 self.peers.len() + 1
745 }
746}
747
748static KDA_MARKED: AtomicBool = AtomicBool::new(false);
749
750pub(crate) fn shard_kda_layer(
754 e: &Engine,
755 rt: &Arc<Glm5TpRt>,
756 la: KdaAttnLayer,
757) -> Result<KdaAttnLayer, Box<dyn std::error::Error>> {
758 if la.tp.is_some() {
759 return Err("shard_kda_layer: layer is already sharded".into());
760 }
761 let ranks = rt.ranks();
762 let heads = la.heads();
763 let head_dim = la.head_dim();
764 let qkv = la.qkv();
765 let kernel = la.conv_kernel();
766 if !heads.is_multiple_of(ranks) {
767 return Err(format!("KDA heads {heads} do not shard across {ranks} ranks").into());
768 }
769 let hl = heads / ranks; let ql = qkv / ranks; let n_embd = la.wo.out_features();
772 if !n_embd.is_multiple_of(ranks) {
773 return Err(format!("KDA wo out {n_embd} does not split across ranks").into());
774 }
775 let hh = n_embd / ranks;
776
777 let mut shard_plan = la.plan;
778 shard_plan.num_heads = hl as u32;
779
780 let conv_host = e.dtoh(&la.conv)?;
783 let conv_rank =
784 |dst: &Engine, r: usize| -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
785 let mut piece = Vec::with_capacity(3 * ql * kernel);
786 for p in 0..3 {
787 let a = (p * qkv + r * ql) * kernel;
788 piece.extend_from_slice(&conv_host[a..a + ql * kernel]);
789 }
790 dst.htod(&piece)
791 };
792
793 let wo_rank = |r: usize| -> usize {
796 match gate_red() {
797 Ok(Some(GateRed::SwapWo)) => (r + 1) % ranks,
798 _ => r,
799 }
800 };
801
802 let rank_shard = |dst: &Engine, r: usize| -> Result<KdaAttnLayer, Box<dyn std::error::Error>> {
803 let wr = wo_rank(r);
804 Ok(KdaAttnLayer {
805 plan: shard_plan,
806 wq: shard_rows(e, dst, &la.wq, r * ql..(r + 1) * ql)?,
807 wk: shard_rows(e, dst, &la.wk, r * ql..(r + 1) * ql)?,
808 wv: shard_rows(e, dst, &la.wv, r * ql..(r + 1) * ql)?,
809 f_a: replicate(e, dst, &la.f_a)?,
810 f_b: shard_rows(e, dst, &la.f_b, r * ql..(r + 1) * ql)?,
811 g_a: replicate(e, dst, &la.g_a)?,
812 g_b: shard_rows(e, dst, &la.g_b, r * ql..(r + 1) * ql)?,
813 b_proj: shard_rows(e, dst, &la.b_proj, r * hl..(r + 1) * hl)?,
814 wo: shard_rows(e, dst, &la.wo, wr * hh..(wr + 1) * hh)?,
818 conv: conv_rank(dst, r)?,
819 a_log: shard_rows(e, dst, &la.a_log, r * hl..(r + 1) * hl)?,
820 dt_bias: shard_rows(e, dst, &la.dt_bias, r * ql..(r + 1) * ql)?,
821 o_norm: replicate(e, dst, &la.o_norm)?,
822 tp: None,
823 })
824 };
825
826 let mut root = rank_shard(e, 0)?;
827 let mut peers = Vec::with_capacity(ranks - 1);
828 for r in 1..ranks {
829 peers.push(rank_shard(&rt.peers[r - 1], r)?);
830 }
831 if !KDA_MARKED.swap(true, Ordering::Relaxed) {
832 eprintln!(
833 "[glm5-tp-kda] head shard armed: ranks={ranks} heads_per_rank={hl} \
834 head_dim={head_dim} wo=column-over-gather transport={} performance_claim=false",
835 rt.transport.name(),
836 );
837 }
838 root.tp = Some(Box::new(Glm5TpKda {
839 rt: Arc::clone(rt),
840 peers,
841 full_qkv: qkv,
842 n_embd,
843 }));
844 Ok(root)
845}
846
847fn ensure_kda_tp_state<'c>(
852 e: &Engine,
853 rt: &Glm5TpRt,
854 la_root: &KdaAttnLayer,
855 cache: &'c mut Cache,
856 il: usize,
857) -> Result<&'c mut Vec<RecurLayer>, Box<dyn std::error::Error>> {
858 if cache.glm5_tp_recur.len() <= il {
859 return Err(format!("glm5-tp: cache carries no TP recur slot for layer {il}").into());
860 }
861 if cache.glm5_tp_recur[il].is_none() {
862 let conv_pad = la_root.conv_width() * (la_root.conv_kernel() - 1);
863 let state = la_root.state_width();
864 let mk = |dev: &Engine| -> Result<RecurLayer, Box<dyn std::error::Error>> {
865 Ok(RecurLayer {
866 conv_state: dev.zeros(conv_pad)?,
867 ssm_state: dev.zeros(state)?,
868 ssm_state_alt: dev.zeros(state)?,
869 })
870 };
871 let mut planes = Vec::with_capacity(rt.ranks());
872 planes.push(mk(e)?);
873 for p in &rt.peers {
874 planes.push(mk(p)?);
875 }
876 cache.glm5_tp_recur[il] = Some(planes);
877 }
878 Ok(cache.glm5_tp_recur[il].as_mut().unwrap())
879}
880
881#[allow(clippy::too_many_arguments)] fn kda_tp_core(
898 e: &Engine,
899 la_root: &KdaAttnLayer,
900 x: &CudaSlice<f32>,
901 t: usize,
902 eps: f32,
903 cache: &mut Cache,
904 il: usize,
905 arm: ConvArm,
906 verify_stash: Option<&mut Glm5TpKdaVerifyStash>,
907 mut scan_clock: Option<&mut u64>,
908) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
909 let tp = la_root
910 .tp
911 .as_ref()
912 .ok_or("kda_tp_core called on an unsharded layer")?;
913 let rt = &tp.rt;
914 let ranks = rt.ranks();
915 let ql = la_root.qkv(); let full = tp.full_qkv;
917 let n_embd = tp.n_embd;
918 let hh = n_embd / ranks;
919 let rows_exact = verify_stash.is_some();
920 let mut captured: Vec<Option<(CudaSlice<f32>, crate::kda::KdaRowsStash)>> =
923 (0..ranks).map(|_| None).collect();
924
925 let hop = rt.hop(e);
926 let x_peers = crate::tp_transport::fanout_f32(&hop, x, x.len())?;
930 let states = ensure_kda_tp_state(e, rt, la_root, cache, il)?;
931
932 let mut gated: Vec<Option<CudaSlice<f32>>> = (0..ranks).map(|_| None).collect();
935 for r in (1..ranks).chain(std::iter::once(0)) {
936 let dev = if r == 0 { e } else { &rt.peers[r - 1] };
937 let la = if r == 0 { la_root } else { &tp.peers[r - 1] };
938 let xin = if r == 0 { x } else { &x_peers[r - 1] };
939 let snap = if rows_exact {
942 Some(dev.clone_dtod(&states[r].ssm_state)?)
943 } else {
944 None
945 };
946 let mut rank_stash: Option<crate::kda::KdaRowsStash> = None;
947 let mut rank_scan_ns = 0u64;
948 let out = {
949 let RecurLayer {
950 conv_state,
951 ssm_state,
952 ssm_state_alt,
953 } = &mut states[r];
954 let out = crate::kda::kda_core_gated(
955 dev,
956 la,
957 xin,
958 t,
959 eps,
960 conv_state,
961 ssm_state,
962 ssm_state_alt,
963 arm,
964 if rows_exact {
965 crate::kda::KdaStash::Rows(&mut rank_stash)
966 } else {
967 crate::kda::KdaStash::None
968 },
969 scan_clock.as_deref_mut().map(|_| &mut rank_scan_ns),
970 None,
971 None,
972 )?;
973 std::mem::swap(ssm_state, ssm_state_alt);
974 out
975 };
976 if let Some(clock) = scan_clock.as_deref_mut() {
977 *clock += rank_scan_ns;
978 }
979 if rows_exact {
980 let snap = snap.expect("verify arm cloned the snapshot above");
981 let rank_stash = rank_stash
982 .ok_or("kda_core_gated returned without filling the requested rows stash")?;
983 captured[r] = Some((snap, rank_stash));
984 }
985 gated[r] = Some(out);
986 }
987 if let Some(stash_vec) = verify_stash {
988 stash_vec.clear();
989 for c in captured {
990 stash_vec.push(c.expect("every rank captured on the verify arm"));
991 }
992 }
993
994 debug_assert_eq!(full, ranks * ql);
998 let gated_refs: Vec<&CudaSlice<f32>> = gated
999 .iter()
1000 .map(|g| g.as_ref().expect("filled above"))
1001 .collect();
1002 let fulls = crate::tp_transport::gather_parts(&hop, &gated_refs, t, ql)?;
1003
1004 let mut ys = Vec::with_capacity(ranks);
1007 if rows_exact {
1008 ys.push(e.matmul_rows_exact(&la_root.wo, &fulls[0], t)?);
1009 for r in 1..ranks {
1010 ys.push(rt.peers[r - 1].matmul_rows_exact(&tp.peers[r - 1].wo, &fulls[r], t)?);
1011 }
1012 } else {
1013 ys.push(e.matmul(&la_root.wo, &fulls[0], t)?);
1014 for r in 1..ranks {
1015 ys.push(rt.peers[r - 1].matmul(&tp.peers[r - 1].wo, &fulls[r], t)?);
1016 }
1017 }
1018
1019 let y_refs: Vec<&CudaSlice<f32>> = ys.iter().collect();
1021 crate::tp_transport::concat_parts_on_root(&hop, &y_refs, t, hh)
1022}
1023
1024#[allow(clippy::too_many_arguments)] pub(crate) fn kda_tp_cached(
1027 e: &Engine,
1028 la_root: &KdaAttnLayer,
1029 x: &CudaSlice<f32>,
1030 t: usize,
1031 eps: f32,
1032 cache: &mut Cache,
1033 il: usize,
1034 arm: ConvArm,
1035) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1036 kda_tp_core(e, la_root, x, t, eps, cache, il, arm, None, None)
1037}
1038
1039pub type Glm5TpKdaVerifyStash = Vec<(CudaSlice<f32>, crate::kda::KdaRowsStash)>;
1047
1048#[allow(clippy::too_many_arguments)] pub(crate) fn kda_tp_verify_rows(
1054 e: &Engine,
1055 la_root: &KdaAttnLayer,
1056 x: &CudaSlice<f32>,
1057 t: usize,
1058 eps: f32,
1059 cache: &mut Cache,
1060 il: usize,
1061 scan_clock: Option<&mut u64>,
1062) -> Result<(CudaSlice<f32>, Glm5TpKdaVerifyStash), Box<dyn std::error::Error>> {
1063 let mut stash: Glm5TpKdaVerifyStash = Vec::new();
1064 let out = kda_tp_core(
1065 e,
1066 la_root,
1067 x,
1068 t,
1069 eps,
1070 cache,
1071 il,
1072 ConvArm::Prefill,
1073 Some(&mut stash),
1074 scan_clock,
1075 )?;
1076 Ok((out, stash))
1077}
1078
1079pub(crate) fn kda_tp_verify_rollback(
1083 e: &Engine,
1084 la_root: &KdaAttnLayer,
1085 stash: &Glm5TpKdaVerifyStash,
1086 keep: usize,
1087 cache: &mut Cache,
1088 il: usize,
1089) -> Result<(), Box<dyn std::error::Error>> {
1090 let tp = la_root
1091 .tp
1092 .as_ref()
1093 .ok_or("kda_tp_verify_rollback called on an unsharded layer")?;
1094 let rt = &tp.rt;
1095 let ranks = rt.ranks();
1096 if stash.len() != ranks {
1097 return Err(format!(
1098 "glm5-tp verify rollback: stash carries {} ranks, the runtime has {ranks}",
1099 stash.len()
1100 )
1101 .into());
1102 }
1103 let states = cache.glm5_tp_recur[il]
1104 .as_mut()
1105 .ok_or_else(|| format!("glm5-tp verify rollback: layer {il} has no per-rank state"))?;
1106 for r in 0..ranks {
1107 let dev = if r == 0 { e } else { &rt.peers[r - 1] };
1108 let la = if r == 0 { la_root } else { &tp.peers[r - 1] };
1109 let (snap, rows) = &stash[r];
1110 crate::kda::kda_verify_rollback_rows_on(dev, la, snap, rows, keep, &mut states[r], il)?;
1111 }
1112 Ok(())
1113}
1114
1115pub struct Glm5TpMla {
1122 pub rt: Arc<Glm5TpRt>,
1123 pub peers: Vec<crate::hybrid::MlaAttnLayer>,
1125 pub full_heads: usize,
1127 pub n_embd: usize,
1129}
1130
1131impl Glm5TpMla {
1132 pub fn ranks(&self) -> usize {
1133 self.peers.len() + 1
1134 }
1135}
1136
1137static MLA_MARKED: AtomicBool = AtomicBool::new(false);
1138
1139pub(crate) fn shard_mla_layer(
1140 e: &Engine,
1141 rt: &Arc<Glm5TpRt>,
1142 la: crate::hybrid::MlaAttnLayer,
1143) -> Result<crate::hybrid::MlaAttnLayer, Box<dyn std::error::Error>> {
1144 use crate::hybrid::{MlaAttnLayer, MlaIndexer};
1145 if la.tp.is_some() {
1146 return Err("shard_mla_layer: layer is already sharded".into());
1147 }
1148 let ranks = rt.ranks();
1149 let g = la.geom;
1150 let nh = g.n_head;
1151 if !nh.is_multiple_of(ranks) {
1152 return Err(format!("MLA heads {nh} do not shard across {ranks} ranks").into());
1153 }
1154 let hl = nh / ranks;
1155 let head_q = g.d_nope + g.d_rope; let n_embd = la.wo.out_features();
1157 if !n_embd.is_multiple_of(ranks) {
1158 return Err(format!("MLA wo out {n_embd} does not split across ranks").into());
1159 }
1160 let hh = n_embd / ranks;
1161
1162 let mut shard_geom = g;
1163 shard_geom.n_head = hl;
1164
1165 let replicate_indexer =
1166 |dst: &Engine, ix: &MlaIndexer| -> Result<MlaIndexer, Box<dyn std::error::Error>> {
1167 Ok(MlaIndexer {
1168 wq_b: replicate(e, dst, &ix.wq_b)?,
1169 wk: replicate(e, dst, &ix.wk)?,
1170 k_norm_w: replicate(e, dst, &ix.k_norm_w)?,
1171 k_norm_b: replicate(e, dst, &ix.k_norm_b)?,
1172 weights_proj: replicate(e, dst, &ix.weights_proj)?,
1173 kpool_gate: replicate(e, dst, &ix.kpool_gate)?,
1174 kpool_ape: replicate(e, dst, &ix.kpool_ape)?,
1175 geom: ix.geom,
1176 })
1177 };
1178
1179 let wo_rank = |r: usize| -> usize {
1181 match gate_red() {
1182 Ok(Some(GateRed::SwapWo)) => (r + 1) % ranks,
1183 _ => r,
1184 }
1185 };
1186
1187 let rank_shard = |dst: &Engine, r: usize| -> Result<MlaAttnLayer, Box<dyn std::error::Error>> {
1188 let wr = wo_rank(r);
1189 Ok(MlaAttnLayer {
1190 wq_a: replicate(e, dst, &la.wq_a)?,
1191 q_a_norm: replicate(e, dst, &la.q_a_norm)?,
1192 wq_b: shard_rows(e, dst, &la.wq_b, r * hl * head_q..(r + 1) * hl * head_q)?,
1193 wkv_a: replicate(e, dst, &la.wkv_a)?,
1194 kv_a_norm: replicate(e, dst, &la.kv_a_norm)?,
1195 wk_b: shard_rows(e, dst, &la.wk_b, r * hl..(r + 1) * hl)?,
1197 wv_b: shard_rows(e, dst, &la.wv_b, r * hl..(r + 1) * hl)?,
1198 wk_b16: None,
1199 wv_b16: None,
1200 wo: shard_rows(e, dst, &la.wo, wr * hh..(wr + 1) * hh)?,
1202 geom: shard_geom,
1203 index: match &la.index {
1204 Some(ix) => Some(replicate_indexer(dst, ix)?),
1205 None => None,
1206 },
1207 tp: None,
1208 tp_shard: true,
1209 })
1210 };
1211
1212 let mut root = rank_shard(e, 0)?;
1213 let mut peers = Vec::with_capacity(ranks - 1);
1214 for r in 1..ranks {
1215 peers.push(rank_shard(&rt.peers[r - 1], r)?);
1216 }
1217 if !MLA_MARKED.swap(true, Ordering::Relaxed) {
1218 eprintln!(
1219 "[glm5-tp-mla] head shard armed: ranks={ranks} heads_per_rank={hl} kv_rank={} \
1220 latent=replicated indexer=replicated wo=column-over-gather transport={} \
1221 performance_claim=false",
1222 g.kv_rank,
1223 rt.transport.name(),
1224 );
1225 }
1226 root.tp = Some(Box::new(Glm5TpMla {
1227 rt: Arc::clone(rt),
1228 peers,
1229 full_heads: nh,
1230 n_embd,
1231 }));
1232 Ok(root)
1233}
1234
1235pub(crate) fn ensure_mla_peer_latent(
1239 rt: &Glm5TpRt,
1240 canonical: &LatentKvLayer,
1241 cache_slot: &mut Option<Vec<LatentKvLayer>>,
1242) -> Result<(), Box<dyn std::error::Error>> {
1243 if cache_slot.is_some() {
1244 return Ok(());
1245 }
1246 let mut planes = Vec::with_capacity(rt.peers.len());
1247 for dev in &rt.peers {
1248 let rows = dev.zeros(canonical.rows.len())?;
1249 let len_d = dev.htod_i32(&[0])?;
1252 let index_rows = match &canonical.index_rows {
1253 Some(p) => Some(dev.zeros(p.len())?),
1254 None => None,
1255 };
1256 planes.push(LatentKvLayer {
1257 rows,
1258 width: canonical.width,
1259 index_width: canonical.index_width,
1260 len: 0,
1261 len_d,
1262 index_rows,
1263 index_ring_rows: canonical.index_ring_rows,
1264 index_pool_keys: None, index_pools_ready: 0,
1266 index_pool: canonical.index_pool,
1267 });
1268 }
1269 *cache_slot = Some(planes);
1270 Ok(())
1271}
1272
1273pub struct EpRankSlab {
1281 pub gate: CudaSlice<u8>,
1282 pub up: CudaSlice<u8>,
1283 pub down: CudaSlice<u8>,
1284 pub n_experts: usize,
1285}
1286
1287pub struct Glm5EpExps {
1297 pub rt: Arc<Glm5TpRt>,
1298 pub slabs: Vec<EpRankSlab>,
1300 pub owner_of: Vec<u8>,
1302 pub local_of: Vec<u32>,
1304 pub ptr_rows: Vec<CudaSlice<u64>>,
1313}
1314
1315impl Glm5EpExps {
1316 pub fn owner(&self, expert: usize) -> usize {
1318 self.owner_of[expert] as usize
1319 }
1320
1321 pub fn ranks(&self) -> usize {
1322 self.slabs.len()
1323 }
1324}
1325
1326static EP_MARKED: AtomicBool = AtomicBool::new(false);
1327
1328pub static GLM5_EP_PEER_SLOT_DISPATCHES: AtomicU64 = AtomicU64::new(0);
1333
1334pub fn glm5_ep_peer_slot_dispatches() -> u64 {
1335 GLM5_EP_PEER_SLOT_DISPATCHES.load(Ordering::Relaxed)
1336}
1337
1338pub static GLM5_EP_DIET_DISPATCHES: AtomicU64 = AtomicU64::new(0);
1345
1346pub fn glm5_ep_diet_dispatches() -> u64 {
1348 GLM5_EP_DIET_DISPATCHES.load(Ordering::Relaxed)
1349}
1350
1351pub static GLM5_EP_DIET_BULK_RETURNS: AtomicU64 = AtomicU64::new(0);
1355
1356pub fn glm5_ep_diet_bulk_returns() -> u64 {
1358 GLM5_EP_DIET_BULK_RETURNS.load(Ordering::Relaxed)
1359}
1360
1361pub static GLM5_EP_DIET_PEER_ROUNDTRIPS_AVOIDED: AtomicU64 = AtomicU64::new(0);
1365
1366pub fn glm5_ep_diet_peer_roundtrips_avoided() -> u64 {
1368 GLM5_EP_DIET_PEER_ROUNDTRIPS_AVOIDED.load(Ordering::Relaxed)
1369}
1370
1371pub static GLM5_EP_DIET_FANOUT_UPLOADS_AVOIDED: AtomicU64 = AtomicU64::new(0);
1377
1378pub fn glm5_ep_diet_fanout_uploads_avoided() -> u64 {
1380 GLM5_EP_DIET_FANOUT_UPLOADS_AVOIDED.load(Ordering::Relaxed)
1381}
1382
1383pub static GLM5_EP_GROUPED_PRIME_DISPATCHES: AtomicU64 = AtomicU64::new(0);
1387
1388pub fn glm5_ep_grouped_prime_dispatches() -> u64 {
1390 GLM5_EP_GROUPED_PRIME_DISPATCHES.load(Ordering::Relaxed)
1391}
1392
1393pub(crate) fn arm_moe_ep(
1397 e: &Engine,
1398 rt: &Arc<Glm5TpRt>,
1399 m: &mut crate::hybrid::MoeWeights,
1400 placement: Option<&[u8]>,
1401) -> Result<(), Box<dyn std::error::Error>> {
1402 if m.glm5_ep.is_some() {
1403 return Err("arm_moe_ep: layer is already EP-armed".into());
1404 }
1405 let ranks = rt.ranks();
1406 let n_expert = m.gate_exps.n_expert;
1407 if !n_expert.is_multiple_of(ranks) {
1408 return Err(format!(
1409 "glm5-tp EP: {n_expert} experts do not partition across {ranks} ranks"
1410 )
1411 .into());
1412 }
1413 if m.gate_exps.layouts.is_some() || m.up_exps.layouts.is_some() || m.down_exps.layouts.is_some()
1414 {
1415 return Err("glm5-tp EP: per-expert mixed layouts are unwired for EP shards".into());
1416 }
1417 let owner_of: Vec<u8> = match placement {
1418 Some(owners) => {
1419 if owners.len() != n_expert {
1422 return Err(format!(
1423 "glm5-tp EP: placement row carries {} owners for a {n_expert}-expert bank",
1424 owners.len()
1425 )
1426 .into());
1427 }
1428 if owners.iter().any(|&r| (r as usize) >= ranks) {
1429 return Err(
1430 format!("glm5-tp EP: placement row names a rank outside TP-{ranks}").into(),
1431 );
1432 }
1433 owners.to_vec()
1434 }
1435 None => crate::ep_map::EpMap::even_owners(n_expert, ranks),
1436 };
1437 let mut local_of = vec![0u32; n_expert];
1439 let mut owned: Vec<Vec<usize>> = vec![Vec::new(); ranks];
1440 for ex in 0..n_expert {
1441 let r = owner_of[ex] as usize;
1442 local_of[ex] = owned[r].len() as u32;
1443 owned[r].push(ex);
1444 }
1445 if owned.iter().any(|o| o.is_empty()) {
1446 return Err("glm5-tp EP: placement leaves a rank with zero experts (refused)".into());
1447 }
1448 let slab =
1449 |dev: &Engine, experts: &[usize]| -> Result<EpRankSlab, Box<dyn std::error::Error>> {
1450 let cut = |h: &crate::model::HostExps,
1457 pad: usize|
1458 -> Result<CudaSlice<u8>, Box<dyn std::error::Error>> {
1459 let stride = h.expert_stride;
1460 let bytes = h.bytes.as_bytes();
1461 let contiguous = experts.windows(2).all(|w| w[1] == w[0] + 1);
1464 if contiguous {
1465 let a = experts[0] * stride;
1466 let b = (experts[experts.len() - 1] + 1) * stride;
1467 return dev.htod_bytes_padded(&bytes[a..b], pad);
1468 }
1469 let mut staged = Vec::with_capacity(experts.len() * stride);
1472 for &ex in experts {
1473 staged.extend_from_slice(&bytes[ex * stride..(ex + 1) * stride]);
1474 }
1475 dev.htod_bytes_padded(&staged, pad)
1476 };
1477 Ok(EpRankSlab {
1478 gate: cut(&m.gate_exps, 8)?,
1479 up: cut(&m.up_exps, 8)?,
1480 down: cut(&m.down_exps, 144)?,
1481 n_experts: experts.len(),
1482 })
1483 };
1484 let mut slabs = Vec::with_capacity(ranks);
1485 for r in 0..ranks {
1486 slabs.push(slab(rank_engine(e, rt, r), &owned[r])?);
1487 }
1488 if matches!(gate_red(), Ok(Some(GateRed::SwapEpGateUp))) {
1490 let root = &mut slabs[0];
1491 std::mem::swap(&mut root.gate, &mut root.up);
1492 }
1493 if matches!(gate_red(), Ok(Some(GateRed::CorruptEpMap))) {
1496 let n0 = owned[0].len() as u32;
1497 for &ex in &owned[0] {
1498 local_of[ex] = n0 - 1 - local_of[ex];
1499 }
1500 }
1501 let ptr_table = |dev: &Engine,
1506 slab: &EpRankSlab,
1507 rank: u8|
1508 -> Result<CudaSlice<u64>, Box<dyn std::error::Error>> {
1509 use cudarc::driver::DevicePtr;
1510 let (pg, pu, pd) = {
1511 let s = dev.stream();
1512 let (pg, _g0) = slab.gate.device_ptr(&s);
1513 let (pu, _g1) = slab.up.device_ptr(&s);
1514 let (pd, _g2) = slab.down.device_ptr(&s);
1515 (pg, pu, pd)
1516 };
1517 let mut host = vec![0u64; 3 * n_expert];
1518 for ex in 0..n_expert {
1519 if owner_of[ex] != rank {
1520 continue; }
1522 let local = local_of[ex] as usize;
1523 host[ex] = pg + (local * m.gate_exps.expert_stride) as u64;
1524 host[n_expert + ex] = pu + (local * m.up_exps.expert_stride) as u64;
1525 host[2 * n_expert + ex] = pd + (local * m.down_exps.expert_stride) as u64;
1526 }
1527 dev.htod_u64(&host)
1528 };
1529 let mut ptr_rows = Vec::with_capacity(ranks);
1530 for r in 0..ranks {
1531 ptr_rows.push(ptr_table(rank_engine(e, rt, r), &slabs[r], r as u8)?);
1532 }
1533 if !EP_MARKED.swap(true, Ordering::Relaxed) {
1534 eprintln!(
1535 "[glm5-tp-ep] expert-parallel armed: experts_per_rank={:?} ownership={} \
1536 router=root combine=slot-ordered-fmaf transport={} \
1537 performance_claim=false",
1538 owned.iter().map(Vec::len).collect::<Vec<_>>(),
1539 if placement.is_some() {
1544 "measured-map"
1545 } else {
1546 "even-split"
1547 },
1548 rt.transport.name(),
1549 );
1550 }
1551 m.dev_exps = None;
1554 m.glm5_ep = Some(Glm5EpExps {
1555 rt: Arc::clone(rt),
1556 slabs,
1557 owner_of,
1558 local_of,
1559 ptr_rows,
1560 });
1561 Ok(())
1562}
1563
1564#[cfg(test)]
1565mod tests {
1566 use super::*;
1567
1568 #[test]
1569 fn parse_is_literal_and_fail_closed() {
1570 assert!(parse_glm5_tp_layer_specs(None, 45).unwrap().is_empty());
1572 assert!(parse_glm5_tp_layer_specs(Some(""), 45).unwrap().is_empty());
1573 assert!(parse_glm5_tp_layer_specs(Some("0"), 45).unwrap().is_empty());
1574 let all = parse_glm5_tp_layer_specs(Some("all@0,1"), 45).unwrap();
1576 assert_eq!(all.len(), 45);
1577 assert_eq!(all[0].devices, vec![0, 1]);
1578 let all4 = parse_glm5_tp_layer_specs(Some("all@0,1"), 4).unwrap();
1579 assert_eq!(all4.len(), 4);
1580 let quad = parse_glm5_tp_layer_specs(Some("all@0,1,2,3"), 45).unwrap();
1582 assert_eq!(quad.len(), 45);
1583 assert_eq!(quad[0].devices, vec![0, 1, 2, 3]);
1584 let r = parse_glm5_tp_layer_specs(Some("0-2@0,1;4@0,1"), 45).unwrap();
1586 assert_eq!(
1587 r.iter().map(|s| s.layer).collect::<Vec<_>>(),
1588 vec![0, 1, 2, 4]
1589 );
1590 assert!(parse_glm5_tp_layer_specs(Some("0@0,0"), 45).is_err());
1592 assert!(parse_glm5_tp_layer_specs(Some("0@0,1;0@0,1"), 45).is_err());
1593 assert!(parse_glm5_tp_layer_specs(Some("banana"), 45).is_err());
1594 }
1595
1596 fn fixture_view() -> Glm5TpModelView {
1597 Glm5TpModelView {
1598 trunk_layers: 4,
1599 layer_class: vec![
1600 Glm5LayerClass::Kda,
1601 Glm5LayerClass::Mla,
1602 Glm5LayerClass::Kda,
1603 Glm5LayerClass::Mla,
1604 ],
1605 layer_is_moe: vec![false, true, true, true],
1606 kda_heads: 4,
1607 kda_head_dim: 128,
1608 mla_heads: 4,
1609 n_routed_experts: 4,
1610 top_k: 2,
1611 }
1612 }
1613
1614 #[test]
1618 fn preflight_geometry_laws_are_dimension_derived() {
1619 let v = fixture_view();
1622 for ranks in GLM5_TP_ALLOWED_RANKS {
1623 assert_eq!(v.kda_heads % ranks, 0);
1624 assert_eq!(v.mla_heads % ranks, 0);
1625 assert_eq!(v.n_routed_experts % ranks, 0);
1626 }
1627 let odd = Glm5TpModelView {
1628 kda_heads: 3,
1629 ..fixture_view()
1630 };
1631 assert_ne!(odd.kda_heads % 2, 0);
1632 let bad_dim = Glm5TpModelView {
1633 kda_head_dim: 64,
1634 ..fixture_view()
1635 };
1636 assert_ne!(bad_dim.kda_head_dim, crate::kda::KDA_HEAD_DIM);
1637 let odd_experts = Glm5TpModelView {
1638 n_routed_experts: 5,
1639 ..fixture_view()
1640 };
1641 assert_ne!(odd_experts.n_routed_experts % 2, 0);
1642 assert!(!GLM5_TP_ALLOWED_RANKS.contains(&3));
1644 }
1645
1646 #[test]
1647 fn armed_check_counts_parse_errors_as_armed() {
1648 for (v, armed) in [
1653 ("", false),
1654 ("0", false),
1655 ("all@0,1", true),
1656 ("all@0,1,2,3", true),
1657 ("junk", true),
1658 ] {
1659 let is_armed = !v.is_empty() && v != "0";
1660 assert_eq!(is_armed, armed);
1661 }
1662 }
1663
1664 #[test]
1665 fn every_refused_door_composition_bites_by_name() {
1666 for (flag, _) in GLM5_TP_REFUSED_DOOR_FLAGS {
1672 let err = refuse_glm5_tp_door_composition(|f| f == flag)
1673 .expect_err("an armed door must refuse");
1674 assert!(err.contains("MEMRA_GLM5_TP"), "{err}");
1675 assert!(err.contains(flag), "{err}");
1676 assert!(err.contains("unproven composition"), "{err}");
1677 }
1678 refuse_glm5_tp_door_composition(|_| false).expect("cold doors must pass");
1680 refuse_glm5_tp_door_composition(|f| f == "MEMRA_GLM5_VERIFY_BATCH")
1684 .expect("verify-batch is refused via the spec co-refusal, not here");
1685 }
1686}