Skip to content

Commit 8e43d47

Browse files
committed
fix(specdec): harden multimodal media delivery
Signed-off-by: Slawomir Kierat <skierat@nvidia.com>
1 parent 5683e84 commit 8e43d47

5 files changed

Lines changed: 193 additions & 8 deletions

File tree

examples/speculative_decoding/distributed_generate/launch_multimodal.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ fi
6363

6464
mkdir -p "$OUTPUT_PATH"
6565

66-
CONTAINER_IMAGE=${CONTAINER_IMAGE:-/lustre/fs1/portfolios/coreai/users/skierat/lmsysorg+sglang+v0.5.3-cu129.sqsh}
66+
# Set CONTAINER_IMAGE to a local .sqsh image to avoid pulling from the registry.
67+
DEFAULT_CONTAINER_IMAGE="lmsysorg/sglang:v0.5.3-cu129"
68+
CONTAINER_IMAGE="${CONTAINER_IMAGE:-$DEFAULT_CONTAINER_IMAGE}"
6769

6870
counter=$START_SHARD
6971
worker_pids=()

examples/speculative_decoding/distributed_generate/server_generate_vlm_sglang.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import tqdm
3232

3333
QWEN_IMAGE_TOKEN = "<|vision_start|><|image_pad|><|vision_end|>"
34+
_UNRESOLVED_MEDIA_PATHS: set[str] = set()
3435

3536

