Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6e7df97
fix(sglang): stop sending min_new_tokens to a tokenizer-free engine
glamr-agent Sep 3, 2026
651ba2c
chore: remove factory working notes
glamr-agent Sep 3, 2026
9b469d7
fix(sglang): clear stop strings on the held-open sidecar path
glamr-agent Sep 3, 2026
a768779
test(sglang): cover a floor below the ceiling and the stop after it
glamr-agent Sep 3, 2026
e815d20
style(sglang): drop narrating comments from the sidecar lowering tests
glamr-agent Sep 3, 2026
bf1c9a8
fix(sglang): gate the min_tokens floor on the engine's tokenizer state
glamr-agent Sep 11, 2026
d5cbb73
Merge remote-tracking branch 'upstream/main' into fix/ff6f2ac3119d
glamr-agent Sep 11, 2026
3427ee0
fix(sglang): route native min-token requests through decoder
glamr-agent Sep 11, 2026
32694da
test(llm): pass the jail seed argument to Decoder::new in the floor test
glamr-agent Sep 11, 2026
06744dd
Merge branch main into fix/ff6f2ac3119d
glamr-agent Sep 11, 2026
b482082
fix(sglang): abort multimodal requests before first chunk
glamr-agent Sep 12, 2026
2492a76
fix(sglang): narrow tokenizer-free min token fallback
jain-ria Sep 15, 2026
3c8797c
ci: supply required framework for Triton tests
glamr-agent Sep 15, 2026
e2941f9
fix(sglang): honor minimum boundary and abort all choices
glamr-agent Sep 15, 2026
f0ac9f3
ci: declare networkx for test collection
glamr-agent Sep 16, 2026
7ca35ce
test(sglang): provide stop state in shutdown context stub
glamr-agent Sep 16, 2026
c66ce8a
fix(sglang): retain context logging after request aborts
glamr-agent Sep 16, 2026
21638df
fix(sglang): abort every unfinished text-stream choice
glamr-agent Sep 16, 2026
c114bb4
Merge branch 'main' into fix/ff6f2ac3119d
MatejKosec Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,7 @@ jobs:
uses: $/.github/workflows/shared-test.yml
with:
test_suite_name: triton
framework: triton
test_type: CPU Test
amd_runner: prod-tester-amd-v2
target_tag_plain: ${{ needs.triton-build.outputs.target_tag_plain }}
Expand Down
47 changes: 28 additions & 19 deletions components/src/dynamo/sglang/request_handlers/handler_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1234,15 +1234,30 @@ def _get_bootstrap_info(engine: sgl.Engine) -> Tuple[str, int]:

return bootstrap_host, bootstrap_port

def _abort_requests(self, request_ids: set[str], context: Context) -> None:
if not request_ids:
return
if not getattr(self.engine, "tokenizer_manager", None):
logging.error("SGLang tokenizer_manager not found for abort requests")
return
for request_id in request_ids:
self.engine.tokenizer_manager.abort_request(rid=request_id, abort_all=False)
request_ids.clear()
logging.info("Aborted Request ID: %s", context.id())

