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