[DSv4] Integrate TRT-LLM DSv4 Attention for SM100/103 (#30805)

Co-authored-by: Yangmin Li <yangminl@nvidia.com>
Co-authored-by: Po-Han Huang (NVIDIA) <53919306+nvpohanh@users.noreply.github.com>
This commit is contained in:
akhilg-nv
2026-09-09 17:40:07 -07:00
committed by GitHub
co-authored by Yangmin Li Po-Han Huang
parent 0084030179
commit 880d6fa64d
13 changed files with 1408 additions and 18 deletions
@@ -134,6 +134,47 @@ def apply_deepseek_v4_defaults(server_args: ServerArgs, model_arch: str) -> None
run_post_process_pass(server_args, _deepseek_v4_kv_cache_dtype)
if cfg.dsv4_attn_backend == "trtllm":
from sglang.srt.utils.common import is_sm100_supported
assert cfg.device == "cuda" and is_sm100_supported(), (
"--dsv4-attn-backend trtllm requires an SM100/SM103 (Blackwell) GPU."
)
# The resolution pipeline materializes "auto" as fp8_e4m3 on CUDA.
assert cfg.kv_cache_dtype in ("auto", "fp8_e4m3"), (
"--dsv4-attn-backend trtllm requires kv_cache_dtype=fp8_e4m3, "
f"got {cfg.kv_cache_dtype}."
)
assert not cfg.enable_hisparse, (
"--dsv4-attn-backend trtllm does not support enable_hisparse."
)
assert not (
cfg.attn_cp_size > 1 or cfg.dcp_size > 1 or cfg.enable_prefill_cp
), (
"--dsv4-attn-backend trtllm does not support context parallelism "
"(prefill CP, attention CP, or decode CP)."
)
# The trtllm backend stores KV in a 512-byte uniform-FP8 layout while
# FlashMLA uses the 584-byte packed layout; the PD handshake only
# compares kv_cache_dtype, so mismatched prefill/decode backends would
# pass the check and transfer garbage. Reject until the handshake
# carries a layout identifier and the path is tested (#37838).
assert cfg.disaggregation_mode == "null", (
"--dsv4-attn-backend trtllm does not support PD disaggregation yet "
"(uniform-FP8 KV layout is not part of the PD handshake; see "
"https://github.com/sgl-project/sglang/issues/37838)."
)
# The trtllm-gen semaphore buffer is sized from the prefill chunk
# bound; with chunking disabled a single long request has no bound.
assert cfg.chunked_prefill_size is not None and cfg.chunked_prefill_size > 0, (
"--dsv4-attn-backend trtllm requires chunked prefill "
"(--chunked-prefill-size > 0)."
)
logger.info(
"DeepSeek V4 attention: trtllm backend enabled "
"(uniform-FP8 KV pool, decode + sparse prefill)."
)
if cfg.max_running_requests is None:
declare_resolution(
server_args,
@@ -230,6 +230,19 @@ class ExecKernel:
resolvable=True,
),
] = None
dsv4_attn_backend: A[
str,
Arg(
help="DeepSeek V4 attention backend. 'auto' (default) resolves to "
"'flashmla'. 'trtllm' (opt-in, SM100/SM103 with FP8 KV cache) "
"switches the SWA/compressed KV pools to a "
"uniform 512-dim FP8 layout and runs decode and sparse prefill "
"through the flashinfer trtllm-gen sparse MLA kernel. The backend "
"choice is shared by prefill and decode.",
choices=["auto", "flashmla", "trtllm"],
resolvable=True,
),
] = "auto"
dsa_paged_mqa_logits_backend: A[
str,
Arg(
@@ -181,12 +181,15 @@ def create_dsv4_backend(runner):
)
return DeepseekV4HipRadixBackend(runner)
else:
from sglang.srt.layers.attention.deepseek_v4_backend import (
DeepseekV4AttnBackend,
from sglang.srt.layers.attention.deepseek_v4_trtllm_backend import (
create_deepseek_v4_attn_backend,
)
logger.info("Using DeepseekV4AttnBackend for dsv4 attention backend (CUDA).")
return DeepseekV4AttnBackend(runner)
backend = create_deepseek_v4_attn_backend(runner)
logger.info(
f"Using {type(backend).__name__} for dsv4 attention backend (CUDA)."
)
return backend
@register_attention_backend("triton")
@@ -193,6 +193,18 @@ class DSV4AttnMetadata:
c128_page_indices: Optional[torch.Tensor] = None
c128_topk_lengths_clamp1: Optional[torch.Tensor] = None
# Combined decode tables. Only the c4 tail and lens vary by layer.
trtllm_swa_lens: Optional[torch.Tensor] = None
trtllm_c4_indices: Optional[torch.Tensor] = None
trtllm_c4_lens: Optional[torch.Tensor] = None
trtllm_c128_indices: Optional[torch.Tensor] = None
trtllm_c128_lens: Optional[torch.Tensor] = None
# Lazy eager-prefill caches: qmeta and per-ratio combined tables.
trtllm_prefill_qmeta: Optional[tuple] = None
trtllm_prefill_swa_lens: Optional[torch.Tensor] = None
trtllm_prefill_c4_indices: Optional[torch.Tensor] = None
trtllm_prefill_c128: Optional[tuple] = None
c1_flashmla_metadata: FlashMLASchedMeta = field(init=False, repr=False)
c4_flashmla_metadata: FlashMLASchedMeta = field(init=False, repr=False)
c128_flashmla_metadata: FlashMLASchedMeta = field(init=False, repr=False)
@@ -236,6 +248,11 @@ class DSV4AttnMetadata:
"c4_sparse_topk_lengths",
"c4_sparse_page_indices",
"c4_sparse_raw_indices",
"trtllm_swa_lens",
"trtllm_c4_indices",
"trtllm_c4_lens",
"trtllm_c128_indices",
"trtllm_c128_lens",
],
assign_fields=[
# Recomputed by the recorded init_forward_metadata_in_graph op
@@ -244,6 +261,11 @@ class DSV4AttnMetadata:
"c1_flashmla_metadata",
"c4_flashmla_metadata",
"c128_flashmla_metadata",
# Eager-only lazy caches are assigned, not content-copied.
"trtllm_prefill_qmeta",
"trtllm_prefill_swa_lens",
"trtllm_prefill_c4_indices",
"trtllm_prefill_c128",
],
)
@@ -261,6 +283,12 @@ class DSV4AttnMetadata:
"c4_topk_lengths_raw",
"c4_topk_lengths_clamp1",
"c4_sparse_topk_lengths",
# Preserve graph-captured table addresses; refill c4 per layer.
"trtllm_swa_lens",
"trtllm_c4_indices",
"trtllm_c4_lens",
"trtllm_c128_indices",
"trtllm_c128_lens",
]
reference_assign_fields = [
"page_table",
@@ -271,6 +299,11 @@ class DSV4AttnMetadata:
"c1_flashmla_metadata",
"c4_flashmla_metadata",
"c128_flashmla_metadata",
# Reset eager-only caches so a replay cannot reuse another shape.
"trtllm_prefill_qmeta",
"trtllm_prefill_swa_lens",
"trtllm_prefill_c4_indices",
"trtllm_prefill_c128",
]
# Keep graph-captured tensor objects alive for fields that captured
# kernels read by address; overwrite only their contents.
@@ -399,6 +432,51 @@ class DSV4AttnMetadata:
self.c4_flashmla_metadata = _create_flashmla_metadata()
self.c128_flashmla_metadata = _create_flashmla_metadata()
def init_trtllm_sparse_buffers(self) -> None:
"""Build decode tables with 128 SWA columns followed by compressed KV.
Indices use -1 for invalid entries; lens include all 128 SWA slots.
Only the c4 tail and lens are filled per layer.
"""
num_tokens = self.seq_lens_casual.shape[0]
assert self.swa_page_indices.shape == (num_tokens, SWA_WINDOW)
# VarSeq reads rows to the 64-token tile boundary. Back every live view
# with an aligned parent whose extra rows contain inert values.
n_pad = (num_tokens + 63) // 64 * 64
def _tile_padded(fill, src=None, width=None):
shape = (n_pad,) if width is None else (n_pad, width)
buf = torch.full(shape, fill, **self.cuda_int32_kwargs)
if src is not None:
buf[:num_tokens].copy_(src)
return buf[:num_tokens]
if n_pad != num_tokens:
self.seq_lens_casual = _tile_padded(1, self.seq_lens_casual)
self.swa_page_indices = _tile_padded(
-1, self.swa_page_indices, width=SWA_WINDOW
)
self.trtllm_swa_lens = _tile_padded(SWA_WINDOW)
if self.c4_sparse_page_indices is not None:
w4 = self.c4_sparse_page_indices.shape[-1]
assert w4 % 4 == 0, f"{w4=}"
# Unwritten c4 rows must remain inert until the per-layer fill.
self.trtllm_c4_indices = _tile_padded(-1, width=SWA_WINDOW + w4)
self.trtllm_c4_indices[:, :SWA_WINDOW].copy_(self.swa_page_indices)
self.trtllm_c4_lens = _tile_padded(SWA_WINDOW)
if self.c128_page_indices is not None:
w128 = self.c128_page_indices.shape[-1]
assert w128 % 4 == 0, f"{w128=}"
self.trtllm_c128_indices = _tile_padded(-1, width=SWA_WINDOW + w128)
self.trtllm_c128_indices[:, :SWA_WINDOW].copy_(self.swa_page_indices)
self.trtllm_c128_indices[:, SWA_WINDOW:].copy_(self.c128_page_indices)
self.trtllm_c128_lens = _tile_padded(
SWA_WINDOW,
(self.c128_topk_lengths_clamp1 + SWA_WINDOW).to(torch.int32),
)
@dataclass
class DSV4Metadata:
@@ -512,6 +590,7 @@ class DeepseekV4AttnBackend(
use_captured_forward_metadata_for_breakable_cuda_graph: bool = True
supports_ragged_verify_graph: bool = True
needs_cpu_seq_lens: bool = False
trtllm_attn: bool = False
def shared_read_ends(self, fm: ForwardMode) -> SharedReadEnds:
# Breakable-graph verify rereads shared state across segments.
@@ -1051,6 +1130,10 @@ class DeepseekV4AttnBackend(
req_pool_indices=req_pool_indices,
)
)
if self.trtllm_attn:
# DP padding can expand a length-one request into nonpositive
# per-token lens; trtllm-gen requires them to remain at least one.
seq_lens_casual = seq_lens_casual.clamp(min=1)
core_attn_metadata = self.make_core_attn_metadata(
req_to_token=self.req_to_token,
req_pool_indices_repeated=req_pool_indices_repeated,
@@ -1737,6 +1820,20 @@ class DeepseekV4AttnBackend(
extra_indices = match_num_queries(extra_indices, value=-1)
extra_topk_lengths = match_num_queries(extra_topk_lengths, value=1)
if self.trtllm_attn:
# The uniform-FP8 pool is readable only by trtllm-gen.
return self._forward_trtllm(
q=q,
layer=layer,
compress_ratio=compress_ratio,
core_attn_metadata=core_attn_metadata,
forward_batch=forward_batch,
attn_sink=attn_sink,
swa_page_indices=swa_page_indices,
extra_indices=extra_indices,
extra_topk_lengths=extra_topk_lengths,
)
if q.ndim == 3:
q = q.unsqueeze(1)
if swa_page_indices.ndim == 2:
@@ -2263,6 +2360,8 @@ class DeepseekV4AttnBackend(
if need_compress:
core_attn_metadata.init_compression_metadata(num_tokens)
core_attn_metadata.init_flashmla_related(is_prefill=is_prefill)
if self.trtllm_attn:
core_attn_metadata.init_trtllm_sparse_buffers()
else:
core_attn_metadata.c4_sparse_topk_lengths = None
core_attn_metadata.c4_sparse_page_indices = None
@@ -2270,6 +2369,8 @@ class DeepseekV4AttnBackend(
core_attn_metadata.c1_flashmla_metadata = _create_flashmla_metadata()
core_attn_metadata.c4_flashmla_metadata = None
core_attn_metadata.c128_flashmla_metadata = None
if self.trtllm_attn:
core_attn_metadata.init_trtllm_sparse_buffers()
return core_attn_metadata
def get_dspark_swa_page_indices(
@@ -2312,14 +2413,17 @@ class DeepseekV4MultiStepBackend(DeepseekV4AttnBackend):
self.speculative_num_steps = speculative_num_steps
self.attn_backends: List[DeepseekV4AttnBackend] = []
for i in range(self.speculative_num_steps):
self.attn_backends.append(
DeepseekV4AttnBackend(
model_runner,
speculative_step_id=i,
topk=self.topk,
speculative_num_steps=self.speculative_num_steps,
)
)
self.attn_backends.append(self._make_step_backend(model_runner, i))
def _make_step_backend(
self, model_runner: ModelRunner, step_id: int
) -> DeepseekV4AttnBackend:
return DeepseekV4AttnBackend(
model_runner,
speculative_step_id=step_id,
topk=self.topk,
speculative_num_steps=self.speculative_num_steps,
)
def init_forward_metadata_in_graph(self, forward_batch: ForwardBatch) -> None:
for attn_backend in self.attn_backends:
@@ -0,0 +1,532 @@
"""DeepSeek V4 trtllm-gen sparse MLA backend for SM100/SM103.
Decode and varlen prefill use a uniform 512-dim FP8 KV cache. Shared metadata
construction preserves the base backend's CUDA-graph replay semantics.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Literal, Optional, Tuple
import torch
from sglang.srt.environ import envs
from sglang.srt.layers.attention.deepseek_v4_backend import (
SWA_WINDOW,
DeepseekV4AttnBackend,
DeepseekV4MultiStepBackend,
)
from sglang.srt.runtime_context import (
get_exec,
get_parallel,
get_schedule,
get_spec,
max_prefill_buffer_tokens,
)
if TYPE_CHECKING:
from sglang.srt.layers.attention.deepseek_v4_backend import DSV4AttnMetadata
from sglang.srt.layers.radix_attention import RadixAttention
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.model_executor.model_runner import ModelRunner
logger = logging.getLogger(__name__)
# Shared zero-initialized workspace managed by the persistent-buffer lifecycle.
_TRTLLM_GEN_WORKSPACE_SIZE_MB = 128
def _get_trtllm_workspace_buffer(device: torch.device) -> torch.Tensor:
from sglang.srt.runtime_context import get_buffer
return get_buffer(
"trtllm_dsv4_zero_workspace",
lambda: torch.zeros(
_TRTLLM_GEN_WORKSPACE_SIZE_MB * 1024 * 1024,
dtype=torch.int8,
device=device,
),
)
_trtllm_semaphore_installed = False
# Capacity is in query rows: requests x draft tokens for decode, sum_q for prefill.
_trtllm_semaphore_rows: int = 0
def _trtllm_query_row_capacity(model_runner: ModelRunner) -> int:
"""Bound query rows across prefill chunks and speculative decode batches.
The DSv4 hook rejects the backend when chunked prefill is disabled, so the
prefill chunk bound is always finite here.
"""
schedule = get_schedule()
rows = max(
schedule.max_prefill_tokens or 0,
max_prefill_buffer_tokens(),
)
spec = get_spec()
rows_per_req = (
(spec.speculative_num_draft_tokens or 1)
if spec.speculative_algorithm is not None
else 1
)
rows = max(rows, (schedule.max_running_requests or 0) * rows_per_req)
return max(rows, 1)
def _install_persistent_trtllm_semaphores(capacity_rows: int) -> None:
"""Install a persistent counter buffer sized by query rows.
FlashInfer sizes this private buffer by request count, while the DSV4
VarSeq kernel indexes it by query row. Remove this workaround once
FlashInfer accepts a caller-owned buffer.
"""
global _trtllm_semaphore_installed, _trtllm_semaphore_rows
_trtllm_semaphore_rows = max(_trtllm_semaphore_rows, capacity_rows)
if _trtllm_semaphore_installed:
return
import flashinfer.mla._core as _fi_core
_orig = _fi_core._get_trtllm_gen_multi_ctas_kv_counter_buffer
# Allocate once outside graph capture. Stream ordering and the kernel's
# counter reset make one shared buffer safe across launches.
state: dict = {}
def _patched(batch_size, num_qo_heads, sm_count, device):
buf = state.get("buf")
if buf is None or buf.device != device:
assert not torch.cuda.is_current_stream_capturing(), (
"persistent trtllm semaphore buffer must be created outside "
"graph capture (first call is expected during eager warmup)"
)
buf = _orig(_trtllm_semaphore_rows, num_qo_heads, sm_count, device)
state["buf"] = buf
return buf
_fi_core._get_trtllm_gen_multi_ctas_kv_counter_buffer = _patched
_trtllm_semaphore_installed = True
logger.info(
"trtllm-gen multi-CTA semaphores: single persistent buffer sized for "
"%d query rows, shared across launches (flashinfer sizing WAR).",
_trtllm_semaphore_rows,
)
def _check_trtllm_query_rows(num_rows: int) -> None:
# A plain exception, not assert: an over-capacity launch scribbles past
# the semaphore buffer, so this must fire even under python -O.
if num_rows > _trtllm_semaphore_rows:
raise RuntimeError(
f"trtllm-gen launch with {num_rows} query rows exceeds the persistent "
f"semaphore capacity of {_trtllm_semaphore_rows} rows derived from "
"--chunked-prefill-size / --max-prefill-tokens / "
"--max-running-requests; lower --chunked-prefill-size."
)
class DeepseekV4TrtllmAttnBackend(DeepseekV4AttnBackend):
"""DSV4 attention through the trtllm-gen sparse MLA kernel."""
trtllm_attn: bool = True
def __init__(
self,
model_runner: ModelRunner,
skip_prefill: bool = False,
speculative_step_id=0,
topk=0,
speculative_num_steps=0,
):
_install_persistent_trtllm_semaphores(_trtllm_query_row_capacity(model_runner))
super().__init__(
model_runner,
skip_prefill=skip_prefill,
speculative_step_id=speculative_step_id,
topk=topk,
speculative_num_steps=speculative_num_steps,
)
assert self.token_to_kv_pool.uniform_fp8, (
"the trtllm backend requires the uniform-FP8 DSv4 KV pool."
)
assert not envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get(), (
"--dsv4-attn-backend trtllm does not support "
"SGLANG_OPT_USE_ONLINE_COMPRESS yet."
)
# CP round-robin reindexing breaks VarSeq's per-request query packing.
assert get_parallel().attn_cp_size == 1, (
"--dsv4-attn-backend trtllm does not support "
"context parallelism (attn_cp_size > 1) yet."
)
self.trtllm_workspace_buffer = _get_trtllm_workspace_buffer(self.device)
def _forward_trtllm(
self,
*,
q: torch.Tensor,
layer: RadixAttention,
compress_ratio: Literal[0, 4, 128],
core_attn_metadata: DSV4AttnMetadata,
forward_batch: ForwardBatch,
attn_sink: torch.Tensor,
swa_page_indices: torch.Tensor,
extra_indices: Optional[torch.Tensor],
extra_topk_lengths: Optional[torch.Tensor],
) -> torch.Tensor:
assert attn_sink is not None
if (
forward_batch.forward_mode.is_decode_or_idle()
or forward_batch.forward_mode.is_target_verify()
or forward_batch.forward_mode.is_draft_extend_v2()
):
return self._forward_trtllm_decode(
q=q,
layer=layer,
compress_ratio=compress_ratio,
core_attn_metadata=core_attn_metadata,
attn_sink=attn_sink,
swa_page_indices=swa_page_indices,
extra_indices=extra_indices,
extra_topk_lengths=extra_topk_lengths,
)
assert forward_batch.forward_mode.is_extend_without_speculative(), (
"uniform-FP8 pool cannot be read by the packed FlashMLA "
f"kernels; unsupported forward mode "
f"{forward_batch.forward_mode} under "
"--dsv4-attn-backend trtllm"
)
return self._forward_trtllm_prefill(
q=q,
layer=layer,
compress_ratio=compress_ratio,
forward_batch=forward_batch,
attn_sink=attn_sink,
swa_page_indices=swa_page_indices,
extra_indices=extra_indices,
extra_topk_lengths=extra_topk_lengths,
)
def _get_trtllm_bmm_scales(self, layer: RadixAttention) -> Tuple[float, float]:
"""Return host scales; KV uses the store path's fixed unit scale.
Tensor scales corrupt split-KV reduction on FlashInfer < 0.6.13.
"""
assert layer.k_scale_float is None or layer.k_scale_float == 1.0, (
"--dsv4-attn-backend trtllm stores KV with a "
"fixed per-tensor scale of 1.0; a non-unit checkpoint kv-cache "
f"scale (k_scale_float={layer.k_scale_float}) is not supported yet."
)
return (self.softmax_scale, 1.0)
def _trtllm_kv_cache_views(
self, layer_id: int, compress_ratio: Literal[0, 4, 128]
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Return HND views of the uniform-FP8 SWA and compressed pools.
SWA-only layers pass the SWA pool as the required compressed tensor;
``sparse_topk_lens`` masks that region.
"""
token_to_kv_pool = self.token_to_kv_pool
swa_buf = token_to_kv_pool.get_swa_key_buffer_radix(layer_id)
swa_page_size = token_to_kv_pool.swa_kv_pool.page_size
swa_kv_cache = swa_buf.view(swa_buf.shape[0], 1, swa_page_size, 512)
if compress_ratio == 0:
compressed_kv_cache = swa_kv_cache
else:
extra_buf = token_to_kv_pool.get_extra_key_buffer(layer_id)
extra_page_size = token_to_kv_pool.get_extra_key_page_size(layer_id)
compressed_kv_cache = extra_buf.view(
extra_buf.shape[0], 1, extra_page_size, 512
)
return swa_kv_cache, compressed_kv_cache
def _forward_trtllm_decode(
self,
*,
q: torch.Tensor,
layer: RadixAttention,
compress_ratio: Literal[0, 4, 128],
core_attn_metadata: DSV4AttnMetadata,
attn_sink: torch.Tensor,
swa_page_indices: torch.Tensor,
extra_indices: Optional[torch.Tensor],
extra_topk_lengths: Optional[torch.Tensor],
) -> torch.Tensor:
"""Run sparse MLA decode with preallocated metadata tables."""
from flashinfer.mla import trtllm_batch_decode_sparse_mla_dsv4
bs, num_heads, head_dim = q.shape
assert head_dim == 512
# Draft-extend metadata predates DP MAX_LEN padding (#27091). Run only
# its covered rows and leave the discarded padding output finite.
n_meta_rows = core_attn_metadata.seq_lens_casual.shape[0]
out_pad_tail = None
if n_meta_rows < bs:
out_pad_tail = torch.zeros(
(bs, num_heads, 512), dtype=torch.bfloat16, device=q.device
)
q = q[:n_meta_rows]
swa_page_indices = swa_page_indices[:n_meta_rows]
if extra_indices is not None:
extra_indices = extra_indices[:n_meta_rows]
if extra_topk_lengths is not None:
extra_topk_lengths = extra_topk_lengths[:n_meta_rows]
bs = n_meta_rows
# Only the c4 tail and lens vary by layer; other table data is prebuilt.
assert swa_page_indices.shape == (bs, SWA_WINDOW)
if compress_ratio == 0:
# Use the metadata view backed by 64-row-aligned storage because
# the VarSeq kernel reads table rows to the tile boundary.
sparse_indices = core_attn_metadata.swa_page_indices
sparse_topk_lens = core_attn_metadata.trtllm_swa_lens
elif compress_ratio == 128:
sparse_indices = core_attn_metadata.trtllm_c128_indices
sparse_topk_lens = core_attn_metadata.trtllm_c128_lens
else:
sparse_indices = core_attn_metadata.trtllm_c4_indices
sparse_topk_lens = core_attn_metadata.trtllm_c4_lens
assert sparse_indices is not None and sparse_topk_lens is not None, (
"trtllm decode requires metadata built with "
"init_trtllm_sparse_buffers (decode-mode DSV4AttnMetadata)"
)
if sparse_indices.shape[0] != bs:
assert sparse_indices.shape[0] > bs, f"{sparse_indices.shape=} {bs=}"
sparse_indices = sparse_indices[:bs]
if sparse_topk_lens.shape[0] != bs:
assert sparse_topk_lens.shape[0] > bs, f"{sparse_topk_lens.shape=}"
sparse_topk_lens = sparse_topk_lens[:bs]
if compress_ratio == 4:
assert extra_indices is not None and extra_topk_lengths is not None
width = extra_indices.shape[-1]
assert SWA_WINDOW + width == sparse_indices.shape[1], (
f"{width=} {sparse_indices.shape=}"
)
sparse_indices[:, SWA_WINDOW:].copy_(extra_indices)
# Lens include all 128 SWA slots; seq_lens controls their validity.
sparse_topk_lens.copy_(extra_topk_lengths)
sparse_topk_lens.add_(SWA_WINDOW)
swa_kv_cache, compressed_kv_cache = self._trtllm_kv_cache_views(
layer.layer_id, compress_ratio
)
# RoPE is already applied; the unit scale makes this a plain e4m3 cast.
q_fp8 = q.to(torch.float8_e4m3fn).view(bs, 1, num_heads, 512)
bmm1_scale, bmm2_scale = self._get_trtllm_bmm_scales(layer)
seq_lens = core_attn_metadata.seq_lens_casual
if seq_lens.shape[0] != bs:
assert seq_lens.shape[0] > bs, f"{seq_lens.shape=} {bs=}"
seq_lens = seq_lens[:bs]
assert attn_sink.dtype == torch.float32
assert self.trtllm_workspace_buffer is not None
_check_trtllm_query_rows(bs)
out = trtllm_batch_decode_sparse_mla_dsv4(
query=q_fp8,
swa_kv_cache=swa_kv_cache,
workspace_buffer=self.trtllm_workspace_buffer,
sparse_indices=sparse_indices,
compressed_kv_cache=compressed_kv_cache,
sparse_topk_lens=sparse_topk_lens,
seq_lens=seq_lens,
bmm1_scale=bmm1_scale,
bmm2_scale=bmm2_scale,
sinks=attn_sink,
kv_layout="HND",
)
if out_pad_tail is not None:
out_pad_tail[:bs] = out.view(bs, num_heads, 512)
return out_pad_tail
return out.view(bs, num_heads, 512)
def _forward_trtllm_prefill(
self,
*,
q: torch.Tensor,
layer: RadixAttention,
compress_ratio: Literal[0, 4, 128],
forward_batch: ForwardBatch,
attn_sink: torch.Tensor,
swa_page_indices: torch.Tensor,
extra_indices: Optional[torch.Tensor],
extra_topk_lengths: Optional[torch.Tensor],
) -> torch.Tensor:
"""Drive the decode kernel as varlen prefill with one table row per token.
``seq_lens`` includes cached prefixes; the kernel derives causal SWA
validity from it. This path runs eagerly.
"""
from flashinfer.mla import trtllm_batch_decode_sparse_mla_dsv4
assert q.ndim == 3, f"{q.shape=}"
num_qo_padded, num_heads, head_dim = q.shape
assert head_dim == 512
# Build VarSeq metadata from the same extend lengths as the sparse tables.
core = self.forward_metadata.core_attn_metadata
if core.trtllm_prefill_qmeta is None:
extend_seq_lens_cpu = forward_batch.extend_seq_lens_cpu
assert extend_seq_lens_cpu is not None and len(extend_seq_lens_cpu) > 0
batch_size = len(extend_seq_lens_cpu)
cum_lens = [0] * (batch_size + 1)
for i, extend_len in enumerate(extend_seq_lens_cpu):
cum_lens[i + 1] = cum_lens[i] + int(extend_len)
sum_q = cum_lens[-1]
max_q_len = max(int(x) for x in extend_seq_lens_cpu)
assert 0 < sum_q <= num_qo_padded, f"{sum_q=} {num_qo_padded=}"
seq_lens_i32 = forward_batch.seq_lens.to(torch.int32)
assert seq_lens_i32.shape == (batch_size,), f"{seq_lens_i32.shape=}"
core.trtllm_prefill_qmeta = (
self._move_to_device(cum_lens),
max_q_len,
sum_q,
seq_lens_i32,
)
cum_seq_lens_q, max_q_len, sum_q, seq_lens = core.trtllm_prefill_qmeta
# Cache layer-invariant table parts per chunk. The VarSeq kernel reads
# rows to a 64-token boundary, so views need inert, tile-aligned parents.
sum_q_pad = (sum_q + 63) // 64 * 64
def _tile_padded_pf(fill, src=None, width=None):
shape = (sum_q_pad,) if width is None else (sum_q_pad, width)
buf = torch.full(shape, fill, **self.cuda_int32_kwargs)
if src is not None:
buf[:sum_q].copy_(src)
return buf[:sum_q]
swa_indices = _tile_padded_pf(-1, swa_page_indices[:sum_q], width=SWA_WINDOW)
assert swa_indices.shape == (sum_q, SWA_WINDOW), f"{swa_indices.shape=}"
if extra_indices is None:
sparse_indices = swa_indices
if core.trtllm_prefill_swa_lens is None:
core.trtllm_prefill_swa_lens = _tile_padded_pf(SWA_WINDOW)
sparse_topk_lens = core.trtllm_prefill_swa_lens
elif compress_ratio == 128:
if core.trtllm_prefill_c128 is None:
width = extra_indices.shape[-1]
assert width % 4 == 0, f"{width=}"
table = _tile_padded_pf(-1, width=SWA_WINDOW + width)
table[:, :SWA_WINDOW].copy_(swa_indices)
table[:, SWA_WINDOW:].copy_(extra_indices[:sum_q])
assert extra_topk_lengths is not None
lens = _tile_padded_pf(
SWA_WINDOW,
extra_topk_lengths[:sum_q].to(torch.int32) + SWA_WINDOW,
)
core.trtllm_prefill_c128 = (table, lens)
sparse_indices, sparse_topk_lens = core.trtllm_prefill_c128
else:
assert extra_topk_lengths is not None
width = extra_indices.shape[-1]
# _pad_last_dim keeps the combined c4 capacity divisible by four.
assert width % 4 == 0, f"{width=}"
if core.trtllm_prefill_c4_indices is None:
core.trtllm_prefill_c4_indices = _tile_padded_pf(
-1, width=SWA_WINDOW + width
)
core.trtllm_prefill_c4_indices[:, :SWA_WINDOW].copy_(swa_indices)
sparse_indices = core.trtllm_prefill_c4_indices
assert sparse_indices.shape == (
sum_q,
SWA_WINDOW + width,
), f"{sparse_indices.shape=} {width=}"
sparse_indices[:, SWA_WINDOW:].copy_(extra_indices[:sum_q])
# Lens include 128 SWA slots; VarSeq metadata controls validity.
sparse_topk_lens = _tile_padded_pf(
SWA_WINDOW,
extra_topk_lengths[:sum_q].to(torch.int32) + SWA_WINDOW,
)
# RoPE is already applied; the unit scale makes this a plain e4m3 cast.
q_fp8 = q[:sum_q].to(torch.float8_e4m3fn)
swa_kv_cache, compressed_kv_cache = self._trtllm_kv_cache_views(
layer.layer_id, compress_ratio
)
bmm1_scale, bmm2_scale = self._get_trtllm_bmm_scales(layer)
assert attn_sink.dtype == torch.float32
assert self.trtllm_workspace_buffer is not None
_check_trtllm_query_rows(sum_q)
out_padded = None
out_arg = None
if num_qo_padded != sum_q:
# Run only real tokens and keep discarded padding rows finite.
out_padded = torch.zeros(
(num_qo_padded, num_heads, 512),
dtype=torch.bfloat16,
device=q.device,
)
out_arg = out_padded[:sum_q]
out = trtllm_batch_decode_sparse_mla_dsv4(
query=q_fp8,
swa_kv_cache=swa_kv_cache,
workspace_buffer=self.trtllm_workspace_buffer,
sparse_indices=sparse_indices,
compressed_kv_cache=compressed_kv_cache,
sparse_topk_lens=sparse_topk_lens,
seq_lens=seq_lens,
out=out_arg,
bmm1_scale=bmm1_scale,
bmm2_scale=bmm2_scale,
sinks=attn_sink,
kv_layout="HND",
cum_seq_lens_q=cum_seq_lens_q,
max_q_len=max_q_len,
)
return out_padded if out_padded is not None else out
class DeepseekV4TrtllmMultiStepBackend(
DeepseekV4MultiStepBackend, DeepseekV4TrtllmAttnBackend
):
"""Multi-step draft wrapper whose per-step backends are trtllm."""
def _make_step_backend(
self, model_runner: ModelRunner, step_id: int
) -> DeepseekV4AttnBackend:
return DeepseekV4TrtllmAttnBackend(
model_runner,
speculative_step_id=step_id,
topk=self.topk,
speculative_num_steps=self.speculative_num_steps,
)
def is_dsv4_trtllm_attn_enabled() -> bool:
return get_exec().kernel.dsv4_attn_backend == "trtllm"
def create_deepseek_v4_attn_backend(
model_runner: ModelRunner, **kwargs
) -> DeepseekV4AttnBackend:
"""Construct the DSV4 backend matching --dsv4-attn-backend."""
cls = (
DeepseekV4TrtllmAttnBackend
if is_dsv4_trtllm_attn_enabled()
else DeepseekV4AttnBackend
)
return cls(model_runner, **kwargs)
def create_deepseek_v4_multistep_backend(
model_runner: ModelRunner, topk: int, speculative_num_steps: int
) -> DeepseekV4MultiStepBackend:
cls = (
DeepseekV4TrtllmMultiStepBackend
if is_dsv4_trtllm_attn_enabled()
else DeepseekV4MultiStepBackend
)
return cls(model_runner, topk=topk, speculative_num_steps=speculative_num_steps)
@@ -0,0 +1,120 @@
"""Unfused compressor store for the trtllm backend's uniform-FP8 KV pool.
The FlashMLA epilogue writes a different packed layout. Keep this pipeline
separate until the fused uniform-FP8 store in PR #32975 replaces it.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.kernels.ops.attention.dsv4 import compress_forward
if TYPE_CHECKING:
from sglang.srt.layers.attention.dsv4.compressor import Compressor
from sglang.srt.layers.attention.dsv4.compressor_v2 import CompressorBackendMixin
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
def _mask_invalid_prefill_compress_rows(
kv_compressed: torch.Tensor,
plan_raw: torch.Tensor,
out_loc: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Make invalid prefill-plan rows safe for BCG's static-shape store."""
valid = plan_raw[:, 0] != -1
kv_compressed = torch.where(
valid.unsqueeze(-1), kv_compressed, torch.zeros_like(kv_compressed)
)
ragged_ids = plan_raw[:, 1].to(torch.int32) & 0xFFFF
safe_ragged_ids = torch.where(valid, ragged_ids, torch.zeros_like(ragged_ids))
mapped_out_loc = out_loc[safe_ragged_ids.long()]
# Reserved slot 0 safely absorbs duplicate invalid writes.
out_loc_to_store = torch.where(
valid, mapped_out_loc, torch.zeros_like(mapped_out_loc)
)
return kv_compressed, out_loc_to_store
def forward_compress_uniform_fp8(
backend: CompressorBackendMixin,
*,
token_to_kv_pool: DeepSeekV4TokenToKVPool,
kv_score_input: torch.Tensor,
state_pool,
compressor: Compressor,
layer_id: int,
) -> None:
"""Compress, normalize, apply RoPE, and store as uniform e4m3."""
from sglang.kernels.ops.attention.deepseek_v4_rope import (
fused_norm_rope_inplace_triton,
)
from sglang.srt.layers.attention.dsv4.compressor_v2 import (
_extract_positions_from_plan,
_use_online_compress,
is_overlap_compress,
)
assert not compressor.is_in_indexer
assert compressor.head_dim == 512, f"{compressor.head_dim=}"
assert not _use_online_compress(compressor.ratio), (
"SGLANG_OPT_USE_ONLINE_COMPRESS is not supported with the "
"uniform-FP8 KV layout yet."
)
compress_ratio = compressor.ratio
head_dim = compressor.head_dim
plan = backend._get_paged_compress_metadata(compress_ratio)
out_loc = backend._get_out_loc(compress_ratio)
coff = 2 if is_overlap_compress(compress_ratio) else 1
kv_score_buffer = state_pool.kv_score_buffer.kv_score.view(
-1, compress_ratio, 2 * head_dim * coff
)
kv_compressed = compress_forward(
kv_score_buffer=kv_score_buffer,
kv_score_input=kv_score_input,
ape=compressor.ape.view(-1, head_dim),
plan=plan,
compress_ratio=compress_ratio,
head_dim=head_dim,
is_online=False,
)
if kv_compressed.shape[0] == 0:
return
plan_raw = plan[1].view(torch.int32)
if plan.is_decode:
# Zero out non-boundary tokens to prevent corrupting kvcache loc 0.
seq_lens_plan = plan_raw[:, 0].to(torch.int32)
is_boundary = (seq_lens_plan % compress_ratio == 0).unsqueeze(-1)
kv_compressed = torch.where(
is_boundary, kv_compressed, torch.zeros_like(kv_compressed)
)
out_loc_to_store = out_loc
else:
kv_compressed, out_loc_to_store = _mask_invalid_prefill_compress_rows(
kv_compressed,
plan_raw,
out_loc,
)
positions = _extract_positions_from_plan(plan, compress_ratio).clamp(min=0)
fused_norm_rope_inplace_triton(
kv_compressed,
compressor.norm.weight,
compressor.norm.variance_epsilon,
compressor.freqs_cis,
positions=positions,
)
token_to_kv_pool.set_extra_key_buffer_fused(
layer_id=layer_id,
loc=out_loc_to_store,
cache_k=kv_compressed,
)
@@ -241,6 +241,22 @@ class CompressorBackendMixin:
is_unified_kv_triton,
)
if token_to_kv_pool.uniform_fp8 and not compressor.is_in_indexer:
# The fused epilogue writes only the packed FlashMLA layout.
from sglang.srt.layers.attention.dsv4.compressor_trtllm import (
forward_compress_uniform_fp8,
)
forward_compress_uniform_fp8(
self,
token_to_kv_pool=token_to_kv_pool,
kv_score_input=kv_score_input,
state_pool=state_pool,
compressor=compressor,
layer_id=layer_id,
)
return
out_loc = self._get_out_loc(compressor.ratio)
use_fp4_indexer = (
compressor.is_in_indexer and self.enable_deepseek_v4_fp4_indexer
@@ -187,6 +187,63 @@ class DeepSeekV4SingleKVPool(KVCache):
raise NotImplementedError("Use get_key_buffer instead.")
class DeepSeekV4UniformFP8KVPool(DeepSeekV4SingleKVPool):
"""Uniform 512-dim FP8 (e4m3) variant of the DSv4 single-KV pool.
Each token is 448 NoPE + 64 RoPE contiguous e4m3 values without in-cache
scales or per-page padding. The backend supplies the dequant scale.
"""
def get_bytes_per_token(self) -> int:
return self.qk_nope_head_dim + self.qk_rope_head_dim
def create_buffer(self, *, num_pages: int):
bytes_per_token = self.get_bytes_per_token()
assert bytes_per_token == 512, (
"DSV4 uniform-FP8 KV layout: qk_nope_head_dim (448) + "
"qk_rope_head_dim (64), all e4m3 = 512 bytes/token"
)
self.kv_cache_total_dim = bytes_per_token
self.bytes_per_page_padded = self.page_size * bytes_per_token
return torch.zeros(
num_pages,
self.page_size * bytes_per_token,
dtype=torch.float8_e4m3fn,
device=self.device,
)
def get_key_buffer(self, layer_id: int):
return self.kv_buffer[layer_id]
def set_key_buffer(
self,
layer_id: int,
loc: torch.Tensor,
cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
):
raise NotImplementedError(
"The packed NopeFp8RopeBf16Pack store does not apply to the "
"uniform-FP8 pool; use set_key_buffer_fused."
)
def set_key_buffer_fused(
self,
layer_id: int,
loc: torch.Tensor,
cache_k: torch.Tensor,
) -> None:
"""Store normed/roped rows as e4m3 with the backend's fixed unit scale.
uint8 views work around index_put not supporting FP8 dtypes.
"""
assert cache_k.dim() == 2 and cache_k.shape[1] == self.kv_cache_total_dim
self.kv_buffer[layer_id].view(torch.uint8).view(-1, self.kv_cache_total_dim)[
loc.long()
] = cache_k.to(torch.float8_e4m3fn).view(torch.uint8)
class HiSparseC4DevicePool(DeepSeekV4SingleKVPool):
def __init__(
self,
@@ -578,6 +635,10 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
# Resolve the unified-kv gate before any sizing so the two cannot drift.
self._unified_kv = is_unified_kv_triton()
# Uniform 512-dim e4m3 layout for the trtllm attention backend
self.uniform_fp8 = (
not self._unified_kv
) and get_exec().kernel.dsv4_attn_backend == "trtllm"
c4_ring_size = self.get_ring_size(4)
if self._unified_kv:
# Unified C4 state is request-addressed: one ring per req slot,
@@ -665,6 +726,13 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
self.swa_req_ring_size = self.unified_swa_ring_size
else:
self.unified_kv_pool = None
kv_pool_cls: type = DeepSeekV4SingleKVPool
if self.uniform_fp8:
assert dtype == torch.float8_e4m3fn, (
"--dsv4-attn-backend trtllm requires "
f"kv_cache_dtype=fp8_e4m3, got {dtype}"
)
kv_pool_cls = DeepSeekV4UniformFP8KVPool
self.swa_kv_pool = self._make_kv_pool(
size=swa_size,
page_size=swa_page_size,
@@ -673,10 +741,14 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
device=device,
enable_memory_saver=enable_memory_saver,
global_page_size=swa_page_size,
cls=kv_pool_cls,
)
c4_kv_pool_type = DeepSeekV4SingleKVPool
c4_kv_pool_type = kv_pool_cls
if enable_hisparse:
assert not self.uniform_fp8, (
"enable_hisparse is not supported with --dsv4-attn-backend trtllm."
)
c4_kv_pool_type = HiSparseC4DevicePool
self.c4_kv_pool = self._make_kv_pool(
size=c4_size,
@@ -697,6 +769,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
device=device,
enable_memory_saver=enable_memory_saver,
global_page_size=page_size,
cls=kv_pool_cls,
)
indexer_size = self.c4_logical_size
@@ -1286,6 +1359,26 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
freqs_cis: torch.Tensor,
positions: torch.Tensor,
) -> None:
if self.uniform_fp8:
# Uniform-FP8 (trtllm-gen) layout: norm + RoPE with the existing
# Triton kernel (in-place on kv; safe -- kv is not read again),
# then a plain e4m3 cast + scatter in the pool setter (per-tensor
# scale 1.0). Fusing the store is deferred to the perf phase.
from sglang.kernels.ops.attention.deepseek_v4_rope import (
fused_norm_rope_inplace_triton,
)
fused_norm_rope_inplace_triton(
kv,
kv_weight,
eps,
freqs_cis,
positions=positions,
)
self.swa_kv_pool.set_key_buffer_fused(
self._swa_local_layer_id(layer_id), swa_loc, kv
)
return
fused_k_norm_rope_flashmla(
kv=kv,
kv_weight=kv_weight,
+7 -5
View File
@@ -433,8 +433,8 @@ class DraftBackendFactory:
DeepseekV4MultiStepBackend,
)
else:
from sglang.srt.layers.attention.deepseek_v4_backend import (
DeepseekV4MultiStepBackend,
from sglang.srt.layers.attention.deepseek_v4_trtllm_backend import (
create_deepseek_v4_multistep_backend as DeepseekV4MultiStepBackend,
)
return (
@@ -573,11 +573,13 @@ class DraftBackendFactory:
"dsv4",
DeepseekV4HipRadixBackend(self.draft_model_runner, skip_prefill=False),
)
from sglang.srt.layers.attention.deepseek_v4_backend import (
DeepseekV4AttnBackend,
from sglang.srt.layers.attention.deepseek_v4_trtllm_backend import (
create_deepseek_v4_attn_backend,
)
return (
"dsv4",
DeepseekV4AttnBackend(self.draft_model_runner, skip_prefill=False),
create_deepseek_v4_attn_backend(
self.draft_model_runner, skip_prefill=False
),
)