spark_model/
mtp_layout.rs1use atlas_core::config::ModelConfig;
21use spark_runtime::weights::WeightStore;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum MtpLayout {
26 MtpPrefix,
29 ExtraLayer { first: usize, count: usize },
33}
34
35fn layer_index(name: &str) -> Option<usize> {
41 let tail = if let Some(pos) = name.find(".layers.") {
42 &name[pos + ".layers.".len()..]
43 } else {
44 name.strip_prefix("layers.")?
45 };
46 let end = tail.find('.')?;
47 tail[..end].parse().ok()
48}
49
50pub fn detect<'a>(
55 names: impl Iterator<Item = &'a str>,
56 num_hidden_layers: usize,
57) -> Option<MtpLayout> {
58 let mut extras: Vec<usize> = Vec::new();
59 let mut has_prefix = false;
60 for n in names {
61 if n.starts_with("mtp.") {
62 has_prefix = true;
63 } else if let Some(idx) = layer_index(n)
64 && idx >= num_hidden_layers
65 {
66 extras.push(idx);
67 }
68 }
69 if has_prefix {
70 return Some(MtpLayout::MtpPrefix);
71 }
72 if extras.is_empty() {
73 return None;
74 }
75 extras.sort_unstable();
76 extras.dedup();
77 Some(MtpLayout::ExtraLayer {
78 first: extras[0],
79 count: extras.len(),
80 })
81}
82
83pub fn detect_in_store(store: &WeightStore, config: &ModelConfig) -> Option<MtpLayout> {
89 detect(store.names(), config.num_hidden_layers)
90}
91
92#[cfg(test)]
93mod tests {
94 use super::{MtpLayout, detect, layer_index};
95
96 #[test]
97 fn glm5_next_layer_45_block_is_detected() {
98 let names = [
101 "model.language_model.layers.44.self_attn.q_proj.weight",
102 "model.language_model.layers.45.eh_proj.weight",
103 "model.language_model.layers.45.shared_head.norm.weight",
104 "model.language_model.layers.45.mlp.experts.7.down_proj.weight",
105 "lm_head.weight",
106 ];
107 assert_eq!(
108 detect(names.iter().copied(), 45),
109 Some(MtpLayout::ExtraLayer {
110 first: 45,
111 count: 1
112 }),
113 );
114 }
115
116 #[test]
117 fn qwen_mtp_prefix_still_detected() {
118 let names = ["model.layers.0.self_attn.q_proj.weight", "mtp.fc.weight"];
119 assert_eq!(
120 detect(names.iter().copied(), 48),
121 Some(MtpLayout::MtpPrefix)
122 );
123 }
124
125 #[test]
126 fn deepseek_multi_module_prefix_still_detected() {
127 let names = ["mtp.0.self_attn.q_proj.weight", "mtp.1.eh_proj.weight"];
128 assert_eq!(
129 detect(names.iter().copied(), 61),
130 Some(MtpLayout::MtpPrefix)
131 );
132 }
133
134 #[test]
135 fn deepseek_v3_nextn_extra_layer_detected() {
136 let names = [
137 "model.layers.60.mlp.gate.weight",
138 "model.layers.61.eh_proj.weight",
139 ];
140 assert_eq!(
141 detect(names.iter().copied(), 61),
142 Some(MtpLayout::ExtraLayer {
143 first: 61,
144 count: 1
145 }),
146 );
147 }
148
149 #[test]
150 fn plain_checkpoint_has_no_mtp() {
151 let names = [
152 "model.layers.0.self_attn.q_proj.weight",
153 "model.layers.47.mlp.down_proj.weight",
154 "model.embed_tokens.weight",
155 "lm_head.weight",
156 ];
157 assert_eq!(detect(names.iter().copied(), 48), None);
158 }
159
160 #[test]
161 fn vision_tower_blocks_are_not_mistaken_for_mtp() {
162 let names = [
165 "model.language_model.layers.0.self_attn.q_proj.weight",
166 "model.visual.blocks.23.attn.proj.weight",
167 "model.visual.merger.proj.weight",
168 ];
169 assert_eq!(detect(names.iter().copied(), 45), None);
170 }
171
172 #[test]
173 fn multi_extra_layers_are_counted() {
174 let names = [
175 "model.layers.61.eh_proj.weight",
176 "model.layers.62.eh_proj.weight",
177 ];
178 assert_eq!(
179 detect(names.iter().copied(), 61),
180 Some(MtpLayout::ExtraLayer {
181 first: 61,
182 count: 2
183 }),
184 );
185 }
186
187 #[test]
188 fn unprefixed_mistral_layer_names_parse() {
189 assert_eq!(layer_index("layers.3.attention.wq.weight"), Some(3));
190 assert_eq!(
191 layer_index("model.language_model.layers.45.eh_proj.weight"),
192 Some(45)
193 );
194 assert_eq!(layer_index("lm_head.weight"), None);
195 assert_eq!(layer_index("model.visual.blocks.2.attn.proj.weight"), None);
196 }
197}