spark_model/weight_loader/
glm5_next.rs1use std::collections::BTreeMap;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24pub enum TensorRole {
25 Embedding,
27 LmHead,
28 FinalNorm,
29 KdaProjection,
31 KdaConv,
32 KdaDecay,
33 KdaGate,
34 KdaNorm,
35 MlaProjection,
37 MlaNorm,
38 Indexer,
40 IndexerNorm,
41 MoeRouter,
43 MoeExpert,
44 MoeShared,
45 DenseFfn,
47 LayerNorm,
49 HyperConnection,
50 MtpProjection,
52 MtpNorm,
53 Vision,
55}
56
57impl TensorRole {
58 pub fn is_norm(self) -> bool {
63 matches!(
64 self,
65 TensorRole::FinalNorm
66 | TensorRole::KdaNorm
67 | TensorRole::MlaNorm
68 | TensorRole::IndexerNorm
69 | TensorRole::LayerNorm
70 | TensorRole::MtpNorm
71 )
72 }
73
74 pub fn is_text_model(self) -> bool {
76 !matches!(self, TensorRole::Vision)
77 }
78}
79
80fn normalize(name: &str) -> String {
83 name.split('.')
84 .map(|seg| {
85 if seg.is_empty() {
86 seg
87 } else if seg.chars().all(|c| c.is_ascii_digit()) || seg == "N" || seg == "E" {
88 "#"
89 } else {
90 seg
91 }
92 })
93 .collect::<Vec<_>>()
94 .join(".")
95}
96
97pub fn classify(name: &str) -> Option<TensorRole> {
102 let n = normalize(name);
103 let s = n.as_str();
104
105 if s == "lm_head.weight" {
107 return Some(TensorRole::LmHead);
108 }
109 if s == "model.language_model.embed_tokens.weight" {
110 return Some(TensorRole::Embedding);
111 }
112 if s == "model.language_model.norm.weight" {
113 return Some(TensorRole::FinalNorm);
114 }
115 if s.starts_with("model.visual.") || s.starts_with("model.vision") {
116 return Some(TensorRole::Vision);
117 }
118
119 let rest = s.strip_prefix("model.language_model.layers.#.")?;
120
121 match rest {
125 "eh_proj.weight" => return Some(TensorRole::MtpProjection),
126 "enorm.weight" | "hnorm.weight" | "shared_head.norm.weight" => {
127 return Some(TensorRole::MtpNorm);
128 }
129 _ => {}
130 }
131
132 match rest {
134 "self_attn.q_proj.weight"
135 | "self_attn.k_proj.weight"
136 | "self_attn.v_proj.weight"
137 | "self_attn.b_proj.weight"
138 | "self_attn.f_a_proj.weight"
139 | "self_attn.f_b_proj.weight"
140 | "self_attn.g_a_proj.weight"
141 | "self_attn.g_b_proj.weight" => return Some(TensorRole::KdaProjection),
142 "self_attn.q_conv1d.weight" | "self_attn.k_conv1d.weight" | "self_attn.v_conv1d.weight" => {
143 return Some(TensorRole::KdaConv);
144 }
145 "self_attn.A_log" | "self_attn.dt_bias" => return Some(TensorRole::KdaDecay),
146 "self_attn.o_norm.weight" => return Some(TensorRole::KdaNorm),
147 _ => {}
148 }
149
150 match rest {
152 "self_attn.q_a_proj.weight"
153 | "self_attn.q_b_proj.weight"
154 | "self_attn.kv_a_proj_with_mqa.weight"
155 | "self_attn.kv_b_proj.weight" => return Some(TensorRole::MlaProjection),
156 "self_attn.q_a_layernorm.weight" | "self_attn.kv_a_layernorm.weight" => {
157 return Some(TensorRole::MlaNorm);
158 }
159 "self_attn.o_proj.weight" => return Some(TensorRole::MlaProjection),
163 _ => {}
164 }
165
166 if let Some(idx) = rest.strip_prefix("self_attn.indexer.") {
168 return Some(match idx {
169 "k_norm.weight" | "k_norm.bias" => TensorRole::IndexerNorm,
170 _ => TensorRole::Indexer,
171 });
172 }
173
174 if rest.starts_with("hc_") {
176 return Some(TensorRole::HyperConnection);
177 }
178
179 if rest == "input_layernorm.weight" || rest == "post_attention_layernorm.weight" {
181 return Some(TensorRole::LayerNorm);
182 }
183
184 if let Some(mlp) = rest.strip_prefix("mlp.") {
186 if mlp.starts_with("gate.") || mlp == "gate.weight" {
187 return Some(TensorRole::MoeRouter);
188 }
189 if mlp.starts_with("experts.#.") {
190 return Some(TensorRole::MoeExpert);
191 }
192 if mlp.starts_with("shared_experts.") {
193 return Some(TensorRole::MoeShared);
194 }
195 if mlp.starts_with("gate_proj.")
197 || mlp.starts_with("up_proj.")
198 || mlp.starts_with("down_proj.")
199 {
200 return Some(TensorRole::DenseFfn);
201 }
202 }
203
204 None
205}
206
207pub fn is_mtp_only_name(name: &str) -> bool {
213 let n = normalize(name);
214 let Some(rest) = n.strip_prefix("model.language_model.layers.#.") else {
215 return false;
216 };
217 matches!(
218 rest,
219 "eh_proj.weight" | "enorm.weight" | "hnorm.weight" | "shared_head.norm.weight"
220 )
221}
222
223#[derive(Debug, Default)]
225pub struct Accounting {
226 pub total: usize,
227 pub by_role: BTreeMap<String, usize>,
228 pub unknown: Vec<String>,
230 pub mtp_layers: Vec<usize>,
232}
233
234pub fn account<'a, I>(names: I) -> Accounting
236where
237 I: IntoIterator<Item = (&'a str, usize)>,
238{
239 let mut acc = Accounting::default();
240 let mut mtp = std::collections::BTreeSet::new();
241 for (name, count) in names {
242 acc.total += count;
243 match classify(name) {
244 Some(role) => {
245 *acc.by_role.entry(format!("{role:?}")).or_insert(0) += count;
246 }
247 None => acc.unknown.push(name.to_string()),
248 }
249 if is_mtp_only_name(name)
250 && let Some(i) = layer_index(name)
251 {
252 mtp.insert(i);
253 }
254 }
255 acc.mtp_layers = mtp.into_iter().collect();
256 acc
257}
258
259pub fn layer_index(name: &str) -> Option<usize> {
262 let mut it = name.split('.');
263 while let Some(seg) = it.next() {
264 if seg == "layers" {
265 return it.next().and_then(|s| s.parse().ok());
266 }
267 }
268 None
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn normalizes_numeric_and_canonical_segments_alike() {
277 assert_eq!(
278 normalize("model.language_model.layers.7.self_attn.o_proj.weight"),
279 normalize("model.language_model.layers.N.self_attn.o_proj.weight")
280 );
281 assert_eq!(
282 normalize("model.language_model.layers.45.mlp.experts.12.down_proj.weight"),
283 normalize("model.language_model.layers.N.mlp.experts.E.down_proj.weight")
284 );
285 }
286
287 #[test]
288 fn mtp_is_found_by_layer_name_not_by_mtp_prefix() {
289 assert!(is_mtp_only_name(
290 "model.language_model.layers.45.eh_proj.weight"
291 ));
292 assert!(!is_mtp_only_name("model.language_model.layers.3.eh_proj"));
293 assert!(!is_mtp_only_name("model.layers.mtp.0.eh_proj.weight"));
295 assert_eq!(
296 layer_index("model.language_model.layers.45.eh_proj.weight"),
297 Some(45)
298 );
299 }
300
301 #[test]
302 fn unknown_tensor_is_refused_not_skipped() {
303 assert_eq!(
304 classify("model.language_model.layers.4.self_attn.wat"),
305 None
306 );
307 let acc = account([("model.language_model.layers.4.self_attn.wat", 1)]);
308 assert_eq!(acc.unknown.len(), 1);
309 }
310
311 #[test]
312 fn norm_tensors_land_in_norm_roles() {
313 for n in [
314 "model.language_model.norm.weight",
315 "model.language_model.layers.0.self_attn.o_norm.weight",
316 "model.language_model.layers.3.self_attn.q_a_layernorm.weight",
317 "model.language_model.layers.3.self_attn.indexer.k_norm.weight",
318 "model.language_model.layers.5.input_layernorm.weight",
319 "model.language_model.layers.45.enorm.weight",
320 ] {
321 let r = classify(n).unwrap_or_else(|| panic!("unclassified: {n}"));
322 assert!(r.is_norm(), "{n} classified as non-norm {r:?}");
323 }
324 }
325}