Skip to main content

skippy_model/
tensor_map.rs

1//! Hugging Face to llama.cpp tensor name mapping.
2
3use anyhow::{Result, anyhow, bail};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum TensorNameMap {
7    Raw,
8    HfToGguf,
9    HfToGgufWithMtp { layer_start: u32 },
10}
11
12impl TensorNameMap {
13    pub fn map_tensor_name(self, name: &str) -> Result<String> {
14        match self {
15            Self::Raw => Ok(name.to_string()),
16            Self::HfToGguf => map_hf_to_gguf(name, None),
17            Self::HfToGgufWithMtp { layer_start } => map_hf_to_gguf(name, Some(layer_start)),
18        }
19    }
20}
21
22fn map_hf_to_gguf(name: &str, mtp_layer_start: Option<u32>) -> Result<String> {
23    if let Some(layer_start) = mtp_layer_start
24        && let Some(normalized) = normalize_inkling_mtp_source_name(name, layer_start)?
25    {
26        return map_hf_to_gguf(&normalized, mtp_layer_start);
27    }
28    if let Some(layer_start) = mtp_layer_start
29        && let Some(normalized) = normalize_qwen_mtp_source_name(name, layer_start)?
30    {
31        return map_hf_to_gguf(&normalized, mtp_layer_start);
32    }
33    if matches!(name, "model.embed_tokens.weight" | "model.llm.embed.weight") {
34        return Ok("token_embd.weight".to_string());
35    }
36    if name == "embed_tokens.weight" {
37        return Ok("token_embd.weight".to_string());
38    }
39    if matches!(name, "lm_head.weight" | "model.llm.unembed.weight") {
40        return Ok("output.weight".to_string());
41    }
42    if matches!(name, "model.norm.weight" | "model.llm.norm.weight") {
43        return Ok("output_norm.weight".to_string());
44    }
45    if name == "model.llm.embed_norm.weight" {
46        return Ok("token_embd_norm.weight".to_string());
47    }
48    if name == "norm.weight" {
49        return Ok("output_norm.weight".to_string());
50    }
51    if let Some(name) = map_mtp_source_tensor(name)? {
52        return Ok(name);
53    }
54    let Some(layer) = HfLayerTensor::parse(name)? else {
55        bail!("no HF->GGUF tensor mapping for {name}");
56    };
57    layer.map()
58}
59
60pub(crate) fn is_mtp_source_tensor(name: &str) -> bool {
61    is_inkling_mtp_source_tensor(name)
62        || is_qwen_mtp_source_tensor(name)
63        || map_mtp_source_tensor(name).is_ok_and(|mapped| mapped.is_some())
64}
65
66pub(crate) fn hf_layer_id(name: &str) -> Result<Option<u32>> {
67    let Some(rest) = name.strip_prefix("model.layers.") else {
68        return Ok(None);
69    };
70    let Some((layer, _suffix)) = rest.split_once('.') else {
71        bail!("malformed layer tensor name {name}");
72    };
73    layer
74        .parse::<u32>()
75        .map(Some)
76        .map_err(|err| anyhow!("malformed layer id in {name}: {err}"))
77}
78
79pub(crate) fn is_shared_mtp_context_tensor(name: &str) -> bool {
80    matches!(
81        name,
82        "model.embed_tokens.weight"
83            | "embed_tokens.weight"
84            | "model.norm.weight"
85            | "norm.weight"
86            | "lm_head.weight"
87            | "model.llm.embed.weight"
88            | "model.llm.embed_norm.weight"
89            | "model.llm.norm.weight"
90            | "model.llm.unembed.weight"
91    )
92}
93
94pub(crate) fn is_inkling_fused_w13(name: &str) -> bool {
95    (name.starts_with("model.layers.") || name.starts_with("model.mtp.layers."))
96        && name.ends_with(".mlp.w13_dn.weight")
97}
98
99pub(crate) fn inkling_mtp_depth(name: &str) -> Result<Option<u32>> {
100    let Some(rest) = name.strip_prefix("model.mtp.layers.") else {
101        return Ok(None);
102    };
103    let Some((depth, _)) = rest.split_once('.') else {
104        bail!("malformed Inkling MTP tensor name {name}");
105    };
106    depth
107        .parse::<u32>()
108        .map(Some)
109        .map_err(|err| anyhow!("malformed Inkling MTP depth in {name}: {err}"))
110}
111
112fn is_inkling_mtp_source_tensor(name: &str) -> bool {
113    name.starts_with("model.mtp.layers.")
114}
115
116fn normalize_inkling_mtp_source_name(name: &str, layer_start: u32) -> Result<Option<String>> {
117    let Some(rest) = name.strip_prefix("model.mtp.layers.") else {
118        return Ok(None);
119    };
120    let Some((depth, suffix)) = rest.split_once('.') else {
121        bail!("malformed Inkling MTP tensor name {name}");
122    };
123    let depth = depth
124        .parse::<u32>()
125        .map_err(|err| anyhow!("malformed Inkling MTP depth in {name}: {err}"))?;
126    let layer = layer_start
127        .checked_add(depth)
128        .ok_or_else(|| anyhow!("Inkling MTP layer id overflow for {name}"))?;
129    let suffix = match suffix {
130        "embed_norm.weight" => "enorm.weight",
131        "hidden_norm.weight" => "hnorm.weight",
132        "input_proj.weight" => "eh_proj.weight",
133        value => value.strip_prefix("transformer_block.").unwrap_or(value),
134    };
135    Ok(Some(format!("model.layers.{layer}.{suffix}")))
136}
137
138fn map_mtp_source_tensor(name: &str) -> Result<Option<String>> {
139    let mapped = match name {
140        "pre_projection" | "pre_projection.weight" => "nextn.pre_projection.weight".to_string(),
141        "post_projection" | "post_projection.weight" => "nextn.post_projection.weight".to_string(),
142        "d2t" | "d2t.weight" => "d2t.weight".to_string(),
143        _ => {
144            let Some(layer) = HfLayerTensor::parse(name)? else {
145                return Ok(None);
146            };
147            let bid = layer.layer;
148            match layer.suffix {
149                "eh_proj" | "eh_proj.weight" => format!("blk.{bid}.nextn.eh_proj.weight"),
150                "embed_tokens" | "embed_tokens.weight" => {
151                    format!("blk.{bid}.nextn.embed_tokens.weight")
152                }
153                "enorm" | "enorm.weight" => format!("blk.{bid}.nextn.enorm.weight"),
154                "hnorm" | "hnorm.weight" => format!("blk.{bid}.nextn.hnorm.weight"),
155                "shared_head.head" | "shared_head.head.weight" | "shared_head.output.weight" => {
156                    format!("blk.{bid}.nextn.shared_head_head.weight")
157                }
158                "shared_head.norm" | "shared_head.norm.weight" => {
159                    format!("blk.{bid}.nextn.shared_head_norm.weight")
160                }
161                _ => return Ok(None),
162            }
163        }
164    };
165    Ok(Some(mapped))
166}
167
168fn is_qwen_mtp_source_tensor(name: &str) -> bool {
169    let name = name.strip_prefix("model.").unwrap_or(name);
170    name.starts_with("mtp.")
171}
172
173fn normalize_qwen_mtp_source_name(name: &str, layer_start: u32) -> Result<Option<String>> {
174    let name = name.strip_prefix("model.").unwrap_or(name);
175    if !name.starts_with("mtp.") {
176        return Ok(None);
177    }
178    let parts = name.splitn(4, '.').collect::<Vec<_>>();
179    if parts.len() == 4 && parts[1] == "layers" {
180        let mtp_idx = parts[2]
181            .parse::<u32>()
182            .map_err(|err| anyhow!("malformed MTP layer id in {name}: {err}"))?;
183        return Ok(Some(format!(
184            "model.layers.{}.{}",
185            layer_start + mtp_idx,
186            parts[3]
187        )));
188    }
189    if parts.len() == 3 {
190        let suffix = match parts[1] {
191            "fc" => "eh_proj",
192            "pre_fc_norm_embedding" => "enorm",
193            "pre_fc_norm_hidden" => "hnorm",
194            "norm" => "shared_head.norm",
195            _ => return Ok(None),
196        };
197        return Ok(Some(format!(
198            "model.layers.{layer_start}.{suffix}.{}",
199            parts[2]
200        )));
201    }
202    Ok(None)
203}
204
205struct HfLayerTensor<'a> {
206    layer: u32,
207    suffix: &'a str,
208}
209
210impl<'a> HfLayerTensor<'a> {
211    fn parse(name: &'a str) -> Result<Option<Self>> {
212        let Some(rest) = name.strip_prefix("model.layers.") else {
213            return Ok(None);
214        };
215        let Some((layer, suffix)) = rest.split_once('.') else {
216            bail!("malformed layer tensor name {name}");
217        };
218        let layer = layer
219            .parse::<u32>()
220            .map_err(|err| anyhow!("malformed layer id in {name}: {err}"))?;
221        Ok(Some(Self { layer, suffix }))
222    }
223
224    fn map(&self) -> Result<String> {
225        let bid = self.layer;
226        match self.suffix {
227            "input_layernorm.weight" => Ok(format!("blk.{bid}.attn_norm.weight")),
228            "post_attention_layernorm.weight" => Ok(format!("blk.{bid}.ffn_norm.weight")),
229            "self_attn.q_proj.weight" => Ok(format!("blk.{bid}.attn_q.weight")),
230            "self_attn.k_proj.weight" => Ok(format!("blk.{bid}.attn_k.weight")),
231            "self_attn.v_proj.weight" => Ok(format!("blk.{bid}.attn_v.weight")),
232            "self_attn.q_proj.bias" => Ok(format!("blk.{bid}.attn_q.bias")),
233            "self_attn.k_proj.bias" => Ok(format!("blk.{bid}.attn_k.bias")),
234            "self_attn.v_proj.bias" => Ok(format!("blk.{bid}.attn_v.bias")),
235            "self_attn.o_proj.weight" => Ok(format!("blk.{bid}.attn_output.weight")),
236            "mamba.in_proj.weight" => Ok(format!("blk.{bid}.ssm_in.weight")),
237            "mamba.conv1d.weight" => Ok(format!("blk.{bid}.ssm_conv1d.weight")),
238            "mamba.conv1d.bias" => Ok(format!("blk.{bid}.ssm_conv1d.bias")),
239            "mamba.dt_bias" => Ok(format!("blk.{bid}.ssm_dt.bias")),
240            "mamba.A_log" => Ok(format!("blk.{bid}.ssm_a")),
241            "mamba.D" => Ok(format!("blk.{bid}.ssm_d")),
242            "mamba.norm.weight" => Ok(format!("blk.{bid}.ssm_norm.weight")),
243            "mamba.out_proj.weight" => Ok(format!("blk.{bid}.ssm_out.weight")),
244            "self_attn.q_norm.weight" => Ok(format!("blk.{bid}.attn_q_norm.weight")),
245            "self_attn.k_norm.weight" => Ok(format!("blk.{bid}.attn_k_norm.weight")),
246            "self_attn.q_a_proj.weight" => Ok(format!("blk.{bid}.attn_q_a.weight")),
247            "self_attn.q_b_proj.weight" => Ok(format!("blk.{bid}.attn_q_b.weight")),
248            "self_attn.q_a_layernorm.weight" => Ok(format!("blk.{bid}.attn_q_a_norm.weight")),
249            "self_attn.kv_a_proj_with_mqa.weight" => Ok(format!("blk.{bid}.attn_kv_a_mqa.weight")),
250            "self_attn.kv_b_proj.weight" => Ok(format!("blk.{bid}.attn_kv_b.weight")),
251            "self_attn.kv_a_layernorm.weight" => Ok(format!("blk.{bid}.attn_kv_a_norm.weight")),
252            "attn_norm.weight" => Ok(format!("blk.{bid}.attn_norm.weight")),
253            "attn.wq_du.weight" => Ok(format!("blk.{bid}.attn_q.weight")),
254            "attn.wk_dv.weight" => Ok(format!("blk.{bid}.attn_k.weight")),
255            "attn.wv_dv.weight" => Ok(format!("blk.{bid}.attn_v.weight")),
256            "attn.wr_du.weight" => Ok(format!("blk.{bid}.attn_r.weight")),
257            "attn.wo_ud.weight" => Ok(format!("blk.{bid}.attn_output.weight")),
258            "attn.q_norm.weight" => Ok(format!("blk.{bid}.attn_q_norm.weight")),
259            "attn.k_norm.weight" => Ok(format!("blk.{bid}.attn_k_norm.weight")),
260            "attn.rel_logits_proj.proj" | "attn.rel_logits_proj.weight" => {
261                Ok(format!("blk.{bid}.attn_rel_proj.weight"))
262            }
263            "attn.k_sconv.weight" => Ok(format!("blk.{bid}.shortconv_k.weight")),
264            "attn.v_sconv.weight" => Ok(format!("blk.{bid}.shortconv_v.weight")),
265            "attn_sconv.weight" => Ok(format!("blk.{bid}.shortconv_attn.weight")),
266            "mlp_sconv.weight" => Ok(format!("blk.{bid}.shortconv_mlp.weight")),
267            "mlp_norm.weight" => Ok(format!("blk.{bid}.ffn_norm.weight")),
268            "mlp.w2_md.weight" => Ok(format!("blk.{bid}.ffn_down.weight")),
269            "mlp.global_scale" | "mlp.global_scale.weight" => {
270                Ok(format!("blk.{bid}.ffn_gscale.weight"))
271            }
272            "mlp.w13_dn.weight" => {
273                bail!("Inkling fused w13 tensor requires streaming deinterleave")
274            }
275            "self_attn.indexer.k_norm.weight" => Ok(format!("blk.{bid}.indexer.k_norm.weight")),
276            "self_attn.indexer.k_norm.bias" => Ok(format!("blk.{bid}.indexer.k_norm.bias")),
277            "self_attn.indexer.weights_proj.weight" => Ok(format!("blk.{bid}.indexer.proj.weight")),
278            "self_attn.indexer.wk.weight" => Ok(format!("blk.{bid}.indexer.attn_k.weight")),
279            "self_attn.indexer.wq_b.weight" => Ok(format!("blk.{bid}.indexer.attn_q_b.weight")),
280            "mlp.down_proj.weight" => Ok(format!("blk.{bid}.ffn_down.weight")),
281            "mlp.gate_proj.weight" => Ok(format!("blk.{bid}.ffn_gate.weight")),
282            "mlp.up_proj.weight" => Ok(format!("blk.{bid}.ffn_up.weight")),
283            "shared_mlp.output_linear.weight" => Ok(format!("blk.{bid}.ffn_down.weight")),
284            "mlp.gate.weight" => Ok(format!("blk.{bid}.ffn_gate_inp.weight")),
285            "mlp.shared_expert_gate" => Ok(format!("blk.{bid}.ffn_gate_inp_shexp.weight")),
286            "mlp.gate.e_score_correction_bias" => Ok(format!("blk.{bid}.exp_probs_b.bias")),
287            "mlp.shared_expert.down_proj.weight" => Ok(format!("blk.{bid}.ffn_down_shexp.weight")),
288            "mlp.shared_expert.gate_proj.weight" => Ok(format!("blk.{bid}.ffn_gate_shexp.weight")),
289            "mlp.shared_expert.up_proj.weight" => Ok(format!("blk.{bid}.ffn_up_shexp.weight")),
290            "mlp.shared_experts.down_proj.weight" => Ok(format!("blk.{bid}.ffn_down_shexp.weight")),
291            "mlp.shared_experts.gate_proj.weight" => Ok(format!("blk.{bid}.ffn_gate_shexp.weight")),
292            "mlp.shared_experts.up_proj.weight" => Ok(format!("blk.{bid}.ffn_up_shexp.weight")),
293            suffix if suffix.starts_with("mlp.experts.") => {
294                bail!(
295                    "expert source tensor {suffix} requires streaming expert merge before GGUF write"
296                )
297            }
298            suffix => bail!("no HF->GGUF mapping for layer tensor suffix {suffix:?}"),
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn maps_glm_moe_lite_direct_tensor_names() {
309        assert_eq!(
310            TensorNameMap::HfToGguf
311                .map_tensor_name("model.embed_tokens.weight")
312                .unwrap(),
313            "token_embd.weight"
314        );
315        assert_eq!(
316            TensorNameMap::HfToGguf
317                .map_tensor_name("model.layers.7.self_attn.kv_a_proj_with_mqa.weight")
318                .unwrap(),
319            "blk.7.attn_kv_a_mqa.weight"
320        );
321        assert_eq!(
322            TensorNameMap::HfToGguf
323                .map_tensor_name("model.layers.7.mlp.shared_experts.gate_proj.weight")
324                .unwrap(),
325            "blk.7.ffn_gate_shexp.weight"
326        );
327    }
328
329    #[test]
330    fn maps_glm_dsa_indexer_tensor_names() {
331        assert_eq!(
332            TensorNameMap::HfToGguf
333                .map_tensor_name("model.layers.7.self_attn.indexer.k_norm.weight")
334                .unwrap(),
335            "blk.7.indexer.k_norm.weight"
336        );
337        assert_eq!(
338            TensorNameMap::HfToGguf
339                .map_tensor_name("model.layers.7.self_attn.indexer.k_norm.bias")
340                .unwrap(),
341            "blk.7.indexer.k_norm.bias"
342        );
343        assert_eq!(
344            TensorNameMap::HfToGguf
345                .map_tensor_name("model.layers.7.self_attn.indexer.weights_proj.weight")
346                .unwrap(),
347            "blk.7.indexer.proj.weight"
348        );
349        assert_eq!(
350            TensorNameMap::HfToGguf
351                .map_tensor_name("model.layers.7.self_attn.indexer.wk.weight")
352                .unwrap(),
353            "blk.7.indexer.attn_k.weight"
354        );
355        assert_eq!(
356            TensorNameMap::HfToGguf
357                .map_tensor_name("model.layers.7.self_attn.indexer.wq_b.weight")
358                .unwrap(),
359            "blk.7.indexer.attn_q_b.weight"
360        );
361    }
362
363    #[test]
364    fn rejects_expert_source_tensors_until_merge_exists() {
365        let err = TensorNameMap::HfToGguf
366            .map_tensor_name("model.layers.1.mlp.experts.0.down_proj.weight")
367            .unwrap_err()
368            .to_string();
369
370        assert!(err.contains("requires streaming expert merge"));
371    }
372
373    #[test]
374    fn maps_qwen_dense_tensor_names() {
375        assert_eq!(
376            TensorNameMap::HfToGguf
377                .map_tensor_name("model.layers.3.self_attn.q_proj.weight")
378                .unwrap(),
379            "blk.3.attn_q.weight"
380        );
381        assert_eq!(
382            TensorNameMap::HfToGguf
383                .map_tensor_name("model.layers.3.self_attn.k_norm.weight")
384                .unwrap(),
385            "blk.3.attn_k_norm.weight"
386        );
387        assert_eq!(
388            TensorNameMap::HfToGguf
389                .map_tensor_name("model.layers.3.self_attn.v_proj.bias")
390                .unwrap(),
391            "blk.3.attn_v.bias"
392        );
393        assert_eq!(
394            TensorNameMap::HfToGguf
395                .map_tensor_name("model.layers.3.mlp.up_proj.weight")
396                .unwrap(),
397            "blk.3.ffn_up.weight"
398        );
399    }
400
401    #[test]
402    fn maps_qwen2_moe_shared_expert_tensor_names() {
403        assert_eq!(
404            TensorNameMap::HfToGguf
405                .map_tensor_name("model.layers.3.mlp.shared_expert_gate")
406                .unwrap(),
407            "blk.3.ffn_gate_inp_shexp.weight"
408        );
409        assert_eq!(
410            TensorNameMap::HfToGguf
411                .map_tensor_name("model.layers.3.mlp.shared_expert.gate_proj.weight")
412                .unwrap(),
413            "blk.3.ffn_gate_shexp.weight"
414        );
415        assert_eq!(
416            TensorNameMap::HfToGguf
417                .map_tensor_name("model.layers.3.mlp.shared_expert.down_proj.weight")
418                .unwrap(),
419            "blk.3.ffn_down_shexp.weight"
420        );
421        assert_eq!(
422            TensorNameMap::HfToGguf
423                .map_tensor_name("model.layers.3.mlp.shared_expert.up_proj.weight")
424                .unwrap(),
425            "blk.3.ffn_up_shexp.weight"
426        );
427    }
428
429    #[test]
430    fn recognizes_known_nextn_mtp_source_tensors() {
431        for name in [
432            "pre_projection",
433            "pre_projection.weight",
434            "post_projection",
435            "d2t",
436            "model.layers.47.eh_proj.weight",
437            "model.layers.47.embed_tokens.weight",
438            "model.layers.47.enorm.weight",
439            "model.layers.47.hnorm.weight",
440            "model.layers.47.shared_head.head.weight",
441            "model.layers.47.shared_head.norm.weight",
442        ] {
443            assert!(is_mtp_source_tensor(name), "{name}");
444        }
445        assert!(!is_mtp_source_tensor(
446            "model.layers.0.self_attn.q_proj.weight"
447        ));
448        assert!(!is_mtp_source_tensor("model.embed_tokens.weight"));
449    }
450
451    #[test]
452    fn maps_known_nextn_mtp_source_tensors() {
453        assert_eq!(
454            TensorNameMap::HfToGguf
455                .map_tensor_name("model.layers.47.eh_proj.weight")
456                .unwrap(),
457            "blk.47.nextn.eh_proj.weight"
458        );
459        assert_eq!(
460            TensorNameMap::HfToGguf
461                .map_tensor_name("model.layers.47.shared_head.norm.weight")
462                .unwrap(),
463            "blk.47.nextn.shared_head_norm.weight"
464        );
465        assert_eq!(
466            TensorNameMap::HfToGguf
467                .map_tensor_name("embed_tokens.weight")
468                .unwrap(),
469            "token_embd.weight"
470        );
471    }
472
473    #[test]
474    fn recognizes_and_maps_qwen_style_mtp_source_tensors() {
475        for name in [
476            "mtp.fc.weight",
477            "model.mtp.pre_fc_norm_embedding.weight",
478            "mtp.pre_fc_norm_hidden.weight",
479            "mtp.norm.weight",
480            "mtp.layers.0.self_attn.q_proj.weight",
481        ] {
482            assert!(is_mtp_source_tensor(name), "{name}");
483        }
484        assert_eq!(
485            TensorNameMap::HfToGgufWithMtp { layer_start: 32 }
486                .map_tensor_name("mtp.fc.weight")
487                .unwrap(),
488            "blk.32.nextn.eh_proj.weight"
489        );
490        assert_eq!(
491            TensorNameMap::HfToGgufWithMtp { layer_start: 32 }
492                .map_tensor_name("model.mtp.norm.weight")
493                .unwrap(),
494            "blk.32.nextn.shared_head_norm.weight"
495        );
496        assert_eq!(
497            TensorNameMap::HfToGgufWithMtp { layer_start: 32 }
498                .map_tensor_name("mtp.layers.1.self_attn.q_proj.weight")
499                .unwrap(),
500            "blk.33.attn_q.weight"
501        );
502    }
503
504    #[test]
505    fn recognizes_and_maps_inkling_mtp_tensors() {
506        let map = TensorNameMap::HfToGgufWithMtp { layer_start: 66 };
507        for (source, expected) in [
508            (
509                "model.mtp.layers.0.embed_norm.weight",
510                "blk.66.nextn.enorm.weight",
511            ),
512            (
513                "model.mtp.layers.1.hidden_norm.weight",
514                "blk.67.nextn.hnorm.weight",
515            ),
516            (
517                "model.mtp.layers.2.input_proj.weight",
518                "blk.68.nextn.eh_proj.weight",
519            ),
520            (
521                "model.mtp.layers.3.transformer_block.attn.wq_du.weight",
522                "blk.69.attn_q.weight",
523            ),
524            (
525                "model.mtp.layers.4.transformer_block.attn.rel_logits_proj.proj",
526                "blk.70.attn_rel_proj.weight",
527            ),
528            (
529                "model.mtp.layers.7.transformer_block.mlp.global_scale",
530                "blk.73.ffn_gscale.weight",
531            ),
532        ] {
533            assert!(is_mtp_source_tensor(source));
534            assert_eq!(map.map_tensor_name(source).unwrap(), expected);
535        }
536        assert_eq!(
537            map.map_tensor_name("model.llm.embed_norm.weight").unwrap(),
538            "token_embd_norm.weight"
539        );
540        assert!(is_shared_mtp_context_tensor("model.llm.unembed.weight"));
541        assert!(is_inkling_fused_w13(
542            "model.mtp.layers.0.transformer_block.mlp.w13_dn.weight"
543        ));
544    }
545
546    #[test]
547    fn rejects_inkling_mtp_layer_overflow() {
548        let error = TensorNameMap::HfToGgufWithMtp {
549            layer_start: u32::MAX,
550        }
551        .map_tensor_name("model.mtp.layers.1.embed_norm.weight")
552        .unwrap_err();
553
554        assert!(error.to_string().contains("MTP layer id overflow"));
555    }
556}