1use anyhow::Result;
15use metrale_cache::kv_cache::{KvCacheConfig, KvCacheDtype, PagedKvCache};
16use metrale_gpu_runtime::gpu::GpuBackend;
17use parking_lot::Mutex;
18
19use super::{MtpHead, MtpQuantization, ProjectionWeight};
20use crate::layers::MoeLayer;
21use crate::weight_map::{DenseWeight, MoeWeights, MtpWeights, QuantizedWeight, quantize_to_nvfp4};
22
23impl MtpHead {
24 pub fn new(
25 weights: MtpWeights,
26 embed_tokens: DenseWeight,
27 target_final_norm: DenseWeight,
28 lm_head_nvfp4: QuantizedWeight,
29 lm_head_nvfp4_t: Option<(QuantizedWeight, u32)>,
32 config: &metrale_config::ModelConfig,
33 gpu: &dyn GpuBackend,
34 quant: MtpQuantization,
35 mtp_vocab_size: u32,
36 max_seq_len: usize,
37 main_kv_blocks: usize,
38 levers: &crate::layers::ops::ModelLevers,
39 ) -> Result<Self> {
40 let stream = gpu.default_stream();
41 let wquant = quant;
46 let quant = wquant.effective_for_head(weights.dense_ffn.is_some());
47 let dense_nvfp4 = quant != wquant;
48 let absmax_k = gpu.kernel("quantize_nvfp4", "nvfp4_global_absmax")?;
49 let nvfp4_k = gpu.kernel("quantize_nvfp4", "quantize_bf16_to_nvfp4")?;
50 let fp8_k = gpu.kernel("gemv_fp8w", "quantize_bf16_to_fp8")?;
51
52 let h = config.hidden_size;
53 let nq = config.num_attention_heads;
54 let nkv = config.num_key_value_heads;
55 let hd = config.head_dim;
56 let inter = if config.moe_intermediate_size > 0 {
57 config.moe_intermediate_size
58 } else {
59 config.intermediate_size
60 };
61
62 let q = |bf16: &DenseWeight, n: usize, k: usize| -> Result<ProjectionWeight> {
63 Self::quantize_proj(bf16, n, k, quant, gpu, absmax_k, nvfp4_k, fp8_k, stream)
64 };
65 let qw = |bf16: &DenseWeight, n: usize, k: usize| -> Result<ProjectionWeight> {
68 Self::quantize_proj(bf16, n, k, wquant, gpu, absmax_k, nvfp4_k, fp8_k, stream)
69 };
70
71 let fc = q(&weights.fc, h, h * 2)?;
72 let q_proj = qw(&weights.q_proj, nq * hd * 2, h)?;
73 let k_proj = q(&weights.k_proj, nkv * hd, h)?;
74 let v_proj = q(&weights.v_proj, nkv * hd, h)?;
75 let o_proj = qw(&weights.o_proj, h, nq * hd)?;
76
77 let dense_ffn_generic = if let Some(dense_ffn) = weights.dense_ffn.as_ref() {
78 Some((
79 qw(&dense_ffn.gate_proj, inter, h)?,
80 qw(&dense_ffn.up_proj, inter, h)?,
81 qw(&dense_ffn.down_proj, h, inter)?,
82 ))
83 } else {
84 None
85 };
86
87 let mut weights = weights;
91 let draft_nvfp4 = super::mtp_experts_nvfp4();
94 anyhow::ensure!(
95 !draft_nvfp4
96 || (dense_ffn_generic.is_none()
97 && quant == MtpQuantization::Bf16
98 && weights.fp8_experts.is_none()),
99 "--mtp-experts-nvfp4 needs a BF16 MoE MTP head on BF16 experts (this head: \
100 {quant:?}, dense FFN {}, FP8 experts {})",
101 dense_ffn_generic.is_some(),
102 weights.fp8_experts.is_some()
103 );
104 let moe_parts = if dense_ffn_generic.is_some() {
105 (None, None, None, None)
106 } else {
107 match quant {
108 MtpQuantization::Nvfp4 => {
109 let gate_nvfp4 = quantize_to_nvfp4(
110 &weights.moe_gate,
111 config.num_experts,
112 h,
113 gpu,
114 absmax_k,
115 nvfp4_k,
116 stream,
117 )?;
118 let mut experts = Vec::with_capacity(weights.experts.len());
119 for (i, de) in weights.experts.iter().enumerate() {
120 let gate_proj = quantize_to_nvfp4(
121 &de.gate_proj,
122 inter,
123 h,
124 gpu,
125 absmax_k,
126 nvfp4_k,
127 stream,
128 )?;
129 let up_proj = quantize_to_nvfp4(
130 &de.up_proj,
131 inter,
132 h,
133 gpu,
134 absmax_k,
135 nvfp4_k,
136 stream,
137 )?;
138 let down_proj = quantize_to_nvfp4(
139 &de.down_proj,
140 h,
141 inter,
142 gpu,
143 absmax_k,
144 nvfp4_k,
145 stream,
146 )?;
147 experts.push(crate::weight_map::ExpertWeight {
148 gate_proj,
149 up_proj,
150 down_proj,
151 });
152 if (i + 1) % 128 == 0 {
153 tracing::info!(
154 " MTP experts quantized: {}/{}",
155 i + 1,
156 weights.experts.len()
157 );
158 }
159 }
160 let shared_gate = quantize_to_nvfp4(
161 &weights.shared_expert.gate_proj,
162 inter,
163 h,
164 gpu,
165 absmax_k,
166 nvfp4_k,
167 stream,
168 )?;
169 let shared_up = quantize_to_nvfp4(
170 &weights.shared_expert.up_proj,
171 inter,
172 h,
173 gpu,
174 absmax_k,
175 nvfp4_k,
176 stream,
177 )?;
178 let shared_down = quantize_to_nvfp4(
179 &weights.shared_expert.down_proj,
180 h,
181 inter,
182 gpu,
183 absmax_k,
184 nvfp4_k,
185 stream,
186 )?;
187 let moe_weights = MoeWeights {
188 gate: weights.moe_gate,
189 shared_expert: crate::weight_map::ExpertWeight {
190 gate_proj: shared_gate,
191 up_proj: shared_up,
192 down_proj: shared_down,
193 },
194 shared_expert_gate: weights.shared_expert_gate,
195 experts,
196 router_pre_norm: None,
197 correction_bias: None,
198 };
199 let moe = MoeLayer::new(
200 moe_weights,
201 config.num_experts,
202 Some(gate_nvfp4),
203 gpu,
204 config,
205 )?;
206 (Some(moe), None, None, None)
207 }
208 MtpQuantization::Fp8 | MtpQuantization::Bf16 if weights.fp8_experts.is_some() => {
209 let moe = Self::new_native_fp8_moe(&mut weights, config, gpu)?;
210 (None, None, None, Some(moe))
211 }
212 MtpQuantization::Bf16 => {
214 let moe = if draft_nvfp4 {
215 Self::new_nvfp4_draft_moe(&weights, config, gpu, absmax_k, nvfp4_k, stream)?
216 } else {
217 Self::new_bf16_moe(&weights, config, gpu)?
218 };
219 (None, None, None, Some(moe))
220 }
221 MtpQuantization::Fp8 => {
223 let mut experts_g = Vec::with_capacity(weights.experts.len());
224 for (i, de) in weights.experts.iter().enumerate() {
225 let gate_proj = q(&de.gate_proj, inter, h)?;
226 let up_proj = q(&de.up_proj, inter, h)?;
227 let down_proj = q(&de.down_proj, h, inter)?;
228 experts_g.push((gate_proj, up_proj, down_proj));
229 if (i + 1) % 128 == 0 {
230 tracing::info!(
231 " MTP experts quantized: {}/{}",
232 i + 1,
233 weights.experts.len()
234 );
235 }
236 }
237 let shared = (
238 q(&weights.shared_expert.gate_proj, inter, h)?,
239 q(&weights.shared_expert.up_proj, inter, h)?,
240 q(&weights.shared_expert.down_proj, h, inter)?,
241 );
242 (None, Some(experts_g), Some(shared), None)
243 }
244 }
245 };
246 let (moe_nvfp4, moe_experts_generic, moe_shared_generic, moe_grouped) = moe_parts;
247
248 let kv_bf16 = matches!(quant, MtpQuantization::Bf16 | MtpQuantization::Fp8);
252 let kv_config = KvCacheConfig {
253 block_size: 16,
254 num_kv_heads: nkv,
255 head_dim: hd,
256 num_layers: 1,
257 dtype: if kv_bf16 {
258 KvCacheDtype::Bf16
259 } else {
260 KvCacheDtype::Fp8
261 },
262 layer_dtypes: vec![],
263 layer_dims: vec![],
264 cache_blocks_per_seq: None,
265 };
266 let per_seq_blocks = max_seq_len / kv_config.block_size + 1;
272 let mtp_num_blocks = per_seq_blocks
273 .saturating_mul(crate::speculative::mtp_max_seqs())
274 .min(main_kv_blocks.max(per_seq_blocks));
275 let propose_meta_stride =
280 super::batch_caps::propose_meta_stride_env(max_seq_len, kv_config.block_size);
281 let kv_cache = PagedKvCache::new(kv_config, mtp_num_blocks, gpu)?;
282
283 let (
284 dense_gemv_k,
285 dense_gemv_fp8w_k,
286 deinterleave_qg_k,
287 moe_topk_k,
288 moe_silu_mul_k,
289 moe_weighted_sum_blend_k,
290 ) = match quant {
291 MtpQuantization::Nvfp4 => (None, None, None, None, None, None),
292 MtpQuantization::Fp8 => (
293 Some(gpu.kernel("gemv", "dense_gemv_bf16")?),
295 Some(gpu.kernel("gemv_fp8w", "dense_gemv_fp8w")?),
296 Some(gpu.kernel("ssm_preprocess", "deinterleave_qg")?),
297 Some(gpu.kernel("moe_topk", "moe_topk_softmax")?),
298 Some(gpu.kernel("moe_silu_mul", "moe_silu_mul")?),
299 Some(gpu.kernel("moe_expert_gemv", "moe_weighted_sum_blend")?),
300 ),
301 MtpQuantization::Bf16 => (
302 Some(gpu.kernel("gemv", "dense_gemv_bf16")?),
303 None,
304 Some(gpu.kernel("ssm_preprocess", "deinterleave_qg")?),
305 Some(gpu.kernel("moe_topk", "moe_topk_softmax")?),
306 Some(gpu.kernel("moe_silu_mul", "moe_silu_mul")?),
307 Some(gpu.kernel("moe_expert_gemv", "moe_weighted_sum_blend")?),
308 ),
309 };
310
311 let effective_vocab = if mtp_vocab_size > 0 {
312 (mtp_vocab_size as usize).min(config.vocab_size)
313 } else {
314 config.vocab_size
315 };
316 let ffn_kind: &str = if dense_ffn_generic.is_some() {
317 "dense FFN"
318 } else if moe_nvfp4.is_some() {
319 "MoE (NVFP4 fused)"
320 } else if moe_grouped.is_some() {
321 "MoE (native FP8 tables; batched propose grouped)"
322 } else {
323 "MoE (per-expert)"
324 };
325 tracing::info!(
326 "MTP head: quant={:?}{wo}, fc=[{h},{h2}], attn Q=[{qd},{h}], ffn={ffn}, \
327 {ne} experts, vocab={ev}/{fv} (LM head {lm:.1} MB)",
328 quant,
329 wo = if dense_nvfp4 {
330 " (weight-only NVFP4 q/o + dense FFN)"
331 } else {
332 ""
333 },
334 h2 = h * 2,
335 qd = nq * hd * 2,
336 ffn = ffn_kind,
337 ne = if dense_ffn_generic.is_some() {
338 0
339 } else {
340 config.num_experts
341 },
342 ev = effective_vocab,
343 fv = config.vocab_size,
344 lm = (effective_vocab * h / 2) as f64 / (1024.0 * 1024.0),
345 );
346
347 let prefill_scratch = if super::mtp_drafter_prefill_enabled(levers)
351 || crate::speculative::mtp_catchup_enabled()
352 {
353 let c = super::prefill::PREFILL_CHUNK;
354 let bf16 = 2usize;
355 Some(super::MtpPrefillScratch {
356 embed: gpu.alloc(c * h * bf16)?,
357 normed_embed: gpu.alloc(c * h * bf16)?,
358 normed_hidden: gpu.alloc(c * h * bf16)?,
359 concat: gpu.alloc(c * 2 * h * bf16)?,
360 fc_out: gpu.alloc(c * h * bf16)?,
361 normed2: gpu.alloc(c * h * bf16)?,
362 k_out: gpu.alloc(c * nkv * hd * bf16)?,
363 v_out: gpu.alloc(c * nkv * hd * bf16)?,
364 q_scratch: gpu.alloc(c * nq * hd * bf16)?,
365 pos_dev: gpu.alloc(c * 4)?,
366 slot_dev: gpu.alloc(c * 8)?,
367 })
368 } else {
369 None
370 };
371
372 Ok(Self {
373 pre_fc_norm_embedding: weights.pre_fc_norm_embedding,
374 pre_fc_norm_hidden: weights.pre_fc_norm_hidden,
375 input_layernorm: weights.input_layernorm,
376 post_attn_layernorm: weights.post_attn_layernorm,
377 norm: weights.norm,
378 fc,
379 q_proj,
380 k_proj,
381 v_proj,
382 o_proj,
383 q_norm: weights.q_norm,
384 k_norm: weights.k_norm,
385 moe_nvfp4,
386 moe_experts_generic,
387 moe_shared_generic,
388 moe_grouped,
389 moe_gate: weights.moe_gate,
390 shared_expert_gate: weights.shared_expert_gate,
391 dense_ffn_generic,
392 quant,
393 mtp_vocab_size,
394 embed_tokens,
395 target_final_norm,
396 lm_head_nvfp4,
397 kv_cache: Mutex::new(kv_cache),
398 attn_layer_idx: 0,
399 rms_norm_k: gpu.kernel("norm", "rms_norm")?,
400 rms_norm_residual_k: gpu.kernel("norm", "rms_norm_residual")?,
401 w4a16_gemv_k: gpu.kernel("w4a16_gemv", "w4a16_gemv")?,
402 w4a16_gemv_sw_k: crate::layers::try_kernel(gpu, "w4a16_gemv", "w4a16_gemv_sw"),
403 gemv_sw: crate::layers::ops::gemv_sw_from(
404 std::env::var("METRALE_NO_GEMV_SW").ok().as_deref(),
405 ),
406 w4a16_gemv_qg_k: gpu.kernel("w4a16_gemv", "w4a16_gemv_qg")?,
407 w4a16_gemv_dual_k: gpu.kernel("w4a16_gemv_fused", "w4a16_gemv_dual")?,
408 rope_k: gpu.kernel("rope", "rope_forward")?,
409 rope_strided_k: crate::layers::try_kernel(gpu, "rope", "rope_forward_strided"),
410 rms_norm_strided_k: crate::layers::try_kernel(gpu, "norm", "rms_norm_strided"),
411 sigmoid_gate_mul_batched_k: crate::layers::try_kernel(
412 gpu,
413 "residual_add",
414 "sigmoid_gate_mul_batched",
415 ),
416 reshape_cache_k: if kv_bf16 {
417 gpu.kernel("reshape_and_cache", "reshape_and_cache_flash")?
418 } else {
419 gpu.kernel("reshape_and_cache", "reshape_and_cache_flash_fp8")?
420 },
421 paged_decode_k: if kv_bf16 {
422 gpu.kernel("paged_decode", "paged_decode_attn")?
423 } else {
424 gpu.kernel("paged_decode_fp8", "paged_decode_attn_fp8")?
425 },
426 kv_bf16,
427 kv_exact: levers.mtp_kv_exact,
428 residual_add_k: gpu.kernel("residual_add", "bf16_residual_add")?,
429 residual_add_rms_norm_k: gpu.kernel("norm", "residual_add_rms_norm")?,
430 sigmoid_gate_mul_k: gpu.kernel("residual_add", "sigmoid_gate_mul")?,
431 bf16_concat_k: gpu.kernel("residual_add", "bf16_concat")?,
432 argmax_k: gpu.kernel("argmax", "argmax_bf16")?,
433 embed_from_argmax_k: gpu.kernel("embed_from_argmax", "embed_from_argmax")?,
434 draft_token_id_dev: gpu.alloc(4)?,
435 last_conf_bits: std::sync::atomic::AtomicU32::new(1.0f32.to_bits()),
436 dense_gemv_k,
437 dense_gemv_fp8w_k,
438 w8a16_gemv_k: gpu.kernel("w8a16_gemv", "w8a16_gemv").ok(),
439 deinterleave_qg_k,
440 moe_topk_k,
441 moe_silu_mul_k,
442 moe_weighted_sum_blend_k,
443 dense_gemm_k: crate::layers::try_kernel(gpu, "gemm", "dense_gemm_bf16"),
446 batched_embed_k: crate::layers::try_kernel(gpu, "embed_from_argmax", "batched_embed"),
447 dense_gemm_pipelined_k: crate::layers::try_kernel(
448 gpu,
449 "gemm",
450 "dense_gemm_bf16_pipelined",
451 ),
452 dense_gemv_batchm_k: crate::layers::try_kernel(
455 gpu,
456 "dense_gemv_bf16_batchm",
457 "dense_gemv_bf16_batchm",
458 ),
459 w4a16_batchm: crate::layers::w4a16_gemv_tiers::W4a16BatchmTiers::resolve(gpu),
460 w4a16_gemv_batch16_k: crate::layers::try_kernel(
461 gpu,
462 "w4a16_gemv",
463 "w4a16_gemv_batch16",
464 ),
465 w4a16_gemv_batch32_k: crate::layers::try_kernel(
466 gpu,
467 "w4a16_gemv",
468 "w4a16_gemv_batch32",
469 ),
470 lm_head_nvfp4_t: lm_head_nvfp4_t
473 .filter(|_| std::env::var_os("METRALE_NO_MTP_LMHEAD_TGEMM").is_none()),
474 w4a16_gemm_t_k: crate::layers::tgemm_kernel(gpu),
475 argmax_batch_k: crate::layers::try_kernel(gpu, "argmax", "argmax_bf16_batch"),
476 argmax_batch_lp_k: crate::layers::try_kernel(gpu, "argmax", "argmax_bf16_batch_lp"),
477 propose_meta: gpu.alloc(super::batch_caps::PROPOSE_META_SEQS * propose_meta_stride)?,
481 propose_meta_stride,
482 prefill_scratch,
483 circuit_draft: parking_lot::RwLock::new(None),
484 })
485 }
486}