[FullCG] Support chunked cached-prefix prefill (#30825)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
paulzhang-tm
2026-07-28 11:20:25 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 1eee8fbdcc
commit 4e5a05148a
10 changed files with 746 additions and 37 deletions
@@ -20,6 +20,7 @@ class TestPrefillCudaGraphPadding(CustomTestCase):
runner = PrefillCudaGraphRunner.__new__(PrefillCudaGraphRunner)
runner._is_full_backend = False
runner.enable_lora = False
runner._capture_chunked_prefix = False
runner.prefill_backend_name = Backend.TC_PIECEWISE
runner.has_mha_companion_layers = False
runner.capture_hidden_mode = CaptureHiddenMode.NULL
@@ -0,0 +1,266 @@
"""CPU coverage for chunked-prefix Full prefill CUDA-graph state."""
import unittest
from types import SimpleNamespace
from unittest.mock import patch
import torch
import sglang.srt.model_executor.runner.prefill_cuda_graph_runner as runner_module
from sglang.srt.model_executor.cuda_graph_config import Backend
from sglang.srt.model_executor.runner.prefill_cuda_graph_runner import (
PrefillCudaGraphRunner,
)
from sglang.srt.model_executor.runner.shape_key import ShapeKey
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=2, suite="base-a-test-cpu")
class _FakeAttentionBackend:
supports_full_cuda_graph_chunked_prefix = True
def __init__(self):
self.calls = []
def prepare_full_cuda_graph_chunked_prefix(self, forward_batch, *, in_capture):
self.calls.append((forward_batch, in_capture))
class _FakeKVIndexKernel:
def __getitem__(self, grid):
del grid
def run(
req_to_token,
req_pool_indices,
starts,
seq_lens,
cu_seq_lens,
output,
req_to_token_stride,
):
del cu_seq_lens, req_to_token_stride
cursor = 0
for row in range(seq_lens.numel()):
seq_len = int(seq_lens[row])
start = int(starts[row])
req = int(req_pool_indices[row])
output[cursor : cursor + seq_len].copy_(
req_to_token[req, start : start + seq_len]
)
cursor += seq_len
return run
class TestPrefillCudaGraphRunnerChunkedPrefix(CustomTestCase):
def test_prefix_chunk_capacity_is_aggregate_and_can_be_overridden(self):
model_runner = SimpleNamespace(
server_args=SimpleNamespace(
chunked_prefill_size=16,
context_length=None,
cuda_graph_config=SimpleNamespace(
prefill=SimpleNamespace(
full_prefill_prefix_chunk_tokens=None, max_bs=8
)
),
),
req_to_token_pool=SimpleNamespace(
req_to_token=torch.empty((1, 32), dtype=torch.int32)
),
)
self.assertEqual(
PrefillCudaGraphRunner._resolve_prefix_chunk_shape(model_runner, 4),
(4, 16),
)
model_runner.server_args.chunked_prefill_size = -1
self.assertEqual(
PrefillCudaGraphRunner._resolve_prefix_chunk_shape(model_runner, 4),
(2, 8),
)
model_runner.server_args.chunked_prefill_size = 16
model_runner.server_args.cuda_graph_config.prefill.full_prefill_prefix_chunk_tokens = (
24
)
self.assertEqual(
PrefillCudaGraphRunner._resolve_prefix_chunk_shape(model_runner, 4),
(6, 24),
)
model_runner.server_args.cuda_graph_config.prefill.full_prefill_prefix_chunk_tokens = (
256
)
self.assertEqual(
PrefillCudaGraphRunner._resolve_prefix_chunk_shape(model_runner, 4),
(32, 128),
)
# At least one token is reserved per request lane even if the requested
# aggregate capacity is smaller than the fixed request-slot count.
model_runner.server_args.cuda_graph_config.prefill.full_prefill_prefix_chunk_tokens = (
2
)
self.assertEqual(
PrefillCudaGraphRunner._resolve_prefix_chunk_shape(model_runner, 4),
(1, 4),
)
model_runner.server_args.cuda_graph_config.prefill.full_prefill_prefix_chunk_tokens = (
0
)
with self.assertRaisesRegex(ValueError, "must be positive"):
PrefillCudaGraphRunner._resolve_prefix_chunk_shape(model_runner, 4)
def test_buffers_are_shared_across_token_buckets(self):
backend = _FakeAttentionBackend()
runner = PrefillCudaGraphRunner.__new__(PrefillCudaGraphRunner)
runner._capture_req_slots = 3
runner._prefix_chunk_len = 2
runner._prefix_chunk_capacity = 6
runner._prefix_max_len = 8
runner._prefix_capture_variants = (1, 2, 4)
runner.device = torch.device("cpu")
runner._prefill_static_buffers = {
"extend_prefix_lens": torch.zeros(3, dtype=torch.int64),
"req_pool_indices": torch.tensor([2, 0, 1], dtype=torch.int64),
}
runner._prefix_capture_batches = {}
runner._prefix_capture_buffers = runner._create_chunked_prefix_buffers()
runner.model_runner = SimpleNamespace(
attn_backend=backend,
req_to_token_pool=SimpleNamespace(
req_to_token=torch.arange(24, dtype=torch.int32).view(3, 8)
),
)
first = SimpleNamespace()
second = SimpleNamespace()
first_key = ShapeKey(size=8, variant_label="chunked_prefix:4")
second_key = ShapeKey(size=16, variant_label="chunked_prefix:4")
with patch.object(
runner_module,
"create_chunked_prefix_cache_kv_indices",
_FakeKVIndexKernel(),
):
runner._prepare_chunked_prefix_capture(first, first_key, 4)
runner._prepare_chunked_prefix_capture(second, second_key, 4)
buffers = runner._prefix_capture_buffers
self.assertIsNotNone(buffers)
# Chunk starts are constant and prefilled at allocation.
self.assertEqual(
buffers.starts_cpu.tolist(),
[[0, 0, 0], [2, 2, 2], [4, 4, 4], [6, 6, 6]],
)
self.assertEqual(first.extend_prefix_lens_cpu, [8, 8, 8])
self.assertEqual(first.prefix_chunk_num_tokens, [6, 6, 6, 6])
self.assertIs(first.prefix_chunk_starts, buffers.starts)
self.assertIs(first.prefix_chunk_seq_lens, buffers.seq_lens)
self.assertIs(first.prefix_chunk_cu_seq_lens, buffers.cu_seq_lens)
self.assertIs(first.prefix_chunk_starts, second.prefix_chunk_starts)
self.assertIs(first.prefix_chunk_seq_lens, second.prefix_chunk_seq_lens)
self.assertIs(
first.prefix_chunk_cu_seq_lens,
second.prefix_chunk_cu_seq_lens,
)
# Per-chunk KV indices are views of one shared 2-D buffer; what
# capture bakes into the graph is the address, so compare pointers.
for kv_chunk_idx in (0, 3):
self.assertEqual(
first.prefix_chunk_kv_indices[kv_chunk_idx].data_ptr(),
buffers.kv_indices[kv_chunk_idx].data_ptr(),
)
self.assertEqual(
first.prefix_chunk_kv_indices[kv_chunk_idx].data_ptr(),
second.prefix_chunk_kv_indices[kv_chunk_idx].data_ptr(),
)
runner._prepare_chunked_prefix_replay(
second_key,
SimpleNamespace(batch_size=2, extend_prefix_lens_cpu=[5, 1]),
)
self.assertEqual(
second.prefix_chunk_seq_lens.tolist(),
[[2, 1, 0], [2, 0, 0], [1, 0, 0], [0, 0, 0]],
)
self.assertEqual(
second.prefix_chunk_kv_indices[0].tolist(),
[16, 17, 0, 0, 0, 0],
)
self.assertEqual(
second.prefix_chunk_kv_indices[1].tolist(),
[18, 19, 0, 0, 0, 0],
)
self.assertEqual(
second.prefix_chunk_kv_indices[2].tolist(),
[20, 0, 0, 0, 0, 0],
)
self.assertEqual(second.prefix_chunk_kv_indices[3].tolist(), [0] * 6)
self.assertEqual(
backend.calls,
[(first, True), (second, True), (second, False)],
)
def test_prefix_gate_only_applies_to_chunked_prefix_variant(self):
runner = PrefillCudaGraphRunner.__new__(PrefillCudaGraphRunner)
runner._capture_req_slots = 4
runner.enable_lora = False
runner.capture_hidden_mode = None
runner.max_num_tokens = 32
runner.capture_num_tokens = [4]
runner.backend = SimpleNamespace()
runner.prefill_backend_name = Backend.FULL
runner.has_mha_companion_layers = False
runner._prefix_chunk_len = 2
runner._prefix_capture_variants = (1, 2, 4)
forward_batch = SimpleNamespace(
batch_size=1,
input_ids=torch.zeros(4, dtype=torch.int64),
input_embeds=None,
replace_embeds=None,
forward_mode=SimpleNamespace(is_target_verify=lambda: False),
capture_hidden_mode=None,
global_num_tokens_cpu=None,
return_logprob=False,
extend_prefix_lens_cpu=[8],
)
# Prefix hits in BCG/TC-piecewise and ordinary non-MLA FullCG use the
# normal graph topology and must retain their existing eligibility.
runner._capture_chunked_prefix = False
for is_full_backend in (False, True):
with self.subTest(is_full_backend=is_full_backend):
runner._is_full_backend = is_full_backend
self.assertTrue(runner.can_run_graph(forward_batch))
# The dedicated chunked-prefix topology has a fixed captured capacity.
runner._is_full_backend = True
runner._capture_chunked_prefix = True
self.assertTrue(runner.can_run_graph(forward_batch))
self.assertEqual(
runner._shape_key(4, forward_batch).variant_label,
"chunked_prefix:4",
)
forward_batch.batch_size = 2
# Capacity is per request, not a sum: three real chunks round up to the
# four-chunk graph even though the aggregate prefix has eight tokens.
forward_batch.extend_prefix_lens_cpu = [5, 3]
self.assertTrue(runner.can_run_graph(forward_batch))
self.assertEqual(
runner._shape_key(4, forward_batch).variant_label,
"chunked_prefix:4",
)
forward_batch.extend_prefix_lens_cpu = [9, 1]
self.assertFalse(runner.can_run_graph(forward_batch))
if __name__ == "__main__":
unittest.main()