-
Notifications
You must be signed in to change notification settings - Fork 128
feat(multimodal): wire vLLM RDMA multimodal tensor pull #1916
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
21a18a0
fd8a446
7031717
7374bfc
928e686
294608d
e0e4cf4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,108 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| """vLLM RDMA multimodal transport E2E test. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Exercises the full RDMA (NIXL) pixel lane end to end: the gateway stages | ||||||||||||||||||||||||||||||||||||||||||||||
| ``pixel_values`` into a pre-registered arena and the vLLM worker pulls them with a | ||||||||||||||||||||||||||||||||||||||||||||||
| one-sided READ instead of receiving them inline. Verifies both that the model | ||||||||||||||||||||||||||||||||||||||||||||||
| still understands the image over RDMA AND that the pixels actually travelled the | ||||||||||||||||||||||||||||||||||||||||||||||
| remote path (``smg_mm_tensor_bytes_total{path="remote"}`` grew), so a silent | ||||||||||||||||||||||||||||||||||||||||||||||
| fall-back to inline fails the test rather than passing on correctness alone. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Requires a gateway built with ``--features mm-rdma`` and a NIXL/UCX-capable host. | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Restore explicit opt-in gating for this RDMA-only test. Removing the 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Usage: | ||||||||||||||||||||||||||||||||||||||||||||||
| pytest e2e_test/chat_completions/test_multimodal_rdma.py -v | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import base64 | ||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import httpx | ||||||||||||||||||||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "images" | ||||||||||||||||||||||||||||||||||||||||||||||
| DOG_IMAGE_PATH = FIXTURES_DIR / "dog.jpg" # Black labrador puppy | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def _image_to_base64_url(path: Path) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||
| data = base64.b64encode(path.read_bytes()).decode("utf-8") | ||||||||||||||||||||||||||||||||||||||||||||||
| return f"data:image/jpeg;base64,{data}" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def _remote_pixel_bytes(metrics_url: str) -> float: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Sum ``smg_mm_tensor_bytes_total`` samples on the RDMA (remote) path.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| resp = httpx.get(f"{metrics_url}/metrics", timeout=10) | ||||||||||||||||||||||||||||||||||||||||||||||
| resp.raise_for_status() | ||||||||||||||||||||||||||||||||||||||||||||||
| total = 0.0 | ||||||||||||||||||||||||||||||||||||||||||||||
| for line in resp.text.splitlines(): | ||||||||||||||||||||||||||||||||||||||||||||||
| if line.startswith("smg_mm_tensor_bytes_total") and 'path="remote"' in line: | ||||||||||||||||||||||||||||||||||||||||||||||
| total += float(line.rsplit(" ", 1)[1]) | ||||||||||||||||||||||||||||||||||||||||||||||
| return total | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win Harden Manual line-splitting assumes each matching sample line has no trailing timestamp field, and the response is never checked for HTTP errors — an unreachable ♻️ Proposed fix using prometheus_client's parser def _remote_pixel_bytes(metrics_url: str) -> float:
"""Sum ``smg_mm_tensor_bytes_total`` samples on the RDMA (remote) path."""
- text = httpx.get(f"{metrics_url}/metrics", timeout=10).text
- total = 0.0
- for line in text.splitlines():
- if line.startswith("smg_mm_tensor_bytes_total") and 'path="remote"' in line:
- total += float(line.rsplit(" ", 1)[1])
- return total
+ from prometheus_client.parser import text_string_to_metric_families
+
+ response = httpx.get(f"{metrics_url}/metrics", timeout=10)
+ response.raise_for_status()
+ total = 0.0
+ for family in text_string_to_metric_families(response.text):
+ if family.name != "smg_mm_tensor_bytes_total":
+ continue
+ for sample in family.samples:
+ if sample.labels.get("path") == "remote":
+ total += sample.value
+ return totalConfirmed via web search that 📝 Committable suggestion
Suggested change
🧰 Tools🪛 ast-grep (0.44.1)[warning] 47-47: Request-controlled URL passed to httpx; validate against an allowlist to prevent SSRF. (avoid-ssrf) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.fixture(scope="class", autouse=True) | ||||||||||||||||||||||||||||||||||||||||||||||
| def _rdma_env(): | ||||||||||||||||||||||||||||||||||||||||||||||
| """Turn the RDMA lane on for both the gateway and the vLLM worker, which | ||||||||||||||||||||||||||||||||||||||||||||||
| inherit this process's environment when launched locally. Restored on teardown. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| - ``SMG_MM_TENSOR_TRANSPORT=rdma``: the first-class transport switch (also set | ||||||||||||||||||||||||||||||||||||||||||||||
| as a gateway CLI flag) that enables the worker-side puller. | ||||||||||||||||||||||||||||||||||||||||||||||
| - ``SMG_MM_PIXEL_RDMA=1``: the legacy puller switch, belt-and-suspenders. | ||||||||||||||||||||||||||||||||||||||||||||||
| - ``SMG_RDMA_LISTEN_IP=127.0.0.1``: the gateway's NIXL listener (loopback). | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| with pytest.MonkeyPatch.context() as mp: | ||||||||||||||||||||||||||||||||||||||||||||||
| mp.setenv("SMG_MM_TENSOR_TRANSPORT", "rdma") | ||||||||||||||||||||||||||||||||||||||||||||||
| mp.setenv("SMG_MM_PIXEL_RDMA", "1") | ||||||||||||||||||||||||||||||||||||||||||||||
| mp.setenv("SMG_RDMA_LISTEN_IP", "127.0.0.1") | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+58
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this opt-in test runs after another Qwen3-VL gRPC class in the same pytest session, Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||
| yield | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.engine("vllm") | ||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.gpu(1) | ||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.e2e | ||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.model("Qwen/Qwen3-VL-8B-Instruct") | ||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.gateway(extra_args=["--multimodal-tensor-transport", "rdma"]) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This new vLLM/gpu1/e2e test lives under Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.parametrize("setup_backend", ["grpc"], indirect=True) | ||||||||||||||||||||||||||||||||||||||||||||||
| class TestMultimodalRdmaQwen3VL: | ||||||||||||||||||||||||||||||||||||||||||||||
| """vLLM multimodal over the RDMA pixel lane via gRPC.""" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def test_single_image_uses_rdma(self, model, setup_backend): | ||||||||||||||||||||||||||||||||||||||||||||||
| _, _, client, gateway = setup_backend | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| response = client.chat.completions.create( | ||||||||||||||||||||||||||||||||||||||||||||||
| model=model, | ||||||||||||||||||||||||||||||||||||||||||||||
| messages=[ | ||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||
| "role": "user", | ||||||||||||||||||||||||||||||||||||||||||||||
| "content": [ | ||||||||||||||||||||||||||||||||||||||||||||||
| {"type": "text", "text": "What animal is in this image?"}, | ||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||
| "type": "image_url", | ||||||||||||||||||||||||||||||||||||||||||||||
| "image_url": {"url": _image_to_base64_url(DOG_IMAGE_PATH)}, | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||
| temperature=0, | ||||||||||||||||||||||||||||||||||||||||||||||
| max_tokens=100, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| text = response.choices[0].message.content | ||||||||||||||||||||||||||||||||||||||||||||||
| assert any(k in text.lower() for k in ["dog", "puppy", "labrador"]), ( | ||||||||||||||||||||||||||||||||||||||||||||||
| f"Expected dog-related content over the RDMA transport, got: {text}" | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("RDMA multimodal response: %s", text) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Critical: the request must have actually used the remote (RDMA) path. | ||||||||||||||||||||||||||||||||||||||||||||||
| # A silent inline fall-back would still answer correctly, so correctness | ||||||||||||||||||||||||||||||||||||||||||||||
| # alone does not prove the lane worked — the transport metric does. | ||||||||||||||||||||||||||||||||||||||||||||||
| remote_bytes = _remote_pixel_bytes(gateway.metrics_url) | ||||||||||||||||||||||||||||||||||||||||||||||
| assert remote_bytes > 0, ( | ||||||||||||||||||||||||||||||||||||||||||||||
| 'expected smg_mm_tensor_bytes_total{path="remote"} > 0; the pixels ' | ||||||||||||||||||||||||||||||||||||||||||||||
| "silently fell back to inline (RDMA lane not exercised)" | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("RDMA remote pixel bytes: %s", remote_bytes) | ||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,14 @@ | |
| _LOCAL_IP_CACHE: dict[str, bool] = {} | ||
|
|
||
|
|
||
| def _rdma_enabled_from_env() -> bool: | ||
| """Whether the RDMA lane is on: the first-class `SMG_MM_TENSOR_TRANSPORT=rdma` | ||
| (matching the gateway) or the legacy `SMG_MM_PIXEL_RDMA` flag.""" | ||
| if os.environ.get("SMG_MM_PIXEL_RDMA") in ("1", "true"): | ||
| return True | ||
| return os.environ.get("SMG_MM_TENSOR_TRANSPORT", "").strip().lower() == "rdma" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a deployment enables RDMA with only the supported deprecated alias Useful? React with 👍 / 👎. |
||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class _RemotePixelDescriptor: | ||
| remote_addr: int | ||
|
|
@@ -121,7 +129,7 @@ def __init__( | |
| self._landing_slot_bytes = 0 | ||
| self._landing_free = None | ||
|
|
||
| if os.environ.get("SMG_MM_PIXEL_RDMA") not in ("1", "true"): | ||
| if not _rdma_enabled_from_env(): | ||
| return | ||
|
|
||
| try: | ||
|
|
@@ -169,6 +177,11 @@ def __init__( | |
| self._nixl_agent = None | ||
| self._landing_free = None | ||
|
|
||
| @property | ||
| def ready(self) -> bool: | ||
| """True once the NIXL agent + landing pool initialized (RDMA pulls possible).""" | ||
| return self._nixl_agent is not None and self._landing_free is not None | ||
|
|
||
| def _ensure_remote_ready(self, ip: str, port: int, remote, room: int) -> None: | ||
| """One-time metadata handshake per gateway listener.""" | ||
| key = (ip, port) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For PRs that touch only the RDMA implementation (for example
crates/mm_rdma/**) or the newscripts/ci_build_wheel_mm_rdma.sh, thedetect-changesfilters in this workflow set neithercommonnorchat-completionsbecause those paths are not listed there. This new job and thee2e-1gpu-mm-rdmajob therefore skip exactly the mm-rdma wheel/e2e validation those changes need, so a broken RDMA crate or build script can merge without exercising the dedicated lane; include these paths in the gate or key it offrust-cias well.Useful? React with 👍 / 👎.