[CI] Derive registered-test kind from the registry call instead of the path (#40294)

This commit is contained in:
Liangsheng Yin
2026-09-20 02:01:22 -07:00
committed by GitHub
parent 671630abf1
commit 0024efa0de
24 changed files with 41 additions and 744 deletions
@@ -1,315 +0,0 @@
"""
End-to-end tests for strict reasoning + constrained decoding.
Tests that the full pipeline works:
- AC-5.1: Strict reasoning + JSON schema constrained generation
- AC-5.2: Strict reasoning + tool call parsing (basic validation only)
These tests launch a real server with a small model and verify
the constrained decoding pipeline produces valid output.
"""
import json
import unittest
import requests
from sglang.srt.utils import kill_process_tree
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
CustomTestCase,
popen_launch_server,
)
register_cuda_ci(est_time=96, stage="base-b", runner_config="1-gpu-small")
register_amd_ci(est_time=120, suite="stage-b-test-1-gpu-small-amd")
MODEL = "Qwen/Qwen3-0.6B"
BASE_URL = "http://127.0.0.1:39877"
API_KEY = "sk-test-1234"
class TestConstrainedReasoningE2E(CustomTestCase):
@classmethod
def setUpClass(cls):
cls.model = MODEL
cls.base_url = BASE_URL
cls.api_key = API_KEY
cls.process = popen_launch_server(
cls.model,
cls.base_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
api_key=cls.api_key,
other_args=[
"--reasoning-parser",
"qwen3",
],
)
@classmethod
def tearDownClass(cls):
kill_process_tree(cls.process.pid)
def _chat(self, **kwargs):
default = {
"model": self.model,
"messages": [
{
"role": "user",
"content": "What is 2+2? Answer with just the number.",
}
],
"temperature": 0,
"max_tokens": 256,
}
default.update(kwargs)
resp = requests.post(
f"{self.base_url}/v1/chat/completions",
headers={"Authorization": f"Bearer {self.api_key}"},
json=default,
timeout=60,
)
self.assertEqual(resp.status_code, 200, f"Request failed: {resp.text}")
return resp.json()
def test_reasoning_with_json_schema(self):
"""AC-5.1: Reasoning + JSON schema produces valid JSON output."""
schema = {
"type": "object",
"properties": {
"answer": {"type": "integer"},
},
"required": ["answer"],
}
data = self._chat(
response_format={
"type": "json_schema",
"json_schema": {
"name": "answer_schema",
"schema": schema,
},
},
chat_template_kwargs={"enable_thinking": True},
separate_reasoning=True,
)
choice = data["choices"][0]
content = choice["message"]["content"] or ""
# Content should be valid JSON conforming to schema when non-empty.
# With small models + separate_reasoning, content may be empty if the
# model puts everything in reasoning_content. That's acceptable.
if content.strip():
try:
parsed = json.loads(content)
self.assertIn("answer", parsed)
self.assertIsInstance(parsed["answer"], int)
except (json.JSONDecodeError, TypeError):
# Small models may produce imperfect JSON
self.assertTrue(
content.strip().startswith("{"),
f"Expected JSON-like output, got: {content!r}",
)
# Content should NOT contain <think> tags (those go to reasoning_content)
self.assertNotIn("<think>", content)
def test_reasoning_disabled_with_json_schema(self):
"""JSON schema still works when reasoning is explicitly disabled."""
schema = {
"type": "object",
"properties": {
"answer": {"type": "integer"},
},
"required": ["answer"],
}
data = self._chat(
response_format={
"type": "json_schema",
"json_schema": {
"name": "answer_schema",
"schema": schema,
},
},
chat_template_kwargs={"enable_thinking": False},
)
choice = data["choices"][0]
content = choice["message"]["content"]
# Should still produce valid JSON
parsed = json.loads(content)
self.assertIn("answer", parsed)
def test_reasoning_with_separate_output(self):
"""Reasoning content is correctly separated from normal content."""
data = self._chat(
chat_template_kwargs={"enable_thinking": True},
separate_reasoning=True,
)
choice = data["choices"][0]
content = choice["message"]["content"]
reasoning = choice["message"].get("reasoning_content")
# Content should not contain think tags
self.assertNotIn("<think>", content)
self.assertNotIn("</think>", content)
def test_tool_call_after_reasoning(self):
"""AC-5.2: Tool call parsing works with reasoning enabled."""
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"},
},
"required": ["location"],
},
},
}
]
data = self._chat(
messages=[
{
"role": "user",
"content": "What's the weather in Paris?",
}
],
tools=tools,
chat_template_kwargs={"enable_thinking": True},
separate_reasoning=True,
)
choice = data["choices"][0]
# The model may or may not produce tool calls (depends on model capability)
# but the response should be well-formed (no crashes)
self.assertIn("message", choice)
self.assertIn("finish_reason", choice)
# finish_reason should be either "stop" or "tool_calls"
self.assertIn(choice["finish_reason"], ["stop", "tool_calls", "length"])
class TestStrictThinkingE2E(CustomTestCase):
"""E2E tests with --enable-strict-thinking flag.
Validates that the strict thinking flag is correctly propagated through
the full pipeline: server_args -> grammar_backend -> ReasonerGrammarBackend
-> token filtering during thinking phase.
"""
@classmethod
def setUpClass(cls):
cls.model = MODEL
cls.base_url = "http://127.0.0.1:39878"
cls.api_key = API_KEY
cls.process = popen_launch_server(
cls.model,
cls.base_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
api_key=cls.api_key,
other_args=[
"--reasoning-parser",
"qwen3",
"--enable-strict-thinking",
],
)
@classmethod
def tearDownClass(cls):
kill_process_tree(cls.process.pid)
def _chat(self, **kwargs):
default = {
"model": self.model,
"messages": [
{
"role": "user",
"content": "What is 2+2? Answer with just the number.",
}
],
"temperature": 0,
"max_tokens": 256,
}
default.update(kwargs)
resp = requests.post(
f"{self.base_url}/v1/chat/completions",
headers={"Authorization": f"Bearer {self.api_key}"},
json=default,
timeout=60,
)
self.assertEqual(resp.status_code, 200, f"Request failed: {resp.text}")
return resp.json()
def test_strict_thinking_with_json_schema(self):
"""Strict thinking + JSON schema: server starts and produces valid output."""
schema = {
"type": "object",
"properties": {
"answer": {"type": "integer"},
},
"required": ["answer"],
}
data = self._chat(
response_format={
"type": "json_schema",
"json_schema": {
"name": "answer_schema",
"schema": schema,
},
},
chat_template_kwargs={"enable_thinking": True},
separate_reasoning=True,
)
choice = data["choices"][0]
content = choice["message"]["content"] or ""
if content.strip():
try:
parsed = json.loads(content)
self.assertIn("answer", parsed)
except (json.JSONDecodeError, TypeError):
self.assertTrue(
content.strip().startswith("{"),
f"Expected JSON-like output, got: {content!r}",
)
# Think tags must not leak into content
self.assertNotIn("<think>", content)
def test_strict_thinking_disabled_per_request(self):
"""When thinking is disabled per-request, strict server still works."""
data = self._chat(
chat_template_kwargs={"enable_thinking": False},
)
choice = data["choices"][0]
self.assertIn("message", choice)
self.assertIn("finish_reason", choice)
# Should complete normally without errors
self.assertIn(choice["finish_reason"], ["stop", "length"])
def test_strict_thinking_separate_reasoning(self):
"""Strict thinking with separate_reasoning produces well-formed output."""
data = self._chat(
chat_template_kwargs={"enable_thinking": True},
separate_reasoning=True,
)
choice = data["choices"][0]
content = choice["message"]["content"] or ""
# Think tags must not leak into content
self.assertNotIn("<think>", content)
self.assertNotIn("</think>", content)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,515 @@
"""Dispatch-contract tests for the unified ``BaseFusedOp`` (RFC #29630, #26426).
``BaseFusedOp`` replaced ``MultiPlatformOp`` as the single operator
abstraction; these tests pin down the parts of that contract that a refactor
could silently break:
- the priority ladder: explicit ``backend=`` > global forced backend > OOT
platform override > declared optimized kernel backends > platform-specific
forward > native fallback;
- the standard ``nn.Module`` behavior (hooks, traversal);
- static-dispatch caching and per-call ``backend_eligible`` gating;
- the torch.compile enter/leave protocol (idempotency, TopK / FusedMoE
special paths);
- the deprecated ``MultiPlatformOp`` alias and its OOT plugin surface.
Platform detection is mocked, so everything here runs on a CPU-only box.
"""
import warnings
import pytest
import torch
from torch import nn
import sglang.kernels.fused_op as fo
from sglang.kernels.fused_op import BaseFusedOp
from sglang.kernels.spec import CapabilityRequirement as Cap
from sglang.kernels.spec import KernelBackend, PlatformInfo
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=12, suite="base-a-test-cpu")
_CUDA = PlatformInfo(device_type="cuda", cuda_arch_major=9, cuda_arch_minor=0)
_HIP = PlatformInfo(device_type="hip")
_CPU = PlatformInfo()
@pytest.fixture(autouse=True)
def _reset_global_state():
saved_oot = {k: dict(v) for k, v in BaseFusedOp._oot_forward_registry.items()}
yield
fo.set_fused_op_backend(None)
fo.disable_fused_op_trace()
fo.clear_fused_op_trace()
BaseFusedOp._oot_forward_registry.clear()
BaseFusedOp._oot_forward_registry.update(saved_oot)
def _mock_platform(monkeypatch, *, key="", info=_CPU, oot_key=None):
monkeypatch.setattr(fo, "_platform_key", lambda: key)
monkeypatch.setattr(fo, "_platform", lambda: info)
monkeypatch.setattr(fo, "_oot_dispatch_key", lambda: oot_key)
class _AllPlatformsOp(BaseFusedOp):
"""Marks which path ran by returning its name."""
op = "test.all_platforms"
def forward_native(self, x):
return "native"
def forward_cuda(self, x):
return "cuda"
def forward_hip(self, x):
return "hip"
def forward_npu(self, x):
return "npu"
def forward_xpu(self, x):
return "xpu"
def forward_musa(self, x):
return "musa"
def forward_cpu(self, x):
return "cpu"
class _CudaOnlyPlatformOp(BaseFusedOp):
op = "test.cuda_only_platform"
def forward_native(self, x):
return "native"
def forward_cuda(self, x):
return "cuda"
class _NativeOnlyOp(BaseFusedOp):
op = "test.native_only"
def forward_native(self, x):
return "native"
class _BackendAndPlatformOp(BaseFusedOp):
"""Declared JIT backend + a CUDA platform forward."""
op = "test.backend_and_platform"
priority = (KernelBackend.JIT, KernelBackend.TORCH)
capabilities = {KernelBackend.JIT: frozenset({Cap.CUDA})}
def forward_native(self, x):
return "native"
def forward_jit(self, x):
return "jit"
def forward_cuda(self, x):
return "cuda"
class _UndeclaredBackendOp(BaseFusedOp):
"""Overrides forward_aiter but does not declare it in ``capabilities``."""
op = "test.undeclared_backend"
def forward_native(self, x):
return "native"
def forward_aiter(self, x):
return "aiter"
# --- nn.Module contract -------------------------------------------------------
def test_is_standard_nn_module(monkeypatch):
_mock_platform(monkeypatch)
op = _NativeOnlyOp()
assert isinstance(op, nn.Module)
parent = nn.Module()
parent.act = op
assert dict(parent.named_modules())["act"] is op
seen = []
op.register_forward_hook(lambda module, args, output: seen.append(output))
assert op(torch.zeros(1)) == "native"
assert seen == ["native"] # __call__ goes through nn.Module, hooks fire
# --- platform dispatch + native fallback ---------------------------------------
@pytest.mark.parametrize(
"key, expect",
[
("cuda", "cuda"),
("hip", "hip"),
("npu", "npu"),
("xpu", "xpu"),
("musa", "musa"),
("cpu", "cpu"),
("", "native"),
],
)
def test_platform_forward_dispatch(monkeypatch, key, expect):
_mock_platform(monkeypatch, key=key)
assert _AllPlatformsOp()(torch.zeros(1)) == expect
@pytest.mark.parametrize(
"key, expect",
[
("hip", "cuda"), # HIP falls back to the CUDA path (hipified kernels)
# MUSA has no implicit CUDA fallback: srt kernel imports are gated on
# is_cuda(), so silently entering forward_cuda on a MUSA box can
# NameError; ops opt in with an explicit forward_musa instead.
("musa", "native"),
("npu", "native"), # no NPU path -> native
("cpu", "native"),
("cuda", "cuda"),
],
)
def test_platform_default_chains(monkeypatch, key, expect):
_mock_platform(monkeypatch, key=key)
assert _CudaOnlyPlatformOp()(torch.zeros(1)) == expect
def test_native_fallback_without_any_override(monkeypatch):
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
assert _NativeOnlyOp()(torch.zeros(1)) == "native"
# --- optimized-backend selection ------------------------------------------------
def test_declared_backend_beats_platform_forward(monkeypatch):
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
assert _BackendAndPlatformOp()(torch.zeros(1)) == "jit"
def test_capability_filters_backend_to_platform_forward(monkeypatch):
# JIT is declared CUDA-only; on HIP the platform chain (-> forward_cuda) runs.
_mock_platform(monkeypatch, key="hip", info=_HIP)
assert _BackendAndPlatformOp()(torch.zeros(1)) == "cuda"
def test_undeclared_backend_not_auto_selected(monkeypatch):
_mock_platform(monkeypatch, key="", info=_CPU)
op = _UndeclaredBackendOp()
assert op(torch.zeros(1)) == "native"
# ... but stays reachable by explicit request.
assert op(torch.zeros(1), backend=KernelBackend.AITER) == "aiter"
def test_priority_order_decides_between_backends(monkeypatch):
class _TwoBackends(BaseFusedOp):
op = "test.two_backends"
priority = (KernelBackend.TRITON, KernelBackend.JIT, KernelBackend.TORCH)
capabilities = {
KernelBackend.TRITON: frozenset(),
KernelBackend.JIT: frozenset(),
}
def forward_native(self, x):
return "native"
def forward_triton(self, x):
return "triton"
def forward_jit(self, x):
return "jit"
class _Flipped(_TwoBackends):
priority = (KernelBackend.JIT, KernelBackend.TRITON, KernelBackend.TORCH)
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
assert _TwoBackends()(torch.zeros(1)) == "triton"
assert _Flipped()(torch.zeros(1)) == "jit"
def test_explicit_backend_beats_forced_global(monkeypatch):
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
op = _BackendAndPlatformOp()
fo.set_fused_op_backend(KernelBackend.TORCH)
assert op(torch.zeros(1)) == "native" # forced global
assert op(torch.zeros(1), backend=KernelBackend.JIT) == "jit" # explicit wins
def test_forced_global_falls_back_when_unimplemented(monkeypatch):
# The global debug switch must not take down ops that lack the forced
# backend (e.g. forcing "torch" on a device-only op like the DSA indexer,
# whose forward_native raises NotImplementedError).
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
class _DeviceOnly(BaseFusedOp):
op = "test.device_only"
def forward_native(self, x):
raise NotImplementedError
def forward_cuda(self, x):
return "cuda"
op = _DeviceOnly()
fo.set_fused_op_backend(KernelBackend.TORCH)
assert op(torch.zeros(1)) == "cuda" # fell back to normal dispatch
fo.set_fused_op_backend(KernelBackend.JIT)
assert op(torch.zeros(1)) == "cuda" # no jit backend -> fall back too
# Explicit per-call selection stays strict.
fo.set_fused_op_backend(None)
with pytest.raises(NotImplementedError):
op(torch.zeros(1), backend=KernelBackend.JIT)
def test_forced_global_beats_platform_and_oot(monkeypatch):
_mock_platform(monkeypatch, key="", oot_key="myplat")
BaseFusedOp.register_oot_forward(
_CudaOnlyPlatformOp, lambda self, x: "oot", "myplat"
)
op = _CudaOnlyPlatformOp()
fo.set_fused_op_backend(KernelBackend.TORCH)
assert op(torch.zeros(1)) == "native"
fo.set_fused_op_backend(None)
assert op(torch.zeros(1)) == "oot"
# --- OOT platform overrides ----------------------------------------------------
def test_oot_registered_forward_wins_over_method(monkeypatch):
class _OotOp(BaseFusedOp):
op = "test.oot"
def forward_native(self, x):
return "native"
def forward_myplat(self, x):
return "method"
_mock_platform(monkeypatch, oot_key="myplat")
assert _OotOp()(torch.zeros(1)) == "method" # forward_<key> lookup
BaseFusedOp.register_oot_forward(_OotOp, lambda self, x: "registered", "myplat")
assert _OotOp()(torch.zeros(1)) == "registered" # registry beats method
def test_oot_registration_is_exact_type(monkeypatch):
_mock_platform(monkeypatch, oot_key="myplat")
BaseFusedOp.register_oot_forward(
_CudaOnlyPlatformOp, lambda self, x: "oot", "myplat"
)
class _Sub(_CudaOnlyPlatformOp):
pass
assert _CudaOnlyPlatformOp()(torch.zeros(1)) == "oot"
# Subclasses do not inherit the registered forward (pre-existing
# MultiPlatformOp semantics: lookup is by exact type).
assert _Sub()(torch.zeros(1)) == "native"
def test_oot_falls_back_to_native(monkeypatch):
_mock_platform(monkeypatch, oot_key="myplat")
assert _CudaOnlyPlatformOp()(torch.zeros(1)) == "native"
def test_oot_registered_fn_is_bound(monkeypatch):
_mock_platform(monkeypatch, oot_key="myplat")
BaseFusedOp.register_oot_forward(
_CudaOnlyPlatformOp, lambda self, x: type(self).__name__, "myplat"
)
assert _CudaOnlyPlatformOp()(torch.zeros(1)) == "_CudaOnlyPlatformOp"
# --- dispatch caching + per-call gates ------------------------------------------
def test_static_dispatch_resolved_once(monkeypatch):
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
op = _CudaOnlyPlatformOp()
calls = []
original = op._resolve_forward_method
monkeypatch.setattr(
op,
"_resolve_forward_method",
lambda: calls.append(1) or original(),
)
op(torch.zeros(1))
op(torch.zeros(1))
assert len(calls) == 1 # hot path must not re-resolve per call
def test_init_preseeded_forward_method_is_kept(monkeypatch):
# srt layers pin instance paths in __init__ (e.g. env-gated aiter modes);
# lazy resolution must not clobber that.
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
op = _AllPlatformsOp()
op._forward_method = op.forward_xpu
assert op(torch.zeros(1)) == "xpu"
def test_backend_eligible_override_gates_per_call(monkeypatch):
class _Gated(BaseFusedOp):
op = "test.gated"
priority = (KernelBackend.JIT, KernelBackend.TORCH)
capabilities = {KernelBackend.JIT: frozenset()}
def forward_native(self, x):
return "native"
def forward_jit(self, x):
return "jit"
def backend_eligible(self, backend, *args, **kwargs):
if not super().backend_eligible(backend, *args, **kwargs):
return False
if backend is KernelBackend.JIT:
return args[0].shape[-1] % 2 == 0
return True
_mock_platform(monkeypatch, key="", info=_CPU)
op = _Gated()
assert op(torch.zeros(4)) == "jit"
assert op(torch.zeros(3)) == "native" # same instance, per-call bounce
assert op(torch.zeros(8)) == "jit"
# --- torch.compile protocol -----------------------------------------------------
def test_enter_leave_torch_compile_roundtrip(monkeypatch):
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
op = _CudaOnlyPlatformOp()
assert op(torch.zeros(1)) == "cuda"
op.enter_torch_compile(num_tokens=16)
assert op.is_torch_compile
assert op(torch.zeros(1)) == "native"
# Reused-module idempotency: a second enter must not overwrite the saved
# original forward, otherwise leave() cannot restore it.
op.enter_torch_compile(num_tokens=16)
op.leave_torch_compile()
assert not op.is_torch_compile
assert op(torch.zeros(1)) == "cuda"
op.leave_torch_compile() # double leave is a no-op
assert op(torch.zeros(1)) == "cuda"
def test_torch_compile_hook_none_keeps_dispatch(monkeypatch):
class _KeepOptimized(_CudaOnlyPlatformOp):
def _torch_compile_forward(self, num_tokens):
return None if num_tokens > 1 else self.forward_native
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
op = _KeepOptimized()
op.enter_torch_compile(num_tokens=8)
assert op.is_torch_compile
assert op(torch.zeros(1)) == "cuda" # dispatch unchanged for bs > 1
op.leave_torch_compile()
op.enter_torch_compile(num_tokens=1)
assert op(torch.zeros(1)) == "native"
op.leave_torch_compile()
def test_topk_compile_hook_is_bs1_only():
from sglang.srt.layers.moe.topk import TopK
class _Probe:
forward_native = "native-sentinel"
assert TopK._torch_compile_forward(_Probe(), num_tokens=1) == "native-sentinel"
assert TopK._torch_compile_forward(_Probe(), num_tokens=2) is None
def test_fused_moe_compile_hook_is_bs1_only():
from sglang.srt.layers.moe.fused_moe_native import fused_moe_forward_native
from sglang.srt.layers.quantization.unquant import UnquantizedFusedMoEMethod
probe = object.__new__(UnquantizedFusedMoEMethod)
assert (
UnquantizedFusedMoEMethod._torch_compile_forward(probe, num_tokens=1)
is fused_moe_forward_native
)
assert UnquantizedFusedMoEMethod._torch_compile_forward(probe, num_tokens=2) is None
# --- tracing --------------------------------------------------------------------
def test_trace_labels_explicit_backend(monkeypatch):
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
op = _CudaOnlyPlatformOp()
fo.enable_fused_op_trace()
op(torch.zeros(2, 3))
op(torch.zeros(2, 3), backend=KernelBackend.TORCH)
_, explicit_rec = fo.get_fused_op_trace()
assert explicit_rec.backend == "torch"
# --- deprecated MultiPlatformOp alias --------------------------------------------
def test_deprecated_alias_contract(monkeypatch):
from sglang.srt.layers.utils import MultiPlatformOp
assert issubclass(MultiPlatformOp, BaseFusedOp)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
class _LegacyOp(MultiPlatformOp):
# Old-style subclass: platform forwards only, no forward_native.
def forward_cuda(self, x):
return "cuda"
assert any(issubclass(w.category, DeprecationWarning) for w in caught)
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
op = _LegacyOp() # instantiable without forward_native (lenient alias)
assert op(torch.zeros(1)) == "cuda"
with pytest.raises(NotImplementedError):
op.forward_native(torch.zeros(1))
# register_oot_forward via the alias lands in the shared registry.
MultiPlatformOp.register_oot_forward(_LegacyOp, lambda self, x: "oot", "aliasplat")
_mock_platform(monkeypatch, key="", oot_key="aliasplat")
assert _LegacyOp()(torch.zeros(1)) == "oot"
def test_deprecated_alias_keeps_legacy_platform_defaults(monkeypatch):
"""Old MultiPlatformOp defined per-platform default methods (hip/musa ->
cuda, npu/xpu/cpu -> native); plugin code may call them directly, and a
subclass without forward_cuda must still raise on CUDA like before."""
from sglang.srt.layers.utils import MultiPlatformOp
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
class _NativeOnlyLegacy(MultiPlatformOp):
def forward_native(self, x):
return "native"
op = _NativeOnlyLegacy()
assert op.forward_cpu(torch.zeros(1)) == "native"
assert op.forward_npu(torch.zeros(1)) == "native"
with pytest.raises(NotImplementedError):
op.forward_hip(torch.zeros(1)) # chains to the raising forward_cuda
_mock_platform(monkeypatch, key="cuda", info=_CUDA)
with pytest.raises(NotImplementedError):
_NativeOnlyLegacy()(torch.zeros(1)) # old CUDA behavior preserved
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__]))
@@ -0,0 +1,532 @@
"""CPU-only tests for the JIT build pipeline: ninja generation and the cache.
Everything here runs against synthetic files, so the invariants that matter — a
bad recorded dependency list never causes reuse, differing flags never share a
directory, a moved clone still hits — are checked without a GPU or a compiler.
"""
from __future__ import annotations
import os
import pathlib
import sys
import msgspec
import pytest
from sglang.kernels.jit.utils.compile import cache, ninja
from sglang.kernels.jit.utils.compile.paths import KERNEL_PATH
from sglang.kernels.jit.utils.compile.spec import BuildSpec
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=8, suite="base-a-test-cpu")
@pytest.fixture(autouse=True)
def _fresh_digests():
cache.clear_digest_cache()
yield
cache.clear_digest_cache()
def _write(path: pathlib.Path, text: str) -> pathlib.Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
return path
def _spec(**overrides) -> BuildSpec:
base = dict(
module_args=("activation", "bf16_t"),
cpp_files=(),
cuda_files=(),
cpp_wrappers=(("run", "Kernel::run"),),
cuda_wrappers=(),
cflags=("-O3",),
cuda_cflags=("-O3",),
ldflags=(),
include_paths=(),
header_only=True,
)
base.update(overrides)
return BuildSpec(**base)
def _build_key(**overrides) -> str:
spec = _spec(**overrides)
return cache.compute_build_key(spec, build_file=ninja.generate(spec))
def _entries(paths) -> list:
out = []
for path in paths:
root, relpath = cache._normalize_path(path)
out.append(
cache._DepEntry(root=root, relpath=relpath, digest=cache._file_digest(path))
)
return out
def _publish_leaf(scope: pathlib.Path, paths, *, module_name="m") -> pathlib.Path:
"""Create a leaf the way commit_build would: name derived from its own list."""
entries = _entries(paths)
leaf = scope / f"{cache._DEPS_KEY_PREFIX}{cache._deps_key(entries)}"
leaf.mkdir(parents=True)
(leaf / cache._DEPS_FILE).write_bytes(msgspec.json.encode(entries))
(leaf / f"{module_name}.so").write_bytes(b"")
return leaf
# --------------------------------------------------------------------------
# Anchor roots
# --------------------------------------------------------------------------
def test_in_tree_paths_normalize_to_an_anchor():
header = KERNEL_PATH / "include" / "sgl_kernel" / "utils.cuh"
root, relpath = cache._normalize_path(header)
assert root == "kernels"
assert relpath == "include/sgl_kernel/utils.cuh"
assert cache._resolve_path(root=root, relpath=relpath) == header
def test_unknown_paths_fall_back_to_absolute(tmp_path):
root, relpath = cache._normalize_path(tmp_path / "elsewhere.h")
assert root == "abs"
assert cache._resolve_path(root=root, relpath=relpath) == tmp_path / "elsewhere.h"
def test_unresolvable_anchor_is_a_miss_not_a_crash():
assert cache._resolve_path(root="pkg:does-not-exist", relpath="x.h") is None
def test_anchor_roots_are_symlink_resolved(tmp_path, monkeypatch):
"""Anchors must be symlink-resolved, since the paths matched against them are.
`/usr/local/cuda` is a symlink to `/usr/local/cuda-<version>`; an unresolved
anchor makes every toolkit header miss it and fall through to `sys`, whose
relpath then carries the CUDA version and breaks reuse across upgrades.
The toolkit stands in for all of them — one comprehension resolves every
anchor — and it is faked rather than read off the machine so this still
guards on the CPU-only runners, which have no toolkit at all.
"""
from sglang.kernels.jit.utils.compile import toolchain
versioned = tmp_path / "cuda-12.9"
versioned.mkdir()
(tmp_path / "cuda").symlink_to(versioned)
monkeypatch.setattr(toolchain, "toolkit_home", lambda: tmp_path / "cuda")
# `__wrapped__` is the undecorated function: the anchors are memoized for
# the process, and the real ones were already computed by an earlier test.
roots = dict(cache._anchor_roots.__wrapped__())
assert roots["toolkit"] == versioned
# --------------------------------------------------------------------------
# build_key — what must and must not change it
# --------------------------------------------------------------------------
@pytest.mark.parametrize(
"overrides",
[
{"module_args": ("activation", "fp16_t")},
{"cuda_cflags": ("-O3", "--use_fast_math")},
{"cflags": ("-O2",)},
{"ldflags": ("-lfoo",)},
{"cpp_wrappers": (("run", "Other::run"),)},
{"include_paths": ("/opt/extra",)},
{"header_only": False, "cpp_files": ("/tmp/x.cpp",), "cpp_wrappers": ()},
],
ids=["args", "cuda_cflags", "cflags", "ldflags", "wrappers", "includes", "mode"],
)
def test_build_key_separates_every_build_input(overrides):
"""Two builds differing in any of these must never share a directory.
Sharing one would let a lookup select a leaf produced under different flags,
which is the failure the whole two-key split exists to prevent.
"""
assert _build_key(**overrides) != _build_key()
def test_build_key_covers_the_whole_ninja_file():
"""Every flag reaching the compiler reaches the key, because the key is
taken over the generated build file itself rather than over a hand-listed
subset of inputs."""
spec = _spec()
baseline = cache.compute_build_key(spec, build_file=ninja.generate(spec))
tampered = ninja.generate(spec).replace("-O3", "-O0")
assert cache.compute_build_key(spec, build_file=tampered) != baseline
def test_build_key_tracks_direct_source_contents(tmp_path):
source = _write(tmp_path / "a.cu", "// v1")
before = _build_key(cuda_files=(str(source),))
source.write_text("// v2")
cache.clear_digest_cache()
assert _build_key(cuda_files=(str(source),)) != before
def test_no_unordered_container_reaches_the_key(monkeypatch):
"""Nothing hashed into a key may iterate in `PYTHONHASHSEED` order.
A set or dict among the hashed parts would make the same tree key
differently in two processes: no error, no wrong result, the cache simply
never hits again. Verified end-to-end by running the key computation under
several hash seeds; this pins the property cheaply.
"""
recorded = []
original = cache._hash_parts
monkeypatch.setattr(
cache,
"_hash_parts",
lambda parts: recorded.append(list(parts)) or original(recorded[-1]),
)
_build_key()
def walk(value, path="parts"):
assert not isinstance(value, (set, frozenset, dict)), (
f"unordered container at {path}: {type(value).__name__}"
)
if isinstance(value, (list, tuple)):
for index, item in enumerate(value):
walk(item, f"{path}[{index}]")
walk(recorded)
def test_build_key_is_independent_of_install_location():
"""Absolute paths are anchor-normalized, which is what lets a second clone
of the same tree reuse the first clone's builds."""
text = ninja.generate(_spec())
assert str(KERNEL_PATH) not in cache._normalize_text(text)
# --------------------------------------------------------------------------
# deps_key — a leaf that reproduces its own name
# --------------------------------------------------------------------------
def test_leaf_is_found_when_nothing_changed(tmp_path):
dep = _write(tmp_path / "dep.h", "// v1")
leaf = _publish_leaf(tmp_path, [dep])
assert cache.find_prebuilt(scope=tmp_path, module_name="m") == leaf / "m.so"
def test_leaf_is_skipped_when_a_dependency_changed(tmp_path):
dep = _write(tmp_path / "dep.h", "// v1")
_publish_leaf(tmp_path, [dep])
dep.write_text("// v2")
cache.clear_digest_cache()
assert cache.find_prebuilt(scope=tmp_path, module_name="m") is None
def test_leaf_survives_an_mtime_only_change(tmp_path):
dep = _write(tmp_path / "dep.h", "// stable")
leaf = _publish_leaf(tmp_path, [dep])
os.utime(dep, (0, 0))
cache.clear_digest_cache()
assert cache.find_prebuilt(scope=tmp_path, module_name="m") == leaf / "m.so"
def test_leaf_is_skipped_when_a_dependency_disappeared(tmp_path):
dep = _write(tmp_path / "dep.h", "// here")
_publish_leaf(tmp_path, [dep])
dep.unlink()
cache.clear_digest_cache()
assert cache.find_prebuilt(scope=tmp_path, module_name="m") is None
def test_a_leaf_that_does_not_match_its_own_name_is_skipped(tmp_path):
"""The recorded list is verified, not trusted.
This is what replaces a shared manifest plus a format-version check: a
truncated, tampered, or foreign list simply fails to reproduce the leaf's
own name, so no schema field has to be believed.
"""
dep = _write(tmp_path / "dep.h", "// v1")
leaf = _publish_leaf(tmp_path, [dep])
other = _write(tmp_path / "other.h", "// x")
(leaf / cache._DEPS_FILE).write_bytes(msgspec.json.encode(_entries([dep, other])))
assert cache.find_prebuilt(scope=tmp_path, module_name="m") is None
@pytest.mark.parametrize(
"payload",
[b"", b"not json", b'[["kernels", "a.h"]]'],
ids=["empty", "garbage", "wrong-shape"],
)
def test_an_unreadable_dependency_list_is_a_miss(tmp_path, payload):
dep = _write(tmp_path / "dep.h", "// v1")
leaf = _publish_leaf(tmp_path, [dep])
(leaf / cache._DEPS_FILE).write_bytes(payload)
assert cache.find_prebuilt(scope=tmp_path, module_name="m") is None
def test_a_foreign_leaf_does_not_block_a_valid_one(tmp_path):
"""A leaf naming a file that does not exist here is skipped, not fatal.
A shared, merged manifest could not do this: one unresolvable entry would
make every lookup fail permanently on this machine.
"""
dep = _write(tmp_path / "dep.h", "// v1")
good = _publish_leaf(tmp_path, [dep])
foreign = tmp_path / f"{cache._DEPS_KEY_PREFIX}{'0' * 16}"
foreign.mkdir()
(foreign / cache._DEPS_FILE).write_bytes(
msgspec.json.encode(
[cache._DepEntry(root="sys", relpath="include/c++/99/absent.h", digest="x")]
)
)
(foreign / "m.so").write_bytes(b"")
os.utime(foreign, None) # make the foreign leaf the newest
assert cache.find_prebuilt(scope=tmp_path, module_name="m") == good / "m.so"
def test_a_hit_survives_an_unwritable_cache(tmp_path, monkeypatch):
"""Touching the leaf is bookkeeping; it must never turn a hit into a crash.
The cache root can be a read-only mount, and a prune racing the lookup
leaves nothing to touch -- either way `os.utime` raises, and before this it
escaped `find_prebuilt` and took `load_jit` down on an otherwise good hit.
"""
header = _write(tmp_path / "a.h", "x")
scope = tmp_path / "scope"
_publish_leaf(scope, [header], module_name="m")
def deny(*args, **kwargs):
raise PermissionError("read-only file system")
monkeypatch.setattr(cache.os, "utime", deny)
assert cache.find_prebuilt(scope=scope, module_name="m") is not None
def test_missing_library_is_not_a_hit(tmp_path):
dep = _write(tmp_path / "dep.h", "// v1")
leaf = _publish_leaf(tmp_path, [dep])
(leaf / "m.so").unlink()
assert cache.find_prebuilt(scope=tmp_path, module_name="m") is None
# --------------------------------------------------------------------------
# Commit-side guards
# --------------------------------------------------------------------------
def test_empty_dependency_scan_is_rejected():
"""An empty scan must not be recorded: it would narrow the checked set to
nothing, the one shape of bad recorded data that could cause reuse."""
assert not cache._covers_direct_sources(entries=[], direct_sources=["/x/a.cuh"])
def test_scan_missing_a_direct_source_is_rejected(tmp_path):
other = _write(tmp_path / "other.h", "// x")
entries = _entries([other])
assert not cache._covers_direct_sources(
entries=entries, direct_sources=[str(tmp_path / "a.cuh")]
)
assert cache._covers_direct_sources(entries=entries, direct_sources=[str(other)])
def test_build_directory_entries_are_dropped(tmp_path):
"""The generated translation unit is not a dependency of itself.
Its path is unstable and its contents are already a function of inputs the
build key covers, so recording it would defeat reuse across clones.
"""
build_dir = (tmp_path / "build").resolve()
generated = _write(build_dir / "cuda.cu", "// generated")
outside = _write(tmp_path / "real.h", "// real")
entries = cache._to_entries(dependencies=[generated, outside], build_dir=build_dir)
assert [entry.relpath for entry in entries] == [str(outside)]
def test_publish_loses_the_race_gracefully(tmp_path):
"""Two processes building identical content: the loser adopts the winner's leaf."""
winner = tmp_path / "leaf"
winner.mkdir()
(winner / "m.so").write_bytes(b"winner")
staging = tmp_path / "staging"
staging.mkdir()
(staging / "m.so").write_bytes(b"loser")
assert cache._publish(staging=staging, leaf=winner) == winner
assert (winner / "m.so").read_bytes() == b"winner"
def test_build_lock_excludes_a_second_holder(tmp_path):
"""One compile per module variant per node, not one per process.
Every tensor-parallel rank hits the same cold cache at the same instant.
Without exclusion each runs a full compile — measured with 8 ranks, all
eight compiled; with it, one compiled and seven took the cache. The lock
saves duplicated work only; the atomic rename is what makes publication
safe, so a missing lock is slow rather than wrong.
"""
import threading
from sglang.kernels.jit.utils.compile import loader
held, release, contender_entered = (threading.Event() for _ in range(3))
def holder():
with loader._build_lock(tmp_path):
held.set()
release.wait(5)
def contender():
with loader._build_lock(tmp_path):
contender_entered.set()
first = threading.Thread(target=holder)
first.start()
assert held.wait(5)
second = threading.Thread(target=contender)
second.start()
assert not contender_entered.wait(0.3), "entered while the lock was held"
release.set()
first.join(5)
second.join(5)
assert contender_entered.is_set(), "never entered after the lock was released"
# --------------------------------------------------------------------------
# ninja generation
# --------------------------------------------------------------------------
def test_generated_ninja_keeps_depfiles_on_disk():
"""`deps = gcc` must not be emitted.
That setting folds each depfile into ninja's binary log and deletes it,
leaving the cache with nothing to record the dependency closure from.
"""
text = ninja.generate(_spec(cuda_wrappers=(("run", "K::run"),)))
assert "deps = gcc" not in text
assert "depfile = $out.d" in text
def test_generated_ninja_asks_both_compilers_for_dependencies():
"""Both rules must write a depfile on every backend.
tvm-ffi's HIP branch declared `depfile =` while running a command that never
produced one, so ROCm builds silently carried no header dependencies.
"""
text = ninja.generate(_spec(cuda_wrappers=(("run", "K::run"),)))
compile_commands = [
line
for line in text.splitlines()
if line.startswith(" command = ") and ' -c "$in"' in line
]
assert len(compile_commands) == 2
assert all('-MD -MF "$out.d"' in line for line in compile_commands)
def test_pure_cpp_module_does_not_link_the_gpu_runtime():
"""A module with no `.cu` sources must not ask the linker for libcudart.
`ngram_corpus` is five .cpp files and no device code, and it is built on
CPU-only CI runners that have no CUDA toolkit — linking it there fails with
`cannot find -lcudart`. tvm-ffi keyed the runtime flags off the presence of
`.cu` sources for exactly this reason.
"""
cpu_only = _spec(cpp_files=("/tmp/a.cpp",), cpp_wrappers=(), header_only=False)
ldflags = next(
line
for line in ninja.generate(cpu_only).splitlines()
if line.startswith("ldflags = ")
)
assert "cudart" not in ldflags and "amdhip" not in ldflags
with_device = _spec(cuda_files=("/tmp/a.cu",), cpp_wrappers=(), header_only=False)
ldflags = next(
line
for line in ninja.generate(with_device).splitlines()
if line.startswith("ldflags = ")
)
assert "cudart" in ldflags or "amdhip" in ldflags
def test_generated_ninja_is_deterministic():
spec = _spec(cuda_wrappers=(("run", "K::run"),))
assert ninja.generate(spec) == ninja.generate(spec)
def test_header_only_module_compiles_through_a_generated_wrapper(tmp_path):
source = str(tmp_path / "kernel.cuh")
units = _spec(
cuda_files=(source,), cuda_wrappers=(("run", "K::run"),)
).translation_units()
assert [unit.filename for unit in units] == ["main.cpp", "cuda.cu"]
generated = next(unit for unit in units if unit.filename == "cuda.cu").source
assert f'#include "{source}"' in generated
assert "TVM_FFI_DLL_EXPORT_TYPED_FUNC(run, (K::run));" in generated
# The wrapper must include the header defining the macro it uses. Every
# kernel in tree happens to drag it in transitively, so dropping this would
# only break whichever future kernel does not.
assert "#include <tvm/ffi/function.h>" in generated
def test_non_header_only_module_compiles_its_sources_in_place(tmp_path):
source = str(tmp_path / "kernel.cu")
units = _spec(
cuda_files=(source,), cpp_wrappers=(), header_only=False
).translation_units()
assert [(unit.filename, unit.source, unit.is_cuda) for unit in units] == [
(source, None, True)
]
# --------------------------------------------------------------------------
# depfile parsing
# --------------------------------------------------------------------------
def test_depfile_parsing_handles_continuations_and_escaped_spaces():
text = "cuda_0.o: /a/cuda.cu \\\n /a/with\\ space.cuh \\\n /b/plain.h\n"
assert ninja._parse_depfile(text) == [
"/a/cuda.cu",
"/a/with space.cuh",
"/b/plain.h",
]
def test_depfile_parsing_ignores_a_target_with_no_prerequisites():
assert ninja._parse_depfile("a.o:\n") == []
# --------------------------------------------------------------------------
# Layout
# --------------------------------------------------------------------------
def test_layout_is_readable_and_scoped_by_build_key():
scope = cache.build_key_dir(
module_name="sgl_kernel_jit_activation_bf16_t", build_key="abc123"
)
assert scope.name == "build-abc123"
assert scope.parent.name == "sgl_kernel_jit_activation_bf16_t"
assert scope.parent.parent.name == cache._target_tag()
def test_module_name_is_derived_from_the_args():
assert _spec().module_name == "sgl_kernel_jit_activation_bf16_t"
def test_relative_sources_resolve_against_csrc():
from sglang.kernels.jit.utils.compile.spec import resolve_sources
assert resolve_sources(["elementwise/activation.cuh"]) == (
str(KERNEL_PATH / "csrc" / "elementwise" / "activation.cuh"),
)
assert resolve_sources(["/usr/include/stdio.h"]) == ("/usr/include/stdio.h",)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -1,172 +0,0 @@
from __future__ import annotations
import unittest
from typing import TYPE_CHECKING, List
import torch
from sglang.srt.entrypoints.engine import Engine
from sglang.srt.layers.sampler import Sampler, register_sampler_backend
from sglang.srt.managers.scheduler import run_scheduler_process
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.mock_model.utils import MOCK_MODEL_PATH
from sglang.test.test_utils import CustomTestCase
if TYPE_CHECKING:
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
register_cuda_ci(est_time=26, stage="base-b", runner_config="1-gpu-small")
register_amd_ci(est_time=120, stage="stage-b", runner_config="1-gpu-small-amd")
CUSTOMIZED_INFO_FIELD = "sampled_token_ids_copy"
CUSTOMIZED_INFO_SAMPLER_BACKEND = "customized_info_probe"
_INPUT_IDS = [464, 9345, 3958, 1752, 13]
_MAX_NEW_TOKENS = 17
class CustomizedInfoSampler(Sampler):
"""Sampler probe that mirrors every sampled token into customized_info.
The scheduler already appends sampled token ids to each request's output_ids.
By copying the same values into customized_info at the sampler boundary, the
test can assert that customized_info is sliced and accumulated exactly like
output_ids throughout the scheduler -> tokenizer manager -> Engine path.
"""
def forward(
self,
logits_output: LogitsProcessorOutput,
sampling_info: SamplingBatchInfo,
return_logprob: bool,
top_logprobs_nums: List[int],
token_ids_logprobs: List[List[int]],
positions: torch.Tensor,
) -> torch.Tensor:
batch_next_token_ids = super().forward(
logits_output,
sampling_info,
return_logprob,
top_logprobs_nums,
token_ids_logprobs,
positions,
)
if logits_output.customized_info is None:
logits_output.customized_info = {}
logits_output.customized_info[CUSTOMIZED_INFO_FIELD] = (
batch_next_token_ids.detach().cpu().tolist()
)
return batch_next_token_ids
def install_customized_info_sampler() -> None:
# Register before ServerArgs validation in the parent and before sampler
# construction in the scheduler subprocess.
register_sampler_backend(
CUSTOMIZED_INFO_SAMPLER_BACKEND,
CustomizedInfoSampler,
)
def run_scheduler_process_with_customized_info_sampler(*args, **kwargs):
# Engine launches the scheduler in a subprocess. Install the sampler there
# too so create_sampler() can resolve CUSTOMIZED_INFO_SAMPLER_BACKEND.
install_customized_info_sampler()
return run_scheduler_process(*args, **kwargs)
class _CustomizedInfoEngine(Engine):
run_scheduler_process_func = staticmethod(
run_scheduler_process_with_customized_info_sampler
)
class TestCustomizedInfoStreaming(CustomTestCase):
@classmethod
def setUpClass(cls):
install_customized_info_sampler()
cls.engine = _CustomizedInfoEngine(
model_path=MOCK_MODEL_PATH,
load_format="dummy",
sampling_backend=CUSTOMIZED_INFO_SAMPLER_BACKEND,
incremental_streaming_output=True,
skip_tokenizer_init=True,
disable_cuda_graph=True,
disable_radix_cache=True,
random_seed=0,
log_level="error",
mem_fraction_static=0.5,
max_total_tokens=1024,
)
@classmethod
def tearDownClass(cls):
cls.engine.shutdown()
def _sampling_params(self, *, stream_interval: int | None = None) -> dict:
sampling_params = {
"temperature": 0.0,
"max_new_tokens": _MAX_NEW_TOKENS,
"ignore_eos": True,
}
if stream_interval is not None:
sampling_params["stream_interval"] = stream_interval
return sampling_params
def _generate(self, *, stream: bool, stream_interval: int | None = None):
self.engine.flush_cache()
# skip_tokenizer_init keeps this test focused on streaming output
# handling; input_ids bypass tokenizer setup while the real Engine,
# scheduler, and tokenizer-manager response path still run.
return self.engine.generate(
input_ids=_INPUT_IDS,
sampling_params=self._sampling_params(stream_interval=stream_interval),
stream=stream,
)
def _assert_customized_info_matches_output_ids(self, output: dict):
# For streaming chunks this should compare per-chunk lists. For the
# non-streaming final response it should compare fully accumulated
# lists. Either failure means customized_info drifted from output_ids.
self.assertIn("output_ids", output)
self.assertIn("meta_info", output)
self.assertIn(CUSTOMIZED_INFO_FIELD, output["meta_info"])
self.assertEqual(
output["meta_info"][CUSTOMIZED_INFO_FIELD], output["output_ids"]
)
def test_non_streaming_returns_accumulated_customized_info(self):
output = self._generate(stream=False)
self._assert_customized_info_matches_output_ids(output)
self.assertEqual(len(output["output_ids"]), _MAX_NEW_TOKENS)
def test_incremental_streaming_returns_chunk_customized_info(self):
chunks = list(self._generate(stream=True, stream_interval=1))
self.assertEqual(len(chunks), _MAX_NEW_TOKENS)
output_ids = []
for chunk in chunks:
self._assert_customized_info_matches_output_ids(chunk)
output_ids.extend(chunk["output_ids"])
self.assertEqual(len(output_ids), _MAX_NEW_TOKENS)
def test_incremental_streaming_interval_returns_chunk_customized_info(self):
chunks = list(self._generate(stream=True, stream_interval=4))
# stream_interval should coalesce multiple scheduler token events into
# at least one multi-token Engine chunk while preserving per-chunk
# customized_info alignment.
self.assertGreater(len(chunks), 1)
self.assertTrue(any(len(chunk["output_ids"]) > 1 for chunk in chunks))
output_ids = []
for chunk in chunks:
self._assert_customized_info_matches_output_ids(chunk)
output_ids.extend(chunk["output_ids"])
self.assertEqual(len(output_ids), _MAX_NEW_TOKENS)
if __name__ == "__main__":
unittest.main()