Split TRTLLM MHA decode batches by KV sequence length (#34888)
This commit is contained in:
@@ -916,6 +916,10 @@ class Envs:
|
|||||||
# None = standard attention. See https://arxiv.org/abs/2512.12087
|
# None = standard attention. See https://arxiv.org/abs/2512.12087
|
||||||
SGLANG_SKIP_SOFTMAX_PREFILL_THRESHOLD_SCALE_FACTOR = EnvFloat(None)
|
SGLANG_SKIP_SOFTMAX_PREFILL_THRESHOLD_SCALE_FACTOR = EnvFloat(None)
|
||||||
SGLANG_SKIP_SOFTMAX_DECODE_THRESHOLD_SCALE_FACTOR = EnvFloat(None)
|
SGLANG_SKIP_SOFTMAX_DECODE_THRESHOLD_SCALE_FACTOR = EnvFloat(None)
|
||||||
|
# Split TRTLLM-GEN decode attention into sorted, equal-size request groups.
|
||||||
|
# One preserves the default single-call path; values above one are useful
|
||||||
|
# for batches whose KV sequence lengths have a large spread.
|
||||||
|
SGLANG_TRTLLM_MHA_DECODE_SEQ_LEN_SPLITS = EnvInt(1)
|
||||||
# SM120 FlashMLA decode backend: "flashinfer" (default), "triton", or "torch".
|
# SM120 FlashMLA decode backend: "flashinfer" (default), "triton", or "torch".
|
||||||
SGLANG_SM120_FLASHMLA_BACKEND = EnvStr("flashinfer")
|
SGLANG_SM120_FLASHMLA_BACKEND = EnvStr("flashinfer")
|
||||||
SGLANG_FLASHINFER_PREFILL_SPLIT_TILE_SIZE = EnvInt(4096)
|
SGLANG_FLASHINFER_PREFILL_SPLIT_TILE_SIZE = EnvInt(4096)
|
||||||
|
|||||||
@@ -271,6 +271,12 @@ class TRTLLMHAAttnBackend(FlashInferAttnBackend):
|
|||||||
"trtllm_mha_default_kv_scale",
|
"trtllm_mha_default_kv_scale",
|
||||||
lambda: torch.ones(1, dtype=torch.float32, device=self.device),
|
lambda: torch.ones(1, dtype=torch.float32, device=self.device),
|
||||||
)
|
)
|
||||||
|
self.decode_seq_len_splits = envs.SGLANG_TRTLLM_MHA_DECODE_SEQ_LEN_SPLITS.get()
|
||||||
|
if self.decode_seq_len_splits < 1:
|
||||||
|
raise ValueError(
|
||||||
|
"SGLANG_TRTLLM_MHA_DECODE_SEQ_LEN_SPLITS must be at least 1, "
|
||||||
|
f"got {self.decode_seq_len_splits}"
|
||||||
|
)
|
||||||
|
|
||||||
def _check_decode_kv_access(self) -> None:
|
def _check_decode_kv_access(self) -> None:
|
||||||
supported_kinds = {
|
supported_kinds = {
|
||||||
@@ -1084,6 +1090,73 @@ class TRTLLMHAAttnBackend(FlashInferAttnBackend):
|
|||||||
assert self.is_nvfp4_kvcache
|
assert self.is_nvfp4_kvcache
|
||||||
return self.kv_cache_quant_method.get_bmm_scales(layer.layer_id)
|
return self.kv_cache_quant_method.get_bmm_scales(layer.layer_id)
|
||||||
|
|
||||||
|
def _run_fixed_q_len_decode(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
kv_cache,
|
||||||
|
block_tables: torch.Tensor,
|
||||||
|
seq_lens: torch.Tensor,
|
||||||
|
*,
|
||||||
|
bmm1_scale,
|
||||||
|
bmm2_scale,
|
||||||
|
window_left: int,
|
||||||
|
sinks: Optional[torch.Tensor],
|
||||||
|
q_len_per_req: int = 1,
|
||||||
|
kv_cache_sf=None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Run decode, optionally sorting and splitting requests by KV length."""
|
||||||
|
|
||||||
|
def run_group(group_query, group_block_tables, group_seq_lens):
|
||||||
|
kwargs = {}
|
||||||
|
if q_len_per_req != 1:
|
||||||
|
kwargs["q_len_per_req"] = q_len_per_req
|
||||||
|
return flashinfer.decode.trtllm_batch_decode_with_kv_cache(
|
||||||
|
query=group_query,
|
||||||
|
kv_cache=kv_cache,
|
||||||
|
workspace_buffer=self.workspace_buffer,
|
||||||
|
block_tables=group_block_tables,
|
||||||
|
seq_lens=group_seq_lens,
|
||||||
|
max_seq_len=self.max_context_len,
|
||||||
|
bmm1_scale=bmm1_scale,
|
||||||
|
bmm2_scale=bmm2_scale,
|
||||||
|
window_left=window_left,
|
||||||
|
sinks=sinks,
|
||||||
|
skip_softmax_threshold_scale_factor=envs.SGLANG_SKIP_SOFTMAX_DECODE_THRESHOLD_SCALE_FACTOR.get(),
|
||||||
|
out_dtype=self.q_data_type,
|
||||||
|
kv_cache_sf=kv_cache_sf,
|
||||||
|
multi_ctas_kv_counter_buffer=self._multi_ctas_kv_counter_buffer,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
num_requests = seq_lens.shape[0]
|
||||||
|
num_splits = min(self.decode_seq_len_splits, num_requests)
|
||||||
|
if num_splits == 1:
|
||||||
|
return run_group(query, block_tables, seq_lens)
|
||||||
|
|
||||||
|
order = torch.argsort(seq_lens)
|
||||||
|
query_by_request = query.view(
|
||||||
|
num_requests, q_len_per_req, query.shape[-2], query.shape[-1]
|
||||||
|
)
|
||||||
|
output_by_request = torch.empty(
|
||||||
|
query_by_request.shape,
|
||||||
|
dtype=self.q_data_type,
|
||||||
|
device=query.device,
|
||||||
|
)
|
||||||
|
for indices in torch.tensor_split(order, num_splits):
|
||||||
|
group_output = run_group(
|
||||||
|
query_by_request.index_select(0, indices).reshape(
|
||||||
|
-1, query.shape[-2], query.shape[-1]
|
||||||
|
),
|
||||||
|
block_tables.index_select(0, indices),
|
||||||
|
seq_lens.index_select(0, indices),
|
||||||
|
)
|
||||||
|
output_by_request.index_copy_(
|
||||||
|
0,
|
||||||
|
indices,
|
||||||
|
group_output.view(-1, q_len_per_req, query.shape[-2], query.shape[-1]),
|
||||||
|
)
|
||||||
|
return output_by_request.view(-1, query.shape[-2], query.shape[-1])
|
||||||
|
|
||||||
def _get_nvfp4_decode_kv_cache(self, layer: RadixAttention) -> tuple[
|
def _get_nvfp4_decode_kv_cache(self, layer: RadixAttention) -> tuple[
|
||||||
tuple[torch.Tensor, torch.Tensor],
|
tuple[torch.Tensor, torch.Tensor],
|
||||||
tuple[torch.Tensor, torch.Tensor],
|
tuple[torch.Tensor, torch.Tensor],
|
||||||
@@ -1170,21 +1243,16 @@ class TRTLLMHAAttnBackend(FlashInferAttnBackend):
|
|||||||
|
|
||||||
page_table = self._get_layer_page_table(layer, forward_batch)
|
page_table = self._get_layer_page_table(layer, forward_batch)
|
||||||
|
|
||||||
o = flashinfer.decode.trtllm_batch_decode_with_kv_cache(
|
o = self._run_fixed_q_len_decode(
|
||||||
query=q,
|
q,
|
||||||
kv_cache=kv_cache,
|
kv_cache,
|
||||||
workspace_buffer=self.workspace_buffer,
|
page_table,
|
||||||
block_tables=page_table,
|
self.forward_metadata.cache_seqlens_int32,
|
||||||
seq_lens=self.forward_metadata.cache_seqlens_int32,
|
|
||||||
max_seq_len=self.max_context_len,
|
|
||||||
bmm1_scale=bmm1_scale,
|
bmm1_scale=bmm1_scale,
|
||||||
bmm2_scale=bmm2_scale,
|
bmm2_scale=bmm2_scale,
|
||||||
window_left=layer.sliding_window_size,
|
window_left=layer.sliding_window_size,
|
||||||
sinks=attention_sink,
|
sinks=attention_sink,
|
||||||
skip_softmax_threshold_scale_factor=envs.SGLANG_SKIP_SOFTMAX_DECODE_THRESHOLD_SCALE_FACTOR.get(),
|
|
||||||
out_dtype=self.q_data_type, # model_runner.dtype
|
|
||||||
kv_cache_sf=kv_cache_block_scales,
|
kv_cache_sf=kv_cache_block_scales,
|
||||||
multi_ctas_kv_counter_buffer=self._multi_ctas_kv_counter_buffer,
|
|
||||||
)
|
)
|
||||||
if self.is_nvfp4_kvcache and o.dtype != self.q_data_type:
|
if self.is_nvfp4_kvcache and o.dtype != self.q_data_type:
|
||||||
o = o.to(self.q_data_type)
|
o = o.to(self.q_data_type)
|
||||||
@@ -1344,21 +1412,16 @@ class TRTLLMHAAttnBackend(FlashInferAttnBackend):
|
|||||||
multi_ctas_kv_counter_buffer=self._multi_ctas_kv_counter_buffer,
|
multi_ctas_kv_counter_buffer=self._multi_ctas_kv_counter_buffer,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
o = flashinfer.decode.trtllm_batch_decode_with_kv_cache(
|
o = self._run_fixed_q_len_decode(
|
||||||
query=q,
|
q,
|
||||||
kv_cache=kv_cache,
|
kv_cache,
|
||||||
workspace_buffer=self.workspace_buffer,
|
page_table,
|
||||||
block_tables=page_table,
|
self.forward_metadata.cache_seqlens_int32,
|
||||||
seq_lens=self.forward_metadata.cache_seqlens_int32,
|
|
||||||
max_seq_len=self.max_context_len,
|
|
||||||
bmm1_scale=bmm1_scale,
|
bmm1_scale=bmm1_scale,
|
||||||
bmm2_scale=bmm2_scale,
|
bmm2_scale=bmm2_scale,
|
||||||
window_left=layer.sliding_window_size,
|
window_left=layer.sliding_window_size,
|
||||||
sinks=attention_sink,
|
sinks=attention_sink,
|
||||||
skip_softmax_threshold_scale_factor=envs.SGLANG_SKIP_SOFTMAX_DECODE_THRESHOLD_SCALE_FACTOR.get(),
|
|
||||||
out_dtype=self.q_data_type,
|
|
||||||
q_len_per_req=self.forward_metadata.max_seq_len_q,
|
q_len_per_req=self.forward_metadata.max_seq_len_q,
|
||||||
multi_ctas_kv_counter_buffer=self._multi_ctas_kv_counter_buffer,
|
|
||||||
)
|
)
|
||||||
elif self.use_fmha_v2 and not cp_v2_active:
|
elif self.use_fmha_v2 and not cp_v2_active:
|
||||||
# CP-v2 must go through cp_strategy.run_attention (per-shard
|
# CP-v2 must go through cp_strategy.run_attention (per-shard
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import unittest
|
|||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
|
from sglang.srt.environ import envs
|
||||||
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
||||||
from sglang.srt.utils import is_flashinfer_available
|
from sglang.srt.utils import is_flashinfer_available
|
||||||
from sglang.srt.utils.common import (
|
from sglang.srt.utils.common import (
|
||||||
@@ -176,8 +177,11 @@ class TestTRTLLMMHADenseAttentionBackendCorrectness(CustomTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_projected_dense_decode_cases(self):
|
def test_projected_dense_decode_cases(self):
|
||||||
for case in self.DECODE_CASES:
|
for case_index, case in enumerate(self.DECODE_CASES):
|
||||||
with self.subTest(case=case.name, backend=case.backend):
|
splits = 2 if case_index == 0 else 1
|
||||||
|
with self.subTest(
|
||||||
|
case=case.name, backend=case.backend
|
||||||
|
), envs.SGLANG_TRTLLM_MHA_DECODE_SEQ_LEN_SPLITS.override(splits):
|
||||||
run_dense_attention_case(
|
run_dense_attention_case(
|
||||||
self,
|
self,
|
||||||
case,
|
case,
|
||||||
@@ -226,7 +230,9 @@ class TestTRTLLMMHADenseAttentionBackendCorrectness(CustomTestCase):
|
|||||||
|
|
||||||
def test_runner_mode_frozen_kv_mtp_cuda_graph_runner_cases(self):
|
def test_runner_mode_frozen_kv_mtp_cuda_graph_runner_cases(self):
|
||||||
for case in self.FROZEN_KV_MTP_RUNNER_CASES:
|
for case in self.FROZEN_KV_MTP_RUNNER_CASES:
|
||||||
with self.subTest(case=case.name, backend=case.backend):
|
with self.subTest(
|
||||||
|
case=case.name, backend=case.backend
|
||||||
|
), envs.SGLANG_TRTLLM_MHA_DECODE_SEQ_LEN_SPLITS.override(2):
|
||||||
run_dense_frozen_kv_mtp_cuda_graph_runner_case(
|
run_dense_frozen_kv_mtp_cuda_graph_runner_case(
|
||||||
self,
|
self,
|
||||||
case,
|
case,
|
||||||
|
|||||||
Reference in New Issue
Block a user