[Perf] Unified memory: close the DCP decode gap on Blackwell (#37926)

This commit is contained in:
Cheng Wan
2026-09-07 01:10:44 -07:00
committed by GitHub
parent a8edafff7c
commit b5766336d4
21 changed files with 609 additions and 75 deletions
@@ -432,6 +432,89 @@ class TestFusedFp8WriteGate(CustomTestCase):
)
@unittest.skipUnless(torch.cuda.is_available(), "the fused translate is Triton")
class TestFusedWriteLocTranslateCuda(CustomTestCase):
"""The fused write-loc translate must agree with its own CPU branch.
The two implementations must not drift: Triton truncates division toward
zero where torch floors it, so a negative loc and a tombstoned v2p row are
where a divergence would appear -- and the CPU branch is all the CPU suites
ever exercise.
"""
def test_cuda_matches_the_cpu_branch(self):
from sglang.kernels.ops.memory.virtual_slot import write_loc_to_kernel_ids
for page_size in (1, 64):
span = page_size * 4
loc = torch.tensor(
[-1, 0, 1, page_size, span, span + 1, 2 * span + 3, 5 * page_size],
dtype=torch.int64,
)
# Size the table past the highest page any loc can name, then
# scramble it and tombstone one row (-1), so neither a dropped
# clamp nor a skipped gather can coincide with the right answer.
num_pages = int(loc.max()) // page_size + 2
v2p = torch.tensor(
[(5 * i + 2) % num_pages for i in range(num_pages)] + [-1],
dtype=torch.int64,
)
v2p[1] = -1
for dcp_size, dcp_rank in ((1, 0), (2, 1), (4, 2)):
kw = dict(
page_size=page_size,
stride=page_size * 3,
dcp_size=dcp_size,
dcp_rank=dcp_rank,
)
cpu = write_loc_to_kernel_ids(loc=loc, v2p=v2p, **kw)
gpu = write_loc_to_kernel_ids(loc=loc.cuda(), v2p=v2p.cuda(), **kw)
self.assertEqual(
cpu.tolist(),
gpu.cpu().tolist(),
f"ps={page_size} dcp_size={dcp_size} rank={dcp_rank}",
)
def test_wide_out_clears_the_stale_tail(self):
"""`out_width` past the batch must zero the tail in the same launch.
This is what lets a backend hand in its whole capture-stable buffer:
a shorter replay leaves stale kernel-facing ids past the batch, and the
captured write kernel consumes the full buffer, so an uncleared tail
scatters pad rows into live KV pages.
"""
from sglang.kernels.ops.memory.virtual_slot import write_loc_to_kernel_ids
v2p = torch.tensor([2, 5, 1, 3, 4], dtype=torch.int64, device="cuda")
loc = torch.tensor([0, 64, 128], dtype=torch.int64, device="cuda")
width = 8
# Poison the whole buffer so an unwritten or uncleared cell is visible.
buf = torch.full((width,), -999, dtype=torch.int64, device="cuda")
write_loc_to_kernel_ids(
loc=loc, v2p=v2p, page_size=64, stride=64 * 2, out=buf, out_width=width
)
self.assertEqual(buf[:3].tolist(), [2 * 128, 5 * 128, 1 * 128])
self.assertEqual(buf[3:].tolist(), [0] * (width - 3))
# And it must agree with the narrow call on the live prefix.
narrow = write_loc_to_kernel_ids(loc=loc, v2p=v2p, page_size=64, stride=64 * 2)
self.assertEqual(narrow.tolist(), buf[:3].tolist())
def test_out_is_written_in_place(self):
# The captured decode path hands in a capture-stable buffer; rebinding
# instead of filling it would leave the graph on a stale pointer.
from sglang.kernels.ops.memory.virtual_slot import write_loc_to_kernel_ids
v2p = torch.tensor([2, 5, -1, 3], dtype=torch.int64, device="cuda")
loc = torch.tensor([0, 64, 128, 192], dtype=torch.int64, device="cuda")
dst = torch.full_like(loc, -7)
ret = write_loc_to_kernel_ids(
loc=loc, v2p=v2p, page_size=64, stride=64 * 2, out=dst
)
self.assertIs(ret, dst)
self.assertEqual(dst.tolist(), [2 * 128, 5 * 128, 0, 3 * 128])
class TestDcpDecodeLayout(CustomTestCase):
"""Rank-local length math the decode page table above is built from."""
@@ -49,11 +49,15 @@ def _reference_chain(
out_buf: torch.Tensor,
valid_bs: int,
total_bs: int,
v2p: torch.Tensor = None,
) -> None:
"""Replicates the _replay_metadata reference ops, in order, in place."""
req_pool_indices[valid_bs:total_bs] = 0
mamba_indices = mapping[req_pool_indices[:total_bs]]
# static pool: _translate_mamba_indices is the identity
if v2p is not None:
# Unified pool: virtual->physical slot translate, and it runs BEFORE
# the padding sentinel -- captured kernels read physical ids.
mamba_indices = v2p[mamba_indices].to(torch.int32)
mamba_indices[valid_bs:] = -1
out_buf[: len(mamba_indices)].copy_(mamba_indices)
@@ -139,6 +143,79 @@ class TestFusedReplayStateIndices(CustomTestCase):
f"{name} guard tail clobbered ({case}): {buf[total_bs:].tolist()}",
)
def _run_v2p_case(self, total_bs: int, num_padding: int, seed: int) -> None:
"""Same equivalence, with the unified pool's virtual slot ids.
The mapping yields VIRTUAL slots there and the kernel folds the v2p
gather in, so the two must still agree element for element.
"""
device = torch.device("cuda")
gen = torch.Generator(device="cpu").manual_seed(seed + 1000)
valid_bs = total_bs - num_padding
req_pool = torch.randint(
0, _REQ_POOL_SIZE, (total_bs + _GUARD,), generator=gen, dtype=torch.int64
)
req_pool[total_bs:] = _GUARD_SENTINEL
mapping = torch.randint(
0, _MAMBA_POOL_SIZE, (_REQ_POOL_SIZE,), generator=gen, dtype=torch.int32
)
# Scrambled table with a tombstone, so a skipped gather cannot pass.
v2p = torch.randperm(_MAMBA_POOL_SIZE + 1, generator=gen).to(torch.int64)
v2p[7] = -1
out = torch.full((total_bs + _GUARD,), _OUT_POISON, dtype=torch.int32)
req_ref, req_fused = req_pool.clone().to(device), req_pool.clone().to(device)
out_ref, out_fused = out.clone().to(device), out.clone().to(device)
mapping_d, v2p_d = mapping.to(device), v2p.to(device)
_reference_chain(
req_pool_indices=req_ref,
mapping=mapping_d,
out_buf=out_ref,
valid_bs=valid_bs,
total_bs=total_bs,
v2p=v2p_d,
)
returned = fused_replay_state_indices(
req_pool_indices=req_fused,
mamba_index_mapping=mapping_d,
out_state_indices=out_fused,
valid_bs=valid_bs,
total_bs=total_bs,
v2p=v2p_d,
)
case = f"v2p {total_bs=} {num_padding=} {seed=}"
self.assertTrue(
torch.equal(out_ref[:total_bs], out_fused[:total_bs]),
f"state indices mismatch ({case}):\n"
f" ref {out_ref[:total_bs].tolist()}\n"
f" fused {out_fused[:total_bs].tolist()}",
)
self.assertTrue(torch.equal(returned, out_fused[:total_bs]), case)
self.assertTrue(
torch.equal(req_pool_ref_tail := req_ref[total_bs:], req_fused[total_bs:]),
f"guard tail diverged ({case}): {req_pool_ref_tail.tolist()}",
)
self.assertTrue(
(out_fused[total_bs:] == _OUT_POISON).all(),
f"out guard tail clobbered ({case})",
)
def test_v2p_matrix(self):
for total_bs in (1, 7, 32, 33):
paddings = sorted(
{0, 1, total_bs // 2, total_bs - 1} & set(range(total_bs))
)
for num_padding in paddings:
for seed in (0, 1):
with self.subTest(
total_bs=total_bs, num_padding=num_padding, seed=seed
):
self._run_v2p_case(
total_bs=total_bs, num_padding=num_padding, seed=seed
)
def test_matrix(self):
# Non-power-of-two sizes (7, 33) exercise the BS_UPPER in_range mask;
# num_padding sweeps none / one / half / all-but-one padded rows.
@@ -3100,5 +3100,82 @@ class TestDcpWidening(unittest.TestCase):
self.assertLess(base_cost * full_entry, mamba_bytes * dcp_size)
class TestFusedWriteLocTranslate(unittest.TestCase):
"""`write_loc_to_kernel_ids` must equal the arithmetic it stands for.
Nothing downstream can tell the two apart except by value, so the reference
here is the definition rather than a recorded expectation. Triton truncates
division toward zero where torch floors it, so the negative-loc and
tombstoned-page cases are the ones that matter.
"""
def _reference(self, loc, v2p, page_size, stride, dcp_size, dcp_rank):
out = []
for raw in loc.tolist():
if raw < 0 or (dcp_size > 1 and raw % dcp_size != dcp_rank):
out.append(0)
continue
collapsed = raw // dcp_size
page = collapsed // page_size
offset = collapsed % page_size if page_size > 1 else 0
out.append(max(int(v2p[page]) * stride + offset, 0))
return out
def _check(self, *, page_size, multiplier, dcp_size, dcp_rank, device):
from sglang.kernels.ops.memory.virtual_slot import write_loc_to_kernel_ids
span = page_size * dcp_size
locs = [0, 1, span - 1, span, 2 * span + 3, 5 * span + dcp_rank, -1]
locs += [3 * span + dcp_rank] # lands on the tombstoned page
loc = torch.tensor(locs, dtype=torch.int64, device=device)
# Table sized past the highest page any loc can name, scrambled, with
# one tombstone (-1) so a missing clamp shows up.
num_pages = max(locs) // (page_size * dcp_size) + 2
v2p = torch.tensor(
[(5 * i + 2) % num_pages for i in range(num_pages)] + [-1],
dtype=torch.int64,
device=device,
)
v2p[min(3, num_pages - 1)] = -1
stride = page_size * multiplier
got = write_loc_to_kernel_ids(
loc=loc,
v2p=v2p,
page_size=page_size,
stride=stride,
dcp_size=dcp_size,
dcp_rank=dcp_rank,
)
want = self._reference(
loc.cpu(), v2p.cpu(), page_size, stride, dcp_size, dcp_rank
)
self.assertEqual(got.tolist(), want, f"ps={page_size} dcp={dcp_size}")
# `out=` must write in place and agree (the cuda-graph-stable path).
dst = torch.full_like(loc, -7)
ret = write_loc_to_kernel_ids(
loc=loc,
v2p=v2p,
page_size=page_size,
stride=stride,
dcp_size=dcp_size,
dcp_rank=dcp_rank,
out=dst,
)
self.assertIs(ret, dst)
self.assertEqual(dst.tolist(), want)
def test_matches_reference_on_cpu(self):
for page_size in (1, 64):
for dcp_size, dcp_rank in ((1, 0), (2, 1), (4, 2)):
self._check(
page_size=page_size,
multiplier=7,
dcp_size=dcp_size,
dcp_rank=dcp_rank,
device="cpu",
)
if __name__ == "__main__":
unittest.main()
@@ -55,6 +55,7 @@ def _run_handler(*, prefill_backend, explicit):
"speculative_eagle_topk": None,
"enable_hierarchical_cache": False,
"enable_lmcache": False,
"enable_two_batch_overlap": False,
"dcp_size": 1,
"cuda_graph_config": cg,
"cuda_graph_backend_prefill": prefill_backend if explicit else None,
@@ -0,0 +1,69 @@
# Copyright 2023-2026 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""`--enable-unified-memory` refuses `--enable-two-batch-overlap`.
BUG REGRESSION. The combination launches and captures fine, then dies in the
forward path on the first captured decode replay. Nothing else rejects the
pair, so without this gate a running server crashes mid-serving.
python -m pytest test/registered/unit/server_args/test_unified_tbo_gate.py -v
"""
import unittest
from types import SimpleNamespace
from sglang.srt.arg_groups.kv_cache_hook import handle_unified_memory_pool
from sglang.srt.model_executor.cuda_graph_config import Backend
from sglang.srt.server_args import ServerArgs
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
def _run_handler(*, unified, tbo):
"""Run just `handle_unified_memory_pool` over a minimal stand-in."""
sa = ServerArgs.__new__(ServerArgs)
for name, value in {
"enable_unified_memory": unified,
"enable_two_batch_overlap": tbo,
"disaggregation_mode": "null",
"speculative_algorithm": None,
"speculative_eagle_topk": None,
"enable_hierarchical_cache": False,
"enable_lmcache": False,
"dcp_size": 1,
"cuda_graph_config": SimpleNamespace(
prefill=SimpleNamespace(backend=Backend.DISABLED),
decode=SimpleNamespace(backend=Backend.FULL),
),
"cuda_graph_backend_prefill": Backend.DISABLED,
}.items():
object.__setattr__(sa, name, value)
handle_unified_memory_pool(sa)
class TestUnifiedTboGate(unittest.TestCase):
def test_tbo_with_unified_memory_is_refused(self):
with self.assertRaises(AssertionError) as ctx:
_run_handler(unified=True, tbo=True)
self.assertIn("two-batch-overlap", str(ctx.exception))
def test_gate_fires_only_on_the_pair(self):
"""An inverted condition here would reject every unified launch."""
_run_handler(unified=True, tbo=False)
_run_handler(unified=False, tbo=True)
if __name__ == "__main__":
unittest.main()