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