class K3DSparkModel(nn.Module):
def __init__(
self,
*,
vllm_config: VllmConfig,
start_layer_id: int,
prefix: str,
) -> None:
super().__init__()
assert vllm_config.speculative_config is not None
self.config = vllm_config.speculative_config.draft_model_config.hf_config
self.quant_config = get_draft_quant_config(vllm_config)
# The frozen target embedding is aliased after the draft checkpoint loads.
self.embed_tokens: nn.Module | None = None
self.context_proj = ReplicatedLinear(
self.config.target_hidden_size * self.config.num_target_layers,
self.config.hidden_size,
bias=False,
return_bias=False,
quant_config=self.quant_config,
prefix=maybe_prefix(prefix, "context_proj"),
)
self.context_norm = RMSNorm(
self.config.hidden_size, eps=self.config.rms_norm_eps
)
self.layers = nn.ModuleList(
[
K3DSparkDecoderLayer(
vllm_config=vllm_config,
config=self.config,
layer_idx=layer_idx,
start_layer_id=start_layer_id,
prefix=prefix,
)
for layer_idx in range(self.config.num_hidden_layers)
]
)
self.final_norm = RMSNorm(self.config.hidden_size, eps=self.config.rms_norm_eps)
self.markov_head = ReplicatedDSparkMarkovHead(
self.config.vocab_size,
self.config.draft_vocab_size,
self.config.markov_rank,
prefix=maybe_prefix(prefix, "markov_head"),
)
self._context_kv_fusion_available: bool | None = None
self._max_num_context_tokens = (
vllm_config.scheduler_config.max_num_batched_tokens
)
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
assert self.embed_tokens is not None
return self.embed_tokens(input_ids)
def combine_hidden_states(self, hidden_states: torch.Tensor) -> torch.Tensor:
return self.context_norm(self.context_proj(hidden_states))
@torch.inference_mode()
def precompute_and_store_context_kv(
self,
context_states: torch.Tensor,
context_positions: torch.Tensor,
context_slot_mapping: torch.Tensor | list[torch.Tensor | None] | None = None,
) -> None:
"""Project target-derived context into each draft layer's latent cache."""
if self._context_kv_fusion_available is None:
self._build_fused_context_kv_buffers()
if self._context_kv_fusion_available:
self._precompute_fused_context_kv(
context_states, context_positions, context_slot_mapping
)
return
# Quantized fallback. Directly invoking the projection modules preserves
# their quantization methods, at the cost of also computing unused Q rows.
for layer_idx, layer in enumerate(self.layers):
attn = layer.self_attn
assert attn.fused_qkv_a_proj is not None
assert attn.q_lora_rank is not None
assert attn.rotary_emb is not None
qkv_lora = attn.fused_qkv_a_proj(context_states)[0]
kv_lora = qkv_lora[..., attn.q_lora_rank :]
kv_c, k_pe = kv_lora.split(
[attn.kv_lora_rank, attn.qk_rope_head_dim], dim=-1
)
kv_c = attn.kv_a_layernorm(kv_c)
k_pe = k_pe.unsqueeze(1)
# DeepSeek YaRN's FlashInfer path requires paired Q/K tensors.
# The vLLM CUDA op supports rotating one tensor in place and
# consumes the same (possibly scaled fp32) cos/sin cache.
rotary_emb = attn.rotary_emb
ops.rotary_embedding(
context_positions,
k_pe,
None,
rotary_emb.head_size,
rotary_emb.cos_sin_cache,
rotary_emb.is_neox_style,
)
slot_mapping = (
context_slot_mapping[layer_idx]
if isinstance(context_slot_mapping, (list, tuple))
else context_slot_mapping
)
if slot_mapping is None:
continue
attn.impl.do_kv_cache_update(
kv_c,
k_pe,
attn.kv_cache,
slot_mapping,
attn.kv_cache_dtype,
attn._k_scale,
)
def _build_fused_context_kv_buffers(self) -> None:
"""Build a cross-layer KV-only A projection after checkpoint loading."""
if self.quant_config is not None:
self._context_kv_fusion_available = False
return
attentions = [layer.self_attn for layer in self.layers]
if not attentions or any(
attn.fused_qkv_a_proj is None
or not hasattr(attn.fused_qkv_a_proj, "weight")
for attn in attentions
):
self._context_kv_fusion_available = False
return
attn0 = attentions[0]
assert attn0.q_lora_rank is not None
kv_width = attn0.kv_lora_rank + attn0.qk_rope_head_dim
kv_weights = []
for attn in attentions:
assert attn.q_lora_rank is not None
assert (
attn.q_lora_rank == attn0.q_lora_rank
and attn.kv_lora_rank == attn0.kv_lora_rank
and attn.qk_rope_head_dim == attn0.qk_rope_head_dim
and attn.kv_a_layernorm.variance_epsilon
== attn0.kv_a_layernorm.variance_epsilon
), "All MLA DSpark layers must share their latent KV geometry."
kv_weights.append(
attn.fused_qkv_a_proj.weight.detach().narrow(
0, attn.q_lora_rank, kv_width
)
)
# [L * (kv_lora_rank + rope_dim), hidden_size]. The underlying fused
# A weights are replicated (`disable_tp=True`), so this is valid on
# every TP rank without communication.
self._fused_context_kv_weight = torch.cat(kv_weights, dim=0)
self._context_kv_norm_weights = torch.stack(
[attn.kv_a_layernorm.weight.detach() for attn in attentions], dim=0
).contiguous()
self._num_context_layers = len(attentions)
self._context_kv_width = kv_width
self._context_kv_lora_rank = attn0.kv_lora_rank
self._context_rope_dim = attn0.qk_rope_head_dim
self._context_rms_norm_eps = attn0.kv_a_layernorm.variance_epsilon
self._context_positions_repeated = torch.empty(
self._num_context_layers * self._max_num_context_tokens,
dtype=torch.int64,
device=self._fused_context_kv_weight.device,
)
self._context_kv_fusion_available = True
def _precompute_fused_context_kv(
self,
context_states: torch.Tensor,
context_positions: torch.Tensor,
context_slot_mapping: torch.Tensor | list[torch.Tensor | None] | None,
) -> None:
num_ctx = context_states.shape[0]
num_layers = self._num_context_layers
# One KV-only GEMM replaces five full Q+KV GEMMs. For K3 this projects
# 5*576 rows rather than 5*2112 rows (72.7% fewer A-projection FLOPs).
all_kv = F.linear(context_states, self._fused_context_kv_weight)
all_kv = all_kv.view(num_ctx, num_layers, self._context_kv_width)
all_kv_c = all_kv[..., : self._context_kv_lora_rank]
all_k_pe = all_kv[..., self._context_kv_lora_rank :]
# Layer-major layout lets the 2-D RMSNorm weights select a distinct row
# for each draft layer in one grouped kernel.
all_kv_c = all_kv_c.permute(1, 0, 2).contiguous()
all_kv_c_normed = torch.empty_like(all_kv_c)
ops.rms_norm(
all_kv_c_normed,
all_kv_c,
self._context_kv_norm_weights,
self._context_rms_norm_eps,
)
all_k_pe = all_k_pe.permute(1, 0, 2).contiguous()
all_k_pe_flat = all_k_pe.view(num_layers * num_ctx, 1, self._context_rope_dim)
repeated_positions = self._context_positions_repeated[: num_layers * num_ctx]
repeated_positions.view(num_layers, num_ctx).copy_(context_positions)
# Keep the single-tensor context RoPE on vLLM's optimized CUDA op;
# DeepSeek YaRN's FlashInfer wrapper assumes a non-null key tensor.
rotary_emb = self.layers[0].self_attn.rotary_emb
assert rotary_emb is not None
ops.rotary_embedding(
repeated_positions,
all_k_pe_flat,
None,
rotary_emb.head_size,
rotary_emb.cos_sin_cache,
rotary_emb.is_neox_style,
)
all_k_pe = all_k_pe_flat.view(num_layers, num_ctx, 1, self._context_rope_dim)
if context_slot_mapping is None:
return
cache_layers = [layer.self_attn for layer in self.layers]
if (
not is_quantized_kv_cache(cache_layers[0].kv_cache_dtype)
and self._has_uniform_block_layout(cache_layers)
and (
isinstance(context_slot_mapping, torch.Tensor)
or all(s is not None for s in context_slot_mapping)
)
):
# Grouped context KV insert only supports unquantized (bf16) KV cache
# and assumes that all layers share the same block layout.
if isinstance(context_slot_mapping, (list, tuple)):
per_layer_slot_mappings = [
s for s in context_slot_mapping if s is not None
]
if len({s.data_ptr() for s in per_layer_slot_mappings}) == 1:
# All rows alias to the same slot mapping.
slot_mapping = (
per_layer_slot_mappings[0].unsqueeze(0).expand(num_layers, -1)
)
else:
slot_mapping = torch.stack(per_layer_slot_mappings, dim=0)
else:
# Broadcast the single shared context_slot_mapping tensor.
slot_mapping = context_slot_mapping.unsqueeze(0).expand(num_layers, -1)
ref_cache = cache_layers[0].kv_cache
ops.concat_and_cache_mla_grouped(
all_kv_c_normed,
all_k_pe.squeeze(2),
self._get_context_kv_cache_ptrs(cache_layers),
slot_mapping,
ref_cache.size(1),
ref_cache.stride(0),
ref_cache.stride(1),
)
return
for layer_idx, layer in enumerate(self.layers):
slot_mapping = (
context_slot_mapping[layer_idx]
if isinstance(context_slot_mapping, (list, tuple))
else context_slot_mapping
)
if slot_mapping is None:
continue
attn = layer.self_attn
attn.impl.do_kv_cache_update(
all_kv_c_normed[layer_idx],
all_k_pe[layer_idx],
attn.kv_cache,
slot_mapping,
attn.kv_cache_dtype,
attn._k_scale,
)
def _has_uniform_block_layout(
self,
cache_layers: list[MultiHeadLatentAttention],
) -> bool:
if not hasattr(self, "_layers_share_kv_block_layout"):
ref_cache = cache_layers[0].kv_cache
self._layers_share_kv_block_layout = all(
cl.kv_cache.size(1) == ref_cache.size(1)
and cl.kv_cache.stride(0) == ref_cache.stride(0)
and cl.kv_cache.stride(1) == ref_cache.stride(1)
for cl in cache_layers
)
return self._layers_share_kv_block_layout
def _get_context_kv_cache_ptrs(
self,
cache_layers: list[MultiHeadLatentAttention],
) -> torch.Tensor:
# The per-layer KV cache base pointers are stable after allocation, so
# build the pointer array once and return it on every call.
if not hasattr(self, "_context_cache_ptrs"):
ref_cache = cache_layers[0].kv_cache
cache_ptrs = torch.tensor(
[cl.kv_cache.data_ptr() for cl in cache_layers],
dtype=torch.int64,
device=ref_cache.device,
)
self._context_cache_ptrs = cache_ptrs
return self._context_cache_ptrs
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
inputs_embeds: torch.Tensor | None = None,
) -> torch.Tensor:
if inputs_embeds is None:
inputs_embeds = self.embed_input_ids(input_ids)
hidden_states = inputs_embeds
residual = None
for layer in self.layers:
hidden_states, residual = layer(
positions=positions,
hidden_states=hidden_states,
residual=residual,
)
hidden_states, _ = self.final_norm(hidden_states, residual)
return hidden_states