main
py 192 lines 6.71 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Test that ``InputProcessor.inject_into_mm_cache()`` correctly injects
4 pre-processed mm_kwargs into the processor cache and reports MM cache
5 hit rate metrics accurately.
6
7 This is used by frameworks like Dynamo that run the HF processor on a
8 frontend and transfer pre-processed mm_kwargs to the backend, avoiding
9 redundant processing.
10 """
11
12 import logging
13
14 import pytest
15 import regex as re
16
17 from tests.entrypoints.multimodal.conftest import TEST_IMAGE_ASSETS
18 from vllm import LLM, SamplingParams
19 from vllm.renderers.params import ChatParams
20 from vllm.v1.metrics import loggers as stat_loggers
21 from vllm.v1.metrics.reader import Counter, Metric
22
23
24 def _make_messages(image_url: str):
25 return [
26 {
27 "role": "user",
28 "content": [
29 {
30 "type": "image_url",
31 "image_url": {"url": image_url},
32 },
33 ],
34 }
35 ]
36
37
38 def _get_counter_value(metrics: list[Metric], name: str):
39 metric = next(m for m in metrics if m.name == name)
40 assert isinstance(metric, Counter)
41 return metric.value
42
43
44 def _get_mm_cache_stats(metrics: list[Metric]):
45 mm_cache_queries = _get_counter_value(metrics, "vllm:mm_cache_queries")
46 mm_cache_hits = _get_counter_value(metrics, "vllm:mm_cache_hits")
47 return mm_cache_queries, mm_cache_hits
48
49
50 def _get_mm_cache_log(llm: LLM, caplog_vllm: pytest.LogCaptureFixture) -> float:
51 caplog_vllm.clear()
52 with caplog_vllm.at_level(logging.INFO, logger=stat_loggers.__name__):
53 llm.llm_engine.do_log_stats()
54
55 assert len(caplog_vllm.records) == 1
56 msg = caplog_vllm.records[0].getMessage()
57
58 assert "MM cache hit rate" in msg
59 match = re.search(r"MM cache hit rate: ([0-9.]+)%", msg)
60 assert match is not None
61 return float(match.group(1))
62
63
64 @pytest.mark.parametrize("image_urls", [TEST_IMAGE_ASSETS[:2]], indirect=True)
65 @pytest.mark.parametrize("mm_processor_cache_type", ["lru", "shm"])
66 def test_inject_into_mm_cache(
67 num_gpus_available,
68 image_urls,
69 mm_processor_cache_type,
70 caplog_vllm,
71 vllm_runner,
72 ):
73 """Test that inject_into_mm_cache() injects pre-processed mm_kwargs into
74 the processor cache and MM cache hit metrics are updated correctly.
75
76 Steps:
77 1. Two normal requests (same image) -> cache miss then hit (baseline)
78 2. Extract cached kwargs, call inject_into_mm_cache with a new hash,
79 then generate with a pre-rendered input -> verifies injection works
80 """
81 with vllm_runner(
82 "llava-hf/llava-1.5-7b-hf",
83 max_model_len=4096,
84 max_num_seqs=5,
85 enforce_eager=True,
86 disable_log_stats=False,
87 limit_mm_per_prompt={"image": 2},
88 mm_processor_cache_type=mm_processor_cache_type,
89 ) as runner:
90 # Step 1: Normal requests to populate the cache
91 runner.llm.chat(_make_messages(image_urls[0]))
92 assert _get_mm_cache_stats(runner.llm.get_metrics()) == (1, 0)
93
94 runner.llm.chat(_make_messages(image_urls[0]))
95 assert _get_mm_cache_stats(runner.llm.get_metrics()) == (2, 1)
96 assert _get_mm_cache_log(runner.llm, caplog_vllm) == pytest.approx(50.0)
97
98 # Step 2: Use a second image to get valid expanded tokens and
99 # placeholder positions via the renderer.
100 runner.llm.chat(_make_messages(image_urls[1]))
101 queries_before = _get_mm_cache_stats(runner.llm.get_metrics())[0] # 3
102
103 renderer = runner.llm.llm_engine.renderer
104 cache = renderer.mm_processor_cache
105 assert cache is not None, "Processor cache should be enabled"
106
107 _, eng_prompts = renderer.render_chat(
108 [_make_messages(image_urls[1])],
109 ChatParams(),
110 )
111 eng_input = eng_prompts[0]
112
113 # Inject pre-processed mm_kwargs with a NEW hash via public API
114 new_mm_hash = "deadbeef" * 8
115 mm_hashes = {"image": [new_mm_hash]}
116 mm_kwargs = eng_input["mm_kwargs"]
117
118 runner.llm.llm_engine.input_processor.inject_into_mm_cache(mm_hashes, mm_kwargs)
119
120 # Build pre-rendered input (no externally_processed flag needed)
121 pre_rendered_input = {
122 "type": "multimodal",
123 "prompt_token_ids": eng_input["prompt_token_ids"],
124 "mm_kwargs": mm_kwargs,
125 "mm_hashes": mm_hashes,
126 "mm_placeholders": eng_input["mm_placeholders"],
127 }
128
129 runner.llm.generate(
130 pre_rendered_input,
131 sampling_params=SamplingParams(max_tokens=1),
132 )
133
134 # Verify cache was queried and injection happened
135 queries_after = _get_mm_cache_stats(runner.llm.get_metrics())[0]
136 assert queries_after > queries_before, (
137 "Cache should have been queried for the injected item"
138 )
139 mm_rate = _get_mm_cache_log(runner.llm, caplog_vllm)
140 assert mm_rate >= 0.0, "MM cache hit rate should be reported"
141
142
143 @pytest.mark.parametrize("image_urls", [TEST_IMAGE_ASSETS[:1]], indirect=True)
144 def test_inject_into_mm_cache_without_cache(
145 num_gpus_available,
146 image_urls,
147 vllm_runner,
148 ):
149 """Test that inject_into_mm_cache works gracefully when processor cache
150 is disabled (mm_processor_cache_gb=0). Should not crash.
151 """
152 with vllm_runner(
153 "llava-hf/llava-1.5-7b-hf",
154 max_model_len=4096,
155 max_num_seqs=5,
156 enforce_eager=True,
157 disable_log_stats=False,
158 limit_mm_per_prompt={"image": 2},
159 mm_processor_cache_gb=0,
160 ) as runner:
161 # Run a normal chat request first to warm up the model.
162 runner.llm.chat(_make_messages(image_urls[0]))
163
164 # Use the renderer to get a proper EngineInput with expanded tokens
165 renderer = runner.llm.llm_engine.renderer
166 _, eng_prompts = renderer.render_chat(
167 [_make_messages(image_urls[0])],
168 ChatParams(),
169 )
170 eng_input = eng_prompts[0]
171
172 mm_hashes = {"image": ["abcd1234" * 8]}
173 mm_kwargs = eng_input["mm_kwargs"]
174
175 # inject_into_mm_cache should not crash even without cache
176 runner.llm.llm_engine.input_processor.inject_into_mm_cache(mm_hashes, mm_kwargs)
177
178 # Build and generate with pre-rendered input
179 pre_rendered_input = {
180 "type": "multimodal",
181 "prompt_token_ids": eng_input["prompt_token_ids"],
182 "mm_kwargs": mm_kwargs,
183 "mm_hashes": mm_hashes,
184 "mm_placeholders": eng_input["mm_placeholders"],
185 }
186
187 result = runner.llm.generate(
188 pre_rendered_input,
189 sampling_params=SamplingParams(max_tokens=1),
190 )
191 assert len(result) == 1, "Should produce one output"
192 assert len(result[0].outputs) >= 1, "Should have at least one output sequence"