[Rebase] Rebase to vllm releases/v0.22.0 (#3891)

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
Signed-off-by: ZhengWG <zwg0606@gmail.com>
Signed-off-by: david6666666 <530634352@qq.com>
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
Signed-off-by: nainiu258 <cperfect02@163.com>
Signed-off-by: nainiu258 <101917677+nainiu258@users.noreply.github.com>
Signed-off-by: JasonJ2021 <jasonj@zju.edu.cn>
Signed-off-by: Zhou Taichang <tzhouam@connect.ust.hk>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: skf <54565339+skf-1999@users.noreply.github.com>
Co-authored-by: Zheng Wengang <zwg0606@gmail.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: WeiQing Chen <40507679+david6666666@users.noreply.github.com>
Co-authored-by: SYLAR <125541396+lishunyang12@users.noreply.github.com>
Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: nainiu258 <101917677+nainiu258@users.noreply.github.com>
Co-authored-by: Jason <72191212+JasonJ2021@users.noreply.github.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
This commit is contained in:
62 files changed
+998 -354

No files matched your search

+1 -1
View File
@@ -107,7 +107,7 @@ steps:
depends_on: upload-merge-pipeline depends_on: upload-merge-pipeline
steps: steps:
- label: "Entrypoint Test with H100" - label: "Entrypoint Test with H100"
timeout_in_minutes: 35 timeout_in_minutes: 60
commands: commands:
- pytest -s -v tests/entrypoints/ -m "advanced_model and cuda and H100" --run-level "advanced_model" - pytest -s -v tests/entrypoints/ -m "advanced_model and cuda and H100" --run-level "advanced_model"
agents: agents:
+1 -1
View File
@@ -183,7 +183,7 @@ steps:
depends_on: upload-ready-pipeline depends_on: upload-ready-pipeline
commands: commands:
- | - |
timeout 15m bash -c " timeout 20m bash -c "
pytest -sv tests/distributed/ -m 'core_model and cuda' --run-level 'core_model' pytest -sv tests/distributed/ -m 'core_model and cuda' --run-level 'core_model'
" "
agents: agents:
View File
Whitespace-only changes.
+3
View File
@@ -14,6 +14,7 @@ models:
supported_tasks: [default_voice, voice_design] supported_tasks: [default_voice, voice_design]
backend: openai-audio-speech backend: openai-audio-speech
endpoint: /v1/audio/speech endpoint: /v1/audio/speech
trust_remote_code: true
task_extra_body: task_extra_body:
default_voice: default_voice:
voice: Vivian voice: Vivian
@@ -27,6 +28,7 @@ models:
supported_tasks: [voice_clone] supported_tasks: [voice_clone]
backend: openai-audio-speech backend: openai-audio-speech
endpoint: /v1/audio/speech endpoint: /v1/audio/speech
trust_remote_code: true
task_extra_body: task_extra_body:
voice_clone: voice_clone:
task_type: Base task_type: Base
@@ -36,6 +38,7 @@ models:
trust_remote_code: true trust_remote_code: true
backend: openai-audio-speech backend: openai-audio-speech
endpoint: /v1/audio/speech endpoint: /v1/audio/speech
trust_remote_code: true
task_extra_body: task_extra_body:
voice_clone: {} voice_clone: {}
default_voice: default_voice:
+4 -1
View File
@@ -1,5 +1,8 @@
# Target vllm version: v0.21.0 (releases/v0.21.0 branch).
# The pinned VLLM_PRECOMPILED_WHEEL_COMMIT and VLLM_BASE_TAG below must
# be updated on every rebase to match the new upstream vllm commit/tag.
ARG VLLM_BASE_IMAGE=vllm/vllm-openai ARG VLLM_BASE_IMAGE=vllm/vllm-openai
ARG VLLM_BASE_TAG=v0.21.0 ARG VLLM_BASE_TAG=v0.22.0
FROM ${VLLM_BASE_IMAGE}:${VLLM_BASE_TAG} FROM ${VLLM_BASE_IMAGE}:${VLLM_BASE_TAG}
ARG APP_DIR=/workspace/vllm-omni ARG APP_DIR=/workspace/vllm-omni
WORKDIR ${APP_DIR} WORKDIR ${APP_DIR}
+1 -1
View File
@@ -74,7 +74,7 @@ Online:
vllm serve Qwen/Qwen-Image \ vllm serve Qwen/Qwen-Image \
--omni \ --omni \
--port 8000 \ --port 8000 \
--quantization-config '{"method":"gguf","gguf_model":"QuantStack/Qwen-Image-GGUF/Qwen_Image-Q4_K_M.gguf"}' --diffusion-quantization-config '{"method":"gguf","gguf_model":"QuantStack/Qwen-Image-GGUF/Qwen_Image-Q4_K_M.gguf"}'
``` ```
## Parameters ## Parameters
+1
View File
@@ -47,6 +47,7 @@ recipes/
| [`Wan-AI/Wan2.2-S2V.md`](./Wan-AI/Wan2.2-S2V.md) | Speech-to-video serving (Wan2.2 14B) | 2x A100/H100 80GB | | [`Wan-AI/Wan2.2-S2V.md`](./Wan-AI/Wan2.2-S2V.md) | Speech-to-video serving (Wan2.2 14B) | 2x A100/H100 80GB |
| [`zai-org/GLM-TTS.md`](./zai-org/GLM-TTS.md) | Online serving for Chinese/English zero-shot voice-cloned TTS | 1x A40 48GB | | [`zai-org/GLM-TTS.md`](./zai-org/GLM-TTS.md) | Online serving for Chinese/English zero-shot voice-cloned TTS | 1x A40 48GB |
| [`GLM/GLM-Image.md`](./GLM/GLM-Image.md) | Online serving for image generation | 1x A800 80GB / 2x A800 80GB | | [`GLM/GLM-Image.md`](./GLM/GLM-Image.md) | Online serving for image generation | 1x A800 80GB / 2x A800 80GB |
|
Within a single recipe file, include different hardware support sections such Within a single recipe file, include different hardware support sections such
as `GPU`, `ROCm`, and `NPU`, and add concrete tested configurations like as `GPU`, `ROCm`, and `NPU`, and add concrete tested configurations like
+6
View File
@@ -1,4 +1,10 @@
# Common dependencies for all platforms # Common dependencies for all platforms
# transformers: keep aligned with upstream vLLM constraint.
# vLLM pins ">= 4.56.0, != 5.0.*, != 5.1.*, != 5.2.*, != 5.3.*, != 5.4.*, != 5.5.0".
# 5.9.0 breaks models that define _keys_to_ignore_on_load_unexpected as a list
# (e.g. OpenMOSS-Team/MOSS-TTS-Nano) because it changed
# _adjust_missing_and_unexpected_keys from list+list to set|set.
transformers >= 4.56.0, != 5.0.*, != 5.1.*, != 5.2.*, != 5.3.*, != 5.4.*, != 5.5.0, < 5.9.0
av>=14.0.0 av>=14.0.0
omegaconf>=2.3.0 omegaconf>=2.3.0
diffusers==0.38.0 diffusers==0.38.0
@@ -223,8 +223,12 @@ def _get_open_port() -> int:
return s.getsockname()[1] return s.getsockname()[1]
def _wait_for_port(host: str, port: int, timeout: int = 1200) -> None: def _wait_for_port(host: str, port: int, timeout: int = 1200, proc: subprocess.Popen | None = None) -> None:
"""Block until the given host:port accepts connections or timeout expires.""" """Block until the given host:port accepts connections or timeout expires.
If *proc* is provided, also monitors the process; raises RuntimeError
immediately if the server process exits before the port becomes available.
"""
start = time.time() start = time.time()
while time.time() - start < timeout: while time.time() - start < timeout:
try: try:
@@ -234,6 +238,10 @@ def _wait_for_port(host: str, port: int, timeout: int = 1200) -> None:
return return
except Exception: except Exception:
pass pass
if proc is not None:
ret = proc.poll()
if ret is not None:
raise RuntimeError(f"Server process exited with code {ret} before port {host}:{port} became ready")
time.sleep(2) time.sleep(2)
raise RuntimeError(f"Server did not start on {host}:{port} within {timeout}s") raise RuntimeError(f"Server did not start on {host}:{port} within {timeout}s")
@@ -331,7 +339,7 @@ class DiffusionServer:
env=env, env=env,
cwd=str(Path(__file__).parent.parent.parent.parent), cwd=str(Path(__file__).parent.parent.parent.parent),
) )
_wait_for_port(self.host, self.port) _wait_for_port(self.host, self.port, proc=self.proc)
print(f"DiffusionServer ready on {self.host}:{self.port}") print(f"DiffusionServer ready on {self.host}:{self.port}")
def __enter__(self): def __enter__(self):
@@ -5,7 +5,8 @@
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct", "model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"use_omni": false, "use_omni": false,
"extra_cli_args": [ "extra_cli_args": [
"--no-enable-prefix-caching" "--no-enable-prefix-caching",
"--skip-mm-profiling"
] ]
}, },
"benchmark_params": [ "benchmark_params": [
@@ -17,7 +17,7 @@ AUDIO_MODEL = {
} }
IMAGE_VIDEO_MODELS = { IMAGE_VIDEO_MODELS = {
"riverclouds/qwen_image_random": {"cuda": 2500, "rocm": 2100}, "riverclouds/qwen_image_random": {"cuda": 2200, "rocm": 2100},
} }
MODELS = {**AUDIO_MODEL, **IMAGE_VIDEO_MODELS} MODELS = {**AUDIO_MODEL, **IMAGE_VIDEO_MODELS}
@@ -301,8 +301,4 @@ def test_vllm_set_forward_context_implementation(monkeypatch):
"vllm_config", "vllm_config",
), ),
), ),
("ir_op_priority", None),
("enable_torch_wrap", vllm_config.compilation_config.ir_enable_torch_wrap),
("enable_torch_wrap_exit", vllm_config.compilation_config.ir_enable_torch_wrap),
("ir_op_priority_exit", None),
], ERROR_MESSAGE ], ERROR_MESSAGE
+3 -3
View File
@@ -85,7 +85,7 @@ class MockOmniRequestOutput:
BASE_HEIGHT = 512 BASE_HEIGHT = 512
BASE_WIDTH = 512 BASE_WIDTH = 512
BASE_INFER_STEPS = 10 BASE_INFER_STEPS = 10
DELAY_BASE = 2 DELAY_BASE = 0.01
class MockDiffusionEngine: class MockDiffusionEngine:
@@ -93,7 +93,7 @@ class MockDiffusionEngine:
def simulate_step_delay(height, width, num_inference_steps) -> float: def simulate_step_delay(height, width, num_inference_steps) -> float:
return (height / BASE_HEIGHT) * (width / BASE_WIDTH) * (num_inference_steps / BASE_INFER_STEPS) return (height / BASE_HEIGHT) * (width / BASE_WIDTH) * (num_inference_steps / BASE_INFER_STEPS)
DELAY_BASE = 2 DELAY_BASE = 0.01
delay_scale = simulate_step_delay( delay_scale = simulate_step_delay(
request.sampling_params.height, request.sampling_params.width, request.sampling_params.num_inference_steps request.sampling_params.height, request.sampling_params.width, request.sampling_params.num_inference_steps
) )
@@ -139,7 +139,7 @@ async def test_proc_process_request_with_batching_async_output():
assert len(results) == len(test_requests) assert len(results) == len(test_requests)
base_time = DELAY_BASE base_time = DELAY_BASE
time_gap_std = DELAY_BASE * 2 * 2 * 1 # height/width/steps infer time scale time_gap_std = DELAY_BASE * 2 * 2 * 1 # height/width/steps infer time scale
eps = 0.5 eps = 0.1
for i, (res, elapsed_time) in enumerate(results): for i, (res, elapsed_time) in enumerate(results):
assert res.request_id == test_requests[i]["request_id"] assert res.request_id == test_requests[i]["request_id"]
assert isinstance(res, MockOmniRequestOutput) assert isinstance(res, MockOmniRequestOutput)
@@ -703,6 +703,7 @@ def test_generation_scheduler_calls_cleanup_on_finished(monkeypatch, mocker: Moc
kv_connector_output=None, kv_connector_output=None,
cudagraph_stats=None, cudagraph_stats=None,
req_id_to_index={"req-s1": 0}, req_id_to_index={"req-s1": 0},
routed_experts=None,
routed_experts_dict=None, routed_experts_dict=None,
) )
@@ -789,6 +790,7 @@ def test_ar_scheduler_defers_cleanup_and_queues_save_on_finished(mocker: MockerF
cudagraph_stats=None, cudagraph_stats=None,
req_id_to_index={"req-ar": 0}, req_id_to_index={"req-ar": 0},
kv_extracted_req_ids=None, kv_extracted_req_ids=None,
routed_experts=None,
routed_experts_dict=None, routed_experts_dict=None,
) )
@@ -68,13 +68,13 @@ def test_omni_coordinator_pub_coalescing_on_rapid_queue_updates():
sub_ctx = zmq.Context.instance() sub_ctx = zmq.Context.instance()
sub = sub_ctx.socket(zmq.SUB) sub = sub_ctx.socket(zmq.SUB)
sub.connect(pub_addr) sub.connect(coordinator.pub_zmq_addr)
sub.setsockopt(zmq.SUBSCRIBE, b"") sub.setsockopt(zmq.SUBSCRIBE, b"")
time.sleep(0.3) # PUB/SUB slow-joiner time.sleep(0.3) # PUB/SUB slow-joiner
client = OmniCoordClientForStage( client = OmniCoordClientForStage(
router_addr, coordinator.router_zmq_addr,
"tcp://stage:coalesce", "tcp://stage:coalesce",
"tcp://stage:coalesce-out", "tcp://stage:coalesce-out",
0, 0,
@@ -131,7 +131,7 @@ def test_omni_coordinator_registration_broadcast():
sub_ctx = zmq.Context.instance() sub_ctx = zmq.Context.instance()
sub = sub_ctx.socket(zmq.SUB) sub = sub_ctx.socket(zmq.SUB)
sub.connect(pub_addr) sub.connect(coordinator.pub_zmq_addr)
sub.setsockopt(zmq.SUBSCRIBE, b"") sub.setsockopt(zmq.SUBSCRIBE, b"")
# ZMQ PUB/SUB slow-joiner: allow SUB to connect before clients register. # ZMQ PUB/SUB slow-joiner: allow SUB to connect before clients register.
@@ -139,9 +139,9 @@ def test_omni_coordinator_registration_broadcast():
# Create 3 stage clients; each auto-registers on init. # Create 3 stage clients; each auto-registers on init.
clients = [ clients = [
OmniCoordClientForStage(router_addr, "tcp://stage:10001", "tcp://stage:10001-out", 0), OmniCoordClientForStage(coordinator.router_zmq_addr, "tcp://stage:10001", "tcp://stage:10001-out", 0),
OmniCoordClientForStage(router_addr, "tcp://stage:10002", "tcp://stage:10002-out", 0), OmniCoordClientForStage(coordinator.router_zmq_addr, "tcp://stage:10002", "tcp://stage:10002-out", 0),
OmniCoordClientForStage(router_addr, "tcp://stage:10003", "tcp://stage:10003-out", 1), OmniCoordClientForStage(coordinator.router_zmq_addr, "tcp://stage:10003", "tcp://stage:10003-out", 1),
] ]
msg = _wait_for_replica_list(sub, expected_count=3) msg = _wait_for_replica_list(sub, expected_count=3)
@@ -183,19 +183,19 @@ def test_omni_coordinator_heartbeat_timeout_handling():
sub_ctx = zmq.Context.instance() sub_ctx = zmq.Context.instance()
sub = sub_ctx.socket(zmq.SUB) sub = sub_ctx.socket(zmq.SUB)
sub.connect(pub_addr) sub.connect(coordinator.pub_zmq_addr)
sub.setsockopt(zmq.SUBSCRIBE, b"") sub.setsockopt(zmq.SUBSCRIBE, b"")
time.sleep(0.3) time.sleep(0.3)
# A and B: real clients that send heartbeats every 5s. # A and B: real clients that send heartbeats every 5s.
client_a = OmniCoordClientForStage(router_addr, "tcp://stage:a", "tcp://stage:a-out", 0) client_a = OmniCoordClientForStage(coordinator.router_zmq_addr, "tcp://stage:a", "tcp://stage:a-out", 0)
client_b = OmniCoordClientForStage(router_addr, "tcp://stage:b", "tcp://stage:b-out", 0) client_b = OmniCoordClientForStage(coordinator.router_zmq_addr, "tcp://stage:b", "tcp://stage:b-out", 0)
# C: raw DEALER that sends only registration, no heartbeat. # C: raw DEALER that sends only registration, no heartbeat.
dealer_ctx = zmq.Context.instance() dealer_ctx = zmq.Context.instance()
dealer_c = dealer_ctx.socket(zmq.DEALER) dealer_c = dealer_ctx.socket(zmq.DEALER)
dealer_c.connect(router_addr) dealer_c.connect(coordinator.router_zmq_addr)
reg_event = { reg_event = {
"input_addr": "tcp://stage:c", "input_addr": "tcp://stage:c",
"output_addr": "tcp://stage:c-out", "output_addr": "tcp://stage:c-out",
@@ -254,12 +254,12 @@ def test_omni_coordinator_replica_shutdown_handling():
sub_ctx = zmq.Context.instance() sub_ctx = zmq.Context.instance()
sub = sub_ctx.socket(zmq.SUB) sub = sub_ctx.socket(zmq.SUB)
sub.connect(pub_addr) sub.connect(coordinator.pub_zmq_addr)
sub.setsockopt(zmq.SUBSCRIBE, b"") sub.setsockopt(zmq.SUBSCRIBE, b"")
time.sleep(0.3) # PUB/SUB slow-joiner time.sleep(0.3) # PUB/SUB slow-joiner
client = OmniCoordClientForStage(router_addr, "tcp://stage:shutdown", "tcp://stage:shutdown-out", 0) client = OmniCoordClientForStage(coordinator.router_zmq_addr, "tcp://stage:shutdown", "tcp://stage:shutdown-out", 0)
msg = _wait_for_replica_list(sub, expected_count=1) msg = _wait_for_replica_list(sub, expected_count=1)
assert msg is not None assert msg is not None
@@ -97,7 +97,7 @@ def _get_diffusion_feature_cases(model: str):
"--vae-patch-parallel-size", "--vae-patch-parallel-size",
"2", "2",
"--vae-use-tiling", "--vae-use-tiling",
"--quantization-config", "--diffusion-quantization-config",
'{"method":"fp8"}', '{"method":"fp8"}',
], ],
), ),
+88 -1
View File
@@ -4,7 +4,7 @@ import sys
import pytest import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from vllm_omni.entrypoints.stage_utils import set_stage_devices from vllm_omni.entrypoints.stage_utils import _map_device_list, set_stage_devices
def _make_dummy_torch(call_log): def _make_dummy_torch(call_log):
@@ -130,3 +130,90 @@ def test_set_stage_devices_npu_platform(mocker: MockerFixture, monkeypatch: pyte
set_stage_devices(stage_id=0, devices="0,1") set_stage_devices(stage_id=0, devices="0,1")
assert os.environ["ASCEND_RT_VISIBLE_DEVICES"] == "4,5" assert os.environ["ASCEND_RT_VISIBLE_DEVICES"] == "4,5"
# ---- _map_device_list unit tests ----
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_idempotency():
"""Device IDs already in the visible set are returned as-is (idempotency)."""
result = _map_device_list(0, ["0", "1"], ["0", "1", "2", "3"])
assert result == ["0", "1"]
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_idempotency_single():
"""Single device ID in visible set passes through."""
result = _map_device_list(1, ["1"], ["0", "1"])
assert result == ["1"]
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_index_mapping():
"""Device IDs < num_visible not in set are treated as indices."""
result = _map_device_list(0, ["0", "1"], ["6", "7", "8", "9"])
assert result == ["6", "7"]
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_physical_fallback():
"""Device IDs >= num_visible are treated as physical IDs and passed through."""
result = _map_device_list(1, ["2"], ["0", "1"])
assert result == ["2"]
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_physical_fallback_multi():
"""Multiple device IDs >= num_visible all pass through as physical IDs."""
result = _map_device_list(1, ["2", "3"], ["0", "1"])
assert result == ["2", "3"]
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_physical_fallback_mixed():
"""When some devices are >= num_visible but some < num_visible, index mapping applies for the subset."""
# "1" < num_visible(2) so index mapping applies: visible[1] = "5"
# "2" >= num_visible(2) so it's dropped via the partial mapping path
result = _map_device_list(1, ["1", "2"], ["0", "5"])
assert result == ["5"]
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_index_with_gaps():
"""Index-based mapping works with non-contiguous visible device lists."""
result = _map_device_list(0, ["0", "1"], ["4", "5", "7"])
assert result == ["4", "5"]
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_index_mapping_no_idempotency():
"""Device IDs < num_visible not in visible set map via index even when no ID matches literally."""
# "0","1" with visible ["2","3"]: idempotency check fails (none in set),
# but index mapping succeeds: visible[0]="2", visible[1]="3"
result = _map_device_list(0, ["0", "1"], ["2", "3"])
assert result == ["2", "3"]
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_partial_mapping():
"""When only a subset can map, returns the mapped subset (no error)."""
result = _map_device_list(0, ["0", "1", "2"], ["5", "6"])
assert result == ["5", "6"]
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_raises_on_non_numeric():
"""Non-numeric device IDs raise ValueError."""
with pytest.raises(ValueError, match="must be non-negative integers"):
_map_device_list(0, ["a"], ["0", "1"])
+32
View File
@@ -160,9 +160,41 @@ def _print_gpu_processes() -> None:
print("=" * 80) print("=" * 80)
def _cleanup_stale_device_locks() -> None:
"""Remove stale device-initialization lock files whose recorded PID is dead.
Lock files at ``/tmp/vllm_omni_device_*_init.lock`` may persist after a
crashed / killed test run and block subsequent orchestrator startups.
"""
import glob as _glob
for lock_file in _glob.glob("/tmp/vllm_omni_device_*_init.lock"):
try:
with open(lock_file) as fh:
content = fh.read().strip()
if not content:
continue
pid = int(content)
except (OSError, ValueError):
continue
try:
os.kill(pid, 0)
except ProcessLookupError:
print(f" Removing stale device lock {lock_file} (PID {pid} is dead)")
try:
os.unlink(lock_file)
except OSError:
pass
except PermissionError:
pass
def run_pre_test_cleanup() -> None: def run_pre_test_cleanup() -> None:
print("Pre-test GPU status:") print("Pre-test GPU status:")
_cleanup_stale_device_locks()
num_gpus = current_omni_platform.device_count() num_gpus = current_omni_platform.device_count()
if num_gpus > 0: if num_gpus > 0:
try: try:
+2 -2
View File
@@ -21,7 +21,7 @@ def cuda_marks(*, res: str, num_cards: int):
return marks return marks
test_distributed = pytest.mark.distributed_cuda(num_cards=num_cards) test_distributed = pytest.mark.distributed_cuda(num_cards=num_cards)
test_skipif = pytest.mark.skipif_cuda( test_skipif = pytest.mark.skipif(
not current_platform.is_cuda() or (current_platform.device_count() < num_cards), not current_platform.is_cuda() or (current_platform.device_count() < num_cards),
reason=f"Need at least {num_cards} CUDA GPUs to run the test.", reason=f"Need at least {num_cards} CUDA GPUs to run the test.",
) )
@@ -52,7 +52,7 @@ def xpu_marks(*, res: str, num_cards: int):
return marks return marks
test_distributed = pytest.mark.distributed_xpu(num_cards=num_cards) test_distributed = pytest.mark.distributed_xpu(num_cards=num_cards)
test_skipif = pytest.mark.skipif_xpu( test_skipif = pytest.mark.skipif(
not current_platform.is_xpu() or (current_platform.device_count() < num_cards), not current_platform.is_xpu() or (current_platform.device_count() < num_cards),
reason=f"Need at least {num_cards} XPUs to run the test.", reason=f"Need at least {num_cards} XPUs to run the test.",
) )
+113 -21
View File
@@ -305,40 +305,132 @@ class OmniServer:
time.sleep(2) time.sleep(2)
raise RuntimeError(f"Server failed to start within {max_wait} seconds") raise RuntimeError(f"Server failed to start within {max_wait} seconds")
@staticmethod
def _reap_zombie(proc: "psutil.Process") -> bool:
"""Reap a zombie child process via ``os.waitpid``.
``psutil.Process.wait()`` uses ``pidfd_open`` + ``poll()``, which
never fires for a zombie (the zombie is already dead and will not
change state). Since the test process is the parent, we can reap
the zombie directly with ``os.waitpid(pid, os.WNOHANG)`` and
retrieve its exit code.
Returns True if the process was a zombie and was reaped.
"""
if proc.status() != psutil.STATUS_ZOMBIE:
return False
try:
os.waitpid(proc.pid, os.WNOHANG)
except ChildProcessError:
pass
return True
@staticmethod
def _wait_or_reap(proc: "psutil.Process", timeout: int) -> None:
"""Wait for *proc* to exit, handling zombie state transparently.
When the process is already a zombie (e.g. orchestrator thread
hung during shutdown and the main process exited), ``pidfd_open``
+ ``poll()`` inside ``psutil`` will never see a state change.
Fall back to ``os.waitpid`` to reap the zombie.
"""
if OmniServer._reap_zombie(proc):
return
try:
proc.wait(timeout=timeout)
except psutil.TimeoutExpired:
OmniServer._reap_zombie(proc)
def _kill_process_tree(self, pid): def _kill_process_tree(self, pid):
"""Kill the process tree rooted at *pid*.
Terminate the parent **first** so the OmniServer can gracefully shut
down its stage-engine children through the orchestrator. This avoids
the ``subprocess died unexpectedly`` ERROR that the APIServer monitor
thread logs when children are killed before the parent, which in turn
can cause CI watchdogs to false-trigger on the upstream ``Shutdown
initiated`` message.
When the parent does not exit within the grace period (e.g. CPU-
offloaded workers stuck in CUDA D-state), the method falls back to
killing children first so the parent can be reaped cleanly.
"""
try: try:
parent = psutil.Process(pid) parent = psutil.Process(pid)
children = parent.children(recursive=True) children = parent.children(recursive=True)
all_pids = [pid] + [child.pid for child in children]
for child in children:
try:
child.terminate()
except psutil.NoSuchProcess:
pass
_, still_alive = psutil.wait_procs(children, timeout=10)
for child in still_alive:
try:
child.kill()
except psutil.NoSuchProcess:
pass
# 1. Terminate the parent first — let it run its graceful
# shutdown cascade (orchestrator → stage pools → engine cores).
try: try:
parent.terminate() parent.terminate()
parent.wait(timeout=10) except psutil.NoSuchProcess:
except (psutil.NoSuchProcess, psutil.TimeoutExpired): pass
# 2. Give the parent time to shut down its children cleanly.
parent_exited = False
try:
parent.wait(timeout=15)
parent_exited = True
except psutil.NoSuchProcess:
parent_exited = True
except psutil.TimeoutExpired:
parent_exited = OmniServer._reap_zombie(parent)
if not parent_exited:
# Parent is stuck — children (e.g. CPU-offloaded CFG workers)
# are likely in uninterruptible sleep. Kill children first
# so the parent can be reaped without lingering as a zombie.
for child in children:
try:
child.kill()
except psutil.NoSuchProcess:
pass
psutil.wait_procs(children, timeout=5)
try: try:
parent.kill() parent.kill()
except psutil.NoSuchProcess: except psutil.NoSuchProcess:
pass pass
OmniServer._wait_or_reap(parent, timeout=5)
else:
# Parent exited cleanly — clean up any remaining children.
for child in children:
try:
if child.is_running():
child.terminate()
except psutil.NoSuchProcess:
pass
gone, still_alive = psutil.wait_procs(children, timeout=10)
for child in still_alive:
try:
child.kill()
except psutil.NoSuchProcess:
pass
try:
if parent.is_running() and not OmniServer._reap_zombie(parent):
parent.kill()
parent.wait(timeout=10)
except psutil.NoSuchProcess:
pass
# 3. Final sweep — ``kill -9`` anything that escaped.
time.sleep(1) time.sleep(1)
alive_processes = [] alive_processes: list[int] = []
for check_pid in all_pids: for child in children:
if psutil.pid_exists(check_pid): try:
alive_processes.append(check_pid) if child.is_running():
alive_processes.append(child.pid)
except psutil.NoSuchProcess:
pass
# Only count the parent as alive if it is NOT a zombie
# (zombies are already dead — just waiting to be reaped).
try:
if parent.is_running() and parent.status() != psutil.STATUS_ZOMBIE:
alive_processes.append(parent.pid)
except psutil.NoSuchProcess:
pass
if alive_processes: if alive_processes:
print(f"Warning: Processes still alive: {alive_processes}") print(f"Warning: Processes still alive: {alive_processes}")
+55 -22
View File
@@ -164,7 +164,8 @@ def test_split_mixed_real_world():
# orchestrator-only keys never reach engine # orchestrator-only keys never reach engine
assert "stage_init_timeout" not in engine assert "stage_init_timeout" not in engine
assert "deploy_config" not in engine assert "deploy_config" not in engine
assert "async_chunk" not in engine # async_chunk is in SHARED_FIELDS so it flows to both
assert "async_chunk" in engine
# ============================================================================ # ============================================================================
@@ -172,24 +173,47 @@ def test_split_mixed_real_world():
# ============================================================================ # ============================================================================
def test_user_typed_unclassified_warns(caplog): def test_user_typed_unclassified_warns(monkeypatch):
"""If the user types a flag we can't route, warn — don't silently drop.""" """If the user types a flag we can't route, warn — don't silently drop."""
raw = {"bogus_flag": "value", "max_num_seqs": 64} import io
with caplog.at_level(logging.WARNING, logger="vllm_omni.engine.arg_utils"):
buf = io.StringIO()
vllm_logger = logging.getLogger("vllm")
orig_handlers = list(vllm_logger.handlers)
vllm_logger.handlers.clear()
handler = logging.StreamHandler(buf)
handler.setLevel(logging.WARNING)
vllm_logger.addHandler(handler)
try:
raw = {"bogus_flag": "value", "max_num_seqs": 64}
split_kwargs(raw, engine_cls=_FakeEngineArgs, user_typed={"bogus_flag"}) split_kwargs(raw, engine_cls=_FakeEngineArgs, user_typed={"bogus_flag"})
assert any("bogus_flag" in rec.message for rec in caplog.records), ( assert "bogus_flag" in buf.getvalue(), f"Expected 'bogus_flag' in log output, got: {buf.getvalue()!r}"
f"Expected warning mentioning 'bogus_flag', got: {[rec.message for rec in caplog.records]}" finally:
) vllm_logger.handlers.clear()
for h in orig_handlers:
vllm_logger.addHandler(h)
def test_unclassified_without_user_typed_silent(caplog): def test_unclassified_without_user_typed_silent():
"""Without user_typed, unclassified keys drop silently (argparse defaults """Without user_typed, unclassified keys drop silently (argparse defaults
for server flags shouldn't spam logs on every launch).""" for server flags shouldn't spam logs on every launch)."""
raw = {"host": "0.0.0.0", "port": 8091, "max_num_seqs": 64} import io
with caplog.at_level(logging.WARNING, logger="vllm_omni.engine.arg_utils"):
vllm_logger = logging.getLogger("vllm")
orig_handlers = list(vllm_logger.handlers)
vllm_logger.handlers.clear()
buf = io.StringIO()
handler = logging.StreamHandler(buf)
handler.setLevel(logging.WARNING)
vllm_logger.addHandler(handler)
try:
raw = {"host": "0.0.0.0", "port": 8091, "max_num_seqs": 64}
split_kwargs(raw, engine_cls=_FakeEngineArgs, user_typed=None) split_kwargs(raw, engine_cls=_FakeEngineArgs, user_typed=None)
# No warnings because we don't know these were user-typed. assert "host" not in buf.getvalue()
assert not any("host" in rec.message or "port" in rec.message for rec in caplog.records) finally:
vllm_logger.handlers.clear()
for h in orig_handlers:
vllm_logger.addHandler(h)
# ============================================================================ # ============================================================================
@@ -290,7 +314,7 @@ def test_internal_blacklist_keys_derived_from_orchestrator():
# Spot-check expected entries # Spot-check expected entries
assert "stage_init_timeout" in blacklist assert "stage_init_timeout" in blacklist
assert "deploy_config" in blacklist assert "deploy_config" in blacklist
assert "async_chunk" in blacklist assert "async_chunk" not in blacklist # now in SHARED_FIELDS
# Shared fields must NOT appear — they flow to both orchestrator and engine # Shared fields must NOT appear — they flow to both orchestrator and engine
assert "model" not in blacklist assert "model" not in blacklist
assert "log_stats" not in blacklist assert "log_stats" not in blacklist
@@ -339,18 +363,31 @@ def test_ambiguous_field_strict_raises():
split_kwargs({"deploy_config": "x"}, engine_cls=_AmbiguousEngine, strict=True) split_kwargs({"deploy_config": "x"}, engine_cls=_AmbiguousEngine, strict=True)
def test_ambiguous_field_non_strict_routes_to_orchestrator(caplog): def test_ambiguous_field_non_strict_routes_to_orchestrator():
"""strict=False logs ERROR but routes the ambiguous field to orchestrator.""" """strict=False logs ERROR but routes the ambiguous field to orchestrator."""
@dataclass @dataclass
class _AmbiguousEngine: class _AmbiguousEngine:
deploy_config: str | None = None deploy_config: str | None = None
with caplog.at_level(logging.ERROR, logger="vllm_omni.engine.arg_utils"): import io
vllm_logger = logging.getLogger("vllm")
orig_handlers = list(vllm_logger.handlers)
vllm_logger.handlers.clear()
buf = io.StringIO()
handler = logging.StreamHandler(buf)
handler.setLevel(logging.ERROR)
vllm_logger.addHandler(handler)
try:
orch, engine = split_kwargs({"deploy_config": "x"}, engine_cls=_AmbiguousEngine, strict=False) orch, engine = split_kwargs({"deploy_config": "x"}, engine_cls=_AmbiguousEngine, strict=False)
assert orch.deploy_config == "x" assert orch.deploy_config == "x"
assert "deploy_config" not in engine assert "deploy_config" not in engine
assert any("both OrchestratorArgs" in r.message for r in caplog.records) assert "both OrchestratorArgs" in buf.getvalue()
finally:
vllm_logger.handlers.clear()
for h in orig_handlers:
vllm_logger.addHandler(h)
# Sentinel-default precedence invariants (#3035) # Sentinel-default precedence invariants (#3035)
@@ -426,24 +463,20 @@ def test_help_text_preserves_default_after_nullify():
_OMNIENGINEARGS_USER_INPUT_FIELDS = frozenset( _OMNIENGINEARGS_USER_INPUT_FIELDS = frozenset(
{ {
"model_stage",
"model_arch", "model_arch",
"engine_output_type", "engine_output_type",
"hf_config_name", "hf_config_name",
"custom_process_next_stage_input_func", "custom_process_next_stage_input_func",
"subtalker_sampling_params", "subtalker_sampling_params",
"async_chunk",
"omni_kv_config", "omni_kv_config",
"quantization_config", "quantization_config",
"worker_type", "worker_type",
"task_type", "task_type",
"worker_cls", "worker_cls",
"enable_sleep_mode",
"omni_master_address", "omni_master_address",
"omni_master_port", "omni_master_port",
"stage_configs_path", "stage_configs_path",
"output_modalities", "output_modalities",
"log_stats",
"custom_pipeline_args", "custom_pipeline_args",
} }
) )
+3 -3
View File
@@ -250,7 +250,7 @@ def _collect_diffusion_records(input_dir: str) -> list[dict[str, Any]]:
def _sort_omni_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]: def _sort_omni_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
by_date_desc = sorted(records, key=lambda r: (r.get("date") or ""), reverse=True) by_date_desc = sorted(records, key=lambda r: r.get("date") or "", reverse=True)
return sorted( return sorted(
by_date_desc, by_date_desc,
key=lambda r: ( key=lambda r: (
@@ -264,8 +264,8 @@ def _sort_omni_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
def _sort_diffusion_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]: def _sort_diffusion_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
by_date_desc = sorted(records, key=lambda r: (r.get("date") or ""), reverse=True) by_date_desc = sorted(records, key=lambda r: r.get("date") or "", reverse=True)
return sorted(by_date_desc, key=lambda r: (r.get("test_name") or "")) return sorted(by_date_desc, key=lambda r: r.get("test_name") or "")
def _ensure_parent_dir(path: str) -> None: def _ensure_parent_dir(path: str) -> None:
+9 -9
View File
@@ -24,9 +24,11 @@ from vllm_omni.core.sched.omni_scheduling_coordinator import (
OmniSchedulingCoordinator, OmniSchedulingCoordinator,
uses_full_payload_input_coordinator, uses_full_payload_input_coordinator,
) )
from vllm_omni.core.sched.utils import omni_routed_experts_for_request
from vllm_omni.distributed.omni_connectors.transfer_adapter.chunk_transfer_adapter import ( from vllm_omni.distributed.omni_connectors.transfer_adapter.chunk_transfer_adapter import (
OmniChunkTransferAdapter, OmniChunkTransferAdapter,
) )
from vllm_omni.engine import OmniEngineCoreOutput
from vllm_omni.engine.serialization import deserialize_additional_information from vllm_omni.engine.serialization import deserialize_additional_information
from vllm_omni.outputs import OmniConnectorOutput from vllm_omni.outputs import OmniConnectorOutput
@@ -321,7 +323,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
req = self.requests.get(req_id) req = self.requests.get(req_id)
if req is not None and not req.is_finished(): if req is not None and not req.is_finished():
outputs[req.client_index].append( outputs[req.client_index].append(
EngineCoreOutput( OmniEngineCoreOutput(
request_id=req_id, request_id=req_id,
new_token_ids=[], new_token_ids=[],
kv_transfer_params={"kv_ready": True}, kv_transfer_params={"kv_ready": True},
@@ -417,11 +419,8 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
stopped = True stopped = True
if stopped: if stopped:
if ( if model_runner_output.routed_experts is not None:
model_runner_output.routed_experts_dict is not None routed_experts = omni_routed_experts_for_request(model_runner_output.routed_experts, request)
and req_id in model_runner_output.routed_experts_dict
):
routed_experts = model_runner_output.routed_experts_dict[req_id]
# Capture finish_reason BEFORE _handle_stopped_request, which may # Capture finish_reason BEFORE _handle_stopped_request, which may
# reset the status to WAITING for streaming requests that continue. # reset the status to WAITING for streaming requests that continue.
@@ -452,7 +451,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
stopped_preempted_reqs.add(request) stopped_preempted_reqs.add(request)
# Extract sample logprobs if needed. # Extract sample logprobs if needed.
if request.sampling_params is not None and request.sampling_params.logprobs is not None and logprobs: if request.sampling_params is not None and request.sampling_params.num_logprobs is not None and logprobs:
new_logprobs = logprobs.slice_request(req_index, len(new_token_ids)) new_logprobs = logprobs.slice_request(req_index, len(new_token_ids))
if num_nans_in_logits is not None and req_id in num_nans_in_logits: if num_nans_in_logits is not None and req_id in num_nans_in_logits:
@@ -463,7 +462,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
if new_token_ids or pooler_output is not None or kv_transfer_params or stopped: if new_token_ids or pooler_output is not None or kv_transfer_params or stopped:
# Add EngineCoreOutput for this Request. # Add EngineCoreOutput for this Request.
outputs[request.client_index].append( outputs[request.client_index].append(
EngineCoreOutput( OmniEngineCoreOutput(
request_id=req_id, request_id=req_id,
new_token_ids=new_token_ids, new_token_ids=new_token_ids,
finish_reason=finish_reason, finish_reason=finish_reason,
@@ -501,7 +500,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
self.finish_requests(failed_kv_load_req_ids, RequestStatus.FINISHED_ERROR) self.finish_requests(failed_kv_load_req_ids, RequestStatus.FINISHED_ERROR)
for request in requests: for request in requests:
outputs[request.client_index].append( outputs[request.client_index].append(
EngineCoreOutput( OmniEngineCoreOutput(
request_id=request.request_id, request_id=request.request_id,
new_token_ids=[], new_token_ids=[],
finish_reason=request.get_finished_reason(), finish_reason=request.get_finished_reason(),
@@ -633,6 +632,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
""" """
req_id = session.request_id req_id = session.request_id
self._new_prompt_len_snapshot[req_id] = len(update.prompt_token_ids) self._new_prompt_len_snapshot[req_id] = len(update.prompt_token_ids)
session.discard_latest_async_tokens = False
outstanding_async_tokens = getattr(session, "num_output_placeholders", 0) outstanding_async_tokens = getattr(session, "num_output_placeholders", 0)
if outstanding_async_tokens > 0: if outstanding_async_tokens > 0:
# Async scheduling may already have sampled the previous # Async scheduling may already have sampled the previous
@@ -29,9 +29,11 @@ from vllm_omni.core.sched.omni_scheduling_coordinator import (
uses_full_payload_input_coordinator, uses_full_payload_input_coordinator,
) )
from vllm_omni.core.sched.output import OmniCachedRequestData, OmniNewRequestData from vllm_omni.core.sched.output import OmniCachedRequestData, OmniNewRequestData
from vllm_omni.core.sched.utils import omni_routed_experts_for_request
from vllm_omni.distributed.omni_connectors.transfer_adapter.chunk_transfer_adapter import ( from vllm_omni.distributed.omni_connectors.transfer_adapter.chunk_transfer_adapter import (
OmniChunkTransferAdapter, OmniChunkTransferAdapter,
) )
from vllm_omni.engine import OmniEngineCoreOutput
from vllm_omni.outputs import OmniConnectorOutput, OmniModelRunnerOutput from vllm_omni.outputs import OmniConnectorOutput, OmniModelRunnerOutput
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -499,11 +501,8 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
stopped = True stopped = True
if stopped: if stopped:
if ( if model_runner_output.routed_experts is not None:
model_runner_output.routed_experts_dict is not None routed_experts = omni_routed_experts_for_request(model_runner_output.routed_experts, request)
and req_id in model_runner_output.routed_experts_dict
):
routed_experts = model_runner_output.routed_experts_dict[req_id]
finish_reason = request.get_finished_reason() finish_reason = request.get_finished_reason()
finished = self._handle_stopped_request(request) finished = self._handle_stopped_request(request)
if not finished: if not finished:
@@ -524,7 +523,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
stopped_running_reqs.add(request) stopped_running_reqs.add(request)
# Extract sample logprobs if needed. # Extract sample logprobs if needed.
if request.sampling_params is not None and request.sampling_params.logprobs is not None and logprobs: if request.sampling_params is not None and request.sampling_params.num_logprobs is not None and logprobs:
new_logprobs = logprobs.slice_request(req_index, len(new_token_ids)) new_logprobs = logprobs.slice_request(req_index, len(new_token_ids))
if new_token_ids and self.structured_output_manager.should_advance(request): if new_token_ids and self.structured_output_manager.should_advance(request):
@@ -543,7 +542,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
prompt_logprobs_tensors = prompt_logprobs_dict.get(req_id) prompt_logprobs_tensors = prompt_logprobs_dict.get(req_id)
if new_token_ids or pooler_output is not None or kv_transfer_params or stopped: if new_token_ids or pooler_output is not None or kv_transfer_params or stopped:
outputs[request.client_index].append( outputs[request.client_index].append(
EngineCoreOutput( OmniEngineCoreOutput(
request_id=req_id, request_id=req_id,
new_token_ids=new_token_ids, new_token_ids=new_token_ids,
finish_reason=finish_reason, finish_reason=finish_reason,
@@ -580,7 +579,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
getattr(request, "external_req_id", None), getattr(request, "external_req_id", None),
) )
outputs[request.client_index].append( outputs[request.client_index].append(
EngineCoreOutput( OmniEngineCoreOutput(
request_id=request.request_id, request_id=request.request_id,
new_token_ids=[], new_token_ids=[],
finish_reason=finish_reason, finish_reason=finish_reason,
@@ -607,7 +606,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
self.finish_requests(failed_kv_load_req_ids, RequestStatus.FINISHED_ERROR) self.finish_requests(failed_kv_load_req_ids, RequestStatus.FINISHED_ERROR)
for request in requests: for request in requests:
outputs[request.client_index].append( outputs[request.client_index].append(
EngineCoreOutput( OmniEngineCoreOutput(
request_id=request.request_id, request_id=request.request_id,
new_token_ids=[], new_token_ids=[],
finish_reason=request.get_finished_reason(), finish_reason=request.get_finished_reason(),
+23
View File
@@ -0,0 +1,23 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Shared utilities for omni schedulers."""
import numpy as np
from vllm.v1.outputs import RoutedExpertsLists
def omni_routed_experts_for_request(routed_experts: RoutedExpertsLists, request) -> np.ndarray | None:
"""Extract per-request routed experts from RoutedExpertsLists using slot_mapping.
Matches upstream RoutedExpertsManager.get() pattern — filters routing_data
rows whose slot_mapping entries belong to this request's block_table.
"""
if routed_experts is None:
return None
slots = getattr(request, "block_table", None)
if slots is None:
return None
slot_set = set(slots)
mask = np.isin(routed_experts.slot_mapping, list(slot_set))
data = routed_experts.routing_data[mask]
return data if data.size > 0 else None
+1 -1
View File
@@ -867,7 +867,7 @@ class DiffusionEngine:
worker_thread.join(timeout=10) worker_thread.join(timeout=10)
if worker_thread.is_alive(): if worker_thread.is_alive():
logger.warning( logger.warning(
"Worker thread did not terminate within 10s; scheduler and executor shutdown will be skipped." "Worker thread did not terminate within 10s; scheduler and executor shutdown will be deferred."
) )
return return
else: else:
+139 -49
View File
@@ -64,6 +64,7 @@ import contextlib
import errno import errno
import logging import logging
import os import os
import time
from collections.abc import Iterable, Iterator from collections.abc import Iterable, Iterator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -112,9 +113,43 @@ def _safe_repo_filename(model: str) -> str:
return model.replace("/", "__").replace(os.sep, "__") + ".lock" return model.replace("/", "__").replace(os.sep, "__") + ".lock"
def _dotfile_lock_acquire(lock_dir: str, model: str, timeout: float = 300.0, poll_interval: float = 0.5) -> bool:
"""Acquire an exclusive lock via atomic directory creation.
``os.makedirs(path, exist_ok=False)`` is atomic on POSIX filesystems
(including Lustre and NFS), making it a reliable fallback when
``fcntl.flock`` is unsupported (``ENOLCK`` on FSx/Lustre mounts
without the ``flock`` mount option).
Returns ``True`` if the lock was acquired, ``False`` on timeout.
The caller must call ``os.rmdir(lock_path)`` to release.
"""
lock_path = os.path.join(lock_dir, _safe_repo_filename(model) + ".dir")
deadline = time.monotonic() + timeout
while True:
try:
os.makedirs(lock_path, exist_ok=False)
logger.info("Acquired dotfile prefetch lock for %s at %s", model, lock_path)
return True
except FileExistsError:
if time.monotonic() >= deadline:
logger.warning(
"Timed out waiting for dotfile prefetch lock %s after %.0fs; proceeding unlocked",
lock_path,
timeout,
)
return False
time.sleep(poll_interval)
@contextlib.contextmanager @contextlib.contextmanager
def _repo_prefetch_lock(model: str) -> Iterator[None]: def _repo_prefetch_lock(model: str) -> Iterator[None]:
"""Hold an exclusive ``fcntl.flock`` keyed on the HF repo id. """Hold an exclusive lock keyed on the HF repo id.
Tries ``fcntl.flock`` first; falls back to an atomic directory-creation
dotfile lock when flock is unsupported (e.g. FSx for Lustre without the
``flock`` mount option, which returns ``ENOLCK``).
Why we still need a node-wide lock on top of ``snapshot_download``'s Why we still need a node-wide lock on top of ``snapshot_download``'s
own per-blob ``.lock`` files: own per-blob ``.lock`` files:
@@ -131,71 +166,93 @@ def _repo_prefetch_lock(model: str) -> Iterator[None]:
* Multiple unrelated processes on the same node (concurrent CI jobs, * Multiple unrelated processes on the same node (concurrent CI jobs,
multiple ``OmniServer`` instances, every ``DiffusionWorker`` multiple ``OmniServer`` instances, every ``DiffusionWorker``
subprocess from the multiproc executor) can enter this code path subprocess from the multiproc executor) can enter this code path
in parallel. ``flock`` on a per-repo file makes the whole in parallel. An exclusive lock makes the whole
``snapshot_download(...)`` call a true node-wide critical ``snapshot_download(...)`` call a true node-wide critical
section, so the *first* entrant fully populates the cache and section, so the *first* entrant fully populates the cache and
every subsequent ``from_pretrained`` sees a warm, complete tree. every subsequent ``from_pretrained`` sees a warm, complete tree.
Best-effort: if we cannot create / acquire the lock (no ``fcntl``, Best-effort: if we cannot acquire any lock (no ``fcntl``, read-only
read-only FS, NFS without flock support) we log and run the download FS, dotfile lock timeout) we log and run the download anyway. Worst
anyway. Worst case behaviour reverts to "snapshot_download + case behaviour reverts to "snapshot_download + transformers v5 race"
transformers v5 race" which is exactly the pre-fix state. which is exactly the pre-fix state.
""" """
lock_dir = None
lock_path = None
dotfile_held = None
fd = None
flock_held = False
# --- fcntl.flock path ---
try: try:
import fcntl # type: ignore[import-not-found] import fcntl # type: ignore[import-not-found]
except ImportError: # pragma: no cover - non-POSIX (Windows) except ImportError: # pragma: no cover - non-POSIX (Windows)
logger.debug("fcntl unavailable on this platform; running prefetch unlocked") fcntl = None
yield
return
try: if fcntl is not None:
lock_dir = _node_lock_dir()
except OSError as exc:
logger.warning("Could not allocate lock dir for prefetch of %s (%s); skipping flock", model, exc)
yield
return
lock_path = os.path.join(lock_dir, _safe_repo_filename(model))
try:
fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o644)
except OSError as exc:
logger.warning("Could not open prefetch lock %s (%s); proceeding without flock", lock_path, exc)
yield
return
locked = False
try:
try: try:
fcntl.flock(fd, fcntl.LOCK_EX) lock_dir = _node_lock_dir()
locked = True
logger.debug("Acquired prefetch flock for %s at %s", model, lock_path)
except OSError as exc: except OSError as exc:
# FS without flock support (ENOLCK / EOPNOTSUPP / EINVAL). logger.warning("Could not allocate lock dir for prefetch of %s (%s); skipping flock", model, exc)
# EACCES on some Lustre configs without ``flock`` mount opt. fcntl = None # force dotfile fallback
if exc.errno in (errno.ENOLCK, errno.EOPNOTSUPP, errno.EACCES, errno.EINVAL):
logger.warning( if fcntl is not None and lock_dir is not None:
"fcntl.flock not supported on '%s' (%s: %s); running prefetch unlocked", lock_path = os.path.join(lock_dir, _safe_repo_filename(model))
lock_path, try:
type(exc).__name__, fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o644)
exc, except OSError as exc:
) logger.warning("Could not open prefetch lock %s (%s); falling back to dotfile lock", lock_path, exc)
else: fd = None
raise
if fd is not None:
try:
fcntl.flock(fd, fcntl.LOCK_EX)
flock_held = True
logger.info("Acquired flock prefetch lock for %s at %s", model, lock_path)
except OSError as exc:
if exc.errno in (errno.ENOLCK, errno.EOPNOTSUPP, errno.EACCES, errno.EINVAL):
logger.warning(
"fcntl.flock not supported on '%s' (%s: %s); falling back to dotfile lock",
lock_path,
type(exc).__name__,
exc,
)
os.close(fd)
fd = None
else:
raise
# --- dotfile fallback ---
if not flock_held:
if lock_dir is None:
try:
lock_dir = _node_lock_dir()
except OSError as exc:
logger.warning("Could not allocate lock dir (%s); running prefetch unlocked", exc)
yield
return
if _dotfile_lock_acquire(lock_dir, model):
dotfile_held = os.path.join(lock_dir, _safe_repo_filename(model) + ".dir")
try:
yield yield
finally: finally:
if locked: if flock_held and fd is not None:
with contextlib.suppress(OSError): with contextlib.suppress(OSError):
fcntl.flock(fd, fcntl.LOCK_UN) fcntl.flock(fd, fcntl.LOCK_UN)
with contextlib.suppress(OSError): if fd is not None:
os.close(fd) with contextlib.suppress(OSError):
os.close(fd)
if dotfile_held is not None:
with contextlib.suppress(OSError):
os.rmdir(dotfile_held)
def prefetch_subfolders( def prefetch_subfolders(
model: str, model: str,
subfolders: Iterable[str], subfolders: Iterable[str],
*, *,
local_files_only: bool = False, local_files_only: bool | None = None,
include_root_metadata: bool = True, include_root_metadata: bool = True,
) -> None: ) -> None:
"""Materialise ``model``'s ``subfolders`` in the HF cache before loading. """Materialise ``model``'s ``subfolders`` in the HF cache before loading.
@@ -206,17 +263,21 @@ def prefetch_subfolders(
subfolders: Iterable of subfolder names (e.g. ``["text_encoder", subfolders: Iterable of subfolder names (e.g. ``["text_encoder",
"vae"]``) whose contents need to be fully present before any "vae"]``) whose contents need to be fully present before any
worker calls ``from_pretrained(subfolder=...)``. worker calls ``from_pretrained(subfolder=...)``.
local_files_only: When True, skip the prefetch entirely. The caller local_files_only: When ``True``, skip the prefetch entirely.
has explicitly promised the cache is already populated (as happens When ``None`` (default), auto-detect: skip if *model* is a
for local model checkouts), so hitting the network would defeat local directory, run otherwise.
the intent and may fail in air-gapped environments.
include_root_metadata: When True, also pull ``*.json`` at the repo include_root_metadata: When True, also pull ``*.json`` at the repo
root so ``model_index.json`` / ``config.json`` resolution during root so ``model_index.json`` / ``config.json`` resolution during
``from_pretrained`` also hits a warm cache. ``from_pretrained`` also hits a warm cache.
""" """
if local_files_only is None:
local_files_only = os.path.isdir(model)
if local_files_only or not model or os.path.isdir(model): if local_files_only or not model or os.path.isdir(model):
return return
logger.info("Prefetching %s subfolders: %s", model, list(subfolders))
try: try:
from huggingface_hub import snapshot_download from huggingface_hub import snapshot_download
except ImportError: # pragma: no cover - huggingface_hub is a hard dep except ImportError: # pragma: no cover - huggingface_hub is a hard dep
@@ -251,6 +312,7 @@ def prefetch_subfolders(
repo_id=model, repo_id=model,
allow_patterns=allow_patterns, allow_patterns=allow_patterns,
) )
logger.info("Prefetch complete for %s", model)
except Exception as exc: except Exception as exc:
# Best-effort: propagate only via logging. The subsequent # Best-effort: propagate only via logging. The subsequent
# ``from_pretrained`` call will raise a clearer, call-site-specific # ``from_pretrained`` call will raise a clearer, call-site-specific
@@ -312,3 +374,31 @@ def _looks_like_auth_error(exc: BaseException) -> bool:
# only load-bearing signal. # only load-bearing signal.
msg = str(exc).lower() msg = str(exc).lower()
return "401 client error" in msg or "403 client error" in msg or "gatedrepo" in msg return "401 client error" in msg or "403 client error" in msg or "gatedrepo" in msg
def retry_on_missing_shard(load_fn, *, max_retries: int = 3, base_delay: float = 5.0):
"""Call *load_fn* with retry on the transformers v5 shard-resolution race.
When the prefetch lock cannot be acquired (e.g. flock unsupported on
the filesystem and dotfile lock times out), ``from_pretrained`` may
still hit the ``cached_files`` race. This wrapper retries with
exponential backoff when the OSError message matches the specific
"does not appear to have a file named" pattern.
"""
for attempt in range(max_retries):
try:
return load_fn()
except OSError as exc:
if "does not appear to have a file" not in str(exc):
raise
if attempt == max_retries - 1:
raise
delay = base_delay * (attempt + 1)
logger.warning(
"from_pretrained failed with shard-resolution race (%s); retrying in %.1fs (attempt %d/%d)",
exc,
delay,
attempt + 1,
max_retries,
)
time.sleep(delay)
@@ -26,7 +26,7 @@ from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_qwenimage import DistributedAutoencoderKLQwenImage from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_qwenimage import DistributedAutoencoderKLQwenImage
from vllm_omni.diffusion.distributed.utils import get_local_device from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.model_loader.hub_prefetch import prefetch_subfolders from vllm_omni.diffusion.model_loader.hub_prefetch import prefetch_subfolders, retry_on_missing_shard
from vllm_omni.diffusion.models.dmd2 import DMD2PipelineMixin from vllm_omni.diffusion.models.dmd2 import DMD2PipelineMixin
from vllm_omni.diffusion.models.qwen_image.cfg_parallel import ( from vllm_omni.diffusion.models.qwen_image.cfg_parallel import (
QwenImageCFGParallelMixin, QwenImageCFGParallelMixin,
@@ -275,21 +275,22 @@ class QwenImagePipeline(nn.Module, QwenImageCFGParallelMixin, DiffusionPipelineP
self.device = get_local_device() self.device = get_local_device()
model = od_config.model model = od_config.model
# Check if model is a local path # Check if model is a local path
local_files_only = os.path.exists(model) local_files_only = os.path.isdir(model)
# See pipeline_qwen_image_edit_plus: guard against transformers v5 # See pipeline_qwen_image_edit_plus: guard against transformers v5
# multi-worker race on partial subfolder shard sets (Buildkite #1043). # multi-worker race on partial subfolder shard sets (Buildkite #1043).
prefetch_subfolders( prefetch_subfolders(
model, model,
["scheduler", "text_encoder", "vae", "tokenizer"], ["scheduler", "text_encoder", "vae", "tokenizer"],
local_files_only=local_files_only,
) )
self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
model, subfolder="scheduler", local_files_only=local_files_only model, subfolder="scheduler", local_files_only=local_files_only
) )
self.text_encoder = Qwen2_5_VLForConditionalGeneration.from_pretrained( self.text_encoder = retry_on_missing_shard(
model, subfolder="text_encoder", local_files_only=local_files_only lambda: Qwen2_5_VLForConditionalGeneration.from_pretrained(
model, subfolder="text_encoder", local_files_only=local_files_only
)
) )
# Qwen2.5-VL ships a vision tower that text-to-image does not use. # Qwen2.5-VL ships a vision tower that text-to-image does not use.
# Drop it while the model is still on CPU, before moving to GPU, so # Drop it while the model is still on CPU, before moving to GPU, so
@@ -27,7 +27,7 @@ from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.utils import get_local_device from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.model_loader.hub_prefetch import prefetch_subfolders from vllm_omni.diffusion.model_loader.hub_prefetch import prefetch_subfolders, retry_on_missing_shard
from vllm_omni.diffusion.models.interface import SupportImageInput from vllm_omni.diffusion.models.interface import SupportImageInput
from vllm_omni.diffusion.models.qwen_image.cfg_parallel import ( from vllm_omni.diffusion.models.qwen_image.cfg_parallel import (
QwenImageCFGParallelMixin, QwenImageCFGParallelMixin,
@@ -243,22 +243,23 @@ class QwenImageEditPipeline(nn.Module, SupportImageInput, QwenImageCFGParallelMi
model = od_config.model model = od_config.model
# Check if model is a local path # Check if model is a local path
local_files_only = os.path.exists(model) local_files_only = os.path.isdir(model)
# See pipeline_qwen_image_edit_plus: guard against transformers v5 # See pipeline_qwen_image_edit_plus: guard against transformers v5
# multi-worker race on partial subfolder shard sets (Buildkite #1043). # multi-worker race on partial subfolder shard sets (Buildkite #1043).
prefetch_subfolders( prefetch_subfolders(
model, model,
["scheduler", "text_encoder", "vae", "tokenizer", "processor"], ["scheduler", "text_encoder", "vae", "tokenizer", "processor"],
local_files_only=local_files_only,
) )
self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
model, subfolder="scheduler", local_files_only=local_files_only model, subfolder="scheduler", local_files_only=local_files_only
) )
self.text_encoder = Qwen2_5_VLForConditionalGeneration.from_pretrained( self.text_encoder = retry_on_missing_shard(
model, subfolder="text_encoder", local_files_only=local_files_only lambda: Qwen2_5_VLForConditionalGeneration.from_pretrained(
).to(self.device) model, subfolder="text_encoder", local_files_only=local_files_only
).to(self.device)
)
self.vae = AutoencoderKLQwenImage.from_pretrained(model, subfolder="vae", local_files_only=local_files_only).to( self.vae = AutoencoderKLQwenImage.from_pretrained(model, subfolder="vae", local_files_only=local_files_only).to(
self.device self.device
@@ -266,8 +267,8 @@ class QwenImageEditPipeline(nn.Module, SupportImageInput, QwenImageCFGParallelMi
transformer_kwargs = get_transformer_config_kwargs(od_config.tf_model_config, QwenImageTransformer2DModel) transformer_kwargs = get_transformer_config_kwargs(od_config.tf_model_config, QwenImageTransformer2DModel)
self.transformer = QwenImageTransformer2DModel(od_config=od_config, **transformer_kwargs) self.transformer = QwenImageTransformer2DModel(od_config=od_config, **transformer_kwargs)
self.tokenizer = Qwen2Tokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only) self.tokenizer = Qwen2Tokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only)
self.processor = Qwen2VLProcessor.from_pretrained( self.processor = retry_on_missing_shard(
model, subfolder="processor", local_files_only=local_files_only lambda: Qwen2VLProcessor.from_pretrained(model, subfolder="processor", local_files_only=local_files_only)
) )
self.stage = None self.stage = None
@@ -25,7 +25,7 @@ from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.utils import get_local_device from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.model_loader.hub_prefetch import prefetch_subfolders from vllm_omni.diffusion.model_loader.hub_prefetch import prefetch_subfolders, retry_on_missing_shard
from vllm_omni.diffusion.model_metadata import QWEN_IMAGE_EDIT_PLUS_MAX_INPUT_IMAGES from vllm_omni.diffusion.model_metadata import QWEN_IMAGE_EDIT_PLUS_MAX_INPUT_IMAGES
from vllm_omni.diffusion.models.interface import SupportImageInput from vllm_omni.diffusion.models.interface import SupportImageInput
from vllm_omni.diffusion.models.qwen_image.cfg_parallel import ( from vllm_omni.diffusion.models.qwen_image.cfg_parallel import (
@@ -208,7 +208,7 @@ class QwenImageEditPlusPipeline(
model = od_config.model model = od_config.model
# Check if model is a local path # Check if model is a local path
local_files_only = os.path.exists(model) local_files_only = os.path.isdir(model)
# Defend against a transformers v5 multi-worker race where a peer # Defend against a transformers v5 multi-worker race where a peer
# worker's partially-written shard set makes our from_pretrained # worker's partially-written shard set makes our from_pretrained
@@ -219,15 +219,16 @@ class QwenImageEditPlusPipeline(
prefetch_subfolders( prefetch_subfolders(
model, model,
["scheduler", "text_encoder", "vae", "tokenizer", "processor"], ["scheduler", "text_encoder", "vae", "tokenizer", "processor"],
local_files_only=local_files_only,
) )
self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
model, subfolder="scheduler", local_files_only=local_files_only model, subfolder="scheduler", local_files_only=local_files_only
) )
self.text_encoder = Qwen2_5_VLForConditionalGeneration.from_pretrained( self.text_encoder = retry_on_missing_shard(
model, subfolder="text_encoder", local_files_only=local_files_only lambda: Qwen2_5_VLForConditionalGeneration.from_pretrained(
).to(self.device) model, subfolder="text_encoder", local_files_only=local_files_only
).to(self.device)
)
self.vae = AutoencoderKLQwenImage.from_pretrained(model, subfolder="vae", local_files_only=local_files_only).to( self.vae = AutoencoderKLQwenImage.from_pretrained(model, subfolder="vae", local_files_only=local_files_only).to(
self.device self.device
@@ -236,8 +237,8 @@ class QwenImageEditPlusPipeline(
transformer_kwargs = get_transformer_config_kwargs(od_config.tf_model_config, QwenImageTransformer2DModel) transformer_kwargs = get_transformer_config_kwargs(od_config.tf_model_config, QwenImageTransformer2DModel)
self.transformer = QwenImageTransformer2DModel(od_config=od_config, **transformer_kwargs) self.transformer = QwenImageTransformer2DModel(od_config=od_config, **transformer_kwargs)
self.tokenizer = Qwen2Tokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only) self.tokenizer = Qwen2Tokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only)
self.processor = Qwen2VLProcessor.from_pretrained( self.processor = retry_on_missing_shard(
model, subfolder="processor", local_files_only=local_files_only lambda: Qwen2VLProcessor.from_pretrained(model, subfolder="processor", local_files_only=local_files_only)
) )
self.stage = None self.stage = None
@@ -24,7 +24,7 @@ from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.utils import get_local_device from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.model_loader.hub_prefetch import prefetch_subfolders from vllm_omni.diffusion.model_loader.hub_prefetch import prefetch_subfolders, retry_on_missing_shard
from vllm_omni.diffusion.models.interface import SupportImageInput from vllm_omni.diffusion.models.interface import SupportImageInput
from vllm_omni.diffusion.models.qwen_image.autoencoder_kl_qwenimage import ( from vllm_omni.diffusion.models.qwen_image.autoencoder_kl_qwenimage import (
AutoencoderKLQwenImage, AutoencoderKLQwenImage,
@@ -216,29 +216,30 @@ class QwenImageLayeredPipeline(nn.Module, SupportImageInput, QwenImageCFGParalle
self.device = get_local_device() self.device = get_local_device()
model = od_config.model model = od_config.model
# Check if model is a local path # Check if model is a local path
local_files_only = os.path.exists(model) local_files_only = os.path.isdir(model)
# See pipeline_qwen_image_edit_plus: guard against transformers v5 # See pipeline_qwen_image_edit_plus: guard against transformers v5
# multi-worker race on partial subfolder shard sets (Buildkite #1043). # multi-worker race on partial subfolder shard sets (Buildkite #1043).
prefetch_subfolders( prefetch_subfolders(
model, model,
["scheduler", "text_encoder", "vae", "tokenizer", "processor"], ["scheduler", "text_encoder", "vae", "tokenizer", "processor"],
local_files_only=local_files_only,
) )
# modules keep same as transformers & diffusers # modules keep same as transformers & diffusers
self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
model, subfolder="scheduler", local_files_only=local_files_only model, subfolder="scheduler", local_files_only=local_files_only
) )
self.text_encoder = Qwen2_5_VLForConditionalGeneration.from_pretrained( self.text_encoder = retry_on_missing_shard(
model, subfolder="text_encoder", local_files_only=local_files_only lambda: Qwen2_5_VLForConditionalGeneration.from_pretrained(
).to(self.device) model, subfolder="text_encoder", local_files_only=local_files_only
).to(self.device)
)
self.vae = AutoencoderKLQwenImage.from_pretrained(model, subfolder="vae", local_files_only=local_files_only).to( self.vae = AutoencoderKLQwenImage.from_pretrained(model, subfolder="vae", local_files_only=local_files_only).to(
self.device self.device
) )
self.tokenizer = Qwen2Tokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only) self.tokenizer = Qwen2Tokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only)
self.processor = Qwen2VLProcessor.from_pretrained( self.processor = retry_on_missing_shard(
model, subfolder="processor", local_files_only=local_files_only lambda: Qwen2VLProcessor.from_pretrained(model, subfolder="processor", local_files_only=local_files_only)
) )
# modules re-implemented for vLLM-Omni # modules re-implemented for vLLM-Omni
@@ -43,17 +43,20 @@ class OmniCoordinator:
heartbeat_timeout: Seconds before a replica is considered heartbeat_timeout: Seconds before a replica is considered
unhealthy if no heartbeat / update is received. unhealthy if no heartbeat / update is received.
""" """
self._router_zmq_addr = router_zmq_addr
self._pub_zmq_addr = pub_zmq_addr
self._heartbeat_timeout = heartbeat_timeout self._heartbeat_timeout = heartbeat_timeout
# Dedicated ZMQ context for this coordinator instance. # Dedicated ZMQ context for this coordinator instance.
self._ctx = zmq.Context() self._ctx = zmq.Context()
self._router = self._ctx.socket(zmq.ROUTER) self._router = self._ctx.socket(zmq.ROUTER)
self._router.bind(self._router_zmq_addr) self._router.bind(router_zmq_addr)
# Recover the actual bound address (port=0 → real port).
self.router_zmq_addr = self._router.getsockopt_string(zmq.LAST_ENDPOINT)
self._pub_zmq_addr = pub_zmq_addr # keep original for internal use
self._pub = self._ctx.socket(zmq.PUB) self._pub = self._ctx.socket(zmq.PUB)
self._pub.bind(self._pub_zmq_addr) self._pub.bind(pub_zmq_addr)
# Recover the actual bound address (port=0 → real port).
self.pub_zmq_addr = self._pub.getsockopt_string(zmq.LAST_ENDPOINT)
self._replicas: dict[str, ReplicaInfo] = {} self._replicas: dict[str, ReplicaInfo] = {}
self._lock = threading.Lock() self._lock = threading.Lock()
+46 -1
View File
@@ -144,7 +144,8 @@ class OmniEngineArgs(EngineArgs):
force_cutlass_fp8: bool | None = None force_cutlass_fp8: bool | None = None
worker_type: str | None = None worker_type: str | None = None
task_type: str | None = None task_type: str | None = None
worker_cls: str = None worker_cls: str = None # type: ignore[assignment] # Upstream default is "auto"; omni resolves
# in __post_init__ based on worker_type (ar/generation), so None is safe here.
enable_sleep_mode: bool = False enable_sleep_mode: bool = False
omni: bool = False omni: bool = False
@@ -444,6 +445,48 @@ class OrchestratorArgs:
# === Pre-built Objects === # === Pre-built Objects ===
parallel_config: Any = None parallel_config: Any = None
# === Diffusion model config ===
num_gpus: int | None = None
model_class_name: str | None = None
diffusion_load_format: str | None = None
diffusers_load_kwargs: str = "{}"
diffusers_call_kwargs: str = "{}"
ulysses_degree: int | None = None
ulysses_mode: str = "strict"
ring_degree: int | None = None
diffusion_quantization_config: str | None = None
use_hsdp: bool = False
hsdp_shard_size: int = -1
hsdp_replicate_size: int = 1
diffusion_attention_backend: str | None = None
diffusion_attention_config: str | None = None
cache_backend: str = "none"
cache_config: str | None = None
enable_cache_dit_summary: bool = False
step_execution: bool = False
vae_use_slicing: bool = False
vae_use_tiling: bool = False
enable_multithread_weight_load: bool = True
num_weight_load_threads: int = 4
enable_cpu_offload: bool = False
enable_layerwise_offload: bool = False
boundary_ratio: float | None = None
flow_shift: float | None = None
diffusion_kv_cache_dtype: str | None = None
diffusion_kv_cache_skip_steps: str | None = None
diffusion_kv_cache_skip_layers: str | None = None
cfg_parallel_size: int = 1
vae_patch_parallel_size: int = 1
default_sampling_params: str | None = None
max_generated_image_size: int | None = None
tts_max_instructions_length: int | None = None
enable_diffusion_pipeline_profiler: bool = False
enable_ar_profiler: bool = False
auxiliary_text_encoder: str | None = None
log_file: str | None = None
replica_id: int | None = None
omni_replica_address: str | None = None
# === Multi-stage guards === # === Multi-stage guards ===
# --tokenizer is captured by the orchestrator and forwarded to stages # --tokenizer is captured by the orchestrator and forwarded to stages
# only when the stage does not define tokenizer/tokenizer_subdir itself. # only when the stage does not define tokenizer/tokenizer_subdir itself.
@@ -459,6 +502,8 @@ SHARED_FIELDS: frozenset[str] = frozenset(
"stage_id", # orch: route (headless); engine: identity "stage_id", # orch: route (headless); engine: identity
"log_stats", # both want the flag "log_stats", # both want the flag
"stage_configs_path", # orch: load legacy YAML; engine: may reference for validation "stage_configs_path", # orch: load legacy YAML; engine: may reference for validation
"async_chunk", # orch: read from CLI, redistribute; engine: per-stage flag
"tokenizer", # orch: detect model type; engine: tokenization
} }
) )
+1 -1
View File
@@ -2124,7 +2124,7 @@ class AsyncOmniEngine:
kwargs.get("diffusion_attention_config"), kwargs.get("diffusion_attention_config"),
attention_backend=kwargs.get("diffusion_attention_backend"), attention_backend=kwargs.get("diffusion_attention_backend"),
) )
quantization_config = kwargs.get("quantization_config") quantization_config = kwargs.get("diffusion_quantization_config")
if quantization_config is not None: if quantization_config is not None:
if ( if (
not hasattr(cfg.engine_args, "quantization_config") not hasattr(cfg.engine_args, "quantization_config")
+4 -19
View File
@@ -1,11 +1,7 @@
from typing import Any from typing import Any
import numpy as np
import torch import torch
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
split_routed_experts,
)
from vllm.outputs import PoolingRequestOutput from vllm.outputs import PoolingRequestOutput
from vllm.sampling_params import RequestOutputKind from vllm.sampling_params import RequestOutputKind
from vllm.tokenizers import TokenizerLike from vllm.tokenizers import TokenizerLike
@@ -172,7 +168,6 @@ class OmniRequestState(RequestState):
finish_reason: FinishReason | None, finish_reason: FinishReason | None,
stop_reason: int | str | None, stop_reason: int | str | None,
kv_transfer_params: dict[str, Any] | None = None, kv_transfer_params: dict[str, Any] | None = None,
routed_experts: np.ndarray | None = None,
) -> OmniRequestOutput | PoolingRequestOutput | None: ) -> OmniRequestOutput | PoolingRequestOutput | None:
"""Create a request output from generation results. """Create a request output from generation results.
@@ -199,7 +194,6 @@ class OmniRequestState(RequestState):
finish_reason, finish_reason,
stop_reason, stop_reason,
kv_transfer_params, kv_transfer_params,
routed_experts,
) )
finished = finish_reason is not None finished = finish_reason is not None
@@ -234,16 +228,7 @@ class OmniRequestState(RequestState):
external_req_id = self.external_req_id external_req_id = self.external_req_id
# Split routing data into prompt and generation portions, matching output = self._new_completion_output(new_token_ids, finish_reason, stop_reason)
# upstream make_request_output behaviour.
prompt_routed_experts = None
gen_routed_experts = None
if routed_experts is not None:
prompt_len = len(self.prompt_token_ids) if self.prompt_token_ids else 0
num_gen = self.detokenizer.num_output_tokens() if self.detokenizer is not None else None
prompt_routed_experts, gen_routed_experts = split_routed_experts(routed_experts, prompt_len, num_gen)
output = self._new_completion_output(new_token_ids, finish_reason, stop_reason, gen_routed_experts)
if self.parent_req is None: if self.parent_req is None:
outputs = [output] outputs = [output]
@@ -258,7 +243,6 @@ class OmniRequestState(RequestState):
outputs, outputs,
finished, finished,
kv_transfer_params, kv_transfer_params,
prompt_routed_experts,
) )
def _new_completion_output( def _new_completion_output(
@@ -266,10 +250,11 @@ class OmniRequestState(RequestState):
token_ids: list[int], token_ids: list[int],
finish_reason: FinishReason | None, finish_reason: FinishReason | None,
stop_reason: int | str | None, stop_reason: int | str | None,
routed_experts: np.ndarray | None = None,
) -> Any: ) -> Any:
# Reuse base text/logprobs logic, then annotate with pooling_result. # Reuse base text/logprobs logic, then annotate with pooling_result.
base_output = super()._new_completion_output(token_ids, finish_reason, stop_reason, routed_experts) # Note: upstream _new_completion_output no longer accepts routed_experts
# as a parameter; it reads from ``self.routed_experts_chunks`` internally.
base_output = super()._new_completion_output(token_ids, finish_reason, stop_reason)
# Inter-stage processors need the full cumulative token sequence. # Inter-stage processors need the full cumulative token sequence.
# In DELTA mode, base_output.token_ids only has the latest step's # In DELTA mode, base_output.token_ids only has the latest step's
@@ -101,6 +101,11 @@ class StageEngineCoreProc(EngineCoreProc):
set_death_signal(signal.SIGTERM) set_death_signal(signal.SIGTERM)
set_process_title(f"StageEngineCoreProc_{stage_label}_replica{omni_replica_id}_DP{dp_rank}") set_process_title(f"StageEngineCoreProc_{stage_label}_replica{omni_replica_id}_DP{dp_rank}")
decorate_logs() decorate_logs()
# Workaround for flashinfer/jit-cache version mismatch in CI.
# The parent process handles this gracefully via ring_globals.py,
# but the subprocess hits an unprotected import in TopKTopPSampler.
# Setting this env var allows the same graceful fallback to work.
os.environ.setdefault("FLASHINFER_DISABLE_VERSION_CHECK", "1")
os.environ["VLLM_OMNI_REPLICA_ID"] = str(max(int(omni_replica_id), 0)) os.environ["VLLM_OMNI_REPLICA_ID"] = str(max(int(omni_replica_id), 0))
engine_core = StageEngineCoreProc( engine_core = StageEngineCoreProc(
+45
View File
@@ -860,6 +860,45 @@ def build_stage0_input_processor(stage_vllm_config: Any) -> InputProcessor:
return input_processor return input_processor
def _cleanup_stale_lock_if_dead(lock_file: str) -> bool:
"""If *lock_file* exists and its recorded PID is dead, unlink the file.
Returns ``True`` if the stale lock was cleaned up (caller should retry),
``False`` otherwise (lock holder appears alive, or file could not be read).
"""
try:
with open(lock_file) as fh:
content = fh.read().strip()
if not content:
return False
pid = int(content)
except (OSError, ValueError):
return False
# Check whether the PID is still alive.
try:
os.kill(pid, 0)
except ProcessLookupError:
# PID does not exist — stale lock.
logger.info(
"Removing stale device lock %s (PID %s is dead)",
lock_file,
pid,
)
try:
os.unlink(lock_file)
return True
except OSError:
logger.debug("Failed to unlink stale lock %s", lock_file)
return False
except PermissionError:
# PID exists but we cannot signal it (different user) — treat as alive.
return False
# PID is alive — legitimate lock holder.
return False
def acquire_device_locks( def acquire_device_locks(
stage_id: int, stage_id: int,
engine_args_dict: dict[str, Any], engine_args_dict: dict[str, Any],
@@ -938,6 +977,7 @@ def acquire_device_locks(
for device_id in devices_to_lock: for device_id in devices_to_lock:
lock_file = f"/tmp/vllm_omni_device_{device_id}_init.lock" lock_file = f"/tmp/vllm_omni_device_{device_id}_init.lock"
lock_acquired = False lock_acquired = False
already_cleaned_stale = False # only try stale cleanup once per device
while not lock_acquired: while not lock_acquired:
try: try:
@@ -952,6 +992,11 @@ def acquire_device_locks(
logger.debug("Acquired exclusive lock for device %s", device_id) logger.debug("Acquired exclusive lock for device %s", device_id)
except BlockingIOError: except BlockingIOError:
os.close(lock_fd) os.close(lock_fd)
# Detect and clean stale locks from dead processes.
if not already_cleaned_stale:
already_cleaned_stale = True
if _cleanup_stale_lock_if_dead(lock_file):
continue # retry flock immediately
if time.time() - wait_start > stage_init_timeout: if time.time() - wait_start > stage_init_timeout:
logger.warning( logger.warning(
"Timeout waiting for device %s initialization lock, proceeding anyway", "Timeout waiting for device %s initialization lock, proceeding anyway",
+7
View File
@@ -22,6 +22,7 @@ from vllm_omni.engine.stage_client import (
StagePoolDiffusionClient, StagePoolDiffusionClient,
StagePoolLLMClient, StagePoolLLMClient,
) )
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.metrics.stats import StageRequestStats as StageRequestMetrics from vllm_omni.metrics.stats import StageRequestStats as StageRequestMetrics
from vllm_omni.metrics.stats import StageStats from vllm_omni.metrics.stats import StageStats
from vllm_omni.metrics.utils import count_tokens_from_outputs from vllm_omni.metrics.utils import count_tokens_from_outputs
@@ -533,6 +534,10 @@ class StagePool:
) -> int: ) -> int:
"""Submit a stage-entry request into this pool.""" """Submit a stage-entry request into this pool."""
params = params_override if params_override is not None else req_state.sampling_params_list[self.stage_id] params = params_override if params_override is not None else req_state.sampling_params_list[self.stage_id]
# Convert plain vllm SamplingParams for single-stage diffusion models
# that receive sampling params from the user/caller directly.
if self.stage_type == "diffusion" and not isinstance(params, OmniDiffusionSamplingParams):
params = OmniDiffusionSamplingParams()
submit_kwargs = dict(submit_kwargs or {}) submit_kwargs = dict(submit_kwargs or {})
if self.stage_type == "diffusion": if self.stage_type == "diffusion":
replica_id = await self._pick_or_select( replica_id = await self._pick_or_select(
@@ -593,6 +598,8 @@ class StagePool:
) -> int: ) -> int:
"""Submit a streaming update to an already admitted request.""" """Submit a streaming update to an already admitted request."""
params = req_state.sampling_params_list[self.stage_id] params = req_state.sampling_params_list[self.stage_id]
if self.stage_type == "diffusion" and not isinstance(params, OmniDiffusionSamplingParams):
params = OmniDiffusionSamplingParams()
replica_id = self.get_bound_replica_id(request_id) replica_id = self.get_bound_replica_id(request_id)
if replica_id is None or self.clients[replica_id] is None: if replica_id is None or self.clients[replica_id] is None:
replica_id = await self._pick_or_select(request_id) replica_id = await self._pick_or_select(request_id)
+1 -1
View File
@@ -436,7 +436,7 @@ class OmniServeCommand(CLISubcommand):
"Equivalent to setting DiffusionParallelConfig.ring_degree.", "Equivalent to setting DiffusionParallelConfig.ring_degree.",
) )
omni_config_group.add_argument( omni_config_group.add_argument(
"--quantization-config", "--diffusion-quantization-config",
type=json.loads, type=json.loads,
default=None, default=None,
help=( help=(
+1 -1
View File
@@ -463,7 +463,7 @@ class OmniBase(PDDisaggregationMixin):
peak_memory_mb=peak_memory_mb, peak_memory_mb=peak_memory_mb,
) )
def shutdown(self) -> None: def shutdown(self, timeout: float | None = None) -> None:
logger.info("[%s] Shutting down", self.__class__.__name__) logger.info("[%s] Shutting down", self.__class__.__name__)
self._shutdown_base() self._shutdown_base()
+23 -6
View File
@@ -55,13 +55,8 @@ from vllm.entrypoints.openai.engine.protocol import (
from vllm.entrypoints.openai.models.protocol import BaseModelPath from vllm.entrypoints.openai.models.protocol import BaseModelPath
from vllm.entrypoints.openai.models.serving import OpenAIServingModels from vllm.entrypoints.openai.models.serving import OpenAIServingModels
from vllm.entrypoints.openai.orca_metrics import metrics_header from vllm.entrypoints.openai.orca_metrics import metrics_header
from vllm.entrypoints.openai.realtime.serving import OpenAIServingRealtime
from vllm.entrypoints.openai.responses.serving import OpenAIServingResponses from vllm.entrypoints.openai.responses.serving import OpenAIServingResponses
from vllm.entrypoints.openai.server_utils import get_uvicorn_log_config from vllm.entrypoints.openai.server_utils import get_uvicorn_log_config
from vllm.entrypoints.openai.speech_to_text.serving import (
OpenAIServingTranscription,
OpenAIServingTranslation,
)
from vllm.entrypoints.openai.utils import validate_json_request from vllm.entrypoints.openai.utils import validate_json_request
from vllm.entrypoints.pooling.classify.serving import ServingClassification from vllm.entrypoints.pooling.classify.serving import ServingClassification
from vllm.entrypoints.pooling.embed.serving import ServingEmbedding as OpenAIServingEmbedding from vllm.entrypoints.pooling.embed.serving import ServingEmbedding as OpenAIServingEmbedding
@@ -74,6 +69,13 @@ from vllm.entrypoints.serve.disagg.serving import ServingTokens
from vllm.entrypoints.serve.instrumentator.basic import base from vllm.entrypoints.serve.instrumentator.basic import base
from vllm.entrypoints.serve.render.serving import OpenAIServingRender from vllm.entrypoints.serve.render.serving import OpenAIServingRender
from vllm.entrypoints.serve.tokenize.serving import OpenAIServingTokenization from vllm.entrypoints.serve.tokenize.serving import OpenAIServingTokenization
from vllm.entrypoints.speech_to_text.realtime.serving import OpenAIServingRealtime
from vllm.entrypoints.speech_to_text.transcription.serving import (
OpenAIServingTranscription,
)
from vllm.entrypoints.speech_to_text.translation.serving import (
OpenAIServingTranslation,
)
from vllm.entrypoints.utils import ( from vllm.entrypoints.utils import (
create_error_response, create_error_response,
load_aware_call, load_aware_call,
@@ -355,7 +357,7 @@ async def omni_run_server(args, **uvicorn_kwargs) -> None:
warnings_module.filterwarnings("ignore", message=".*PydanticSerializationUnexpectedValue.*", category=UserWarning) warnings_module.filterwarnings("ignore", message=".*PydanticSerializationUnexpectedValue.*", category=UserWarning)
# Add process-specific prefix to stdout and stderr. # Add process-specific prefix to stdout and stderr.
decorate_logs("APIServer") decorate_logs("APIServer", skip_if_decorated=True)
listen_address, sock = setup_openai_server(args) listen_address, sock = setup_openai_server(args)
@@ -646,6 +648,19 @@ async def omni_init_app_state(
state.vllm_config = vllm_config state.vllm_config = vllm_config
# Propagate enable_in_reasoning to the API-server process. The engine core
# runs in a separate process, so the contextvar that backs
# `get_current_vllm_config_or_none()` is None on this stack. Tool parsers
# call `get_enable_structured_outputs_in_reasoning()` during request
# handling and need to see the real flag, otherwise they silently fall
# back to False and mismatch the engine-side bitmask gating.
if vllm_config is not None:
from vllm.tool_parsers.structural_tag_registry import (
set_enable_structured_outputs_in_reasoning,
)
set_enable_structured_outputs_in_reasoning(vllm_config.structured_outputs_config.enable_in_reasoning)
# Get supported tasks # Get supported tasks
supported_tasks: set[str] = {"generate"} supported_tasks: set[str] = {"generate"}
if hasattr(engine_client, "get_supported_tasks"): if hasattr(engine_client, "get_supported_tasks"):
@@ -1531,6 +1546,7 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request)
prompt=request.prompt, prompt=request.prompt,
extra_body=extra_body, extra_body=extra_body,
request_id=f"img_gen-{random_uuid()}", request_id=f"img_gen-{random_uuid()}",
raw_request=raw_request,
) )
if isinstance(generation_result, ErrorResponse): if isinstance(generation_result, ErrorResponse):
return JSONResponse( return JSONResponse(
@@ -1937,6 +1953,7 @@ async def edit_images(
output_format=output_format, output_format=output_format,
output_compression=output_compression, output_compression=output_compression,
size=size_str, size=size_str,
raw_request=raw_request,
) )
if stream and not isinstance(generation_result, ErrorResponse): if stream and not isinstance(generation_result, ErrorResponse):
return StreamingResponse( return StreamingResponse(
@@ -9,8 +9,8 @@ from uuid import uuid4
import numpy as np import numpy as np
from vllm.entrypoints.openai.engine.protocol import UsageInfo from vllm.entrypoints.openai.engine.protocol import UsageInfo
from vllm.entrypoints.openai.realtime.connection import RealtimeConnection as VllmRealtimeConnection from vllm.entrypoints.speech_to_text.realtime.connection import RealtimeConnection as VllmRealtimeConnection
from vllm.entrypoints.openai.realtime.protocol import TranscriptionDelta, TranscriptionDone from vllm.entrypoints.speech_to_text.realtime.protocol import TranscriptionDelta, TranscriptionDone
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm_omni.entrypoints.async_omni import AsyncOmni from vllm_omni.entrypoints.async_omni import AsyncOmni
+23 -1
View File
@@ -2574,6 +2574,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
output_format: str = "png", output_format: str = "png",
output_compression: int = 100, output_compression: int = 100,
size: str = "auto", size: str = "auto",
raw_request: Request | None = None,
) -> tuple[list[Image.Image], dict[str, Any], float, str | None] | ErrorResponse | AsyncIterator[str]: ) -> tuple[list[Image.Image], dict[str, Any], float, str | None] | ErrorResponse | AsyncIterator[str]:
"""Generate diffusion images and return raw images plus generation stats.""" """Generate diffusion images and return raw images plus generation stats."""
if request_id is None: if request_id is None:
@@ -2635,6 +2636,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
output_format=output_format, output_format=output_format,
output_compression=output_compression, output_compression=output_compression,
size=size, size=size,
raw_request=raw_request,
) )
result = None result = None
@@ -2659,6 +2661,16 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
peak_memory_mb = result.peak_memory_mb peak_memory_mb = result.peak_memory_mb
cot_output = None cot_output = None
req_out = getattr(result, "request_output", None)
if req_out:
prompt_obj = getattr(req_out, "prompt", None)
if isinstance(prompt_obj, dict):
extra = prompt_obj.get("extra", {})
if isinstance(extra, dict):
ar_text = extra.get("ar_generated_text")
if isinstance(ar_text, str) and ar_text.strip():
cot_output = ar_text
req_out = getattr(result, "request_output", None) req_out = getattr(result, "request_output", None)
if req_out: if req_out:
prompt_obj = getattr(req_out, "prompt", None) prompt_obj = getattr(req_out, "prompt", None)
@@ -2679,6 +2691,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
output_format: str, output_format: str,
output_compression: int, output_compression: int,
size: str, size: str,
raw_request: Request | None = None,
) -> AsyncIterator[str]: ) -> AsyncIterator[str]:
"""Yield image edit SSE chunks from multi-stage diffusion outputs.""" """Yield image edit SSE chunks from multi-stage diffusion outputs."""
created = int(time.time()) created = int(time.time())
@@ -2730,7 +2743,16 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
logger.error("EngineDeadError during streaming image edit: %s", exc) logger.error("EngineDeadError during streaming image edit: %s", exc)
data = self.create_streaming_error_response(exc) data = self.create_streaming_error_response(exc)
yield f"data: {data}\n\n" yield f"data: {data}\n\n"
terminate_if_errored(engine=self.engine_client) if raw_request is not None:
terminate_if_errored(
server=raw_request.app.state.server,
engine=self.engine_client,
)
else:
logger.warning(
"[OmniOpenAIServingChat] Engine dead during streaming image edit, "
"but cannot terminate server (no raw_request context)."
)
except Exception as exc: except Exception as exc:
logger.exception("Streaming image edit failed: %s", exc) logger.exception("Streaming image edit failed: %s", exc)
chunk = ImageEditStreamError( chunk = ImageEditStreamError(
+18
View File
@@ -114,6 +114,24 @@ def _map_device_list(stage_id: int, device_list: list[str], visible_device_list:
return list(device_list) return list(device_list)
logical_ids = [int(device) for device in device_list] logical_ids = [int(device) for device in device_list]
# If ALL logical device IDs are >= num_visible, none can be 0-based indices
# into the visible list — they must be physical device IDs. This happens
# during multi-replica stage initialization where split_devices_for_replicas
# assigns physical device IDs (e.g. "2") that are not part of the current
# process's CUDA_VISIBLE_DEVICES but are valid physical GPUs. Pass them
# through directly so the replica subprocess inherits the correct device.
if logical_ids and all(d >= num_visible for d in logical_ids):
logger.warning(
"Stage %s has device IDs %s, none of which are < the visible device count %d "
"(visible=%s). Treating them as physical device IDs and passing through.",
stage_id,
device_list,
num_visible,
visible_device_list,
)
return list(device_list)
mapped_devices = [visible_device_list[idx] for idx in logical_ids if idx < num_visible] mapped_devices = [visible_device_list[idx] for idx in logical_ids if idx < num_visible]
mapping_pairs = [ mapping_pairs = [
f"{logical_id}->{visible_device_list[logical_id]}" for logical_id in logical_ids if logical_id < num_visible f"{logical_id}->{visible_device_list[logical_id]}" for logical_id in logical_ids if logical_id < num_visible
@@ -2,7 +2,6 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import os import os
from collections.abc import Iterable, Mapping, Sequence from collections.abc import Iterable, Mapping, Sequence
from dataclasses import replace
from functools import partial from functools import partial
from threading import Lock from threading import Lock
@@ -365,6 +364,12 @@ class CosyVoice3Model(
else: else:
raise ValueError(f"Model stage not supported {self.model_stage}") raise ValueError(f"Model stage not supported {self.model_stage}")
def get_language_model(self) -> "nn.Module":
"""Return the language model for upstream MoE detection."""
if hasattr(self.model, "get_language_model"):
return self.model.get_language_model()
return self.model
def _create_llm_vllm_config(self, parent_config: VllmConfig) -> VllmConfig: def _create_llm_vllm_config(self, parent_config: VllmConfig) -> VllmConfig:
"""Create VllmConfig for the inner Qwen2 LLM. """Create VllmConfig for the inner Qwen2 LLM.
@@ -542,17 +547,21 @@ class CosyVoice3Model(
if self.model_stage != "cosyvoice3_talker": if self.model_stage != "cosyvoice3_talker":
return None return None
sampler = getattr(self, "_talker_sampler", None)
if sampler is None:
sampler = Sampler()
self._talker_sampler = sampler
if not self._cosyvoice3_ras_enabled(sampling_metadata): if not self._cosyvoice3_ras_enabled(sampling_metadata):
sampler = getattr(self, "_talker_sampler", None)
if sampler is None:
sampler = Sampler()
self._talker_sampler = sampler
return sampler(logits=logits, sampling_metadata=sampling_metadata) return sampler(logits=logits, sampling_metadata=sampling_metadata)
logits = logits.to(torch.float32) logits = logits.to(torch.float32)
sampling_for_processors = replace(sampling_metadata, no_penalties=True) # Apply logits processors directly — RAS handles its own repetition
logits = sampler.apply_logits_processors(logits, sampling_for_processors, predict_bonus_token=False) # logic. We avoid instantiating Sampler() here because its import
# chain pulls in flashinfer / GPU deps that fail in CPU-only tests.
if sampling_metadata.allowed_token_ids_mask is not None:
logits.masked_fill_(sampling_metadata.allowed_token_ids_mask, float("-inf"))
for processor in sampling_metadata.logitsprocs.non_argmax_invariant:
logits = processor.apply(logits)
sampling_cfg = dict(self.config.llm.get("sampling", {})) sampling_cfg = dict(self.config.llm.get("sampling", {}))
default_top_p = float(sampling_cfg.get("top_p", 0.8)) default_top_p = float(sampling_cfg.get("top_p", 0.8))
@@ -243,6 +243,12 @@ class CovoAudioForConditionalGeneration(
else lambda *args, **kwargs: None else lambda *args, **kwargs: None
) )
def get_language_model(self) -> "nn.Module":
"""Return the language model for upstream MoE detection."""
if hasattr(self.model, "get_language_model"):
return self.model.get_language_model()
return self.model
def fused_thinker_talker_preprocess( def fused_thinker_talker_preprocess(
self, self,
input_ids: torch.Tensor | None, input_ids: torch.Tensor | None,
@@ -143,6 +143,12 @@ class MingFlashOmniForConditionalGeneration(
f"For image generation use stage_type: diffusion with model_arch: MingImagePipeline." f"For image generation use stage_type: diffusion with model_arch: MingImagePipeline."
) )
def get_language_model(self) -> "nn.Module":
"""Return the language model for upstream MoE detection."""
if hasattr(self.model, "get_language_model"):
return self.model.get_language_model()
return self.model
def forward( def forward(
self, self,
input_ids: torch.Tensor, input_ids: torch.Tensor,
@@ -400,6 +400,13 @@ class MossTTSNanoForGeneration(nn.Module):
runtime_additional_information: list[dict[str, Any]] | None = None, runtime_additional_information: list[dict[str, Any]] | None = None,
**kwargs: Any, **kwargs: Any,
) -> OmniOutput: ) -> OmniOutput:
if self._lm is None or self._audio_tokenizer is None:
# load_weights() loads the real LM from HuggingFace. When
# load_format=dummy the vllm DummyModelLoader skips calling
# model.load_weights(), so we do it lazily on first forward.
# Must happen before _make_dummy_hidden so self._device is set.
self.load_weights([])
sr = getattr(self.config, "audio_tokenizer_sample_rate", 48000) sr = getattr(self.config, "audio_tokenizer_sample_rate", 48000)
sr_tensor = torch.tensor(sr, dtype=torch.int32) sr_tensor = torch.tensor(sr, dtype=torch.int32)
empty = torch.zeros((0,), dtype=torch.float32) empty = torch.zeros((0,), dtype=torch.float32)
@@ -418,9 +425,6 @@ class MossTTSNanoForGeneration(nn.Module):
}, },
) )
if self._lm is None or self._audio_tokenizer is None:
raise RuntimeError("MOSS-TTS-Nano model not loaded. Was load_weights() called?")
outputs: list[torch.Tensor] = [] outputs: list[torch.Tensor] = []
srs: list[torch.Tensor] = [] srs: list[torch.Tensor] = []
last_chunk_flags: list[bool] = [] last_chunk_flags: list[bool] = []
@@ -91,18 +91,17 @@ class Qwen2_5OmniForConditionalGeneration(
elif self.model_stage == "talker": elif self.model_stage == "talker":
multimodal_config.skip_mm_profiling = True multimodal_config.skip_mm_profiling = True
# register the process function for the talker stage
self.has_preprocess = True self.has_preprocess = True
self.set_custom_preprocess(self.talker_preprocess) self.set_custom_preprocess(self.talker_preprocess)
self.thinker = None self.thinker = None
# Initialize talker model wrapper (handles projection + LM) # Initialize talker model wrapper (handles projection + LM)
self.talker = init_vllm_registered_model( with self._mark_language_model(vllm_config=vllm_config):
vllm_config=vllm_config, self.talker = init_vllm_registered_model(
prefix=maybe_prefix(prefix, "talker"), vllm_config=vllm_config,
hf_config=talker_config, prefix=maybe_prefix(prefix, "talker"),
# Use registry architecture key hf_config=talker_config,
architectures=["Qwen2_5OmniTalkerModel"], architectures=["Qwen2_5OmniTalkerModel"],
) )
self.model = self.talker self.model = self.talker
self.token2wav = None self.token2wav = None
# set suppress start id according to token2wav # set suppress start id according to token2wav
@@ -130,12 +129,13 @@ class Qwen2_5OmniForConditionalGeneration(
self.token2wav_config = getattr(config, "token2wav_config", None) self.token2wav_config = getattr(config, "token2wav_config", None)
self.token2wav = None self.token2wav = None
if self.token2wav_config is not None: if self.token2wav_config is not None:
self.token2wav = init_vllm_registered_model( with self._mark_language_model(vllm_config=vllm_config):
vllm_config=vllm_config, self.token2wav = init_vllm_registered_model(
prefix=maybe_prefix(prefix, "token2wav"), vllm_config=vllm_config,
hf_config=self.token2wav_config, prefix=maybe_prefix(prefix, "token2wav"),
architectures=["Qwen2_5OmniToken2WavModel"], hf_config=self.token2wav_config,
) architectures=["Qwen2_5OmniToken2WavModel"],
)
# voice resources (loaded on demand) # voice resources (loaded on demand)
self._token2wav_conds: dict[str, torch.Tensor] = {} self._token2wav_conds: dict[str, torch.Tensor] = {}
self._token2wav_ref_mels: dict[str, torch.Tensor] = {} self._token2wav_ref_mels: dict[str, torch.Tensor] = {}
@@ -149,6 +149,18 @@ class Qwen2_5OmniForConditionalGeneration(
(self.thinker.make_empty_intermediate_tensors) if self.model_stage == "thinker" else lambda: None (self.thinker.make_empty_intermediate_tensors) if self.model_stage == "thinker" else lambda: None
) )
def get_language_model(self) -> nn.Module:
"""Return the language model for MoE detection in upstream load_model.
Upstream GPUModelRunner.load_model() calls get_language_model() to find
the MoE model inside VLM wrappers. For thinker (which IS MoE), delegate
to the child. For talker/code2wav (not MoE, not SupportsMultiModal),
return self.model directly to avoid NotImplementedError.
"""
if hasattr(self.model, "get_language_model"):
return self.model.get_language_model()
return self.model
# -------------------- Device utilities -------------------- # -------------------- Device utilities --------------------
@staticmethod @staticmethod
def _module_device(module: nn.Module) -> torch.device: def _module_device(module: nn.Module) -> torch.device:
@@ -278,6 +278,12 @@ class Qwen3OmniMoeForConditionalGeneration(
return self.model.sampler return self.model.sampler
return Sampler() return Sampler()
def get_language_model(self) -> torch.nn.Module:
"""Delegate to the active stage's language model for upstream MoE resolution."""
if hasattr(self.model, "get_language_model"):
return self.model.get_language_model()
return self.model
def embed_input_ids( def embed_input_ids(
self, self,
input_ids: torch.Tensor, input_ids: torch.Tensor,
+2 -1
View File
@@ -2,6 +2,7 @@ from vllm.model_executor.models.registry import (
_VLLM_MODELS, _VLLM_MODELS,
_LazyRegisteredModel, _LazyRegisteredModel,
_ModelRegistry, _ModelRegistry,
_resolve_module_name,
) )
_OMNI_MODELS = { _OMNI_MODELS = {
@@ -261,7 +262,7 @@ OmniModelRegistry = _ModelRegistry(
{ {
**{ **{
model_arch: _LazyRegisteredModel( model_arch: _LazyRegisteredModel(
module_name=f"vllm.model_executor.models.{mod_relname}", module_name=_resolve_module_name(mod_relname),
class_name=cls_name, class_name=cls_name,
) )
for model_arch, (mod_relname, cls_name) in _VLLM_MODELS.items() for model_arch, (mod_relname, cls_name) in _VLLM_MODELS.items()
@@ -159,6 +159,13 @@ class VoxtralTTSForConditionalGeneration(
raise ValueError("Invalid model stage") raise ValueError("Invalid model stage")
# -------------------- CUDA Graph for acoustic transformer -------------------- # -------------------- CUDA Graph for acoustic transformer --------------------
def get_language_model(self) -> "nn.Module":
"""Return the language model for upstream MoE detection."""
if hasattr(self.model, "get_language_model"):
return self.model.get_language_model()
return self.model
def _enable_acoustic_transformer_cudagraph(self): def _enable_acoustic_transformer_cudagraph(self):
"""Initialize and capture CUDA graphs for compute_mm_logits.""" """Initialize and capture CUDA graphs for compute_mm_logits."""
if self.model_stage != "audio_generation" or not hasattr(self, "_vllm_config"): if self.model_stage != "audio_generation" or not hasattr(self, "_vllm_config"):
+22
View File
@@ -128,3 +128,25 @@ for module_name, module in list(sys.modules.items()):
module.StreamingUpdate = OmniStreamingUpdate module.StreamingUpdate = OmniStreamingUpdate
if hasattr(module, "EngineCoreRequest") and module.EngineCoreRequest == _OriginalEngineCoreRequest: if hasattr(module, "EngineCoreRequest") and module.EngineCoreRequest == _OriginalEngineCoreRequest:
module.EngineCoreRequest = OmniEngineCoreRequest module.EngineCoreRequest = OmniEngineCoreRequest
# Patch: add qwen3_omni_moe to vllm's chat template fallback registry.
# Qwen/Qwen3-Omni-30B-A3B-Instruct stores its chat_template in a standalone
# chat_template.json (not in tokenizer_config.json). transformers < 5.9.0
# does not load this file, so the tokenizer has no chat_template attribute.
# vllm's resolve_chat_template falls back to MODEL_TYPE_TO_CHAT_TEMPLATE
# which has "qwen" but not "qwen3_omni_moe". Register the same fallback.
def _patch_chat_template_registry():
try:
from vllm.transformers_utils.chat_templates.registry import (
_MODEL_TYPE_TO_CHAT_TEMPLATE_FALLBACK,
_get_qwen_chat_template_fallback,
)
if "qwen3_omni_moe" not in _MODEL_TYPE_TO_CHAT_TEMPLATE_FALLBACK:
_MODEL_TYPE_TO_CHAT_TEMPLATE_FALLBACK["qwen3_omni_moe"] = _get_qwen_chat_template_fallback
except ImportError:
pass
_patch_chat_template_registry()
+1 -1
View File
@@ -229,4 +229,4 @@ class CudaOmniPlatform(OmniPlatform, CudaPlatformBase):
if envs.VLLM_USE_OINK_OPS: if envs.VLLM_USE_OINK_OPS:
rms_norm = ["oink"] + default rms_norm = ["oink"] + default
return IrOpPriorityConfig.with_default(default, rms_norm=rms_norm) return IrOpPriorityConfig.with_default(default, rms_norm=rms_norm, fused_add_rms_norm=rms_norm)
+2
View File
@@ -11,6 +11,7 @@ from vllm.config import CUDAGraphMode, VllmConfig
from vllm.forward_context import BatchDescriptor from vllm.forward_context import BatchDescriptor
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.platforms import Platform from vllm.platforms import Platform
from vllm.platforms.interface import PlatformEnum
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -239,6 +240,7 @@ class OmniPlatform(Platform):
class UnspecifiedOmniPlatform(OmniPlatform): class UnspecifiedOmniPlatform(OmniPlatform):
_omni_enum = OmniPlatformEnum.UNSPECIFIED _omni_enum = OmniPlatformEnum.UNSPECIFIED
_enum = PlatformEnum.UNSPECIFIED
device_type = "" device_type = ""
@classmethod @classmethod
@@ -16,11 +16,6 @@ from vllm.distributed.kv_transfer import get_kv_transfer_group, has_kv_transfer_
from vllm.distributed.parallel_state import get_pp_group, get_tp_group from vllm.distributed.parallel_state import get_pp_group, get_tp_group
from vllm.forward_context import BatchDescriptor from vllm.forward_context import BatchDescriptor
from vllm.logger import logger from vllm.logger import logger
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
extract_routed_experts_for_current_batch,
get_global_experts_capturer,
issue_routing_d2h_copy,
)
from vllm.sequence import IntermediateTensors from vllm.sequence import IntermediateTensors
from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
from vllm.v1.outputs import ( from vllm.v1.outputs import (
@@ -257,7 +252,7 @@ class NPUARModelRunner(OmniNPUModelRunner):
intermediate_tensors: IntermediateTensors | None = None, intermediate_tensors: IntermediateTensors | None = None,
) -> OmniModelRunnerOutput | IntermediateTensors | None: ) -> OmniModelRunnerOutput | IntermediateTensors | None:
if self.vllm_config.model_config.enable_return_routed_experts: if self.vllm_config.model_config.enable_return_routed_experts:
capturer = get_global_experts_capturer() capturer = self.routed_experts_capturer
if capturer is not None and hasattr(capturer, "finalize_pending_copy"): if capturer is not None and hasattr(capturer, "finalize_pending_copy"):
capturer.finalize_pending_copy() capturer.finalize_pending_copy()
if self.ascend_config.profiling_chunk_config.enabled: if self.ascend_config.profiling_chunk_config.enabled:
@@ -728,12 +723,7 @@ class NPUARModelRunner(OmniNPUModelRunner):
deferred_state_corrections_fn() deferred_state_corrections_fn()
if self.vllm_config.model_config.enable_return_routed_experts and hasattr(self, "_positions_cpu"): if self.vllm_config.model_config.enable_return_routed_experts and hasattr(self, "_positions_cpu"):
issue_routing_d2h_copy( self._omni_routed_experts_d2h(scheduler_output)
input_batch_req_ids=self.input_batch.req_ids,
num_scheduled_tokens=scheduler_output.num_scheduled_tokens,
positions=self.positions,
positions_cpu=self._positions_cpu,
)
return None return None
@@ -911,17 +901,11 @@ class NPUARModelRunner(OmniNPUModelRunner):
if self.speculative_config is not None: if self.speculative_config is not None:
self.finalize_kv_connector() self.finalize_kv_connector()
routed_experts_dict = None routed_experts_lists = None
if self.model_config.enable_return_routed_experts: if self.model_config.enable_return_routed_experts:
capturer = get_global_experts_capturer() capturer = self.routed_experts_capturer
if capturer is not None and hasattr(self.input_batch, "num_tokens_no_spec"): if capturer is not None and hasattr(self.input_batch, "num_tokens_no_spec"):
routed_experts_dict = extract_routed_experts_for_current_batch( routed_experts_lists = self._omni_extract_routed_experts(scheduler_output)
req_ids=req_ids_output_copy,
requests=self.requests,
req_id_to_index=self.input_batch.req_id_to_index,
num_tokens_no_spec=self.input_batch.num_tokens_no_spec,
max_model_len=self.max_model_len,
)
# -------------------------------------- Omni-new ------------------------------------------------- # -------------------------------------- Omni-new -------------------------------------------------
engine_output_type, downstream_req_ids = self._resolve_pooler_payload_req_ids(req_ids_output_copy) engine_output_type, downstream_req_ids = self._resolve_pooler_payload_req_ids(req_ids_output_copy)
@@ -1042,9 +1026,9 @@ class NPUARModelRunner(OmniNPUModelRunner):
kv_connector_output=kv_connector_output, kv_connector_output=kv_connector_output,
ec_connector_output=ec_connector_output if self.supports_mm_inputs else None, ec_connector_output=ec_connector_output if self.supports_mm_inputs else None,
cudagraph_stats=cudagraph_stats, cudagraph_stats=cudagraph_stats,
routed_experts_dict=routed_experts_dict,
) )
model_runner_output.kv_extracted_req_ids = kv_extracted_req_ids model_runner_output.kv_extracted_req_ids = kv_extracted_req_ids
model_runner_output.routed_experts = routed_experts_lists
# -------------------------------------- Omni-new ------------------------------------------------- # -------------------------------------- Omni-new -------------------------------------------------
if self.ascend_config.profiling_chunk_config.enabled and hasattr(self, "_execution_start_time"): if self.ascend_config.profiling_chunk_config.enabled and hasattr(self, "_execution_start_time"):
@@ -15,11 +15,6 @@ from vllm.distributed.ec_transfer import get_ec_transfer, has_ec_transfer
from vllm.distributed.kv_transfer import get_kv_transfer_group, has_kv_transfer_group from vllm.distributed.kv_transfer import get_kv_transfer_group, has_kv_transfer_group
from vllm.distributed.parallel_state import get_pp_group from vllm.distributed.parallel_state import get_pp_group
from vllm.logger import logger from vllm.logger import logger
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
extract_routed_experts_for_current_batch,
get_global_experts_capturer,
issue_routing_d2h_copy,
)
from vllm.sequence import IntermediateTensors from vllm.sequence import IntermediateTensors
from vllm.utils.math_utils import cdiv from vllm.utils.math_utils import cdiv
from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
@@ -78,7 +73,7 @@ class NPUGenerationModelRunner(OmniNPUModelRunner):
intermediate_tensors: IntermediateTensors | None = None, intermediate_tensors: IntermediateTensors | None = None,
) -> OmniModelRunnerOutput | IntermediateTensors | None: ) -> OmniModelRunnerOutput | IntermediateTensors | None:
if self.vllm_config.model_config.enable_return_routed_experts: if self.vllm_config.model_config.enable_return_routed_experts:
capturer = get_global_experts_capturer() capturer = self.routed_experts_capturer
if capturer is not None and hasattr(capturer, "finalize_pending_copy"): if capturer is not None and hasattr(capturer, "finalize_pending_copy"):
capturer.finalize_pending_copy() capturer.finalize_pending_copy()
if self.ascend_config.profiling_chunk_config.enabled: if self.ascend_config.profiling_chunk_config.enabled:
@@ -387,12 +382,7 @@ class NPUGenerationModelRunner(OmniNPUModelRunner):
deferred_state_corrections_fn() deferred_state_corrections_fn()
if self.vllm_config.model_config.enable_return_routed_experts and hasattr(self, "_positions_cpu"): if self.vllm_config.model_config.enable_return_routed_experts and hasattr(self, "_positions_cpu"):
issue_routing_d2h_copy( self._omni_routed_experts_d2h(scheduler_output)
input_batch_req_ids=self.input_batch.req_ids,
num_scheduled_tokens=scheduler_output.num_scheduled_tokens,
positions=self.positions,
positions_cpu=self._positions_cpu,
)
return None return None
@@ -479,17 +469,11 @@ class NPUGenerationModelRunner(OmniNPUModelRunner):
# [Omni] Copy req_id mappings to avoid async scheduling mutation. # [Omni] Copy req_id mappings to avoid async scheduling mutation.
req_ids_output_copy = self.input_batch.req_ids.copy() req_ids_output_copy = self.input_batch.req_ids.copy()
req_id_to_index_output_copy = self.input_batch.req_id_to_index.copy() req_id_to_index_output_copy = self.input_batch.req_id_to_index.copy()
routed_experts_dict = None routed_experts_lists = None
if self.vllm_config.model_config.enable_return_routed_experts and hasattr( if self.vllm_config.model_config.enable_return_routed_experts and hasattr(
self.input_batch, "num_tokens_no_spec" self.input_batch, "num_tokens_no_spec"
): ):
routed_experts_dict = extract_routed_experts_for_current_batch( routed_experts_lists = self._omni_extract_routed_experts(scheduler_output)
req_ids=req_ids_output_copy,
requests=self.requests,
req_id_to_index=self.input_batch.req_id_to_index,
num_tokens_no_spec=self.input_batch.num_tokens_no_spec,
max_model_len=self.max_model_len,
)
output = OmniModelRunnerOutput( output = OmniModelRunnerOutput(
req_ids=req_ids_output_copy, req_ids=req_ids_output_copy,
req_id_to_index=req_id_to_index_output_copy, req_id_to_index=req_id_to_index_output_copy,
@@ -500,8 +484,8 @@ class NPUGenerationModelRunner(OmniNPUModelRunner):
kv_connector_output=kv_connector_output, kv_connector_output=kv_connector_output,
ec_connector_output=ec_connector_output if self.supports_mm_inputs else None, ec_connector_output=ec_connector_output if self.supports_mm_inputs else None,
cudagraph_stats=cudagraph_stats, cudagraph_stats=cudagraph_stats,
routed_experts_dict=routed_experts_dict,
) )
output.routed_experts = routed_experts_lists
# -------------------------------------- Omni-new ------------------------------------------------- # -------------------------------------- Omni-new -------------------------------------------------
if self.speculative_config is not None: if self.speculative_config is not None:
+1 -1
View File
@@ -172,4 +172,4 @@ class RocmOmniPlatform(OmniPlatform, RocmPlatform):
else: else:
rms_norm = default rms_norm = default
return IrOpPriorityConfig.with_default(default, rms_norm=rms_norm) return IrOpPriorityConfig.with_default(default, rms_norm=rms_norm, fused_add_rms_norm=rms_norm)
+57 -3
View File
@@ -10,19 +10,73 @@ different QuantizationConfig subclass in the OMNI context (e.g. GGUF).
from __future__ import annotations from __future__ import annotations
import inspect import inspect
import sys
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
from types import ModuleType
from typing import Any from typing import Any
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.model_executor.layers.quantization import (
# ---------------------------------------------------------------------------
# Stub the ``humming`` package so that vLLM's lazy import inside
# ``get_quantization_config()`` (which unconditionally does
# ``from .humming import HummingConfig``) does not crash when the real
# ``humming`` wheel is not installed. Only populate the bare-minimum
# names that ``humming.py`` accesses at module level.
# ---------------------------------------------------------------------------
def _register_humming_stubs() -> None:
"""Register stub ``humming`` sub-modules so that the optional
humming quantization backend can be imported without the real wheel."""
if "humming" in sys.modules:
return # already present (real or stub)
# --- sub-modules ---
submodules: dict[str, tuple[str, ...]] = {
"humming": (),
"humming.config": ("GemmType",),
"humming.dtypes": ("DataType",),
"humming.layer": ("HummingLayerMeta", "HummingMethod"),
"humming.schema": (
"BaseInputSchema",
"BaseWeightSchema",
"HummingInputSchema",
"HummingWeightSchema",
),
"humming.utils": (),
"humming.utils.weight": ("quantize_weight",),
}
registry: dict[str, ModuleType] = {}
for name, attrs in submodules.items():
mod = ModuleType(name)
for attr in attrs:
setattr(mod, attr, type(attr, (), {}))
registry[name] = mod
# wire parent references
registry["humming"].config = registry["humming.config"]
registry["humming"].dtypes = registry["humming.dtypes"]
registry["humming"].layer = registry["humming.layer"]
registry["humming"].schema = registry["humming.schema"]
registry["humming"].utils = registry["humming.utils"]
registry["humming.utils"].weight = registry["humming.utils.weight"]
for name, mod in registry.items():
sys.modules[name] = mod
_register_humming_stubs()
from vllm.model_executor.layers.quantization import ( # noqa: E402
QUANTIZATION_METHODS, QUANTIZATION_METHODS,
get_quantization_config, get_quantization_config,
) )
from vllm.model_executor.layers.quantization.base_config import ( from vllm.model_executor.layers.quantization.base_config import ( # noqa: E402
QuantizationConfig, QuantizationConfig,
) )
from .component_config import ComponentQuantizationConfig from .component_config import ComponentQuantizationConfig # noqa: E402
logger = init_logger(__name__) logger = init_logger(__name__)
+16 -34
View File
@@ -19,11 +19,6 @@ from vllm.distributed.kv_transfer import get_kv_transfer_group, has_kv_transfer_
from vllm.distributed.parallel_state import get_pp_group, get_tp_group from vllm.distributed.parallel_state import get_pp_group, get_tp_group
from vllm.forward_context import set_forward_context from vllm.forward_context import set_forward_context
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
extract_routed_experts_for_current_batch,
get_global_experts_capturer,
issue_routing_d2h_copy,
)
from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
from vllm.v1.outputs import AsyncModelRunnerOutput, make_empty_encoder_model_runner_output from vllm.v1.outputs import AsyncModelRunnerOutput, make_empty_encoder_model_runner_output
from vllm.v1.spec_decode.dflash import DFlashProposer from vllm.v1.spec_decode.dflash import DFlashProposer
@@ -363,6 +358,9 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
if self.execute_model_state is not None: if self.execute_model_state is not None:
raise RuntimeError("State error: sample_tokens() must be called after execute_model() returns None.") raise RuntimeError("State error: sample_tokens() must be called after execute_model() returns None.")
if self.routed_experts_initialized:
self.routed_experts_capturer.clear_buffer()
if not getattr(self, "_warmup_state_cleared", False): if not getattr(self, "_warmup_state_cleared", False):
self._warmup_state_cleared = True self._warmup_state_cleared = True
if hasattr(self.model, "_clear_warmup_state"): if hasattr(self.model, "_clear_warmup_state"):
@@ -414,7 +412,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
) )
if self.routed_experts_initialized: if self.routed_experts_initialized:
capturer = get_global_experts_capturer() capturer = self.routed_experts_capturer
if capturer is not None and hasattr(capturer, "finalize_pending_copy"): if capturer is not None and hasattr(capturer, "finalize_pending_copy"):
capturer.finalize_pending_copy() capturer.finalize_pending_copy()
@@ -679,7 +677,6 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
if not get_pp_group().is_last_rank: if not get_pp_group().is_last_rank:
# Return the intermediate tensors. # Return the intermediate tensors.
assert isinstance(hidden_states, IntermediateTensors) assert isinstance(hidden_states, IntermediateTensors)
hidden_states.kv_connector_output = kv_connector_output
self.kv_connector_output = kv_connector_output self.kv_connector_output = kv_connector_output
return hidden_states return hidden_states
@@ -692,7 +689,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
kv_connector_output, kv_connector_output,
) )
sample_hidden_states = hidden_states[logits_indices] sample_hidden_states = hidden_states[logits_indices.to(hidden_states.device)]
# Try with sampling_metadata first; fall back to without for models that don't support it # Try with sampling_metadata first; fall back to without for models that don't support it
try: try:
logits = self.model.compute_logits( logits = self.model.compute_logits(
@@ -704,7 +701,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
# Rare case. # Rare case.
assert not self.is_pooling_model assert not self.is_pooling_model
sample_hidden_states = hidden_states[logits_indices] sample_hidden_states = hidden_states[logits_indices.to(hidden_states.device)]
if not get_pp_group().is_last_rank: if not get_pp_group().is_last_rank:
all_gather_tensors = { all_gather_tensors = {
"residual": not is_residual_scattered_for_sp(self.vllm_config, num_tokens_padded) "residual": not is_residual_scattered_for_sp(self.vllm_config, num_tokens_padded)
@@ -753,12 +750,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
deferred_state_corrections_fn() deferred_state_corrections_fn()
if self.routed_experts_initialized and hasattr(self, "_positions_cpu"): if self.routed_experts_initialized and hasattr(self, "_positions_cpu"):
issue_routing_d2h_copy( self._omni_routed_experts_d2h(scheduler_output)
input_batch_req_ids=self.input_batch.req_ids,
num_scheduled_tokens=scheduler_output.num_scheduled_tokens,
positions=self.positions,
positions_cpu=self._positions_cpu,
)
return None return None
@@ -830,18 +822,14 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
if self.execute_model_state is None: if self.execute_model_state is None:
kv_connector_output = self.kv_connector_output kv_connector_output = self.kv_connector_output
self.kv_connector_output = None self.kv_connector_output = None
# Nothing to do (PP non-final rank case), output isn't used. # receive sampled token ids from the last PP rank.
if not kv_connector_output: if self.use_async_scheduling and not get_pp_group().is_last_rank:
return None # type: ignore[return-value] self._pp_receive_prev_sampled_token_ids_to_input_batch()
# In case of PP with kv transfer, we need to pass through the # In case of PP with kv transfer, we need to pass through the
# kv_connector_output # kv_connector_output
if kv_connector_output.is_empty(): return self.attach_omni_connector_output(
return self.attach_omni_connector_output(EMPTY_MODEL_RUNNER_OUTPUT) OmniModelRunnerOutput.with_kv_conn_output_only(kv_connector_output)
)
output = copy(EMPTY_MODEL_RUNNER_OUTPUT)
output.kv_connector_output = kv_connector_output
return self.attach_omni_connector_output(output)
# Unpack ephemeral state. # Unpack ephemeral state.
( (
@@ -1108,15 +1096,9 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
self.accumulate_full_payload_output(rid, pooler_output[i], req_state) self.accumulate_full_payload_output(rid, pooler_output[i], req_state)
with record_function_or_nullcontext("gpu_model_runner: ModelRunnerOutput"): with record_function_or_nullcontext("gpu_model_runner: ModelRunnerOutput"):
routed_experts_dict = None routed_experts_lists = None
if self.routed_experts_initialized: if self.routed_experts_initialized:
routed_experts_dict = extract_routed_experts_for_current_batch( routed_experts_lists = self._omni_extract_routed_experts(scheduler_output)
req_ids=req_ids_output_copy,
requests=self.requests,
req_id_to_index=self.input_batch.req_id_to_index,
num_tokens_no_spec=self.input_batch.num_tokens_no_spec,
max_model_len=self.max_model_len,
)
output = OmniModelRunnerOutput( output = OmniModelRunnerOutput(
req_ids=req_ids_output_copy, req_ids=req_ids_output_copy,
req_id_to_index=req_id_to_index_output_copy, req_id_to_index=req_id_to_index_output_copy,
@@ -1128,10 +1110,10 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
ec_connector_output=ec_connector_output if self.supports_mm_inputs else None, ec_connector_output=ec_connector_output if self.supports_mm_inputs else None,
num_nans_in_logits=num_nans_in_logits, num_nans_in_logits=num_nans_in_logits,
cudagraph_stats=cudagraph_stats, cudagraph_stats=cudagraph_stats,
routed_experts_dict=routed_experts_dict,
) )
output.kv_extracted_req_ids = kv_extracted_req_ids output.kv_extracted_req_ids = kv_extracted_req_ids
output.omni_connector_output = self.get_omni_connector_output() output.omni_connector_output = self.get_omni_connector_output()
output.routed_experts = routed_experts_lists
if not self.use_async_scheduling: if not self.use_async_scheduling:
return output return output
+16 -34
View File
@@ -8,7 +8,6 @@ from __future__ import annotations
import gc import gc
import logging import logging
from copy import copy
from dataclasses import replace from dataclasses import replace
import numpy as np import numpy as np
@@ -18,11 +17,6 @@ from vllm.distributed.ec_transfer import get_ec_transfer, has_ec_transfer
from vllm.distributed.kv_transfer import get_kv_transfer_group, has_kv_transfer_group from vllm.distributed.kv_transfer import get_kv_transfer_group, has_kv_transfer_group
from vllm.distributed.parallel_state import get_pp_group from vllm.distributed.parallel_state import get_pp_group
from vllm.forward_context import set_forward_context from vllm.forward_context import set_forward_context
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
extract_routed_experts_for_current_batch,
get_global_experts_capturer,
issue_routing_d2h_copy,
)
from vllm.utils.math_utils import cdiv from vllm.utils.math_utils import cdiv
from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
from vllm.v1.outputs import AsyncModelRunnerOutput, make_empty_encoder_model_runner_output from vllm.v1.outputs import AsyncModelRunnerOutput, make_empty_encoder_model_runner_output
@@ -112,6 +106,9 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
if self.execute_model_state is not None: if self.execute_model_state is not None:
raise RuntimeError("State error: sample_tokens() must be called after execute_model() returns None.") raise RuntimeError("State error: sample_tokens() must be called after execute_model() returns None.")
if self.routed_experts_initialized:
self.routed_experts_capturer.clear_buffer()
if hasattr(self, "_omni_connector"): if hasattr(self, "_omni_connector"):
for request in getattr(scheduler_output, "pending_input_registrations", []): for request in getattr(scheduler_output, "pending_input_registrations", []):
self.register_chunk_recv(request) self.register_chunk_recv(request)
@@ -123,7 +120,7 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
self.flush_full_payload_outputs(flush_ids) self.flush_full_payload_outputs(flush_ids)
if self.routed_experts_initialized: if self.routed_experts_initialized:
capturer = get_global_experts_capturer() capturer = self.routed_experts_capturer
if capturer is not None and hasattr(capturer, "finalize_pending_copy"): if capturer is not None and hasattr(capturer, "finalize_pending_copy"):
capturer.finalize_pending_copy() capturer.finalize_pending_copy()
@@ -369,12 +366,7 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
deferred_state_corrections_fn() deferred_state_corrections_fn()
if self.routed_experts_initialized and hasattr(self, "_positions_cpu"): if self.routed_experts_initialized and hasattr(self, "_positions_cpu"):
issue_routing_d2h_copy( self._omni_routed_experts_d2h(scheduler_output)
input_batch_req_ids=self.input_batch.req_ids,
num_scheduled_tokens=scheduler_output.num_scheduled_tokens,
positions=self.positions,
positions_cpu=self._positions_cpu,
)
return None return None
@@ -390,18 +382,14 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
self.kv_connector_output = None self.kv_connector_output = None
if self.execute_model_state is None: if self.execute_model_state is None:
# Nothing to do (PP non-final rank case), output isn't used. # receive sampled token ids from the last PP rank.
if not kv_connector_output: if self.use_async_scheduling and not get_pp_group().is_last_rank:
return None # type: ignore[return-value] self._pp_receive_prev_sampled_token_ids_to_input_batch()
# In case of PP with kv transfer, we need to pass through the # In case of PP with kv transfer, we need to pass through the
# kv_connector_output # kv_connector_output
if kv_connector_output.is_empty(): return self.attach_omni_connector_output(
return self.attach_omni_connector_output(EMPTY_MODEL_RUNNER_OUTPUT) OmniModelRunnerOutput.with_kv_conn_output_only(kv_connector_output)
)
output = copy(EMPTY_MODEL_RUNNER_OUTPUT)
output.kv_connector_output = kv_connector_output
return self.attach_omni_connector_output(output)
# Unpack ephemeral state. # Unpack ephemeral state.
( (
@@ -462,15 +450,9 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
# [Omni] Copy req_id mappings to avoid async scheduling mutation. # [Omni] Copy req_id mappings to avoid async scheduling mutation.
req_ids_output_copy = self.input_batch.req_ids.copy() req_ids_output_copy = self.input_batch.req_ids.copy()
req_id_to_index_output_copy = self.input_batch.req_id_to_index.copy() req_id_to_index_output_copy = self.input_batch.req_id_to_index.copy()
routed_experts_dict = None routed_experts_lists = None
if self.routed_experts_initialized: if self.routed_experts_initialized:
routed_experts_dict = extract_routed_experts_for_current_batch( routed_experts_lists = self._omni_extract_routed_experts(scheduler_output)
req_ids=req_ids_output_copy,
requests=self.requests,
req_id_to_index=self.input_batch.req_id_to_index,
num_tokens_no_spec=self.input_batch.num_tokens_no_spec,
max_model_len=self.max_model_len,
)
if self._should_accumulate_full_payload_output(): if self._should_accumulate_full_payload_output():
for i, rid in enumerate(req_ids_output_copy): for i, rid in enumerate(req_ids_output_copy):
req_state = self.requests.get(rid) req_state = self.requests.get(rid)
@@ -488,9 +470,9 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
num_nans_in_logits={}, num_nans_in_logits={},
cudagraph_stats=cudagraph_stats, cudagraph_stats=cudagraph_stats,
ec_connector_output=ec_connector_output if self.supports_mm_inputs else None, ec_connector_output=ec_connector_output if self.supports_mm_inputs else None,
routed_experts_dict=routed_experts_dict,
) )
output.omni_connector_output = self.get_omni_connector_output() output.omni_connector_output = self.get_omni_connector_output()
output.routed_experts = routed_experts_lists
if not self.use_async_scheduling: if not self.use_async_scheduling:
return output return output
@@ -544,9 +526,9 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
) )
@torch.inference_mode() @torch.inference_mode()
def _dummy_sampler_run(self, hidden_states: torch.Tensor) -> None: def _dummy_sampler_run(self, hidden_states: torch.Tensor) -> torch.Tensor:
logger.warning("Dummy sampler run is not implemented for generation model") logger.warning("Dummy sampler run is not implemented for generation model")
return None return torch.tensor([])
@torch.inference_mode() @torch.inference_mode()
def _dummy_run( def _dummy_run(
+57 -1
View File
@@ -36,6 +36,7 @@ from vllm_omni.platforms import current_omni_platform
if TYPE_CHECKING: if TYPE_CHECKING:
from vllm.v1.core.sched.output import SchedulerOutput from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.outputs import RoutedExpertsLists
else: else:
xgr = LazyLoader("xgr", globals(), "xgrammar") xgr = LazyLoader("xgr", globals(), "xgrammar")
xgr_torch_compile = LazyLoader( xgr_torch_compile = LazyLoader(
@@ -57,6 +58,52 @@ class OmniGPUModelRunner(GPUModelRunner):
# when we initialize the metadata builders if enabled # when we initialize the metadata builders if enabled
self.omni_prefix_cache = None self.omni_prefix_cache = None
def _omni_routed_experts_d2h(self, scheduler_output) -> None:
"""Issue routed-experts D2H copy matching upstream GPUModelRunner pattern.
Upstream does this inline in ``execute_model``:
buf = self.routed_experts_capturer.get_device_buffer()
total = scheduler_output.total_num_scheduled_tokens
self.routed_experts_cpu[:total].copy_(buf[:total], non_blocking=True)
self.routed_experts_slot_mapping_cpu[:total].copy_(
self.routed_experts_slot_mapping_device[:total], non_blocking=True)
"""
if not self.routed_experts_initialized:
return
buf = self.routed_experts_capturer.get_device_buffer()
total = scheduler_output.total_num_scheduled_tokens
self.routed_experts_cpu[:total].copy_(buf[:total], non_blocking=True)
if hasattr(self, "routed_experts_slot_mapping_device"):
self.routed_experts_slot_mapping_cpu[:total].copy_(
self.routed_experts_slot_mapping_device[:total],
non_blocking=True,
)
def _omni_extract_routed_experts(self, scheduler_output) -> "RoutedExpertsLists | None":
"""Extract routed experts matching upstream GPUModelRunner pattern.
Upstream (sync path, sample_tokens):
total = scheduler_output.total_num_scheduled_tokens
output.routed_experts = RoutedExpertsLists(
routing_data=self.routed_experts_cpu[:total].numpy(),
slot_mapping=self.routed_experts_slot_mapping_cpu[:total].numpy(),
)
Returns RoutedExpertsLists (batch-level, with slot_mapping) so that
downstream schedulers can use slot_mapping to map back to requests.
"""
from vllm.v1.outputs import RoutedExpertsLists
if not self.routed_experts_initialized:
return None
total = scheduler_output.total_num_scheduled_tokens
if total <= 0:
return None
return RoutedExpertsLists(
routing_data=self.routed_experts_cpu[:total].numpy(),
slot_mapping=self.routed_experts_slot_mapping_cpu[:total].numpy(),
)
def initialize_metadata_builders(self, kv_cache_config, kernel_block_sizes): def initialize_metadata_builders(self, kv_cache_config, kernel_block_sizes):
"""Override to fix scheduler_metadata buffer size for FA3 + CUDA graph. """Override to fix scheduler_metadata buffer size for FA3 + CUDA graph.
@@ -747,6 +794,15 @@ class OmniGPUModelRunner(GPUModelRunner):
raise ValueError(f"Invalid hidden states type: {type(hidden_states)}") raise ValueError(f"Invalid hidden states type: {type(hidden_states)}")
return text_hidden_states, multimodal_outputs return text_hidden_states, multimodal_outputs
def _dummy_sampler_run(self, hidden_states: torch.Tensor) -> torch.Tensor:
# Models loaded with load_format=dummy (e.g. MossTTSNano) may
# produce CPU hidden_states while the upstream sampler warmup
# expects GPU tensors. Skip the warmup in that case — a
# meaningful warmup requires real weights anyway.
if hidden_states.device != self.device:
return torch.tensor([])
return super()._dummy_sampler_run(hidden_states=hidden_states)
@torch.inference_mode() @torch.inference_mode()
def _dummy_run( def _dummy_run(
self, self,
@@ -1100,7 +1156,7 @@ class OmniGPUModelRunner(GPUModelRunner):
self.eplb_step(is_dummy=True, is_profile=is_profile) self.eplb_step(is_dummy=True, is_profile=is_profile)
logit_indices = np.cumsum(num_scheduled_tokens) - 1 logit_indices = np.cumsum(num_scheduled_tokens) - 1
logit_indices_device = torch.from_numpy(logit_indices).to(self.device, non_blocking=True) logit_indices_device = torch.from_numpy(logit_indices).to(hidden_states.device, non_blocking=True)
return hidden_states, hidden_states[logit_indices_device] return hidden_states, hidden_states[logit_indices_device]
# ------------------------------------------------------------------ # ------------------------------------------------------------------