1use parking_lot::Mutex;
44use std::any::Any;
45
46use anyhow::Result;
47use metrale_cache::kv_cache::{KvCacheConfig, KvCacheDtype, PagedKvCache};
48use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
49
50use crate::weight_loader::deepseek_v4::DeepseekV4MtpModule;
51use metrale_model_layers::layer::{AttnMetadataDev, ForwardContext, LayerState};
52use metrale_model_layers::layers::mtp_meta::{MTP_META_OFFSET, pack_mtp_attn_meta};
53use metrale_model_layers::layers::ops;
54use metrale_model_layers::speculative::{DraftProposer, ProposerState};
55use metrale_model_layers::weight_map::DenseWeight;
56
57mod proposer;
58
59pub struct DeepseekV4MtpProposerState {
64 pub block_table: Vec<u32>,
65 pub seq_len: usize,
66 pub last_num_drafted: usize,
67 pub body_state: Box<dyn LayerState>,
68}
69
70impl ProposerState for DeepseekV4MtpProposerState {
71 fn as_any(&self) -> &dyn Any {
72 self
73 }
74 fn as_any_mut(&mut self) -> &mut dyn Any {
75 self
76 }
77}
78
79pub struct DeepseekV4MtpHead {
83 module: DeepseekV4MtpModule,
84 embed_tokens: DenseWeight,
85 lm_head: DenseWeight,
86 mtp_vocab_size: u32,
87 kv_cache: Mutex<PagedKvCache>,
88
89 rms_norm_k: KernelHandle,
90 dense_gemv_k: KernelHandle,
91 residual_add_k: KernelHandle,
92 hc_expand_k: KernelHandle,
93 hc_head_k: KernelHandle,
94 argmax_k: KernelHandle,
95}
96
97impl DeepseekV4MtpHead {
98 pub fn new(
101 module: DeepseekV4MtpModule,
102 embed_tokens: DenseWeight,
103 lm_head: DenseWeight,
104 config: &metrale_config::ModelConfig,
105 gpu: &dyn GpuBackend,
106 mtp_vocab_size: u32,
107 max_seq_len: usize,
108 ) -> Result<Self> {
109 let mla_cache_dim = config.kv_lora_rank + config.qk_rope_head_dim;
113 let num_layers = config.num_hidden_layers + 1;
117 let kv_config = KvCacheConfig {
118 block_size: 16,
119 num_kv_heads: 1,
120 head_dim: mla_cache_dim,
121 num_layers,
122 dtype: KvCacheDtype::Bf16,
123 layer_dtypes: vec![],
124 layer_dims: vec![],
125 cache_blocks_per_seq: None,
126 };
127 let mtp_num_blocks = max_seq_len / kv_config.block_size + 1;
128 let kv_cache = PagedKvCache::new(kv_config, mtp_num_blocks, gpu)?;
129
130 Ok(Self {
131 module,
132 embed_tokens,
133 lm_head,
134 mtp_vocab_size,
135 kv_cache: Mutex::new(kv_cache),
136 rms_norm_k: gpu.kernel("rms_norm_vanilla", "rms_norm_vanilla")?,
139 dense_gemv_k: gpu.kernel("gemv", "dense_gemv_bf16")?,
140 residual_add_k: gpu.kernel("residual_add", "bf16_residual_add")?,
141 hc_expand_k: gpu.kernel("hyper_connection", "hc_expand")?,
142 hc_head_k: gpu.kernel("hyper_connection", "hc_head")?,
143 argmax_k: gpu.kernel("argmax", "argmax_bf16")?,
144 })
145 }
146
147 pub fn alloc_state_inner(&self, gpu: &dyn GpuBackend) -> Result<DeepseekV4MtpProposerState> {
150 Ok(DeepseekV4MtpProposerState {
151 block_table: Vec::new(),
152 seq_len: 0,
153 last_num_drafted: 0,
154 body_state: self.module.body.alloc_state(gpu)?,
155 })
156 }
157
158 #[allow(clippy::too_many_arguments)]
160 fn forward_one(
161 &self,
162 token: u32,
163 target_hidden: DevicePtr,
164 position: usize,
165 state: &mut DeepseekV4MtpProposerState,
166 ctx: &ForwardContext,
167 stream: u64,
168 grammar_bitmask: Option<&[i32]>,
169 ) -> Result<u32> {
170 let h = ctx.config.hidden_size as u32;
171 let eps = ctx.config.rms_norm_eps as f32;
172 let hc_mult = ctx.config.hc_mult as u32;
173 let row_bytes = h as usize * 2;
174
175 let embed_out = ctx.buffers.ssm_qkvz();
177 let src = self.embed_tokens.weight.offset(token as usize * row_bytes);
178 ctx.gpu.copy_d2d_async(src, embed_out, row_bytes, stream)?;
179
180 let normed_embed = ctx.buffers.ssm_deinterleaved();
183 ops::rms_norm(
184 ctx.gpu,
185 self.rms_norm_k,
186 embed_out,
187 &self.module.enorm,
188 normed_embed,
189 1,
190 h,
191 eps,
192 stream,
193 )?;
194 let normed_hidden = ctx.buffers.ssm_gates();
195 ops::rms_norm(
196 ctx.gpu,
197 self.rms_norm_k,
198 target_hidden,
199 &self.module.hnorm,
200 normed_hidden,
201 1,
202 h,
203 eps,
204 stream,
205 )?;
206
207 let h_in = ctx.buffers.hidden_states();
210 let h_branch = ctx.buffers.norm_output();
211 ops::dense_gemv(
212 ctx.gpu,
213 self.dense_gemv_k,
214 normed_embed,
215 &self.module.e_proj,
216 h_in,
217 h,
218 h,
219 stream,
220 )?;
221 ops::dense_gemv(
222 ctx.gpu,
223 self.dense_gemv_k,
224 normed_hidden,
225 &self.module.h_proj,
226 h_branch,
227 h,
228 h,
229 stream,
230 )?;
231 ops::residual_add(ctx.gpu, self.residual_add_k, h_in, h_branch, h, stream)?;
232
233 let hc_streams = ctx.buffers.hc_streams();
235 ops::hc_expand(
236 ctx.gpu,
237 self.hc_expand_k,
238 h_in,
239 hc_streams,
240 1,
241 h,
242 hc_mult,
243 stream,
244 )?;
245
246 let mut kv_cache = self.kv_cache.lock();
249 let bs = kv_cache.block_size();
250 let blocks_needed = (state.seq_len / bs) + 1;
251 while state.block_table.len() < blocks_needed {
252 state.block_table.push(kv_cache.alloc_block()?);
253 }
254
255 let meta_base = ctx.buffers.scratch().offset(MTP_META_OFFSET);
259 let max_blocks = state.block_table.len() as u32;
260 let block_idx = state.block_table[state.seq_len / bs];
261 let global_slot = (block_idx as i64) * (bs as i64) + ((state.seq_len % bs) as i64);
262 let actual_seq_len = (state.seq_len + 1) as i32;
263
264 let meta_buf = pack_mtp_attn_meta(
267 position as u32,
268 global_slot,
269 actual_seq_len,
270 &state.block_table,
271 ctx.buffers.scratch_bytes().saturating_sub(MTP_META_OFFSET),
272 )?;
273 ctx.gpu.copy_h2d_async(&meta_buf, meta_base, stream)?;
274
275 let mtp_meta = AttnMetadataDev {
276 positions: meta_base,
277 positions_h: meta_base,
278 positions_w: meta_base,
279 slot: meta_base.offset(8),
280 seq_len: meta_base.offset(16),
281 block_table: meta_base.offset(256),
282 max_blocks_per_seq: max_blocks,
283 num_seqs: 1,
284 seq_slot: metrale_gpu_runtime::gpu::DevicePtr(0),
285 moe_row_adapter: metrale_gpu_runtime::gpu::DevicePtr::NULL,
286 };
287
288 if let Some(tid_buf) = ctx.token_ids {
291 ctx.gpu
292 .copy_h2d_async(&token.to_le_bytes(), tid_buf, stream)?;
293 }
294
295 let mtp_ctx = ForwardContext {
298 buffers: ctx.buffers,
299 hc_row_offset: ctx.hc_row_offset,
300 gpu: ctx.gpu,
301 config: ctx.config,
302 dispatch: ctx.dispatch,
303 derived: ctx.derived,
304 levers: ctx.levers,
305 stats: ctx.stats,
306 attn_metadata: Some(mtp_meta),
307 profile: ctx.profile,
308 comm: None,
312 graph_capture: false,
313 decode_step: false,
314 gdn_exact_replay: false,
315 gdn_write_on_accept: false,
316 token_ids: ctx.token_ids,
317 host_token_ids: None,
318 routed_lora_layers: None,
319 midchunk_capture: None,
320 moe_lora_route: metrale_model_layers::layer::MoeLoraRoute::Skip,
321 };
322
323 let body_scratch = ctx.buffers.hidden_states();
328 let mut disk_block_ids: Vec<u32> = Vec::new();
329 let mut disk_last_offloaded: Vec<u32> = vec![0u32; 1];
330 let residual = ctx.buffers.residual();
331 self.module.body.decode(
332 body_scratch,
333 residual,
334 state.body_state.as_mut(),
335 &mut kv_cache,
336 state.seq_len,
337 &mut state.block_table,
338 &mut disk_block_ids,
339 &mut disk_last_offloaded,
340 &mtp_ctx,
341 stream,
342 )?;
343 drop(kv_cache);
344
345 let h_out = ctx.buffers.hidden_states();
347 if let Some(ref head) = self.module.hc_head {
348 anyhow::ensure!(
352 head.lowrank.is_none(),
353 "deepseek_v4_mtp: low-rank mHC head reached the Sinkhorn MTP \
354 path; this module has no low-rank dispatch"
355 );
356 ops::hc_head(
357 ctx.gpu,
358 self.hc_head_k,
359 hc_streams,
360 head.hc_fn,
361 head.hc_scale,
362 head.hc_base,
363 h_out,
364 1,
365 h,
366 hc_mult,
367 eps,
368 ctx.config.hc_eps,
369 stream,
370 )?;
371 } else {
372 ctx.gpu
374 .copy_d2d_async(hc_streams, h_out, row_bytes, stream)?;
375 }
376
377 let final_normed = ctx.buffers.norm_output();
379 ops::rms_norm(
380 ctx.gpu,
381 self.rms_norm_k,
382 h_out,
383 &self.module.norm,
384 final_normed,
385 1,
386 h,
387 eps,
388 stream,
389 )?;
390 let v = if self.mtp_vocab_size > 0 {
391 self.mtp_vocab_size.min(ctx.config.vocab_size as u32)
392 } else {
393 ctx.config.vocab_size as u32
394 };
395 let logits = ctx.buffers.logits();
396 ops::dense_gemv(
397 ctx.gpu,
398 self.dense_gemv_k,
399 final_normed,
400 &self.lm_head,
401 logits,
402 v,
403 h,
404 stream,
405 )?;
406
407 let out_ptr = ctx.buffers.scratch();
409 let token_id = if let Some(bitmask) = grammar_bitmask {
410 argmax_grammar_masked(ctx.gpu, logits, v as usize, bitmask, position)?
411 } else {
412 ops::argmax_bf16(ctx.gpu, self.argmax_k, logits, out_ptr, v, stream)?;
413 let mut buf = [0u8; 4];
414 ctx.gpu.copy_d2h(out_ptr, &mut buf)?;
415 u32::from_le_bytes(buf)
416 };
417
418 state.seq_len += 1;
419 Ok(token_id)
420 }
421}
422
423fn argmax_grammar_masked(
427 gpu: &dyn GpuBackend,
428 logits: DevicePtr,
429 vocab: usize,
430 bitmask: &[i32],
431 position: usize,
432) -> Result<u32> {
433 let mut bf16_buf = vec![0u8; vocab * 2];
434 gpu.copy_d2h(logits, &mut bf16_buf)?;
435
436 let mut best_tok = 0u32;
437 let mut best_val = f32::NEG_INFINITY;
438 let mut any_allowed = false;
439 for tok in 0..vocab {
440 let word = tok / 32;
441 let bit = tok % 32;
442 let allowed = word < bitmask.len() && (bitmask[word] & (1i32 << bit)) != 0;
443 if !allowed {
444 continue;
445 }
446 any_allowed = true;
447 let hi = u16::from_le_bytes([bf16_buf[2 * tok], bf16_buf[2 * tok + 1]]);
449 let val = f32::from_bits((hi as u32) << 16);
450 if val > best_val {
451 best_val = val;
452 best_tok = tok as u32;
453 }
454 }
455 if !any_allowed {
456 tracing::warn!(
457 "V4 MTP grammar mask allowed zero tokens at pos {position}; \
458 returning 0 as pad-draft (will be rejected at verify)."
459 );
460 return Ok(0);
461 }
462 Ok(best_tok)
463}