chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,315 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""The baseclass of a backend for grammar-guided constrained decoding."""
import logging
import time
from concurrent.futures import Future, ThreadPoolExecutor
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
import torch
from sglang.srt.parser.reasoning_parser import ReasoningParser
from sglang.srt.server_args import ServerArgs
logger = logging.getLogger(__name__)
@dataclass
class GrammarStats:
compilation_time: Optional[float] = None
schema_count: Optional[int] = None
ebnf_size: Optional[int] = None
is_cache_hit: bool = False
is_grammar_aborted: bool = False
tree_traversal_time: List[float] = field(default_factory=list)
dispatch_type: Optional[str] = None
num_timeout: int = 0
class BaseGrammarObject:
def __init__(self):
self._finished = False
self.grammar_stats = None
self.current_token = None
def maybe_init_reasoning(self, reasoning: bool):
pass
def accept_token(self, token: int) -> None:
"""
Accept a token in the grammar.
"""
raise NotImplementedError()
def rollback(self, k: int):
raise NotImplementedError()
def is_terminated(self):
return False
def allocate_vocab_mask(
self, vocab_size: int, batch_size: int, device
) -> torch.Tensor:
raise NotImplementedError()
def fill_vocab_mask(self, vocab_mask: torch.Tensor, idx: int) -> None:
raise NotImplementedError()
@staticmethod
def move_vocab_mask(vocab_mask: torch.Tensor, device) -> torch.Tensor:
raise NotImplementedError()
@staticmethod
def apply_vocab_mask(logits: torch.Tensor, vocab_mask: torch.Tensor) -> None:
raise NotImplementedError()
def copy(self) -> "BaseGrammarObject":
return self
@property
def finished(self):
return self._finished
@finished.setter
def finished(self, finished):
self._finished = finished
def try_jump_forward(self, tokenizer) -> Optional[Tuple[List[int], str]]:
"""
Try to jump forward in the grammar.
Returns:
A jump forward helper which may be used in `jump_forward_str_state`.
None if the jump forward is not possible.
"""
raise NotImplementedError()
def jump_forward_str_state(self, helper: Tuple[List[int], str]) -> Tuple[str, int]:
"""
Jump forward for the grammar.
Returns:
A tuple of the jump forward string and the next state of the grammar
(which can be used in `jump_and_retokenize` if needed).
"""
raise NotImplementedError()
def jump_and_retokenize(
self, old_output_ids: List[int], new_output_ids: List[int], next_state: int
) -> None:
"""
Jump forward occurs, and update the grammar state if needed.
"""
raise NotImplementedError()
class InvalidGrammarObject(BaseGrammarObject):
"""Represents a grammar that failed to compile, carrying the original error message."""
def __init__(self, error_message: str = "Unknown grammar error"):
super().__init__()
self.error_message = error_message
def __repr__(self):
return f"InvalidGrammarObject(error_message={self.error_message!r})"
class BaseGrammarBackend:
_enable_strict_thinking: bool = False
def __init__(self):
self.executor = ThreadPoolExecutor()
self.cache: Dict[Tuple[str, str], BaseGrammarObject] = {}
def _not_supported(self, key_type: str, key_string: str) -> BaseGrammarObject:
logger.warning(f"Skip unsupported {key_type=}, {key_string=}")
return InvalidGrammarObject()
@property
def enable_strict_thinking(self):
return self._enable_strict_thinking
@property
def is_support_token_filter(self):
return False
def set_token_filter(
self, vocab_mask, token_ids, batch_idx, is_allowed=True, reset_vocab_mask=True
):
"""Set or clear specific tokens in the vocab mask. No-op by default."""
pass
def init_strict_reasoning_grammar(self, reasoning: bool):
"""Create a grammar object for strict token filtering only. Returns None by default."""
return None
def dispatch_fallback(self, key_type: str, key_string: str) -> BaseGrammarObject:
"""
This function should not be reached in any case.
"""
raise ValueError(f"Invalid key_type: {key_type}={key_string}")
def dispatch_json(self, key_string: str) -> BaseGrammarObject:
return self._not_supported("json", key_string)
def dispatch_regex(self, key_string: str) -> BaseGrammarObject:
return self._not_supported("regex", key_string)
def dispatch_ebnf(self, key_string: str) -> BaseGrammarObject:
return self._not_supported("ebnf", key_string)
def dispatch_structural_tag(self, key_string: str) -> BaseGrammarObject:
return self._not_supported("structural_tag", key_string)
def _init_value_dispatch(
self, key: Tuple[str, str], require_reasoning: bool
) -> BaseGrammarObject:
s = time.perf_counter()
key_type, key_string = key
if key_type == "json":
grammar = self.dispatch_json(key_string)
elif key_type == "regex":
grammar = self.dispatch_regex(key_string)
elif key_type == "ebnf":
grammar = self.dispatch_ebnf(key_string)
elif key_type == "structural_tag":
grammar = self.dispatch_structural_tag(key_string)
else:
grammar = self.dispatch_fallback(key_type, key_string)
if grammar is not None and grammar.grammar_stats is not None:
grammar.grammar_stats.compilation_time = time.perf_counter() - s
return grammar
def get_cached_or_future_value(
self, key: Tuple[str, str], require_reasoning: bool
) -> Tuple[BaseGrammarObject | Future[BaseGrammarObject], bool]:
value = self.cache.get(key)
if value:
copied_value = value.copy()
copied_value.maybe_init_reasoning(require_reasoning)
return copied_value, True
value = self.executor.submit(self._init_value_dispatch, key, require_reasoning)
return value, False
def set_cache(self, key: Tuple[str, str], value: BaseGrammarObject):
self.cache[key] = value
def reset(self):
self.cache.clear()
GRAMMAR_BACKEND_REGISTRY = {}
def register_grammar_backend(name, init_func):
GRAMMAR_BACKEND_REGISTRY[name] = init_func
def create_grammar_backend(
server_args: ServerArgs,
tokenizer,
vocab_size: int,
eos_token_ids: Optional[set] = None,
think_end_id: Optional[int] = None,
) -> Optional[BaseGrammarBackend]:
name = server_args.grammar_backend
# Custom grammar backend has the highest priority
if name in GRAMMAR_BACKEND_REGISTRY:
return GRAMMAR_BACKEND_REGISTRY[name](
server_args, tokenizer, vocab_size, eos_token_ids
)
# Default grammar backends
if name == "outlines":
from sglang.srt.constrained.outlines_backend import OutlinesGrammarBackend
grammar_backend = OutlinesGrammarBackend(
tokenizer,
whitespace_pattern=server_args.constrained_json_whitespace_pattern,
)
elif name == "xgrammar":
from sglang.srt.constrained.xgrammar_backend import (
TokenizerNotSupportedError,
XGrammarGrammarBackend,
)
# Convert Set[int] to List[int] if needed
eos_list = list(eos_token_ids) if eos_token_ids else None
try:
grammar_backend = XGrammarGrammarBackend(
tokenizer,
vocab_size=vocab_size,
model_eos_token_ids=eos_list,
any_whitespace=not server_args.constrained_json_disable_any_whitespace,
)
except TokenizerNotSupportedError as e:
if server_args.enable_strict_thinking:
raise ValueError(
f"--enable-strict-thinking requires a grammar backend with "
f"token filtering support, but XGrammar failed to initialize: "
f"{e}. Cannot fall back to grammar_backend='none' with strict "
f"thinking enabled."
) from e
logger.warning(
f"Grammar backend disabled because tokenizer is not supported by XGrammar: {e}. "
"Falling back to grammar_backend='none'. "
"Structured outputs (JSON schema, regex, EBNF) will not be available."
)
server_args.override("grammar.import_fallback", grammar_backend="none")
return None
elif name == "llguidance":
from sglang.srt.constrained.llguidance_backend import GuidanceBackend
grammar_backend = GuidanceBackend(
tokenizer=tokenizer,
any_whitespace=not server_args.constrained_json_disable_any_whitespace,
whitespace_pattern=server_args.constrained_json_whitespace_pattern,
)
elif name == "none":
if server_args.enable_strict_thinking:
raise ValueError(
"--enable-strict-thinking requires a grammar backend that supports "
"token filtering, but grammar_backend='none' was specified. Use "
"--grammar-backend xgrammar or another backend that supports token "
"filtering."
)
return None
else:
raise ValueError(f"Invalid grammar backend: {name}")
if server_args.reasoning_parser and think_end_id is not None:
from sglang.srt.constrained.reasoner_grammar_backend import (
ReasonerGrammarBackend,
)
reasoning_parser = ReasoningParser(
model_type=server_args.reasoning_parser,
stream_reasoning=False,
tokenizer=tokenizer,
)
grammar_backend = ReasonerGrammarBackend(
grammar_backend,
reasoning_parser,
tokenizer,
enable_strict_thinking=server_args.enable_strict_thinking,
)
return grammar_backend
@@ -0,0 +1,311 @@
from __future__ import annotations
import logging
import time
from concurrent import futures
from typing import TYPE_CHECKING, List
import torch
from sglang.srt.constrained.base_grammar_backend import (
InvalidGrammarObject,
create_grammar_backend,
)
from sglang.srt.constrained.reasoner_grammar_backend import ReasonerGrammarObject
from sglang.srt.distributed.communication_tags import P2PTag
from sglang.srt.environ import envs
if TYPE_CHECKING:
from sglang.srt.managers.io_struct import AbortReq
from sglang.srt.managers.schedule_batch import Req
from sglang.srt.managers.scheduler import Scheduler
logger = logging.getLogger(__name__)
class GrammarManager:
def __init__(self, scheduler: Scheduler):
self.scheduler = scheduler
self.server_args = scheduler.server_args
self.grammar_queue: List[Req] = []
if not self.server_args.skip_tokenizer_init:
self.grammar_backend = create_grammar_backend(
self.server_args,
scheduler.tokenizer,
scheduler.model_config.vocab_size,
scheduler.model_config.hf_eos_token_id,
think_end_id=scheduler.model_config.think_end_id,
)
else:
self.grammar_backend = None
self._enable_strict_thinking = (
self.grammar_backend.enable_strict_thinking
if self.grammar_backend is not None
else False
)
self.grammar_sync_group = scheduler.dp_tp_cpu_group
self.grammar_sync_size = scheduler.dp_tp_group.world_size
self.grammar_sync_entry = scheduler.dp_tp_group.first_rank
self.is_grammar_sync_entry = scheduler.dp_tp_group.is_first_rank
self.pp_rank = scheduler.ps.pp_rank
self.pp_size = scheduler.ps.pp_size
self.pp_group = scheduler.pp_group
self.grammar_pp_sync_work_list = []
self.SGLANG_GRAMMAR_POLL_INTERVAL = envs.SGLANG_GRAMMAR_POLL_INTERVAL.get()
self.SGLANG_GRAMMAR_MAX_POLL_ITERATIONS = (
envs.SGLANG_GRAMMAR_MAX_POLL_ITERATIONS.get()
)
def __len__(self):
return len(self.grammar_queue)
def clear(self):
if self.grammar_backend:
self.grammar_backend.reset()
def has_waiting_grammars(self) -> bool:
return len(self.grammar_queue) > 0
def _drain_pp_sync_work(self):
for p2p_work in self.grammar_pp_sync_work_list:
p2p_work.work.wait()
self.grammar_pp_sync_work_list.clear()
def _pp_sync_ready_failed(
self,
ready_req_idxs: set[int],
failed_req_idxs: set[int],
) -> tuple[set[int], set[int]]:
"""
Synchronize ready/failed grammar request indexes across the PP pipeline.
PP0 provides the data. Each later PP rank receives it from the previous
rank and asynchronously forwards it to the next rank.
"""
if self.pp_size <= 1 or self.pp_group is None:
return ready_req_idxs, failed_req_idxs
self._drain_pp_sync_work()
data = (ready_req_idxs, failed_req_idxs)
if self.pp_rank > 0:
data = self.pp_group.recv_object(
src=self.pp_rank - 1,
tag=P2PTag.GRAMMAR_PP_SYNC,
)
if self.pp_rank + 1 < self.pp_size:
self.grammar_pp_sync_work_list.extend(
self.pp_group.send_object(
data,
dst=self.pp_rank + 1,
async_send=True,
tag=P2PTag.GRAMMAR_PP_SYNC,
)
)
return data
def abort_requests(self, recv_req: AbortReq):
for req in self.grammar_queue:
if recv_req.abort_all or req.rid.startswith(recv_req.rid):
logger.debug(f"Abort grammar queue request. {req.rid=}")
if isinstance(req.grammar, futures.Future) and req.grammar:
req.grammar.cancel()
req.set_finish_with_abort("Aborted by AbortReq.")
def _get_request_thinking_budget(self, req: Req) -> int | None:
custom_params = req.sampling_params.custom_params
if not isinstance(custom_params, dict):
return None
thinking_budget = custom_params.get("thinking_budget")
return thinking_budget if isinstance(thinking_budget, int) else None
def _apply_request_reasoning_budget(self, req: Req) -> None:
thinking_budget = self._get_request_thinking_budget(req)
if thinking_budget is None:
return
if isinstance(req.grammar, ReasonerGrammarObject):
req.grammar.max_think_tokens = thinking_budget
def process_req_with_grammar(self, req: Req) -> bool:
# Init grammar cache for this request
add_to_grammar_queue = False
if (
req.sampling_params.json_schema is not None
or req.sampling_params.regex is not None
or req.sampling_params.ebnf is not None
or req.sampling_params.structural_tag is not None
):
if self.grammar_backend is None:
error_msg = "Grammar-based generation (json_schema, regex, ebnf, structural_tag) is not supported when the server is launched with --grammar-backend none"
req.set_finish_with_abort(error_msg)
else:
if req.sampling_params.json_schema is not None:
key = ("json", req.sampling_params.json_schema)
elif req.sampling_params.regex is not None:
key = ("regex", req.sampling_params.regex)
elif req.sampling_params.ebnf is not None:
key = ("ebnf", req.sampling_params.ebnf)
elif req.sampling_params.structural_tag:
key = ("structural_tag", req.sampling_params.structural_tag)
value, cache_hit = self.grammar_backend.get_cached_or_future_value(
key, req.require_reasoning
)
req.grammar = value
if not cache_hit:
req.grammar_key = key
add_to_grammar_queue = True
else:
if isinstance(
value, InvalidGrammarObject
): # We hit a cached invalid grammar.
error_msg = (
f"Failed to compile {key[0]} grammar: {value.error_message}"
)
req.set_finish_with_abort(error_msg)
else:
self._apply_request_reasoning_budget(req)
elif self._enable_strict_thinking:
grammar_obj = self.grammar_backend.init_strict_reasoning_grammar(
req.require_reasoning
)
if grammar_obj is not None:
req.grammar = grammar_obj
self._apply_request_reasoning_budget(req)
if add_to_grammar_queue:
self.grammar_queue.append(req)
return add_to_grammar_queue
def get_ready_grammar_requests(self) -> List[Req]:
"""
Move requests whose grammar objects are ready from grammar_queue to waiting_queue.
For PP0, DP/TP group rank i returns two sets ready_reqs_i,
failed_reqs_i. ready_reqs_all = all_gather(ready_reqs_i) within
PP0's DP/TP group. failed_reqs_all = all_gather(failed_reqs_i)
within PP0's DP/TP group.
ready_reqs = intersect(ready_reqs_all)
failed_reqs = union(failed_reqs_all)
PP0 then propagates the synced result to later PP ranks. Later PP
ranks receive and apply the propagated ready/failed decision.
"""
assert self.grammar_backend
ready_req_idxs: set[int] = set()
failed_req_idxs: set[int] = set()
if self.pp_rank == 0:
# Poll for ready requests
start_time = time.perf_counter()
while time.perf_counter() - start_time < self.SGLANG_GRAMMAR_POLL_INTERVAL:
for i, req in enumerate(self.grammar_queue):
if i in ready_req_idxs:
continue
if (
req.finished() or req.grammar is None
): # It is aborted by AbortReq
ready_req_idxs.add(i)
continue
assert isinstance(req.grammar, futures.Future), f"{req=}"
if req.grammar.done():
ready_req_idxs.add(i)
if len(ready_req_idxs) == len(self.grammar_queue):
break
# Sleep a bit to avoid busy waiting
time.sleep(self.SGLANG_GRAMMAR_POLL_INTERVAL / 10)
# Check failed requests
for i, req in enumerate(self.grammar_queue):
if i not in ready_req_idxs:
# grammar_wait_ct is only updated on PP0; later PP ranks
# receive PP0's ready/failed decision through PP sync.
self.grammar_queue[i].grammar_wait_ct += 1
if (
self.grammar_queue[i].grammar_wait_ct
>= self.SGLANG_GRAMMAR_MAX_POLL_ITERATIONS
):
# Timeout after max poll iterations
# The actual waiting time is SGLANG_GRAMMAR_MAX_POLL_ITERATIONS * max(SGLANG_GRAMMAR_POLL_INTERVAL, GPU_forward_batch_latency)
failed_req_idxs.add(i)
# Sync ready and failed requests across all TP ranks in PP0.
if self.grammar_sync_size == 1:
synced_ready_req_idxs = ready_req_idxs
synced_failed_req_idxs = failed_req_idxs
else:
all_gather_output = [None] * self.grammar_sync_size
torch.distributed.all_gather_object(
all_gather_output,
(ready_req_idxs, failed_req_idxs),
group=self.grammar_sync_group,
)
synced_ready_req_idxs = set.intersection(
*[x[0] for x in all_gather_output]
)
synced_failed_req_idxs = set.union(*[x[1] for x in all_gather_output])
else:
synced_ready_req_idxs = ready_req_idxs
synced_failed_req_idxs = failed_req_idxs
# Propagate PP0's grammar queue decision to later PP ranks.
(
synced_ready_req_idxs,
synced_failed_req_idxs,
) = self._pp_sync_ready_failed(
synced_ready_req_idxs,
synced_failed_req_idxs,
)
# Return ready requests
return_reqs: List[Req] = []
for i in synced_ready_req_idxs:
req = self.grammar_queue[i]
return_reqs.append(req)
if req.finished() or req.grammar is None: # It is aborted by AbortReq
continue
assert isinstance(req.grammar, futures.Future) and req.grammar_key
try:
req.grammar = req.grammar.result()
except Exception as e:
logger.error(
f"Grammar compilation raised an exception: {e}, "
f"grammar_key={req.grammar_key}"
)
req.grammar = InvalidGrammarObject(f"Grammar compilation failed: {e}")
self.grammar_backend.set_cache(req.grammar_key, req.grammar.copy())
self._apply_request_reasoning_budget(req)
if isinstance(req.grammar, InvalidGrammarObject):
error_msg = f"Failed to compile {req.grammar_key[0]} grammar: {req.grammar.error_message}"
req.set_finish_with_abort(error_msg)
# Return failed requests
for i in synced_failed_req_idxs:
req = self.grammar_queue[i]
return_reqs.append(req)
assert isinstance(req.grammar, futures.Future) and req.grammar_key
req.grammar.cancel()
self.grammar_backend.set_cache(
req.grammar_key, InvalidGrammarObject("Grammar preprocessing timed out")
)
error_msg = f"Grammar preprocessing timed out: {req.grammar_key=}"
req.set_finish_with_abort(error_msg)
# Remove finished requests from grammar_queue
self.grammar_queue = [
req
for i, req in enumerate(self.grammar_queue)
if i not in synced_ready_req_idxs and i not in synced_failed_req_idxs
]
return return_reqs
@@ -0,0 +1,190 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Constrained decoding with llguidance backend."""
import json
import logging
import os
from typing import List, Optional, Tuple
import torch
from llguidance import LLMatcher, LLTokenizer, StructTag, grammar_from
from llguidance.hf import from_tokenizer
from llguidance.torch import (
allocate_token_bitmask,
apply_token_bitmask_inplace,
fill_next_token_bitmask,
)
from sglang.srt.constrained.base_grammar_backend import (
BaseGrammarBackend,
BaseGrammarObject,
InvalidGrammarObject,
)
from sglang.srt.constrained.utils import is_legacy_structural_tag
logger = logging.getLogger(__name__)
class GuidanceGrammar(BaseGrammarObject):
def __init__(self, llguidance_tokenizer: LLTokenizer, serialized_grammar: str):
super().__init__()
self.llguidance_tokenizer = llguidance_tokenizer
self.serialized_grammar = serialized_grammar
self.ll_matcher = LLMatcher(
self.llguidance_tokenizer,
self.serialized_grammar,
log_level=int(os.environ.get("LLGUIDANCE_LOG_LEVEL", "1")),
)
self._check_err()
self.eos_token = self.llguidance_tokenizer.eos_token
def accept_token(self, token: int):
if self.finished:
return
if self.ll_matcher.is_stopped() and token == self.eos_token:
self.finished = True
return
self.ll_matcher.consume_token(token)
self._check_err()
def rollback(self, num_tokens: int) -> None:
if num_tokens <= 0:
return
if self.finished:
self.finished = False
# EOS token after stop isn't tracked in ll_matcher
num_tokens -= 1
self.ll_matcher.rollback(num_tokens)
self._check_err()
def is_terminated(self):
return self.finished
def fill_vocab_mask(self, vocab_mask: torch.Tensor, idx: int) -> None:
fill_next_token_bitmask(self.ll_matcher, vocab_mask, idx)
self._check_err()
def allocate_vocab_mask(
self, vocab_size: int, batch_size: int, device
) -> torch.Tensor:
return allocate_token_bitmask(batch_size, self.llguidance_tokenizer.vocab_size)
@staticmethod
def move_vocab_mask(vocab_mask: torch.Tensor, device) -> torch.Tensor:
return vocab_mask.to(device, non_blocking=True)
@staticmethod
def apply_vocab_mask(logits: torch.Tensor, vocab_mask: torch.Tensor) -> None:
apply_token_bitmask_inplace(logits, vocab_mask)
def copy(self):
return GuidanceGrammar(
llguidance_tokenizer=self.llguidance_tokenizer,
serialized_grammar=self.serialized_grammar,
)
def try_jump_forward(self, tokenizer) -> Optional[Tuple[List[int], str]]:
ff_tokens = self.ll_matcher.compute_ff_tokens()
if ff_tokens:
return ff_tokens, ""
else:
return None
def jump_forward_str_state(self, helper: Tuple[List[int], str]) -> Tuple[str, int]:
return "", -1
def jump_and_retokenize(
self, old_output_ids: List[int], new_output_ids: List[int], next_state: int
):
pass
def _check_err(self) -> None:
if self.ll_matcher.is_error():
raise ValueError(self.ll_matcher.get_error())
class GuidanceBackend(BaseGrammarBackend):
def __init__(
self,
tokenizer,
any_whitespace: bool = True,
whitespace_pattern: Optional[str] = None,
n_vocab: Optional[int] = None,
):
super().__init__()
self.tokenizer = tokenizer
self.any_whitespace = any_whitespace
self.whitespace_pattern = whitespace_pattern
self.llguidance_tokenizer = from_tokenizer(self.tokenizer, n_vocab)
def _from_serialized(self, serialized_grammar) -> BaseGrammarObject:
try:
return GuidanceGrammar(
llguidance_tokenizer=self.llguidance_tokenizer,
serialized_grammar=serialized_grammar,
)
except Exception as e:
logger.error(f"Hit invalid grammar: {serialized_grammar=}, {e=}")
return InvalidGrammarObject(str(e))
def dispatch_json(self, key_string: str) -> BaseGrammarObject:
try:
serialized_grammar = LLMatcher.grammar_from_json_schema(
key_string,
defaults={
"whitespace_flexible": self.any_whitespace,
"whitespace_pattern": self.whitespace_pattern,
},
)
except Exception as e:
logger.error(f"Hit invalid json_schema: {key_string=}, {e=}")
return InvalidGrammarObject(str(e))
return self._from_serialized(serialized_grammar)
def dispatch_regex(self, key_string: str) -> BaseGrammarObject:
serialized_grammar = grammar_from("regex", key_string)
return self._from_serialized(serialized_grammar)
def dispatch_ebnf(self, key_string: str) -> BaseGrammarObject:
try:
serialized_grammar = grammar_from("ebnf", key_string)
return self._from_serialized(serialized_grammar)
except ValueError as e:
logger.error(f"Hit invalid ebnf: {key_string=}, {e=}")
return InvalidGrammarObject(str(e))
def dispatch_structural_tag(self, key_string: str) -> BaseGrammarObject:
try:
structural_tag = json.loads(key_string)
assert is_legacy_structural_tag(structural_tag)
tags = [
StructTag(
begin=structure["begin"],
grammar=structure["schema"],
end=structure["end"],
trigger=structural_tag["triggers"][0], # TODO?
)
for structure in structural_tag["structures"]
]
g = StructTag.to_grammar(tags)
return self._from_serialized(g)
except Exception as e:
logger.error(f"Hit invalid structural_tag: {key_string=}, {e=}")
return InvalidGrammarObject(str(e))
@@ -0,0 +1,190 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Constrained decoding with outlines backend."""
import json
import logging
from typing import Dict, List, Optional, Tuple, Union
import interegular
import torch
from outlines.fsm.guide import RegexGuide
from outlines.models.transformers import TransformerTokenizer
from pydantic import BaseModel
from sglang.srt.constrained.base_grammar_backend import (
BaseGrammarBackend,
BaseGrammarObject,
InvalidGrammarObject,
)
from sglang.srt.constrained.outlines_jump_forward import OutlinesJumpForwardMap
try:
from outlines.fsm.json_schema import build_regex_from_schema
except ImportError:
from outlines_core.fsm.json_schema import build_regex_from_schema
logger = logging.getLogger(__name__)
class OutlinesGrammar(BaseGrammarObject):
def __init__(
self,
guide: RegexGuide,
jump_forward_map: Union[OutlinesJumpForwardMap, None],
) -> None:
super().__init__()
self.guide = guide
self.jump_forward_map = jump_forward_map
self.state = 0
def accept_token(self, token: int):
self.state = self.guide.get_next_state(self.state, token)
def allocate_vocab_mask(
self, vocab_size: int, batch_size: int, device
) -> torch.Tensor:
return torch.zeros(batch_size, vocab_size, dtype=torch.bool, device=device)
@staticmethod
def move_vocab_mask(vocab_mask: torch.Tensor, device) -> torch.Tensor:
return vocab_mask
def fill_vocab_mask(self, vocab_mask: torch.Tensor, idx: int) -> None:
tokens = torch.tensor(
self.guide.get_next_instruction(self.state).tokens, dtype=torch.int64
).to(vocab_mask.device, non_blocking=True)
vocab_mask = vocab_mask[idx]
vocab_mask.fill_(1)
vocab_mask.scatter_(0, tokens, torch.zeros_like(tokens, dtype=torch.bool))
@staticmethod
def apply_vocab_mask(logits: torch.Tensor, vocab_mask: torch.Tensor):
logits.masked_fill_(vocab_mask, float("-inf"))
def copy(self):
return OutlinesGrammar(self.guide, self.jump_forward_map)
def try_jump_forward(self, tokenizer) -> Optional[Tuple]:
if not self.jump_forward_map:
return None
jump_forward_bytes = self.jump_forward_map.jump_forward_byte(self.state)
if jump_forward_bytes is None or len(jump_forward_bytes) <= 1:
return None
# preprocess the jump forward string
suffix_bytes = []
continuation_range = range(0x80, 0xC0)
cur_state = self.state
while (
len(jump_forward_bytes) and jump_forward_bytes[0][0] in continuation_range
):
# continuation bytes
byte_edge = jump_forward_bytes.pop(0)
suffix_bytes.append(byte_edge[0])
cur_state = byte_edge[1]
suffix_tokens = [f"<0x{hex(b)[2:].upper()}>" for b in suffix_bytes]
suffix_ids = tokenizer.convert_tokens_to_ids(suffix_tokens)
return suffix_ids, cur_state
def jump_forward_str_state(self, helper: Tuple[List[int], str]) -> Tuple[str, int]:
_, cur_state = helper
return self.jump_forward_map.jump_forward_symbol(cur_state)
def jump_and_retokenize(
self, old_output_ids: List[int], new_output_ids: List[int], next_state: int
):
self.state = next_state
class OutlinesGrammarBackend(BaseGrammarBackend):
def __init__(
self,
tokenizer,
whitespace_pattern: str | None,
):
super().__init__()
try:
self.outlines_tokenizer = TransformerTokenizer(tokenizer)
except AttributeError:
# FIXME: tmp fix for chatglm2 & chatglm3 (pad_token_id=0)
origin_pad_token_id = tokenizer.pad_token_id
def fset(self, value):
self._value = value
type(tokenizer).pad_token_id = property(
fget=type(tokenizer).pad_token_id.fget, fset=fset
)
self.outlines_tokenizer = TransformerTokenizer(tokenizer)
self.outlines_tokenizer.tokenizer.pad_token_id = origin_pad_token_id
self.outlines_tokenizer.pad_token_id = origin_pad_token_id
self.outlines_tokenizer.pad_token = (
self.outlines_tokenizer.tokenizer.pad_token
)
self.outlines_tokenizer.vocabulary = (
self.outlines_tokenizer.tokenizer.get_vocab()
)
self.whitespace_pattern = whitespace_pattern
def _compile_regex(self, regex: str) -> BaseGrammarObject:
try:
if hasattr(RegexGuide, "from_regex"):
# outlines >= 0.1.1
guide = RegexGuide.from_regex(regex, self.outlines_tokenizer)
else:
# outlines <= 0.0.46
guide = RegexGuide(regex, self.outlines_tokenizer)
except interegular.patterns.InvalidSyntax as e:
logger.error(f"Hit invalid regex schema: {regex=}, {e=}")
return InvalidGrammarObject(str(e))
jump_forward_map = None
return OutlinesGrammar(guide, jump_forward_map)
def dispatch_ebnf(self, key_string: str):
return super().dispatch_ebnf(key_string)
def dispatch_structural_tag(self, key_string: str):
return super().dispatch_structural_tag(key_string)
def dispatch_json(self, key_string: str):
try:
regex = build_regex_from_object(
key_string,
whitespace_pattern=self.whitespace_pattern,
)
except (NotImplementedError, json.decoder.JSONDecodeError, ValueError) as e:
logger.error(f"Hit invalid json_schema: {key_string=}, {e=}")
return InvalidGrammarObject(str(e))
return self._compile_regex(regex)
def dispatch_regex(self, key_string: str):
return self._compile_regex(key_string)
def build_regex_from_object(
object: Union[str, BaseModel, Dict], whitespace_pattern: Optional[str] = None
):
if isinstance(object, type(BaseModel)):
schema = json.dumps(object.model_json_schema())
elif isinstance(object, Dict):
schema = json.dumps(object)
else:
schema = object
return build_regex_from_schema(schema, whitespace_pattern)
@@ -0,0 +1,200 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""
Faster constrained decoding with jump forward decoding / compressed finite state machine.
Reference: https://lmsys.org/blog/2024-02-05-compressed-fsm/
"""
import dataclasses
import logging
from collections import defaultdict
from typing import Optional
import interegular
from interegular import InvalidSyntax
from outlines.caching import cache
from sglang.srt.utils import get_bool_env_var
try:
# outlines >= 0.1.0
from outlines_core.fsm.outlines_core_rs import FSMInfo
from outlines_core.fsm.regex import make_byte_level_fsm, make_deterministic_fsm
except ImportError:
# outlines <= 0.0.46
from outlines.fsm.regex import FSMInfo, make_byte_level_fsm, make_deterministic_fsm
IP_REGEX = r"((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)"
# Env var was set in sglang.srt.server_args.ServerArgs.__post_init__
DISABLE_DISK_CACHE = get_bool_env_var("SGLANG_DISABLE_OUTLINES_DISK_CACHE", "true")
logger = logging.getLogger(__name__)
@dataclasses.dataclass
class JumpEdge:
symbol: str = None
symbol_next_state: int = None
byte: int = None
byte_next_state: int = None
def disk_cache(expire: Optional[float] = None, typed=False, ignore=()):
if not DISABLE_DISK_CACHE:
return cache(expire, typed, ignore)
else:
return lambda fn: None
@disk_cache()
def init_state_to_jump_forward(regex_string):
try:
regex_pattern = interegular.parse_pattern(regex_string)
except InvalidSyntax as e:
logger.warning(f"skip invalid regex: {regex_string}, {e=}")
return
byte_fsm = make_byte_level_fsm(regex_pattern.to_fsm().reduce(), keep_utf8=True)
regex_fsm, _ = make_deterministic_fsm(byte_fsm)
fsm_info: FSMInfo = regex_fsm.fsm_info
symbol_to_id = fsm_info.alphabet_symbol_mapping
id_to_symbol = {}
for symbol, id_ in symbol_to_id.items():
id_to_symbol.setdefault(id_, []).append(symbol)
transitions = fsm_info.transitions
outgoings_ct = defaultdict(int)
# NOTE(lsyin): Final states can lead to terminate, so they have one outgoing edge naturally
for s in fsm_info.finals:
outgoings_ct[s] = 1
state_to_jump_forward = {}
for (state, id_), next_state in transitions.items():
if id_ == fsm_info.alphabet_anything_value:
# Arbitrarily symbol cannot be recognized as jump forward
continue
symbols = id_to_symbol[id_]
for c in symbols:
if len(c) > 1:
# Skip byte level transitions like c = "5E"
continue
outgoings_ct[state] += 1
if outgoings_ct[state] > 1:
if state in state_to_jump_forward:
del state_to_jump_forward[state]
break
state_to_jump_forward[state] = JumpEdge(
symbol=c,
symbol_next_state=next_state,
)
# Process the byte level jump forward
outgoings_ct = defaultdict(int)
for s in fsm_info.finals:
outgoings_ct[s] = 1
for (state, id_), next_state in transitions.items():
if id_ == fsm_info.alphabet_anything_value:
continue
symbols = id_to_symbol[id_]
for c in symbols:
byte_ = None
if len(c) == 1 and ord(c) < 0x80:
# ASCII character
byte_ = ord(c)
elif len(c) > 1:
# FIXME: This logic is due to the leading \x00
# https://github.com/outlines-dev/outlines/pull/930
byte_ = int(symbols[0][1:], 16)
if byte_ is not None:
outgoings_ct[state] += 1
if outgoings_ct[state] > 1:
if state in state_to_jump_forward:
del state_to_jump_forward[state]
break
e = state_to_jump_forward.get(state, JumpEdge())
e.byte = byte_
e.byte_next_state = next_state
state_to_jump_forward[state] = e
return state_to_jump_forward
class OutlinesJumpForwardMap:
def __init__(self, regex_string):
self.state_to_jump_forward = init_state_to_jump_forward(regex_string)
def jump_forward_symbol(self, state):
jump_forward_str = ""
next_state = state
while state in self.state_to_jump_forward:
e = self.state_to_jump_forward[state]
if e.symbol is None:
break
jump_forward_str += e.symbol
next_state = e.symbol_next_state
state = next_state
return jump_forward_str, next_state
def jump_forward_byte(self, state):
if state not in self.state_to_jump_forward:
return None
jump_forward_bytes = []
next_state = None
while state in self.state_to_jump_forward:
e = self.state_to_jump_forward[state]
assert e.byte is not None and e.byte_next_state is not None
jump_forward_bytes.append((e.byte, e.byte_next_state))
next_state = e.byte_next_state
state = next_state
return jump_forward_bytes
def is_jump_forward_symbol_state(self, state):
return (
state in self.state_to_jump_forward
and self.state_to_jump_forward[state].symbol is not None
)
def test_main(regex_string):
jump_forward_map = OutlinesJumpForwardMap(regex_string)
for state, e in jump_forward_map.state_to_jump_forward.items():
if e.symbol is not None:
jump_forward_str, next_state = jump_forward_map.jump_forward_symbol(state)
print(f"{state} -> {next_state}", jump_forward_str)
bytes_ = jump_forward_map.jump_forward_byte(state)
print(f"{state} -> {bytes_[-1][1]}", [hex(b) for b, _ in bytes_])
if __name__ == "__main__":
import outlines
outlines.caching.clear_cache()
test_main(r"The google's DNS sever address is " + IP_REGEX)
test_main(r"霍格沃茨特快列车|霍比特人比尔博")
# 霍格: \xe9\x9c\x8d \xe6\xa0\xbc ...
# 霍比: \xe9\x9c\x8d \xe6\xaf\x94 ...
test_main(r"[-+]?[0-9]+[ ]*")
@@ -0,0 +1,327 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""The baseclass of a backend for reasoner grammar-guided constrained decoding."""
import logging
from typing import List, Optional, Tuple, Union
import torch
from transformers import PreTrainedTokenizer, PreTrainedTokenizerFast
from sglang.srt.environ import envs
from sglang.srt.parser.reasoning_parser import ReasoningParser
from .base_grammar_backend import (
BaseGrammarBackend,
BaseGrammarObject,
InvalidGrammarObject,
)
logger = logging.getLogger(__name__)
class ReasonerGrammarObject(BaseGrammarObject):
"""Wraps a grammar object to handle reasoning (think/generation) phases.
State machine (must call maybe_init_reasoning before use):
THINKING (tokens_in_think >= 0, tokens_after_end == -1)
-> grammar not consulted, optional token filtering
GENERATION (tokens_after_end >= 0)
-> grammar consulted for accept/fill/rollback
When enable_token_filter=True (strict mode), fill_vocab_mask filters
excluded tokens during THINKING and enforces max_think_tokens budget.
When the budget is exhausted, only think_end_id is allowed, forcing the
model to exit the thinking phase.
When enable_token_filter=False (non-strict mode), fill_vocab_mask is
a no-op during THINKING.
"""
def __init__(
self,
grammar: Optional[BaseGrammarObject],
think_end_id: int,
think_excluded_token_ids: Optional[List[int]] = None,
max_think_tokens: int = -1,
enable_token_filter: bool = False,
token_filter_fn=None,
allocate_vocab_mask_fn=None,
move_vocab_mask_fn=None,
apply_vocab_mask_fn=None,
):
super().__init__()
self.grammar = grammar
self.think_end_id = think_end_id
self.think_excluded_token_ids = think_excluded_token_ids
self.max_think_tokens = max_think_tokens
self.enable_token_filter = enable_token_filter
self.token_filter_fn = token_filter_fn
self.allocate_vocab_mask_fn = allocate_vocab_mask_fn
self.move_vocab_mask_fn = move_vocab_mask_fn
self.apply_vocab_mask_fn = apply_vocab_mask_fn
self._think_end_id_list = [think_end_id]
self.tokens_in_think = -1
self.tokens_after_end = -1
def maybe_init_reasoning(self, reasoning: bool):
if reasoning:
self.tokens_in_think = 0
self.tokens_after_end = -1
else:
self.tokens_in_think = -1
self.tokens_after_end = 0
def _is_thinking(self):
return self.tokens_in_think >= 0 and self.tokens_after_end == -1
def _is_generation(self):
return self.tokens_after_end >= 0
def transfer_state(self, token: int) -> None:
if self._is_thinking():
if token == self.think_end_id:
self.tokens_after_end = 0
else:
self.tokens_in_think += 1
elif self._is_generation():
self.tokens_after_end += 1
def rollback_state(self):
if self._is_thinking():
if self.tokens_in_think > 0:
self.tokens_in_think -= 1
elif self._is_generation():
if self.tokens_after_end == 0:
self.tokens_after_end = -1
elif self.tokens_after_end > 0:
self.tokens_after_end -= 1
def accept_token(self, token: int):
# Track the last accepted token on the wrapper itself (mirroring
# XGrammarGrammar.accept_token). Disaggregation's process_prebuilt uses
# `grammar.current_token is None` to detect a retracted request whose
# token was already accepted and must not be re-accepted. Without this,
# a ReasonerGrammarObject's current_token stays None forever (the inner
# grammar's is updated, not the wrapper's), so the guard never fires and
# the token is accepted twice -> "Tokens not accepted" -> FINISH_ABORT.
self.current_token = token
if self._is_generation() and self.grammar is not None:
self.grammar.accept_token(token)
self.transfer_state(token)
def is_terminated(self):
if self.grammar is not None:
return self.grammar.is_terminated()
return False
def rollback(self, k):
if self.grammar is not None:
steps_after = min(k, max(0, self.tokens_after_end))
if steps_after > 0:
self.grammar.rollback(steps_after)
for _ in range(k):
self.rollback_state()
def _can_think_more(self):
return self.max_think_tokens < 0 or self.tokens_in_think < self.max_think_tokens
def _do_token_filter(self, vocab_mask, token_ids, idx, is_allowed=True):
if self.token_filter_fn is not None:
self.token_filter_fn(vocab_mask, token_ids, idx, is_allowed)
def fill_vocab_mask(self, vocab_mask: torch.Tensor, idx: int) -> None:
if self._is_thinking():
if not self.enable_token_filter:
return
if self._can_think_more():
self._do_token_filter(
vocab_mask, self.think_excluded_token_ids, idx, is_allowed=False
)
else:
self._do_token_filter(
vocab_mask, self._think_end_id_list, idx, is_allowed=True
)
return
if self._is_generation() and self.grammar is not None:
self.grammar.fill_vocab_mask(vocab_mask, idx)
def allocate_vocab_mask(self, vocab_size, batch_size, device):
if self.grammar is not None:
return self.grammar.allocate_vocab_mask(vocab_size, batch_size, device)
if self.allocate_vocab_mask_fn is not None:
return self.allocate_vocab_mask_fn(vocab_size, batch_size, device)
return None
def move_vocab_mask(self, vocab_mask, device):
if self.grammar is not None:
return self.grammar.move_vocab_mask(vocab_mask, device)
if self.move_vocab_mask_fn is not None:
return self.move_vocab_mask_fn(vocab_mask, device)
return vocab_mask
@property
def apply_vocab_mask(self):
if self.grammar is not None:
return self.grammar.apply_vocab_mask
return self.apply_vocab_mask_fn
def copy(self):
new_obj = ReasonerGrammarObject(
self.grammar.copy() if self.grammar is not None else None,
self.think_end_id,
self.think_excluded_token_ids,
self.max_think_tokens,
self.enable_token_filter,
self.token_filter_fn,
self.allocate_vocab_mask_fn,
self.move_vocab_mask_fn,
self.apply_vocab_mask_fn,
)
new_obj.tokens_in_think = self.tokens_in_think
new_obj.tokens_after_end = self.tokens_after_end
new_obj._finished = self._finished
return new_obj
@property
def finished(self):
if self.grammar is not None:
return self.grammar.finished
return self._finished
@finished.setter
def finished(self, finished):
if self.grammar is not None:
self.grammar.finished = finished
else:
self._finished = finished
def try_jump_forward(self, tokenizer):
if self.grammar is not None:
return self.grammar.try_jump_forward(tokenizer)
return None
def jump_forward_str_state(self, helper):
if self.grammar is not None:
return self.grammar.jump_forward_str_state(helper)
return None
def jump_and_retokenize(self, old_output_ids, new_output_ids, next_state):
if self.grammar is not None:
return self.grammar.jump_and_retokenize(
old_output_ids, new_output_ids, next_state
)
class ReasonerGrammarBackend(BaseGrammarBackend):
def __init__(
self,
grammar_backend: BaseGrammarBackend,
reasoning_parser: ReasoningParser,
tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast],
enable_strict_thinking: bool = False,
):
super().__init__()
self.grammar_backend = grammar_backend
think_end_ids = tokenizer.encode(
reasoning_parser.detector.think_end_token, add_special_tokens=False
)
if not think_end_ids:
raise ValueError(
f"think_end_token '{reasoning_parser.detector.think_end_token}' "
f"could not be encoded by the tokenizer."
)
if len(think_end_ids) != 1:
raise ValueError(
f"think_end_token '{reasoning_parser.detector.think_end_token}' "
"must encode to exactly one token for constrained reasoning."
)
self.think_end_id = think_end_ids[0]
self._enable_strict_thinking = enable_strict_thinking
self.think_excluded_token_ids = self._get_think_excluded_token_ids(
reasoning_parser, tokenizer
)
self.max_think_tokens = envs.SGLANG_MAX_THINK_TOKENS.get()
if (
self.enable_strict_thinking
and self.think_excluded_token_ids is not None
and not self.grammar_backend.is_support_token_filter
):
raise ValueError(
"Strict reasoning format requested but the grammar backend does not "
"support token filtering. Use a grammar backend that supports token "
"filtering (e.g., xgrammar) or disable strict reasoning mode."
)
self.enable_token_filter = (
self.enable_strict_thinking
and self.think_excluded_token_ids is not None
and self.grammar_backend.is_support_token_filter
)
self._token_filter_fn = (
self.grammar_backend.set_token_filter if self.enable_token_filter else None
)
def _get_think_excluded_token_ids(
self,
reasoning_parser: ReasoningParser,
tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast],
) -> Optional[List[int]]:
excluded_ids = []
if (not self.enable_strict_thinking) or (
not reasoning_parser.detector.think_excluded_tokens
):
return None
for token in reasoning_parser.detector.think_excluded_tokens:
new_ids = tokenizer.encode(token, add_special_tokens=False)
if not new_ids:
raise ValueError(
f"think_excluded_token '{token}' could not be encoded by the "
f"tokenizer. All excluded tokens must be encodable for strict "
f"reasoning mode to function correctly."
)
excluded_ids += new_ids
return excluded_ids
def _make_grammar_object(
self, grammar: Optional[BaseGrammarObject], reasoning: bool
) -> ReasonerGrammarObject:
obj = ReasonerGrammarObject(
grammar=grammar,
think_end_id=self.think_end_id,
think_excluded_token_ids=self.think_excluded_token_ids,
max_think_tokens=self.max_think_tokens,
enable_token_filter=self.enable_token_filter,
token_filter_fn=self._token_filter_fn,
allocate_vocab_mask_fn=self.grammar_backend.allocate_vocab_mask,
move_vocab_mask_fn=self.grammar_backend.move_vocab_mask,
apply_vocab_mask_fn=self.grammar_backend.apply_vocab_mask,
)
obj.maybe_init_reasoning(reasoning)
return obj
def init_strict_reasoning_grammar(
self, reasoning: bool
) -> Optional[BaseGrammarObject]:
"""Create a grammar object for strict token filtering only (no inner grammar)."""
if not self.enable_strict_thinking:
return None
return self._make_grammar_object(None, reasoning)
def _init_value_dispatch(
self, key: Tuple[str, str], reasoning: bool
) -> Optional[BaseGrammarObject]:
ret = self.grammar_backend._init_value_dispatch(key, reasoning)
if ret is None or isinstance(ret, InvalidGrammarObject):
return ret
return self._make_grammar_object(ret, reasoning)
@@ -0,0 +1,63 @@
# Copyright 2026 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Torch fallback for token filter operations (non-CUDA devices and HIP).
Sets or clears specific bits in an int32 bitmask by token ID. The token list
is typically tiny (< 10 entries); aggregation is done in Python with the actual
bitmask operations using torch tensor indexing.
"""
import ctypes
from typing import List
import torch
def set_token_filter_torch(
vocab_mask: torch.Tensor,
token_ids: List[int],
batch_idx: int,
is_allowed: bool = True,
reset_vocab_mask: bool = True,
):
if reset_vocab_mask:
vocab_mask[batch_idx].fill_(-1 if (not is_allowed) else 0)
if not token_ids:
return
# Aggregate bit masks per int32 element to handle duplicate indices.
aggregated: dict[int, int] = {}
for token_id in token_ids:
element_idx = token_id // 32
bit_idx = token_id % 32
aggregated[element_idx] = aggregated.get(element_idx, 0) | (1 << bit_idx)
row = vocab_mask[batch_idx]
element_indices = torch.tensor(
list(aggregated.keys()), dtype=torch.long, device=row.device
)
bitmasks = torch.tensor(
[
ctypes.c_int32(mask if is_allowed else ~mask).value
for mask in aggregated.values()
],
dtype=row.dtype,
device=row.device,
)
if is_allowed:
row[element_indices] = torch.bitwise_or(row[element_indices], bitmasks)
else:
row[element_indices] = torch.bitwise_and(row[element_indices], bitmasks)
+12
View File
@@ -0,0 +1,12 @@
from typing import Dict
def is_legacy_structural_tag(obj: Dict) -> bool:
# test whether an object is a legacy structural tag
# see `StructuralTagResponseFormat` at `sglang.srt.entrypoints.openai.protocol`
if obj.get("structures", None) is not None:
assert obj.get("triggers", None) is not None
return True
else:
assert obj.get("format", None) is not None
return False
@@ -0,0 +1,419 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Constrained decoding with xgrammar backend."""
import dataclasses
import json
import logging
from typing import Dict, List, Optional, Tuple, Union
import torch
from xgrammar import (
CompiledGrammar,
GrammarCompiler,
GrammarMatcher,
StructuralTag,
StructuralTagItem,
TokenizerInfo,
allocate_token_bitmask,
)
from sglang.srt.constrained.base_grammar_backend import (
BaseGrammarBackend,
BaseGrammarObject,
GrammarStats,
InvalidGrammarObject,
)
from sglang.srt.constrained.utils import is_legacy_structural_tag
from sglang.srt.utils import is_hip
_is_hip = is_hip()
if _is_hip:
from sgl_kernel import apply_token_bitmask_inplace_cuda
else:
from sglang.kernels.ops.grammar.bitmask_ops import (
apply_token_bitmask_inplace_triton,
)
from sglang.kernels.ops.grammar.token_filter_ops import set_token_filter_triton
from sglang.srt.constrained.torch_ops.token_filter_torch_ops import (
set_token_filter_torch,
)
logger = logging.getLogger(__name__)
MAX_ROLLBACK_TOKENS = 200
class XGrammarGrammar(BaseGrammarObject):
def __init__(
self,
matcher: GrammarMatcher,
vocab_size: int,
ctx: CompiledGrammar,
override_stop_tokens: Optional[Union[List[int], int]],
key_string: Optional[str] = None,
grammar_stats: Optional[GrammarStats] = GrammarStats(),
) -> None:
super().__init__()
self.matcher = matcher
self.vocab_size = vocab_size
self.ctx = ctx
self.override_stop_tokens = override_stop_tokens
self.accepted_tokens = []
self.key_string = key_string
self.grammar_stats = grammar_stats
def accept_token(self, token: int):
if not self.is_terminated():
self.current_token = token
accepted = self.matcher.accept_token(token)
if not accepted:
# log for debugging
raise ValueError(
f"Tokens not accepted: {token}\n"
f"Accepted tokens: {self.accepted_tokens}\n"
f"Key string: {self.key_string}"
)
else:
self.accepted_tokens.append(token)
def rollback(self, k: int):
self.matcher.rollback(k)
self.accepted_tokens = self.accepted_tokens[:-k]
def is_terminated(self):
return self.matcher.is_terminated()
def allocate_vocab_mask(
self, vocab_size: int, batch_size: int, device
) -> torch.Tensor:
return allocate_token_bitmask(batch_size, vocab_size)
def fill_vocab_mask(self, vocab_mask: torch.Tensor, idx: int) -> None:
self.matcher.fill_next_token_bitmask(vocab_mask, idx)
@staticmethod
def move_vocab_mask(vocab_mask: torch.Tensor, device) -> torch.Tensor:
return vocab_mask.to(device, non_blocking=True)
def apply_vocab_mask(self, logits: torch.Tensor, vocab_mask: torch.Tensor) -> None:
if logits.device.type in {"cuda", "xpu", "musa"}:
if _is_hip:
apply_token_bitmask_inplace_cuda(logits, vocab_mask)
else:
apply_token_bitmask_inplace_triton(logits, vocab_mask)
elif logits.device.type == "npu":
import sgl_kernel_npu # noqa: F401
torch.ops.npu.apply_token_bitmask(logits, vocab_mask)
else:
raise RuntimeError(f"Unsupported device: {logits.device.type}")
def copy(self):
matcher = GrammarMatcher(
self.ctx,
max_rollback_tokens=MAX_ROLLBACK_TOKENS,
override_stop_tokens=self.override_stop_tokens,
)
if grammar_stats := self.grammar_stats:
grammar_stats = dataclasses.replace(
grammar_stats, is_cache_hit=True, tree_traversal_time=[]
)
return XGrammarGrammar(
matcher,
self.vocab_size,
self.ctx,
self.override_stop_tokens,
self.key_string,
grammar_stats,
)
def try_jump_forward(self, tokenizer) -> Optional[Tuple[List[int], str]]:
s = self.matcher.find_jump_forward_string()
if s:
return [], s
return None
def jump_forward_str_state(self, helper: Tuple[List[int], str]) -> Tuple[str, int]:
_, data = helper
return data, -1
def jump_and_retokenize(
self, old_output_ids: List[int], new_output_ids: List[int], next_state: int
):
k = 0
for i, old_id in enumerate(old_output_ids):
if old_id == new_output_ids[i]:
k = i + 1
else:
break
# rollback to the last token that is the same
if k < len(old_output_ids):
self.matcher.rollback(len(old_output_ids) - k)
for i in range(k, len(new_output_ids)):
if not self.matcher.accept_token(new_output_ids[i]):
raise ValueError(
f"Token not accepted during retokenization: {new_output_ids[i]} "
f"at position {i}\n"
f"Old output IDs: {old_output_ids}\n"
f"New output IDs: {new_output_ids}\n"
f"Key string: {self.key_string}"
)
def __repr__(self):
return f"XGrammarGrammar({self.key_string=}, {self.accepted_tokens=}, {self.current_token=})"
class TokenizerNotSupportedError(Exception):
"""Raised when tokenizer is not supported by XGrammar backend."""
pass
class XGrammarGrammarBackend(BaseGrammarBackend):
def __init__(
self,
tokenizer,
vocab_size: int,
model_eos_token_ids: Optional[List[int]] = None,
any_whitespace: bool = True,
):
super().__init__()
if hasattr(tokenizer, "init_xgrammar"):
# For special tokenizer
tokenizer_info, override_stop_tokens = tokenizer.init_xgrammar()
if tokenizer_info is None:
# Not supported tokenizer
raise TokenizerNotSupportedError(
f"Tokenizer type {type(tokenizer).__name__} is not supported by XGrammar"
)
else:
# Create TokenizerInfo with model's EOS tokens as the authoritative stop tokens
# This ensures consistency between what the model considers EOS and what XGrammar uses
try:
tokenizer_info = TokenizerInfo.from_huggingface(
tokenizer, vocab_size=vocab_size, stop_token_ids=model_eos_token_ids
)
override_stop_tokens = None
except Exception as e:
raise TokenizerNotSupportedError(
f"Failed to create XGrammar TokenizerInfo from tokenizer: {e}"
)
self.grammar_compiler = GrammarCompiler(tokenizer_info=tokenizer_info)
self.vocab_size = vocab_size
self.override_stop_tokens = override_stop_tokens
self.any_whitespace = any_whitespace
@property
def is_support_token_filter(self):
return True
@staticmethod
def allocate_vocab_mask(vocab_size: int, batch_size: int, device) -> torch.Tensor:
return allocate_token_bitmask(batch_size, vocab_size)
@staticmethod
def move_vocab_mask(vocab_mask: torch.Tensor, device) -> torch.Tensor:
return vocab_mask.to(device, non_blocking=True)
@staticmethod
def apply_vocab_mask(logits: torch.Tensor, vocab_mask: torch.Tensor) -> None:
if logits.device.type in {"cuda", "npu", "xpu", "musa"}:
if _is_hip:
apply_token_bitmask_inplace_cuda(logits, vocab_mask)
else:
apply_token_bitmask_inplace_triton(logits, vocab_mask)
else:
raise RuntimeError(f"Unsupported device: {logits.device.type}")
@staticmethod
def set_token_filter(
vocab_mask: torch.Tensor,
token_ids: List[int],
batch_idx: int,
is_allowed: bool = True,
reset_vocab_mask: bool = True,
):
if _is_hip or (vocab_mask.device.type != "cuda"):
set_token_filter_torch(
vocab_mask,
token_ids,
batch_idx,
is_allowed=is_allowed,
reset_vocab_mask=reset_vocab_mask,
)
else:
set_token_filter_triton(
vocab_mask,
token_ids,
batch_idx,
is_allowed=is_allowed,
reset_vocab_mask=reset_vocab_mask,
)
@staticmethod
def _sanitize_structural_format(structural_format):
"""Recursively replace missing json_schema fields with an empty schema."""
if not isinstance(structural_format, dict):
return
fmt_type = structural_format.get("type")
if fmt_type in {"json_schema", "qwen_xml_parameter"}:
if structural_format.get("json_schema") is None:
structural_format["json_schema"] = {}
if fmt_type == "tag":
XGrammarGrammarBackend._sanitize_structural_format(
structural_format.get("content")
)
elif fmt_type in {"sequence", "or"}:
for element in structural_format.get("elements", []):
XGrammarGrammarBackend._sanitize_structural_format(element)
elif fmt_type in {"triggered_tags", "tags_with_separator"}:
for tag in structural_format.get("tags", []):
XGrammarGrammarBackend._sanitize_structural_format(tag)
@staticmethod
def _sanitize_structural_tag_structures(structural_tag: Dict) -> None:
for structure in structural_tag.get("structures", []):
if structure.get("schema") is None:
structure["schema"] = {}
def _from_context(
self, ctx: CompiledGrammar, key_string: str, grammar_stats: GrammarStats
) -> XGrammarGrammar:
matcher = GrammarMatcher(
ctx,
max_rollback_tokens=MAX_ROLLBACK_TOKENS,
override_stop_tokens=self.override_stop_tokens,
)
return XGrammarGrammar(
matcher,
self.vocab_size,
ctx,
self.override_stop_tokens,
key_string,
grammar_stats,
)
def dispatch_json(self, key_string: str) -> BaseGrammarObject:
try:
if key_string == "$$ANY$$":
# Note: This builtin JSON grammar includes *all* valid JSON (including, for example, arrays at the root)
ctx = self.grammar_compiler.compile_builtin_json_grammar()
else:
ctx = self.grammar_compiler.compile_json_schema(
schema=key_string, any_whitespace=self.any_whitespace
)
except (RuntimeError, json.decoder.JSONDecodeError, UnicodeDecodeError) as e:
logger.error(f"Hit invalid json_schema: {key_string=}, {e=}")
return InvalidGrammarObject(str(e))
return self._from_context(ctx, key_string, GrammarStats(dispatch_type="json"))
def dispatch_ebnf(self, key_string: str) -> BaseGrammarObject:
try:
ctx = self.grammar_compiler.compile_grammar(key_string)
except RuntimeError as e:
logger.error(f"Hit invalid ebnf: {key_string=}, {e=}")
return InvalidGrammarObject(str(e))
return self._from_context(ctx, key_string, GrammarStats(dispatch_type="ebnf"))
def dispatch_regex(self, key_string: str) -> BaseGrammarObject:
try:
ctx = self.grammar_compiler.compile_regex(key_string)
except RuntimeError as e:
logger.error(f"Hit invalid regex: {key_string=}, {e=}")
return InvalidGrammarObject(str(e))
return self._from_context(ctx, key_string, GrammarStats(dispatch_type="regex"))
def dispatch_structural_tag(self, key_string: str) -> BaseGrammarObject:
try:
# TODO(dark): it's REALLY stupid to construct object from string and decode it again
structural_tag = json.loads(key_string)
if is_legacy_structural_tag(structural_tag):
self._sanitize_structural_tag_structures(structural_tag)
tags = [
StructuralTagItem(
begin=structure["begin"],
schema=json.dumps(structure["schema"]),
end=structure["end"],
)
for structure in structural_tag["structures"]
]
new_tag = StructuralTag.from_legacy_structural_tag(
tags, structural_tag["triggers"]
)
new_tag.format.at_least_one = structural_tag.get("at_least_one", False)
ctx = self.grammar_compiler.compile_structural_tag(new_tag)
else:
format_dict = structural_tag.get("format")
if isinstance(format_dict, dict):
self._sanitize_structural_format(format_dict)
structural_tag["format"] = format_dict
key_string = json.dumps(structural_tag)
ctx = self.grammar_compiler.compile_structural_tag(key_string)
except (RuntimeError, json.decoder.JSONDecodeError) as e:
logger.error(f"Hit invalid structural_tag: {key_string=}, {e=}")
return InvalidGrammarObject(str(e))
return self._from_context(
ctx, key_string, GrammarStats(dispatch_type="structural_tag")
)
def reset(self):
super().reset()
self.grammar_compiler.clear_cache()
def demo_test():
from transformers import AutoConfig, AutoTokenizer
from sglang.test.test_utils import DEFAULT_MODEL_NAME_FOR_TEST
tokenizer = AutoTokenizer.from_pretrained(DEFAULT_MODEL_NAME_FOR_TEST)
hf_config = AutoConfig.from_pretrained(DEFAULT_MODEL_NAME_FOR_TEST)
# Should use vocab size from model config
vocab_size = hf_config.vocab_size
eos_token_id = tokenizer.eos_token_id
backend = XGrammarGrammarBackend(
tokenizer, vocab_size=vocab_size, model_eos_token_ids=[eos_token_id]
)
regex = r"hello (world|there)"
grammar = backend.dispatch_regex(regex)
tokens = [
tokenizer.encode(t, add_special_tokens=False)[0] for t in ["hello", " world"]
]
# Test termination
grammar.accept_token(tokens[0]) # accept "hello"
grammar.accept_token(tokens[1]) # accept " world"
grammar.accept_token(eos_token_id) # accept EOS
assert grammar.is_terminated()
# Test rollback the terminated state
grammar.rollback(1)
assert not grammar.is_terminated()
if __name__ == "__main__":
demo_test()