1use std::collections::{BTreeMap, BTreeSet};
24
25use anyhow::{Result, bail};
26use metrale_config::{LayerType, ModelConfig};
27
28mod budget;
29pub use budget::{StateBudget, StructuralAccounting};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Mixer {
35 Kda,
37 Dsa,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Mlp {
44 Dense,
45 RoutedMoe,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct SkeletonLayer {
51 pub index: usize,
52 pub mixer: Mixer,
53 pub mlp: Mlp,
54 pub hyper_connection: bool,
56 pub is_mtp: bool,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum ResidualStep {
66 SaveResidual,
68 HcPre(Site),
70 Norm(&'static str),
72 Mixer,
74 Mlp,
76 HcPost(Site),
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum Site {
82 Attn,
83 Ffn,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum FinalStep {
89 HyperHeadMean,
91 Norm(&'static str),
92 LmHead,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum StateKind {
98 KdaRecurrent,
101 SparseKv,
103}
104
105#[derive(Debug, Clone)]
106pub struct Glm5NextTextSkeleton {
107 pub hidden_size: usize,
108 pub hc_mult: usize,
109 pub layers: Vec<SkeletonLayer>,
111 pub mtp: Option<SkeletonLayer>,
114}
115
116const NON_LAYER_TENSORS: [&str; 3] = [
117 "model.language_model.embed_tokens.weight",
118 "model.language_model.norm.weight",
119 "lm_head.weight",
120];
121
122const KDA_ATTN: [&str; 15] = [
124 "self_attn.q_proj.weight",
125 "self_attn.k_proj.weight",
126 "self_attn.v_proj.weight",
127 "self_attn.b_proj.weight",
128 "self_attn.f_a_proj.weight",
129 "self_attn.f_b_proj.weight",
130 "self_attn.g_a_proj.weight",
131 "self_attn.g_b_proj.weight",
132 "self_attn.q_conv1d.weight",
133 "self_attn.k_conv1d.weight",
134 "self_attn.v_conv1d.weight",
135 "self_attn.A_log",
136 "self_attn.dt_bias",
137 "self_attn.o_norm.weight",
138 "self_attn.o_proj.weight",
139];
140
141const DSA_ATTN: [&str; 14] = [
146 "self_attn.q_a_proj.weight",
147 "self_attn.q_a_layernorm.weight",
148 "self_attn.q_b_proj.weight",
149 "self_attn.kv_a_proj_with_mqa.weight",
150 "self_attn.kv_a_layernorm.weight",
151 "self_attn.kv_b_proj.weight",
152 "self_attn.o_proj.weight",
153 "self_attn.indexer.wq_b.weight",
154 "self_attn.indexer.wk.weight",
155 "self_attn.indexer.k_norm.weight",
156 "self_attn.indexer.k_norm.bias",
157 "self_attn.indexer.weights_proj.weight",
158 "self_attn.indexer.index_kpool_compress_ape",
159 "self_attn.indexer.index_kpool_compress_gate",
160];
161
162const LAYER_NORMS: [&str; 2] = ["input_layernorm.weight", "post_attention_layernorm.weight"];
163
164const HC_PARAMS: [&str; 6] = [
165 "hc_attn_fn",
166 "hc_attn_base",
167 "hc_attn_scale",
168 "hc_ffn_fn",
169 "hc_ffn_base",
170 "hc_ffn_scale",
171];
172
173const MTP_HEAD: [&str; 4] = [
176 "eh_proj.weight",
177 "enorm.weight",
178 "hnorm.weight",
179 "shared_head.norm.weight",
180];
181
182fn qualify(layer: usize, leaf: &str) -> String {
183 format!("model.language_model.layers.{layer}.{leaf}")
184}
185
186impl Glm5NextTextSkeleton {
187 pub fn from_config(cfg: &ModelConfig) -> Result<Self> {
192 if cfg.model_type != "glm5_next" {
193 bail!(
194 "Glm5NextTextSkeleton built from a {:?} config",
195 cfg.model_type
196 );
197 }
198 if cfg.layer_types.len() != cfg.num_hidden_layers {
199 bail!(
200 "layer_types has {} entries, num_hidden_layers is {}",
201 cfg.layer_types.len(),
202 cfg.num_hidden_layers
203 );
204 }
205 if cfg.hc_mult == 0 {
206 bail!("glm5_next skeleton needs hc_mult > 0; got 0 (mHC is not optional here)");
207 }
208 let dense: BTreeSet<usize> = cfg.mlp_only_layers.iter().copied().collect();
209
210 let mixer_of = |t: LayerType, i: usize| -> Result<Mixer> {
211 Ok(match t {
212 LayerType::LinearAttention => Mixer::Kda,
213 LayerType::SparseAttention => Mixer::Dsa,
214 other => {
215 bail!("layer {i}: GLM-5.3-Flash has no {other:?} layers; refusing to bind one")
216 }
217 })
218 };
219
220 let mut layers = Vec::with_capacity(cfg.num_hidden_layers);
221 for (i, t) in cfg.layer_types.iter().enumerate() {
222 layers.push(SkeletonLayer {
223 index: i,
224 mixer: mixer_of(*t, i)?,
225 mlp: if dense.contains(&i) {
226 Mlp::Dense
227 } else {
228 Mlp::RoutedMoe
229 },
230 hyper_connection: true,
231 is_mtp: false,
232 });
233 }
234
235 let mtp = match cfg.mtp_layer_types.len() {
238 0 => None,
239 1 => {
240 let i = cfg.num_hidden_layers;
241 Some(SkeletonLayer {
242 index: i,
243 mixer: mixer_of(cfg.mtp_layer_types[0], i)?,
244 mlp: Mlp::RoutedMoe,
245 hyper_connection: false,
247 is_mtp: true,
248 })
249 }
250 n => bail!("glm5_next skeleton expects 0 or 1 MTP layers, config declares {n}"),
251 };
252
253 Ok(Self {
254 hidden_size: cfg.hidden_size,
255 hc_mult: cfg.hc_mult,
256 layers,
257 mtp,
258 })
259 }
260
261 pub fn all_layers(&self) -> Vec<SkeletonLayer> {
263 let mut v = self.layers.clone();
264 v.extend(self.mtp);
265 v
266 }
267
268 pub fn structural_tensors(&self, l: &SkeletonLayer) -> Vec<String> {
270 let mut v: Vec<String> = match l.mixer {
271 Mixer::Kda => KDA_ATTN.iter().map(|t| qualify(l.index, t)).collect(),
272 Mixer::Dsa => DSA_ATTN.iter().map(|t| qualify(l.index, t)).collect(),
273 };
274 v.extend(LAYER_NORMS.iter().map(|t| qualify(l.index, t)));
275 if l.hyper_connection {
276 v.extend(HC_PARAMS.iter().map(|t| qualify(l.index, t)));
277 }
278 if l.is_mtp {
279 v.extend(MTP_HEAD.iter().map(|t| qualify(l.index, t)));
280 }
281 v
282 }
283
284 pub fn structural_tensor_set(&self) -> BTreeSet<String> {
287 let mut s: BTreeSet<String> = NON_LAYER_TENSORS.iter().map(|t| t.to_string()).collect();
288 for l in self.all_layers() {
289 s.extend(self.structural_tensors(&l));
290 }
291 s
292 }
293
294 pub fn residual_plan(&self, l: &SkeletonLayer) -> Vec<ResidualStep> {
299 let mut p = Vec::new();
300 for (site, norm, sub) in [
301 (Site::Attn, LAYER_NORMS[0], ResidualStep::Mixer),
302 (Site::Ffn, LAYER_NORMS[1], ResidualStep::Mlp),
303 ] {
304 if l.hyper_connection {
305 p.push(ResidualStep::SaveResidual);
306 p.push(ResidualStep::HcPre(site));
307 p.push(ResidualStep::Norm(norm));
308 p.push(sub);
309 p.push(ResidualStep::HcPost(site));
310 } else {
311 p.push(ResidualStep::SaveResidual);
312 p.push(ResidualStep::Norm(norm));
313 p.push(sub);
314 }
315 }
316 p
317 }
318
319 pub fn final_plan(&self) -> [FinalStep; 3] {
321 [
322 FinalStep::HyperHeadMean,
323 FinalStep::Norm("model.language_model.norm.weight"),
324 FinalStep::LmHead,
325 ]
326 }
327
328 pub fn state_plan(&self) -> BTreeMap<usize, StateKind> {
330 self.all_layers()
331 .iter()
332 .map(|l| {
333 (
334 l.index,
335 match l.mixer {
336 Mixer::Kda => StateKind::KdaRecurrent,
337 Mixer::Dsa => StateKind::SparseKv,
338 },
339 )
340 })
341 .collect()
342 }
343
344 pub fn kda_state_layers(&self) -> Vec<usize> {
346 self.state_plan()
347 .into_iter()
348 .filter(|(_, k)| *k == StateKind::KdaRecurrent)
349 .map(|(i, _)| i)
350 .collect()
351 }
352
353 pub fn kv_cache_layers(&self) -> Vec<usize> {
355 self.state_plan()
356 .into_iter()
357 .filter(|(_, k)| *k == StateKind::SparseKv)
358 .map(|(i, _)| i)
359 .collect()
360 }
361
362 pub fn state_budget(&self, cfg: &ModelConfig, num_spec: usize) -> StateBudget {
365 let kda_layers = self.kda_state_layers().len();
366 let kv_layers = self.layers.iter().filter(|l| l.mixer == Mixer::Dsa).count();
367 let heads = cfg.linear_num_value_heads.max(1);
368 let hd = cfg.linear_value_head_dim.max(1);
369 let conv_dim = cfg.linear_num_key_heads * cfg.linear_key_head_dim * 2
370 + cfg.linear_num_value_heads * cfg.linear_value_head_dim;
371 StateBudget {
372 kda_recurrent: kda_layers * heads * hd * hd * 4,
373 kda_conv: kda_layers * conv_dim * (cfg.linear_conv_kernel_dim - 1 + num_spec) * 2,
374 dsa_kv_per_token: kv_layers * cfg.kv_lora_rank * 2,
375 dsa_indexer_per_token: kv_layers * (cfg.index_head_dim * 2 * 2 + 1),
378 mhc_highway_per_token: self.hc_mult * self.hidden_size * 4,
379 moe_routing_per_token: cfg.num_experts * 4 + cfg.num_experts_per_tok * 8,
380 }
381 }
382
383 pub fn account(&self, available: &BTreeSet<String>) -> StructuralAccounting {
390 let required = self.structural_tensor_set();
391 let mut missing = Vec::new();
392 for r in &required {
393 if !available.contains(r) {
394 missing.push(r.clone());
395 }
396 }
397 let mut unexpected = Vec::new();
398 let mut deferred = 0usize;
399 for a in available {
400 if required.contains(a) {
401 continue;
402 }
403 let is_mlp = a.contains(".mlp.");
404 let is_vision = a.starts_with("model.visual.") || a.starts_with("model.vision");
405 if is_mlp || is_vision {
406 deferred += 1;
407 } else {
408 unexpected.push(a.clone());
409 }
410 }
411 StructuralAccounting {
412 required: required.len(),
413 bound: required.len() - missing.len(),
414 missing,
415 unexpected,
416 deferred,
417 }
418 }
419}