Skip to content

Commit b3f617f

Browse files
nashjojokaixiang.mkx
andauthored
fix: defer optional vLLM imports for Tinker clients (#631)
Co-authored-by: kaixiang.mkx <kaixiang.mkx@alibaba-inc.com>
1 parent 382dfc6 commit b3f617f

3 files changed

Lines changed: 154 additions & 9 deletions

File tree

‎examples/learn_to_ask/data_prepare/3_rollout_then_evaluate.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"prompt_learn2ask",
2121
os.path.join(os.path.dirname(__file__), "..", "workflow", "prompt_learn2ask.py"),
2222
)
23+
if spec is None or spec.loader is None:
24+
raise ImportError("Cannot load the learn-to-ask prompt module.")
2325
prompt_module = importlib.util.module_from_spec(spec)
2426
spec.loader.exec_module(prompt_module)
2527

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""CPU regressions for optional vLLM imports and parser delegation."""
2+
3+
import importlib.util
4+
import subprocess
5+
import sys
6+
import textwrap
7+
import unittest
8+
from pathlib import Path
9+
from types import ModuleType
10+
from unittest.mock import AsyncMock, MagicMock, patch
11+
12+
from trinity.common.models.mm_utils import vLLMMultiModalRender
13+
14+
_NO_VLLM = """
15+
import builtins
16+
original_import = builtins.__import__
17+
def without_vllm(name, *args, **kwargs):
18+
if name == "vllm" or name.startswith("vllm."):
19+
raise ModuleNotFoundError("No module named 'vllm'", name="vllm")
20+
return original_import(name, *args, **kwargs)
21+
builtins.__import__ = without_vllm
22+
"""
23+
24+
25+
class OptionalVLLMImportTest(unittest.TestCase):
26+
def run_without_vllm(self, script):
27+
result = subprocess.run(
28+
[sys.executable, "-c", _NO_VLLM + textwrap.dedent(script)],
29+
cwd=Path(__file__).resolve().parents[3],
30+
capture_output=True,
31+
text=True,
32+
timeout=45,
33+
)
34+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
35+
36+
def test_text_helpers_work_without_vllm(self):
37+
self.run_without_vllm(
38+
"""
39+
from trinity.common.models.mm_utils import build_mm_message, has_multi_modal_content
40+
message = build_mm_message("hello", [], [])
41+
assert message == {"role": "user", "content": "hello"}
42+
assert not has_multi_modal_content([message])
43+
"""
44+
)
45+
46+
@unittest.skipUnless(importlib.util.find_spec("tinker"), "Tinker SDK is not installed")
47+
def test_tinker_model_imports_without_vllm(self):
48+
self.run_without_vllm(
49+
"""
50+
from trinity.common.models.tinker_model import TinkerModel
51+
assert TinkerModel.__name__ == "TinkerModel"
52+
"""
53+
)
54+
55+
def test_vllm_renderer_reports_missing_optional_dependency(self):
56+
self.run_without_vllm(
57+
"""
58+
from trinity.common.models.mm_utils import vLLMMultiModalRender
59+
try:
60+
vLLMMultiModalRender("unused-model-path")
61+
except ImportError as error:
62+
assert "vLLMMultiModalRender" in str(error)
63+
assert "'vllm' extra" in str(error)
64+
assert isinstance(error.__cause__, ModuleNotFoundError)
65+
else:
66+
raise AssertionError("Renderer must require vLLM")
67+
"""
68+
)
69+
70+
def test_renderer_preserves_unrelated_import_failures(self):
71+
original_import = __import__
72+
73+
def broken_dependency(name, *args, **kwargs):
74+
if name == "vllm.config":
75+
raise ModuleNotFoundError(
76+
"No module named 'some_dependency'", name="some_dependency"
77+
)
78+
return original_import(name, *args, **kwargs)
79+
80+
with patch("builtins.__import__", side_effect=broken_dependency):
81+
with self.assertRaisesRegex(ModuleNotFoundError, "some_dependency"):
82+
vLLMMultiModalRender("unused-model-path")
83+
84+
85+
class VLLMParserDelegationTest(unittest.IsolatedAsyncioTestCase):
86+
async def test_sync_and_async_parsers_receive_normalized_messages(self):
87+
# No model load or media download: preserve the renderer/parser boundary.
88+
renderer = vLLMMultiModalRender.__new__(vLLMMultiModalRender)
89+
renderer.model_config = object()
90+
renderer.mm_processor_kwargs = {"example_option": True}
91+
renderer._media_connector_config = {"media_io_kwargs": {"image": {"timeout": 3}}}
92+
messages = [
93+
{"role": "user", "content": [{"type": "image", "image": "https://example.org/img.png"}]}
94+
]
95+
normalized = [
96+
{
97+
"role": "user",
98+
"content": [
99+
{"type": "image_url", "image_url": {"url": "https://example.org/img.png"}}
100+
],
101+
}
102+
]
103+
conversation, media = [{"role": "user", "content": "image"}], {"image": object()}
104+
chat_utils = ModuleType("vllm.entrypoints.chat_utils")
105+
sync_parser = MagicMock(return_value=(conversation, media, None))
106+
async_parser = AsyncMock(return_value=(conversation, media, None))
107+
chat_utils.parse_chat_messages = sync_parser
108+
chat_utils.parse_chat_messages_async = async_parser
109+
with patch.dict(sys.modules, {"vllm.entrypoints.chat_utils": chat_utils}):
110+
self.assertEqual(
111+
renderer.process_messages(messages, content_format="openai"), (conversation, media)
112+
)
113+
self.assertEqual(
114+
await renderer.process_messages_async(messages, content_format="openai"),
115+
(conversation, media),
116+
)
117+
expected = {
118+
"messages": normalized,
119+
"model_config": renderer.model_config,
120+
"content_format": "openai",
121+
"media_io_kwargs": renderer._media_connector_config["media_io_kwargs"],
122+
"mm_processor_kwargs": renderer.mm_processor_kwargs,
123+
}
124+
sync_parser.assert_called_once_with(**expected)
125+
async_parser.assert_awaited_once_with(**expected)
126+
self.assertEqual(messages[0]["content"][0]["type"], "image")

‎trinity/common/models/mm_utils.py‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,30 @@
2222
`MultiModalRender` normalizes legacy transformers-style message parts
2323
(e.g., type=image with url/path/base64) into vLLM/OpenAI-style part schema
2424
before calling vLLM `parse_chat_messages`.
25+
vLLM is loaded only by its renderer, so text-only backends can import the
26+
shared helpers without installing the optional vLLM dependency.
2527
"""
28+
from __future__ import annotations
29+
2630
import json
2731
import re
2832
from abc import ABC, abstractmethod
2933
from pathlib import Path
30-
from typing import Any, Dict, List, Optional, Union
34+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
3135

