Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

525 lines
22 KiB
Python

import logging
from typing import List, Optional
import numpy as np
import torch
from sgl_kernel.speculative import reconstruct_indices_from_tree_mask
from sglang.kernels.ops.speculative.cache_locs import (
assign_extend_cache_locs_func as assign_extend_cache_locs_func,
)
from sglang.srt.layers.utils.logprob import compute_spec_v2_logprobs
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.scheduler import GenerationBatchResult
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.model_executor.forward_batch_info import ForwardMode
from sglang.srt.observability.req_time_stats import set_time_batch
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.base_spec_worker import BaseSpecWorker, EagleDraftWorkerBase
from sglang.srt.speculative.cpp_ngram.ngram_corpus import NgramCorpus
from sglang.srt.speculative.eagle_utils import eagle_sample
from sglang.srt.speculative.ngram_info import NgramVerifyInput
from sglang.srt.speculative.spec_utils import (
commit_mamba_states_after_verify,
generate_token_bitmask,
move_accept_tokens_to_target_kvcache,
prepare_mamba_track_for_verify,
record_stream_for_v2_verify,
)
from sglang.srt.utils import is_cpu
from sglang.srt.utils.async_probe import maybe_detect_inf, maybe_detect_nan
_is_cpu = is_cpu()
logger = logging.getLogger(__name__)
USE_FULL_MASK = True
class NGRAMWorker(BaseSpecWorker):
def alloc_memory_pool(self, **kwargs):
# The target memory pool does not exist yet when __init__ runs.
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
self._target_worker.get_memory_pool()
)
self.max_batch_size = self.model_runner.max_running_requests
self._init_preallocated_tensors()
def __init__(
self,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: Optional[int],
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_worker: TpModelWorker,
):
self.server_args = server_args
self.enable_overlap = not server_args.disable_overlap_schedule
self._target_worker = target_worker
self.model_runner = target_worker.model_runner
self.tp_rank = tp_rank
self.page_size = server_args.page_size
self.draft_token_num: int = server_args.speculative_num_draft_tokens
self.max_trie_depth: int = server_args.speculative_ngram_max_trie_depth
self.speculative_num_draft_tokens = server_args.speculative_num_draft_tokens
self.topk = server_args.speculative_eagle_topk
self.speculative_num_steps = server_args.speculative_num_steps
# req_to_token_pool / token_to_kv_pool_allocator are set in
# alloc_memory_pool(), after the target pools are allocated.
self.device = server_args.device
self.adaptive_controller = None
# rids of the last decode batch; used to erase corpus match state for
# requests that left the batch (see forward_batch_generation).
self._prev_decode_rids: set = set()
self.ngram_corpus = NgramCorpus(
min_bfs_breadth=server_args.speculative_ngram_min_bfs_breadth,
max_bfs_breadth=server_args.speculative_ngram_max_bfs_breadth,
match_type=server_args.speculative_ngram_match_type,
capacity=server_args.speculative_ngram_capacity,
max_trie_depth=server_args.speculative_ngram_max_trie_depth,
draft_token_num=server_args.speculative_num_draft_tokens,
external_sam_budget=server_args.speculative_ngram_external_sam_budget,
external_corpus_max_tokens=server_args.speculative_ngram_external_corpus_max_tokens,
)
if server_args.speculative_ngram_external_corpus_path is not None:
from sglang.srt.speculative.cpp_ngram.external_corpus import (
iter_external_corpus_chunks,
)
corpus_path = server_args.speculative_ngram_external_corpus_path
chunks = list(
iter_external_corpus_chunks(
corpus_path,
target_worker.tokenizer,
server_args.speculative_ngram_external_corpus_max_tokens,
)
)
loaded = self.add_external_corpus(corpus_path, chunks)
self.commit_corpus_load(corpus_path, loaded)
logger.info(
"Loaded external ngram corpus '%s' (%d tokens).",
corpus_path,
loaded,
)
@property
def target_worker(self) -> TpModelWorker:
return self._target_worker
@property
def draft_worker(self) -> Optional[EagleDraftWorkerBase]:
# NGRAM has no draft model; drafts come from the CPU-side corpus.
return None
def clear_cache_pool(self):
self.ngram_corpus.reset()
self._prev_decode_rids = set()
def update_weights_from_tensor(self, recv_req):
# NGRAM has no draft weights of its own — the n-gram corpus is a CPU
# lookup structure built from request token streams — and its
# `model_runner` is shared with the target worker. The scheduler
# mixin dispatches via `self.draft_worker or self.tp_worker`, so
# without this method any caller of `update_weights_from_tensor`
# under `--speculative-algorithm NGRAM` raises AttributeError.
return self.target_worker.update_weights_from_tensor(recv_req)
def add_external_corpus(self, corpus_id: str, token_chunks: list[list[int]]) -> int:
return self.ngram_corpus.load_external_corpus_named(corpus_id, token_chunks)
def commit_corpus_load(self, corpus_id: str, loaded_token_count: int) -> None:
self.ngram_corpus.commit_external_corpus_load(corpus_id, loaded_token_count)
def remove_external_corpus(self, corpus_id: str) -> None:
self.ngram_corpus.remove_external_corpus(corpus_id)
def list_external_corpora(self) -> dict[str, int]:
return self.ngram_corpus.list_external_corpora()
def _efficient_concat_last_n(self, seq1: List[int], seq2: List[int], n: int):
seq2_len = len(seq2)
if seq2_len >= n:
return seq2[-n:]
need_from_seq1 = n - seq2_len
return seq1[-need_from_seq1:] + seq2
def _init_preallocated_tensors(self):
max_total_drafts = self.max_batch_size * self.draft_token_num
max_total_mask_size = (
self.max_batch_size * self.draft_token_num * self.draft_token_num
)
self.draft_tokens = torch.empty(
(max_total_drafts,), dtype=torch.int64, device=self.device
)
self.retrieve_indexes = torch.empty(
(self.max_batch_size, self.draft_token_num),
dtype=torch.int64,
device=self.device,
)
self.retrieve_next_token = torch.empty(
(self.max_batch_size, self.draft_token_num),
dtype=torch.int64,
device=self.device,
)
self.retrieve_next_sibling = torch.empty(
(self.max_batch_size, self.draft_token_num),
dtype=torch.int64,
device=self.device,
)
self.positions = torch.empty(
(max_total_drafts,), dtype=torch.int64, device=self.device
)
self.tree_mask = torch.empty(
(max_total_mask_size,), dtype=torch.bool, device=self.device
)
self.draft_tokens_batch = []
self.tree_mask_batch = []
self.retrieve_indexes_batch = []
self.retrieve_next_token_batch = []
self.retrieve_next_sibling_batch = []
self.positions_batch = []
for bs in range(0, self.max_batch_size + 1):
self.retrieve_indexes_batch.append(self.retrieve_indexes[:bs, :])
self.retrieve_next_token_batch.append(self.retrieve_next_token[:bs, :])
self.retrieve_next_sibling_batch.append(self.retrieve_next_sibling[:bs, :])
self.positions_batch.append(self.positions[: bs * self.draft_token_num])
self.draft_tokens_batch.append(
self.draft_tokens[: bs * self.draft_token_num]
)
self.tree_mask_batch.append(
self.tree_mask[: bs * self.draft_token_num * self.draft_token_num]
)
def on_verify_complete_cpu(
self, num_correct_drafts_per_req: list[int], batch_size: int = 0
) -> None:
# Signature must match BaseSpecWorker.on_verify_complete_cpu; the
# result processor calls it with batch_size as a keyword argument.
if self.adaptive_controller is not None:
self.adaptive_controller.on_verify_complete(num_correct_drafts_per_req)
def _prepare_draft_tokens(
self, batch: ScheduleBatch
) -> tuple[np.ndarray, np.ndarray]:
bs = len(batch.reqs)
stride = self.draft_token_num
prev_token_ids, prev_accept_lens = (
batch.spec_info.accept_tokens,
batch.spec_info.accept_lens,
)
if not prev_token_ids.is_cpu:
prev_token_ids = prev_token_ids.cpu()
prev_accept_lens = prev_accept_lens.cpu()
# Worker-level staging: written here at draft prep, consumed by
# _update_ngram_corpus after verify within the same forward call.
self.prev_token_ids = prev_token_ids.tolist()
self.prev_accept_lens = prev_accept_lens.tolist()
self.ngram_corpus.synchronize()
req_ids = []
batch_tokens = []
total_lens = []
assert len(batch.reqs) == len(self.prev_accept_lens)
# Overlap mode processes results one iteration behind, so the last
# round's accepted tokens are not yet in req.output_ids and must be
# spliced in from spec_info. Sync mode and grammar batches process
# results before the next draft prep, so output_ids is already
# complete and splicing would duplicate the tail.
use_prev_tokens = self.enable_overlap and not batch.has_grammar
i = 0
for req in batch.reqs:
prev_tokens = (
self.prev_token_ids[i * stride : i * stride + self.prev_accept_lens[i]]
if use_prev_tokens
else []
)
check_token = self._efficient_concat_last_n(
list(req.origin_input_ids),
list(req.output_ids[-self.max_trie_depth :]) + prev_tokens,
self.max_trie_depth,
)
req_ids.append(req.rid)
batch_tokens.append(check_token)
i += 1
total_lens.append(
len(req.origin_input_ids) + len(req.output_ids) + len(prev_tokens)
)
req_drafts, mask = self.ngram_corpus.batch_get(
req_ids, batch_tokens, total_lens
)
total_draft_token_num = len(req_drafts)
# Check if speculative decoding is needed; here we always enforce it
assert (
total_draft_token_num == bs * self.draft_token_num
), f"{total_draft_token_num=}, {bs=}, {self.draft_token_num=}"
return req_drafts, mask
def _prepare_for_speculative_decoding(self, batch: ScheduleBatch):
# Decode-only: extend goes through the plain target forward, and an
# IDLE batch must keep its forward_mode instead of being rewritten to
# TARGET_VERIFY below (relevant once DP attention support lands).
if not batch.forward_mode.is_decode():
return
bs = len(batch.reqs)
retrieve_index = self.retrieve_indexes_batch[bs]
retrieve_next_token = self.retrieve_next_token_batch[bs]
retrieve_next_sibling = self.retrieve_next_sibling_batch[bs]
positions = self.positions_batch[bs]
tree_mask = self.tree_mask_batch[bs]
draft_tokens = self.draft_tokens_batch[bs]
req_drafts, mask = self._prepare_draft_tokens(batch)
tree_mask.copy_(torch.from_numpy(mask), non_blocking=True)
draft_tokens.copy_(torch.from_numpy(req_drafts), non_blocking=True)
# generate positions and some indices using tree_mask
reconstruct_indices_from_tree_mask(
tree_mask,
batch.seq_lens,
positions, # mutable
retrieve_index, # mutable
retrieve_next_token, # mutable
retrieve_next_sibling, # mutable
bs,
self.draft_token_num,
)
# NOTE: QLEN_MASK is faster than FULL_MASK, but requires corresponding changes in flashinfer.
# Testing shows about 8% performance improvement (the effect is roughly proportional to batch size).
if USE_FULL_MASK and not _is_cpu:
tree_mask = []
mask = mask.reshape(bs, self.draft_token_num, self.draft_token_num)
# TODO(siyuan): the for loop here leads to significant overhead in large batch size. Can be written into a kernel.
for i in range(bs):
seq_len = batch.seq_lens_cpu[i]
req_mask = torch.ones(
(self.draft_token_num, seq_len), device=self.device
)
req_mask = torch.cat(
(
req_mask,
torch.from_numpy(mask[i]).to(
device=self.device, non_blocking=True
),
),
dim=1,
).to(torch.bool)
tree_mask.append(req_mask.flatten())
tree_mask = torch.cat(tree_mask, dim=0)
batch.forward_mode = ForwardMode.TARGET_VERIFY
batch.input_ids = draft_tokens
batch.out_cache_loc = assign_extend_cache_locs_func(
req_pool_indices=batch.req_pool_indices,
req_to_token=batch.req_to_token_pool.req_to_token,
start_offset=batch.seq_lens,
end_offset=batch.seq_lens + self.draft_token_num,
batch_size=bs,
draft_token_num=self.draft_token_num,
device=self.device,
)
prepare_mamba_track_for_verify(batch)
batch.spec_info = NgramVerifyInput(
draft_token=draft_tokens,
custom_mask=tree_mask,
positions=positions,
retrieve_index=retrieve_index,
retrieve_next_token=retrieve_next_token,
retrieve_next_sibling=retrieve_next_sibling,
draft_token_num=self.draft_token_num,
)
def _update_ngram_corpus(self, batch: ScheduleBatch):
batch_tokens = []
i, stride = 0, self.draft_token_num
# Same splice condition as _prepare_draft_tokens: only overlap mode
# has accepted tokens missing from req.output_ids.
use_prev_tokens = self.enable_overlap and not batch.has_grammar
for req in batch.reqs:
# FIXME: Whether to insert 'extend' into the cache or not, after testing,
# there is not much difference, so we will not insert it for now.
# if batch.forward_mode.is_extend():
# put_ids = req.origin_input_ids + req.output_ids
# else:
prev_tokens = (
self.prev_token_ids[i * stride : i * stride + self.prev_accept_lens[i]]
if use_prev_tokens
else []
)
put_ids = self._efficient_concat_last_n(
list(req.origin_input_ids),
list(req.output_ids[-self.max_trie_depth :]) + prev_tokens,
self.max_trie_depth,
)
batch_tokens.append(put_ids)
i += 1
self.ngram_corpus.batch_put(batch_tokens)
def forward_batch_generation(
self, batch: ScheduleBatch, on_publish=None
) -> GenerationBatchResult:
fwd_stream = torch.get_device_module(self.device).current_stream()
record_stream_for_v2_verify(batch, None, fwd_stream)
bs = len(batch.reqs)
set_time_batch(batch.reqs, "set_spec_draft_start_time", trace_only=True)
self._prepare_for_speculative_decoding(batch)
set_time_batch(batch.reqs, "set_spec_draft_end_time", trace_only=True)
verify_input: NgramVerifyInput = batch.spec_info
accept_lens = torch.ones(bs, dtype=torch.int32, device=self.device)
if batch.forward_mode.is_target_verify():
# Prepare grammar data on CPU if needed
if batch.has_grammar:
retrieve_next_token_cpu = verify_input.retrieve_next_token.cpu()
retrieve_next_sibling_cpu = verify_input.retrieve_next_sibling.cpu()
draft_tokens_cpu = verify_input.draft_token.view(
verify_input.retrieve_next_token.shape
).cpu()
batch_result = self.target_worker.forward_batch_generation(
batch, is_verify=True
)
logits_output, can_run_cuda_graph = (
batch_result.logits_output,
batch_result.can_run_cuda_graph,
)
verify_input: NgramVerifyInput = batch.spec_info
vocab_mask = None
if batch.has_grammar:
# Generate the logit mask for structured output.
# Overlap the CPU operations for bitmask generation with the forward pass.
vocab_mask = generate_token_bitmask(
batch.reqs,
verify_input,
retrieve_next_token_cpu,
retrieve_next_sibling_cpu,
draft_tokens_cpu,
batch.sampling_info.vocab_size,
)
if vocab_mask is not None:
assert verify_input.grammar is not None
vocab_mask = vocab_mask.to(verify_input.retrieve_next_token.device)
# NOTE (sk): otherwise, this vocab mask will be the one from the previous extend stage
# and will be applied to produce wrong results
batch.sampling_info.vocab_mask = None
# Sample
maybe_detect_nan(
logits_output.next_token_logits, "verify: target model logits"
)
maybe_detect_inf(
logits_output.next_token_logits, "verify: target model logits"
)
(
predict,
accept_lens,
accept_index,
) = eagle_sample(verify_input, batch, logits_output, vocab_mask)
new_seq_lens = batch.seq_lens + accept_lens
commit_mamba_states_after_verify(
self.target_worker,
batch,
accept_lens,
accept_index,
self.draft_token_num,
)
accept_tokens = predict[accept_index].flatten()
next_token_ids = accept_tokens
# The KV mover expects drafts-only counts. NGRAM's
# accept_lens includes the bonus token, matching scheduler output.
num_correct_drafts_per_req = accept_lens - 1
move_accept_tokens_to_target_kvcache(
batch,
accept_index,
num_correct_drafts_per_req,
self.token_to_kv_pool_allocator,
)
if batch.return_logprob:
# The last arg is the accept_index row width minus 1. NGRAM's
# accept_index is (bs, draft_token_num) -- the tree depth is not
# bounded by spec_steps like EAGLE's (bs, spec_steps + 1).
compute_spec_v2_logprobs(
batch,
logits_output,
predict,
accept_index,
self.draft_token_num - 1,
)
if on_publish is not None:
on_publish(new_seq_lens)
self._update_ngram_corpus(batch)
# Erase match state of requests that left the decode batch.
# req.finished() is unusable here: under overlap it flips at result
# processing, one iteration after the request left the batch.
# The last batch's entries persist while idle (bounded, small).
cur_rids = {req.rid for req in batch.reqs}
departed_rids = self._prev_decode_rids - cur_rids
if departed_rids:
self.ngram_corpus.erase_match_state(list(departed_rids))
self._prev_decode_rids = cur_rids
batch.forward_mode = ForwardMode.DECODE
else:
batch_result = self.target_worker.forward_batch_generation(batch)
logits_output, predict, can_run_cuda_graph = (
batch_result.logits_output,
batch_result.next_token_ids,
batch_result.can_run_cuda_graph,
)
new_seq_lens = batch.seq_lens.clone()
accept_tokens = torch.zeros(
bs, self.draft_token_num, dtype=torch.int32, device=self.device
)
accept_tokens[:, 0] = predict
accept_tokens = accept_tokens.flatten()
next_token_ids = predict
if on_publish is not None:
on_publish(new_seq_lens)
# Construct the next draft input
next_draft_input = NgramVerifyInput(
draft_token_num=self.draft_token_num,
new_seq_lens=new_seq_lens,
accept_tokens=accept_tokens,
accept_lens=accept_lens,
)
return GenerationBatchResult(
logits_output=logits_output,
next_token_ids=next_token_ids,
can_run_cuda_graph=can_run_cuda_graph,
accept_lens=accept_lens,
# Consumed by the non-overlap V2 scheduler branch to advance
# batch.seq_lens after the isolation restore; overlap mode relays
# it via on_publish instead.
new_seq_lens=new_seq_lens,
next_draft_input=next_draft_input,
speculative_num_draft_tokens=self.speculative_num_draft_tokens,
)