Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@
"top_k",
"min_p",
)
BYPASS_REMOTE_PREFILL_ANNOTATION = "x-bypass-remote-prefill"


def _raise_if_conditional_disagg_bypass(request: Dict[str, Any]) -> None:
if BYPASS_REMOTE_PREFILL_ANNOTATION not in (request.get("annotations") or []):
return
raise RuntimeError(
f"Detected request annotation {BYPASS_REMOTE_PREFILL_ANNOTATION!r}, but "
"SGLang backend does not support conditional disaggregation yet. "
"Use vLLM or TensorRT-LLM for conditional disaggregation."
)


def _nvext_extra_field_requested(request: Dict[str, Any], field: str) -> bool:
Expand Down Expand Up @@ -342,6 +353,7 @@ async def generate(
RuntimeError: If no bootstrap info received from prefill worker.
"""
logging.debug(f"New Request ID: {context.id()}")
_raise_if_conditional_disagg_bypass(request)
trace_id = context.trace_id
sampling_params = self._build_sampling_params(request)
input_param = self._get_input_param(request)
Expand Down
50 changes: 44 additions & 6 deletions components/src/dynamo/trtllm/request_handlers/handler_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@

logger = logging.getLogger(__name__)

BYPASS_REMOTE_PREFILL_ANNOTATION = "x-bypass-remote-prefill"


class TRTLLMEnginePauseController:
"""Adapts TRT-LLM sleep/wake to the standard pause controller interface.
Expand Down Expand Up @@ -715,10 +717,31 @@ def _setup_disaggregated_params_for_mode(
disaggregated_params = None
epd_metadata: dict[str, Any] = {}

# Canary probe: use its pre-built disagg params (skip prefill_result decode
# and skip the mode-specific request_type overrides).
if request.get(HEALTH_CHECK_KEY) and request.get("disaggregated_params"):
return LlmDisaggregatedParams(**request["disaggregated_params"]), None, {}
use_request_disagg_params = request.get(HEALTH_CHECK_KEY) or (
self.disaggregation_mode == DisaggregationMode.DECODE
and BYPASS_REMOTE_PREFILL_ANNOTATION in (request.get("annotations") or [])
)

if (
use_request_disagg_params
and ep_disaggregated_params is not None
and BYPASS_REMOTE_PREFILL_ANNOTATION in (request.get("annotations") or [])
):
disaggregated_params = DisaggregatedParamsCodec.decode(
ep_disaggregated_params
)
disaggregated_params.request_type = "context_and_generation"
return disaggregated_params, ep_disaggregated_params, {}

# Canary probes and text-only conditional-disagg bypasses run a full
# context+generation request on a disagg-mode worker, so they use
# the pre-built params and skip the normal prefill-result handoff.
if use_request_disagg_params and request.get("disaggregated_params"):
return (
LlmDisaggregatedParams(**request["disaggregated_params"]),
ep_disaggregated_params,
{},
)
Comment thread
karen-sy marked this conversation as resolved.

# PREFILL mode: setup context_only params
if self.disaggregation_mode == DisaggregationMode.PREFILL:
Expand Down Expand Up @@ -998,6 +1021,18 @@ async def _generate_locally_impl(

# Normalize OpenAI format to TRT-LLM internal format
self._normalize_request_format(request)
bypass_remote_prefill = (
self.disaggregation_mode == DisaggregationMode.DECODE
and BYPASS_REMOTE_PREFILL_ANNOTATION in (request.get("annotations") or [])
)
if bypass_remote_prefill:
request_id = request.get("id") or request.get("request_id", "unknown-id")
logging.debug(
"DECODE: conditional-disagg bypass annotation present; "
"running request %s as AGG (prefill+decode on this worker).",
request_id,
)
request["disaggregated_params"] = {"request_type": "context_and_generation"}

# Setup disaggregated params based on PREFILL/DECODE mode
(
Expand Down Expand Up @@ -1027,6 +1062,7 @@ async def _generate_locally_impl(
if (
self.disaggregation_mode == DisaggregationMode.DECODE
and disaggregated_params is None
and not bypass_remote_prefill
):
logging.error("DECODE: disaggregated_params is None but required!")
logging.error(f"DECODE: Request keys: {list(request.keys())}")
Expand Down Expand Up @@ -1183,11 +1219,13 @@ async def _generate_locally_impl(
cache_salt=cache_salt,
)

# In disagg decode mode, wrap abort() to defer until first token
# (KV transfer complete).
# In disagg decode mode with remote prefill, wrap abort() to defer
# until the first token is received (KV transfer complete).
abort_guard = (
_DeferredAbort(generation_result)
if self.disaggregation_mode == DisaggregationMode.DECODE
and disaggregated_params is not None
and not bypass_remote_prefill
else None
)

Expand Down
24 changes: 23 additions & 1 deletion components/src/dynamo/trtllm/tests/test_trtllm_handler_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
from dynamo.trtllm.constants import DisaggregationMode
from dynamo.trtllm.health_check import TrtllmHealthCheckPayload
from dynamo.trtllm.multimodal_processor import MultimodalRequestProcessor
from dynamo.trtllm.request_handlers.handler_base import HandlerBase
from dynamo.trtllm.request_handlers.handler_base import (
BYPASS_REMOTE_PREFILL_ANNOTATION,
HandlerBase,
)

pytestmark = [
pytest.mark.unit,
Expand Down Expand Up @@ -665,6 +668,13 @@ def _make_prefill_handler(self, machine_id: int = 42) -> HandlerBase:
handler.disaggregation_mode = DisaggregationMode.PREFILL
return handler

def _make_decode_handler(self) -> HandlerBase:
config = MagicMock()
config.shutdown_event = None
handler = _ConcreteHandler(config)
handler.disaggregation_mode = DisaggregationMode.DECODE
return handler

def test_disagg_request_id_populated_in_prefill_mode(self):
"""When mode is PREFILL and no ep_disaggregated_params, disagg_request_id is set."""
handler = self._make_prefill_handler()
Expand Down Expand Up @@ -730,6 +740,18 @@ def test_different_machine_ids_produce_different_id_ranges(self):
)
assert params_a.disagg_request_id != params_b.disagg_request_id

def test_decode_conditional_bypass_uses_request_disagg_params(self):
"""Conditional-disagg bypass runs full context+generation on decode."""
handler = self._make_decode_handler()
params, _, _ = handler._setup_disaggregated_params_for_mode(
request={
"annotations": [BYPASS_REMOTE_PREFILL_ANNOTATION],
"disaggregated_params": {"request_type": "context_and_generation"},
},
ep_disaggregated_params=None,
)
assert params.request_type == "context_and_generation"


class TestHealthCheckPriority:
"""Verify generate_locally forwards the correct priority to generate_async.
Expand Down
25 changes: 24 additions & 1 deletion components/src/dynamo/vllm/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@
configure_dynamo_logging()
logger = logging.getLogger(__name__)

# Marker set by the Rust conditional-disagg bypass path. When present on a
# DECODE-mode worker, the request runs as local prefill+decode instead of
# expecting KV-transfer metadata from an upstream prefill worker.
BYPASS_REMOTE_PREFILL_ANNOTATION = "x-bypass-remote-prefill"

_GENERATE_REASONING_SUPPORT_CACHE_ATTR = "_dynamo_generate_reasoning_support"
_DELTA_REQUEST_OUTPUT_KIND = RequestOutputKind.DELTA
_RL_INIT_WEIGHTS_TIMEOUT_ENV = "DYN_RL_INIT_WEIGHTS_TIMEOUT_S"
Expand Down Expand Up @@ -3035,6 +3040,15 @@ async def _generate_token_mode(self, request, context, request_id):

mode = cast(DisaggregationMode, self.config.disaggregation_mode)
is_decode_only = mode == DisaggregationMode.DECODE
if is_decode_only and BYPASS_REMOTE_PREFILL_ANNOTATION in (
request.get("annotations") or []
):
logger.debug(
"DECODE: conditional-disagg bypass annotation present; "
"running request as AGG (prefill+decode on this worker)."
)
is_decode_only = False
mode = DisaggregationMode.AGGREGATED
has_mm_data = request.get("multi_modal_data") is not None
custom_prompt: EmbedsPrompt | TokensPrompt | None = None

Expand Down Expand Up @@ -3244,11 +3258,20 @@ async def _generate_text_mode(self, request, context, request_id):

trace_headers = context.trace_headers()

is_decode_only = self.config.disaggregation_mode == DisaggregationMode.DECODE
if is_decode_only and BYPASS_REMOTE_PREFILL_ANNOTATION in (
request.get("annotations") or []
):
logger.debug(
"DECODE: conditional-disagg bypass annotation present; "
"running text-mode request as AGG (prefill+decode on this worker)."
)
is_decode_only = False

# Mirror _generate_token_mode: in disagg decode mode route aborts through
# the per-request deferred guard so engine_client.abort() never fires in
# the unsafe pre-first-token window, and the admin abort_request route can
# reach this request via self._deferred_aborts.
is_decode_only = self.config.disaggregation_mode == DisaggregationMode.DECODE
async with _deferred_abort_guard(
self.engine_client,
request_id,
Expand Down
106 changes: 106 additions & 0 deletions components/src/dynamo/vllm/tests/test_vllm_worker_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import asyncio
import base64
import json
from contextlib import asynccontextmanager
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

Expand Down Expand Up @@ -796,6 +797,111 @@ async def test_aggregated_mode_calls_extract_multimodal_data(self):
assert len(chunks) == 1
assert chunks[0]["status"] == "error"

async def test_decode_only_bypass_annotation_runs_as_agg(self):
"""Decode worker with conditional-disagg bypass annotation runs as AGG."""
handler = _make_decode_handler(
model="Qwen/Qwen3-VL-2B-Instruct",
disaggregation_mode="DECODE",
)
handler._multimodal_request_processor.extract_multimodal_data = AsyncMock(
return_value=None
)
handler._build_prompt_from_request = MagicMock(
return_value=(None, {"status": "error", "message": "test stop"})
)

request = {
"token_ids": [1, 2, 3],
"multi_modal_data": {"image_url": [{"Url": "http://img.png"}]},
"sampling_options": {},
"stop_conditions": {},
"output_options": {},
"annotations": [mod.BYPASS_REMOTE_PREFILL_ANNOTATION],
}
context = MagicMock()

chunks = []
async for chunk in handler._generate_token_mode(request, context, "req-1"):
chunks.append(chunk)

handler._multimodal_request_processor.extract_multimodal_data.assert_awaited_once()
assert len(chunks) == 1
assert chunks[0]["message"] == "test stop"

async def test_decode_only_bypass_annotation_text_only_does_not_require_prefill_kv_params(
self,
):
"""Text-only bypass does not require incoming prefill KV params."""
handler = _make_decode_handler(disaggregation_mode="DECODE")
handler._build_prompt_from_request = MagicMock(
return_value=(None, {"status": "error", "message": "stop"})
)

request = {
"token_ids": [1, 2, 3],
"sampling_options": {},
"stop_conditions": {},
"output_options": {},
"annotations": [mod.BYPASS_REMOTE_PREFILL_ANNOTATION],
}
context = MagicMock()

chunks = []
async for chunk in handler._generate_token_mode(request, context, "req-1"):
chunks.append(chunk)

assert len(chunks) == 1
assert chunks[0]["message"] == "stop"

async def test_decode_only_bypass_annotation_text_mode_runs_as_agg(self):
"""Text-mode conditional-disagg bypass is not treated as decode-only."""
handler = _make_decode_handler(disaggregation_mode="DECODE")
handler.input_param_manager = MagicMock()
handler.input_param_manager.get_input_param.return_value = [1, 2, 3]
handler.engine_client = MagicMock()
handler.default_sampling_params = {}

async def _empty_generate(*args, **kwargs):
if False:
yield None

handler.engine_client.generate = _empty_generate

killed_future = asyncio.get_event_loop().create_future()
killed_future.set_result(None)
context = MagicMock()
context.async_killed_or_stopped.return_value = killed_future
context.trace_headers.return_value = {}

decode_only_values = []

@asynccontextmanager
async def _capture_guard(
engine_client,
request_id,
is_decode_only,
registry=None,
on_engine_dead=None,
):
decode_only_values.append(is_decode_only)
yield None

request = {
"token_ids": [1, 2, 3],
"sampling_options": {},
"stop_conditions": {},
"output_options": {},
"annotations": [mod.BYPASS_REMOTE_PREFILL_ANNOTATION],
}

with patch.object(mod, "_deferred_abort_guard", _capture_guard):
chunks = []
async for chunk in handler._generate_text_mode(request, context, "req-1"):
chunks.append(chunk)

assert chunks == []
assert decode_only_values == [False]

@pytest.mark.parametrize(
"mm_processor_kwargs",
[None, {"use_audio_in_video": True}],
Expand Down
Loading