59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
606 lines
21 KiB
Python
606 lines
21 KiB
Python
# Copyright (c) 2026 LightSeek Foundation
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
"""Host-function-driven grammar pipeline.
|
|
|
|
Captures ``fill_next_token_bitmask`` + H2D as graph nodes via
|
|
``cudaLaunchHostFunc`` (see ``tokenspeed.runtime.utils.hostfunc``) so the
|
|
grammar fill lives inside the CUDA graph and overlaps with the model
|
|
forward on a side stream.
|
|
|
|
Deferred advance: the matcher advance for step N's sampled tokens runs
|
|
inside step N+1's ``build`` hostfunc, not at the tail of step N's
|
|
graph. ``schedule_post_sampler`` does a main-stream D2H of the sampler
|
|
output into a pinned buffer shared across steps. The next step's
|
|
``fork_event`` (recorded on main inside captured ``schedule_fill``)
|
|
transitively waits for this D2H before the side-stream build reads
|
|
the pinned memory. Main only joins on ``bitmask_event`` before
|
|
apply_mask, so forward(N+1) overlaps with the prev-step matcher
|
|
advance and this step's mask fill. The shared pinned buffer is
|
|
read-only for the next step's build; ``post_process`` reads its own
|
|
per-step CPU tensor produced by ``.to('cpu')`` in execute_forward_op,
|
|
so step N+1 overwriting pinned does not race with commit(N).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import queue
|
|
import threading
|
|
from dataclasses import dataclass, field
|
|
|
|
import torch
|
|
|
|
from tokenspeed.runtime.utils import get_colorful_logger
|
|
from tokenspeed.runtime.utils.hostfunc import hostfunc
|
|
|
|
logger = get_colorful_logger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class GrammarStepInputs:
|
|
"""Per-batch grammar state assembled by the event loop.
|
|
|
|
``grammars[i]`` is the matcher for request ``i`` in the batch, or
|
|
``None`` if that request has no grammar. ``advance_mask[i] == False``
|
|
means slot ``i`` is an intermediate chunked-prefill chunk whose
|
|
sampled token must NOT advance the matcher (the output is discarded
|
|
by post_process). ``advance_mask is None`` means "advance all".
|
|
|
|
Pass ``None`` instead of an instance when no request in the batch
|
|
has a grammar — the executor's setup_grammar_step short-circuits.
|
|
"""
|
|
|
|
grammars: list[object | None]
|
|
advance_mask: list[bool] | None = None
|
|
|
|
|
|
@dataclass
|
|
class GrammarStepCompletion:
|
|
"""Per-step handoff between the build hostfunc and post_process.
|
|
|
|
The next step's ``build`` normally sets ``event`` after advancing
|
|
the matcher and recording ``terminated_at[i]`` — the index of the
|
|
token (within this step's accepted-token chain) that terminated
|
|
request ``i``'s matcher, or -1. If no next step is dispatched
|
|
(request was the last live one), post_process advances on the
|
|
host instead; ``lock`` ensures exactly one path wins.
|
|
|
|
``advance_mask[i] == False`` means slot i's sampled token was garbage
|
|
(intermediate chunked-prefill chunk) and MUST NOT advance the matcher.
|
|
"""
|
|
|
|
event: threading.Event = field(default_factory=threading.Event)
|
|
terminated_at: list[int] = field(default_factory=list)
|
|
|
|
lock: threading.Lock = field(default_factory=threading.Lock)
|
|
grammars: list | None = None
|
|
|
|
bs: int = 0
|
|
tokens_per_req: int = 1
|
|
|
|
advance_mask: list[bool] | None = None
|
|
|
|
|
|
class CapturableGrammarExecutor:
|
|
"""Buffers + hostfuncs for an in-graph grammar fill + apply."""
|
|
|
|
def __init__(
|
|
self,
|
|
max_bs: int,
|
|
vocab_size: int,
|
|
max_tokens_per_req: int = 1,
|
|
device: torch.device = torch.device("cuda"),
|
|
) -> None:
|
|
|
|
self.max_bs = max_bs
|
|
self.vocab_size = vocab_size
|
|
self.max_tokens_per_req = max(1, max_tokens_per_req)
|
|
self.bitmask_width = (vocab_size + 31) // 32
|
|
self.device = device
|
|
|
|
n_rows = max_bs * self.max_tokens_per_req
|
|
|
|
with torch.device(device):
|
|
|
|
self.bitmask = torch.full(
|
|
(n_rows, self.bitmask_width), -1, dtype=torch.int32
|
|
)
|
|
|
|
self.bitmask_host = torch.full(
|
|
(n_rows, self.bitmask_width),
|
|
-1,
|
|
dtype=torch.int32,
|
|
pin_memory=True,
|
|
)
|
|
|
|
# Draft tokens for spec verify; col 0 is unused for non-spec.
|
|
self.candidates_host = torch.zeros(
|
|
(max_bs, self.max_tokens_per_req), dtype=torch.int32, pin_memory=True
|
|
)
|
|
|
|
# Sampler output copied to pinned memory at the tail of each step.
|
|
# Read by the NEXT step's build (via fork_event ordering); overwritten
|
|
# at the next step's tail (strictly after that step's bitmask_event,
|
|
# hence after build's read).
|
|
self.output_tokens_host = torch.zeros(
|
|
(max_bs * self.max_tokens_per_req,), dtype=torch.int32, pin_memory=True
|
|
)
|
|
|
|
self.accept_lengths_host = torch.zeros(
|
|
(max_bs,), dtype=torch.int32, pin_memory=True
|
|
)
|
|
|
|
# One queue entry per replay: CPU pushes via add_batch, the
|
|
# fetch_batch hostfunc pops + shifts current into prev.
|
|
self.queue = queue.Queue()
|
|
self.current_batch: dict | None = None
|
|
self.prev_batch: dict | None = None
|
|
|
|
self.stream = torch.cuda.Stream()
|
|
self.fork_event = torch.cuda.Event()
|
|
self.bitmask_event = torch.cuda.Event()
|
|
|
|
def add_batch(
|
|
self,
|
|
grammars: list,
|
|
bs: int,
|
|
has_candidates: bool = False,
|
|
tokens_per_req: int = 1,
|
|
advance_mask: list[bool] | None = None,
|
|
) -> GrammarStepCompletion:
|
|
"""Push request state for the next captured iteration.
|
|
|
|
Must be called exactly once before each replay (including
|
|
capture warmup); ``fetch_batch`` raises on an empty queue.
|
|
|
|
``advance_mask[i] == False`` disables matcher advance for slot
|
|
i at this step — set it for intermediate chunked-prefill chunks
|
|
whose sampled tokens are discarded by output_processor.
|
|
``None`` defaults to all-True.
|
|
"""
|
|
grammars_list = list(grammars)
|
|
|
|
if advance_mask is None:
|
|
|
|
advance_list = [True] * bs
|
|
|
|
else:
|
|
|
|
advance_list = list(advance_mask)
|
|
|
|
if len(advance_list) != bs:
|
|
raise ValueError(f"advance_mask length {len(advance_list)} != bs {bs}")
|
|
|
|
completion = GrammarStepCompletion(
|
|
grammars=grammars_list,
|
|
bs=bs,
|
|
tokens_per_req=tokens_per_req,
|
|
advance_mask=advance_list,
|
|
)
|
|
|
|
self.queue.put(
|
|
{
|
|
"grammars": grammars_list,
|
|
"bs": bs,
|
|
"has_candidates": has_candidates,
|
|
"tokens_per_req": tokens_per_req,
|
|
"advance_mask": advance_list,
|
|
"completion": completion,
|
|
}
|
|
)
|
|
|
|
return completion
|
|
|
|
def reset_state(self) -> None:
|
|
"""Drop any warmup-run state held by prev/current pointers."""
|
|
self.prev_batch = None
|
|
self.current_batch = None
|
|
|
|
@hostfunc
|
|
def fetch_batch(self) -> None:
|
|
"""Pop batch from queue, setting prev to current and current to the new batch."""
|
|
self.prev_batch = self.current_batch
|
|
self.current_batch = self.queue.get_nowait()
|
|
|
|
@hostfunc
|
|
def build(self) -> None:
|
|
"""Advance matcher by prev step's outputs, then fill this step's bitmask."""
|
|
self.prev_batch and self._advance_prev(self.prev_batch)
|
|
self._fill_current(self.current_batch)
|
|
|
|
def _advance_prev(self, prev: dict) -> None:
|
|
"""Advance each prev-step grammar by its accepted tokens and record
|
|
which (if any) terminated in this step."""
|
|
completion: GrammarStepCompletion = prev["completion"]
|
|
|
|
# Lock serializes with post_process's host-side fallback:
|
|
# exactly one path advances the matcher and fires the event.
|
|
with completion.lock:
|
|
|
|
if completion.event.is_set():
|
|
return
|
|
|
|
grammars = prev["grammars"]
|
|
stride = prev["tokens_per_req"]
|
|
bs = prev["bs"]
|
|
advance_mask = prev["advance_mask"]
|
|
terminated_at = [-1] * bs
|
|
|
|
for i, grammar in enumerate(grammars):
|
|
|
|
if (
|
|
grammar is None
|
|
or grammar.finished
|
|
or grammar.is_terminated()
|
|
or not advance_mask[i]
|
|
):
|
|
continue
|
|
|
|
n_accepted = int(self.accept_lengths_host[i].item())
|
|
|
|
for j in range(n_accepted):
|
|
|
|
tok = int(self.output_tokens_host[i * stride + j].item())
|
|
|
|
try:
|
|
|
|
grammar.accept_token(tok)
|
|
|
|
except Exception:
|
|
|
|
break
|
|
|
|
if grammar.is_terminated():
|
|
|
|
terminated_at[i] = j
|
|
break
|
|
|
|
completion.terminated_at = terminated_at
|
|
completion.event.set()
|
|
|
|
def _fill_current(self, batch: dict | None) -> None:
|
|
"""Fill bitmask_host for this step's grammars."""
|
|
if batch is None:
|
|
|
|
self.bitmask_host.fill_(-1)
|
|
|
|
return
|
|
|
|
grammars = batch["grammars"]
|
|
bs = batch["bs"]
|
|
n = self.max_tokens_per_req
|
|
has_candidates = batch["has_candidates"]
|
|
|
|
# Spec verify binds bitmask[:bs*n] for rejection_sampling;
|
|
# non-spec binds bitmask[:bs] for Sampler.sample.
|
|
per_req_rows = n if has_candidates else 1
|
|
self.bitmask_host[: bs * n].fill_(-1)
|
|
|
|
for i, grammar in enumerate(grammars):
|
|
|
|
if grammar is None or grammar.finished or grammar.is_terminated():
|
|
|
|
continue
|
|
|
|
row_base = i * per_req_rows
|
|
advanced = 0
|
|
|
|
for pos in range(n):
|
|
|
|
if grammar.is_terminated():
|
|
|
|
break
|
|
|
|
grammar.fill_vocab_mask(self.bitmask_host, row_base + pos)
|
|
|
|
if pos + 1 == n or not has_candidates:
|
|
|
|
break
|
|
|
|
# col 0 was consumed by a previous step's advance;
|
|
# walk cols 1..n-1 to produce per-position masks.
|
|
next_tok = int(self.candidates_host[i, pos + 1].item())
|
|
|
|
if not grammar.try_accept_token(next_tok):
|
|
|
|
break
|
|
|
|
advanced += 1
|
|
|
|
# Undo the draft walk — the real advance happens in the
|
|
# NEXT step's build based on the sampler's accepted count.
|
|
if advanced:
|
|
|
|
grammar.rollback(advanced)
|
|
|
|
def schedule_fill(
|
|
self,
|
|
input_ids_buf_slice: torch.Tensor | None = None,
|
|
) -> None:
|
|
"""Fork grammar work onto the side stream for this step.
|
|
|
|
Side stream: wait(fork_event) → D2H candidates (spec) →
|
|
fetch_batch → build → H2D bitmask → bitmask_event. Main rejoins
|
|
via wait_bitmask before apply_mask; forward on main overlaps
|
|
with the advance + fill on the side stream.
|
|
"""
|
|
self.fork_event.record()
|
|
|
|
with torch.cuda.stream(self.stream):
|
|
|
|
torch.cuda.current_stream().wait_event(self.fork_event)
|
|
|
|
if input_ids_buf_slice is not None:
|
|
|
|
bs = input_ids_buf_slice.shape[0] // self.max_tokens_per_req
|
|
|
|
self.candidates_host[:bs].copy_(
|
|
input_ids_buf_slice.view(bs, self.max_tokens_per_req),
|
|
non_blocking=True,
|
|
)
|
|
|
|
self.fetch_batch()
|
|
self.build()
|
|
|
|
self.bitmask.copy_(self.bitmask_host, non_blocking=True)
|
|
self.bitmask_event.record()
|
|
|
|
def wait_bitmask(self) -> None:
|
|
"""Join the side stream on the main stream before apply_mask."""
|
|
torch.cuda.current_stream().wait_event(self.bitmask_event)
|
|
|
|
def schedule_post_sampler(
|
|
self,
|
|
output_tokens: torch.Tensor,
|
|
accept_lengths: torch.Tensor,
|
|
) -> None:
|
|
"""Main-stream D2H of sampler output into the pinned buffer."""
|
|
|
|
n = output_tokens.numel()
|
|
self.output_tokens_host[:n].copy_(output_tokens.flatten(), non_blocking=True)
|
|
|
|
m = accept_lengths.numel()
|
|
self.accept_lengths_host[:m].copy_(accept_lengths, non_blocking=True)
|
|
|
|
|
|
class EagerGrammarBuffers:
|
|
"""GPU + pinned-CPU buffers for the non-CUDA grammar fallback path.
|
|
|
|
``CapturableGrammarExecutor`` uses ``cudaLaunchHostFunc`` for its
|
|
side-stream fill, which is CUDA-only. On HIP / CPU we fall back to a
|
|
synchronous D2H + CPU xgrammar fill + H2D, which needs its own
|
|
pre-allocated buffers (kept off ``InputBuffers`` so model-input state
|
|
isn't muddled with grammar state).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
max_bs: int,
|
|
vocab_size: int,
|
|
max_tokens_per_req: int = 1,
|
|
device: str = "cuda",
|
|
) -> None:
|
|
|
|
self.max_bs = max_bs
|
|
self.vocab_bitmask_width = (vocab_size + 31) // 32
|
|
self.max_tokens_per_req = max(1, max_tokens_per_req)
|
|
|
|
with torch.device(device):
|
|
self.vocab_mask_buf = torch.full(
|
|
(max_bs, self.vocab_bitmask_width), -1, dtype=torch.int32
|
|
)
|
|
# Spec-verify grammar bitmask: flat [max_bs * n, width] to match
|
|
# the apply kernel's expected shape.
|
|
if self.max_tokens_per_req > 1:
|
|
self.vocab_mask_spec_buf = torch.full(
|
|
(max_bs * self.max_tokens_per_req, self.vocab_bitmask_width),
|
|
-1,
|
|
dtype=torch.int32,
|
|
)
|
|
|
|
# Pinned staging for the H2D copy.
|
|
self.vocab_mask_cpu_buf = torch.full(
|
|
(max_bs, self.vocab_bitmask_width),
|
|
-1,
|
|
dtype=torch.int32,
|
|
pin_memory=True,
|
|
)
|
|
if self.max_tokens_per_req > 1:
|
|
self.vocab_mask_spec_cpu_buf = torch.full(
|
|
(max_bs * self.max_tokens_per_req, self.vocab_bitmask_width),
|
|
-1,
|
|
dtype=torch.int32,
|
|
pin_memory=True,
|
|
)
|
|
# Draft candidates D2H'd per step so the CPU grammar fill can
|
|
# walk the draft chain position-by-position.
|
|
self.candidates_cpu_buf = torch.zeros(
|
|
(max_bs, self.max_tokens_per_req),
|
|
dtype=torch.int32,
|
|
pin_memory=True,
|
|
)
|
|
|
|
|
|
def bind_grammar_mask_buf(
|
|
info,
|
|
eager_buffers: EagerGrammarBuffers | None,
|
|
bs: int,
|
|
*,
|
|
spec: bool,
|
|
capturable: CapturableGrammarExecutor | None,
|
|
grammar_backend: str,
|
|
) -> None:
|
|
"""Bind the preallocated grammar bitmask buffer onto ``info``.
|
|
|
|
The captured sampler always takes the apply-mask branch when a buffer
|
|
is bound; for non-grammar batches the buffer stays all-ones so apply
|
|
is a no-op. When no buffer is allocated (grammar disabled) this is a
|
|
no-op and sampling skips the mask entirely.
|
|
"""
|
|
if capturable is None and eager_buffers is None:
|
|
return
|
|
|
|
from tokenspeed.runtime.grammar.base_grammar_backend import (
|
|
get_apply_vocab_mask_func,
|
|
)
|
|
|
|
if capturable is not None:
|
|
n = capturable.max_tokens_per_req
|
|
info.vocab_mask = (
|
|
capturable.bitmask[: bs * n] if spec else capturable.bitmask[:bs]
|
|
)
|
|
elif spec and eager_buffers.max_tokens_per_req > 1:
|
|
info.vocab_mask = eager_buffers.vocab_mask_spec_buf[
|
|
: bs * eager_buffers.max_tokens_per_req
|
|
]
|
|
else:
|
|
info.vocab_mask = eager_buffers.vocab_mask_buf[:bs]
|
|
info.apply_vocab_mask = get_apply_vocab_mask_func(grammar_backend)
|
|
|
|
|
|
def _fill_eager_bitmask(
|
|
grammars: list,
|
|
bs: int,
|
|
eager_buffers: EagerGrammarBuffers,
|
|
spec_num_tokens: int,
|
|
is_spec_decode: bool,
|
|
input_ids_buf,
|
|
) -> None:
|
|
"""Sync, walk grammars on host, H2D the bitmask. Non-CUDA path only."""
|
|
if is_spec_decode:
|
|
eager_buffers.candidates_cpu_buf[:bs].copy_(
|
|
input_ids_buf[: bs * spec_num_tokens].view(bs, spec_num_tokens),
|
|
non_blocking=True,
|
|
)
|
|
sync_ev = torch.cuda.Event()
|
|
sync_ev.record()
|
|
sync_ev.synchronize()
|
|
cand_cpu = eager_buffers.candidates_cpu_buf
|
|
active = bs * spec_num_tokens
|
|
cpu_buf = eager_buffers.vocab_mask_spec_cpu_buf
|
|
gpu_buf = eager_buffers.vocab_mask_spec_buf
|
|
cpu_buf[:active].fill_(-1)
|
|
for i, grammar in enumerate(grammars):
|
|
if grammar is None or grammar.finished or grammar.is_terminated():
|
|
continue
|
|
row_base = i * spec_num_tokens
|
|
advanced = 0
|
|
for pos in range(spec_num_tokens):
|
|
if grammar.is_terminated():
|
|
break
|
|
grammar.fill_vocab_mask(cpu_buf, row_base + pos)
|
|
if pos + 1 == spec_num_tokens:
|
|
break
|
|
next_tok = int(cand_cpu[i, pos + 1].item())
|
|
if not grammar.try_accept_token(next_tok):
|
|
break
|
|
advanced += 1
|
|
if advanced:
|
|
grammar.rollback(advanced)
|
|
gpu_buf[:active].copy_(cpu_buf[:active], non_blocking=True)
|
|
else:
|
|
cpu_buf = eager_buffers.vocab_mask_cpu_buf
|
|
gpu_buf = eager_buffers.vocab_mask_buf
|
|
cpu_buf[:bs].fill_(-1)
|
|
for i, grammar in enumerate(grammars):
|
|
if grammar and not grammar.finished and not grammar.is_terminated():
|
|
grammar.fill_vocab_mask(cpu_buf, i)
|
|
gpu_buf[:bs].copy_(cpu_buf[:bs], non_blocking=True)
|
|
|
|
|
|
GrammarRuntime = CapturableGrammarExecutor | EagerGrammarBuffers
|
|
|
|
|
|
def setup_grammar_step(
|
|
*,
|
|
sampling_info,
|
|
bs: int,
|
|
is_spec_decode: bool,
|
|
spec_num_tokens: int,
|
|
grammar_inputs: GrammarStepInputs | None,
|
|
grammar_runtime: GrammarRuntime | None,
|
|
input_ids_buf,
|
|
grammar_backend: str,
|
|
) -> GrammarStepCompletion | None:
|
|
"""Bind the bitmask buffer and dispatch one step of grammar work.
|
|
|
|
``grammar_runtime`` is one of:
|
|
- ``CapturableGrammarExecutor`` (CUDA): enqueues an ``add_batch`` so
|
|
the side-stream hostfunc fills the bitmask in parallel with the
|
|
forward. Returns the per-step ``GrammarStepCompletion``.
|
|
- ``EagerGrammarBuffers`` (non-CUDA fallback): syncs, runs the
|
|
xgrammar fill on host, H2Ds the bitmask. Returns ``None``.
|
|
- ``None``: grammar disabled, no-op.
|
|
"""
|
|
if grammar_runtime is None:
|
|
return None
|
|
|
|
capturable = (
|
|
grammar_runtime
|
|
if isinstance(grammar_runtime, CapturableGrammarExecutor)
|
|
else None
|
|
)
|
|
eager_buffers = (
|
|
grammar_runtime if isinstance(grammar_runtime, EagerGrammarBuffers) else None
|
|
)
|
|
|
|
bind_grammar_mask_buf(
|
|
sampling_info,
|
|
eager_buffers,
|
|
bs,
|
|
spec=is_spec_decode,
|
|
capturable=capturable,
|
|
grammar_backend=grammar_backend,
|
|
)
|
|
|
|
grammars = grammar_inputs.grammars if grammar_inputs is not None else [None] * bs
|
|
advance_mask = grammar_inputs.advance_mask if grammar_inputs is not None else None
|
|
|
|
if capturable is not None:
|
|
# Always push (even all-None) to keep the captured hostfunc queue
|
|
# 1:1 with replays.
|
|
tokens_per_req = spec_num_tokens if is_spec_decode else 1
|
|
return capturable.add_batch(
|
|
grammars=grammars,
|
|
bs=bs,
|
|
has_candidates=is_spec_decode,
|
|
tokens_per_req=tokens_per_req,
|
|
advance_mask=advance_mask,
|
|
)
|
|
|
|
# Fill the bound buffer every step. When no request has a grammar we
|
|
# still need to write all-ones (-1) to clear any leftover bits from a
|
|
# prior grammar-batch — the captured graph reads from this same memory
|
|
# whether or not we filled it this step.
|
|
if any(grammars):
|
|
_fill_eager_bitmask(
|
|
grammars,
|
|
bs,
|
|
eager_buffers,
|
|
spec_num_tokens,
|
|
is_spec_decode,
|
|
input_ids_buf,
|
|
)
|
|
elif is_spec_decode and eager_buffers.max_tokens_per_req > 1:
|
|
eager_buffers.vocab_mask_spec_buf[: bs * spec_num_tokens].fill_(-1)
|
|
else:
|
|
eager_buffers.vocab_mask_buf[:bs].fill_(-1)
|
|
return None
|