Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

4307 lines
159 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Inference-only DeepSeek V4 model skeleton.
This module intentionally registers only architecture pieces that map to the
DeepSeek V4 Flash checkpoint. The sparse MLA forward path still fails loudly
until the HCA/CSA cache kernels are wired into TokenSpeed.
"""
from __future__ import annotations
import os
import re
from collections.abc import Iterable
from dataclasses import dataclass
import torch
import torch.nn.functional as F
try:
# Optional dependency; the module-level wrapper imports the external
# `deep_gemm` package unguarded, which is not installed in baseline V4
# builds. Callsites guard usage with `deep_gemm is not None`.
from tokenspeed_kernel.thirdparty import deep_gemm
except ImportError:
deep_gemm = None # type: ignore[assignment]
from tokenspeed_kernel.ops.attention.cuda.deepseek_v4 import (
has_indexer_mxfp4_paged_gather,
has_persistent_topk,
indexer_mxfp4_paged_gather,
persistent_topk,
)
from tokenspeed_kernel.ops.attention.triton.deepseek_v4 import (
deepseek_v4_indexer_decode_metadata_compute,
)
from tokenspeed_kernel.platform import current_platform
from tokenspeed_kernel.thirdparty.cuda import (
dsv3_router_gemm,
hash_softplus_sqrt_topk_flash,
softplus_sqrt_topk_flash,
)
from tokenspeed_kernel.thirdparty.triton import (
stage_deepseek_v4_mega_moe_inputs as _stage_deepseek_v4_mega_moe_inputs,
)
from tokenspeed_kernel.thirdparty.trtllm import (
fast_topk_v2,
)
from torch import nn
from transformers import PretrainedConfig
from tokenspeed.runtime.configs.deepseek_v4_cache_spec import (
DEEPSEEK_V4_MXFP4_BLOCK_SIZE,
V4_KERNEL_BLOCK_ROWS,
deepseek_v4_indexer_mxfp4_layout_from_row_bytes,
deepseek_v4_indexer_mxfp4_scale_dim,
deepseek_v4_indexer_mxfp4_value_bytes,
deepseek_v4_nope_dim,
v4_compressed_kv_group_id,
)
from tokenspeed.runtime.distributed import Mapping
from tokenspeed.runtime.distributed.comm_manager import CommManager
from tokenspeed.runtime.distributed.process_group_manager import (
process_group_manager as pg_manager,
)
from tokenspeed.runtime.execution.breakable_cuda_graph import (
break_point,
current_forward_ctx,
slice_to_real_tokens,
)
from tokenspeed.runtime.execution.context import ForwardContext
from tokenspeed.runtime.execution.cuda_graph_wrapper import get_is_capture_mode
from tokenspeed.runtime.execution.forward_batch_info import ForwardMode
from tokenspeed.runtime.layers.attention.deepseek_v4.metadata import (
DeepseekV4ForwardMetadata,
DeepseekV4IndexerBatchMetadata,
DeepseekV4IndexerDecodePlan,
DeepseekV4IndexerPrefillChunkPlan,
DeepseekV4IndexerPrefillMetadata,
DeepseekV4SparseIndexerMetadata,
)
from tokenspeed.runtime.layers.attention.deepseek_v4_ops import (
deepseek_v4_csa_compress_kv_cache_insert,
deepseek_v4_csa_indexer_cache_insert,
deepseek_v4_fused_inv_rope_fp8_quant,
deepseek_v4_hca_compress_kv_cache_insert,
deepseek_v4_prepare_indexer_q_mxfp4,
fused_qnorm_rope_kv_insert,
save_deepseek_v4_compressor_state,
)
from tokenspeed.runtime.layers.attention.kv_cache.deepseek_v4 import (
_group_slot_mapping_from_raw,
_mask_invalid_graph_tokens,
)
from tokenspeed.runtime.layers.deepseek_v4_mhc import mhc_fused_hc as fast_mhc_fused_hc
from tokenspeed.runtime.layers.deepseek_v4_mhc import mhc_post as fast_mhc_post
from tokenspeed.runtime.layers.deepseek_v4_mhc import mhc_pre as fast_mhc_pre
from tokenspeed.runtime.layers.layernorm import FusedRMSNorm, RMSNorm
from tokenspeed.runtime.layers.linear import (
ColumnParallelLinear,
MergedColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from tokenspeed.runtime.layers.moe import (
ExpertCheckpointSchema,
build_moe_checkpoint_loader,
)
from tokenspeed.runtime.layers.moe.expert import MoELayer
from tokenspeed.runtime.layers.moe.topk import (
BypassedTopKOutput,
StandardTopKOutput,
TopK,
)
from tokenspeed.runtime.layers.moe.utils import RoutingMethodType, get_moe_backend
from tokenspeed.runtime.layers.quantization import Mxfp4Config
from tokenspeed.runtime.layers.quantization.base_config import QuantizationConfig
from tokenspeed.runtime.layers.rotary_embedding import get_rope
from tokenspeed.runtime.layers.vocab_parallel_embedding import VocabParallelEmbedding
from tokenspeed.runtime.model_loader.weight_utils import default_weight_loader
from tokenspeed.runtime.models.base import BaseCausalLM
from tokenspeed.runtime.moe.expert_location import ModelConfigForExpertLocation
from tokenspeed.runtime.utils import (
add_prefix,
get_colorful_logger,
set_weight_attrs,
)
from tokenspeed.runtime.utils.cuda_stream import StreamFork
from tokenspeed.runtime.utils.custom_ops import direct_register_custom_op
from tokenspeed.runtime.utils.env import global_server_args_dict, pdl_enabled
from tokenspeed.runtime.utils.nvtx import nvtx_range
_platform = current_platform()
logger = get_colorful_logger(__name__)
def _deepseek_v4_metadata_matches_tokens(metadata, num_tokens: int) -> bool:
return (
metadata is not None
and getattr(metadata, "token_to_req_indices", None) is not None
and metadata.token_to_req_indices.numel() == num_tokens
)
def _deepseek_v4_indexer_token_split(
forward_mode: ForwardMode | None,
metadata,
total_tokens: int,
) -> tuple[int, int]:
if forward_mode is not None and forward_mode.is_mixed():
return int(metadata.num_prefill_tokens), metadata.decode_token_count()
if forward_mode is not None and forward_mode.is_decode():
return 0, int(total_tokens)
return int(total_tokens), 0
def _deepseek_v4_forward_metadata(ctx: ForwardContext):
metadata = getattr(ctx.attn_backend, "forward_metadata", None)
forward_mode = getattr(ctx, "forward_mode", None)
if forward_mode is not None and forward_mode.is_extend_or_mixed():
return getattr(ctx.attn_backend, "forward_prefill_metadata", None) or metadata
if forward_mode is not None and forward_mode.is_decode_or_idle():
input_num_tokens = getattr(ctx, "input_num_tokens", None)
decode_metadata = getattr(ctx.attn_backend, "forward_decode_metadata", None)
if input_num_tokens is not None and _deepseek_v4_metadata_matches_tokens(
decode_metadata,
input_num_tokens,
):
return decode_metadata
prefill_metadata = getattr(ctx.attn_backend, "forward_prefill_metadata", None)
if input_num_tokens is not None and _deepseek_v4_metadata_matches_tokens(
prefill_metadata,
input_num_tokens,
):
return prefill_metadata
return decode_metadata or metadata or prefill_metadata
return metadata
def _dequant_fp8_weight(layer: nn.Module, shape: tuple[int, ...]) -> torch.Tensor:
weight = layer.weight.view(*shape)
scale = getattr(layer, "weight_scale_inv", None)
if scale is None or weight.dtype != torch.float8_e4m3fn:
return weight.float()
block_n, block_k = getattr(layer.quant_config, "weight_block_size", (128, 128))
if len(shape) == 2:
out_dim, in_dim = shape
scale = scale.view(
(out_dim + block_n - 1) // block_n,
(in_dim + block_k - 1) // block_k,
)
expanded_scale = (
scale.float()
.repeat_interleave(block_n, dim=0)
.repeat_interleave(block_k, dim=1)
)
return weight.float() * expanded_scale[:out_dim, :in_dim]
groups, out_dim, in_dim = shape
scale = scale.view(
groups,
(out_dim + block_n - 1) // block_n,
(in_dim + block_k - 1) // block_k,
)
expanded_scale = (
scale.float()
.repeat_interleave(block_n, dim=1)
.repeat_interleave(block_k, dim=2)
)
return weight.float() * expanded_scale[:, :out_dim, :in_dim]
def _deepseek_v4_router_gemm(
hidden_states: torch.Tensor,
weight: torch.Tensor,
) -> torch.Tensor:
if (
hidden_states.dim() == 2
and hidden_states.shape[0] > 0
and hidden_states.is_cuda
and hidden_states.dtype == torch.bfloat16
and weight.dtype in (torch.bfloat16, torch.float32)
and (_platform.is_hopper or _platform.is_blackwell)
):
return dsv3_router_gemm(
hidden_states,
weight,
out_dtype=torch.float32,
enable_pdl=pdl_enabled(),
)
x = (
hidden_states
if hidden_states.dtype == weight.dtype
else hidden_states.to(weight.dtype)
)
return F.linear(x, weight, None).to(torch.float32)
def _deepseek_v4_bf16_linear_fp32(
hidden_states: torch.Tensor,
weight: torch.Tensor,
) -> torch.Tensor | None:
if (
hidden_states.dim() == 2
and hidden_states.shape[0] > 0
and hidden_states.is_cuda
and hidden_states.dtype == torch.bfloat16
and weight.is_cuda
and weight.dtype == torch.bfloat16
and weight.dim() == 2
and hidden_states.shape[1] == weight.shape[1]
and (_platform.is_hopper or _platform.is_blackwell)
):
return dsv3_router_gemm(
hidden_states,
weight,
out_dtype=torch.float32,
enable_pdl=False,
)
return None
def _deepseek_v4_fused_select_experts(
router_logits: torch.Tensor,
top_k: int,
renormalize: bool,
*,
correction_bias: torch.Tensor | None = None,
hash_indices_table: torch.Tensor | None = None,
input_ids: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor] | None:
if (
not router_logits.is_cuda
or router_logits.dim() != 2
or top_k <= 0
or top_k > 32
or router_logits.dtype not in (torch.float32, torch.float16, torch.bfloat16)
):
return None
num_experts = router_logits.shape[1]
topk_weights = torch.empty(
router_logits.shape[0],
top_k,
dtype=torch.float32,
device=router_logits.device,
)
topk_ids = torch.empty(
router_logits.shape[0],
top_k,
dtype=torch.int32,
device=router_logits.device,
)
if num_experts not in (256, 384) or top_k != 6 or not renormalize:
return None
logits_f32 = router_logits.float().contiguous()
try:
if hash_indices_table is not None:
if input_ids is None:
raise ValueError("hash-routed DeepSeek V4 MoE requires input_ids")
hash_softplus_sqrt_topk_flash(
logits_f32,
input_ids.reshape(-1).to(device=router_logits.device).contiguous(),
hash_indices_table.to(
device=router_logits.device, dtype=torch.int32
).contiguous(),
topk_ids,
topk_weights,
1.0,
renormalize,
)
elif correction_bias is not None:
softplus_sqrt_topk_flash(
logits_f32,
correction_bias.to(
device=router_logits.device, dtype=torch.float32
).contiguous(),
topk_ids,
topk_weights,
1.0,
renormalize,
)
else:
return None
except (AttributeError, RuntimeError):
return None
return topk_weights, topk_ids
def _deepseek_v4_reorder_c4_ape_2604(ape: torch.Tensor) -> torch.Tensor:
"""Convert C4 overlap APE from checkpoint layout to runtime window layout."""
if ape.dim() != 2 or ape.shape[0] != 4 or ape.shape[1] % 2 != 0:
raise ValueError(f"expected C4 APE [4, even], got {tuple(ape.shape)}")
older, newer = ape.chunk(2, dim=-1)
return torch.cat([older, newer], dim=0).reshape_as(ape)
def mhc_pre(
residual: torch.Tensor,
fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_eps: float,
hc_eps: float,
sinkhorn_iters: int,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return fast_mhc_pre(
residual,
fn,
hc_scale,
hc_base,
rms_eps,
hc_eps,
sinkhorn_iters,
)
def mhc_post(
hidden_states: torch.Tensor,
residual: torch.Tensor,
post: torch.Tensor,
comb: torch.Tensor,
) -> torch.Tensor:
return fast_mhc_post(hidden_states, residual, post, comb)
def mhc_fused_hc(
x_prev: torch.Tensor,
residual_prev: torch.Tensor,
post_prev: torch.Tensor,
comb_prev: torch.Tensor,
fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_eps: float,
hc_eps: float,
sinkhorn_iters: int,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
return fast_mhc_fused_hc(
x_prev,
residual_prev,
post_prev,
comb_prev,
fn,
hc_scale,
hc_base,
rms_eps,
hc_eps,
sinkhorn_iters,
)
def hc_head(
hidden_states: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_norm_eps: float,
hc_eps: float,
) -> torch.Tensor:
shape, dtype = hidden_states.size(), hidden_states.dtype
x = hidden_states.flatten(1).float()
rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + rms_norm_eps)
mixes = F.linear(x, hc_fn.float()) * rsqrt
pre = torch.sigmoid(mixes * hc_scale.float() + hc_base.float()) + hc_eps
y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=1)
return y.to(dtype)
def deepseek_v4_select_experts(
router_logits: torch.Tensor,
top_k: int,
renormalize: bool,
*,
correction_bias: torch.Tensor | None = None,
hash_indices_table: torch.Tensor | None = None,
input_ids: torch.Tensor | None = None,
need_scores: bool = True,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""DeepSeek V4 MoE routing.
DeepSeek V4 uses sqrt(softplus(logits)) as expert scores. Correction bias
only affects expert selection; the gathered expert weights come from the
unbiased scores. Hash-routed layers use checkpoint-provided expert ids but
still gather weights from the gate scores.
Set ``need_scores=False`` when the caller discards the third return value
(e.g. mega_moe) to skip the redundant sqrt(softplus(logits)) computation.
"""
fused_topk = _deepseek_v4_fused_select_experts(
router_logits,
top_k,
renormalize,
correction_bias=correction_bias,
hash_indices_table=hash_indices_table,
input_ids=input_ids,
)
if fused_topk is not None:
topk_weights, topk_ids = fused_topk
if need_scores:
scores = torch.sqrt(F.softplus(router_logits.float()))
else:
scores = router_logits
return topk_weights, topk_ids, scores
scores = torch.sqrt(F.softplus(router_logits.float()))
if hash_indices_table is not None:
if input_ids is None:
raise ValueError("hash-routed DeepSeek V4 MoE requires input_ids")
topk_ids = hash_indices_table[input_ids.reshape(-1)].to(
device=scores.device,
dtype=torch.long,
)
else:
scores_for_choice = scores
if correction_bias is not None:
scores_for_choice = scores_for_choice + correction_bias.to(
device=scores.device,
dtype=scores.dtype,
).unsqueeze(0)
topk_ids = torch.topk(scores_for_choice, k=top_k, dim=-1, sorted=True)[1]
topk_weights = scores.gather(1, topk_ids)
if renormalize:
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True).clamp_min(
torch.finfo(topk_weights.dtype).tiny
)
return topk_weights.to(torch.float32), topk_ids.to(torch.int32), scores
def pack_topk_as_router_logits(
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
num_experts: int,
) -> torch.Tensor:
"""Encode preselected top-k weights for BYPASSED TokenSpeed MoE backends.
MXFP4 backends currently build routing data from logits internally. Packing
the normalized top-k weights as log-probabilities with very negative values
elsewhere makes their TopK -> Softmax/Renormalize route recover the same
selected ids and weights without changing the shared backend.
"""
router_logits = torch.full(
(topk_ids.shape[0], num_experts),
-1e20,
dtype=torch.float32,
device=topk_weights.device,
)
safe_weights = topk_weights.clamp_min(torch.finfo(torch.float32).tiny)
router_logits.scatter_(1, topk_ids.long(), safe_weights.log())
return router_logits
def _deepseek_v4_deepgemm_fp4_indexer_available(index_q: torch.Tensor) -> bool:
return (
deep_gemm is not None
and index_q.is_cuda
and index_q.dim() >= 3
and index_q.shape[-2] in (32, 64)
and (index_q.shape[-1] * 2) % DEEPSEEK_V4_MXFP4_BLOCK_SIZE == 0
)
def _deepseek_v4_indexer_mxfp4_cache_view(
cache_2d: torch.Tensor,
block_size: int,
) -> torch.Tensor:
row_bytes = cache_2d.shape[1] // block_size
return torch.as_strided(
cache_2d,
(cache_2d.shape[0], block_size, 1, row_bytes),
(cache_2d.stride(0), row_bytes, row_bytes, 1),
)
def _deepseek_v4_gather_paged_indexer_mxfp4_cache(
cache_2d: torch.Tensor,
block_table: torch.Tensor,
cu_seq_lens: torch.Tensor,
block_size: int,
out: tuple[torch.Tensor, torch.Tensor] | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
_, value_bytes, scale_bytes = deepseek_v4_indexer_mxfp4_layout_from_row_bytes(
cache_2d.shape[1] // block_size
)
if out is None:
total_rows = int(cu_seq_lens[-1].item()) if cu_seq_lens.numel() else 0
values = torch.empty(
(total_rows, value_bytes),
dtype=torch.uint8,
device=cache_2d.device,
)
scales = torch.empty(
(total_rows, scale_bytes),
dtype=torch.uint8,
device=cache_2d.device,
)
else:
if out[0].shape[0] != out[1].shape[0]:
raise ValueError(
"DeepSeek V4 paged gather workspace value/scale rows must match, "
f"got values={out[0].shape[0]}, scales={out[1].shape[0]}"
)
total_rows = int(out[0].shape[0])
values = out[0][:total_rows]
scales = out[1][:total_rows]
if total_rows == 0:
return values.view(torch.int8), scales.view(torch.int32).squeeze(-1)
if not (cache_2d.is_cuda and block_table.is_cuda and cu_seq_lens.is_cuda):
raise RuntimeError(
"DeepSeek V4 paged MXFP4 gather requires cache, block table, "
"and sequence lengths on CUDA"
)
if not has_indexer_mxfp4_paged_gather():
raise RuntimeError(
"DeepSeek V4 paged MXFP4 gather requires the CUDA paged gather op"
)
indexer_mxfp4_paged_gather(
kv_cache=cache_2d,
values_out=values,
scales_out=scales,
block_table=block_table,
cu_seq_lens=cu_seq_lens,
cache_block_size=block_size,
)
return values.view(torch.int8), scales.view(torch.int32).squeeze(-1)
def _deepseek_v4_indexer_topk_from_logits(
logits: torch.Tensor,
lengths: torch.Tensor,
topk_tokens: int,
*,
next_n: int = 1,
use_prefill_topk_op: bool = False,
row_starts: torch.Tensor | None = None,
row_ends: torch.Tensor | None = None,
out: torch.Tensor | None = None,
persistent_topk_workspace: torch.Tensor | None = None,
) -> torch.Tensor:
if not logits.is_cuda or logits.dtype != torch.float32:
raise RuntimeError("DeepSeek V4 indexer top-k requires CUDA float32 logits")
if not lengths.is_cuda or lengths.device != logits.device:
raise RuntimeError(
"DeepSeek V4 indexer top-k requires CUDA length tensors "
"on the logits device"
)
if row_starts is not None and (
not row_starts.is_cuda or row_starts.device != logits.device
):
raise RuntimeError(
"DeepSeek V4 indexer top-k requires row_starts on the logits device"
)
if row_ends is not None and (
not row_ends.is_cuda or row_ends.device != logits.device
):
raise RuntimeError(
"DeepSeek V4 indexer top-k requires row_ends on the logits device"
)
lengths_for_kernel = lengths.to(torch.int32).contiguous()
length_rows = lengths_for_kernel.reshape(-1)
num_tokens = length_rows.numel()
if out is None:
topk = torch.empty(
(num_tokens, topk_tokens),
device=logits.device,
dtype=torch.int32,
)
else:
topk = out[:num_tokens]
topk.fill_(-1)
if num_tokens == 0:
return topk
max_len = logits.shape[1] if logits.dim() == 2 else 0
if max_len <= 0:
return topk
row_starts_for_kernel: torch.Tensor | None = None
row_ends_for_kernel: torch.Tensor | None = None
if row_starts is not None or row_ends is not None:
if row_starts is None:
row_starts_for_kernel = torch.zeros_like(length_rows)
else:
row_starts_for_kernel = row_starts.to(torch.int32).reshape(-1)
if row_ends is None:
row_ends_for_kernel = row_starts_for_kernel + length_rows
else:
row_ends_for_kernel = row_ends.to(torch.int32).reshape(-1)
length_rows = (row_ends_for_kernel - row_starts_for_kernel).clamp_min(0)
if use_prefill_topk_op:
return _deepseek_v4_indexer_topk_from_logits_prefill_op(
logits,
length_rows,
topk_tokens,
row_starts=row_starts_for_kernel,
row_ends=row_ends_for_kernel,
out=topk,
)
if row_starts_for_kernel is not None or row_ends_for_kernel is not None:
raise RuntimeError(
"DeepSeek V4 decode indexer top-k does not support row ranges"
)
if topk_tokens not in (512, 1024, 2048):
raise RuntimeError(
"DeepSeek V4 decode indexer top-k supports topk_tokens in "
"{512, 1024, 2048}"
)
if (
persistent_topk_workspace is not None
and persistent_topk_workspace.is_cuda
and persistent_topk_workspace.device == logits.device
and persistent_topk_workspace.numel() >= 1024 * 1024
and persistent_topk_workspace.dtype == torch.uint8
):
if not has_persistent_topk():
raise RuntimeError(
"DeepSeek V4 persistent top-k workspace was provided, "
"but the persistent top-k kernel is unavailable"
)
persistent_topk(
logits.contiguous(),
lengths_for_kernel,
topk,
persistent_topk_workspace,
topk_tokens,
max_len,
)
return topk
fast_topk_v2(
logits.contiguous(),
lengths_for_kernel,
topk,
topk_tokens,
next_n,
)
return topk
def _deepseek_v4_indexer_topk_from_logits_prefill_op(
logits: torch.Tensor,
length_rows: torch.Tensor,
topk_tokens: int,
*,
row_starts: torch.Tensor | None = None,
row_ends: torch.Tensor | None = None,
out: torch.Tensor,
) -> torch.Tensor:
"""Use the local TRT-LLM CUDA prefill selector."""
if not logits.is_cuda or logits.dtype != torch.float32:
raise RuntimeError("DeepSeek V4 prefill indexer requires CUDA float32 logits")
trtllm_ops = getattr(torch.ops, "trtllm", None)
if trtllm_ops is None or not hasattr(trtllm_ops, "indexer_topk_prefill"):
raise RuntimeError(
"DeepSeek V4 prefill indexer requires the CUDA prefill top-k op"
)
num_rows = length_rows.numel()
if num_rows == 0:
return out[:0]
logits = logits.contiguous()
if row_starts is None:
row_starts_for_kernel = torch.zeros(
num_rows,
device=logits.device,
dtype=torch.int32,
)
else:
row_starts_for_kernel = (
row_starts.to(
device=logits.device,
dtype=torch.int32,
)
.reshape(-1)
.contiguous()
)
if row_ends is None:
row_ends_for_kernel = (
row_starts_for_kernel
+ length_rows.to(device=logits.device, dtype=torch.int32).reshape(-1)
).contiguous()
else:
row_ends_for_kernel = (
row_ends.to(
device=logits.device,
dtype=torch.int32,
)
.reshape(-1)
.contiguous()
)
topk = out[:num_rows]
topk.fill_(-1)
trtllm_ops.indexer_topk_prefill(
logits,
row_starts_for_kernel,
row_ends_for_kernel,
topk,
topk_tokens,
)
return topk
@dataclass(frozen=True)
class _DeepseekV4IndexerPrefillChunk:
token_start: int
token_end: int
req_start: int
req_end: int
query_start: int
query_end: int
skip_kv_gather: bool = False
def _deepseek_v4_indexer_prefill_max_logits_bytes(
max_logits_bytes: int | None = None,
) -> int:
if max_logits_bytes is not None:
return max(1, int(max_logits_bytes))
max_logits_mb = global_server_args_dict["deepseek_v4_indexer_prefill_max_logits_mb"]
return max(1, int(max_logits_mb) * 1024 * 1024)
def _deepseek_v4_indexer_prefill_workspace_size(
seq_lens_cpu: torch.Tensor,
workspace_size: int | None = None,
) -> int:
if workspace_size is not None:
return max(1, int(workspace_size))
context_len = global_server_args_dict.get("max_model_len")
if isinstance(context_len, int) and context_len > 0:
return context_len * 40
max_seq_len = int(seq_lens_cpu.max().item()) if seq_lens_cpu.numel() else 1
return max(1, max_seq_len) * 40
def _deepseek_v4_indexer_prefill_request_chunks(
*,
seq_lens_cpu: torch.Tensor,
query_lens_cpu: torch.Tensor,
compress_ratio: int,
num_tokens: int,
max_logits_bytes: int | None = None,
workspace_size: int | None = None,
request_offset: int = 0,
) -> list[_DeepseekV4IndexerPrefillChunk]:
"""Build request/query-slice sparse-indexer prefill chunks."""
if num_tokens == 0:
return []
seq_lens = seq_lens_cpu.detach().cpu().to(torch.int64)
query_lens = query_lens_cpu.detach().cpu().to(torch.int64)
if seq_lens.numel() != query_lens.numel():
return []
query_lens_list = [max(0, int(x)) for x in query_lens.tolist()]
if sum(query_lens_list) != num_tokens:
return []
compressed_seq_lens = torch.div(
seq_lens,
max(1, int(compress_ratio)),
rounding_mode="floor",
)
compressed_seq_lens_list = [max(0, int(x)) for x in compressed_seq_lens.tolist()]
workspace_rows = _deepseek_v4_indexer_prefill_workspace_size(
seq_lens,
workspace_size,
)
max_logits_elems = (
_deepseek_v4_indexer_prefill_max_logits_bytes(max_logits_bytes) // 4
)
max_logits_elems = max(1, max_logits_elems)
query_offsets = [0]
for query_len in query_lens_list:
query_offsets.append(query_offsets[-1] + query_len)
chunks: list[_DeepseekV4IndexerPrefillChunk] = []
n_reqs = len(query_lens_list)
end = 0
while end < n_reqs:
start = end
chunk_m = 0
chunk_n = 0
while end < n_reqs:
q_len = query_lens_list[end]
seq_len = compressed_seq_lens_list[end]
new_m = chunk_m + q_len
new_n = chunk_n + seq_len
if new_n <= workspace_rows and new_m * new_n <= max_logits_elems:
chunk_m = new_m
chunk_n = new_n
end += 1
else:
break
if end == start:
chunk_m = query_lens_list[end]
chunk_n = compressed_seq_lens_list[end]
end += 1
if chunk_m <= 0:
continue
req_start = start + request_offset
req_end = end + request_offset
max_q = max(1, max_logits_elems // chunk_n) if chunk_n > 0 else chunk_m
chunk_token_start = query_offsets[start]
for query_start in range(0, chunk_m, max_q):
query_end = min(query_start + max_q, chunk_m)
chunks.append(
_DeepseekV4IndexerPrefillChunk(
token_start=chunk_token_start + query_start,
token_end=chunk_token_start + query_end,
req_start=req_start,
req_end=req_end,
query_start=query_start,
query_end=query_end,
skip_kv_gather=query_start > 0,
)
)
return chunks
def _deepseek_v4_indexer_decode_max_len(
block_table: torch.Tensor,
cache_block_size: int,
compress_ratio: int,
) -> int:
context_len = global_server_args_dict.get("max_model_len")
if isinstance(context_len, int) and context_len > 0:
return max(1, (context_len + compress_ratio - 1) // compress_ratio)
return max(
1,
(block_table.shape[1] * cache_block_size + compress_ratio - 1)
// compress_ratio,
)
def _deepseek_v4_indexer_prefill_request_gather_plan(
*,
seq_lens_cpu: torch.Tensor,
query_lens_cpu: torch.Tensor,
block_table: torch.Tensor,
cache_block_size: int,
compress_ratio: int,
req_start: int,
req_end: int,
query_start: int,
query_end: int,
build_slots: bool = True,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int]:
device = block_table.device
num_rows = max(0, int(query_end) - int(query_start))
if num_rows == 0 or req_end <= req_start:
empty_i32 = torch.empty(0, dtype=torch.int32, device=device)
empty_i64 = torch.empty(0, dtype=torch.int64, device=device)
return empty_i64, empty_i32, empty_i32, empty_i32, 0
seq_lens_list = (
seq_lens_cpu.detach().cpu().to(torch.int64)[req_start:req_end].tolist()
)
query_lens_list = (
query_lens_cpu.detach().cpu().to(torch.int64)[req_start:req_end].tolist()
)
if len(seq_lens_list) != len(query_lens_list):
empty_i32 = torch.empty(0, dtype=torch.int32, device=device)
empty_i64 = torch.empty(0, dtype=torch.int64, device=device)
return empty_i64, empty_i32, empty_i32, empty_i32, 0
ratio = max(1, int(compress_ratio))
seq_lens_list = [max(0, int(x)) for x in seq_lens_list]
query_lens_list = [max(0, int(x)) for x in query_lens_list]
compressed_lens_list = [seq_len // ratio for seq_len in seq_lens_list]
total_k = sum(compressed_lens_list)
query_offsets: list[int] = [0]
for query_len in query_lens_list:
query_offsets.append(query_offsets[-1] + query_len)
req_local_list: list[int] = []
row_lens_list: list[int] = []
req_local = 0
last_req = max(0, len(query_lens_list) - 1)
for row_offset in range(int(query_start), int(query_end)):
while req_local < last_req and row_offset >= query_offsets[req_local + 1]:
req_local += 1
local_query_offset = row_offset - query_offsets[req_local]
prefix_len = max(0, seq_lens_list[req_local] - query_lens_list[req_local])
row_lens_list.append((prefix_len + local_query_offset + 1) // ratio)
req_local_list.append(req_local)
max_len = max(row_lens_list) if row_lens_list else 0
compressed_lens = torch.tensor(
compressed_lens_list,
dtype=torch.int64,
device=device,
)
cu_seq_lens = torch.empty(
compressed_lens.numel() + 1,
dtype=torch.int32,
device=device,
)
cu_seq_lens[:1] = 0
torch.cumsum(compressed_lens.to(torch.int32), dim=0, out=cu_seq_lens[1:])
req_local_tensor = torch.tensor(req_local_list, dtype=torch.int64, device=device)
row_lens = torch.tensor(row_lens_list, dtype=torch.int32, device=device)
cu_start = cu_seq_lens[:-1][req_local_tensor]
cu_end = cu_start + row_lens
if total_k <= 0 or not build_slots:
empty_i64 = torch.empty(0, dtype=torch.int64, device=device)
return empty_i64, cu_start, cu_end, row_lens, max_len
req_ids = torch.repeat_interleave(
torch.arange(req_start, req_end, device=device, dtype=torch.int64),
compressed_lens,
output_size=total_k,
)
req_local_for_k = req_ids - int(req_start)
group_bases = cu_seq_lens[:-1][req_local_for_k].to(torch.int64)
local = torch.arange(total_k, device=device, dtype=torch.int64) - group_bases
pages = torch.div(local, cache_block_size, rounding_mode="floor")
page_offsets = local % cache_block_size
page_ids = block_table[req_ids, pages.long()].to(torch.int64)
slots = page_ids * cache_block_size + page_offsets
return slots, cu_start, cu_end, row_lens, max_len
def _deepseek_v4_indexer_prefill_chunk_total_rows(
*,
seq_lens_cpu: torch.Tensor,
compress_ratio: int,
req_start: int,
req_end: int,
) -> int:
ratio = max(1, int(compress_ratio))
seq_lens = seq_lens_cpu.detach().cpu().to(torch.int64)[req_start:req_end].tolist()
return sum(max(0, int(seq_len)) // ratio for seq_len in seq_lens)
def _deepseek_v4_indexer_prefill_metadata(
*,
metadata: DeepseekV4ForwardMetadata,
block_table: torch.Tensor,
cache_block_size: int,
compress_ratio: int,
num_prefill_tokens: int,
) -> DeepseekV4IndexerPrefillMetadata:
device = block_table.device
if num_prefill_tokens <= 0:
return DeepseekV4IndexerPrefillMetadata.empty(device)
seq_lens_cpu = getattr(metadata, "seq_lens_cpu", None)
query_lens_cpu = getattr(metadata, "query_lens_cpu", None)
num_prefill_reqs = int(getattr(metadata, "num_prefill_reqs", 0) or 0)
if seq_lens_cpu is None or query_lens_cpu is None or num_prefill_reqs <= 0:
return DeepseekV4IndexerPrefillMetadata.empty(device)
seq_lens_cpu = seq_lens_cpu[:num_prefill_reqs]
query_lens_cpu = query_lens_cpu[:num_prefill_reqs]
cache_key = (compress_ratio, cache_block_size, num_prefill_tokens)
cache = metadata.indexer.prefill_plan_cache
cached = cache.get(cache_key)
if cached is not None and cached.slots.device == device:
return cached
chunks = _deepseek_v4_indexer_prefill_request_chunks(
seq_lens_cpu=seq_lens_cpu,
query_lens_cpu=query_lens_cpu,
compress_ratio=compress_ratio,
num_tokens=num_prefill_tokens,
)
if not chunks:
out = DeepseekV4IndexerPrefillMetadata.empty(device)
cache[cache_key] = out
return out
chunk_plans: list[DeepseekV4IndexerPrefillChunkPlan] = []
slot_parts: list[torch.Tensor] = []
cu_seq_lens_parts: list[torch.Tensor] = []
cu_seqlen_k_start_parts: list[torch.Tensor] = []
cu_seqlen_k_end_parts: list[torch.Tensor] = []
seq_lens_k_parts: list[torch.Tensor] = []
slot_offset = 0
cu_seq_offset = 0
row_offset = 0
for chunk in chunks:
slots, cu_seqlen_k_start, cu_seqlen_k_end, seq_lens_k, max_seqlen_k = (
_deepseek_v4_indexer_prefill_request_gather_plan(
seq_lens_cpu=seq_lens_cpu,
query_lens_cpu=query_lens_cpu,
block_table=block_table,
cache_block_size=cache_block_size,
compress_ratio=compress_ratio,
req_start=chunk.req_start,
req_end=chunk.req_end,
query_start=chunk.query_start,
query_end=chunk.query_end,
build_slots=False,
)
)
slot_count = _deepseek_v4_indexer_prefill_chunk_total_rows(
seq_lens_cpu=seq_lens_cpu,
compress_ratio=compress_ratio,
req_start=chunk.req_start,
req_end=chunk.req_end,
)
compressed_lens = torch.div(
seq_lens_cpu[chunk.req_start : chunk.req_end].to(
dtype=torch.int32,
device=device,
),
max(1, int(compress_ratio)),
rounding_mode="floor",
)
cu_seq_lens = torch.empty(
compressed_lens.numel() + 1,
dtype=torch.int32,
device=device,
)
cu_seq_lens[:1] = 0
torch.cumsum(compressed_lens, dim=0, out=cu_seq_lens[1:])
slot_end = slot_offset + slot_count
cu_seq_end = cu_seq_offset + cu_seq_lens.numel()
row_end = row_offset + seq_lens_k.numel()
chunk_plans.append(
DeepseekV4IndexerPrefillChunkPlan(
token_start=chunk.token_start,
token_end=chunk.token_end,
request_start=chunk.req_start,
request_end=chunk.req_end,
slot_start=slot_offset,
slot_end=slot_end,
gather_row_start=row_offset,
gather_row_end=row_end,
max_seq_len_k=max_seqlen_k,
cu_seq_lens_start=cu_seq_offset,
cu_seq_lens_end=cu_seq_end,
skip_kv_gather=chunk.skip_kv_gather,
)
)
if slots.numel() > 0:
slot_parts.append(slots)
cu_seq_lens_parts.append(cu_seq_lens)
cu_seqlen_k_start_parts.append(cu_seqlen_k_start)
cu_seqlen_k_end_parts.append(cu_seqlen_k_end)
seq_lens_k_parts.append(seq_lens_k)
slot_offset = slot_end
cu_seq_offset = cu_seq_end
row_offset = row_end
out = DeepseekV4IndexerPrefillMetadata(
chunks=tuple(chunk_plans),
chunk_specs=torch.tensor(
[
[
chunk.token_start,
chunk.token_end,
chunk.request_start,
chunk.request_end,
1 if chunk.skip_kv_gather else 0,
]
for chunk in chunk_plans
],
dtype=torch.int64,
device="cpu",
),
chunk_offsets=torch.tensor(
[
[
chunk.slot_start,
chunk.slot_end,
chunk.gather_row_start,
chunk.gather_row_end,
chunk.max_seq_len_k,
chunk.cu_seq_lens_start,
chunk.cu_seq_lens_end,
]
for chunk in chunk_plans
],
dtype=torch.int64,
device="cpu",
),
slots=(
torch.cat(slot_parts, dim=0)
if slot_parts
else torch.empty(0, dtype=torch.int64, device=device)
),
cu_seq_lens=(
torch.cat(cu_seq_lens_parts, dim=0)
if cu_seq_lens_parts
else torch.empty(0, dtype=torch.int32, device=device)
),
cu_seqlen_k_start=(
torch.cat(cu_seqlen_k_start_parts, dim=0)
if cu_seqlen_k_start_parts
else torch.empty(0, dtype=torch.int32, device=device)
),
cu_seqlen_k_end=(
torch.cat(cu_seqlen_k_end_parts, dim=0)
if cu_seqlen_k_end_parts
else torch.empty(0, dtype=torch.int32, device=device)
),
seq_lens_k=(
torch.cat(seq_lens_k_parts, dim=0)
if seq_lens_k_parts
else torch.empty(0, dtype=torch.int32, device=device)
),
)
cache[cache_key] = out
return out
def _deepseek_v4_indexer_decode_plan(
*,
positions: torch.Tensor,
token_to_req_indices: torch.Tensor,
block_table: torch.Tensor,
cache_block_size: int,
compress_ratio: int,
metadata: DeepseekV4ForwardMetadata | None = None,
is_valid_token: torch.Tensor | None = None,
block_table_base_offsets: torch.Tensor | None = None,
) -> DeepseekV4IndexerDecodePlan:
num_tokens = positions.numel()
key = (int(compress_ratio), int(cache_block_size), int(num_tokens))
indexer_metadata = metadata.indexer if metadata is not None else None
cache = None if indexer_metadata is None else indexer_metadata.decode_plan_cache
refreshed_keys = (
None
if indexer_metadata is None
else indexer_metadata.decode_plan_refreshed_keys
)
cached = cache.get(key) if cache is not None else None
# Hot path: the attention metadata builder hook pre-builds the plan tensors
# before per-layer parallel work so indexer layers only read cached buffers.
if cached is not None and refreshed_keys is not None and key in refreshed_keys:
return cached
if num_tokens == 0:
context_lens = torch.empty((0, 1), dtype=torch.int32, device=positions.device)
block_tables = torch.empty(
(0, 1),
dtype=torch.int32,
device=block_table.device,
)
plan = DeepseekV4IndexerDecodePlan(context_lens, block_tables, 0)
if cache is not None:
cache[key] = plan
if refreshed_keys is not None:
refreshed_keys.add(key)
return plan
rows = int(block_table.shape[0]) if block_table.ndim >= 1 else 0
cols = int(block_table.shape[1]) if block_table.ndim >= 2 else 0
max_len = _deepseek_v4_indexer_decode_max_len(
block_table,
cache_block_size,
compress_ratio,
)
max_blocks = max(1, (max_len + cache_block_size - 1) // cache_block_size)
expected_context_shape = (num_tokens, 1)
expected_block_shape = (num_tokens, max_blocks)
if (
cached is None
or cached.context_lens.shape != expected_context_shape
or cached.context_lens.device != positions.device
or cached.context_lens.dtype != torch.int32
or cached.block_table.shape != expected_block_shape
or cached.block_table.device != block_table.device
or cached.block_table.dtype != torch.int32
):
context_lens = torch.empty(
expected_context_shape,
dtype=torch.int32,
device=positions.device,
)
block_tables = torch.empty(
expected_block_shape,
dtype=torch.int32,
device=block_table.device,
)
plan = DeepseekV4IndexerDecodePlan(
context_lens=context_lens,
block_table=block_tables,
max_context_len=max_len,
)
if cache is not None:
cache[key] = plan
else:
plan = cached
plan.max_context_len = max_len
if rows <= 0 or cols <= 0:
plan.context_lens.zero_()
plan.block_table.zero_()
plan.max_context_len = 0
else:
deepseek_v4_indexer_decode_metadata_compute(
positions=positions,
token_to_req_indices=token_to_req_indices,
block_table=block_table,
cache_block_size=cache_block_size,
compress_ratio=compress_ratio,
max_blocks=max_blocks,
out_context_lens=plan.context_lens,
out_block_tables=plan.block_table,
block_table_base_offsets=block_table_base_offsets,
)
if is_valid_token is None:
is_valid_token = getattr(metadata, "is_valid_token", None)
if is_valid_token is not None:
valid = is_valid_token[:num_tokens].to(
device=plan.context_lens.device,
dtype=torch.bool,
)
with torch.inference_mode():
plan.context_lens.masked_fill_(~valid.view(num_tokens, 1), 0)
plan.block_table.masked_fill_(
~valid.to(device=plan.block_table.device).view(num_tokens, 1),
0,
)
if refreshed_keys is not None:
refreshed_keys.add(key)
return plan
def _deepseek_v4_indexer_decode_schedule_metadata(
*,
positions: torch.Tensor,
cache_block_size: int,
compress_ratio: int,
metadata: DeepseekV4ForwardMetadata | None,
context_lens: torch.Tensor | None = None,
) -> torch.Tensor | None:
if positions.numel() == 0:
return None
if deep_gemm is None:
return None
num_tokens = positions.numel()
if context_lens is None:
compressed_lens = torch.div(
positions.to(torch.int64) + 1,
compress_ratio,
rounding_mode="floor",
).clamp_min(0)
context_lens = compressed_lens.to(torch.int32).view(num_tokens, 1).contiguous()
schedule_key = (compress_ratio, cache_block_size, num_tokens)
indexer_metadata = metadata.indexer if metadata is not None else None
schedule_cache = (
None
if indexer_metadata is None
else indexer_metadata.decode_schedule_metadata_cache
)
schedule_metadata = (
schedule_cache.get(schedule_key) if schedule_cache is not None else None
)
with nvtx_range("indexer_decode_schedule_metadata"):
refreshed = deep_gemm.get_paged_mqa_logits_metadata(
context_lens,
cache_block_size,
deep_gemm.get_num_sms(),
)
if schedule_metadata is not None:
if (
schedule_metadata.shape == refreshed.shape
and schedule_metadata.device == refreshed.device
and schedule_metadata.dtype == refreshed.dtype
):
with torch.inference_mode():
schedule_metadata.copy_(refreshed)
return schedule_metadata
if schedule_cache is not None:
schedule_cache[schedule_key] = refreshed
return refreshed
schedule_metadata = refreshed
if schedule_cache is not None:
schedule_cache[schedule_key] = schedule_metadata
return schedule_metadata
def _deepseek_v4_indexer_topk_prefill_deepgemm(
*,
cache_2d: torch.Tensor,
block_table: torch.Tensor,
cu_seq_lens: torch.Tensor,
cu_start: torch.Tensor,
cu_end: torch.Tensor,
row_lens: torch.Tensor,
max_len: int,
index_q: tuple[torch.Tensor, torch.Tensor],
weights: torch.Tensor,
cache_block_size: int,
topk_tokens: int,
use_prefill_topk_op: bool,
gathered_k: tuple[torch.Tensor, torch.Tensor] | None = None,
gather_workspace: tuple[torch.Tensor, torch.Tensor] | None = None,
) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor] | None]:
q_values, q_scales = index_q
if not _deepseek_v4_deepgemm_fp4_indexer_available(q_values):
raise RuntimeError("DeepSeek V4 sparse indexer requires DeepGEMM FP4 support")
num_tokens = q_values.shape[0]
if num_tokens == 0:
return (
torch.empty(
(0, topk_tokens),
device=q_values.device,
dtype=torch.int32,
),
gathered_k,
)
if max_len <= 0:
return (
torch.full(
(num_tokens, topk_tokens),
-1,
device=q_values.device,
dtype=torch.int32,
),
gathered_k,
)
if gathered_k is None:
with nvtx_range("indexer_topk_prefill_gather_paged_mxfp4"):
gathered_k = _deepseek_v4_gather_paged_indexer_mxfp4_cache(
cache_2d,
block_table,
cu_seq_lens,
cache_block_size,
out=gather_workspace,
)
k_values, k_scales = gathered_k
with nvtx_range("indexer_topk_prefill_deepgemm_logits"):
logits = deep_gemm.fp8_fp4_mqa_logits(
q=(q_values.contiguous().view(torch.int8), q_scales.contiguous()),
kv=(k_values.contiguous(), k_scales.contiguous()),
weights=weights.contiguous(),
cu_seq_len_k_start=cu_start,
cu_seq_len_k_end=cu_end,
clean_logits=False,
max_seqlen_k=max_len,
logits_dtype=torch.float32,
)
with nvtx_range("indexer_topk_prefill_select"):
return (
_deepseek_v4_indexer_topk_from_logits(
logits,
row_lens,
topk_tokens,
use_prefill_topk_op=use_prefill_topk_op,
),
gathered_k,
)
def _deepseek_v4_indexer_topk_from_cache_deepgemm_decode(
*,
cache_2d: torch.Tensor,
positions: torch.Tensor,
token_to_req_indices: torch.Tensor,
block_table: torch.Tensor,
cache_block_size: int,
index_q: tuple[torch.Tensor, torch.Tensor],
weights: torch.Tensor,
compress_ratio: int,
topk_tokens: int,
metadata: DeepseekV4ForwardMetadata | None = None,
schedule_metadata: torch.Tensor | None = None,
decode_context_lens: torch.Tensor | None = None,
decode_block_table: torch.Tensor | None = None,
decode_max_context_len: int | None = None,
is_valid_token: torch.Tensor | None = None,
out: torch.Tensor | None = None,
persistent_topk_workspace: torch.Tensor | None = None,
) -> torch.Tensor:
q_values, q_scales = index_q
if not _deepseek_v4_deepgemm_fp4_indexer_available(q_values):
raise RuntimeError("DeepSeek V4 sparse indexer requires DeepGEMM FP4 support")
num_tokens = positions.numel()
if num_tokens == 0:
if out is not None:
return out[:0]
return torch.empty((0, topk_tokens), device=positions.device, dtype=torch.int32)
if decode_context_lens is not None and decode_block_table is not None:
context_lens = decode_context_lens
block_tables = decode_block_table
max_len = (
int(decode_max_context_len)
if decode_max_context_len is not None
else int(context_lens.max().item())
)
else:
decode_plan = _deepseek_v4_indexer_decode_plan(
positions=positions,
token_to_req_indices=token_to_req_indices,
block_table=block_table,
cache_block_size=cache_block_size,
compress_ratio=compress_ratio,
metadata=metadata,
is_valid_token=is_valid_token,
)
context_lens = decode_plan.context_lens
block_tables = decode_plan.block_table
max_len = decode_plan.max_context_len
topk = (
torch.empty(
(num_tokens, topk_tokens),
device=positions.device,
dtype=torch.int32,
)
if out is None
else out[:num_tokens]
)
if max_len <= 0:
topk.fill_(-1)
return topk
kv_cache = _deepseek_v4_indexer_mxfp4_cache_view(cache_2d, cache_block_size)
schedule_key = (compress_ratio, cache_block_size, num_tokens)
indexer_metadata = metadata.indexer if metadata is not None else None
schedule_cache = (
None
if indexer_metadata is None
else indexer_metadata.decode_schedule_metadata_cache
)
if schedule_metadata is None:
schedule_metadata = (
schedule_cache.get(schedule_key) if schedule_cache is not None else None
)
if schedule_metadata is None:
with nvtx_range("indexer_decode_schedule_metadata"):
schedule_metadata = deep_gemm.get_paged_mqa_logits_metadata(
context_lens,
cache_block_size,
deep_gemm.get_num_sms(),
)
if schedule_cache is not None:
schedule_cache[schedule_key] = schedule_metadata
with nvtx_range("indexer_decode_deepgemm_logits"):
logits = deep_gemm.fp8_fp4_paged_mqa_logits(
q=(
q_values.contiguous().view(torch.int8).unsqueeze(1),
q_scales.contiguous().unsqueeze(1),
),
kv_cache=kv_cache,
weights=weights.contiguous(),
context_lens=context_lens,
block_table=block_tables,
schedule_meta=schedule_metadata,
max_context_len=max_len,
clean_logits=False,
logits_dtype=torch.float32,
)
with nvtx_range("indexer_decode_topk"):
return _deepseek_v4_indexer_topk_from_logits(
logits,
context_lens,
topk_tokens,
next_n=1,
out=out,
persistent_topk_workspace=persistent_topk_workspace,
)
def _deepseek_v4_sparse_attn_indexer_native(
*,
cache_2d: torch.Tensor,
positions: torch.Tensor,
token_to_req_indices: torch.Tensor,
block_table: torch.Tensor,
seq_lens_cpu: torch.Tensor,
query_lens_cpu: torch.Tensor,
prefill_chunk_specs: torch.Tensor,
prefill_chunk_offsets: torch.Tensor,
prefill_slots: torch.Tensor,
prefill_cu_seq_lens: torch.Tensor,
prefill_cu_seqlen_k_start: torch.Tensor,
prefill_cu_seqlen_k_end: torch.Tensor,
prefill_seq_lens_k: torch.Tensor,
packed_q_values: torch.Tensor,
packed_q_scales: torch.Tensor,
packed_weights: torch.Tensor,
decode_schedule_metadata: torch.Tensor | None,
decode_context_lens: torch.Tensor | None,
decode_block_table: torch.Tensor | None,
decode_max_context_len: int,
topk_indices_buffer: torch.Tensor,
prefill_gather_values_workspace: torch.Tensor,
prefill_gather_scales_workspace: torch.Tensor,
persistent_topk_workspace: torch.Tensor,
cache_block_size: int,
compress_ratio: int,
topk_tokens: int,
num_prefill_tokens: int,
num_decode_tokens: int,
) -> torch.Tensor:
total_tokens = positions.numel()
topk_out = topk_indices_buffer[:total_tokens]
topk_out.fill_(-1)
if total_tokens == 0:
return topk_out
def fill_prefill() -> None:
if num_prefill_tokens <= 0:
return
if prefill_chunk_specs.numel() == 0:
raise RuntimeError(
"DeepSeek V4 sparse indexer prefill requires prepared chunk metadata"
)
gather_cache_key = None
gathered_k = None
num_chunks = prefill_chunk_specs.shape[0]
for chunk_idx in range(num_chunks):
(
token_start,
token_end,
req_start,
req_end,
skip_kv_gather_raw,
) = prefill_chunk_specs[chunk_idx].tolist()
(
slot_start,
slot_end,
row_start,
row_end,
max_seqlen_k,
cu_seq_start,
cu_seq_end,
) = prefill_chunk_offsets[chunk_idx].tolist()
skip_kv_gather = bool(int(skip_kv_gather_raw))
gather_rows = max(0, slot_end - slot_start)
gather_workspace = None
if (
prefill_gather_values_workspace.numel() > 0
and prefill_gather_scales_workspace.numel() > 0
and gather_rows <= prefill_gather_values_workspace.shape[0]
and gather_rows <= prefill_gather_scales_workspace.shape[0]
):
gather_workspace = (
prefill_gather_values_workspace[:gather_rows],
prefill_gather_scales_workspace[:gather_rows],
)
with nvtx_range("indexer_topk_deepgemm_prefill"):
key = (req_start, req_end)
reuse_k = None
if skip_kv_gather and gather_cache_key == key:
reuse_k = gathered_k
if prefill_cu_seq_lens.numel() == 0 or cu_seq_end <= cu_seq_start:
raise RuntimeError(
"DeepSeek V4 sparse indexer prefill metadata is incomplete"
)
topk, next_gathered_k = _deepseek_v4_indexer_topk_prefill_deepgemm(
cache_2d=cache_2d,
block_table=block_table[req_start:req_end],
cu_seq_lens=prefill_cu_seq_lens[cu_seq_start:cu_seq_end],
cu_start=prefill_cu_seqlen_k_start[row_start:row_end],
cu_end=prefill_cu_seqlen_k_end[row_start:row_end],
row_lens=prefill_seq_lens_k[row_start:row_end],
max_len=max_seqlen_k,
cache_block_size=cache_block_size,
index_q=(
packed_q_values[token_start:token_end],
packed_q_scales[token_start:token_end],
),
weights=packed_weights[token_start:token_end],
topk_tokens=topk_tokens,
use_prefill_topk_op=True,
gathered_k=reuse_k,
gather_workspace=gather_workspace,
)
if next_gathered_k is not None:
gather_cache_key = key
gathered_k = next_gathered_k
topk_out[token_start:token_end].copy_(topk)
def fill_decode() -> None:
if num_decode_tokens <= 0:
return
decode_start = num_prefill_tokens
decode_end = decode_start + num_decode_tokens
decode_positions = positions[decode_start:decode_end]
decode_token_to_req = token_to_req_indices[decode_start:decode_end]
decode_out = topk_out[decode_start:decode_end]
with nvtx_range("indexer_topk_deepgemm_decode"):
_deepseek_v4_indexer_topk_from_cache_deepgemm_decode(
cache_2d=cache_2d,
positions=decode_positions,
token_to_req_indices=decode_token_to_req,
block_table=block_table,
cache_block_size=cache_block_size,
index_q=(
packed_q_values[decode_start:decode_end],
packed_q_scales[decode_start:decode_end],
),
weights=packed_weights[decode_start:decode_end],
compress_ratio=compress_ratio,
topk_tokens=topk_tokens,
schedule_metadata=decode_schedule_metadata,
decode_context_lens=decode_context_lens,
decode_block_table=decode_block_table,
decode_max_context_len=decode_max_context_len,
out=decode_out,
persistent_topk_workspace=persistent_topk_workspace,
)
fill_prefill()
fill_decode()
return topk_out
def _deepseek_v4_sparse_attn_indexer_op(
cache_2d: torch.Tensor,
positions: torch.Tensor,
token_to_req_indices: torch.Tensor,
block_table: torch.Tensor,
seq_lens_cpu: torch.Tensor,
query_lens_cpu: torch.Tensor,
prefill_chunk_specs: torch.Tensor,
prefill_chunk_offsets: torch.Tensor,
prefill_slots: torch.Tensor,
prefill_cu_seq_lens: torch.Tensor,
prefill_cu_seqlen_k_start: torch.Tensor,
prefill_cu_seqlen_k_end: torch.Tensor,
prefill_seq_lens_k: torch.Tensor,
packed_q_values: torch.Tensor,
packed_q_scales: torch.Tensor,
packed_weights: torch.Tensor,
decode_schedule_metadata: torch.Tensor,
decode_context_lens: torch.Tensor,
decode_block_table: torch.Tensor,
decode_max_context_len: int,
topk_indices_buffer: torch.Tensor,
prefill_gather_values_workspace: torch.Tensor,
prefill_gather_scales_workspace: torch.Tensor,
persistent_topk_workspace: torch.Tensor,
cache_block_size: int,
compress_ratio: int,
topk_tokens: int,
num_prefill_tokens: int,
num_decode_tokens: int,
) -> torch.Tensor:
schedule_metadata = (
decode_schedule_metadata if decode_schedule_metadata.numel() > 0 else None
)
context_lens = decode_context_lens if decode_context_lens.numel() > 0 else None
decode_blocks = decode_block_table if decode_block_table.numel() > 0 else None
return _deepseek_v4_sparse_attn_indexer_native(
cache_2d=cache_2d,
positions=positions,
token_to_req_indices=token_to_req_indices,
block_table=block_table,
seq_lens_cpu=seq_lens_cpu,
query_lens_cpu=query_lens_cpu,
prefill_chunk_specs=prefill_chunk_specs,
prefill_chunk_offsets=prefill_chunk_offsets,
prefill_slots=prefill_slots,
prefill_cu_seq_lens=prefill_cu_seq_lens,
prefill_cu_seqlen_k_start=prefill_cu_seqlen_k_start,
prefill_cu_seqlen_k_end=prefill_cu_seqlen_k_end,
prefill_seq_lens_k=prefill_seq_lens_k,
packed_q_values=packed_q_values,
packed_q_scales=packed_q_scales,
packed_weights=packed_weights,
decode_schedule_metadata=schedule_metadata,
decode_context_lens=context_lens,
decode_block_table=decode_blocks,
decode_max_context_len=decode_max_context_len,
topk_indices_buffer=topk_indices_buffer,
prefill_gather_values_workspace=prefill_gather_values_workspace,
prefill_gather_scales_workspace=prefill_gather_scales_workspace,
persistent_topk_workspace=persistent_topk_workspace,
cache_block_size=cache_block_size,
compress_ratio=compress_ratio,
topk_tokens=topk_tokens,
num_prefill_tokens=num_prefill_tokens,
num_decode_tokens=num_decode_tokens,
)
def _deepseek_v4_sparse_attn_indexer_fake(
cache_2d: torch.Tensor,
positions: torch.Tensor,
token_to_req_indices: torch.Tensor,
block_table: torch.Tensor,
seq_lens_cpu: torch.Tensor,
query_lens_cpu: torch.Tensor,
prefill_chunk_specs: torch.Tensor,
prefill_chunk_offsets: torch.Tensor,
prefill_slots: torch.Tensor,
prefill_cu_seq_lens: torch.Tensor,
prefill_cu_seqlen_k_start: torch.Tensor,
prefill_cu_seqlen_k_end: torch.Tensor,
prefill_seq_lens_k: torch.Tensor,
packed_q_values: torch.Tensor,
packed_q_scales: torch.Tensor,
packed_weights: torch.Tensor,
decode_schedule_metadata: torch.Tensor,
decode_context_lens: torch.Tensor,
decode_block_table: torch.Tensor,
decode_max_context_len: int,
topk_indices_buffer: torch.Tensor,
prefill_gather_values_workspace: torch.Tensor,
prefill_gather_scales_workspace: torch.Tensor,
persistent_topk_workspace: torch.Tensor,
cache_block_size: int,
compress_ratio: int,
topk_tokens: int,
num_prefill_tokens: int,
num_decode_tokens: int,
) -> torch.Tensor:
del (
cache_2d,
positions,
token_to_req_indices,
block_table,
seq_lens_cpu,
query_lens_cpu,
prefill_chunk_specs,
prefill_chunk_offsets,
prefill_slots,
prefill_cu_seq_lens,
prefill_cu_seqlen_k_start,
prefill_cu_seqlen_k_end,
prefill_seq_lens_k,
packed_q_values,
packed_q_scales,
packed_weights,
decode_schedule_metadata,
decode_context_lens,
decode_block_table,
decode_max_context_len,
cache_block_size,
prefill_gather_values_workspace,
prefill_gather_scales_workspace,
persistent_topk_workspace,
compress_ratio,
topk_tokens,
num_prefill_tokens,
num_decode_tokens,
)
return topk_indices_buffer
direct_register_custom_op(
op_name="deepseek_v4_sparse_attn_indexer",
op_func=_deepseek_v4_sparse_attn_indexer_op,
mutates_args=[
"topk_indices_buffer",
"prefill_gather_values_workspace",
"prefill_gather_scales_workspace",
"persistent_topk_workspace",
],
fake_impl=_deepseek_v4_sparse_attn_indexer_fake,
)
def _deepseek_v4_sparse_attn_indexer(
*,
indexer_metadata: DeepseekV4SparseIndexerMetadata,
indexer_cache: torch.Tensor,
indexer_block_table: torch.Tensor,
indexer_block_size: int,
compress_ratio: int,
packed_q_values: torch.Tensor,
packed_q_scales: torch.Tensor,
packed_weights: torch.Tensor,
topk_indices_buffer: torch.Tensor,
prefill_gather_values_workspace: torch.Tensor,
prefill_gather_scales_workspace: torch.Tensor,
persistent_topk_workspace: torch.Tensor,
topk_tokens: int,
) -> torch.Tensor:
batch_metadata = indexer_metadata.batch_metadata
prefill_metadata = indexer_metadata.prefill_metadata
if batch_metadata is None or prefill_metadata is None:
raise RuntimeError("DeepSeek V4 sparse indexer metadata is not prepared")
positions = batch_metadata.positions
decode_plan = indexer_metadata.decode_plan
decode_schedule_metadata = indexer_metadata.decode_schedule_metadata
decode_context_lens = None if decode_plan is None else decode_plan.context_lens
decode_block_table = None if decode_plan is None else decode_plan.block_table
decode_max_context_len = 0 if decode_plan is None else decode_plan.max_context_len
if decode_schedule_metadata is None:
decode_schedule_metadata = torch.empty(
0,
dtype=torch.int32,
device=positions.device,
)
if decode_context_lens is None:
decode_context_lens = torch.empty(
(0, 1),
dtype=torch.int32,
device=positions.device,
)
if decode_block_table is None:
decode_block_table = torch.empty(
(0, 1),
dtype=indexer_block_table.dtype,
device=indexer_block_table.device,
)
if not positions.is_cuda:
raise RuntimeError("DeepSeek V4 sparse indexer requires CUDA tensors")
return torch.ops.tokenspeed.deepseek_v4_sparse_attn_indexer(
indexer_cache,
positions,
batch_metadata.token_to_req_indices,
indexer_block_table,
batch_metadata.seq_lens_cpu,
batch_metadata.query_lens_cpu,
prefill_metadata.chunk_specs,
prefill_metadata.chunk_offsets,
prefill_metadata.slots,
prefill_metadata.cu_seq_lens,
prefill_metadata.cu_seqlen_k_start,
prefill_metadata.cu_seqlen_k_end,
prefill_metadata.seq_lens_k,
packed_q_values,
packed_q_scales,
packed_weights,
decode_schedule_metadata,
decode_context_lens,
decode_block_table,
decode_max_context_len,
topk_indices_buffer,
prefill_gather_values_workspace,
prefill_gather_scales_workspace,
persistent_topk_workspace,
indexer_block_size,
compress_ratio,
topk_tokens,
batch_metadata.num_prefill_tokens,
batch_metadata.num_decode_tokens,
)
def _deepseek_v4_mega_moe_max_num_tokens() -> int:
override = int(
global_server_args_dict.get("deepseek_v4_mega_moe_max_num_tokens", 0) or 0
)
if override > 0:
return override
candidates = [
global_server_args_dict.get("chunked_prefill_size", 0),
global_server_args_dict.get("prefill_graph_max_tokens", 0),
global_server_args_dict.get("max_cudagraph_capture_size", 0),
global_server_args_dict.get("max_num_seqs", 0),
]
return max([int(value or 0) for value in candidates] + [1])
class _DeepseekV4TopKBuffer:
def __init__(self, topk_tokens: int) -> None:
self.topk_tokens = topk_tokens
self.buffer: torch.Tensor | None = None
def get(self, num_tokens: int, device: torch.device) -> torch.Tensor:
rows = max(num_tokens, _deepseek_v4_mega_moe_max_num_tokens())
needs_alloc = (
self.buffer is None
or self.buffer.device != device
or self.buffer.shape[0] < rows
or self.buffer.shape[1] != self.topk_tokens
)
if needs_alloc:
if torch.cuda.is_available() and torch.cuda.is_current_stream_capturing():
raise RuntimeError(
"DeepSeek V4 top-k buffer must be allocated before CUDA graph "
"capture"
)
self.buffer = torch.empty(
(rows, self.topk_tokens),
dtype=torch.int32,
device=device,
)
return self.buffer[:num_tokens]
def _deepseek_v4_padded_heads(num_local_heads: int) -> int:
if num_local_heads <= 64:
return 64
if num_local_heads <= 128:
return 128
raise ValueError(
f"DeepSeek V4 attention supports at most 128 local heads, got {num_local_heads}"
)
def _deepseek_v4_sanitize_swa_slot_mapping(
slot_mapping: torch.Tensor,
capacity: int,
is_valid_token: torch.Tensor | None = None,
) -> torch.Tensor:
# Fail closed: with no writable SWA capacity every slot is masked, so an
# unchecked mapping can never reach the fused cache-insert kernels.
if capacity <= 0:
return torch.full_like(slot_mapping, -1)
slot_mapping = _mask_invalid_graph_tokens(slot_mapping, is_valid_token)
valid = (slot_mapping >= 0) & (slot_mapping < capacity)
return torch.where(
valid,
slot_mapping,
torch.full_like(slot_mapping, -1),
)
def _deepseek_v4_swa_slot_mapping(
ctx: ForwardContext,
positions: torch.Tensor,
out_cache_loc: torch.Tensor,
) -> torch.Tensor:
"""Build the SWA write-slot mapping, already sanitized for cache inserts.
The returned mapping has invalid CUDA-graph tokens and out-of-capacity
slots masked to -1, so per-layer SWA inserts can consume it directly.
Sanitizing here keeps the mask/clamp elementwise chain at once per step;
doing it per layer previously baked ~7 tiny kernels x 61 layers into the
captured decode graph.
"""
if positions.numel() == 0:
return out_cache_loc
metadata = _deepseek_v4_forward_metadata(ctx)
if metadata is None:
raise RuntimeError("DeepSeek V4 attention requires forward metadata")
cache_metadata = metadata.cache
token_to_req_indices = metadata.token_to_req_indices[: positions.numel()]
if cache_metadata.swa_block_table is None:
slot_mapping = out_cache_loc
elif token_to_req_indices.numel() != positions.numel() and (
token_to_req_indices.numel() <= 0
or positions.numel() % token_to_req_indices.numel() != 0
):
slot_mapping = out_cache_loc
else:
slot_mapping = _group_slot_mapping_from_raw(
positions,
token_to_req_indices,
cache_metadata.swa_block_table,
ctx.token_to_kv_pool.swa_block_size,
base_offsets=cache_metadata.swa_base_logical_page,
)
is_valid_token = getattr(metadata, "is_valid_token", None)
if is_valid_token is not None:
is_valid_token = is_valid_token[: positions.numel()]
# Attribute access is deliberately unguarded: a pool without
# swa_capacity_slots must fail fast here rather than skip the bounds
# check that protects the fused cache-insert kernels.
return _deepseek_v4_sanitize_swa_slot_mapping(
slot_mapping,
ctx.token_to_kv_pool.swa_capacity_slots,
is_valid_token,
)
def _attention_use_fp4_indexer_cache(config: PretrainedConfig) -> bool:
override = global_server_args_dict.get("attention_use_fp4_indexer_cache", None)
if override is not None:
return bool(override)
attention_config = getattr(config, "attention_config", None)
if isinstance(attention_config, dict):
return bool(attention_config.get("use_fp4_indexer_cache", False))
return bool(getattr(attention_config, "use_fp4_indexer_cache", False))
def deepseek_v4_rope_config(
config: PretrainedConfig, compress_ratio: int
) -> tuple[float, dict | None]:
"""Return the per-layer DeepSeek V4 RoPE base and scaling config.
DeepSeek V4 uses ordinary RoPE for SWA-only layers. Compressed layers
use the checkpoint's separate `compress_rope_theta` together with YaRN.
"""
if compress_ratio <= 1:
return float(getattr(config, "rope_theta", 10000.0)), None
rope_scaling = getattr(config, "rope_scaling", None)
if rope_scaling is not None:
rope_scaling = dict(rope_scaling)
rope_scaling["rope_type"] = "deepseek_yarn"
rope_scaling["mscale"] = 0
rope_scaling["mscale_all_dim"] = 0
return (
float(
getattr(
config,
"compress_rope_theta",
getattr(config, "rope_theta", 10000.0),
)
),
rope_scaling,
)
class DeepseekV4MLP(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
mapping: Mapping,
quant_config: QuantizationConfig | None,
prefix: str,
swiglu_limit: float | None = None,
reduce_results: bool = False,
) -> None:
super().__init__()
if hidden_act != "silu":
raise ValueError(f"Unsupported activation: {hidden_act}")
tp = mapping.dense
self.gate_up_proj = MergedColumnParallelLinear(
hidden_size,
[intermediate_size] * 2,
bias=False,
tp_rank=tp.tp_rank,
tp_size=tp.tp_size,
tp_group=tp.tp_group,
quant_config=quant_config,
prefix=add_prefix("gate_up_proj", prefix),
)
self.down_proj = RowParallelLinear(
intermediate_size,
hidden_size,
bias=False,
reduce_results=reduce_results,
tp_rank=tp.tp_rank,
tp_size=tp.tp_size,
tp_group=tp.tp_group,
quant_config=quant_config,
prefix=add_prefix("down_proj", prefix),
)
self.swiglu_limit = swiglu_limit
self.reduce_results = reduce_results
self.tp_rank = tp.tp_rank
self.tp_size = tp.tp_size
self.tp_group = tp.tp_group
def forward(self, x: torch.Tensor) -> torch.Tensor:
if x.shape[0] == 0:
return x.new_empty((0, self.down_proj.output_size))
gate_up, _ = self.gate_up_proj(x)
_use_fused_swiglu = (
gate_up.ndim == 2
and gate_up.shape[-1] % 256 == 0
and getattr(self.down_proj, "_use_deep_gemm_fp8", False)
)
if _use_fused_swiglu:
from tokenspeed_kernel.ops.activation.triton import (
fused_swiglu_fp8_ue8m0,
)
x_fp8, scale = fused_swiglu_fp8_ue8m0(
gate_up, swiglu_limit=self.swiglu_limit or 0.0
)
out, _ = self.down_proj(x_fp8, scale=scale)
else:
gate, up = gate_up.float().chunk(2, dim=-1)
if self.swiglu_limit is not None and self.swiglu_limit > 0:
gate = torch.clamp(gate, max=self.swiglu_limit)
up = torch.clamp(up, min=-self.swiglu_limit, max=self.swiglu_limit)
x = (F.silu(gate) * up).to(x.dtype)
out, _ = self.down_proj(x)
return out
class DeepseekV4MoEGate(nn.Module):
def __init__(
self,
config: PretrainedConfig,
layer_index: int,
hash_indices_dtype: torch.dtype = torch.int32,
) -> None:
super().__init__()
self.weight = nn.Parameter(
torch.empty(config.n_routed_experts, config.hidden_size)
)
self.is_hash_moe = layer_index < config.num_hash_layers
if self.is_hash_moe:
self.tid2eid = nn.Parameter(
torch.empty(
config.vocab_size,
config.num_experts_per_tok,
dtype=hash_indices_dtype,
),
requires_grad=False,
)
self.e_score_correction_bias = None
elif getattr(config, "topk_method", None) == "noaux_tc":
self.register_parameter("tid2eid", None)
self.e_score_correction_bias = nn.Parameter(
torch.empty(config.n_routed_experts, dtype=torch.float32),
requires_grad=False,
)
else:
self.register_parameter("tid2eid", None)
self.e_score_correction_bias = None
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
return _deepseek_v4_router_gemm(hidden_states, self.weight)
class DeepseekV4MegaMoEExperts(nn.Module):
_symm_buffer_cache: dict[tuple[int, int, int, int, int, int, int], object] = {}
def __init__(
self,
*,
num_experts: int,
num_local_experts: int,
top_k: int,
hidden_size: int,
intermediate_size: int,
mapping: Mapping,
prefix: str,
swiglu_limit: float | None,
) -> None:
super().__init__()
self.prefix = prefix
self.num_experts = num_experts
self.num_local_experts = num_local_experts
self.top_k = top_k
self.hidden_size = hidden_size
self.intermediate_size = intermediate_size
self.mapping = mapping
self.max_num_tokens = _deepseek_v4_mega_moe_max_num_tokens()
# DeepGEMM bakes the activation clamp into the kernel as a compile-time
# template arg, so the warmup below must use the same value serving does
# (the caller passes it to ``forward``) for the pre-compiled tiles to
# match the served kernels.
self.swiglu_limit = swiglu_limit
weight_attrs = {"weight_loader": self.weight_loader}
self.w13_weight = nn.Parameter(
torch.zeros(
num_local_experts,
2 * intermediate_size,
hidden_size // 2,
dtype=torch.uint8,
),
requires_grad=False,
)
set_weight_attrs(self.w13_weight, weight_attrs)
self.w13_weight_scale = nn.Parameter(
torch.zeros(
num_local_experts,
2 * intermediate_size,
hidden_size // DEEPSEEK_V4_MXFP4_BLOCK_SIZE,
dtype=torch.uint8,
),
requires_grad=False,
)
set_weight_attrs(self.w13_weight_scale, weight_attrs)
self.w2_weight = nn.Parameter(
torch.zeros(
num_local_experts,
hidden_size,
intermediate_size // 2,
dtype=torch.uint8,
),
requires_grad=False,
)
set_weight_attrs(self.w2_weight, weight_attrs)
self.w2_weight_scale = nn.Parameter(
torch.zeros(
num_local_experts,
hidden_size,
intermediate_size // DEEPSEEK_V4_MXFP4_BLOCK_SIZE,
dtype=torch.uint8,
),
requires_grad=False,
)
set_weight_attrs(self.w2_weight_scale, weight_attrs)
self._transformed_l1_weights: tuple[torch.Tensor, torch.Tensor] | None = None
self._transformed_l2_weights: tuple[torch.Tensor, torch.Tensor] | None = None
def weight_loader(
self,
param: nn.Parameter,
loaded_weight: torch.Tensor,
shard_id: str,
local_expert_id: int,
) -> None:
expert_data = param.data[local_expert_id]
if shard_id in ("w1", "w3"):
if param is not self.w13_weight and param is not self.w13_weight_scale:
raise ValueError(f"Unexpected MegaMoE w13 shard target: {shard_id}")
shard_offset = 0 if shard_id == "w1" else self.intermediate_size
expert_data = expert_data.narrow(0, shard_offset, self.intermediate_size)
elif shard_id == "w2":
if param is not self.w2_weight and param is not self.w2_weight_scale:
raise ValueError(f"Unexpected MegaMoE w2 shard target: {shard_id}")
else:
raise ValueError(f"Unsupported DeepSeek V4 MegaMoE shard id: {shard_id}")
if expert_data.dtype == torch.uint8 and loaded_weight.dtype == getattr(
torch, "float8_e8m0fnu", None
):
loaded_weight = loaded_weight.view(torch.uint8)
if expert_data.shape != loaded_weight.shape:
raise ValueError(
f"DeepSeek V4 MegaMoE expert weight shape mismatch for "
f"{self.prefix}: parameter shard {tuple(expert_data.shape)} "
f"vs checkpoint {tuple(loaded_weight.shape)}"
)
expert_data.copy_(loaded_weight)
@staticmethod
def _ue8m0_to_float(sf: torch.Tensor) -> torch.Tensor:
if sf.dtype == torch.uint8:
return (sf.to(torch.int32) << 23).view(torch.float32)
return sf.float()
def _check_runtime_supported(self) -> None:
if not torch.cuda.is_available():
raise NotImplementedError("DeepSeek V4 MegaMoE requires CUDA.")
device = self.w13_weight.device
if device.type != "cuda":
raise NotImplementedError(
"DeepSeek V4 MegaMoE expert weights must be loaded on CUDA."
)
if torch.cuda.get_device_capability(device)[0] != 10:
raise NotImplementedError("DeepGEMM MegaMoE requires SM100 GPUs.")
if self.hidden_size % 128 != 0 or self.intermediate_size % 128 != 0:
raise ValueError(
"DeepGEMM MegaMoE requires hidden and intermediate sizes "
"to be multiples of 128."
)
def finalize_weights(self) -> None:
if self._transformed_l1_weights is not None:
return
self._check_runtime_supported()
if deep_gemm is None:
raise RuntimeError(
"DeepSeek V4 MegaMoE backend requires a DeepGEMM package with "
"fp8_fp4_mega_moe support; pass --moe-backend mega_moe only "
"when DeepGEMM is installed."
)
w13_scale = deep_gemm.transform_sf_into_required_layout(
self._ue8m0_to_float(self.w13_weight_scale.data).contiguous(),
2 * self.intermediate_size,
self.hidden_size,
(1, DEEPSEEK_V4_MXFP4_BLOCK_SIZE),
self.num_local_experts,
)
w2_scale = deep_gemm.transform_sf_into_required_layout(
self._ue8m0_to_float(self.w2_weight_scale.data).contiguous(),
self.hidden_size,
self.intermediate_size,
(1, DEEPSEEK_V4_MXFP4_BLOCK_SIZE),
self.num_local_experts,
)
l1, l2 = deep_gemm.transform_weights_for_mega_moe(
(self.w13_weight.data.view(torch.int8).contiguous(), w13_scale),
(self.w2_weight.data.view(torch.int8).contiguous(), w2_scale),
)
# L2 data tensor may alias the input .contiguous() buffer — clone
# to break the reference so the original weight storage can be freed.
self._transformed_l1_weights = l1
self._transformed_l2_weights = (l2[0].clone(), l2[1])
del self.w13_weight
del self.w13_weight_scale
del self.w2_weight
del self.w2_weight_scale
def get_symm_buffer(self):
if deep_gemm is None:
raise RuntimeError("DeepGEMM MegaMoE symbols are unavailable.")
group = pg_manager.get_process_group("nccl", self.mapping.moe.tp_ep_group)
device = torch.cuda.current_device()
key = (
id(group),
device,
self.num_experts,
self.max_num_tokens,
self.top_k,
self.hidden_size,
self.intermediate_size,
)
symm_buffer = self._symm_buffer_cache.get(key)
if symm_buffer is None:
symm_buffer = deep_gemm.get_symm_buffer_for_mega_moe(
group,
self.num_experts,
self.max_num_tokens,
self.top_k,
self.hidden_size,
self.intermediate_size,
)
self._symm_buffer_cache[key] = symm_buffer
return symm_buffer
def forward(
self,
hidden_states: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
*,
activation_clamp: float | None,
fast_math: bool = True,
) -> torch.Tensor:
if hidden_states.shape[0] > self.max_num_tokens:
raise ValueError(
f"DeepSeek V4 MegaMoE got {hidden_states.shape[0]} tokens, "
f"but the symmetric buffer was sized for {self.max_num_tokens}."
)
y = torch.empty_like(hidden_states, dtype=torch.bfloat16)
symm_buffer = self.get_symm_buffer()
num_tokens = hidden_states.shape[0]
_stage_deepseek_v4_mega_moe_inputs(
hidden_states,
topk_weights,
topk_ids,
symm_buffer.x[:num_tokens],
symm_buffer.x_sf[:num_tokens],
symm_buffer.topk_idx[:num_tokens],
symm_buffer.topk_weights[:num_tokens],
)
if self._transformed_l1_weights is None or self._transformed_l2_weights is None:
raise RuntimeError(
"DeepseekV4MegaMoEExperts.finalize_weights() must run via "
"post_load_weights() before forward()"
)
if deep_gemm is None:
raise RuntimeError("deep_gemm is required for fp8_fp4_mega_moe.")
deep_gemm.fp8_fp4_mega_moe(
y,
self._transformed_l1_weights,
self._transformed_l2_weights,
symm_buffer,
activation_clamp=activation_clamp,
fast_math=fast_math,
)
return y
class DeepseekV4MoE(nn.Module):
def __init__(
self,
config: PretrainedConfig,
mapping: Mapping,
quant_config: QuantizationConfig | None,
layer_index: int,
prefix: str,
aux_stream: torch.cuda.Stream | None = None,
) -> None:
super().__init__()
self.config = config
self.mapping = mapping
self.layer_index = layer_index
self.n_shared_experts = config.n_shared_experts
self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
self.scoring_func = getattr(config, "scoring_func", "sqrtsoftplus")
if self.scoring_func != "sqrtsoftplus":
raise ValueError(
f"Unsupported DeepSeek V4 MoE scoring: {self.scoring_func}"
)
self.stream_fork = StreamFork(aux_stream)
self.use_mega_moe = get_moe_backend().is_mega_moe()
if self.use_mega_moe:
if deep_gemm is None:
raise RuntimeError(
"DeepSeek V4 MegaMoE backend requires an external DeepGEMM "
"package with fp8_fp4_mega_moe support."
)
if mapping.moe.ep_size <= 1:
raise ValueError("DeepSeek V4 MegaMoE requires expert parallelism.")
if mapping.moe.tp_size != 1:
raise ValueError("DeepSeek V4 MegaMoE does not support mixed TP/EP.")
if global_server_args_dict.get("ep_num_redundant_experts", 0):
raise ValueError(
"DeepSeek V4 MegaMoE does not support redundant EP experts."
)
if config.n_routed_experts % mapping.moe.ep_size != 0:
raise ValueError(
"DeepSeek V4 MegaMoE requires n_routed_experts divisible by "
"EP size."
)
self.hash_indices_dtype = torch.int64 if self.use_mega_moe else torch.int32
self.gate = DeepseekV4MoEGate(
config,
layer_index,
hash_indices_dtype=self.hash_indices_dtype,
)
if config.n_shared_experts is not None:
self.shared_experts = DeepseekV4MLP(
config.hidden_size,
config.moe_intermediate_size * config.n_shared_experts,
config.hidden_act,
mapping,
quant_config,
add_prefix("shared_experts", prefix),
swiglu_limit=getattr(config, "swiglu_limit", None),
reduce_results=False,
)
else:
self.shared_experts = None
if self.use_mega_moe:
self.experts = DeepseekV4MegaMoEExperts(
num_experts=config.n_routed_experts,
num_local_experts=config.n_routed_experts // mapping.moe.ep_size,
top_k=config.num_experts_per_tok,
hidden_size=config.hidden_size,
intermediate_size=config.moe_intermediate_size,
mapping=mapping,
prefix=add_prefix("experts", prefix),
swiglu_limit=getattr(config, "swiglu_limit", None),
)
self.topk = None
else:
routed_quant_config = Mxfp4Config(
ignored_layers=quant_config.ignored_layers,
is_checkpoint_mxfp4_serialized=True,
)
self.experts = MoELayer(
top_k=config.num_experts_per_tok,
num_experts=config.n_routed_experts
+ global_server_args_dict["ep_num_redundant_experts"],
hidden_size=config.hidden_size,
intermediate_size=config.moe_intermediate_size,
quant_config=routed_quant_config,
layer_index=layer_index,
prefix=prefix,
tp_rank=mapping.moe.tp_rank,
tp_size=mapping.moe.tp_size,
ep_rank=mapping.moe.ep_rank,
ep_size=mapping.moe.ep_size,
activation="swiglu",
swiglu_limit=getattr(config, "swiglu_limit", None),
with_bias=True,
routing_config={
"routed_scaling_factor": self.routed_scaling_factor,
"normalize_topk_weights": config.norm_topk_prob,
"correction_bias": self.gate.e_score_correction_bias,
"routing_method_type": RoutingMethodType.Renormalize,
},
)
self.topk = TopK(
top_k=config.num_experts_per_tok,
renormalize=config.norm_topk_prob,
correction_bias=self.gate.e_score_correction_bias,
routed_scaling_factor=self.routed_scaling_factor,
output_format=self.experts.topk_output_format,
)
def _select_experts(
self,
hidden_states: torch.Tensor,
input_ids: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
router_logits = self.gate(hidden_states)
fmt = getattr(self.experts, "topk_output_format", None)
need_scores = fmt is not None and not fmt.is_bypassed()
return deepseek_v4_select_experts(
router_logits,
self.config.num_experts_per_tok,
self.config.norm_topk_prob,
correction_bias=self.gate.e_score_correction_bias,
hash_indices_table=self.gate.tid2eid,
input_ids=input_ids,
need_scores=need_scores,
)
def _make_topk_output(
self,
hidden_states: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
router_scores: torch.Tensor,
):
if self.experts.topk_output_format.is_bypassed():
router_logits = pack_topk_as_router_logits(
topk_weights, topk_ids, self.config.n_routed_experts
)
return BypassedTopKOutput(
hidden_states, router_logits, self.topk.topk_config
)
return StandardTopKOutput(topk_weights, topk_ids, router_scores)
def _forward_shared_experts(
self,
hidden_states: torch.Tensor,
ctx: ForwardContext | None = None,
comm_manager: CommManager | None = None,
) -> torch.Tensor | None:
if self.shared_experts is None:
return None
if comm_manager is not None:
hidden_states = comm_manager.pre_dense_comm(hidden_states, ctx)
if hidden_states.shape[0] == 0:
return None
with nvtx_range("moe_shared_experts"):
shared = self.shared_experts(hidden_states)
if comm_manager is not None:
shared, _ = comm_manager.post_dense_comm(shared, None, ctx)
return shared
def forward_mega_moe(
self,
hidden_states: torch.Tensor,
input_ids: torch.Tensor,
ctx: ForwardContext,
comm_manager: CommManager,
) -> torch.Tensor:
if hidden_states.shape[0] == 0:
topk_weights = hidden_states.new_empty(
(0, self.config.num_experts_per_tok), dtype=torch.float32
)
topk_ids = torch.empty(
(0, self.config.num_experts_per_tok),
device=hidden_states.device,
dtype=torch.int64,
)
else:
with nvtx_range("moe_select_experts"):
topk_weights, topk_ids, _ = self._select_experts(
hidden_states, input_ids
)
shared = None
with self.stream_fork.scope(enable=get_is_capture_mode()) as fork:
with nvtx_range("moe_mega_experts"):
if topk_ids.dtype != torch.int64:
topk_ids = topk_ids.to(torch.int64)
if self.routed_scaling_factor != 1.0:
topk_weights = topk_weights * self.routed_scaling_factor
routed = self.experts(
hidden_states,
topk_weights,
topk_ids,
activation_clamp=getattr(self.config, "swiglu_limit", None),
)
with fork.branch():
shared = self._forward_shared_experts(
hidden_states,
ctx=ctx,
comm_manager=comm_manager,
)
return routed + shared if shared is not None else routed
def forward_normal(
self,
hidden_states: torch.Tensor,
input_ids: torch.Tensor,
num_global_tokens: int,
max_num_tokens_per_gpu: int,
) -> torch.Tensor:
if hidden_states.shape[0] == 0:
return hidden_states
with nvtx_range("moe_select_experts"):
topk_weights, topk_ids, router_scores = self._select_experts(
hidden_states, input_ids
)
with nvtx_range("moe_make_topk_output"):
topk_output = self._make_topk_output(
hidden_states, topk_weights, topk_ids, router_scores
)
shared = None
with self.stream_fork.scope(enable=get_is_capture_mode()) as fork:
with nvtx_range("moe_experts"):
routed = self.experts(
hidden_states=hidden_states,
topk_output=topk_output,
num_global_tokens=num_global_tokens,
max_num_tokens_per_gpu=max_num_tokens_per_gpu,
)
if self.routed_scaling_factor != 1.0:
routed *= self.routed_scaling_factor
with fork.branch():
shared = self._forward_shared_experts(hidden_states)
return routed + shared if shared is not None else routed
def forward(
self,
hidden_states: torch.Tensor,
input_ids: torch.Tensor,
num_global_tokens: int,
max_num_tokens_per_gpu: int,
ctx: ForwardContext | None = None,
comm_manager: CommManager | None = None,
) -> torch.Tensor:
if self.use_mega_moe:
return self.forward_mega_moe(
hidden_states,
input_ids,
ctx,
comm_manager,
)
return self.forward_normal(
hidden_states, input_ids, num_global_tokens, max_num_tokens_per_gpu
)
class DeepseekV4Compressor(nn.Module):
def __init__(
self,
config: PretrainedConfig,
hidden_size: int,
head_dim: int,
compress_ratio: int,
prefix: str,
) -> None:
super().__init__()
self.compress_ratio = compress_ratio
self.head_dim = head_dim
self.overlap = compress_ratio == 4
self.coff = 2 if self.overlap else 1
state_dtype = torch.float32
self.ape = nn.Parameter(
torch.empty(compress_ratio, self.coff * head_dim, dtype=state_dtype),
requires_grad=False,
)
self._ape_reordered = False
self.fused_wkv_wgate = MergedColumnParallelLinear(
hidden_size,
[self.coff * head_dim, self.coff * head_dim],
bias=False,
quant_config=None,
prefix=add_prefix("fused_wkv_wgate", prefix),
)
self.norm = RMSNorm(head_dim, eps=config.rms_norm_eps)
def process_weights_after_loading(self, module=None) -> None:
del module
if not self.overlap or self._ape_reordered:
return
with torch.no_grad():
self.ape.data.copy_(_deepseek_v4_reorder_c4_ape_2604(self.ape.data))
self._ape_reordered = True
def compute_kv_score(self, hidden_states: torch.Tensor) -> torch.Tensor:
"""Pre-compute the input GEMM (fused_wkv_wgate @ hidden_states).
Can be launched on an aux stream in parallel with the main Q/KV
projection so that ``forward()`` only needs the lightweight state
update + cache write phase.
"""
weight_shape = (
self.fused_wkv_wgate.output_size_per_partition,
self.fused_wkv_wgate.input_size,
)
weight = self.fused_wkv_wgate.weight.view(*weight_shape)
if weight.dtype == torch.float8_e4m3fn:
weight = _dequant_fp8_weight(self.fused_wkv_wgate, weight_shape)
kv_score = _deepseek_v4_bf16_linear_fp32(hidden_states, weight)
if kv_score is None:
kv_score = torch.matmul(hidden_states.float(), weight.float().T)
return kv_score
def forward(
self,
hidden_states: torch.Tensor,
positions: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
layer_index: int,
cos_sin_cache: torch.Tensor,
*,
state_cache: torch.Tensor | None = None,
state_block_table: torch.Tensor | None = None,
state_block_size: int | None = None,
state_base_logical_page: torch.Tensor | None = None,
state_slot_mapping: torch.Tensor | None = None,
write_compressed_cache: bool = True,
compressor_slot_cache: dict | None = None,
kv_score: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
pool = ctx.token_to_kv_pool
metadata = _deepseek_v4_forward_metadata(ctx)
if metadata is None:
raise RuntimeError("DeepSeek V4 compressor requires forward metadata")
profile_prefix = (
f"indexer_compressor_c{self.compress_ratio}"
if not write_compressed_cache
else f"compressor_c{self.compress_ratio}"
)
if kv_score is None:
with nvtx_range(f"{profile_prefix}_dequant_weight"):
weight_shape = (
self.fused_wkv_wgate.output_size_per_partition,
self.fused_wkv_wgate.input_size,
)
weight = self.fused_wkv_wgate.weight.view(*weight_shape)
if weight.dtype == torch.float8_e4m3fn:
weight = _dequant_fp8_weight(self.fused_wkv_wgate, weight_shape)
with nvtx_range(f"{profile_prefix}_matmul"):
kv_score = _deepseek_v4_bf16_linear_fp32(hidden_states, weight)
if kv_score is None:
kv_score = torch.matmul(hidden_states.float(), weight.float().T)
kv, score = kv_score.split([self.coff * self.head_dim] * 2, dim=-1)
if state_cache is None:
state_cache = pool.get_compressor_state_buffer(layer_index)
cache_metadata = metadata.cache
# state/compressed slot mappings depend only on (per-step state, ratio), so reuse
# them across layers of the same ratio within a step. Attn-compressor path only:
# the indexer-compressor passes an explicit state_block_table and is excluded.
memo = compressor_slot_cache if state_block_table is None else None
if state_block_table is None:
state_block_table = cache_metadata.compressor_state_block_tables.get(
self.compress_ratio
)
state_base_logical_page = (
cache_metadata.compressor_state_base_logical_pages.get(
self.compress_ratio
)
)
if state_block_size is None:
state_block_size = (
pool.get_compressor_state_block_size(layer_index)
if state_block_table is not None
else pool.state_block_size
)
valid_token = (
metadata.is_valid_token[: positions.numel()]
if getattr(metadata, "is_valid_token", None) is not None
else None
)
if state_slot_mapping is None:
state_hit = (
memo.get(("state", self.compress_ratio)) if memo is not None else None
)
if state_hit is not None:
state_slot_mapping, state_block_table = state_hit
else:
if state_block_table is None:
raise RuntimeError(
"DeepSeek V4 missing paged-cache block table for compressor "
f"state ratio={self.compress_ratio}"
)
state_slot_mapping = _group_slot_mapping_from_raw(
positions,
metadata.token_to_req_indices[: positions.numel()],
state_block_table,
state_block_size,
base_offsets=state_base_logical_page,
)
state_slot_mapping = _mask_invalid_graph_tokens(
state_slot_mapping,
valid_token,
)
if memo is not None:
memo[("state", self.compress_ratio)] = (
state_slot_mapping,
state_block_table,
)
with nvtx_range(f"{profile_prefix}_save_state"):
save_deepseek_v4_compressor_state(
kv=kv,
score=score,
ape=self.ape,
state_cache=state_cache,
slot_mapping=state_slot_mapping,
positions=positions,
block_size=state_block_size,
compress_ratio=self.compress_ratio,
)
if not write_compressed_cache:
return kv, score
kv_cache_block_size = pool.get_compressed_block_size(layer_index)
compressed_hit = (
memo.get(("compressed", self.compress_ratio)) if memo is not None else None
)
if compressed_hit is not None:
compressed_slots = compressed_hit
else:
with nvtx_range(f"{profile_prefix}_compressed_slot_mapping"):
compressed_slots = cache_metadata.compressed_slot_mapping(
positions,
self.compress_ratio,
token_to_req_indices=metadata.token_to_req_indices[
: positions.numel()
],
query_start_loc=metadata.query_start_loc,
seq_lens=metadata.seq_lens,
kv_cache_block_size=kv_cache_block_size,
use_decode_cache=(
ctx.forward_mode is not None and ctx.forward_mode.is_decode()
),
is_valid_token=valid_token,
)
if memo is not None:
memo[("compressed", self.compress_ratio)] = compressed_slots
with nvtx_range(f"{profile_prefix}_cache_insert"):
insert = (
deepseek_v4_csa_compress_kv_cache_insert
if self.compress_ratio == 4
else deepseek_v4_hca_compress_kv_cache_insert
)
insert(
state_cache=state_cache,
token_to_req_indices=metadata.token_to_req_indices[: positions.numel()],
positions=positions,
compressor_slot_mapping=state_slot_mapping,
block_table=state_block_table,
block_table_base_offsets=state_base_logical_page,
compressor_block_size=state_block_size,
rms_norm_weight=self.norm.weight,
rms_norm_eps=self.norm.variance_epsilon,
cos_sin_cache=cos_sin_cache,
kv_cache_2d=pool.get_compressed_kv_buffer_2d(layer_index),
kv_slot_mapping=compressed_slots,
kv_cache_block_size=kv_cache_block_size,
compress_ratio=self.compress_ratio,
)
return kv, score
class DeepseekV4Indexer(nn.Module):
def __init__(
self,
config: PretrainedConfig,
mapping: Mapping,
quant_config: QuantizationConfig | None,
prefix: str,
compress_ratio: int,
topk_buffer: _DeepseekV4TopKBuffer | None = None,
) -> None:
super().__init__()
self.wq_b = ReplicatedLinear(
config.q_lora_rank,
config.index_n_heads * config.index_head_dim,
bias=False,
quant_config=quant_config,
prefix=add_prefix("wq_b", prefix),
)
self.weights_proj = ReplicatedLinear(
config.hidden_size,
config.index_n_heads,
bias=False,
quant_config=None,
prefix=add_prefix("weights_proj", prefix),
)
self.compressor = DeepseekV4Compressor(
config,
config.hidden_size,
config.index_head_dim,
compress_ratio,
add_prefix("compressor", prefix),
)
self.use_fp4_cache = _attention_use_fp4_indexer_cache(config)
self.compress_ratio = compress_ratio
self.n_head = int(config.index_n_heads)
self.head_dim = int(config.index_head_dim)
self.topk_tokens = int(config.index_topk)
self.topk_buffer = topk_buffer
self.softmax_scale = self.head_dim**-0.5
value_bytes = deepseek_v4_indexer_mxfp4_value_bytes(self.head_dim)
scale_bytes = deepseek_v4_indexer_mxfp4_scale_dim(self.head_dim)
self.register_buffer(
"_prefill_gather_values_workspace",
torch.empty((0, value_bytes), dtype=torch.uint8),
persistent=False,
)
self.register_buffer(
"_prefill_gather_scales_workspace",
torch.empty((0, scale_bytes), dtype=torch.uint8),
persistent=False,
)
workspace_rows = 1024 * 1024 if self.topk_tokens in (512, 1024, 2048) else 0
self.register_buffer(
"_persistent_topk_workspace",
torch.empty((workspace_rows,), dtype=torch.uint8),
persistent=False,
)
def _prefill_gather_workspace(
self,
rows: int,
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor]:
rows = max(0, int(rows))
value_bytes = deepseek_v4_indexer_mxfp4_value_bytes(self.head_dim)
scale_bytes = deepseek_v4_indexer_mxfp4_scale_dim(self.head_dim)
if (
self._prefill_gather_values_workspace.device != device
or self._prefill_gather_values_workspace.shape[0] < rows
):
self._prefill_gather_values_workspace = torch.empty(
(rows, value_bytes),
dtype=torch.uint8,
device=device,
)
if (
self._prefill_gather_scales_workspace.device != device
or self._prefill_gather_scales_workspace.shape[0] < rows
):
self._prefill_gather_scales_workspace = torch.empty(
(rows, scale_bytes),
dtype=torch.uint8,
device=device,
)
return (
self._prefill_gather_values_workspace[:rows],
self._prefill_gather_scales_workspace[:rows],
)
def prepare_decode_metadata(
self,
*,
positions: torch.Tensor,
metadata: DeepseekV4ForwardMetadata,
ctx: ForwardContext,
indexer_block_size: int,
) -> None:
if not self.use_fp4_cache or not positions.is_cuda:
return
forward_mode = ctx.forward_mode
if forward_mode is not None and forward_mode.is_mixed():
num_prefill_tokens = int(metadata.num_prefill_tokens)
num_decode_tokens = metadata.decode_token_count()
elif forward_mode is not None and forward_mode.is_decode():
num_prefill_tokens = 0
num_decode_tokens = positions.numel()
else:
return
if num_decode_tokens <= 0:
return
decode_start = num_prefill_tokens
decode_end = decode_start + num_decode_tokens
decode_positions = positions[decode_start:decode_end]
decode_valid_token = (
metadata.is_valid_token[decode_start:decode_end]
if getattr(metadata, "is_valid_token", None) is not None
else None
)
indexer_block_table = metadata.cache.compressed_block_table(
self.compress_ratio,
indexer_block_size,
)
indexer_block_table_base_offsets = None
if indexer_block_table is not metadata.cache.block_table:
indexer_block_table_base_offsets = (
metadata.cache.paged_cache_block_table_base_offsets.get(
v4_compressed_kv_group_id(self.compress_ratio)
)
)
decode_plan = _deepseek_v4_indexer_decode_plan(
positions=decode_positions,
token_to_req_indices=metadata.token_to_req_indices[decode_start:decode_end],
block_table=indexer_block_table,
cache_block_size=indexer_block_size,
compress_ratio=self.compress_ratio,
metadata=metadata,
is_valid_token=decode_valid_token,
block_table_base_offsets=indexer_block_table_base_offsets,
)
_deepseek_v4_indexer_decode_schedule_metadata(
positions=decode_positions,
cache_block_size=indexer_block_size,
compress_ratio=self.compress_ratio,
metadata=metadata,
context_lens=decode_plan.context_lens,
)
def _forward_sparse_indexer_custom_op(
self,
*,
hidden_states: torch.Tensor,
qr: torch.Tensor,
positions: torch.Tensor,
metadata: DeepseekV4ForwardMetadata,
ctx: ForwardContext,
indexer_cache: torch.Tensor,
indexer_block_size: int,
cos_sin_cache: torch.Tensor,
) -> torch.Tensor:
if not self.use_fp4_cache:
raise RuntimeError(
"DeepSeek V4 sparse indexer requires MXFP4 indexer cache"
)
if not positions.is_cuda:
raise RuntimeError("DeepSeek V4 sparse indexer requires CUDA tensors")
forward_mode = ctx.forward_mode
total_tokens = positions.numel()
if total_tokens == 0:
return torch.empty(
(0, self.topk_tokens),
device=positions.device,
dtype=torch.int32,
)
num_prefill_tokens, num_decode_tokens = _deepseek_v4_indexer_token_split(
forward_mode,
metadata,
total_tokens,
)
with nvtx_range("indexer_wq_b"):
index_q, _ = self.wq_b(qr)
index_q = index_q.view(-1, self.n_head, self.head_dim)
with nvtx_range("indexer_weights_proj"):
weights, _ = self.weights_proj(hidden_states)
with nvtx_range("indexer_prepare_mxfp4"):
packed_index_q, packed_weights = deepseek_v4_prepare_indexer_q_mxfp4(
index_q=index_q,
positions=positions,
cos_sin_cache=cos_sin_cache,
weights=weights,
softmax_scale=self.softmax_scale,
head_scale=self.n_head**-0.5,
)
packed_indexer_available = _deepseek_v4_deepgemm_fp4_indexer_available(
packed_index_q[0]
)
if not packed_indexer_available:
raise RuntimeError(
"DeepSeek V4 sparse indexer requires DeepGEMM FP4 support"
)
empty_cpu = torch.empty(0, dtype=torch.int32, device="cpu")
seq_lens_cpu = (
metadata.seq_lens_cpu[: metadata.num_prefill_reqs]
if metadata.seq_lens_cpu is not None and num_prefill_tokens > 0
else empty_cpu
)
query_lens_cpu = (
metadata.query_lens_cpu[: metadata.num_prefill_reqs]
if metadata.query_lens_cpu is not None and num_prefill_tokens > 0
else empty_cpu
)
indexer_block_table = metadata.cache.compressed_block_table(
self.compress_ratio,
indexer_block_size,
)
prefill_metadata = _deepseek_v4_indexer_prefill_metadata(
metadata=metadata,
block_table=indexer_block_table,
cache_block_size=indexer_block_size,
compress_ratio=self.compress_ratio,
num_prefill_tokens=num_prefill_tokens,
)
decode_schedule_metadata = None
decode_plan = None
if num_decode_tokens > 0:
decode_start = num_prefill_tokens
decode_end = decode_start + num_decode_tokens
decode_positions = positions[decode_start:decode_end]
decode_valid_token = (
metadata.is_valid_token[decode_start:decode_end]
if getattr(metadata, "is_valid_token", None) is not None
else None
)
decode_plan = _deepseek_v4_indexer_decode_plan(
positions=decode_positions,
token_to_req_indices=metadata.token_to_req_indices[
decode_start:decode_end
],
block_table=indexer_block_table,
cache_block_size=indexer_block_size,
compress_ratio=self.compress_ratio,
metadata=metadata,
is_valid_token=decode_valid_token,
)
decode_schedule_metadata = _deepseek_v4_indexer_decode_schedule_metadata(
positions=decode_positions,
cache_block_size=indexer_block_size,
compress_ratio=self.compress_ratio,
metadata=metadata,
context_lens=decode_plan.context_lens,
)
indexer_metadata = DeepseekV4SparseIndexerMetadata(
batch_metadata=DeepseekV4IndexerBatchMetadata(
positions=positions,
token_to_req_indices=metadata.token_to_req_indices[:total_tokens],
seq_lens_cpu=seq_lens_cpu,
query_lens_cpu=query_lens_cpu,
num_prefill_tokens=num_prefill_tokens,
num_decode_tokens=num_decode_tokens,
),
prefill_metadata=prefill_metadata,
decode_plan=decode_plan,
decode_schedule_metadata=decode_schedule_metadata,
)
prefill_gather_values, prefill_gather_scales = self._prefill_gather_workspace(
prefill_metadata.max_gather_rows(),
positions.device,
)
topk_out = (
self.topk_buffer.get(total_tokens, positions.device)
if self.topk_buffer is not None
else torch.empty(
(total_tokens, self.topk_tokens),
device=positions.device,
dtype=torch.int32,
)
)[:total_tokens]
return _deepseek_v4_sparse_attn_indexer(
indexer_metadata=indexer_metadata,
indexer_cache=indexer_cache,
indexer_block_table=indexer_block_table,
indexer_block_size=indexer_block_size,
compress_ratio=self.compress_ratio,
packed_q_values=packed_index_q[0],
packed_q_scales=packed_index_q[1],
packed_weights=packed_weights,
topk_indices_buffer=topk_out,
prefill_gather_values_workspace=prefill_gather_values,
prefill_gather_scales_workspace=prefill_gather_scales,
persistent_topk_workspace=self._persistent_topk_workspace,
topk_tokens=self.topk_tokens,
)
def forward(
self,
hidden_states: torch.Tensor,
qr: torch.Tensor,
positions: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
layer_index: int,
cos_sin_cache: torch.Tensor,
compressor_slot_cache: dict,
indexer_compressor_kv_score: torch.Tensor | None = None,
) -> torch.Tensor:
pool = ctx.token_to_kv_pool
metadata = _deepseek_v4_forward_metadata(ctx)
if metadata is None:
raise RuntimeError("DeepSeek V4 indexer requires forward metadata")
cache_metadata = metadata.cache
indexer_state = pool.get_indexer_state_buffer(layer_index)
valid_token = (
metadata.is_valid_token[: positions.numel()]
if getattr(metadata, "is_valid_token", None) is not None
else None
)
idx_hit = (
compressor_slot_cache.get("indexer_state")
if compressor_slot_cache is not None
else None
)
if idx_hit is not None:
(
indexer_state_slot_mapping,
indexer_state_block_table,
indexer_state_block_size,
indexer_state_base_logical_page,
) = idx_hit
else:
indexer_state_block_table = cache_metadata.indexer_state_block_table
indexer_state_base_logical_page = (
cache_metadata.indexer_state_base_logical_page
)
if indexer_state_block_table is None:
raise RuntimeError(
"DeepSeek V4 missing paged-cache block table for indexer "
"compressor state"
)
indexer_state_block_size = pool.get_indexer_state_block_size(layer_index)
indexer_state_slot_mapping = _group_slot_mapping_from_raw(
positions,
metadata.token_to_req_indices[: positions.numel()],
indexer_state_block_table,
indexer_state_block_size,
base_offsets=indexer_state_base_logical_page,
)
indexer_state_slot_mapping = _mask_invalid_graph_tokens(
indexer_state_slot_mapping,
valid_token,
)
if compressor_slot_cache is not None:
compressor_slot_cache["indexer_state"] = (
indexer_state_slot_mapping,
indexer_state_block_table,
indexer_state_block_size,
indexer_state_base_logical_page,
)
with nvtx_range("indexer_compressor_total"):
self.compressor(
hidden_states=hidden_states,
positions=positions,
ctx=ctx,
out_cache_loc=out_cache_loc,
layer_index=layer_index,
cos_sin_cache=cos_sin_cache,
state_cache=indexer_state,
state_block_table=indexer_state_block_table,
state_block_size=indexer_state_block_size,
state_base_logical_page=indexer_state_base_logical_page,
state_slot_mapping=indexer_state_slot_mapping,
write_compressed_cache=False,
kv_score=indexer_compressor_kv_score,
)
with nvtx_range("indexer_compressed_slot_mapping"):
indexer_block_size = pool.get_indexer_block_size(layer_index)
compressed_slots = cache_metadata.compressed_slot_mapping(
positions,
self.compress_ratio,
token_to_req_indices=metadata.token_to_req_indices[: positions.numel()],
query_start_loc=metadata.query_start_loc,
seq_lens=metadata.seq_lens,
kv_cache_block_size=indexer_block_size,
use_decode_cache=(
ctx.forward_mode is not None and ctx.forward_mode.is_decode()
),
is_valid_token=valid_token,
)
with nvtx_range("indexer_cache_insert"):
deepseek_v4_csa_indexer_cache_insert(
state_cache=indexer_state,
token_to_req_indices=metadata.token_to_req_indices[: positions.numel()],
positions=positions,
compressor_slot_mapping=indexer_state_slot_mapping,
block_table=indexer_state_block_table,
block_table_base_offsets=indexer_state_base_logical_page,
compressor_block_size=indexer_state_block_size,
rms_norm_weight=self.compressor.norm.weight,
rms_norm_eps=self.compressor.norm.variance_epsilon,
cos_sin_cache=cos_sin_cache,
kv_cache_2d=pool.get_indexer_kv_buffer_2d(layer_index),
kv_slot_mapping=compressed_slots,
kv_cache_block_size=indexer_block_size,
use_fp4_cache=self.use_fp4_cache,
compress_ratio=self.compress_ratio,
)
return self._forward_sparse_indexer_custom_op(
hidden_states=hidden_states,
qr=qr,
positions=positions,
metadata=metadata,
ctx=ctx,
indexer_cache=pool.get_indexer_kv_buffer_2d(layer_index),
indexer_block_size=indexer_block_size,
cos_sin_cache=cos_sin_cache,
)
class DeepseekV4Attention(nn.Module):
def __init__(
self,
config: PretrainedConfig,
mapping: Mapping,
layer_index: int,
quant_config: QuantizationConfig | None,
prefix: str,
aux_stream: torch.cuda.Stream | None = None,
topk_buffer: _DeepseekV4TopKBuffer | None = None,
cache_layer_index: int | None = None,
) -> None:
super().__init__()
# `layer_index` addresses checkpoint/config metadata; `cache_layer_index`
# addresses this model's compact KV cache slot.
self.layer_index = layer_index
self.cache_layer_index = (
layer_index if cache_layer_index is None else cache_layer_index
)
self.stream_fork = StreamFork(aux_stream)
self.compress_ratio = max(1, int(config.compress_ratios[layer_index]))
if self.compress_ratio <= 1:
self.attention_kind = "swa"
elif self.compress_ratio == 4:
self.attention_kind = "csa"
elif self.compress_ratio == 128:
self.attention_kind = "hca"
else:
raise ValueError(
f"Unsupported DeepSeek V4 compress_ratio={self.compress_ratio}; "
"expected 1, 4, or 128."
)
self.num_heads = int(config.num_attention_heads)
tp_rank = mapping.attn.tp_rank
tp_size = mapping.attn.tp_size
tp_group = mapping.attn.tp_group
if self.num_heads % tp_size != 0:
raise ValueError(
f"num_attention_heads={self.num_heads} must be divisible "
f"by attn_tp_size={tp_size}"
)
self.num_local_heads = self.num_heads // tp_size
self.padded_heads = _deepseek_v4_padded_heads(self.num_local_heads)
self.head_dim = int(config.head_dim)
self.qk_rope_head_dim = int(config.qk_rope_head_dim)
self.nope_head_dim = deepseek_v4_nope_dim(
self.head_dim,
self.qk_rope_head_dim,
)
self.swa_window = int(getattr(config, "sliding_window", 128))
self.scale = self.head_dim**-0.5
self.q_lora_rank = config.q_lora_rank
self.o_lora_rank = config.o_lora_rank
self.o_groups = config.o_groups
self.num_local_groups = self.o_groups // tp_size
num_local = self.num_local_heads
self.attn_sink = nn.Parameter(
torch.full((self.padded_heads,), -float("inf"), dtype=torch.float32),
requires_grad=False,
)
set_weight_attrs(
self.attn_sink,
{
"weight_loader": lambda param, loaded_weight: param.data.__setitem__(
slice(num_local),
loaded_weight[tp_rank * num_local : (tp_rank + 1) * num_local],
),
},
)
rope_base, rope_scaling = deepseek_v4_rope_config(config, self.compress_ratio)
self.rotary_emb = get_rope(
self.qk_rope_head_dim,
rotary_dim=self.qk_rope_head_dim,
max_position=getattr(config, "max_position_embeddings", 8192),
base=rope_base,
rope_scaling=rope_scaling,
is_neox_style=False,
)
self.indexer_rotary_emb = self.rotary_emb
self.fused_wqa_wkv = MergedColumnParallelLinear(
config.hidden_size,
[self.q_lora_rank, self.head_dim],
bias=False,
quant_config=quant_config,
prefix=add_prefix("fused_wqa_wkv", prefix),
)
self.q_norm = RMSNorm(self.q_lora_rank, eps=config.rms_norm_eps)
self.wq_b = ColumnParallelLinear(
self.q_lora_rank,
self.num_heads * self.head_dim,
bias=False,
quant_config=quant_config,
prefix=add_prefix("wq_b", prefix),
tp_rank=tp_rank,
tp_size=tp_size,
tp_group=tp_group,
)
self.kv_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
self.fused_qkv_norm = FusedRMSNorm(self.q_norm, self.kv_norm)
# Fused QKV-RMSNorm: fuses q_a/kv_a RMSNorm into one kernel instead of two
# sequential RMSNorm + a kv.contiguous() copy. Validated via GSM8K.
self.use_fused_qkv_rmsnorm = True
self.wo_a = ColumnParallelLinear(
self.num_heads * self.head_dim // self.o_groups,
self.o_groups * self.o_lora_rank,
bias=False,
quant_config=quant_config,
prefix=add_prefix("wo_a", prefix),
tp_rank=tp_rank,
tp_size=tp_size,
tp_group=tp_group,
)
self.wo_a.is_bmm = True
self.wo_a.bmm_batch_size = self.num_local_groups
# SM100 packs the FP8 output-proj scales as INT32 UE8M0 (fp8_einsum
# recipe (1,1,128)); SM90 keeps FP32 block scales (recipe (1,128,128)).
self._o_tma_aligned = torch.cuda.get_device_capability()[0] >= 10
self.wo_b = RowParallelLinear(
self.o_groups * self.o_lora_rank,
config.hidden_size,
bias=False,
quant_config=quant_config,
prefix=add_prefix("wo_b", prefix),
tp_rank=tp_rank,
tp_size=tp_size,
tp_group=tp_group,
)
if self.compress_ratio > 1:
self.compressor = DeepseekV4Compressor(
config,
config.hidden_size,
self.head_dim,
self.compress_ratio,
add_prefix("compressor", prefix),
)
else:
self.compressor = None
if self.compress_ratio == 4:
self.indexer = DeepseekV4Indexer(
config,
mapping,
quant_config,
add_prefix("indexer", prefix),
self.compress_ratio,
topk_buffer=topk_buffer,
)
else:
self.indexer = None
def _project_q_kv(
self, hidden_states: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
qr_kv, _ = self.fused_wqa_wkv(hidden_states)
qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1)
if self.use_fused_qkv_rmsnorm and qr.is_cuda and qr.shape[0] > 0:
qr_norm = torch.empty(
qr.shape,
dtype=qr.dtype,
device=qr.device,
)
kv_norm = torch.empty(
kv.shape,
dtype=kv.dtype,
device=kv.device,
)
self.fused_qkv_norm(
input_q_a=qr,
input_kv_a=kv,
output_q_a=qr_norm,
output_kv_a=kv_norm,
)
qr = qr_norm
kv = kv_norm
else:
qr = self.q_norm(qr)
kv = self.kv_norm(kv.contiguous())
q, _ = self.wq_b(qr)
q = q.view(-1, self.num_local_heads, self.head_dim)
return q, kv, qr
def _insert_swa_cache(
self,
q: torch.Tensor,
kv: torch.Tensor,
swa_kv_cache: torch.Tensor,
slot_mapping: torch.Tensor,
positions: torch.Tensor,
cos_sin_cache: torch.Tensor,
block_size: int,
) -> None:
# slot_mapping arrives pre-sanitized from _deepseek_v4_swa_slot_mapping
# (invalid tokens and out-of-capacity slots masked to -1); sanitizing
# here again would re-run the mask chain once per layer.
if q.shape[0] == 0:
return
fused_qnorm_rope_kv_insert(
q=q,
kv=kv,
swa_kv_cache_2d=swa_kv_cache.view(swa_kv_cache.shape[0], -1),
slot_mapping=slot_mapping,
positions=positions,
cos_sin_cache=cos_sin_cache,
rms_norm_eps=self.q_norm.variance_epsilon,
block_size=block_size,
)
def _project_attention_output(
self,
attn_output: torch.Tensor,
positions: torch.Tensor,
cos_sin_cache: torch.Tensor,
) -> torch.Tensor:
# Inverse RoPE + grouped block-scaled FP8 quant in one Triton kernel,
# then a single native FP8 GEMM via deep_gemm.fp8_einsum -- replaces the
# inv-rope + per-group online-quant + GEMM chain. wo_a's block scale was
# transformed into the deep_gemm MN-major layout at load time.
heads_per_group = self.num_local_heads // self.num_local_groups
o_fp8, o_scale = deepseek_v4_fused_inv_rope_fp8_quant(
attn_output,
positions,
cos_sin_cache,
n_groups=self.num_local_groups,
heads_per_group=heads_per_group,
nope_dim=self.nope_head_dim,
rope_dim=self.qk_rope_head_dim,
tma_aligned_scales=self._o_tma_aligned,
)
in_dim = self.num_heads * self.head_dim // self.o_groups
weight = self.wo_a.weight.view(self.num_local_groups, self.o_lora_rank, in_dim)
block_n, block_k = self.wo_a._deep_gemm_block_size
recipe = (1, 1, block_n) if self._o_tma_aligned else (1, block_n, block_k)
z = torch.empty(
(attn_output.shape[0], self.num_local_groups, self.o_lora_rank),
device=attn_output.device,
dtype=torch.bfloat16,
)
deep_gemm.fp8_einsum(
"bhr,hdr->bhd",
(o_fp8, o_scale),
(weight, self.wo_a.weight_scale_inv),
z,
recipe=recipe,
)
out, _ = self.wo_b(z.flatten(1))
return out
@break_point
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
swa_slot_mapping: torch.Tensor | None = None,
compressor_slot_cache: dict | None = None,
) -> torch.Tensor:
"""DSA attention, one COARSE breakable-graph break point.
Per layer it does multiple paged-cache writes (SWA / compressor /
indexer), a data/length-dependent indexer -> top-k stage, the FlashMLA
sparse kernel, AND aux-stream forks -- none capturable into a CUDA
graph. Under a prefill-graph capture the whole attention runs eager
(reading the live ``ctx``) while the layer's norms + MoE stay graphed;
direct call otherwise (see ``break_point``). Padding rows are handled
by the existing ``metadata.is_valid_token`` masking, and the
token-shaped inputs are sliced to the real count DSA kernels assert
(see ``slice_to_real_tokens`` below). The cross-layer SWA slot mapping
and compressor memo are shared via ctx -- replay-safe because ctx is
rebound to the live forward (see ``DeepseekV4Model.forward``).
"""
if hidden_states.shape[0] == 0:
return hidden_states
# Cross-layer compressor memo, created once per forward by the first layer.
if compressor_slot_cache is None:
compressor_slot_cache = ctx.dsa_compressor_slot_cache
if compressor_slot_cache is None:
compressor_slot_cache = ctx.dsa_compressor_slot_cache = {}
profile_prefix = f"attn_{self.attention_kind}"
cos_sin_cache = self.rotary_emb.cos_sin_cache
if cos_sin_cache.dtype != torch.float32:
cos_sin_cache = cos_sin_cache.float()
pool = ctx.token_to_kv_pool
metadata = ctx.attn_backend.forward_metadata
if metadata is None:
raise RuntimeError("DeepSeek V4 attention requires forward metadata")
# Slice padded inputs to the real count the DSA kernels assert (see
# docstring). Only under a breakable capture/replay (ambient ctx set):
# eager forwards are never padded, and the MTP draft path reaches here
# with metadata whose token_to_req_indices does NOT describe q's rows.
token_to_req = getattr(metadata, "token_to_req_indices", None)
if current_forward_ctx() is not None and token_to_req is not None:
positions, hidden_states, out_cache_loc, swa_slot_mapping = (
slice_to_real_tokens(
token_to_req.numel(),
positions,
hidden_states,
out_cache_loc,
swa_slot_mapping,
)
)
# --- Phase 1: pre-compute input GEMMs in parallel ---
# Q/KV projection on main stream; compressor GEMM(s) on aux stream.
# After this phase, each compressor's forward() receives its
# pre-computed kv_score and skips the internal GEMM, removing the
# shared-weight dependency that prevents safe multi-stream overlap.
compressor_kv_score: torch.Tensor | None = None
indexer_compressor_kv_score: torch.Tensor | None = None
with self.stream_fork.scope(
enable=self.stream_fork.aux_stream is not None
) as fork:
with nvtx_range(f"{profile_prefix}_project_q_kv"):
q, kv, qr = self._project_q_kv(hidden_states)
with fork.branch():
if self.compressor is not None:
with nvtx_range(f"{profile_prefix}_compressor_gemm"):
compressor_kv_score = self.compressor.compute_kv_score(
hidden_states
)
if self.indexer is not None:
with nvtx_range(f"{profile_prefix}_indexer_compressor_gemm"):
indexer_compressor_kv_score = (
self.indexer.compressor.compute_kv_score(hidden_states)
)
if swa_slot_mapping is None:
# Cross-layer SWA slot mapping (per-token, layer-invariant), first layer computes.
swa_slot_mapping = ctx.dsa_swa_slot_mapping
if swa_slot_mapping is None:
swa_slot_mapping = _deepseek_v4_swa_slot_mapping(
ctx,
positions,
out_cache_loc,
)
ctx.dsa_swa_slot_mapping = swa_slot_mapping
def insert_swa_cache() -> None:
with nvtx_range(f"{profile_prefix}_insert_swa_cache"):
self._insert_swa_cache(
q=q,
kv=kv,
swa_kv_cache=pool.get_swa_kv_buffer(self.cache_layer_index),
slot_mapping=swa_slot_mapping,
positions=positions,
cos_sin_cache=cos_sin_cache,
block_size=pool.swa_block_size,
)
def run_compressor() -> None:
if self.compressor is None:
return
with nvtx_range(f"{profile_prefix}_compressor"):
self.compressor(
hidden_states=hidden_states,
positions=positions,
ctx=ctx,
out_cache_loc=out_cache_loc,
layer_index=self.cache_layer_index,
cos_sin_cache=cos_sin_cache,
compressor_slot_cache=compressor_slot_cache,
kv_score=compressor_kv_score,
)
# --- Phase 2: state-update + cache-write overlap ---
# With GEMMs already done, the remaining work per stream is lightweight
# state updates and paged cache writes — safe to overlap.
topk_indices = None
if self.indexer is not None:
if self.compressor is None:
raise RuntimeError("compressor is required when indexer is enabled.")
with nvtx_range(f"{profile_prefix}_indexer_prepare_decode_metadata"):
self.indexer.prepare_decode_metadata(
positions=positions,
metadata=metadata,
ctx=ctx,
indexer_block_size=pool.get_indexer_block_size(
self.cache_layer_index
),
)
with self.stream_fork.scope(
enable=self.stream_fork.aux_stream is not None
) as fork:
with nvtx_range(f"{profile_prefix}_indexer"):
topk_indices = self.indexer(
hidden_states=hidden_states,
qr=qr,
positions=positions,
ctx=ctx,
out_cache_loc=out_cache_loc,
layer_index=self.cache_layer_index,
cos_sin_cache=cos_sin_cache,
compressor_slot_cache=compressor_slot_cache,
indexer_compressor_kv_score=indexer_compressor_kv_score,
)
with fork.branch():
insert_swa_cache()
run_compressor()
elif self.compressor is not None:
with self.stream_fork.scope(
enable=self.stream_fork.aux_stream is not None
) as fork:
run_compressor()
with fork.branch():
insert_swa_cache()
else:
insert_swa_cache()
forward_mode = ctx.forward_mode
if forward_mode is None:
raise RuntimeError("DeepSeek V4 attention requires forward mode")
if forward_mode.is_mixed():
with nvtx_range(f"{profile_prefix}_mixed_backend"):
attn_output = ctx.attn_backend.forward_deepseek_v4_mixed(
q=q,
positions=positions,
token_to_kv_pool=pool,
layer_id=self.cache_layer_index,
kind=self.attention_kind,
compress_ratio=self.compress_ratio,
num_local_heads=self.num_local_heads,
padded_heads=self.padded_heads,
head_dim=self.head_dim,
window_size=self.swa_window,
softmax_scale=self.scale,
attn_sink=self.attn_sink,
topk_indices=topk_indices,
)
elif forward_mode.is_decode():
with nvtx_range(f"{profile_prefix}_decode_backend"):
attn_output = ctx.attn_backend.forward_deepseek_v4_decode(
q=q,
positions=positions,
token_to_kv_pool=pool,
layer_id=self.cache_layer_index,
kind=self.attention_kind,
compress_ratio=self.compress_ratio,
num_local_heads=self.num_local_heads,
padded_heads=self.padded_heads,
head_dim=self.head_dim,
window_size=self.swa_window,
softmax_scale=self.scale,
attn_sink=self.attn_sink,
topk_indices=topk_indices,
)
elif forward_mode.is_extend_or_mixed():
with nvtx_range(f"{profile_prefix}_prefill_backend"):
attn_output = ctx.attn_backend.forward_deepseek_v4_prefill(
q=q,
positions=positions,
token_to_kv_pool=pool,
layer_id=self.cache_layer_index,
kind=self.attention_kind,
compress_ratio=self.compress_ratio,
num_local_heads=self.num_local_heads,
padded_heads=self.padded_heads,
head_dim=self.head_dim,
window_size=self.swa_window,
softmax_scale=self.scale,
attn_sink=self.attn_sink,
topk_indices=topk_indices,
)
else:
raise RuntimeError(f"Unsupported DeepSeek V4 forward mode: {forward_mode}")
with nvtx_range(f"{profile_prefix}_output_proj"):
return self._project_attention_output(
attn_output,
positions,
cos_sin_cache,
)
class DeepseekV4DecoderLayer(nn.Module):
def __init__(
self,
config: PretrainedConfig,
layer_id: int,
mapping: Mapping,
quant_config: QuantizationConfig | None,
prefix: str,
aux_stream: torch.cuda.Stream | None = None,
topk_buffer: _DeepseekV4TopKBuffer | None = None,
cache_layer_index: int | None = None,
) -> None:
super().__init__()
self.mapping = mapping
self.layer_id = layer_id
self.hidden_size = config.hidden_size
self.rms_norm_eps = config.rms_norm_eps
self.hc_mult = config.hc_mult
self.hc_sinkhorn_iters = config.hc_sinkhorn_iters
self.hc_eps = config.hc_eps
mix_hc = (2 + self.hc_mult) * self.hc_mult
hc_dim = self.hc_mult * config.hidden_size
self.attn = DeepseekV4Attention(
config,
mapping,
layer_id,
quant_config,
add_prefix("attn", prefix),
aux_stream=aux_stream,
topk_buffer=topk_buffer,
cache_layer_index=cache_layer_index,
)
self.ffn = DeepseekV4MoE(
config,
mapping,
quant_config,
layer_id,
add_prefix("ffn", prefix),
aux_stream=aux_stream,
)
self.comm_manager = CommManager(
mapping=mapping,
layer_id=layer_id,
is_moe=True,
prev_is_moe=True,
)
self.attn_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.ffn_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.hc_attn_fn = nn.Parameter(
torch.empty(mix_hc, hc_dim, dtype=torch.float32), requires_grad=False
)
self.hc_ffn_fn = nn.Parameter(
torch.empty(mix_hc, hc_dim, dtype=torch.float32), requires_grad=False
)
self.hc_attn_base = nn.Parameter(
torch.empty(mix_hc, dtype=torch.float32), requires_grad=False
)
self.hc_ffn_base = nn.Parameter(
torch.empty(mix_hc, dtype=torch.float32), requires_grad=False
)
self.hc_attn_scale = nn.Parameter(
torch.empty(3, dtype=torch.float32), requires_grad=False
)
self.hc_ffn_scale = nn.Parameter(
torch.empty(3, dtype=torch.float32), requires_grad=False
)
def _pre_mlp_input_ids_comm(
self, input_ids: torch.Tensor, ctx: ForwardContext
) -> torch.Tensor:
if not self.mapping.moe.has_tp_ep:
return input_ids
if self.comm_manager.use_all_reduce(is_moe=True):
return input_ids
token_counts = self.comm_manager.moe_tp_ep_group_scattered_num_tokens(ctx)
max_tokens = max(token_counts)
padded = torch.empty(
(max_tokens,), device=input_ids.device, dtype=input_ids.dtype
)
padded[: input_ids.shape[0]].copy_(input_ids)
if input_ids.shape[0] < max_tokens:
padded[input_ids.shape[0] :].zero_()
gathered = [torch.empty_like(padded) for _ in token_counts]
group = pg_manager.get_process_group("nccl", self.mapping.moe.tp_ep_group)
torch.distributed.all_gather(gathered, padded, group=group)
return torch.cat(
[tokens[:count] for tokens, count in zip(gathered, token_counts)], dim=0
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
input_ids: torch.Tensor,
swa_slot_mapping: torch.Tensor | None = None,
compressor_slot_cache: dict | None = None,
hc_x_prev: torch.Tensor | None = None,
hc_post_prev: torch.Tensor | None = None,
hc_comb_prev: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
if hc_x_prev is not None:
with nvtx_range("hc_fused_attn_pre"):
residual, hidden_states, post, comb = mhc_fused_hc(
hc_x_prev,
hidden_states,
hc_post_prev,
hc_comb_prev,
self.hc_attn_fn,
self.hc_attn_scale,
self.hc_attn_base,
self.rms_norm_eps,
self.hc_eps,
self.hc_sinkhorn_iters,
)
else:
residual = hidden_states
with nvtx_range("hc_attn_pre"):
hidden_states, post, comb = mhc_pre(
hidden_states,
self.hc_attn_fn,
self.hc_attn_scale,
self.hc_attn_base,
self.rms_norm_eps,
self.hc_eps,
self.hc_sinkhorn_iters,
)
with nvtx_range("attn_norm"):
hidden_states = self.attn_norm(hidden_states)
with nvtx_range("attn_total"):
hidden_states = self.attn(
positions,
hidden_states,
ctx,
out_cache_loc,
swa_slot_mapping,
compressor_slot_cache,
)
with nvtx_range("hc_fused_ffn_pre"):
residual, hidden_states, post, comb = mhc_fused_hc(
hidden_states,
residual,
post,
comb,
self.hc_ffn_fn,
self.hc_ffn_scale,
self.hc_ffn_base,
self.rms_norm_eps,
self.hc_eps,
self.hc_sinkhorn_iters,
)
with nvtx_range("ffn_norm"):
hidden_states = self.ffn_norm(hidden_states)
ffn_input_ids = input_ids
use_mega_moe = getattr(self.ffn, "use_mega_moe", False)
if use_mega_moe:
token_counts = self.comm_manager.moe_tp_ep_group_scattered_num_tokens(ctx)
num_global_tokens = sum(token_counts)
max_num_tokens_per_gpu = max(token_counts) if token_counts else 0
else:
token_counts = None
with nvtx_range("pre_mlp_comm"):
hidden_states = self.comm_manager.pre_mlp_comm(hidden_states, ctx)
if self.ffn.gate.is_hash_moe:
with nvtx_range("pre_mlp_input_ids_comm"):
ffn_input_ids = self._pre_mlp_input_ids_comm(input_ids, ctx)
with nvtx_range("moe_get_num_tokens"):
num_global_tokens, max_num_tokens_per_gpu = (
self.comm_manager.get_num_tokens(ctx)
)
with nvtx_range("ffn_total"):
hidden_states = self.ffn(
hidden_states,
ffn_input_ids,
num_global_tokens,
max_num_tokens_per_gpu,
ctx=ctx if use_mega_moe else None,
comm_manager=self.comm_manager if use_mega_moe else None,
)
if not use_mega_moe:
with nvtx_range("post_mlp_comm"):
hidden_states, _ = self.comm_manager.post_mlp_comm(
hidden_states, None, ctx
)
# Defer ffn post_mapping to next layer's fused_hc
return residual, hidden_states, post, comb
class DeepseekV4Model(nn.Module):
fall_back_to_pt_during_load = False
def __init__(
self,
config: PretrainedConfig,
mapping: Mapping,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
self.mapping = mapping
self.hc_mult = config.hc_mult
self.hc_eps = config.hc_eps
self.rms_norm_eps = config.rms_norm_eps
self.aux_stream = torch.cuda.Stream() if torch.cuda.is_available() else None
self.topk_indices_buffer = _DeepseekV4TopKBuffer(int(config.index_topk))
self.embed_tokens = VocabParallelEmbedding(
config.vocab_size,
config.hidden_size,
tp_rank=mapping.attn.tp_rank,
tp_size=mapping.attn.tp_size,
tp_group=mapping.attn.tp_group,
prefix=add_prefix("embed_tokens", prefix),
)
self.layers = nn.ModuleList(
[
DeepseekV4DecoderLayer(
config,
layer_id,
mapping,
quant_config,
add_prefix(f"layers.{layer_id}", prefix),
aux_stream=self.aux_stream,
topk_buffer=self.topk_indices_buffer,
)
for layer_id in range(config.num_hidden_layers)
]
)
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
hc_dim = config.hc_mult * config.hidden_size
self.hc_head_fn = nn.Parameter(
torch.empty(config.hc_mult, hc_dim, dtype=torch.float32),
requires_grad=False,
)
self.hc_head_base = nn.Parameter(
torch.empty(config.hc_mult, dtype=torch.float32), requires_grad=False
)
self.hc_head_scale = nn.Parameter(
torch.empty(1, dtype=torch.float32), requires_grad=False
)
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
input_embeds: torch.Tensor | None = None,
) -> tuple[torch.Tensor, list[torch.Tensor] | None]:
hidden_states = input_embeds
if hidden_states is None:
with nvtx_range("embed_tokens"):
hidden_states = self.embed_tokens(input_ids)
with nvtx_range("hc_repeat"):
hidden_states = hidden_states.unsqueeze(1).repeat(1, self.hc_mult, 1)
# Layer-invariant DSA memos: computing them HERE (graphed scope) would bake
# dummy addresses; the first attention break computes them LIVE onto ctx.
ctx.dsa_swa_slot_mapping = None
ctx.dsa_compressor_slot_cache = None
hc_x_prev = None
hc_post_prev = None
hc_comb_prev = None
for layer in self.layers:
hidden_states, hc_x_prev, hc_post_prev, hc_comb_prev = layer(
positions,
hidden_states,
ctx,
out_cache_loc,
input_ids,
None, # swa_slot_mapping: first break computes + caches on ctx
None, # compressor_slot_cache: shared dict created on ctx
hc_x_prev,
hc_post_prev,
hc_comb_prev,
)
with nvtx_range("hc_ffn_post_final"):
hidden_states = mhc_post(
hc_x_prev, hidden_states, hc_post_prev, hc_comb_prev
)
aux_hidden_states = None
if (
ctx.capture_hidden_mode is not None
and ctx.capture_hidden_mode.need_capture()
):
# V4 MTP consumes the pre-hc_head hypercompressed residual.
aux_hidden_states = [hidden_states.flatten(1)]
with nvtx_range("hc_head"):
hidden_states = hc_head(
hidden_states,
self.hc_head_fn,
self.hc_head_scale,
self.hc_head_base,
self.rms_norm_eps,
self.hc_eps,
)
with nvtx_range("final_norm"):
hidden_states = self.norm(hidden_states)
return hidden_states, aux_hidden_states
class DeepseekV4ForCausalLM(BaseCausalLM):
model_cls = DeepseekV4Model
def get_stacked_params_mapping(self):
return [
("gate_up_proj", "w1", 0),
("gate_up_proj", "w3", 1),
("attn.fused_wqa_wkv", "attn.wq_a", 0),
("attn.fused_wqa_wkv", "attn.wkv", 1),
("compressor.fused_wkv_wgate", "compressor.wkv", 0),
("compressor.fused_wkv_wgate", "compressor.wgate", 1),
]
@staticmethod
def _map_weight_name(name: str) -> str:
if name.startswith("layers."):
name = "model." + name
elif name.startswith("embed."):
name = name.replace("embed.", "model.embed_tokens.", 1)
elif name.startswith("norm."):
name = "model." + name
elif name.startswith("hc_head"):
name = "model." + name
elif name == "head.weight":
name = "lm_head.weight"
if ".shared_experts.w2" in name:
name = name.replace(".shared_experts.w2", ".shared_experts.down_proj")
if ".ffn.gate.bias" in name:
name = name.replace(".ffn.gate.bias", ".ffn.gate.e_score_correction_bias")
if re.search(r"\.experts\.\d+\.w[123]\.scale$", name):
name = name.replace(".scale", ".weight_scale")
elif name.endswith(".scale"):
name = name[:-6] + ".weight_scale_inv"
return name
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
stacked_params_mapping = self.get_stacked_params_mapping()
params_dict = dict(self.named_parameters())
moe_loader = build_moe_checkpoint_loader(
params_dict=params_dict,
expert_schema=ExpertCheckpointSchema(
gate_proj_name="w1",
down_proj_name="w2",
up_proj_name="w3",
),
num_experts=self.config.n_routed_experts,
ep_rank=self.mapping.moe.ep_rank,
ep_size=self.mapping.moe.ep_size,
)
for raw_name, loaded_weight in weights:
name = self._map_weight_name(raw_name)
if name.startswith("mtp."):
continue
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name or ".experts." in name:
continue
name = name.replace(weight_name, param_name)
param = params_dict.get(name)
if param is None:
break
param.weight_loader(param, loaded_weight, shard_id)
break
else:
if moe_loader.matches(name):
moe_loader.load(name, loaded_weight)
continue
param = params_dict.get(name)
if param is None:
continue
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
del params_dict, moe_loader
self.post_load_weights()
self.warmup_deep_gemm()
def post_load_weights(self):
mega_moe_experts: list[DeepseekV4MegaMoEExperts] = []
for module in self.modules():
if isinstance(module, DeepseekV4Compressor):
module.process_weights_after_loading()
elif isinstance(module, DeepseekV4MegaMoEExperts):
module.finalize_weights()
mega_moe_experts.append(module)
elif isinstance(module, MoELayer):
module.process_weights_after_loading(module)
def warmup_deep_gemm(self) -> None:
"""Pre-compile all DeepGEMM JIT kernels used by this model.
Called after post_load_weights (not inside it) so that
finalize_weights temporaries have been freed by GC and there is
enough GPU memory for the symmetric buffer allocation.
"""
if deep_gemm is None:
return
import gc
gc.collect()
torch.cuda.empty_cache()
self._warmup_mega_moe_jit()
self._warmup_prefill_jit()
def _warmup_mega_moe_jit(self) -> None:
if os.environ.get("TOKENSPEED_DISABLE_MEGA_MOE_WARMUP") == "1":
return
for module in self.modules():
if not isinstance(module, DeepseekV4MegaMoEExperts):
continue
if module._transformed_l1_weights is None:
continue
logger.info("Pre-compiling DeepGEMM mega-MoE kernel variants...")
group = pg_manager.get_process_group(
"nccl",
module.mapping.moe.tp_ep_group,
)
if torch.distributed.is_initialized():
torch.distributed.barrier(group=group)
deep_gemm.warmup_mega_moe_jit(
num_experts=module.num_experts,
max_num_tokens=module.max_num_tokens,
top_k=module.top_k,
hidden_size=module.hidden_size,
device=torch.device("cuda", torch.cuda.current_device()),
transformed_l1_weights=module._transformed_l1_weights,
transformed_l2_weights=module._transformed_l2_weights,
symm_buffer=module.get_symm_buffer(),
activation_clamp=module.swiglu_limit,
)
return
def _warmup_prefill_jit(self) -> None:
if deep_gemm is None:
return
if torch.cuda.get_device_capability()[0] < 10:
return
config = self.config
tp_size = self.mapping.attn.tp_size if self.mapping else 1
logger.info("Pre-compiling DeepGEMM prefill kernel variants...")
deep_gemm.warmup_prefill_jit(
hidden_size=config.hidden_size,
num_attention_heads=config.num_attention_heads,
head_dim=getattr(config, "head_dim", 128),
hc_mult=getattr(config, "hc_mult", 0),
kv_lora_rank=getattr(config, "kv_lora_rank", 0),
index_n_heads=getattr(config, "index_n_heads", 0),
index_head_dim=getattr(config, "index_head_dim", 0),
indexer_cache_block_size=V4_KERNEL_BLOCK_ROWS,
# With MTP/speculation the verify step flattens bs*num_draft_tokens
# into the decode indexer's num_tokens, so the paged-logits metadata
# kernel (JIT-keyed on the 32-aligned token count) hits larger
# buckets than plain decode. Scale the warmup ceiling by the draft
# factor so those buckets are covered (no-op when speculation is off).
max_decode_tokens=max(
int(global_server_args_dict.get("max_cudagraph_capture_size", 0) or 0),
int(global_server_args_dict.get("max_num_seqs", 0) or 0),
1,
)
* (
int(global_server_args_dict.get("speculative_num_draft_tokens", 1) or 1)
if global_server_args_dict.get("speculative_algorithm")
else 1
),
mxfp4_block_size=DEEPSEEK_V4_MXFP4_BLOCK_SIZE,
tp_size=tp_size,
# Prefill GEMM/prenorm M is capped per forward by chunked_prefill_size
# (continuous batching), the same ceiling mega_moe warms to. Hardcoding
# 8192 would leave M in (8192, chunked_prefill_size] to JIT inline.
max_tokens=_deepseek_v4_mega_moe_max_num_tokens(),
device=torch.device("cuda", torch.cuda.current_device()),
)
def post_quant_warmup(self) -> None:
"""Called by the weight loader after all quant process_weights_after_loading."""
if deep_gemm is not None:
deep_gemm.warmup_fp8_gemm_nt_from_model(
self, max_tokens=_deepseek_v4_mega_moe_max_num_tokens()
)
@classmethod
def get_model_config_for_expert_location(cls, config):
return ModelConfigForExpertLocation(
num_layers=config.num_hidden_layers,
num_logical_experts=config.n_routed_experts,
num_groups=getattr(config, "n_group", 0),
)
EntryClass = [
DeepseekV4ForCausalLM,
]