async def _handle_cancellation(
self, request_id_future: asyncio.Future, context: Context
self,
request_id_future: asyncio.Future,
context: Context,
request_ids: set[str] | None = None,
):
"""Background task to handle cancellation and shutdown by monitoring both signals.

Args:
request_id_future: Future that will be set with the SGLang request ID
when the first response arrives.
context: Context object for cancellation handling.
request_ids: Live set of unfinished parallel-choice IDs, when available.

Raises:
EngineShutdown: If shutdown event was triggered.
Expand Down Expand Up @@ -1288,22 +1303,9 @@ async def _handle_cancellation(
f"Cancellation or shutdown signal received for SGLang Request ID {sglang_request_id}, Context: {context.id()}"
)

# Call abort_request on the tokenizer_manager through the engine
if (
hasattr(self.engine, "tokenizer_manager")
and self.engine.tokenizer_manager
):
logging.info(
f"Calling SGLang abort_request for Request ID {sglang_request_id}"
)
self.engine.tokenizer_manager.abort_request(
rid=sglang_request_id, abort_all=False
)
logging.info(f"Aborted Request ID: {context.id()}")
else:
logging.error(
f"SGLang tokenizer_manager not found for abort request: {context.id()}"
)
self._abort_requests(
request_ids if request_ids is not None else {sglang_request_id}, context
)

# Check which event triggered and raise EngineShutdown if shutdown
if shutdown_task and shutdown_task in done:
Expand Down Expand Up @@ -1333,7 +1335,10 @@ async def _handle_cancellation(

@asynccontextmanager
async def _cancellation_monitor(
self, request_id_future: asyncio.Future, context: Context
self,
request_id_future: asyncio.Future,
context: Context,
request_ids: set[str] | None = None,
) -> AsyncGenerator[asyncio.Task, None]:
"""
Context manager for monitoring request cancellation and shutdown.
Expand All @@ -1346,6 +1351,7 @@ async def _cancellation_monitor(
request_id_future: Future that will be set with the SGLang request ID
when the first response arrives.
context: Context object for cancellation handling
request_ids: Live set of unfinished parallel-choice IDs to abort on exit.

Yields:
asyncio.Task: The cancellation monitoring task being managed
Expand All @@ -1354,12 +1360,15 @@ async def _cancellation_monitor(

# Start the cancellation monitoring task
cancellation_task = asyncio.create_task(
self._handle_cancellation(request_id_future, context)
self._handle_cancellation(request_id_future, context, request_ids)
)

try:
yield cancellation_task
finally:
# Stream closure can beat the monitor's cancellation wake-up.
if request_ids is not None:
self._abort_requests(request_ids, context)
# Clean up the background cancellation task
request_id = "unknown"
if request_id_future.done() and not request_id_future.cancelled():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,12 +361,25 @@ def _build_sampling_params(self, request: Dict[str, Any]) -> Dict[str, Any]:
_plain = stop_conditions.get("stop_token_ids") or []
_merged = list(set(_hidden).union(_plain))
stop_token_ids = _merged if _merged else None
# A tokenizer-free SGLang rejects positive min_new_tokens. Let
# Dynamo's decoder enforce the floor and keep the engine running so
# it cannot stop before Dynamo reaches it.
min_tokens = stop_conditions.get("min_tokens")
hold_engine_open = bool(
min_tokens
and min_tokens > 0
and self.config.server_args.skip_tokenizer_init
)
if hold_engine_open:
stop_token_ids = None

param_mapping = {
"n": sampling_opts.get("n"),
"max_new_tokens": stop_conditions.get("max_tokens"),
Comment thread
glamr-agent marked this conversation as resolved.
Resolved
"min_new_tokens": stop_conditions.get("min_tokens"),
"ignore_eos": stop_conditions.get("ignore_eos"),
"min_new_tokens": None if hold_engine_open else min_tokens,
Comment thread
glamr-agent marked this conversation as resolved.
Resolved
"ignore_eos": True
if hold_engine_open
else stop_conditions.get("ignore_eos"),
"stop_token_ids": stop_token_ids,
**_sampling_option_params(sampling_opts),
**self._get_guided_decoding_params(
Expand Down Expand Up @@ -694,16 +707,23 @@ async def _process_token_stream(
"""
# Use Future pattern for request ID - will be set when first response arrives
request_id_future: asyncio.Future[str] = asyncio.Future()
request_ids: set[str] = set()
first_output_seen = False
async with self._cancellation_monitor(request_id_future, context):
async with self._cancellation_monitor(request_id_future, context, request_ids):
async for res in stream_source:
meta_info = res.get("meta_info", {})
# Extract SGLang request ID from the first response and set the future
if not request_id_future.done():
sglang_request_id = meta_info.get("id")
if sglang_request_id:
sglang_request_id = meta_info.get("id")
if sglang_request_id:
request_ids.add(sglang_request_id)
if not request_id_future.done():
request_id_future.set_result(sglang_request_id)
logging.debug(f"New SGLang Request ID: {sglang_request_id}")
if meta_info.get("finish_reason"):
request_ids.discard(sglang_request_id)
if context.is_stopped():
# A choice's first chunk can arrive after the monitor fired.
self._abort_requests(request_ids, context)
continue

# Check cancellation before yielding to allow proper cleanup.
# This lets SGLang proceed to the second token generation, which will
Expand Down Expand Up @@ -832,16 +852,22 @@ async def _process_text_stream(

# Use Future pattern for request ID - will be set when first response arrives
request_id_future: asyncio.Future[str] = asyncio.Future()
request_ids: set[str] = set()
first_output_seen = False
async with self._cancellation_monitor(request_id_future, context):
async with self._cancellation_monitor(request_id_future, context, request_ids):
async for res in stream_source:
meta_info = res.get("meta_info", {})
# Extract SGLang request ID from the first response and set the future
if not request_id_future.done():
sglang_request_id = meta_info.get("id")
if sglang_request_id:
sglang_request_id = meta_info.get("id")
if sglang_request_id:
request_ids.add(sglang_request_id)
if not request_id_future.done():
request_id_future.set_result(sglang_request_id)
logging.debug(f"New SGLang Request ID: {sglang_request_id}")
if meta_info.get("finish_reason"):
request_ids.discard(sglang_request_id)
if context.is_stopped():
self._abort_requests(request_ids, context)
continue

# Check cancellation before yielding to allow proper cleanup.
# This lets SGLang proceed to the second token generation, which will
Expand Down
68 changes: 57 additions & 11 deletions components/src/dynamo/sglang/tests/test_sglang_decode_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,20 @@ def test_openai_stop_sampling_params_maps_token_id_stop_array():
}


def _new_decode_handler(*, use_sglang_tokenizer: bool = False, enable_rl: bool = False):
def _new_decode_handler(
*,
use_sglang_tokenizer: bool = False,
skip_tokenizer_init: bool = False,
enable_rl: bool = False,
):
handler = DecodeWorkerHandler.__new__(DecodeWorkerHandler)
handler.shutdown_event = None
handler.use_sglang_tokenizer = use_sglang_tokenizer
handler.config = SimpleNamespace(
server_args=SimpleNamespace(served_model_name="test-model"),
server_args=SimpleNamespace(
served_model_name="test-model",
skip_tokenizer_init=skip_tokenizer_init,
),
dynamo_args=SimpleNamespace(enable_rl=enable_rl),
)
handler._first_token_source = None
Expand All @@ -268,7 +276,7 @@ async def test_shutdown_abort_chunk_raises_engine_shutdown(processor_name):
handler = _new_decode_handler()
handler.shutdown_event = asyncio.Event()
handler.shutdown_event.set()
context = SimpleNamespace(id=lambda: "request-id")
context = SimpleNamespace(id=lambda: "request-id", is_stopped=lambda: False)

async def stream():
yield {
Expand Down Expand Up @@ -813,23 +821,55 @@ def test_guided_decoding_params_are_accepted_by_sglang(guided_decoding):
SamplingParams(**sampling_params)


def test_build_sampling_params_maps_min_tokens_for_token_requests():
handler = _new_decode_handler(use_sglang_tokenizer=False)
def test_build_sampling_params_holds_tokenizer_free_engine_open_for_min_tokens():
handler = _new_decode_handler(
use_sglang_tokenizer=False,
skip_tokenizer_init=True,
)

sampling_params = handler._build_sampling_params(
{
"sampling_options": {},
"stop_conditions": {
"max_tokens": 64,
"min_tokens": 64,
"ignore_eos": True,
"min_tokens": 8,
"stop_token_ids": [7],
"stop_token_ids_hidden": [151643],
},
}
)

assert sampling_params["min_new_tokens"] == 64
assert sampling_params["max_new_tokens"] == 64
assert "min_new_tokens" not in sampling_params
assert "stop_token_ids" not in sampling_params
assert sampling_params["ignore_eos"] is True
assert sampling_params["max_new_tokens"] == 64
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

from sglang.srt.sampling.sampling_params import SamplingParams

SamplingParams(**sampling_params).normalize(tokenizer=None)


def test_build_sampling_params_forwards_min_tokens_when_engine_has_tokenizer():
handler = _new_decode_handler(
use_sglang_tokenizer=False,
skip_tokenizer_init=False,
)

sampling_params = handler._build_sampling_params(
{
"sampling_options": {},
"stop_conditions": {
"max_tokens": 64,
"min_tokens": 8,
"stop_token_ids": [7],
},
}
)

assert sampling_params["min_new_tokens"] == 8
assert sampling_params["stop_token_ids"] == [7]
assert "ignore_eos" not in sampling_params
assert sampling_params["max_new_tokens"] == 64


def test_build_sampling_params_maps_min_tokens_for_sglang_tokenizer_requests():
Expand All @@ -844,7 +884,10 @@ def test_build_sampling_params_maps_min_tokens_for_sglang_tokenizer_requests():


def test_build_sampling_params_omits_min_new_tokens_when_min_tokens_absent():
handler = _new_decode_handler(use_sglang_tokenizer=False)
handler = _new_decode_handler(
use_sglang_tokenizer=False,
skip_tokenizer_init=True,
)

sampling_params = handler._build_sampling_params(
{"sampling_options": {}, "stop_conditions": {"max_tokens": 8}}
Expand All @@ -854,7 +897,10 @@ def test_build_sampling_params_omits_min_new_tokens_when_min_tokens_absent():


def test_build_sampling_params_forwards_explicit_zero_min_tokens():
handler = _new_decode_handler(use_sglang_tokenizer=False)
handler = _new_decode_handler(
use_sglang_tokenizer=False,
skip_tokenizer_init=True,
)

sampling_params = handler._build_sampling_params(
{"sampling_options": {}, "stop_conditions": {"max_tokens": 8, "min_tokens": 0}}
Expand Down
2 changes: 2 additions & 0 deletions container/deps/requirements.test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ mistral-common>=1.11.2
mypy==1.18.2
# For NATS object store verification in router tests
nats-py==2.12.0
# Router benchmark tests import graph synthesis during whole-tree collection.
networkx
openai==2.32.0 # for Fault Tolerance migration tests
# Protobuf definitions the OTLP parity receiver decodes; see grpcio above.
# Version-matched to the opentelemetry-* pins in requirements.common.txt.
Expand Down
Loading
Loading