Skip to content

Commit 2056a42

Browse files
authored
[bug fix] Fix the placeholder in qwen prompt and add some unittests (#4065)
* fix the placeholder in qwen prompt * fix the placeholder in qwen prompt * add soem unittests for qwen_vl_processor
1 parent 850465e commit 2056a42

2 files changed

Lines changed: 116 additions & 7 deletions

File tree

fastdeploy/input/qwen_vl_processor/process.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ def text2ids(self, text, images=None, videos=None):
166166
}
167167

168168
# Define placeholders and their lengths
169-
IMAGE_PLACEHOLDER = "<|image@placeholder|>"
170-
VIDEO_PLACEHOLDER = "<|video@placeholder|>"
169+
IMAGE_PLACEHOLDER = "<|image_pad|>"
170+
VIDEO_PLACEHOLDER = "<|video_pad|>"
171171
IMAGE_PLACEHOLDER_LEN = len(IMAGE_PLACEHOLDER)
172172
VIDEO_PLACEHOLDER_LEN = len(VIDEO_PLACEHOLDER)
173173

tests/input/test_qwen_vl_processor.py

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@ def setUp(self):
111111
}
112112
limit_mm_per_prompt = {"image": 1, "video": 1, "audio": 1}
113113

114-
model_name_or_path = "/ModelData/Qwen2.5-VL-7B-Instruct"
114+
self.model_name_or_path = "/ModelData/Qwen2.5-VL-7B-Instruct"
115115
self.processor = QwenVLProcessor(
116116
config=config,
117-
model_name_or_path=model_name_or_path,
117+
model_name_or_path=self.model_name_or_path,
118118
limit_mm_per_prompt=limit_mm_per_prompt,
119119
mm_processor_kwargs=mm_processor_kwargs,
120120
reasoning_parser_obj=None,
@@ -137,7 +137,7 @@ def test_process_request(self):
137137
3. Video processing produces expected output dimensions
138138
4. Correct counts for images (1) and videos (1)
139139
"""
140-
prompt = {
140+
message = {
141141
"request_id": "12345",
142142
"messages": [
143143
{
@@ -151,7 +151,7 @@ def test_process_request(self):
151151
],
152152
}
153153

154-
request = Request.from_dict(prompt)
154+
request = Request.from_dict(message)
155155
result = self.processor.process_request(request, 1024 * 100)
156156

157157
self.assertEqual(result.prompt_token_ids_len, result.multimodal_inputs["position_ids"].shape[0])
@@ -219,9 +219,11 @@ def test_prompt(self):
219219
3. Video processing produces expected output dimensions
220220
4. Correct counts for images (1) and videos (1)
221221
"""
222+
IMAGE_PLACEHOLDER = "<|image_pad|>"
223+
VIDEO_PLACEHOLDER = "<|video_pad|>"
222224
prompt = {
223225
"request_id": "12345",
224-
"prompt": "<|image@placeholder|><|video@placeholder|>Describe image and video.",
226+
"prompt": f"{IMAGE_PLACEHOLDER}{VIDEO_PLACEHOLDER}Describe image and video.",
225227
"multimodal_data": {
226228
"image": [mock_pil_image(10, 2100)],
227229
"video": [{"video": b"123", "fps": 5}],
@@ -243,6 +245,113 @@ def test_prompt(self):
243245
self.assertEqual(result.multimodal_inputs["pic_cnt"], 1)
244246
self.assertEqual(result.multimodal_inputs["video_cnt"], 1)
245247

248+
def test_message_and_prompt(self):
249+
"""
250+
Test consistency between message-based and prompt-based processing
251+
252+
Validates that processing a request through:
253+
1. The message format (with image/video URLs)
254+
2. The prompt format (with direct image/video data)
255+
produces identical tokenization and multimodal input results.
256+
257+
Checks:
258+
1. Prompt token IDs match between both processing methods
259+
2. Grid dimensions (THW) match between both methods
260+
3. Position IDs match between both methods
261+
"""
262+
# Create test request in message format
263+
request = {
264+
"request_id": "12345",
265+
"messages": [
266+
{
267+
"role": "user",
268+
"content": [
269+
{"type": "image_url", "image_url": {"url": "file://demo.jpeg"}},
270+
{"type": "video_url", "video_url": {"url": "file://3_frame_video.mp4"}},
271+
{"type": "text", "text": "Describe image and video."},
272+
],
273+
}
274+
],
275+
}
276+
result = self.processor.process_request_dict(request, 1024 * 100)
277+
278+
# Create equivalent request in prompt format
279+
prompt = {
280+
"request_id": "12345",
281+
"prompt": request["text_after_process"],
282+
"multimodal_data": {
283+
"image": [mock_pil_image(480, 640)],
284+
"video": [{"video": b"123"}],
285+
},
286+
}
287+
request2 = Request.from_dict(prompt)
288+
result2 = self.processor.process_request(request2, 1024 * 100)
289+
290+
# Verify both processing methods produce identical results
291+
self.assertEqual(result["prompt_token_ids"], result2.prompt_token_ids)
292+
self.assertTrue(np.equal(result["multimodal_inputs"]["grid_thw"], result2.multimodal_inputs["grid_thw"]).all())
293+
self.assertTrue(
294+
np.equal(result["multimodal_inputs"]["position_ids"], result2.multimodal_inputs["position_ids"]).all()
295+
)
296+
297+
def test_apply_chat_template(self):
298+
"""
299+
Test the consistency between:
300+
1. Directly applying chat template using HuggingFace tokenizer
301+
2. Applying chat template through the processor's request processing
302+
303+
This test verifies that:
304+
- The processor correctly handles multimodal messages (image, video, text)
305+
- The text_after_process field matches the output from direct tokenizer application
306+
- The chat template application preserves the message structure and content
307+
308+
Test Steps:
309+
1. Create sample multimodal messages with image, video and text content
310+
2. Apply chat template directly using the tokenizer
311+
3. Process the same messages through the processor
312+
4. Compare the outputs to ensure consistency
313+
"""
314+
from transformers import AutoTokenizer
315+
316+
tokenizer = AutoTokenizer.from_pretrained(self.model_name_or_path)
317+
318+
# Sample multimodal messages containing image, video and text
319+
messages = [
320+
{
321+
"role": "user",
322+
"content": [
323+
{"type": "image_url", "image_url": {"url": "file://demo.jpeg"}},
324+
{"type": "video", "video": {"url": "file://3_frame_video.mp4"}},
325+
{"type": "text", "text": "Describe image and video."},
326+
],
327+
}
328+
]
329+
330+
# Apply chat template directly using the tokenizer
331+
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
332+
333+
# Create equivalent request dictionary
334+
request = {
335+
"request_id": "12345",
336+
"messages": [
337+
{
338+
"role": "user",
339+
"content": [
340+
{"type": "image_url", "image_url": {"url": "file://demo.jpeg"}},
341+
{"type": "video_url", "video_url": {"url": "file://3_frame_video.mp4"}},
342+
{"type": "text", "text": "Describe image and video."},
343+
],
344+
}
345+
],
346+
}
347+
348+
# Process request through the processor
349+
self.processor.process_request_dict(request, 1024 * 100)
350+
prompt2 = request["text_after_process"]
351+
352+
# Verify both methods produce identical prompt strings
353+
self.assertEqual(prompt, prompt2)
354+
246355

247356
if __name__ == "__main__":
248357
unittest.main()

0 commit comments

Comments
 (0)