Merge branch 'main' into dsv41-pd

This commit is contained in:
2026-09-22 14:56:35 +08:00
795 changed files with 55262 additions and 8438 deletions
+2 -2
View File
@@ -3,11 +3,11 @@
This page covers principles and essentials: folder layout, how to run tests, registration, and suite selection. For complete references, see the skill guides:
- **Writing tests** — templates, fixtures, model selection, complete suite tables, checklist: [`.claude/skills/write-sglang-test/SKILL.md`](../.claude/skills/write-sglang-test/SKILL.md)
- **CI pipeline internals** — stage flow diagrams, fast-fail layers, gating, partitioning, execution modes, debugging failures: [`.claude/skills/ci-workflow-guide/SKILL.md`](../.claude/skills/ci-workflow-guide/SKILL.md)
- **CI pipeline internals** — stage flow diagrams, fail-fast layers, gating, partitioning, execution modes, debugging failures: [`.claude/skills/ci-workflow-guide/SKILL.md`](../.claude/skills/ci-workflow-guide/SKILL.md)
## CI Pipeline Overview
The CI pipeline runs in three sequential stages: **A** (pre-flight, ~3 min) → **B** (basic, ~30 min) → **C** (advanced, ~30 min). Kernel and multimodal-gen tests run in parallel with stage B. For details on stage gating, fast-fail mechanisms, execution modes (PR vs scheduled vs manual dispatch), and debugging CI failures, see the [CI workflow guide](../.claude/skills/ci-workflow-guide/SKILL.md).
The CI pipeline runs in three sequential stages: **A** (pre-flight, ~3 min) → **B** (basic, ~30 min) → **C** (advanced, ~30 min). Kernel and multimodal-gen tests run in parallel with stage B. For details on stage gating, fail-fast mechanisms, execution modes (PR vs scheduled vs manual dispatch), and debugging CI failures, see the [CI workflow guide](../.claude/skills/ci-workflow-guide/SKILL.md).
## Folder Organization
+3 -4
View File
@@ -14,7 +14,7 @@ from sglang.srt.layers.moe.token_dispatcher.flashinfer import FlashinferDispatch
from sglang.srt.layers.moe.utils import initialize_moe_config
from sglang.srt.runtime_context import get_context, publish
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
from sglang.test.test_utils import CustomTestCase
from sglang.test.test_utils import CustomTestCase, publish_build_topology
class TestFlashinferDispatcher(CustomTestCase):
@@ -44,9 +44,8 @@ class TestFlashinferDispatcher(CustomTestCase):
publish(server_args, role="scheduler")
initialize_moe_config()
initialize_model_parallel(
tensor_model_parallel_size=world_size, expert_model_parallel_size=world_size
)
publish_build_topology(tp_size=world_size, ep_size=world_size, world_rank=rank)
initialize_model_parallel()
@classmethod
def tearDownClass(cls):
@@ -0,0 +1,36 @@
"""KV-canary end-to-end on Intel XPU with pipeline parallelism.
``--pp 2`` routes the run through ``Qwen3ForCausalLM.set_embed_and_head`` (mha mode
is Qwen/Qwen3-0.6B), the embedding/head handoff that syncs and releases the device
cache. Needs two XPU cards, so it is manual until a 2-card lane is confirmed.
"""
from __future__ import annotations
import unittest
from sglang.srt.kv_canary.config import CanaryMode
from sglang.test.kv_canary.e2e_base import CanaryE2EBase
class TestXPUCanaryPipelineParallel(CanaryE2EBase):
"""Clean canary run across a pipeline-parallel XPU pair."""
model_mode = "mha"
kv_canary_mode = CanaryMode.LOG
# --disable-cuda-graph is mandatory, not tuning: install_canary refuses a captured decode
# on a device that routes to the torch reference (host work and D2H, so replay checks nothing).
extra_server_args = ("--device", "xpu", "--disable-cuda-graph", "--pp", "2")
# The torch reference folds the chain slot-by-slot on the host, so the workload is much
# smaller than the CUDA-tuned defaults on the shared base.
default_parallel_n = 2
default_max_new_tokens = 32
default_request_timeout = 120.0
def test_no_violation(self) -> None:
self.send_parallel_requests()
self.assert_no_violation(wait_seconds=2.0)
if __name__ == "__main__":
unittest.main()
@@ -45,7 +45,6 @@ def test_mooncake_te_condition(server_args: ServerArgs) -> bool:
"""
Test the condition logic for using MooncakeTransferEngine.
"""
from sglang.srt.model_executor.model_runner import ModelRunner
dummy_runner = SimpleNamespace(server_args=server_args, gpu_id=0)
init_called = False
@@ -69,7 +68,11 @@ def test_mooncake_te_condition(server_args: ServerArgs) -> bool:
return_value="127.0.0.1",
),
):
ModelRunner.init_shared_mooncake_transfer_engine(dummy_runner)
from sglang.srt.distributed.device_communicators.mooncake_transfer_engine import (
maybe_init_shared_mooncake_transfer_engine,
)
maybe_init_shared_mooncake_transfer_engine(gpu_id=dummy_runner.gpu_id)
return init_called
+5 -3
View File
@@ -18,7 +18,7 @@ from sglang.srt.distributed.parallel_state import (
initialize_model_parallel,
)
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
from sglang.test.test_utils import CustomTestCase
from sglang.test.test_utils import CustomTestCase, publish_build_topology
def get_open_port() -> int:
@@ -98,7 +98,8 @@ class TestCustomAllReduce(CustomTestCase):
distributed_init_method=distributed_init_method,
local_rank=rank,
)
initialize_model_parallel(tensor_model_parallel_size=world_size)
publish_build_topology(tp_size=world_size, world_rank=rank)
initialize_model_parallel()
group = get_tensor_model_parallel_group().device_group
# Set global server args to avoid "Global server args is not set yet!" error
@@ -161,7 +162,8 @@ class TestCustomAllReduce(CustomTestCase):
distributed_init_method=distributed_init_method,
local_rank=rank,
)
initialize_model_parallel(tensor_model_parallel_size=world_size)
publish_build_topology(tp_size=world_size, world_rank=rank)
initialize_model_parallel()
group = get_tensor_model_parallel_group().device_group
# Set global server args to avoid "Global server args is not set yet!" error
+13 -4
View File
@@ -15,7 +15,7 @@ import torch
from sglang.benchmark.one_batch import TreeCacheNamespace
from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
from sglang.srt.distributed import bootstrap
from sglang.srt.managers.schedule_batch import Req, ScheduleBatch
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
from sglang.srt.model_executor.forward_context import (
@@ -23,7 +23,7 @@ from sglang.srt.model_executor.forward_context import (
set_forward_context,
)
from sglang.srt.model_executor.model_runner import ModelRunner
from sglang.srt.runtime_context import publish
from sglang.srt.runtime_context import SpawnRanks, publish
from sglang.srt.sampling.sampling_params import SamplingParams
from sglang.srt.server_args import PortArgs, ServerArgs
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
@@ -57,15 +57,24 @@ class TestForwardSplitPrefill(CustomTestCase):
cls.port_args = PortArgs.init_new(cls.server_args)
publish(cls.server_args, role="scheduler")
publish(
cls.server_args,
role="scheduler",
ranks=SpawnRanks(world_rank=0, gpu_id=0),
)
# Load model and tokenizer
cls.model_config = ModelConfig.from_server_args(cls.server_args)
bootstrap.init_parallel_runtime(
server_args=cls.server_args,
model_config=cls.model_config,
device=cls.device,
dist_port=cls.port_args.nccl_port,
)
cls.model_runner = ModelRunner(
model_config=cls.model_config,
mem_fraction_static=cls.server_args.mem_fraction_static,
gpu_id=0,
ps=ParallelState.trivial(tp_size=cls.tp_size),
nccl_port=cls.port_args.nccl_port,
server_args=cls.server_args,
)
+5 -3
View File
@@ -23,7 +23,7 @@ from sglang.srt.distributed.parallel_state import (
graph_capture,
initialize_model_parallel,
)
from sglang.test.test_utils import CustomTestCase
from sglang.test.test_utils import CustomTestCase, publish_build_topology
torch.manual_seed(42)
random.seed(44) # keep the deterministic seed
@@ -117,7 +117,8 @@ class TestQuickAllReduce(CustomTestCase):
distributed_init_method=distributed_init_method,
local_rank=rank,
)
initialize_model_parallel(tensor_model_parallel_size=world_size)
publish_build_topology(tp_size=world_size, world_rank=rank)
initialize_model_parallel()
group = get_tensor_model_parallel_group().device_group
# A small all_reduce for warmup.
@@ -186,7 +187,8 @@ class TestQuickAllReduce(CustomTestCase):
distributed_init_method=distributed_init_method,
local_rank=rank,
)
initialize_model_parallel(tensor_model_parallel_size=world_size)
publish_build_topology(tp_size=world_size, world_rank=rank)
initialize_model_parallel()
group = get_tensor_model_parallel_group().device_group
for sz in self.TEST_SIZES:
+15 -5
View File
@@ -9,7 +9,7 @@ import torch.nn.functional as F
from transformers import AutoModel, AutoProcessor, AutoTokenizer
from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
from sglang.srt.distributed import bootstrap
from sglang.srt.entrypoints.openai.protocol import ChatCompletionRequest
from sglang.srt.managers.mm_utils import embed_mm_inputs, init_mm_embedding_cache
from sglang.srt.managers.schedule_batch import (
@@ -20,7 +20,7 @@ from sglang.srt.managers.schedule_batch import (
from sglang.srt.model_executor.model_runner import ModelRunner
from sglang.srt.multimodal.processors.base_processor import BaseMultimodalProcessor
from sglang.srt.parser.conversation import generate_chat_conv
from sglang.srt.runtime_context import publish
from sglang.srt.runtime_context import SpawnRanks, get_device, publish
from sglang.srt.server_args import ServerArgs
from sglang.test.test_utils import download_image_with_retry
@@ -146,12 +146,22 @@ class VisionLLMLogitsBase(unittest.IsolatedAsyncioTestCase):
model_path=self.model_path,
disable_cuda_graph=True,
)
publish(server_args, role="scheduler")
publish(
server_args,
role="scheduler",
ranks=SpawnRanks(world_rank=0, gpu_id=0),
)
model_config = ModelConfig(self.model_path, model_override_args="{}")
bootstrap.init_parallel_runtime(
server_args=server_args,
model_config=model_config,
device=get_device().device,
dist_port=12435,
)
self.model_runner = ModelRunner(
model_config=ModelConfig(self.model_path, model_override_args="{}"),
model_config=model_config,
mem_fraction_static=0.8,
gpu_id=0,
ps=ParallelState.trivial(),
nccl_port=12435,
server_args=server_args,
)
+3 -1
View File
@@ -24,6 +24,7 @@ import unittest
import torch
from sglang.srt.environ import envs
from sglang.test.test_utils import publish_build_topology
MODEL = "Qwen/Qwen2-0.5B"
@@ -43,7 +44,8 @@ def _init_model_parallel() -> None:
local_rank=0,
distributed_init_method="tcp://127.0.0.1:29634",
)
initialize_model_parallel(tensor_model_parallel_size=1)
publish_build_topology(tp_size=1)
initialize_model_parallel()
monkey_patch_vllm_parallel_state()
except AssertionError:
pass
@@ -0,0 +1,127 @@
"""ROCm coverage for the decode-sized Qwen3.5 MoE softmax router."""
import unittest
from unittest.mock import patch
import torch
from sglang.srt.layers.moe import topk as topk_module
from sglang.srt.utils import is_gfx95_supported
from sglang.test.ci.ci_register import register_amd_ci
from sglang.test.test_utils import CustomTestCase
register_amd_ci(est_time=10, suite="stage-b-test-1-gpu-small-amd")
@unittest.skipUnless(
torch.cuda.is_available() and torch.version.hip and is_gfx95_supported(),
"requires AMD gfx95",
)
class TestQwen35MoeSoftmaxTopK(CustomTestCase):
def test_triton_dispatch_matches_aiter(self):
from aiter.fused_moe import fused_topk as aiter_fused_topk
for num_tokens in (4, 12, 128):
with self.subTest(num_tokens=num_tokens):
torch.manual_seed(num_tokens)
hidden_states = torch.randn(
num_tokens, 4096, device="cuda", dtype=torch.bfloat16
)
router_logits = torch.randn(
num_tokens, 512, device="cuda", dtype=torch.bfloat16
)
ref_weights = torch.empty(
num_tokens, 10, device="cuda", dtype=torch.float32
)
ref_ids = torch.empty(num_tokens, 10, device="cuda", dtype=torch.int32)
ref_weights, ref_ids = aiter_fused_topk(
hidden_states,
router_logits,
10,
True,
topk_ids=ref_ids,
topk_weights=ref_weights,
)
with (
patch.object(topk_module, "_use_aiter", True),
patch.object(topk_module, "_is_gfx95", True),
patch.object(
topk_module,
"aiter_fused_topk",
side_effect=AssertionError("AITER top-k should be bypassed"),
create=True,
),
):
weights, ids = topk_module.fused_topk(
hidden_states,
router_logits,
topk=10,
renormalize=True,
)
torch.testing.assert_close(ids, ref_ids, rtol=0, atol=0)
torch.testing.assert_close(weights, ref_weights, rtol=1e-5, atol=1e-6)
def test_dispatch_envelope_is_narrow(self):
hidden_states = torch.empty(128, 4096, device="cuda", dtype=torch.bfloat16)
logits = torch.empty(128, 512, device="cuda", dtype=torch.bfloat16)
packed = torch.empty(1, device="cuda")
with (
patch.object(topk_module, "_use_aiter", True),
patch.object(topk_module, "_is_gfx95", True),
):
self.assertTrue(
topk_module._use_rocm_triton_softmax_topk(
hidden_states, logits, 10, None, 0, None
)
)
self.assertFalse(
topk_module._use_rocm_triton_softmax_topk(
torch.empty(129, 4096, device="cuda", dtype=torch.bfloat16),
torch.empty(129, 512, device="cuda", dtype=torch.bfloat16),
10,
None,
0,
None,
)
)
self.assertFalse(
topk_module._use_rocm_triton_softmax_topk(
hidden_states, logits.float(), 10, None, 0, None
)
)
self.assertFalse(
topk_module._use_rocm_triton_softmax_topk(
hidden_states[:, :2048], logits, 10, None, 0, None
)
)
self.assertFalse(
topk_module._use_rocm_triton_softmax_topk(
hidden_states, logits, 8, None, 0, None
)
)
self.assertFalse(
topk_module._use_rocm_triton_softmax_topk(
hidden_states,
logits,
10,
torch.empty(512, device="cuda"),
0,
None,
)
)
self.assertFalse(
topk_module._use_rocm_triton_softmax_topk(
hidden_states, logits, 10, None, 1, None
)
)
self.assertFalse(
topk_module._use_rocm_triton_softmax_topk(
hidden_states, logits, 10, None, 0, packed
)
)
if __name__ == "__main__":
unittest.main()
@@ -137,6 +137,42 @@ class TestGdnReplayssmSpecFold(CustomTestCase):
)
self.assertTrue(torch.equal(out_plain, out_ring), f"{dtype=}")
def test_ring_write_accepts_strided_qkv_views(self):
inputs = _make_window(12)
packed_qkv = torch.cat(
[inputs[name].reshape(B * T, -1) for name in ("q", "k", "v")],
dim=-1,
)
q, k, v = packed_qkv.split([H * K, H * K, HV * V], dim=-1)
strided_qkv = {
"q": q.view(1, B * T, H, K),
"k": k.view(1, B * T, H, K),
"v": v.view(1, B * T, HV, V),
}
self.assertTrue(
all(not tensor.is_contiguous() for tensor in strided_qkv.values())
)
def run(qkv):
state = self._state(torch.float32).unsqueeze(0).contiguous()
rings = _make_rings()
output = _run_verify(
{**inputs, **qkv},
self.gating,
state[0],
self.slots,
rings=rings,
)
_fold(state, rings, self.slots, self.accept_lens)
return {"output": output, "state": state, **rings}
contiguous = run(
{name: tensor.contiguous() for name, tensor in strided_qkv.items()}
)
strided = run(strided_qkv)
for name in contiguous:
self.assertTrue(torch.equal(contiguous[name], strided[name]), name)
def test_fold_matches_snapshot_baseline(self):
for dtype in (torch.float32, torch.bfloat16):
state = self._state(dtype)
@@ -1,6 +1,6 @@
import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import torch
@@ -8,6 +8,7 @@ from sglang.srt.layers.attention.hybrid_linear_attn_backend import (
HybridLinearAttnBackend,
MambaAttnBackendBase,
)
from sglang.srt.layers.attention.linear import gdn_backend
from sglang.srt.model_executor.forward_batch_info import ForwardMode
from sglang.srt.utils import is_hip
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
@@ -316,6 +317,24 @@ class TestTritonGDNBackendCorrectness(CustomTestCase):
):
run_gdn_eagle_verify_case(self, case, topk=topk, spec_kind=spec_kind)
def test_triton_target_verify_skips_prefill_qkv_materialization(self):
case, topk, spec_kind = self.EAGLE_VERIFY_CASES[0]
with patch.object(
gdn_backend,
"fused_qkv_split_gdn_prefill",
side_effect=AssertionError,
):
run_gdn_eagle_verify_case(self, case, topk=topk, spec_kind=spec_kind)
def test_triton_prefill_keeps_contiguous_qkv_materialization(self):
with patch.object(
gdn_backend,
"fused_qkv_split_gdn_prefill",
wraps=gdn_backend.fused_qkv_split_gdn_prefill,
) as split_spy:
run_gdn_attention_case(self, self.CASES[0])
split_spy.assert_called()
def test_runner_mode_eagle_verify_cuda_graph_cases(self):
for case, topk, spec_kind in self.EAGLE_VERIFY_CUDA_GRAPH_CASES:
with self.subTest(
@@ -0,0 +1,60 @@
"""The only test in the tree that bounds speculative decoding LATENCY; every
other one bounds accept length. CUDA only -- AMD bounds are unmeasured.
"""
import unittest
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kits.perf_bench_kit import at_least, at_most, check_perf
from sglang.test.test_utils import (
DEFAULT_DRAFT_MODEL_EAGLE3,
DEFAULT_TARGET_MODEL_EAGLE3,
CustomTestCase,
run_bench_serving,
)
register_cuda_ci(est_time=145, stage="extra-a", runner_config="1-gpu-large")
class TestEagle3Latency(CustomTestCase):
def test_online_latency_eagle3(self):
res = run_bench_serving(
model=DEFAULT_TARGET_MODEL_EAGLE3,
num_prompts=300,
request_rate=8,
sharegpt_context_len=3072,
disable_ignore_eos=True,
dataset_name="sharegpt",
other_server_args=[
"--speculative-algorithm",
"EAGLE3",
"--speculative-draft-model-path",
DEFAULT_DRAFT_MODEL_EAGLE3,
"--speculative-num-steps",
"5",
"--speculative-eagle-topk",
"4",
"--speculative-num-draft-tokens",
"16",
"--mem-fraction-static",
"0.7",
# The draft checkpoint ships fp16 and the target bf16; the CUDA
# rmsnorm path rejects a weight and activation pair that disagree.
"--dtype",
"float16",
],
need_warmup=True,
seed=42,
)
check_perf(
self,
at_most(
"median_e2e_latency_ms", res["median_e2e_latency_ms"], 1150, unit="ms"
),
at_least("accept_length", res["accept_length"], 2.3),
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,61 @@
"""Latency and throughput of the /v1/embeddings endpoint."""
import unittest
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.kits.perf_bench_kit import (
at_least,
at_most,
check_batch_scaling,
check_perf,
)
from sglang.test.test_utils import (
DEFAULT_SMALL_EMBEDDING_MODEL_NAME_FOR_TEST,
CustomTestCase,
run_embeddings_benchmark,
run_embeddings_benchmark_multi,
)
register_cuda_ci(est_time=245, stage="extra-a", runner_config="1-gpu-large")
register_amd_ci(est_time=240, suite="stage-b-test-1-gpu-large-amd")
class TestEmbeddingsAPI(CustomTestCase):
def test_embeddings_api_latency_throughput(self):
res = run_embeddings_benchmark(
model=DEFAULT_SMALL_EMBEDDING_MODEL_NAME_FOR_TEST,
num_requests=1000,
batch_size=1,
input_tokens=500,
other_server_args=[],
need_warmup=True,
)
self.assertEqual(res["successful_requests"], res["total_requests"])
check_perf(
self,
at_most("avg_latency_ms", res["avg_latency_ms"], 23, amd=35, unit="ms"),
at_most("p95_latency_ms", res["p95_latency_ms"], 34, amd=40, unit="ms"),
at_least("throughput", res["throughput"], 45, amd=30, unit="req/s"),
)
def test_embeddings_api_batch_scaling(self):
check_batch_scaling(
self,
lambda batch_sizes: run_embeddings_benchmark_multi(
DEFAULT_SMALL_EMBEDDING_MODEL_NAME_FOR_TEST,
batch_sizes,
num_requests=500,
input_tokens=500,
),
# batch size, avg ms, p95 ms, then the same two relaxed for mi300x
[
(10, 44, 52, 80, 90),
(25, 72, 101, 140, 150),
(50, 126, 200, 230, 240),
],
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,134 @@
"""Latency of the LoRA serving path, with and without adapter churn."""
import asyncio
import itertools
import unittest
import requests
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.kits.perf_bench_kit import at_most, check_perf
from sglang.test.test_utils import (
DEFAULT_MODEL_NAME_FOR_TEST,
CustomTestCase,
run_bench_serving,
)
register_cuda_ci(est_time=490, stage="extra-a", runner_config="1-gpu-large")
register_amd_ci(est_time=430, suite="stage-b-test-1-gpu-large-amd")
class TestLoRALatency(CustomTestCase):
def test_online_lora_latency(self):
res = self._run_lora_latency_test(enable_background_task=False)
check_perf(
self,
at_most(
"median_e2e_latency_ms",
res["median_e2e_latency_ms"],
2270,
amd=3320,
unit="ms",
),
# mi300x is about twice as slow as mi325 on LoRA TTFT.
at_most("median_ttft_ms", res["median_ttft_ms"], 52, amd=100, unit="ms"),
)
def test_online_lora_latency_with_concurrent_adapter_updates(self):
res = self._run_lora_latency_test(enable_background_task=True)
check_perf(
self,
at_most(
"median_e2e_latency_ms",
res["median_e2e_latency_ms"],
3420,
amd=6000,
unit="ms",
),
at_most("median_ttft_ms", res["median_ttft_ms"], 55, amd=130, unit="ms"),
)
def _run_lora_latency_test(self, enable_background_task: bool):
async def lora_loader_unloader_task(
base_url: str,
start_event: asyncio.Event,
stop_event: asyncio.Event,
):
"""
A background task that repeatedly loads and unloads a LoRA adapter.
"""
await start_event.wait()
path_cycler = itertools.cycle(
[
"pbevan11/llama-3.1-8b-ocr-correction",
"faridlazuarda/valadapt-llama-3.1-8B-it-chinese",
"philschmid/code-llama-3-1-8b-text-to-sql-lora",
]
)
load_url = f"{base_url}/load_lora_adapter"
unload_url = f"{base_url}/unload_lora_adapter"
num_updates = 0
while not stop_event.is_set():
lora_path = next(path_cycler)
response = await asyncio.to_thread(
requests.post,
load_url,
json={"lora_name": lora_path, "lora_path": lora_path},
)
self.assertTrue(
response.ok, f"Failed to load LoRA adapter: {response.text}"
)
num_updates += 1
if stop_event.is_set():
break
await asyncio.sleep(1)
response = await asyncio.to_thread(
requests.post,
unload_url,
json={"lora_name": lora_path},
)
self.assertTrue(
response.ok, f"Failed to unload LoRA adapter: {response.text}"
)
num_updates += 1
await asyncio.sleep(1)
background_task = lora_loader_unloader_task if enable_background_task else None
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=400,
request_rate=8,
other_server_args=[
"--enable-lora",
"--max-loras-per-batch",
"1",
"--disable-radix-cache",
"--random-seed",
"42",
"--mem-fraction-static",
"0.8",
"--lora-paths",
"nvidia/llama-3.1-nemoguard-8b-topic-control",
"--max-lora-rank",
"256",
],
dataset_name="random",
random_input_len=256,
random_output_len=256,
lora_name=["nvidia/llama-3.1-nemoguard-8b-topic-control"],
background_task=background_task,
)
return res
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,53 @@
"""Throughput of the MoE model on two GPUs, batched and at batch size one."""
import unittest
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.kits.perf_bench_kit import at_least, check_perf
from sglang.test.test_utils import (
DEFAULT_MOE_MODEL_NAME_FOR_TEST,
CustomTestCase,
run_bench_offline_throughput,
run_bench_serving,
)
register_cuda_ci(est_time=290, stage="extra-a", runner_config="2-gpu-large")
register_amd_ci(est_time=770, suite="stage-b-test-2-gpu-large-amd")
class TestMoEThroughput(CustomTestCase):
def test_moe_offline_throughput_default(self):
res = run_bench_serving(
model=DEFAULT_MOE_MODEL_NAME_FOR_TEST,
num_prompts=300,
request_rate=float("inf"),
other_server_args=["--tp", "2"],
)
check_perf(
self,
at_least(
"output_throughput",
res["output_throughput"],
2660,
amd=2100,
unit="token/s",
),
)
def test_moe_tp2_bs1(self):
output_throughput = run_bench_offline_throughput(
DEFAULT_MOE_MODEL_NAME_FOR_TEST,
["--tp", "2", "--cuda-graph-max-bs-decode", "2"],
)
check_perf(
self,
at_least(
"output_throughput", output_throughput, 139, amd=85, unit="token/s"
),
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,70 @@
"""Throughput of pipeline parallelism on two GPUs, decode and long prefill."""
import unittest
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.kits.perf_bench_kit import at_least, check_perf
from sglang.test.test_utils import (
DEFAULT_MOE_MODEL_NAME_FOR_TEST,
CustomTestCase,
is_in_amd_ci,
run_bench_serving,
)
register_cuda_ci(est_time=490, stage="extra-a", runner_config="2-gpu-large")
register_amd_ci(est_time=1030, suite="stage-b-test-2-gpu-large-amd")
class TestPPThroughput(CustomTestCase):
def test_pp_offline_throughput_default_decode(self):
res = run_bench_serving(
model=DEFAULT_MOE_MODEL_NAME_FOR_TEST,
num_prompts=1000,
request_rate=float("inf"),
random_input_len=1,
random_output_len=1024,
other_server_args=["--pp-size", "2"],
need_warmup=True,
seed=42,
)
check_perf(
self,
at_least(
"output_throughput", res["output_throughput"], 6250, unit="token/s"
),
)
def test_pp_long_context_prefill(self):
res = run_bench_serving(
model="meta-llama/Llama-3.3-70B-Instruct",
num_prompts=4,
request_rate=float("inf"),
random_input_len=128000,
random_output_len=1,
dataset_name="random",
other_server_args=[
"--quantization",
"fp8",
"--pp-size",
"2",
]
+ (["--mem-fraction-static", "0.7"] if is_in_amd_ci() else []),
need_warmup=False,
seed=42,
)
check_perf(
self,
at_least(
"input_throughput",
res["input_throughput"],
4380,
amd=2190,
unit="token/s",
),
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,55 @@
"""Latency and throughput of the /v1/score endpoint."""
import unittest
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.kits.perf_bench_kit import (
at_least,
at_most,
check_batch_scaling,
check_perf,
)
from sglang.test.test_utils import (
DEFAULT_SMALL_MODEL_NAME_FOR_TEST_SCORE,
CustomTestCase,
run_score_benchmark,
run_score_benchmark_multi,
)
register_cuda_ci(est_time=215, stage="extra-a", runner_config="1-gpu-large")
register_amd_ci(est_time=210, suite="stage-b-test-1-gpu-large-amd")
class TestScoreAPI(CustomTestCase):
def test_score_api_latency_throughput(self):
res = run_score_benchmark(
model=DEFAULT_SMALL_MODEL_NAME_FOR_TEST_SCORE,
num_requests=1000,
batch_size=10,
other_server_args=[],
need_warmup=True,
)
self.assertEqual(res["successful_requests"], res["total_requests"])
check_perf(
self,
at_most("avg_latency_ms", res["avg_latency_ms"], 31, amd=60, unit="ms"),
at_most("p95_latency_ms", res["p95_latency_ms"], 37, amd=65, unit="ms"),
at_least("throughput", res["throughput"], 32, amd=16, unit="req/s"),
)
def test_score_api_batch_scaling(self):
check_batch_scaling(
self,
lambda batch_sizes: run_score_benchmark_multi(
DEFAULT_SMALL_MODEL_NAME_FOR_TEST_SCORE,
batch_sizes,
num_requests=500,
),
# batch size, avg ms, p95 ms, then the same two relaxed for mi300x
[(10, 32, 40, 60, 65), (25, 37, 42, 70, 80), (50, 54, 64, 80, 90)],
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,40 @@
"""Latency of the default serving path on one large GPU."""
import unittest
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.kits.perf_bench_kit import at_most, check_perf
from sglang.test.test_utils import (
DEFAULT_MODEL_NAME_FOR_TEST,
CustomTestCase,
run_bench_serving,
)
register_cuda_ci(est_time=190, stage="extra-a", runner_config="1-gpu-large")
register_amd_ci(est_time=165, suite="stage-b-test-1-gpu-large-amd")
class TestServingLatency(CustomTestCase):
def test_online_latency_default(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=100,
request_rate=1,
other_server_args=[],
)
check_perf(
self,
at_most(
"median_e2e_latency_ms",
res["median_e2e_latency_ms"],
9140,
unit="ms",
),
at_most("median_ttft_ms", res["median_ttft_ms"], 84, amd=115, unit="ms"),
at_most("median_itl_ms", res["median_itl_ms"], 9, unit="ms"),
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,107 @@
"""Offline throughput of the default serving path on one large GPU."""
import unittest
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.kits.perf_bench_kit import at_least, check_perf
from sglang.test.test_utils import (
DEFAULT_MODEL_NAME_FOR_TEST,
DEFAULT_MODEL_NAME_FOR_TEST_FP8,
CustomTestCase,
run_bench_serving,
)
register_cuda_ci(est_time=710, stage="extra-a", runner_config="1-gpu-large")
register_amd_ci(est_time=810, suite="stage-b-test-1-gpu-large-amd")
class TestServingThroughput(CustomTestCase):
def test_offline_throughput_default(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=500,
request_rate=float("inf"),
other_server_args=[],
)
check_perf(
self,
at_least(
"output_throughput",
res["output_throughput"],
4000,
amd=3050,
unit="token/s",
),
)
def test_offline_throughput_non_stream_small_batch_size(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=200,
request_rate=float("inf"),
other_server_args=["--max-running-requests", "10"],
dataset_name="sharegpt",
random_input_len=None,
random_output_len=None,
disable_stream=True,
need_warmup=True,
)
check_perf(
self,
at_least(
"output_throughput",
res["output_throughput"],
1100,
amd=1000,
unit="token/s",
),
)
def test_offline_throughput_with_triton_attention_backend(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=500,
request_rate=float("inf"),
other_server_args=[
"--attention-backend",
"triton",
"--context-length",
"8192",
],
)
check_perf(
self,
at_least(
"output_throughput",
res["output_throughput"],
3730,
amd=2700,
unit="token/s",
),
)
def test_offline_throughput_default_fp8(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST_FP8,
num_prompts=500,
request_rate=float("inf"),
other_server_args=[],
)
check_perf(
self,
at_least(
"output_throughput",
res["output_throughput"],
4860,
amd=3500,
unit="token/s",
),
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,33 @@
"""Throughput of torch.compile at batch size one across two GPUs."""
import unittest
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.kits.perf_bench_kit import at_least, check_perf
from sglang.test.test_utils import (
DEFAULT_MODEL_NAME_FOR_TEST,
CustomTestCase,
run_bench_offline_throughput,
)
register_cuda_ci(est_time=75, stage="extra-a", runner_config="2-gpu-large")
register_amd_ci(est_time=280, suite="stage-b-test-2-gpu-large-amd")
class TestTorchCompileThroughput(CustomTestCase):
def test_torch_compile_tp2_bs1(self):
output_throughput = run_bench_offline_throughput(
DEFAULT_MODEL_NAME_FOR_TEST,
["--tp", "2", "--enable-torch-compile", "--cuda-graph-max-bs-decode", "2"],
)
check_perf(
self,
at_least(
"output_throughput", output_throughput, 255, amd=200, unit="token/s"
),
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,25 @@
"""VLM serving perf on the aiter attention backend."""
import unittest
from sglang.test.ci.ci_register import register_amd_ci
from sglang.test.kits.vlm_perf_kit import check_vlm_serving_perf
from sglang.test.test_utils import CustomTestCase
register_amd_ci(est_time=300, suite="stage-b-test-1-gpu-small-amd")
class TestVLMServingAiter(CustomTestCase):
def test_vlm_serving_aiter(self):
check_vlm_serving_perf(
self,
"aiter",
output_throughput=2000,
e2e_ms=16500,
ttft_ms=150,
itl_ms=8,
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,26 @@
"""VLM serving perf on the fa3 attention backend."""
import unittest
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kits.vlm_perf_kit import check_vlm_serving_perf
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=150, stage="extra-a", runner_config="1-gpu-large")
class TestVLMServingFa3(CustomTestCase):
def test_vlm_serving_fa3(self):
check_vlm_serving_perf(
self,
"fa3",
# No offline bound: never measured on this lane.
output_throughput=15640,
e2e_ms=11000,
ttft_ms=100,
itl_ms=5.2,
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,25 @@
"""VLM serving perf on the flashinfer attention backend."""
import unittest
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kits.vlm_perf_kit import check_vlm_serving_perf
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=195, stage="extra-a", runner_config="1-gpu-small")
class TestVLMServingFlashinfer(CustomTestCase):
def test_vlm_serving_flashinfer(self):
check_vlm_serving_perf(
self,
"flashinfer",
output_throughput=5940,
e2e_ms=17480,
ttft_ms=83,
itl_ms=8.4,
)
if __name__ == "__main__":
unittest.main()
@@ -23,7 +23,11 @@ from sglang.srt.utils.rank_consensus_checker import (
shutdown,
)
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase, find_available_port
from sglang.test.test_utils import (
CustomTestCase,
find_available_port,
publish_build_topology,
)
register_cpu_ci(est_time=193, suite="stage-a-test-cpu-intel")
@@ -80,11 +84,8 @@ def run_distributed_test(
backend="gloo",
)
initialize_model_parallel(
tensor_model_parallel_size=tp_size,
pipeline_model_parallel_size=pp_size,
backend="gloo",
)
publish_build_topology(tp_size=tp_size, pp_size=pp_size, world_rank=rank)
initialize_model_parallel(backend="gloo")
fn()
except Exception as e:
+3 -1
View File
@@ -134,7 +134,9 @@ class TestFilterDcpLocalChunkKvIndices(CustomTestCase):
def test_identity_without_dcp(self):
kv = torch.arange(37)
with rc.get_parallel().override(dcp_enabled=False, dcp_size=1, dcp_rank=0):
with rc.get_parallel().override(
dcp_enabled=False, dcp_size=1, dcp_rank=0, attn_dcp_rank=0
):
self.assertIs(
filter_dcp_local_chunk_kv_indices(
kv, torch.tensor([0]), torch.tensor([37])
@@ -25,6 +25,7 @@ import torch
import torch.distributed as dist
import sglang.srt.distributed.parallel_state as ps
from sglang.srt.runtime_context import get_parallel
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(
@@ -130,6 +131,7 @@ def init_distributed():
local_rank=local_rank,
backend="nccl",
)
get_parallel().override_permanently(world_group=coord)
cpu_group = coord.cpu_group
nccl_group = coord.device_group
@@ -459,29 +459,11 @@ class TestDisaggregationPauseResumeDecodeRetract(PDDisaggregationServerBase):
self._run_pause_on_decode_running_batch("retract", weight_update=True)
)
async def _get_decode_num_running_reqs(self, session):
"""Query current decode running_batch size from /v1/loads."""
async with session.get(
self.decode_url + "/v1/loads?include=core",
timeout=aiohttp.ClientTimeout(total=5),
) as resp:
resp.raise_for_status()
body = await resp.json()
return sum(load["num_running_reqs"] for load in body["loads"])
async def _wait_for_decode_running_batch(self, session, timeout):
deadline = asyncio.get_running_loop().time() + timeout
while asyncio.get_running_loop().time() < deadline:
if await self._get_decode_num_running_reqs(session) > 0:
return
await asyncio.sleep(0.2)
self.fail("Timed out waiting for decode running_batch to become non-empty")
async def _run_pause_on_decode_running_batch(self, mode, weight_update=False):
num_requests = 2
max_new_tokens = 512
prompt = "Write a detailed numbered explanation of distributed inference. " * 12
decode_started = [asyncio.Event() for _ in range(num_requests)]
async def _post(session, url, json_data, timeout=30):
async with session.post(
@@ -493,20 +475,37 @@ class TestDisaggregationPauseResumeDecodeRetract(PDDisaggregationServerBase):
return await resp.json()
async def _generate(session, request_id):
return await _post(
session,
async with session.post(
self.lb_url + "/generate",
{
json={
"text": f"Request {request_id}: {prompt}",
"background": True,
"stream": True,
"sampling_params": {
"temperature": 0,
"ignore_eos": True,
"max_new_tokens": max_new_tokens,
},
},
timeout=180,
)
timeout=aiohttp.ClientTimeout(total=180),
) as resp:
resp.raise_for_status()
response = None
async for line in resp.content:
line = line.strip()
if not line.startswith(b"data: "):
continue
data = line[len(b"data: ") :]
if data == b"[DONE]":
break
response = json.loads(data)
self.assertNotIn("error", response)
# Prefill produces the first token. A later token proves this
# request has reached running_batch on the decode worker.
if response["meta_info"]["completion_tokens"] > 1:
decode_started[request_id].set()
self.assertIsNotNone(response, "Generation stream returned no output")
return response
async with aiohttp.ClientSession() as session:
tasks = [
@@ -515,12 +514,17 @@ class TestDisaggregationPauseResumeDecodeRetract(PDDisaggregationServerBase):
decode_paused = False
try:
await self._wait_for_decode_running_batch(session, timeout=30)
await asyncio.sleep(0.1)
# /v1/loads can still report a previous batch. Wait for every
# current request to decode so none can arrive in the prealloc
# queue after the pause and prevent the weight-update flush.
await asyncio.wait_for(
asyncio.gather(*(event.wait() for event in decode_started)),
timeout=30,
)
self.assertTrue(
any(not task.done() for task in tasks),
"All requests finished before decode retract pause was issued.",
all(not task.done() for task in tasks),
"A request finished before decode retract pause was issued.",
)
await _post(
@@ -580,6 +584,9 @@ class TestDisaggregationPauseResumeDecodeRetract(PDDisaggregationServerBase):
for response in responses:
self.assertIn("text", response)
self.assertGreater(len(response["text"]), 0)
self.assertEqual(
response["meta_info"]["completion_tokens"], max_new_tokens
)
self.assertGreater(
sum(
@@ -27,9 +27,10 @@ from sglang.srt.managers.schedule_batch import Req
from sglang.srt.mem_cache.allocator import TokenToKVPoolAllocator
from sglang.srt.mem_cache.base_prefix_cache import MatchPrefixParams
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
from sglang.srt.mem_cache.mamba_radix_cache import MambaRadixCache
from sglang.srt.mem_cache.memory_pool import HybridLinearKVPool, HybridReqToTokenPool
from sglang.srt.mem_cache.radix_cache import RadixKey
from sglang.srt.mem_cache.unified_cache.components.base import ComponentType
from sglang.srt.mem_cache.unified_radix_cache import UnifiedRadixCache
from sglang.srt.sampling.sampling_params import SamplingParams
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
from sglang.srt.utils import get_device
@@ -354,16 +355,8 @@ class TestOptimisticPrefillMambaAdmission(CustomTestCase):
class TestOptimisticPrefillMambaRetryRelease(CustomTestCase):
"""Optimistic retry cleanup must not treat the donated Mamba checkpoint
as a second donation.
Bug mechanism: the retry path first inserts the unfinished prefix, which
donates the tracked checkpoint and clears ``mamba_last_track_seqlen``.
Releasing with ``is_insert=True`` afterwards re-enters the donation path
with the cleared marker, inserting a zero-length radix entry that pins a
clone of the request's live state. Releasing with ``is_insert=False``
retains the donated prefix/checkpoint and frees only the uncached tail
and the request-owned Mamba buffers.
"""An optimistic-prefill retry leaves the donated prefix and exactly one
Mamba checkpoint in the tree -- not zero, and not a second pinned clone.
"""
SIZE = 128
@@ -373,7 +366,7 @@ class TestOptimisticPrefillMambaRetryRelease(CustomTestCase):
def _setup_mamba_tree(self):
server_args = ServerArgs(model_path="dummy", page_size=1)
# MambaRadixCache reads mamba_cache_chunk_size, whose property
# The mamba component reads mamba_cache_chunk_size, whose property
# otherwise loads the HF config for the dummy model.
server_args._mamba_cache_chunk_size = FLA_CHUNK_SIZE
set_global_server_args_for_scheduler(server_args)
@@ -427,13 +420,14 @@ class TestOptimisticPrefillMambaRetryRelease(CustomTestCase):
kvcache=pool,
need_sort=False,
)
tree = MambaRadixCache(
tree = UnifiedRadixCache(
params=CacheInitParams(
disable=False,
req_to_token_pool=req_to_token_pool,
token_to_kv_pool_allocator=allocator,
page_size=1,
enable_mamba_extra_buffer=True,
tree_components=(ComponentType.FULL, ComponentType.MAMBA),
)
)
return tree, allocator, req_to_token_pool
@@ -459,7 +453,7 @@ class TestOptimisticPrefillMambaRetryRelease(CustomTestCase):
req.kv.kv_committed_len = len(self.PROMPT)
req.kv.kv_allocated_len = len(self.PROMPT)
req.kv.mamba_last_track_seqlen = self.TRACK_SEQLEN
req.last_node = tree.root_node
req.last_node = tree.root_node_handle()
scheduler = SimpleNamespace(
tree_cache=tree,
@@ -478,13 +472,15 @@ class TestOptimisticPrefillMambaRetryRelease(CustomTestCase):
scheduler, req
)
# The donated prefix and exactly one checkpoint stay in the tree; the
# old double-donation path either asserts or pins a second state.
self.assertEqual(tree.total_size(), (self.TRACK_SEQLEN, 1))
match = tree.match_prefix(
MatchPrefixParams(key=RadixKey(array("q", self.PROMPT)))
)
self.assertIsNotNone(match.last_device_node.mamba_value)
self.assertIsNotNone(
tree.tree_core.get_component_device_value(
match.last_device_node, ComponentType.MAMBA
)
)
# Only the uncached tail and the request-owned Mamba buffers are
# freed; the tree keeps the donated checkpoint slot.
@@ -18,17 +18,16 @@ import unittest
import requests
import torch
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.ci.ci_register import register_xpu_ci
from sglang.test.server_fixtures.disaggregation_fixture import (
PDDisaggregationServerBase,
)
from sglang.test.test_utils import DEFAULT_SMALL_MODEL_NAME_FOR_TEST_QWEN
register_cuda_ci(
register_xpu_ci(
est_time=300,
stage="base-a",
runner_config="1-gpu-small",
disabled="Intel XPU only — not available in standard CUDA CI",
suite="stage-b-test-1-gpu-xpu",
disabled="XPU CI image does not include an XPU-compatible NIXL/UCX build",
)
_XPU_AVAILABLE = torch.xpu.is_available()
@@ -5,6 +5,7 @@ import subprocess
import threading
import time
import unittest
from concurrent.futures import ThreadPoolExecutor
import grpc
import openai
@@ -1522,23 +1523,28 @@ class TestEPDDisaggregationGrpcEncoderOnly(PDDisaggregationServerBase):
image_path = os.path.abspath("examples/assets/example_image.png")
try:
stub.SchedulerReceiveUrl(
sglang_encoder_pb2.SchedulerReceiveUrlRequest(
req_id=req_id,
receive_url=f"{self.base_host}:{recv_port}",
receive_count=1,
),
timeout=60,
)
stub.Encode(
sglang_encoder_pb2.EncodeRequest(
mm_items=[image_path],
req_id=req_id,
num_parts=1,
part_idx=0,
),
timeout=300,
)
# A scheduler registers concurrently with Encode, never before it:
# the request state only exists once Encode dispatches.
with ThreadPoolExecutor(max_workers=1) as pool:
registration = pool.submit(
stub.SchedulerReceiveUrl,
sglang_encoder_pb2.SchedulerReceiveUrlRequest(
req_id=req_id,
receive_url=f"{self.base_host}:{recv_port}",
receive_count=1,
),
timeout=60,
)
stub.Encode(
sglang_encoder_pb2.EncodeRequest(
mm_items=[image_path],
req_id=req_id,
num_parts=1,
part_idx=0,
),
timeout=300,
)
registration.result(timeout=60)
poller = zmq.Poller()
poller.register(recv_socket, zmq.POLLIN)
@@ -464,6 +464,29 @@ class TestKimiLinearPDDCP4(GSM8KMixin, PDDisaggregationServerBase):
f"niah prompt_tokens={LONG_CONTEXT_TOKENS} depth={needle_depth}"
),
)
# Prefill now holds the long prefix. Decode must receive it
# again, even though prefill computes almost no new tokens.
# OSL > 1 checks that decode actually reads the transferred KV.
self._flush_cache(self.decode_url)
cached_actual = self._generate(
self.base_url,
prompt,
max_new_tokens=16,
ignore_eos=False,
)
self.assertGreater(
cached_actual["meta_info"]["cached_tokens"],
CHUNKED_PREFILL_SIZE,
)
self.assertIn(NIAH_KEY, cached_actual["text"])
self._assert_output_parity(
reference,
cached_actual,
label=(
f"cached niah prompt_tokens={LONG_CONTEXT_TOKENS} "
f"depth={needle_depth}"
),
)
def _assert_batch_completes(self, batch_size: int):
response = requests.post(
@@ -50,6 +50,8 @@ class TestDPAttentionDP2TP2(
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
other_args=[
"--trust-remote-code",
"--constrained-json-max-whitespace-cnt",
"4",
"--tp",
"2",
"--enable-dp-attention",
@@ -0,0 +1,240 @@
"""Compare unified-memory HiCache reloads against a resident-cache reference.
Evict a target prefix with distinct filler requests, require a host hit on
reload, and compare generated text and output logprobs. Both servers use the
same unified-memory configuration to keep attention reduction order comparable.
Covers GDN, SWA, tri-pool, and MLA layouts.
"""
import os
import time
import unittest
import requests
from sglang.srt.utils import kill_process_tree
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
CustomTestCase,
popen_launch_server,
)
register_cuda_ci(est_time=600, stage="extra-a", runner_config="2-gpu-large")
_COMMON_ARGS = [
"--trust-remote-code",
"--enable-unified-memory",
"--enable-cache-report",
"--max-running-requests",
"1",
"--context-length",
"4096",
]
# Distinct filler prefixes must evict the target from device memory.
_SMALL_POOL = ["--max-total-tokens", "8192"]
_PREFIX = (
"The following is a detailed technical description of a distributed inference "
"system with paged attention, radix prefix caching and hierarchical offload. "
) * 90
_TARGET = _PREFIX + " Question one:"
_CONTINUATION = _TARGET + " Explain how it works."
def _generate(base_url, text, max_new_tokens=32, logprobs=True):
payload = {
"text": text,
"sampling_params": {"temperature": 0.0, "max_new_tokens": max_new_tokens},
}
if logprobs:
payload["return_logprob"] = True
# Output logprobs suffice; asking for prompt logprobs from zero
# caps the reusable prefix at zero and bypasses HiCache entirely.
payload["logprob_start_len"] = -1
resp = requests.post(f"{base_url}/generate", json=payload, timeout=600)
assert resp.status_code == 200, resp.text
data = resp.json()
lp = (
[t[0] for t in data["meta_info"]["output_token_logprobs"]] if logprobs else None
)
return data["text"], lp, data["meta_info"]
class UnifiedMemoryHiCacheBase(CustomTestCase):
"""Compare identical unified-memory configurations with and without HiCache."""
model: str = ""
extra_args: list = []
server_env: dict = {}
@classmethod
def setUpClass(cls):
if cls is UnifiedMemoryHiCacheBase:
raise unittest.SkipTest("base class")
base_args = _COMMON_ARGS + cls.extra_args
cls.hicache_url = "http://127.0.0.1:8157"
cls.reference_url = "http://127.0.0.1:8158"
env = {**os.environ, **cls.server_env} if cls.server_env else None
hicache_args = ["--enable-hierarchical-cache"]
if "--hicache-size" not in base_args:
hicache_args += ["--hicache-ratio", "4"]
cls.process_hicache = popen_launch_server(
cls.model,
cls.hicache_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
other_args=base_args + hicache_args,
env=env,
)
cls.addClassCleanup(kill_process_tree, cls.process_hicache.pid)
cls.process_reference = popen_launch_server(
cls.model,
cls.reference_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
other_args=base_args + ["--base-gpu-id", "1"],
env=env,
)
cls.addClassCleanup(kill_process_tree, cls.process_reference.pid)
def _force_host_round_trip(self):
"""Evict the target off the device so the next hit must come from L2."""
for i in range(8):
_generate(
self.hicache_url,
f"Document {i}. "
+ (f"Unique filler {i} about an unrelated subject. " * 300),
max_new_tokens=8,
logprobs=False,
)
def _flush_both(self):
"""Reset cache state to match prefill boundaries and reduction order."""
for url in (self.hicache_url, self.reference_url):
requests.post(f"{url}/flush_cache", timeout=180)
time.sleep(3)
def test_load_back_matches_no_hicache(self):
"""Host reloads preserve generated text and logprobs within tolerance."""
self._flush_both()
cold_text, cold_lp, _ = _generate(self.hicache_url, _TARGET)
ref_cold_text, ref_cold_lp, _ = _generate(self.reference_url, _TARGET)
self._force_host_round_trip()
# Extend the prefix so both servers compute new KV rows. Repeating it
# would let only the resident reference reuse its original final-token KV.
warm_text, warm_lp, warm_meta = _generate(self.hicache_url, _CONTINUATION)
ref_text, ref_lp, _ = _generate(self.reference_url, _CONTINUATION)
self.assertGreater(
(warm_meta.get("cached_tokens_details") or {}).get("host", 0),
0,
msg=f"Target did not reload from host: {warm_meta}",
)
self.assertEqual(cold_text, ref_cold_text)
self.assertEqual(warm_text, ref_text)
# Match the reference's prefill boundary in each comparison: cold
# against cold, and an L2 prefix hit against a resident prefix hit.
for label, lp, reference in (
("cold", cold_lp, ref_cold_lp),
("after-L2-reload", warm_lp, ref_lp),
):
self.assertEqual(len(lp), len(reference))
delta = max(abs(a - b) for a, b in zip(lp, reference))
self.assertAlmostEqual(
delta,
0.0,
places=5,
msg=f"{label} diverged from the no-HiCache reference by {delta}",
)
def test_server_survives_the_round_trip(self):
"""Cache churn must leave both schedulers healthy."""
self._force_host_round_trip()
for url in (self.hicache_url, self.reference_url):
resp = requests.get(f"{url}/health", timeout=30)
self.assertEqual(resp.status_code, 200)
class TestUnifiedMemoryHiCacheGDN(UnifiedMemoryHiCacheBase):
"""MHA full attention with envelope-strided gated-delta-net state."""
model = "yujiepan/qwen3.5-tiny-random"
extra_args = _SMALL_POOL + [
"--linear-attn-backend",
"triton",
"--mamba-backend",
"triton",
"--max-mamba-cache-size",
"8",
"--mem-fraction-static",
"0.6",
]
class TestUnifiedMemoryHiCacheSWA(UnifiedMemoryHiCacheBase):
"""Hybrid SWA reloads bind pages to the full-attention pool's virtual IDs."""
model = "yujiepan/gemma-4e-tiny-random"
extra_args = _SMALL_POOL + [
"--attention-backend",
"triton",
"--mem-fraction-static",
"0.7",
]
class TestUnifiedMemoryHiCacheTriPool(UnifiedMemoryHiCacheBase):
"""Full attention, sliding-window attention, and ShortConv state together."""
# The test revision is the reduced checkpoint used by Inkling CI.
model = "thinkingmachines/Inkling"
server_env = {"SGLANG_ENABLE_UNIFIED_RADIX_TREE": "1"}
extra_args = _SMALL_POOL + [
"--revision",
"test",
"--attention-backend",
"triton",
"--page-size",
"128",
"--mamba-radix-cache-strategy",
"extra_buffer",
"--swa-full-tokens-ratio",
"0.8",
"--max-mamba-cache-size",
"8",
"--mamba-full-memory-ratio",
"0.1",
"--mem-fraction-static",
"0.5",
"--cuda-graph-backend-prefill",
"disabled",
# Bound total host memory across all three component pools.
"--hicache-size",
"8",
]
class TestUnifiedMemoryHiCacheMLA(UnifiedMemoryHiCacheBase):
"""MLA full attention with KDA state and MLA-specific transfer pointers."""
model = "yujiepan/kimi-linear-tiny-random"
extra_args = _SMALL_POOL + [
"--max-mamba-cache-size",
"8",
"--mem-fraction-static",
"0.5",
"--linear-attn-backend",
"triton",
"--mamba-backend",
"triton",
"--attention-backend",
"triton",
"--cuda-graph-backend-decode",
"disabled",
"--cuda-graph-backend-prefill",
"disabled",
]
if __name__ == "__main__":
unittest.main()
@@ -56,11 +56,7 @@ class TestDeepseekV3MTP(GSM8KMixin, DefaultServerBase):
f"{acc_length=:.2f}\n"
f"{speed=:.2f} token/s\n"
)
self.assertGreater(acc_length, 2.8)
if is_in_amd_ci():
self.assertGreater(speed, 15)
else:
self.assertGreater(speed, 130)
self.assertGreater(speed, 130)
if __name__ == "__main__":
@@ -10,7 +10,7 @@ from sglang.test.server_fixtures.dsa_mtp_fixture import (
register_cuda_ci(
est_time=400,
stage="base-c",
stage="nightly",
runner_config="8-gpu-h200",
)
@@ -10,7 +10,7 @@ from sglang.test.server_fixtures.dsa_mtp_fixture import (
register_cuda_ci(
est_time=400,
stage="base-c",
stage="nightly",
runner_config="8-gpu-h200",
)
@@ -3,7 +3,6 @@ import unittest
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.gpt_oss_common import BaseTestGptOss
register_cuda_ci(est_time=128, stage="base-c", runner_config="4-gpu-h100")
register_cuda_ci(est_time=119, stage="base-c", runner_config="4-gpu-b200")
@@ -17,7 +17,7 @@ from sglang.test.test_utils import (
popen_launch_server,
)
register_cuda_ci(est_time=1472, stage="base-c", runner_config="8-gpu-b300")
register_cuda_ci(est_time=1472, stage="extra-b", runner_config="8-gpu-b300")
MODEL_PATH = "moonshotai/Kimi-K3"
DSPARK_DRAFT_MODEL = "RadixArk/Kimi-K3-DSpark"
+1 -1
View File
@@ -5,7 +5,7 @@ from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kits.eval_accuracy_kit import GSM8KMixin
from sglang.test.server_fixtures.mmmu_fixture import MMMUServerBase
register_cuda_ci(est_time=317, stage="base-c", runner_config="8-gpu-h200")
register_cuda_ci(est_time=317, stage="extra-b", runner_config="8-gpu-h200")
MIMO_V2_MODEL = "XiaomiMiMo/MiMo-V2.5"
MIMO_V2_OTHER_ARGS = [
@@ -0,0 +1,397 @@
"""Isolated gfx950 numerical tests for GLM-5.3-Flash Quark MoE."""
import unittest
from types import SimpleNamespace
import torch
import torch.nn.functional as F
from aiter.ops.flydsl.moe_common import GateMode
from aiter.ops.shuffle import shuffle_weight
from aiter.ops.triton.quant import dynamic_mxfp4_quant
from aiter.utility.fp4_utils import e8m0_shuffle
from sglang.srt.layers.moe.moe_runner.aiter import (
AiterMoeQuantInfo,
AiterQuantType,
AiterRunnerCore,
AiterRunnerInput,
)
from sglang.srt.layers.quantization.fp8_utils import dequant_mxfp4
from sglang.srt.utils import is_gfx95_supported, is_hip
from sglang.test.ci.ci_register import register_amd_ci
from sglang.test.test_utils import CustomTestCase
register_amd_ci(est_time=180, suite="stage-b-test-1-gpu-small-amd-mi35x")
@unittest.skipUnless(
torch.cuda.is_available() and is_hip() and is_gfx95_supported(),
"requires one gfx950 GPU",
)
class TestGLM53FlashQuarkMoE(CustomTestCase):
hidden_size = 4096
intermediate_size = 2048
num_experts = 9
swiglu_limit = 10.0
@classmethod
def setUpClass(cls):
super().setUpClass()
torch.manual_seed(7)
cls.weights = cls._make_mxfp4_bank()
cls.weights["w13_deq"] = cls._dequant(
cls.weights["w13_raw"], cls.weights["s13_raw"]
)
cls.weights["w2_deq"] = cls._dequant(
cls.weights["w2_raw"], cls.weights["s2_raw"]
)
cls.runner = AiterRunnerCore(
SimpleNamespace(
no_combine=False,
activation="silu",
gemm1_alpha=None,
gemm1_clamp_limit=None,
)
)
@classmethod
def _make_mxfp4_bank(cls):
gate_weights = []
up_weights = []
down_weights = []
gate_scales = []
up_scales = []
down_scales = []
for expert in range(cls.num_experts):
generator = torch.Generator(device="cuda")
generator.manual_seed(100 + expert)
gate = (
torch.randn(
cls.intermediate_size,
cls.hidden_size,
generator=generator,
device="cuda",
dtype=torch.bfloat16,
)
* 0.05
)
up = (
torch.randn(
cls.intermediate_size,
cls.hidden_size,
generator=generator,
device="cuda",
dtype=torch.bfloat16,
)
* 0.05
)
down = (
torch.randn(
cls.hidden_size,
cls.intermediate_size,
generator=generator,
device="cuda",
dtype=torch.bfloat16,
)
* 0.01
)
gate_q, gate_s = dynamic_mxfp4_quant(gate)
up_q, up_s = dynamic_mxfp4_quant(up)
down_q, down_s = dynamic_mxfp4_quant(down)
gate_weights.append(gate_q)
up_weights.append(up_q)
down_weights.append(down_q)
gate_scales.append(gate_s)
up_scales.append(up_s)
down_scales.append(down_s)
w13 = torch.cat([torch.stack(gate_weights), torch.stack(up_weights)], dim=1)
w2 = torch.stack(down_weights)
s13 = torch.cat([torch.stack(gate_scales), torch.stack(up_scales)], dim=1)
s2 = torch.stack(down_scales)
return {
"w13_raw": w13,
"w2_raw": w2,
"s13_raw": s13,
"s2_raw": s2,
"w13": shuffle_weight(w13.contiguous(), (16, 16)),
"w2": shuffle_weight(w2.contiguous(), (16, 16)),
"s13": e8m0_shuffle(s13.view(-1, s13.shape[-1])).view_as(s13),
"s2": e8m0_shuffle(s2.view(-1, s2.shape[-1])).view_as(s2),
}
@staticmethod
def _quantize_fp8_weight(weight):
rows, width = weight.shape
blocks = (
weight.float().view(rows // 128, 128, width // 128, 128).permute(0, 2, 1, 3)
)
scale = blocks.abs().amax(dim=(2, 3)).clamp(min=1e-12) / 448.0
quantized = (blocks / scale[:, :, None, None]).to(torch.float8_e4m3fn)
return (
quantized.permute(0, 2, 1, 3).reshape(rows, width),
scale,
)
@staticmethod
def _dequantize_fp8_weight(weight, scale):
return weight.float() * scale.repeat_interleave(128, dim=0).repeat_interleave(
128, dim=1
)
@staticmethod
def _quant_dequant_fp8_activation(activation):
tokens, width = activation.shape
groups = activation.float().view(tokens, width // 128, 128)
scale = groups.abs().amax(dim=-1).clamp(min=1e-12) / 448.0
quantized = (groups / scale.unsqueeze(-1)).to(torch.float8_e4m3fn)
return (quantized.float() * scale.unsqueeze(-1)).reshape(tokens, width)
@classmethod
def tearDownClass(cls):
if hasattr(cls, "weights"):
del cls.weights
if hasattr(cls, "runner"):
del cls.runner
torch.cuda.empty_cache()
super().tearDownClass()
@classmethod
def _dequant(cls, weight, scale):
experts, rows, packed = weight.shape
blocks = packed // 16
return dequant_mxfp4(
weight.view(experts, rows, blocks, 16),
scale,
torch.bfloat16,
)
@classmethod
def _quant_dequant_activation(cls, activation):
quantized, scale = dynamic_mxfp4_quant(activation)
tokens, packed = quantized.shape
blocks = packed // 16
return dequant_mxfp4(
quantized.view(1, tokens, blocks, 16),
scale.view(1, tokens, blocks),
torch.bfloat16,
).squeeze(0)
@classmethod
def _torch_oracle(cls, hidden_states, topk_ids, topk_weights):
w13 = cls.weights["w13_deq"]
w2 = cls.weights["w2_deq"]
output = torch.zeros_like(hidden_states)
hidden_qdq = cls._quant_dequant_activation(hidden_states)
for token in range(hidden_states.shape[0]):
for route in range(topk_ids.shape[1]):
expert = int(topk_ids[token, route])
gate = F.linear(
hidden_qdq[token].float(),
w13[expert, : cls.intermediate_size].float(),
)
up = F.linear(
hidden_qdq[token].float(),
w13[expert, cls.intermediate_size :].float(),
)
gate = gate.clamp(max=cls.swiglu_limit)
up = up.clamp(min=-cls.swiglu_limit, max=cls.swiglu_limit)
activated = F.silu(gate) * up
activated = cls._quant_dequant_activation(
activated.unsqueeze(0).bfloat16()
).squeeze(0)
expert_output = F.linear(activated.float(), w2[expert].float())
output[token] += (expert_output * topk_weights[token, route]).to(
output.dtype
)
return output
@classmethod
def _aiter(cls, hidden_states, topk_ids, topk_weights):
w13 = cls.weights["w13"].view(torch.float4_e2m1fn_x2)
w2 = cls.weights["w2"].view(torch.float4_e2m1fn_x2)
w13.is_shuffled = True
w2.is_shuffled = True
quant_info = AiterMoeQuantInfo(
w13_weight=w13,
w2_weight=w2,
quant_type=AiterQuantType.PER_1X32,
w13_scale=cls.weights["s13"],
w2_scale=cls.weights["s2"],
swiglu_limit=cls.swiglu_limit,
fused_moe_kwargs={"gate_mode": GateMode.SEPARATED.value},
)
runner_input = AiterRunnerInput(
hidden_states=hidden_states,
topk_ids=topk_ids.to(torch.int32),
topk_weights=topk_weights.to(torch.float32),
quant_type=AiterQuantType.PER_1X32,
)
return cls.runner.run(runner_input, quant_info, {}).hidden_states
def _assert_numerics(self, actual, expected, max_abs=None):
self.assertTrue(torch.isfinite(actual).all())
actual_float = actual.float()
expected_float = expected.float()
cosine = F.cosine_similarity(
actual_float.flatten().unsqueeze(0),
expected_float.flatten().unsqueeze(0),
).item()
self.assertGreater(cosine, 0.98)
relative_l2 = (
torch.linalg.vector_norm(actual_float - expected_float)
/ torch.linalg.vector_norm(expected_float).clamp(min=1e-12)
).item()
self.assertLess(relative_l2, 0.20)
if max_abs is not None:
self.assertLess(
(actual_float - expected_float).abs().max().item(),
max_abs,
)
def test_top1_and_top8_match_dequantized_oracle(self):
for tokens in (1, 8, 17, 32, 64, 128):
generator = torch.Generator(device="cuda")
generator.manual_seed(tokens)
hidden = (
torch.randn(
tokens,
self.hidden_size,
generator=generator,
device="cuda",
dtype=torch.bfloat16,
)
* 0.5
)
# topk=9 models eight routed experts plus one fused shared slot.
for topk in (1, 8, 9):
with self.subTest(tokens=tokens, topk=topk):
ids = torch.arange(topk, device="cuda", dtype=torch.int64).repeat(
tokens, 1
)
weights = torch.rand(
tokens,
topk,
generator=generator,
device="cuda",
dtype=torch.float32,
)
if topk > 1:
weights /= weights.sum(dim=-1, keepdim=True)
expected = self._torch_oracle(hidden, ids, weights)
actual = self._aiter(hidden, ids, weights)
repeated = self._aiter(hidden, ids, weights)
self._assert_numerics(actual, expected, max_abs=0.75)
if topk == 1:
torch.testing.assert_close(actual, repeated, atol=0, rtol=0)
else:
# Stage-2 combines top-k routes with atomics; reduction
# order may differ while remaining BF16-equivalent.
torch.testing.assert_close(
actual, repeated, atol=2e-2, rtol=1e-2
)
def test_clamp_boundary(self):
hidden = torch.full(
(1, self.hidden_size),
4.0,
device="cuda",
dtype=torch.bfloat16,
)
ids = torch.tensor([[0]], device="cuda", dtype=torch.int64)
weights = torch.ones((1, 1), device="cuda", dtype=torch.float32)
expected = self._torch_oracle(hidden, ids, weights)
actual = self._aiter(hidden, ids, weights)
self._assert_numerics(actual, expected)
def test_plain_block_fp8_matches_separated_oracle(self):
generator = torch.Generator(device="cuda")
generator.manual_seed(1234)
gate = (
torch.randn(
self.intermediate_size,
self.hidden_size,
generator=generator,
device="cuda",
dtype=torch.bfloat16,
)
* 0.05
)
up = (
torch.randn(
self.intermediate_size,
self.hidden_size,
generator=generator,
device="cuda",
dtype=torch.bfloat16,
)
* 0.05
)
down = (
torch.randn(
self.hidden_size,
self.intermediate_size,
generator=generator,
device="cuda",
dtype=torch.bfloat16,
)
* 0.01
)
gate_q, gate_s = self._quantize_fp8_weight(gate)
up_q, up_s = self._quantize_fp8_weight(up)
down_q, down_s = self._quantize_fp8_weight(down)
w13_raw = torch.cat([gate_q, up_q], dim=0).unsqueeze(0)
w13_scale = torch.cat([gate_s, up_s], dim=0).unsqueeze(0)
w2_raw = down_q.unsqueeze(0)
w2_scale = down_s.unsqueeze(0)
w13 = shuffle_weight(w13_raw.contiguous(), (16, 16))
w2 = shuffle_weight(w2_raw.contiguous(), (16, 16))
quant_info = AiterMoeQuantInfo(
w13_weight=w13,
w2_weight=w2,
quant_type=AiterQuantType.PER_128X128,
w13_scale=w13_scale,
w2_scale=w2_scale,
swiglu_limit=self.swiglu_limit,
fused_moe_kwargs={"gate_mode": GateMode.SEPARATED.value},
)
gate_deq = self._dequantize_fp8_weight(gate_q, gate_s)
up_deq = self._dequantize_fp8_weight(up_q, up_s)
down_deq = self._dequantize_fp8_weight(down_q, down_s)
for tokens in (1, 8, 32):
with self.subTest(tokens=tokens):
hidden = (
torch.randn(
tokens,
self.hidden_size,
generator=generator,
device="cuda",
dtype=torch.bfloat16,
)
* 0.5
)
hidden_qdq = self._quant_dequant_fp8_activation(hidden)
gate_out = F.linear(hidden_qdq, gate_deq).clamp(max=self.swiglu_limit)
up_out = F.linear(hidden_qdq, up_deq).clamp(
-self.swiglu_limit, self.swiglu_limit
)
activated = self._quant_dequant_fp8_activation(
(F.silu(gate_out) * up_out).bfloat16()
)
expected = F.linear(activated, down_deq).bfloat16()
runner_input = AiterRunnerInput(
hidden_states=hidden,
topk_ids=torch.zeros((tokens, 1), device="cuda", dtype=torch.int32),
topk_weights=torch.ones(
(tokens, 1), device="cuda", dtype=torch.float32
),
quant_type=AiterQuantType.PER_128X128,
)
actual = self.runner.run(runner_input, quant_info, {}).hidden_states
self._assert_numerics(actual, expected, max_abs=0.75)
if __name__ == "__main__":
unittest.main()
@@ -10,7 +10,11 @@ import torch
from transformers import MistralConfig, PretrainedConfig
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import DEFAULT_SMALL_MODEL_NAME_FOR_TEST, CustomTestCase
from sglang.test.test_utils import (
DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
CustomTestCase,
publish_build_topology,
)
register_cuda_ci(est_time=60, stage="base-b", runner_config="1-gpu-small")
@@ -77,7 +81,8 @@ class TestDraftEmbedScan(CustomTestCase):
init_distributed_environment(
world_size=1, rank=0, local_rank=0, distributed_init_method="env://"
)
initialize_model_parallel(tensor_model_parallel_size=1)
publish_build_topology(tp_size=1)
initialize_model_parallel()
torch.set_default_dtype(torch.bfloat16)
torch.cuda.set_device(0)
@@ -38,6 +38,7 @@ from sglang.srt.distributed.parallel_state import (
initialize_model_parallel,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import publish_build_topology
register_cuda_ci(est_time=18, stage="base-b", runner_config="2-gpu-large")
@@ -238,10 +239,10 @@ def _worker_main(local_rank: int, world_size: int):
init_distributed_environment(
world_size=world_size, rank=local_rank, local_rank=local_rank
)
initialize_model_parallel(
tensor_model_parallel_size=world_size,
expert_model_parallel_size=world_size,
publish_build_topology(
tp_size=world_size, ep_size=world_size, world_rank=local_rank
)
initialize_model_parallel()
from sglang.srt.eplb.lplb_solver import clear_global_lplb_solvers
@@ -14,6 +14,7 @@ from sglang.kernels.jit.benchmark import marker
from sglang.kernels.jit.benchmark.utils import get_benchmark_range, multigpu_bench_main
from sglang.kernels.jit.utils import cache_once, is_arch_support_pdl
from sglang.kernels.ops.communication.mp import register_comm_cleanup
from sglang.srt.runtime_context import get_parallel
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_cuda_ci(
@@ -61,6 +62,7 @@ def _init_cpu_group() -> dist.ProcessGroup:
local_rank=local_rank,
backend="nccl",
)
get_parallel().override_permanently(world_group=coord)
atexit.register(dist.destroy_process_group)
torch.cuda.set_stream(torch.cuda.Stream())
return coord.cpu_group
@@ -33,6 +33,7 @@ from sglang.srt.distributed.device_communicators.triton_symm_mem_ag import (
all_gather_inner,
create_state,
)
from sglang.srt.runtime_context import get_parallel
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_cuda_ci(
@@ -77,6 +78,7 @@ def _init_cpu_group() -> dist.ProcessGroup:
local_rank=local_rank,
backend="nccl",
)
get_parallel().override_permanently(world_group=ps._WORLD)
atexit.register(dist.destroy_process_group)
logging.disable(logging.INFO)
torch.cuda.set_stream(torch.cuda.Stream())
@@ -36,6 +36,7 @@ from sglang.kernels.ops.communication.mp import register_comm_cleanup
from sglang.srt.distributed.device_communicators.custom_all_reduce_v2 import (
CustomAllReduceV2,
)
from sglang.srt.runtime_context import get_parallel
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_cuda_ci(
@@ -108,6 +109,7 @@ def _init_cpu_group() -> dist.ProcessGroup:
local_rank=local_rank,
backend="nccl",
)
get_parallel().override_permanently(world_group=coord)
atexit.register(dist.destroy_process_group)
logging.disable(logging.INFO)
torch.cuda.set_stream(torch.cuda.Stream())
@@ -24,8 +24,11 @@ from sgl_kernel import transfer_kv_all_layer, transfer_kv_per_layer
from sglang.kernels.jit.benchmark import marker
from sglang.kernels.jit.benchmark.utils import get_benchmark_range
from sglang.kernels.ops.kvcache.hicache import (
transfer_hicache_all_layer,
transfer_hicache_one_layer,
DEFAULT_BLOCK_QUOTA,
TMA_BLOCK_QUOTA,
_default_unroll,
_jit_hicache_module,
_jit_hicache_tma_module,
)
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
@@ -108,15 +111,19 @@ def sglang_jit_transfer_one(
indices_src: torch.Tensor,
element_dim: int,
) -> None:
"""SGL JIT Kernel for single layer transfer."""
transfer_hicache_one_layer(
k_cache_dst,
v_cache_dst,
"""SGL JIT register kernel for single layer transfer (bypasses TMA routing)."""
element_size = element_dim * k_cache_dst.element_size()
_jit_hicache_module(
element_size=element_size,
unroll=_default_unroll(element_size),
block_quota=DEFAULT_BLOCK_QUOTA,
).launch_one(
k_cache_dst.view(-1, element_dim),
v_cache_dst.view(-1, element_dim),
indices_dst,
k_cache_src,
v_cache_src,
k_cache_src.view(-1, element_dim),
v_cache_src.view(-1, element_dim),
indices_src,
element_dim=element_dim,
)
@@ -153,17 +160,58 @@ def sglang_jit_transfer_all(
stride_bytes: int,
element_size: int,
) -> None:
"""SGL JIT Kernel for all layer transfer."""
transfer_hicache_all_layer(
"""SGL JIT register kernel for all layer transfer (bypasses TMA routing)."""
_jit_hicache_module(
element_size=element_size,
unroll=_default_unroll(element_size),
block_quota=DEFAULT_BLOCK_QUOTA,
).launch_all(
k_ptrs_dst,
v_ptrs_dst,
indices_dst,
k_ptrs_src,
v_ptrs_src,
indices_src,
kv_cache_src_stride_bytes=stride_bytes,
kv_cache_dst_stride_bytes=stride_bytes,
element_size=element_size,
stride_bytes,
stride_bytes,
)
def sglang_tma_transfer_one(
k_cache_dst: torch.Tensor,
v_cache_dst: torch.Tensor,
indices_dst: torch.Tensor,
k_cache_src: torch.Tensor,
v_cache_src: torch.Tensor,
indices_src: torch.Tensor,
) -> None:
"""SGL TMA staging kernel for single layer transfer."""
_jit_hicache_tma_module(block_quota=TMA_BLOCK_QUOTA).launch_one(
k_cache_dst, v_cache_dst, indices_dst, k_cache_src, v_cache_src, indices_src
)
def sglang_tma_transfer_all(
k_ptrs_dst: torch.Tensor,
v_ptrs_dst: torch.Tensor,
indices_dst: torch.Tensor,
k_ptrs_src: torch.Tensor,
v_ptrs_src: torch.Tensor,
indices_src: torch.Tensor,
stride_bytes: int,
element_size: int,
) -> None:
"""SGL TMA staging kernel for all layer transfer."""
_jit_hicache_tma_module(block_quota=TMA_BLOCK_QUOTA).launch_all(
k_ptrs_dst,
v_ptrs_dst,
indices_dst,
k_ptrs_src,
v_ptrs_src,
indices_src,
stride_bytes,
stride_bytes,
element_size,
)
@@ -191,6 +239,13 @@ ELEMENT_SIZE_RANGE = get_benchmark_range(
LINE_VALS = ["aot", "jit", "torch"]
if DISABLE_TORCH:
LINE_VALS.remove("torch")
# The TMA staging kernel needs sm_90+ (cp.async.bulk); skip the line elsewhere.
if (
torch.cuda.is_available()
and torch.version.hip is None
and torch.cuda.get_device_capability()[0] >= 9
):
LINE_VALS.insert(2, "tma")
# =============================================================================
@@ -246,6 +301,17 @@ def benchmark_one_layer_h2d(element_size: int, batch_size: int, provider: str):
)
for i in range(NUM_LAYERS)
],
"tma": lambda: [
sglang_tma_transfer_one(
k_cache_dst[i],
v_cache_dst[i],
indices_dst_gpu,
k_cache_src[i],
v_cache_src[i],
indices_src_gpu,
)
for i in range(NUM_LAYERS)
],
"torch": lambda: [
pytorch_transfer(
k_cache_dst[i],
@@ -329,6 +395,16 @@ def benchmark_all_layer_d2h(element_size: int, batch_size: int, provider: str):
element_bytes,
element_bytes,
),
"tma": lambda: sglang_tma_transfer_all(
k_ptrs_dst,
v_ptrs_dst,
indices_dst_gpu,
k_ptrs_src,
v_ptrs_src,
indices_src_gpu,
element_bytes,
element_bytes,
),
"torch": lambda: [
pytorch_transfer(
k_caches_dst[i],
@@ -0,0 +1,316 @@
import unittest
from unittest.mock import patch
import torch
from sglang.kernels.ops.attention.dsv4.fp4_indexer import quantize_fp4_indexer_tensor
from sglang.srt.layers.attention.dsv4 import dense_prefill_indexer
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=120, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
def make_inputs(request_lengths, ratio=1, zero_queries=False, seed=17):
torch.manual_seed(seed)
rows = sum(q for q, _ in request_lengths)
q = torch.randn((rows, 32, 128), dtype=torch.bfloat16, device="cuda")
if zero_queries:
q.zero_()
packed, scales = quantize_fp4_indexer_tensor(q.flatten(0, 1), rne=True)
kv = quantize_fp4_indexer_tensor(
torch.randn(
(sum(n for _, n in request_lengths), 128),
dtype=torch.bfloat16,
device="cuda",
),
rne=True,
)
starts, lengths = [], []
start = 0
for queries, context in request_lengths:
starts.extend([start] * queries)
lengths.extend(
(position + 1) // ratio
for position in range(context * ratio - queries, context * ratio)
)
start += context
return dict(
q=(packed.view(rows, 32, 64), scales.view(rows, 32)),
kv=kv,
weights=torch.rand((rows, 32), dtype=torch.float32, device="cuda"),
starts=torch.tensor(starts, dtype=torch.int32, device="cuda"),
lengths=torch.tensor(lengths, dtype=torch.int32, device="cuda"),
request_lengths=request_lengths,
topk=512,
candidate_topk_blocks=2,
candidate_block_size=8,
)
def dense_scores(inputs):
from deep_gemm import fp8_fp4_mqa_logits
width = (max(n for _, n in inputs["request_lengths"]) + 3) // 4 * 4
scores = fp8_fp4_mqa_logits(
inputs["q"],
inputs["kv"],
inputs["weights"],
inputs["starts"],
inputs["starts"] + inputs["lengths"],
False,
width,
)
return scores.masked_fill_(
torch.arange(width, device="cuda")[None, :] >= inputs["lengths"][:, None],
-torch.inf,
)
@unittest.skipUnless(
torch.cuda.is_available() and torch.cuda.get_device_capability()[0] == 10,
"requires SM100",
)
class TestDensePrefillIndexer(CustomTestCase):
def assert_topk(self, inputs, selected, scores):
columns = (selected - inputs["starts"][:, None]).long()
valid = selected >= 0
expected_count = torch.isfinite(scores).sum(-1).clamp_max(inputs["topk"])
torch.testing.assert_close(valid.sum(-1), expected_count)
self.assertTrue(
(~valid | ((columns >= 0) & (columns < inputs["lengths"][:, None]))).all()
)
actual = scores.gather(1, columns.clamp(0, scores.shape[1] - 1)).masked_fill(
~valid, -torch.inf
)
expected = scores.topk(min(inputs["topk"], scores.shape[1]), dim=-1).values
expected = torch.nn.functional.pad(
expected, (0, inputs["topk"] - expected.shape[1]), value=-torch.inf
)
torch.testing.assert_close(
actual.sort(descending=True).values, expected, rtol=1e-5, atol=1e-5
)
ordered = (
columns.masked_fill(~valid, torch.iinfo(torch.int64).max).sort().values
)
self.assertTrue(
(
(ordered[:, 1:] != ordered[:, :-1])
| (ordered[:, 1:] == torch.iinfo(torch.int64).max)
).all()
)
def test_ragged_source_consumer_and_replay(self):
for ratio in (1, 2):
for zero_queries in (False, True):
with self.subTest(ratio=ratio, zero_queries=zero_queries):
inputs = make_inputs(
[(0, 0), (1, 1), (33, 511), (257, 4097)],
ratio=ratio,
zero_queries=zero_queries,
)
inputs["candidate_topk_blocks"] = 128
scores = dense_scores(inputs)
consumer_inputs = make_inputs(
inputs["request_lengths"],
ratio=ratio,
zero_queries=zero_queries,
seed=29,
)
consumer_inputs["kv"] = inputs["kv"]
consumer_inputs["candidate_topk_blocks"] = 128
consumer_scores = dense_scores(consumer_inputs)
with patch.object(
dense_prefill_indexer, "_SCORE_BUDGET_BYTES", 128 << 10
):
selected, candidates = dense_prefill_indexer.dense_prefill_topk(
**inputs, publish_candidates=True, candidates=None
)
self.assert_topk(inputs, selected, scores)
row = 0
for (queries, context), blocks in zip(
inputs["request_lengths"], candidates.request_blocks
):
local = scores[row : row + queries, :context]
if queries and context:
padded = torch.nn.functional.pad(
local, (0, -context % 8), value=-torch.inf
)
block_scores = padded.unflatten(-1, (-1, 8)).amax(-1)
last = (inputs["lengths"][row : row + queries] - 1) // 8
block_scores.masked_fill_(
torch.arange(block_scores.shape[1], device="cuda")[
None, :
]
== last[:, None],
torch.inf,
)
chosen_scores = block_scores.gather(
1, blocks.long().clamp_min(0)
).masked_fill(blocks < 0, -torch.inf)
torch.testing.assert_close(
chosen_scores.sort(descending=True).values,
block_scores.topk(blocks.shape[1]).values,
rtol=1e-5,
atol=1e-5,
)
columns = torch.arange(context, device="cuda")
member = (
columns[None, :, None] // 8 == blocks[:, None, :]
).any(-1)
consumer_scores[
row : row + queries, :context
].masked_fill_(
~member,
-torch.inf,
)
row += queries
self.assertGreater(
torch.isfinite(consumer_scores[-1]).sum().item(),
inputs["topk"],
)
self.assertLess(
torch.isfinite(consumer_scores[-1]).sum().item(),
inputs["lengths"][-1].item(),
)
selected, published = dense_prefill_indexer.dense_prefill_topk(
**consumer_inputs,
publish_candidates=False,
candidates=candidates,
)
self.assertIsNone(published)
self.assert_topk(consumer_inputs, selected, consumer_scores)
tail_lengths = [0, 0, 7, 31]
rows, row = [], 0
for (queries, _), tail in zip(
inputs["request_lengths"], tail_lengths
):
rows.extend(range(row + queries - tail, row + queries))
row += queries
rows = torch.tensor(rows, dtype=torch.int64, device="cuda")
tail_inputs = dict(
consumer_inputs,
q=tuple(t[rows] for t in consumer_inputs["q"]),
weights=consumer_inputs["weights"][rows],
starts=inputs["starts"][rows],
lengths=inputs["lengths"][rows],
request_lengths=list(
zip(
tail_lengths,
[n for _, n in inputs["request_lengths"]],
)
),
)
selected, _ = dense_prefill_indexer.dense_prefill_topk(
**tail_inputs,
publish_candidates=False,
candidates=candidates.tail(tail_lengths),
)
self.assert_topk(tail_inputs, selected, consumer_scores[rows])
def test_unfiltered_and_zero_length_requests(self):
for request_lengths in ([(257, 8192)], [(1, 0), (1, 1), (0, 7)]):
for publish in (False, True):
with self.subTest(request_lengths=request_lengths, publish=publish):
inputs = make_inputs(request_lengths)
selected, candidates = dense_prefill_indexer.dense_prefill_topk(
**inputs, publish_candidates=publish, candidates=None
)
self.assert_topk(inputs, selected, dense_scores(inputs))
if publish:
self.assertEqual(
[b.shape[0] for b in candidates.request_blocks],
[q for q, _ in request_lengths],
)
def test_empty_queries_or_context(self):
for request_lengths, shape, block_shapes in (
([], (0, 512), []),
([(0, 0)], (0, 512), [(0, 0)]),
([(0, 0), (0, 17)], (0, 512), [(0, 0), (0, 2)]),
([(1, 0)], (1, 512), [(1, 0)]),
):
with self.subTest(request_lengths=request_lengths):
inputs = make_inputs(request_lengths)
selected, candidates = dense_prefill_indexer.dense_prefill_topk(
**inputs, publish_candidates=True, candidates=None
)
torch.testing.assert_close(
selected, torch.full(shape, -1, dtype=torch.int32, device="cuda")
)
self.assertEqual(
[tuple(b.shape) for b in candidates.request_blocks], block_shapes
)
def test_score_budget_includes_allocation_padding(self):
from deep_gemm import fp8_fp4_mqa_logits
inputs = make_inputs([(13, 257)])
expected = dense_scores(inputs)
for budget in (32 << 10, 28 << 10, 8 << 10):
with self.subTest(budget=budget):
allocations = []
def checked_logits(*args, **kwargs):
before = torch.cuda.memory_allocated()
logits = fp8_fp4_mqa_logits(*args, **kwargs)
allocations.append(torch.cuda.memory_allocated() - before)
return logits
with (
patch.object(dense_prefill_indexer, "_SCORE_BUDGET_BYTES", budget),
patch("deep_gemm.fp8_fp4_mqa_logits", new=checked_logits),
):
selected, _ = dense_prefill_indexer.dense_prefill_topk(
**inputs, publish_candidates=True, candidates=None
)
self.assertTrue(allocations)
self.assertLessEqual(max(allocations), budget)
self.assert_topk(inputs, selected, expected)
def test_score_memory_is_bounded(self):
for context, limit_gib in ((65536, 3), (65535, 5)):
with self.subTest(context=context):
inputs = make_inputs([(16384, context)])
inputs["candidate_topk_blocks"] = 2048
torch.cuda.synchronize()
torch.cuda.reset_peak_memory_stats()
baseline = torch.cuda.memory_allocated()
selected, candidates = dense_prefill_indexer.dense_prefill_topk(
**inputs, publish_candidates=True, candidates=None
)
torch.cuda.synchronize()
self.assertLess(
torch.cuda.max_memory_allocated() - baseline, limit_gib << 30
)
self.assertEqual(tuple(selected.shape), (16384, 512))
self.assertEqual(
tuple(candidates.request_blocks[0].shape), (16384, 2048)
)
del selected
selected, published = dense_prefill_indexer.dense_prefill_topk(
**inputs, publish_candidates=False, candidates=candidates
)
self.assertIsNone(published)
del selected
torch.cuda.synchronize()
baseline = torch.cuda.memory_allocated()
for _ in range(3):
torch.cuda.reset_peak_memory_stats()
selected, published = dense_prefill_indexer.dense_prefill_topk(
**inputs, publish_candidates=False, candidates=candidates
)
torch.cuda.synchronize()
self.assertIsNone(published)
self.assertLess(
torch.cuda.max_memory_allocated() - baseline, 4 << 30
)
self.assertEqual(tuple(selected.shape), (16384, 512))
del selected
torch.cuda.synchronize()
self.assertEqual(torch.cuda.memory_allocated(), baseline)
del inputs, candidates
if __name__ == "__main__":
unittest.main()
@@ -10,7 +10,7 @@ from sglang.kernels.ops.attention.dsa_metadata import (
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=10, stage="base-b", runner_config="1-gpu-large")
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_amd_ci(est_time=15, stage="stage-b", runner_config="1-gpu-large-amd")
@@ -0,0 +1,139 @@
"""Lifetime of the DSA multi-CTAs KV counter across CUDA graph capture.
The decode graphs record this buffer's address, so nothing after capture may
reallocate it. _forward_trtllm, the production caller, needs a live FlashInfer
kernel and is not covered here.
"""
import unittest
import weakref
from types import SimpleNamespace
import torch
from sglang.srt.layers.attention.dsa_backend import DeepseekSparseAttnBackend
from sglang.srt.layers.attention.trtllm_mla_backend import (
TRTLLM_MLA_MAX_BATCH_SIZE,
grow_multi_ctas_kv_counter_buffer_if_needed,
make_persistent_multi_ctas_kv_counter_buffer,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=15, stage="base-b-kernel-unit", runner_config="1-gpu-large")
_NUM_Q_HEADS = 128
_MAX_CTX_LEN = 64
# 1024 captured requests x 9 draft tokens: a supported speculative capture whose
# query-row count exceeds TRTLLM_MLA_MAX_BATCH_SIZE.
_CAPTURED_BS = 1024
_NUM_DRAFT_TOKENS = 9
_NUM_CAPTURED_ROWS = _CAPTURED_BS * _NUM_DRAFT_TOKENS
def _make_backend(*, allocate_counter: bool = True):
backend = object.__new__(DeepseekSparseAttnBackend)
backend.device = "cuda"
backend.num_q_heads = _NUM_Q_HEADS
backend.real_page_size = 64
backend.hisparse_coordinator = None
backend.speculative_num_draft_tokens = _NUM_DRAFT_TOKENS
backend.dsa_index_kpool = 1
backend.use_fused_topk = False
backend.dsa_topk_backend = SimpleNamespace(should_use_topk_v2=lambda: False)
backend.dsa_index_topk = 2048
backend.dsa_decode_impl = "trtllm"
backend.req_to_token = torch.zeros(
8, _MAX_CTX_LEN, dtype=torch.int32, device="cuda"
)
backend._multi_ctas_kv_counter_buffer = (
make_persistent_multi_ctas_kv_counter_buffer(
device=torch.device("cuda"),
num_q_heads=_NUM_Q_HEADS,
max_batch_size=48,
)
if allocate_counter
else None
)
return backend
def _would_grow(backend, rows: int) -> bool:
return (
grow_multi_ctas_kv_counter_buffer_if_needed(
buffer=backend._multi_ctas_kv_counter_buffer,
device=torch.device("cuda"),
num_q_heads=backend.num_q_heads,
batch_size=rows,
)
is not backend._multi_ctas_kv_counter_buffer
)
@unittest.skipUnless(torch.cuda.is_available(), "needs a CUDA device")
class TestMultiCtasKvCounterLifetime(CustomTestCase):
def test_request_sized_counter_would_grow_at_capture(self):
"""The premise: sizing by requests undercounts captured query rows."""
self.assertGreater(_NUM_CAPTURED_ROWS, TRTLLM_MLA_MAX_BATCH_SIZE)
self.assertTrue(_would_grow(_make_backend(), _NUM_CAPTURED_ROWS))
def test_init_cuda_graph_state_sizes_for_query_rows(self):
backend = _make_backend()
backend.init_cuda_graph_state(
max_bs=_CAPTURED_BS, max_num_tokens=_NUM_CAPTURED_ROWS
)
self.assertFalse(_would_grow(backend, _NUM_CAPTURED_ROWS))
def test_init_cuda_graph_state_is_grow_only(self):
"""A later, smaller graph must not discard an earlier allocation."""
backend = _make_backend()
backend.init_cuda_graph_state(
max_bs=_CAPTURED_BS, max_num_tokens=_NUM_CAPTURED_ROWS
)
sized = backend._multi_ctas_kv_counter_buffer
backend.init_cuda_graph_state(max_bs=8, max_num_tokens=64)
self.assertIs(backend._multi_ctas_kv_counter_buffer, sized)
def test_init_cuda_graph_state_tolerates_backends_without_a_counter(self):
"""Non-TRT-LLM branches leave the counter None; sizing must not read it."""
backend = _make_backend(allocate_counter=False)
backend.init_cuda_graph_state(
max_bs=_CAPTURED_BS, max_num_tokens=_NUM_CAPTURED_ROWS
)
self.assertIsNone(backend._multi_ctas_kv_counter_buffer)
def test_counter_field_defaults_to_none_on_the_class(self):
"""The sizing hook is unconditional, so every branch must leave it readable."""
self.assertIsNone(DeepseekSparseAttnBackend._multi_ctas_kv_counter_buffer)
def test_oversized_eager_call_keeps_the_captured_allocation(self):
"""Holds only a weakref and an address, so a rebinding implementation
drops the last strong reference and the assertions see it."""
backend = _make_backend()
backend.init_cuda_graph_state(
max_bs=_CAPTURED_BS, max_num_tokens=_NUM_CAPTURED_ROWS
)
captured_ref = weakref.ref(backend._multi_ctas_kv_counter_buffer)
captured_ptr = backend._multi_ctas_kv_counter_buffer.data_ptr()
counter = backend._multi_ctas_kv_counter_for(_NUM_CAPTURED_ROWS * 2)
self.assertIsNot(counter, backend._multi_ctas_kv_counter_buffer)
del counter
self.assertIsNotNone(captured_ref())
self.assertIs(backend._multi_ctas_kv_counter_buffer, captured_ref())
self.assertEqual(backend._multi_ctas_kv_counter_buffer.data_ptr(), captured_ptr)
def test_within_capacity_eager_call_reuses_the_captured_allocation(self):
backend = _make_backend()
backend.init_cuda_graph_state(
max_bs=_CAPTURED_BS, max_num_tokens=_NUM_CAPTURED_ROWS
)
self.assertIs(
backend._multi_ctas_kv_counter_for(_NUM_CAPTURED_ROWS),
backend._multi_ctas_kv_counter_buffer,
)
if __name__ == "__main__":
unittest.main()
@@ -7,13 +7,19 @@ import sglang.kernels.ops.attention.dsa.transform_index as transform_index_modul
from sglang.kernels.ops.attention.dsa.transform_index import (
transform_index_page_table_decode_fast,
transform_index_page_table_prefill_fast,
transform_index_page_table_prefill_ref,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=9, stage="base-b", runner_config="1-gpu-large")
register_cuda_ci(est_time=9, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_amd_ci(est_time=9, suite="stage-b-test-1-gpu-small-amd-mi35x")
TOPK = 2048
# k-pool appends up to index_kpool - 1 open-tail tokens to index_topk, so the
# width the indexer hands over is not a power of two: 2048 + 4 - 1 for
# GLM-5.3-Flash. See get_dsa_mtp_topk_width() in srt/configs/model_config.py.
KPOOL_TOPK = 2051
@unittest.skipUnless(torch.cuda.is_available(), "CUDA is required for this test.")
@@ -33,9 +39,11 @@ class TestDSATransformIndex(CustomTestCase):
)
return columns.unsqueeze(0) + row_bias
def _make_topk(self, rows: int, context_length: int) -> torch.Tensor:
def _make_topk(
self, rows: int, context_length: int, topk_width: int = TOPK
) -> torch.Tensor:
topk = (
torch.arange(TOPK, dtype=torch.int64, device=self.device)
torch.arange(topk_width, dtype=torch.int64, device=self.device)
.remainder(context_length)
.repeat(rows, 1)
)
@@ -52,10 +60,11 @@ class TestDSATransformIndex(CustomTestCase):
extend_lens_cpu: list[int],
output_num_tokens: int,
page_table_is_expanded: bool,
topk_width: int = TOPK,
) -> torch.Tensor:
real_num_tokens = sum(extend_lens_cpu)
expected = torch.full(
(output_num_tokens, TOPK),
(output_num_tokens, topk_width),
-1,
dtype=torch.int32,
device=self.device,
@@ -91,14 +100,15 @@ class TestDSATransformIndex(CustomTestCase):
*,
zero_row_stride: bool = False,
provide_result: bool = False,
topk_width: int = TOPK,
) -> None:
if zero_row_stride:
page_table = self._make_page_table(1, context_length).expand(batch_size, -1)
else:
page_table = self._make_page_table(batch_size, context_length)
topk_indices = self._make_topk(batch_size, context_length)
topk_indices = self._make_topk(batch_size, context_length, topk_width)
expected = torch.empty(
(batch_size, TOPK), dtype=torch.int32, device=self.device
(batch_size, topk_width), dtype=torch.int32, device=self.device
)
torch.gather(
page_table,
@@ -127,6 +137,7 @@ class TestDSATransformIndex(CustomTestCase):
page_table_is_expanded: bool,
topk_padding: int = 0,
output_padding: int = 0,
topk_width: int = TOPK,
) -> None:
real_num_tokens = sum(extend_lens_cpu)
page_table_rows = (
@@ -135,13 +146,14 @@ class TestDSATransformIndex(CustomTestCase):
topk_num_tokens = real_num_tokens + topk_padding
output_num_tokens = topk_num_tokens + output_padding
page_table = self._make_page_table(page_table_rows, context_length)
topk_indices = self._make_topk(topk_num_tokens, context_length)
topk_indices = self._make_topk(topk_num_tokens, context_length, topk_width)
expected = self._expected(
page_table,
topk_indices,
extend_lens_cpu,
output_num_tokens,
page_table_is_expanded,
topk_width,
)
actual = transform_index_page_table_prefill_fast(
@@ -235,6 +247,43 @@ class TestDSATransformIndex(CustomTestCase):
output_padding=8,
)
def test_prefill_kpool_tail_width(self):
extend_lens_cpu = [0, 3, 1, 0, 4]
real_num_tokens = sum(extend_lens_cpu)
topk_num_tokens = real_num_tokens + 5
output_num_tokens = topk_num_tokens + 7
context_length = 8192
# KPool can append three tail positions after the 2048 history indices.
tail_indices = torch.tensor(
[context_length - 3, context_length - 2, context_length - 1],
dtype=torch.int64,
device=self.device,
).repeat(topk_num_tokens, 1)
topk_indices = torch.cat(
[self._make_topk(topk_num_tokens, context_length), tail_indices], dim=1
)
topk_indices[0, -1] = -1
self.assertEqual(topk_indices.shape[1], 2051)
for page_table_is_expanded in (False, True):
with self.subTest(page_table_is_expanded=page_table_is_expanded):
page_table_rows = (
real_num_tokens if page_table_is_expanded else len(extend_lens_cpu)
)
page_table = self._make_page_table(page_table_rows, context_length)
kwargs = dict(
page_table=page_table,
topk_indices=topk_indices,
extend_lens_cpu=extend_lens_cpu,
output_num_tokens=output_num_tokens,
page_table_is_expanded=page_table_is_expanded,
)
expected = transform_index_page_table_prefill_ref(**kwargs)
actual = transform_index_page_table_prefill_fast(**kwargs)
torch.cuda.synchronize()
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
def test_large_batch_size(self):
self._check_case(
[1] * 8192,
@@ -265,6 +314,26 @@ class TestDSATransformIndex(CustomTestCase):
self._check_decode_case(8192, 4096)
self._check_decode_case(2, 1_000_000)
def test_decode_fast_kpool_widths(self):
# 2051 is the GLM-5.3-Flash k-pool width; the others cover a partial
# trailing tile and a width below one tile.
for topk_width in (KPOOL_TOPK, 515, 257):
with self.subTest(topk_width=topk_width):
self._check_decode_case(17, 8192, topk_width=topk_width)
self._check_decode_case(
17, 8192, topk_width=topk_width, provide_result=True
)
def test_prefill_kpool_widths(self):
for topk_width in (KPOOL_TOPK, 515, 257):
with self.subTest(topk_width=topk_width):
self._check_case(
[2, 1],
4096,
page_table_is_expanded=False,
topk_width=topk_width,
)
if __name__ == "__main__":
unittest.main()
@@ -23,7 +23,7 @@ from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(
est_time=240,
stage="base-b",
stage="base-b-kernel-unit",
runner_config="1-gpu-small",
)
@@ -14,7 +14,7 @@ from sglang.kernels.ops.mamba.causal_conv1d_triton import (
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=90, stage="base-b", runner_config="1-gpu-large")
register_cuda_ci(est_time=90, stage="base-b-kernel-unit", runner_config="1-gpu-large")
_DEVICE = "cuda"
@@ -204,7 +204,7 @@ def _compare_case(case, num_warps, use_ring=False):
rings_fus = {name: buf.clone() for name, buf in template.items()}
else:
rings_ref = rings_fus = None
o_ref, conv_ref, win_ref, ic_ref = _run_reference(
o_ref, _, win_ref, ic_ref = _run_reference(
inp, B, T, H, HV, K, V, lower_bound, rings=rings_ref
)
o_fus, conv_fus, win_fus, ic_fus = _run_fused(
@@ -213,13 +213,13 @@ def _compare_case(case, num_warps, use_ring=False):
idx_vals = inp["idx_vals"]
valid_rows = [i for i, slot in enumerate(idx_vals) if slot >= 0]
touched_slots = [slot for slot in idx_vals if slot >= 0]
o_ref_v = o_ref.reshape(B, T, HV, V)[valid_rows]
o_fus_v = o_fus.reshape(B, T, HV, V)[valid_rows]
# One bf16 ulp: the fused and reference tiles reduce K in different orders.
torch.testing.assert_close(o_fus_v, o_ref_v, rtol=2**-7, atol=1e-7)
assert torch.equal(conv_ref[touched_slots], conv_fus[touched_slots])
# conv_state is read-only in verify; the commit scatter advances it.
assert torch.equal(inp["conv_pool"], conv_fus)
assert torch.equal(win_ref[valid_rows], win_fus[valid_rows])
if use_ring:
# Full-tensor bitwise: ring values are elementwise (conv FMA chain,
@@ -239,6 +239,28 @@ def test_matches_unfused_reference(case):
_compare_case(case, num_warps=4)
def test_output_does_not_depend_on_cta_scheduling():
"""The verify output must not change with how the CTAs happen to be
scheduled. H=1 with HV=16 shares one Q/K history across 16 V tiles."""
if torch.cuda.get_device_capability()[0] < 9:
pytest.skip("green contexts need SM90 or newer")
from flashinfer.green_ctx import split_device_green_ctx_by_sm_count
case = (1, 6, 1, 16, 128, 128, 4, False, None, False, 1)
B, T, H, HV, K, V, W, has_bias, lower_bound, neg_slot, seed = case
inp = _make_inputs(B, T, H, HV, K, V, W, has_bias, neg_slot, seed)
full = _run_fused(inp, B, T, H, HV, K, V, lower_bound, num_warps=4)[0]
streams, _ = split_device_green_ctx_by_sm_count(torch.device("cuda:0"), [8])
# The green stream is non-blocking, so it must be told to wait for the
# inputs produced above; synchronize() afterwards only waits on the consumer.
streams[0].wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(streams[0]):
squeezed = _run_fused(inp, B, T, H, HV, K, V, lower_bound, num_warps=4)[0]
streams[0].synchronize()
assert torch.equal(full, squeezed)
@pytest.mark.parametrize("case", _RING_CASES)
def test_replayssm_ring_matches_unfused(case):
_compare_case(case, num_warps=4, use_ring=True)
@@ -11,6 +11,7 @@ import sys
import pytest
import torch
from sglang.srt.utils import is_gfx95_supported
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
try:
@@ -19,6 +20,7 @@ try:
fused_recurrent_gated_delta_rule_update,
)
from sglang.kernels.ops.attention.fla.fused_sigmoid_gating_recurrent import (
_select_recurrent_launch_config,
fused_sigmoid_gating_delta_rule_update,
)
@@ -180,6 +182,57 @@ def test_fused_gdn_mtp_precision(N: int, T: int):
torch.testing.assert_close(out_ref, out_fused, rtol=1e-2, atol=1e-2)
@pytest.mark.skipif(not KERNELS_AVAILABLE, reason="Kernel not available")
@pytest.mark.parametrize("N", [1, 3, 16])
def test_qwen35_tp4_fused_gdn_mtp_precision(N: int):
"""Exercise the gfx950 TP4 launch shape against the reference path."""
T, H, HV, K, V = 4, 4, 16, 128, 128
A_log, dt_bias, a, b, q, k, v, state, indices, cu_seqlens = _make_tensors(
N, T, H, HV, K, V
)
out_ref = run_reference(
A_log,
dt_bias,
q,
k,
v,
a,
b,
state.clone(),
indices,
cu_seqlens,
disable_state_update=True,
)
out_fused = run_fused_mtp(
A_log,
dt_bias,
q,
k,
v,
a,
b,
state.clone(),
indices,
cu_seqlens,
disable_state_update=True,
)
torch.testing.assert_close(out_ref, out_fused, rtol=1e-2, atol=1e-2)
@pytest.mark.skipif(
not (torch.version.hip and is_gfx95_supported()), reason="requires AMD gfx95"
)
def test_qwen35_tp4_launch_config_is_narrow():
assert _select_recurrent_launch_config(1, 4, 16, 128, 128, False) == (8, 4)
assert _select_recurrent_launch_config(3, 4, 16, 128, 128, False) == (16, 2)
assert _select_recurrent_launch_config(32, 4, 16, 128, 128, False) == (16, 2)
assert _select_recurrent_launch_config(33, 4, 16, 128, 128, False) == (32, 1)
assert _select_recurrent_launch_config(3, 8, 32, 128, 128, False) == (32, 1)
assert _select_recurrent_launch_config(3, 4, 16, 128, 128, True) == (32, 1)
@pytest.mark.skipif(not KERNELS_AVAILABLE, reason="Kernels not available")
@pytest.mark.parametrize("N", [1, 16, 128])
def test_mtp_single_step_decode(N: int):
@@ -14,7 +14,7 @@ from sglang.kernels.ops.attention.triton_gdn_fused_proj import (
from sglang.kernels.ops.mamba.causal_conv1d_triton import causal_conv1d_update
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=7, stage="base-b", runner_config="1-gpu-large")
register_cuda_ci(est_time=7, stage="base-b-kernel-unit", runner_config="1-gpu-large")
def _reference(
@@ -1,4 +1,5 @@
import unittest
from unittest.mock import patch
import torch
import torch.nn.functional as F
@@ -10,6 +11,8 @@ from sglang.kernels.ops.attention.linear.kda_nvidia_prefill import (
from sglang.kernels.ops.attention.linear.kda_ptx_prefill import (
chunk_kda_fwd as ptx_chunk_kda_fwd,
)
from sglang.srt.layers.attention.linear.kernels.kda_ptx import PtxKDAKernel
from sglang.srt.layers.attention.linear.kernels.kda_triton import TritonKDAKernel
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
@@ -79,6 +82,45 @@ def _reference(q, k, v, gate, beta, a_log, dt_bias, state, fused_qk_norm):
class TestKdaPrefill(CustomTestCase):
@torch.inference_mode()
def test_ptx_padded_raw_beta(self):
"""Raw beta must match Triton, including final state after neutral padding."""
if not torch.cuda.is_available() or torch.cuda.get_device_capability() != (
10,
3,
):
self.skipTest("PTX KDA prefill requires GB300")
q, k, v, gate, beta, a_log, dt_bias, state = _inputs(2, seq_len=1025)
state.fill_(0.1)
actual_state = state.clone()
inputs = dict(
q=q,
k=k,
v=v,
g=gate,
beta=beta,
cache_indices=torch.zeros(1, device="cuda", dtype=torch.int32),
query_start_loc=torch.tensor([0, 1025], device="cuda", dtype=torch.int32),
A_log=a_log,
dt_bias=dt_bias,
lower_bound=-5.0,
beta_is_raw=True,
extend_seq_lens_cpu=[1025],
)
kernel = PtxKDAKernel()
with patch.object(
kernel._triton,
"extend",
side_effect=AssertionError("PTX unexpectedly fell back to Triton"),
):
actual = kernel.extend(**inputs, ssm_states=actual_state)
# Triton may mutate inputs, so run the reference last.
expected = TritonKDAKernel().extend(**inputs, ssm_states=state)
torch.testing.assert_close(
actual.float(), expected.float(), rtol=2e-2, atol=3e-2
)
torch.testing.assert_close(actual_state, state, rtol=2e-2, atol=3e-2)
@torch.inference_mode()
def test_nvidia_prefill(self):
if not torch.cuda.is_available() or torch.cuda.get_device_capability()[0] != 10:
@@ -22,6 +22,9 @@ cluster floor and pool size are per-arch (see topk_v2.cuh), so the (batch, seq)
grid below brackets the fixed boundaries (8192/8193, 16384/16385) exactly and
spans the arch-dependent ones, across k in {512,1024,2048} and identity/perm
page tables.
``test_topk_v2_packed_rows`` covers the DSA extend layout on top of that: all
requests packed into one score buffer, sharing a table row per request.
"""
from __future__ import annotations
@@ -33,9 +36,11 @@ import torch
from sglang.kernels.ops.attention.dsv4.topk import (
plan_topk_v2,
topk_transform_packed_v2,
topk_transform_paged_v2,
topk_transform_ragged_v2,
)
from sglang.srt.utils import is_hip
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_cuda_ci(est_time=90, stage="base-b-kernel-unit", runner_config="1-gpu-large")
@@ -465,5 +470,143 @@ def test_topk_v2_ragged_no_row_starts(k: int) -> None:
assert sorted(explicit[i]) == sorted(implicit[i]), f"row {i} differs"
@pytest.mark.skipif(
not is_hip(), reason="packed layout is compiled under USE_ROCM only"
)
@pytest.mark.parametrize("k", [512, 2048])
@pytest.mark.parametrize(
"extend_lens",
[
[7], # one request
[4, 4], # equal row counts
[1, 13, 2], # ragged, including a single-row request
],
)
@torch.inference_mode()
def test_topk_v2_packed_rows(extend_lens: list[int], k: int) -> None:
"""DSA extend layout: batch-global packed scores + shared page-table rows.
Rows are causal within a request; a distinct page-table permutation per request
catches row/request index mix-ups, and the ragged case leaves most window starts
off the 16-byte load boundary (the production case).
"""
torch.manual_seed(4242 + k + len(extend_lens))
device = "cuda"
# Keep every row longer than k so no row takes the trivial path.
prefix = k + 1024
kv_lens = [prefix + e for e in extend_lens]
k_offsets = [0]
for kv in kv_lens[:-1]:
k_offsets.append(k_offsets[-1] + kv)
total_kv = sum(kv_lens)
row_starts, lengths, row_to_batch = [], [], []
for i, e in enumerate(extend_lens):
for local in range(e):
row_starts.append(k_offsets[i])
lengths.append(kv_lens[i] - e + local + 1)
row_to_batch.append(i)
rows = len(lengths)
width = (total_kv + 3) & ~3
scores = torch.randn(rows, width, dtype=torch.float32, device=device)[:, :total_kv]
lengths_t = torch.tensor(lengths, dtype=torch.int32, device=device)
row_starts_t = torch.tensor(row_starts, dtype=torch.int32, device=device)
row_to_batch_t = torch.tensor(row_to_batch, dtype=torch.int32, device=device)
num_pages = (max(kv_lens) + PAGE_SIZE - 1) // PAGE_SIZE
page_table, inv_cpu = _make_page_table(
len(extend_lens), num_pages, "perm", device, per_row=True
)
out = torch.full((rows, k), -1, dtype=torch.int32, device=device)
# The kernel masks in place, so reference values must be read before the call.
scores_cpu = scores.cpu()
topk_transform_packed_v2(
scores,
lengths_t,
page_table,
out,
PAGE_SIZE,
row_starts=row_starts_t,
row_to_batch=row_to_batch_t,
)
torch.cuda.synchronize()
out_cpu = out.cpu().tolist()
for r in range(rows):
L, start, req = lengths[r], row_starts[r], row_to_batch[r]
window = scores_cpu[r, start : start + L]
ref = torch.topk(window, k, sorted=False).indices.tolist()
our = _invert(out_cpu[r], inv_cpu[req])
_assert_topk_close(window.unsqueeze(0), [ref], [our], 1, [L], k)
@pytest.mark.skipif(
not is_hip(), reason="packed layout is compiled under USE_ROCM only"
)
@pytest.mark.parametrize("residue", [1, 2, 3])
@pytest.mark.parametrize("boundary", [8192, 16384])
@torch.inference_mode()
def test_topk_v2_packed_level_boundary(boundary: int, residue: int) -> None:
"""Rows whose length sits on an implementation's max_seq_len boundary.
The masked head widens the problem by ``residue``, so a row of exactly
``boundary`` tokens spills past the register implementation sized for it. The
packed kernel picks the implementation per row from the widened length, so
these must still be exact; a compile-time choice made from the un-widened
length would overflow.
"""
torch.manual_seed(boundary + residue)
device = "cuda"
k = 512
# Request 0 exists only to push request 1's window off the 16-byte boundary.
kv_lens = [residue, boundary + 1]
lengths = [residue] + [boundary - 1, boundary, boundary + 1]
row_starts = [0] + [residue] * 3
row_to_batch = [0, 1, 1, 1]
rows = len(lengths)
total_kv = sum(kv_lens)
width = (total_kv + 3) & ~3
scores = torch.randn(rows, width, dtype=torch.float32, device=device)[:, :total_kv]
lengths_t = torch.tensor(lengths, dtype=torch.int32, device=device)
row_starts_t = torch.tensor(row_starts, dtype=torch.int32, device=device)
row_to_batch_t = torch.tensor(row_to_batch, dtype=torch.int32, device=device)
num_pages = (max(kv_lens) + PAGE_SIZE - 1) // PAGE_SIZE
page_table, inv_cpu = _make_page_table(
len(kv_lens), num_pages, "perm", device, per_row=True
)
out = torch.full((rows, k), -1, dtype=torch.int32, device=device)
scores_cpu = scores.cpu()
topk_transform_packed_v2(
scores,
lengths_t,
page_table,
out,
PAGE_SIZE,
row_starts=row_starts_t,
row_to_batch=row_to_batch_t,
)
torch.cuda.synchronize()
out_cpu = out.cpu().tolist()
for r in range(rows):
L, start, req = lengths[r], row_starts[r], row_to_batch[r]
window = scores_cpu[r, start : start + L]
our = _invert(out_cpu[r], inv_cpu[req])
if L <= k:
# Trivial path: every position, then -1 padding.
assert sorted(our[:L]) == list(range(L)), f"row {r} trivial output wrong"
assert all(v == -1 for v in out_cpu[r][L:]), f"row {r} padding wrong"
continue
ref = torch.topk(window, k, sorted=False).indices.tolist()
_assert_topk_close(window.unsqueeze(0), [ref], [our], 1, [L], k)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -37,6 +37,7 @@ from sglang.kernels.ops.communication.mp import register_comm_cleanup
from sglang.srt.distributed.device_communicators.custom_all_reduce_v2 import (
CustomAllReduceV2,
)
from sglang.srt.runtime_context import get_parallel
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kernels.utils import multigpu_pytest_main
@@ -129,6 +130,7 @@ def _init_cpu_group_once() -> dist.ProcessGroup:
local_rank=local_rank,
backend="nccl",
)
get_parallel().override_permanently(world_group=coord)
atexit.register(dist.destroy_process_group)
cpu_group = coord.cpu_group
assert isinstance(cpu_group, dist.ProcessGroup)
@@ -14,6 +14,7 @@ from sglang.srt.distributed import parallel_state as ps
from sglang.srt.environ import envs
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kernels.utils import multigpu_pytest_main
from sglang.test.test_utils import publish_build_topology
register_cuda_ci(est_time=45, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
@@ -37,7 +38,8 @@ def group():
local_rank=local_rank,
distributed_init_method="env://",
)
ps.initialize_model_parallel(tensor_model_parallel_size=world_size)
publish_build_topology(tp_size=world_size, world_rank=rank)
ps.initialize_model_parallel()
yield ps.get_tp_group()
ps.destroy_model_parallel()
ps.destroy_distributed_environment()
@@ -31,6 +31,7 @@ from sglang.srt.distributed.device_communicators.triton_symm_mem_ag import (
all_gather_inner,
create_state,
)
from sglang.srt.runtime_context import get_parallel
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kernels.utils import multigpu_pytest_main
@@ -70,6 +71,7 @@ def _init_cpu_group_once() -> dist.ProcessGroup:
local_rank=local_rank,
backend="nccl",
)
get_parallel().override_permanently(world_group=ps._WORLD)
atexit.register(dist.destroy_process_group)
logging.disable(logging.INFO)
torch.cuda.set_stream(torch.cuda.Stream())
@@ -24,6 +24,7 @@ from sglang.kernels.ops.communication.mp import register_comm_cleanup
from sglang.srt.distributed.device_communicators.custom_all_reduce_v2 import (
CustomAllReduceV2,
)
from sglang.srt.runtime_context import get_parallel
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kernels.utils import multigpu_pytest_main
@@ -92,6 +93,7 @@ def _init_cpu_group_once() -> dist.ProcessGroup:
local_rank=local_rank,
backend="nccl",
)
get_parallel().override_permanently(world_group=coord)
atexit.register(dist.destroy_process_group)
cpu_group = coord.cpu_group
assert isinstance(cpu_group, dist.ProcessGroup)
@@ -6,6 +6,7 @@ import pytest
import torch
from torch import nn
from sglang.srt.configs.qwen4_exp import Qwen4ExpTextConfig
from sglang.srt.layers.quantization.unquant import UnquantizedEmbeddingMethod
from sglang.srt.layers.vocab_parallel_embedding import (
VocabParallelEmbeddingShardIndices,
@@ -15,6 +16,7 @@ from sglang.srt.models.qwen4_exp import (
Qwen4ExpPinnedHostEmbedding,
Qwen4ExpPLELayer,
)
from sglang.srt.runtime_context import get_context, get_parallel
from sglang.srt.utils import set_weight_attrs
from sglang.test.ci.ci_register import register_cuda_ci
@@ -191,6 +193,52 @@ def test_qwen4_ple_prefetch_buffer_lifecycle(monkeypatch):
assert set(layer._graph_prefetch_buffers) == {3, 5}
@pytest.fixture
def single_rank_runtime_context():
"""``Qwen4ExpPLELayer.__init__`` reads the TP topology through
``VocabParallelEmbedding``; pin it to one rank without a process group."""
override = get_context().override_server_args(tp_size=1)
override.install()
try:
with get_parallel().override(
tp_rank=0, tp_size=1, attn_tp_rank=0, attn_tp_size=1
):
yield
finally:
override.restore()
def test_qwen4_ple_offload_avoids_device_table(single_rank_runtime_context):
# sgl-project/sglang#39841: the table was built on the device before the
# host table existed, so the flag needed a full per-rank shard of free VRAM.
# Small everywhere except the n-gram table (16 heads x ~20k rows x 4 dims),
# which must dominate the layer's footprint for the peak check to bite.
config = Qwen4ExpTextConfig(
vocab_size=64,
hidden_size=16,
hc_count=2,
ple_embed_dim=64,
ngram_size=3,
heads_per_ngram=8,
ngram_vocab_size_base=20_000,
eos_token_id=1,
ple_offload_embedding=True,
)
torch.cuda.synchronize()
torch.cuda.reset_peak_memory_stats()
base = torch.cuda.memory_allocated()
with torch.device("cuda"): # the model loader builds every layer this way
layer = Qwen4ExpPLELayer(config, prefix="ple", layer_id=0, ple_layer_index=0)
peak = torch.cuda.max_memory_allocated() - base
emb = layer.ple_embedding.ngram_embedding
table_bytes = emb.weight.numel() * emb.weight.element_size()
assert peak < table_bytes // 2, (peak, table_bytes)
assert emb.weight.device.type == "cpu" and emb.weight.is_pinned()
assert emb.weight_scale.is_cuda
assert not any(t.is_meta for t in (*layer.parameters(), *layer.buffers()))
def _file_backend_supported() -> bool:
from sglang.srt.models.qwen4_exp_ple_table import device_uses_host_page_tables
@@ -30,6 +30,7 @@ from sglang.kernels.ops.kimi_k3 import all_reduce
from sglang.srt.distributed.device_communicators.custom_all_reduce_v2 import (
CustomAllReduceV2,
)
from sglang.srt.runtime_context import get_parallel
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kernels.utils import multigpu_pytest_main
@@ -65,6 +66,7 @@ def _init_world():
local_rank=local_rank,
backend="nccl",
)
get_parallel().override_permanently(world_group=coord)
atexit.register(dist.destroy_process_group)
logging.disable(logging.INFO)
torch.cuda.set_stream(torch.cuda.Stream())
@@ -20,6 +20,7 @@ from sglang.kernels.ops.kimi_k3 import (
from sglang.srt.distributed.device_communicators.custom_all_reduce_v2 import (
CustomAllReduceV2,
)
from sglang.srt.runtime_context import get_parallel
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kernels.utils import multigpu_pytest_main
@@ -53,6 +54,7 @@ def _init_world():
local_rank=local_rank,
backend="nccl",
)
get_parallel().override_permanently(world_group=coord)
atexit.register(dist.destroy_process_group)
cpu_group = coord.cpu_group
assert isinstance(cpu_group, dist.ProcessGroup)
@@ -20,6 +20,7 @@ from sglang.kernels.ops.kv_canary.verify import (
from sglang.kernels.ops.kv_canary.verify_ref import (
_compute_real_kv_hash_scalar,
launch_canary_verify_kernel_torch_reference,
materialize_real_kv_sources,
)
from sglang.kernels.ops.kv_canary.write_ref import (
launch_canary_write_kernel_torch_reference,
@@ -917,14 +918,18 @@ class TestRealKvHash:
positions = [0, 1, 2]
running = splitmix64(consts.CANARY_CHAIN_ANCHOR)
host_sources = materialize_real_kv_sources(
real_kv_sources=sources_cuda,
real_kv_hash_mode=consts.RealKvHashMode.ALL,
slot_indices=slot_indices,
work_device=torch.device("cpu"),
)
real_kv_hashes: list[int] = []
for slot_idx in slot_indices:
real_kv_hashes.append(
_compute_real_kv_hash_scalar(
real_kv_sources=sources_cuda,
real_kv_hash_mode=consts.RealKvHashMode.ALL,
slot_idx=slot_idx,
work_device=torch.device("cpu"),
host_sources=host_sources,
)
)
@@ -1030,6 +1035,21 @@ class TestRealKvSource:
read_bytes=0,
)
def test_real_kv_source_rejects_row_narrower_than_page(self) -> None:
"""A row too narrow for its page must raise: neither fold reports it.
The CUDA fold reads past the row and the torch fold's dim-1 slice clamps to
the row end, so the tail slots of the page hash 0 bytes and the chain still
verifies clean.
"""
with pytest.raises(ValueError, match="page_size"):
RealKvSource(
tensor=torch.zeros((1, 16), dtype=torch.uint8, device=_DEVICE),
page_size=2,
num_bytes_per_token=16,
read_bytes=16,
)
def test_real_kv_source_padding_below_4(self) -> None:
"""Host wrapper pads to 4 slots when fewer sources are supplied; dummy slots are never dereferenced."""
buf_pair = _buf_pair()
@@ -1310,12 +1330,16 @@ class TestLayoutAndScheduling:
# byte-by-byte loop, so the stamped real_kv_hash matches what the kernel /
# verify reference will recompute. A byte-by-byte fold was the previous bug
# here and triggered REAL_KV_HASH violations on otherwise clean chains.
host_sources = materialize_real_kv_sources(
real_kv_sources=sources_cuda,
real_kv_hash_mode=consts.RealKvHashMode.ALL,
slot_indices=slot_indices,
work_device=_DEVICE,
)
rkv_values = [
_compute_real_kv_hash_scalar(
slot_idx=slot_idx,
real_kv_sources=sources_cuda,
real_kv_hash_mode=consts.RealKvHashMode.ALL,
work_device=_DEVICE,
host_sources=host_sources,
)
for slot_idx in slot_indices
]
@@ -0,0 +1,173 @@
import sys
import pytest
import torch
from sglang.kernels.ops.kvcache.hicache import _jit_hicache_tma_module
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=60, stage="base-b-kernel-unit", runner_config="1-gpu-large")
pytestmark = pytest.mark.skipif(
not torch.cuda.is_available()
or torch.version.hip is not None
or torch.cuda.get_device_capability()[0] < 9,
reason="HiCache TMA kernel requires SM90+",
)
POOL_TOKENS = 8192
NUM_LAYERS = 3
ROW_DIM = 256 # 512-byte bf16 rows: below the register kernel's 128 B unit width x 4
def _token_indices(num_tokens: int, page_size: int, dtype: torch.dtype, seed: int):
gen = torch.Generator().manual_seed(seed)
pages = torch.randperm(POOL_TOKENS // page_size, generator=gen)[
: num_tokens // page_size
]
idx = (pages[:, None] * page_size + torch.arange(page_size)).reshape(-1)
return idx.to(device="cuda", dtype=dtype)
def _fill(t: torch.Tensor, seed: int) -> None:
t.view(torch.int16).copy_(
torch.randint(
0,
30000,
t.shape,
dtype=torch.int16,
generator=torch.Generator().manual_seed(seed),
)
)
def _host_view(layout: str, layer: int):
if layout == "layer_first":
return torch.empty(POOL_TOKENS, ROW_DIM, dtype=torch.bfloat16, pin_memory=True)
# page_first: [tokens, layers, dim]; a per-layer view has strided rows
return torch.empty(
POOL_TOKENS, NUM_LAYERS, ROW_DIM, dtype=torch.bfloat16, pin_memory=True
)[:, layer]
@pytest.mark.parametrize("host_layout", ["layer_first", "page_first"])
@pytest.mark.parametrize("index_dtype", [torch.int64, torch.int32])
@pytest.mark.parametrize("page_size", [128, 1])
def test_one_layer_roundtrip(
host_layout: str, index_dtype: torch.dtype, page_size: int
) -> None:
"""H2D then D2H of one layer; page runs take the single-op paths (bulk copy,
tensor-map box, bulk store), scattered rows take the per-row paths, and the
odd token count leaves a partial tail chunk."""
module = _jit_hicache_tma_module(block_quota=2)
num_tokens = 2048 + (96 if page_size == 1 else 0)
k_host, v_host = _host_view(host_layout, 1), _host_view(host_layout, 2)
k_dev = torch.zeros(POOL_TOKENS, ROW_DIM, dtype=torch.bfloat16, device="cuda")
v_dev = torch.zeros_like(k_dev)
_fill(k_host, 1)
_fill(v_host, 2)
host_idx = _token_indices(num_tokens, page_size, index_dtype, seed=3)
dev_idx = _token_indices(num_tokens, page_size, index_dtype, seed=4)
module.launch_one(k_dev, v_dev, dev_idx, k_host, v_host, host_idx)
torch.cuda.synchronize()
assert torch.equal(k_dev[dev_idx.long()].cpu(), k_host[host_idx.cpu().long()])
assert torch.equal(v_dev[dev_idx.long()].cpu(), v_host[host_idx.cpu().long()])
untouched = torch.ones(POOL_TOKENS, dtype=torch.bool, device="cuda")
untouched[dev_idx.long()] = False
assert not k_dev[untouched].any() and not v_dev[untouched].any()
_fill(k_dev, 5)
_fill(v_dev, 6)
k_host.zero_()
v_host.zero_()
module.launch_one(k_host, v_host, host_idx, k_dev, v_dev, dev_idx)
torch.cuda.synchronize()
assert torch.equal(k_host[host_idx.cpu().long()], k_dev[dev_idx.long()].cpu())
assert torch.equal(v_host[host_idx.cpu().long()], v_dev[dev_idx.long()].cpu())
def _ptr_table(tensors) -> torch.Tensor:
return torch.tensor(
[t.data_ptr() for t in tensors], dtype=torch.uint64, device="cuda"
)
def test_all_layer_tables_lf_to_pf() -> None:
"""All-layer D2H through per-layer pointer tables into a page-first host pool
(strided destination rows), the write-back shape."""
module = _jit_hicache_tma_module(block_quota=2)
k_dev = [
torch.empty(POOL_TOKENS, ROW_DIM, dtype=torch.bfloat16, device="cuda")
for _ in range(NUM_LAYERS)
]
v_dev = [torch.empty_like(k_dev[0]) for _ in range(NUM_LAYERS)]
for i, t in enumerate(k_dev + v_dev):
_fill(t, 10 + i)
k_host = torch.zeros(
POOL_TOKENS, NUM_LAYERS, ROW_DIM, dtype=torch.bfloat16, pin_memory=True
)
v_host = torch.zeros_like(k_host).pin_memory()
host_idx = _token_indices(2048, 128, torch.int64, seed=7)
dev_idx = _token_indices(2048, 128, torch.int64, seed=8)
row_bytes = ROW_DIM * 2
module.launch_all(
_ptr_table([k_host[:, l] for l in range(NUM_LAYERS)]),
_ptr_table([v_host[:, l] for l in range(NUM_LAYERS)]),
host_idx,
_ptr_table(k_dev),
_ptr_table(v_dev),
dev_idx,
row_bytes,
NUM_LAYERS * row_bytes,
row_bytes,
)
torch.cuda.synchronize()
for l in range(NUM_LAYERS):
assert torch.equal(k_host[host_idx.cpu(), l], k_dev[l][dev_idx].cpu())
assert torch.equal(v_host[host_idx.cpu(), l], v_dev[l][dev_idx].cpu())
untouched = torch.ones(POOL_TOKENS, dtype=torch.bool)
untouched[host_idx.cpu()] = False
assert not k_host[untouched].any() and not v_host[untouched].any()
def test_mla_single_buffer() -> None:
"""MLA rows (576 x bf16 = 1152 B, not a multiple of 128 B) through the
single-buffer entry points, one layer and all layers."""
module = _jit_hicache_tma_module(block_quota=2)
dim = 576
dev = [
torch.empty(POOL_TOKENS, dim, dtype=torch.bfloat16, device="cuda")
for _ in range(NUM_LAYERS)
]
for i, t in enumerate(dev):
_fill(t, 20 + i)
host = torch.zeros(
POOL_TOKENS, NUM_LAYERS, dim, dtype=torch.bfloat16, pin_memory=True
)
host_idx = _token_indices(1024, 128, torch.int64, seed=9)
dev_idx = _token_indices(1024, 128, torch.int64, seed=10)
row_bytes = dim * 2
module.launch_all_mla(
_ptr_table([host[:, l] for l in range(NUM_LAYERS)]),
host_idx,
_ptr_table(dev),
dev_idx,
row_bytes,
NUM_LAYERS * row_bytes,
row_bytes,
)
torch.cuda.synchronize()
for l in range(NUM_LAYERS):
assert torch.equal(host[host_idx.cpu(), l], dev[l][dev_idx].cpu())
dev[0].zero_()
module.launch_one_mla(dev[0], dev_idx, host[:, 0], host_idx)
torch.cuda.synchronize()
assert torch.equal(dev[0][dev_idx].cpu(), host[host_idx.cpu(), 0])
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -3,26 +3,44 @@ import sys
import pytest
import torch
from sglang.kernels.ops.kvcache.hisparse import (
load_cache_to_device_buffer_dsv4_mla,
load_cache_to_device_buffer_mla,
transfer_cache_dsv4_mla,
from sglang.srt.utils import (
get_device,
get_device_module,
is_cuda,
is_hip,
is_xpu,
)
from sglang.test.ci.ci_register import (
register_amd_ci,
register_cuda_ci,
register_xpu_ci,
)
from sglang.srt.utils import is_cuda, is_hip, is_npu, is_xpu
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_amd_ci(est_time=30, stage="stage-b", runner_config="1-gpu-small-amd")
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_xpu_ci(est_time=60, suite="stage-b-test-1-gpu-xpu")
if is_xpu():
from sgl_kernel import (
load_cache_to_device_buffer_dsv4_mla,
load_cache_to_device_buffer_mla,
transfer_cache_dsv4_mla,
)
else:
from sglang.kernels.ops.kvcache.hisparse import (
load_blocks_to_device_buffer_mha,
load_cache_to_device_buffer_dsv4_mla,
load_cache_to_device_buffer_mla,
transfer_cache_dsv4_mla,
)
pytestmark = pytest.mark.skipif(
not torch.cuda.is_available()
or is_npu()
or is_xpu()
or not (is_cuda() or is_hip()),
reason="HiSparse JIT tests require CUDA/ROCm.",
not (is_cuda() or is_hip() or is_xpu()),
reason="HiSparse kernel tests require CUDA/ROCm/XPU.",
)
DEVICE = "cuda"
DEVICE = get_device()
DTYPE = torch.float32
KV_DIM = 8
HOT_BUFFER_SIZE = 4
@@ -131,7 +149,7 @@ def _run_kernel(
block_size=256,
num_real_reqs=torch.tensor([num_real_reqs], dtype=torch.int32, device=DEVICE),
)
torch.cuda.synchronize()
get_device_module().synchronize()
return out
@@ -171,7 +189,7 @@ def _make_state(
device_buffer[device_buffer_locs[rid, HOT_BUFFER_SIZE]].copy_(
host_cache[newest_token].to(DEVICE, non_blocking=True)
)
torch.cuda.synchronize()
get_device_module().synchronize()
return {
"host_cache": host_cache,
@@ -199,7 +217,7 @@ def test_transfer_cache_dsv4_mla_copies_paged_token() -> None:
src_indices=torch.tensor([src_loc], dtype=torch.int64, device=DEVICE),
dst_indices=torch.tensor([dst_loc], dtype=torch.int64, device=DEVICE),
)
torch.cuda.synchronize()
get_device_module().synchronize()
assert torch.equal(
_read_dsv4_token(dst_cache, dst_loc).to(DEVICE),
@@ -251,7 +269,7 @@ def test_dsv4_swap_in_reads_paged_host_layout() -> None:
block_size=256,
num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
)
torch.cuda.synchronize()
get_device_module().synchronize()
assert out.item() == swap_loc
assert torch.equal(
@@ -351,6 +369,84 @@ def test_load_cache_to_device_buffer_hits_newest_and_updates_lru() -> None:
)
@pytest.mark.skipif(is_xpu(), reason="MiniMax MHA block swap-in has no XPU kernel.")
def test_load_blocks_to_device_buffer_mha_handles_partial_newest_block() -> None:
"""A partial newest block must not consume slots for its invalid tail."""
sparse_block_size = 4
hot_buffer_size = 8
host_k = _host_cache()
host_v = _host_cache()
host_v.add_(1000)
device_k = torch.full(
(DEVICE_CACHE_SIZE, 1, KV_DIM), -1, dtype=DTYPE, device=DEVICE
)
device_v = torch.full_like(device_k, -1)
device_buffer_locs = torch.arange(
hot_buffer_size + 1, dtype=torch.int32, device=DEVICE
).view(1, -1)
device_buffer_tokens = torch.tensor(
[[0, 1, 2, 3, -1, -1, -1, -1, -1]],
dtype=torch.int32,
device=DEVICE,
)
for slot, token in enumerate([0, 1, 2, 3]):
device_k[device_buffer_locs[0, slot]].copy_(host_k[token], non_blocking=True)
device_v[device_buffer_locs[0, slot]].copy_(host_v[token], non_blocking=True)
device_k[device_buffer_locs[0, hot_buffer_size]].copy_(
host_k[10], non_blocking=True
)
device_v[device_buffer_locs[0, hot_buffer_size]].copy_(
host_v[10], non_blocking=True
)
top_k_blocks = torch.tensor([[0, 2]], dtype=torch.int32, device=DEVICE)
out = torch.full(
(1, top_k_blocks.size(1) * sparse_block_size),
-1,
dtype=torch.int32,
device=DEVICE,
)
lru_slots = torch.arange(hot_buffer_size, dtype=torch.int16, device=DEVICE).view(
1, -1
)
load_blocks_to_device_buffer_mha(
top_k_blocks=top_k_blocks,
device_buffer_tokens=device_buffer_tokens,
host_cache_locs=torch.arange(
HOST_CACHE_SIZE, dtype=torch.int64, device=DEVICE
).view(1, -1),
device_buffer_locs=device_buffer_locs,
host_cache_k=host_k,
host_cache_v=host_v,
device_buffer_k=device_k,
device_buffer_v=device_v,
top_k_device_locs=out,
req_pool_indices=torch.tensor([0], dtype=torch.int64, device=DEVICE),
seq_lens=torch.tensor([11], dtype=torch.int32, device=DEVICE),
lru_slots=lru_slots,
item_size_bytes=ITEM_SIZE_BYTES,
hot_buffer_size=hot_buffer_size,
sparse_block_size=sparse_block_size,
num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
)
get_device_module().synchronize()
assert torch.equal(
out.cpu(), torch.tensor([[0, 1, 2, 3, 4, 5, 8, -1]], dtype=torch.int32)
)
assert torch.equal(device_k[4].cpu(), host_k[8])
assert torch.equal(device_v[4].cpu(), host_v[8])
assert torch.equal(device_k[5].cpu(), host_k[9])
assert torch.equal(device_v[5].cpu(), host_v[9])
assert torch.equal(
device_buffer_tokens.cpu(),
torch.tensor([[0, 1, 2, 3, 8, 9, -1, -1, -1]], dtype=torch.int32),
)
assert torch.equal(
lru_slots.cpu(), torch.tensor([[6, 7, 4, 5, 0, 1, 2, 3]], dtype=torch.int16)
)
def test_load_cache_to_device_buffer_miss_uses_updated_lru_slot() -> None:
state = _long_case()
@@ -427,7 +523,7 @@ def test_load_cache_to_device_buffer_miss_copy_is_byte_exact(
)
for slot in range(HOT_BUFFER_SIZE):
device_buffer[slot].copy_(host_cache[slot].to(DEVICE))
torch.cuda.synchronize()
get_device_module().synchronize()
top_k_tokens = torch.tensor([[miss_token]], dtype=torch.int32, device=DEVICE)
out = torch.full_like(top_k_tokens, -1)
@@ -454,7 +550,7 @@ def test_load_cache_to_device_buffer_miss_copy_is_byte_exact(
block_size=256,
num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
)
torch.cuda.synchronize()
get_device_module().synchronize()
# The miss evicts the LRU head (slot 0, physical loc 0) and lands there.
assert torch.equal(out.cpu(), torch.tensor([[0]], dtype=torch.int32))
@@ -593,7 +689,7 @@ def test_load_cache_to_device_buffer_dsv4_mla_miss_copy_layout() -> None:
block_size=256,
num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
)
torch.cuda.synchronize()
get_device_module().synchronize()
assert torch.equal(out.cpu(), torch.tensor([[9]], dtype=torch.int32))
@@ -666,7 +762,7 @@ def test_load_cache_to_device_buffer_dsv4_fused_copy_multi_miss() -> None:
block_size=256,
num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
)
torch.cuda.synchronize()
get_device_module().synchronize()
# Which slot each miss evicts is up to the LRU, so take the destinations
# from the kernel; only require that they are distinct and in range.
@@ -741,7 +837,7 @@ def test_load_cache_to_device_buffer_rocm_large_lru_writeback() -> None:
block_size=1024,
num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
)
torch.cuda.synchronize()
get_device_module().synchronize()
expected_lru = torch.cat(
[
@@ -0,0 +1,138 @@
"""MXFP8 KV cache must never write the reserved CUDA-graph padding slot.
Padding lanes carry undefined activations that quantize to NaN payload and
0xFF e8m0 scales; attention reads slot 0 back for padded page-table entries,
so a poisoned slot 0 defeats probability masking (0 * NaN = NaN in PV).
Asserts require slot 0 to stay exactly zero, so finite-garbage writes fail too.
"""
import pytest
import torch
from sglang.srt.utils import get_device_sm
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=60, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
requires_sm100 = pytest.mark.skipif(
not torch.cuda.is_available() or get_device_sm() < 100,
reason="MXFP8 KV cache requires SM100+",
)
DEV, HD, PS, NHKV = "cuda", 128, 128, 2
def _make_pool(**kwargs):
from sglang.srt.mem_cache.memory_pool import MHATokenToKVPoolMXFP8
return MHATokenToKVPoolMXFP8(
size=4 * PS,
page_size=PS,
dtype=torch.float8_e4m3fn,
head_num=NHKV,
head_dim=HD,
layer_num=1,
device=DEV,
enable_memory_saver=False,
**kwargs,
)
class _Layer:
layer_id = 0
def _quantize(k, v):
from sglang.kernels.ops.quantization.mxfp8_quant import to_mxfp8
km, vm = to_mxfp8(k), to_mxfp8(v)
return (
km.data,
vm.data,
km.scale.view(torch.float8_e8m0fnu),
vm.scale.view(torch.float8_e8m0fnu),
)
def _assert_slot0_zero(pool):
kc, vc = pool.get_kv_buffer(0)
ksf, vsf = pool.get_kv_scale_buffer(0)
s0k = kc.view(-1, PS, NHKV, HD)[0, 0].view(torch.uint8)
s0v = vc.view(-1, PS, NHKV, HD)[0, 0].view(torch.uint8)
assert int(s0k.sum()) == 0, "reserved slot K payload written"
assert int(s0v.sum()) == 0, "reserved slot V payload written"
zero_loc = torch.zeros(1, dtype=torch.int64, device=DEV)
s0_ksf = pool._read_sf_interleaved(ksf, zero_loc).view(torch.uint8)
s0_vsf = pool._read_sf_interleaved(vsf, zero_loc).view(torch.uint8)
assert int(s0_ksf.sum()) == 0, "reserved slot K scales written"
assert int(s0_vsf.sum()) == 0, "reserved slot V scales written"
@requires_sm100
def test_direct_path_skips_reserved_slot():
torch.manual_seed(0)
pool = _make_pool()
k = torch.randn(4, NHKV, HD, dtype=torch.bfloat16, device=DEV) * 0.5
v = torch.randn(4, NHKV, HD, dtype=torch.bfloat16, device=DEV) * 0.5
k[[0, 2]] = float("nan")
v[[0, 2]] = float("nan")
kq, vq, ks, vs = _quantize(k, v)
loc = torch.tensor([0, 7, 0, 9], dtype=torch.int64, device=DEV)
pool.set_kv_buffer(_Layer(), loc, kq, vq, ks, vs)
_assert_slot0_zero(pool)
kc, _ = pool.get_kv_buffer(0)
got = kc.view(-1, PS, NHKV, HD)[0, 7].view(torch.uint8)
assert torch.equal(got, kq[1].view(torch.uint8)), "non-reserved write corrupted"
@requires_sm100
def test_fused_quant_store_path_skips_reserved_slot():
"""k_scale=None routes to the fused quant_store_kv_mxfp8 kernel."""
torch.manual_seed(2)
pool = _make_pool()
k = torch.randn(4, NHKV, HD, dtype=torch.bfloat16, device=DEV) * 0.5
v = torch.randn(4, NHKV, HD, dtype=torch.bfloat16, device=DEV) * 0.5
k[[1, 3]] = float("nan")
v[[1, 3]] = float("nan")
loc = torch.tensor([5, 0, 9, 0], dtype=torch.int64, device=DEV)
pool.set_kv_buffer(_Layer(), loc, k, v)
_assert_slot0_zero(pool)
kc, _ = pool.get_kv_buffer(0)
valid = kc.view(-1, PS, NHKV, HD)[0, 5].float()
assert not torch.isnan(valid).any() and valid.abs().sum() > 0, (
"fused valid write lost"
)
@requires_sm100
def test_set_kv_buffer_is_cuda_graph_capture_safe():
torch.manual_seed(5)
pool = _make_pool(enable_alt_stream=False)
k = torch.randn(2, NHKV, HD, dtype=torch.bfloat16, device=DEV) * 0.5
v = torch.randn(2, NHKV, HD, dtype=torch.bfloat16, device=DEV) * 0.5
kq, vq, ks, vs = _quantize(k, v)
loc = torch.tensor([0, 7], dtype=torch.int64, device=DEV)
for _ in range(2): # warmup
pool.set_kv_buffer(_Layer(), loc, kq, vq, ks, vs)
torch.cuda.synchronize()
g = torch.cuda.CUDAGraph()
with torch.cuda.graph(g):
pool.set_kv_buffer(_Layer(), loc, kq, vq, ks, vs)
g.replay()
torch.cuda.synchronize()
_assert_slot0_zero(pool)
kc, _ = pool.get_kv_buffer(0)
got = kc.view(-1, PS, NHKV, HD)[0, 7].view(torch.uint8)
assert torch.equal(got, kq[1].view(torch.uint8)), "captured valid write lost"
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -1,8 +1,13 @@
import concurrent.futures
import unittest
from types import SimpleNamespace
import numpy as np
import torch
from sglang.kernels.ops.kvcache.pd_dcp_gather import copy_mla_rows_into_pack
from sglang.srt.disaggregation.common.staging_buffer import StagingBuffer
from sglang.srt.disaggregation.mooncake.conn import MooncakeKVManager
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
@@ -36,6 +41,135 @@ class TestPdDcpGather(CustomTestCase):
torch.testing.assert_close(packed0, kv0[row_indices], rtol=0, atol=0)
torch.testing.assert_close(packed1, kv1[row_indices], rtol=0, atol=0)
def test_packed_tp2_pp2_to_dcp4_preserves_kv(self):
"""Packing must preserve both target rows and draft head shards across PP stages."""
for custom_pool in (False, True):
for capacity in (256 * (2 * 64 + 2 * 512), 256 * 2 * 64 // 4):
for rank in range(4):
with self.subTest(
custom_pool=custom_pool, capacity=capacity, rank=rank
):
self._check_packed_transfer(rank, custom_pool, capacity)
def _check_packed_transfer(self, rank, custom_pool, capacity):
page, tokens, chunk = 64, 521, 256
src_pages = np.array([7, 1, 9, 3, 4, 11, 2, 5, 8], dtype=np.int32)
dst_pages = np.array([4, 1, 6], dtype=np.int32)
layers, widths = [3, 11, 19, 27, 28, 28], [64] * 4 + [256] * 2
logical = torch.arange(tokens, device="cuda")
src_rows = (
torch.as_tensor(src_pages, device="cuda")[logical // page] * page
+ logical % page
)
values = [
(
(logical[:, None] + 256) * 13
+ torch.arange(width, device="cuda") * 7
+ entry * 31
)
.remainder(251)
.to(torch.uint8)
for entry, width in enumerate([64] * 4 + [1024] * 2)
]
destinations = [
torch.full((2048, w), 165, dtype=torch.uint8, device="cuda") for w in widths
]
expected = [x.clone() for x in destinations]
owned = logical[rank::4]
target_rows = (
torch.as_tensor(dst_pages, device="cuda")[owned // 256] * page
+ owned % 256 // 4
)
draft_rows = (
torch.as_tensor(dst_pages, device="cuda")[logical // 256] * 256
+ logical % 256
)
for entry in range(4):
expected[entry][target_rows] = values[entry][owned]
for entry in (4, 5):
expected[entry][draft_rows] = values[entry][
:, rank * 256 : (rank + 1) * 256
]
pack = StagingBuffer(capacity, "cuda:0", 0)
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
for stage, entries in enumerate(([0, 1], [2, 3, 4, 5])):
sources = []
for entry in entries:
data = values[entry]
if entry >= 4:
start = (rank // 2) * 512
data = data[:, start : start + 512]
source = torch.full(
(1024, data.shape[1]), 165, dtype=torch.uint8, device="cuda"
)
source[src_rows] = data
sources.append(source)
buffers = sources + destinations + [pack.buffer]
def transfer(session, blocks):
def view(ptr, size):
for tensor in buffers:
offset = ptr - tensor.data_ptr()
if 0 <= offset and offset + size <= tensor.numel():
return tensor.flatten()[offset : offset + size]
raise AssertionError(
f"Transfer outside registered buffers: {ptr}, {size}"
)
for src, dst, size in blocks:
view(dst, size).copy_(view(src, size))
torch.cuda.synchronize()
return 0
manager = SimpleNamespace(
is_mla_backend=False,
kv_args=SimpleNamespace(
page_size=page,
kv_layer_ids=[layers[e] for e in entries],
kv_data_ptrs=[x.data_ptr() for x in sources],
num_draft_entries=2 if stage else 0,
engine_rank=stage * 2 + rank // 2,
),
attn_tp_size=2,
max_transfer_batch_indices=37,
enable_custom_mem_pool=custom_pool,
enable_deferred_decode_kv_release=False,
_transfer_data=transfer,
)
manager._await_transfer_futures = lambda futures: (
MooncakeKVManager._await_transfer_futures(manager, futures)
)
for start in range(0, tokens, chunk):
count = min(chunk, tokens - start)
result = MooncakeKVManager.send_kvcache_dcp(
manager,
"session",
src_pages[start // page : (start + count + page - 1) // page],
[x.data_ptr() for x in destinations],
dst_pages,
dcp_token_item_lens=[x.shape[1] for x in sources],
dst_dcp_size=4,
dst_dcp_rank=rank,
src_page_offset=start // page,
decode_prefix_len=256,
num_kv_tokens=count,
executor=executor,
dst_layer_ids=layers,
pack_buffer=pack,
dst_kv_item_lens=[
page * w * (4 if e >= 4 else 1)
for e, w in enumerate(widths)
],
dst_tp_rank=rank,
dst_attn_tp_size=4,
)
self.assertEqual(result, 0)
for entry in entries:
torch.testing.assert_close(
destinations[entry], expected[entry], rtol=0, atol=0
)
if __name__ == "__main__":
unittest.main()
@@ -16,7 +16,7 @@ import torch
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=8, stage="base-b", runner_config="1-gpu-small")
register_cuda_ci(est_time=8, stage="base-b-kernel-unit", runner_config="1-gpu-small")
pytestmark = pytest.mark.skipif(
not torch.cuda.is_available(),
@@ -0,0 +1,213 @@
"""The AITER mHC route on gfx950: gate, fallback latch, and kernel numerics vs the Torch oracle."""
import sys
import types
import unittest
from unittest.mock import patch
import torch
from sglang.kernels.ops.layernorm import mhc
from sglang.srt.environ import envs
from sglang.srt.utils import is_gfx95_supported, is_hip
from sglang.test.ci.ci_register import register_amd_ci
from sglang.test.test_utils import CustomTestCase
register_amd_ci(est_time=120, suite="stage-b-test-1-gpu-small-amd-mi35x")
@unittest.skipUnless(
torch.cuda.is_available() and is_hip() and is_gfx95_supported(),
"requires one gfx950 GPU",
)
class TestAiterMHCGLM53Flash(CustomTestCase):
hidden_size = 4096
hc_mult = 4
rms_eps = 1e-6
hc_eps = 1e-6
def setUp(self):
mhc._AITER_MHC_RUNTIME_DISABLED = False
def _inputs(self, tokens: int, seed: int = 0):
torch.manual_seed(seed)
device = torch.device("cuda")
mix_size = 2 * self.hc_mult + self.hc_mult**2
residual = (
torch.randn(
tokens,
self.hc_mult,
self.hidden_size,
device=device,
dtype=torch.bfloat16,
)
* 0.1
)
fn = (
torch.randn(
mix_size,
self.hc_mult * self.hidden_size,
device=device,
dtype=torch.float32,
)
* 0.01
)
scale = torch.tensor([0.5, 0.25, 0.25], device=device, dtype=torch.float32)
base = torch.zeros(mix_size, device=device, dtype=torch.float32)
return residual, fn, scale, base
def _rmsnorm(self, x, weight):
return (
x.float()
* torch.rsqrt(x.float().square().mean(dim=-1, keepdim=True) + self.rms_eps)
* weight.float()
).to(x.dtype)
def test_gate_selects_aiter_on_gfx950(self):
"""A gate that resolves False on real hardware silently serves the Torch path."""
with envs.SGLANG_USE_AITER.override(True):
self.assertTrue(mhc._use_aiter_mhc())
def test_hip_without_aiter_stays_on_torch_and_never_loads_tilelang(self):
"""The TileLang/DeepGEMM flags default on; only the HIP gate keeps them off this device."""
residual, fn, scale, base = self._inputs(8)
x = residual.reshape(8, self.hc_mult * self.hidden_size)
_, _, layer_ref = mhc._mhc_pre_torch(
residual, fn, scale, base, self.rms_eps, self.hc_eps, self.hc_eps, 2.0, 4
)
with (
envs.SGLANG_USE_AITER.override(False),
envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.override(True),
envs.SGLANG_OPT_USE_TILELANG_MHC_POST.override(True),
envs.SGLANG_OPT_DEEPGEMM_HC_PRENORM.override(True),
patch.object(
mhc, "_load_tilelang", side_effect=AssertionError("TileLang imported")
),
):
self.assertFalse(mhc._use_aiter_mhc())
self.assertFalse(mhc._use_tilelang_mhc_pre())
self.assertFalse(mhc._use_tilelang_mhc_post())
self.assertFalse(mhc._use_deep_gemm_hc_prenorm())
layer_input, h_res, h_post, norm_fused = mhc.hc_pre(
x, fn, scale, base, self.hc_mult, self.rms_eps, self.hc_eps, 4
)
out = mhc.hc_post(layer_input, x, h_post, h_res, self.hc_mult)
self.assertFalse(norm_fused)
torch.testing.assert_close(layer_input, layer_ref)
self.assertTrue(torch.isfinite(out).all())
def test_aiter_import_and_runtime_failures_latch_to_torch(self):
"""A missing symbol or a raising kernel must disable the route once, not fail the request."""
residual, fn, scale, base = self._inputs(8)
x = residual.reshape(8, self.hc_mult * self.hidden_size)
modules = {
"aiter": types.ModuleType("aiter"),
"aiter.ops": types.ModuleType("aiter.ops"),
"aiter.ops.mhc": types.ModuleType("aiter.ops.mhc"),
}
with patch.dict(sys.modules, modules):
result = mhc._try_aiter_mhc_pre(
residual,
fn,
scale,
base,
self.rms_eps,
self.hc_eps,
self.hc_eps,
2.0,
4,
None,
None,
)
self.assertIsNone(result)
self.assertTrue(mhc._AITER_MHC_RUNTIME_DISABLED)
mhc._AITER_MHC_RUNTIME_DISABLED = False
def fail_post(*_args, **_kwargs):
raise RuntimeError("synthetic failure")
failing = types.ModuleType("aiter.ops.mhc")
failing.mhc_post = fail_post
modules["aiter.ops.mhc"] = failing
with envs.SGLANG_USE_AITER.override(False):
layer_input, h_res, h_post, _ = mhc.hc_pre(
x, fn, scale, base, self.hc_mult, self.rms_eps, self.hc_eps, 4
)
with (
patch.dict(sys.modules, modules),
envs.SGLANG_USE_AITER.override(True),
):
out = mhc.hc_post(layer_input, x, h_post, h_res, self.hc_mult)
self.assertTrue(mhc._AITER_MHC_RUNTIME_DISABLED)
self.assertTrue(torch.isfinite(out).all())
def test_aiter_pre_post_match_torch_oracle(self):
"""A positional or kwarg mixup in the AITER call shows up only against the real kernel."""
norm_weight = torch.linspace(
0.75, 1.25, self.hidden_size, device="cuda", dtype=torch.bfloat16
)
for tokens in (1, 8, 17, 32, 64, 128):
for sinkhorn_iters in (2, 20):
for fused_norm in (False, True):
with self.subTest(
tokens=tokens, sinkhorn_iters=sinkhorn_iters, norm=fused_norm
):
residual, fn, scale, base = self._inputs(tokens)
post_ref, comb_ref, layer_ref = mhc._mhc_pre_torch(
residual,
fn,
scale,
base,
self.rms_eps,
self.hc_eps,
self.hc_eps,
2.0,
sinkhorn_iters,
)
result = mhc._try_aiter_mhc_pre(
residual,
fn,
scale,
base,
self.rms_eps,
self.hc_eps,
self.hc_eps,
2.0,
sinkhorn_iters,
norm_weight if fused_norm else None,
self.rms_eps if fused_norm else None,
)
self.assertIsNotNone(result, "AITER mHC pre fell back")
post_out, comb_out, layer_out = result
if fused_norm:
layer_ref = self._rmsnorm(layer_ref, norm_weight)
torch.cuda.synchronize()
torch.testing.assert_close(
post_out, post_ref, atol=2e-3, rtol=2e-3
)
torch.testing.assert_close(
comb_out, comb_ref, atol=2e-3, rtol=2e-3
)
torch.testing.assert_close(
layer_out, layer_ref, atol=2e-2, rtol=2e-2
)
x = (layer_ref.float() * 0.75).to(layer_ref.dtype)
post_ref_out = mhc._mhc_post_torch(
x, residual, post_ref, comb_ref
)
post_out_actual = mhc._try_aiter_mhc_post(
x, residual, post_out, comb_out
)
self.assertIsNotNone(
post_out_actual, "AITER mHC post fell back"
)
torch.testing.assert_close(
post_out_actual, post_ref_out, atol=2e-2, rtol=2e-2
)
if __name__ == "__main__":
unittest.main()
@@ -1,4 +1,5 @@
from contextlib import nullcontext
from types import SimpleNamespace
import pytest
import torch
@@ -12,12 +13,7 @@ register_cuda_ci(est_time=10, stage="base-b", runner_config="1-gpu-large")
@pytest.fixture
def stated_tp_group():
"""A TP group for a test that runs in a process without one.
The production call passes the group *into* `use_symmetric_memory`, so
stubbing that context manager does not stop the read -- the argument is
evaluated first. Stating it on the context answers every spelling.
"""
"""Provide a TP-group placeholder for kernels with mocked symmetric memory."""
from sglang.srt.runtime_context import get_parallel
with get_parallel().override(tp_group=None):
@@ -25,7 +21,7 @@ def stated_tp_group():
@pytest.mark.parametrize("hidden_size", [4096, 7168])
@pytest.mark.parametrize("num_tokens", [0, 1, 8, 17, 32, 64])
@pytest.mark.parametrize("num_tokens", [0, 1, 6, 8, 17, 32, 64])
@pytest.mark.parametrize("use_norm", [False, True])
def test_mhc_fused_post_pre_matches_unfused(
monkeypatch, hidden_size, num_tokens, use_norm, stated_tp_group
@@ -34,10 +30,7 @@ def test_mhc_fused_post_pre_matches_unfused(
pytest.skip("CUDA is required for TileLang mHC kernels")
monkeypatch.setattr(mhc, "is_dsa_prefill_cp_interleave", lambda: False)
# This is a single-process kernel unit test with no TP group initialized.
# mhc_pre / mhc_fused_post_pre allocate the MoE input in the symmetric-memory
# pool, which asks for the TP group; bypassing the allocation is enough, and
# then nothing asks. Mirrors the workaround in test_mxfp4_sm90_cutlass.py.
# Disable symmetric-memory allocation for this single-process kernel test.
monkeypatch.setattr(mhc, "use_symmetric_memory", lambda *a, **kw: nullcontext())
monkeypatch.setattr(mhc, "is_allocation_symmetric", lambda: False)
torch.manual_seed(0)
@@ -107,6 +100,18 @@ def test_mhc_fused_post_pre_matches_unfused(
norm_eps=norm_eps,
)
if hidden_size == 4096 and num_tokens in (0, 1, 6, 17):
_check_glm_boundary(
x,
residual,
post_prev,
comb_prev,
fn,
hc_scale,
hc_base,
use_norm=use_norm,
)
torch.cuda.synchronize()
if num_tokens == 0:
assert residual_out.shape == residual.shape
@@ -136,6 +141,72 @@ def test_mhc_fused_post_pre_matches_unfused(
torch.testing.assert_close(layer_out, layer_ref, atol=layer_atol, rtol=layer_rtol)
def _check_glm_boundary(x, residual, post, comb, fn, scale, base, *, use_norm):
from sglang.srt.environ import envs
from sglang.srt.layers.communicator_mhc import MHCState
from sglang.srt.layers.layernorm import RMSNorm
from sglang.srt.models.glm5_next import Glm5NextDecoderLayer
layer = Glm5NextDecoderLayer.__new__(Glm5NextDecoderLayer)
torch.nn.Module.__init__(layer)
layer.config = SimpleNamespace(
mhc=True,
hc_mult=4,
rms_norm_eps=1e-6,
hc_eps=1e-6,
hc_sinkhorn_iters=20,
)
layer.hc_ffn_fn = torch.nn.Parameter(fn)
layer.hc_ffn_scale = torch.nn.Parameter(scale)
layer.hc_ffn_base = torch.nn.Parameter(base)
norm = RMSNorm(x.shape[-1], eps=1e-6).to(x) if use_norm else None
states = [
MHCState(
hc_mult=4,
hc_attn_pre=layer.hc_attn_pre,
hc_ffn_pre=layer.hc_ffn_pre,
hc_post=layer.hc_post,
hc_ffn_post_pre=callback,
h_res=comb.flatten(1),
h_post=post.flatten(1),
)
for callback in (None, layer.hc_ffn_post_pre)
]
# Literal, not derived from the cutoff constant: deriving it makes this a
# mirror that stays green when the cutoff moves. None is the empty batch,
# which attn_to_mlp short-circuits before reaching the callback.
fused_expected = {1: True, 6: True, 17: False}[x.shape[0]] if x.shape[0] else None
with envs.SGLANG_OPT_FUSE_MHC_POST_PRE.override(True):
if x.shape[0] > 0:
declined = (
layer.hc_ffn_post_pre(
hidden_states=x,
residual=residual.flatten(1),
h_res=comb.flatten(1),
h_post=post.flatten(1),
out_norm_weight=None,
out_norm_eps=None,
)
is None
)
assert declined is not fused_expected, (
f"num_tokens={x.shape[0]} fused={not declined}, "
f"expected fused={fused_expected}"
)
outputs = [s.attn_to_mlp(x, residual.flatten(1), norm) for s in states]
torch.testing.assert_close(outputs[0][0], outputs[1][0], atol=2e-2, rtol=2e-2)
torch.testing.assert_close(outputs[0][1], outputs[1][1], atol=0, rtol=0)
torch.testing.assert_close(states[0].h_res, states[1].h_res, atol=1e-3, rtol=1e-3)
torch.testing.assert_close(states[0].h_post, states[1].h_post, atol=1e-3, rtol=1e-3)
# The next combine must consume the FFN mixing matrices, not attention's.
torch.testing.assert_close(
states[0].mlp_combine(x, outputs[0][1]),
states[1].mlp_combine(x, outputs[1][1]),
atol=2e-3,
rtol=2e-2,
)
if __name__ == "__main__":
import sys
@@ -17,7 +17,7 @@ from sglang.kernels.ops.mamba.lfm_short_conv import (
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=15, stage="base-b", runner_config="1-gpu-large")
register_cuda_ci(est_time=15, stage="base-b-kernel-unit", runner_config="1-gpu-large")
PAD_SLOT_ID = -1
requires_sm90 = unittest.skipUnless(
@@ -19,6 +19,7 @@ import pytest
import torch
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import publish_build_topology
register_cuda_ci(est_time=12, stage="base-b-kernel-unit", runner_config="1-gpu-large")
@@ -52,12 +53,8 @@ def _runtime_scaffolding():
if not torch.distributed.is_initialized():
init_distributed_environment(world_size=1, rank=0, local_rank=0, backend="gloo")
if not model_parallel_is_initialized():
initialize_model_parallel(
tensor_model_parallel_size=1,
expert_model_parallel_size=1,
pipeline_model_parallel_size=1,
backend="gloo",
)
publish_build_topology(tp_size=1, ep_size=1, pp_size=1)
initialize_model_parallel(backend="gloo")
def _interleave_w13_rows(w13: torch.Tensor) -> torch.Tensor:
@@ -0,0 +1,107 @@
"""Parity coverage for the fused DSA k-pool top-k / pool-expansion / tail JIT kernel.
``fast_kpool_topk_transform_fused`` is the only implementation for the pooled
group budgets GLM-5.3-Flash uses (``index_topk=2048`` over ``index_kpool=4``
gives ``group_topk=512``); ``kpool_fp8_index`` has no Python fallback in that
range, so a build or numerical break here takes the model down rather than
making it slower.
The radix selector does not specify an output order and DSA attention is
permutation-invariant over the selected set, so the pooled columns are compared
as a set. The tail columns are positional and are compared exactly.
Registered for AMD only: the kernel had no direct coverage on any platform, and
adding CUDA coverage for it is not this change's call to make.
"""
import unittest
import torch
from sglang.kernels.ops.moe.kpool_topk_transform import fast_kpool_topk_transform_fused
from sglang.test.ci.ci_register import register_amd_ci
from sglang.test.test_utils import CustomTestCase
register_amd_ci(est_time=60, stage="jit-kernel-unit", runner_config="amd")
@unittest.skipUnless(torch.cuda.is_available(), "Test requires a GPU")
class TestKpoolTopkTransformFused(CustomTestCase):
POOL_SIZE = 4
def _distinct_scores(self, rows: int, groups: int) -> torch.Tensor:
"""Strictly distinct scores per row, so top-k selection has no ties to break."""
return torch.stack(
[torch.randperm(groups, dtype=torch.float32) for _ in range(rows)]
).cuda()
def _expected_tokens(
self, score_row: torch.Tensor, group_topk: int
) -> torch.Tensor:
"""The pooled top-k groups expanded to their ``pool_size`` token ids."""
groups = torch.topk(score_row.float().cpu(), group_topk).indices
offsets = torch.arange(self.POOL_SIZE, dtype=torch.int64)
return (groups.unsqueeze(1) * self.POOL_SIZE + offsets).reshape(-1)
def _run(self, rows, groups, topk, seq_lens_host=None):
torch.manual_seed(0)
score = self._distinct_scores(rows, groups)
lengths = torch.full((rows,), groups, dtype=torch.int32, device="cuda")
seq_lens = (
torch.tensor(seq_lens_host, dtype=torch.int32, device="cuda")
if seq_lens_host is not None
else None
)
out = fast_kpool_topk_transform_fused(
score=score,
lengths=lengths,
pool_size=self.POOL_SIZE,
topk=topk,
seq_lens=seq_lens,
)
return score, out.cpu()
def _assert_pooled_columns(self, score, out, topk):
group_topk = topk // self.POOL_SIZE
for row in range(score.shape[0]):
selected = out[row, :topk]
expected = self._expected_tokens(score[row], group_topk)
self.assertEqual(
sorted(selected.tolist()),
sorted(expected.tolist()),
msg=f"row {row}: selected token set differs from torch.topk",
)
def test_group_topk_512_matches_reference(self):
# GLM-5.3-Flash: index_topk=2048 over index_kpool=4.
score, out = self._run(rows=2, groups=1024, topk=2048)
self._assert_pooled_columns(score, out, topk=2048)
def test_group_topk_128_matches_reference(self):
score, out = self._run(rows=2, groups=512, topk=512)
self._assert_pooled_columns(score, out, topk=512)
def test_tail_columns_hold_the_trailing_partial_pool(self):
groups, topk = 1024, 2048
for extra in range(self.POOL_SIZE):
with self.subTest(tail=extra):
seq_len = groups * self.POOL_SIZE + extra
score, out = self._run(
rows=2, groups=groups, topk=topk, seq_lens_host=[seq_len] * 2
)
self._assert_pooled_columns(score, out, topk=topk)
expected_tail = [seq_len - extra + i for i in range(extra)]
expected_tail += [-1] * (self.POOL_SIZE - 1 - extra)
for row in range(out.shape[0]):
self.assertEqual(out[row, topk:].tolist(), expected_tail)
def test_output_width_carries_the_tail_columns(self):
# kpool_fp8_index feeds this width straight into the page-table transform,
# so it is 2048 + 3 = 2051 for GLM-5.3-Flash rather than a round 2048.
topk = 2048
_, out = self._run(rows=1, groups=1024, topk=topk, seq_lens_host=[1024 * 4 + 1])
self.assertEqual(tuple(out.shape), (1, topk + self.POOL_SIZE - 1))
if __name__ == "__main__":
unittest.main()
@@ -37,12 +37,7 @@ dev = "cuda"
@pytest.fixture
def stated_tp_group():
"""A TP group for a test that runs in a process without one.
The production call passes the group *into* `use_symmetric_memory`, so
stubbing that context manager does not stop the read -- the argument is
evaluated first. Stating it on the context answers every spelling.
"""
"""Provide a TP-group placeholder for kernels with mocked symmetric memory."""
from sglang.srt.runtime_context import get_parallel
with get_parallel().override(tp_group=None):
@@ -0,0 +1,439 @@
"""Unit test: windowed draft-decode KV index builder (StreamingLLM sink + window).
Covers the ``window_size`` / ``sink_size`` additions to
``generate_draft_decode_kv_indices`` (the shared kv-index kernel used by the
built-in MTP/NEXTN + EAGLE draft-decode path on BOTH the Triton and FlashInfer
draft attention backends).
Properties checked:
1. IDENTITY (off-by-default is a no-op) -- ``window_size == 0`` reproduces the
full-KV read plan. Both the CSR offsets (``kv_indptr``) and the gathered
slots (``kv_indices``) are checked against an independent closed-form oracle
derived only from the input ``seq_lens`` (NOT from the kernel's packing
logic), so an offset bug is not mirrored by the oracle. This is the
losslessness/regression guarantee.
2. KV-INDPTR lengths -- with a window each draft-decode step keeps
``min(seq_len, sink + window)`` base tokens + ``it + 1`` never-windowed tree
tokens; compared to a clamp+cumsum oracle. Parametrized past the
256K/512K/1M thresholds (incl. the perf-smoke W4032/S64 params).
3. CONTENT -- the actual gathered slots are, in order,
``[first sink] + [most-recent window] + [draft tree]`` (the StreamingLLM
layout), read back at buffer offset 0 (num_seqs=1, topk=1).
3b. TREE (``topk > 1``) -- with a tree draft every ``(request, topk)`` slot gets
its own copy of the windowed base list plus its own branch of tree tokens.
The windowed per-slot write offset is ``topk_id * (kept + it + 1)``, so a
windowing bug there would silently overlap neighbouring branches while the
lengths still look right. Slices are located by the *oracle* ``kv_indptr``
(independent closed form) and their content checked, which ties the offset
and the CSR bounds together. Covered for ``page_size == 1`` and for the
``page_size > 1 and topk > 1`` paged-tree branch, where the tree tokens are
read from the UNCAPPED ``seq_len`` but written at the capped offset.
4. RESOLUTION -- ``resolve_draft_decode_window`` maps server args to the
kernel's (window, sink), and returns (0, 0) for a draft model that already
has a sliding window of its own (that per-layer window wins over the flag,
since this index builder emits one KV list for all draft layers). It warns
only when it substitutes a different window than the one requested.
5. BACKEND WIRING -- both ``TritonMultiStepDraftBackend`` and
``FlashInferMultiStepDraftBackend`` resolve the pair in ``__init__`` and
forward it to the kernel, so windowing is honored identically on both
backends (CPU-only guard).
The kernel is imported normally from ``sglang`` (no by-file-path staging), so it
tracks the installed tree.
"""
import inspect
import unittest
from types import SimpleNamespace
from unittest import mock
import torch
from sglang.srt.speculative import spec_utils
from sglang.srt.speculative.spec_utils import (
generate_draft_decode_kv_indices,
resolve_draft_decode_window,
)
from sglang.srt.utils import next_power_of_2
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=120, stage="base-b-kernel-unit", runner_config="1-gpu-large")
_HAS_CUDA = torch.cuda.is_available()
# --- Harness: mirror {Triton,FlashInfer}MultiStepDraftBackend.common_template ---
def _make_inputs(seq_lens, num_steps, topk, page_size=1, pool_len=None):
dev = "cuda"
num_seqs = len(seq_lens)
bs = num_seqs * topk
seq_lens_t = torch.tensor(seq_lens, dtype=torch.int64, device=dev)
max_seq = int(seq_lens_t.max().item())
if pool_len is None:
# Room for every branch's tree tokens: topk*num_steps slots (page_size==1)
# or up to topk*num_new_pages*page_size past the prefix's last page.
pool_len = max_seq + topk * (num_steps + page_size) + 16
# Unique physical slot ids per (req, position) so gather checks are unambiguous.
req_to_token = (
torch.arange(num_seqs * pool_len, dtype=torch.int64, device=dev).reshape(
num_seqs, pool_len
)
+ 1000
)
req_pool_indices = torch.arange(num_seqs, dtype=torch.int64, device=dev)
positions = torch.empty(bs, dtype=torch.int64, device=dev)
for bid in range(num_seqs):
for tk in range(topk):
positions[bid * topk + tk] = seq_lens[bid]
width = bs * (max_seq + num_steps) + 16
kv_indices = torch.zeros((num_steps, width), dtype=torch.int64, device=dev)
kv_indptr = torch.zeros((num_steps, bs + 1), dtype=torch.int64, device=dev)
return dict(
req_pool_indices=req_pool_indices,
req_to_token=req_to_token,
seq_lens=seq_lens_t,
positions=positions,
kv_indices=kv_indices,
kv_indptr=kv_indptr,
pool_len=pool_len,
page_size=page_size,
num_seqs=num_seqs,
topk=topk,
num_steps=num_steps,
bs=bs,
)
def _run(io, window_size=0, sink_size=0):
kv_indices = io["kv_indices"].clone()
kv_indptr = io["kv_indptr"].clone()
generate_draft_decode_kv_indices[(io["num_steps"], io["num_seqs"], io["topk"])](
io["req_pool_indices"],
io["req_to_token"],
io["seq_lens"],
kv_indices,
kv_indptr,
io["positions"],
io["pool_len"],
kv_indices.shape[1],
kv_indptr.shape[1],
next_power_of_2(io["num_seqs"]),
next_power_of_2(io["num_steps"]),
next_power_of_2(io["bs"]),
io["page_size"],
window_size,
sink_size,
)
torch.cuda.synchronize()
return kv_indices, kv_indptr
def _expected_kv_indptr(io, cap=None):
"""Closed form: kv_indptr[it][zid] = sum(clamp(pos[:zid], cap)) + zid*(it+1)."""
pos = io["positions"].to("cpu")
if cap is not None:
pos = torch.clamp(pos, max=cap)
csum = torch.cat([torch.zeros(1, dtype=torch.int64), torch.cumsum(pos, 0)])
out = torch.zeros((io["num_steps"], io["bs"] + 1), dtype=torch.int64)
for it in range(io["num_steps"]):
for zid in range(io["bs"] + 1):
out[it, zid] = csum[zid] + zid * (it + 1)
return out
def _expected_base(r2t, seq_len, window, sink):
"""StreamingLLM base gather for one request (window==0 => full context)."""
if window == 0:
return r2t[0:seq_len]
cap = window + sink
kept = min(seq_len, cap)
s_eff = min(sink, seq_len)
recent_start = seq_len - (kept - s_eff)
return torch.cat([r2t[0:s_eff], r2t[recent_start : recent_start + (kept - s_eff)]])
def _expected_tree(r2t, seq_len, topk_id, topk, num_steps, n_tree, page_size):
"""This branch's tree tokens, always read from the UNCAPPED prefix end.
Mirrors the kernel's two extend layouts (linear, and the paged per-topk page
stride), both of which windowing must leave untouched.
"""
if page_size == 1 or topk == 1:
start = seq_len + topk_id * num_steps
else:
last_page_len = seq_len % page_size
num_new_pages_per_topk = (
last_page_len + num_steps + page_size - 1
) // page_size
start = (
(seq_len // page_size) * page_size
+ topk_id * num_new_pages_per_topk * page_size
+ last_page_len
)
return r2t[start : start + n_tree]
@unittest.skipUnless(_HAS_CUDA, "draft-decode kv-index kernel requires a CUDA GPU")
class TestDraftDecodeWindowKernel(unittest.TestCase):
def test_window_zero_is_full_kv(self):
"""window_size==0 => original full-KV read plan (indptr + content)."""
for seq_lens, num_steps, topk in [
([128], 4, 1),
([300, 130, 517], 5, 1),
([1024, 777], 4, 2),
]:
with self.subTest(seq_lens=seq_lens, num_steps=num_steps, topk=topk):
io = _make_inputs(seq_lens, num_steps, topk)
self._assert_slot_layout(io, 0, 0)
def test_windowed_kv_indptr_lengths(self):
for seq_lens, window, sink in [
([5000, 2048, 900], 1024, 64),
([5000, 2048, 900], 1024, 0), # pure recent window
([100, 200], 4096, 64), # cap exceeds seq_len -> keep all
# long-context regression (perf smoke used W4032/S64 past 256K/512K/1M)
([1029306], 4032, 64),
([1048576, 300000, 70000], 4032, 64),
([262145, 131072], 4032, 64),
]:
with self.subTest(seq_lens=seq_lens, window=window, sink=sink):
io = _make_inputs(seq_lens, num_steps=4, topk=1)
_, indptr = _run(io, window_size=window, sink_size=sink)
expected = _expected_kv_indptr(io, cap=window + sink)
self.assertTrue(torch.equal(indptr.cpu(), expected))
def test_windowed_content_sink_plus_recent(self):
for seq_len, window, sink in [
(5000, 1024, 64),
(65536, 4032, 64),
(262145, 4032, 64), # just past native 262144
(524288, 4032, 64),
(1029306, 4032, 64), # exact perf-smoke seq_len (W4032/S64)
]:
with self.subTest(seq_len=seq_len, window=window, sink=sink):
io = _make_inputs([seq_len], num_steps=4, topk=1)
kv_indices, _ = _run(io, window_size=window, sink_size=sink)
r2t = io["req_to_token"][0].cpu()
kept = min(seq_len, window + sink)
expected_base = _expected_base(r2t, seq_len, window, sink)
for step in range(io["num_steps"]):
row = kv_indices[step].cpu()
self.assertTrue(
torch.equal(row[0:kept], expected_base),
f"base gather wrong at step {step} (seq_len={seq_len})",
)
n_tree = step + 1
self.assertTrue(
torch.equal(
row[kept : kept + n_tree], r2t[seq_len : seq_len + n_tree]
),
f"tree tokens wrong at step {step} (seq_len={seq_len})",
)
def _assert_slot_layout(self, io, window, sink):
"""Every (request, topk) slot holds [sink + recent] + its own tree branch.
Slots are located by the closed-form kv_indptr oracle, so this also pins
the per-slot write offset (topk_id * (kept + iters)). ``topk == 1`` is a
one-branch tree and ``window == 0`` asks for the whole prefix, so this
covers the identity case too.
"""
cap = None if window == 0 else window + sink
kv_indices, indptr = _run(io, window_size=window, sink_size=sink)
expected_indptr = _expected_kv_indptr(io, cap=cap)
self.assertTrue(torch.equal(indptr.cpu(), expected_indptr))
topk, num_steps = io["topk"], io["num_steps"]
seq_lens = io["seq_lens"].tolist()
for step in range(num_steps):
row = kv_indices[step].cpu()
n_tree = step + 1
for bid, seq_len in enumerate(seq_lens):
r2t = io["req_to_token"][bid].cpu()
kept = seq_len if cap is None else min(seq_len, cap)
for topk_id in range(topk):
zid = bid * topk + topk_id
start = int(expected_indptr[step, zid])
where = f"step={step} bid={bid} topk_id={topk_id}"
self.assertTrue(
torch.equal(
row[start : start + kept],
_expected_base(r2t, seq_len, window, sink),
),
f"base gather wrong at {where}",
)
self.assertTrue(
torch.equal(
row[start + kept : start + kept + n_tree],
_expected_tree(
r2t,
seq_len,
topk_id,
topk,
num_steps,
n_tree,
io["page_size"],
),
),
f"tree tokens wrong at {where}",
)
def test_tree_topk_layout(self):
"""topk > 1 (tree draft): per-branch slices, windowed and unwindowed."""
for seq_lens, num_steps, topk, window, sink in [
([600], 4, 2, 128, 16),
([600, 250], 3, 4, 128, 16),
([5000, 900], 4, 2, 1024, 64),
([5000, 900], 4, 2, 1024, 0), # pure recent window
([300, 130], 3, 2, 4096, 64), # cap exceeds seq_len -> keep all
([65536, 1029306], 3, 2, 4032, 64), # long-context tree
([600, 250], 3, 4, 0, 0), # off-by-default control
]:
with self.subTest(seq_lens=seq_lens, topk=topk, window=window, sink=sink):
io = _make_inputs(seq_lens, num_steps, topk)
self._assert_slot_layout(io, window, sink)
def test_tree_topk_paged(self):
"""page_size > 1 AND topk > 1: the paged-tree extend branch.
The tree tokens are read from the uncapped prefix end with a per-topk page
stride and stored at the CAPPED offset, so this is the one place where the
windowed and unwindowed lengths must both be respected in one statement.
"""
for seq_lens, num_steps, topk, page_size, window, sink in [
([600], 4, 2, 4, 128, 16),
([601, 255], 3, 2, 8, 128, 16), # prefix not page-aligned
([604, 256], 3, 4, 4, 128, 0), # page-aligned prefix, no sink
([5000, 900], 4, 2, 16, 1024, 64),
([601, 255], 3, 2, 8, 0, 0), # off-by-default control
]:
with self.subTest(
seq_lens=seq_lens, topk=topk, page_size=page_size, window=window
):
io = _make_inputs(seq_lens, num_steps, topk, page_size=page_size)
self._assert_slot_layout(io, window, sink)
class TestResolveDraftDecodeWindow(unittest.TestCase):
"""CPU-only: server args -> (window, sink), incl. the opt-out for an SWA draft."""
@staticmethod
def _runner(window=None, sink=None, native_window=None, declared=None):
server_args = SimpleNamespace(
speculative_draft_window_size=window,
speculative_draft_sink_size=sink,
)
if declared is not None:
server_args._resolved_overrides = (
("handle_speculative_decoding", declared),
)
return SimpleNamespace(
server_args=server_args,
sliding_window_size=native_window,
)
def test_unset_is_full_attention(self):
self.assertEqual(resolve_draft_decode_window(self._runner()), (0, 0))
def test_window_only(self):
self.assertEqual(
resolve_draft_decode_window(self._runner(window=4032)), (4032, 0)
)
def test_window_and_sink(self):
self.assertEqual(
resolve_draft_decode_window(self._runner(window=4032, sink=64)), (4032, 64)
)
def test_declared_values_win_over_the_raw_fields(self):
"""handle_speculative_decoding declares these fields instead of assigning
them, so a raw field read would answer with the unvalidated input."""
runner = self._runner(
window=None,
sink=None,
declared={
"speculative_draft_window_size": 4032,
"speculative_draft_sink_size": 64,
},
)
self.assertEqual(resolve_draft_decode_window(runner), (4032, 64))
def test_native_sliding_window_draft_opts_out(self):
"""A draft with its own window keeps it: the flag must not override it."""
with mock.patch.object(spec_utils.logger, "warning") as warn:
self.assertEqual(
resolve_draft_decode_window(
self._runner(window=4032, sink=64, native_window=1024)
),
(0, 0),
)
warn.assert_called_once()
def test_native_window_equal_to_flag_is_quiet(self):
"""LlamaForCausalLMEagle3 routes this flag into its own window, so an equal
native window is the requested one applied per layer, not a conflict."""
with mock.patch.object(spec_utils.logger, "warning") as warn:
self.assertEqual(
resolve_draft_decode_window(
self._runner(window=4032, sink=64, native_window=4032)
),
(0, 0),
)
warn.assert_not_called()
def test_no_native_window_still_windows(self):
"""Guard must not fire for full-attention drafts (0 / None / absent)."""
for native in (None, 0):
with self.subTest(native_window=native):
self.assertEqual(
resolve_draft_decode_window(
self._runner(window=4032, sink=64, native_window=native)
),
(4032, 64),
)
runner = self._runner(window=4032, sink=64)
del runner.sliding_window_size
self.assertEqual(resolve_draft_decode_window(runner), (4032, 64))
class TestDraftDecodeWindowBackendWiring(unittest.TestCase):
"""CPU-only guard: both draft backends must forward window/sink to the kernel.
The kernel tests above prove correctness once the args reach the kernel; this
guards the per-backend plumbing (init resolution + kernel passthrough) so it
cannot silently regress on either backend without a GPU-loaded model.
"""
def _assert_backend_wires(self, cls):
init_src = inspect.getsource(cls.__init__)
self.assertIn("resolve_draft_decode_window", init_src)
self.assertIn("self.draft_window_size", init_src)
self.assertIn("self.draft_sink_size", init_src)
tmpl_src = inspect.getsource(cls.common_template)
self.assertIn("self.draft_window_size", tmpl_src)
self.assertIn("self.draft_sink_size", tmpl_src)
def test_triton_backend_wires_window_sink(self):
try:
from sglang.srt.layers.attention.triton_backend import (
TritonMultiStepDraftBackend,
)
except ImportError as e: # pragma: no cover
self.skipTest(f"triton backend import unavailable: {e}")
self._assert_backend_wires(TritonMultiStepDraftBackend)
def test_flashinfer_backend_wires_window_sink(self):
try:
from sglang.srt.layers.attention.flashinfer_backend import (
FlashInferMultiStepDraftBackend,
)
except ImportError as e: # pragma: no cover
self.skipTest(f"flashinfer backend import unavailable: {e}")
self._assert_backend_wires(FlashInferMultiStepDraftBackend)
if __name__ == "__main__":
unittest.main()
@@ -1,6 +1,6 @@
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_cuda_ci(est_time=10, stage="base-b", runner_config="1-gpu-small")
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-small")
register_amd_ci(est_time=10, suite="nightly-amd-kernel-1-gpu", nightly=True)
import unittest
@@ -1,6 +1,6 @@
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=10, stage="base-b", runner_config="1-gpu-small")
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-small")
import unittest
@@ -0,0 +1,77 @@
from __future__ import annotations
import unittest
from unittest import mock
import torch
from sglang.srt.kv_canary import api
from sglang.srt.kv_canary.api import torch_reference_conflicts_with_decode_graph
from sglang.srt.model_executor.cuda_graph_config import (
Backend,
CudaGraphConfig,
PhaseConfig,
)
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
register_cpu_ci(est_time=10, suite="base-a-test-cpu")
class TestTorchReferenceConflictsWithDecodeGraph(CustomTestCase):
"""The refusal that keeps a graph-captured torch reference from passing silently.
The reference path does host work and D2H, so its launches leave nothing in a
captured decode graph and every replay verifies clean. Each case below pins one
branch of the gate; the platform capability is patched rather than probed so the
CPU lane exercises all four.
"""
def _publish_decode_backend(self, backend: str) -> None:
override = get_context().override_server_args(
cuda_graph_config=CudaGraphConfig(decode=PhaseConfig(backend=backend))
)
override.install()
self.addCleanup(override.restore)
def _patch_graph_support(self, supported: bool) -> None:
patcher = mock.patch.object(
api.current_platform, "support_cuda_graph", return_value=supported
)
patcher.start()
self.addCleanup(patcher.stop)
def test_reference_device_with_captured_decode_conflicts(self) -> None:
self._patch_graph_support(True)
self._publish_decode_backend(Backend.FULL)
self.assertTrue(
torch_reference_conflicts_with_decode_graph(torch.device("xpu"))
)
def test_reference_device_with_decode_graph_disabled_is_allowed(self) -> None:
self._patch_graph_support(True)
self._publish_decode_backend(Backend.DISABLED)
self.assertFalse(
torch_reference_conflicts_with_decode_graph(torch.device("xpu"))
)
def test_platform_without_graph_capture_is_allowed(self) -> None:
"""A device that never captures (CPU) keeps canary on the reference path."""
self._patch_graph_support(False)
self._publish_decode_backend(Backend.FULL)
self.assertFalse(
torch_reference_conflicts_with_decode_graph(torch.device("cpu"))
)
def test_cuda_device_is_never_refused(self) -> None:
"""CUDA/HIP run the real kernels, so the gate must not fire on them."""
self._patch_graph_support(True)
self._publish_decode_backend(Backend.FULL)
self.assertFalse(
torch_reference_conflicts_with_decode_graph(torch.device("cuda"))
)
if __name__ == "__main__":
unittest.main()
@@ -1,16 +1,22 @@
from __future__ import annotations
import unittest
from typing import cast
import torch
from sglang.srt.kv_canary.runner.future_tensor import FutureTensors
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.srt.utils import create_device_stream, get_current_device_stream_fast
from sglang.test.ci.ci_register import (
register_amd_ci,
register_cuda_ci,
register_xpu_ci,
)
from sglang.test.kv_canary.fixtures import DEFAULT_DEVICE
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=10, stage="extra-a", runner_config="1-gpu-small")
register_amd_ci(est_time=20, suite="extra-a-test-1-gpu-small-amd")
register_xpu_ci(est_time=20, suite="stage-b-test-1-gpu-xpu")
class _FakeEvent:
@@ -22,32 +28,30 @@ class _FakeEvent:
class TestFutureTensors(CustomTestCase):
def test_cuda_stage_then_wait_returns_host_copy(self) -> None:
"""Verify staged CUDA tensors are copied back on wait."""
device = torch.device("cuda")
alt_stream = torch.cuda.Stream(device=device)
default_stream = torch.cuda.current_stream(device)
def test_device_stage_then_wait_returns_host_copy(self) -> None:
"""Verify staged device tensors are copied back on wait."""
alt_stream = create_device_stream(DEFAULT_DEVICE)
default_stream = get_current_device_stream_fast()
self.assertNotEqual(alt_stream.stream_id, default_stream.stream_id)
src_first = torch.tensor([41], dtype=torch.int32, device=device)
src_first = torch.tensor([41], dtype=torch.int32, device=DEFAULT_DEVICE)
future_first = FutureTensors.device_to_host(
xs_device=src_first, d2h_stream=alt_stream
)
result_first = future_first.wait()
self.assertEqual(int(result_first.item()), 41)
src_second = torch.tensor([97], dtype=torch.int32, device=device)
src_second = torch.tensor([97], dtype=torch.int32, device=DEFAULT_DEVICE)
future_second = FutureTensors.device_to_host(
xs_device=src_second, d2h_stream=alt_stream
)
result_second = future_second.wait()
self.assertEqual(int(result_second.item()), 97)
def test_cuda_pinned_when_stream_is_provided(self) -> None:
"""Verify CUDA staging uses pinned host memory with a stream."""
device = torch.device("cuda")
alt_stream = torch.cuda.Stream(device=device)
src = torch.tensor([5], dtype=torch.int32, device=device)
def test_device_pinned_when_stream_is_provided(self) -> None:
"""Verify device staging uses pinned host memory with a stream."""
alt_stream = create_device_stream(DEFAULT_DEVICE)
src = torch.tensor([5], dtype=torch.int32, device=DEFAULT_DEVICE)
future = FutureTensors.device_to_host(xs_device=src, d2h_stream=alt_stream)
staged_tensors = [
v for v in future._data.values() if isinstance(v, torch.Tensor)
@@ -56,12 +60,11 @@ class TestFutureTensors(CustomTestCase):
self.assertTrue(all(t.is_pinned() for t in staged_tensors))
self.assertEqual(int(future.wait().item()), 5)
def test_cuda_each_call_allocates_fresh_host(self) -> None:
"""Verify each CUDA staging call owns a fresh host buffer."""
device = torch.device("cuda")
alt_stream = torch.cuda.Stream(device=device)
src_a = torch.tensor([13], dtype=torch.int32, device=device)
src_b = torch.tensor([29], dtype=torch.int32, device=device)
def test_device_each_call_allocates_fresh_host(self) -> None:
"""Verify each device staging call owns a fresh host buffer."""
alt_stream = create_device_stream(DEFAULT_DEVICE)
src_a = torch.tensor([13], dtype=torch.int32, device=DEFAULT_DEVICE)
src_b = torch.tensor([29], dtype=torch.int32, device=DEFAULT_DEVICE)
future_a = FutureTensors.device_to_host(xs_device=src_a, d2h_stream=alt_stream)
future_b = FutureTensors.device_to_host(xs_device=src_b, d2h_stream=alt_stream)
ptrs_a = {
@@ -77,11 +80,10 @@ class TestFutureTensors(CustomTestCase):
def test_dict_of_all_tensors_roundtrip(self) -> None:
"""Verify a dict of multiple tensors round-trips entry-by-entry."""
device = torch.device("cuda")
stream = torch.cuda.Stream(device=device)
stream = create_device_stream(DEFAULT_DEVICE)
src = {
"x": torch.tensor([11, 22], dtype=torch.int64, device=device),
"y": torch.tensor([99], dtype=torch.int32, device=device),
"x": torch.tensor([11, 22], dtype=torch.int64, device=DEFAULT_DEVICE),
"y": torch.tensor([99], dtype=torch.int32, device=DEFAULT_DEVICE),
}
future = FutureTensors.device_to_host(xs_device=src, d2h_stream=stream)
out = future.wait()
@@ -93,14 +95,13 @@ class TestFutureTensors(CustomTestCase):
def test_dict_mixes_tensor_and_passthrough(self) -> None:
"""Verify non-tensor dict entries ride through verbatim alongside staging."""
device = torch.device("cuda")
stream = torch.cuda.Stream(device=device)
stream = create_device_stream(DEFAULT_DEVICE)
sentinel_obj = {"nested": [1, 2, 3]}
src = {
"step": 42,
"label": "decode",
"extra": sentinel_obj,
"counter": torch.tensor([7], dtype=torch.int32, device=device),
"counter": torch.tensor([7], dtype=torch.int32, device=DEFAULT_DEVICE),
}
future = FutureTensors.device_to_host(xs_device=src, d2h_stream=stream)
out = future.wait()
@@ -113,9 +114,8 @@ class TestFutureTensors(CustomTestCase):
def test_dict_passthrough_preserves_tensor_value(self) -> None:
"""Verify tensors share device memory but non-tensor types are not staged."""
device = torch.device("cuda")
stream = torch.cuda.Stream(device=device)
src_tensor = torch.tensor([3], dtype=torch.int32, device=device)
stream = create_device_stream(DEFAULT_DEVICE)
src_tensor = torch.tensor([3], dtype=torch.int32, device=DEFAULT_DEVICE)
src = {"step": 100, "buf": src_tensor}
future = FutureTensors.device_to_host(xs_device=src, d2h_stream=stream)
out = future.wait()
@@ -128,8 +128,7 @@ class TestFutureTensors(CustomTestCase):
def test_dict_without_tensor_raises(self) -> None:
"""Verify a tensor-less dict raises (no device to anchor the d2h sync)."""
device = torch.device("cuda")
stream = torch.cuda.Stream(device=device)
stream = create_device_stream(DEFAULT_DEVICE)
with self.assertRaises(ValueError):
FutureTensors.device_to_host(
xs_device={"step": 0, "label": "decode"}, d2h_stream=stream
@@ -137,9 +136,8 @@ class TestFutureTensors(CustomTestCase):
def test_wait_called_twice_raises(self) -> None:
"""Verify wait() after the first drain raises (state cleared)."""
device = torch.device("cuda")
stream = torch.cuda.Stream(device=device)
src = torch.tensor([3], dtype=torch.int32, device=device)
stream = create_device_stream(DEFAULT_DEVICE)
src = torch.tensor([3], dtype=torch.int32, device=DEFAULT_DEVICE)
future = FutureTensors.device_to_host(xs_device=src, d2h_stream=stream)
self.assertEqual(int(future.wait().item()), 3)
with self.assertRaises(RuntimeError):
@@ -149,9 +147,7 @@ class TestFutureTensors(CustomTestCase):
"""Verify wait() syncs the event exactly once and clears internal state."""
tensor = torch.tensor([1, 2, 3])
event = _FakeEvent()
future = FutureTensors(
_data={"x": tensor}, _event=cast(torch.cuda.Event, event)
)
future = FutureTensors(_data={"x": tensor}, _event=event)
result = future.wait()
self.assertIs(result["x"], tensor)
@@ -166,11 +162,10 @@ class TestFutureTensors(CustomTestCase):
def test_dict_anchor_picked_from_first_tensor(self) -> None:
"""Verify staging works when the first key is a non-tensor (anchor must scan)."""
device = torch.device("cuda")
stream = torch.cuda.Stream(device=device)
stream = create_device_stream(DEFAULT_DEVICE)
src = {
"step": 5,
"buf": torch.tensor([17], dtype=torch.int32, device=device),
"buf": torch.tensor([17], dtype=torch.int32, device=DEFAULT_DEVICE),
}
out = FutureTensors.device_to_host(xs_device=src, d2h_stream=stream).wait()
self.assertEqual(out["step"], 5)
@@ -6,15 +6,21 @@ from types import SimpleNamespace
import torch
from sglang.srt.kv_canary.plan_input import PlanInput
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.ci.ci_register import (
register_amd_ci,
register_cuda_ci,
register_xpu_ci,
)
from sglang.test.kv_canary.fixtures import (
DEFAULT_DEVICE,
DEFAULT_DEVICE_MODULE,
make_forward_batch,
)
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=9, stage="extra-a", runner_config="1-gpu-small")
register_amd_ci(est_time=30, suite="extra-a-test-1-gpu-small-amd")
register_xpu_ci(est_time=30, suite="stage-b-test-1-gpu-xpu")
def _make_static_plan_input(*, bs_capacity: int, device) -> PlanInput:
@@ -122,7 +128,7 @@ class TestSelfUnitPlanInput(CustomTestCase):
fb.req_all_ids_lens = torch.tensor([7, 9], dtype=torch.int64, pin_memory=True)
plan = _make_static_plan_input(bs_capacity=4, device=self.device)
plan.fill_from_forward_batch(forward_batch=fb)
torch.cuda.synchronize()
DEFAULT_DEVICE_MODULE.synchronize()
self.assertEqual(
plan.req_to_verify_expected_tokens_valid_lens[:2].tolist(), [7, 9]
)
@@ -10,12 +10,21 @@ from sglang.srt.kv_canary.req_to_expected_token_ids_manager import (
compute_req_all_ids_info,
populate_req_to_expected_token_ids,
)
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.kv_canary.fixtures import DEFAULT_DEVICE, make_forward_batch
from sglang.test.ci.ci_register import (
register_amd_ci,
register_cuda_ci,
register_xpu_ci,
)
from sglang.test.kv_canary.fixtures import (
DEFAULT_DEVICE,
DEFAULT_DEVICE_MODULE,
make_forward_batch,
)
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=11, stage="extra-a", runner_config="1-gpu-small")
register_amd_ci(est_time=15, suite="extra-a-test-1-gpu-small-amd")
register_xpu_ci(est_time=30, suite="stage-b-test-1-gpu-xpu")
def _make_req(*, origin: list[int], output: list[int]) -> SimpleNamespace:
@@ -92,7 +101,7 @@ class TestPopulateReqToExpectedTokenIds(CustomTestCase):
populate_req_to_expected_token_ids(
forward_batch=fb, req_to_verify_expected_tokens=pool
)
torch.cuda.synchronize()
DEFAULT_DEVICE_MODULE.synchronize()
self.assertTrue(torch.equal(pool, original))
def test_no_op_when_pool_is_none(self) -> None:
@@ -117,7 +126,7 @@ class TestPopulateReqToExpectedTokenIds(CustomTestCase):
populate_req_to_expected_token_ids(
forward_batch=fb, req_to_verify_expected_tokens=pool
)
torch.cuda.synchronize()
DEFAULT_DEVICE_MODULE.synchronize()
self.assertTrue(torch.equal(pool, original))
def test_raises_when_lens_length_mismatches_batch_size(self) -> None:
@@ -154,7 +163,7 @@ class TestPopulateReqToExpectedTokenIds(CustomTestCase):
populate_req_to_expected_token_ids(
forward_batch=fb, req_to_verify_expected_tokens=pool
)
torch.cuda.synchronize()
DEFAULT_DEVICE_MODULE.synchronize()
pool_cpu = pool.cpu()
self.assertEqual(pool_cpu[1, :3].tolist(), [10, 20, 30])
@@ -15,24 +15,28 @@ from sglang.srt.kv_canary.runner.swa_divergence import (
SwaDivergenceReporter,
compute_swa_full_idx_divergence,
)
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.kv_canary.fixtures import make_buffer_group
from sglang.srt.utils import create_device_stream
from sglang.test.ci.ci_register import (
register_amd_ci,
register_cuda_ci,
register_xpu_ci,
)
from sglang.test.kv_canary.fixtures import DEFAULT_DEVICE, make_buffer_group
from sglang.test.kv_canary.runner_test_base import CanaryManagerTestCase, make_manager
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=11, stage="extra-a", runner_config="1-gpu-small")
register_amd_ci(est_time=45, suite="extra-a-test-1-gpu-small-amd")
_DEVICE = torch.device("cuda")
register_xpu_ci(est_time=60, suite="stage-b-test-1-gpu-xpu")
_EMPTY_FORWARD_BATCH = SimpleNamespace(
req_pool_indices=torch.empty(0, dtype=torch.int64, device=_DEVICE),
seq_lens=torch.empty(0, dtype=torch.int64, device=_DEVICE),
req_pool_indices=torch.empty(0, dtype=torch.int64, device=DEFAULT_DEVICE),
seq_lens=torch.empty(0, dtype=torch.int64, device=DEFAULT_DEVICE),
)
def _make_verify_plan(value: int) -> VerifyPlan:
plan = VerifyPlan.allocate(verify_capacity=4, device=_DEVICE)
plan = VerifyPlan.allocate(verify_capacity=4, device=DEFAULT_DEVICE)
plan.verify_num_valid.copy_(torch.tensor([value], dtype=torch.int32))
return plan
@@ -46,11 +50,13 @@ def _make_req_to_token_pool_stub(req_to_token: torch.Tensor) -> SimpleNamespace:
def _make_identity_mapping(size: int) -> torch.Tensor:
return torch.arange(size, dtype=torch.int64, device=_DEVICE)
return torch.arange(size, dtype=torch.int64, device=DEFAULT_DEVICE)
def _make_identity_req_to_token(num_reqs: int, max_seq_len: int) -> torch.Tensor:
base = torch.arange(num_reqs * max_seq_len, dtype=torch.int64, device=_DEVICE)
base = torch.arange(
num_reqs * max_seq_len, dtype=torch.int64, device=DEFAULT_DEVICE
)
return base.view(num_reqs, max_seq_len)
@@ -83,9 +89,9 @@ def _run_compute(
class TestSwaDivergenceReporter(CustomTestCase):
def test_swa_divergence_log_emitted(self) -> None:
d2h_stream = torch.cuda.Stream(device=_DEVICE)
d2h_stream = create_device_stream(DEFAULT_DEVICE)
stats = SwaDivergenceReporter(
device=_DEVICE,
device=DEFAULT_DEVICE,
d2h_stream=d2h_stream,
interval=10,
swa_allocator=None,
@@ -96,13 +102,13 @@ class TestSwaDivergenceReporter(CustomTestCase):
for forward_idx in range(3):
stats.observe_after_invoke_plan(
group=make_buffer_group(
device=_DEVICE, kind=PoolKind.FULL, has_v=False, num_slots=1
device=DEFAULT_DEVICE, kind=PoolKind.FULL, has_v=False, num_slots=1
),
verify_plan=_make_verify_plan(10),
)
stats.observe_after_invoke_plan(
group=make_buffer_group(
device=_DEVICE, kind=PoolKind.SWA, has_v=False, num_slots=1
device=DEFAULT_DEVICE, kind=PoolKind.SWA, has_v=False, num_slots=1
),
verify_plan=_make_verify_plan(3),
)
@@ -115,13 +121,13 @@ class TestSwaDivergenceReporter(CustomTestCase):
# the staged future hangs onto it. forward_ct is now 4.
stats.observe_after_invoke_plan(
group=make_buffer_group(
device=_DEVICE, kind=PoolKind.FULL, has_v=False, num_slots=1
device=DEFAULT_DEVICE, kind=PoolKind.FULL, has_v=False, num_slots=1
),
verify_plan=_make_verify_plan(10),
)
stats.observe_after_invoke_plan(
group=make_buffer_group(
device=_DEVICE, kind=PoolKind.SWA, has_v=False, num_slots=1
device=DEFAULT_DEVICE, kind=PoolKind.SWA, has_v=False, num_slots=1
),
verify_plan=_make_verify_plan(3),
)
@@ -150,9 +156,9 @@ class TestSwaDivergenceReporter(CustomTestCase):
self.assertEqual(fields.swa_full_idx_divergence, 0)
def test_swa_divergence_counts_monotonic_increasing(self) -> None:
d2h_stream = torch.cuda.Stream(device=_DEVICE)
d2h_stream = create_device_stream(DEFAULT_DEVICE)
stats = SwaDivergenceReporter(
device=_DEVICE,
device=DEFAULT_DEVICE,
d2h_stream=d2h_stream,
interval=10,
swa_allocator=None,
@@ -188,13 +194,19 @@ class TestSwaDivergenceReporter(CustomTestCase):
for _ in range(5):
stats.observe_after_invoke_plan(
group=make_buffer_group(
device=_DEVICE, kind=PoolKind.FULL, has_v=False, num_slots=1
device=DEFAULT_DEVICE,
kind=PoolKind.FULL,
has_v=False,
num_slots=1,
),
verify_plan=_make_verify_plan(7),
)
stats.observe_after_invoke_plan(
group=make_buffer_group(
device=_DEVICE, kind=PoolKind.SWA, has_v=False, num_slots=1
device=DEFAULT_DEVICE,
kind=PoolKind.SWA,
has_v=False,
num_slots=1,
),
verify_plan=_make_verify_plan(2),
)
@@ -216,8 +228,8 @@ class TestSwaFullIdxDivergenceCompute(CustomTestCase):
req_to_token = _make_identity_req_to_token(num_reqs=4, max_seq_len=16)
forward_batch = _make_forward_batch(
req_pool_indices=torch.empty(0, dtype=torch.int64, device=_DEVICE),
seq_lens=torch.empty(0, dtype=torch.int64, device=_DEVICE),
req_pool_indices=torch.empty(0, dtype=torch.int64, device=DEFAULT_DEVICE),
seq_lens=torch.empty(0, dtype=torch.int64, device=DEFAULT_DEVICE),
)
self.assertEqual(
@@ -234,8 +246,10 @@ class TestSwaFullIdxDivergenceCompute(CustomTestCase):
req_to_token = _make_identity_req_to_token(num_reqs=4, max_seq_len=16)
forward_batch = _make_forward_batch(
req_pool_indices=torch.tensor([0, 2], dtype=torch.int64, device=_DEVICE),
seq_lens=torch.tensor([8, 5], dtype=torch.int64, device=_DEVICE),
req_pool_indices=torch.tensor(
[0, 2], dtype=torch.int64, device=DEFAULT_DEVICE
),
seq_lens=torch.tensor([8, 5], dtype=torch.int64, device=DEFAULT_DEVICE),
)
self.assertEqual(
@@ -256,8 +270,10 @@ class TestSwaFullIdxDivergenceCompute(CustomTestCase):
mapping[17] = 60
forward_batch = _make_forward_batch(
req_pool_indices=torch.tensor([0, 1], dtype=torch.int64, device=_DEVICE),
seq_lens=torch.tensor([8, 8], dtype=torch.int64, device=_DEVICE),
req_pool_indices=torch.tensor(
[0, 1], dtype=torch.int64, device=DEFAULT_DEVICE
),
seq_lens=torch.tensor([8, 8], dtype=torch.int64, device=DEFAULT_DEVICE),
)
self.assertEqual(
@@ -281,8 +297,10 @@ class TestSwaFullIdxDivergenceCompute(CustomTestCase):
mapping[7] = 42
forward_batch = _make_forward_batch(
req_pool_indices=torch.tensor([0], dtype=torch.int64, device=_DEVICE),
seq_lens=torch.tensor([8], dtype=torch.int64, device=_DEVICE),
req_pool_indices=torch.tensor(
[0], dtype=torch.int64, device=DEFAULT_DEVICE
),
seq_lens=torch.tensor([8], dtype=torch.int64, device=DEFAULT_DEVICE),
)
self.assertEqual(
@@ -302,8 +320,10 @@ class TestSwaFullIdxDivergenceCompute(CustomTestCase):
mapping[28] = 77
forward_batch = _make_forward_batch(
req_pool_indices=torch.tensor([0], dtype=torch.int64, device=_DEVICE),
seq_lens=torch.tensor([10], dtype=torch.int64, device=_DEVICE),
req_pool_indices=torch.tensor(
[0], dtype=torch.int64, device=DEFAULT_DEVICE
),
seq_lens=torch.tensor([10], dtype=torch.int64, device=DEFAULT_DEVICE),
)
self.assertEqual(
@@ -325,12 +345,16 @@ class TestSwaFullIdxDivergenceCompute(CustomTestCase):
mapping[33] = 100
fb_req0 = _make_forward_batch(
req_pool_indices=torch.tensor([0], dtype=torch.int64, device=_DEVICE),
seq_lens=torch.tensor([4], dtype=torch.int64, device=_DEVICE),
req_pool_indices=torch.tensor(
[0], dtype=torch.int64, device=DEFAULT_DEVICE
),
seq_lens=torch.tensor([4], dtype=torch.int64, device=DEFAULT_DEVICE),
)
fb_req2 = _make_forward_batch(
req_pool_indices=torch.tensor([2], dtype=torch.int64, device=_DEVICE),
seq_lens=torch.tensor([4], dtype=torch.int64, device=_DEVICE),
req_pool_indices=torch.tensor(
[2], dtype=torch.int64, device=DEFAULT_DEVICE
),
seq_lens=torch.tensor([4], dtype=torch.int64, device=DEFAULT_DEVICE),
)
self.assertEqual(
@@ -363,15 +387,17 @@ class TestSwaDivergenceReporterWithCompute(CustomTestCase):
mapping[2] = 52
forward_batch = _make_forward_batch(
req_pool_indices=torch.tensor([0], dtype=torch.int64, device=_DEVICE),
seq_lens=torch.tensor([8], dtype=torch.int64, device=_DEVICE),
req_pool_indices=torch.tensor(
[0], dtype=torch.int64, device=DEFAULT_DEVICE
),
seq_lens=torch.tensor([8], dtype=torch.int64, device=DEFAULT_DEVICE),
)
swa_allocator = _make_allocator_stub(mapping)
req_to_token_pool = _make_req_to_token_pool_stub(req_to_token)
d2h_stream = torch.cuda.Stream(device=_DEVICE)
d2h_stream = create_device_stream(DEFAULT_DEVICE)
stats = SwaDivergenceReporter(
device=_DEVICE,
device=DEFAULT_DEVICE,
d2h_stream=d2h_stream,
interval=10,
swa_allocator=swa_allocator,
@@ -379,13 +405,13 @@ class TestSwaDivergenceReporterWithCompute(CustomTestCase):
)
stats.observe_after_invoke_plan(
group=make_buffer_group(
device=_DEVICE, kind=PoolKind.FULL, has_v=False, num_slots=1
device=DEFAULT_DEVICE, kind=PoolKind.FULL, has_v=False, num_slots=1
),
verify_plan=_make_verify_plan(11),
)
stats.observe_after_invoke_plan(
group=make_buffer_group(
device=_DEVICE, kind=PoolKind.SWA, has_v=False, num_slots=1
device=DEFAULT_DEVICE, kind=PoolKind.SWA, has_v=False, num_slots=1
),
verify_plan=_make_verify_plan(3),
)
@@ -17,6 +17,7 @@ from sglang.srt.distributed.parallel_state import (
from sglang.srt.runtime_context import get_parallel
from sglang.srt.utils import get_device, get_device_count
from sglang.test.ci.ci_register import register_cuda_ci, register_xpu_ci
from sglang.test.test_utils import publish_build_topology
register_cuda_ci(est_time=30, stage="base-b", runner_config="2-gpu-large")
register_xpu_ci(est_time=60, suite="nightly-xpu-2-gpu", nightly=True)
@@ -105,7 +106,8 @@ def mixer2_gated_norm_tensor_parallel(
local_rank=local_rank,
backend=get_default_distributed_backend(device.type),
)
initialize_model_parallel(tensor_model_parallel_size=world_size)
publish_build_topology(tp_size=world_size, world_rank=local_rank)
initialize_model_parallel()
# create random weights an inputs
weight = torch.rand((hidden_size,), dtype=dtype, device=device)
@@ -127,8 +129,24 @@ def mixer2_gated_norm_tensor_parallel(
)
mixer.weight.weight_loader(mixer.weight, weight)
# m2 reads tp via get_parallel().tp_size/rank — force it through the context.
with get_parallel().override(tp_size=1, tp_rank=0):
with get_parallel().override(
tp_size=1,
tp_rank=0,
tp_group=None,
attn_tp_size=1,
attn_tp_rank=0,
attn_tp_group=None,
attn_dp_size=1,
attn_dp_rank=0,
attn_cp_size=1,
attn_cp_rank=0,
attn_cp_group=None,
moe_ep_size=1,
moe_ep_rank=0,
moe_ep_group=None,
moe_dp_size=1,
moe_tp_size=1,
):
# create gated-norm without TP to compute reference
mixer_single_gpu = m2.Mixer2RMSNormGated(
full_hidden_size=hidden_size,
@@ -1,396 +0,0 @@
from __future__ import annotations
import socket
import sys
from dataclasses import dataclass
import pytest
import torch
import torch.nn.functional as F
from sglang.kernels.ops.attention.fla.layernorm_gated import (
_layer_norm_fwd as layer_norm_fwd,
)
from sglang.kernels.ops.attention.fla.layernorm_gated import (
layernorm_fn,
rms_norm_ref,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(
est_time=60,
stage="base-b",
runner_config="2-gpu-large",
disabled="Temporarily disabled",
)
# Optional dependency in sglang repo; skip collection cleanly if absent.
custom_all_reduce_utils = pytest.importorskip(
"sglang.srt.distributed.device_communicators.custom_all_reduce_utils"
)
parallel_state = pytest.importorskip("sglang.srt.distributed.parallel_state")
update_environment_variables = custom_all_reduce_utils.update_environment_variables
init_distributed_environment = parallel_state.init_distributed_environment
initialize_model_parallel = parallel_state.initialize_model_parallel
NUM_GPUS = 2
def _find_free_port() -> int:
# Avoid hard-coded port collisions when pytest runs tests in parallel.
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("localhost", 0))
s.listen(1)
return int(s.getsockname()[1])
def _skip_if_no_cuda_or_not_enough_gpus(required_gpus: int = NUM_GPUS) -> None:
if not torch.cuda.is_available():
pytest.skip("CUDA device not available")
if torch.cuda.device_count() < required_gpus:
pytest.skip(f"Need >= {required_gpus} GPUs, got {torch.cuda.device_count()}")
def _skip_if_dtype_unsupported(dtype: torch.dtype) -> None:
if dtype is torch.bfloat16 and not torch.cuda.is_bf16_supported():
pytest.skip("bfloat16 not supported on this CUDA device")
def _setup_sglang_distributed(
local_rank: int,
world_size: int,
master_port: int,
dtype: torch.dtype,
) -> torch.device:
# Match sglang test style: set per-rank CUDA device + default dtype/device.
torch.manual_seed(0)
torch.cuda.manual_seed_all(0)
device = torch.device(f"cuda:{local_rank}")
torch.cuda.set_device(device)
if hasattr(torch, "set_default_device"):
torch.set_default_device(device)
if hasattr(torch, "set_default_dtype"):
torch.set_default_dtype(dtype)
update_environment_variables(
{
"RANK": str(local_rank),
"LOCAL_RANK": str(local_rank),
"WORLD_SIZE": str(world_size),
"MASTER_ADDR": "localhost",
"MASTER_PORT": str(master_port),
}
)
init_distributed_environment(
world_size=world_size, rank=local_rank, local_rank=local_rank
)
initialize_model_parallel(tensor_model_parallel_size=world_size)
return device
def layer_norm_ref(
x: torch.Tensor,
weight: torch.Tensor,
bias: torch.Tensor | None,
z: torch.Tensor | None = None,
eps: float = 1e-6,
group_size: int | None = None,
norm_before_gate: bool = True,
is_rms_norm: bool = False,
) -> torch.Tensor:
"""Reference implementation for both LayerNorm and RMSNorm (supports optional gate + group norm)."""
if is_rms_norm:
return rms_norm_ref(
x,
weight,
bias,
z=z,
eps=eps,
group_size=group_size,
norm_before_gate=norm_before_gate,
upcast=True,
)
dtype = x.dtype
x_f = x.float()
w_f = weight.float()
b_f = bias.float() if bias is not None else None
z_f = z.float() if z is not None else None
if z_f is not None and not norm_before_gate:
x_f = x_f * F.silu(z_f)
if group_size is None:
mean = x_f.mean(dim=-1, keepdim=True)
var = (x_f - mean).square().mean(dim=-1, keepdim=True)
rstd = torch.rsqrt(var + eps)
out = (x_f - mean) * rstd * w_f
if b_f is not None:
out = out + b_f
else:
hidden = x_f.shape[-1]
assert hidden % group_size == 0
ng = hidden // group_size
xg = x_f.view(*x_f.shape[:-1], ng, group_size)
mean = xg.mean(dim=-1, keepdim=True)
var = (xg - mean).square().mean(dim=-1, keepdim=True)
rstd = torch.rsqrt(var + eps)
xg = (xg - mean) * rstd
out = xg.reshape(*x_f.shape[:-1], hidden) * w_f
if b_f is not None:
out = out + b_f
if z_f is not None and norm_before_gate:
out = out * F.silu(z_f)
return out.to(dtype)
@dataclass(frozen=True)
class FwdCase:
name: str
with_gate: bool
norm_before_gate: bool
group_size: int | None
is_rms_norm: bool
CASES: list[FwdCase] = [
FwdCase(
"layernorm",
with_gate=False,
norm_before_gate=True,
group_size=None,
is_rms_norm=False,
),
FwdCase(
"rmsnorm",
with_gate=False,
norm_before_gate=True,
group_size=None,
is_rms_norm=True,
),
FwdCase(
"layernorm_gate_pre",
with_gate=True,
norm_before_gate=True,
group_size=None,
is_rms_norm=False,
),
FwdCase(
"layernorm_gate_post",
with_gate=True,
norm_before_gate=False,
group_size=None,
is_rms_norm=False,
),
FwdCase(
"rmsnorm_gate_pre",
with_gate=True,
norm_before_gate=True,
group_size=None,
is_rms_norm=True,
),
FwdCase(
"group_layernorm",
with_gate=False,
norm_before_gate=True,
group_size=128,
is_rms_norm=False,
),
FwdCase(
"group_rmsnorm",
with_gate=False,
norm_before_gate=True,
group_size=128,
is_rms_norm=True,
),
]
@pytest.mark.parametrize("num_tokens", [128])
@pytest.mark.parametrize("hidden_size", [256])
@pytest.mark.parametrize("dtype", [torch.bfloat16])
@pytest.mark.parametrize("case", CASES, ids=lambda c: c.name)
def test_layernorm_guard_fwd_spawn(
num_tokens: int,
hidden_size: int,
dtype: torch.dtype,
case: FwdCase,
device: str = "cuda",
):
_skip_if_no_cuda_or_not_enough_gpus(NUM_GPUS)
_skip_if_dtype_unsupported(dtype)
if case.group_size is not None and hidden_size % case.group_size != 0:
pytest.skip(
f"hidden_size {hidden_size} not divisible by group_size {case.group_size}"
)
master_port = _find_free_port()
world_size = NUM_GPUS
torch.multiprocessing.spawn(
_layernorm_guard_fwd_worker,
args=(
world_size,
master_port,
num_tokens,
hidden_size,
dtype,
case,
device,
),
nprocs=world_size,
join=True,
)
def _layernorm_guard_fwd_worker(
local_rank: int,
world_size: int,
master_port: int,
num_tokens: int,
hidden_size: int,
dtype: torch.dtype,
case: FwdCase,
device: str,
):
device = _setup_sglang_distributed(local_rank, world_size, master_port, dtype)
with torch.inference_mode():
torch.manual_seed(42 + local_rank)
torch.cuda.manual_seed_all(42 + local_rank)
x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
z = (
torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
if case.with_gate
else None
)
weight = torch.randn(hidden_size, dtype=dtype, device=device)
bias = (
None
if case.is_rms_norm
else torch.randn(hidden_size, dtype=dtype, device=device)
)
eps = 1e-6
out, mean, rstd = layer_norm_fwd(
x,
weight,
bias,
eps,
z=z,
group_size=case.group_size,
norm_before_gate=case.norm_before_gate,
is_rms_norm=case.is_rms_norm,
)
ref_out = layer_norm_ref(
x,
weight,
bias,
z=z,
eps=eps,
group_size=case.group_size,
norm_before_gate=case.norm_before_gate,
is_rms_norm=case.is_rms_norm,
)
assert out.shape == x.shape
assert out.dtype == x.dtype
torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
# mean/rstd shape checks (same spirit as original vLLM tests)
if case.group_size is None:
if not case.is_rms_norm:
assert mean.shape == (num_tokens,)
assert rstd.shape == (num_tokens,)
else:
ngroups = hidden_size // case.group_size
if not case.is_rms_norm:
assert mean.shape == (ngroups * num_tokens,)
assert rstd.shape == (ngroups * num_tokens,)
@pytest.mark.parametrize("dtype", [torch.bfloat16])
def test_layernorm_guard_misc_spawn(dtype: torch.dtype, device: str = "cuda"):
_skip_if_no_cuda_or_not_enough_gpus(NUM_GPUS)
_skip_if_dtype_unsupported(dtype)
master_port = _find_free_port()
world_size = NUM_GPUS
torch.multiprocessing.spawn(
_layernorm_guard_misc_worker,
args=(world_size, master_port, dtype, device),
nprocs=world_size,
join=True,
)
def _layernorm_guard_misc_worker(
local_rank: int,
world_size: int,
master_port: int,
dtype: torch.dtype,
device: str,
):
device = _setup_sglang_distributed(local_rank, world_size, master_port, dtype)
with torch.inference_mode():
torch.manual_seed(123 + local_rank)
torch.cuda.manual_seed_all(123 + local_rank)
# 1) rows_per_block-like sizes
hidden_size = 1024
weight = torch.randn(hidden_size, dtype=dtype, device=device)
bias = torch.randn(hidden_size, dtype=dtype, device=device)
eps = 1e-6
for num_tokens in [513]:
x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
out, _, _ = layer_norm_fwd(x, weight, bias, eps, z=None, is_rms_norm=False)
ref = layer_norm_ref(x, weight, bias, z=None, eps=eps, is_rms_norm=False)
torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2)
# 2) strided input (slice then contiguous)
num_tokens = 128
x_large = torch.randn(num_tokens, hidden_size * 2, dtype=dtype, device=device)
x = x_large[:, :hidden_size]
x_contig = x.contiguous()
out, _, _ = layer_norm_fwd(
x_contig, weight, bias, eps, z=None, is_rms_norm=False
)
ref = layer_norm_ref(x_contig, weight, bias, z=None, eps=eps, is_rms_norm=False)
torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2)
# 3) provided output buffer
num_tokens = 256
x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)
out_buf = torch.empty_like(x)
out, _, _ = layer_norm_fwd(
x, weight, bias, eps, z=None, out=out_buf, is_rms_norm=False
)
assert out.data_ptr() == out_buf.data_ptr()
ref = layer_norm_ref(x, weight, bias, z=None, eps=eps, is_rms_norm=False)
torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2)
# 4) multidimensional input via autograd fn
for shape in [(4, 16, 1024)]:
hs = shape[-1]
x = torch.randn(*shape, dtype=dtype, device=device)
w = torch.randn(hs, dtype=dtype, device=device)
b = torch.randn(hs, dtype=dtype, device=device)
out = layernorm_fn(x, w, b, z=None, eps=eps)
ref = layer_norm_ref(x, w, b, z=None, eps=eps, is_rms_norm=False)
torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2)
if __name__ == "__main__":
sys.exit(pytest.main([__file__]))
@@ -47,6 +47,13 @@ class TestPostCaptureKVSizing(CustomTestCase):
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
env={**os.environ, "SGLANG_ENABLE_POST_CAPTURE_KV_SIZING": "1"},
return_stdout_stderr=(cls.stdout, cls.stderr),
other_args=[
"--enable-hierarchical-cache",
"--hicache-mem-layout",
"page_first",
"--hicache-size",
"1",
],
)
@classmethod
@@ -78,6 +85,10 @@ class TestPostCaptureKVSizing(CustomTestCase):
"or the resize path did not run.",
)
self.assertGreater(float(m.group(1)), 0)
logs = self._server_logs()
staging = logs.find("HiCache staging prepared before KV sizing:")
self.assertGreaterEqual(staging, 0, "HiCache staging was not prepared")
self.assertLess(staging, m.start())
def test_server_info_pool_sized(self):
info = requests.get(f"{self.base_url}/server_info").json()
+47
View File
@@ -0,0 +1,47 @@
from __future__ import annotations
import unittest
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.mock_model.utils import run_mock_model_bench_serving
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=50, stage="extra-a", runner_config="2-gpu-large")
class TestE2EContextParallel(CustomTestCase):
def test_cp_prefill_then_decode_no_canary_violation(self) -> None:
# CP prefill enters the transformer body directly, while decode calls
# the outer model.forward. Both need exactly one canary bracket.
run_mock_model_bench_serving(
extra_server_args=[
"--tp",
"2",
"--attn-cp-size",
"2",
"--enable-prefill-cp",
"--cp-strategy",
"zigzag",
"--attention-backend",
"fa3",
"--kv-canary-real-data",
"all",
"--mem-fraction-static",
"0.2",
"--max-total-tokens",
"4096",
"--max-running-requests",
"8",
"--context-length",
"256",
"--cuda-graph-max-bs-decode",
"4",
],
num_prompts=4,
random_input_len=32,
random_output_len=8,
)
if __name__ == "__main__":
unittest.main()
@@ -1,3 +1,5 @@
import os
import subprocess
import unittest
from sglang.test.ascend.e2e.test_npu_accuracy_utils import (
@@ -8,6 +10,8 @@ from sglang.test.ascend.e2e.test_npu_performance_utils import (
DEEPSEEK_V4_FLASH_0731_W8A8_MODEL_PATH,
)
from sglang.test.ci.ci_register import register_npu_ci
from sglang.test.test_utils import DEFAULT_URL_FOR_TEST
from sglang.utils import wait_for_server
register_npu_ci(
est_time=3600,
@@ -29,6 +33,7 @@ DEEPSEEK_V4_FLASH_W8A8_DSPARK_8P_ENVS = {
"HCCL_SOCKET_IFNAME": "lo",
"GLOO_SOCKET_IFNAME": "lo",
"HCCL_OP_EXPANSION_MODE": "AIV",
"SGLANG_NPU_USE_MULTI_STREAM": "1",
# skip gpu branch
"SGLANG_OPT_FP8_WO_A_GEMM": "0",
"SGLANG_OPT_USE_OVERLAP_STORE_CACHE": "False",
@@ -43,10 +48,13 @@ DEEPSEEK_V4_FLASH_W8A8_DSPARK_8P_ENVS = {
# DSPARK
"SGLANG_RAGGED_VERIFY_MODE": "static",
"SGLANG_DSPARK_FAST_KERNEL": "0",
# mtp
"SGLANG_ENABLE_SPEC_V2": "1",
"SGLANG_ENABLE_OVERLAP_PLAN_STREAM": "1",
# deepep
"DEEP_NORMAL_MODE_USE_INT8_QUANT": "1",
"DEEPEP_HCCL_BUFFSIZE": "2048",
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "64",
"DEEPEP_HCCL_BUFFSIZE": "2500",
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "96",
"DEEPEP_HYBRID_DEPLOYMENT": "1",
# war barrier
"SGLANG_ENABLE_WAR_BARRIER": "1",
@@ -66,15 +74,15 @@ DEEPSEEK_V4_FLASH_W8A8_DSPARK_8P_OTHER_ARGS = [
"--watchdog-timeout",
9000,
"--mem-fraction-static",
0.62,
0.68,
"--prefill-max-requests",
32,
192,
"--max-prefill-tokens",
131072,
80000,
"--chunked-prefill-size",
131072,
"--max-running-requests",
96,
192,
"--dp-size",
16,
"--enable-dp-attention",
@@ -87,6 +95,8 @@ DEEPSEEK_V4_FLASH_W8A8_DSPARK_8P_OTHER_ARGS = [
"--enable-dp-lm-head",
"--kv-cache-dtype",
"bfloat16",
"--load-balance-method",
"round_robin",
"--speculative-algorithm",
"DSPARK",
"--speculative-draft-model-path",
@@ -96,16 +106,17 @@ DEEPSEEK_V4_FLASH_W8A8_DSPARK_8P_OTHER_ARGS = [
"--speculative-draft-attention-backend",
"ascend",
"--speculative-num-draft-tokens",
6,
7,
"--speculative-dspark-block-size",
5,
"--skip-server-warmup",
6,
"--cuda-graph-bs-decode",
1,
2,
4,
5,
6,
8,
10,
"--disable-radix-cache",
]
@@ -136,6 +147,31 @@ class TestNPUDeepSeekV4FlashW8A88PGPQA(TestNpuAccuracyTestCaseBase):
timeout = 6000
seed = 1
@classmethod
def setUpClass(cls):
"""Launch server via `python3 -m sglang.launch_server` instead of `sglang serve`."""
cls._setup_per_case_output()
cls.base_url = DEFAULT_URL_FOR_TEST
env = os.environ.copy()
if cls.envs:
env.update(cls.envs)
_, host, port = cls.base_url.split(":")
command = [
"python3",
"-m",
"sglang.launch_server",
"--model-path",
cls.model,
*[str(x) for x in cls.other_args],
"--host",
host[2:],
"--port",
port,
]
cls.process = subprocess.Popen(command, env=env)
wait_for_server(cls.base_url, timeout=cls.server_timeout, process=cls.process)
def test_npu_deepseek_v4_flash_w8a8_8p_gpqa(self):
"""Run NPU accuracy test for DeepSeek-V4-Flash W8A8 8p DSPARK GPQA."""
self.run_accuracy()
@@ -38,8 +38,6 @@ class TestAscendMhaHicache(CustomTestCase):
"--attention-backend",
"ascend",
"--enable-hierarchical-cache",
"--hicache-ratio",
1.2,
]
def test_a_gsm8k(self):
@@ -40,8 +40,6 @@ class TestAscendMlaHicache(CustomTestCase):
"--tp-size",
4,
"--enable-hierarchical-cache",
"--hicache-ratio",
1.2,
]
def test_a_gsm8k(self):
@@ -1,3 +1,5 @@
import os
import subprocess
import unittest
from sglang.test.ascend.e2e.test_npu_performance_utils import (
@@ -7,6 +9,8 @@ from sglang.test.ascend.e2e.test_npu_performance_utils import (
TestNpuPerformanceTestCaseBase,
)
from sglang.test.ci.ci_register import register_npu_ci
from sglang.test.test_utils import DEFAULT_URL_FOR_TEST
from sglang.utils import wait_for_server
register_npu_ci(est_time=1800, suite="nightly-perf-16-npu-a3", nightly=True)
register_npu_ci(est_time=1800, suite="nightly-perf-16-npu-a3-cann910", nightly=True)
@@ -23,9 +27,11 @@ DEEPSEEK_V4_FLASH_W8A8_8P_ENVS = {
"SGLANG_NPU_USE_MULTI_STREAM": "1",
# deepep
"DEEP_NORMAL_MODE_USE_INT8_QUANT": "1",
"DEEPEP_HCCL_BUFFSIZE": "2048",
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "35",
"DEEPEP_HCCL_BUFFSIZE": "2500",
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "96",
"DEEPEP_HYBRID_DEPLOYMENT": "1",
"SGLANG_RAGGED_VERIFY_MODE": "static",
"SGLANG_DSPARK_FAST_KERNEL": "0",
# war barrier
"SGLANG_ENABLE_WAR_BARRIER": "1",
"SGLANG_FORCE_COARSE_WAR_BARRIER": "1",
@@ -55,7 +61,7 @@ DEEPSEEK_V4_FLASH_W8A8_8P_OTHER_ARGS = [
"--device",
"npu",
"--prefill-max-requests",
160,
192,
"--max-prefill-tokens",
80000,
"--attention-backend",
@@ -67,7 +73,7 @@ DEEPSEEK_V4_FLASH_W8A8_8P_OTHER_ARGS = [
"--chunked-prefill-size",
131072,
"--max-running-requests",
160,
192,
"--dp-size",
16,
"--enable-dp-attention",
@@ -80,24 +86,28 @@ DEEPSEEK_V4_FLASH_W8A8_8P_OTHER_ARGS = [
"--enable-dp-lm-head",
"--kv-cache-dtype",
"bfloat16",
"--skip-server-warmup",
"--load-balance-method",
"round_robin",
"--cuda-graph-bs-decode",
1,
2,
4,
6,
8,
10,
# MTP (EAGLE) configuration.
# DSPARK configuration.
"--speculative-algorithm",
"EAGLE",
"--speculative-num-steps",
2,
"--speculative-eagle-topk",
1,
"DSPARK",
"--speculative-draft-model-path",
DEEPSEEK_V4_FLASH_0731_W8A8_MODEL_PATH,
"--speculative-draft-model-quantization",
"modelslim",
"--speculative-draft-attention-backend",
"ascend",
"--speculative-num-draft-tokens",
3,
"--ep-size",
16,
7,
"--speculative-dspark-block-size",
6,
"--disable-radix-cache",
]
@@ -111,18 +121,42 @@ class TestNPUDeepSeekV4FlashW8A88PIn8kOut1k50ms(TestNpuPerformanceTestCaseBase):
other_args = DEEPSEEK_V4_FLASH_W8A8_8P_OTHER_ARGS
envs = DEEPSEEK_V4_FLASH_W8A8_8P_ENVS
dataset_name = "random"
dataset_path = "/root/.cache/modelscope/hub/datasets/gsm8k_deepseekv4/cache0_8000/formal_run1_160_8000_cache0.json"
input_len = 8000
output_len = 1000
num_prompts = 160
max_concurrency = 160
random_range_ratio = 1
warmup_requests = 16
warmup_requests = 32
request_rate = float("inf")
seed = 1
tpot = 50
max_attempts = 3
output_token_throughput = 2825
output_token_throughput = 3100
accept_rate = 0.5
@classmethod
def setUpClass(cls):
"""Launch server via `python3 -m sglang.launch_server` instead of `sglang serve`."""
cls._setup_per_case_output()
cls.base_url = DEFAULT_URL_FOR_TEST
env = os.environ.copy()
if cls.envs:
env.update(cls.envs)
_, host, port = cls.base_url.split(":")
command = [
"python3",
"-m",
"sglang.launch_server",
"--model-path",
cls.model,
*[str(x) for x in cls.other_args],
"--host",
host[2:],
"--port",
port,
]
cls.process = subprocess.Popen(command, env=env)
wait_for_server(cls.base_url, timeout=cls.timeout, process=cls.process)
def test_npu_deepseek_v4_flash_w8a8_8p_in8k_out1k_50ms(self):
"""Run NPU performance test for DeepSeek-V4-Flash W8A8 8p in8k out1k."""
@@ -2,6 +2,7 @@
import unittest
import uuid
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, List
from urllib.parse import urlparse
@@ -61,26 +62,39 @@ class TestEncoderServerMetrics(CustomTestCase):
self.assertEqual(health.status_code, 200)
req_id = f"metrics-probe-{uuid.uuid4().hex}"
requests.post(
f"{DEFAULT_URL_FOR_TEST}/scheduler_receive_url",
json={
"req_id": req_id,
"receive_url": f"{base_host}:{recv_port}",
"receive_count": 1,
},
)
response = requests.post(
f"{DEFAULT_URL_FOR_TEST}/encode",
json={
"req_id": req_id,
"modality": "IMAGE",
"mm_items": [f"data:image/png;base64,{MINIMUM_PNG_PICTURE_BASE64}"],
"num_parts": 1,
"part_idx": 0,
"embedding_port": None,
},
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
)
# A scheduler registers concurrently with /encode, never before
# it: the request state only exists once /encode dispatches.
with ThreadPoolExecutor(max_workers=1) as pool:
registration = pool.submit(
requests.post,
f"{DEFAULT_URL_FOR_TEST}/scheduler_receive_url",
json={
"req_id": req_id,
"receive_url": f"{base_host}:{recv_port}",
"receive_count": 1,
},
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
)
response = requests.post(
f"{DEFAULT_URL_FOR_TEST}/encode",
json={
"req_id": req_id,
"modality": "IMAGE",
"mm_items": [
f"data:image/png;base64,{MINIMUM_PNG_PICTURE_BASE64}"
],
"num_parts": 1,
"part_idx": 0,
"embedding_port": None,
},
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
)
registration_response = registration.result(
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH
)
# A 200 from /encode alone does not prove the embedding reached
# anyone; the registration is the other half of that contract.
self.assertEqual(registration_response.status_code, 200)
self.assertEqual(response.status_code, 200)
metrics_response = requests.get(f"{DEFAULT_URL_FOR_TEST}/metrics")
@@ -14,7 +14,7 @@ import torch
from sglang.srt.layers import communicator as comm
from sglang.srt.layers.communicator import LayerCommunicator, ScatterMode
from sglang.test.ci.ci_register import register_amd_ci
from sglang.test.test_utils import CustomTestCase
from sglang.test.test_utils import CustomTestCase, publish_build_topology
register_amd_ci(est_time=240, suite="stage-c-test-large-8-gpu-amd")
@@ -64,7 +64,8 @@ def _run_residual_accuracy_check():
distributed_init_method="env://",
backend="nccl",
)
initialize_model_parallel(tensor_model_parallel_size=world_size)
publish_build_topology(tp_size=world_size, world_rank=rank)
initialize_model_parallel()
dtype = torch.bfloat16
eps = 1e-6
@@ -37,10 +37,7 @@ def _mock_global_server_args(backend="pytorch"):
class _DummyTPGroup:
device_group = None
# `Sampler.__init__` asks the context for the group; state one for the rest
# of the process, since this process has no distributed init. Not the scoped
# `override()`: its context manager would be collected here and take the
# value back down with it.
# Provide a TP group for sampler initialization without distributed setup.
get_parallel().override_permanently(tp_group=_DummyTPGroup())
from sglang.srt.runtime_context import get_flags
@@ -1,53 +0,0 @@
import unittest
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import (
DEFAULT_MODEL_NAME_FOR_TEST,
DEFAULT_MOE_MODEL_NAME_FOR_TEST,
CustomTestCase,
is_in_amd_ci,
is_in_ci,
run_bench_offline_throughput,
write_github_step_summary,
)
register_cuda_ci(est_time=162, stage="extra-a", runner_config="2-gpu-large")
register_amd_ci(est_time=630, suite="stage-b-test-2-gpu-large-amd")
class TestBenchOneBatch2GPU(CustomTestCase):
def test_moe_tp2_bs1(self):
output_throughput = run_bench_offline_throughput(
DEFAULT_MOE_MODEL_NAME_FOR_TEST,
["--tp", "2", "--cuda-graph-max-bs-decode", "2"],
)
if is_in_ci():
write_github_step_summary(
f"### test_moe_tp2_bs1 (Mixtral-8x7B)\n"
f"output_throughput: {output_throughput:.2f} token/s\n"
)
if is_in_amd_ci():
self.assertGreater(output_throughput, 85)
else:
self.assertGreater(output_throughput, 125)
def test_torch_compile_tp2_bs1(self):
output_throughput = run_bench_offline_throughput(
DEFAULT_MODEL_NAME_FOR_TEST,
["--tp", "2", "--enable-torch-compile", "--cuda-graph-max-bs-decode", "2"],
)
if is_in_ci():
write_github_step_summary(
f"### test_torch_compile_tp2_bs1 (Mixtral-8x7B)\n"
f"output_throughput: {output_throughput:.2f} token/s\n"
)
if is_in_amd_ci():
self.assertGreater(output_throughput, 200)
else:
self.assertGreater(output_throughput, 220)
if __name__ == "__main__":
unittest.main()
@@ -1,84 +0,0 @@
"""
Performance tests for single GPU that need H200 (80GB) - FP8 and EAGLE tests.
"""
import unittest
from sglang.srt.utils import is_hip
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import (
DEFAULT_DRAFT_MODEL_EAGLE,
DEFAULT_MODEL_NAME_FOR_TEST_FP8,
DEFAULT_TARGET_MODEL_EAGLE,
CustomTestCase,
is_in_amd_ci,
is_in_ci,
run_bench_serving,
write_github_step_summary,
)
register_cuda_ci(est_time=275, stage="extra-a", runner_config="1-gpu-large")
register_amd_ci(est_time=300, suite="stage-b-test-1-gpu-large-amd")
class TestBenchServing1GPULarge(CustomTestCase):
def test_offline_throughput_default_fp8(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST_FP8,
num_prompts=500,
request_rate=float("inf"),
other_server_args=[],
)
if is_in_ci():
write_github_step_summary(
f"### test_offline_throughput_default_fp8\n"
f"Output throughput: {res['output_throughput']:.2f} token/s\n"
)
if is_in_amd_ci():
self.assertGreater(res["output_throughput"], 3500)
else:
self.assertGreater(res["output_throughput"], 4300)
@unittest.skipIf(is_hip(), "Skip Eagle test for ROCm")
def test_online_latency_eagle(self):
res = run_bench_serving(
model=DEFAULT_TARGET_MODEL_EAGLE,
num_prompts=300,
request_rate=8,
sharegpt_context_len=3072,
disable_ignore_eos=True,
dataset_name="sharegpt",
other_server_args=[
"--speculative-algorithm",
"EAGLE",
"--speculative-draft-model-path",
DEFAULT_DRAFT_MODEL_EAGLE,
"--speculative-num-steps",
"5",
"--speculative-eagle-topk",
"4",
"--speculative-num-draft-tokens",
"16",
"--mem-fraction-static",
"0.7",
],
need_warmup=True,
seed=42,
)
if is_in_ci():
write_github_step_summary(
f"### test_online_latency_eagle\n"
f"median_e2e_latency_ms: {res['median_e2e_latency_ms']:.2f} ms\n"
f"accept_length: {res['accept_length']:.2f} \n"
)
if is_in_amd_ci():
self.assertLess(res["median_e2e_latency_ms"], 1800)
else:
self.assertLess(res["median_e2e_latency_ms"], 900)
self.assertGreater(res["accept_length"], 3.0)
if __name__ == "__main__":
unittest.main()
@@ -1,267 +0,0 @@
"""
Performance tests for single GPU - LLM throughput/latency and LoRA tests.
Works on 5090 (32GB).
"""
import asyncio
import itertools
import unittest
import requests
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import (
DEFAULT_MODEL_NAME_FOR_TEST,
CustomTestCase,
is_in_amd_ci,
is_in_ci,
run_bench_serving,
write_github_step_summary,
)
register_cuda_ci(est_time=1264, stage="extra-a", runner_config="1-gpu-large")
register_amd_ci(est_time=1100, suite="stage-b-test-1-gpu-large-amd")
class TestBenchServing1GPUPart1(CustomTestCase):
def test_offline_throughput_default(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=500,
request_rate=float("inf"),
other_server_args=[],
)
if is_in_ci():
write_github_step_summary(
f"### test_offline_throughput_default\n"
f"Output throughput: {res['output_throughput']:.2f} token/s\n"
)
if is_in_amd_ci():
self.assertGreater(res["output_throughput"], 3050)
else:
self.assertGreater(res["output_throughput"], 3800)
def test_offline_throughput_non_stream_small_batch_size(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=200,
request_rate=float("inf"),
other_server_args=["--max-running-requests", "10"],
dataset_name="sharegpt",
random_input_len=None,
random_output_len=None,
disable_stream=True,
need_warmup=True,
)
if is_in_ci():
write_github_step_summary(
f"### test_offline_throughput_non_stream_small_batch_size\n"
f"Output throughput: {res['output_throughput']:.2f} token/s\n"
)
if is_in_amd_ci():
self.assertGreater(res["output_throughput"], 1000)
else:
self.assertGreater(res["output_throughput"], 1050)
def test_offline_throughput_without_radix_cache(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=500,
request_rate=float("inf"),
other_server_args=["--disable-radix-cache"],
)
if is_in_ci():
write_github_step_summary(
f"### test_offline_throughput_without_radix_cache\n"
f"Output throughput: {res['output_throughput']:.2f} token/s\n"
)
if is_in_amd_ci():
self.assertGreater(res["output_throughput"], 3050)
else:
self.assertGreater(res["output_throughput"], 3800)
def test_offline_throughput_without_chunked_prefill(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=500,
request_rate=float("inf"),
other_server_args=["--chunked-prefill-size", "-1"],
)
if is_in_ci():
write_github_step_summary(
f"### test_offline_throughput_without_chunked_prefill\n"
f"Output throughput: {res['output_throughput']:.2f} token/s\n"
)
self.assertGreater(res["output_throughput"], 2600)
def test_offline_throughput_with_triton_attention_backend(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=500,
request_rate=float("inf"),
other_server_args=[
"--attention-backend",
"triton",
"--context-length",
"8192",
],
)
if is_in_ci():
write_github_step_summary(
f"### test_offline_throughput_with_triton_attention_backend\n"
f"Output throughput: {res['output_throughput']:.2f} token/s\n"
)
if is_in_amd_ci():
self.assertGreater(res["output_throughput"], 2700)
else:
self.assertGreater(res["output_throughput"], 3700)
def test_online_latency_default(self):
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=100,
request_rate=1,
other_server_args=[],
)
if is_in_ci():
write_github_step_summary(
f"### test_online_latency_default\n"
f"median_e2e_latency_ms: {res['median_e2e_latency_ms']:.2f} ms\n"
)
self.assertLess(res["median_e2e_latency_ms"], 11000)
if is_in_amd_ci():
self.assertLess(res["median_ttft_ms"], 115)
else:
self.assertLess(res["median_ttft_ms"], 86)
self.assertLess(res["median_itl_ms"], 10)
def test_online_lora_latency(self):
res = self._run_lora_latency_test(enable_background_task=False)
if is_in_ci():
write_github_step_summary(
f"### test_online_lora_latency\n"
f"median_e2e_latency_ms: {res['median_e2e_latency_ms']:.2f} ms\n"
f"median_ttft_ms: {res['median_ttft_ms']:.2f} ms\n"
)
if is_in_amd_ci():
self.assertLess(res["median_e2e_latency_ms"], 3320)
else:
self.assertLess(res["median_e2e_latency_ms"], 2400)
# relax for mi300x (LoRA TTFT ~2x slower than mi325)
if is_in_amd_ci():
self.assertLess(res["median_ttft_ms"], 100)
else:
self.assertLess(res["median_ttft_ms"], 58)
def test_online_lora_latency_with_concurrent_adapter_updates(self):
res = self._run_lora_latency_test(enable_background_task=True)
if is_in_ci():
write_github_step_summary(
f"### test_online_lora_latency_with_concurrent_adapter_updates\n"
f"median_e2e_latency_ms: {res['median_e2e_latency_ms']:.2f} ms\n"
f"median_ttft_ms: {res['median_ttft_ms']:.2f} ms\n"
)
if is_in_amd_ci():
self.assertLess(res["median_e2e_latency_ms"], 6000)
else:
self.assertLess(res["median_e2e_latency_ms"], 4000)
# relax for mi300x (LoRA TTFT ~2x slower than mi325)
if is_in_amd_ci():
self.assertLess(res["median_ttft_ms"], 130)
else:
self.assertLess(res["median_ttft_ms"], 80)
def _run_lora_latency_test(self, enable_background_task: bool):
"""
Run a latency test for LoRA with the specified background task setting.
"""
async def lora_loader_unloader_task(
base_url: str,
start_event: asyncio.Event,
stop_event: asyncio.Event,
):
"""
A background task that repeatedly loads and unloads a LoRA adapter.
"""
await start_event.wait()
path_cycler = itertools.cycle(
[
"pbevan11/llama-3.1-8b-ocr-correction",
"faridlazuarda/valadapt-llama-3.1-8B-it-chinese",
"philschmid/code-llama-3-1-8b-text-to-sql-lora",
]
)
load_url = f"{base_url}/load_lora_adapter"
unload_url = f"{base_url}/unload_lora_adapter"
num_updates = 0
while not stop_event.is_set():
lora_path = next(path_cycler)
response = await asyncio.to_thread(
requests.post,
load_url,
json={"lora_name": lora_path, "lora_path": lora_path},
)
self.assertTrue(
response.ok, f"Failed to load LoRA adapter: {response.text}"
)
num_updates += 1
if stop_event.is_set():
break
await asyncio.sleep(1)
response = await asyncio.to_thread(
requests.post,
unload_url,
json={"lora_name": lora_path},
)
self.assertTrue(
response.ok, f"Failed to unload LoRA adapter: {response.text}"
)
num_updates += 1
await asyncio.sleep(1)
background_task = lora_loader_unloader_task if enable_background_task else None
res = run_bench_serving(
model=DEFAULT_MODEL_NAME_FOR_TEST,
num_prompts=400,
request_rate=8,
other_server_args=[
"--enable-lora",
"--max-loras-per-batch",
"1",
"--disable-radix-cache",
"--random-seed",
"42",
"--mem-fraction-static",
"0.8",
"--lora-paths",
"nvidia/llama-3.1-nemoguard-8b-topic-control",
"--max-lora-rank",
"256",
],
dataset_name="random",
random_input_len=256,
random_output_len=256,
lora_name=["nvidia/llama-3.1-nemoguard-8b-topic-control"],
background_task=background_task,
)
return res
if __name__ == "__main__":
unittest.main()
@@ -1,211 +0,0 @@
"""
Performance tests for single GPU - VLM, Score API, and Embeddings API tests.
Works on 5090 (32GB).
"""
import unittest
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import (
DEFAULT_SMALL_EMBEDDING_MODEL_NAME_FOR_TEST,
DEFAULT_SMALL_MODEL_NAME_FOR_TEST_SCORE,
DEFAULT_SMALL_VLM_MODEL_NAME_FOR_TEST,
CustomTestCase,
is_in_amd_ci,
is_in_ci,
run_bench_serving,
run_embeddings_benchmark,
run_score_benchmark,
write_github_step_summary,
)
register_cuda_ci(est_time=909, stage="extra-a", runner_config="1-gpu-large")
register_amd_ci(est_time=900, suite="stage-b-test-1-gpu-large-amd")
class TestBenchServing1GPUPart2(CustomTestCase):
@unittest.skip(
"Qwen2.5-VL server crashes with SIGBUS (exit code -7) on main; disable until fixed"
)
def test_vlm_offline_throughput(self):
res = run_bench_serving(
model=DEFAULT_SMALL_VLM_MODEL_NAME_FOR_TEST,
num_prompts=200,
request_rate=float("inf"),
other_server_args=[
"--mem-fraction-static",
"0.7",
],
dataset_name="mmmu",
)
if is_in_ci():
write_github_step_summary(
f"### test_vlm_offline_throughput\n"
f"Output throughput: {res['output_throughput']:.2f} token/s\n"
)
# relax for mi300x
if is_in_amd_ci():
self.assertGreater(res["output_throughput"], 900)
else:
self.assertGreater(res["output_throughput"], 2500)
def test_vlm_online_latency(self):
res = run_bench_serving(
model=DEFAULT_SMALL_VLM_MODEL_NAME_FOR_TEST,
num_prompts=250,
request_rate=1,
other_server_args=[
"--mem-fraction-static",
"0.7",
],
dataset_name="mmmu",
)
if is_in_ci():
write_github_step_summary(
f"### test_vlm_online_latency\n"
f"median_e2e_latency_ms: {res['median_e2e_latency_ms']:.2f} ms\n"
)
self.assertLess(res["median_e2e_latency_ms"], 16500)
if is_in_amd_ci():
self.assertLess(res["median_ttft_ms"], 150)
else:
self.assertLess(res["median_ttft_ms"], 100)
self.assertLess(res["median_itl_ms"], 8)
def test_score_api_latency_throughput(self):
"""Test score API latency and throughput performance"""
res = run_score_benchmark(
model=DEFAULT_SMALL_MODEL_NAME_FOR_TEST_SCORE,
num_requests=1000,
batch_size=10,
other_server_args=[],
need_warmup=True,
)
if is_in_ci():
write_github_step_summary(
f"### test_score_api_throughput\n"
f"Average latency: {res['avg_latency_ms']:.2f} ms\n"
f"P95 latency: {res['p95_latency_ms']:.2f} ms\n"
f"Score API throughput: {res['throughput']:.2f} req/s\n"
f"Successful requests: {res['successful_requests']}/{res['total_requests']}\n"
)
self.assertEqual(res["successful_requests"], res["total_requests"])
# relax for mi300x
if is_in_amd_ci():
self.assertLess(res["avg_latency_ms"], 60)
self.assertLess(res["p95_latency_ms"], 65)
self.assertGreater(res["throughput"], 16)
else:
self.assertLess(res["avg_latency_ms"], 48)
self.assertLess(res["p95_latency_ms"], 50)
self.assertGreater(res["throughput"], 20)
def test_score_api_batch_scaling(self):
"""Test score API performance with different batch sizes"""
batch_sizes = [10, 25, 50]
for batch_size in batch_sizes:
res = run_score_benchmark(
model=DEFAULT_SMALL_MODEL_NAME_FOR_TEST_SCORE,
num_requests=500,
batch_size=batch_size,
)
if is_in_ci():
write_github_step_summary(
f"### test_score_api_batch_scaling_size_{batch_size}\n"
f"Batch size: {batch_size}\n"
f"Average latency: {res['avg_latency_ms']:.2f} ms\n"
f"P95 latency: {res['p95_latency_ms']:.2f} ms\n"
f"Throughput: {res['throughput']:.2f} req/s\n"
f"Successful requests: {res['successful_requests']}/{res['total_requests']}\n"
)
self.assertEqual(res["successful_requests"], res["total_requests"])
# relax for mi300x
if is_in_amd_ci():
bounds = {10: (60, 65), 25: (70, 80), 50: (80, 90)}
default_bounds = (90, 90)
else:
bounds = {10: (45, 50), 25: (50, 60), 50: (60, 65)}
default_bounds = (60, 65)
avg_latency_bound, p95_latency_bound = bounds.get(
batch_size, default_bounds
)
self.assertLess(res["avg_latency_ms"], avg_latency_bound)
self.assertLess(res["p95_latency_ms"], p95_latency_bound)
def test_embeddings_api_latency_throughput(self):
"""Test embeddings API latency and throughput performance"""
res = run_embeddings_benchmark(
model=DEFAULT_SMALL_EMBEDDING_MODEL_NAME_FOR_TEST,
num_requests=1000,
batch_size=1,
input_tokens=500,
other_server_args=[],
need_warmup=True,
)
if is_in_ci():
write_github_step_summary(
f"### test_embeddings_api_throughput\n"
f"Average latency: {res['avg_latency_ms']:.2f} ms\n"
f"P95 latency: {res['p95_latency_ms']:.2f} ms\n"
f"Embeddings API throughput: {res['throughput']:.2f} req/s\n"
f"Successful requests: {res['successful_requests']}/{res['total_requests']}\n"
)
self.assertEqual(res["successful_requests"], res["total_requests"])
# relax for mi300x
if is_in_amd_ci():
self.assertLess(res["avg_latency_ms"], 35)
self.assertLess(res["p95_latency_ms"], 40)
self.assertGreater(res["throughput"], 30)
else:
self.assertLess(res["avg_latency_ms"], 20)
self.assertLess(res["p95_latency_ms"], 25)
self.assertGreater(res["throughput"], 60)
def test_embeddings_api_batch_scaling(self):
"""Test embeddings API performance with different batch sizes"""
batch_sizes = [10, 25, 50]
for batch_size in batch_sizes:
res = run_embeddings_benchmark(
model=DEFAULT_SMALL_EMBEDDING_MODEL_NAME_FOR_TEST,
num_requests=500,
batch_size=batch_size,
input_tokens=500,
)
if is_in_ci():
write_github_step_summary(
f"### test_embeddings_api_batch_scaling_size_{batch_size}\n"
f"Batch size: {batch_size}\n"
f"Average latency: {res['avg_latency_ms']:.2f} ms\n"
f"P95 latency: {res['p95_latency_ms']:.2f} ms\n"
f"Throughput: {res['throughput']:.2f} req/s\n"
f"Successful requests: {res['successful_requests']}/{res['total_requests']}\n"
)
self.assertEqual(res["successful_requests"], res["total_requests"])
# relax for mi300x
if is_in_amd_ci():
bounds = {10: (80, 90), 25: (140, 150), 50: (230, 240)}
default_bounds = (300, 300)
else:
bounds = {10: (60, 65), 25: (115, 120), 50: (190, 195)}
default_bounds = (250, 250)
avg_latency_bound, p95_latency_bound = bounds.get(
batch_size, default_bounds
)
self.assertLess(res["avg_latency_ms"], avg_latency_bound)
self.assertLess(res["p95_latency_ms"], p95_latency_bound)
if __name__ == "__main__":
unittest.main()
@@ -1,108 +0,0 @@
"""
Performance tests for 2-GPU that need large GPUs (H200 80GB) - MoE and Pipeline Parallel tests.
"""
import unittest
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import (
DEFAULT_MOE_MODEL_NAME_FOR_TEST,
CustomTestCase,
is_in_amd_ci,
is_in_ci,
run_bench_serving,
write_github_step_summary,
)
register_cuda_ci(est_time=687, stage="extra-a", runner_config="2-gpu-large")
register_amd_ci(est_time=1450, suite="stage-b-test-2-gpu-large-amd")
class TestBenchServing2GPU(CustomTestCase):
def test_moe_offline_throughput_default(self):
res = run_bench_serving(
model=DEFAULT_MOE_MODEL_NAME_FOR_TEST,
num_prompts=300,
request_rate=float("inf"),
other_server_args=["--tp", "2"],
)
if is_in_ci():
write_github_step_summary(
f"### test_moe_offline_throughput_default\n"
f"Output throughput: {res['output_throughput']:.2f} token/s\n"
)
if is_in_amd_ci():
self.assertGreater(res["output_throughput"], 2100)
else:
self.assertGreater(res["output_throughput"], 2200)
def test_moe_offline_throughput_without_radix_cache(self):
res = run_bench_serving(
model=DEFAULT_MOE_MODEL_NAME_FOR_TEST,
num_prompts=300,
request_rate=float("inf"),
other_server_args=["--tp", "2", "--disable-radix-cache"],
)
if is_in_ci():
write_github_step_summary(
f"### test_moe_offline_throughput_without_radix_cache\n"
f"Output throughput: {res['output_throughput']:.2f} token/s\n"
)
if is_in_amd_ci():
self.assertGreater(res["output_throughput"], 2100)
else:
self.assertGreater(res["output_throughput"], 2200)
def test_pp_offline_throughput_default_decode(self):
res = run_bench_serving(
model=DEFAULT_MOE_MODEL_NAME_FOR_TEST,
num_prompts=1000,
request_rate=float("inf"),
random_input_len=1,
random_output_len=1024,
other_server_args=["--pp-size", "2"],
need_warmup=True,
seed=42,
)
if is_in_ci():
write_github_step_summary(
f"### test_pp_offline_throughput_default_decode\n"
f"Output throughput: {res['output_throughput']:.2f} token/s\n"
)
self.assertGreater(res["output_throughput"], 6700)
def test_pp_long_context_prefill(self):
res = run_bench_serving(
model="meta-llama/Llama-3.3-70B-Instruct",
num_prompts=4,
request_rate=float("inf"),
random_input_len=128000,
random_output_len=1,
dataset_name="random",
other_server_args=[
"--quantization",
"fp8",
"--pp-size",
"2",
]
+ (["--mem-fraction-static", "0.7"] if is_in_amd_ci() else []),
need_warmup=False,
seed=42,
)
if is_in_ci():
write_github_step_summary(
f"### test_pp_long_context_latency_prefill\n"
f"input_throughput: {res['input_throughput']:.2f} ms\n"
)
if is_in_amd_ci():
self.assertGreater(res["input_throughput"], 3000)
else:
self.assertGreater(res["input_throughput"], 4000)
if __name__ == "__main__":
unittest.main()

Some files were not shown because too many files have changed in this diff Show More