1use std::collections::HashMap;
10
11use anyhow::{Context, Result, bail, ensure};
12use safetensors::tensor::Dtype;
13
14use crate::mxfp4_e8m0::{dequant_nvfp4_e8m0_to_bf16, dequant_nvfp4_e8m0_to_f32};
15
16pub const PACKED_CLASSES: &[&str] = &[
18 "language_model.model.layers.*.block_sparse_moe.experts.*.w1.weight_packed",
19 "language_model.model.layers.*.block_sparse_moe.experts.*.w1.weight_scale",
20 "language_model.model.layers.*.block_sparse_moe.experts.*.w2.weight_packed",
21 "language_model.model.layers.*.block_sparse_moe.experts.*.w2.weight_scale",
22 "language_model.model.layers.*.block_sparse_moe.experts.*.w3.weight_packed",
23 "language_model.model.layers.*.block_sparse_moe.experts.*.w3.weight_scale",
24];
25
26pub fn allow_mxfp4() -> bool {
28 std::env::var("K3_ALLOW_MXFP4").as_deref() == Ok("1")
29}
30
31pub fn is_expert_packed(name: &str) -> bool {
32 name.contains("block_sparse_moe.experts.") && name.ends_with(".weight_packed")
33}
34
35pub fn is_expert_scale(name: &str) -> bool {
36 name.contains("block_sparse_moe.experts.") && name.ends_with(".weight_scale")
37}
38
39pub fn unpacked_name(packed_name: &str) -> Option<String> {
40 packed_name
41 .strip_suffix("weight_packed")
42 .map(|p| format!("{p}weight"))
43}
44
45pub fn scale_name(packed_name: &str) -> Option<String> {
46 packed_name
47 .strip_suffix("weight_packed")
48 .map(|p| format!("{p}weight_scale"))
49}
50
51pub fn unpack_expert(packed: &[u8], scale: &[u8], n: usize, k: usize) -> Result<Vec<f32>> {
53 dequant_nvfp4_e8m0_to_f32(packed, scale, n, k)
54}
55
56pub fn unpack_expert_bf16(packed: &[u8], scale: &[u8], n: usize, k: usize) -> Result<Vec<u16>> {
58 dequant_nvfp4_e8m0_to_bf16(packed, scale, n, k)
59}
60
61#[derive(Default)]
62pub(super) struct PackedSink {
63 packed: HashMap<String, (Vec<usize>, Vec<u8>)>,
64 scales: HashMap<String, (Vec<usize>, Vec<u8>)>,
65}
66
67impl PackedSink {
68 pub(super) fn take(
69 &mut self,
70 name: &str,
71 dtype: Dtype,
72 shape: &[usize],
73 data: &[u8],
74 ) -> Result<bool> {
75 if is_expert_packed(name) {
76 if !allow_mxfp4() {
77 bail!("S5 MXFP4 not this slice ({name})");
78 }
79 ensure!(
80 dtype == Dtype::U8,
81 "{name}: packed MXFP4 wants U8, got {dtype:?}"
82 );
83 self.packed
84 .insert(name.to_string(), (shape.to_vec(), data.to_vec()));
85 return Ok(true);
86 }
87 if is_expert_scale(name) {
88 if !allow_mxfp4() {
89 bail!("S5 MXFP4 not this slice ({name})");
90 }
91 ensure!(
92 dtype == Dtype::U8,
93 "{name}: E8M0 weight_scale wants U8, got {dtype:?}"
94 );
95 self.scales
96 .insert(name.to_string(), (shape.to_vec(), data.to_vec()));
97 return Ok(true);
98 }
99 Ok(false)
100 }
101
102 pub(super) fn finish(self, out: &mut HashMap<String, Vec<f32>>) -> Result<()> {
103 for (name, (shape, data)) in &self.packed {
104 let sname = scale_name(name).expect("weight_packed suffix");
105 let (_sshape, sdata) = self
106 .scales
107 .get(&sname)
108 .with_context(|| format!("{name}: missing DSV4-mapped E8M0 {sname}"))?;
109 let (n, k) = packed_nk(shape)?;
110 let vals = unpack_expert(data, sdata, n, k)?;
111 let dest = unpacked_name(name).expect("weight_packed suffix");
112 if out.insert(dest.clone(), vals).is_some() {
113 bail!("duplicate unpacked {dest}");
114 }
115 }
116 Ok(())
117 }
118}
119
120fn packed_nk(shape: &[usize]) -> Result<(usize, usize)> {
121 match *shape {
122 [n, k2] => Ok((n, k2.checked_mul(2).expect("k packed"))),
123 [len] => Ok((1, len.checked_mul(2).expect("k packed 1d"))),
124 _ => bail!("MXFP4 packed shape {shape:?}: want [n, k/2] or [k/2]"),
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::kimi_k3::cpu_weights::K3CpuModel;
132 use crate::numeric::f32_to_bf16;
133 use safetensors::tensor::{TensorView, serialize};
134 use std::path::PathBuf;
135 use std::process::Command;
136
137 const TINY_CFG: &str = r#"{
138 "model_type": "kimi_k3",
139 "text_config": {
140 "activation_situ_beta": 4.0,
141 "activation_situ_linear_beta": 25.0,
142 "attn_res_block_size": 2,
143 "first_k_dense_replace": 1,
144 "head_dim": 2,
145 "hidden_act": "situ",
146 "hidden_size": 4,
147 "intermediate_size": 4,
148 "kv_lora_rank": 2,
149 "latent_moe_use_norm": true,
150 "linear_attn_config": {
151 "full_attn_layers": [2],
152 "head_dim": 2,
153 "kda_layers": [1],
154 "num_heads": 1,
155 "short_conv_kernel_size": 4,
156 "use_full_rank_gate": true
157 },
158 "mla_use_nope": true,
159 "mla_use_output_gate": true,
160 "model_type": "kimi_linear",
161 "moe_intermediate_size": 4,
162 "moe_renormalize": true,
163 "moe_router_activation_func": "sigmoid",
164 "num_attention_heads": 1,
165 "num_experts": 2,
166 "num_experts_per_token": 1,
167 "num_hidden_layers": 2,
168 "num_shared_experts": 1,
169 "q_lora_rank": 4,
170 "qk_nope_head_dim": 2,
171 "qk_rope_head_dim": 2,
172 "rms_norm_eps": 1e-5,
173 "rope_theta": 10000.0,
174 "routed_expert_hidden_size": 4,
175 "topk_method": "noaux_tc",
176 "v_head_dim": 2,
177 "vocab_size": 8
178 }
179 }"#;
180
181 fn pack_nibble(n: usize, k: usize, nib: u8) -> (Vec<u8>, Vec<usize>) {
182 let total = n * k;
183 let mut packed = vec![0u8; total / 2];
184 for i in 0..total {
185 if i.is_multiple_of(2) {
186 packed[i / 2] |= nib & 0x0F;
187 } else {
188 packed[i / 2] |= (nib & 0x0F) << 4;
189 }
190 }
191 (packed, vec![n, k / 2])
192 }
193
194 #[test]
195 fn packed_classes_match_official_tsv() {
196 let tsv = include_str!("../../../../docs/k3/official-weight-classes.tsv");
197 let mut from_tsv: Vec<&str> = tsv
198 .lines()
199 .filter_map(|line| {
200 let class = line.split('\t').next_back()?;
201 (class.contains("experts.")
202 && (class.ends_with("weight_packed") || class.ends_with("weight_scale")))
203 .then_some(class)
204 })
205 .collect();
206 from_tsv.sort_unstable();
207 let mut ours = PACKED_CLASSES.to_vec();
208 ours.sort_unstable();
209 assert_eq!(ours, from_tsv);
210 }
211
212 #[test]
213 fn k3_names_map_to_dsv4_weight_and_scale() {
214 let packed = "language_model.model.layers.12.block_sparse_moe.experts.7.w1.weight_packed";
215 assert_eq!(
216 unpacked_name(packed).as_deref(),
217 Some("language_model.model.layers.12.block_sparse_moe.experts.7.w1.weight")
218 );
219 assert_eq!(
220 scale_name(packed).as_deref(),
221 Some("language_model.model.layers.12.block_sparse_moe.experts.7.w1.weight_scale")
222 );
223 assert_ne!(
225 scale_name(packed).unwrap().as_str().rsplit('.').next(),
226 Some("scale")
227 );
228 }
229
230 #[test]
231 fn synthetic_packed_round_trips_dsv4_e8m0() {
232 let n = 1;
234 let k = crate::mxfp4_e8m0::GROUP_SIZE;
235 let (packed, _) = pack_nibble(n, k, 2);
236 let scales = vec![127u8];
237 let got = unpack_expert(&packed, &scales, n, k).unwrap();
238 assert_eq!(got, vec![1.0; k]);
239 let bf = unpack_expert_bf16(&packed, &scales, n, k).unwrap();
240 let want: Vec<u16> = vec![1.0; k].into_iter().map(f32_to_bf16).collect();
241 assert_eq!(bf, want);
242 }
243
244 #[test]
245 fn flipped_e8m0_scale_diverges_from_bf16_reference() {
246 let n = 1;
247 let k = crate::mxfp4_e8m0::GROUP_SIZE;
248 let (packed, _) = pack_nibble(n, k, 6); let good_scale = vec![127u8];
250 let mut bad_scale = good_scale.clone();
251 bad_scale[0] ^= 0x01; let good = unpack_expert_bf16(&packed, &good_scale, n, k).unwrap();
253 let bad = unpack_expert_bf16(&packed, &bad_scale, n, k).unwrap();
254 let ref_bf: Vec<u16> = unpack_expert(&packed, &good_scale, n, k)
255 .unwrap()
256 .into_iter()
257 .map(f32_to_bf16)
258 .collect();
259 assert_eq!(good, ref_bf, "good unpack is the BF16 reference");
260 assert_ne!(bad, ref_bf, "flipped E8M0 scale must diverge");
261 }
262
263 #[test]
264 fn from_pretrained_allow_mxfp4_skips_s5_bail() {
265 const THIS: &str = "kimi_k3::mxfp4::tests::from_pretrained_allow_mxfp4_skips_s5_bail";
266 const MARKER: &str = "K3_MXFP4_ALLOW_CHILD";
267 if std::env::var_os(MARKER).is_some() {
268 let dir = scratch_packed_only();
269 let err = K3CpuModel::from_pretrained(&dir).unwrap_err().to_string();
270 let _ = std::fs::remove_dir_all(&dir);
271 assert!(
272 !err.contains("S5 MXFP4 not this slice"),
273 "K3_ALLOW_MXFP4=1 must not use the S5 refuse: {err}"
274 );
275 return;
276 }
277 let output = Command::new(std::env::current_exe().unwrap())
278 .args(["--exact", THIS])
279 .env(MARKER, "1")
280 .env("K3_ALLOW_MXFP4", "1")
281 .output()
282 .unwrap();
283 assert!(
284 output.status.success(),
285 "ALLOW child failed:\n{}",
286 String::from_utf8_lossy(&output.stdout)
287 );
288 }
289
290 fn scratch_packed_only() -> PathBuf {
291 let nanos = std::time::SystemTime::now()
292 .duration_since(std::time::UNIX_EPOCH)
293 .unwrap()
294 .as_nanos();
295 let dir =
296 std::env::temp_dir().join(format!("k3-mxfp4-allow-{}-{nanos}", std::process::id()));
297 std::fs::create_dir_all(&dir).unwrap();
298 std::fs::write(dir.join("config.json"), TINY_CFG).unwrap();
299 let (packed, _) = pack_nibble(1, 32, 2);
300 let scale = vec![127u8];
301 let pv = TensorView::new(Dtype::U8, vec![1, 16], &packed).unwrap();
302 let sv = TensorView::new(Dtype::U8, vec![1], &scale).unwrap();
303 let bytes = serialize(
304 [
305 (
306 "language_model.model.layers.1.block_sparse_moe.experts.0.w1.weight_packed"
307 .to_string(),
308 pv,
309 ),
310 (
311 "language_model.model.layers.1.block_sparse_moe.experts.0.w1.weight_scale"
312 .to_string(),
313 sv,
314 ),
315 ],
316 None,
317 )
318 .unwrap();
319 std::fs::write(dir.join("model.safetensors"), bytes).unwrap();
320 dir
321 }
322}