3637
def _load_json_or_jsonl(path: str) -> list[dict[str, Any]]:
@@ -101,6 +102,8 @@ def _resolve_media_path(
101102
) -> str | None:
102103
if not path:
103104
return None
105+
if path.startswith(("http://", "https://", "data:")):
106+
return path
104107
candidate = Path(path)
105108
if candidate.is_absolute() and candidate.exists():
106109
return str(candidate)
@@ -121,7 +124,10 @@ def _resolve_media_path(
121124
return str(rooted)
122125
if candidate.exists():
123126
return str(candidate)
124-
return path
127+
if path not in _UNRESOLVED_MEDIA_PATHS:
128+
print(f"WARNING: could not resolve media path: {path}")
129+
_UNRESOLVED_MEDIA_PATHS.add(path)
130+
return None
125131

126132

127133
def _as_openai_media_value(
@@ -132,9 +138,16 @@ def _as_openai_media_value(
132138
if media_url_base:
133139
candidate = Path(path)
134140
if candidate.is_absolute():
135-
return f"{media_url_base.rstrip('/')}{quote(str(candidate), safe='/')}"
136-
if not candidate.is_absolute():
137-
return f"{media_url_base.rstrip('/')}/{quote(path, safe='/')}"
141+
if media_root:
142+
try:
143+
path = str(candidate.relative_to(media_root))
144+
except ValueError:
145+
# The local HTTP server deliberately exposes only media_root.
146+
# Leave paths outside it local for SGLang to resolve directly.
147+
return path
148+
else:
149+
return f"{media_url_base.rstrip('/')}{quote(path, safe='/')}"
150+
return f"{media_url_base.rstrip('/')}/{quote(path, safe='/')}"
138151
# Do not convert local paths to file://. This SGLang build falls through to
139152
# the base64 loader for file:// videos and raises "Incorrect padding".
140153
return path

examples/speculative_decoding/distributed_generate/worker_multimodal.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,19 +91,19 @@ trap 'exit 130' INT
9191
trap 'exit 143' TERM
9292

9393
if [ "$API_MODE" = "openai" ]; then
94-
python3 -m http.server "$MEDIA_HTTP_PORT" --bind 127.0.0.1 --directory / \
94+
python3 -m http.server "$MEDIA_HTTP_PORT" --bind 127.0.0.1 --directory /media_data \
9595
>/tmp/multimodal_media_http_${MEDIA_HTTP_PORT}.log 2>&1 &
9696
MEDIA_HTTP_PID=$!
9797

9898
echo "Waiting for media HTTP server at $MEDIA_URL_BASE..."
9999
for _ in $(seq 1 30); do
100-
if curl -fsS "$MEDIA_URL_BASE/media_data/" >/dev/null 2>&1; then
100+
if curl -fsS "$MEDIA_URL_BASE/" >/dev/null 2>&1; then
101101
echo "Media HTTP server is up."
102102
break
103103
fi
104104
sleep 1
105105
done
106-
if ! curl -fsS "$MEDIA_URL_BASE/media_data/" >/dev/null 2>&1; then
106+
if ! curl -fsS "$MEDIA_URL_BASE/" >/dev/null 2>&1; then
107107
echo "ERROR: media HTTP server did not start. See /tmp/multimodal_media_http_${MEDIA_HTTP_PORT}.log" >&2
108108
exit 1
109109
fi
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Tests for the DFlash JSONL merger recipe."""
17+
18+
import json
19+
import subprocess
20+
import sys
21+
from pathlib import Path
22+
23+
REPO_ROOT = Path(__file__).parents[3]
24+
MERGER = REPO_ROOT / "examples/speculative_decoding/recipes/merge_dflash_datasets.py"
25+
26+
27+
def write_record(path: Path, record_id: str, prompt: str, answer: str) -> None:
28+
"""Write a one-turn DFlash-training conversation."""
29+
30+
path.write_text(
31+
json.dumps(
32+
{
33+
"id": record_id,
34+
"messages": [
35+
{"role": "user", "content": prompt},
36+
{"role": "assistant", "content": answer},
37+
],
38+
}
39+
)
40+
+ "\n",
41+
encoding="utf-8",
42+
)
43+
44+
45+
def test_merge_keeps_synthetic_variants_together(tmp_path):
46+
"""Single and parallel merges deduplicate matching variants from one synthetic shard."""
47+
48+
synthetic_output = tmp_path / "synthetic-output"
49+
synthetic_output.mkdir()
50+
write_record(
51+
synthetic_output / "output-00000-00000-temp-0.0.jsonl",
52+
"duplicate-first",
53+
"Describe the image.",
54+
"The image shows a blue square.",
55+
)
56+
write_record(
57+
synthetic_output / "output-00000-00000-temp-0.1.jsonl",
58+
"duplicate-second",
59+
"Describe the image.",
60+
"The image shows a blue square.",
61+
)
62+
write_record(
63+
synthetic_output / "output-00001-00001-temp-0.0.jsonl",
64+
"synthetic-unique",
65+
"Describe the video.",
66+
"The video shows a red circle.",
67+
)
68+
curated_text = tmp_path / "curated-text.jsonl"
69+
write_record(curated_text, "curated-unique", "What is two plus two?", "Four.")
70+
expected_ids = {"duplicate-first", "synthetic-unique", "curated-unique"}
71+
for jobs in (1, 2):
72+
output = tmp_path / f"merged-{jobs}.jsonl"
73+
subprocess.run(
74+
[
75+
sys.executable,
76+
str(MERGER),
77+
"--source",
78+
f"synthetic={synthetic_output}",
79+
"--source",
80+
f"curated={curated_text}",
81+
"--output",
82+
str(output),
83+
"--jobs",
84+
str(jobs),
85+
],
86+
check=True,
87+
capture_output=True,
88+
text=True,
89+
)
90+
91+
records = [json.loads(line) for line in output.read_text(encoding="utf-8").splitlines()]
92+
assert {record["id"] for record in records} == expected_ids
93+
assert not list(tmp_path.glob(f".{output.name}.parallel-*"))
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Tests for the multimodal SGLang generation client."""
17+
18+
import importlib.util
19+
from pathlib import Path
20+
21+
_SCRIPT_PATH = (
22+
Path(__file__).parents[3]
23+
/ "examples/speculative_decoding/distributed_generate/server_generate_vlm_sglang.py"
24+
)
25+
_spec = importlib.util.spec_from_file_location("server_generate_vlm_sglang", _SCRIPT_PATH)
26+
assert _spec is not None and _spec.loader is not None
27+
server_generate_vlm_sglang = importlib.util.module_from_spec(_spec)
28+
_spec.loader.exec_module(server_generate_vlm_sglang)
29+
30+
31+
def test_resolve_media_path_supports_local_and_remote_media(tmp_path):
32+
"""Existing local media and supported remote media remain usable."""
33+
34+
local_media = tmp_path / "image.jpg"
35+
local_media.touch()
36+
remote_media = "https://example.com/image.jpg"
37+
38+
assert server_generate_vlm_sglang._resolve_media_path("image.jpg", str(tmp_path), None) == str(
39+
local_media
40+
)
41+
assert (
42+
server_generate_vlm_sglang._resolve_media_path(remote_media, str(tmp_path), None)
43+
== remote_media
44+
)
45+
46+
47+
def test_resolve_media_path_returns_none_and_warns_once_for_missing_media(monkeypatch, capsys):
48+
"""Missing local media can fall back to video or be skipped by the caller."""
49+
50+
monkeypatch.setattr(server_generate_vlm_sglang, "_UNRESOLVED_MEDIA_PATHS", set())
51+
52+
assert server_generate_vlm_sglang._resolve_media_path("missing.mp4", None, None) is None
53+
assert server_generate_vlm_sglang._resolve_media_path("missing.mp4", None, None) is None
54+
55+
assert capsys.readouterr().out == "WARNING: could not resolve media path: missing.mp4\n"
56+
57+
58+
def test_openai_media_value_is_relative_to_the_media_root(tmp_path):
59+
"""The local HTTP server exposes media files, not the container root."""
60+
61+
media_root = tmp_path / "media"
62+
media_path = media_root / "videos" / "clip.mp4"
63+
media_path.parent.mkdir(parents=True)
64+
media_path.touch()
65+
input_path = tmp_path / "input" / "private.json"
66+
input_path.parent.mkdir()
67+
input_path.touch()
68+
69+
assert (
70+
server_generate_vlm_sglang._as_openai_media_value(
71+
str(media_path), "http://127.0.0.1:18080", str(media_root), None
72+
)
73+
== "http://127.0.0.1:18080/videos/clip.mp4"
74+
)
75+
assert server_generate_vlm_sglang._as_openai_media_value(
76+
str(input_path), "http://127.0.0.1:18080", str(media_root), None
77+
) == str(input_path)

0 commit comments

Comments
 (0)