1use std::collections::HashMap;
14
15use anyhow::{Context, Result, bail, ensure};
16use half::bf16;
17use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
18use metrale_model_weights::kimi_k3_host::{LatentMoeConfig, situ_glu_vec};
19
20use metrale_model_layers::layers::ops::moe_w4a16_grouped_gemm_ptrtable;
21use metrale_model_layers::weight_map::QuantizedWeight;
22
23pub const MODULE: &str = "moe_w4a16";
27pub const PTRTABLE_E8M0: &str = "moe_w4a16_grouped_gemm_ptrtable_e8m0";
28pub const E8M0_ENTRY: &str = PTRTABLE_E8M0;
29
30#[derive(Clone, Copy, Debug)]
31pub struct K3MoeGemmKernels {
32 pub ptrtable: KernelHandle,
33}
34
35impl K3MoeGemmKernels {
36 pub fn resolve(gpu: &dyn GpuBackend) -> Result<Self> {
37 Ok(Self {
38 ptrtable: gpu.kernel(MODULE, PTRTABLE_E8M0).with_context(|| {
39 format!(
40 "K3 MXFP4: {MODULE}::{PTRTABLE_E8M0} missing; \
41 packed experts cannot silently run host F32"
42 )
43 })?,
44 })
45 }
46}
47
48#[allow(clippy::too_many_arguments)]
52pub fn launch_k3_moe_e8m0_ptrtable(
53 gpu: &dyn GpuBackend,
54 kernel: KernelHandle,
55 a: DevicePtr,
56 packed: &[QuantizedWeight],
57 c: DevicePtr,
58 expert_offsets: DevicePtr,
59 sorted_token_ids: DevicePtr,
60 num_experts: u32,
61 n_out: u32,
62 k: u32,
63 stream: u64,
64) -> Result<()> {
65 ensure!(
66 packed.len() == num_experts as usize && num_experts > 0,
67 "K3 MXFP4: ptrtable length {} != num_experts {num_experts}",
68 packed.len()
69 );
70 let mut hold = Vec::new();
71 let run = (|| {
72 let (packed_ptrs, scale_ptrs, scale2_vals) = upload_ptr_table(gpu, packed, &mut hold)?;
73 moe_w4a16_grouped_gemm_ptrtable(
74 gpu,
75 kernel,
76 a,
77 packed_ptrs,
78 scale_ptrs,
79 scale2_vals,
80 c,
81 expert_offsets,
82 sorted_token_ids,
83 num_experts,
84 n_out,
85 k,
86 1,
87 stream,
88 )
89 .context("moe_w4a16_grouped_gemm_ptrtable_e8m0")
90 })();
91 for p in hold {
92 let _ = gpu.free(p);
93 }
94 run
95}
96
97#[allow(clippy::too_many_arguments)]
103pub fn launch_k3_latent_moe_experts(
104 gpu: &dyn GpuBackend,
105 kernels: &K3MoeGemmKernels,
106 packed: &[(String, QuantizedWeight)],
107 latent: &[f32],
108 ids: &[usize],
109 mix_w: &[f32],
110 cfg: &LatentMoeConfig,
111 stream: u64,
112) -> Result<Vec<f32>> {
113 ensure!(
114 latent.len() == cfg.latent,
115 "K3 MXFP4: latent {} != {}",
116 latent.len(),
117 cfg.latent
118 );
119 ensure!(ids.len() == mix_w.len(), "K3 MXFP4: ids/weights rank");
120 if ids.is_empty() {
121 return Ok(vec![0.0; cfg.latent]);
122 }
123 let table = index_packed(packed)?;
124 let w1 = gather_proj(&table, ids, 0, "w1")?;
125 let w2 = gather_proj(&table, ids, 1, "w2")?;
126 let w3 = gather_proj(&table, ids, 2, "w3")?;
127 let m = ids.len();
128 let gate_up_weights: Vec<_> = w1.into_iter().chain(w3).collect();
132 let gate_up_input: Vec<f32> = latent
133 .iter()
134 .copied()
135 .cycle()
136 .take(2 * m * cfg.latent)
137 .collect();
138 let gate_up = gemm_rows(
139 gpu,
140 kernels,
141 &gate_up_input,
142 2 * m,
143 &gate_up_weights,
144 cfg.expert_hidden as u32,
145 cfg.latent as u32,
146 stream,
147 )?;
148 let (gate, up) = gate_up.split_at(m * cfg.expert_hidden);
149 let mut mid = Vec::with_capacity(m * cfg.expert_hidden);
150 for e in 0..m {
151 let g = &gate[e * cfg.expert_hidden..(e + 1) * cfg.expert_hidden];
152 let u = &up[e * cfg.expert_hidden..(e + 1) * cfg.expert_hidden];
153 mid.extend(situ_glu_vec(g, u, cfg.situ_beta, cfg.situ_linear_beta));
154 }
155 let down_rows = gemm_rows(
156 gpu,
157 kernels,
158 &mid,
159 m,
160 &w2,
161 cfg.latent as u32,
162 cfg.expert_hidden as u32,
163 stream,
164 )?;
165 let mut mixed = vec![0.0f32; cfg.latent];
166 for (e, &w) in mix_w.iter().enumerate() {
167 let y = &down_rows[e * cfg.latent..(e + 1) * cfg.latent];
168 for (acc, yy) in mixed.iter_mut().zip(y) {
169 *acc += w * *yy;
170 }
171 }
172 Ok(mixed)
173}
174
175fn gemm_rows(
176 gpu: &dyn GpuBackend,
177 kernels: &K3MoeGemmKernels,
178 a_f32: &[f32],
179 m: usize,
180 packed: &[QuantizedWeight],
181 n_out: u32,
182 k: u32,
183 stream: u64,
184) -> Result<Vec<f32>> {
185 ensure!(
186 a_f32.len() == m * k as usize,
187 "K3 MXFP4: A {} vs {m}x{k}",
188 a_f32.len()
189 );
190 let mut hold = Vec::new();
191 let run = (|| {
192 let a = up_bf16(gpu, a_f32, &mut hold)?;
193 let c = gpu.alloc((m * n_out as usize * 2).max(1))?;
194 hold.push(c);
195 let off: Vec<i32> = (0..=m as i32).collect();
196 let ids: Vec<i32> = (0..m as i32).collect();
197 let offsets = up_i32(gpu, &off, &mut hold)?;
198 let sorted = up_i32(gpu, &ids, &mut hold)?;
199 launch_k3_moe_e8m0_ptrtable(
200 gpu,
201 kernels.ptrtable,
202 a,
203 packed,
204 c,
205 offsets,
206 sorted,
207 m as u32,
208 n_out,
209 k,
210 stream,
211 )?;
212 gpu.synchronize(stream)?;
213 let mut raw = vec![0u8; m * n_out as usize * 2];
214 gpu.copy_d2h(c, &mut raw)?;
215 Ok(bf16_to_f32(&raw))
216 })();
217 for p in hold {
218 let _ = gpu.free(p);
219 }
220 run
221}
222
223fn upload_ptr_table(
224 gpu: &dyn GpuBackend,
225 packed: &[QuantizedWeight],
226 hold: &mut Vec<DevicePtr>,
227) -> Result<(DevicePtr, DevicePtr, DevicePtr)> {
228 let n = packed.len();
229 let packed_bytes: Vec<u8> = packed
230 .iter()
231 .flat_map(|w| w.weight.0.to_le_bytes())
232 .collect();
233 let scale_bytes: Vec<u8> = packed
234 .iter()
235 .flat_map(|w| w.weight_scale.0.to_le_bytes())
236 .collect();
237 let scale2_bytes: Vec<u8> = packed
238 .iter()
239 .flat_map(|w| w.weight_scale_2.to_le_bytes())
240 .collect();
241 let packed_ptrs = gpu.alloc((n * 8).max(1))?;
242 hold.push(packed_ptrs);
243 gpu.copy_h2d(&packed_bytes, packed_ptrs)?;
244 let scale_ptrs = gpu.alloc((n * 8).max(1))?;
245 hold.push(scale_ptrs);
246 gpu.copy_h2d(&scale_bytes, scale_ptrs)?;
247 let scale2_vals = gpu.alloc((n * 4).max(1))?;
248 hold.push(scale2_vals);
249 gpu.copy_h2d(&scale2_bytes, scale2_vals)?;
250 Ok((packed_ptrs, scale_ptrs, scale2_vals))
251}
252
253fn index_packed(
254 packed: &[(String, QuantizedWeight)],
255) -> Result<HashMap<(usize, usize), QuantizedWeight>> {
256 let mut t = HashMap::new();
257 for (prefix, w) in packed {
258 let (id, proj) = parse_expert_proj(prefix)?;
259 if t.insert((id, proj), *w).is_some() {
260 bail!("K3 MXFP4: duplicate packed {prefix}");
261 }
262 }
263 Ok(t)
264}
265
266fn parse_expert_proj(prefix: &str) -> Result<(usize, usize)> {
267 let (head, proj) = prefix
268 .rsplit_once('.')
269 .with_context(|| format!("K3 MXFP4: expert prefix {prefix}"))?;
270 let proj_i = match proj {
271 "w1" => 0,
272 "w2" => 1,
273 "w3" => 2,
274 _ => bail!("K3 MXFP4: expected w1|w2|w3 in {prefix}"),
275 };
276 let id_s = head
277 .rsplit_once(".experts.")
278 .map(|(_, id)| id)
279 .with_context(|| format!("K3 MXFP4: experts.id in {prefix}"))?;
280 let id: usize = id_s
281 .parse()
282 .with_context(|| format!("K3 MXFP4: expert id {id_s}"))?;
283 Ok((id, proj_i))
284}
285
286fn gather_proj(
287 table: &HashMap<(usize, usize), QuantizedWeight>,
288 ids: &[usize],
289 proj: usize,
290 name: &str,
291) -> Result<Vec<QuantizedWeight>> {
292 ids.iter()
293 .map(|&id| {
294 table.get(&(id, proj)).copied().with_context(|| {
295 format!(
296 "K3 MXFP4: expert {id} {name} missing; packed experts cannot silently run host F32"
297 )
298 })
299 })
300 .collect()
301}
302
303fn up_bf16(gpu: &dyn GpuBackend, v: &[f32], hold: &mut Vec<DevicePtr>) -> Result<DevicePtr> {
304 let b: Vec<u8> = v
305 .iter()
306 .flat_map(|&f| bf16::from_f32(f).to_le_bytes())
307 .collect();
308 let p = gpu.alloc(b.len().max(1))?;
309 hold.push(p);
310 gpu.copy_h2d(&b, p)?;
311 Ok(p)
312}
313
314fn up_i32(gpu: &dyn GpuBackend, v: &[i32], hold: &mut Vec<DevicePtr>) -> Result<DevicePtr> {
315 let b: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
316 let p = gpu.alloc(b.len().max(1))?;
317 hold.push(p);
318 gpu.copy_h2d(&b, p)?;
319 Ok(p)
320}
321
322fn bf16_to_f32(raw: &[u8]) -> Vec<f32> {
323 raw.chunks_exact(2)
324 .map(|b| bf16::from_le_bytes([b[0], b[1]]).to_f32())
325 .collect()
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use metrale_gpu_runtime::gpu::mock::{MockArg, MockGpuBackend};
332 use metrale_gpu_runtime::kernel_args::div_ceil;
333
334 fn dummy_qw(gpu: &MockGpuBackend) -> QuantizedWeight {
335 QuantizedWeight {
336 weight: gpu.alloc(16).unwrap(),
337 weight_scale: gpu.alloc(1).unwrap(),
338 weight_scale_2: 1.0,
339 input_scale: DevicePtr::NULL,
340 weight_scale_2_vec: DevicePtr::NULL,
341 act: metrale_config::Nvfp4Act::Unstamped,
342 }
343 }
344
345 #[test]
346 fn resolve_looks_up_e8m0_ptrtable() {
347 let gpu = MockGpuBackend::new();
348 let _ = K3MoeGemmKernels::resolve(&gpu).unwrap();
349 assert_eq!(
350 gpu.kernel_lookups_snapshot(),
351 vec![(MODULE.to_string(), PTRTABLE_E8M0.to_string())]
352 );
353 }
354
355 #[test]
356 fn deny_kernel_resolve_bails_not_silent_cpu() {
357 let gpu = MockGpuBackend::new();
358 gpu.deny_kernel(MODULE, PTRTABLE_E8M0);
359 let err = K3MoeGemmKernels::resolve(&gpu).unwrap_err().to_string();
360 assert!(
361 err.contains(PTRTABLE_E8M0) && err.contains("cannot silently run host F32"),
362 "{err}"
363 );
364 assert_eq!(gpu.launch_count(), 0);
365 }
366
367 #[test]
368 fn mock_launch_contract_one_expert() {
369 let gpu = MockGpuBackend::new();
370 let k = K3MoeGemmKernels::resolve(&gpu).unwrap();
371 gpu.set_kernel_n_tile(k.ptrtable, 64);
373 let packed = dummy_qw(&gpu);
374 let a = gpu.alloc(64).unwrap();
375 let c = gpu.alloc(128).unwrap();
376 let off = gpu.alloc(8).unwrap();
377 let ids = gpu.alloc(4).unwrap();
378 let n_out = 64u32;
379 let kk = 32u32;
380 launch_k3_moe_e8m0_ptrtable(&gpu, k.ptrtable, a, &[packed], c, off, ids, 1, n_out, kk, 3)
381 .unwrap();
382 let launches = gpu.launches_snapshot();
383 assert_eq!(launches.len(), 1);
384 assert_eq!(launches[0].grid, [div_ceil(n_out, 64), 1, 1]);
385 assert_eq!(launches[0].block, [128, 1, 1]);
386 assert_eq!(launches[0].stream, 3);
387 assert_eq!(launches[0].args.len(), 10);
388 assert_eq!(
389 launches[0].args[7],
390 MockArg::Bytes(1u32.to_le_bytes().to_vec())
391 );
392 assert_eq!(
393 launches[0].args[8],
394 MockArg::Bytes(n_out.to_le_bytes().to_vec())
395 );
396 assert_eq!(
397 launches[0].args[9],
398 MockArg::Bytes(kk.to_le_bytes().to_vec())
399 );
400 }
401
402 #[test]
403 fn parse_k3_expert_prefix() {
404 let p = "language_model.model.layers.12.block_sparse_moe.experts.7.w1";
405 assert_eq!(parse_expert_proj(p).unwrap(), (7, 0));
406 assert_eq!(
407 parse_expert_proj("model.layers.1.block_sparse_moe.experts.0.w3").unwrap(),
408 (0, 2)
409 );
410 }
411
412 #[test]
413 fn empty_ids_does_not_launch() {
414 let gpu = MockGpuBackend::new();
415 let k = K3MoeGemmKernels::resolve(&gpu).unwrap();
416 let cfg = LatentMoeConfig {
417 hidden: 2,
418 latent: 2,
419 expert_hidden: 2,
420 n_routed: 1,
421 top_k: 1,
422 n_shared: 0,
423 situ_beta: 4.0,
424 situ_linear_beta: 25.0,
425 use_norm: false,
426 renormalize: true,
427 };
428 let y =
429 launch_k3_latent_moe_experts(&gpu, &k, &[], &[1.0, 0.0], &[], &[], &cfg, 0).unwrap();
430 assert_eq!(y, vec![0.0, 0.0]);
431 assert_eq!(gpu.launch_count(), 0);
432 }
433
434 #[test]
435 fn missing_packed_proj_bails_not_cpu() {
436 let gpu = MockGpuBackend::new();
437 let k = K3MoeGemmKernels::resolve(&gpu).unwrap();
438 let cfg = LatentMoeConfig {
439 hidden: 2,
440 latent: 2,
441 expert_hidden: 2,
442 n_routed: 1,
443 top_k: 1,
444 n_shared: 0,
445 situ_beta: 4.0,
446 situ_linear_beta: 25.0,
447 use_norm: false,
448 renormalize: true,
449 };
450 let packed = [(
451 "model.layers.1.block_sparse_moe.experts.0.w1".to_string(),
452 dummy_qw(&gpu),
453 )];
454 let before = gpu.launch_count();
455 let err =
456 launch_k3_latent_moe_experts(&gpu, &k, &packed, &[1.0, 0.0], &[0], &[1.0], &cfg, 0)
457 .unwrap_err()
458 .to_string();
459 assert!(
460 err.contains("w2") && err.contains("cannot silently run host F32"),
461 "{err}"
462 );
463 assert_eq!(gpu.launch_count(), before);
464 }
465}