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