Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
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
18 changes: 18 additions & 0 deletions .github/actions/compliance-extract/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ inputs:
description: 'EPP image reference (frontend only) — must equal what the build passed as EPP_IMAGE so the epp stage resolves the freshly-built ECR image rather than the released tag rendered from container/context.yaml. The compliance stages read /epp''s SBOM and license texts out of this stage, so a mismatch would attribute a different build of the binary than the one shipped. Empty for non-frontend targets, which have no epp stage.'
required: false
default: ''
extra_build_args:
description: 'Newline-separated build arguments; must match the image build, including base image and optional packages.'
required: false
default: ''
sccache_bucket:
description: 'SCCACHE S3 bucket — must equal what the build passed so wheel_builder_base''s SCCACHE_* ARGs/ENV match and the heavy Rust layer cache-hits. Normally the extract never runs sccache (everything cache-hits), but when the shared builder has evicted layers a stage re-runs here for real, so the extract also passes the IRSA secrets (see the buildx steps) to let that sccache authenticate.'
required: false
Expand Down Expand Up @@ -129,6 +133,7 @@ runs:
SCCACHE_REGION: ${{ inputs.sccache_region }}
GIT_SHA: ${{ inputs.git_sha }}
EPP_IMAGE: ${{ inputs.epp_image }}
EXTRA_BUILD_ARGS: ${{ inputs.extra_build_args }}
run: |
mkdir -p /tmp/compliance
# Reuse the warm cache the build just populated by matching ALL the
Expand Down Expand Up @@ -174,6 +179,11 @@ runs:
# --build-arg (it would clear the ARG and reintroduce the failure).
EPP_IMAGE_ARG=""
[ -n "${EPP_IMAGE:-}" ] && EPP_IMAGE_ARG="--build-arg EPP_IMAGE=${EPP_IMAGE}"
EXTRA_ARGS=()
while IFS= read -r build_arg || [ -n "$build_arg" ]; do
[ -z "$build_arg" ] && continue
EXTRA_ARGS+=(--build-arg "$build_arg")
done <<< "$EXTRA_BUILD_ARGS"
docker buildx build \
--builder ${{ inputs.builder_name }} \
--platform ${{ inputs.platform }} \
Expand All @@ -183,6 +193,7 @@ runs:
--build-arg SCCACHE_BUCKET="${SCCACHE_BUCKET}" \
--build-arg SCCACHE_REGION="${SCCACHE_REGION}" \
$EPP_IMAGE_ARG \
"${EXTRA_ARGS[@]}" \
--output type=local,dest=/tmp/compliance \
--progress=plain \
-f "${DOCKERFILE_PATH}" \
Expand Down Expand Up @@ -356,6 +367,7 @@ runs:
SCCACHE_REGION: ${{ inputs.sccache_region }}
GIT_SHA: ${{ inputs.git_sha }}
EPP_IMAGE: ${{ inputs.epp_image }}
EXTRA_BUILD_ARGS: ${{ inputs.extra_build_args }}
run: |
mkdir -p /tmp/sources
# ENABLE_SOURCE_ARCHIVAL=true to actually run sources_collect; the other
Expand Down Expand Up @@ -385,6 +397,11 @@ runs:
# resolves the GC'd default staging tag. Empty for non-frontend targets.
EPP_IMAGE_ARG=""
[ -n "${EPP_IMAGE:-}" ] && EPP_IMAGE_ARG="--build-arg EPP_IMAGE=${EPP_IMAGE}"
EXTRA_ARGS=()
while IFS= read -r build_arg || [ -n "$build_arg" ]; do
[ -z "$build_arg" ] && continue
EXTRA_ARGS+=(--build-arg "$build_arg")
done <<< "$EXTRA_BUILD_ARGS"
docker buildx build \
--builder ${{ inputs.builder_name }} \
--platform ${{ inputs.platform }} \
Expand All @@ -395,6 +412,7 @@ runs:
--build-arg SCCACHE_REGION="${SCCACHE_REGION}" \
--build-arg ENABLE_SOURCE_ARCHIVAL=true \
$EPP_IMAGE_ARG \
"${EXTRA_ARGS[@]}" \
--output type=local,dest=/tmp/sources \
--progress=plain \
-f "${DOCKERFILE_PATH}" \
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,10 @@ jobs:
uses: $/.github/workflows/shared-build-image.yml
with:
framework: vllm
extra_build_args: |
RUNTIME_IMAGE=${{ vars.ECR_REGISTRY }}/ai-dynamo/vllm-base
Comment thread
NVShreyas marked this conversation as resolved.
Resolved
RUNTIME_IMAGE_TAG=vllm-nightly-8a728663c1c3
ENABLE_VLLM_OMNI=false
target: runtime
cuda_version: '["13.0"]'
platform: 'linux/amd64,linux/arm64'
Expand Down Expand Up @@ -414,6 +418,10 @@ jobs:
uses: $/.github/workflows/shared-build-image.yml
with:
framework: vllm
extra_build_args: |
RUNTIME_IMAGE=${{ vars.ECR_REGISTRY }}/ai-dynamo/vllm-base
RUNTIME_IMAGE_TAG=vllm-nightly-8a728663c1c3
ENABLE_VLLM_OMNI=false
target: dev
cuda_version: '["13.0"]'
platform: 'linux/amd64,linux/arm64'
Expand All @@ -437,6 +445,9 @@ jobs:
build_timeout_minutes: 60
# local-dev remaps to the host user; ARG USER_UID/USER_GID have no defaults.
extra_build_args: |
RUNTIME_IMAGE=${{ vars.ECR_REGISTRY }}/ai-dynamo/vllm-base
RUNTIME_IMAGE_TAG=vllm-nightly-8a728663c1c3
ENABLE_VLLM_OMNI=false
USER_UID=1000
USER_GID=1000
secrets: inherit
Expand Down Expand Up @@ -604,6 +615,10 @@ jobs:
uses: $/.github/workflows/shared-build-image.yml
with:
framework: vllm
extra_build_args: |
RUNTIME_IMAGE=${{ vars.ECR_REGISTRY }}/ai-dynamo/vllm-base
RUNTIME_IMAGE_TAG=vllm-nightly-8a728663c1c3
ENABLE_VLLM_OMNI=false
target: runtime
cuda_version: '["13.0"]'
platform: 'linux/amd64,linux/arm64'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/shared-build-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ jobs:
# EPP_IMAGE so the epp stage cache-hits instead of resolving the
# rendered default upstream staging tag, which gets garbage-collected.
epp_image: ${{ steps.build-epp-image.outputs.epp_image_uri || '' }}
extra_build_args: ${{ inputs.extra_build_args }}
# Match the build's sccache build-args so the extract reuses the warm
# wheel_builder/pre_runtime cache instead of a cold rebuild.
sccache_bucket: ${{ secrets.SCCACHE_S3_BUCKET }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,23 @@ def test_conditional_disagg_config_rejects_invalid_json(
kv_router_args._conditional_disagg_config_arg(config_json)


def test_removed_remote_kv_threshold_is_not_a_frontend_option(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_clear_rejection_threshold_env(monkeypatch)
monkeypatch.setenv("DYN_REMOTE_KV_WAITING_TOKENS_THRESHOLD", "8192")
parser = argparse.ArgumentParser()
FrontendArgGroup().add_arguments(parser)

config = FrontendConfig.from_cli_args(parser.parse_args([]))
config.validate()
assert not hasattr(config, "remote_kv_waiting_tokens_threshold")
assert "remote_kv_waiting_tokens_threshold" not in config.router_kwargs()
with pytest.raises(SystemExit) as error:
parser.parse_args(["--remote-kv-waiting-tokens-threshold", "8192"])
assert error.value.code == 2


def test_frontend_rejection_thresholds_default_to_none(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
Expand Down
6 changes: 1 addition & 5 deletions components/src/dynamo/frontend/prepost.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@
ChatCompletionNamedToolChoiceParam,
ChatCompletionRequest,
)
from vllm.entrypoints.openai.engine.protocol import (
DeltaFunctionCall,
DeltaMessage,
DeltaToolCall,
)
from vllm.reasoning import ReasoningParser
from vllm.renderers import ChatParams, merge_kwargs
from vllm.sampling_params import SamplingParams
Expand All @@ -37,6 +32,7 @@

from .thinking import apply_default_thinking_mode_to_template_kwargs
from .utils import legacy_guided_decoding
from .vllm_protocol import DeltaFunctionCall, DeltaMessage, DeltaToolCall

if TYPE_CHECKING:
from vllm.config import ModelConfig
Expand Down
31 changes: 31 additions & 0 deletions components/src/dynamo/frontend/vllm_protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Protocol types shared by the released and MiniMax nightly vLLM layouts."""

try:
from vllm.entrypoints.generate.base.protocol import (
DeltaFunctionCall as DeltaFunctionCall,
)
from vllm.entrypoints.generate.base.protocol import DeltaMessage as DeltaMessage
from vllm.entrypoints.generate.base.protocol import DeltaToolCall as DeltaToolCall
from vllm.entrypoints.generate.base.protocol import (
FunctionDefinition as FunctionDefinition,
)
except ModuleNotFoundError as exc:
# The nightly moved these types out of openai.engine. Only fall back for
# that module move; missing dependencies inside the new module must fail.
if exc.name not in {
"vllm.entrypoints.generate",
"vllm.entrypoints.generate.base",
"vllm.entrypoints.generate.base.protocol",
}:
raise
from vllm.entrypoints.openai.engine.protocol import (
DeltaFunctionCall as DeltaFunctionCall,
)
from vllm.entrypoints.openai.engine.protocol import DeltaMessage as DeltaMessage
from vllm.entrypoints.openai.engine.protocol import DeltaToolCall as DeltaToolCall
from vllm.entrypoints.openai.engine.protocol import (
FunctionDefinition as FunctionDefinition,
)
2 changes: 2 additions & 0 deletions components/src/dynamo/vllm/instrumented_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2157,6 +2157,8 @@ def _compute_queued(self) -> QueuedRequestMetrics:
for request in self.waiting:
if request.status == RequestStatus.PREEMPTED:
decode_kv.add(request.num_computed_tokens)
elif request.status == RequestStatus.WAITING_FOR_REMOTE_KVS:
decode_kv.add(request.num_computed_tokens)
else:
prefill.add(request.num_tokens)

Expand Down
33 changes: 19 additions & 14 deletions components/src/dynamo/vllm/tests/test_vllm_api_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,25 @@ def test_new_request_data_has_fields_instrumented_scheduler_sets():
)


def test_scheduler_output_new_connector_fields_remain_optional():
"""Dynamo constructs SchedulerOutput directly without v0.27's new fields."""
from vllm.v1.core.sched.output import SchedulerOutput

fields = {field.name: field for field in dataclasses.fields(SchedulerOutput)}
for field_name in ("ec_manager_metadata", "partial_tail_offloads"):
assert field_name in fields, (
f"vLLM SchedulerOutput.{field_name} is gone — re-audit Dynamo's "
"direct SchedulerOutput construction."
)
assert fields[field_name].default is None, (
f"vLLM SchedulerOutput.{field_name} became required — update every "
"InstrumentedScheduler constructor."
)
def test_scheduler_output_construction_without_optional_connector_fields():
"""The benchmark scheduler can construct an empty output on both vLLM pins."""
from vllm.v1.core.sched.output import CachedRequestData, SchedulerOutput

output = SchedulerOutput(
scheduled_new_reqs=[],
scheduled_cached_reqs=CachedRequestData.make_empty(),
num_scheduled_tokens={},
total_num_scheduled_tokens=0,
scheduled_spec_decode_tokens={},
scheduled_encoder_inputs={},
num_common_prefix_blocks=[0],
finished_req_ids=set(),
free_encoder_mm_hashes=[],
)
assert output.total_num_scheduled_tokens == 0
assert output.kv_connector_metadata is None
assert output.ec_connector_metadata is None
assert output.ec_manager_metadata is None


def test_async_scheduler_has_methods_instrumented_scheduler_overrides():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,25 @@ def test_skipped_waiting_for_remote_kvs_counts_as_queued_decode():
assert q.sum_decode_kv_tokens == 1500


def test_main_waiting_queue_remote_kvs_counts_as_queued_decode():
"""A transient remote-KV wait in the main queue is not prefill work."""

q = _run_compute_queued(
waiting=[
_make_request(
RequestStatus.WAITING_FOR_REMOTE_KVS,
num_tokens=1024,
num_computed_tokens=768,
),
],
skipped_waiting=[],
)
assert q.num_prefill_requests == 0
assert q.sum_prefill_tokens == 0
assert q.num_decode_requests == 1
assert q.sum_decode_kv_tokens == 768


def test_skipped_waiting_for_structured_output_counts_as_queued_prefill():
"""Structured-output grammar compile wait has no KV computed yet; prefill."""

Expand Down
28 changes: 24 additions & 4 deletions components/src/dynamo/vllm/tests/test_vllm_kv_events_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
This test is the early warning for vLLM KV-event wire-format changes.

In the normal case, if this fails, first check whether
`lib/kv-router/src/zmq_wire.rs` already accepts the new upstream vLLM event
`lib/kv-router/src/zmq_wire/deserialize.rs` already accepts the new upstream vLLM event
shape. If not, update that compatibility layer before updating this test.

That file is Dynamo's compatibility layer for vLLM KV events:
Expand Down Expand Up @@ -83,6 +83,8 @@ def test_block_stored_fields(self):
10. kv_cache_spec_kind (semantic cache type; optional for older vLLM)
11. kv_cache_spec_sliding_window (semantic cache window; optional for older vLLM)
12. locality (per-tier storage locality; optional for older vLLM)
13. ownership (offloading tier; optional for older vLLM)
14. session_id (request attribution; optional for older vLLM)

If vLLM adds/removes/reorders fields, this test will fail.
"""
Expand All @@ -104,6 +106,10 @@ def test_block_stored_fields(self):
expected_fields.append("kv_cache_spec_sliding_window")
if _has_locality(BlockStored):
expected_fields.append("locality")
if "ownership" in BlockStored.__struct_fields__:
expected_fields.append("ownership")
if "session_id" in BlockStored.__struct_fields__:
expected_fields.append("session_id")
expected_fields = tuple(expected_fields)

actual_fields = BlockStored.__struct_fields__
Expand All @@ -112,7 +118,7 @@ def test_block_stored_fields(self):
f"Expected: {expected_fields}\n"
f"Actual: {actual_fields}\n"
f"Required follow-up:\n"
f" - Update lib/kv-router/src/zmq_wire.rs to match the new BlockStored wire format.\n"
f" - Update lib/kv-router/src/zmq_wire/deserialize.rs to match the new BlockStored wire format.\n"
f" - Update this test's expected_fields and msgpack shape checks.\n"
f" - If needed, add or update a regression test in lib/llm/src/kv_router/publisher.rs."
)
Expand All @@ -131,6 +137,8 @@ def test_block_removed_fields(self):
expected_fields.append("kv_cache_spec_sliding_window")
if _has_locality(BlockRemoved):
expected_fields.append("locality")
if "ownership" in BlockRemoved.__struct_fields__:
expected_fields.append("ownership")
expected_fields = tuple(expected_fields)

actual_fields = BlockRemoved.__struct_fields__
Expand All @@ -139,7 +147,7 @@ def test_block_removed_fields(self):
f"Expected: {expected_fields}\n"
f"Actual: {actual_fields}\n"
f"Required follow-up:\n"
f" - Update lib/kv-router/src/zmq_wire.rs RawKvEvent::BlockRemoved seq deserializer.\n"
f" - Update lib/kv-router/src/zmq_wire/deserialize.rs RawKvEvent::BlockRemoved seq deserializer.\n"
f" - Update this test's expected_fields."
)

Expand All @@ -157,7 +165,7 @@ def test_event_batch_fields(self):
f"Expected: {expected_fields}\n"
f"Actual: {actual_fields}\n"
f"Required follow-up:\n"
f" - Update lib/kv-router/src/zmq_wire.rs KvEventBatch Deserialize impl.\n"
f" - Update lib/kv-router/src/zmq_wire/deserialize.rs KvEventBatch Deserialize impl.\n"
f" - Update subscriber.rs VllmEventBatch tuple if batch field order changes.\n"
f" - Update this test's expected_fields."
)
Expand Down Expand Up @@ -211,6 +219,10 @@ def test_block_stored_serialization_format(self):
event_kwargs["kv_cache_spec_sliding_window"] = 128
if _has_locality(BlockStored):
event_kwargs["locality"] = "LOCAL"
if "ownership" in BlockStored.__struct_fields__:
event_kwargs["ownership"] = "kvcr"
if "session_id" in BlockStored.__struct_fields__:
event_kwargs["session_id"] = "session-1"
event = BlockStored(**event_kwargs)

encoded = msgspec.msgpack.encode(event)
Expand All @@ -234,6 +246,10 @@ def test_block_stored_serialization_format(self):
assert decoded["kv_cache_spec_sliding_window"] == 128
if _has_locality(BlockStored):
assert decoded["locality"] == "LOCAL"
if "ownership" in BlockStored.__struct_fields__:
assert decoded["ownership"] == "kvcr"
if "session_id" in BlockStored.__struct_fields__:
assert decoded["session_id"] == "session-1"

def test_block_stored_tuple_extra_keys_serialization_format(self):
"""Verify multimodal tuple extra_keys keep the vLLM 0.19 wire shape."""
Expand Down Expand Up @@ -288,6 +304,8 @@ def test_block_removed_serialization_format(self):
event_kwargs["kv_cache_spec_sliding_window"] = 128
if _has_locality(BlockRemoved):
event_kwargs["locality"] = "REMOTE"
if "ownership" in BlockRemoved.__struct_fields__:
event_kwargs["ownership"] = "kvcr"
event = BlockRemoved(**event_kwargs)

decoded = msgspec.msgpack.decode(msgspec.msgpack.encode(event))
Expand All @@ -305,3 +323,5 @@ def test_block_removed_serialization_format(self):
), "kv_cache_spec_sliding_window has wrong value"
if _has_locality(BlockRemoved):
assert decoded["locality"] == "REMOTE"
if "ownership" in BlockRemoved.__struct_fields__:
assert decoded["ownership"] == "kvcr"
Loading
Loading