Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1583 lines
64 KiB
Python
Executable File
1583 lines
64 KiB
Python
Executable File
"""
|
|
Support attention backend for TRTLLM MLA kernels from flashinfer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import math
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Optional, Union
|
|
|
|
import torch
|
|
import triton
|
|
|
|
from sglang.kernels.ops.attention.fixup_zero_kv import fixup_zero_kv_rows
|
|
from sglang.kernels.ops.attention.pad import (
|
|
pad_draft_extend_query as pad_draft_extend_query_triton,
|
|
)
|
|
from sglang.kernels.ops.attention.pad import (
|
|
unpad_draft_extend_output as unpad_draft_extend_output_triton,
|
|
)
|
|
from sglang.kernels.ops.attention.set_mla_kv_concat_q import (
|
|
can_use_set_mla_kv_concat_q,
|
|
can_use_set_mla_kv_concat_q_fp8,
|
|
)
|
|
from sglang.kernels.ops.attention.set_mla_kv_concat_q import (
|
|
covered as set_mla_kv_concat_q_covered,
|
|
)
|
|
from sglang.kernels.ops.attention.set_mla_kv_concat_q import (
|
|
covered_fp8 as set_mla_kv_concat_q_fp8_covered,
|
|
)
|
|
from sglang.kernels.ops.attention.set_mla_kv_concat_q import (
|
|
set_mla_kv_concat_q,
|
|
set_mla_kv_concat_q_fp8,
|
|
)
|
|
from sglang.kernels.ops.attention.utils import (
|
|
concat_mla_absorb_q_general,
|
|
mla_quantize_and_rope_for_fp8,
|
|
mla_quantize_without_rope_for_fp8,
|
|
)
|
|
from sglang.kernels.ops.kvcache.kv_indices import (
|
|
create_flashmla_kv_indices_triton,
|
|
get_num_kv_index_blocks_flashmla,
|
|
get_num_page_per_block_flashmla,
|
|
)
|
|
from sglang.kernels.ops.quantization.fp8_kernel import scaled_fp8_quant
|
|
from sglang.srt.environ import envs
|
|
from sglang.srt.layers.attention.flashinfer_mla_backend import (
|
|
FlashInferMLAAttnBackend,
|
|
FlashInferMLAMultiStepDraftBackend,
|
|
)
|
|
from sglang.srt.layers.attention.unified_mem_hooks import unified_mla_hooks
|
|
from sglang.srt.layers.attention.verify_mask import VerifyMask, maybe_create_verify_mask
|
|
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
|
|
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph import (
|
|
is_in_breakable_cuda_graph,
|
|
)
|
|
from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph import (
|
|
is_in_tc_piecewise_cuda_graph,
|
|
)
|
|
from sglang.srt.runtime_context import (
|
|
get_buffer,
|
|
get_parallel,
|
|
get_schedule,
|
|
get_spec,
|
|
)
|
|
from sglang.srt.utils import is_flashinfer_available, is_float4_e2m1fn_x2
|
|
|
|
if is_flashinfer_available():
|
|
import flashinfer
|
|
|
|
if TYPE_CHECKING:
|
|
from sglang.srt.layers.radix_attention import RadixAttention
|
|
from sglang.srt.model_executor.model_runner import ModelRunner
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Constants
|
|
DEFAULT_WORKSPACE_SIZE_MB = 150 # Memory workspace size in MB
|
|
|
|
# Block constraint from flashinfer requirements
|
|
# From flashinfer.decode._check_trtllm_gen_mla_shape:
|
|
# block_num % (128 / block_size) == 0
|
|
# This imposes that the total number of blocks must be divisible by
|
|
# (128 / block_size). We capture the 128 constant here so we can
|
|
# compute the LCM with other padding constraints.
|
|
TRTLLM_BLOCK_CONSTRAINT = 128
|
|
|
|
TRTLLM_MLA_MAX_BATCH_SIZE = 8192
|
|
|
|
|
|
def _multi_ctas_kv_counter_bytes(
|
|
device: torch.device, num_q_heads: int, batch_size: int
|
|
) -> int:
|
|
sm_count = flashinfer.utils.get_device_sm_count(device)
|
|
return flashinfer.utils.get_trtllm_gen_multi_ctas_kv_counter_bytes(
|
|
batch_size, num_q_heads, sm_count
|
|
)
|
|
|
|
|
|
def make_persistent_multi_ctas_kv_counter_buffer(
|
|
device: torch.device, num_q_heads: int, max_batch_size: int
|
|
) -> torch.Tensor:
|
|
num_bytes = _multi_ctas_kv_counter_bytes(
|
|
device, num_q_heads, max(TRTLLM_MLA_MAX_BATCH_SIZE, max_batch_size)
|
|
)
|
|
return torch.zeros(num_bytes, dtype=torch.uint8, device=device)
|
|
|
|
|
|
def grow_multi_ctas_kv_counter_buffer_if_needed(
|
|
buffer: torch.Tensor, device: torch.device, num_q_heads: int, batch_size: int
|
|
) -> torch.Tensor:
|
|
required_bytes = _multi_ctas_kv_counter_bytes(device, num_q_heads, batch_size)
|
|
if buffer.numel() >= required_bytes:
|
|
return buffer
|
|
return torch.zeros(required_bytes, dtype=torch.uint8, device=device)
|
|
|
|
|
|
def _quantize_fp8_qkv(q, k, v, layer):
|
|
q = q.to(torch.float8_e4m3fn)
|
|
|
|
k_scale = getattr(layer, "k_scale_float", None)
|
|
if k_scale is None:
|
|
k_scale = 1.0
|
|
if k_scale != 1.0:
|
|
assert hasattr(layer, "k_scale"), "k_scale is not set"
|
|
k_2d, _ = scaled_fp8_quant(
|
|
k.reshape(-1, k.shape[-1]).contiguous(), layer.k_scale
|
|
)
|
|
k = k_2d.reshape(k.shape)
|
|
else:
|
|
k = k.to(torch.float8_e4m3fn)
|
|
|
|
v_scale = getattr(layer, "v_scale_float", None)
|
|
if v_scale is None:
|
|
v_scale = 1.0
|
|
if v_scale != 1.0:
|
|
assert hasattr(layer, "v_scale"), "v_scale is not set"
|
|
v_2d, _ = scaled_fp8_quant(
|
|
v.reshape(-1, v.shape[-1]).contiguous(), layer.v_scale
|
|
)
|
|
v = v_2d.reshape(v.shape)
|
|
else:
|
|
v = v.to(torch.float8_e4m3fn)
|
|
|
|
return q, k, v, k_scale, v_scale
|
|
|
|
|
|
# cute-dsl needs its own workspace: it overwrites the buffer with split-KV
|
|
# partials, which corrupts the trtllm-gen multiCtasKv counters that rely on the
|
|
# zero-init buffer (they share it under attention-backend=cutedsl_mla, where
|
|
# draft-extend falls back to trtllm-gen) and deadlocks the reduction.
|
|
global_cute_dsl_workspace_buffer = None
|
|
|
|
|
|
@dataclass
|
|
class TRTLLMMLAPrefillMetadata:
|
|
"""Metadata for TRTLLM MLA prefill operations."""
|
|
|
|
max_seq_len: int
|
|
cum_seq_lens: torch.Tensor
|
|
seq_lens: torch.Tensor
|
|
fallback_to_flashinfer_impl: bool = False
|
|
|
|
|
|
from sglang.kernels.jit.utils import is_arch_support_pdl
|
|
|
|
# Arm PDL on the trtllm-gen decode launch so its prolog overlaps the tail of
|
|
# the query-prep kernels (which already trigger their PDL secondary).
|
|
_ENABLE_PDL = is_arch_support_pdl()
|
|
|
|
|
|
@dataclass
|
|
class TRTLLMMLADecodeMetadata:
|
|
"""Metadata for TRTLLM MLA decode operations."""
|
|
|
|
block_kv_indices: Optional[torch.Tensor] = None
|
|
max_seq_len_k: Optional[int] = None
|
|
max_seq_len_q: Optional[int] = None
|
|
sum_seq_lens_q: Optional[int] = None
|
|
cu_seqlens_q: Optional[torch.Tensor] = None
|
|
seq_lens_q: Optional[torch.Tensor] = None
|
|
seq_lens_k: Optional[torch.Tensor] = None
|
|
global_seq_lens_k: Optional[torch.Tensor] = None
|
|
|
|
|
|
class TRTLLMMLABackend(FlashInferMLAAttnBackend):
|
|
"""TRTLLM MLA attention kernel from flashinfer."""
|
|
|
|
# trtllm-gen kernels rebuild metadata from preallocated buffers and never
|
|
# read seq_lens_cpu / seq_lens_sum; opt out of the D2H sync.
|
|
needs_cpu_seq_lens: bool = False
|
|
|
|
# Ragged verify: the packed query is front-aligned into the dense
|
|
# [bs, draft_token_num] layout in forward_extend; metadata stays uniform.
|
|
supports_ragged_verify_graph: bool = True
|
|
|
|
def __init__(
|
|
self,
|
|
model_runner: ModelRunner,
|
|
skip_prefill: bool = False,
|
|
kv_indptr_buf: Optional[torch.Tensor] = None,
|
|
q_indptr_decode_buf: Optional[torch.Tensor] = None,
|
|
backend: str = "trtllm-gen",
|
|
):
|
|
super().__init__(
|
|
model_runner,
|
|
skip_prefill,
|
|
kv_indptr_buf,
|
|
q_indptr_decode_buf,
|
|
)
|
|
|
|
config = model_runner.model_config
|
|
|
|
# Model parameters
|
|
self.num_q_heads = config.num_attention_heads // get_parallel().attn_tp_size
|
|
self.num_kv_heads = config.get_num_kv_heads(get_parallel().attn_tp_size)
|
|
self.num_local_heads = config.num_attention_heads // get_parallel().attn_tp_size
|
|
|
|
# MLA-specific dimensions
|
|
self.kv_lora_rank = config.kv_lora_rank
|
|
self.qk_nope_head_dim = config.qk_nope_head_dim
|
|
self.qk_rope_head_dim = config.qk_rope_head_dim
|
|
self.v_head_dim = config.v_head_dim
|
|
self.kv_cache_dim = self.kv_lora_rank + self.qk_rope_head_dim
|
|
|
|
# Runtime parameters
|
|
self.backend = backend
|
|
self.data_type = model_runner.kv_cache_dtype
|
|
self.q_data_type = model_runner.dtype
|
|
self.page_size = model_runner.page_size
|
|
self.req_to_token = model_runner.req_to_token_pool.req_to_token
|
|
|
|
# Workspace allocation
|
|
self.workspace_size = DEFAULT_WORKSPACE_SIZE_MB * 1024 * 1024
|
|
if self.backend == "cute-dsl":
|
|
# Separate buffer from trtllm-gen (see note above); safe to share
|
|
# among cute-dsl instances.
|
|
global global_cute_dsl_workspace_buffer
|
|
if global_cute_dsl_workspace_buffer is None:
|
|
global_cute_dsl_workspace_buffer = torch.zeros(
|
|
self.workspace_size,
|
|
dtype=torch.int8,
|
|
device=model_runner.device,
|
|
)
|
|
self.workspace_buffer = global_cute_dsl_workspace_buffer
|
|
else:
|
|
self.workspace_buffer = get_buffer(
|
|
"trtllm_mla_zero_workspace",
|
|
lambda: torch.zeros(
|
|
self.workspace_size,
|
|
dtype=torch.int8,
|
|
device=model_runner.device,
|
|
),
|
|
)
|
|
|
|
self._multi_ctas_kv_counter_buffer = (
|
|
make_persistent_multi_ctas_kv_counter_buffer(
|
|
torch.device(self.device),
|
|
self.num_q_heads,
|
|
max_batch_size=model_runner.max_running_requests,
|
|
)
|
|
)
|
|
|
|
# CUDA graph state
|
|
self.decode_cuda_graph_metadata = {}
|
|
self.decode_cuda_graph_kv_indices = None
|
|
self.padded_q_buffer = None
|
|
self.unpad_output_buffer = None
|
|
self.forward_prefill_metadata: Optional[TRTLLMMLAPrefillMetadata] = None
|
|
self.forward_decode_metadata: Union[TRTLLMMLADecodeMetadata, None] = None
|
|
|
|
self.disable_chunked_prefix_cache = get_schedule().disable_chunked_prefix_cache
|
|
|
|
self.num_draft_tokens = get_spec().speculative_num_draft_tokens
|
|
self.dense_q_indptr_verify = (
|
|
self.q_indptr_decode * self.num_draft_tokens
|
|
if self.num_draft_tokens
|
|
else None
|
|
)
|
|
self._verify_mask = None
|
|
# Tree-mask scratch is fetched from the target backend only.
|
|
self.is_draft_runner = model_runner.is_draft_worker
|
|
|
|
# Unified-memory dense-view hooks (None on the static pool). req_to_token
|
|
# holds VIRTUAL token ids; the block table needs DENSE page ids, so the
|
|
# kv-index kernels gather virtual->physical page through `_v2p_page_table`
|
|
# then scale by `_kernel_page_multiplier` (= num MLA layers). See
|
|
# build_dense_mla_views / create_flashmla_kv_indices_triton.
|
|
_hooks = unified_mla_hooks(model_runner.token_to_kv_pool_allocator)
|
|
self._v2p_page_table = _hooks.v2p_page_table
|
|
self._kernel_page_multiplier = _hooks.kernel_page_multiplier
|
|
self._unified_mla = _hooks.enabled
|
|
# virtual token id -> DENSE kernel-facing id, for the KV write loc.
|
|
self._translate_kv_loc_dense = _hooks.translate_kv_loc_dense
|
|
# Per-forward dense write loc ([:n] view of a capture-stable buffer),
|
|
# set by the cuda-graph out-graph hook; None on the eager path (where the
|
|
# write translates through the pool's _full_translate hook instead).
|
|
self._decode_dense_loc: Optional[torch.Tensor] = None
|
|
self.cuda_graph_out_cache_loc_dense: Optional[torch.Tensor] = None
|
|
# Fused KV-scatter + q-concat on the decode dense-loc path (one launch
|
|
# instead of set_mla_kv_buffer + concat_mla_absorb_q). Disabled under
|
|
# async asserts: the fused path writes the pool directly and would
|
|
# skip the pool's OOB probe.
|
|
self._fused_set_kv_concat_q = (
|
|
self.data_type == torch.bfloat16
|
|
and not envs.SGLANG_ENABLE_ASYNC_ASSERT.get()
|
|
and can_use_set_mla_kv_concat_q(
|
|
self.kv_lora_rank * 2, self.qk_rope_head_dim * 2
|
|
)
|
|
)
|
|
# fp8 sibling: quantize + KV scatter + q concat in one launch
|
|
# (replaces mla_quantize_without_rope_for_fp8's concat + three aten
|
|
# casts plus the KV-row write on the fp8 decode path).
|
|
self._fused_set_kv_concat_q_fp8 = (
|
|
self.data_type == torch.float8_e4m3fn
|
|
and not envs.SGLANG_ENABLE_ASYNC_ASSERT.get()
|
|
and self.kv_lora_rank == 512
|
|
and self.qk_rope_head_dim == 64
|
|
and can_use_set_mla_kv_concat_q_fp8()
|
|
)
|
|
|
|
def _calc_padded_blocks(self, max_seq_len: int) -> int:
|
|
"""
|
|
Calculate padded block count that satisfies both TRT-LLM and Triton constraints.
|
|
|
|
Args:
|
|
max_seq_len: Maximum sequence length in tokens
|
|
|
|
Returns:
|
|
Number of blocks padded to satisfy all constraints
|
|
"""
|
|
blocks = triton.cdiv(max_seq_len, self.page_size)
|
|
|
|
# Apply dual constraints (take LCM to satisfy both):
|
|
# 1. TRT-LLM: block_num % (128 / page_size) == 0
|
|
# 2. Triton: number of pages per block
|
|
trtllm_constraint = TRTLLM_BLOCK_CONSTRAINT // self.page_size
|
|
triton_constraint = get_num_page_per_block_flashmla(self.page_size)
|
|
constraint_lcm = math.lcm(trtllm_constraint, triton_constraint)
|
|
|
|
if blocks % constraint_lcm != 0:
|
|
blocks = triton.cdiv(blocks, constraint_lcm) * constraint_lcm
|
|
return blocks
|
|
|
|
def _create_block_kv_indices(
|
|
self,
|
|
batch_size: int,
|
|
max_blocks: int,
|
|
req_pool_indices: torch.Tensor,
|
|
seq_lens: torch.Tensor,
|
|
device: torch.device,
|
|
) -> torch.Tensor:
|
|
"""
|
|
Create block KV indices tensor using Triton kernel.
|
|
|
|
Args:
|
|
batch_size: Batch size
|
|
max_blocks: Maximum number of blocks per sequence
|
|
req_pool_indices: Request pool indices
|
|
seq_lens: Sequence lengths
|
|
device: Target device
|
|
|
|
Returns:
|
|
Block KV indices tensor
|
|
"""
|
|
block_kv_indices = torch.full(
|
|
(batch_size, max_blocks), -1, dtype=torch.int32, device=device
|
|
)
|
|
|
|
create_flashmla_kv_indices_triton[
|
|
(
|
|
batch_size,
|
|
get_num_kv_index_blocks_flashmla(max_blocks, self.page_size),
|
|
)
|
|
](
|
|
self.req_to_token,
|
|
req_pool_indices,
|
|
seq_lens,
|
|
None,
|
|
block_kv_indices,
|
|
self.req_to_token.stride(0),
|
|
max_blocks,
|
|
PAGED_SIZE=self.page_size,
|
|
v2p_ptr=self._v2p_page_table,
|
|
PAGE_MULT=self._kernel_page_multiplier,
|
|
)
|
|
|
|
return block_kv_indices
|
|
|
|
def init_cuda_graph_state(
|
|
self,
|
|
max_bs: int,
|
|
max_num_tokens: int,
|
|
kv_indices_buf: Optional[torch.Tensor] = None,
|
|
):
|
|
"""Initialize CUDA graph state for TRTLLM MLA."""
|
|
|
|
max_blocks_per_seq = self._calc_padded_blocks(self.max_context_len)
|
|
|
|
self.decode_cuda_graph_kv_indices = torch.full(
|
|
(max_bs, max_blocks_per_seq), -1, dtype=torch.int32, device=self.device
|
|
)
|
|
# Unified pool: capture-stable buffer for the DENSE KV write loc, filled
|
|
# out-of-graph in init_forward_metadata_out_graph so the in-graph
|
|
# set_mla_kv_buffer captures no translate.
|
|
if self._unified_mla:
|
|
self.cuda_graph_out_cache_loc_dense = torch.zeros(
|
|
max_num_tokens, dtype=torch.int64, device=self.device
|
|
)
|
|
num_tokens_per_req = max_num_tokens // max_bs
|
|
|
|
if is_float4_e2m1fn_x2(self.data_type):
|
|
# Buffer for padded query: (max_bs, max_draft_tokens, num_q_heads, v_head_dim)
|
|
self.store_dtype = torch.uint8
|
|
self.padded_q_buffer = torch.zeros(
|
|
(max_bs, num_tokens_per_req // 2, self.num_q_heads, self.kv_cache_dim),
|
|
dtype=self.store_dtype,
|
|
device=self.device,
|
|
)
|
|
|
|
# Buffer for unpadded output: (max_num_tokens, num_q_heads, v_head_dim)
|
|
self.unpad_output_buffer = torch.zeros(
|
|
(max_num_tokens // 2, self.num_q_heads, 512),
|
|
dtype=self.store_dtype,
|
|
device=self.device,
|
|
)
|
|
else:
|
|
# Buffer for padded query: (max_bs, max_draft_tokens, num_q_heads, v_head_dim)
|
|
self.padded_q_buffer = torch.zeros(
|
|
(max_bs, num_tokens_per_req, self.num_q_heads, self.kv_cache_dim),
|
|
dtype=self.data_type,
|
|
device=self.device,
|
|
)
|
|
|
|
# Buffer for unpadded output: (max_num_tokens, num_q_heads, v_head_dim)
|
|
self.unpad_output_buffer = torch.zeros(
|
|
(max_num_tokens, self.num_q_heads, 512),
|
|
dtype=self.data_type,
|
|
device=self.device,
|
|
)
|
|
|
|
# Target verify never reaches the parent's mask read: it is excluded from
|
|
# every super() dispatch (init_forward_metadata, _out_graph, forward_extend)
|
|
# and runs the trtllm-gen kernel, which takes no mask.
|
|
self._verify_mask = maybe_create_verify_mask(
|
|
is_draft_runner=self.is_draft_runner,
|
|
skip_prefill=self.skip_prefill,
|
|
max_bs=max_bs,
|
|
max_context_len=self.max_context_len,
|
|
num_draft_tokens=self.num_draft_tokens,
|
|
device=self.device,
|
|
is_read=False,
|
|
)
|
|
|
|
super().init_cuda_graph_state(max_bs, max_num_tokens, kv_indices_buf)
|
|
|
|
@property
|
|
def verify_mask(self) -> Optional[VerifyMask]:
|
|
return self._verify_mask
|
|
|
|
def _init_cuda_graph_metadata(
|
|
self,
|
|
bs: int,
|
|
num_tokens: int,
|
|
forward_mode: ForwardMode,
|
|
seq_lens: torch.Tensor,
|
|
device: torch.device,
|
|
):
|
|
"""Allocate persistent metadata buffers for CUDA graph capture."""
|
|
if forward_mode.is_target_verify() and bs in self.decode_cuda_graph_metadata:
|
|
# Token tiers at the same slot count must share one per-bs buffer
|
|
# set (each graph bakes in the tensors it captured).
|
|
self.forward_decode_metadata = self.decode_cuda_graph_metadata[bs]
|
|
return
|
|
metadata = TRTLLMMLADecodeMetadata()
|
|
|
|
if forward_mode.is_target_verify():
|
|
metadata.seq_lens_k = torch.zeros((bs,), dtype=torch.int32, device=device)
|
|
metadata.global_seq_lens_k = torch.zeros(
|
|
(bs,), dtype=torch.int32, device=device
|
|
)
|
|
elif forward_mode.is_draft_extend_v2():
|
|
num_tokens_per_req = self.num_draft_tokens
|
|
metadata.max_seq_len_q = num_tokens_per_req
|
|
metadata.sum_seq_lens_q = num_tokens_per_req * bs
|
|
metadata.cu_seqlens_q = torch.arange(
|
|
0,
|
|
bs * num_tokens_per_req + 1,
|
|
num_tokens_per_req,
|
|
dtype=torch.int32,
|
|
device=device,
|
|
)
|
|
metadata.seq_lens_q = torch.full(
|
|
(bs,), num_tokens_per_req, dtype=torch.int32, device=device
|
|
)
|
|
metadata.seq_lens_k = torch.zeros((bs,), dtype=torch.int32, device=device)
|
|
|
|
if metadata.seq_lens_k is None:
|
|
# Plain decode: static int32 seq_lens buffer, refreshed by the
|
|
# capture+replay body below (same pattern as target-verify).
|
|
metadata.seq_lens_k = torch.zeros((bs,), dtype=torch.int32, device=device)
|
|
|
|
# Capture with full width so future longer sequences are safe during replay.
|
|
max_blocks_per_seq = self._calc_padded_blocks(self.max_context_len)
|
|
block_kv_indices = self.decode_cuda_graph_kv_indices[:bs, :max_blocks_per_seq]
|
|
metadata.block_kv_indices = block_kv_indices
|
|
metadata.max_seq_len_k = self.max_context_len
|
|
|
|
self.decode_cuda_graph_metadata[bs] = metadata
|
|
self.forward_decode_metadata = metadata
|
|
|
|
def _apply_cuda_graph_metadata(
|
|
self,
|
|
bs: int,
|
|
req_pool_indices: torch.Tensor,
|
|
seq_lens: torch.Tensor,
|
|
forward_mode: ForwardMode,
|
|
):
|
|
"""Shared decode / target-verify / draft-extend capture+replay body.
|
|
|
|
Public entry: :py:meth:`init_forward_metadata_out_graph` (which routes
|
|
the non-decode-family modes to the FlashInferMLA parent).
|
|
"""
|
|
metadata = self.decode_cuda_graph_metadata[bs]
|
|
|
|
if forward_mode.is_target_verify():
|
|
# Intentional int64 -> int32 same-kind out= downcast.
|
|
torch.add(
|
|
seq_lens[:bs],
|
|
self.num_draft_tokens,
|
|
out=metadata.global_seq_lens_k,
|
|
)
|
|
metadata.seq_lens_k.copy_(metadata.global_seq_lens_k)
|
|
seq_lens = metadata.seq_lens_k
|
|
elif forward_mode.is_draft_extend_v2():
|
|
num_tokens_per_req = self.num_draft_tokens
|
|
metadata.max_seq_len_q = num_tokens_per_req
|
|
metadata.sum_seq_lens_q = num_tokens_per_req * bs
|
|
seq_lens = seq_lens[:bs]
|
|
metadata.seq_lens_k.copy_(seq_lens)
|
|
elif metadata.seq_lens_k is not None:
|
|
# Plain decode: int64 -> int32 downcast copy into the static
|
|
# buffer (once per step, replacing the per-layer conversion).
|
|
metadata.seq_lens_k.copy_(seq_lens[:bs])
|
|
|
|
# Update block indices for new sequences.
|
|
create_flashmla_kv_indices_triton[
|
|
(
|
|
bs,
|
|
get_num_kv_index_blocks_flashmla(
|
|
metadata.block_kv_indices.shape[1], self.page_size
|
|
),
|
|
)
|
|
](
|
|
self.req_to_token,
|
|
req_pool_indices[:bs],
|
|
seq_lens,
|
|
None,
|
|
metadata.block_kv_indices,
|
|
self.req_to_token.stride(0),
|
|
metadata.block_kv_indices.shape[1],
|
|
PAGED_SIZE=self.page_size,
|
|
v2p_ptr=self._v2p_page_table,
|
|
PAGE_MULT=self._kernel_page_multiplier,
|
|
)
|
|
|
|
def get_cuda_graph_seq_len_fill_value(self) -> int:
|
|
"""Get the fill value for sequence lengths in CUDA graph."""
|
|
return 1
|
|
|
|
def init_mha_chunk_metadata(
|
|
self, forward_batch: ForwardBatch, disable_flashinfer_ragged: bool = False
|
|
) -> None:
|
|
has_prefix = any(forward_batch.extend_prefix_lens_cpu)
|
|
fallback_to_flashinfer_impl = (
|
|
(self.disable_chunked_prefix_cache and has_prefix)
|
|
or is_in_tc_piecewise_cuda_graph()
|
|
or is_in_breakable_cuda_graph()
|
|
)
|
|
if fallback_to_flashinfer_impl:
|
|
super().init_mha_chunk_metadata(
|
|
forward_batch, disable_flashinfer_ragged=True
|
|
)
|
|
|
|
def init_forward_metadata_out_graph(
|
|
self,
|
|
forward_batch: ForwardBatch,
|
|
in_capture: bool = False,
|
|
):
|
|
forward_mode = forward_batch.forward_mode
|
|
|
|
if (
|
|
not forward_mode.is_decode_or_idle()
|
|
and not forward_mode.is_target_verify()
|
|
and not forward_mode.is_draft_extend_v2()
|
|
):
|
|
return super().init_forward_metadata_out_graph(
|
|
forward_batch, in_capture=in_capture
|
|
)
|
|
|
|
bs = forward_batch.batch_size
|
|
if in_capture:
|
|
num_tokens = forward_batch.positions.numel()
|
|
self._init_cuda_graph_metadata(
|
|
bs,
|
|
num_tokens,
|
|
forward_mode,
|
|
forward_batch.seq_lens,
|
|
forward_batch.seq_lens.device,
|
|
)
|
|
self._apply_cuda_graph_metadata(
|
|
bs=bs,
|
|
req_pool_indices=forward_batch.req_pool_indices,
|
|
seq_lens=forward_batch.seq_lens,
|
|
forward_mode=forward_mode,
|
|
)
|
|
else:
|
|
self._apply_cuda_graph_metadata(
|
|
bs=bs,
|
|
req_pool_indices=forward_batch.req_pool_indices,
|
|
seq_lens=forward_batch.seq_lens,
|
|
forward_mode=forward_mode,
|
|
)
|
|
|
|
# Unified pool: precompute the DENSE KV write loc into the capture-stable
|
|
# buffer (both capture and each replay-prep run this out of the graph),
|
|
# so the in-graph set_mla_kv_buffer writes a dense loc without capturing
|
|
# a translate.
|
|
if self._unified_mla and (
|
|
forward_mode.is_decode_or_idle() or forward_mode.is_target_verify()
|
|
):
|
|
out_cache_loc = forward_batch.out_cache_loc
|
|
n = out_cache_loc.shape[0]
|
|
dst = self.cuda_graph_out_cache_loc_dense[:n]
|
|
self._translate_kv_loc_dense(out_cache_loc, out=dst)
|
|
# Replay-prep receives the RAW (unpadded) out_cache_loc
|
|
# (build_replay_fb_view), but the captured write kernel consumes the
|
|
# full captured tier of this buffer. Zero the tail so pad rows write
|
|
# to the dense sink (row 0) instead of stale dense locs left by
|
|
# earlier larger replays — a stale tail scatters pad-row garbage into
|
|
# live KV pages. Mirrors the runner's PaddingPolicy.ZERO on its own
|
|
# out_cache_loc slot.
|
|
self.cuda_graph_out_cache_loc_dense[n:].zero_()
|
|
self._decode_dense_loc = dst
|
|
else:
|
|
self._decode_dense_loc = None
|
|
|
|
def init_forward_metadata(self, forward_batch: ForwardBatch):
|
|
"""Initialize the metadata for a forward pass."""
|
|
# Eager path: no capture-stable dense write loc; the pool's _full_translate
|
|
# hook translates the write loc (safe out of a cuda graph).
|
|
self._decode_dense_loc = None
|
|
# Delegate to parent for non-decode modes.
|
|
if (
|
|
forward_batch.forward_mode.is_extend()
|
|
and not forward_batch.forward_mode.is_target_verify()
|
|
and not forward_batch.forward_mode.is_draft_extend_v2()
|
|
):
|
|
# For extend batch with prefix length > 0, fallback to ragged kernel implemented in flashinfer MLA backend
|
|
# when chunked prefix cache is disabled.
|
|
# Also fallback to flashinfer MLA backend under a captured prefill graph
|
|
has_prefix = any(forward_batch.extend_prefix_lens_cpu)
|
|
fallback_to_flashinfer_impl = (
|
|
(self.disable_chunked_prefix_cache and has_prefix)
|
|
or is_in_tc_piecewise_cuda_graph()
|
|
or is_in_breakable_cuda_graph()
|
|
)
|
|
if fallback_to_flashinfer_impl:
|
|
super().init_forward_metadata(forward_batch)
|
|
|
|
seq_lens = forward_batch.seq_lens - forward_batch.extend_prefix_lens
|
|
cum_seq_lens_q = torch.cat(
|
|
(
|
|
torch.zeros(
|
|
1, dtype=torch.int32, device=forward_batch.seq_lens.device
|
|
),
|
|
torch.cumsum(seq_lens, dim=0),
|
|
)
|
|
).int()
|
|
max_seq_len = max(forward_batch.extend_seq_lens_cpu)
|
|
self.forward_prefill_metadata = TRTLLMMLAPrefillMetadata(
|
|
max_seq_len,
|
|
cum_seq_lens_q,
|
|
seq_lens,
|
|
fallback_to_flashinfer_impl,
|
|
)
|
|
elif (
|
|
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()
|
|
):
|
|
bs = forward_batch.batch_size
|
|
self.forward_decode_metadata = TRTLLMMLADecodeMetadata()
|
|
# This is necessary because the backend instance persists across forward passes,
|
|
# and forward_prefill_metadata from a previous regular extend call could still be set.
|
|
if (
|
|
forward_batch.forward_mode.is_target_verify()
|
|
or forward_batch.forward_mode.is_draft_extend_v2()
|
|
):
|
|
self.forward_prefill_metadata = None
|
|
# Never read max_seq from the GPU tensor (.max().item() blocks the
|
|
# host on the stream backlog); max_seq only sizes the block table /
|
|
# scheduling hint, so the static context bound is a safe fallback.
|
|
if getattr(forward_batch, "seq_lens_cpu", None) is not None:
|
|
max_seq = forward_batch.seq_lens_cpu.max().item()
|
|
else:
|
|
max_seq = self.max_context_len
|
|
|
|
seq_lens = forward_batch.seq_lens
|
|
|
|
if forward_batch.forward_mode.is_target_verify():
|
|
max_seq = max_seq + self.num_draft_tokens
|
|
seq_lens = seq_lens + self.num_draft_tokens
|
|
self.forward_decode_metadata.seq_lens_k = seq_lens.to(torch.int32)
|
|
self.forward_decode_metadata.global_seq_lens_k = (
|
|
self.forward_decode_metadata.seq_lens_k
|
|
)
|
|
elif forward_batch.forward_mode.is_decode_or_idle():
|
|
# One int32 conversion per step; forward_decode reads it back
|
|
# so the per-layer .to(int32) in _run_decode_kernel stays a
|
|
# no-op (24 elementwise copies/step otherwise).
|
|
self.forward_decode_metadata.seq_lens_k = seq_lens.to(torch.int32)
|
|
elif forward_batch.forward_mode.is_draft_extend_v2():
|
|
sum_seq_lens_q = sum(forward_batch.extend_seq_lens_cpu)
|
|
max_seq_len_q = max(forward_batch.extend_seq_lens_cpu)
|
|
cu_seqlens_q = torch.nn.functional.pad(
|
|
torch.cumsum(
|
|
forward_batch.extend_seq_lens, dim=0, dtype=torch.int32
|
|
),
|
|
(1, 0),
|
|
)
|
|
# see NOTE(draft_extend seq_len handling)
|
|
seq_lens = seq_lens - forward_batch.extend_seq_lens + max_seq_len_q
|
|
|
|
self.forward_decode_metadata.max_seq_len_q = max_seq_len_q
|
|
self.forward_decode_metadata.sum_seq_lens_q = sum_seq_lens_q
|
|
self.forward_decode_metadata.cu_seqlens_q = cu_seqlens_q
|
|
self.forward_decode_metadata.seq_lens_q = forward_batch.extend_seq_lens
|
|
self.forward_decode_metadata.seq_lens_k = seq_lens.to(torch.int32)
|
|
|
|
max_seqlen_pad = self._calc_padded_blocks(max_seq)
|
|
block_kv_indices = self._create_block_kv_indices(
|
|
bs,
|
|
max_seqlen_pad,
|
|
forward_batch.req_pool_indices,
|
|
seq_lens,
|
|
seq_lens.device,
|
|
)
|
|
|
|
self.forward_decode_metadata.block_kv_indices = block_kv_indices
|
|
self.forward_decode_metadata.max_seq_len_k = int(max_seq)
|
|
self.forward_decode_metadata.batch_size = bs
|
|
|
|
forward_batch.decode_trtllm_mla_metadata = self.forward_decode_metadata
|
|
else:
|
|
return super().init_forward_metadata(forward_batch)
|
|
|
|
def pad_draft_extend_query(
|
|
self,
|
|
q: torch.Tensor,
|
|
padded_q: torch.Tensor,
|
|
seq_lens_q: torch.Tensor,
|
|
cu_seqlens_q: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
"""Pad draft extended query using Triton kernel."""
|
|
return pad_draft_extend_query_triton(
|
|
q,
|
|
padded_q,
|
|
seq_lens_q,
|
|
cu_seqlens_q,
|
|
)
|
|
|
|
def unpad_draft_extend_output(
|
|
self,
|
|
raw_out: torch.Tensor,
|
|
cu_seqlens_q: torch.Tensor,
|
|
seq_lens_q: torch.Tensor,
|
|
sum_seq_lens_q: int,
|
|
zero_uncovered: bool = False,
|
|
) -> torch.Tensor:
|
|
"""Unpad draft extended output using Triton kernel.
|
|
|
|
zero_uncovered: ragged verify's clamped rows leave output positions
|
|
unwritten; zero the destination so those discarded rows stay finite
|
|
(draft_extend writes every position and does not need this).
|
|
"""
|
|
output_buffer = self.unpad_output_buffer
|
|
if zero_uncovered:
|
|
if output_buffer is not None:
|
|
output_buffer[:sum_seq_lens_q].zero_()
|
|
else:
|
|
# No persistent buffer without cuda graph state; the triton
|
|
# wrapper's dynamic fallback is torch.empty.
|
|
output_buffer = torch.zeros(
|
|
(sum_seq_lens_q, raw_out.shape[2], raw_out.shape[3]),
|
|
dtype=raw_out.dtype,
|
|
device=raw_out.device,
|
|
)
|
|
return unpad_draft_extend_output_triton(
|
|
raw_out,
|
|
cu_seqlens_q,
|
|
seq_lens_q,
|
|
sum_seq_lens_q,
|
|
output_buffer,
|
|
)
|
|
|
|
def _compute_decode_bmm1_scale(self, layer: RadixAttention) -> float:
|
|
"""BMM1 scale q_scale * k_scale * softmax_scale. k_scale only
|
|
applies when the KV cache stores FP8."""
|
|
q_scale = 1.0
|
|
if self.data_type == torch.float8_e4m3fn:
|
|
k_scale = (
|
|
layer.k_scale_float
|
|
if getattr(layer, "k_scale_float", None) is not None
|
|
else 1.0
|
|
)
|
|
else:
|
|
if getattr(layer, "k_scale_float", None) is not None:
|
|
logger.warning_once(
|
|
"Checkpoint has k_scale but KV cache dtype is not FP8. "
|
|
"Ignoring k_scale for BMM1 (k_scale=%.4f, kv_dtype=%s).",
|
|
layer.k_scale_float,
|
|
self.data_type,
|
|
)
|
|
k_scale = 1.0
|
|
return q_scale * k_scale * layer.scaling
|
|
|
|
def _dense_q_indptr(self, bs: int, draft_token_num: int) -> torch.Tensor:
|
|
"""Query indptr for a dense [bs, draft_token_num] verify batch."""
|
|
if draft_token_num == self.num_draft_tokens:
|
|
return self.dense_q_indptr_verify[: bs + 1]
|
|
return self.q_indptr_decode[: bs + 1] * draft_token_num
|
|
|
|
def _run_decode_kernel(
|
|
self,
|
|
query: torch.Tensor,
|
|
kv_cache: torch.Tensor,
|
|
block_tables: torch.Tensor,
|
|
seq_lens: torch.Tensor,
|
|
max_seq_len: int,
|
|
layer: RadixAttention,
|
|
*,
|
|
causal_seqs: Optional[torch.Tensor] = None,
|
|
cp_world: int = 1,
|
|
cp_rank: int = 0,
|
|
return_lse: bool = False,
|
|
) -> torch.Tensor:
|
|
"""Hook for subclasses to swap the decode/spec-verify kernel.
|
|
|
|
The DCP arguments belong to the hook contract because forward_extend
|
|
passes them on the DCP target-verify path. This implementation does not
|
|
forward them to the kernel and returns no LSE, so only the DCP-capable
|
|
subclasses serve them."""
|
|
if cp_world > 1 or return_lse:
|
|
raise NotImplementedError(
|
|
"trtllm_mla does not forward the cyclic DCP metadata to its "
|
|
"decode kernel and returns no rank-local LSE for the cross-rank "
|
|
"merge; select cutedsl_mla or tokenspeed_mla for a DCP "
|
|
"target-verify run"
|
|
)
|
|
|
|
# Scale computation for TRTLLM MLA kernel BMM1 operation:
|
|
# The final BMM1 scale is computed as: q_scale * k_scale * softmax_scale
|
|
# Scale components:
|
|
# - q_scale: Query scaling factor (set to 1.0 for both FP16/FP8 paths)
|
|
# - k_scale: Key scaling factor from model checkpoint. Only applied when KV cache
|
|
# stores FP8-quantized values, to compensate for the quantization scaling.
|
|
# For BF16/FP16 KV cache, k_scale must be 1.0 since values are unscaled.
|
|
# - softmax_scale: Attention softmax scaling = 1/sqrt(head_dim), pre-computed as layer.scaling
|
|
bmm1_scale = self._compute_decode_bmm1_scale(layer)
|
|
seq_lens_i32 = (
|
|
seq_lens if seq_lens.dtype == torch.int32 else seq_lens.to(torch.int32)
|
|
)
|
|
extra_kwargs = {"backend": self.backend} if self.backend != "trtllm-gen" else {}
|
|
if self.backend == "trtllm-gen":
|
|
extra_kwargs["multi_ctas_kv_counter_buffer"] = (
|
|
self._multi_ctas_kv_counter_buffer
|
|
)
|
|
return flashinfer.decode.trtllm_batch_decode_with_kv_cache_mla(
|
|
query=query,
|
|
kv_cache=kv_cache,
|
|
enable_pdl=_ENABLE_PDL,
|
|
workspace_buffer=self.workspace_buffer,
|
|
qk_nope_head_dim=self.qk_nope_head_dim,
|
|
kv_lora_rank=self.kv_lora_rank,
|
|
qk_rope_head_dim=self.qk_rope_head_dim,
|
|
block_tables=block_tables,
|
|
seq_lens=seq_lens_i32,
|
|
max_seq_len=max_seq_len,
|
|
bmm1_scale=bmm1_scale,
|
|
skip_softmax_threshold_scale_factor=envs.SGLANG_SKIP_SOFTMAX_DECODE_THRESHOLD_SCALE_FACTOR.get(),
|
|
**extra_kwargs,
|
|
)
|
|
|
|
def _run_prefill_kernel(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
layer: RadixAttention,
|
|
batch_size: int,
|
|
cum_seq_lens_q: torch.Tensor,
|
|
max_q_len: int,
|
|
seq_lens_kv: torch.Tensor,
|
|
cum_seq_lens_kv: torch.Tensor,
|
|
max_kv_len: int,
|
|
is_causal: bool,
|
|
return_lse: bool,
|
|
out_buffer: torch.Tensor,
|
|
o_sf_scale: float = 1.0,
|
|
):
|
|
"""Hook for subclasses to swap the ragged prefill kernel. Q/K/V arrive
|
|
in model-native dtype; subclasses do any kernel-specific quantization.
|
|
Returns the output tensor or (output, lse) if return_lse."""
|
|
q_scale = k_scale = v_scale = 1.0
|
|
if self.data_type == torch.float8_e4m3fn:
|
|
q, k, v, k_scale, v_scale = _quantize_fp8_qkv(q, k, v, layer)
|
|
return flashinfer.prefill.trtllm_ragged_attention_deepseek(
|
|
query=q,
|
|
key=k,
|
|
value=v,
|
|
workspace_buffer=self.workspace_buffer,
|
|
batch_size=batch_size,
|
|
window_left=-1,
|
|
enable_pdl=False,
|
|
max_q_len=max_q_len,
|
|
bmm1_scale=q_scale * k_scale * layer.scaling,
|
|
bmm2_scale=v_scale,
|
|
cum_seq_lens_q=cum_seq_lens_q,
|
|
cum_seq_lens_kv=cum_seq_lens_kv,
|
|
seq_lens=seq_lens_kv,
|
|
max_kv_len=max_kv_len,
|
|
is_causal=is_causal,
|
|
return_lse=return_lse,
|
|
o_sf_scale=o_sf_scale,
|
|
out=out_buffer,
|
|
skip_softmax_threshold_scale_factor=envs.SGLANG_SKIP_SOFTMAX_PREFILL_THRESHOLD_SCALE_FACTOR.get(),
|
|
)
|
|
|
|
def _set_kv_and_concat_q_fused(
|
|
self,
|
|
layer: RadixAttention,
|
|
loc: torch.Tensor,
|
|
k: torch.Tensor,
|
|
k_rope: torch.Tensor,
|
|
q: torch.Tensor,
|
|
q_rope: torch.Tensor,
|
|
) -> Optional[torch.Tensor]:
|
|
"""Decode: scatter the KV row at ``loc`` (already physical — the
|
|
dense-loc buffer on the unified pool, or out_cache_loc on the static
|
|
pool where ``_full_translate`` is identity) and build the
|
|
[q_nope | q_rope] fmha query in one kernel launch (saves one launch
|
|
per MLA layer and keeps the PDL chain intact).
|
|
|
|
Returns the concatenated query, or None when the fused kernel does
|
|
not cover the inputs (caller falls back to the two-kernel path).
|
|
"""
|
|
k_nope_2d = k.view(k.shape[0], -1)
|
|
k_rope_2d = k_rope.view(k_rope.shape[0], -1)
|
|
q_nope = q.view(-1, layer.tp_q_head_num, layer.v_head_dim)
|
|
q_rope_3d = q_rope.view(
|
|
-1, layer.tp_q_head_num, layer.head_dim - layer.v_head_dim
|
|
)
|
|
# Same raw per-layer buffer the decode kernel reads below (bf16-only
|
|
# gate means store_dtype == dtype, so no view); get_key_buffer applies
|
|
# the hybrid pool's full-attention layer-id mapping.
|
|
kv_raw = self.token_to_kv_pool.get_key_buffer(layer.layer_id)
|
|
kv_2d = kv_raw.view(kv_raw.shape[0], -1) if kv_raw.dim() != 2 else kv_raw
|
|
if not set_mla_kv_concat_q_covered(
|
|
kv_buffer=kv_2d,
|
|
loc=loc,
|
|
k_nope=k_nope_2d,
|
|
k_rope=k_rope_2d,
|
|
q_nope=q_nope,
|
|
q_rope=q_rope_3d,
|
|
):
|
|
return None
|
|
return set_mla_kv_concat_q(
|
|
kv_buffer=kv_2d,
|
|
loc=loc,
|
|
cache_k_nope=k_nope_2d,
|
|
cache_k_rope=k_rope_2d,
|
|
q_nope=q_nope,
|
|
q_rope=q_rope_3d,
|
|
)
|
|
|
|
def _set_kv_and_concat_q_fp8_fused(
|
|
self,
|
|
layer: RadixAttention,
|
|
loc: torch.Tensor,
|
|
q: torch.Tensor,
|
|
q_rope: torch.Tensor,
|
|
k: torch.Tensor,
|
|
k_rope: torch.Tensor,
|
|
) -> Optional[torch.Tensor]:
|
|
"""fp8-KV decode: quantize + scatter the KV row at ``loc`` (already
|
|
physical) and build the fp8 [q_nope | q_rope] query in one launch.
|
|
|
|
Returns the fp8 query, or None when the fused kernel does not cover
|
|
the inputs (caller falls back to the aten quantize chain).
|
|
"""
|
|
k_nope_2d = k.view(k.shape[0], -1)
|
|
k_rope_2d = k_rope.view(k_rope.shape[0], -1)
|
|
q_nope = q.view(-1, layer.tp_q_head_num, layer.v_head_dim)
|
|
q_rope_3d = q_rope.view(
|
|
-1, layer.tp_q_head_num, layer.head_dim - layer.v_head_dim
|
|
)
|
|
# fp8 view of the pool's uint8 store; same buffer the decode kernel
|
|
# reads below.
|
|
kv_raw = self.token_to_kv_pool.get_key_buffer(layer.layer_id)
|
|
kv_2d = kv_raw.view(kv_raw.shape[0], -1) if kv_raw.dim() != 2 else kv_raw
|
|
if not set_mla_kv_concat_q_fp8_covered(
|
|
kv_buffer=kv_2d,
|
|
loc=loc,
|
|
k_nope=k_nope_2d,
|
|
k_rope=k_rope_2d,
|
|
q_nope=q_nope,
|
|
q_rope=q_rope_3d,
|
|
):
|
|
return None
|
|
parallel = get_parallel()
|
|
return set_mla_kv_concat_q_fp8(
|
|
kv_buffer=kv_2d,
|
|
loc=loc,
|
|
cache_k_nope=k_nope_2d,
|
|
cache_k_rope=k_rope_2d,
|
|
q_nope=q_nope,
|
|
q_rope=q_rope_3d,
|
|
# DCP cyclic KV sharding: virtual loc -> owner mask + loc//world
|
|
# (identity when attn_dcp_size == 1).
|
|
dcp_world_size=parallel.attn_dcp_size,
|
|
dcp_rank=parallel.attn_dcp_rank,
|
|
)
|
|
|
|
def forward_decode(
|
|
self,
|
|
q: torch.Tensor, # q_nope
|
|
k: torch.Tensor, # k_nope
|
|
v: torch.Tensor, # not used in this backend
|
|
layer: RadixAttention,
|
|
forward_batch: ForwardBatch,
|
|
save_kv_cache: bool = True,
|
|
q_rope: Optional[torch.Tensor] = None,
|
|
k_rope: Optional[torch.Tensor] = None,
|
|
cos_sin_cache: Optional[torch.Tensor] = None,
|
|
is_neox: Optional[bool] = False,
|
|
llama_4_scaling: Optional[torch.Tensor] = None,
|
|
) -> torch.Tensor:
|
|
"""Run forward for decode using TRTLLM MLA kernel."""
|
|
merge_query = q_rope is not None
|
|
fused_fp8_query = None
|
|
if self.data_type == torch.float8_e4m3fn:
|
|
assert q_rope is not None and k_rope is not None
|
|
if cos_sin_cache is None:
|
|
if save_kv_cache and self._fused_set_kv_concat_q_fp8:
|
|
loc = (
|
|
self._decode_dense_loc
|
|
if self._decode_dense_loc is not None
|
|
else (
|
|
None if self._unified_mla else forward_batch.out_cache_loc
|
|
)
|
|
)
|
|
if loc is not None:
|
|
# Fused: bf16->fp8 quantize + KV scatter + q concat
|
|
# in one launch; None when not covered.
|
|
fused_fp8_query = self._set_kv_and_concat_q_fp8_fused(
|
|
layer=layer,
|
|
loc=loc,
|
|
q=q,
|
|
q_rope=q_rope,
|
|
k=k,
|
|
k_rope=k_rope,
|
|
)
|
|
if fused_fp8_query is None:
|
|
q, k, k_rope = mla_quantize_without_rope_for_fp8(
|
|
q, q_rope, k.squeeze(1), k_rope.squeeze(1)
|
|
)
|
|
else:
|
|
q, k, k_rope = mla_quantize_and_rope_for_fp8(
|
|
q,
|
|
q_rope,
|
|
k.squeeze(1),
|
|
k_rope.squeeze(1),
|
|
forward_batch.positions,
|
|
cos_sin_cache,
|
|
is_neox,
|
|
self.kv_lora_rank,
|
|
self.qk_rope_head_dim,
|
|
)
|
|
merge_query = False
|
|
|
|
# Save KV cache if requested (the fused fp8 path already wrote it)
|
|
query = fused_fp8_query
|
|
if query is None and save_kv_cache:
|
|
assert (
|
|
k is not None and k_rope is not None
|
|
), "For populating trtllm_mla kv cache, both k_nope and k_rope should be not None."
|
|
if self._decode_dense_loc is not None:
|
|
# cuda-graph path: dense write loc precomputed out-of-graph, so
|
|
# the in-graph write captures no translate allocation.
|
|
if merge_query and self._fused_set_kv_concat_q:
|
|
# Fused: KV scatter + [q_nope | q_rope] concat in one
|
|
# launch; None when the inputs are not covered.
|
|
query = self._set_kv_and_concat_q_fused(
|
|
layer=layer,
|
|
loc=self._decode_dense_loc,
|
|
k=k,
|
|
k_rope=k_rope,
|
|
q=q,
|
|
q_rope=q_rope,
|
|
)
|
|
if query is None:
|
|
self.token_to_kv_pool.set_mla_kv_buffer(
|
|
layer, self._decode_dense_loc, k, k_rope, loc_is_dense=True
|
|
)
|
|
else:
|
|
# eager (or static pool): the pool's _full_translate handles it.
|
|
if (
|
|
merge_query
|
|
and self._fused_set_kv_concat_q
|
|
and not self._unified_mla
|
|
):
|
|
# Static pool: _full_translate is identity, so
|
|
# out_cache_loc is already the physical write loc.
|
|
query = self._set_kv_and_concat_q_fused(
|
|
layer=layer,
|
|
loc=forward_batch.out_cache_loc,
|
|
k=k,
|
|
k_rope=k_rope,
|
|
q=q,
|
|
q_rope=q_rope,
|
|
)
|
|
if query is None:
|
|
self.token_to_kv_pool.set_mla_kv_buffer(
|
|
layer, forward_batch.out_cache_loc, k, k_rope
|
|
)
|
|
|
|
# Prepare query tensor inline (already built when the fused save-KV
|
|
# path ran)
|
|
if query is None:
|
|
if merge_query:
|
|
# For FP16 path, we merge the query and rope parts into a single tensor
|
|
q_nope = q.view(-1, layer.tp_q_head_num, layer.v_head_dim)
|
|
q_rope_reshaped = q_rope.view(
|
|
-1, layer.tp_q_head_num, layer.head_dim - layer.v_head_dim
|
|
)
|
|
query = concat_mla_absorb_q_general(q_nope, q_rope_reshaped)
|
|
else:
|
|
# For FP8 path, we already have the query and rope parts merged because of the quantize_and_rope_for_fp8 function
|
|
query = q.view(-1, layer.tp_q_head_num, layer.head_dim)
|
|
|
|
# Apply llama 4 scaling if provided
|
|
if llama_4_scaling is not None:
|
|
query = query.to(self.q_data_type) * llama_4_scaling
|
|
query = query.to(self.data_type)
|
|
|
|
# Ensure query has shape [bs, acc_q_len, num_q_heads, head_dim] when seq_len 1
|
|
if query.dim() == 3:
|
|
query = query.unsqueeze(1)
|
|
|
|
# Prepare KV cache inline
|
|
k_cache = self.token_to_kv_pool.get_key_buffer(layer.layer_id)
|
|
kv_cache = k_cache.view(-1, self.page_size, self.kv_cache_dim).unsqueeze(1)
|
|
|
|
# Get metadata
|
|
metadata = (
|
|
getattr(forward_batch, "decode_trtllm_mla_metadata", None)
|
|
or self.forward_decode_metadata
|
|
)
|
|
|
|
# Backstop: metadata was built pre-pad (marked) and DP padding then
|
|
# grew the batch. The marker path deliberately does not re-plan
|
|
# post-pad (DSA can't rebuild on a padded batch, see #27091), so this
|
|
# local re-plan catches the size mismatch.
|
|
batch_size = getattr(metadata, "batch_size", None)
|
|
if batch_size is not None and batch_size < forward_batch.batch_size:
|
|
self.init_forward_metadata(forward_batch)
|
|
metadata = forward_batch.decode_trtllm_mla_metadata
|
|
|
|
raw_out = self._run_decode_kernel(
|
|
query=query,
|
|
kv_cache=kv_cache,
|
|
block_tables=metadata.block_kv_indices,
|
|
seq_lens=(
|
|
metadata.seq_lens_k
|
|
if metadata.seq_lens_k is not None
|
|
else forward_batch.seq_lens
|
|
),
|
|
max_seq_len=metadata.max_seq_len_k,
|
|
layer=layer,
|
|
)
|
|
|
|
# Reshape output directly without slicing
|
|
output = raw_out.view(-1, layer.tp_q_head_num * layer.v_head_dim)
|
|
return output
|
|
|
|
def forward_extend(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
layer: RadixAttention,
|
|
forward_batch: ForwardBatch,
|
|
save_kv_cache: bool = True,
|
|
q_rope: Optional[torch.Tensor] = None,
|
|
k_rope: Optional[torch.Tensor] = None,
|
|
cos_sin_cache: Optional[torch.Tensor] = None,
|
|
is_neox: Optional[bool] = False,
|
|
llama_4_scaling: Optional[torch.Tensor] = None,
|
|
) -> torch.Tensor:
|
|
|
|
# The fallback belongs to genuine extend forwards only. Target-verify /
|
|
# draft-extend must never honor it: `forward_prefill_metadata` is a
|
|
# stale leftover from the last prefill there (eager init clears it,
|
|
# but decode-graph capture does not), and capturing verify through the
|
|
# flashinfer path binds the graph to prefill-planned wrapper buffers,
|
|
# which fault (illegal address) at replay.
|
|
if (
|
|
forward_batch.forward_mode.is_extend()
|
|
and not forward_batch.forward_mode.is_target_verify()
|
|
and not forward_batch.forward_mode.is_draft_extend_v2()
|
|
and self.forward_prefill_metadata is not None
|
|
and self.forward_prefill_metadata.fallback_to_flashinfer_impl
|
|
):
|
|
return super().forward_extend(
|
|
q, k, v, layer, forward_batch, save_kv_cache, q_rope, k_rope
|
|
)
|
|
|
|
# TODO refactor to avoid code duplication
|
|
merge_query = q_rope is not None
|
|
if (
|
|
self.data_type == torch.float8_e4m3fn
|
|
) and forward_batch.forward_mode.is_target_verify():
|
|
assert q_rope is not None and k_rope is not None
|
|
if cos_sin_cache is None:
|
|
q, k, k_rope = mla_quantize_without_rope_for_fp8(
|
|
q, q_rope, k.squeeze(1), k_rope.squeeze(1)
|
|
)
|
|
else:
|
|
q, k, k_rope = mla_quantize_and_rope_for_fp8(
|
|
q,
|
|
q_rope,
|
|
k.squeeze(1),
|
|
k_rope.squeeze(1),
|
|
forward_batch.positions,
|
|
cos_sin_cache,
|
|
is_neox,
|
|
self.kv_lora_rank,
|
|
self.qk_rope_head_dim,
|
|
)
|
|
merge_query = False
|
|
|
|
# Save KV cache if requested
|
|
if save_kv_cache:
|
|
assert (
|
|
k is not None and k_rope is not None
|
|
), "For populating trtllm_mla kv cache, both k_nope and k_rope should be not None."
|
|
if self._decode_dense_loc is not None:
|
|
self.token_to_kv_pool.set_mla_kv_buffer(
|
|
layer, self._decode_dense_loc, k, k_rope, loc_is_dense=True
|
|
)
|
|
else:
|
|
self.token_to_kv_pool.set_mla_kv_buffer(
|
|
layer, forward_batch.out_cache_loc, k, k_rope
|
|
)
|
|
|
|
# TODO refactor to avoid code duplication
|
|
# Prepare query tensor inline
|
|
if merge_query:
|
|
# For FP16 path, we merge the query and rope parts into a single tensor
|
|
q_nope = q.view(-1, layer.tp_q_head_num, layer.v_head_dim)
|
|
q_rope_reshaped = q_rope.view(
|
|
-1, layer.tp_q_head_num, layer.head_dim - layer.v_head_dim
|
|
)
|
|
q = concat_mla_absorb_q_general(q_nope, q_rope_reshaped)
|
|
|
|
q = q.view(-1, layer.tp_q_head_num, layer.head_dim)
|
|
|
|
# Apply llama 4 scaling if provided
|
|
if llama_4_scaling is not None:
|
|
q = q.to(self.q_data_type) * llama_4_scaling
|
|
q = q.to(self.data_type)
|
|
|
|
if (
|
|
forward_batch.forward_mode.is_target_verify()
|
|
or forward_batch.forward_mode.is_draft_extend_v2()
|
|
):
|
|
metadata = (
|
|
getattr(forward_batch, "decode_trtllm_mla_metadata", None)
|
|
or self.forward_decode_metadata
|
|
)
|
|
|
|
# Backstop: metadata was built pre-pad (marked) and DP padding
|
|
# then grew the batch. The marker path deliberately does not
|
|
# re-plan post-pad (DSA can't rebuild on a padded batch, see
|
|
# #27091), so this local re-plan catches the size mismatch.
|
|
batch_size = getattr(metadata, "batch_size", None)
|
|
if batch_size is not None and batch_size < forward_batch.batch_size:
|
|
self.init_forward_metadata(forward_batch)
|
|
metadata = forward_batch.decode_trtllm_mla_metadata
|
|
|
|
# Ensure query has shape [bs, num_draft_tokens, num_q_heads, head_dim]
|
|
bs = forward_batch.batch_size
|
|
|
|
k_cache = self.token_to_kv_pool.get_key_buffer(layer.layer_id)
|
|
kv_cache = k_cache.view(-1, self.page_size, self.kv_cache_dim).unsqueeze(1)
|
|
|
|
q = q.to(self.data_type)
|
|
|
|
if forward_batch.forward_mode.is_target_verify():
|
|
draft_token_num = forward_batch.spec_info.draft_token_num
|
|
dcp_enabled = get_parallel().dcp_enabled
|
|
max_seq_len = metadata.max_seq_len_k + (
|
|
0 if dcp_enabled else draft_token_num
|
|
)
|
|
ragged_layout = forward_batch.spec_info.ragged_verify_layout
|
|
if ragged_layout is None or dcp_enabled:
|
|
q = q.view(bs, -1, layer.tp_q_head_num, layer.head_dim)
|
|
needs_unpad = False
|
|
else:
|
|
if ragged_layout.bs != bs:
|
|
# Capped variant: the dense [bs, draft_token_num] q
|
|
# buffer below cannot take a row the full-coverage pad
|
|
# may inflate past the verify window, and it keeps
|
|
# qo_indptr consistent with the clamped lens (same
|
|
# contract as the KDA dense path).
|
|
ragged_layout = ragged_layout.padded_to_bucket(
|
|
padded_bs=bs, cap=draft_token_num
|
|
)
|
|
total_tokens = q.shape[0]
|
|
seq_lens_q = torch.clamp(
|
|
ragged_layout.verify_lens, max=draft_token_num
|
|
)
|
|
cu_seqlens_q = ragged_layout.qo_indptr_device
|
|
padded_q = torch.zeros(
|
|
(bs, draft_token_num, layer.tp_q_head_num, layer.head_dim),
|
|
dtype=q.dtype,
|
|
device=q.device,
|
|
)
|
|
q = self.pad_draft_extend_query(
|
|
q, padded_q, seq_lens_q, cu_seqlens_q
|
|
)
|
|
needs_unpad = True
|
|
unpad_zero_uncovered = True
|
|
unpad_seq_lens_q = seq_lens_q
|
|
unpad_cu_seqlens_q = cu_seqlens_q
|
|
unpad_sum_seq_lens_q = total_tokens
|
|
else:
|
|
# draft_extend: handle varying num_correct_drafts_per_req. If total_tokens % bs == 0,
|
|
# we can directly reshape q; otherwise, pad to max_seq_len_q.
|
|
total_tokens = q.shape[0]
|
|
tokens_per_seq = total_tokens // bs if bs > 0 else 0
|
|
can_direct_view = bs > 0 and (total_tokens % bs == 0)
|
|
|
|
if can_direct_view:
|
|
max_seq_len = metadata.max_seq_len_k + tokens_per_seq
|
|
q = q.view(bs, tokens_per_seq, layer.tp_q_head_num, layer.head_dim)
|
|
needs_unpad = False
|
|
else:
|
|
# Varying lengths: pad q to (bs, max_seq_len_q, ...)
|
|
actual_seq_lens_q = forward_batch.extend_seq_lens
|
|
actual_max_seq_len_q = max(forward_batch.extend_seq_lens_cpu)
|
|
max_seq_len = metadata.max_seq_len_k + actual_max_seq_len_q
|
|
|
|
actual_cu_seqlens_q = torch.nn.functional.pad(
|
|
torch.cumsum(actual_seq_lens_q, dim=0, dtype=torch.int32),
|
|
(1, 0),
|
|
)
|
|
|
|
if self.padded_q_buffer is not None:
|
|
padded_q = self.padded_q_buffer[
|
|
:bs, :actual_max_seq_len_q, :, :
|
|
].to(dtype=q.dtype)
|
|
padded_q.zero_()
|
|
else:
|
|
padded_q = torch.zeros(
|
|
(
|
|
bs,
|
|
actual_max_seq_len_q,
|
|
layer.tp_q_head_num,
|
|
layer.head_dim,
|
|
),
|
|
dtype=q.dtype,
|
|
device=q.device,
|
|
)
|
|
|
|
q = self.pad_draft_extend_query(
|
|
q, padded_q, actual_seq_lens_q, actual_cu_seqlens_q
|
|
)
|
|
needs_unpad = True
|
|
unpad_zero_uncovered = False
|
|
unpad_seq_lens_q = actual_seq_lens_q
|
|
unpad_cu_seqlens_q = actual_cu_seqlens_q
|
|
unpad_sum_seq_lens_q = total_tokens
|
|
|
|
assert kv_cache.dtype == self.data_type
|
|
|
|
if (
|
|
forward_batch.forward_mode.is_target_verify()
|
|
and get_parallel().dcp_enabled
|
|
):
|
|
raw_out, lse = self._run_decode_kernel(
|
|
query=q,
|
|
kv_cache=kv_cache,
|
|
block_tables=metadata.block_kv_indices,
|
|
seq_lens=metadata.seq_lens_k,
|
|
max_seq_len=max_seq_len,
|
|
layer=layer,
|
|
causal_seqs=metadata.global_seq_lens_k,
|
|
cp_world=get_parallel().dcp_size,
|
|
cp_rank=get_parallel().dcp_rank,
|
|
return_lse=True,
|
|
)
|
|
output = raw_out.view(
|
|
bs * draft_token_num,
|
|
layer.tp_q_head_num,
|
|
layer.v_head_dim,
|
|
)
|
|
lse = lse.view(bs * draft_token_num, layer.tp_q_head_num)
|
|
fixup_zero_kv_rows(
|
|
output,
|
|
lse,
|
|
metadata.seq_lens_k,
|
|
self._dense_q_indptr(bs, draft_token_num),
|
|
draft_token_num,
|
|
)
|
|
return output.flatten(1), lse
|
|
|
|
raw_out = self._run_decode_kernel(
|
|
query=q,
|
|
kv_cache=kv_cache,
|
|
block_tables=metadata.block_kv_indices,
|
|
seq_lens=metadata.seq_lens_k,
|
|
max_seq_len=max_seq_len,
|
|
layer=layer,
|
|
)
|
|
|
|
if needs_unpad:
|
|
# Unpad the output for draft_extend mode with varying lengths
|
|
# Use the actual values computed during padding, not from metadata
|
|
output = self.unpad_draft_extend_output(
|
|
raw_out,
|
|
unpad_cu_seqlens_q,
|
|
unpad_seq_lens_q,
|
|
unpad_sum_seq_lens_q,
|
|
zero_uncovered=unpad_zero_uncovered,
|
|
)
|
|
output = output.view(-1, layer.tp_q_head_num * layer.v_head_dim)
|
|
else:
|
|
output = raw_out.view(-1, layer.tp_q_head_num * layer.v_head_dim)
|
|
return output
|
|
|
|
if k_rope is not None:
|
|
k = torch.cat([k, k_rope], dim=-1)
|
|
k = k.view(-1, layer.tp_k_head_num, layer.head_dim)
|
|
v = v.view(-1, layer.tp_k_head_num, layer.v_head_dim)
|
|
|
|
# When chunked prefix cache is enabled, dispatch to different path for ragged attention.
|
|
if forward_batch.attn_attend_prefix_cache:
|
|
# MHA for chunked prefix kv cache when running model with MLA
|
|
assert forward_batch.prefix_chunk_idx is not None
|
|
assert forward_batch.prefix_chunk_cu_seq_lens is not None
|
|
assert q_rope is None
|
|
assert k_rope is None
|
|
chunk_idx = forward_batch.prefix_chunk_idx
|
|
|
|
out = torch.empty(
|
|
q.shape[0],
|
|
layer.tp_q_head_num,
|
|
layer.v_head_dim,
|
|
dtype=self.q_data_type,
|
|
device=q.device,
|
|
)
|
|
result = self._run_prefill_kernel(
|
|
q=q,
|
|
k=k,
|
|
v=v,
|
|
layer=layer,
|
|
batch_size=forward_batch.batch_size,
|
|
cum_seq_lens_q=self.forward_prefill_metadata.cum_seq_lens,
|
|
max_q_len=self.forward_prefill_metadata.max_seq_len,
|
|
seq_lens_kv=forward_batch.prefix_chunk_seq_lens[chunk_idx],
|
|
cum_seq_lens_kv=forward_batch.prefix_chunk_cu_seq_lens[chunk_idx],
|
|
max_kv_len=forward_batch.prefix_chunk_max_seq_lens[chunk_idx],
|
|
is_causal=False,
|
|
return_lse=True,
|
|
out_buffer=out,
|
|
o_sf_scale=-1.0,
|
|
)
|
|
|
|
# The TRT-LLM ragged attention cubin kernel does not correctly
|
|
# handle rows with kv_len == 0: it leaves stale data in the
|
|
# workspace softmaxStats buffer and may produce non-zero output
|
|
# for those rows. Fix up by forcing out=0 and lse=-inf for
|
|
# zero-KV rows so that downstream merge_state ignores them.
|
|
# Skip entirely when this chunk has no zero-KV rows (pure CPU
|
|
# check, precomputed in prepare_chunked_prefix_cache_info).
|
|
if forward_batch.prefix_chunk_has_zero_kv[chunk_idx]:
|
|
out_tensor, lse_tensor = result
|
|
fixup_zero_kv_rows(
|
|
out_tensor,
|
|
lse_tensor,
|
|
forward_batch.prefix_chunk_seq_lens[chunk_idx],
|
|
self.forward_prefill_metadata.cum_seq_lens,
|
|
self.forward_prefill_metadata.max_seq_len,
|
|
)
|
|
|
|
return result
|
|
else:
|
|
out = torch.empty(
|
|
q.shape[0],
|
|
q.shape[1],
|
|
v.shape[2],
|
|
device=q.device,
|
|
dtype=self.q_data_type,
|
|
)
|
|
return self._run_prefill_kernel(
|
|
q=q,
|
|
k=k,
|
|
v=v,
|
|
layer=layer,
|
|
batch_size=forward_batch.batch_size,
|
|
cum_seq_lens_q=self.forward_prefill_metadata.cum_seq_lens,
|
|
max_q_len=self.forward_prefill_metadata.max_seq_len,
|
|
seq_lens_kv=self.forward_prefill_metadata.seq_lens,
|
|
cum_seq_lens_kv=self.forward_prefill_metadata.cum_seq_lens,
|
|
max_kv_len=self.forward_prefill_metadata.max_seq_len,
|
|
is_causal=True,
|
|
return_lse=forward_batch.mha_return_lse,
|
|
out_buffer=out,
|
|
o_sf_scale=1.0,
|
|
)
|
|
|
|
|
|
class TRTLLMMLAMultiStepDraftBackend(FlashInferMLAMultiStepDraftBackend):
|
|
"""Multi-step draft backend for TRT-LLM MLA used by EAGLE."""
|
|
|
|
# Per-step draft decode never reads seq_lens_cpu / seq_lens_sum; opt out so
|
|
# decide_needs_cpu_seq_lens' OR over the backends stays False.
|
|
needs_cpu_seq_lens: bool = False
|
|
|
|
def __init__(
|
|
self,
|
|
model_runner: ModelRunner,
|
|
topk: int,
|
|
speculative_num_steps: int,
|
|
backend: str = "trtllm-gen",
|
|
):
|
|
super().__init__(model_runner, topk, speculative_num_steps)
|
|
|
|
for i in range(self.speculative_num_steps - 1):
|
|
self.attn_backends[i] = TRTLLMMLABackend(
|
|
model_runner,
|
|
skip_prefill=True,
|
|
kv_indptr_buf=self.kv_indptr[i],
|
|
q_indptr_decode_buf=self.q_indptr_decode,
|
|
backend=backend,
|
|
)
|
|
|
|
def init_forward_metadata(self, forward_batch: ForwardBatch):
|
|
for i in range(self.speculative_num_steps - 1):
|
|
self.attn_backends[i].init_forward_metadata(forward_batch)
|
|
|
|
def init_forward_metadata_out_graph(
|
|
self,
|
|
forward_batch: ForwardBatch,
|
|
in_capture: bool = False,
|
|
):
|
|
from sglang.srt.model_executor.forward_batch_info import build_inner_fb_view
|
|
|
|
if in_capture:
|
|
return super().init_forward_metadata_out_graph(
|
|
forward_batch, in_capture=in_capture
|
|
)
|
|
inner_fb = build_inner_fb_view(
|
|
forward_batch,
|
|
bs=forward_batch.batch_size,
|
|
forward_mode=ForwardMode.DECODE,
|
|
)
|
|
for i in range(self.speculative_num_steps - 1):
|
|
self.attn_backends[i].init_forward_metadata_out_graph(inner_fb)
|