1use anyhow::{Result, bail};
12use metrale_cache::kv_cache::KvCacheDtype;
13use metrale_config::ModelConfig;
14use metrale_gpu_runtime::gpu::GpuBackend;
15use metrale_model_weights::weights::WeightStore;
16
17use metrale_model_arch::weight_loader::ModelWeightLoader;
18use metrale_model_layers::layer::TransformerLayer;
19use metrale_model_layers::weight_map::DenseWeight;
20
21mod bf16;
22mod classes;
23mod dry_run;
24mod mxfp4;
25mod tp;
26
27pub use dry_run::{KimiK3DryRun, dry_run_index_json, dry_run_weight_map};
28
29pub struct KimiK3WeightLoader;
30
31impl KimiK3WeightLoader {
32 pub fn dry_run(index_json: &str, language_model_only: bool) -> Result<KimiK3DryRun> {
33 dry_run::dry_run_index_json(index_json, language_model_only)
34 }
35}
36
37pub fn refuse_mxfp4(store: &WeightStore) -> Result<()> {
40 if store.names().any(|n| n.contains("weight_packed"))
41 && !metrale_model_weights::kimi_k3_host::mxfp4::allow_mxfp4()
42 {
43 bail!("S5 MXFP4 not this slice");
44 }
45 Ok(())
46}
47
48impl ModelWeightLoader for KimiK3WeightLoader {
49 fn supports_tp(&self) -> bool {
50 true
53 }
54
55 fn binds_vision_encoder(&self) -> bool {
56 false
57 }
58
59 fn load_layers(
60 &self,
61 store: &WeightStore,
62 config: &ModelConfig,
63 gpu: &dyn GpuBackend,
64 _layer_kv_dtypes: &[KvCacheDtype],
65 ) -> Result<Vec<Box<dyn TransformerLayer>>> {
66 refuse_mxfp4(store)?;
67 bf16::load_layers(store, config, gpu)
68 }
69
70 fn load_embedding(
71 &self,
72 store: &WeightStore,
73 config: &ModelConfig,
74 gpu: &dyn GpuBackend,
75 ) -> Result<DenseWeight> {
76 refuse_mxfp4(store)?;
77 bf16::load_embedding(store, config, gpu)
78 }
79
80 fn load_final_norm(
81 &self,
82 store: &WeightStore,
83 config: &ModelConfig,
84 gpu: &dyn GpuBackend,
85 ) -> Result<DenseWeight> {
86 refuse_mxfp4(store)?;
87 bf16::load_final_norm(store, config, gpu)
88 }
89
90 fn load_lm_head(
91 &self,
92 store: &WeightStore,
93 config: &ModelConfig,
94 gpu: &dyn GpuBackend,
95 ) -> Result<DenseWeight> {
96 refuse_mxfp4(store)?;
97 bf16::load_lm_head(store, config, gpu)
98 }
99
100 fn load_mtp_weights(
101 &self,
102 _store: &WeightStore,
103 _config: &ModelConfig,
104 _gpu: &dyn GpuBackend,
105 ) -> Result<Option<metrale_model_layers::weight_map::MtpWeights>> {
106 Ok(None)
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::factory::loader_for_config;
114 use metrale_config::parse_config;
115 use metrale_gpu_runtime::gpu::GpuBackend;
116 use metrale_model_weights::weights::{WeightDtype, WeightTensor};
117 use std::collections::HashMap;
118 use std::process::Command;
119
120 #[test]
121 fn kimi_k3_supports_tp() {
122 assert!(KimiK3WeightLoader.supports_tp());
123 }
124
125 #[test]
126 fn kimi_k3_does_not_probe_nvfp4_tgemm() {
127 assert!(
128 !metrale_model_layers::layers::tgemm_probe_ok("kimi_k3"),
129 "BF16/FP32 twin must not look up w4a16_gemm_t_p3"
130 );
131 assert!(metrale_model_layers::layers::tgemm_probe_ok("qwen3_5_moe"));
132 }
133
134 #[test]
135 fn loader_for_config_kimi_k3() {
136 let mut config = ModelConfig::qwen3_next_80b_nvfp4();
137 for ty in ["kimi_k3", "kimi_linear", "Kimi-K3"] {
138 config.model_type = ty.to_string();
139 let loader = loader_for_config(&config).expect(ty);
140 let store = WeightStore::empty();
141 let gpu = metrale_gpu_runtime::gpu::mock::MockGpuBackend::new();
142 let err = match loader.load_layers(&store, &config, &gpu, &[]) {
143 Ok(_) => panic!("{ty}: empty store must not bind"),
144 Err(e) => e.to_string(),
145 };
146 assert!(
147 err.contains("not found") || err.contains("S5 MXFP4"),
148 "{ty}: {err}"
149 );
150 assert!(!err.contains("K3-WIP"), "{ty}: stale WIP bail: {err}");
151 }
152 }
153
154 #[test]
155 fn load_bails_on_mxfp4_packed() {
156 let mut config = ModelConfig::qwen3_next_80b_nvfp4();
157 config.model_type = "kimi_k3".into();
158 let gpu = metrale_gpu_runtime::gpu::mock::MockGpuBackend::new();
159 let ptr = gpu.alloc(4).unwrap();
160 let store = WeightStore::from_map(HashMap::from([(
161 "language_model.model.layers.1.block_sparse_moe.experts.0.w1.weight_packed".to_string(),
162 WeightTensor {
163 ptr,
164 shape: vec![2, 2],
165 dtype: WeightDtype::UInt8,
166 },
167 )]));
168 let loader = KimiK3WeightLoader;
169 let err = match loader.load_layers(&store, &config, &gpu, &[]) {
170 Ok(_) => panic!("packed MXFP4 must not bind layers"),
171 Err(e) => e.to_string(),
172 };
173 assert!(err.contains("S5 MXFP4 not this slice"), "{err}");
174 let err = match loader.load_embedding(&store, &config, &gpu) {
175 Ok(_) => panic!("packed MXFP4 must not bind embed"),
176 Err(e) => e.to_string(),
177 };
178 assert!(err.contains("S5 MXFP4 not this slice"), "{err}");
179 }
180
181 #[test]
182 fn packed_tp_refuses_before_weight_binding() {
183 let mut config = ModelConfig::qwen3_next_80b_nvfp4();
184 config.tp_world_size = 8;
185 let gpu = metrale_gpu_runtime::gpu::mock::MockGpuBackend::new();
186 let store = WeightStore::from_map(HashMap::from([(
187 "language_model.model.layers.1.block_sparse_moe.experts.0.w1.weight_packed".into(),
188 WeightTensor {
189 ptr: gpu.alloc(16).unwrap(),
190 shape: vec![1, 16],
191 dtype: WeightDtype::UInt8,
192 },
193 )]));
194 let err = match bf16::load_layers(&store, &config, &gpu) {
195 Ok(_) => panic!("packed TP must refuse an unmarked store"),
196 Err(err) => err.to_string(),
197 };
198 assert!(err.contains("does not slice packed MXFP4"), "{err}");
199 }
200
201 #[test]
202 fn load_allow_mxfp4_lands_packed_experts() {
203 let this = format!(
204 "{}::load_allow_mxfp4_lands_packed_experts",
205 module_path!()
206 .split_once("::")
207 .map_or(module_path!(), |(_, p)| p)
208 );
209 const MARKER: &str = "K3_MXFP4_GPU_ALLOW_CHILD";
210 if std::env::var_os(MARKER).is_some() {
211 const TWIN: &str = include_str!("../../../docs/k3/fixtures/Kimi-K3-0.40B-config.json");
212 let mut config = parse_config(TWIN).expect("0.40B twin");
213 config.tp_world_size = 1;
214 let gpu = metrale_gpu_runtime::gpu::mock::MockGpuBackend::new();
215 let graph = metrale_model_weights::kimi_k3_host::K3Graph::from_config(&config);
216 let mut map = HashMap::new();
217 let mut put = |name: String, shape: Vec<usize>, dtype: WeightDtype| {
218 let n = shape.iter().product::<usize>().max(1);
219 let ptr = gpu.alloc(n.max(4)).unwrap();
220 map.insert(name, WeightTensor { ptr, shape, dtype });
221 };
222 put(
223 bf16::text_key(&config, "model.embed_tokens.weight"),
224 vec![2],
225 WeightDtype::BF16,
226 );
227 put(
228 bf16::text_key(&config, "model.norm.weight"),
229 vec![2],
230 WeightDtype::BF16,
231 );
232 put(
233 bf16::text_key(&config, "model.output_attn_res_proj.weight"),
234 vec![2],
235 WeightDtype::BF16,
236 );
237 put(
238 bf16::text_key(&config, "model.output_attn_res_norm.weight"),
239 vec![2],
240 WeightDtype::BF16,
241 );
242 put(
243 bf16::text_key(&config, "lm_head.weight"),
244 vec![2],
245 WeightDtype::BF16,
246 );
247 let packed_w1 = bf16::text_key(
248 &config,
249 "model.layers.1.block_sparse_moe.experts.0.w1.weight",
250 );
251 for spec in &graph.layers {
252 for k in bf16::layer_keys(
253 &config,
254 spec.index,
255 spec.mixer,
256 spec.mlp,
257 config.num_experts,
258 ) {
259 if k == packed_w1 {
260 continue;
261 }
262 put(k, vec![2], WeightDtype::BF16);
263 }
264 }
265 let prefix = packed_w1.trim_end_matches(".weight");
266 put(
267 format!("{prefix}.weight_packed"),
268 vec![config.moe_intermediate_size, config.moe_latent_size / 2],
269 WeightDtype::UInt8,
270 );
271 put(
272 format!("{prefix}.weight_scale"),
273 vec![config.moe_intermediate_size, config.moe_latent_size / 32],
274 WeightDtype::UInt8,
275 );
276 let store = WeightStore::from_map(map);
277 refuse_mxfp4(&store).expect("K3_ALLOW_MXFP4=1 must skip S5 refuse");
278 let loader = KimiK3WeightLoader;
279 let layers = loader
280 .load_layers(&store, &config, &gpu, &[])
281 .expect("packed expert must land on DSV4 E8M0");
282 assert_eq!(layers.len(), 8);
283 return;
284 }
285 let output = Command::new(std::env::current_exe().unwrap())
286 .args(["--exact", this.as_str()])
287 .env(MARKER, "1")
288 .env("K3_ALLOW_MXFP4", "1")
289 .output()
290 .unwrap();
291 assert!(
292 output.status.success(),
293 "ALLOW child failed:\n{}\n{}",
294 String::from_utf8_lossy(&output.stdout),
295 String::from_utf8_lossy(&output.stderr)
296 );
297 assert!(
298 String::from_utf8_lossy(&output.stdout).contains("1 passed"),
299 "the child must run exactly this test: {}",
300 String::from_utf8_lossy(&output.stdout)
301 );
302 }
303
304 #[test]
305 fn load_bf16_twin_binds_layers() {
306 const TWIN: &str = include_str!("../../../docs/k3/fixtures/Kimi-K3-0.40B-config.json");
307 let config = parse_config(TWIN).expect("0.40B twin");
308 let gpu = metrale_gpu_runtime::gpu::mock::MockGpuBackend::new();
309 let graph = metrale_model_weights::kimi_k3_host::K3Graph::from_config(&config);
310 let mut map = HashMap::new();
311 let mut put = |name: String| {
312 let ptr = gpu.alloc(4).unwrap();
313 map.insert(
314 name,
315 WeightTensor {
316 ptr,
317 shape: vec![2],
318 dtype: WeightDtype::BF16,
319 },
320 );
321 };
322 put(bf16::text_key(&config, "model.embed_tokens.weight"));
323 put(bf16::text_key(&config, "model.norm.weight"));
324 put(bf16::text_key(&config, "model.output_attn_res_proj.weight"));
325 put(bf16::text_key(&config, "model.output_attn_res_norm.weight"));
326 put(bf16::text_key(&config, "lm_head.weight"));
327 for spec in &graph.layers {
328 for k in bf16::layer_keys(
329 &config,
330 spec.index,
331 spec.mixer,
332 spec.mlp,
333 config.num_experts,
334 ) {
335 put(k);
336 }
337 }
338 let store = WeightStore::from_map(map);
339 let loader = KimiK3WeightLoader;
340 let layers = loader.load_layers(&store, &config, &gpu, &[]).unwrap();
341 assert_eq!(layers.len(), 8);
342 for layer in &layers {
343 assert!(layer.decode_graph_unsupported());
344 assert!(layer.decode_multi_seq_unsupported());
345 assert!(layer.decode_verify_multi_unsupported());
346 assert!(layer.decode_rollback_unsupported());
347 assert!(layer.has_aux_state());
348 }
349 loader.load_embedding(&store, &config, &gpu).unwrap();
350 loader.load_final_norm(&store, &config, &gpu).unwrap();
351 loader.load_lm_head(&store, &config, &gpu).unwrap();
352 }
353}