dsv4.1: remaining model and runtime integration (#38798)

Co-authored-by: BBuf <1182563586@qq.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Xiaoyu Zhang <xiaoyu.zhang@radixark.ai>
Co-authored-by: Yuwei An <ayw.sirius19@gmail.com>
Co-authored-by: Khoa Pham <khoa.pham@radixark.ai>
Co-authored-by: Yuhao Yang <47235274+yhyang201@users.noreply.github.com>
Co-authored-by: Zhichen Zeng <zczeng@uw.edu>
Co-authored-by: Ke Bao <ispobaoke@gmail.com>
This commit is contained in:
Liangsheng Yin
2026-09-18 02:55:30 -07:00
committed by GitHub
co-authored by BBuf Claude Opus 5 Xiaoyu Zhang Yuwei An Khoa Pham Yuhao Yang Zhichen Zeng Ke Bao
parent 1b200ffaaa
commit a6cf05817f
103 changed files with 8807 additions and 723 deletions
@@ -126,6 +126,26 @@ class TestDSV4AttentionBackendCorrectness(CustomTestCase):
extend_lens=(16,),
compress_ratio=128,
),
DSV4AttentionCase(
name="dsv4_c2_extend",
backend="dsv4",
forward_mode=ForwardMode.EXTEND,
num_heads=64,
page_size=DSV4_PAGE_SIZE,
# Odd lengths: the ratio-2 causal count (pos + 1) // 2 rounds down.
prefix_lens=(33,),
extend_lens=(7,),
compress_ratio=2,
),
DSV4AttentionCase(
name="dsv4_c2_decode",
backend="dsv4",
forward_mode=ForwardMode.DECODE,
num_heads=64,
page_size=DSV4_PAGE_SIZE,
prefix_lens=(65,),
compress_ratio=2,
),
DSV4AttentionCase(
name="dsv4_c128_decode",
backend="dsv4",
@@ -533,6 +553,7 @@ class TestDSV4BreakableCudaGraphMetadataContract(CustomTestCase):
backend.model_runner = SimpleNamespace(
spec_algorithm=SpeculativeAlgorithm.DFLASH
)
backend.token_to_kv_pool = SimpleNamespace(request_window=None)
backend.forward_metadata = DSV4Metadata(
self._make_core_metadata(0), indexer_metadata=None
)
@@ -577,6 +598,7 @@ class TestDSV4BreakableCudaGraphMetadataContract(CustomTestCase):
backend.model_runner = SimpleNamespace(
spec_algorithm=SpeculativeAlgorithm.DFLASH
)
backend.token_to_kv_pool = SimpleNamespace(request_window=None)
backend.forward_metadata = DSV4Metadata(
self._make_core_metadata(0), indexer_metadata=None
)
@@ -794,7 +816,8 @@ class TestDSV4SwaOutCacheLocResolution(CustomTestCase):
backend = object.__new__(DeepseekV4AttnBackend)
backend.forward_metadata = None
backend.token_to_kv_pool = SimpleNamespace(
translate_loc_from_full_to_swa=lambda loc: mapping[loc]
translate_loc_from_full_to_swa=lambda loc: mapping[loc],
request_window=None,
)
return backend
@@ -175,7 +175,9 @@ def _make_backend(
dsv4_prefill_backend: str = "auto",
) -> DeepseekV4AttnBackend:
backend = DeepseekV4AttnBackend.__new__(DeepseekV4AttnBackend)
backend.forward_metadata = SimpleNamespace(sparse_prefill_cache=None)
backend.forward_metadata = SimpleNamespace(
sparse_prefill_cache=None, late_layer_tail=None
)
backend.req_to_token = req_to_token
backend.sparse_prefill_workspace = SparsePrefillWorkspace(device)
backend.softmax_scale = 512**-0.5
@@ -1,6 +1,8 @@
import unittest
from types import SimpleNamespace
import torch
from sglang.srt.arg_groups.overrides import resolution_result
from sglang.srt.arg_groups.speculative_hook import (
_handle_dspark,
@@ -120,5 +122,37 @@ class TestDsparkDpAttentionMoeA2aGate(CustomTestCase):
_handle_dspark(server_args)
class TestDsparkFoldedSamplingDefault(CustomTestCase):
def test_sharded_greedy_default_and_sampling_override(self):
from sglang.srt.environ import DsparkFoldedSampling, envs
from sglang.srt.speculative.dspark_components.dspark_draft_sampler import (
_resolve_folded_sampling,
)
model = SimpleNamespace(
lm_head=SimpleNamespace(org_vocab_size=128, weight=torch.empty(1)),
markov_head=SimpleNamespace(supports_sharded_greedy=True),
)
args = dict(
model=model,
gamma=5,
max_bs=64,
device="cpu",
tp_rank=0,
available_memory_gb=16,
)
with envs.SGLANG_DSPARK_FOLDED_SAMPLING.override(
DsparkFoldedSampling.AUTO.value
):
self.assertFalse(_resolve_folded_sampling(**args))
model.markov_head.supports_sharded_greedy = False
self.assertTrue(_resolve_folded_sampling(**args))
model.markov_head.supports_sharded_greedy = True
with envs.SGLANG_DSPARK_FOLDED_SAMPLING.override(
DsparkFoldedSampling.FORCE.value
):
self.assertTrue(_resolve_folded_sampling(**args))
if __name__ == "__main__":
unittest.main()
@@ -86,6 +86,7 @@ def _prefill_queue(sched):
def _decode_queue(sched):
q = SimpleNamespace(
scheduler=sched,
token_to_kv_pool_allocator=MagicMock(),
retracted_queue=[],
pending_reqs=[],
_check_if_req_exceed_kv_capacity=MagicMock(return_value=False),
@@ -906,7 +906,7 @@ def _buf_infos(*ptrs):
def _make_dsv4_target(*, unified, mapping=None):
pool = object.__new__(DeepSeekV4TokenToKVPool)
pool.compression_ratios = [0, 4, 128]
pool.compression_ratios = [0, 2, 1, 4, 128]
pool._unified_kv = unified
pool.page_size = 256
pool.sliding_window = 128
@@ -7,6 +7,7 @@ SM90 / SM100 / SM120.
"""
import unittest
from types import SimpleNamespace
from unittest import mock
import torch
@@ -267,24 +268,26 @@ class TestMxfp8LinearBackends(_LinearBackendCheck):
is_backend_supported.assert_called_once_with("cute-dsl", 107)
def _build_block32_layer(n: int, k: int, keep_plain_weight_layout: bool = False):
quant_config = Fp8Config(
is_checkpoint_fp8_serialized=True,
activation_scheme="dynamic",
weight_block_size=[32, 32],
scale_fmt="ue8m0",
)
layer = _make_linear(quant_config, n, k)
if keep_plain_weight_layout:
layer.keep_plain_weight_layout = True
w = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
w_fp8, scale_e8m0, w_dequant = _quantize_fp8_block32_ue8m0(w)
load_linear_weights(layer, weight=w_fp8, weight_scale_inv=scale_e8m0)
return layer, w_dequant
class TestBlockFp8AsMxfp8Linear(_LinearBackendCheck):
"""A 32-wide-K ue8m0 block-fp8 weight served through the MXFP8 GEMMs."""
@staticmethod
def _build_layer(n: int, k: int, keep_plain_weight_layout: bool = False):
quant_config = Fp8Config(
is_checkpoint_fp8_serialized=True,
activation_scheme="dynamic",
weight_block_size=[32, 32],
scale_fmt="ue8m0",
)
layer = _make_linear(quant_config, n, k)
if keep_plain_weight_layout:
layer.keep_plain_weight_layout = True
w = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
w_fp8, scale_e8m0, w_dequant = _quantize_fp8_block32_ue8m0(w)
load_linear_weights(layer, weight=w_fp8, weight_scale_inv=scale_e8m0)
return layer, w_dequant
_build_layer = staticmethod(_build_block32_layer)
def _run(self, backend: str):
self._check_backend(
@@ -339,6 +342,120 @@ class TestBlockFp8AsMxfp8Linear(_LinearBackendCheck):
plain_layer.quant_method.apply(plain_layer, Mxfp8SwizzledInput(q, s))
@unittest.skipUnless(
"flashinfer_cutedsl" in _block32_backends(),
"block-fp8-as-MXFP8 prefill tuning needs the FlashInfer CuTe-DSL kernel",
)
class TestBlockFp8AsMxfp8PrefillAutotune(_LinearBackendCheck):
"""The startup hook that tunes those layers for the prefill M buckets."""
def setUp(self):
super().setUp()
patcher = mock.patch.object(
fp8_utils,
"FP8_GEMM_RUNNER_BACKEND",
Fp8GemmRunnerBackend.FLASHINFER_CUTEDSL,
)
patcher.start()
self.addCleanup(patcher.stop)
torch.manual_seed(7)
@staticmethod
def _ready_layer(n: int, k: int, keep_plain_weight_layout: bool = False):
layer, _ = _build_block32_layer(n, k, keep_plain_weight_layout)
layer.quant_method.process_weights_after_loading(layer)
return layer
def test_model_hook_deduplicates_ready_block_fp8_weights(self):
from sglang.srt.models.deepseek_v4 import DeepseekV4ForCausalLM
layers = torch.nn.ModuleList()
methods = []
for _ in range(2):
layer = self._ready_layer(128, 128)
methods.append(layer.quant_method)
layer.quant_method.apply = mock.Mock()
layers.append(layer)
# An unprepared layer intentionally has no swizzled scale buffer.
fallback = self._ready_layer(128, 128, keep_plain_weight_layout=True)
layers.append(fallback)
model = SimpleNamespace(
config=SimpleNamespace(model_type="deepseek_v41"), model=layers
)
count = DeepseekV4ForCausalLM.autotune_prefill_kernels(
model, 4096, dtype=torch.bfloat16
)
self.assertEqual(count, 1)
methods[0].apply.assert_called_once()
self.assertEqual(methods[0].apply.call_args.args[1].shape, (4096, 128))
methods[1].apply.assert_not_called()
for method in methods:
self.assertEqual(method.mxfp8_prefill_autotune_min_tokens, 4096)
self.assertIsNone(fallback.quant_method.mxfp8_prefill_autotune_min_tokens)
def test_block_fp8_dispatch_keeps_decode_and_determinism_pinned(self):
layer = self._ready_layer(128, 128)
method = layer.quant_method
method.mxfp8_prefill_autotune_min_tokens = 4096
call = mock.Mock(return_value=torch.empty(0))
method.w8a8_mxfp8_linear = call
for rows, invariant, deterministic, expected in (
(6, False, False, None),
(4096, False, False, False),
(4096, True, False, True),
(4096, False, True, True),
):
with self.subTest(
rows=rows, invariant=invariant, deterministic=deterministic
):
with (
mock.patch(
"sglang.srt.batch_invariant_ops.is_batch_invariant_mode_enabled",
return_value=invariant,
),
mock.patch(
"sglang.srt.runtime_context.get_exec",
return_value=SimpleNamespace(
deterministic=SimpleNamespace(
enable_deterministic_inference=deterministic
)
),
),
):
method.apply(layer, torch.empty(rows, 128, device="cuda"))
self.assertEqual(call.call_args.kwargs.get("pin_tactic"), expected)
def test_prefill_tuning_leaves_decode_bit_identical(self):
"""Tuning the prefill buckets must not move the decode tactic: below the
stamped min_tokens the output has to stay bit-for-bit what it was."""
from flashinfer.autotuner import autotune
from sglang.srt.models.deepseek_v4 import DeepseekV4ForCausalLM
runtime_patch = mock.patch(
"sglang.srt.runtime_context.get_exec",
return_value=SimpleNamespace(
deterministic=SimpleNamespace(enable_deterministic_inference=False)
),
)
runtime_patch.start()
self.addCleanup(runtime_patch.stop)
layer = self._ready_layer(1792, 5120)
method = layer.quant_method
x = torch.randn(6, 5120, device="cuda", dtype=torch.bfloat16)
original = method.apply(layer, x)
model = SimpleNamespace(
config=SimpleNamespace(model_type="deepseek_v41"),
model=torch.nn.ModuleList([layer]),
)
with autotune(True):
DeepseekV4ForCausalLM.autotune_prefill_kernels(
model, 4096, dtype=torch.bfloat16
)
self.assertEqual(method.mxfp8_prefill_autotune_min_tokens, 4096)
torch.testing.assert_close(method.apply(layer, x), original, rtol=0, atol=0)
@unittest.skipIf(get_device_sm() < 90, "FP8 GEMM backends require SM90+")
class TestModeloptFp8PerTensorLinear(_LinearBackendCheck):
"""Per-tensor FP8 (ModelOptFp8LinearMethod, static scales) on the auto
@@ -0,0 +1,114 @@
"""A TP-sharded MXFP4 trtllm-gen MoE whose per-rank intermediate size needs
padding must sum to the unsharded experts' output."""
import unittest
from contextlib import nullcontext
from types import SimpleNamespace
from unittest.mock import Mock, patch
import torch
from sglang.srt.layers.moe.token_dispatcher import StandardDispatchOutput
from sglang.srt.layers.moe.topk import StandardTopKOutput
from sglang.srt.layers.quantization import mxfp4_flashinfer_trtllm_moe as mxfp4
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=60, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
def make_layer(weights):
layer = torch.nn.Module()
names = (
"w13_weight",
"w2_weight",
"w13_weight_scale_inv",
"w2_weight_scale_inv",
)
for name, tensor in zip(names, weights):
layer.register_parameter(name, torch.nn.Parameter(tensor, requires_grad=False))
layer.num_experts = weights[0].shape[0]
layer.num_local_experts = layer.num_experts
layer.moe_ep_rank = 0
return layer
def make_weights(intermediate, hidden=256, device="cpu"):
experts = 8
def fp4_packed(*shape):
return torch.randint(-128, 128, shape, dtype=torch.int8, device=device)
def e8m0_scales(*shape):
return torch.randint(-6, -3, shape, device=device).float().exp2()
return (
fp4_packed(experts, 2 * intermediate, hidden // 2),
fp4_packed(experts, hidden, intermediate // 2),
e8m0_scales(experts, 2 * intermediate, hidden // 32),
e8m0_scales(experts, hidden, intermediate // 32),
)
class TestMxfp4TrtllmPadding(CustomTestCase):
@unittest.skipUnless(
torch.cuda.is_available() and torch.cuda.get_device_capability()[0] == 10,
"Requires Blackwell",
)
def test_tp4_matches_unsharded_experts(self):
torch.manual_seed(42)
weights = make_weights(2304, hidden=5120, device="cuda")
def prepare(tensors):
layer = make_layer(tensors)
method = object.__new__(mxfp4.Mxfp4FlashinferTrtllmMoEMethod)
method._fp8 = Mock()
method.prefix = "test.experts"
method.flashinfer_mxfp4_moe_precision = "default"
method.process_weights_after_loading(layer)
method.create_moe_runner(layer, SimpleNamespace(swiglu_limit=10.0))
return method, layer
full = prepare([tensor.clone() for tensor in weights])
shards = []
for rank in range(4):
start = rank * 576
end = start + 576
w13, w2, s13, s2 = weights
shard = (
torch.cat(
(w13[:, start:end], w13[:, 2304 + start : 2304 + end]), dim=1
),
w2[..., start // 2 : end // 2].contiguous(),
torch.cat(
(s13[:, start:end], s13[:, 2304 + start : 2304 + end]), dim=1
),
s2[..., start // 32 : end // 32].contiguous(),
)
shards.append(prepare(shard))
with (
patch.object(mxfp4, "get_tp_group", return_value=None),
patch.object(mxfp4, "is_allocation_symmetric", return_value=False),
patch.object(mxfp4, "use_symmetric_memory", return_value=nullcontext()),
):
for tokens in (1, 64):
with self.subTest(tokens=tokens):
x = torch.randn(tokens, 5120, dtype=torch.bfloat16, device="cuda")
logits = torch.randn(tokens, 8, device="cuda")
scores, ids = logits.softmax(-1).topk(6, dim=-1)
topk = StandardTopKOutput(scores, ids.to(torch.int32), logits)
dispatch = StandardDispatchOutput(x, None, topk)
reference = full[0].apply(full[1], dispatch).hidden_states.float()
actual = sum(
method.apply(layer, dispatch).hidden_states.float()
for method, layer in shards
)
rmse = torch.linalg.norm(actual - reference) / torch.linalg.norm(
reference
)
self.assertLess(rmse.item(), 0.01)
if __name__ == "__main__":
unittest.main()
@@ -531,5 +531,26 @@ class TestDSV4NonPagedIndexer(CustomTestCase):
self.assertEqual(call.kwargs, {"clean_logits": False, "max_seqlen_k": 128})
class TestCandidateIndexerGating(CustomTestCase):
def test_candidate_indexer_gating(self):
from sglang.srt.layers.attention.dsv4 import candidate_indexer
def platform(sm):
return patch.object(
candidate_indexer, "get_platform", lambda: SimpleNamespace(device_sm=sm)
)
flag = "sglang.srt.layers.deep_gemm_wrapper.configurer.DEEPGEMM_PAGED_SPARSE_MQA_LOGITS"
# V4 models have no candidate source; Hopper selects through masks inline.
with platform(100), patch(flag, True):
self.assertIsNone(candidate_indexer.make_candidate_indexer(0, 8))
with platform(90), patch(flag, False):
self.assertIsNone(candidate_indexer.make_candidate_indexer(2048, 8))
# Blackwell without DeepGEMM's sparse logits fails instead of falling back.
with platform(100), patch(flag, False):
with self.assertRaises(RuntimeError):
candidate_indexer.make_candidate_indexer(2048, 8)
if __name__ == "__main__":
unittest.main()
@@ -529,6 +529,8 @@ class TestDecodePrebuilt(unittest.TestCase):
scheduler.policy = MagicMock()
scheduler.schedule_stream = MagicMock()
scheduler.forward_stream = MagicMock()
scheduler.ngram_embedding_manager = MagicMock()
scheduler.chunked_req = None
return scheduler
def test_waiting_queue_is_sorted_before_prebuilt_selection(self):
@@ -5,11 +5,17 @@ from unittest.mock import MagicMock, patch
import torch
from sglang.kernels.ops.attention.dsv4.kv_layout import (
KVLayout,
is_valid_kv_layout_pair,
)
from sglang.srt.mem_cache.allocator.swa import SWATokenToKVPoolAllocator
from sglang.srt.mem_cache.deepseek_v4_memory_pool import (
DeepSeekV4SingleKVPool,
DeepSeekV4TokenToKVPool,
_CompressedPoolConfig,
)
from sglang.srt.runtime_context import get_context
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
@@ -25,6 +31,8 @@ class TestDSV4CompressedPools(CustomTestCase):
pool = DeepSeekV4TokenToKVPool.__new__(DeepSeekV4TokenToKVPool)
pool._unified_kv = unified
pool.uniform_fp8 = False
pool.kv_layout = KVLayout.V4
pool.compressed_kv_layout_option = None
pool.compressed_pool_configs = {
4: _CompressedPoolConfig(
256, 64, torch.bfloat16, indexer_size=1024
@@ -176,5 +184,151 @@ class TestDSV4CompressedPools(CustomTestCase):
pool.get_index_k_page_size(128)
HEAD_DIM = 512
ROPE_DIM = 64
PAGE_SIZE = 256
FULL_SIZE = 4 * PAGE_SIZE
class TestV41KVPoolLayouts(CustomTestCase):
"""A V4.1-layout pool hands the attention kernel page-aligned buffers and
picks the compressed layout each ratio asks for."""
def setUp(self):
super().setUp()
override = get_context().override_server_args(page_size=PAGE_SIZE)
override.install()
self.addCleanup(override.restore)
def make_pool(self, ratios, kv_source_layers, kv_layout, compressed=None, **sizes):
return DeepSeekV4TokenToKVPool(
max_num_reqs=16,
swa_size=FULL_SIZE,
c4_size=sizes.get("c4_size", 0),
c128_size=sizes.get("c128_size", 0),
c4_state_pool_size=sizes.get("c4_state_pool_size", 0),
c128_state_pool_size=sizes.get("c128_state_pool_size", 0),
page_size=PAGE_SIZE,
swa_page_size=PAGE_SIZE,
dtype=torch.float8_e4m3fn,
c4_state_dtype=torch.float32,
c128_state_dtype=torch.float32,
qk_nope_head_dim=HEAD_DIM - ROPE_DIM,
qk_rope_head_dim=ROPE_DIM,
indexer_head_dim=128,
layer_num=len(ratios),
device="cpu",
enable_memory_saver=False,
compression_ratios=ratios,
kv_source_layers=kv_source_layers,
full_size=FULL_SIZE,
kv_layout=kv_layout,
compressed_kv_layout=compressed,
)
def assert_kernel_requirements(self, pool, layout):
"""Pages start on the kernel's alignment, and its
(num_pages, page_size, 1, bytes_per_token) view walks one token per row."""
for buf in pool.kv_buffer:
self.assertEqual(buf.stride(0) % layout.page_align, 0)
bpt = layout.bytes_per_token
view = buf[:, : pool.page_size * bpt].view(
buf.shape[0], pool.page_size, 1, bpt
)
self.assertEqual(view.stride(1), bpt)
self.assertEqual(view.stride(0), pool.bytes_per_page_padded)
def test_v41_pool_buffers(self):
for option, expect in ((None, KVLayout.V41_FP4), ("fp8", KVLayout.V41)):
with self.subTest(compressed=option):
pool = self.make_pool([0, 0, 2, 1, 1], [2, 3], KVLayout.V41, option)
self.assert_kernel_requirements(pool.swa_kv_pool, KVLayout.V41)
self.assertEqual(pool.get_swa_key_bytes_per_token(), 528)
for ratio in (1, 2):
layer_id = pool.sources_by_ratio[ratio][0]
self.assertIs(pool.get_extra_key_layout(layer_id), expect)
self.assertEqual(
pool.get_extra_key_bytes_per_token(layer_id),
expect.bytes_per_token,
)
self.assertTrue(is_valid_kv_layout_pair(pool.kv_layout, expect))
self.assert_kernel_requirements(pool.kv_pools[ratio], expect)
# A pool of the fp4 layout cannot be the main cache.
with self.assertRaises(AssertionError):
self.make_pool([0], [], KVLayout.V41_FP4)
def test_v41_pool_with_c4_c128(self):
pool = self.make_pool(
[0, 4, 128],
[],
KVLayout.V41,
c4_size=PAGE_SIZE,
c128_size=PAGE_SIZE,
c4_state_pool_size=16,
c128_state_pool_size=16,
)
for ratio in (4, 128):
self.assertEqual(pool.kv_pools[ratio].page_size, PAGE_SIZE // ratio)
# The 2-token c128 page is the only production page that pads.
self.assertEqual(pool.kv_pools[128].bytes_per_page_padded, 1536)
class TestPagedDSparkWithEncoderReplay(CustomTestCase):
def setUp(self):
super().setUp()
override = get_context().override_server_args(
enable_encoder_swa_bounded_replay=True,
speculative_algorithm="DSPARK",
speculative_num_draft_tokens=6,
speculative_dspark_block_size=5,
page_size=256,
max_running_requests=2,
chunked_prefill_size=256,
)
override.install()
self.addCleanup(override.restore)
def make_pool(self, *, draft):
return DeepSeekV4TokenToKVPool(
max_num_reqs=2,
num_req_slots=3,
swa_size=1024,
c4_size=0,
c128_size=0,
c4_state_pool_size=0,
c128_state_pool_size=0,
page_size=256,
swa_page_size=256,
dtype=torch.float8_e4m3fn,
c4_state_dtype=torch.float32,
c128_state_dtype=torch.bfloat16,
qk_nope_head_dim=448,
qk_rope_head_dim=64,
indexer_head_dim=128,
layer_num=3,
device="cpu",
enable_memory_saver=False,
compression_ratios=[0, 0, 0],
online_mtp_max_draft_tokens=6,
full_size=2048,
is_draft_worker=draft,
)
def test_target_window_and_draft_paged_storage_share_allocator_mapping(self):
target = self.make_pool(draft=False)
draft = self.make_pool(draft=True)
allocator = SWATokenToKVPoolAllocator(
2048, 1024, 256, torch.float8_e4m3fn, "cpu", target, False
)
draft.register_mapping(allocator.full_to_swa_index_mapping)
allocator.full_to_swa_index_mapping[256:512] = torch.arange(768, 1024)
self.assertEqual(
draft.translate_loc_from_full_to_swa(
torch.tensor([256, 300, 511])
).tolist(),
[768, 812, 1023],
)
if __name__ == "__main__":
unittest.main()
@@ -48,6 +48,7 @@ class TestUnifiedRadixHiCacheDispatch(unittest.TestCase):
)
kvcache = _mock_kvcache(DeepSeekV4TokenToKVPool)
kvcache.swa_kv_pool = MagicMock()
strategy = _select_strategy(kvcache, {FULL, SWA})
self.assertIsInstance(strategy, _DeepSeekV4Strategy)
@@ -141,6 +142,7 @@ class TestUnifiedRadixHiCacheDispatch(unittest.TestCase):
for cls in (SWAKVPool, DeepSeekV4TokenToKVPool):
kvcache = _mock_kvcache(cls)
kvcache.swa_kv_pool = MagicMock()
with self.assertRaises(AssertionError) as cm:
_select_strategy(kvcache, {FULL})
self.assertIn("No matching HiCache strategy", str(cm.exception))
@@ -6,9 +6,10 @@ reduction holds only if ranks also enter tuning with the same cache, so these
cover that gate and the digest it decides on.
"""
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.ci.ci_register import register_cpu_ci, register_cuda_ci
register_cpu_ci(est_time=52, suite="base-a-test-cpu")
register_cpu_ci(est_time=57, suite="base-a-test-cpu")
register_cuda_ci(est_time=25, stage="base-b-kernel-unit", runner_config="1-gpu-large")
import json
import multiprocessing
@@ -16,11 +17,15 @@ import os
import tempfile
import traceback
import unittest
from contextlib import nullcontext
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
import torch
import torch.distributed as dist
from sglang.srt.model_executor.runner import flashinfer_autotune as autotune
from sglang.srt.model_executor.runner.flashinfer_autotune import (
_autotune_cache_digest,
_autotune_tactic_sync_group,
@@ -160,5 +165,113 @@ class TestDropDivergedAutotuneCache(CustomTestCase):
)
class TestModelPrefillAutotune(CustomTestCase):
"""Model kernel warmup must cover prefill without a speculative dummy batch."""
def setUp(self):
self.hook = Mock(return_value=1)
self.mr = SimpleNamespace(
model=SimpleNamespace(autotune_prefill_kernels=self.hook),
is_generation=True,
is_draft_worker=False,
dtype=torch.bfloat16,
)
self.runner = SimpleNamespace(model_runner=self.mr)
# No dummy-buffer or attention APIs: this path must not build a
# TARGET_VERIFY batch or mutate request/KV state.
for target, kwargs in (
("max_prefill_buffer_tokens", {"return_value": 65536}),
(
"flashinfer_autotune_context",
{"side_effect": lambda *a, **k: nullcontext()},
),
):
p = patch.object(autotune, target, **kwargs)
setattr(self, target, p.start())
self.addCleanup(p.stop)
p = patch.object(
autotune.envs.SGLANG_FLASHINFER_AUTOTUNE_EXTEND, "get", return_value=False
)
p.start()
self.addCleanup(p.stop)
def test_declining_model_never_enters_the_autotune_context(self):
self.mr.model.wants_prefill_autotune = lambda: False
autotune.maybe_flashinfer_autotune_extend(self.runner, decode_num_tokens=384)
self.hook.assert_not_called()
self.flashinfer_autotune_context.assert_not_called()
def test_extend_pass_is_opt_in(self):
# A draft worker keeps its own warmup; a model without the hook opts out.
for draft, has_hook in ((True, True), (False, False)):
with self.subTest(draft=draft, has_hook=has_hook):
self.mr.is_draft_worker = draft
if not has_hook:
del self.mr.model.autotune_prefill_kernels
autotune.maybe_flashinfer_autotune_extend(
self.runner, decode_num_tokens=384
)
self.hook.assert_not_called()
self.flashinfer_autotune_context.assert_not_called()
@unittest.skipUnless(torch.cuda.is_available(), "FlashInfer requires CUDA")
class TestAutotuneCachePhases(CustomTestCase):
"""Loaded target tactics survive draft warmup, unless cache reuse is off."""
def test_target_and_draft_cache_reuse(self):
from flashinfer.autotuner import AutoTuner, _collect_metadata
tuner = AutoTuner.get()
tuner.clear_cache()
self.addCleanup(tuner.clear_cache)
runner = SimpleNamespace(
device="cuda",
forward_stream=torch.cuda.Stream(),
tp_group=SimpleNamespace(world_size=1),
)
with tempfile.TemporaryDirectory() as directory:
target, draft = (
Path(directory) / name for name in ("target.json", "draft.json")
)
for path, key, tactic in (
(target, "target_prefill", 7),
(draft, "draft_decode", 3),
):
path.write_text(
json.dumps(
{"_metadata": _collect_metadata(), key: ["TestRunner", tactic]}
)
)
with (
patch.object(
autotune,
"flashinfer_autotune_cache_path",
side_effect=[target, draft, draft],
),
patch.object(
autotune, "get_flashinfer_autotune_skip_ops", return_value=set()
),
autotune.envs.SGLANG_FLASHINFER_AUTOTUNE_CACHE.override(True),
):
with autotune.flashinfer_autotune_context(runner, run_lm_head=False):
self.assertEqual(
tuner._file_configs["target_prefill"], ("TestRunner", 7)
)
# No profiling: this models a restart that loads tactics from disk.
self.assertFalse(tuner.profiling_cache)
with autotune.flashinfer_autotune_context(runner, run_lm_head=False):
pass
saved = json.loads(draft.read_text())
self.assertEqual(saved["target_prefill"], ["TestRunner", 7])
self.assertEqual(saved["draft_decode"], ["TestRunner", 3])
with (
autotune.envs.SGLANG_FLASHINFER_AUTOTUNE_CACHE.override(False),
autotune.flashinfer_autotune_context(runner, run_lm_head=False),
):
self.assertNotIn("target_prefill", tuner._file_configs)
self.assertNotIn("draft_decode", tuner._file_configs)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,61 @@
"""Weight-update entry points refuse a model carrying compensated-mHC derived
weight caches before any weight is written."""
import unittest
from types import SimpleNamespace
from unittest.mock import Mock, patch
import torch
from sglang.srt.model_executor.model_runner_components.weight_updater import (
WeightUpdater,
_unsupported_derived_weight_cache_error,
)
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
class TestCompensatedMhcUpdateGuard(CustomTestCase):
def test_all_update_entries_reject_before_writes(self):
for field in ("_hc_attn_tf32_parts", "_hc_ffn_tf32_parts"):
for method, args in (
("update_weights_from_tensor", ([], "direct")),
("update_weights_from_distributed", ([], [], [], "unused")),
("update_weights_from_disk", ("unused", "auto")),
("update_weights_from_ipc", (SimpleNamespace(),)),
):
with self.subTest(field=field, method=method, args=args):
model = torch.nn.Sequential(torch.nn.Linear(1, 1))
original = model[0].weight.detach().clone()
setattr(model[0], field, (torch.ones(1), torch.zeros(1)))
model.load_weights = Mock()
updater = SimpleNamespace(
get_model=lambda: model, _assert_weight_cache_inactive=Mock()
)
with patch(
"sglang.srt.model_executor.model_runner_components.weight_updater.default_weight_loader"
) as loader:
ok, message = getattr(WeightUpdater, method)(updater, *args)
self.assertFalse(ok)
self.assertIn("compensated mHC", message)
loader.assert_not_called()
model.load_weights.assert_not_called()
torch.testing.assert_close(
model[0].weight, original, rtol=0, atol=0
)
def test_models_without_derived_splits_keep_update_support(self):
model = torch.nn.Sequential(torch.nn.Linear(1, 1))
model[0]._hc_attn_tf32_parts = model[0]._hc_ffn_tf32_parts = None
with patch(
"sglang.kernels.ops.attention.dsv4.gemm.hpc_bf16xfp32_gemm_enabled",
return_value=False,
):
self.assertIsNone(_unsupported_derived_weight_cache_error(model))
self.assertIsNone(_unsupported_derived_weight_cache_error())
if __name__ == "__main__":
unittest.main()
@@ -1152,6 +1152,9 @@ class TestSWAPoolFloor(CustomTestCase):
cfg.c4_ring_size = 8
cfg.c4_shrink_factor = 1
cfg._unified = unified
cfg.operator_swa_ratio = None
cfg.swa_cap_tokens = None
cfg.swa_prefix_tails = 0
return cfg._compute_dsv4_sizes(max_tokens, page_size)
def test_dsv4_rejects_single_page_pool(self):
@@ -1214,6 +1217,11 @@ class TestSWAPoolFloor(CustomTestCase):
cfg.disaggregation_mode = None
cfg.disaggregation_decode_extra_slots = 0
cfg._unified = True
cfg.operator_swa_ratio = None
cfg.swa_cap_tokens = None
cfg.swa_prefix_tails = 0
cfg.request_window_bytes = 0
cfg.bytes_per_swa_token = 0.0
cfg._unified_fp8 = False
# object.__new__ skips __init__; bf16 unified row is 2B * latent
cfg._unified_row_bytes = cfg.attn_head_dim * 2
@@ -1231,6 +1239,50 @@ class TestSWAPoolFloor(CustomTestCase):
+ cfg._get_c128_state_fixed_bytes(max_running_requests)
)
def test_dsv4_paged_dspark_budget_reserves_window_and_draft_layers(self):
from sglang.srt.model_executor.pool_configurator import DSV4PoolConfigurator
_publish_config(
self,
enable_encoder_swa_bounded_replay=True,
speculative_algorithm="DSPARK",
speculative_num_draft_tokens=6,
speculative_dspark_block_size=5,
page_size=256,
max_running_requests=2,
chunked_prefill_size=256,
)
cfg = SimpleNamespace(
qk_nope_head_dim=448,
qk_rope_head_dim=64,
index_head_dim=128,
context_len=131072,
compress_ratios=[0, 0] + [2] * 18 + [1] * 20,
window_size=128,
hf_config=SimpleNamespace(kv_source_layer_ids=[2, 8, 14, 20]),
)
spec = SimpleNamespace(is_dspark=lambda: True, is_none=lambda: False)
kvc = SimpleNamespace(
kv_cache_dtype_str="fp8_e4m3",
model_config=cfg,
layer_info=SimpleNamespace(start_layer=0, end_layer=40),
ps=SimpleNamespace(pp_size=1, attn_dp_size=1),
sliding_window_size=128,
page_size=256,
spec_algorithm=spec,
spec_aux_config=SimpleNamespace(dflash_draft_num_layers=3),
)
planner = DSV4PoolConfigurator(kvc)
self.assertEqual(planner.bytes_per_swa_token, 3 * 584)
budget = 256 * 1024 * 1024
sizes = planner.calculate_pool_sizes(budget, 256)
self.assertEqual(sizes.swa_max_total_num_tokens, planner.swa_cap_tokens)
self.assertLessEqual(
sizes.full_max_total_num_tokens * planner.bytes_per_full_token
+ planner._get_swa_fixed_bytes(),
budget,
)
def test_dsv4_unified_c4_state_not_token_scaled(self):
# Unified-KV sizes the c4 state ring from max_running_requests in
# finalize_with_max_running_requests, so it must not scale here.
@@ -55,6 +55,7 @@ class TestDeepseekV4RoPEPolicy(CustomTestCase):
o_lora_rank=8,
rms_norm_eps=1e-6,
compress_ratios=[compress_ratio],
q_head_norm=True,
rope_theta=10_000,
compress_rope_theta=160_000,
max_position_embeddings=128,
@@ -57,6 +57,10 @@ class TestDeepseekV4SharedExpertFusionPolicy(CustomTestCase):
quantization_config={},
rope_scaling={},
compress_ratios=[],
kv_source_layer_ids=[],
index_source_layer_ids=[],
engram_layer_ids=[],
engram_num_embeddings=[],
n_shared_experts=1,
dspark_markov_rank=1,
num_nextn_predict_layers=1,
@@ -88,7 +88,7 @@ class _Harness(deepseek_v4.MQALayer):
dtype=torch.bfloat16,
)
)
self.wo_b = lambda value: (value, None)
self.wo_b = lambda value, skip_all_reduce=False: (value, None)
self.prepare_kwargs = None
def _forward_prepare(
@@ -1718,6 +1718,14 @@ class TestGoldenModelOverrides(_IsolatedPublish):
"swa_full_tokens_ratio",
_deepseek_v4_overrides(_args(swa_full_tokens_ratio=0.5), hf),
)
# V4.1 leaves the ratio unset (cap-mode SWA sizing).
hf41 = SimpleNamespace(
architectures=["DeepseekV4ForCausalLM"], model_type="deepseek_v41"
)
self.assertNotIn(
"swa_full_tokens_ratio",
_deepseek_v4_overrides(_args(fp8_gemm_runner_backend="triton"), hf41),
)
# An explicit user choice takes precedence over the model default.
self.assertNotIn(
"moe_runner_backend",