fix(spec_v2): support EAGLE with spec_v2

1. add pass for spec_v2 in base_attn

2. fix: EAGLE with spec_v2 overlap Grammar accept_token failed

Signed-off-by: wxiwnd <wxiwnd@outlook.com>
This commit is contained in:
laoyao0822
2026-03-25 21:17:09 +08:00
committed by wxiwnd
parent 101100e25b
commit ef607c35c9
3 changed files with 124 additions and 14 deletions

View File

@@ -137,9 +137,9 @@ class DecodeReqToTokenPool:
# Indices of reqs that already have a req_pool_idx and will reuse
# their existing slot (e.g. chunked prefill continuing across chunks).
reusing = [i for i, r in enumerate(reqs) if r.req_pool_idx is not None]
assert (
len(reusing) <= 1
), "only one chunked request may reuse req_pool_idx in a batch"
assert len(reusing) <= 1, (
"only one chunked request may reuse req_pool_idx in a batch"
)
assert all(
reqs[i].is_chunked > 0 or reqs[i].kv_committed_len > 0 for i in reusing
), "reusing request must be chunked or have committed KV"
@@ -166,7 +166,6 @@ class DecodeReqToTokenPool:
class HybridMambaDecodeReqToTokenPool(HybridReqToTokenPool):
def __init__(
self,
size: int,
@@ -793,9 +792,9 @@ class DecodePreallocQueue:
"""Pre-allocate the memory for req_to_token and token_kv_pool"""
req_pool_indices = self.req_to_token_pool.alloc([req])
assert (
req_pool_indices is not None
), "req_pool_indices is full! There is a bug in memory estimation."
assert req_pool_indices is not None, (
"req_pool_indices is full! There is a bug in memory estimation."
)
# Alloc all tokens for the prebuilt req (except for the reserved input token for decoding)
fill_len = len(req.origin_input_ids) + max(len(req.output_ids) - 1, 0)
@@ -814,9 +813,9 @@ class DecodePreallocQueue:
extend_num_tokens=fill_len,
)
assert (
kv_loc is not None
), "KV cache is full! There is a bug in memory estimation."
assert kv_loc is not None, (
"KV cache is full! There is a bug in memory estimation."
)
self.req_to_token_pool.write((req.req_pool_idx, slice(0, len(kv_loc))), kv_loc)
@@ -1008,7 +1007,6 @@ class DecodeTransferQueue:
class SchedulerDisaggregationDecodeMixin:
@torch.no_grad()
def event_loop_normal_disagg_decode(self: Scheduler):
"""A normal scheduler loop for decode worker in disaggregation mode."""
@@ -1023,6 +1021,18 @@ class SchedulerDisaggregationDecodeMixin:
# Get the next batch to run
batch = self.get_next_disagg_decode_batch_to_run()
self.cur_batch = batch
disable_overlap_for_batch = self.is_disable_overlap_for_batch(batch)
def pop_and_process():
# Process the results of the last batch
tmp_batch, tmp_result = self.result_queue.popleft()
self.process_batch_result(tmp_batch, tmp_result)
# If we need grammar sync (spec + grammar), process the last batch
# results first so that the grammar state is up-to-date before
# generating the bitmask for the current batch.
if disable_overlap_for_batch:
pop_and_process()
# Launch the current batch
if batch:
@@ -1040,6 +1050,11 @@ class SchedulerDisaggregationDecodeMixin:
self.result_queue = deque()
self.last_batch: Optional[ScheduleBatch] = None
def pop_and_process():
# Process the results of the last batch
tmp_batch, tmp_result = self.result_queue.popleft()
self.process_batch_result(tmp_batch, tmp_result)
while True:
# Receive requests
recv_reqs = self.recv_requests()
@@ -1050,6 +1065,13 @@ class SchedulerDisaggregationDecodeMixin:
# Get the next batch to run
batch = self.get_next_disagg_decode_batch_to_run()
self.cur_batch = batch
disable_overlap_for_batch = self.is_disable_overlap_for_batch(batch)
# If we need grammar sync (spec + grammar), process the last batch
# results first so that the grammar state is up-to-date before
# generating the bitmask for the current batch.
if self.last_batch and disable_overlap_for_batch:
pop_and_process()
# Launch the current batch
if batch:
@@ -1060,8 +1082,8 @@ class SchedulerDisaggregationDecodeMixin:
# Process the last batch
if self.last_batch:
tmp_batch, tmp_result = self.result_queue.popleft()
self.process_batch_result(tmp_batch, tmp_result)
if not disable_overlap_for_batch:
pop_and_process()
elif batch is None:
self.self_check_during_idle()

View File

@@ -75,7 +75,10 @@ class AttentionBackend(ABC):
Here, we need to redo the computation of all metadata of the attention backend
that depends on tree mask and position buffers.
"""
raise NotImplementedError()
# if self.topk <= 1:
# return
# raise NotImplementedError()
pass
@debug_kernel_api
def forward(

View File

@@ -0,0 +1,85 @@
"""Unit tests for overlap disaggregation decode event loop ordering."""
import unittest
from types import SimpleNamespace
from typing import Any, cast
from sglang.srt.disaggregation.decode import SchedulerDisaggregationDecodeMixin
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=6, suite="stage-a-test-cpu")
class FakeBatch:
def __init__(self, label: str):
self.label = label
def copy(self):
return self
class TestOverlapDisaggDecodeEventLoop(CustomTestCase):
def test_processes_previous_result_before_disabled_overlap_launch(self):
class StopLoop(Exception):
pass
batch1 = FakeBatch("batch-1")
batch2 = FakeBatch("batch-2")
batches = iter([batch1, batch2])
events = []
def get_next_batch():
return next(batches)
def run_batch(batch):
result = f"result-{batch.label}"
events.append(f"run:{batch.label}")
return result
def process_batch_result(batch, result):
events.append(f"process:{batch.label}")
def launch_batch_sample_if_needed(result):
events.append(f"sample:{result}")
if result == "result-batch-2":
raise StopLoop()
scheduler = cast(
Any,
SimpleNamespace(
recv_requests=lambda: [],
process_input_requests=lambda reqs: None,
process_decode_queue=lambda: None,
get_next_disagg_decode_batch_to_run=get_next_batch,
run_batch=run_batch,
process_batch_result=process_batch_result,
is_disable_overlap_for_batch=lambda batch: batch is batch2,
launch_batch_sample_if_needed=launch_batch_sample_if_needed,
self_check_during_idle=lambda: None,
),
)
with self.assertRaises(StopLoop):
SchedulerDisaggregationDecodeMixin.event_loop_overlap_disagg_decode(
scheduler
)
self.assertEqual(
events,
[
"run:batch-1",
"sample:result-batch-1",
"process:batch-1",
"run:batch-2",
"sample:result-batch-2",
],
)
self.assertEqual(len(scheduler.result_queue), 1)
queued_batch, queued_result = scheduler.result_queue[0]
self.assertIs(queued_batch, batch2)
self.assertEqual(queued_result, "result-batch-2")
if __name__ == "__main__":
unittest.main()