use foundation::LogitValue;
use super::batched_layers::{encode_dense_ffn_batched, encode_full_attention_block_batched};
pub(crate) use super::mtp_state::MtpState;
use super::mtp_state::{
FC, FINAL_NORM, PRE_FC_NORM_EMBEDDING, PRE_FC_NORM_HIDDEN, TRUNK_FINAL_NORM,
};
use crate::families::qwen::{
dense, encode_full_attention_block, prefixed_layer_tensor, QkNormConvention, MTP_PREFIX,
RMS_EPS,
};
use crate::real_forward::{RealForwardError, RealForwardRunner};
use crate::real_forward_dispatch::{encode_embed_any, encode_gemm_rows, encode_gemv_any};
use crate::real_forward_utils::norm_view;
use crate::vision::RopePosition;
impl RealForwardRunner {
pub fn mtp_draft_step(
&mut self,
next_token: i32,
position: usize,
logits: &mut [LogitValue],
) -> Result<(), RealForwardError> {
self.mtp_step(next_token, position, 0, Some(logits))
}
pub fn mtp_prime_step(
&mut self,
next_token: i32,
position: usize,
) -> Result<(), RealForwardError> {
self.mtp_step(next_token, position, 0, None)
}
pub(crate) fn mtp_prime_step_row(
&mut self,
next_token: i32,
position: usize,
h_row: usize,
) -> Result<(), RealForwardError> {
self.mtp_step(next_token, position, h_row, None)
}
pub(crate) fn mtp_prime_rows_batched(
&mut self,
next_tokens: &[i32],
start: usize,
copy_to_row0: Option<usize>,
) -> Result<(), RealForwardError> {
let arch = self.arch.clone();
let hidden = arch.hidden_size as usize;
let inter = arch.intermediate_size as usize;
let vocab = arch.vocab_size as usize;
let use_silu = arch.hidden_activation.contains("silu");
let k = next_tokens.len();
let gpu_err = RealForwardError::Gpu;
let row = |t: usize| (t * hidden * 2) as u64;
let Some(mtp) = self.real_mtp.as_ref() else {
return Err(RealForwardError::Unsupported(
"no MTP head state to prime".to_string(),
));
};
let cursor = mtp.kv_position();
if start != cursor {
return Err(RealForwardError::Unsupported(format!(
"MTP batched prime at position {start} but the head's KV covers [0, {cursor})"
)));
}
let capacity = mtp.kv.capacity(0);
if start % capacity + k > capacity {
return Err(RealForwardError::Unsupported(format!(
"MTP batched prime of {k} rows at {start} wraps the head's KV capacity {capacity}"
)));
}
if let Some(&bad) = next_tokens.iter().find(|&&t| (t as usize) >= vocab) {
return Err(RealForwardError::Unsupported(format!(
"prime token id {bad} outside vocab {vocab}"
)));
}
let qwen = self
.real_qwen
.as_ref()
.ok_or_else(|| RealForwardError::Unsupported("not a Qwen install".to_string()))?;
let Some(batched) = qwen.batched_prefill.as_ref() else {
return Err(RealForwardError::Unsupported(
"MTP batched prime needs the chunked driver's batched-arm scratch".to_string(),
));
};
if k > batched.batch || copy_to_row0.is_some_and(|r| r < k || r >= batched.batch) {
return Err(RealForwardError::Unsupported(format!(
"MTP batched prime of {k} rows (row-0 copy {copy_to_row0:?}) does not fit the \
{}-row prefill scratch",
batched.batch
)));
}
let embed_name = "language_model.model.embed_tokens.weight";
let pass = self.context.begin_pass_labeled("mtp batched prime");
let (context, weights, index, scratch) =
(&mut self.context, &self.weights, &self.index, &self.scratch);
if k > 0 {
let trunk_norm = norm_view(weights, index, TRUNK_FINAL_NORM, hidden)?;
let pre_h = norm_view(weights, index, PRE_FC_NORM_HIDDEN, hidden)?;
for t in 0..k {
gpu::encode_rms_norm_bf16w(
context,
&pass,
(&scratch.x, row(t)),
trunk_norm,
(&batched.normed, row(t)),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
gpu::encode_rms_norm_bf16w_centered(
context,
&pass,
(&batched.normed, row(t)),
pre_h,
(&mtp.concat, 2 * row(t) + hidden as u64 * 2),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
}
let pre_e = norm_view(weights, index, PRE_FC_NORM_EMBEDDING, hidden)?;
for (t, &token) in next_tokens.iter().enumerate() {
encode_embed_any(
context,
&pass,
weights,
index,
embed_name,
(&batched.normed, row(t)),
token as u32,
hidden as u32,
vocab,
1.0,
)?;
gpu::encode_rms_norm_bf16w_centered(
context,
&pass,
(&batched.normed, row(t)),
pre_e,
(&mtp.concat, 2 * row(t)),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
}
encode_gemm_rows(
context,
&pass,
weights,
index,
FC,
hidden,
2 * hidden,
(&mtp.concat, 0),
(&scratch.x, 0),
k,
batched.steel_gemm,
)?;
let input_norm = norm_view(
weights,
index,
&prefixed_layer_tensor(MTP_PREFIX, 0, "input_layernorm.weight"),
hidden,
)?;
for t in 0..k {
gpu::encode_rms_norm_bf16w_centered(
context,
&pass,
(&scratch.x, row(t)),
input_norm,
(&batched.normed, row(t)),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
}
encode_full_attention_block_batched(
context,
&pass,
weights,
index,
&arch,
qwen,
scratch,
batched,
&mtp.kv,
MTP_PREFIX,
0,
QkNormConvention::Centered,
start,
k,
)?;
let post_attn = norm_view(
weights,
index,
&prefixed_layer_tensor(MTP_PREFIX, 0, "post_attention_layernorm.weight"),
hidden,
)?;
for t in 0..k {
gpu::encode_residual_add(
context,
&pass,
(&scratch.x, row(t)),
(&batched.o, row(t)),
hidden as u32,
)
.map_err(gpu_err)?;
gpu::encode_rms_norm_bf16w_centered(
context,
&pass,
(&scratch.x, row(t)),
post_attn,
(&batched.moe_x, row(t)),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
}
encode_dense_ffn_batched(
context, &pass, weights, index, scratch, batched, MTP_PREFIX, 0, hidden, inter,
use_silu, k,
)?;
}
if let Some(r) = copy_to_row0 {
gpu::encode_dflash_copy_rows(
context,
&pass,
(&scratch.x, row(r)),
(&scratch.x, 0),
1,
hidden as u32,
hidden as u32,
)
.map_err(gpu_err)?;
}
pass.commit_and_wait();
self.real_mtp
.as_mut()
.expect("checked above")
.kv
.advance_by(k);
Ok(())
}
pub fn mtp_rewind_to(&mut self, position: usize) -> Result<(), RealForwardError> {
let mtp = self.real_mtp.as_mut().ok_or_else(|| {
RealForwardError::Unsupported("no MTP head state to rewind".to_string())
})?;
let cursor = mtp.kv.position();
if position > cursor {
return Err(RealForwardError::Unsupported(format!(
"MTP rewind target {position} is ahead of the head's cursor {cursor}"
)));
}
mtp.kv.rewind_by(cursor - position);
Ok(())
}
pub fn mtp_kv_position(&self) -> usize {
self.real_mtp.as_ref().map_or(0, |m| m.kv_position())
}
fn mtp_step(
&mut self,
next_token: i32,
position: usize,
h_row: usize,
logits: Option<&mut [LogitValue]>,
) -> Result<(), RealForwardError> {
let arch = self.arch.clone();
let hidden = arch.hidden_size as usize;
let inter = arch.intermediate_size as usize;
let vocab = arch.vocab_size as usize;
let use_silu = arch.hidden_activation.contains("silu");
let gpu_err = RealForwardError::Gpu;
if self.real_mtp.is_none() {
return Err(RealForwardError::Unsupported(
"no MTP head state; set TURBOSPARK_MTP_DRAFT=<depth> before opening the model"
.to_string(),
));
}
if (next_token as usize) >= vocab {
return Err(RealForwardError::Unsupported(format!(
"draft token id {next_token} outside vocab {vocab}"
)));
}
if h_row >= crate::real_forward_types::MAX_PREFILL_ROWS {
return Err(RealForwardError::Unsupported(format!(
"MTP hidden row {h_row} is past scratch.x's {} rows",
crate::real_forward_types::MAX_PREFILL_ROWS
)));
}
if let Some(out) = logits.as_ref() {
if vocab != out.len() {
return Err(RealForwardError::Unsupported(format!(
"vocab mismatch: model has {vocab}, caller expected {}",
out.len()
)));
}
}
let cursor = self.real_mtp.as_ref().expect("checked above").kv_position();
if position != cursor {
return Err(RealForwardError::Unsupported(format!(
"MTP step at position {position} but the head's KV covers [0, {cursor}): \
prime the head over the prompt with mtp_prime_step, and rewind it with \
mtp_rewind_to after a rejected draft (docs/MTP_SPECULATIVE.md)"
)));
}
let embed_name = "language_model.model.embed_tokens.weight";
let head_name = if arch.tie_word_embeddings {
embed_name.to_string()
} else {
"language_model.lm_head.weight".to_string()
};
let pass = self.context.begin_pass_labeled("mtp draft");
let (context, weights, index, scratch, qwen, mtp) = (
&mut self.context,
&self.weights,
&self.index,
&self.scratch,
self.real_qwen
.as_ref()
.ok_or_else(|| RealForwardError::Unsupported("not a Qwen install".to_string()))?,
self.real_mtp.as_ref().expect("checked above"),
);
let trunk_norm = norm_view(weights, index, TRUNK_FINAL_NORM, hidden)?;
gpu::encode_rms_norm_bf16w(
context,
&pass,
(&scratch.x, (h_row * hidden * 2) as u64),
trunk_norm,
(&scratch.normed, 0),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
let w = norm_view(weights, index, PRE_FC_NORM_HIDDEN, hidden)?;
gpu::encode_rms_norm_bf16w_centered(
context,
&pass,
(&scratch.normed, 0),
w,
(&mtp.concat, hidden as u64 * 2),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
encode_embed_any(
context,
&pass,
weights,
index,
embed_name,
(&scratch.normed, 0),
next_token as u32,
hidden as u32,
vocab,
1.0,
)?;
let w = norm_view(weights, index, PRE_FC_NORM_EMBEDDING, hidden)?;
gpu::encode_rms_norm_bf16w_centered(
context,
&pass,
(&scratch.normed, 0),
w,
(&mtp.concat, 0),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
encode_gemv_any(
context,
&pass,
weights,
index,
FC,
hidden,
2 * hidden,
(&mtp.concat, 0),
(&scratch.x, 0),
)?;
let input_norm = norm_view(
weights,
index,
&prefixed_layer_tensor(MTP_PREFIX, 0, "input_layernorm.weight"),
hidden,
)?;
gpu::encode_rms_norm_bf16w_centered(
context,
&pass,
(&scratch.x, 0),
input_norm,
(&scratch.normed, 0),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
encode_full_attention_block(
context,
&pass,
weights,
index,
&arch,
qwen,
scratch,
&mtp.kv,
MTP_PREFIX,
0,
position,
QkNormConvention::Centered,
RopePosition::Sequential,
)?;
gpu::encode_residual_add(
context,
&pass,
(&scratch.x, 0),
(&scratch.o, 0),
hidden as u32,
)
.map_err(gpu_err)?;
let post_attn = norm_view(
weights,
index,
&prefixed_layer_tensor(MTP_PREFIX, 0, "post_attention_layernorm.weight"),
hidden,
)?;
gpu::encode_rms_norm_bf16w_centered(
context,
&pass,
(&scratch.x, 0),
post_attn,
(&qwen.moe_x, 0),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
dense::encode_qwen_layer_dense(
context, &pass, weights, index, scratch, qwen, &scratch.x, 0, MTP_PREFIX, 0, hidden,
inter, use_silu,
)?;
if logits.is_some() {
let final_norm = norm_view(weights, index, FINAL_NORM, hidden)?;
gpu::encode_rms_norm_bf16w_centered(
context,
&pass,
(&scratch.x, 0),
final_norm,
(&scratch.normed, 0),
hidden as u32,
RMS_EPS,
)
.map_err(gpu_err)?;
encode_gemv_any(
context,
&pass,
weights,
index,
&head_name,
vocab,
hidden,
(&scratch.normed, 0),
(&scratch.logits, 0),
)?;
}
pass.commit_and_wait();
self.real_mtp.as_mut().expect("checked above").kv.advance();
let has_logits = logits.is_some();
if let Some(out) = logits {
gpu::read_buffer_f16_into(&self.scratch.logits, 0, out);
}
if let Some(dir) = self.real_mtp.as_ref().and_then(|m| m.dump_dir.as_deref()) {
self.dump_mtp_stage(dir, next_token, position, hidden, vocab, has_logits);
}
Ok(())
}
pub fn speculation_blocker(&self) -> Option<String> {
MtpState::speculation_blocker(&self.index, &self.arch, self.real_mtp.is_some())
}
pub fn mtp_draft_depth(&self) -> usize {
self.real_mtp.as_ref().map_or(0, |m| m.depth)
}
}