1use anyhow::{Context, Result, bail};
28use atlas_core::config::ModelConfig;
29use spark_runtime::gpu::{DevicePtr, GpuBackend};
30use spark_runtime::kv_cache::KvCacheDtype;
31use spark_runtime::weights::{WeightDtype, WeightStore, WeightTensor};
32
33use super::ModelWeightLoader;
34use crate::layer::TransformerLayer;
35use crate::layers::glm5next_dsa::build::build_dsa_weights;
36use crate::layers::glm5next_dsa::layer::{Glm5NextDsaLayer, Glm5NextDsaLayerKernels};
37use crate::layers::glm5next_dsa::{Glm5NextDsaConfig, Glm5NextDsaKernels};
38use crate::layers::glm5next_kda::binding::{
39 KdaDtype, KdaTensorSource, RawTensor, bind_kda_weights,
40};
41use crate::layers::glm5next_kda::tp::KdaTpPlan;
42use crate::layers::glm5next_kda::tp_bind::KdaShardedSource;
43use crate::layers::glm5next_kda::{Glm5NextKdaConfig, Glm5NextKdaKernels, Glm5NextKdaLayer};
44use crate::layers::glm5next_layer::{Glm5NextLayer, Glm5NextMhc, Glm5NextMixer, Glm5NextMlpSite};
45use crate::layers::glm5next_mlp::weights::{Glm5NextExpertWeights, Nvfp4Proj};
46use crate::layers::glm5next_mlp::{Glm5NextMlpConfig, Glm5NextMlpKernels, build as mlp_build};
47use crate::layers::glm5next_skeleton::{Glm5NextTextSkeleton, Mixer, Mlp};
48use crate::layers::ops::{Glm5NextMhcKernels, Glm5NextMhcSiteWeights, MHC_MIX_MAX_TOKENS, mix_hc};
49use crate::weight_map::DenseWeight;
50
51pub struct Glm5NextWeightLoader;
52
53fn qualify(layer: usize, leaf: &str) -> String {
58 format!("model.language_model.layers.{layer}.{leaf}")
59}
60
61fn is_reuploaded(name: &str, num_layers: usize) -> bool {
77 let Some(rest) = name.strip_prefix("model.language_model.layers.") else {
78 return false;
79 };
80 let Some((idx, rel)) = rest.split_once('.') else {
81 return false;
82 };
83 let Ok(idx) = idx.parse::<usize>() else {
84 return false;
85 };
86 idx < num_layers && !rel.starts_with("mlp.experts.")
87}
88
89fn host_bytes(gpu: &dyn GpuBackend, t: &WeightTensor) -> Result<Vec<u8>> {
91 let mut b = vec![0u8; t.byte_size()];
92 gpu.copy_d2h(t.ptr, &mut b)?;
93 Ok(b)
94}
95
96fn host_f32(gpu: &dyn GpuBackend, t: &WeightTensor, what: &str) -> Result<Vec<f32>> {
102 let b = host_bytes(gpu, t)?;
103 match t.dtype {
104 WeightDtype::BF16 => Ok(b
105 .chunks_exact(2)
106 .map(|c| half::bf16::from_bits(u16::from_le_bytes([c[0], c[1]])).to_f32())
107 .collect()),
108 WeightDtype::FP32 => Ok(b
109 .chunks_exact(4)
110 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
111 .collect()),
112 other => bail!(
113 "{what}: dtype {other:?} cannot be read as f32 without a conversion this loader refuses to guess"
114 ),
115 }
116}
117
118fn upload_f32(gpu: &dyn GpuBackend, v: &[f32]) -> Result<DevicePtr> {
119 let b: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
120 let p = gpu.alloc(b.len().max(1))?;
121 gpu.copy_h2d(&b, p)?;
122 Ok(p)
123}
124
125pub(super) struct LayerSource {
127 names: Vec<String>,
128 tensors: std::collections::BTreeMap<String, (WeightDtype, Vec<usize>, Vec<u8>)>,
129}
130
131impl LayerSource {
132 pub(super) fn collect(gpu: &dyn GpuBackend, store: &WeightStore, layer: usize) -> Result<Self> {
133 let prefix = format!("model.language_model.layers.{layer}.");
134 let mut names = Vec::new();
135 let mut tensors = std::collections::BTreeMap::new();
136 let rels: Vec<String> = store
137 .names()
138 .filter_map(|n| n.strip_prefix(&prefix).map(|r| r.to_string()))
139 .collect();
140 for rel in rels {
141 let rel = rel.as_str();
142 if rel.starts_with("mlp.experts.") {
145 names.push(rel.to_string());
146 continue;
147 }
148 names.push(rel.to_string());
149 let t = store.get(&format!("{prefix}{rel}"))?;
150 tensors.insert(
151 rel.to_string(),
152 (t.dtype, t.shape.clone(), host_bytes(gpu, t)?),
153 );
154 }
155 Ok(Self { names, tensors })
156 }
157
158 pub(super) fn f32(&self, name: &str) -> Result<Vec<f32>> {
159 let (dtype, _, bytes) = self
160 .tensors
161 .get(name)
162 .with_context(|| format!("missing tensor {name}"))?;
163 match dtype {
164 WeightDtype::BF16 => Ok(bytes
165 .chunks_exact(2)
166 .map(|c| half::bf16::from_bits(u16::from_le_bytes([c[0], c[1]])).to_f32())
167 .collect()),
168 WeightDtype::FP32 => Ok(bytes
169 .chunks_exact(4)
170 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
171 .collect()),
172 other => bail!("{name}: dtype {other:?} is not a plain float tensor"),
173 }
174 }
175}
176
177impl KdaTensorSource for LayerSource {
178 fn get(&self, name: &str) -> Option<RawTensor<'_>> {
179 let (dtype, shape, bytes) = self.tensors.get(name)?;
180 let dtype = match dtype {
185 WeightDtype::BF16 => KdaDtype::Bf16,
186 WeightDtype::FP32 => KdaDtype::F32,
187 _ => return None,
188 };
189 Some(RawTensor {
190 dtype,
191 shape: shape.clone(),
192 bytes,
193 })
194 }
195 fn names(&self) -> Vec<String> {
196 self.names.clone()
197 }
198}
199
200fn bind_mhc_site(
206 gpu: &dyn GpuBackend,
207 src: &LayerSource,
208 site: &str,
209 hc_mult: usize,
210 hidden: usize,
211) -> Result<Glm5NextMhcSiteWeights> {
212 let f = src.f32(&format!("hc_{site}_fn"))?;
213 let want = mix_hc(hc_mult) * hc_mult * hidden;
214 if f.len() != want {
215 bail!(
216 "hc_{site}_fn has {} elements, expected mix_hc({hc_mult}) * {hc_mult} * {hidden} = {want}",
217 f.len()
218 );
219 }
220 let base = src.f32(&format!("hc_{site}_base"))?;
221 if base.len() != mix_hc(hc_mult) {
222 bail!(
223 "hc_{site}_base has {} entries, expected mix_hc({hc_mult}) = {}",
224 base.len(),
225 mix_hc(hc_mult)
226 );
227 }
228 let scale = src.f32(&format!("hc_{site}_scale"))?;
229 if scale.len() != 3 {
230 bail!(
231 "hc_{site}_scale has {} entries, expected 3 (pre, post, comb)",
232 scale.len()
233 );
234 }
235 Ok(Glm5NextMhcSiteWeights {
236 hc_fn: upload_f32_as_bf16(gpu, &f)?,
241 hc_fn_bf16: true,
242 hc_scale: upload_f32(gpu, &scale)?,
243 hc_base: upload_f32(gpu, &base)?,
244 mix: gpu.alloc(MHC_MIX_MAX_TOKENS * mix_hc(hc_mult) * 4)?,
246 })
247}
248
249pub(super) fn bind_expert(
251 gpu: &dyn GpuBackend,
252 store: &WeightStore,
253 layer: usize,
254 id: usize,
255) -> Result<Glm5NextExpertWeights> {
256 let proj = |p: &str| -> Result<Nvfp4Proj> {
257 let base = format!("mlp.experts.{id}.{p}");
258 let packed = store.get(&qualify(layer, &format!("{base}.weight")))?;
259 let scale = store.get(&qualify(layer, &format!("{base}.weight_scale")))?;
260 let s2 = store.get(&qualify(layer, &format!("{base}.weight_scale_2")))?;
261 if packed.dtype != WeightDtype::UInt8 {
262 bail!(
263 "{base}.weight is {:?}, expected packed U8 NVFP4",
264 packed.dtype
265 );
266 }
267 let s2 = host_f32(gpu, s2, &format!("{base}.weight_scale_2"))?;
268 let [s2] = s2[..] else {
269 bail!("{base}.weight_scale_2 is not a scalar");
270 };
271 Ok(Nvfp4Proj {
272 packed: packed.ptr,
273 scale: scale.ptr,
274 scale_2: s2,
275 })
276 };
277 Ok(Glm5NextExpertWeights {
278 gate_proj: proj("gate_proj")?,
279 up_proj: proj("up_proj")?,
280 down_proj: proj("down_proj")?,
281 })
282}
283
284fn dense(store: &WeightStore, name: &str) -> Result<DenseWeight> {
285 Ok(DenseWeight {
286 weight: store.get(name)?.ptr,
287 })
288}
289
290impl ModelWeightLoader for Glm5NextWeightLoader {
291 fn binds_vision_encoder(&self) -> bool {
298 false
299 }
300
301 fn supports_tp(&self) -> bool {
304 true
305 }
306
307 fn load_layers(
308 &self,
309 store: &WeightStore,
310 config: &ModelConfig,
311 gpu: &dyn GpuBackend,
312 _layer_kv_dtypes: &[KvCacheDtype],
313 ) -> Result<Vec<Box<dyn TransformerLayer>>> {
314 let skeleton = Glm5NextTextSkeleton::from_config(config)?;
315 let kda_cfg = Glm5NextKdaConfig {
320 hidden: config.hidden_size,
321 heads: config.linear_num_value_heads,
322 head_dim: config.linear_value_head_dim,
323 conv_kernel: config.linear_conv_kernel_dim,
324 gate_lower_bound: config.linear_gate_lower_bound,
325 rms_norm_eps: config.rms_norm_eps as f32,
326 l2_eps: 1e-6,
327 chunk: 32,
328 };
329 kda_cfg.validate()?;
330 let gate_rank = {
334 let n = qualify(0, "self_attn.f_a_proj.weight");
335 let t = store.get(&n).with_context(|| {
336 format!("glm5_next: {n} is needed to size the KDA gate bottleneck")
337 })?;
338 *t.shape.first().context("f_a_proj has no rows")?
339 };
340 let kda_plan = KdaTpPlan::from_config(config, gate_rank)?;
341 let dsa_cfg = Glm5NextDsaConfig::from_config(config)?;
342 let mlp_cfg = Glm5NextMlpConfig::from_config(config)?;
343
344 let kda_kernels = Glm5NextKdaKernels::resolve(gpu)?;
345 let dsa_kernels = Glm5NextDsaKernels::resolve(gpu)?;
346 let dsa_layer_kernels = Glm5NextDsaLayerKernels::resolve(gpu)?;
347 let mlp_kernels = Glm5NextMlpKernels::resolve(gpu)?;
348 let mhc_kernels_probe = Glm5NextMhcKernels::resolve(gpu)?;
349 let rms_norm_k = gpu.kernel("rms_norm_vanilla", "rms_norm_vanilla")?;
350 let add_k = crate::layers::try_kernel(gpu, "bf16_add", "bf16_add_inplace");
353
354 let verify_k = (crate::layers::ops::DENSE_GEMV_BATCHM_MAX_M as usize)
374 .max(crate::layers::glm5next_layer::PREFILL_ROWS)
375 .max(crate::layers::glm5next_layer::prefill_rows());
376 let kda_ws = std::sync::Arc::new(crate::layers::glm5next_kda::Glm5NextKdaWorkspace::new(
377 gpu, &kda_cfg, verify_k,
378 )?);
379
380 let dsa_plan = crate::layers::glm5next_dsa::tp::DsaTpPlan::new(
381 config.tp_rank,
382 config.tp_world_size.max(1),
383 &dsa_cfg,
384 )?;
385 let last = skeleton.layers.len() - 1;
386 let mut out: Vec<Box<dyn TransformerLayer>> = Vec::with_capacity(skeleton.layers.len());
387
388 let mut attn_layer_idx = 0usize;
393
394 for sl in &skeleton.layers {
395 let idx = sl.index;
396 let src = LayerSource::collect(gpu, store, idx)
397 .with_context(|| format!("glm5_next: collecting layer {idx}"))?;
398
399 let mixer = match sl.mixer {
400 Mixer::Kda => {
401 let sharded = KdaShardedSource::new(&src, &kda_plan)?;
404 let (w, _report) = bind_kda_weights(gpu, &kda_cfg, idx, &sharded)?;
405 Glm5NextMixer::Kda {
406 layer: Box::new(Glm5NextKdaLayer::new(idx, kda_cfg, w, kda_kernels)?),
407 ws: kda_ws.clone(),
408 cfg: kda_cfg,
409 }
410 }
411 Mixer::Dsa => {
412 let load = |n: &str| src.f32(n);
413 let w = build_dsa_weights(gpu, &dsa_cfg, &dsa_plan, &load)?;
414 Glm5NextMixer::Dsa(Box::new(Glm5NextDsaLayer {
415 persist_bt: std::env::var("ATLAS_GLM_DSA_ALLOC_PER_STEP").as_deref()
419 != Ok("1"),
420 cfg: dsa_cfg,
421 weights: w,
422 kernels: dsa_layer_kernels,
423 select_kernels: dsa_kernels,
424 decode_kernel:
425 crate::layers::glm5next_dsa::attend::Glm5NextDsaDecodeKernel::resolve(
426 gpu,
427 )?,
428 workspace: crate::layers::glm5next_dsa::layer::Glm5NextDsaWorkspace::new(
429 gpu, &dsa_cfg, verify_k,
430 )?,
431 layer_idx: idx,
432 attn_layer_idx: {
433 let a = attn_layer_idx;
434 attn_layer_idx += 1;
435 a
436 },
437 rms_eps: config.rms_norm_eps as f32,
438 kv_scale: 1.0,
439 }))
440 }
441 };
442
443 let load = |n: &str| src.f32(n);
444 let mlp = match sl.mlp {
445 Mlp::Dense => Glm5NextMlpSite::Dense(mlp_build::build_dense_mlp(
446 gpu,
447 &mlp_cfg,
448 config.tp_rank,
449 config.intermediate_size,
450 "mlp",
451 &load,
452 )?),
453 Mlp::RoutedMoe => {
454 let expert = |id: usize| bind_expert(gpu, store, idx, id);
455 Glm5NextMlpSite::Moe(Box::new(mlp_build::build_moe(
456 gpu,
457 &mlp_cfg,
458 config.tp_rank,
459 config.shared_expert_intermediate_size,
460 &load,
461 &expert,
462 )?))
463 }
464 };
465
466 let mhc = if sl.hyper_connection {
467 Some(Glm5NextMhc {
468 kernels: mhc_kernels_probe,
469 attn: bind_mhc_site(gpu, &src, "attn", config.hc_mult, config.hidden_size)?,
470 ffn: bind_mhc_site(gpu, &src, "ffn", config.hc_mult, config.hidden_size)?,
471 hc_mult: config.hc_mult,
472 sinkhorn_iters: config.hc_sinkhorn_iters,
473 hc_eps: config.hc_eps,
474 })
475 } else {
476 None
477 };
478
479 out.push(Box::new(Glm5NextLayer {
480 layer_idx: idx,
481 mixer,
482 mlp,
483 mlp_cfg,
484 mlp_kernels,
485 mlp_ws: crate::layers::glm5next_mlp::forward::Glm5NextMlpWorkspace::new(
486 gpu, &mlp_cfg, verify_k,
487 )?,
488 mhc,
489 input_norm: upload_f32_as_bf16(gpu, &src.f32("input_layernorm.weight")?)?,
490 post_attn_norm: upload_f32_as_bf16(
491 gpu,
492 &src.f32("post_attention_layernorm.weight")?,
493 )?,
494 rms_norm_k,
495 add_k,
496 rms_eps: config.rms_norm_eps as f32,
497 hidden: config.hidden_size,
498 mixer_all_reduce: match sl.mixer {
499 Mixer::Kda => kda_plan.needs_output_all_reduce(),
500 Mixer::Dsa => dsa_plan.needs_output_all_reduce(),
501 },
502 is_first: idx == 0,
503 is_last: idx == last,
504 }));
505 }
506 Ok(out)
507 }
508
509 fn load_embedding(
510 &self,
511 store: &WeightStore,
512 _config: &ModelConfig,
513 _gpu: &dyn GpuBackend,
514 ) -> Result<DenseWeight> {
515 dense(store, "model.language_model.embed_tokens.weight")
516 }
517
518 fn load_final_norm(
519 &self,
520 store: &WeightStore,
521 _config: &ModelConfig,
522 _gpu: &dyn GpuBackend,
523 ) -> Result<DenseWeight> {
524 dense(store, "model.language_model.norm.weight")
525 }
526
527 fn load_lm_head(
528 &self,
529 store: &WeightStore,
530 _config: &ModelConfig,
531 _gpu: &dyn GpuBackend,
532 ) -> Result<DenseWeight> {
533 dense(store, "lm_head.weight")
534 }
535
536 fn load_mtp_weights(
539 &self,
540 _store: &WeightStore,
541 _config: &ModelConfig,
542 _gpu: &dyn GpuBackend,
543 ) -> Result<Option<crate::weight_loader::MtpWeights>> {
544 Ok(None)
545 }
546
547 fn prune_after_load(
549 &self,
550 store: &mut WeightStore,
551 config: &ModelConfig,
552 gpu: &dyn GpuBackend,
553 ) -> Result<()> {
554 let n = config.num_hidden_layers;
555 let (count, bytes) = store.free_matching(gpu, |name| is_reuploaded(name, n))?;
556 tracing::info!(
557 "glm5_next: released {count} store tensors ({:.2} GB) already re-uploaded by the \
558 binders; routed experts and the MTP block kept",
559 bytes as f64 / 1e9,
560 );
561 Ok(())
562 }
563}
564
565pub(super) fn upload_f32_as_bf16(gpu: &dyn GpuBackend, v: &[f32]) -> Result<DevicePtr> {
566 let b: Vec<u8> = v
567 .iter()
568 .flat_map(|x| half::bf16::from_f32(*x).to_le_bytes())
569 .collect();
570 let p = gpu.alloc(b.len().max(1))?;
571 gpu.copy_h2d(&b, p)?;
572 Ok(p)
573}
574
575#[cfg(test)]
576mod prune_tests {
577 use super::is_reuploaded;
578
579 #[test]
580 fn prunes_only_the_reuploaded_layer_tensors() {
581 let n = 45;
582 assert!(is_reuploaded(
584 "model.language_model.layers.0.self_attn.q_proj.weight",
585 n
586 ));
587 assert!(is_reuploaded(
588 "model.language_model.layers.44.mlp.gate.weight",
589 n
590 ));
591 assert!(is_reuploaded(
592 "model.language_model.layers.3.hc_attn_fn.weight",
593 n
594 ));
595 assert!(!is_reuploaded(
597 "model.language_model.layers.7.mlp.experts.12.down_proj.weight",
598 n
599 ));
600 assert!(!is_reuploaded(
602 "model.language_model.layers.45.self_attn.q_proj.weight",
603 n
604 ));
605 assert!(!is_reuploaded(
607 "model.language_model.embed_tokens.weight",
608 n
609 ));
610 assert!(!is_reuploaded("lm_head.weight", n));
611 assert!(!is_reuploaded("model.language_model.layers.x.foo", n));
613 }
614}
615
616pub(super) fn layer_source(
618 gpu: &dyn GpuBackend,
619 store: &WeightStore,
620 layer: usize,
621) -> Result<LayerSource> {
622 LayerSource::collect(gpu, store, layer)
623}
624
625pub(super) fn bind_expert_at(
627 gpu: &dyn GpuBackend,
628 store: &WeightStore,
629 layer: usize,
630 id: usize,
631) -> Result<Glm5NextExpertWeights> {
632 bind_expert(gpu, store, layer, id)
633}
634
635pub(super) fn upload_bf16(gpu: &dyn GpuBackend, v: &[f32]) -> Result<DevicePtr> {
637 upload_f32_as_bf16(gpu, v)
638}
639
640#[cfg(test)]
641mod vision_capability_tests {
642 use super::Glm5NextWeightLoader;
643 use crate::weight_loader::ModelWeightLoader;
644
645 #[test]
646 fn glm5_next_declares_itself_text_only() {
647 assert!(
650 !Glm5NextWeightLoader.binds_vision_encoder(),
651 "GLM-5.3's port binds no vision encoder; saying otherwise makes the \
652 weight loader read the tower into unified memory for nothing"
653 );
654 }
655
656 #[test]
657 fn a_multimodal_loader_still_declares_true_by_default() {
658 assert!(crate::weight_loader::qwen35::Qwen35WeightLoader.binds_vision_encoder());
661 }
662}