[Core] Cleanup Omni connector runtime and transfer backends (#5096)
Signed-off-by: spencerr221 <liubingyu62@gmail.com>
This commit is contained in:
1 parent
2cf5f57b3e
commit
dcf8b07d39
53 files changed
+349
-448
No files matched your search
@@ -65,7 +65,7 @@ connectors:
|
||||
|
||||
| Connector class | Use case | `extra` keys |
|
||||
|-----------------|----------|--------------|
|
||||
| `SharedMemoryConnector` | Same-host KV transfer between stages (default for bundled YAMLs). | `shm_threshold_bytes` (int, default `65536`). |
|
||||
| `SharedMemoryConnector` | Same-host KV transfer between stages (default for bundled YAMLs). | None. All payloads use shared memory. |
|
||||
| `MooncakeStoreConnector` | Cross-host KV transfer over TCP. Required for multi-node deployments. | `host`, `metadata_server`, `master`, `segment` (int bytes), `localbuf` (int bytes), `proto` (`"tcp"` / `"rdma"`). |
|
||||
|
||||
A stage references a connector by name in its `input_connectors` / `output_connectors`:
|
||||
|
||||
@@ -66,8 +66,6 @@ runtime:
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
```
|
||||
|
||||
Wire stages to connectors:
|
||||
|
||||
@@ -8,9 +8,7 @@ auto-configured when no explicit connector is specified for an edge.
|
||||
## How It Works
|
||||
|
||||
All payloads are serialized and stored in shared memory (`/dev/shm`); the SHM
|
||||
segment name is returned in metadata. The configuration exposes a
|
||||
`shm_threshold_bytes` field for a future inline-vs-SHM split, but the current
|
||||
implementation always uses shared memory regardless of payload size.
|
||||
segment name is returned in metadata.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -19,8 +17,6 @@ runtime:
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
```
|
||||
|
||||
## Notes
|
||||
@@ -166,27 +162,10 @@ This path is mainly for older code paths and is not the preferred mode for the c
|
||||
|
||||
### 6. Key Implementation Characteristics
|
||||
|
||||
#### 6.1 Threshold Exists, but the Current Code Always Uses SHM
|
||||
#### 6.1 All Payloads Use Shared Memory
|
||||
|
||||
The class keeps a `shm_threshold_bytes` field and still exposes metrics for inline writes. However, the current implementation uses:
|
||||
|
||||
```python
|
||||
if True:
|
||||
...
|
||||
```
|
||||
|
||||
inside `put()`, which means the current code path always writes to shared memory.
|
||||
|
||||
So the design still suggests a future split between:
|
||||
|
||||
- small payloads inline
|
||||
- large payloads in shared memory
|
||||
|
||||
but the current behavior is effectively:
|
||||
|
||||
- all payloads go through shared memory
|
||||
|
||||
This should be documented because it affects real runtime behavior.
|
||||
`put()` writes every serialized payload to shared memory. The connector has no
|
||||
inline-payload path or size threshold.
|
||||
|
||||
#### 6.2 Cleanup Is Currently Passive
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ When running on Ray, the system automatically adapts its communication strategy:
|
||||
|
||||
* **Cross-Node**: Recommended to use `MooncakeTransferEngineConnector` (RDMA, fastest) or `MooncakeStoreConnector` (TCP fallback).
|
||||
* **Same-Node**: Can still use `SharedMemoryConnector` for efficiency, or Ray's native object store (plasma).
|
||||
* **SHM threshold default differs**: when `worker_backend="ray"`, the SharedMemoryConnector default threshold is set to `sys.maxsize`, which forces payloads to go inline (no SHM). Override `shm_threshold_bytes` in the connector config if you want SHM for Ray runs.
|
||||
* **SHM payload routing**: `SharedMemoryConnector` always stores payloads in `/dev/shm`.
|
||||
|
||||
### 2.4 Internal Helpers
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ def main(args):
|
||||
stage_init_timeout=args.stage_init_timeout,
|
||||
batch_timeout=args.batch_timeout,
|
||||
init_timeout=args.init_timeout,
|
||||
shm_threshold_bytes=args.shm_threshold_bytes,
|
||||
)
|
||||
|
||||
# Stage 0: fused_thinker_talker
|
||||
|
||||
@@ -188,7 +188,6 @@ def main(args):
|
||||
init_sleep_seconds=args.init_sleep_seconds,
|
||||
batch_timeout=args.batch_timeout,
|
||||
init_timeout=args.init_timeout,
|
||||
shm_threshold_bytes=args.shm_threshold_bytes,
|
||||
)
|
||||
|
||||
thinker_sampling_params = SamplingParams(
|
||||
|
||||
@@ -258,7 +258,6 @@ def main(args):
|
||||
"init_sleep_seconds": args.init_sleep_seconds,
|
||||
"batch_timeout": args.batch_timeout,
|
||||
"init_timeout": args.init_timeout,
|
||||
"shm_threshold_bytes": args.shm_threshold_bytes,
|
||||
"worker_backend": args.worker_backend,
|
||||
"ray_address": args.ray_address,
|
||||
"trust_remote_code": True,
|
||||
|
||||
@@ -140,7 +140,6 @@ def build_engine_kwargs(args, stats_log_file: str | None) -> dict:
|
||||
"stage_init_timeout": args.stage_init_timeout,
|
||||
"init_timeout": args.init_timeout,
|
||||
"batch_timeout": args.batch_timeout,
|
||||
"shm_threshold_bytes": args.shm_threshold_bytes,
|
||||
"worker_backend": args.worker_backend,
|
||||
}
|
||||
if stats_log_file is not None:
|
||||
|
||||
@@ -302,7 +302,6 @@ def test_from_pipeline_config_maps_orchestrator_cli_overrides():
|
||||
"omni_dp_size_local": 2,
|
||||
"omni_lb_policy": "round_robin",
|
||||
"omni_heartbeat_timeout": 9.5,
|
||||
"shm_threshold_bytes": 4096,
|
||||
"batch_timeout": 3,
|
||||
},
|
||||
)
|
||||
@@ -317,7 +316,6 @@ def test_from_pipeline_config_maps_orchestrator_cli_overrides():
|
||||
assert orchestrator_config.omni_dp_size_local == 2
|
||||
assert orchestrator_config.omni_lb_policy == "round_robin"
|
||||
assert orchestrator_config.omni_heartbeat_timeout == 9.5
|
||||
assert orchestrator_config.shm_threshold_bytes == 4096
|
||||
assert orchestrator_config.batch_timeout == 3
|
||||
|
||||
|
||||
|
||||
@@ -142,11 +142,10 @@ def test_recv_no_connector():
|
||||
def test_shm_connector_flow(mocker: MockerFixture):
|
||||
"""
|
||||
Verify the full flow: Send -> Adapter -> Connector -> Adapter -> Recv.
|
||||
Using real SharedMemoryConnector (inline mode for simplicity).
|
||||
Using real SharedMemoryConnector and key-addressed SHM metadata.
|
||||
"""
|
||||
# 1. Setup Connector
|
||||
config = {"shm_threshold_bytes": 1024, "inline_small_payloads": True}
|
||||
connector = SharedMemoryConnector(config)
|
||||
connector = SharedMemoryConnector({})
|
||||
connectors_map = {("0", "1"): connector}
|
||||
|
||||
# 2. Setup Data
|
||||
|
||||
@@ -60,10 +60,9 @@ def test_ndarray_serialization():
|
||||
|
||||
def test_create_shm_connector():
|
||||
"""Test creating SharedMemoryConnector via Factory."""
|
||||
spec = ConnectorSpec(name="SharedMemoryConnector", extra={"shm_threshold_bytes": 1024})
|
||||
spec = ConnectorSpec(name="SharedMemoryConnector")
|
||||
connector = OmniConnectorFactory.create_connector(spec)
|
||||
assert isinstance(connector, SharedMemoryConnector)
|
||||
assert connector.threshold == 1024
|
||||
|
||||
|
||||
def test_create_unknown_connector():
|
||||
@@ -75,21 +74,20 @@ def test_create_unknown_connector():
|
||||
|
||||
@pytest.fixture
|
||||
def shm_connector():
|
||||
config = {"shm_threshold_bytes": 100, "inline_small_payloads": True}
|
||||
return SharedMemoryConnector(config)
|
||||
connector = SharedMemoryConnector({})
|
||||
yield connector
|
||||
connector.close()
|
||||
|
||||
|
||||
def test_put_get_inline(shm_connector):
|
||||
"""Test inline transfer for small data."""
|
||||
def test_put_get_small_payload_uses_shm(shm_connector):
|
||||
"""Small payloads use key-addressed SHM; inline metadata is retired."""
|
||||
data = {"small": "data"}
|
||||
|
||||
success, size, metadata = shm_connector.put("stage_0", "stage_1", "req_1", data)
|
||||
assert success is True
|
||||
assert "inline_bytes" in metadata
|
||||
assert "shm" not in metadata
|
||||
assert "shm" in metadata
|
||||
assert "inline_bytes" not in metadata
|
||||
assert "size" in metadata
|
||||
assert shm_connector._metrics["inline_writes"] == 1
|
||||
assert shm_connector._metrics["shm_writes"] == 0
|
||||
|
||||
# Retrieve
|
||||
retrieved_data, ret_size = shm_connector.get("stage_0", "stage_1", "req_1", metadata)
|
||||
@@ -195,3 +193,42 @@ def test_mooncake_connector_defaults_missing_host_to_detected_ip(monkeypatch: py
|
||||
assert connector.get_connection_info()["host"] == "10.20.30.40"
|
||||
finally:
|
||||
connector.close()
|
||||
|
||||
|
||||
def test_mooncake_get_counts_zmq_timeout_separately(mocker: MockerFixture):
|
||||
import vllm_omni.distributed.omni_connectors.connectors.mooncake_transfer_engine_connector as mooncake_module
|
||||
|
||||
connector = object.__new__(mooncake_module.MooncakeTransferEngineConnector)
|
||||
connector._closed = False
|
||||
connector._metrics = {"errors": 0, "timeouts": 0}
|
||||
connector._resolve_metadata = mocker.Mock(
|
||||
return_value={"source_host": "127.0.0.1", "source_port": 1234, "data_size": 8}
|
||||
)
|
||||
recv_buffer = mocker.Mock()
|
||||
connector._alloc_recv_buffer = mocker.Mock(return_value=(recv_buffer, 1000))
|
||||
connector._request_transfer = mocker.Mock(side_effect=mooncake_module.zmq.Again())
|
||||
|
||||
assert connector.get("s0", "s1", "req") is None
|
||||
assert connector._metrics == {"errors": 0, "timeouts": 1}
|
||||
recv_buffer.release.assert_called_once()
|
||||
connector._closed = True
|
||||
|
||||
|
||||
def test_mooncake_get_counts_cuda_sync_failure_as_error(mocker: MockerFixture):
|
||||
import vllm_omni.distributed.omni_connectors.connectors.mooncake_transfer_engine_connector as mooncake_module
|
||||
|
||||
connector = object.__new__(mooncake_module.MooncakeTransferEngineConnector)
|
||||
connector._closed = False
|
||||
connector._metrics = {"errors": 0, "timeouts": 0}
|
||||
connector._resolve_metadata = mocker.Mock(
|
||||
return_value={"source_host": "127.0.0.1", "source_port": 1234, "data_size": 8}
|
||||
)
|
||||
recv_buffer = mocker.Mock()
|
||||
connector._alloc_recv_buffer = mocker.Mock(return_value=(recv_buffer, 1000))
|
||||
connector._request_transfer = mocker.Mock(return_value=mooncake_module.TRANS_DONE)
|
||||
connector._sync_receive_buffer_if_needed = mocker.Mock(side_effect=RuntimeError("CUDA sync failed"))
|
||||
|
||||
assert connector.get("s0", "s1", "req") is None
|
||||
assert connector._metrics == {"errors": 1, "timeouts": 0}
|
||||
recv_buffer.release.assert_called_once()
|
||||
connector._closed = True
|
||||
@@ -48,7 +48,6 @@ def test_load_qwen_yaml_configs(yaml_file):
|
||||
print(f"Testing config load: {yaml_file.name}")
|
||||
try:
|
||||
# Attempt to load the config
|
||||
# default_shm_threshold doesn't matter much for loading correctness, using default
|
||||
config = load_omni_transfer_config(yaml_file)
|
||||
|
||||
assert config is not None, "Config should not be None"
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for SharedMemoryConnector focusing on TP / CFG / metadata fallback."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.distributed.omni_connectors.connectors.shm_connector import (
|
||||
SharedMemoryConnector,
|
||||
@@ -13,7 +16,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
@pytest.fixture()
|
||||
def connector():
|
||||
c = SharedMemoryConnector({"shm_threshold_bytes": 0})
|
||||
c = SharedMemoryConnector({})
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
@@ -36,6 +39,30 @@ class TestKeyBasedReadWrite:
|
||||
assert obj == data
|
||||
assert rsize == size
|
||||
assert "test_key_1" not in connector._pending_keys
|
||||
assert connector._metrics["gets"] == 1
|
||||
|
||||
def test_tensor_payload_removes_lock_file(self, connector):
|
||||
key = "tensor_payload"
|
||||
payload = torch.ones(2, 2)
|
||||
ok, _, metadata = connector.put("s0", "s1", key, payload)
|
||||
assert ok
|
||||
|
||||
result = connector.get("s0", "s1", key, metadata=metadata)
|
||||
|
||||
assert result is not None
|
||||
assert torch.equal(result[0], payload)
|
||||
assert not os.path.exists(f"/dev/shm/shm_{key}_lockfile.lock")
|
||||
|
||||
def test_falsey_payload_removes_lock_file(self, connector):
|
||||
key = "falsey_payload"
|
||||
ok, _, metadata = connector.put("s0", "s1", key, 0)
|
||||
assert ok
|
||||
|
||||
result = connector.get("s0", "s1", key, metadata=metadata)
|
||||
|
||||
assert result is not None
|
||||
assert result[0] == 0
|
||||
assert not os.path.exists(f"/dev/shm/shm_{key}_lockfile.lock")
|
||||
|
||||
def test_get_nonexistent_key_returns_none(self, connector):
|
||||
result = connector.get("s0", "s1", "no_such_key_xyz", metadata=None)
|
||||
|
||||
@@ -141,7 +141,6 @@ _DEPLOY_CONFIG = {
|
||||
"connectors": {
|
||||
"shared_memory_connector": {
|
||||
"name": "SharedMemoryConnector",
|
||||
"extra": {"shm_threshold_bytes": 65536},
|
||||
},
|
||||
},
|
||||
"stages": [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from contextlib import ExitStack
|
||||
|
||||
import pytest
|
||||
@@ -67,11 +66,9 @@ async def generate(
|
||||
@pytest.mark.asyncio
|
||||
async def test_abort():
|
||||
with ExitStack() as after:
|
||||
# Avoid SHM IPC in tests to prevent /dev/shm exhaustion and SIGBUS.
|
||||
engine = AsyncOmni(
|
||||
model=model,
|
||||
stage_configs_path=stage_config,
|
||||
shm_threshold_bytes=sys.maxsize,
|
||||
)
|
||||
after.callback(engine.shutdown)
|
||||
|
||||
|
||||
@@ -121,9 +121,7 @@ class _FakeStage:
|
||||
self._in_q = in_q
|
||||
self._out_q = out_q
|
||||
|
||||
def init_stage_worker(
|
||||
self, model: str, *, is_async=False, shm_threshold_bytes=65536, ctx=None, batch_timeout=10, **kwargs
|
||||
):
|
||||
def init_stage_worker(self, model: str, *, is_async=False, ctx=None, batch_timeout=10, **kwargs):
|
||||
self._proc = _ns(
|
||||
start=lambda: None,
|
||||
join=lambda timeout=None: None,
|
||||
|
||||
@@ -2333,7 +2333,6 @@ class OmniRunner:
|
||||
# production default in AsyncOmniEngine remains 600s; this only
|
||||
# affects the test runner wrapper.
|
||||
init_timeout: int = 1800,
|
||||
shm_threshold_bytes: int = 65536,
|
||||
log_stats: bool = False,
|
||||
stage_configs_path: str | None = None,
|
||||
**kwargs,
|
||||
@@ -2353,7 +2352,6 @@ class OmniRunner:
|
||||
stage_init_timeout=stage_init_timeout,
|
||||
batch_timeout=batch_timeout,
|
||||
init_timeout=init_timeout,
|
||||
shm_threshold_bytes=shm_threshold_bytes,
|
||||
stage_configs_path=stage_configs_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -110,7 +110,6 @@ class TestMixinAsyncChunkSendRecv(unittest.TestCase):
|
||||
|
||||
sender = MixinHost()
|
||||
sender.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=0, async_chunk=True),
|
||||
)
|
||||
sender._omni_connector = connector
|
||||
@@ -147,7 +146,6 @@ class TestMixinAsyncChunkSendRecv(unittest.TestCase):
|
||||
|
||||
sender = MixinHost()
|
||||
sender.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=0, async_chunk=True),
|
||||
)
|
||||
sender._omni_connector = connector
|
||||
@@ -180,7 +178,6 @@ class TestMixinKVCacheTransfer(unittest.TestCase):
|
||||
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(),
|
||||
kv_transfer_manager=mock_kvm,
|
||||
)
|
||||
@@ -202,7 +199,6 @@ class TestMixinKVCacheTransfer(unittest.TestCase):
|
||||
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(),
|
||||
kv_transfer_manager=mock_kvm,
|
||||
)
|
||||
@@ -219,7 +215,6 @@ class TestMixinKVCacheTransfer(unittest.TestCase):
|
||||
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(),
|
||||
kv_transfer_manager=mock_kvm,
|
||||
)
|
||||
@@ -257,7 +252,6 @@ class TestMixinKVCacheTransfer(unittest.TestCase):
|
||||
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(),
|
||||
kv_transfer_manager=mock_kvm,
|
||||
)
|
||||
@@ -281,7 +275,6 @@ class TestOmniConnectorOutput(unittest.TestCase):
|
||||
def test_output_aggregation(self):
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(),
|
||||
)
|
||||
|
||||
@@ -310,7 +303,6 @@ class TestMixinNoConnector(unittest.TestCase):
|
||||
def test_no_connector(self):
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(),
|
||||
)
|
||||
self.assertIsNone(host._omni_connector)
|
||||
@@ -336,7 +328,6 @@ class TestFinishedLoadReqsDrain(unittest.TestCase):
|
||||
def test_finished_load_reqs_flow_to_chunk_ready(self):
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(),
|
||||
)
|
||||
|
||||
@@ -389,7 +380,6 @@ class TestFullPayloadSendWithCustomFunc(unittest.TestCase):
|
||||
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(),
|
||||
)
|
||||
host._omni_connector = MockConnector(stage_id=0)
|
||||
@@ -424,7 +414,6 @@ class TestFullPayloadSendWithCustomFunc(unittest.TestCase):
|
||||
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(),
|
||||
)
|
||||
host._omni_connector = MockConnector(stage_id=0)
|
||||
@@ -453,7 +442,6 @@ class TestKVSentReqIdsAccumulation(unittest.TestCase):
|
||||
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(),
|
||||
kv_transfer_manager=mock_kvm,
|
||||
)
|
||||
@@ -488,7 +476,6 @@ class TestChunkStreamCompletedGuard(unittest.TestCase):
|
||||
def _make_host(self, stage_id: int = 1) -> MixinHost:
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=stage_id, async_chunk=True),
|
||||
)
|
||||
host._omni_connector = MockConnector(stage_id=stage_id)
|
||||
@@ -587,7 +574,6 @@ class TestCleanupFinishedRequest(unittest.TestCase):
|
||||
def _make_host(self, stage_id: int = 1) -> MixinHost:
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=stage_id, async_chunk=True),
|
||||
)
|
||||
host._omni_connector = MockConnector(stage_id=stage_id)
|
||||
@@ -771,7 +757,6 @@ class TestSendChunkCachesMapping(unittest.TestCase):
|
||||
"""send_chunk should cache the internal→external mapping."""
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=0, async_chunk=True),
|
||||
)
|
||||
host._omni_connector = MockConnector(stage_id=0)
|
||||
@@ -802,7 +787,6 @@ class TestLocalPayloadCacheLifecycle(unittest.TestCase):
|
||||
def _make_host(self) -> MixinHost:
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=0),
|
||||
)
|
||||
host._omni_connector = MockConnector(stage_id=0)
|
||||
@@ -901,7 +885,6 @@ class TestTPAsyncChunkFanout(unittest.TestCase):
|
||||
def _make_host(self, rank: int) -> MixinHost:
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=2, async_chunk=True, worker_type="gen"),
|
||||
)
|
||||
host._omni_connector = MagicMock()
|
||||
@@ -984,7 +967,6 @@ class TestKVTransferLifecycle(unittest.TestCase):
|
||||
def _make_host(self) -> MixinHost:
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=0),
|
||||
)
|
||||
return host
|
||||
@@ -1039,7 +1021,6 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
|
||||
def test_send_side_request_payload_not_cleared_before_payload_is_consumable(self):
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=1, async_chunk=True, worker_type="ar"),
|
||||
)
|
||||
host._request_ids_mapping["r1"] = "r1"
|
||||
@@ -1063,7 +1044,6 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
|
||||
def test_payload_consumable_ignores_token_horizon_only_updates(self):
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=1, async_chunk=True, worker_type="ar"),
|
||||
)
|
||||
payload = {
|
||||
@@ -1084,7 +1064,6 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
|
||||
def test_payload_consumable_accepts_decode_embeddings(self):
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=1, async_chunk=True, worker_type="ar"),
|
||||
)
|
||||
payload = {
|
||||
@@ -1098,7 +1077,6 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
|
||||
def test_ar_metadata_only_followup_chunk_does_not_rewake_request(self):
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=1, async_chunk=True, worker_type="ar"),
|
||||
)
|
||||
host._omni_connector = MagicMock()
|
||||
@@ -1138,7 +1116,6 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
|
||||
def test_non_ar_recv_does_not_overwrite_unconsumed_staged_chunk(self):
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=2, async_chunk=True, worker_type="gen"),
|
||||
)
|
||||
host._omni_connector = MagicMock()
|
||||
@@ -1164,7 +1141,6 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
|
||||
def test_non_ar_recv_waits_for_scheduler_handoff_before_fetching_next_chunk(self):
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=2, async_chunk=True, worker_type="gen"),
|
||||
)
|
||||
host._omni_connector = MagicMock()
|
||||
@@ -1208,7 +1184,7 @@ class TestAsyncPayloadLifecycle(unittest.TestCase):
|
||||
class TestRankAwareKVRouting(unittest.TestCase):
|
||||
def _make_host(self, *, from_tp: int, to_tp: int, local_rank: int) -> MixinHost:
|
||||
host = MixinHost()
|
||||
host.init_omni_connectors(vllm_config=None, model_config=_make_model_config(stage_id=1))
|
||||
host.init_omni_connectors(model_config=_make_model_config(stage_id=1))
|
||||
host._from_tp = from_tp
|
||||
host._to_tp = to_tp
|
||||
host._local_rank = local_rank
|
||||
@@ -1287,7 +1263,7 @@ class TestConnectorConfigValidation(unittest.TestCase):
|
||||
model_config.stage_connector_config = {"name": " "}
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "missing connector name"):
|
||||
host.init_omni_connectors(vllm_config=None, model_config=model_config)
|
||||
host.init_omni_connectors(model_config=model_config)
|
||||
|
||||
|
||||
class _FailingConnector:
|
||||
@@ -1319,7 +1295,6 @@ class TestSendRetry(unittest.TestCase):
|
||||
def _make_sender(self, connector):
|
||||
sender = MixinHost()
|
||||
sender.init_omni_connectors(
|
||||
vllm_config=None,
|
||||
model_config=_make_model_config(stage_id=0, async_chunk=True),
|
||||
)
|
||||
sender._omni_connector = connector
|
||||
|
||||
@@ -821,7 +821,6 @@ class VllmOmniOrchestratorConfig:
|
||||
omni_dp_size_local: int = Field(default=1, ge=1)
|
||||
omni_lb_policy: str = "random"
|
||||
omni_heartbeat_timeout: float = Field(default=30.0, gt=0.0)
|
||||
shm_threshold_bytes: int = Field(default=65536, ge=0)
|
||||
batch_timeout: int = Field(default=10, ge=0)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -40,8 +40,6 @@ stages:
|
||||
connectors:
|
||||
shared_memory_connector:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
|
||||
rdma_connector:
|
||||
name: MooncakeTransferEngineConnector
|
||||
|
||||
@@ -31,8 +31,6 @@ connectors:
|
||||
# Alternative SHM connector with an explicit threshold.
|
||||
shared_memory_connector:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536 # 64KB threshold
|
||||
|
||||
stages:
|
||||
- stage_id: 0
|
||||
|
||||
@@ -9,7 +9,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -20,7 +20,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
# Tightened from 10ms to 1ms: chunks become available sooner under
|
||||
# cross-GPU pipelining, so connector idle waiting dominates if left high.
|
||||
|
||||
@@ -10,7 +10,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.001
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -10,7 +10,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.001
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -17,7 +17,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 5000
|
||||
|
||||
@@ -16,7 +16,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
# Stream PCM bytes back to the client as Stage 1 emits each chunk.
|
||||
codec_streaming: true
|
||||
# Streaming chunk shape for talker2code2wav_async_chunk:
|
||||
|
||||
@@ -15,7 +15,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
# Stream PCM bytes back to the client as Stage 1 emits each chunk.
|
||||
codec_streaming: true
|
||||
# Streaming chunk shape for talker2code2wav_async_chunk:
|
||||
|
||||
@@ -21,7 +21,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
# Stream PCM bytes back to the client as Stage 1 emits each chunk.
|
||||
codec_streaming: true
|
||||
# Streaming chunk shape for talker2code2wav_async_chunk:
|
||||
|
||||
@@ -27,8 +27,6 @@ connectors:
|
||||
memory_pool_device: "npu"
|
||||
shared_memory_connector:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
|
||||
stages:
|
||||
- stage_id: 0
|
||||
|
||||
@@ -4,7 +4,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: false
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 5000
|
||||
|
||||
@@ -17,7 +17,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: false
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 5000
|
||||
|
||||
@@ -13,7 +13,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 365536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.001
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -50,8 +50,6 @@ stages:
|
||||
connectors:
|
||||
shared_memory_connector:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536 # 64KB threshold
|
||||
|
||||
edges:
|
||||
- from: 0
|
||||
|
||||
@@ -10,7 +10,6 @@ connectors:
|
||||
shm:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -8,7 +8,6 @@ connectors:
|
||||
shm:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -11,7 +11,6 @@ connectors:
|
||||
shm:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -9,7 +9,6 @@ connectors:
|
||||
shm:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.005
|
||||
connector_get_max_wait_first_chunk: 1000
|
||||
|
||||
@@ -11,7 +11,6 @@ connectors:
|
||||
shm:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.005
|
||||
connector_get_max_wait_first_chunk: 1000
|
||||
|
||||
@@ -8,7 +8,6 @@ connectors:
|
||||
shm:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -10,7 +10,6 @@ connectors:
|
||||
shm:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -17,7 +17,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -15,7 +15,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -10,7 +10,6 @@ connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
+229
-170
@@ -30,6 +30,9 @@ except ImportError:
|
||||
# Stale buffer TTL: buffers older than this are automatically reclaimed
|
||||
# to prevent memory leaks when receiver crashes or gives up.
|
||||
_BUFFER_TTL_SECONDS = 300 # 5 minutes
|
||||
_BASE_TRANSFER_TIMEOUT_MS = 30_000
|
||||
_TRANSFER_TIMEOUT_STEP_BYTES = 100 * 1024 * 1024
|
||||
_TRANSFER_TIMEOUT_PER_STEP_MS = 5_000
|
||||
|
||||
# ZMQ Message constants
|
||||
TRANS_DONE = b"trans_done"
|
||||
@@ -119,6 +122,7 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
self._worker_local = threading.local()
|
||||
self._last_ttl_check: float = _time_mod.monotonic()
|
||||
self._sender_endpoints: dict[int, tuple[str, int]] = {}
|
||||
self._in_flight_buffers: set[str] = set()
|
||||
|
||||
self._metrics = {
|
||||
"puts": 0,
|
||||
@@ -155,6 +159,7 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
# --- Memory Pool Configuration ---
|
||||
self.pool_size = config.get("memory_pool_size", 1024**3) # Default 1GB
|
||||
self.pool_device = config.get("memory_pool_device", "cpu")
|
||||
self.buffer_ttl_seconds = float(config.get("buffer_ttl_seconds", _BUFFER_TTL_SECONDS))
|
||||
|
||||
# --- Sender Configuration (for receiver to query without metadata) ---
|
||||
# When receiver doesn't have metadata, it uses these to connect to sender
|
||||
@@ -175,7 +180,12 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
|
||||
self.engine_id = str(uuid.uuid4())
|
||||
|
||||
# --- Mooncake Engine Init ---
|
||||
self._init_engine()
|
||||
self._init_pool()
|
||||
self._init_listener(config)
|
||||
|
||||
def _init_engine(self) -> None:
|
||||
"""Initialize the Mooncake engine and discover the local RPC port."""
|
||||
self.engine = TransferEngine()
|
||||
# Note: For P2P handshake mode, local_hostname should be just the IP address.
|
||||
# Mooncake will auto-assign an RPC port, retrievable via get_rpc_port().
|
||||
@@ -186,7 +196,8 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
self.rpc_port = self.engine.get_rpc_port()
|
||||
logger.info(f"MooncakeTransferEngineConnector initialized at {self.host}:{self.rpc_port}")
|
||||
|
||||
# --- Pool Allocation & Registration ---
|
||||
def _init_pool(self) -> None:
|
||||
"""Allocate and register the memory pool used by TransferEngine."""
|
||||
logger.info(f"Allocating RDMA Memory Pool: {self.pool_size / 1024**2:.2f} MB on {self.pool_device}")
|
||||
try:
|
||||
if self.pool_device == "cpu":
|
||||
@@ -207,9 +218,8 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
|
||||
self.allocator = BufferAllocator(self.pool_size, alignment=4096) # 4KB alignment for safety
|
||||
|
||||
# --- State Management & Background Threads ---
|
||||
# Most fields already initialized at the top of __init__ for teardown
|
||||
# safety. Only create the real ZMQ context and thread pool here.
|
||||
def _init_listener(self, config: dict[str, Any]) -> None:
|
||||
"""Initialize control-plane sockets and start the sender listener."""
|
||||
self.zmq_ctx = zmq.Context()
|
||||
self._sender_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="mooncake-sender")
|
||||
|
||||
@@ -642,44 +652,18 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
raise RuntimeError("Cannot get data: MooncakeTransferEngineConnector is closed")
|
||||
|
||||
get_key = self._make_key(get_key, from_stage, to_stage)
|
||||
|
||||
_t0 = _time_mod.perf_counter()
|
||||
|
||||
metadata = self._resolve_metadata(get_key, metadata)
|
||||
if not metadata:
|
||||
# Path 3: no metadata at all — query default sender
|
||||
if not self.sender_host or not self.sender_zmq_port or str(self.sender_host).lower() == "auto":
|
||||
raise RuntimeError(
|
||||
f"get(metadata=None) requires sender info to be resolved, "
|
||||
f"but sender_host={self.sender_host!r}, sender_zmq_port={self.sender_zmq_port!r}. "
|
||||
f"Call update_sender_info(host, port) before using get() without metadata."
|
||||
)
|
||||
metadata = self._query_metadata_from_sender(get_key)
|
||||
if not metadata:
|
||||
return None
|
||||
elif "data_size" not in metadata:
|
||||
# Path 2: partial metadata (host/port only) — query that sender
|
||||
partial_host = metadata.get("source_host")
|
||||
partial_port = metadata.get("source_port")
|
||||
if not partial_host or not partial_port:
|
||||
logger.warning(
|
||||
"get(%s): partial metadata missing source_host/source_port, cannot resolve data_size. metadata=%s",
|
||||
get_key,
|
||||
metadata,
|
||||
)
|
||||
return None
|
||||
queried = self._query_metadata_at(get_key, str(partial_host), int(partial_port))
|
||||
if not queried:
|
||||
return None
|
||||
metadata = queried
|
||||
return None
|
||||
|
||||
_t1 = _time_mod.perf_counter()
|
||||
_query_ms = (_t1 - _t0) * 1000
|
||||
query_ms = (_t1 - _t0) * 1000
|
||||
|
||||
src_host = metadata.get("source_host")
|
||||
src_port = metadata.get("source_port")
|
||||
data_size = metadata.get("data_size", 0)
|
||||
is_fast_path = metadata.get("is_fast_path", False)
|
||||
|
||||
data_size = int(metadata.get("data_size", 0) or 0)
|
||||
is_fast_path = bool(metadata.get("is_fast_path", False))
|
||||
if not src_host or not src_port or str(src_host).lower() == "auto":
|
||||
logger.error(
|
||||
f"Invalid metadata for {get_key}: source_host={src_host!r}, "
|
||||
@@ -691,27 +675,116 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
logger.warning(f"Skipping get for {get_key}: data_size is 0 (metadata={metadata})")
|
||||
return None
|
||||
|
||||
# 1. Allocate Destination Buffer from Pool
|
||||
# For non-fast-path (serialized objects), allocate a few extra bytes
|
||||
# as padding. Mooncake's TCP/memcpy transport may prepend a leading
|
||||
# protocol byte (0x01) before the payload. The extra padding allows
|
||||
# the receiver to detect and skip this byte without truncating the
|
||||
# actual payload. The padding is harmless for fast-path (ManagedBuffer)
|
||||
# because the caller reads exact-size slices.
|
||||
_MOONCAKE_TCP_PADDING = 4 if not is_fast_path else 0
|
||||
alloc_size = data_size + _MOONCAKE_TCP_PADDING
|
||||
try:
|
||||
offset = self.allocator.alloc(alloc_size)
|
||||
recv_buffer = ManagedBuffer(self.allocator, offset, alloc_size, self.pool)
|
||||
dst_ptr = self.base_ptr + offset
|
||||
recv_buffer, dst_ptr = self._alloc_recv_buffer(data_size, is_fast_path)
|
||||
except MemoryError:
|
||||
logger.error(f"Failed to allocate {alloc_size} bytes in receive pool")
|
||||
return None
|
||||
|
||||
_t2 = _time_mod.perf_counter()
|
||||
_alloc_ms = (_t2 - _t1) * 1000
|
||||
alloc_ms = (_t2 - _t1) * 1000
|
||||
try:
|
||||
resp = self._request_transfer(
|
||||
get_key=get_key,
|
||||
source_host=str(src_host),
|
||||
source_port=int(src_port),
|
||||
dst_ptr=dst_ptr,
|
||||
data_size=data_size,
|
||||
)
|
||||
except zmq.Again as e:
|
||||
self._metrics["timeouts"] += 1
|
||||
logger.error(f"RDMA Get timed out: {e}", exc_info=True)
|
||||
recv_buffer.release()
|
||||
return None
|
||||
except Exception as e:
|
||||
self._metrics["errors"] += 1
|
||||
logger.error(f"RDMA transfer request failed: {e}", exc_info=True)
|
||||
recv_buffer.release()
|
||||
return None
|
||||
|
||||
# 2. Prepare Handshake
|
||||
_t3 = _time_mod.perf_counter()
|
||||
rdma_ms = (_t3 - _t2) * 1000
|
||||
if resp != TRANS_DONE:
|
||||
self._metrics["errors"] += 1
|
||||
logger.error(f"RDMA Get failed: received {resp} instead of TRANS_DONE")
|
||||
recv_buffer.release()
|
||||
return None
|
||||
|
||||
try:
|
||||
sync_ms = self._sync_receive_buffer_if_needed()
|
||||
except Exception as e:
|
||||
self._metrics["errors"] += 1
|
||||
logger.error(f"Failed to synchronize Mooncake receive buffer: {e}", exc_info=True)
|
||||
recv_buffer.release()
|
||||
return None
|
||||
|
||||
try:
|
||||
return self._decode_result(
|
||||
get_key=get_key,
|
||||
recv_buffer=recv_buffer,
|
||||
data_size=data_size,
|
||||
is_fast_path=is_fast_path,
|
||||
query_ms=query_ms,
|
||||
alloc_ms=alloc_ms,
|
||||
rdma_ms=rdma_ms,
|
||||
sync_ms=sync_ms,
|
||||
start_time=_t0,
|
||||
)
|
||||
except Exception as e:
|
||||
self._metrics["errors"] += 1
|
||||
logger.error(f"RDMA result decode failed for {get_key}: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
def _resolve_metadata(self, get_key: str, metadata: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
"""Resolve full transfer metadata for a receive request."""
|
||||
if not metadata:
|
||||
# Path 3: no metadata at all — query default sender
|
||||
if not self.sender_host or not self.sender_zmq_port or str(self.sender_host).lower() == "auto":
|
||||
raise RuntimeError(
|
||||
f"get(metadata=None) requires sender info to be resolved, "
|
||||
f"but sender_host={self.sender_host!r}, sender_zmq_port={self.sender_zmq_port!r}. "
|
||||
f"Call update_sender_info(host, port) before using get() without metadata."
|
||||
)
|
||||
return self._query_metadata_from_sender(get_key)
|
||||
|
||||
if "data_size" in metadata:
|
||||
# Path 1: metadata is complete
|
||||
return metadata
|
||||
|
||||
# Path 2: partial metadata (host/port only) — query that sender
|
||||
partial_host = metadata.get("source_host")
|
||||
partial_port = metadata.get("source_port")
|
||||
if not partial_host or not partial_port:
|
||||
logger.warning(
|
||||
"get(%s): partial metadata missing source_host/source_port, cannot resolve data_size. metadata=%s",
|
||||
get_key,
|
||||
metadata,
|
||||
)
|
||||
return None
|
||||
return self._query_metadata_at(get_key, str(partial_host), int(partial_port))
|
||||
|
||||
def _alloc_recv_buffer(self, data_size: int, is_fast_path: bool) -> tuple[ManagedBuffer, int]:
|
||||
"""Allocate the receive-side buffer and return ``(buffer, dst_ptr)``."""
|
||||
# For non-fast-path serialized objects, allocate a few extra bytes as
|
||||
# padding. Mooncake's TCP/memcpy transport may prepend a protocol byte.
|
||||
padding = 4 if not is_fast_path else 0
|
||||
alloc_size = data_size + padding
|
||||
try:
|
||||
offset = self.allocator.alloc(alloc_size)
|
||||
except MemoryError:
|
||||
logger.error(f"Failed to allocate {alloc_size} bytes in receive pool")
|
||||
raise
|
||||
recv_buffer = ManagedBuffer(self.allocator, offset, alloc_size, self.pool)
|
||||
return recv_buffer, self.base_ptr + offset
|
||||
|
||||
def _request_transfer(
|
||||
self,
|
||||
get_key: str,
|
||||
source_host: str,
|
||||
source_port: int,
|
||||
dst_ptr: int,
|
||||
data_size: int,
|
||||
) -> bytes:
|
||||
"""Ask the sender to write this key into the local receive buffer."""
|
||||
agent_meta = MooncakeAgentMetadata(
|
||||
remote_hostname=self.host,
|
||||
remote_port=self.rpc_port,
|
||||
@@ -720,109 +793,82 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
lengths=[data_size],
|
||||
)
|
||||
|
||||
# 3. ZMQ Transaction (uses cached socket to avoid TCP reconnection)
|
||||
# Timeout scales with data size: base 30s + 5s per 100MB.
|
||||
# Large RDMA transfers (esp. loopback or cross-NIC) can take tens of
|
||||
# seconds; if we time out too early the receiver closes and deregisters
|
||||
# its memory, causing the sender's in-flight write to fail with ret=-1.
|
||||
_base_timeout_ms = 30000
|
||||
_size_timeout_ms = max(0, (data_size // (100 * 1024 * 1024))) * 5000
|
||||
_total_timeout_ms = _base_timeout_ms + _size_timeout_ms
|
||||
zmq_addr = f"tcp://{src_host}:{src_port}"
|
||||
req_socket = self._get_req_socket(zmq_addr, timeout_ms=_total_timeout_ms)
|
||||
|
||||
size_timeout_ms = (data_size // _TRANSFER_TIMEOUT_STEP_BYTES) * _TRANSFER_TIMEOUT_PER_STEP_MS
|
||||
total_timeout_ms = _BASE_TRANSFER_TIMEOUT_MS + size_timeout_ms
|
||||
zmq_addr = f"tcp://{source_host}:{source_port}"
|
||||
req_socket = self._get_req_socket(zmq_addr, timeout_ms=total_timeout_ms)
|
||||
try:
|
||||
req_socket.send(msgspec.msgpack.encode(agent_meta))
|
||||
resp = req_socket.recv()
|
||||
|
||||
_t3 = _time_mod.perf_counter()
|
||||
_rdma_ms = (_t3 - _t2) * 1000
|
||||
|
||||
if resp == TRANS_DONE:
|
||||
# Success
|
||||
# Ensure data is visible on GPU
|
||||
# Note: RDMA write visibility on GPU usually requires some form of fence/sync.
|
||||
# torch.accelerator.synchronize() is a heavy hammer but safe.
|
||||
# Ideally Mooncake engine provides a way to poll for completion that guarantees visibility.
|
||||
# TODO(wzliu): Replace synchronize with cuda event in the future for better performance.
|
||||
if self.pool.is_cuda:
|
||||
with torch.cuda.device(self.pool.device):
|
||||
torch.cuda.current_stream().synchronize()
|
||||
|
||||
_t4 = _time_mod.perf_counter()
|
||||
_sync_ms = (_t4 - _t3) * 1000
|
||||
|
||||
if is_fast_path:
|
||||
# Return ManagedBuffer directly for ALL fast_path data
|
||||
# (both bytes and tensor payloads). Caller is responsible
|
||||
# for consuming the data and calling release() afterwards.
|
||||
# This avoids the expensive to_bytes() copy (~54ms for 115MB).
|
||||
#
|
||||
# Usage patterns for callers:
|
||||
# - Zero-copy read: mv = memoryview(buf.tensor.numpy())
|
||||
# - Copy to GPU: buf.tensor.to(device); buf.release()
|
||||
# - Fallback: data = buf.to_bytes(); buf.release()
|
||||
_t5 = _time_mod.perf_counter()
|
||||
_total_ms = (_t5 - _t0) * 1000
|
||||
_mbps = (data_size / 1024 / 1024) / (_total_ms / 1000) if _total_ms > 0 else 0
|
||||
logger.info(
|
||||
f"[RDMA GET] {get_key}: query={_query_ms:.1f}ms, alloc={_alloc_ms:.1f}ms, "
|
||||
f"rdma={_rdma_ms:.1f}ms, sync={_sync_ms:.1f}ms, "
|
||||
f"total={_total_ms:.1f}ms, {_mbps:.1f} MB/s (fast_path, zero-copy)"
|
||||
)
|
||||
self._metrics["gets"] += 1
|
||||
self._metrics["bytes_transferred"] += data_size
|
||||
return recv_buffer, data_size
|
||||
else:
|
||||
# If it was a serialized object or generic bytes, we assume standard Omni behavior:
|
||||
# Deserialize and return object. This requires a copy (to_bytes).
|
||||
# We MUST release the buffer after deserialization.
|
||||
try:
|
||||
_t_copy_start = _time_mod.perf_counter()
|
||||
raw_bytes = recv_buffer.to_bytes()
|
||||
_t_copy_end = _time_mod.perf_counter()
|
||||
_copy_ms = (_t_copy_end - _t_copy_start) * 1000
|
||||
|
||||
# Try deserializing the first data_size bytes. Mooncake's
|
||||
# TCP/memcpy transport may prepend a leading protocol byte
|
||||
# (0x01) before the payload — handle this by retrying from
|
||||
# offset 1 if the first attempt raises DecodeError.
|
||||
_t_deser_start = _time_mod.perf_counter()
|
||||
payload = raw_bytes[:data_size]
|
||||
try:
|
||||
val = OmniSerializer.deserialize(payload)
|
||||
except msgspec.DecodeError:
|
||||
# Likely a leading Mooncake TCP protocol byte — retry
|
||||
# from offset 1, using the extra padding bytes if needed.
|
||||
payload = raw_bytes[1 : data_size + 1]
|
||||
val = OmniSerializer.deserialize(payload)
|
||||
_t_deser_end = _time_mod.perf_counter()
|
||||
_deser_ms = (_t_deser_end - _t_deser_start) * 1000
|
||||
|
||||
_total_ms = (_t_deser_end - _t0) * 1000
|
||||
_mbps = (data_size / 1024 / 1024) / (_total_ms / 1000) if _total_ms > 0 else 0
|
||||
logger.info(
|
||||
f"[RDMA GET] {get_key}: query={_query_ms:.1f}ms, alloc={_alloc_ms:.1f}ms, "
|
||||
f"rdma={_rdma_ms:.1f}ms, sync={_sync_ms:.1f}ms, copy={_copy_ms:.1f}ms, "
|
||||
f"deser={_deser_ms:.1f}ms, total={_total_ms:.1f}ms, {_mbps:.1f} MB/s"
|
||||
)
|
||||
self._metrics["gets"] += 1
|
||||
self._metrics["bytes_transferred"] += data_size
|
||||
return val, data_size
|
||||
finally:
|
||||
recv_buffer.release()
|
||||
else:
|
||||
self._metrics["errors"] += 1
|
||||
logger.error(f"RDMA Get failed: received {resp} instead of TRANS_DONE")
|
||||
recv_buffer.release()
|
||||
return None
|
||||
except Exception as e:
|
||||
# Socket may be stuck after timeout; discard it
|
||||
return req_socket.recv()
|
||||
except Exception:
|
||||
# Socket may be stuck after timeout; discard it.
|
||||
self._invalidate_req_socket(zmq_addr)
|
||||
self._metrics["timeouts"] += 1
|
||||
logger.error(f"RDMA Get error: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _sync_receive_buffer_if_needed(self) -> float:
|
||||
"""Synchronize GPU-visible receive memory until Mooncake exposes a finer fence."""
|
||||
t0 = _time_mod.perf_counter()
|
||||
if self.pool.is_cuda:
|
||||
with torch.cuda.device(self.pool.device):
|
||||
torch.cuda.current_stream().synchronize()
|
||||
return (_time_mod.perf_counter() - t0) * 1000
|
||||
|
||||
def _decode_result(
|
||||
self,
|
||||
get_key: str,
|
||||
recv_buffer: ManagedBuffer,
|
||||
data_size: int,
|
||||
is_fast_path: bool,
|
||||
query_ms: float,
|
||||
alloc_ms: float,
|
||||
rdma_ms: float,
|
||||
sync_ms: float,
|
||||
start_time: float,
|
||||
) -> tuple[Any, int]:
|
||||
"""Decode a completed transfer result."""
|
||||
if is_fast_path:
|
||||
try:
|
||||
total_ms = (_time_mod.perf_counter() - start_time) * 1000
|
||||
mbps = (data_size / 1024 / 1024) / (total_ms / 1000) if total_ms > 0 else 0
|
||||
logger.info(
|
||||
f"[RDMA GET] {get_key}: query={query_ms:.1f}ms, alloc={alloc_ms:.1f}ms, "
|
||||
f"rdma={rdma_ms:.1f}ms, sync={sync_ms:.1f}ms, "
|
||||
f"total={total_ms:.1f}ms, {mbps:.1f} MB/s (fast_path, zero-copy)"
|
||||
)
|
||||
self._metrics["gets"] += 1
|
||||
self._metrics["bytes_transferred"] += data_size
|
||||
return recv_buffer, data_size
|
||||
except Exception:
|
||||
recv_buffer.release()
|
||||
raise
|
||||
|
||||
try:
|
||||
copy_start = _time_mod.perf_counter()
|
||||
raw_bytes = recv_buffer.to_bytes()
|
||||
copy_ms = (_time_mod.perf_counter() - copy_start) * 1000
|
||||
|
||||
deser_start = _time_mod.perf_counter()
|
||||
payload = raw_bytes[:data_size]
|
||||
try:
|
||||
value = OmniSerializer.deserialize(payload)
|
||||
except msgspec.DecodeError:
|
||||
payload = raw_bytes[1 : data_size + 1]
|
||||
value = OmniSerializer.deserialize(payload)
|
||||
deser_ms = (_time_mod.perf_counter() - deser_start) * 1000
|
||||
|
||||
total_ms = (_time_mod.perf_counter() - start_time) * 1000
|
||||
mbps = (data_size / 1024 / 1024) / (total_ms / 1000) if total_ms > 0 else 0
|
||||
logger.info(
|
||||
f"[RDMA GET] {get_key}: query={query_ms:.1f}ms, alloc={alloc_ms:.1f}ms, "
|
||||
f"rdma={rdma_ms:.1f}ms, sync={sync_ms:.1f}ms, copy={copy_ms:.1f}ms, "
|
||||
f"deser={deser_ms:.1f}ms, total={total_ms:.1f}ms, {mbps:.1f} MB/s"
|
||||
)
|
||||
self._metrics["gets"] += 1
|
||||
self._metrics["bytes_transferred"] += data_size
|
||||
return value, data_size
|
||||
finally:
|
||||
recv_buffer.release()
|
||||
return None
|
||||
|
||||
def cleanup(self, request_id: str, from_stage: str | None = None, to_stage: str | None = None) -> None:
|
||||
"""Release the producer-side buffer associated with the request.
|
||||
@@ -847,6 +893,7 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
with self._local_buffers_lock:
|
||||
item = self._local_buffers.pop(request_id, None)
|
||||
if item:
|
||||
self._in_flight_buffers.discard(request_id)
|
||||
# item is (src_addrs, lengths, holder, should_release, is_fast_path, created_at)
|
||||
_, _, holder, should_release, _, _ = item
|
||||
if should_release and isinstance(holder, ManagedBuffer):
|
||||
@@ -904,6 +951,7 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
if should_release and isinstance(holder, ManagedBuffer):
|
||||
holder.release()
|
||||
self._local_buffers.clear()
|
||||
self._in_flight_buffers.clear()
|
||||
|
||||
# 5. Close thread-local cached REQ sockets (for the calling thread).
|
||||
# Sockets cached in *other* threads are not directly accessible here
|
||||
@@ -945,21 +993,26 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
# -------------------------------------------------------
|
||||
|
||||
def _cleanup_stale_buffers(self) -> None:
|
||||
"""Reclaim buffers older than ``_BUFFER_TTL_SECONDS``.
|
||||
"""Reclaim stale buffers not currently used by a transfer.
|
||||
|
||||
Prevents permanent memory leaks when a receiver crashes or times out
|
||||
without ever pulling the data.
|
||||
TODO(wzliu): In extreme rare case, long transfer time, there might exist
|
||||
TTL cleanup vs in-flight transfer conflict, which will be handled in the next PR.
|
||||
"""
|
||||
if self.buffer_ttl_seconds <= 0:
|
||||
return
|
||||
now = _time_mod.monotonic()
|
||||
with self._local_buffers_lock:
|
||||
stale_keys = [k for k, v in self._local_buffers.items() if now - v[5] > _BUFFER_TTL_SECONDS]
|
||||
stale_keys = [
|
||||
k
|
||||
for k, v in self._local_buffers.items()
|
||||
if k not in self._in_flight_buffers and now - v[5] > self.buffer_ttl_seconds
|
||||
]
|
||||
for k in stale_keys:
|
||||
item = self._local_buffers.pop(k)
|
||||
_, _, holder, should_release, _, _ = item
|
||||
if should_release and isinstance(holder, ManagedBuffer):
|
||||
holder.release()
|
||||
logger.warning(f"TTL expired ({_BUFFER_TTL_SECONDS}s): cleaned up stale buffer for {k}")
|
||||
logger.warning(f"TTL expired ({self.buffer_ttl_seconds}s): cleaned up stale buffer for {k}")
|
||||
|
||||
def _zmq_listener_loop(self):
|
||||
socket = self.zmq_ctx.socket(zmq.ROUTER)
|
||||
@@ -1058,30 +1111,36 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
|
||||
|
||||
with self._local_buffers_lock:
|
||||
item = self._local_buffers.get(meta.request_id)
|
||||
if item:
|
||||
self._in_flight_buffers.add(meta.request_id)
|
||||
|
||||
if not item:
|
||||
response_queue.put((identity, TRANS_ERROR))
|
||||
self._notify_listener(notify_addr)
|
||||
return
|
||||
|
||||
src_addrs, src_lengths, _, _, _, _ = item
|
||||
remote_session = f"{meta.remote_hostname}:{meta.remote_port}"
|
||||
try:
|
||||
src_addrs, src_lengths, _, _, _, _ = item
|
||||
remote_session = f"{meta.remote_hostname}:{meta.remote_port}"
|
||||
|
||||
# RDMA Write
|
||||
ret = self.engine.batch_transfer_sync_write(remote_session, src_addrs, meta.dst_addrs, src_lengths)
|
||||
# RDMA Write
|
||||
ret = self.engine.batch_transfer_sync_write(remote_session, src_addrs, meta.dst_addrs, src_lengths)
|
||||
|
||||
if ret == 0:
|
||||
self.cleanup(meta.request_id)
|
||||
response_queue.put((identity, TRANS_DONE))
|
||||
else:
|
||||
# Keep buffer in _local_buffers so receiver can retry on transient failures.
|
||||
# Buffer will be cleaned up when: (a) a retry succeeds, (b) close() is called,
|
||||
# or (c) a future TTL mechanism reclaims stale entries.
|
||||
logger.warning(
|
||||
f"RDMA write failed for {meta.request_id} to {remote_session} "
|
||||
f"(ret={ret}). Buffer retained for retry."
|
||||
)
|
||||
response_queue.put((identity, TRANS_ERROR))
|
||||
if ret == 0:
|
||||
self.cleanup(meta.request_id)
|
||||
response_queue.put((identity, TRANS_DONE))
|
||||
else:
|
||||
# Keep buffer in _local_buffers so receiver can retry on transient failures.
|
||||
# Buffer will be cleaned up when: (a) a retry succeeds, (b) close() is called,
|
||||
# or (c) TTL reclaims it after it is no longer in-flight.
|
||||
logger.warning(
|
||||
f"RDMA write failed for {meta.request_id} to {remote_session} "
|
||||
f"(ret={ret}). Buffer retained for retry."
|
||||
)
|
||||
response_queue.put((identity, TRANS_ERROR))
|
||||
finally:
|
||||
with self._local_buffers_lock:
|
||||
self._in_flight_buffers.discard(meta.request_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Push failed: {e}")
|
||||
|
||||
@@ -27,16 +27,11 @@ class SharedMemoryConnector(OmniConnectorBase):
|
||||
def __init__(self, config: dict[str, Any]):
|
||||
self.config = config
|
||||
self.stage_id = config.get("stage_id", -1)
|
||||
self.device = config.get("device", "cuda:0")
|
||||
self.threshold = int(config.get("shm_threshold_bytes", 65536))
|
||||
self.inline_small_payloads = bool(config.get("inline_small_payloads", False))
|
||||
self._pending_keys: set[str] = set()
|
||||
self._metrics = {
|
||||
"puts": 0,
|
||||
"gets": 0,
|
||||
"bytes_transferred": 0,
|
||||
"shm_writes": 0,
|
||||
"inline_writes": 0,
|
||||
}
|
||||
|
||||
def put(
|
||||
@@ -47,33 +42,18 @@ class SharedMemoryConnector(OmniConnectorBase):
|
||||
data: Any,
|
||||
) -> tuple[bool, int, dict[str, Any] | None]:
|
||||
try:
|
||||
# Always serialize first to check size (and for SHM writing)
|
||||
# Note: For extremely large objects in "inline" mode (e.g. Ray),
|
||||
# we might double-serialize if we're not careful, but here we assume
|
||||
# if it's huge we use SHM, or if Ray, threshold is maxsize.
|
||||
payload = self.serialize_obj(data)
|
||||
size = len(payload)
|
||||
|
||||
# The legacy async-chunk adapter transfers only the connector key
|
||||
# across stages; it cannot deliver inline metadata to the receiver.
|
||||
# Keep key-addressed SHM as the default and only inline payloads
|
||||
# when the caller explicitly enables the metadata-aware path.
|
||||
if size >= self.threshold or not self.inline_small_payloads:
|
||||
lock_file = f"/dev/shm/shm_{put_key}_lockfile.lock"
|
||||
with open(lock_file, "wb+") as lockf:
|
||||
fcntl.flock(lockf, fcntl.LOCK_EX)
|
||||
meta = shm_write_bytes(payload, name=put_key)
|
||||
fcntl.flock(lockf, fcntl.LOCK_UN)
|
||||
lock_file = f"/dev/shm/shm_{put_key}_lockfile.lock"
|
||||
with open(lock_file, "wb+") as lockf:
|
||||
fcntl.flock(lockf, fcntl.LOCK_EX)
|
||||
meta = shm_write_bytes(payload, name=put_key)
|
||||
fcntl.flock(lockf, fcntl.LOCK_UN)
|
||||
|
||||
# meta contains {'name': ..., 'size': ...}
|
||||
metadata = {"shm": meta, "size": size}
|
||||
self._pending_keys.add(put_key)
|
||||
self._metrics["shm_writes"] += 1
|
||||
else:
|
||||
# Inline small payloads to avoid the mmap + file-lock overhead
|
||||
# that dominates codec chunk transfers.
|
||||
metadata = {"inline_bytes": payload, "size": size}
|
||||
self._metrics["inline_writes"] += 1
|
||||
# meta contains {'name': ..., 'size': ...}
|
||||
metadata = {"shm": meta, "size": size}
|
||||
self._pending_keys.add(put_key)
|
||||
|
||||
self._metrics["puts"] += 1
|
||||
self._metrics["bytes_transferred"] += size
|
||||
@@ -84,22 +64,26 @@ class SharedMemoryConnector(OmniConnectorBase):
|
||||
logger.error(f"SharedMemoryConnector put failed for req {put_key}: {e}")
|
||||
return False, 0, None
|
||||
|
||||
def _get_data_with_lock(self, lock_file: str, shm_handle: dict):
|
||||
obj = None
|
||||
def _get_data_with_lock(self, lock_file: str, shm_handle: dict[str, Any]) -> tuple[Any, int] | None:
|
||||
deserialized = False
|
||||
try:
|
||||
with open(lock_file, "rb+") as lockf:
|
||||
fcntl.flock(lockf, fcntl.LOCK_EX)
|
||||
data_bytes = shm_read_bytes(shm_handle)
|
||||
fcntl.flock(lockf, fcntl.LOCK_UN)
|
||||
obj = self.deserialize_obj(data_bytes)
|
||||
return obj, int(shm_handle.get("size", 0))
|
||||
result = (obj, int(shm_handle.get("size", 0)))
|
||||
deserialized = True
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"SharedMemoryConnector shm get failed for req : {e}")
|
||||
return None
|
||||
finally:
|
||||
# If data has been received, delete lock_file.
|
||||
if obj and os.path.exists(lock_file):
|
||||
os.remove(lock_file)
|
||||
if deserialized:
|
||||
try:
|
||||
os.remove(lock_file)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def _get_by_key(self, get_key: str) -> tuple[Any, int] | None:
|
||||
"""Read a SHM segment addressed purely by *get_key*."""
|
||||
@@ -142,31 +126,21 @@ class SharedMemoryConnector(OmniConnectorBase):
|
||||
if isinstance(metadata, dict) and get_key in metadata:
|
||||
metadata = metadata.get(get_key)
|
||||
|
||||
if not isinstance(metadata, dict):
|
||||
return self._get_by_key(get_key)
|
||||
|
||||
if "inline_bytes" in metadata:
|
||||
try:
|
||||
obj = self.deserialize_obj(metadata["inline_bytes"])
|
||||
self._pending_keys.discard(get_key)
|
||||
return obj, int(metadata.get("size", 0))
|
||||
except Exception as e:
|
||||
logger.error(f"SharedMemoryConnector inline get failed for req {get_key}: {e}")
|
||||
return None
|
||||
|
||||
if "shm" in metadata:
|
||||
if isinstance(metadata, dict) and "shm" in metadata:
|
||||
shm_handle = metadata["shm"]
|
||||
lock_file = f"/dev/shm/shm_{shm_handle['name']}_lockfile.lock"
|
||||
result = self._get_data_with_lock(lock_file, shm_handle)
|
||||
if result is not None:
|
||||
self._pending_keys.discard(get_key)
|
||||
return result
|
||||
else:
|
||||
# Missing or non-SHM metadata falls back to key-based lookup.
|
||||
result = self._get_by_key(get_key)
|
||||
else:
|
||||
result = self._get_by_key(get_key)
|
||||
|
||||
# Metadata is a dict but has no SHM-specific handle (e.g. RDMA-
|
||||
# style source_host/source_port). Fall back to key-based read.
|
||||
return self._get_by_key(get_key)
|
||||
|
||||
return self._get_by_key(get_key)
|
||||
if result is not None:
|
||||
self._metrics["gets"] += 1
|
||||
return result
|
||||
|
||||
def cleanup(self, request_id: str) -> None:
|
||||
"""Best-effort cleanup of unconsumed SHM segments for *request_id*.
|
||||
@@ -217,4 +191,4 @@ class SharedMemoryConnector(OmniConnectorBase):
|
||||
self._pending_keys.clear()
|
||||
|
||||
def health(self) -> dict[str, Any]:
|
||||
return {"status": "healthy", "threshold": self.threshold, **self._metrics}
|
||||
return {"status": "healthy", **self._metrics}
|
||||
@@ -4,7 +4,6 @@
|
||||
"""Utilities for OmniConnector configuration and validation."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@@ -42,7 +41,6 @@ KV_REPLICA_PORT_STRIDE = 1024
|
||||
|
||||
def initialize_connectors_from_config(
|
||||
config_path: str | Path | None = None,
|
||||
default_shm_threshold: int = 65536,
|
||||
purpose: str = "request_forwarding",
|
||||
caller_stage_id: int | str | None = None,
|
||||
is_sender: bool | None = None,
|
||||
@@ -53,7 +51,7 @@ def initialize_connectors_from_config(
|
||||
Returns:
|
||||
tuple: (OmniTransferConfig, dict of {(from, to): connector_instance})
|
||||
"""
|
||||
transfer_config = load_omni_transfer_config(config_path, default_shm_threshold=default_shm_threshold)
|
||||
transfer_config = load_omni_transfer_config(config_path)
|
||||
|
||||
if not transfer_config:
|
||||
logger.info("No OmniTransferConfig provided")
|
||||
@@ -190,7 +188,6 @@ def get_connectors_config_for_stage(transfer_config: OmniTransferConfig | None,
|
||||
def load_omni_transfer_config(
|
||||
config_path: str | Path | None = None,
|
||||
config_dict: dict[str, Any] | None = None,
|
||||
default_shm_threshold: int = 65536,
|
||||
) -> OmniTransferConfig | None:
|
||||
"""Load OmniTransferConfig from file or dict."""
|
||||
if config_path is None and config_dict is None:
|
||||
@@ -325,10 +322,7 @@ def load_omni_transfer_config(
|
||||
expected_edges.add(edge_key)
|
||||
if edge_key not in connectors:
|
||||
logger.info(f"Auto-configuring SharedMemoryConnector for edge {edge_key}")
|
||||
connectors[edge_key] = ConnectorSpec(
|
||||
name="SharedMemoryConnector",
|
||||
extra={"shm_threshold_bytes": default_shm_threshold},
|
||||
)
|
||||
connectors[edge_key] = ConnectorSpec(name="SharedMemoryConnector")
|
||||
|
||||
# Fallback: infer edges from engine_input_source for each stage
|
||||
for stage_config in stage_args:
|
||||
@@ -343,9 +337,7 @@ def load_omni_transfer_config(
|
||||
|
||||
if edge_key not in connectors:
|
||||
logger.info(f"Auto-configuring SharedMemoryConnector for edge {edge_key}")
|
||||
connectors[edge_key] = ConnectorSpec(
|
||||
name="SharedMemoryConnector", extra={"shm_threshold_bytes": default_shm_threshold}
|
||||
)
|
||||
connectors[edge_key] = ConnectorSpec(name="SharedMemoryConnector")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to auto-configure SHM connectors: {e}")
|
||||
@@ -369,22 +361,16 @@ def load_omni_transfer_config(
|
||||
|
||||
|
||||
def initialize_orchestrator_connectors(
|
||||
config_path: str | None, worker_backend: str | None = "multi_process", shm_threshold_bytes: int = 65536
|
||||
config_path: str | None,
|
||||
) -> tuple[OmniTransferConfig | None, dict[tuple[str, str], OmniConnectorBase]]:
|
||||
"""Initialize connectors shared at orchestrator level.
|
||||
Args:
|
||||
config_path: The path to the configuration file.
|
||||
worker_backend: The backend to use for the worker.
|
||||
Returns:
|
||||
A tuple containing the OmniTransferConfig and a dictionary of connectors.
|
||||
"""
|
||||
if worker_backend == "ray":
|
||||
default_shm_threshold = sys.maxsize
|
||||
else:
|
||||
default_shm_threshold = max(0, shm_threshold_bytes)
|
||||
transfer_config, connectors = initialize_connectors_from_config(
|
||||
config_path,
|
||||
default_shm_threshold=default_shm_threshold,
|
||||
purpose="request_forwarding",
|
||||
caller_stage_id="orchestrator",
|
||||
is_sender=True,
|
||||
|
||||
@@ -423,7 +423,6 @@ class OrchestratorArgs:
|
||||
init_timeout: int = 600
|
||||
|
||||
# === Cross-stage Communication ===
|
||||
shm_threshold_bytes: int = 65536
|
||||
batch_timeout: int = 10
|
||||
|
||||
# === Cluster / Backend ===
|
||||
|
||||
@@ -93,7 +93,6 @@ runtime:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
|
||||
@@ -323,7 +323,6 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
}
|
||||
if getattr(self.model_config, "model_arch", None) in _OMNI_CONNECTOR_INIT_ARCHS:
|
||||
self.init_omni_connectors(
|
||||
vllm_config=self.vllm_config,
|
||||
model_config=self.model_config,
|
||||
kv_transfer_manager=self.kv_transfer_manager,
|
||||
)
|
||||
|
||||
@@ -70,7 +70,6 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
|
||||
}
|
||||
if getattr(self.model_config, "model_arch", None) in _OMNI_CONNECTOR_INIT_ARCHS:
|
||||
self.init_omni_connectors(
|
||||
vllm_config=self.vllm_config,
|
||||
model_config=self.model_config,
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import inspect
|
||||
import os
|
||||
import threading
|
||||
from collections import defaultdict, deque
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import torch
|
||||
@@ -36,6 +35,7 @@ from vllm_omni.worker.payload_span import (
|
||||
_EMBED_SPAN_GROUPS: tuple[tuple[str, str, str], ...] = (("decode", "decode_token_start", "decode_token_end"),)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.config.model import OmniModelConfig
|
||||
from vllm_omni.distributed.omni_connectors.connectors.base import (
|
||||
OmniConnectorBase,
|
||||
)
|
||||
@@ -87,14 +87,12 @@ class OmniConnectorModelRunnerMixin:
|
||||
|
||||
def init_omni_connectors(
|
||||
self,
|
||||
vllm_config: Any,
|
||||
model_config: Any,
|
||||
model_config: OmniModelConfig,
|
||||
kv_transfer_manager: OmniKVTransferManager | None = None,
|
||||
) -> None:
|
||||
"""Initialize connectors and background threads.
|
||||
|
||||
Args:
|
||||
vllm_config: Full vLLM config object.
|
||||
model_config: Stage-level model config with connector settings.
|
||||
kv_transfer_manager: Existing KV transfer manager to delegate to.
|
||||
"""
|
||||
@@ -1024,31 +1022,6 @@ class OmniConnectorModelRunnerMixin:
|
||||
self._work_available.set()
|
||||
return sent_ids
|
||||
|
||||
def recv_stage_inputs(self, scheduler_output: Any) -> dict[str, Any] | None:
|
||||
"""Compatibility wrapper for ``recv_full_payload_inputs``."""
|
||||
return self.recv_full_payload_inputs(scheduler_output)
|
||||
|
||||
def accumulate_batch_output(
|
||||
self,
|
||||
req_id: str,
|
||||
pooler_output: Any,
|
||||
request: Any,
|
||||
) -> None:
|
||||
"""Compatibility wrapper for ``accumulate_full_payload_output``."""
|
||||
self.accumulate_full_payload_output(req_id, pooler_output, request)
|
||||
|
||||
def flush_batch_outputs(self, finished_req_ids: set[str]) -> None:
|
||||
"""Compatibility wrapper for ``flush_full_payload_outputs``."""
|
||||
self.flush_full_payload_outputs(finished_req_ids)
|
||||
|
||||
def send_stage_outputs(
|
||||
self,
|
||||
scheduler_output: Any,
|
||||
outputs: dict[str, tuple[Any, Any] | Any],
|
||||
) -> list[str]:
|
||||
"""Compatibility wrapper for ``send_full_payload_outputs``."""
|
||||
return self.send_full_payload_outputs(scheduler_output, outputs)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Streaming chunk mode (recv_chunk / send_chunk)
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -1505,20 +1478,6 @@ class OmniConnectorModelRunnerMixin:
|
||||
# Output aggregation
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _empty_output_with_connector_signals(self) -> Any:
|
||||
"""Return a minimal ModelRunnerOutput carrying pending connector signals.
|
||||
|
||||
Used by early-return paths (e.g. ``num_scheduled_tokens == 0``)
|
||||
that still need to deliver ``omni_connector_output`` to the
|
||||
Scheduler so that WAITING_FOR_INPUT / WAITING_FOR_CHUNK
|
||||
transitions are not lost.
|
||||
"""
|
||||
from vllm_omni.outputs import OmniModelRunnerOutput
|
||||
|
||||
output = OmniModelRunnerOutput(req_ids=[], req_id_to_index={})
|
||||
output.omni_connector_output = self.get_omni_connector_output()
|
||||
return output
|
||||
|
||||
def get_omni_connector_output(self) -> OmniConnectorOutput:
|
||||
"""Collect and reset transfer results for this execute_model cycle.
|
||||
|
||||
@@ -2088,48 +2047,6 @@ class OmniConnectorModelRunnerMixin:
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@staticmethod
|
||||
def _freeze_request_attr(value: Any) -> Any:
|
||||
if isinstance(value, list):
|
||||
return list(value)
|
||||
if isinstance(value, tuple):
|
||||
return list(value)
|
||||
if isinstance(value, torch.Tensor):
|
||||
return value.clone()
|
||||
raw_list = getattr(value, "_x", None)
|
||||
if raw_list is not None:
|
||||
return list(raw_list)
|
||||
return value
|
||||
|
||||
def _snapshot_request_for_send(self, request: Any, external_req_id: str) -> Any:
|
||||
finished = bool(getattr(request, "is_finished", lambda: False)())
|
||||
attrs: dict[str, Any] = {}
|
||||
try:
|
||||
attrs.update(vars(request))
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
for name in (
|
||||
"request_id",
|
||||
"req_id",
|
||||
"external_req_id",
|
||||
"prompt_token_ids",
|
||||
"output_token_ids",
|
||||
"all_token_ids",
|
||||
"additional_information",
|
||||
"sampling_params",
|
||||
"multi_modal_data",
|
||||
"mm_hashes",
|
||||
):
|
||||
if hasattr(request, name):
|
||||
attrs[name] = self._freeze_request_attr(getattr(request, name))
|
||||
|
||||
attrs["external_req_id"] = external_req_id
|
||||
attrs["_frozen_is_finished"] = finished
|
||||
snapshot = SimpleNamespace(**attrs)
|
||||
snapshot.is_finished = lambda: finished
|
||||
return snapshot
|
||||
|
||||
@staticmethod
|
||||
def _create_connector(model_config: Any) -> OmniConnectorBase | None:
|
||||
"""Create a connector from model_config, or None if unconfigured."""
|
||||
|
||||
Reference in new issue
Block a user