1use std::ops::Range;
53use std::sync::Arc;
54use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
55
56use cudarc::driver::CudaSlice;
57
58use crate::Engine;
59use crate::kda::{ConvArm, KdaAttnLayer};
60use crate::model::GpuTensor;
61use memra_kv::{Cache, LatentKvLayer, RecurLayer};
62
63pub const GLM5_TP_ALLOWED_RANKS: [usize; 2] = [2, 4];
74
75pub type Glm5TpLayerSpec = crate::tp::StepEpLayerSpec;
76
77pub fn glm5_tp_env_raw() -> Option<String> {
83 std::env::var("MEMRA_GLM5_TP").ok()
84}
85
86pub fn glm5_tp_armed() -> bool {
90 matches!(glm5_tp_env_raw().as_deref(), Some(v) if !v.is_empty() && v != "0")
91}
92
93pub fn parse_glm5_tp_layer_specs(
97 value: Option<&str>,
98 trunk_layers: usize,
99) -> Result<Vec<Glm5TpLayerSpec>, String> {
100 crate::tp::parse_layer_specs_for_trunk("MEMRA_GLM5_TP", value, Some(trunk_layers))
101}
102
103pub fn gate_same_device() -> bool {
109 std::env::var("MEMRA_GLM5_TP_GATE_SAME_DEV").as_deref() == Ok("1")
110}
111
112#[derive(Clone, Copy, PartialEq, Eq, Debug)]
126pub enum GateRed {
127 SwapWo,
128 SwapEpGateUp,
129 SkipPeerCombine,
130 CorruptEpMap,
131}
132
133pub fn gate_red() -> Result<Option<GateRed>, String> {
134 match std::env::var("MEMRA_GLM5_TP_GATE_RED").ok().as_deref() {
135 None | Some("") => Ok(None),
136 Some("swap-wo") => Ok(Some(GateRed::SwapWo)),
137 Some("swap-ep-gateup") => Ok(Some(GateRed::SwapEpGateUp)),
138 Some("skip-peer-combine") => Ok(Some(GateRed::SkipPeerCombine)),
139 Some("corrupt-ep-map") => Ok(Some(GateRed::CorruptEpMap)),
140 Some(other) => Err(format!(
141 "MEMRA_GLM5_TP_GATE_RED={other:?} is not a known red arm \
142 (swap-wo | swap-ep-gateup | skip-peer-combine | corrupt-ep-map)"
143 )),
144 }
145}
146
147pub struct Glm5TpRt {
155 pub peers: Vec<Engine>,
156 pub root_dev: usize,
157 pub peer_devs: Vec<usize>,
158 pub same_device_gate: bool,
162 pub transport: crate::glm5_tp_transport::Glm5TpTransport,
166 link: Option<crate::glm5_tp_transport::PeerPullLink>,
169}
170
171impl Glm5TpRt {
172 pub fn new(devices: &[usize]) -> Result<Self, Box<dyn std::error::Error>> {
173 let root_dev = devices[0];
174 let peer_devs: Vec<usize> = devices[1..].to_vec();
175 for &d in &peer_devs {
176 if d == root_dev || peer_devs.iter().filter(|&&x| x == d).count() > 1 {
177 return Err(format!(
178 "MEMRA_GLM5_TP rank devices must be distinct in serving; got {devices:?} \
179 (the same-device form exists only for the rig gate binary)"
180 )
181 .into());
182 }
183 }
184 let mut peers = Vec::with_capacity(peer_devs.len());
185 for &d in &peer_devs {
186 peers.push(Engine::new(d)?);
187 }
188 Ok(Self {
189 peers,
190 root_dev,
191 peer_devs,
192 same_device_gate: false,
193 transport: crate::glm5_tp_transport::Glm5TpTransport::HostCanonical,
194 link: None,
195 })
196 }
197
198 pub fn new_gate_same_device(
204 root_dev: usize,
205 ranks: usize,
206 ) -> Result<Self, Box<dyn std::error::Error>> {
207 let mut peers = Vec::with_capacity(ranks - 1);
208 for _ in 1..ranks {
209 peers.push(Engine::new(root_dev)?);
210 }
211 Ok(Self {
212 peers,
213 root_dev,
214 peer_devs: vec![root_dev; ranks - 1],
215 same_device_gate: true,
216 transport: crate::glm5_tp_transport::Glm5TpTransport::HostCanonical,
217 link: None,
218 })
219 }
220
221 pub fn ranks(&self) -> usize {
223 self.peers.len() + 1
224 }
225
226 pub fn arm_transport(&mut self, root: &Engine) -> Result<(), Box<dyn std::error::Error>> {
231 let transport = crate::glm5_tp_transport::transport_env()?;
232 let engines: Vec<&Engine> = std::iter::once(root).chain(self.peers.iter()).collect();
233 let link =
234 crate::glm5_tp_transport::arm_transport(transport, &engines, self.same_device_gate)?;
235 self.transport = transport;
236 self.link = link;
237 Ok(())
238 }
239
240 pub fn hop<'a>(&'a self, root: &'a Engine) -> crate::glm5_tp_transport::Hop<'a> {
244 crate::glm5_tp_transport::Hop {
245 engines: std::iter::once(root).chain(self.peers.iter()).collect(),
246 transport: self.transport,
247 link: self.link.as_ref(),
248 }
249 }
250}
251
252pub struct Glm5TpModelView {
260 pub trunk_layers: usize,
261 pub layer_class: Vec<Glm5LayerClass>,
263 pub layer_is_moe: Vec<bool>,
265 pub kda_heads: usize,
266 pub kda_head_dim: usize,
267 pub mla_heads: usize,
268 pub n_routed_experts: usize,
269 pub top_k: usize,
270}
271
272#[derive(Clone, Copy, PartialEq, Eq, Debug)]
273pub enum Glm5LayerClass {
274 Kda,
275 Mla,
276}
277
278pub struct Glm5TpLoadPlan {
282 pub rt: Arc<Glm5TpRt>,
283 pub layers: std::collections::BTreeSet<usize>,
284 pub ep_map: Option<crate::ep_map::EpMap>,
285}
286
287fn load_glm5_ep_map(
294 view: &Glm5TpModelView,
295 layers: &std::collections::BTreeSet<usize>,
296 ranks: usize,
297) -> Result<Option<crate::ep_map::EpMap>, Box<dyn std::error::Error>> {
298 let Some((flag, path)) = crate::ep_map::ep_map_env()? else {
299 return Ok(None);
300 };
301 if path.is_empty() {
302 return Err(format!(
303 "{flag} is set but empty (fail-closed: unset the flag for \
304 the even split; an empty value never silently means default)"
305 )
306 .into());
307 }
308 let text = std::fs::read_to_string(&path)
309 .map_err(|e| format!("{flag}={path}: cannot read the map file ({e}) — refused by name"))?;
310 let map = crate::ep_map::EpMap::parse(&text).map_err(|e| format!("{flag}={path}: {e}"))?;
311 if map.ranks != ranks {
312 return Err(format!(
313 "{flag}={path}: map declares ranks={}, this load is TP-{ranks} \
314 (re-mint the map for the armed rank count)",
315 map.ranks
316 )
317 .into());
318 }
319 if map.n_experts != view.n_routed_experts {
320 return Err(format!(
321 "{flag}={path}: map declares expert_count={}, the model routes {}",
322 map.n_experts, view.n_routed_experts
323 )
324 .into());
325 }
326 if map.entry_rank != 0 {
327 return Err(format!(
328 "{flag}={path}: entry_rank={} but the glm5 TP first-hop card is \
329 rank 0 (root: router + combine + shared expert) — re-mint with \
330 --entry-rank 0 (refused rather than silently remapping ranks)",
331 map.entry_rank
332 )
333 .into());
334 }
335 let ep_layers: Vec<usize> = layers
336 .iter()
337 .copied()
338 .filter(|&il| view.layer_is_moe[il])
339 .collect();
340 map.validate_layer_cover(&ep_layers)
341 .map_err(|e| format!("{flag}={path}: {e}"))?;
342 let digest = {
344 use sha2::{Digest, Sha256};
345 let mut h = Sha256::new();
346 h.update(text.as_bytes());
347 let out = h.finalize();
348 out.iter().map(|b| format!("{b:02x}")).collect::<String>()
349 };
350 eprintln!(
351 "[glm5-tp-preflight] ep-map armed path={path} sha256={digest} layers={} \
352 experts={} ranks={} entry_rank={} performance_claim=false",
353 map.layers.len(),
354 map.n_experts,
355 map.ranks,
356 map.entry_rank,
357 );
358 Ok(Some(map))
359}
360
361pub const GLM5_TP_REFUSED_DOOR_FLAGS: [(&str, &str); 4] = [
370 (
371 "MEMRA_HC_FUSED_PRE",
372 "the fused mHC pre-chain is gated on the unsharded walk only",
373 ),
374 (
375 "MEMRA_HC_DECODE_WS",
376 "the workspace decode walk carries no TP mixer branches",
377 ),
378 (
379 "MEMRA_KDA_FUSED_PROJ",
380 "the fused six-projection door (either operand arm) is gated on full-width \
381 projections, never head shards",
382 ),
383 (
384 "MEMRA_MLA_DECODE_SPLIT",
385 "the absorb/decompress split is gated on the full-head geometry",
386 ),
387];
388
389pub fn refuse_glm5_tp_door_composition(armed: impl Fn(&str) -> bool) -> Result<(), String> {
396 crate::tp::refuse_door_composition("MEMRA_GLM5_TP", &GLM5_TP_REFUSED_DOOR_FLAGS, armed)
397}
398
399pub fn prepare_glm5_tp_load(
402 e: &Engine,
403 view: &Glm5TpModelView,
404) -> Result<Option<Glm5TpLoadPlan>, Box<dyn std::error::Error>> {
405 let raw = glm5_tp_env_raw();
406 let specs = parse_glm5_tp_layer_specs(raw.as_deref(), view.trunk_layers)?;
407 if specs.is_empty() {
408 return Ok(None);
409 }
410
411 if crate::pp::pp_cuts(view.trunk_layers).is_some() {
414 return Err(
415 "MEMRA_GLM5_TP + MEMRA_PP_STAGES>1: the TP x PP composition is unwired and \
416 refuses until its own gate exists (stage 5 of the tp2 lane names it)"
417 .into(),
418 );
419 }
420 if !crate::tp::step_tp_layer_specs()?.is_empty()
421 || !crate::tp::step_ep_layer_specs()?.is_empty()
422 {
423 return Err(
424 "MEMRA_GLM5_TP + MEMRA_STEP_TP/MEMRA_STEP_EP: the step and glm5 parallel \
425 contracts never co-arm"
426 .into(),
427 );
428 }
429 refuse_glm5_tp_door_composition(|flag| std::env::var(flag).as_deref() == Ok("1"))?;
430
431 let devices = specs[0].devices.clone();
434 let ranks = devices.len();
435 if !GLM5_TP_ALLOWED_RANKS.contains(&ranks) {
436 return Err(format!(
437 "MEMRA_GLM5_TP names {ranks} devices per layer; the qualified rank envelope is \
438 {GLM5_TP_ALLOWED_RANKS:?} (TP-3 is a head-padding question, not built — see the \
439 module doc)"
440 )
441 .into());
442 }
443
444 if view.layer_class.len() != view.trunk_layers || view.layer_is_moe.len() != view.trunk_layers {
446 return Err(format!(
447 "glm5-tp preflight: layer class map ({}/{}) does not cover the {}-layer trunk",
448 view.layer_class.len(),
449 view.layer_is_moe.len(),
450 view.trunk_layers
451 )
452 .into());
453 }
454 if !view.kda_heads.is_multiple_of(ranks) || view.kda_heads == 0 {
455 return Err(format!(
456 "glm5-tp: {} KDA heads do not shard across {ranks} ranks",
457 view.kda_heads
458 )
459 .into());
460 }
461 if view.kda_head_dim != crate::kda::KDA_HEAD_DIM {
462 return Err(format!(
463 "glm5-tp: KDA head_dim {} is not the {} the scan kernel is instantiated for",
464 view.kda_head_dim,
465 crate::kda::KDA_HEAD_DIM
466 )
467 .into());
468 }
469 if !view.mla_heads.is_multiple_of(ranks) || view.mla_heads == 0 {
470 return Err(format!(
471 "glm5-tp: {} MLA heads do not shard across {ranks} ranks",
472 view.mla_heads
473 )
474 .into());
475 }
476 if !view.n_routed_experts.is_multiple_of(ranks) || view.n_routed_experts == 0 {
477 return Err(format!(
478 "glm5-tp: {} routed experts do not partition across {ranks} ranks",
479 view.n_routed_experts
480 )
481 .into());
482 }
483 if view.top_k > view.n_routed_experts {
484 return Err("glm5-tp: top_k exceeds the routed expert count".into());
485 }
486
487 for s in &specs {
488 if s.devices != devices {
489 return Err(format!(
490 "MEMRA_GLM5_TP carries ONE runtime group: layer {} names devices {:?}, \
491 the first spec names {:?}",
492 s.layer, s.devices, devices
493 )
494 .into());
495 }
496 if s.layer >= view.trunk_layers {
497 return Err(format!(
498 "MEMRA_GLM5_TP layer {} outside the {}-layer trunk",
499 s.layer, view.trunk_layers
500 )
501 .into());
502 }
503 }
504 let root_dev = e.ctx().ordinal();
505 if devices[0] != root_dev {
506 return Err(format!(
507 "MEMRA_GLM5_TP rank list {:?} must start with the owning device {root_dev} \
508 (the owner-first rank law)",
509 devices
510 )
511 .into());
512 }
513
514 let red = gate_red()?;
516 let same_dev = gate_same_device();
517 if let Some(red) = red {
518 eprintln!("[glm5-tp-preflight] GATE RED ARM armed: {red:?} — outputs MUST diverge");
519 }
520 let mut rt = if same_dev {
521 eprintln!(
522 "[glm5-tp-preflight] GATE same-device emulation: {} peer ranks are additional \
523 contexts on device {root_dev} (spec devices {:?} are logical rank ids)",
524 ranks - 1,
525 &devices[1..],
526 );
527 Glm5TpRt::new_gate_same_device(root_dev, ranks)?
528 } else {
529 Glm5TpRt::new(&devices)?
530 };
531 rt.arm_transport(e)?;
534 let rt = Arc::new(rt);
535 let layers: std::collections::BTreeSet<usize> = specs.iter().map(|s| s.layer).collect();
536 let ep_map = load_glm5_ep_map(view, &layers, ranks)?;
537 let (mut kda_n, mut mla_n, mut moe_n) = (0usize, 0usize, 0usize);
538 for &il in &layers {
539 match view.layer_class[il] {
540 Glm5LayerClass::Kda => kda_n += 1,
541 Glm5LayerClass::Mla => mla_n += 1,
542 }
543 if view.layer_is_moe[il] {
544 moe_n += 1;
545 }
546 }
547 eprintln!(
548 "[glm5-tp-preflight] armed ranks={ranks} devices={devices:?} layers={} \
549 kda_shard={kda_n} mla_shard={mla_n} moe_ep={moe_n} kda_heads_per_rank={} \
550 mla_heads_per_rank={} experts_per_rank={} transport={} \
551 weights_loaded=false performance_claim=false",
552 layers.len(),
553 view.kda_heads / ranks,
554 view.mla_heads / ranks,
555 view.n_routed_experts / ranks,
556 rt.transport.name(),
557 );
558 Ok(Some(Glm5TpLoadPlan { rt, layers, ep_map }))
559}
560
561fn outer_rows(ne: &[u64]) -> (usize, usize) {
566 let outer = *ne.last().expect("tensor has at least one axis") as usize;
569 let inner: usize = ne[..ne.len() - 1].iter().map(|&d| d as usize).product();
570 (outer, inner.max(1))
571}
572
573fn shard_rows(
577 src_engine: &Engine,
578 dst: &Engine,
579 t: &GpuTensor,
580 rows: Range<usize>,
581) -> Result<GpuTensor, Box<dyn std::error::Error>> {
582 match t {
583 GpuTensor::Float { data, ne } => {
584 let (outer, inner) = outer_rows(ne);
585 if rows.end > outer {
586 return Err(format!("shard rows {rows:?} exceed outer axis {outer}").into());
587 }
588 let host = src_engine.dtoh(data)?;
589 let piece = &host[rows.start * inner..rows.end * inner];
590 let mut ne2 = ne.clone();
591 *ne2.last_mut().unwrap() = (rows.end - rows.start) as u64;
592 Ok(GpuTensor::Float {
593 data: dst.htod(piece)?,
594 ne: ne2,
595 })
596 }
597 GpuTensor::FloatBf16 { data, ne } => {
598 let (outer, inner) = outer_rows(ne);
599 if rows.end > outer {
600 return Err(format!("shard rows {rows:?} exceed outer axis {outer}").into());
601 }
602 let host = src_engine.dtoh_u8(data)?;
603 let piece = &host[rows.start * inner * 2..rows.end * inner * 2];
604 let mut ne2 = ne.clone();
605 *ne2.last_mut().unwrap() = (rows.end - rows.start) as u64;
606 Ok(GpuTensor::FloatBf16 {
607 data: dst.htod_bytes(piece)?,
608 ne: ne2,
609 })
610 }
611 GpuTensor::Quant {
612 bytes,
613 qtype,
614 row_bytes,
615 ne,
616 scale,
617 rp,
618 fp8,
619 rp4,
620 blk,
621 f16,
622 #[cfg(memra_cutlass)]
623 cutlass,
624 } => {
625 if *rp {
626 return Err(
627 "glm5-tp shard: rp split-plane mirror layout is unwired — load \
628 the TP-armed tensor with MEMRA_RP=0 (raw layout is bit-identical \
629 by the mirror's own contract)"
630 .into(),
631 );
632 }
633 if fp8.is_some() || rp4.is_some() || blk.is_some() || f16.is_some() {
634 return Err(
635 "glm5-tp shard: a decode/prefill mirror (fp8/rp4/blk/f16) is present on a \
636 TP-armed tensor — mirrors are unwired for shards in v1; disable the \
637 mirror door for this load"
638 .into(),
639 );
640 }
641 #[cfg(memra_cutlass)]
642 if cutlass.is_some() {
643 return Err("glm5-tp shard: cutlass prefill operand unwired for shards".into());
644 }
645 let (outer, inner) = outer_rows(ne);
646 if ne.len() != 2 {
647 return Err("glm5-tp shard: quantized shards are 2D-only in v1".into());
648 }
649 let _ = inner;
650 if rows.end > outer {
651 return Err(format!("shard rows {rows:?} exceed outer axis {outer}").into());
652 }
653 let host = src_engine.dtoh_u8(bytes)?;
654 let piece = &host[rows.start * row_bytes..rows.end * row_bytes];
655 let mut ne2 = ne.clone();
656 *ne2.last_mut().unwrap() = (rows.end - rows.start) as u64;
657 Ok(GpuTensor::Quant {
658 bytes: dst.htod_bytes(piece)?,
659 qtype: *qtype,
660 row_bytes: *row_bytes,
661 ne: ne2,
662 scale: *scale,
663 rp: false,
664 fp8: None,
665 rp4: None,
666 blk: None,
667 f16: None,
668 #[cfg(memra_cutlass)]
669 cutlass: None,
670 })
671 }
672 }
673}
674
675fn replicate(
677 src_engine: &Engine,
678 dst: &Engine,
679 t: &GpuTensor,
680) -> Result<GpuTensor, Box<dyn std::error::Error>> {
681 let (outer, _) = outer_rows(t.ne());
682 shard_rows(src_engine, dst, t, 0..outer)
683}
684
685pub(crate) fn rank_engine<'a>(e: &'a Engine, rt: &'a Glm5TpRt, r: usize) -> &'a Engine {
688 if r == 0 { e } else { &rt.peers[r - 1] }
689}
690
691pub struct Glm5TpKda {
699 pub rt: Arc<Glm5TpRt>,
700 pub peers: Vec<KdaAttnLayer>,
702 pub full_qkv: usize,
704 pub n_embd: usize,
706}
707
708impl Glm5TpKda {
709 pub fn ranks(&self) -> usize {
710 self.peers.len() + 1
711 }
712}
713
714static KDA_MARKED: AtomicBool = AtomicBool::new(false);
715
716pub(crate) fn shard_kda_layer(
720 e: &Engine,
721 rt: &Arc<Glm5TpRt>,
722 la: KdaAttnLayer,
723) -> Result<KdaAttnLayer, Box<dyn std::error::Error>> {
724 if la.tp.is_some() {
725 return Err("shard_kda_layer: layer is already sharded".into());
726 }
727 let ranks = rt.ranks();
728 let heads = la.heads();
729 let head_dim = la.head_dim();
730 let qkv = la.qkv();
731 let kernel = la.conv_kernel();
732 if !heads.is_multiple_of(ranks) {
733 return Err(format!("KDA heads {heads} do not shard across {ranks} ranks").into());
734 }
735 let hl = heads / ranks; let ql = qkv / ranks; let n_embd = la.wo.out_features();
738 if !n_embd.is_multiple_of(ranks) {
739 return Err(format!("KDA wo out {n_embd} does not split across ranks").into());
740 }
741 let hh = n_embd / ranks;
742
743 let mut shard_plan = la.plan;
744 shard_plan.num_heads = hl as u32;
745
746 let conv_host = e.dtoh(&la.conv)?;
749 let conv_rank =
750 |dst: &Engine, r: usize| -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
751 let mut piece = Vec::with_capacity(3 * ql * kernel);
752 for p in 0..3 {
753 let a = (p * qkv + r * ql) * kernel;
754 piece.extend_from_slice(&conv_host[a..a + ql * kernel]);
755 }
756 dst.htod(&piece)
757 };
758
759 let wo_rank = |r: usize| -> usize {
762 match gate_red() {
763 Ok(Some(GateRed::SwapWo)) => (r + 1) % ranks,
764 _ => r,
765 }
766 };
767
768 let rank_shard = |dst: &Engine, r: usize| -> Result<KdaAttnLayer, Box<dyn std::error::Error>> {
769 let wr = wo_rank(r);
770 Ok(KdaAttnLayer {
771 plan: shard_plan,
772 wq: shard_rows(e, dst, &la.wq, r * ql..(r + 1) * ql)?,
773 wk: shard_rows(e, dst, &la.wk, r * ql..(r + 1) * ql)?,
774 wv: shard_rows(e, dst, &la.wv, r * ql..(r + 1) * ql)?,
775 f_a: replicate(e, dst, &la.f_a)?,
776 f_b: shard_rows(e, dst, &la.f_b, r * ql..(r + 1) * ql)?,
777 g_a: replicate(e, dst, &la.g_a)?,
778 g_b: shard_rows(e, dst, &la.g_b, r * ql..(r + 1) * ql)?,
779 b_proj: shard_rows(e, dst, &la.b_proj, r * hl..(r + 1) * hl)?,
780 wo: shard_rows(e, dst, &la.wo, wr * hh..(wr + 1) * hh)?,
784 conv: conv_rank(dst, r)?,
785 a_log: shard_rows(e, dst, &la.a_log, r * hl..(r + 1) * hl)?,
786 dt_bias: shard_rows(e, dst, &la.dt_bias, r * ql..(r + 1) * ql)?,
787 o_norm: replicate(e, dst, &la.o_norm)?,
788 tp: None,
789 })
790 };
791
792 let mut root = rank_shard(e, 0)?;
793 let mut peers = Vec::with_capacity(ranks - 1);
794 for r in 1..ranks {
795 peers.push(rank_shard(&rt.peers[r - 1], r)?);
796 }
797 if !KDA_MARKED.swap(true, Ordering::Relaxed) {
798 eprintln!(
799 "[glm5-tp-kda] head shard armed: ranks={ranks} heads_per_rank={hl} \
800 head_dim={head_dim} wo=column-over-gather transport={} performance_claim=false",
801 rt.transport.name(),
802 );
803 }
804 root.tp = Some(Box::new(Glm5TpKda {
805 rt: Arc::clone(rt),
806 peers,
807 full_qkv: qkv,
808 n_embd,
809 }));
810 Ok(root)
811}
812
813fn ensure_kda_tp_state<'c>(
818 e: &Engine,
819 rt: &Glm5TpRt,
820 la_root: &KdaAttnLayer,
821 cache: &'c mut Cache,
822 il: usize,
823) -> Result<&'c mut Vec<RecurLayer>, Box<dyn std::error::Error>> {
824 if cache.glm5_tp_recur.len() <= il {
825 return Err(format!("glm5-tp: cache carries no TP recur slot for layer {il}").into());
826 }
827 if cache.glm5_tp_recur[il].is_none() {
828 let conv_pad = la_root.conv_width() * (la_root.conv_kernel() - 1);
829 let state = la_root.state_width();
830 let mk = |dev: &Engine| -> Result<RecurLayer, Box<dyn std::error::Error>> {
831 Ok(RecurLayer {
832 conv_state: dev.zeros(conv_pad)?,
833 ssm_state: dev.zeros(state)?,
834 ssm_state_alt: dev.zeros(state)?,
835 })
836 };
837 let mut planes = Vec::with_capacity(rt.ranks());
838 planes.push(mk(e)?);
839 for p in &rt.peers {
840 planes.push(mk(p)?);
841 }
842 cache.glm5_tp_recur[il] = Some(planes);
843 }
844 Ok(cache.glm5_tp_recur[il].as_mut().unwrap())
845}
846
847#[allow(clippy::too_many_arguments)] pub(crate) fn kda_tp_cached(
858 e: &Engine,
859 la_root: &KdaAttnLayer,
860 x: &CudaSlice<f32>,
861 t: usize,
862 eps: f32,
863 cache: &mut Cache,
864 il: usize,
865 arm: ConvArm,
866) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
867 let tp = la_root
868 .tp
869 .as_ref()
870 .ok_or("kda_tp_cached called on an unsharded layer")?;
871 let rt = &tp.rt;
872 let ranks = rt.ranks();
873 let ql = la_root.qkv(); let full = tp.full_qkv;
875 let n_embd = tp.n_embd;
876 let hh = n_embd / ranks;
877
878 let hop = rt.hop(e);
879 let x_peers = crate::glm5_tp_transport::fanout_f32(&hop, x, x.len())?;
883
884 let states = ensure_kda_tp_state(e, rt, la_root, cache, il)?;
885
886 let mut gated: Vec<Option<CudaSlice<f32>>> = (0..ranks).map(|_| None).collect();
889 for r in 1..ranks {
890 let RecurLayer {
891 conv_state,
892 ssm_state,
893 ssm_state_alt,
894 } = &mut states[r];
895 let out = crate::kda::kda_core_gated(
896 &rt.peers[r - 1],
897 &tp.peers[r - 1],
898 &x_peers[r - 1],
899 t,
900 eps,
901 conv_state,
902 ssm_state,
903 ssm_state_alt,
904 arm,
905 crate::kda::KdaStash::None,
906 None,
907 )?;
908 std::mem::swap(ssm_state, ssm_state_alt);
909 gated[r] = Some(out);
910 }
911 {
912 let RecurLayer {
913 conv_state,
914 ssm_state,
915 ssm_state_alt,
916 } = &mut states[0];
917 let out = crate::kda::kda_core_gated(
918 e,
919 la_root,
920 x,
921 t,
922 eps,
923 conv_state,
924 ssm_state,
925 ssm_state_alt,
926 arm,
927 crate::kda::KdaStash::None,
928 None,
929 )?;
930 std::mem::swap(ssm_state, ssm_state_alt);
931 gated[0] = Some(out);
932 }
933
934 debug_assert_eq!(full, ranks * ql);
938 let gated_refs: Vec<&CudaSlice<f32>> = gated
939 .iter()
940 .map(|g| g.as_ref().expect("filled above"))
941 .collect();
942 let fulls = crate::glm5_tp_transport::gather_parts(&hop, &gated_refs, t, ql)?;
943
944 let mut ys = Vec::with_capacity(ranks);
947 ys.push(e.matmul(&la_root.wo, &fulls[0], t)?); for r in 1..ranks {
949 ys.push(rt.peers[r - 1].matmul(&tp.peers[r - 1].wo, &fulls[r], t)?);
950 }
951
952 let y_refs: Vec<&CudaSlice<f32>> = ys.iter().collect();
954 crate::glm5_tp_transport::concat_parts_on_root(&hop, &y_refs, t, hh)
955}
956
957pub type Glm5TpKdaVerifyStash = Vec<(CudaSlice<f32>, crate::kda::KdaRowsStash)>;
965
966#[allow(clippy::too_many_arguments)] pub(crate) fn kda_tp_verify_rows(
979 e: &Engine,
980 la_root: &KdaAttnLayer,
981 x: &CudaSlice<f32>,
982 t: usize,
983 eps: f32,
984 cache: &mut Cache,
985 il: usize,
986) -> Result<(CudaSlice<f32>, Glm5TpKdaVerifyStash), Box<dyn std::error::Error>> {
987 let tp = la_root
988 .tp
989 .as_ref()
990 .ok_or("kda_tp_verify_rows called on an unsharded layer")?;
991 let rt = &tp.rt;
992 let ranks = rt.ranks();
993 let ql = la_root.qkv(); let full = tp.full_qkv;
995 let n_embd = tp.n_embd;
996 let hh = n_embd / ranks;
997
998 let hop = rt.hop(e);
999 let x_peers = crate::glm5_tp_transport::fanout_f32(&hop, x, x.len())?;
1000 let states = ensure_kda_tp_state(e, rt, la_root, cache, il)?;
1001
1002 let mut gated: Vec<Option<CudaSlice<f32>>> = (0..ranks).map(|_| None).collect();
1003 let mut per_rank: Glm5TpKdaVerifyStash = Vec::with_capacity(ranks);
1004 for r in 0..ranks {
1005 let dev = if r == 0 { e } else { &rt.peers[r - 1] };
1006 let la = if r == 0 { la_root } else { &tp.peers[r - 1] };
1007 let xin = if r == 0 { x } else { &x_peers[r - 1] };
1008 let snap = dev.clone_dtod(&states[r].ssm_state)?;
1011 let mut stash: Option<crate::kda::KdaRowsStash> = None;
1012 let out = {
1013 let RecurLayer {
1014 conv_state,
1015 ssm_state,
1016 ssm_state_alt,
1017 } = &mut states[r];
1018 let out = crate::kda::kda_core_gated(
1019 dev,
1020 la,
1021 xin,
1022 t,
1023 eps,
1024 conv_state,
1025 ssm_state,
1026 ssm_state_alt,
1027 ConvArm::Prefill,
1028 crate::kda::KdaStash::Rows(&mut stash),
1029 None,
1030 )?;
1031 std::mem::swap(ssm_state, ssm_state_alt);
1032 out
1033 };
1034 let stash =
1035 stash.ok_or("kda_core_gated returned without filling the requested rows stash")?;
1036 per_rank.push((snap, stash));
1037 gated[r] = Some(out);
1038 }
1039
1040 debug_assert_eq!(full, ranks * ql);
1041 let gated_refs: Vec<&CudaSlice<f32>> = gated
1042 .iter()
1043 .map(|g| g.as_ref().expect("filled above"))
1044 .collect();
1045 let fulls = crate::glm5_tp_transport::gather_parts(&hop, &gated_refs, t, ql)?;
1046
1047 let mut ys = Vec::with_capacity(ranks);
1049 ys.push(e.matmul_rows_exact(&la_root.wo, &fulls[0], t)?);
1050 for r in 1..ranks {
1051 ys.push(rt.peers[r - 1].matmul_rows_exact(&tp.peers[r - 1].wo, &fulls[r], t)?);
1052 }
1053 let y_refs: Vec<&CudaSlice<f32>> = ys.iter().collect();
1054 let mixed = crate::glm5_tp_transport::concat_parts_on_root(&hop, &y_refs, t, hh)?;
1055 Ok((mixed, per_rank))
1056}
1057
1058pub(crate) fn kda_tp_verify_rollback(
1062 e: &Engine,
1063 la_root: &KdaAttnLayer,
1064 stash: &Glm5TpKdaVerifyStash,
1065 keep: usize,
1066 cache: &mut Cache,
1067 il: usize,
1068) -> Result<(), Box<dyn std::error::Error>> {
1069 let tp = la_root
1070 .tp
1071 .as_ref()
1072 .ok_or("kda_tp_verify_rollback called on an unsharded layer")?;
1073 let rt = &tp.rt;
1074 let ranks = rt.ranks();
1075 if stash.len() != ranks {
1076 return Err(format!(
1077 "glm5-tp verify rollback: stash carries {} ranks, the runtime has {ranks}",
1078 stash.len()
1079 )
1080 .into());
1081 }
1082 let states = cache.glm5_tp_recur[il]
1083 .as_mut()
1084 .ok_or_else(|| format!("glm5-tp verify rollback: layer {il} has no per-rank state"))?;
1085 for r in 0..ranks {
1086 let dev = if r == 0 { e } else { &rt.peers[r - 1] };
1087 let la = if r == 0 { la_root } else { &tp.peers[r - 1] };
1088 let (snap, rows) = &stash[r];
1089 crate::kda::kda_verify_rollback_rows_on(dev, la, snap, rows, keep, &mut states[r], il)?;
1090 }
1091 Ok(())
1092}
1093
1094pub struct Glm5TpMla {
1101 pub rt: Arc<Glm5TpRt>,
1102 pub peers: Vec<crate::hybrid::MlaAttnLayer>,
1104 pub full_heads: usize,
1106 pub n_embd: usize,
1108}
1109
1110impl Glm5TpMla {
1111 pub fn ranks(&self) -> usize {
1112 self.peers.len() + 1
1113 }
1114}
1115
1116static MLA_MARKED: AtomicBool = AtomicBool::new(false);
1117
1118pub(crate) fn shard_mla_layer(
1119 e: &Engine,
1120 rt: &Arc<Glm5TpRt>,
1121 la: crate::hybrid::MlaAttnLayer,
1122) -> Result<crate::hybrid::MlaAttnLayer, Box<dyn std::error::Error>> {
1123 use crate::hybrid::{MlaAttnLayer, MlaIndexer};
1124 if la.tp.is_some() {
1125 return Err("shard_mla_layer: layer is already sharded".into());
1126 }
1127 let ranks = rt.ranks();
1128 let g = la.geom;
1129 let nh = g.n_head;
1130 if !nh.is_multiple_of(ranks) {
1131 return Err(format!("MLA heads {nh} do not shard across {ranks} ranks").into());
1132 }
1133 let hl = nh / ranks;
1134 let head_q = g.d_nope + g.d_rope; let n_embd = la.wo.out_features();
1136 if !n_embd.is_multiple_of(ranks) {
1137 return Err(format!("MLA wo out {n_embd} does not split across ranks").into());
1138 }
1139 let hh = n_embd / ranks;
1140
1141 let mut shard_geom = g;
1142 shard_geom.n_head = hl;
1143
1144 let replicate_indexer =
1145 |dst: &Engine, ix: &MlaIndexer| -> Result<MlaIndexer, Box<dyn std::error::Error>> {
1146 Ok(MlaIndexer {
1147 wq_b: replicate(e, dst, &ix.wq_b)?,
1148 wk: replicate(e, dst, &ix.wk)?,
1149 k_norm_w: replicate(e, dst, &ix.k_norm_w)?,
1150 k_norm_b: replicate(e, dst, &ix.k_norm_b)?,
1151 weights_proj: replicate(e, dst, &ix.weights_proj)?,
1152 kpool_gate: replicate(e, dst, &ix.kpool_gate)?,
1153 kpool_ape: replicate(e, dst, &ix.kpool_ape)?,
1154 geom: ix.geom,
1155 })
1156 };
1157
1158 let wo_rank = |r: usize| -> usize {
1160 match gate_red() {
1161 Ok(Some(GateRed::SwapWo)) => (r + 1) % ranks,
1162 _ => r,
1163 }
1164 };
1165
1166 let rank_shard = |dst: &Engine, r: usize| -> Result<MlaAttnLayer, Box<dyn std::error::Error>> {
1167 let wr = wo_rank(r);
1168 Ok(MlaAttnLayer {
1169 wq_a: replicate(e, dst, &la.wq_a)?,
1170 q_a_norm: replicate(e, dst, &la.q_a_norm)?,
1171 wq_b: shard_rows(e, dst, &la.wq_b, r * hl * head_q..(r + 1) * hl * head_q)?,
1172 wkv_a: replicate(e, dst, &la.wkv_a)?,
1173 kv_a_norm: replicate(e, dst, &la.kv_a_norm)?,
1174 wk_b: shard_rows(e, dst, &la.wk_b, r * hl..(r + 1) * hl)?,
1176 wv_b: shard_rows(e, dst, &la.wv_b, r * hl..(r + 1) * hl)?,
1177 wo: shard_rows(e, dst, &la.wo, wr * hh..(wr + 1) * hh)?,
1179 geom: shard_geom,
1180 index: match &la.index {
1181 Some(ix) => Some(replicate_indexer(dst, ix)?),
1182 None => None,
1183 },
1184 tp: None,
1185 })
1186 };
1187
1188 let mut root = rank_shard(e, 0)?;
1189 let mut peers = Vec::with_capacity(ranks - 1);
1190 for r in 1..ranks {
1191 peers.push(rank_shard(&rt.peers[r - 1], r)?);
1192 }
1193 if !MLA_MARKED.swap(true, Ordering::Relaxed) {
1194 eprintln!(
1195 "[glm5-tp-mla] head shard armed: ranks={ranks} heads_per_rank={hl} kv_rank={} \
1196 latent=replicated indexer=replicated wo=column-over-gather transport={} \
1197 performance_claim=false",
1198 g.kv_rank,
1199 rt.transport.name(),
1200 );
1201 }
1202 root.tp = Some(Box::new(Glm5TpMla {
1203 rt: Arc::clone(rt),
1204 peers,
1205 full_heads: nh,
1206 n_embd,
1207 }));
1208 Ok(root)
1209}
1210
1211pub(crate) fn ensure_mla_peer_latent(
1215 rt: &Glm5TpRt,
1216 canonical: &LatentKvLayer,
1217 cache_slot: &mut Option<Vec<LatentKvLayer>>,
1218) -> Result<(), Box<dyn std::error::Error>> {
1219 if cache_slot.is_some() {
1220 return Ok(());
1221 }
1222 let mut planes = Vec::with_capacity(rt.peers.len());
1223 for dev in &rt.peers {
1224 let rows = dev.zeros(canonical.rows.len())?;
1225 let len_d = dev.htod_i32(&[0])?;
1228 let index_rows = match &canonical.index_rows {
1229 Some(p) => Some(dev.zeros(p.len())?),
1230 None => None,
1231 };
1232 planes.push(LatentKvLayer {
1233 rows,
1234 width: canonical.width,
1235 index_width: canonical.index_width,
1236 len: 0,
1237 len_d,
1238 index_rows,
1239 index_ring_rows: canonical.index_ring_rows,
1240 index_pool_keys: None, index_pools_ready: 0,
1242 index_pool: canonical.index_pool,
1243 });
1244 }
1245 *cache_slot = Some(planes);
1246 Ok(())
1247}
1248
1249pub struct EpRankSlab {
1257 pub gate: CudaSlice<u8>,
1258 pub up: CudaSlice<u8>,
1259 pub down: CudaSlice<u8>,
1260 pub n_experts: usize,
1261}
1262
1263pub struct Glm5EpExps {
1273 pub rt: Arc<Glm5TpRt>,
1274 pub slabs: Vec<EpRankSlab>,
1276 pub owner_of: Vec<u8>,
1278 pub local_of: Vec<u32>,
1280 pub ptr_rows: Vec<CudaSlice<u64>>,
1289}
1290
1291impl Glm5EpExps {
1292 pub fn owner(&self, expert: usize) -> usize {
1294 self.owner_of[expert] as usize
1295 }
1296
1297 pub fn ranks(&self) -> usize {
1298 self.slabs.len()
1299 }
1300}
1301
1302static EP_MARKED: AtomicBool = AtomicBool::new(false);
1303
1304pub static GLM5_EP_PEER_SLOT_DISPATCHES: AtomicU64 = AtomicU64::new(0);
1309
1310pub fn glm5_ep_peer_slot_dispatches() -> u64 {
1311 GLM5_EP_PEER_SLOT_DISPATCHES.load(Ordering::Relaxed)
1312}
1313
1314pub static GLM5_EP_DIET_DISPATCHES: AtomicU64 = AtomicU64::new(0);
1321
1322pub fn glm5_ep_diet_dispatches() -> u64 {
1324 GLM5_EP_DIET_DISPATCHES.load(Ordering::Relaxed)
1325}
1326
1327pub static GLM5_EP_DIET_BULK_RETURNS: AtomicU64 = AtomicU64::new(0);
1331
1332pub fn glm5_ep_diet_bulk_returns() -> u64 {
1334 GLM5_EP_DIET_BULK_RETURNS.load(Ordering::Relaxed)
1335}
1336
1337pub static GLM5_EP_DIET_PEER_ROUNDTRIPS_AVOIDED: AtomicU64 = AtomicU64::new(0);
1341
1342pub fn glm5_ep_diet_peer_roundtrips_avoided() -> u64 {
1344 GLM5_EP_DIET_PEER_ROUNDTRIPS_AVOIDED.load(Ordering::Relaxed)
1345}
1346
1347pub static GLM5_EP_DIET_FANOUT_UPLOADS_AVOIDED: AtomicU64 = AtomicU64::new(0);
1353
1354pub fn glm5_ep_diet_fanout_uploads_avoided() -> u64 {
1356 GLM5_EP_DIET_FANOUT_UPLOADS_AVOIDED.load(Ordering::Relaxed)
1357}
1358
1359pub static GLM5_EP_GROUPED_PRIME_DISPATCHES: AtomicU64 = AtomicU64::new(0);
1363
1364pub fn glm5_ep_grouped_prime_dispatches() -> u64 {
1366 GLM5_EP_GROUPED_PRIME_DISPATCHES.load(Ordering::Relaxed)
1367}
1368
1369pub(crate) fn arm_moe_ep(
1373 e: &Engine,
1374 rt: &Arc<Glm5TpRt>,
1375 m: &mut crate::hybrid::MoeWeights,
1376 placement: Option<&[u8]>,
1377) -> Result<(), Box<dyn std::error::Error>> {
1378 if m.glm5_ep.is_some() {
1379 return Err("arm_moe_ep: layer is already EP-armed".into());
1380 }
1381 let ranks = rt.ranks();
1382 let n_expert = m.gate_exps.n_expert;
1383 if !n_expert.is_multiple_of(ranks) {
1384 return Err(format!(
1385 "glm5-tp EP: {n_expert} experts do not partition across {ranks} ranks"
1386 )
1387 .into());
1388 }
1389 if m.gate_exps.layouts.is_some() || m.up_exps.layouts.is_some() || m.down_exps.layouts.is_some()
1390 {
1391 return Err("glm5-tp EP: per-expert mixed layouts are unwired for EP shards".into());
1392 }
1393 let owner_of: Vec<u8> = match placement {
1394 Some(owners) => {
1395 if owners.len() != n_expert {
1398 return Err(format!(
1399 "glm5-tp EP: placement row carries {} owners for a {n_expert}-expert bank",
1400 owners.len()
1401 )
1402 .into());
1403 }
1404 if owners.iter().any(|&r| (r as usize) >= ranks) {
1405 return Err(
1406 format!("glm5-tp EP: placement row names a rank outside TP-{ranks}").into(),
1407 );
1408 }
1409 owners.to_vec()
1410 }
1411 None => crate::ep_map::EpMap::even_owners(n_expert, ranks),
1412 };
1413 let mut local_of = vec![0u32; n_expert];
1415 let mut owned: Vec<Vec<usize>> = vec![Vec::new(); ranks];
1416 for ex in 0..n_expert {
1417 let r = owner_of[ex] as usize;
1418 local_of[ex] = owned[r].len() as u32;
1419 owned[r].push(ex);
1420 }
1421 if owned.iter().any(|o| o.is_empty()) {
1422 return Err("glm5-tp EP: placement leaves a rank with zero experts (refused)".into());
1423 }
1424 let slab =
1425 |dev: &Engine, experts: &[usize]| -> Result<EpRankSlab, Box<dyn std::error::Error>> {
1426 let cut = |h: &crate::model::HostExps,
1433 pad: usize|
1434 -> Result<CudaSlice<u8>, Box<dyn std::error::Error>> {
1435 let stride = h.expert_stride;
1436 let bytes = h.bytes.as_bytes();
1437 let contiguous = experts.windows(2).all(|w| w[1] == w[0] + 1);
1440 if contiguous {
1441 let a = experts[0] * stride;
1442 let b = (experts[experts.len() - 1] + 1) * stride;
1443 return dev.htod_bytes_padded(&bytes[a..b], pad);
1444 }
1445 let mut staged = Vec::with_capacity(experts.len() * stride);
1448 for &ex in experts {
1449 staged.extend_from_slice(&bytes[ex * stride..(ex + 1) * stride]);
1450 }
1451 dev.htod_bytes_padded(&staged, pad)
1452 };
1453 Ok(EpRankSlab {
1454 gate: cut(&m.gate_exps, 8)?,
1455 up: cut(&m.up_exps, 8)?,
1456 down: cut(&m.down_exps, 144)?,
1457 n_experts: experts.len(),
1458 })
1459 };
1460 let mut slabs = Vec::with_capacity(ranks);
1461 for r in 0..ranks {
1462 slabs.push(slab(rank_engine(e, rt, r), &owned[r])?);
1463 }
1464 if matches!(gate_red(), Ok(Some(GateRed::SwapEpGateUp))) {
1466 let root = &mut slabs[0];
1467 std::mem::swap(&mut root.gate, &mut root.up);
1468 }
1469 if matches!(gate_red(), Ok(Some(GateRed::CorruptEpMap))) {
1472 let n0 = owned[0].len() as u32;
1473 for &ex in &owned[0] {
1474 local_of[ex] = n0 - 1 - local_of[ex];
1475 }
1476 }
1477 let ptr_table = |dev: &Engine,
1482 slab: &EpRankSlab,
1483 rank: u8|
1484 -> Result<CudaSlice<u64>, Box<dyn std::error::Error>> {
1485 use cudarc::driver::DevicePtr;
1486 let (pg, pu, pd) = {
1487 let s = dev.stream();
1488 let (pg, _g0) = slab.gate.device_ptr(&s);
1489 let (pu, _g1) = slab.up.device_ptr(&s);
1490 let (pd, _g2) = slab.down.device_ptr(&s);
1491 (pg, pu, pd)
1492 };
1493 let mut host = vec![0u64; 3 * n_expert];
1494 for ex in 0..n_expert {
1495 if owner_of[ex] != rank {
1496 continue; }
1498 let local = local_of[ex] as usize;
1499 host[ex] = pg + (local * m.gate_exps.expert_stride) as u64;
1500 host[n_expert + ex] = pu + (local * m.up_exps.expert_stride) as u64;
1501 host[2 * n_expert + ex] = pd + (local * m.down_exps.expert_stride) as u64;
1502 }
1503 dev.htod_u64(&host)
1504 };
1505 let mut ptr_rows = Vec::with_capacity(ranks);
1506 for r in 0..ranks {
1507 ptr_rows.push(ptr_table(rank_engine(e, rt, r), &slabs[r], r as u8)?);
1508 }
1509 if !EP_MARKED.swap(true, Ordering::Relaxed) {
1510 eprintln!(
1511 "[glm5-tp-ep] expert-parallel armed: experts_per_rank={:?} ownership={} \
1512 router=root combine=slot-ordered-fmaf transport={} \
1513 performance_claim=false",
1514 owned.iter().map(Vec::len).collect::<Vec<_>>(),
1515 if placement.is_some() {
1520 "measured-map"
1521 } else {
1522 "even-split"
1523 },
1524 rt.transport.name(),
1525 );
1526 }
1527 m.dev_exps = None;
1530 m.glm5_ep = Some(Glm5EpExps {
1531 rt: Arc::clone(rt),
1532 slabs,
1533 owner_of,
1534 local_of,
1535 ptr_rows,
1536 });
1537 Ok(())
1538}
1539
1540#[cfg(test)]
1541mod tests {
1542 use super::*;
1543
1544 #[test]
1545 fn parse_is_literal_and_fail_closed() {
1546 assert!(parse_glm5_tp_layer_specs(None, 45).unwrap().is_empty());
1548 assert!(parse_glm5_tp_layer_specs(Some(""), 45).unwrap().is_empty());
1549 assert!(parse_glm5_tp_layer_specs(Some("0"), 45).unwrap().is_empty());
1550 let all = parse_glm5_tp_layer_specs(Some("all@0,1"), 45).unwrap();
1552 assert_eq!(all.len(), 45);
1553 assert_eq!(all[0].devices, vec![0, 1]);
1554 let all4 = parse_glm5_tp_layer_specs(Some("all@0,1"), 4).unwrap();
1555 assert_eq!(all4.len(), 4);
1556 let quad = parse_glm5_tp_layer_specs(Some("all@0,1,2,3"), 45).unwrap();
1558 assert_eq!(quad.len(), 45);
1559 assert_eq!(quad[0].devices, vec![0, 1, 2, 3]);
1560 let r = parse_glm5_tp_layer_specs(Some("0-2@0,1;4@0,1"), 45).unwrap();
1562 assert_eq!(
1563 r.iter().map(|s| s.layer).collect::<Vec<_>>(),
1564 vec![0, 1, 2, 4]
1565 );
1566 assert!(parse_glm5_tp_layer_specs(Some("0@0,0"), 45).is_err());
1568 assert!(parse_glm5_tp_layer_specs(Some("0@0,1;0@0,1"), 45).is_err());
1569 assert!(parse_glm5_tp_layer_specs(Some("banana"), 45).is_err());
1570 }
1571
1572 fn fixture_view() -> Glm5TpModelView {
1573 Glm5TpModelView {
1574 trunk_layers: 4,
1575 layer_class: vec![
1576 Glm5LayerClass::Kda,
1577 Glm5LayerClass::Mla,
1578 Glm5LayerClass::Kda,
1579 Glm5LayerClass::Mla,
1580 ],
1581 layer_is_moe: vec![false, true, true, true],
1582 kda_heads: 4,
1583 kda_head_dim: 128,
1584 mla_heads: 4,
1585 n_routed_experts: 4,
1586 top_k: 2,
1587 }
1588 }
1589
1590 #[test]
1594 fn preflight_geometry_laws_are_dimension_derived() {
1595 let v = fixture_view();
1598 for ranks in GLM5_TP_ALLOWED_RANKS {
1599 assert_eq!(v.kda_heads % ranks, 0);
1600 assert_eq!(v.mla_heads % ranks, 0);
1601 assert_eq!(v.n_routed_experts % ranks, 0);
1602 }
1603 let odd = Glm5TpModelView {
1604 kda_heads: 3,
1605 ..fixture_view()
1606 };
1607 assert_ne!(odd.kda_heads % 2, 0);
1608 let bad_dim = Glm5TpModelView {
1609 kda_head_dim: 64,
1610 ..fixture_view()
1611 };
1612 assert_ne!(bad_dim.kda_head_dim, crate::kda::KDA_HEAD_DIM);
1613 let odd_experts = Glm5TpModelView {
1614 n_routed_experts: 5,
1615 ..fixture_view()
1616 };
1617 assert_ne!(odd_experts.n_routed_experts % 2, 0);
1618 assert!(!GLM5_TP_ALLOWED_RANKS.contains(&3));
1620 }
1621
1622 #[test]
1623 fn armed_check_counts_parse_errors_as_armed() {
1624 for (v, armed) in [
1629 ("", false),
1630 ("0", false),
1631 ("all@0,1", true),
1632 ("all@0,1,2,3", true),
1633 ("junk", true),
1634 ] {
1635 let is_armed = !v.is_empty() && v != "0";
1636 assert_eq!(is_armed, armed);
1637 }
1638 }
1639
1640 #[test]
1641 fn every_refused_door_composition_bites_by_name() {
1642 for (flag, _) in GLM5_TP_REFUSED_DOOR_FLAGS {
1648 let err = refuse_glm5_tp_door_composition(|f| f == flag)
1649 .expect_err("an armed door must refuse");
1650 assert!(err.contains("MEMRA_GLM5_TP"), "{err}");
1651 assert!(err.contains(flag), "{err}");
1652 assert!(err.contains("unproven composition"), "{err}");
1653 }
1654 refuse_glm5_tp_door_composition(|_| false).expect("cold doors must pass");
1656 refuse_glm5_tp_door_composition(|f| f == "MEMRA_GLM5_VERIFY_BATCH")
1659 .expect("verify-batch is refused via the spec co-refusal, not here");
1660 }
1661}