[Core] Add NIXL omni connector (#6093)

Signed-off-by: yuanwu <yuan.wu@intel.com>
Signed-off-by: Yuan Wu <yuan.wu@intel.com>
This commit is contained in:
Yuan Wu authored and GitHub committed 2026-09-13 16:35:02 +08:00
1 parent 01a2f93256
commit 6753bbd18c
33 files changed
+3032 -66

No files matched your search

+1
View File
@@ -136,6 +136,7 @@ nav:
- Mooncake Store Connector: design/feature/omni_connectors/mooncake_store_connector.md
- Mooncake Transfer Engine Connector: design/feature/omni_connectors/mooncake_transfer_engine_connector.md
- Mori Transfer Engine Connector: design/feature/omni_connectors/mori_transfer_engine_connector.md
- NIXL Connector: design/feature/omni_connectors/nixl_connector.md
- Shared Memory Connector: design/feature/omni_connectors/shared_memory_connector.md
- Yuanrong Store Connector: design/feature/omni_connectors/yuanrong_connector.md
- Yuanrong Transfer Engine Connector: design/feature/omni_connectors/yuanrong_transfer_engine_connector.md
@@ -90,6 +90,13 @@ depends on the installed kernels and model path.
| `VLLM_VIDEO_ASYNC_CHUNK` | `on` or `off`; default `on` | Streaming video output; read on attribute access | Environment-only setting. Values are trimmed and case-normalized; an invalid value warns once and uses `on`. | Experimental |
| `VLLM_VIDEO_AUDIO_DELTA_MODE` | `fast` or `slow`; default `fast` | Streaming video audio deltas; read on attribute access | Environment-only setting. Values are trimmed and case-normalized; an invalid value warns once and uses `fast`. | Experimental |
### NIXL stage transfer
| Name | Type and default | Applies to and read time | Precedence and invalid values | Lifecycle |
| --- | --- | --- | --- | --- |
| `VLLM_OMNI_NIXL_LEASE_S` | Float seconds; default `3600` | Unclaimed producer payload expiry; connector construction | Nonempty environment value overrides `lease_seconds`; empty falls back to config/default. Invalid floats raise `ValueError`. Claimed READ allocations never expire by time. | Experimental |
| `VLLM_OMNI_NIXL_XFER_TIMEOUT_S` | Float seconds; default `300` | Receiver READ wait; connector construction | Nonempty environment value overrides `transfer_timeout_s`; empty falls back to config/default. Invalid floats raise `ValueError`. Timeout does not cancel DMA or release active allocations. | Experimental |
### Server storage
Storage names use Pydantic's nested-settings delimiter (`__`). They are read
@@ -9,6 +9,7 @@ Backend-specific setup lives in separate docs:
- [MooncakeStoreConnector](omni_connectors/mooncake_store_connector.md)
- [MooncakeTransferEngineConnector](omni_connectors/mooncake_transfer_engine_connector.md)
- [MoriTransferEngineConnector](omni_connectors/mori_transfer_engine_connector.md)
- [NixlConnector](omni_connectors/nixl_connector.md)
- [YuanrongConnector](omni_connectors/yuanrong_connector.md)
- [YuanrongTransferEngineConnector](omni_connectors/yuanrong_transfer_engine_connector.md)
@@ -25,6 +26,7 @@ Current connectors operate in D2H2D (device to host to device) mode.
| Multi node (Mooncake Store) | MooncakeStoreConnector | TCP-based, requires Mooncake Master + metadata server. |
| Multi node (Mooncake RDMA) | MooncakeTransferEngineConnector | RDMA/TCP direct transfer with managed memory pool. Fastest. |
| Multi node (Mori RDMA) | MoriTransferEngineConnector | RDMA direct transfer via Mori IOEngine. |
| Multi node (NIXL) | NixlConnector | NIXL `READ` over UCX; the only backend covering Intel XPU. |
| Multi node (Yuanrong) | YuanrongConnector | Requires Yuanrong Datasystem + etcd. |
| Ascend NPU P2P (Yuanrong TE) | YuanrongTransferEngineConnector | Uses Yuanrong TransferEngine directly. Configure NPU device IPv4 and `memory_pool_device: "npu"`. |
@@ -0,0 +1,135 @@
# NixlConnector
## When to Use
Multi-node or intra-node stage transfer over NIXL, which brokers RDMA/shared-memory
transports through a single API. Useful when the deployment already standardises on
NIXL for KV transfer, or when the target accelerator has no Mooncake/Mori backend --
NIXL reaches Intel XPU through UCX's Level Zero support.
## Mechanism
Uses vLLM's `NixlWrapper` (`vllm.distributed.nixl_utils`) to register the producer's
tensors and let the consumer pull them with a NIXL `READ`.
- Data Plane: NIXL agent-to-agent `READ`, GPU-to-GPU where the backend allows it.
- Control Plane: a ZMQ ROUTER socket atomically claims the source allocation before
returning descriptors. Forwarded `put()` metadata includes the generation and
endpoint; the consumer claims that generation before submitting any READ.
Transfer metadata uses schema version 1. Tensor descriptors are grouped by NIXL
memory type (`DRAM`, `VRAM`, and so on), while each descriptor retains its global
tensor index so mixed CPU/accelerator payloads can be reconstructed in order.
Payloads are not restricted to tensors. A single tensor or a list of tensors is
transferred as-is; a nested structure has its tensor leaves extracted and shipped
alongside a msgpack-encoded skeleton; anything else is msgpack-encoded into one
uint8 tensor. Mapping and sequence structure is preserved. `msgspec.Struct` values,
including `OmniPayloadStruct`, are normalised to mappings so their tensor leaves use
the direct NIXL path rather than CPU object serialisation.
Each NIXL agent uses strict thread synchronization. A consumer performs one short
metadata query per scheduler poll (10ms by default), which prevents an unavailable
producer from blocking unrelated requests. The regular transfer-completion handshake
keeps the configured transfer timeout.
## Installation
CUDA hosts can install the published wheel:
```bash
pip install nixl
```
For Intel XPU, use `docker/Dockerfile.xpu`. Its upstream vLLM XPU base image
provides a matching torch-xpu, UCX with Level Zero support, and NIXL runtime.
## Configuration
```yaml
connectors:
nixl_connector:
name: NixlConnector
extra:
host: "auto"
zmq_port: 50061
backends: ["UCX"]
stages:
- stage_id: 0
output_connectors:
to_stage_1: nixl_connector
- stage_id: 1
input_connectors:
from_stage_0: nixl_connector
```
Parameters:
- `host`: address the producer binds its handshake socket to (`"auto"` to detect).
- `zmq_port`: handshake port. If a producer omits it, an ephemeral port is opened
for ownership claims and returned in `put()` metadata. The value in the deploy YAML is a
base port. Purpose, replica, rank, and producer-stage offsets are added centrally,
so colocated connectors and tensor-parallel workers receive distinct endpoints
without one config entry per rank.
- `sender_host` / `sender_zmq_port`: consumer-side override naming the producer's
handshake endpoint. Only needed when the consumer cannot learn it from metadata.
In replicated deployments, the producer endpoint is carried with each request and
selected at `get()` time; it is not mutable connector-wide state.
Explicit sender ports are preserved, not recomputed using the receiver replica.
Intermediate NIXL stages retain their incoming receiver configuration and a
separate outgoing bind configuration, so one worker can receive and publish.
- `backends`: NIXL backends to register memory with. Defaults to `["UCX"]`.
- `receive_device`: forces where received tensors land. By default the consumer
keeps the producer's device *type* but uses its own current device of that type.
- `memory_type`: overrides the NIXL memory type, otherwise `DRAM` for CPU tensors
and `VRAM` for accelerator tensors.
- `lease_seconds`: how long a `put()` payload stays registered while waiting to be
read (default 3600). The consumer reports completion, so this only bounds payloads
nobody has claimed. An internal reaper releases only unclaimed registrations.
`VLLM_OMNI_NIXL_LEASE_S` overrides it.
- `transfer_timeout_s`: how long a `get()` waits for its `READ` to complete
(default 300). NIXL 1.3 has no transfer cancellation API, so a timed-out transfer's
buffers and registrations remain owned by the connector until NIXL reports a
terminal state; `close()` waits for that state before releasing them.
`VLLM_OMNI_NIXL_XFER_TIMEOUT_S` overrides it.
### Source ownership and failure limits
Claims and expiry use the same lock. Completion identifies both a payload
generation and a unique READ claim; duplicate or stale ACKs cannot release a
different reader or replacement payload. Replacing a claimed key is rejected.
Terminal errors also release claims; timeout, unknown status, and active sibling
transfers retain the complete destination bundle and source claim until terminal.
Zero-element tensors retain their shape/dtype/skeleton positions but never create
DMA descriptors or registrations.
A lost metadata response, lost completion ACK, or abandoned consumer can retain a
claim indefinitely. NIXL 1.3 cannot prove remote cancellation, so neither TTL nor
`cleanup()` frees those allocations. Producer `close()` rejects new work and
retains its agent, listener and claimed allocations; a later `close()` finishes
teardown after claims drain. Permanently abandoned claims remain until process
exit. This favors memory safety over bounded shutdown/memory usage. Only trusted
peers may access this unauthenticated control plane. Both endpoints must use the
claim-aware protocol; rolling compatibility with older GET_META clients is not
provided. Legacy externally-owned metadata without a generation is accepted only
under the caller's lifetime guarantee, not managed as a leased allocation here.
## Validation
The native CUDA smoke test requires NIXL and at least two visible CUDA devices:
```bash
UCX_RCACHE_MAX_UNRELEASED=1024 pytest -o addopts='' -q -s \
tests/distributed/omni_connectors/test_nixl_connector_native.py
```
It starts independent producer and consumer processes, loads the real UCX backend,
performs the ZMQ `GET_META` handshake and a native NIXL `READ` from GPU 0 to GPU 1,
checks mixed CPU/GPU tensor leaves from an `OmniPayloadStruct`, sends `XFER_DONE`,
and verifies that producer registrations are released. This validates same-host,
cross-GPU operation. Inter-node UCX transport is supported by the design but was not
validated by this test.
For more details, refer to the [NIXL repository](https://github.com/ai-dynamo/nixl).
+1
View File
@@ -36,6 +36,7 @@ implementation contract; it is not, by itself, a general support claim.
- [Mooncake Store Connector](feature/omni_connectors/mooncake_store_connector.md)
- [Mooncake Transfer Engine Connector](feature/omni_connectors/mooncake_transfer_engine_connector.md)
- [Mori Transfer Engine Connector](feature/omni_connectors/mori_transfer_engine_connector.md)
- [NIXL Connector](feature/omni_connectors/nixl_connector.md)
- [Shared Memory Connector](feature/omni_connectors/shared_memory_connector.md)
- [Yuanrong Store Connector](feature/omni_connectors/yuanrong_connector.md)
- [Yuanrong Transfer Engine Connector](feature/omni_connectors/yuanrong_transfer_engine_connector.md)
+1 -1
View File
@@ -164,7 +164,7 @@ def test_inventory_matches_reviewed_snapshot_counts():
"""Make an inventory expansion an explicit review decision."""
category_counts = Counter(item.category for item in ENVIRONMENT_VARIABLE_INVENTORY.values())
assert category_counts == {
EnvironmentVariableCategory.PUBLIC_OMNI: 25,
EnvironmentVariableCategory.PUBLIC_OMNI: 27,
EnvironmentVariableCategory.INHERITED_VLLM: 20,
EnvironmentVariableCategory.PLATFORM_EXTERNAL: 27,
EnvironmentVariableCategory.MODEL_SPECIFIC: 63,
@@ -58,6 +58,7 @@ def _make_request(req_id: str, status: str = "waiting") -> SimpleNamespace:
num_computed_tokens=0,
_all_token_ids=[],
_output_token_ids=[],
payload_sender_info=None,
)
@@ -270,6 +271,19 @@ class TestWaitingForInputTransition(unittest.TestCase):
self.assertEqual(len(coord.pending_input_registrations), 1)
self.assertEqual(coord.pending_input_registrations[0].request_id, "r1")
def test_pending_input_registration_carries_payload_sender_info(self):
coord = OmniSchedulingCoordinator(stage_id=1)
req = _make_request("r1", status=RequestStatus.WAITING_FOR_INPUT)
req.payload_sender_info = {"host": "10.0.0.1", "zmq_port": 50051}
waiting = MockQueue([req])
coord.process_pending_full_payload_inputs(waiting, set())
self.assertEqual(
coord.pending_input_registrations[0].payload_sender_info,
{"host": "10.0.0.1", "zmq_port": 50051},
)
def test_idle_cycles_retain_received_marker_before_request_appears(self):
coord = OmniSchedulingCoordinator(stage_id=1)
coord._full_payload_input_received.add("late")
@@ -4,7 +4,7 @@
import threading
from collections import deque
from types import MethodType, SimpleNamespace
from unittest.mock import Mock, patch
from unittest.mock import Mock, call, patch
import pytest
import torch
@@ -543,7 +543,7 @@ def test_create_connector_config_parsing(monkeypatch, raw_cfg, expected_name, ex
assert connector == "ok"
assert isinstance(captured["spec"], ConnectorSpec)
assert captured["spec"].name == expected_name
assert captured["spec"].extra == expected_extra
assert captured["spec"].extra == {**expected_extra, "stage_id": 0}
def test_load_poll(build_adapter):
@@ -570,8 +570,51 @@ def test_load_poll(build_adapter):
assert request.additional_information == payload
assert adapter.get_req_chunk["req-1"] == 1
assert "req-1" not in adapter._finished_load_reqs
assert "req-1" in adapter.upstream_exhausted_requests
assert "req-1" not in adapter._pending_load_reqs
def test_load_poll_uses_request_scoped_payload_sender_endpoint(build_adapter):
adapter, connector = build_adapter(stage_id=2, model_mode="ar")
request = _req("req-1", RequestStatus.WAITING, external_req_id="external-1")
request.payload_sender_info = {"host": "10.0.0.1", "zmq_port": 50051}
connector.get.return_value = None
adapter._poll_single_request(_dequeue_load_entry(adapter, request))
connector.get.assert_called_once_with(
"1",
"2",
"req-1_1_0",
{"source_host": "10.0.0.1", "source_port": 50051},
)
def test_load_poll_keeps_concurrent_payload_sender_endpoints_distinct(build_adapter):
adapter, connector = build_adapter(stage_id=2, model_mode="ar")
first = _req("req-1", RequestStatus.WAITING)
first.payload_sender_info = {"host": "10.0.0.1", "zmq_port": 50051}
second = _req("req-2", RequestStatus.WAITING)
second.payload_sender_info = {"host": "10.0.0.2", "zmq_port": 51051}
connector.get.return_value = None
second_entry = _dequeue_load_entry(adapter, second)
first_entry = _dequeue_load_entry(adapter, first)
adapter._poll_single_request(second_entry)
adapter._poll_single_request(first_entry)
assert connector.get.call_args_list == [
call(
"1",
"2",
"req-2_1_0",
{"source_host": "10.0.0.2", "source_port": 51051},
),
call(
"1",
"2",
"req-1_1_0",
{"source_host": "10.0.0.1", "source_port": 50051},
),
]
def test_load_async_does_not_requeue_registered_inflight_request(build_adapter):
@@ -2770,7 +2813,8 @@ def test_successful_put_keeps_terminal_chunk_sequence(build_adapter, finish_insi
scheduler.add_request(final_update)
return True, 1, {}
def get_by_key(_from_stage, _to_stage, key):
def get_by_key(_from_stage, _to_stage, key, metadata=None):
assert metadata is None
payload = payloads.pop(key, None)
return (payload, 1) if payload is not None else None
@@ -0,0 +1,891 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
Unit tests for the NixlConnector metadata handshake.
NIXL itself is stubbed out, so these tests exercise the ZMQ control plane and
the payload normalisation rather than any actual RDMA transfer.
"""
import ctypes
import sys
import time
import types
import msgspec
import pytest
import torch
import zmq
from vllm_omni.data_entry_keys import HiddenStatesStruct, MetaStruct, OmniPayloadStruct
pytestmark = [pytest.mark.cpu, pytest.mark.parallel, pytest.mark.core_model]
PORT = 47431
def _wait_for_metadata(consumer, key, metadata=None, *, timeout=2.0):
"""Retry outside the connector's deliberately bounded metadata query."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
resolved = consumer._resolve_metadata(key, metadata)
if resolved is not None:
return resolved
time.sleep(0.01)
pytest.fail(f"Timed out waiting for metadata for {key!r}")
@pytest.mark.parametrize("misses", [0, 2])
def test_wait_for_metadata_retries_until_success(monkeypatch, misses):
expected = {"claim_id": "claim"}
forwarded = {"source_host": "127.0.0.1", "source_port": PORT}
calls = []
def resolve(key, metadata):
calls.append((key, metadata))
return None if len(calls) <= misses else expected
ticks = iter(range(10))
monkeypatch.setattr(time, "monotonic", lambda: next(ticks))
monkeypatch.setattr(time, "sleep", lambda _: None)
consumer = types.SimpleNamespace(_resolve_metadata=resolve)
assert _wait_for_metadata(consumer, "retry", forwarded, timeout=5) is expected
assert calls == [("retry", forwarded)] * (misses + 1)
def test_wait_for_metadata_stops_at_deadline(monkeypatch):
calls = []
ticks = iter([0.0, 0.0, 1.0, 2.0])
monkeypatch.setattr(time, "monotonic", lambda: next(ticks))
monkeypatch.setattr(time, "sleep", lambda _: None)
consumer = types.SimpleNamespace(_resolve_metadata=lambda *args: calls.append(args))
with pytest.raises(pytest.fail.Exception, match="Timed out waiting for metadata for 'missing'"):
_wait_for_metadata(consumer, "missing")
assert calls == [("missing", None)] * 2
@pytest.mark.parametrize("async_chunk", [False, True])
def test_three_stage_incoming_and_outgoing_endpoints(nixl_connector_cls, async_chunk):
from vllm_omni.distributed.omni_connectors.utils.config import ConnectorSpec, OmniTransferConfig
from vllm_omni.distributed.omni_connectors.utils.initialization import resolve_connector_spec
from vllm_omni.engine.stage_init_utils import get_stage_connector_spec
config = OmniTransferConfig(
connectors={
("0", "1"): ConnectorSpec(name="NixlConnector", extra={"host": "127.0.0.1", "zmq_port": PORT}),
("1", "2"): ConnectorSpec(name="NixlConnector", extra={"host": "127.0.0.1", "zmq_port": PORT + 10}),
}
)
connectors = []
try:
for stage in range(3):
spec = get_stage_connector_spec(config, stage, async_chunk)
resolved = resolve_connector_spec(ConnectorSpec(**spec), stage_id=stage, role=spec["extra"]["role"])
connectors.append(nixl_connector_cls(resolved.extra))
first, middle, last = connectors
assert middle._sender_zmq_port == PORT
assert middle._serving_handshake
assert middle._zmq_port == PORT + 11
first.put("0", "1", "incoming", torch.ones(1))
incoming = _wait_for_metadata(middle, "incoming")
assert incoming["sender_zmq_port"] == PORT
assert incoming["generation"] == first._published["incoming"]["generation"]
middle.put("1", "2", "outgoing", torch.ones(1))
outgoing = _wait_for_metadata(last, "outgoing")
assert outgoing["sender_zmq_port"] == PORT + 11
assert outgoing["generation"] == middle._published["outgoing"]["generation"]
finally:
for connector in reversed(connectors):
for pending in connector._pending.values():
pending.claims.clear() # No DMA in this metadata-only probe.
connector.close()
@pytest.mark.parametrize("name", ["NixlConnector", "MooncakeTransferEngineConnector"])
def test_explicit_sender_port_is_not_derived(name):
from vllm_omni.distributed.omni_connectors.utils.config import ConnectorSpec
from vllm_omni.distributed.omni_connectors.utils.initialization import resolve_connector_spec
spec = ConnectorSpec(name=name, extra={"sender_zmq_port": 55000, "from_stage": 0})
resolved = resolve_connector_spec(spec, stage_id=1, role="receiver", replica_id=3, local_rank=2)
assert resolved.extra["sender_zmq_port"] == 55000
assert spec.extra == {"sender_zmq_port": 55000, "from_stage": 0}
spec.extra["zmq_port"] = "${UNUSED_NIXL_BASE_PORT}"
assert resolve_connector_spec(spec, stage_id=1, role="receiver").extra["sender_zmq_port"] == 55000
@pytest.mark.parametrize("need_recv_cache", [False, True])
@pytest.mark.parametrize("update_before_init", [False, True])
def test_manager_receiver_does_not_bind_shared_producer_port(producer, need_recv_cache, update_before_init):
"""Payload-only and KV receivers must dial, not bind, the incoming edge."""
from vllm_omni.distributed.omni_connectors.kv_transfer_manager import (
OmniKVCacheConfig,
OmniKVTransferManager,
)
manager = OmniKVTransferManager(
OmniKVCacheConfig(
connector_config={
"type": "NixlConnector",
"role": "receiver",
"host": "127.0.0.1",
"zmq_port": PORT,
"backends": ["UCX"],
},
from_stage="0",
to_stage="1",
stage_id=1,
need_recv_cache=need_recv_cache,
)
)
sender_info = {"host": producer.host, "zmq_port": producer._zmq_port}
if update_before_init:
manager.update_sender_info(sender_info)
receiver = manager.connector
try:
assert receiver is not None, "Receiver incorrectly tried to bind the producer's occupied port"
assert receiver._zmq_port is None
assert not receiver._serving_handshake
if not update_before_init:
assert receiver._sender_zmq_port is None
manager.update_sender_info(sender_info)
assert receiver._sender_zmq_port == producer._zmq_port
assert manager.config.connector_config["zmq_port"] == PORT # Do not mutate the shared specification.
success, _, _ = producer.put("0", "1", "manager-receive", torch.ones(1))
assert success
metadata = _wait_for_metadata(receiver, "manager-receive")
assert metadata["generation"] == producer._published["manager-receive"]["generation"]
receiver._notify_transfer_done("manager-receive", metadata)
assert not producer._pending
finally:
if receiver is not None:
receiver.close()
@pytest.mark.parametrize("need_recv_cache", [False, True])
def test_manager_receiver_preserves_explicit_standalone_sender(producer, need_recv_cache):
from vllm_omni.distributed.omni_connectors.kv_transfer_manager import (
OmniKVCacheConfig,
OmniKVTransferManager,
)
manager = OmniKVTransferManager(
OmniKVCacheConfig(
connector_config={
"type": "NixlConnector",
"role": "receiver",
"zmq_port": PORT,
"sender_host": producer.host,
"sender_zmq_port": producer._zmq_port,
},
need_recv_cache=need_recv_cache,
)
)
receiver = manager.connector
try:
assert receiver is not None
assert receiver._zmq_port is None and not receiver._serving_handshake
assert (receiver._sender_host, receiver._sender_zmq_port) == (producer.host, producer._zmq_port)
success, _, _ = producer.put("0", "1", "standalone-receive", torch.ones(1))
assert success
metadata = _wait_for_metadata(receiver, "standalone-receive")
assert metadata["generation"] == producer._published["standalone-receive"]["generation"]
receiver._notify_transfer_done("standalone-receive", metadata)
assert not producer._pending
finally:
if receiver is not None:
receiver.close()
def test_middle_stage_advertises_its_outgoing_replica_endpoint():
from vllm_omni.engine.stage_engine_core_client import StageEngineCoreClient
client = object.__new__(StageEngineCoreClient)
client.stage_id = 1
client.replica_id = 3
client.vllm_config = types.SimpleNamespace(
model_config=types.SimpleNamespace(
stage_connector_config={
"name": "NixlConnector",
"extra": {
"role": "receiver",
"host": "10.0.0.1",
"zmq_port": 47000,
"from_stage": 0,
"outgoing": {"host": "10.0.0.2", "zmq_port": 48000, "from_stage": 1},
},
}
)
)
assert client._build_payload_sender_info() == {"host": "10.0.0.2", "zmq_port": 51073}
@pytest.mark.parametrize("direct", [False, True])
@pytest.mark.usefixtures("reliable_claim_queries")
def test_claimed_source_survives_expiry_and_cleanup(producer, consumer, direct):
_, _, metadata = producer.put("0", "1", "claimed", torch.ones(1))
resolved = consumer._resolve_metadata("claimed", metadata if direct else None)
pending = producer._pending["claimed"]
pending.deadline = 0
producer._cleanup_expired_pending()
assert producer._pending.get("claimed") is pending
producer.cleanup("claimed")
assert producer._pending.get("claimed") is pending
assert producer._agent.registered
consumer._notify_transfer_done("claimed", resolved)
consumer._notify_transfer_done("claimed", resolved)
assert producer._pending == {}
assert producer._agent.registered == []
@pytest.mark.usefixtures("reliable_claim_queries")
def test_claims_are_generation_scoped_and_duplicate_ack_cannot_release_sibling(producer, consumer):
_, _, original = producer.put("0", "1", "owners", torch.ones(1))
first = consumer._resolve_metadata("owners", original)
second = consumer._resolve_metadata("owners", original)
assert first["claim_id"] != second["claim_id"]
assert not producer.put("0", "1", "owners", torch.zeros(1))[0]
consumer._notify_transfer_done("owners", first)
consumer._notify_transfer_done("owners", first)
assert len(producer._pending["owners"].claims) == 1
consumer._notify_transfer_done("owners", second)
assert not producer._pending
producer.put("0", "1", "owners", torch.zeros(1))
consumer._notify_transfer_done("owners", second)
assert "owners" in producer._pending
assert consumer._resolve_metadata("owners", original) is None
@pytest.mark.parametrize("get_metadata", [None, {"schema_version": 1, "tensor_specs": [], "descriptor_groups": []}])
@pytest.mark.usefixtures("reliable_claim_queries")
def test_abandoned_claim_survives_close_and_late_completion(producer, consumer, get_metadata):
_, _, metadata = producer.put("0", "1", "abandoned", torch.ones(1))
claimed = consumer._resolve_metadata("abandoned", metadata)
producer._pending["abandoned"].deadline = 0
producer.close()
assert producer._closing and not producer._closed
assert producer._agent.registered
assert producer._listener_thread.is_alive()
with pytest.raises(RuntimeError, match="closed"):
producer.put("0", "1", "new", torch.ones(1))
metrics = dict(producer._metrics)
with pytest.raises(RuntimeError, match="Cannot get data: NixlConnector is closed"):
producer.get("0", "1", "new", get_metadata)
assert producer._metrics == metrics
assert producer._pending["abandoned"].claims == {claimed["claim_id"]}
assert consumer._resolve_metadata("abandoned", metadata) is None
consumer._notify_transfer_done("abandoned", claimed)
assert not producer._pending
assert not producer._agent.registered
assert producer._listener_thread.is_alive()
producer.close()
assert not producer._agent.registered
assert producer._closed
@pytest.fixture
def copying_native_agent(consumer, monkeypatch):
"""Strict fake descriptors and actual CPU copies; not native NIXL evidence."""
calls = []
def descriptors(regions, memory_type):
assert regions and all(region[0] > 0 and region[1] > 0 for region in regions)
return regions
def prepare(operation, local, local_ids, remote, remote_ids):
assert operation == "READ"
assert local_ids == remote_ids == list(range(len(local)))
return list(zip(local, remote, strict=True))
def transfer(pairs):
calls.append(pairs)
for destination, source in pairs:
assert destination[1] == source[1] > 0
ctypes.memmove(destination[0], source[0], source[1])
agent = consumer._agent
monkeypatch.setattr(agent, "add_remote_agent", lambda metadata: "producer", raising=False)
monkeypatch.setattr(agent, "get_xfer_descs", descriptors, raising=False)
monkeypatch.setattr(agent, "prep_xfer_dlist", lambda agent, descs: descs, raising=False)
monkeypatch.setattr(agent, "make_prepped_xfer", prepare, raising=False)
monkeypatch.setattr(agent, "transfer", transfer, raising=False)
monkeypatch.setattr(agent, "check_xfer_state", lambda handle: "DONE", raising=False)
for method in ("release_xfer_handle", "release_dlist_handle", "remove_remote_agent"):
monkeypatch.setattr(agent, method, lambda handle: None, raising=False)
return calls
@pytest.mark.parametrize("case", ["empty", "all_empty", "mixed", "scalar", "structured_empty", "structured_mixed"])
def test_zero_byte_leaves_roundtrip_without_native_descriptors(producer, consumer, copying_native_agent, case):
empty = torch.empty((2, 0, 3), dtype=torch.float64)
other = torch.empty((0,), dtype=torch.int64)
scalar = torch.tensor(7)
vector = torch.arange(6, dtype=torch.float32)
payload = {
"empty": empty,
"all_empty": [empty, other],
"mixed": [empty, scalar, other, vector],
"scalar": scalar,
"structured_empty": {"leaves": (empty, [other]), "label": "empty"},
"structured_mixed": {"leaves": (empty, [scalar, other, vector]), "label": "mixed"},
}[case]
ok, size, metadata = producer.put("0", "1", "empty-slots", payload)
assert ok
indices = [index for group in metadata["descriptor_groups"] for index in group["tensor_indices"]]
assert sorted(indices) == [i for i, spec in enumerate(metadata["tensor_specs"]) if spec["size"]]
actual, received_size = consumer.get("0", "1", "empty-slots", metadata)
if isinstance(payload, dict):
assert actual["label"] == payload["label"]
torch.testing.assert_close(actual["leaves"], payload["leaves"], rtol=0, atol=0)
else:
torch.testing.assert_close(actual, payload, rtol=0, atol=0)
assert size == received_size
if case in ("empty", "all_empty"):
assert not copying_native_agent
assert not producer._pending and not producer._agent.registered
assert not consumer._agent.registered
@pytest.mark.parametrize("direct", [False, True])
@pytest.mark.parametrize("outcome", ["done", "error", "timeout", "unknown"])
def test_read_ownership_through_terminal_and_deferred_paths(
producer, consumer, copying_native_agent, monkeypatch, direct, outcome
):
import threading
_, _, metadata = producer.put("0", "1", "active-read", torch.ones(2))
entered, proceed = threading.Event(), threading.Event()
state = ["PROC"]
monkeypatch.setattr(consumer._agent, "check_xfer_state", lambda handle: state[0])
def wait(handle, key):
entered.set()
assert proceed.wait(5)
if outcome == "done":
state[0] = "DONE"
elif outcome == "error":
state[0] = "ERR"
raise RuntimeError("terminal error")
else:
if outcome == "unknown":
state[0] = "UNKNOWN"
raise TimeoutError("still owned")
monkeypatch.setattr(consumer, "_wait_for_transfer", wait)
worker = threading.Thread(target=consumer.get, args=("0", "1", "active-read", metadata if direct else None))
worker.start()
try:
assert entered.wait(5)
pending = producer._pending["active-read"]
pending.deadline = 0
producer._cleanup_expired_pending()
producer.cleanup("active-read")
assert producer._pending["active-read"] is pending
assert producer._agent.registered
finally:
proceed.set()
worker.join(5)
assert not worker.is_alive()
if outcome in ("timeout", "unknown"):
assert producer._agent.registered
assert consumer._deferred_transfers
consumer._reap_deferred_transfers()
assert producer._agent.registered
state[0] = "DONE"
deadline = time.monotonic() + 5
while producer._pending and time.monotonic() < deadline:
time.sleep(0.01)
assert not producer._pending
assert not producer._agent.registered
class _FakeNixlAgent:
def __init__(self, name, config=None):
self.name = name
self.config = config
self.registered = []
def get_reg_descs(self, regions, memory_type):
return ("reg", tuple(regions), memory_type)
def register_memory(self, descs, backends=None):
self.registered.append(descs)
def deregister_memory(self, descs):
if descs in self.registered:
self.registered.remove(descs)
def get_agent_metadata(self):
return f"agent-metadata-{self.name}".encode()
@pytest.fixture
def nixl_connector_cls(monkeypatch):
"""Import NixlConnector with vLLM's optional NIXL dependency stubbed out."""
stub = types.ModuleType("vllm.distributed.nixl_utils")
stub.NixlWrapper = _FakeNixlAgent # type: ignore[attr-defined]
config_module = types.ModuleType("test_nixl_config_api")
config_sync_type = types.SimpleNamespace(NIXL_THREAD_SYNC_STRICT="strict")
config_module.nixl_thread_sync_t = config_sync_type # type: ignore[attr-defined]
def agent_config(**kwargs):
return kwargs
agent_config.__module__ = config_module.__name__
config_module.nixl_agent_config = agent_config # type: ignore[attr-defined]
stub.nixl_agent_config = agent_config # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "vllm.distributed.nixl_utils", stub)
monkeypatch.setitem(sys.modules, config_module.__name__, config_module)
sync_type = types.SimpleNamespace(NIXL_THREAD_SYNC_STRICT="wrong-public-enum")
nixl_module = types.ModuleType("nixl")
nixl_module.nixl_thread_sync_t = sync_type # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "nixl", nixl_module)
from vllm_omni.distributed.omni_connectors.connectors.nixl_connector import NixlConnector
return NixlConnector
@pytest.fixture
def producer(nixl_connector_cls):
connector = nixl_connector_cls({"role": "sender", "host": "127.0.0.1", "zmq_port": PORT})
yield connector
# These control-plane tests do not submit DMA. Explicitly complete claims
# made by metadata-only probes before tearing down the shared port.
for key, pending in list(connector._pending.items()):
for claim in list(pending.claims):
from vllm_omni.distributed.omni_connectors.connectors.nixl_connector import _XFER_DONE_MSG
connector._handle_handshake_message(
_XFER_DONE_MSG
+ msgspec.msgpack.encode({"key": key, "generation": pending.generation, "claim_id": claim})
)
connector.close()
@pytest.fixture
def consumer(nixl_connector_cls):
connector = nixl_connector_cls({"role": "receiver", "sender_host": "127.0.0.1", "sender_zmq_port": PORT})
yield connector
connector.close()
@pytest.fixture
def reliable_claim_queries(producer, consumer, monkeypatch):
"""Ownership probes need exact claims, not claims retained after lost replies.
Exercise the real resolver, wire encoding and producer handler synchronously;
discovery tests separately cover bounded queries over real ZMQ sockets.
"""
import uuid
from vllm_omni.distributed.omni_connectors.connectors.nixl_connector import _GET_META_MSG, _META_NOT_FOUND
def query(key, host, port, *, generation=None):
assert (host, port) == (producer.host, producer._zmq_port)
request = {"key": key, "generation": generation, "claim_id": uuid.uuid4().hex}
reply = producer._handle_handshake_message(_GET_META_MSG + msgspec.msgpack.encode(request))
return None if reply == _META_NOT_FOUND else msgspec.msgpack.decode(reply)
monkeypatch.setattr(consumer, "_query_metadata_at", query)
def test_put_publishes_its_handshake_endpoint(producer):
ok, size, metadata = producer.put("0", "1", "req-0", torch.arange(8, dtype=torch.float32))
assert ok is True
assert size == 32
assert metadata["sender_host"] == "127.0.0.1"
assert metadata["sender_zmq_port"] == PORT
def test_agent_uses_strict_thread_synchronization(producer):
assert producer._agent.config["sync_mode"] == "strict"
def test_handshake_serves_metadata_when_caller_has_none(producer, consumer):
_, _, published = producer.put("0", "1", "req-1", torch.arange(4, dtype=torch.float32))
resolved = _wait_for_metadata(consumer, "req-1")
# msgpack has no tuple type, so region descriptors arrive as lists; get()
# re-tuples them before handing them to NIXL.
assert resolved.pop("claim_id")
assert resolved == msgspec.msgpack.decode(msgspec.msgpack.encode(published))
assert [tuple(region) for group in resolved["descriptor_groups"] for region in group["regions"]] == [
region for group in published["descriptor_groups"] for region in group["regions"]
]
def test_legacy_metadata_without_generation_skips_the_handshake(consumer):
"""Externally owned metadata without a generation needs no ownership claim."""
direct = {"schema_version": 1, "kind": "tensors", "tensor_specs": []}
assert consumer._resolve_metadata("req-2", direct) is direct
def test_source_endpoint_metadata_overrides_configured_sender(consumer, monkeypatch):
queried = []
expected = {"schema_version": 1, "kind": "tensors", "tensor_specs": []}
def query(get_key, host, port):
queried.append((get_key, host, port))
return expected
monkeypatch.setattr(consumer, "_query_metadata_at", query)
resolved = consumer._resolve_metadata(
"req-tp4-rank3",
{"source_host": "10.0.0.3", "source_port": PORT + 3 * 16},
)
assert resolved is expected
assert queried == [("req-tp4-rank3", "10.0.0.3", PORT + 3 * 16)]
def test_unknown_key_is_queried_once(producer, consumer, monkeypatch):
socket = consumer._get_req_socket(f"tcp://127.0.0.1:{PORT}")
send_count = 0
original_send = socket.send
def count_send(message):
nonlocal send_count
send_count += 1
return original_send(message)
monkeypatch.setattr(socket, "send", count_send)
assert consumer._resolve_metadata("never-published", None) is None
assert send_count == 1
assert socket.getsockopt(zmq.RCVTIMEO) == 10
@pytest.mark.usefixtures("reliable_claim_queries")
def test_transfer_done_releases_the_producer_buffer(producer, consumer):
_, _, metadata = producer.put("0", "1", "req-3", torch.arange(4, dtype=torch.float32))
assert producer._pending and producer._agent.registered
metadata = consumer._resolve_metadata("req-3", metadata)
consumer._notify_transfer_done("req-3", metadata)
assert producer._pending == {}
assert producer._published == {}
assert producer._agent.registered == []
def test_direct_metadata_has_ephemeral_ownership_endpoint(nixl_connector_cls):
connector = nixl_connector_cls({})
try:
assert connector._zmq_ctx is not None
assert connector._listener_thread is not None
_, _, metadata = connector.put("0", "1", "req-4", torch.zeros(2))
assert metadata["sender_host"]
assert metadata["sender_zmq_port"] > 0
finally:
connector.close()
def test_idle_producer_lease_expires_without_another_put(nixl_connector_cls):
connector = nixl_connector_cls({"role": "sender", "lease_seconds": 0.01})
try:
connector.put("0", "1", "req-expire", torch.zeros(2))
deadline = time.monotonic() + 1.0
while connector._pending and time.monotonic() < deadline:
time.sleep(0.01)
assert connector._pending == {}
assert connector._published == {}
assert connector._agent.registered == []
assert connector._registered_descs == []
finally:
connector.close()
def test_close_stops_lease_reaper(nixl_connector_cls):
connector = nixl_connector_cls({"role": "sender"})
lease_thread = connector._lease_thread
connector.close()
assert lease_thread is not None
assert not lease_thread.is_alive()
assert connector._lease_thread is None
def test_deferred_transfer_is_retained_while_active(nixl_connector_cls):
from vllm_omni.distributed.omni_connectors.connectors.nixl_connector import _DeferredTransfer
connector = nixl_connector_cls({"role": "receiver"})
released = []
connector._agent.check_xfer_state = lambda handle: "PROC"
connector._agent.release_xfer_handle = lambda handle: released.append(("handle", handle))
connector._agent.release_dlist_handle = lambda handle: released.append(("dlist", handle))
connector._agent.remove_remote_agent = lambda agent: released.append(("agent", agent))
transfer = _DeferredTransfer(
tensors=[torch.zeros(1)],
registrations=["registration"],
dlists=["local", "remote"],
handles=["transfer"],
remote_agent="producer",
)
connector._defer_transfer(transfer)
try:
connector._reap_deferred_transfers()
assert released == []
assert transfer in connector._deferred_transfers
assert transfer.tensors
finally:
connector._agent.check_xfer_state = lambda handle: "DONE"
connector.close()
def test_deferred_transfer_releases_exactly_once_after_done(nixl_connector_cls):
from vllm_omni.distributed.omni_connectors.connectors.nixl_connector import _DeferredTransfer
connector = nixl_connector_cls({"role": "receiver"})
released = []
connector._agent.check_xfer_state = lambda handle: "DONE"
connector._agent.release_xfer_handle = lambda handle: released.append(("handle", handle))
connector._agent.release_dlist_handle = lambda handle: released.append(("dlist", handle))
connector._agent.remove_remote_agent = lambda agent: released.append(("agent", agent))
connector._agent.deregister_memory = lambda descs: released.append(("registration", descs))
transfer = _DeferredTransfer(
tensors=[torch.zeros(1)],
registrations=["registration"],
dlists=["local", "remote"],
handles=["transfer"],
remote_agent="producer",
)
connector._defer_transfer(transfer)
try:
connector._reap_deferred_transfers()
connector._reap_deferred_transfers()
assert released == [
("handle", "transfer"),
("dlist", "local"),
("dlist", "remote"),
("agent", "producer"),
("registration", "registration"),
]
assert connector._deferred_transfers == []
assert transfer.tensors == []
finally:
connector.close()
def test_active_sibling_transfer_requires_deferred_ownership(nixl_connector_cls):
connector = nixl_connector_cls({"role": "receiver"})
connector._agent.check_xfer_state = lambda handle: {"failed": "ERR", "active": "PROC"}[handle]
try:
assert connector._transfers_may_be_active(["failed", "active"]) is True
assert connector._transfers_may_be_active(["failed"]) is False
finally:
connector.close()
def test_get_defers_complete_ownership_when_sibling_transfer_is_active(nixl_connector_cls):
connector = nixl_connector_cls({"role": "receiver"})
connector._agent.add_remote_agent = lambda metadata: "producer"
connector._agent.get_xfer_descs = lambda regions, memory_type: (regions, memory_type)
connector._agent.prep_xfer_dlist = lambda agent, descs: (agent, tuple(descs[0]))
handles = iter(["failed", "active"])
connector._agent.make_prepped_xfer = lambda *_args: next(handles)
connector._agent.transfer = lambda handle: None
connector._agent.check_xfer_state = lambda handle: {"failed": "ERR", "active": "PROC"}[handle]
released = []
connector._agent.release_xfer_handle = lambda handle: released.append(("handle", handle))
connector._agent.release_dlist_handle = lambda handle: released.append(("dlist", handle))
connector._agent.remove_remote_agent = lambda agent: released.append(("agent", agent))
connector._agent.deregister_memory = lambda descs: released.append(("registration", descs))
metadata = {
"schema_version": 1,
"kind": "tensors",
"agent_metadata": b"producer-metadata",
"tensor_specs": [
{"shape": [1], "dtype": "torch.float32", "device": "cpu", "size": 4},
{"shape": [1], "dtype": "torch.float32", "device": "cpu", "size": 4},
],
"descriptor_groups": [
{"memory_type": "DRAM", "tensor_indices": [0], "regions": [(1, 4, 0, "")]},
{"memory_type": "DRAM", "tensor_indices": [1], "regions": [(2, 4, 0, "")]},
],
"size": 8,
}
try:
assert connector.get("0", "1", "req-active-sibling", metadata) is None
assert released == []
assert len(connector._deferred_transfers) == 1
deferred = connector._deferred_transfers[0]
assert deferred.handles == ["failed", "active"]
assert len(deferred.registrations) == 2
assert len(deferred.dlists) == 4
assert deferred.remote_agent == "producer"
assert len(deferred.tensors) == 2
finally:
connector._agent.check_xfer_state = lambda handle: "DONE"
connector.close()
@pytest.mark.parametrize(
"payload,expected_kind",
[
(torch.zeros(4), "tensors"),
([torch.zeros(2), torch.ones(3)], "tensors"),
({"hidden": torch.zeros(4), "meta": {"token_role_ids": [1, 2]}}, "structured"),
({"prompt": "a cat", "steps": 8}, "object"),
],
)
def test_payload_kinds_round_trip_through_the_handshake(producer, consumer, payload, expected_kind):
_, _, published = producer.put("0", "1", "req-kind", payload)
assert published["kind"] == expected_kind
resolved = _wait_for_metadata(consumer, "req-kind")
assert resolved["kind"] == expected_kind
assert resolved["tensor_specs"] == published["tensor_specs"]
def test_structured_payload_groups_descriptors_by_memory_type(producer, monkeypatch):
monkeypatch.setattr(
producer,
"_resolve_memory_type",
lambda tensor: "DRAM" if tensor.dtype == torch.uint8 else "VRAM",
)
_, _, metadata = producer.put(
"0",
"1",
"req-mixed",
{"meta": "value", "hidden": torch.ones(4, dtype=torch.float32)},
)
assert metadata["schema_version"] == 1
assert metadata["descriptor_groups"] == [
{
"memory_type": "DRAM",
"tensor_indices": [0],
"regions": metadata["descriptor_groups"][0]["regions"],
},
{
"memory_type": "VRAM",
"tensor_indices": [1],
"regions": metadata["descriptor_groups"][1]["regions"],
},
]
assert len(producer._agent.registered) == 2
def test_omni_payload_struct_uses_structured_tensor_path(producer):
payload = OmniPayloadStruct(
hidden_states=HiddenStatesStruct(output=torch.ones(4, dtype=torch.float32)),
meta=MetaStruct(left_context_size=2),
)
_, _, metadata = producer.put("0", "1", "req-struct", payload)
skeleton, tensors = producer._extract_tensor_leaves(payload)
assert metadata["kind"] == "structured"
assert len(tensors) == 1
assert torch.equal(tensors[0], payload.hidden_states.output)
assert skeleton["meta"]["left_context_size"] == 2
def test_partial_group_registration_failure_rolls_back(producer, monkeypatch):
monkeypatch.setattr(
producer,
"_resolve_memory_type",
lambda tensor: "DRAM" if tensor.dtype == torch.uint8 else "VRAM",
)
original_register = producer._agent.register_memory
def fail_second_group(descs, backends=None):
if descs[2] == "VRAM":
raise RuntimeError("registration failed")
original_register(descs, backends=backends)
monkeypatch.setattr(producer._agent, "register_memory", fail_second_group)
success, _, metadata = producer.put(
"0",
"1",
"req-partial",
{"meta": "value", "hidden": torch.ones(4, dtype=torch.float32)},
)
assert success is False
assert metadata is None
assert producer._agent.registered == []
assert producer._registered_descs == []
assert "req-partial" not in producer._pending
def test_reusing_put_key_releases_previous_registration(producer):
producer.put("0", "1", "req-reuse", torch.zeros(2))
previous_registration = producer._agent.registered[0]
producer.put("0", "1", "req-reuse", torch.ones(2))
assert previous_registration not in producer._agent.registered
assert len(producer._agent.registered) == 1
assert len(producer._registered_descs) == 1
def test_expired_snapshot_cannot_claim_replacement(producer):
from vllm_omni.distributed.omni_connectors.connectors.nixl_connector import _PendingPayload
old = _PendingPayload([torch.zeros(1)], ["old"], 0.0)
replacement = _PendingPayload([torch.ones(1)], ["new"], time.monotonic() + 60)
producer._pending["req-race"] = replacement
assert producer._take_pending("req-race", expected=old) is None
assert producer._pending["req-race"] is replacement
@pytest.mark.parametrize(
"indices",
[
[0, 0],
[0],
[0, 2],
[-1, 0],
],
)
def test_descriptor_groups_require_exact_tensor_index_partition(nixl_connector_cls, indices):
metadata = {
"schema_version": 1,
"descriptor_groups": [
{
"memory_type": "DRAM",
"tensor_indices": indices,
"regions": [(1, 4, 0, "")] * len(indices),
}
],
}
with pytest.raises(RuntimeError, match="exact partition"):
nixl_connector_cls._validated_descriptor_groups(
metadata, [{"shape": [1], "dtype": "torch.float32", "size": 4}] * 2
)
def test_receive_device_ignores_the_producer_index(nixl_connector_cls):
"""A producer on cuda:3 must not dictate the consumer's card."""
connector = nixl_connector_cls({})
try:
assert connector._resolve_receive_device("cpu") == torch.device("cpu")
assert connector._resolve_receive_device(None) == torch.device("cpu")
finally:
connector.close()
def test_receive_device_config_wins(nixl_connector_cls):
connector = nixl_connector_cls({"receive_device": "cpu"})
try:
assert connector._resolve_receive_device("cuda:3") == torch.device("cpu")
finally:
connector.close()
@@ -0,0 +1,204 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import multiprocessing as mp
import socket
import time
from typing import Any
import pytest
import torch
from vllm_omni.data_entry_keys import HiddenStatesStruct, MetaStruct, OmniPayloadStruct
from vllm_omni.platforms import current_omni_platform
pytestmark = [pytest.mark.core_model, pytest.mark.cuda, pytest.mark.parallel]
def _native_nixl_available() -> bool:
try:
from vllm.distributed.nixl_utils import NixlWrapper
except ImportError:
return False
return NixlWrapper is not None and torch.cuda.is_available() and torch.accelerator.device_count() >= 2
def _free_port() -> int:
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
return int(sock.getsockname()[1])
def _producer(port: int, ready: Any, consumed: Any, result: Any, claimed: Any, proceed: Any, wire: Any) -> None:
from vllm_omni.distributed.omni_connectors.connectors.nixl_connector import NixlConnector
current_omni_platform.set_device(0)
connector = NixlConnector(
{
"role": "sender",
"host": "127.0.0.1",
"zmq_port": port,
"agent_name": "native-smoke-producer",
}
)
try:
payload = OmniPayloadStruct(
hidden_states=HiddenStatesStruct(output=torch.arange(8, dtype=torch.float32, device="cuda:0")),
meta=MetaStruct(token_role_ids=torch.tensor([1, 2, 3], dtype=torch.int64, device="cpu")),
kv_metadata={"empty": torch.empty((2, 0, 3), device="cuda:0")},
request_id="native-smoke",
)
success, size, metadata = connector.put("0", "1", "native-smoke", payload)
result.put(("put", success, size, metadata["kind"] if metadata else None))
wire.put(metadata)
ready.set()
if not claimed.wait(timeout=30):
raise TimeoutError("consumer did not claim source")
pending = connector._pending["native-smoke"]
pending.deadline = 0
connector._cleanup_expired_pending()
connector.cleanup("native-smoke")
assert connector._pending["native-smoke"] is pending
assert pending.claims and connector._registered_descs
result.put(("source_retained", len(pending.claims)))
proceed.set()
if not consumed.wait(timeout=30):
raise TimeoutError("consumer did not complete the native NIXL transfer")
deadline = time.monotonic() + 10
while connector._pending and time.monotonic() < deadline:
time.sleep(0.01)
result.put(("cleanup", len(connector._pending), len(connector._registered_descs)))
except Exception as error:
result.put(("producer_error", repr(error)))
raise
finally:
connector.close()
def _consumer(
port: int, ready: Any, consumed: Any, result: Any, claimed: Any, proceed: Any, wire: Any, direct: bool
) -> None:
from vllm_omni.distributed.omni_connectors.kv_transfer_manager import (
OmniKVCacheConfig,
OmniKVTransferManager,
)
if not ready.wait(timeout=30):
raise TimeoutError("producer did not publish native NIXL metadata")
current_omni_platform.set_device(1)
manager = OmniKVTransferManager(
OmniKVCacheConfig(
connector_config={
"type": "NixlConnector",
"role": "receiver",
"host": "127.0.0.1",
"zmq_port": port, # Shared deployment edge: already bound by producer.
"backends": ["UCX"],
"receive_device": "cuda",
"agent_name": "native-smoke-consumer",
},
from_stage="0",
to_stage="1",
stage_id=1,
need_recv_cache=direct, # Exercise both payload-only and KV receiver setup.
)
)
manager.update_sender_info({"host": "127.0.0.1", "zmq_port": port})
connector = manager.connector
assert connector is not None, "Receiver must not bind the producer's occupied port"
assert connector._zmq_port is None and not connector._serving_handshake
assert connector._backends == ["UCX"]
try:
original_wait = connector._wait_for_transfer
states = []
def wait(handle, key):
states.append(connector._agent.check_xfer_state(handle))
claimed.set()
if not proceed.wait(timeout=30):
raise TimeoutError("producer did not verify active ownership")
original_wait(handle, key)
connector._wait_for_transfer = wait
metadata = wire.get(timeout=5)
received = None
deadline = time.monotonic() + 30
while received is None and time.monotonic() < deadline:
received = connector.get(
"0",
"1",
"native-smoke",
metadata if direct else {"source_host": "127.0.0.1", "source_port": port},
)
if received is None:
raise TimeoutError("native NIXL transfer did not complete")
payload, size = received
hidden = payload["hidden_states"]["output"]
token_roles = payload["meta"]["token_role_ids"]
empty = payload["kv_metadata"]["empty"]
assert empty.shape == (2, 0, 3) and str(empty.device) == "cuda:1"
result.put(("native_states", states))
result.put(
(
"get",
size,
hidden.cpu().tolist(),
str(hidden.dtype),
list(hidden.shape),
str(hidden.device),
token_roles.cpu().tolist(),
str(token_roles.dtype),
list(token_roles.shape),
str(token_roles.device),
)
)
consumed.set()
except Exception as error:
result.put(("consumer_error", repr(error)))
raise
finally:
connector.close()
@pytest.mark.skipif(not _native_nixl_available(), reason="requires NIXL and at least two CUDA devices")
@pytest.mark.parametrize("direct", [False, True])
def test_native_two_process_structured_mixed_device_transfer(direct):
context = mp.get_context("spawn")
ready = context.Event()
consumed = context.Event()
claimed = context.Event()
proceed = context.Event()
wire = context.Queue()
result = context.Queue()
port = _free_port()
producer = context.Process(target=_producer, args=(port, ready, consumed, result, claimed, proceed, wire))
consumer = context.Process(target=_consumer, args=(port, ready, consumed, result, claimed, proceed, wire, direct))
producer.start()
consumer.start()
producer.join(timeout=60)
consumer.join(timeout=60)
if producer.is_alive():
producer.terminate()
if consumer.is_alive():
consumer.terminate()
records = [result.get(timeout=5) for _ in range(5)]
print("Native ownership records:", records)
assert producer.exitcode == 0, records
assert consumer.exitcode == 0, records
assert records[0][0] == "put"
assert records[0][1] is True
assert records[0][2] >= 56
assert records[0][3] == "structured"
get_record = next(record for record in records if record[0] == "get")
assert get_record[1] == records[0][2]
assert get_record[2:6] == (
list(range(8)),
"torch.float32",
[8],
"cuda:1",
)
assert get_record[6:] == ([1, 2, 3], "torch.int64", [3], "cuda:1")
assert next(record for record in records if record[0] == "cleanup") == ("cleanup", 0, 0)
assert next(record for record in records if record[0] == "source_retained") == ("source_retained", 1)
@@ -1,13 +1,22 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import asyncio
from collections.abc import Callable
from types import SimpleNamespace
from typing import Any
import pytest
from vllm import SamplingParams
from vllm.v1.engine.core_client import AsyncMPClient, DPLBAsyncMPClient
from vllm_omni.engine.cfg_companion_tracker import CfgCompanionTracker
from vllm_omni.engine.messages import OutputMessage
from vllm_omni.engine.orchestrator import Orchestrator, OrchestratorRequestState
from vllm_omni.engine.stage_engine_core_client import StageEngineCoreClient
from vllm_omni.engine.stage_engine_core_client import (
DPLBStageEngineCoreClient,
StageEngineCoreClient,
)
from vllm_omni.engine.stage_pool import StagePool
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
@@ -28,7 +37,7 @@ class _DummySenderStage:
class _DummyDiffusionStage:
stage_type = "diffusion"
final_output = True
custom_process_input_func = None
custom_process_input_func: Callable[..., Any] | None = None
def __init__(self, engine_input_source=None):
self.engine_input_source = engine_input_source or [0]
@@ -54,6 +63,97 @@ def _build_sender_pool(stage_id: int, sender_info: dict[str, object]) -> StagePo
)
@pytest.mark.parametrize(
("client_class", "base_client_class"),
[
(StageEngineCoreClient, AsyncMPClient),
(DPLBStageEngineCoreClient, DPLBAsyncMPClient),
],
)
def test_stage_engine_core_client_builds_payload_sender_info_after_base_init(
monkeypatch, client_class, base_client_class
):
vllm_config = SimpleNamespace(
model_config=SimpleNamespace(
omni_kv_config=None,
hf_config=None,
stage_connector_config=None,
)
)
metadata = SimpleNamespace(
stage_id=0,
replica_id=0,
stage_type="llm",
model_stage="main",
is_comprehension=False,
requires_multimodal_data=False,
engine_input_source=[],
final_output=False,
final_output_type=None,
default_sampling_params=None,
prompt_transform_func=None,
prompt_expand_func=None,
custom_process_input_func=None,
)
def fake_base_init(self, config, *_args, **_kwargs):
self.vllm_config = config
self.resources = SimpleNamespace(engine_dead=False)
monkeypatch.setattr(base_client_class, "__init__", fake_base_init)
client = client_class(vllm_config, object, metadata=metadata)
assert client.vllm_config is vllm_config
assert client.get_payload_sender_info() is None
@pytest.mark.parametrize("outgoing", [False, True])
@pytest.mark.parametrize("base_port", [None, 48000])
def test_payload_sender_endpoint_matches_resolver_with_unequal_replicas(outgoing, base_port):
from vllm_omni.distributed.omni_connectors.utils.config import ConnectorSpec
from vllm_omni.distributed.omni_connectors.utils.initialization import resolve_connector_spec
edge = {"host": "10.0.0.2", "from_stage": 1}
if base_port is not None:
edge["zmq_port"] = base_port
extra = (
{
"role": "receiver",
"host": "10.0.0.1",
"zmq_port": 47000,
"from_stage": 0,
"outgoing": edge,
}
if outgoing
else {"role": "sender", **edge}
)
client = object.__new__(StageEngineCoreClient)
client.stage_id = 1
client.replica_id = 3
client.vllm_config = SimpleNamespace(
model_config=SimpleNamespace(stage_connector_config={"name": "NixlConnector", "extra": extra})
)
producer = resolve_connector_spec(
ConnectorSpec(name="NixlConnector", extra=extra),
stage_id=1,
role=extra["role"],
replica_id=client.replica_id,
)
consumer = resolve_connector_spec(
ConnectorSpec(name="NixlConnector", extra=edge), stage_id=2, role="receiver", replica_id=7
)
sender_info = client._build_payload_sender_info()
assert sender_info == {
"host": "10.0.0.2",
"zmq_port": (50051 if base_port is None else base_port) + 3 * 1024 + 1,
}
assert sender_info["zmq_port"] == producer.extra["zmq_port"]
assert sender_info["zmq_port"] != consumer.extra["sender_zmq_port"]
def test_stage_engine_core_client_builds_kv_sender_info_from_tcp_address():
client = object.__new__(StageEngineCoreClient)
client.stage_id = 0
@@ -259,3 +359,58 @@ def test_prewarm_diffusion_attaches_kv_sender_info():
0: {"host": "10.0.0.3", "zmq_port": 50151},
}
assert req_state.stage_submit_ts[1] > 0
def test_prewarm_submits_bound_payload_endpoint_for_concurrent_replicas():
orchestrator = object.__new__(Orchestrator)
endpoints = {"a": {"host": "10.0.0.2", "zmq_port": 52099}, "b": {"host": "10.0.0.3", "zmq_port": 54147}}
source = SimpleNamespace(
get_bound_client=lambda key: SimpleNamespace(get_payload_sender_info=lambda: endpoints[key]),
get_bound_replica_id=lambda key: {"a": 2, "b": 4}[key],
)
submitted = {}
async def submit(key, state, request, **kwargs):
await asyncio.sleep(0)
submitted[key] = request
target = SimpleNamespace(
stage_type="llm",
stage_client=SimpleNamespace(engine_input_source=[0]),
stage_vllm_config=SimpleNamespace(model_config=SimpleNamespace(hf_config=None, max_model_len=64)),
submit_initial=submit,
get_bound_replica_id=lambda key: {"a": 7, "b": 1}[key],
)
orchestrator.stage_pools = [source, target]
orchestrator._stage_receives_async_chunks = lambda stage: True
orchestrator._record_duplex_stage_submission = lambda *args: None
orchestrator._emit_tx_edge = lambda **kwargs: None
async def dispatch(callback, **kwargs):
await callback()
return True
orchestrator._dispatch_or_fail_request = dispatch
async def run():
await asyncio.gather(
*[
orchestrator._prewarm_async_chunk_stages(
key,
SimpleNamespace(prompt_token_ids=[1]),
OrchestratorRequestState(
request_id=key,
prompt={},
sampling_params_list=[SamplingParams(), SamplingParams()],
final_stage_id=1,
),
)
for key in endpoints
]
)
asyncio.run(run())
from vllm_omni.engine import OmniEngineCoreRequest
assert all(isinstance(request, OmniEngineCoreRequest) for request in submitted.values())
assert {key: request.payload_sender_info for key, request in submitted.items()} == endpoints
@@ -104,9 +104,13 @@ class FakeInputProcessor:
class FakePrewarmPool:
"""Single-replica pool double that binds requests only on submission."""
stage_type = "llm"
def __init__(self, role: str) -> None:
self.stage_client = SimpleNamespace()
self._bound_request_ids: set[str] = set()
self.stage_vllm_config = SimpleNamespace(
model_config=SimpleNamespace(
max_model_len=64,
@@ -115,12 +119,16 @@ class FakePrewarmPool:
)
self.submitted: list[Any] = []
async def submit_initial(self, _request_id, _req_state, request, prompt_text=None):
async def submit_initial(self, request_id, _req_state, request, prompt_text=None):
self.submitted.append(request)
self._bound_request_ids.add(request_id)
return 0
def get_bound_replica_id(self, _request_id):
return 0
def get_bound_replica_id(self, request_id):
return 0 if request_id in self._bound_request_ids else None
def get_bound_client(self, request_id):
return self.stage_client if self.get_bound_replica_id(request_id) is not None else None
def _duplex_stage_port_submission():
@@ -261,11 +269,14 @@ async def test_forward_text_prompt_uses_target_stage_input_processor() -> None:
@pytest.mark.asyncio
async def test_async_prewarm_skips_outgoing_only_stage() -> None:
@pytest.mark.parametrize("payload_sender_info", [None, {"host": "10.0.0.2", "zmq_port": 52099}])
async def test_async_prewarm_skips_outgoing_only_stage(payload_sender_info) -> None:
orchestrator = object.__new__(Orchestrator)
stage0 = FakePrewarmPool("sender")
stage1 = FakePrewarmPool("sender")
stage2 = FakePrewarmPool("receiver")
if payload_sender_info is not None:
stage1.stage_client.get_payload_sender_info = MagicMock(return_value=payload_sender_info)
orchestrator.stage_pools = [stage0, stage1, stage2]
orchestrator._emit_tx_edge = lambda **_kwargs: None
orchestrator._record_duplex_stage_submission = MagicMock()
@@ -285,7 +296,14 @@ async def test_async_prewarm_skips_outgoing_only_stage() -> None:
assert prewarmed is True
assert stage1.submitted == []
assert stage1.get_bound_client("req-prewarm") is None
assert len(stage2.submitted) == 1
assert stage2.get_bound_client("req-prewarm") is stage2.stage_client
assert stage2.submitted[0].payload_sender_info == payload_sender_info
if payload_sender_info is not None:
stage1.stage_client.get_payload_sender_info.assert_called_once_with()
assert stage2.submitted[0].external_req_id == "req-prewarm"
assert stage2.submitted[0].resumable is True
assert 1 not in req_state.stage_submit_ts
assert 2 in req_state.stage_submit_ts
orchestrator._record_duplex_stage_submission.assert_called_once_with(
+192
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import time
import unittest
from copy import deepcopy
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock, patch
@@ -20,6 +21,10 @@ import torch
from vllm_omni.distributed.omni_connectors.kv_transfer_manager import (
OmniKVTransferManager,
)
from vllm_omni.distributed.omni_connectors.utils.config import ConnectorSpec
from vllm_omni.distributed.omni_connectors.utils.initialization import (
resolve_connector_spec,
)
from vllm_omni.outputs import OmniConnectorOutput
from vllm_omni.worker.omni_connector_model_runner_mixin import (
OmniConnectorModelRunnerMixin,
@@ -564,6 +569,55 @@ class TestChunkStreamCompletedGuard(unittest.TestCase):
host.shutdown_omni_connectors()
def test_poll_uses_request_scoped_payload_sender_endpoint(self):
host = self._make_host(stage_id=1)
host._omni_connector.get = MagicMock(return_value=None)
req = _make_request("req-1", "ext-req-1")
req.payload_sender_info = {"host": "10.0.0.1", "zmq_port": 50051}
host.register_chunk_recv(req)
host._poll_single_request("req-1")
host._omni_connector.get.assert_called_once_with(
"0",
"1",
"ext-req-1_0_0",
{"source_host": "10.0.0.1", "source_port": 50051},
)
host.shutdown_omni_connectors()
def test_concurrent_requests_keep_distinct_payload_sender_endpoints(self):
host = self._make_host(stage_id=1)
host._omni_connector.get = MagicMock(return_value=None)
first = _make_request("req-1", "ext-req-1")
first.payload_sender_info = {"host": "10.0.0.1", "zmq_port": 50051}
second = _make_request("req-2", "ext-req-2")
second.payload_sender_info = {"host": "10.0.0.2", "zmq_port": 51051}
host.register_chunk_recv(first)
host.register_chunk_recv(second)
host._poll_single_request("req-2")
host._poll_single_request("req-1")
self.assertEqual(
host._omni_connector.get.call_args_list,
[
unittest.mock.call(
"0",
"1",
"ext-req-2_0_0",
{"source_host": "10.0.0.2", "source_port": 51051},
),
unittest.mock.call(
"0",
"1",
"ext-req-1_0_0",
{"source_host": "10.0.0.1", "source_port": 50051},
),
],
)
host.shutdown_omni_connectors()
def test_finish_sentinel_populates_completed_set(self):
"""Receiving is_finished=True adds to _chunk_stream_completed."""
host = self._make_host(stage_id=1)
@@ -1381,6 +1435,144 @@ class TestConnectorConfigValidation(unittest.TestCase):
host.init_omni_connectors(model_config=model_config)
class TestRankAwareHandshakePort(unittest.TestCase):
"""A sender must bind the port the orchestrator advertises for its rank."""
@staticmethod
def _resolve(name, extra, local_rank=0):
spec = resolve_connector_spec(
ConnectorSpec(name=name, extra=extra),
stage_id=int(extra.get("stage_id", 0)),
role=extra.get("role"),
local_rank=local_rank,
)
return spec.extra
def test_sender_port_matches_the_orchestrator_formula(self):
for local_rank in (0, 1, 3):
with self.subTest(local_rank=local_rank):
extra = {"zmq_port": 50071, "stage_id": 2, "role": "sender"}
resolved = self._resolve("NixlConnector", extra, local_rank=local_rank)
self.assertEqual(resolved["zmq_port"], 50071 + 2 + local_rank * 16)
self.assertEqual(extra["zmq_port"], 50071, "the source config must not be mutated")
def test_receiver_gets_sender_endpoint_without_a_bind_port(self):
receiver = self._resolve(
"NixlConnector",
{"zmq_port": 50071, "stage_id": 3, "from_stage": 2, "role": "receiver"},
local_rank=1,
)
self.assertNotIn("zmq_port", receiver)
self.assertEqual(receiver["sender_zmq_port"], 50071 + 2 + 16)
def test_handshake_less_connector_keeps_its_config(self):
shm = self._resolve("SharedMemoryConnector", {"zmq_port": 50071, "role": "sender"}, local_rank=1)
self.assertEqual(shm["zmq_port"], 50071)
def test_unresolvable_port_is_rejected(self):
extra = {"zmq_port": "${MISSING_PORT_VAR}", "stage_id": 0, "role": "sender"}
with self.assertRaises(ValueError):
self._resolve("NixlConnector", extra)
@pytest.mark.parametrize(
("extra", "expected"),
[
(
{"role": "sender", "from_stage": 2, "zmq_port": 50071},
{"zmq_port": 52153},
),
(
{"role": "receiver", "from_stage": 1, "zmq_port": 50071, "host": "producer"},
{"sender_zmq_port": 52152, "sender_host": "producer"},
),
(
{
"role": "receiver",
"from_stage": 1,
"zmq_port": 50071,
"sender_zmq_port": 49000,
"sender_host": "bound-producer",
"outgoing": {"from_stage": 2, "zmq_port": 51071, "host": "local-worker"},
},
{"sender_zmq_port": 49000, "sender_host": "bound-producer", "zmq_port": 53153, "host": "local-worker"},
),
],
)
def test_split_runtime_factory_resolves_nixl_endpoints(extra, expected):
"""The runner must pass the resolved spec, not the raw config, to the factory."""
from vllm_omni.distributed.omni_connectors.model_runner import omni_connector_runtime as runtime
original = deepcopy(extra)
config = SimpleNamespace(stage_id=2, stage_connector_config={"name": "NixlConnector", "extra": extra})
with (
patch.object(runtime, "get_local_tp_rank", return_value=2, create=True),
patch.object(runtime, "get_omni_replica_id", return_value=2, create=True),
patch.object(runtime.OmniConnectorFactory, "create_connector") as factory,
):
assert MixinHost._create_connector(config) is factory.return_value
spec = factory.call_args.args[0]
assert spec.name == "NixlConnector"
assert spec.extra["stage_id"] == 2
for key, value in expected.items():
assert spec.extra[key] == value
if extra["role"] == "receiver" and "outgoing" not in extra:
assert "zmq_port" not in spec.extra
assert extra == original
@pytest.mark.parametrize("async_chunk", [False, True])
def test_split_payload_endpoint_retry_cleanup_and_reuse(async_chunk):
host = MixinHost()
host.init_omni_connectors(_make_model_config(async_chunk=async_chunk))
host._stage_id = 1
host._omni_connector = MagicMock()
host._omni_connector.get.return_value = None
first = _make_request("req-1", "ext-req-1")
first.payload_sender_info = {"host": "producer-a", "zmq_port": "50051"}
second = _make_request("req-2", "ext-req-2")
second.payload_sender_info = {"host": "producer-b", "zmq_port": 51051}
try:
host.register_chunk_recv(first)
host.register_chunk_recv(second)
for req_id in ("req-1", "req-2", "req-1"):
assert not host._poll_single_request(req_id)
assert host._omni_connector.get.call_args_list == [
unittest.mock.call("0", "1", "ext-req-1_0_0", {"source_host": "producer-a", "source_port": 50051}),
unittest.mock.call("0", "1", "ext-req-2_0_0", {"source_host": "producer-b", "source_port": 51051}),
unittest.mock.call("0", "1", "ext-req-1_0_0", {"source_host": "producer-a", "source_port": 50051}),
]
host.cleanup_finished_request("req-1")
assert "req-1" not in host._pending_load_reqs
assert host._pending_load_reqs["req-2"] is second
host.register_chunk_recv(_make_request("req-1", "reused"))
assert not host._poll_single_request("req-1")
host._omni_connector.get.assert_called_with("0", "1", "reused_0_0")
finally:
host.shutdown_omni_connectors()
@pytest.mark.parametrize("method", ["_recv_full_payload_result", "_recv_async_chunk_result"])
@pytest.mark.parametrize("rank", [0, 1])
def test_split_payload_sender_metadata_respects_tp_leader(method, rank):
host = MixinHost()
connector = MagicMock()
metadata = {"source_host": "producer", "source_port": 50051}
with (
patch.object(host, "_get_local_tp_group", return_value=_FakeTPGroup(world_size=2, rank_in_group=rank)),
patch.object(host, "is_data_transfer_rank", return_value=rank == 0),
):
result = getattr(host, method)(connector, "0", "1", "req_0_0", metadata)
if rank == 0:
connector.get.assert_called_once_with("0", "1", "req_0_0", metadata)
assert result is connector.get.return_value
else:
connector.get.assert_not_called()
assert result is None
class _FailingConnector:
"""Connector whose put() fails a configurable number of times."""
@@ -67,6 +67,8 @@ _PUBLIC_OMNI = (
"VLLM_OMNI_EVENT_DRIVEN_ORCH",
"VLLM_OMNI_FUSED_QK_NORM_ROPE_MIN_TOKENS",
"VLLM_OMNI_INPUT_WAIT_TIMEOUT_S",
"VLLM_OMNI_NIXL_LEASE_S",
"VLLM_OMNI_NIXL_XFER_TIMEOUT_S",
"VLLM_OMNI_ORCH_MONITOR_PATH",
"VLLM_OMNI_SERVER_STORAGE__FILE_CONCURRENCY",
"VLLM_OMNI_SERVER_STORAGE__FILE_TTL",
@@ -121,6 +121,7 @@ class OmniSchedulingCoordinator:
OmniChunkRecvHandle(
request_id=request.request_id,
external_req_id=getattr(request, "external_req_id", None),
payload_sender_info=getattr(request, "payload_sender_info", None),
)
)
elif request.status == RequestStatus.WAITING_FOR_INPUT:
@@ -134,6 +135,7 @@ class OmniSchedulingCoordinator:
OmniChunkRecvHandle(
request_id=request.request_id,
external_req_id=getattr(request, "external_req_id", None),
payload_sender_info=getattr(request, "payload_sender_info", None),
)
)
if to_remove:
+1
View File
@@ -105,6 +105,7 @@ class OmniChunkRecvHandle:
request_id: str
external_req_id: str | None = None
payload_sender_info: dict[str, object] | None = None
@dataclass
+2
View File
@@ -7,6 +7,7 @@ from .omni_connectors import (
MooncakeStoreConnector,
MooncakeTransferEngineConnector,
MoriTransferEngineConnector,
NixlConnector,
OmniConnectorBase,
OmniConnectorFactory,
OmniTransferConfig,
@@ -27,6 +28,7 @@ __all__ = [
"MooncakeStoreConnector",
"MooncakeTransferEngineConnector",
"MoriTransferEngineConnector",
"NixlConnector",
"SharedMemoryConnector",
"YuanrongConnector",
"YuanrongTransferEngineConnector",
@@ -22,6 +22,11 @@ try:
from .connectors.mori_transfer_engine_connector import MoriTransferEngineConnector
except ImportError:
MoriTransferEngineConnector = None # RDMA deps (msgspec/zmq/mori) not installed
try:
from .connectors.nixl_connector import NixlConnector
except ImportError:
NixlConnector = None # NIXL deps not installed
from .factory import OmniConnectorFactory
from .utils.config import ConnectorSpec, OmniTransferConfig
from .utils.initialization import (
@@ -50,6 +55,7 @@ __all__ = [
"MooncakeStoreConnector",
"MooncakeTransferEngineConnector",
"MoriTransferEngineConnector",
"NixlConnector",
"SharedMemoryConnector",
"YuanrongConnector",
"YuanrongTransferEngineConnector",
File diff suppressed because it is too large. Load diff
@@ -125,6 +125,17 @@ def _create_mori_transfer_engine_connector(config: dict[str, Any]) -> OmniConnec
return MoriTransferEngineConnector(config)
def _create_nixl_connector(config: dict[str, Any]) -> OmniConnectorBase:
try:
from .connectors.nixl_connector import NixlConnector
except ImportError:
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from omni_connectors.connectors.nixl_connector import NixlConnector
return NixlConnector(config)
# Register connectors
OmniConnectorFactory.register_connector("MooncakeStoreConnector", _create_mooncake_store_connector)
OmniConnectorFactory.register_connector("MooncakeTransferEngineConnector", _create_mooncake_transfer_engine_connector)
@@ -132,5 +143,6 @@ OmniConnectorFactory.register_connector("SharedMemoryConnector", _create_shm_con
OmniConnectorFactory.register_connector("YuanrongConnector", _create_yuanrong_connector)
OmniConnectorFactory.register_connector("YuanrongTransferEngineConnector", _create_yuanrong_transfer_engine_connector)
OmniConnectorFactory.register_connector("MoriTransferEngineConnector", _create_mori_transfer_engine_connector)
OmniConnectorFactory.register_connector("NixlConnector", _create_nixl_connector)
# Backward-compatible aliases – will be removed in the future
OmniConnectorFactory.register_connector("MooncakeConnector", _create_mooncake_store_connector)
@@ -564,6 +564,13 @@ class OmniKVTransferManager:
# the wrong process. Explicit sender_* YAML values
# are still preserved for standalone connector use.
if c_type == "NixlConnector" and c_extra.get("role") == "receiver":
# This manager receives on the shared incoming edge;
# its YAML zmq_port belongs to the producer. NIXL treats
# an explicit zmq_port as a local listener, even for a
# receiver. Keep only the request-scoped sender_* endpoint.
c_extra.pop("zmq_port", None)
logger.info(
"Initializing OmniConnector type=%s role=%s",
c_type,
@@ -922,7 +929,9 @@ class OmniKVTransferManager:
The base host/port are also stored so that the receive path can
construct per-rank metadata for heterogeneous TP scenarios.
"""
if not self.config.need_recv_cache:
# A stage can sit on the receiving end of a payload-only edge, where the
# sender endpoint still has to be applied even though no KV is expected.
if not self.config.need_recv_cache and (self.config.connector_config or {}).get("role") != "receiver":
return
actual_info = self._resolve_sender_info(sender_info, sender_stage_id=sender_stage_id)
@@ -182,14 +182,19 @@ class _OmniConnectorPayloadTransportMixin(_OmniConnectorRuntimeMixin):
from_stage: str,
to_stage: str,
connector_get_key: str,
metadata: dict[str, Any] | None = None,
) -> Any:
"""Receive one ordinary non-KV stage payload on the local leader rank only."""
tp_group = self._get_local_tp_group()
if tp_group is None or getattr(tp_group, "world_size", 1) <= 1:
return connector.get(from_stage, to_stage, connector_get_key)
if metadata is None:
return connector.get(from_stage, to_stage, connector_get_key)
return connector.get(from_stage, to_stage, connector_get_key, metadata)
if not self.is_data_transfer_rank():
return None
return connector.get(from_stage, to_stage, connector_get_key)
if metadata is None:
return connector.get(from_stage, to_stage, connector_get_key)
return connector.get(from_stage, to_stage, connector_get_key, metadata)
def _recv_full_payload_result(
self,
@@ -197,6 +202,7 @@ class _OmniConnectorPayloadTransportMixin(_OmniConnectorRuntimeMixin):
from_stage: str,
to_stage: str,
connector_get_key: str,
metadata: dict[str, Any] | None = None,
) -> Any:
"""Receive one full-payload transfer on the local leader rank only."""
return self._recv_ordinary_stage_result(
@@ -204,6 +210,7 @@ class _OmniConnectorPayloadTransportMixin(_OmniConnectorRuntimeMixin):
from_stage,
to_stage,
connector_get_key,
metadata,
)
def _recv_async_chunk_result(
@@ -212,6 +219,7 @@ class _OmniConnectorPayloadTransportMixin(_OmniConnectorRuntimeMixin):
from_stage: str,
to_stage: str,
connector_get_key: str,
metadata: dict[str, Any] | None = None,
) -> Any:
"""Receive one ordinary async chunk on the local leader rank only."""
return self._recv_ordinary_stage_result(
@@ -219,6 +227,7 @@ class _OmniConnectorPayloadTransportMixin(_OmniConnectorRuntimeMixin):
from_stage,
to_stage,
connector_get_key,
metadata,
)
@staticmethod
@@ -1027,6 +1036,14 @@ class _OmniConnectorPayloadTransportMixin(_OmniConnectorRuntimeMixin):
chunk_id = self._get_req_chunk[req_id]
external_req_id = self._request_ids_mapping.get(req_id, req_id)
connector_get_key = f"{external_req_id}_{target_stage_id}_{chunk_id}"
request = self._pending_load_reqs.get(req_id)
sender_info = getattr(request, "payload_sender_info", None)
metadata = None
if isinstance(sender_info, dict):
host = sender_info.get("host")
port = sender_info.get("zmq_port")
if host and port:
metadata = {"source_host": str(host), "source_port": int(port)}
if self._async_chunk:
result = self._recv_async_chunk_result(
@@ -1034,6 +1051,7 @@ class _OmniConnectorPayloadTransportMixin(_OmniConnectorRuntimeMixin):
str(target_stage_id),
str(self._stage_id),
connector_get_key,
metadata,
)
else:
result = self._recv_full_payload_result(
@@ -1041,6 +1059,7 @@ class _OmniConnectorPayloadTransportMixin(_OmniConnectorRuntimeMixin):
str(target_stage_id),
str(self._stage_id),
connector_get_key,
metadata,
)
if result is None:
@@ -19,6 +19,8 @@ from vllm_omni.distributed.omni_connectors.utils.config import (
ConnectorSpec,
get_stage_connector_role,
)
from vllm_omni.distributed.omni_connectors.utils.initialization import resolve_connector_spec
from vllm_omni.distributed.omni_connectors.utils.kv_utils import get_local_tp_rank, get_omni_replica_id
logger = init_logger("vllm_omni.worker.omni_connector_model_runner_mixin")
@@ -186,7 +188,11 @@ class _OmniConnectorRuntimeMixin:
getattr(topology, "target_tp_size", None),
getattr(topology, "local_rank", None),
)
if all(isinstance(value, int) for value in effective_mapping):
if (
isinstance(effective_mapping[0], int)
and isinstance(effective_mapping[1], int)
and isinstance(effective_mapping[2], int)
):
rank_cfg = {
"from_tp": effective_mapping[0],
"to_tp": effective_mapping[1],
@@ -521,7 +527,15 @@ class _OmniConnectorRuntimeMixin:
elif not isinstance(extra, dict):
raise RuntimeError(f"Invalid extra config for connector {name}: expected dict, got {type(extra).__name__}")
spec = ConnectorSpec(name=name, extra=extra)
stage_id = int(getattr(model_config, "stage_id", extra.get("stage_id", 0)))
role = extra.get("role")
spec = resolve_connector_spec(
ConnectorSpec(name=name, extra=extra),
stage_id=stage_id,
role=str(role) if role is not None else None,
local_rank=get_local_tp_rank(),
replica_id=get_omni_replica_id(),
)
try:
return OmniConnectorFactory.create_connector(spec)
except Exception as exc:
@@ -20,6 +20,8 @@ from vllm_omni.data_entry_keys import MetaStruct, OmniPayloadStruct, unflatten_p
from ..adapter import construct_next_stage_streaming_input_prompt
from ..factory import OmniConnectorFactory
from ..utils.config import ConnectorSpec, stage_receives_chunks
from ..utils.initialization import resolve_connector_spec
from ..utils.kv_utils import get_local_tp_rank, get_omni_replica_id
from ..utils.logging import get_connector_logger
from .base import OmniTransferAdapterBase
@@ -47,10 +49,20 @@ class _SenderGeneration:
class _LoadEntry:
"""Identify one receiver registration across queue and I/O boundaries."""
__slots__ = ("request",)
__slots__ = ("request", "source_metadata")
def __init__(self, request: Request) -> None:
self.request = request
sender_info = getattr(request, "payload_sender_info", None)
self.source_metadata = None
if isinstance(sender_info, dict):
host = sender_info.get("host")
port = sender_info.get("zmq_port")
if host and port:
self.source_metadata = {
"source_host": str(host),
"source_port": int(port),
}
@property
def request_id(self) -> str:
@@ -280,11 +292,20 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
"extra": getattr(connector_config, "extra", {}),
}
connector_specs = ConnectorSpec(
connector_spec = ConnectorSpec(
name=connector_config.get("name", "SharedMemoryConnector"),
extra=connector_config.get("extra", {}),
)
return OmniConnectorFactory.create_connector(connector_specs)
stage_id = int(getattr(model_config, "stage_id", connector_spec.extra.get("stage_id", 0)))
role = connector_spec.extra.get("role")
resolved_spec = resolve_connector_spec(
connector_spec,
stage_id=stage_id,
role=str(role) if role is not None else None,
local_rank=get_local_tp_rank(),
replica_id=get_omni_replica_id(),
)
return OmniConnectorFactory.create_connector(resolved_spec)
def load_async(self, request: Request):
"""Register a request for asynchronous chunk retrieval.
@@ -484,6 +505,7 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
str(target_stage_id),
str(stage_id),
connector_get_key,
entry.source_metadata,
)
except Exception as e:
logger.error(f"SharedMemoryConnector get failed for req {connector_get_key}: {e}")
@@ -13,6 +13,7 @@ TRANSFER_ENGINE_CONNECTOR_NAMES = frozenset(
"MooncakeTransferEngineConnector",
"MoriTransferEngineConnector",
"YuanrongTransferEngineConnector",
"NixlConnector",
}
)
@@ -4,9 +4,10 @@
"""Utilities for OmniConnector configuration and validation."""
import json
import os
import warnings
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal
from ..factory import OmniConnectorFactory
from .config import TRANSFER_ENGINE_CONNECTOR_NAMES, ConnectorSpec, OmniTransferConfig
@@ -38,6 +39,91 @@ KV_RANK_PORT_STRIDE = 16
# comfortably sized block per replica for TP-rank and stage offsets.
KV_REPLICA_PORT_STRIDE = 1024
REQUEST_FORWARDING_PORT_OFFSET = 0
ORCHESTRATOR_PORT_OFFSET = 200
ConnectorPurpose = Literal["request_forwarding", "kv_transfer", "orchestrator"]
def compute_connector_zmq_port(
base_port: int,
*,
purpose: ConnectorPurpose,
from_stage: int,
local_rank: int = 0,
replica_id: int = 0,
) -> int:
"""Return a collision-free connector endpoint for an edge and worker."""
purpose_offsets = {
"request_forwarding": REQUEST_FORWARDING_PORT_OFFSET,
"kv_transfer": KV_TRANSFER_PORT_OFFSET,
"orchestrator": ORCHESTRATOR_PORT_OFFSET,
}
if purpose not in purpose_offsets:
raise ValueError(f"Unsupported connector purpose: {purpose}")
if not 0 <= from_stage < KV_RANK_PORT_STRIDE:
raise ValueError(f"from_stage must be in [0, {KV_RANK_PORT_STRIDE}), got {from_stage}")
if local_rank < 0:
raise ValueError(f"local_rank must be non-negative, got {local_rank}")
if replica_id < 0:
raise ValueError(f"replica_id must be non-negative, got {replica_id}")
port = (
base_port
+ purpose_offsets[purpose]
+ replica_id * KV_REPLICA_PORT_STRIDE
+ local_rank * KV_RANK_PORT_STRIDE
+ from_stage
)
if not 1 <= port <= 65535:
raise ValueError(f"Resolved connector port must be in [1, 65535], got {port}")
return port
def resolve_connector_spec(
spec: ConnectorSpec,
*,
stage_id: int,
role: str | None,
purpose: ConnectorPurpose = "request_forwarding",
local_rank: int = 0,
replica_id: int = 0,
) -> ConnectorSpec:
"""Copy and resolve a connector spec for one stage worker."""
extra = dict(spec.extra or {})
extra["stage_id"] = stage_id
if role is not None:
extra["role"] = role
if spec.name not in TRANSFER_ENGINE_CONNECTOR_NAMES:
return ConnectorSpec(name=spec.name, extra=extra)
resolved_port = None
if role != "receiver" or "sender_zmq_port" not in extra:
resolved_port = compute_connector_zmq_port(
expand_env_int(extra.get("zmq_port", 50051), "zmq_port"),
purpose=purpose,
from_stage=int(extra.get("from_stage", stage_id)),
local_rank=local_rank,
replica_id=replica_id,
)
if role == "receiver":
extra.setdefault("sender_host", extra.get("host", "127.0.0.1"))
extra.setdefault("sender_zmq_port", resolved_port)
extra.pop("zmq_port", None)
outgoing = extra.get("outgoing")
if spec.name == "NixlConnector" and isinstance(outgoing, dict):
extra["zmq_port"] = compute_connector_zmq_port(
expand_env_int(outgoing.get("zmq_port", 50051), "zmq_port"),
purpose=purpose,
from_stage=int(outgoing.get("from_stage", stage_id)),
local_rank=local_rank,
replica_id=replica_id,
)
extra["host"] = outgoing.get("host", "auto")
else:
extra["zmq_port"] = resolved_port
return ConnectorSpec(name=spec.name, extra=extra)
def initialize_connectors_from_config(
config_path: str | Path | None = None,
@@ -82,52 +168,40 @@ def create_connectors_from_config(
Returns:
A dictionary of connectors.
"""
purpose_port_offsets = {
"request_forwarding": 0,
"kv_transfer": KV_TRANSFER_PORT_OFFSET,
}
port_offset = purpose_port_offsets.get(purpose, 0)
orchestrator_port_offset = 200
connectors = {}
for edge_key, connector_spec in connectors_config.items():
from_stage, to_stage = edge_key
try:
if connector_spec.name in TRANSFER_ENGINE_CONNECTOR_NAMES:
extra = dict(connector_spec.extra) if connector_spec.extra else {}
base_port = expand_env_int(extra.get("zmq_port", 50051), "zmq_port")
try:
stage_offset = int(from_stage)
except (TypeError, ValueError):
stage_offset = 0
local_rank = 0 if str(caller_stage_id) == "orchestrator" else get_connector_local_rank()
if str(caller_stage_id) == "orchestrator":
rank0_port = base_port + orchestrator_port_offset + stage_offset
else:
rank0_port = base_port + port_offset + stage_offset
adjusted_port = rank0_port + local_rank * KV_RANK_PORT_STRIDE
extra["zmq_port"] = adjusted_port
from_stage_id = int(from_stage)
except (TypeError, ValueError) as exc:
raise ValueError(f"Transfer-engine stage id must be an integer, got {from_stage!r}") from exc
is_orchestrator = str(caller_stage_id) == "orchestrator"
local_rank = 0 if is_orchestrator else get_connector_local_rank()
resolved_purpose: ConnectorPurpose = "orchestrator" if is_orchestrator else purpose # type: ignore[assignment]
if is_sender is not None:
extra["role"] = "sender" if is_sender else "receiver"
if not is_sender:
extra.setdefault("sender_host", extra.get("host", "127.0.0.1"))
extra.setdefault("sender_zmq_port", adjusted_port)
elif caller_stage_id is not None:
caller_str = str(caller_stage_id)
if caller_str == from_stage:
extra["role"] = "sender"
elif caller_str == to_stage:
extra["role"] = "receiver"
extra.setdefault("sender_host", extra.get("host", "127.0.0.1"))
extra.setdefault("sender_zmq_port", adjusted_port)
else:
extra["role"] = "sender"
role = "sender" if is_sender else "receiver"
elif caller_stage_id is not None and str(caller_stage_id) == to_stage:
role = "receiver"
elif caller_stage_id is not None and str(caller_stage_id) == from_stage:
role = "sender"
else:
extra["role"] = extra.get("role", "auto")
connector = OmniConnectorFactory.create_connector(ConnectorSpec(name=connector_spec.name, extra=extra))
role = str(extra.get("role", "auto"))
extra["from_stage"] = from_stage_id
extra["to_stage"] = int(to_stage)
replica_id = max(int(os.environ.get("VLLM_OMNI_REPLICA_ID", "0")), 0)
resolved_spec = resolve_connector_spec(
ConnectorSpec(name=connector_spec.name, extra=extra),
stage_id=int(caller_stage_id) if str(caller_stage_id).isdigit() else from_stage_id,
role=role,
purpose=resolved_purpose,
local_rank=local_rank,
replica_id=replica_id,
)
connector = OmniConnectorFactory.create_connector(resolved_spec)
else:
connector = OmniConnectorFactory.create_connector(connector_spec)
connectors[edge_key] = connector
@@ -174,12 +248,16 @@ def get_connectors_config_for_stage(transfer_config: OmniTransferConfig | None,
# Incoming edge → this stage is the receiver
extra = dict(spec.extra) if spec.extra else {}
extra.setdefault("role", "receiver")
extra["from_stage"] = int(from_stage)
extra["to_stage"] = int(to_stage)
stage_connectors_config[f"from_stage_{from_stage}"] = {"spec": {"name": spec.name, "extra": extra}}
elif from_stage == target_stage and target_stage == "0":
# Outgoing edge for stage 0 — included for async_chunk spec
# extraction (omni_stage.py), NOT for connector instantiation.
extra = dict(spec.extra) if spec.extra else {}
extra.setdefault("role", "sender")
extra["from_stage"] = int(from_stage)
extra["to_stage"] = int(to_stage)
stage_connectors_config[f"to_stage_{to_stage}"] = {"spec": {"name": spec.name, "extra": extra}}
return stage_connectors_config
@@ -16,7 +16,7 @@ from vllm.distributed.parallel_state import (
)
from vllm.logger import init_logger
from .initialization import KV_RANK_PORT_STRIDE, KV_REPLICA_PORT_STRIDE, KV_TRANSFER_PORT_OFFSET
from .initialization import compute_connector_zmq_port
logger = init_logger(__name__)
@@ -123,12 +123,12 @@ def kv_zmq_port(
"""
replica = get_omni_replica_id() if replica_id is None else max(int(replica_id), 0)
return (
base_port
+ KV_TRANSFER_PORT_OFFSET
+ replica * KV_REPLICA_PORT_STRIDE
+ local_rank * KV_RANK_PORT_STRIDE
+ from_stage
return compute_connector_zmq_port(
base_port,
purpose="kv_transfer",
from_stage=from_stage,
local_rank=local_rank,
replica_id=replica,
)
+5
View File
@@ -79,6 +79,7 @@ class OmniEngineCoreRequest(EngineCoreRequest):
# GPUModelRunner.model_intermediate_buffer instead of using the deprecated
# additional_information request transport.
model_intermediate_buffer: dict[str, Any] | None = None
payload_sender_info: dict[str, Any] | None = None
@classmethod
def from_request(
@@ -88,6 +89,7 @@ class OmniEngineCoreRequest(EngineCoreRequest):
prompt_embeds: torch.Tensor | None = None,
additional_information: AdditionalInformationPayload | None = None,
model_intermediate_buffer: dict[str, Any] | None = None,
payload_sender_info: dict[str, Any] | None = None,
) -> "OmniEngineCoreRequest":
"""Clone an EngineCoreRequest into an OmniEngineCoreRequest with optional payload overrides."""
@@ -97,6 +99,8 @@ class OmniEngineCoreRequest(EngineCoreRequest):
additional_information = getattr(request, "additional_information", None)
if model_intermediate_buffer is None:
model_intermediate_buffer = getattr(request, "model_intermediate_buffer", None)
if payload_sender_info is None:
payload_sender_info = getattr(request, "payload_sender_info", None)
return cls(
request_id=request.request_id,
@@ -121,6 +125,7 @@ class OmniEngineCoreRequest(EngineCoreRequest):
abort_immediately=request.abort_immediately,
additional_information=additional_information,
model_intermediate_buffer=model_intermediate_buffer,
payload_sender_info=payload_sender_info,
)
+22
View File
@@ -2148,6 +2148,7 @@ class Orchestrator:
*,
mm_features: list | None = None,
resumable: bool = False,
payload_sender_info: dict[str, Any] | None = None,
) -> Any:
next_pool = self.stage_pools[next_stage_id]
if self._next_stage_input_is_tokens(next_input):
@@ -2160,6 +2161,7 @@ class Orchestrator:
resumable=resumable,
)
request.external_req_id = request.request_id
request.payload_sender_info = payload_sender_info
return request
processor = self._get_stage_input_processor(next_stage_id)
@@ -2181,6 +2183,7 @@ class Orchestrator:
)
request = self._upgrade_processed_stage_request(request, next_input)
request.external_req_id = req_id
request.payload_sender_info = payload_sender_info
return request
@staticmethod
@@ -2829,6 +2832,7 @@ class Orchestrator:
params=params,
mm_features=mm_features,
resumable=next_stage_resumable,
payload_sender_info=self._build_payload_sender_info(src_stage_id, request_id=req_id),
)
if already_submitted:
@@ -2967,6 +2971,7 @@ class Orchestrator:
model_config=next_pool.stage_vllm_config.model_config,
resumable=downstream_resumable,
)
request.payload_sender_info = self._build_payload_sender_info(next_stage_id - 1, request_id=request_id)
request.external_req_id = request.request_id
submitted = await self._dispatch_or_fail_request(
lambda: next_pool.submit_initial(
@@ -3043,6 +3048,23 @@ class Orchestrator:
return sender_infos or None
def _build_payload_sender_info(
self,
sender_stage_id: int,
*,
request_id: str | None = None,
) -> dict[str, Any] | None:
if sender_stage_id < 0 or sender_stage_id >= len(self.stage_pools):
return None
sender_pool = self.stage_pools[sender_stage_id]
sender_stage = sender_pool.get_bound_client(request_id) if request_id is not None else None
if sender_stage is None:
sender_stage = sender_pool.stage_client
get_sender_info = getattr(sender_stage, "get_payload_sender_info", None)
if not callable(get_sender_info):
return None
return get_sender_info()
# ---- Shutdown / lifecycle ----
def _shutdown_stages(self) -> None:
+2
View File
@@ -94,6 +94,8 @@ class StagePoolLLMClient(StagePoolClient, Protocol):
kv_transfer_port_offset: int = ...,
) -> dict[str, Any] | None: ...
def get_payload_sender_info(self) -> dict[str, Any] | None: ...
class StagePoolDiffusionClient(StagePoolClient, Protocol):
"""Pool-facing API for diffusion stages."""
@@ -1,3 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
Stage Engine Core Client for vLLM-Omni multi-stage runtime.
@@ -210,6 +213,7 @@ class StageEngineCoreClientBase(StageClientBase):
)
raise
self._payload_sender_info = self._build_payload_sender_info()
self._initialize_kv_sender_endpoint()
logger.info(
@@ -291,6 +295,39 @@ class StageEngineCoreClientBase(StageClientBase):
return None
return connector_config
def _build_payload_sender_info(self) -> dict[str, Any] | None:
model_config = getattr(self.vllm_config, "model_config", None)
connector_config = getattr(model_config, "stage_connector_config", None)
if not isinstance(connector_config, dict):
return None
extra = connector_config.get("extra")
if not isinstance(extra, dict):
return None
if isinstance(extra.get("outgoing"), dict):
extra = extra["outgoing"]
elif extra.get("role") != "sender":
return None
base_port = extra.get("zmq_port", 50051)
if base_port is None:
return None
sender_host = self._resolve_sender_host_from_config(extra)
if sender_host is None:
return None
from vllm_omni.distributed.omni_connectors.utils.initialization import compute_connector_zmq_port
return {
"host": sender_host,
"zmq_port": compute_connector_zmq_port(
int(os.path.expandvars(str(base_port))),
purpose="request_forwarding",
from_stage=int(extra.get("from_stage", self.stage_id)),
replica_id=self.replica_id,
),
}
def get_payload_sender_info(self) -> dict[str, Any] | None:
return dict(self._payload_sender_info) if self._payload_sender_info is not None else None
def _resolve_sender_host_from_config(self, connector_config: dict[str, Any]) -> str | None:
host = connector_config.get("sender_host") or connector_config.get("host")
if host in {None, "", "auto", "*", "0.0.0.0", "::"}:
@@ -88,6 +88,7 @@ class StageEngineCoreProc(EngineCoreProc):
scheduler_request, current_wave = super().preprocess_add_request(request)
scheduler_request.additional_information = request.additional_information
scheduler_request.external_req_id = getattr(request, "external_req_id", request.request_id)
scheduler_request.payload_sender_info = request.payload_sender_info
return scheduler_request, current_wave
@staticmethod
+25 -2
View File
@@ -50,6 +50,9 @@ from vllm_omni.config.omni_config import (
)
from vllm_omni.config.stage_config import StageType
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.distributed.omni_connectors.utils.config import (
TRANSFER_ENGINE_CONNECTOR_NAMES,
)
from vllm_omni.engine.arg_utils import OmniEngineArgs
from vllm_omni.entrypoints.stage_utils import _to_dict, set_stage_devices
from vllm_omni.entrypoints.utils import filter_dataclass_kwargs
@@ -1882,17 +1885,37 @@ def get_stage_connector_spec(
stage_connectors_cfg = get_stage_connector_config(omni_transfer_config, stage_id)
for cfg in stage_connectors_cfg.values():
return dict(cfg.get("spec", {}))
connector_spec = dict(cfg.get("spec", {}))
if connector_spec.get("name") == "NixlConnector":
# A middle worker uses one connector for both directions. Preserve
# the incoming edge and carry the outgoing bind config separately.
for (source, target), outgoing in getattr(omni_transfer_config, "connectors", {}).items():
if source == str(stage_id):
if outgoing.name != "NixlConnector":
raise ValueError("A NIXL middle stage requires NIXL on its outgoing edge")
extra = dict(connector_spec.get("extra", {}))
extra["outgoing"] = {**(outgoing.extra or {}), "from_stage": int(source), "to_stage": int(target)}
connector_spec["extra"] = extra
break
if connector_spec.get("name") not in TRANSFER_ENGINE_CONNECTOR_NAMES:
extra = dict(connector_spec.get("extra", {}))
extra.pop("from_stage", None)
extra.pop("to_stage", None)
connector_spec["extra"] = extra
return connector_spec
# A producer does not consume connector data itself. Keep its connector
# for both async-chunk and terminal full-payload sends, but mark it
# sender-only so the scheduler does not park orchestrator-provided inputs
# waiting for an upstream payload.
target_stage = str(stage_id)
for (from_stage, _to_stage), spec in getattr(omni_transfer_config, "connectors", {}).items():
for (from_stage, to_stage), spec in getattr(omni_transfer_config, "connectors", {}).items():
if from_stage == target_stage:
extra = dict(spec.extra or {})
extra.setdefault("role", "sender")
if spec.name in TRANSFER_ENGINE_CONNECTOR_NAMES:
extra["from_stage"] = int(from_stage)
extra["to_stage"] = int(to_stage)
return {"name": spec.name, "extra": extra}
return {}