Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions components/src/dynamo/frontend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
from typing import TYPE_CHECKING, Any, Optional

import uvloop
from packaging.version import Version

from dynamo.common.config_dump import dump_config
from dynamo.common.configuration.groups.router_args import build_router_config
Expand Down Expand Up @@ -303,10 +302,13 @@ def parse_args() -> tuple[FrontendConfig, Optional[Namespace], Optional[Namespac
try:
from vllm.engine.arg_utils import AsyncEngineArgs

if Version(importlib.metadata.version("vllm")).release >= (0, 29):
try:
from vllm.entrypoints.common.cli_args import FrontendArgs
except ModuleNotFoundError as exc:
if exc.name != "vllm.entrypoints.common.cli_args":
raise
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
# vLLM 0.32 (CPU and XPU images); remove once they ship 0.33.
from vllm.entrypoints.launchers.cli_args import FrontendArgs
else:
from vllm.entrypoints.openai.cli_args import FrontendArgs
except ModuleNotFoundError:
logger.exception("Flag '--chat-processor vllm' requires vllm be installed.")
sys.exit(1)
Expand Down
42 changes: 41 additions & 1 deletion components/src/dynamo/vllm/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
import socket
from typing import Any, Dict, Optional

from vllm.config import SpeculativeConfig
from vllm.distributed.kv_events import KVEventsConfig
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.engine.arg_utils import (
AsyncEngineArgs,
is_cloud_storage,
maybe_override_with_speculators,
)

try:
from vllm.utils import FlexibleArgumentParser
Expand Down Expand Up @@ -394,6 +399,8 @@ def update_engine_config_with_dynamo(
kv_cfg = create_kv_events_config(engine_config)
defaults["kv_events_config"] = kv_cfg
dynamo_config.use_kv_events = kv_cfg is not None and kv_cfg.enable_kv_cache_events
if dynamo_config.use_kv_events:
_keep_legacy_block_hashes(engine_config)

logger.info(
f"Using kv_events_config for publishing vLLM kv events over zmq: {kv_cfg} "
Expand Down Expand Up @@ -609,6 +616,39 @@ def create_kv_events_config(
return None


def _keep_legacy_block_hashes(engine_config: AsyncEngineArgs) -> None:
"""Default vLLM's EAGLE/MTP lookahead block hashes off for KV event publishers:
Dynamo's KV indexers hash only each block's own tokens."""
# vLLM 0.32 (CPU and XPU images); remove once they ship 0.33.
if "disable_lookahead_block_hashes" not in SpeculativeConfig.__dataclass_fields__:
return
spec = engine_config.speculative_config
if (
spec is None
and engine_config.spec_method is None
and not _is_speculators_checkpoint(engine_config)
):
return
engine_config.speculative_config = {
"disable_lookahead_block_hashes": True,
**(spec or {}),
}


def _is_speculators_checkpoint(engine_config: AsyncEngineArgs) -> bool:
"""Whether vLLM will derive a speculative config from the model checkpoint."""
if is_cloud_storage(engine_config.model):
return False
_, _, spec = maybe_override_with_speculators(
model=engine_config.model,
tokenizer=engine_config.tokenizer,
trust_remote_code=engine_config.trust_remote_code,
revision=engine_config.revision,
hf_token=engine_config.hf_token,
)
return spec is not None


def _set_state_agent_topic(
engine_config: AsyncEngineArgs, events: KVEventsConfig
) -> None:
Expand Down
3 changes: 2 additions & 1 deletion components/src/dynamo/vllm/embedding_worker_processes.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ def _client_listeners(
_CHILD_ROLE = "child"
_RPC_BASE_PATH_ENV = "VLLM_RPC_BASE_PATH"
# Re-check the launch shape when the supported pin moves.
_VERIFIED_VLLM_VERSIONS = ("0.32.0",)
# 0.32.0: vLLM 0.32 (CPU and XPU images); remove once they ship 0.33.
_VERIFIED_VLLM_VERSIONS = ("0.32.0", "0.33.0")


def is_embedding_process_child() -> bool:
Expand Down
91 changes: 76 additions & 15 deletions components/src/dynamo/vllm/tests/test_vllm_kv_events_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,15 @@
"""

import importlib
import json
from types import SimpleNamespace

import pytest
from packaging.version import Version

from dynamo.vllm.args import update_engine_config_with_dynamo
from dynamo.vllm.constants import DisaggregationMode

# Import vllm first to ensure it's properly loaded before accessing submodules.
# This works around potential issues with pytest's import machinery.
_vllm = importlib.import_module("vllm")
Expand Down Expand Up @@ -86,10 +91,11 @@ def test_block_stored_fields(self):
12. locality (per-tier storage locality; optional for older vLLM)
13. ownership (secondary offloading tier; added in vLLM 0.29)
14. session_id (request session; added in vLLM 0.30)
15. lookahead_token_ids (EAGLE/MTP lookahead hashes; added in vLLM 0.33)

If vLLM adds/removes/reorders fields, this test will fail.
"""
expected_fields = [
expected_fields = (
"block_hashes",
"parent_block_hash",
"token_ids",
Expand All @@ -98,20 +104,14 @@ def test_block_stored_fields(self):
"medium",
"lora_name",
"extra_keys",
]
if _has_group_idx(BlockStored):
expected_fields.append("group_idx")
if _has_kv_cache_spec_kind(BlockStored):
expected_fields.append("kv_cache_spec_kind")
if _has_kv_cache_spec_sliding_window(BlockStored):
expected_fields.append("kv_cache_spec_sliding_window")
if _has_locality(BlockStored):
expected_fields.append("locality")
if Version(_vllm.__version__).release >= (0, 29):
expected_fields.append("ownership")
if Version(_vllm.__version__).release >= (0, 30):
expected_fields.append("session_id")
expected_fields = tuple(expected_fields)
"group_idx",
"kv_cache_spec_kind",
"kv_cache_spec_sliding_window",
"locality",
"ownership",
"session_id",
"lookahead_token_ids",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
)

actual_fields = BlockStored.__struct_fields__
assert actual_fields == expected_fields, (
Expand Down Expand Up @@ -318,3 +318,64 @@ def test_block_removed_serialization_format(self):
), "kv_cache_spec_sliding_window has wrong value"
if _has_locality(BlockRemoved):
assert decoded["locality"] == "REMOTE"


_SPECULATORS_CHECKPOINT_CONFIG = {
"speculators_model_type": "eagle3",
"speculators_config": {
"proposal_methods": [{"speculative_tokens": 1}],
"verifier": {"name_or_path": "Qwen/Qwen3-0.6B"},
},
"transformer_layer_config": {},
}


@pytest.mark.parametrize(
("checkpoint_config", "speculative_config"),
[
(None, {"method": "eagle3", "num_speculative_tokens": 1}),
(_SPECULATORS_CHECKPOINT_CONFIG, None),
],
ids=["speculative-config", "speculators-checkpoint"],
)
def test_kv_event_publisher_keeps_legacy_block_hashes(
tmp_path, checkpoint_config, speculative_config
):
"""Dynamo's KV indexers hash each block's own tokens, so a worker publishing
KV events must not default to vLLM's EAGLE/MTP lookahead block hashes,
including those vLLM derives from a speculators checkpoint."""
if checkpoint_config is not None:
(tmp_path / "config.json").write_text(json.dumps(checkpoint_config))

dynamo_config = SimpleNamespace(
disaggregation_mode=DisaggregationMode.AGGREGATED,
use_kv_events=False,
embedding_worker=False,
classify_worker=False,
headless=False,
enable_multimodal=False,
fpm_trace=False,
benchmark_mode=None,
)
# AsyncEngineArgs would resolve the model from Hugging Face on construction.
engine_args = SimpleNamespace(
model=str(tmp_path),
tokenizer=None,
trust_remote_code=False,
revision=None,
hf_token=None,
enable_prefix_caching=True,
block_size=16,
kv_transfer_config=None,
spec_method=None,
speculative_config=speculative_config,
kv_events_config=_kv_events.KVEventsConfig(enable_kv_cache_events=True),
)

update_engine_config_with_dynamo(dynamo_config, engine_args)

assert dynamo_config.use_kv_events
assert engine_args.speculative_config == {
"disable_lookahead_block_hashes": True,
**(speculative_config or {}),
}
2 changes: 2 additions & 0 deletions components/src/dynamo/vllm/tests/test_vllm_prompt_embeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class MockHandler:
handler.model_config = Mock(enable_prompt_embeds=True)
handler.model_config.dtype = torch.float32
handler.model_config.get_hidden_size.return_value = 4096
# A non-OPT text config: vLLM checks word_embed_proj_dim before hidden_size.
handler.model_config.hf_text_config = Mock(spec=[])
handler._decode_prompt_embeds = BaseWorkerHandler._decode_prompt_embeds.__get__( # type: ignore
handler
)
Expand Down
3 changes: 3 additions & 0 deletions components/src/dynamo/vllm/tests/test_vllm_renderer_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ def test_engine_core_struct_contract(self):
"new_sampling_mask",
"spec_decode_metrics",
"prompt_token_id_logprobs",
"error",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
)

if EngineCoreRequest.__module__ == "vllm_omni.engine":
Expand All @@ -417,6 +418,8 @@ def test_engine_core_struct_contract(self):
"new_prompt_len_snapshot",
"num_generation_tokens",
"is_coalesced",
"stage_payload",
"stage_work",
)

actual_request_fields = EngineCoreRequest.__struct_fields__
Expand Down
8 changes: 6 additions & 2 deletions container/context.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,12 @@ vllm:
base_image: nvcr.io/nvidia/cuda-dl-base
runtime_image: vllm/vllm-openai
base_image_tag: 25.11-cuda13.0-devel-ubuntu24.04
runtime_image_tag: v0.32.0-ubuntu2404
vllm_omni_ref: "v0.32.0rc1"
runtime_image_tag: v0.33.0
# Unreleased vllm-omni#9648 head for vLLM 0.33; replace with the Omni release.
vllm_omni_ref: "0.33.0.dev0+gfe886bbb379396a2947c99ae041f7c35006c1900"
Comment thread
JulienDarve marked this conversation as resolved.
Resolved
# Omni caps Transformers below 5.20; the vLLM 0.33 image ships 5.20.2.
transformers_version: "5.19.0"
tokenizers_version: "0.23.3"
# Baseline is vllm-openai's TRUE base, nvidia/cuda:13.0.2-base-ubuntu24.04
# (Docker Hub), NOT a self-baseline of the third-party vllm/vllm-openai image.
# vllm-openai is built FROM nvidia/cuda, so this attributes everything vLLM
Expand Down
12 changes: 10 additions & 2 deletions container/deps/vllm/install_vllm_omni.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ VLLM_OMNI_PROTECTED_OVERRIDES_FILE="${VLLM_OMNI_PROTECTED_OVERRIDES_FILE:-/tmp/v
PROTECTED_CONSTRAINTS="$(mktemp /tmp/vllm-openai-protected.XXXXXX.txt)"
PROTECTED_OVERRIDES="$(mktemp /tmp/vllm-openai-overrides.XXXXXX.txt)"
VLLM_OMNI_VERSION="${VLLM_OMNI_REF#v}"
VLLM_OMNI_REQUIREMENT="vllm-omni==${VLLM_OMNI_VERSION}"

# An unreleased commit, written as <version>+g<sha>, builds from GitHub's source
# archive (the image has no git) under that version.
if [[ "${VLLM_OMNI_VERSION}" =~ \+g([0-9a-f]{40})$ ]]; then
export VLLM_OMNI_VERSION_OVERRIDE="${VLLM_OMNI_VERSION}"
VLLM_OMNI_REQUIREMENT="vllm-omni @ https://github.com/vllm-project/vllm-omni/archive/${BASH_REMATCH[1]}.tar.gz"
fi

cleanup() {
rm -rf "${PROTECTED_CONSTRAINTS}" "${PROTECTED_OVERRIDES}"
Expand Down Expand Up @@ -58,11 +66,11 @@ if [ "${VLLM_OMNI_TARGET_DEVICE}" = "cuda" ]; then
uv pip install --system \
--prerelease=allow \
--constraints "${PROTECTED_CONSTRAINTS}" \
"vllm-omni==${VLLM_OMNI_VERSION}"
"${VLLM_OMNI_REQUIREMENT}"
else
uv pip install \
--prerelease=allow \
--constraints "${PROTECTED_CONSTRAINTS}" \
"${OVERRIDE_ARGS[@]}" \
"vllm-omni==${VLLM_OMNI_VERSION}"
"${VLLM_OMNI_REQUIREMENT}"
fi
2 changes: 1 addition & 1 deletion docs/fern/components/releases.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const CURRENT_WHEEL = "1.6.0";
export const MAIN_TOT: BackendPins = {
sglang: "0.5.22",
trtllm: "1.4.0rc2",
vllm: "0.32.0",
vllm: "0.33.0",
nixlSglang: "1.5.0",
nixlTrtllm: "1.3.1",
nixlVllm: "1.5.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ Speculative decoding in vLLM uses Eagle3 as the draft model. The launch script c

See `examples/backends/vllm/launch/agg_spec_decoding.sh` for the full configuration.

### With KV-Aware Routing

When a worker publishes KV events, as it does for KV-aware routing, Dynamo sets `disable_lookahead_block_hashes: true` in the speculative config unless you set it yourself. vLLM's EAGLE and MTP lookahead block hashes also cover tokens after each block, while Dynamo's KV router expects one hash per block. The cost is that these workers do not get the extra prefix-cache reuse that lookahead hashes provide. Setting `disable_lookahead_block_hashes` to `false` on a worker that publishes KV events is not supported.

## Limitations

- Currently only supports Eagle3 as the draft model
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ address.
</ParamField>

<ParamField path="--dyn-structural-tag" type="boolean or JSON object" default="enabled">
Structural tags default to enabled with scope `always` and schema mode `auto` for regular workers. Pass `false` to disable optional guidance, or provide a JSON object for advanced options. Rust retains native tags for Kimi K2 required/named and Kimi K3 named choices; Python vLLM and SGLang respect the opt-out. Of the native Rust sidecars, only SGLang shares this default; the vLLM and TensorRT-LLM sidecars default to `false` until their engines compile these tags. See [Structural Tag](../../use-cases/tool-calling-and-reasoning/structural-tag.md) for configuration and backend compatibility limitations.
Structural tags default to enabled with scope `always` and schema mode `auto` for regular workers. Pass `false` to disable optional guidance, or provide a JSON object for advanced options. Rust retains native tags for Kimi K2 required/named and Kimi K3 named choices; Python vLLM and SGLang respect the opt-out. Of the native Rust sidecars, SGLang shares this default, and vLLM shares it on vLLM 0.33.0 or later; the vLLM sidecar on older vLLM and the TensorRT-LLM sidecar default to `false` because their engines reject these tags. See [Structural Tag](../../use-cases/tool-calling-and-reasoning/structural-tag.md) for configuration and backend compatibility limitations.

Environment variable: `DYN_STRUCTURAL_TAG`
</ParamField>
Expand Down
2 changes: 1 addition & 1 deletion docs/fern/pages/reference/general/compatibility.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ Current stable release: v1.6.0 (container tag `1.6.0`, wheel version `1.6.0`).

| Dynamo | Type | SGLang | TensorRT-LLM | vLLM | NIXL (SGL / TRT / vLLM) | UCX |
| --- | --- | --- | --- | --- | --- | --- |
| main (ToT) | development head | 0.5.22 | 1.4.0rc2 | 0.32.0 | 1.5.0 / 1.3.1 / 1.5.0 | - |
| main (ToT) | development head | 0.5.22 | 1.4.0rc2 | 0.33.0 | 1.5.0 / 1.3.1 / 1.5.0 | - |
Comment thread
JulienDarve marked this conversation as resolved.
Resolved
| v1.6.0 | stable | 0.5.21 | 1.3.0rc29 | 0.31.0 | 1.5.0 / 1.3.1 / 1.4.1 | 1.21.x |
| v1.5.1 | patch | 0.5.18 | 1.3.0rc25 | 0.28.0 | 1.4.0 / 1.3.1 / 1.3.2 | 1.21.x |
| v1.5.0 | stable | 0.5.18 | 1.3.0rc25 | 0.28.0 | 1.4.0 / 1.3.1 / 1.3.2 | 1.21.x |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Current stable release: v1.6.0 (Oct 22, 2026; container tag `1.6.0`, wheel versi

| Version | Kind | Date | SGLang | TensorRT-LLM | vLLM | NIXL (SGL / TRT / vLLM) | UCX | Notes | Delta |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| main (ToT) | development head | - | 0.5.22 | 1.4.0rc2 | 0.32.0 | 1.5.0 / 1.3.1 / 1.5.0 | - | - | - |
| main (ToT) | development head | - | 0.5.22 | 1.4.0rc2 | 0.33.0 | 1.5.0 / 1.3.1 / 1.5.0 | - | - | - |
| v1.6.0 | stable | Oct 22, 2026 | 0.5.21 | 1.3.0rc29 | 0.31.0 | 1.5.0 / 1.3.1 / 1.4.1 | 1.21.x | [release notes](https://docs.nvidia.com/dynamo/dev/reference/releases/v1-6-0) | vLLM worker services renamed to worker, prefill and decode; Dynamo Operator no longer adopts resources it does not own and validates multinode settings more strictly; Grove v0.1.0-alpha.14 required for new multinode deployments, with Coherent updates now opt-in; AIConfigurator options renamed to AISimulate (--aic-* to --ais-*); Router policy YAML takes precedence over the scoring flags, which are deprecated with removal targeted for v1.7.0; structural tags on by default for supported parsers; TensorRT-LLM AutoDeploy backend removed and the TensorRT-LLM sidecar moved to the OpenEngine gRPC API; invalid DYN_REQUEST_PLANE and DYN_EVENT_PLANE values fail at startup. |
| v1.5.1 | patch | Oct 6, 2026 | 0.5.18 | 1.3.0rc25 | 0.28.0 | 1.4.0 / 1.3.1 / 1.3.2 | 1.21.x | [release notes](https://docs.nvidia.com/dynamo/dev/reference/releases/v1-5-0#v151) | Patch release. Fixes Router overload-hint expiry, min_tokens on tokenizer-free SGLang decode workers, split stop-sequence leaks, Qwen3-VL video routing across mixed workers, and reinforcement learning worker discovery on Kubernetes. Bounds client-supplied multimodal input (inline data: URL size, remote media downloads, image dimensions, base64 audio) and leaves aiohttp as the only HTTP client backend. Media fetches through an ambient HTTP proxy now require DYN_MM_TRUST_EGRESS_PROXY=1, and local SGLang diffusion input_reference files require DYN_MM_LOCAL_PATH. ModelExpress moves to v0.6.0. Backend pins are unchanged from v1.5.0. |
| v1.5.0 | stable | Sep 18, 2026 | 0.5.18 | 1.3.0rc25 | 0.28.0 | 1.4.0 / 1.3.1 / 1.3.2 | 1.21.x | [release notes](https://docs.nvidia.com/dynamo/dev/reference/releases/v1-5-0) | KVBM deprecated with removal targeted for v1.7.0; CRD storage version promoted to v1beta1 and the v1alpha1 admission webhook endpoints removed; Go EPP removed in favor of the Rust EPP shipped inside the Frontend image; Dynamo Snapshot moved to a standalone operator and chart, dropping the bundled snapshot chart, snapshot-agent image and DynamoCheckpoint CRD; AIConfigurator renamed to AISimulate; unified-backend entry point removed from the vLLM and SGLang workers; UCX 1.21.x. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ python3 -m dynamo.vllm ... --dyn-structural-tag false
python3 -m dynamo.sglang ... --dyn-structural-tag false
python3 -m dynamo.trtllm ... --dyn-structural-tag false
dynamo-sglang-sidecar ... --dyn-structural-tag false
dynamo-vllm-sidecar ... --dyn-structural-tag false
```

Or set the equivalent environment variable before starting the worker:
Expand Down Expand Up @@ -168,10 +169,12 @@ environment variables remain accepted with a deprecation warning.
The native Rust sidecars accept `--dyn-structural-tag` and `DYN_STRUCTURAL_TAG`
as described in [CLI Flags](#cli-flags), but not the legacy flags or their
environment variables. The SGLang sidecar uses the same default-on policy. The
vLLM and TensorRT-LLM sidecars default to `false`: vLLM's gRPC server compiles
structured outputs with guidance, which rejects these tags until
vllm-project/vllm#61892, and the xgrammar version TensorRT-LLM pins rejects
several tool-call tag formats.
vLLM sidecar uses it when vLLM reports version 0.33 or later, the first release
whose gRPC server compiles these tags with xgrammar. Against older vLLM it
defaults to `false`, because that gRPC server compiles them with guidance,
which rejects them. 0.33.0 release candidates and development builds also
default to `false`. The TensorRT-LLM sidecar defaults to `false`: the
Comment thread
JulienDarve marked this conversation as resolved.
Resolved
xgrammar version TensorRT-LLM pins rejects several tool-call tag formats.

Setting `strict: false` on a tool relaxes its argument schema. On vLLM
0.30.0, pinned by Dynamo's CUDA image, auto choice with every tool explicitly
Expand Down
1 change: 1 addition & 0 deletions lib/sidecar/vllm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ async-trait = { workspace = true }
base64 = "0.22"
clap = { version = "4", features = ["derive", "env"] }
futures = { workspace = true }
pep440_rs = "0.7"
Comment thread
JulienDarve marked this conversation as resolved.
Resolved
Comment thread
JulienDarve marked this conversation as resolved.
Resolved
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
Expand Down
Loading
Loading