Route the eager forward path through the CUDA graph input-buffer registry (#27407)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cheng Wan
2026-06-06 14:35:53 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 84ca0ffb8c
commit 9097647090
5 changed files with 407 additions and 45 deletions
@@ -437,6 +437,65 @@ class TestMissingAndOptionalSlots(unittest.TestCase):
)
)
def test_extract_carries_none_for_absent_plain_slot(self):
# A plain copy slot absent this iter (mrope on a non-multimodal batch)
# must be carried as None, not exposed as the stale/zero buffer.
r = _make_registry(max_bs=4, max_num_tokens=8)
r.register_slot(
GraphSlot("input_ids", lambda bs, mt: (mt,), torch.int64, axis="tokens")
)
r.register_slot(
GraphSlot(
"mrope_positions",
lambda bs, mt: (3, mt),
torch.int64,
axis="tokens",
slice_fn=lambda buf, n: buf[:, :n],
)
)
fb = _MiniForwardBatch(
batch_size=2,
input_ids=torch.arange(2, dtype=torch.int64),
mrope_positions=None, # non-multimodal: FB doesn't carry it
)
r.fill_from(fb, raw_bs=2, padded_bs=2, raw_num_tokens=2, padded_num_tokens=2)
fb_view = r.extract_buffer(
padded_bs=2, padded_num_tokens=2, forward_batch_template=fb
)
# input_ids was present -> buffer-backed; mrope absent -> carried None.
self.assertEqual(
fb_view.input_ids.data_ptr(), r.get_slot("input_ids").buffer.data_ptr()
)
self.assertIsNone(fb_view.mrope_positions)
def test_extract_exposes_computed_slot_even_when_fb_field_none(self):
# A computed slot (copy_from_fb=False) is always exposed, even when its
# FB field is None — the None-skip carry applies only to plain copies.
def _fill_two(buf, fb, ctx):
buf.fill_(2)
r = _make_registry(max_bs=4, max_num_tokens=8)
r.register_slot(
GraphSlot(
"global_num_tokens_gpu",
lambda bs, mt: (1,),
torch.int32,
axis="none",
copy_from_fb=False,
post_fill=_fill_two,
)
)
fb = _MiniForwardBatch(batch_size=2, global_num_tokens_gpu=None)
r.fill_from(fb, raw_bs=2, padded_bs=2, raw_num_tokens=2, padded_num_tokens=2)
fb_view = r.extract_buffer(
padded_bs=2, padded_num_tokens=2, forward_batch_template=fb
)
self.assertIsNotNone(fb_view.global_num_tokens_gpu)
self.assertEqual(
fb_view.global_num_tokens_gpu.data_ptr(),
r.get_slot("global_num_tokens_gpu").buffer.data_ptr(),
)
class TestPostFillHook(unittest.TestCase):
def test_post_fill_runs_after_copy(self):
@@ -888,6 +947,61 @@ class TestBuildDecodeRegistry(unittest.TestCase):
# local = clamp(100 - rank*4, 0, 4) = 4 (NOT the raw FB copy of 100).
self.assertEqual(int(src.num_token_non_padded.item()), 4)
def test_register_global_num_tokens_false_carries_fb_values(self):
# register_global_num_tokens=False (eager) excludes the computed
# global_num_tokens_* slots so the batch's DP values are carried, not
# clobbered by the zero buffer extract_buffer would otherwise expose.
from sglang.srt.model_executor.cuda_graph_buffer_registry import (
build_decode_registry,
)
reg = build_decode_registry(
device=torch.device("cpu"),
max_bs=4,
max_num_token=8,
seq_len_fill_value=5,
cache_loc_dtype=torch.int64,
register_global_num_tokens=False,
share_pool=False,
source=None,
)
self.assertFalse(reg.has_slot("global_num_tokens_gpu"))
self.assertFalse(reg.has_slot("global_num_tokens_for_logprob_gpu"))
gnt = torch.tensor([37], dtype=torch.int32)
gntlp = torch.tensor([41], dtype=torch.int32)
fb = _MiniForwardBatch(
batch_size=2,
input_ids=torch.arange(2, dtype=torch.int64),
positions=torch.arange(2, dtype=torch.int64),
out_cache_loc=torch.arange(2, dtype=torch.int64),
req_pool_indices=torch.zeros(2, dtype=torch.int64),
seq_lens=torch.full((2,), 5, dtype=torch.int32),
seq_lens_cpu=torch.full((2,), 5, dtype=torch.int32),
global_num_tokens_gpu=gnt,
global_num_tokens_for_logprob_gpu=gntlp,
)
reg.fill_from(fb, raw_bs=2, padded_bs=2, raw_num_tokens=2, padded_num_tokens=2)
fb_view = reg.extract_buffer(
padded_bs=2, padded_num_tokens=2, forward_batch_template=fb
)
# Carried from the batch (same tensors), not a zero registry buffer.
self.assertIs(fb_view.global_num_tokens_gpu, gnt)
self.assertIs(fb_view.global_num_tokens_for_logprob_gpu, gntlp)
# Default (graph path) still registers the computed slots.
reg2 = build_decode_registry(
device=torch.device("cpu"),
max_bs=4,
max_num_token=8,
seq_len_fill_value=5,
cache_loc_dtype=torch.int64,
share_pool=False,
source=None,
)
self.assertTrue(reg2.has_slot("global_num_tokens_gpu"))
self.assertTrue(reg2.has_slot("global_num_tokens_for_logprob_gpu"))
def test_source_with_ngram_registers_structured_slots(self):
from sglang.srt.model_executor.cuda_graph_buffer_registry import (
build_decode_registry,
@@ -1188,6 +1302,42 @@ class TestBuildPrefillRegistry(unittest.TestCase):
)
self.assertTrue(torch.all(ids[3:8] == 0))
def test_register_input_embeds_false_keeps_mrope_carries_embeds(self):
# register_input_embeds=False (eager): mrope stays registered but
# input_embeds is carried from the FB (a read input), not a zero buffer.
from sglang.srt.model_executor.cuda_graph_buffer_registry import (
build_prefill_registry,
)
reg = build_prefill_registry(
device=torch.device("cpu"),
max_bs=2,
max_num_token=8,
cache_loc_dtype=torch.int64,
is_multimodal=True,
hidden_size=4,
embed_dtype=torch.float32,
register_input_embeds=False,
share_pool=False,
source=None,
)
self.assertTrue(reg.has_slot("mrope_positions"))
self.assertFalse(reg.has_slot("input_embeds"))
# extract_buffer carries the FB's real input_embeds (not a zero buffer).
embeds = torch.randn(3, 4)
fb = _MiniForwardBatch(
batch_size=1,
input_ids=torch.tensor([1, 2, 3], dtype=torch.int64),
positions=torch.tensor([0, 1, 2], dtype=torch.int64),
out_cache_loc=torch.tensor([7, 8, 9], dtype=torch.int64),
input_embeds=embeds,
)
reg.fill_from(fb, raw_bs=1, padded_bs=1, raw_num_tokens=3, padded_num_tokens=3)
fb_view = reg.extract_buffer(
padded_bs=1, padded_num_tokens=3, forward_batch_template=fb
)
self.assertIs(fb_view.input_embeds, embeds)
class TestFillOncePolicy(unittest.TestCase):
"""FILL_ONCE initializes the whole buffer at alloc and never resets the