[unified-memory] Support fa3, the default MLA backend on pre-Blackwell hosts (#33046)
This commit is contained in:
@@ -193,6 +193,11 @@ def _fused_metadata_kernel_general(
|
|||||||
use_swa: tl.constexpr,
|
use_swa: tl.constexpr,
|
||||||
SHIFT: tl.constexpr,
|
SHIFT: tl.constexpr,
|
||||||
BLOCK_COLS: tl.constexpr,
|
BLOCK_COLS: tl.constexpr,
|
||||||
|
# Unified-memory dense-view path (page-major envelope shared with the mamba
|
||||||
|
# sub-pool). Both default to the identity for the statically-partitioned
|
||||||
|
# pool, where req_to_token already holds physical ids.
|
||||||
|
v2p_ptr=None,
|
||||||
|
PAGE_MULT: tl.constexpr = 1,
|
||||||
):
|
):
|
||||||
pid_b = tl.program_id(0) # batch index
|
pid_b = tl.program_id(0) # batch index
|
||||||
pid_c = tl.program_id(1) # column chunk index
|
pid_c = tl.program_id(1) # column chunk index
|
||||||
@@ -251,6 +256,13 @@ def _fused_metadata_kernel_general(
|
|||||||
else:
|
else:
|
||||||
page_table_val = page_index >> SHIFT
|
page_table_val = page_index >> SHIFT
|
||||||
|
|
||||||
|
# Unified memory: virtual page -> physical page -> that layer's dense block.
|
||||||
|
# Derived from page_table_val, NOT page_index, which the SWA branch below
|
||||||
|
# still needs in virtual space. Masked so padded lanes never index the table.
|
||||||
|
if v2p_ptr is not None:
|
||||||
|
page_table_val = tl.load(v2p_ptr + page_table_val, mask=mask, other=0)
|
||||||
|
page_table_val = page_table_val * PAGE_MULT
|
||||||
|
|
||||||
# Store to page_table
|
# Store to page_table
|
||||||
pt_offsets = i * page_table_stride_0 + col_offsets * page_table_stride_1
|
pt_offsets = i * page_table_stride_0 + col_offsets * page_table_stride_1
|
||||||
tl.store(page_table + pt_offsets, page_table_val, mask=mask, cache_modifier=".cg")
|
tl.store(page_table + pt_offsets, page_table_val, mask=mask, cache_modifier=".cg")
|
||||||
@@ -295,6 +307,9 @@ def _fused_metadata_kernel_ps1_no_swa(
|
|||||||
max_seq_pages,
|
max_seq_pages,
|
||||||
seq_len_delta: tl.constexpr,
|
seq_len_delta: tl.constexpr,
|
||||||
BLOCK_COLS: tl.constexpr,
|
BLOCK_COLS: tl.constexpr,
|
||||||
|
# Unified-memory dense-view path; identity defaults for the static pool.
|
||||||
|
v2p_ptr=None,
|
||||||
|
PAGE_MULT: tl.constexpr = 1,
|
||||||
):
|
):
|
||||||
pid_b = tl.program_id(0) # batch index
|
pid_b = tl.program_id(0) # batch index
|
||||||
pid_c = tl.program_id(1) # column chunk index
|
pid_c = tl.program_id(1) # column chunk index
|
||||||
@@ -338,6 +353,10 @@ def _fused_metadata_kernel_ps1_no_swa(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# page_table = page_index // 1 = page_index
|
# page_table = page_index // 1 = page_index
|
||||||
|
# Unified memory: at page_size 1 the virtual token id IS the virtual page id.
|
||||||
|
if v2p_ptr is not None:
|
||||||
|
page_index = tl.load(v2p_ptr + page_index, mask=mask, other=0)
|
||||||
|
page_index = page_index * PAGE_MULT
|
||||||
pt_offsets = i * page_table_stride_0 + col_offsets * page_table_stride_1
|
pt_offsets = i * page_table_stride_0 + col_offsets * page_table_stride_1
|
||||||
tl.store(page_table + pt_offsets, page_index, mask=mask, cache_modifier=".cg")
|
tl.store(page_table + pt_offsets, page_index, mask=mask, cache_modifier=".cg")
|
||||||
|
|
||||||
@@ -565,6 +584,8 @@ def normal_decode_set_metadata(
|
|||||||
page_size: int,
|
page_size: int,
|
||||||
swa_page_table: Optional[torch.Tensor] = None,
|
swa_page_table: Optional[torch.Tensor] = None,
|
||||||
token_to_kv_pool: Optional["SWAKVPool"] = None,
|
token_to_kv_pool: Optional["SWAKVPool"] = None,
|
||||||
|
v2p_page_table: Optional[torch.Tensor] = None,
|
||||||
|
kernel_page_multiplier: int = 1,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Fused Triton implementation that replaces 4-5 sequential CUDA kernels with 1-2 kernels:
|
Fused Triton implementation that replaces 4-5 sequential CUDA kernels with 1-2 kernels:
|
||||||
@@ -572,8 +593,14 @@ def normal_decode_set_metadata(
|
|||||||
2. cu_seqlens_k = cumsum(cache_seqlens) (prefix-sum)
|
2. cu_seqlens_k = cumsum(cache_seqlens) (prefix-sum)
|
||||||
3. page_indices = req_to_token[pool_idx, stride_idx] (2-D gather)
|
3. page_indices = req_to_token[pool_idx, stride_idx] (2-D gather)
|
||||||
4. page_table = page_indices // page_size (floor-divide)
|
4. page_table = page_indices // page_size (floor-divide)
|
||||||
|
4b. (unified memory) page_table = v2p_page_table[page] * kernel_page_multiplier
|
||||||
5. (optional) swa_page_table for sliding window attention
|
5. (optional) swa_page_table for sliding window attention
|
||||||
|
|
||||||
|
Step 4b is folded in rather than applied afterwards so the capture-stable
|
||||||
|
page_table is written already translated: no separate pass a caller could
|
||||||
|
forget, and no temporary to keep pointer-stable across cuda-graph replays.
|
||||||
|
Identity (None / 1) for the statically-partitioned pool.
|
||||||
|
|
||||||
Achieves ~5.2x speedup on H200 hardware for typical decode workloads.
|
Achieves ~5.2x speedup on H200 hardware for typical decode workloads.
|
||||||
|
|
||||||
Contract: only the live prefix (cdiv(cache_seqlens, page_size) pages) of each
|
Contract: only the live prefix (cdiv(cache_seqlens, page_size) pages) of each
|
||||||
@@ -633,6 +660,8 @@ def normal_decode_set_metadata(
|
|||||||
max_seq_pages,
|
max_seq_pages,
|
||||||
seq_len_delta,
|
seq_len_delta,
|
||||||
BLOCK_COLS=BLOCK_COLS,
|
BLOCK_COLS=BLOCK_COLS,
|
||||||
|
v2p_ptr=v2p_page_table,
|
||||||
|
PAGE_MULT=kernel_page_multiplier,
|
||||||
num_warps=8,
|
num_warps=8,
|
||||||
num_stages=3,
|
num_stages=3,
|
||||||
)
|
)
|
||||||
@@ -696,6 +725,8 @@ def normal_decode_set_metadata(
|
|||||||
use_swa,
|
use_swa,
|
||||||
shift,
|
shift,
|
||||||
BLOCK_COLS=BLOCK_COLS,
|
BLOCK_COLS=BLOCK_COLS,
|
||||||
|
v2p_ptr=v2p_page_table,
|
||||||
|
PAGE_MULT=kernel_page_multiplier,
|
||||||
num_warps=4,
|
num_warps=4,
|
||||||
num_stages=3,
|
num_stages=3,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from sglang.kernels.ops.kvcache.trtllm_mha_page_table import (
|
|||||||
)
|
)
|
||||||
from sglang.srt.configs.model_config import AttentionArch
|
from sglang.srt.configs.model_config import AttentionArch
|
||||||
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
|
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
|
||||||
|
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.layers.attention.verify_mask import VerifyMask, maybe_create_verify_mask
|
||||||
from sglang.srt.layers.cp.base import CPAttentionBackendKind, get_cp_strategy
|
from sglang.srt.layers.cp.base import CPAttentionBackendKind, get_cp_strategy
|
||||||
from sglang.srt.layers.cp.utils import is_cp_v2_active
|
from sglang.srt.layers.cp.utils import is_cp_v2_active
|
||||||
@@ -184,6 +185,11 @@ class FlashAttentionBackend(AttentionBackend):
|
|||||||
# seq_lens_cpu / seq_lens_sum D2H sync is ever needed.
|
# seq_lens_cpu / seq_lens_sum D2H sync is ever needed.
|
||||||
self.needs_cpu_seq_lens = False
|
self.needs_cpu_seq_lens = False
|
||||||
self.use_mla = model_runner.model_config.attention_arch == AttentionArch.MLA
|
self.use_mla = model_runner.model_config.attention_arch == AttentionArch.MLA
|
||||||
|
# Unified pool: req_to_token holds VIRTUAL ids but the MLA per-layer views
|
||||||
|
# are DENSE, so every page_table needs remapping. MLA-only -- the MHA/SWA
|
||||||
|
# sub-pools keep the strided envelope layout FA3 cannot read at all.
|
||||||
|
self._unified_hooks = unified_mla_hooks(model_runner.token_to_kv_pool_allocator)
|
||||||
|
self._unified_dense = self._unified_hooks.enabled and self.use_mla
|
||||||
self.skip_prefill = skip_prefill
|
self.skip_prefill = skip_prefill
|
||||||
self.attn_cp_size = model_runner.ps.attn_cp_size
|
self.attn_cp_size = model_runner.ps.attn_cp_size
|
||||||
self._verify_mask = None
|
self._verify_mask = None
|
||||||
@@ -1040,6 +1046,26 @@ class FlashAttentionBackend(AttentionBackend):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Unified pool: one remap for every eager branch above, which all filled
|
||||||
|
# page_table with VIRTUAL token ids. Rebinding is safe here because those
|
||||||
|
# branches each produced a fresh tensor; the captured path instead folds
|
||||||
|
# the remap into normal_decode_set_metadata, which must write in place.
|
||||||
|
#
|
||||||
|
# Placed BEFORE the `// page_size` reduction, in token space: since
|
||||||
|
# dense(t) = phys_page * (ps * L) + t % ps, dense(page_start) // ps is
|
||||||
|
# phys_page * L, the dense page id the kernel wants. One site then serves
|
||||||
|
# both page sizes, and it inherits translate_kv_loc_dense's tombstone
|
||||||
|
# clamp so an unwritten req_to_token slot lands in the page-0 sink.
|
||||||
|
if self._unified_dense and metadata.page_table is not None:
|
||||||
|
# Flattened: the page_size == 1 translate path uses index_select,
|
||||||
|
# which rejects a 2-D index.
|
||||||
|
pt = metadata.page_table
|
||||||
|
metadata.page_table = (
|
||||||
|
self._unified_hooks.translate_kv_loc_dense(pt.reshape(-1))
|
||||||
|
.to(torch.int32)
|
||||||
|
.view(pt.shape)
|
||||||
|
)
|
||||||
|
|
||||||
# Convert the page table to a strided format which is needed by FA3 API
|
# Convert the page table to a strided format which is needed by FA3 API
|
||||||
if self.page_size > 1:
|
if self.page_size > 1:
|
||||||
self.strided_indices = torch.arange(
|
self.strided_indices = torch.arange(
|
||||||
@@ -2632,6 +2658,12 @@ class FlashAttentionBackend(AttentionBackend):
|
|||||||
if self.use_sliding_window_kv_pool
|
if self.use_sliding_window_kv_pool
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
v2p_page_table=(
|
||||||
|
self._unified_hooks.v2p_page_table
|
||||||
|
if self._unified_dense
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
kernel_page_multiplier=self._unified_hooks.kernel_page_multiplier,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -2748,6 +2780,12 @@ class FlashAttentionBackend(AttentionBackend):
|
|||||||
if self.use_sliding_window_kv_pool
|
if self.use_sliding_window_kv_pool
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
v2p_page_table=(
|
||||||
|
self._unified_hooks.v2p_page_table
|
||||||
|
if self._unified_dense
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
kernel_page_multiplier=self._unified_hooks.kernel_page_multiplier,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._maybe_update_local_attn_metadata_for_replay(
|
self._maybe_update_local_attn_metadata_for_replay(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
|
|||||||
from sglang.srt.layers.attention.flashinfer_backend import (
|
from sglang.srt.layers.attention.flashinfer_backend import (
|
||||||
create_flashinfer_kv_indices_triton,
|
create_flashinfer_kv_indices_triton,
|
||||||
)
|
)
|
||||||
|
from sglang.srt.layers.attention.unified_mem_hooks import unified_mla_hooks
|
||||||
from sglang.srt.layers.dcp import (
|
from sglang.srt.layers.dcp import (
|
||||||
DecodeContextParallelMetadata,
|
DecodeContextParallelMetadata,
|
||||||
update_local_kv_lens_for_dcp,
|
update_local_kv_lens_for_dcp,
|
||||||
@@ -66,51 +67,6 @@ if is_flashinfer_available():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class UnifiedMLAHooks:
|
|
||||||
"""Allocator hooks the paged MLA backends need under the unified memory pool.
|
|
||||||
|
|
||||||
All-``None``/1/``False`` for the statically-partitioned pool, where
|
|
||||||
``req_to_token`` already holds physical ids.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Page-level virtual->physical table, gathered through by the block-table kernel.
|
|
||||||
v2p_page_table: Optional[torch.Tensor]
|
|
||||||
# Virtual token id -> DENSE kernel-facing id.
|
|
||||||
translate_kv_loc_dense: Optional[Callable[..., torch.Tensor]]
|
|
||||||
# Dense page stride scale (= number of full-attention MLA layers).
|
|
||||||
kernel_page_multiplier: int
|
|
||||||
enabled: bool
|
|
||||||
|
|
||||||
|
|
||||||
def unified_mla_hooks(allocator) -> UnifiedMLAHooks:
|
|
||||||
"""Probe ``allocator`` for the unified-pool dense-view hooks.
|
|
||||||
|
|
||||||
Detection keys on the page-level v2p table, NOT on
|
|
||||||
``kernel_page_multiplier > 1``: a configuration with exactly ONE
|
|
||||||
full-attention layer (e.g. a pipeline-parallel rank that owns a single MLA
|
|
||||||
layer) has multiplier 1 while its ``req_to_token`` still holds VIRTUAL ids.
|
|
||||||
With multiplier 1 the dense id collapses onto the physical id, so the v2p
|
|
||||||
gather alone is the whole translation -- skipping it would leave the block
|
|
||||||
table and the KV write loc in virtual space and silently address the wrong
|
|
||||||
pages once virtual and physical diverge (e.g. after compaction).
|
|
||||||
"""
|
|
||||||
v2p = getattr(allocator, "full_v2p_page_table", None)
|
|
||||||
if v2p is None:
|
|
||||||
return UnifiedMLAHooks(
|
|
||||||
v2p_page_table=None,
|
|
||||||
translate_kv_loc_dense=None,
|
|
||||||
kernel_page_multiplier=1,
|
|
||||||
enabled=False,
|
|
||||||
)
|
|
||||||
return UnifiedMLAHooks(
|
|
||||||
v2p_page_table=v2p,
|
|
||||||
translate_kv_loc_dense=getattr(allocator, "translate_kv_loc_dense", None),
|
|
||||||
kernel_page_multiplier=getattr(allocator, "kernel_page_multiplier", 1),
|
|
||||||
enabled=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DecodeMetadata:
|
class DecodeMetadata:
|
||||||
decode_wrapper: BatchMLAPagedAttentionWrapper
|
decode_wrapper: BatchMLAPagedAttentionWrapper
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ from sglang.srt.environ import envs
|
|||||||
from sglang.srt.layers.attention.flashinfer_mla_backend import (
|
from sglang.srt.layers.attention.flashinfer_mla_backend import (
|
||||||
FlashInferMLAAttnBackend,
|
FlashInferMLAAttnBackend,
|
||||||
FlashInferMLAMultiStepDraftBackend,
|
FlashInferMLAMultiStepDraftBackend,
|
||||||
unified_mla_hooks,
|
|
||||||
)
|
)
|
||||||
|
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.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.forward_batch_info import ForwardBatch, ForwardMode
|
||||||
from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph import (
|
from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph import (
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Copyright 2023-2026 SGLang Team
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
# ==============================================================================
|
||||||
|
"""Allocator hooks the paged MLA attention backends need under the unified
|
||||||
|
memory pool.
|
||||||
|
|
||||||
|
Lives in its own module because three unrelated backend families consume it
|
||||||
|
(fa3, flashinfer_mla, and trtllm_mla with its cutedsl_mla / tokenspeed_mla
|
||||||
|
subclasses) and none of them should have to import another's module to get it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
class UnifiedMLAHooks(msgspec.Struct, frozen=True):
|
||||||
|
"""Dense-view hooks for one KV allocator.
|
||||||
|
|
||||||
|
All-``None``/1/``False`` for the statically-partitioned pool, where
|
||||||
|
``req_to_token`` already holds physical ids and no translation is needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Page-level virtual->physical table, gathered through by block-table kernels.
|
||||||
|
v2p_page_table: Optional[torch.Tensor]
|
||||||
|
# Virtual token id -> DENSE kernel-facing id (tombstones clamped to the sink).
|
||||||
|
translate_kv_loc_dense: Optional[Callable[..., torch.Tensor]]
|
||||||
|
# Dense page stride scale (= number of full-attention MLA layers).
|
||||||
|
kernel_page_multiplier: int
|
||||||
|
enabled: bool
|
||||||
|
|
||||||
|
|
||||||
|
_STATIC_POOL = UnifiedMLAHooks(
|
||||||
|
v2p_page_table=None,
|
||||||
|
translate_kv_loc_dense=None,
|
||||||
|
kernel_page_multiplier=1,
|
||||||
|
enabled=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def unified_mla_hooks(allocator) -> UnifiedMLAHooks:
|
||||||
|
"""Probe ``allocator`` for the unified-pool dense-view hooks.
|
||||||
|
|
||||||
|
Detection keys on the v2p table, NOT on ``kernel_page_multiplier > 1``: a
|
||||||
|
rank owning exactly ONE full-attention layer has multiplier 1 while its
|
||||||
|
``req_to_token`` is still virtual. There the dense id collapses onto the
|
||||||
|
physical id, so the v2p gather alone is the whole translation.
|
||||||
|
"""
|
||||||
|
v2p = getattr(allocator, "full_v2p_page_table", None)
|
||||||
|
if v2p is None:
|
||||||
|
return _STATIC_POOL
|
||||||
|
return UnifiedMLAHooks(
|
||||||
|
v2p_page_table=v2p,
|
||||||
|
translate_kv_loc_dense=getattr(allocator, "translate_kv_loc_dense", None),
|
||||||
|
kernel_page_multiplier=getattr(allocator, "kernel_page_multiplier", 1),
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
@@ -7712,12 +7712,14 @@ class ServerArgs:
|
|||||||
# are the RESOLVED ids from _resolved_attention_backends: "flashinfer" is
|
# are the RESOLVED ids from _resolved_attention_backends: "flashinfer" is
|
||||||
# FlashInferMLAAttnBackend for an MLA model, "trtllm_mla" the trtllm
|
# FlashInferMLAAttnBackend for an MLA model, "trtllm_mla" the trtllm
|
||||||
# decode kernel; "cutedsl_mla" and "tokenspeed_mla" subclass
|
# decode kernel; "cutedsl_mla" and "tokenspeed_mla" subclass
|
||||||
# TRTLLMMLABackend and inherit its dense read/write path.
|
# TRTLLMMLABackend and inherit its dense read/write path; "fa3" remaps its
|
||||||
|
# page_table (in-kernel for captured decode, one funnel for eager).
|
||||||
# flashmla / cutlass_mla share the create_flashmla block-table path and
|
# flashmla / cutlass_mla share the create_flashmla block-table path and
|
||||||
# can be added the same way once exercised.
|
# can be added the same way once exercised.
|
||||||
if self.enable_unified_memory and self.use_mla_backend():
|
if self.enable_unified_memory and self.use_mla_backend():
|
||||||
allowed_full = {
|
allowed_full = {
|
||||||
"triton",
|
"triton",
|
||||||
|
"fa3",
|
||||||
"trtllm_mla",
|
"trtllm_mla",
|
||||||
"flashinfer",
|
"flashinfer",
|
||||||
"cutedsl_mla",
|
"cutedsl_mla",
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
"""Kimi-Linear (MLA full attention + KDA linear attention) served from the
|
"""Kimi-Linear (MLA full attention + KDA linear attention) served from the
|
||||||
unified memory pool.
|
unified memory pool.
|
||||||
|
|
||||||
`--enable-unified-memory` replaces the statically-partitioned hybrid pools with
|
Under `--enable-unified-memory` the MLA full side is exposed as DENSE per-layer
|
||||||
one byte buffer split dynamically between the full-attention KV sub-pool and the
|
views and every loc the kernels see is a translated virtual id, so the whole
|
||||||
per-request KDA state sub-pool. For an MLA model the full side is exposed as
|
read/write path differs from the static pool. The unit tests pin that pool in
|
||||||
DENSE per-layer views (`build_dense_mla_views`) and every loc the kernels see is
|
isolation; this is the end-to-end guard. `test_prefix_cache_branching` carries
|
||||||
a translated virtual id, so the whole read/write path differs from the static
|
most of the weight: a radix hit replays virtual locs whose physical pages may
|
||||||
pool: `translate_kv_loc_dense` for kv_indices and the cuda-graph write loc,
|
have moved under compaction.
|
||||||
`HybridLinearKVPool._full_translate` for the model-level MLA entry points, and
|
|
||||||
page-envelope relocation on allocator compaction.
|
|
||||||
|
|
||||||
None of that is covered by the CPU/GPU unit tests, which pin the pool in
|
No `--attention-backend` is pinned on purpose -- the test runs whatever the host
|
||||||
isolation. This is the end-to-end guard: accuracy must match the static-pool
|
resolves to (`fa3` on this suite's H100 runner, also the H200 default). Both
|
||||||
baseline, and the prefix-cache branching case must still hit, since a radix hit
|
defects found in review on #32972 were reachable only under a resolved default,
|
||||||
replays virtual locs whose physical pages may have moved under compaction.
|
which a pinned test hides by construction.
|
||||||
|
|
||||||
Reference numbers on 2x H200 TP2, GSM8K 400 examples (2026-07-30):
|
Reference GSM8K, all with `--enable-unified-memory`:
|
||||||
static pools 0.915, `--enable-unified-memory` 0.900 (1 sigma ~= 0.015) -- both with
|
- 2x H200 TP2, resolved default (fa3): 0.917 @400, vs 0.915 static (1 sigma
|
||||||
the attention backend pinned to triton, as this test runs it. For reference the
|
~= 0.015). This file as written scores 0.920 @200.
|
||||||
paged MLA kernels land in the same band on a single B300 TP1, GSM8K 200, unified
|
- 2x H200 TP2, `--attention-backend triton`: 0.900 @400.
|
||||||
(2026-07-31): 0.915 with flashinfer prefill+decode, 0.900 with trtllm_mla. Those
|
- 1x B300 TP1: 0.915 flashinfer, 0.900 trtllm_mla, @200.
|
||||||
are not exercised here (see the comment on `other_args`).
|
|
||||||
Nightly-only: it needs a second full 48B server launch, which is too much to add
|
Nightly-only: a second full 48B server launch is too much for per-PR CI on top
|
||||||
to per-PR CI on top of the existing Kimi-Linear e2e coverage.
|
of the existing Kimi-Linear e2e coverage.
|
||||||
|
|
||||||
python -m pytest test/registered/models_e2e/test_kimi_linear_unified_memory.py -v
|
python -m pytest test/registered/models_e2e/test_kimi_linear_unified_memory.py -v
|
||||||
"""
|
"""
|
||||||
@@ -45,7 +43,7 @@ class TestKimiLinearUnifiedMemory(
|
|||||||
model = KIMI_LINEAR_MODEL
|
model = KIMI_LINEAR_MODEL
|
||||||
cache_chunk_size = 64
|
cache_chunk_size = 64
|
||||||
# Same bar as the static-pool Kimi-Linear e2e test: unified memory must not
|
# Same bar as the static-pool Kimi-Linear e2e test: unified memory must not
|
||||||
# cost accuracy (measured 0.900 vs 0.915 static, see the module docstring).
|
# cost accuracy (measured 0.917 vs 0.915 static, see the module docstring).
|
||||||
gsm8k_score_threshold = 0.88
|
gsm8k_score_threshold = 0.88
|
||||||
other_args = [
|
other_args = [
|
||||||
"--trust-remote-code",
|
"--trust-remote-code",
|
||||||
@@ -54,22 +52,6 @@ class TestKimiLinearUnifiedMemory(
|
|||||||
"--chunked-prefill-size",
|
"--chunked-prefill-size",
|
||||||
"2048",
|
"2048",
|
||||||
"--enable-unified-memory",
|
"--enable-unified-memory",
|
||||||
# Pinned because the resolved default is not portable: on pre-Blackwell
|
|
||||||
# (this suite's runner is H100) an unspecified backend resolves to `fa3`,
|
|
||||||
# which cannot read the dense views at all, so the un-pinned form fails at
|
|
||||||
# startup with the page-major allowlist assertion. Unified memory on such a
|
|
||||||
# host currently REQUIRES an explicit compatible --attention-backend; that
|
|
||||||
# is a real usability gap, tracked separately, not something this test can
|
|
||||||
# paper over.
|
|
||||||
#
|
|
||||||
# Consequence to keep in mind: pinning triton means this test does NOT
|
|
||||||
# cover the paged MLA backends (trtllm_mla / flashinfer / cutedsl_mla /
|
|
||||||
# tokenspeed_mla), which is where dense-id translation bugs live -- a
|
|
||||||
# captured flashinfer decode reading untranslated virtual ids scored GSM8K
|
|
||||||
# 0.000 on a healthy server. Those paths are covered by the unit tests plus
|
|
||||||
# manual B300 runs; an sm100-gated case here would close the gap.
|
|
||||||
"--attention-backend",
|
|
||||||
"triton",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,12 @@ block table filled with DENSE page ids:
|
|||||||
|
|
||||||
dense_page(virtual_page) = v2p[virtual_page] * layer_num
|
dense_page(virtual_page) = v2p[virtual_page] * layer_num
|
||||||
|
|
||||||
`create_flashmla_kv_indices_triton` does that in-kernel via `v2p_ptr` / `PAGE_MULT`
|
Three backend families reach that same formula by different routes:
|
||||||
(trtllm_mla / cutedsl_mla / tokenspeed_mla), and the flashinfer_mla updaters do it
|
- `create_flashmla_kv_indices_triton` in-kernel via `v2p_ptr` / `PAGE_MULT`
|
||||||
by post-gathering `translate_kv_loc_dense` over the token-level kv_indices.
|
(trtllm_mla / cutedsl_mla / tokenspeed_mla);
|
||||||
|
- the flashinfer_mla updaters, post-gathering `translate_kv_loc_dense` over the
|
||||||
|
token-level kv_indices;
|
||||||
|
- `normal_decode_set_metadata` in-kernel, for fa3's captured-decode page table.
|
||||||
|
|
||||||
Covered here:
|
Covered here:
|
||||||
- kernel identity: `v2p_ptr=None, PAGE_MULT=1` is byte-identical to main;
|
- kernel identity: `v2p_ptr=None, PAGE_MULT=1` is byte-identical to main;
|
||||||
@@ -30,7 +33,10 @@ Covered here:
|
|||||||
ragged sequence lengths and a non-identity v2p permutation;
|
ragged sequence lengths and a non-identity v2p permutation;
|
||||||
- padded block-table lanes never index the v2p table out of bounds;
|
- padded block-table lanes never index the v2p table out of bounds;
|
||||||
- the token-level dense translate the flashinfer updaters apply agrees with the
|
- the token-level dense translate the flashinfer updaters apply agrees with the
|
||||||
page-level block table the trtllm path builds.
|
page-level block table the trtllm path builds;
|
||||||
|
- fa3's fused metadata kernels agree with the same reference, on both the
|
||||||
|
page_size == 1 fast path (which is what Kimi-Linear takes: fa3 imposes no
|
||||||
|
page-size constraint) and the general path.
|
||||||
|
|
||||||
python -m pytest test/registered/unit/mem_cache/test_unified_mla_dense_block_table.py -v
|
python -m pytest test/registered/unit/mem_cache/test_unified_mla_dense_block_table.py -v
|
||||||
"""
|
"""
|
||||||
@@ -199,6 +205,105 @@ class TestDenseBlockTable(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(_HAS_CUDA, "requires CUDA")
|
||||||
|
class TestFa3MetadataDenseBlockTable(unittest.TestCase):
|
||||||
|
"""fa3 folds the unified remap into `normal_decode_set_metadata`, the fused
|
||||||
|
gather that writes its captured-decode page table, so the kernel itself has
|
||||||
|
to get the mapping right. Two kernels back it: a page_size == 1 / no-SWA fast
|
||||||
|
path (what Kimi-Linear takes, since fa3 imposes no page-size constraint) and
|
||||||
|
a general one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _run(self, page_size, *, v2p, mult, bs=5, max_ctx=2048):
|
||||||
|
from sglang.kernels.ops.attention.metadata import normal_decode_set_metadata
|
||||||
|
|
||||||
|
maker = TestDenseBlockTable._make_batch
|
||||||
|
rt, rpi, sl, v2p_full = maker(self, page_size, bs=bs, max_ctx=max_ctx)
|
||||||
|
v2p_arg = v2p_full if v2p else None
|
||||||
|
|
||||||
|
max_pages = (max_ctx + page_size - 1) // page_size
|
||||||
|
page_table = torch.zeros((bs, max_pages), dtype=torch.int32, device=_DEV)
|
||||||
|
cache_seqlens = torch.zeros((bs,), dtype=torch.int32, device=_DEV)
|
||||||
|
cu_seqlens_k = torch.zeros((bs + 1,), dtype=torch.int32, device=_DEV)
|
||||||
|
strided = torch.arange(0, max_ctx, page_size, device=_DEV)
|
||||||
|
max_seq_pages = (int(sl.max().item()) + page_size - 1) // page_size
|
||||||
|
|
||||||
|
normal_decode_set_metadata(
|
||||||
|
cache_seqlens,
|
||||||
|
cu_seqlens_k,
|
||||||
|
page_table,
|
||||||
|
rt,
|
||||||
|
rpi,
|
||||||
|
strided,
|
||||||
|
max_seq_pages,
|
||||||
|
sl.to(torch.int64),
|
||||||
|
0,
|
||||||
|
page_size,
|
||||||
|
v2p_page_table=v2p_arg,
|
||||||
|
kernel_page_multiplier=mult,
|
||||||
|
)
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
want = _reference(rt, rpi, sl, page_size, v2p=v2p_arg, mult=mult)
|
||||||
|
return page_table, want, sl
|
||||||
|
|
||||||
|
def _assert_live_prefix(self, got, want, sl, page_size):
|
||||||
|
"""The kernel contract only (re)writes each row's live page prefix; the
|
||||||
|
tail keeps stale values that consumers bound by cache_seqlens."""
|
||||||
|
for r in range(got.shape[0]):
|
||||||
|
n_pages = (int(sl[r].item()) + page_size - 1) // page_size
|
||||||
|
self.assertTrue(
|
||||||
|
torch.equal(got[r, :n_pages].long(), want[r, :n_pages]),
|
||||||
|
f"row {r} (page_size={page_size}):\n"
|
||||||
|
f"got ={got[r, :n_pages]}\nwant={want[r, :n_pages]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_identity_when_hooks_absent(self):
|
||||||
|
"""Static pool: no v2p, multiplier 1 -> byte-identical to pre-change."""
|
||||||
|
for page_size in (1, 64):
|
||||||
|
got, want, sl = self._run(page_size, v2p=False, mult=1)
|
||||||
|
self._assert_live_prefix(got, want, sl, page_size)
|
||||||
|
|
||||||
|
def test_dense_mapping_ps1_fast_path(self):
|
||||||
|
got, want, sl = self._run(1, v2p=True, mult=_LAYERS)
|
||||||
|
self._assert_live_prefix(got, want, sl, 1)
|
||||||
|
|
||||||
|
def test_dense_mapping_general_path(self):
|
||||||
|
got, want, sl = self._run(64, v2p=True, mult=_LAYERS)
|
||||||
|
self._assert_live_prefix(got, want, sl, 64)
|
||||||
|
|
||||||
|
def test_single_full_attention_layer(self):
|
||||||
|
"""multiplier 1 with a real v2p: the gather alone is the translation."""
|
||||||
|
for page_size in (1, 64):
|
||||||
|
got, want, sl = self._run(page_size, v2p=True, mult=1)
|
||||||
|
self._assert_live_prefix(got, want, sl, page_size)
|
||||||
|
virtual = _reference(
|
||||||
|
*TestDenseBlockTable._make_batch(self, page_size)[:3],
|
||||||
|
page_size,
|
||||||
|
v2p=None,
|
||||||
|
mult=1,
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
torch.equal(want, virtual),
|
||||||
|
"test batch degenerated: v2p is the identity on the pages used",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_agrees_with_flashmla_block_table(self):
|
||||||
|
"""fa3 and trtllm_mla build the same table two different ways; a
|
||||||
|
disagreement means one family is addressing the wrong pages."""
|
||||||
|
for page_size in (1, 64):
|
||||||
|
got, _, sl = self._run(page_size, v2p=True, mult=_LAYERS)
|
||||||
|
rt, rpi, sl2, v2p = TestDenseBlockTable._make_batch(self, page_size)
|
||||||
|
other = _fill_block_table(
|
||||||
|
rt, rpi, sl2, page_size, v2p=v2p, mult=_LAYERS
|
||||||
|
).long()
|
||||||
|
for r in range(got.shape[0]):
|
||||||
|
n_pages = (int(sl[r].item()) + page_size - 1) // page_size
|
||||||
|
self.assertTrue(
|
||||||
|
torch.equal(got[r, :n_pages].long(), other[r, :n_pages]),
|
||||||
|
f"fa3 and flashmla block tables disagree (row {r}, ps={page_size})",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestUnifiedMLAHookDetection(unittest.TestCase):
|
class TestUnifiedMLAHookDetection(unittest.TestCase):
|
||||||
"""`unified_mla_hooks` decides whether the paged MLA backends translate at
|
"""`unified_mla_hooks` decides whether the paged MLA backends translate at
|
||||||
all. Getting the predicate wrong is silent: the block table and KV write loc
|
all. Getting the predicate wrong is silent: the block table and KV write loc
|
||||||
@@ -207,7 +312,7 @@ class TestUnifiedMLAHookDetection(unittest.TestCase):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _probe(**attrs):
|
def _probe(**attrs):
|
||||||
from sglang.srt.layers.attention.flashinfer_mla_backend import (
|
from sglang.srt.layers.attention.unified_mem_hooks import (
|
||||||
unified_mla_hooks,
|
unified_mla_hooks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,15 @@
|
|||||||
The page-major envelope K/V views are strided, which only the Triton attention
|
The page-major envelope K/V views are strided, which only the Triton attention
|
||||||
kernels read. The one exception is the unified-memory MLA pool: it exposes each
|
kernels read. The one exception is the unified-memory MLA pool: it exposes each
|
||||||
layer as a DENSE contiguous view (`build_dense_mla_views`), so the paged MLA
|
layer as a DENSE contiguous view (`build_dense_mla_views`), so the paged MLA
|
||||||
backends (`trtllm_mla` and its `cutedsl_mla` / `tokenspeed_mla` subclasses, plus
|
backends can read it directly once their kv_indices / block tables are remapped
|
||||||
`flashinfer`'s MLA backend) can read it directly once their kv_indices / block
|
to dense ids -- `fa3`, `flashinfer`'s MLA backend, and `trtllm_mla` with its
|
||||||
tables are remapped to dense ids.
|
`cutedsl_mla` / `tokenspeed_mla` subclasses.
|
||||||
|
|
||||||
Pinned here so the exception cannot silently widen to a backend that has no
|
Pinned here so the exception cannot silently widen to a backend that has no
|
||||||
dense-id remapping (`fa3`, `flashmla`, ...) or leak into the MHA path.
|
dense-id remapping (`flashmla`, `cutlass_mla`, ...) or leak into the MHA path.
|
||||||
|
`fa3` matters most: it is the resolved default on pre-Blackwell hosts, so it is
|
||||||
|
the one entry whose absence used to make `--enable-unified-memory` fail to boot
|
||||||
|
under its own default configuration.
|
||||||
|
|
||||||
python -m pytest test/registered/unit/server_args/test_page_major_backend_allowlist.py -v
|
python -m pytest test/registered/unit/server_args/test_page_major_backend_allowlist.py -v
|
||||||
"""
|
"""
|
||||||
@@ -66,9 +69,15 @@ def _accepts(backend: str, *, use_mla: bool, unified: bool = True) -> bool:
|
|||||||
|
|
||||||
class TestPageMajorBackendAllowlist(unittest.TestCase):
|
class TestPageMajorBackendAllowlist(unittest.TestCase):
|
||||||
# Wired for the dense per-layer MLA views (see the module docstring).
|
# Wired for the dense per-layer MLA views (see the module docstring).
|
||||||
DENSE_MLA_BACKENDS = ("trtllm_mla", "flashinfer", "cutedsl_mla", "tokenspeed_mla")
|
DENSE_MLA_BACKENDS = (
|
||||||
|
"fa3",
|
||||||
|
"trtllm_mla",
|
||||||
|
"flashinfer",
|
||||||
|
"cutedsl_mla",
|
||||||
|
"tokenspeed_mla",
|
||||||
|
)
|
||||||
# No dense-id remapping: must stay rejected until they get one.
|
# No dense-id remapping: must stay rejected until they get one.
|
||||||
UNWIRED_BACKENDS = ("fa3", "flashmla", "cutlass_mla", "trtllm_mha", "aiter")
|
UNWIRED_BACKENDS = ("flashmla", "cutlass_mla", "trtllm_mha", "aiter")
|
||||||
|
|
||||||
def test_triton_always_allowed(self):
|
def test_triton_always_allowed(self):
|
||||||
for use_mla in (True, False):
|
for use_mla in (True, False):
|
||||||
|
|||||||
Reference in New Issue
Block a user