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 )?;
972 std::mem::swap(ssm_state, ssm_state_alt);
973 out
974 };
975 if let Some(clock) = scan_clock.as_deref_mut() {
976 *clock += rank_scan_ns;
977 }
978 if rows_exact {
979 let snap = snap.expect("verify arm cloned the snapshot above");
980 let rank_stash = rank_stash
981 .ok_or("kda_core_gated returned without filling the requested rows stash")?;
982 captured[r] = Some((snap, rank_stash));
983 }
984 gated[r] = Some(out);
985 }
986 if let Some(stash_vec) = verify_stash {
987 stash_vec.clear();
988 for c in captured {
989 stash_vec.push(c.expect("every rank captured on the verify arm"));
990 }
991 }
992
993 debug_assert_eq!(full, ranks * ql);
997 let gated_refs: Vec<&CudaSlice<f32>> = gated
998 .iter()
999 .map(|g| g.as_ref().expect("filled above"))
1000 .collect();
1001 let fulls = crate::tp_transport::gather_parts(&hop, &gated_refs, t, ql)?;
1002
1003 let mut ys = Vec::with_capacity(ranks);
1006 if rows_exact {
1007 ys.push(e.matmul_rows_exact(&la_root.wo, &fulls[0], t)?);
1008 for r in 1..ranks {
1009 ys.push(rt.peers[r - 1].matmul_rows_exact(&tp.peers[r - 1].wo, &fulls[r], t)?);
1010 }
1011 } else {
1012 ys.push(e.matmul(&la_root.wo, &fulls[0], t)?);
1013 for r in 1..ranks {
1014 ys.push(rt.peers[r - 1].matmul(&tp.peers[r - 1].wo, &fulls[r], t)?);
1015 }
1016 }
1017
1018 let y_refs: Vec<&CudaSlice<f32>> = ys.iter().collect();
1020 crate::tp_transport::concat_parts_on_root(&hop, &y_refs, t, hh)
1021}
1022
1023#[allow(clippy::too_many_arguments)] pub(crate) fn kda_tp_cached(
1026 e: &Engine,
1027 la_root: &KdaAttnLayer,
1028 x: &CudaSlice<f32>,
1029 t: usize,
1030 eps: f32,
1031 cache: &mut Cache,
1032 il: usize,
1033 arm: ConvArm,
1034) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1035 kda_tp_core(e, la_root, x, t, eps, cache, il, arm, None, None)
1036}
1037
1038pub type Glm5TpKdaVerifyStash = Vec<(CudaSlice<f32>, crate::kda::KdaRowsStash)>;
1046
1047#[allow(clippy::too_many_arguments)] pub(crate) fn kda_tp_verify_rows(
1053 e: &Engine,
1054 la_root: &KdaAttnLayer,
1055 x: &CudaSlice<f32>,
1056 t: usize,
1057 eps: f32,
1058 cache: &mut Cache,
1059 il: usize,
1060 scan_clock: Option<&mut u64>,
1061) -> Result<(CudaSlice<f32>, Glm5TpKdaVerifyStash), Box<dyn std::error::Error>> {
1062 let mut stash: Glm5TpKdaVerifyStash = Vec::new();
1063 let out = kda_tp_core(
1064 e,
1065 la_root,
1066 x,
1067 t,
1068 eps,
1069 cache,
1070 il,
1071 ConvArm::Prefill,
1072 Some(&mut stash),
1073 scan_clock,
1074 )?;
1075 Ok((out, stash))
1076}
1077
1078pub(crate) fn kda_tp_verify_rollback(
1082 e: &Engine,
1083 la_root: &KdaAttnLayer,
1084 stash: &Glm5TpKdaVerifyStash,
1085 keep: usize,
1086 cache: &mut Cache,
1087 il: usize,
1088) -> Result<(), Box<dyn std::error::Error>> {
1089 let tp = la_root
1090 .tp
1091 .as_ref()
1092 .ok_or("kda_tp_verify_rollback called on an unsharded layer")?;
1093 let rt = &tp.rt;
1094 let ranks = rt.ranks();
1095 if stash.len() != ranks {
1096 return Err(format!(
1097 "glm5-tp verify rollback: stash carries {} ranks, the runtime has {ranks}",
1098 stash.len()
1099 )
1100 .into());
1101 }
1102 let states = cache.glm5_tp_recur[il]
1103 .as_mut()
1104 .ok_or_else(|| format!("glm5-tp verify rollback: layer {il} has no per-rank state"))?;
1105 for r in 0..ranks {
1106 let dev = if r == 0 { e } else { &rt.peers[r - 1] };
1107 let la = if r == 0 { la_root } else { &tp.peers[r - 1] };
1108 let (snap, rows) = &stash[r];
1109 crate::kda::kda_verify_rollback_rows_on(dev, la, snap, rows, keep, &mut states[r], il)?;
1110 }
1111 Ok(())
1112}
1113
1114pub struct Glm5TpMla {
1121 pub rt: Arc<Glm5TpRt>,
1122 pub peers: Vec<crate::hybrid::MlaAttnLayer>,
1124 pub full_heads: usize,
1126 pub n_embd: usize,
1128}
1129
1130impl Glm5TpMla {
1131 pub fn ranks(&self) -> usize {
1132 self.peers.len() + 1
1133 }
1134}
1135
1136static MLA_MARKED: AtomicBool = AtomicBool::new(false);
1137
1138pub(crate) fn shard_mla_layer(
1139 e: &Engine,
1140 rt: &Arc<Glm5TpRt>,
1141 la: crate::hybrid::MlaAttnLayer,
1142) -> Result<crate::hybrid::MlaAttnLayer, Box<dyn std::error::Error>> {
1143 use crate::hybrid::{MlaAttnLayer, MlaIndexer};
1144 if la.tp.is_some() {
1145 return Err("shard_mla_layer: layer is already sharded".into());
1146 }
1147 let ranks = rt.ranks();
1148 let g = la.geom;
1149 let nh = g.n_head;
1150 if !nh.is_multiple_of(ranks) {
1151 return Err(format!("MLA heads {nh} do not shard across {ranks} ranks").into());
1152 }
1153 let hl = nh / ranks;
1154 let head_q = g.d_nope + g.d_rope; let n_embd = la.wo.out_features();
1156 if !n_embd.is_multiple_of(ranks) {
1157 return Err(format!("MLA wo out {n_embd} does not split across ranks").into());
1158 }
1159 let hh = n_embd / ranks;
1160
1161 let mut shard_geom = g;
1162 shard_geom.n_head = hl;
1163
1164 let replicate_indexer =
1165 |dst: &Engine, ix: &MlaIndexer| -> Result<MlaIndexer, Box<dyn std::error::Error>> {
1166 Ok(MlaIndexer {
1167 wq_b: replicate(e, dst, &ix.wq_b)?,
1168 wk: replicate(e, dst, &ix.wk)?,
1169 k_norm_w: replicate(e, dst, &ix.k_norm_w)?,
1170 k_norm_b: replicate(e, dst, &ix.k_norm_b)?,
1171 weights_proj: replicate(e, dst, &ix.weights_proj)?,
1172 kpool_gate: replicate(e, dst, &ix.kpool_gate)?,
1173 kpool_ape: replicate(e, dst, &ix.kpool_ape)?,
1174 geom: ix.geom,
1175 })
1176 };
1177
1178 let wo_rank = |r: usize| -> usize {
1180 match gate_red() {
1181 Ok(Some(GateRed::SwapWo)) => (r + 1) % ranks,
1182 _ => r,
1183 }
1184 };
1185
1186 let rank_shard = |dst: &Engine, r: usize| -> Result<MlaAttnLayer, Box<dyn std::error::Error>> {
1187 let wr = wo_rank(r);
1188 Ok(MlaAttnLayer {
1189 wq_a: replicate(e, dst, &la.wq_a)?,
1190 q_a_norm: replicate(e, dst, &la.q_a_norm)?,
1191 wq_b: shard_rows(e, dst, &la.wq_b, r * hl * head_q..(r + 1) * hl * head_q)?,
1192 wkv_a: replicate(e, dst, &la.wkv_a)?,
1193 kv_a_norm: replicate(e, dst, &la.kv_a_norm)?,
1194 wk_b: shard_rows(e, dst, &la.wk_b, r * hl..(r + 1) * hl)?,
1196 wv_b: shard_rows(e, dst, &la.wv_b, r * hl..(r + 1) * hl)?,
1197 wo: shard_rows(e, dst, &la.wo, wr * hh..(wr + 1) * hh)?,
1199 geom: shard_geom,
1200 index: match &la.index {
1201 Some(ix) => Some(replicate_indexer(dst, ix)?),
1202 None => None,
1203 },
1204 tp: None,
1205 tp_shard: true,
1206 })
1207 };
1208
1209 let mut root = rank_shard(e, 0)?;
1210 let mut peers = Vec::with_capacity(ranks - 1);
1211 for r in 1..ranks {
1212 peers.push(rank_shard(&rt.peers[r - 1], r)?);
1213 }
1214 if !MLA_MARKED.swap(true, Ordering::Relaxed) {
1215 eprintln!(
1216 "[glm5-tp-mla] head shard armed: ranks={ranks} heads_per_rank={hl} kv_rank={} \
1217 latent=replicated indexer=replicated wo=column-over-gather transport={} \
1218 performance_claim=false",
1219 g.kv_rank,
1220 rt.transport.name(),
1221 );
1222 }
1223 root.tp = Some(Box::new(Glm5TpMla {
1224 rt: Arc::clone(rt),
1225 peers,
1226 full_heads: nh,
1227 n_embd,
1228 }));
1229 Ok(root)
1230}
1231
1232pub(crate) fn ensure_mla_peer_latent(
1236 rt: &Glm5TpRt,
1237 canonical: &LatentKvLayer,
1238 cache_slot: &mut Option<Vec<LatentKvLayer>>,
1239) -> Result<(), Box<dyn std::error::Error>> {
1240 if cache_slot.is_some() {
1241 return Ok(());
1242 }
1243 let mut planes = Vec::with_capacity(rt.peers.len());
1244 for dev in &rt.peers {
1245 let rows = dev.zeros(canonical.rows.len())?;
1246 let len_d = dev.htod_i32(&[0])?;
1249 let index_rows = match &canonical.index_rows {
1250 Some(p) => Some(dev.zeros(p.len())?),
1251 None => None,
1252 };
1253 planes.push(LatentKvLayer {
1254 rows,
1255 width: canonical.width,
1256 index_width: canonical.index_width,
1257 len: 0,
1258 len_d,
1259 index_rows,
1260 index_ring_rows: canonical.index_ring_rows,
1261 index_pool_keys: None, index_pools_ready: 0,
1263 index_pool: canonical.index_pool,
1264 });
1265 }
1266 *cache_slot = Some(planes);
1267 Ok(())
1268}
1269
1270pub struct EpRankSlab {
1278 pub gate: CudaSlice<u8>,
1279 pub up: CudaSlice<u8>,
1280 pub down: CudaSlice<u8>,
1281 pub n_experts: usize,
1282}
1283
1284pub struct Glm5EpExps {
1294 pub rt: Arc<Glm5TpRt>,
1295 pub slabs: Vec<EpRankSlab>,
1297 pub owner_of: Vec<u8>,
1299 pub local_of: Vec<u32>,
1301 pub ptr_rows: Vec<CudaSlice<u64>>,
1310}
1311
1312impl Glm5EpExps {
1313 pub fn owner(&self, expert: usize) -> usize {
1315 self.owner_of[expert] as usize
1316 }
1317
1318 pub fn ranks(&self) -> usize {
1319 self.slabs.len()
1320 }
1321}
1322
1323static EP_MARKED: AtomicBool = AtomicBool::new(false);
1324
1325pub static GLM5_EP_PEER_SLOT_DISPATCHES: AtomicU64 = AtomicU64::new(0);
1330
1331pub fn glm5_ep_peer_slot_dispatches() -> u64 {
1332 GLM5_EP_PEER_SLOT_DISPATCHES.load(Ordering::Relaxed)
1333}
1334
1335pub static GLM5_EP_DIET_DISPATCHES: AtomicU64 = AtomicU64::new(0);
1342
1343pub fn glm5_ep_diet_dispatches() -> u64 {
1345 GLM5_EP_DIET_DISPATCHES.load(Ordering::Relaxed)
1346}
1347
1348pub static GLM5_EP_DIET_BULK_RETURNS: AtomicU64 = AtomicU64::new(0);
1352
1353pub fn glm5_ep_diet_bulk_returns() -> u64 {
1355 GLM5_EP_DIET_BULK_RETURNS.load(Ordering::Relaxed)
1356}
1357
1358pub static GLM5_EP_DIET_PEER_ROUNDTRIPS_AVOIDED: AtomicU64 = AtomicU64::new(0);
1362
1363pub fn glm5_ep_diet_peer_roundtrips_avoided() -> u64 {
1365 GLM5_EP_DIET_PEER_ROUNDTRIPS_AVOIDED.load(Ordering::Relaxed)
1366}
1367
1368pub static GLM5_EP_DIET_FANOUT_UPLOADS_AVOIDED: AtomicU64 = AtomicU64::new(0);
1374
1375pub fn glm5_ep_diet_fanout_uploads_avoided() -> u64 {
1377 GLM5_EP_DIET_FANOUT_UPLOADS_AVOIDED.load(Ordering::Relaxed)
1378}
1379
1380pub static GLM5_EP_GROUPED_PRIME_DISPATCHES: AtomicU64 = AtomicU64::new(0);
1384
1385pub fn glm5_ep_grouped_prime_dispatches() -> u64 {
1387 GLM5_EP_GROUPED_PRIME_DISPATCHES.load(Ordering::Relaxed)
1388}
1389
1390pub(crate) fn arm_moe_ep(
1394 e: &Engine,
1395 rt: &Arc<Glm5TpRt>,
1396 m: &mut crate::hybrid::MoeWeights,
1397 placement: Option<&[u8]>,
1398) -> Result<(), Box<dyn std::error::Error>> {
1399 if m.glm5_ep.is_some() {
1400 return Err("arm_moe_ep: layer is already EP-armed".into());
1401 }
1402 let ranks = rt.ranks();
1403 let n_expert = m.gate_exps.n_expert;
1404 if !n_expert.is_multiple_of(ranks) {
1405 return Err(format!(
1406 "glm5-tp EP: {n_expert} experts do not partition across {ranks} ranks"
1407 )
1408 .into());
1409 }
1410 if m.gate_exps.layouts.is_some() || m.up_exps.layouts.is_some() || m.down_exps.layouts.is_some()
1411 {
1412 return Err("glm5-tp EP: per-expert mixed layouts are unwired for EP shards".into());
1413 }
1414 let owner_of: Vec<u8> = match placement {
1415 Some(owners) => {
1416 if owners.len() != n_expert {
1419 return Err(format!(
1420 "glm5-tp EP: placement row carries {} owners for a {n_expert}-expert bank",
1421 owners.len()
1422 )
1423 .into());
1424 }
1425 if owners.iter().any(|&r| (r as usize) >= ranks) {
1426 return Err(
1427 format!("glm5-tp EP: placement row names a rank outside TP-{ranks}").into(),
1428 );
1429 }
1430 owners.to_vec()
1431 }
1432 None => crate::ep_map::EpMap::even_owners(n_expert, ranks),
1433 };
1434 let mut local_of = vec![0u32; n_expert];
1436 let mut owned: Vec<Vec<usize>> = vec![Vec::new(); ranks];
1437 for ex in 0..n_expert {
1438 let r = owner_of[ex] as usize;
1439 local_of[ex] = owned[r].len() as u32;
1440 owned[r].push(ex);
1441 }
1442 if owned.iter().any(|o| o.is_empty()) {
1443 return Err("glm5-tp EP: placement leaves a rank with zero experts (refused)".into());
1444 }
1445 let slab =
1446 |dev: &Engine, experts: &[usize]| -> Result<EpRankSlab, Box<dyn std::error::Error>> {
1447 let cut = |h: &crate::model::HostExps,
1454 pad: usize|
1455 -> Result<CudaSlice<u8>, Box<dyn std::error::Error>> {
1456 let stride = h.expert_stride;
1457 let bytes = h.bytes.as_bytes();
1458 let contiguous = experts.windows(2).all(|w| w[1] == w[0] + 1);
1461 if contiguous {
1462 let a = experts[0] * stride;
1463 let b = (experts[experts.len() - 1] + 1) * stride;
1464 return dev.htod_bytes_padded(&bytes[a..b], pad);
1465 }
1466 let mut staged = Vec::with_capacity(experts.len() * stride);
1469 for &ex in experts {
1470 staged.extend_from_slice(&bytes[ex * stride..(ex + 1) * stride]);
1471 }
1472 dev.htod_bytes_padded(&staged, pad)
1473 };
1474 Ok(EpRankSlab {
1475 gate: cut(&m.gate_exps, 8)?,
1476 up: cut(&m.up_exps, 8)?,
1477 down: cut(&m.down_exps, 144)?,
1478 n_experts: experts.len(),
1479 })
1480 };
1481 let mut slabs = Vec::with_capacity(ranks);
1482 for r in 0..ranks {
1483 slabs.push(slab(rank_engine(e, rt, r), &owned[r])?);
1484 }
1485 if matches!(gate_red(), Ok(Some(GateRed::SwapEpGateUp))) {
1487 let root = &mut slabs[0];
1488 std::mem::swap(&mut root.gate, &mut root.up);
1489 }
1490 if matches!(gate_red(), Ok(Some(GateRed::CorruptEpMap))) {
1493 let n0 = owned[0].len() as u32;
1494 for &ex in &owned[0] {
1495 local_of[ex] = n0 - 1 - local_of[ex];
1496 }
1497 }
1498 let ptr_table = |dev: &Engine,
1503 slab: &EpRankSlab,
1504 rank: u8|
1505 -> Result<CudaSlice<u64>, Box<dyn std::error::Error>> {
1506 use cudarc::driver::DevicePtr;
1507 let (pg, pu, pd) = {
1508 let s = dev.stream();
1509 let (pg, _g0) = slab.gate.device_ptr(&s);
1510 let (pu, _g1) = slab.up.device_ptr(&s);
1511 let (pd, _g2) = slab.down.device_ptr(&s);
1512 (pg, pu, pd)
1513 };
1514 let mut host = vec![0u64; 3 * n_expert];
1515 for ex in 0..n_expert {
1516 if owner_of[ex] != rank {
1517 continue; }
1519 let local = local_of[ex] as usize;
1520 host[ex] = pg + (local * m.gate_exps.expert_stride) as u64;
1521 host[n_expert + ex] = pu + (local * m.up_exps.expert_stride) as u64;
1522 host[2 * n_expert + ex] = pd + (local * m.down_exps.expert_stride) as u64;
1523 }
1524 dev.htod_u64(&host)
1525 };
1526 let mut ptr_rows = Vec::with_capacity(ranks);
1527 for r in 0..ranks {
1528 ptr_rows.push(ptr_table(rank_engine(e, rt, r), &slabs[r], r as u8)?);
1529 }
1530 if !EP_MARKED.swap(true, Ordering::Relaxed) {
1531 eprintln!(
1532 "[glm5-tp-ep] expert-parallel armed: experts_per_rank={:?} ownership={} \
1533 router=root combine=slot-ordered-fmaf transport={} \
1534 performance_claim=false",
1535 owned.iter().map(Vec::len).collect::<Vec<_>>(),
1536 if placement.is_some() {
1541 "measured-map"
1542 } else {
1543 "even-split"
1544 },
1545 rt.transport.name(),
1546 );
1547 }
1548 m.dev_exps = None;
1551 m.glm5_ep = Some(Glm5EpExps {
1552 rt: Arc::clone(rt),
1553 slabs,
1554 owner_of,
1555 local_of,
1556 ptr_rows,
1557 });
1558 Ok(())
1559}
1560
1561#[cfg(test)]
1562mod tests {
1563 use super::*;
1564
1565 #[test]
1566 fn parse_is_literal_and_fail_closed() {
1567 assert!(parse_glm5_tp_layer_specs(None, 45).unwrap().is_empty());
1569 assert!(parse_glm5_tp_layer_specs(Some(""), 45).unwrap().is_empty());
1570 assert!(parse_glm5_tp_layer_specs(Some("0"), 45).unwrap().is_empty());
1571 let all = parse_glm5_tp_layer_specs(Some("all@0,1"), 45).unwrap();
1573 assert_eq!(all.len(), 45);
1574 assert_eq!(all[0].devices, vec![0, 1]);
1575 let all4 = parse_glm5_tp_layer_specs(Some("all@0,1"), 4).unwrap();
1576 assert_eq!(all4.len(), 4);
1577 let quad = parse_glm5_tp_layer_specs(Some("all@0,1,2,3"), 45).unwrap();
1579 assert_eq!(quad.len(), 45);
1580 assert_eq!(quad[0].devices, vec![0, 1, 2, 3]);
1581 let r = parse_glm5_tp_layer_specs(Some("0-2@0,1;4@0,1"), 45).unwrap();
1583 assert_eq!(
1584 r.iter().map(|s| s.layer).collect::<Vec<_>>(),
1585 vec![0, 1, 2, 4]
1586 );
1587 assert!(parse_glm5_tp_layer_specs(Some("0@0,0"), 45).is_err());
1589 assert!(parse_glm5_tp_layer_specs(Some("0@0,1;0@0,1"), 45).is_err());
1590 assert!(parse_glm5_tp_layer_specs(Some("banana"), 45).is_err());
1591 }
1592
1593 fn fixture_view() -> Glm5TpModelView {
1594 Glm5TpModelView {
1595 trunk_layers: 4,
1596 layer_class: vec![
1597 Glm5LayerClass::Kda,
1598 Glm5LayerClass::Mla,
1599 Glm5LayerClass::Kda,
1600 Glm5LayerClass::Mla,
1601 ],
1602 layer_is_moe: vec![false, true, true, true],
1603 kda_heads: 4,
1604 kda_head_dim: 128,
1605 mla_heads: 4,
1606 n_routed_experts: 4,
1607 top_k: 2,
1608 }
1609 }
1610
1611 #[test]
1615 fn preflight_geometry_laws_are_dimension_derived() {
1616 let v = fixture_view();
1619 for ranks in GLM5_TP_ALLOWED_RANKS {
1620 assert_eq!(v.kda_heads % ranks, 0);
1621 assert_eq!(v.mla_heads % ranks, 0);
1622 assert_eq!(v.n_routed_experts % ranks, 0);
1623 }
1624 let odd = Glm5TpModelView {
1625 kda_heads: 3,
1626 ..fixture_view()
1627 };
1628 assert_ne!(odd.kda_heads % 2, 0);
1629 let bad_dim = Glm5TpModelView {
1630 kda_head_dim: 64,
1631 ..fixture_view()
1632 };
1633 assert_ne!(bad_dim.kda_head_dim, crate::kda::KDA_HEAD_DIM);
1634 let odd_experts = Glm5TpModelView {
1635 n_routed_experts: 5,
1636 ..fixture_view()
1637 };
1638 assert_ne!(odd_experts.n_routed_experts % 2, 0);
1639 assert!(!GLM5_TP_ALLOWED_RANKS.contains(&3));
1641 }
1642
1643 #[test]
1644 fn armed_check_counts_parse_errors_as_armed() {
1645 for (v, armed) in [
1650 ("", false),
1651 ("0", false),
1652 ("all@0,1", true),
1653 ("all@0,1,2,3", true),
1654 ("junk", true),
1655 ] {
1656 let is_armed = !v.is_empty() && v != "0";
1657 assert_eq!(is_armed, armed);
1658 }
1659 }
1660
1661 #[test]
1662 fn every_refused_door_composition_bites_by_name() {
1663 for (flag, _) in GLM5_TP_REFUSED_DOOR_FLAGS {
1669 let err = refuse_glm5_tp_door_composition(|f| f == flag)
1670 .expect_err("an armed door must refuse");
1671 assert!(err.contains("MEMRA_GLM5_TP"), "{err}");
1672 assert!(err.contains(flag), "{err}");
1673 assert!(err.contains("unproven composition"), "{err}");
1674 }
1675 refuse_glm5_tp_door_composition(|_| false).expect("cold doors must pass");
1677 refuse_glm5_tp_door_composition(|f| f == "MEMRA_GLM5_VERIFY_BATCH")
1681 .expect("verify-batch is refused via the spec co-refusal, not here");
1682 }
1683}