3236
import numpy as np
3337
import torch
3438
import transformers
35-
from vllm.config import ModelConfig
36-
from vllm.entrypoints.chat_utils import (
37-
ChatTemplateContentFormat,
38-
ConversationMessage,
39-
parse_chat_messages,
40-
parse_chat_messages_async,
41-
)
42-
from vllm.inputs import MultiModalDataDict
4339

4440
from trinity.utils.log import get_logger
4541

42+
if TYPE_CHECKING:
43+
from vllm.entrypoints.chat_utils import (
44+
ChatTemplateContentFormat,
45+
ConversationMessage,
46+
)
47+
from vllm.inputs import MultiModalDataDict
48+
4649
_MM_TYPE_TO_URL_FIELD = {
4750
"image": "image_url",
4851
"video": "video_url",
@@ -275,6 +278,16 @@ def __init__(
275278
self.mm_processor_kwargs = mm_processor_kwargs or {}
276279

277280
# Initialize ModelConfig
281+
try:
282+
from vllm.config import ModelConfig
283+
except ModuleNotFoundError as exc:
284+
if exc.name != "vllm":
285+
raise
286+
raise ImportError(
287+
"vLLM is required for vLLMMultiModalRender. Install Trinity-RFT with the "
288+
"'vllm' extra to use this renderer."
289+
) from exc
290+
278291
self.model_config = ModelConfig(
279292
model=model_path,
280293
tokenizer=model_path,
@@ -420,6 +433,8 @@ def process_messages(
420433
"""
421434
normalized_messages = self._normalize_messages_for_vllm(messages)
422435

436+
from vllm.entrypoints.chat_utils import parse_chat_messages
437+
423438
conversation, mm_data, _ = parse_chat_messages(
424439
messages=normalized_messages,
425440
model_config=self.model_config,
@@ -440,6 +455,8 @@ async def process_messages_async(
440455
"""
441456
normalized_messages = self._normalize_messages_for_vllm(messages)
442457

458+
from vllm.entrypoints.chat_utils import parse_chat_messages_async
459+
443460
conversation, mm_data, _ = await parse_chat_messages_async(
444461
messages=normalized_messages,
445462
model_config=self.model_config,

0 commit comments

Comments
 (0)