multimodalart HF Staff commited on
Commit
4775ba1
·
verified ·
1 Parent(s): d6a6022

drop unused vllm/sglang paths and stale example clips

Browse files
examples/autoshot_test.mp4 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:ce2f996801297b2237ab43988a3e6b412bab8e17409381a156bcad988de96dd8
3
- size 2803654
 
 
 
 
examples/fast_cut.mp4 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7e9cbcd462edbf48fe8096c15326874326a3a1315d80bb09ab63d11067356728
3
- size 349550
 
 
 
 
examples/movieshots2.mp4 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:97b2decf2b87ef120973a74d8bd866fe20311749e02cad49b740c5a51130af94
3
- size 2149571
 
 
 
 
examples/vdb_clip.mp4 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:b4c83c438b96c11a00c7235454461ff7714db3cbc9c752050cdf4823351d0ab6
3
- size 2827210
 
 
 
 
transvlm/inference/sglang_clip_engine.py DELETED
@@ -1,444 +0,0 @@
1
- # Copyright © 2026 HeyGen
2
- # SPDX-License-Identifier: Apache-2.0
3
-
4
- """One SGLang engine, reused across many clips or windows — the vLLM engine's sibling.
5
-
6
- Mirrors :class:`transvlm.inference.clip_engine.ClipInferenceEngine`'s public
7
- surface (``load`` / ``infer`` / ``close``, plus the ``fps`` and ``model_load_sec``
8
- attributes) and returns the same :class:`ClipResult`, so ``infer_clips.py`` and
9
- ``infer_video.py`` can swap backends without changing their output schema. That
10
- matters: the scoring entry point and the comparison script both key off that schema.
11
-
12
- **Preprocessing is shared, not reimplemented.** The backend-neutral helpers
13
- (``build_messages``, ``read_user_input``, ``_rebased``) and the whole
14
- ``process_vision_info`` chain are imported from ``clip_engine``, so both backends decode,
15
- resize (``smart_resize`` / pixel budget) and channel-concatenate through the *same* code.
16
- Raw video never reaches the engine. Only the final hand-off differs, and it has to:
17
-
18
- * vLLM takes the ``(N, 6, H, W)`` tensor plus its metadata and runs the HF processor
19
- **server-side** inside the EngineCore worker.
20
- * SGLang 0.5.8 cannot do that, so the processor runs **client-side** here and the engine
21
- receives ``pixel_values_videos`` + ``video_grid_thw`` + already-expanded ``input_ids``.
22
-
23
- One measurement consequence to keep in mind when reading RTF numbers: this backend's
24
- ``preprocess_sec`` includes patchify and timestamp expansion, while vLLM's does not — that
25
- cost sits inside vLLM's ``generate_sec`` instead. Only end-to-end wall clock is directly
26
- comparable across the two.
27
-
28
- Two facts about SGLang worth stating because they are the opposite of vLLM's:
29
-
30
- * **No model fork is needed for 6 channels.** ``Qwen3VLVisionPatchEmbed`` reads
31
- ``in_channels`` from the config, so the checkpoint's 6-channel patch embed loads natively.
32
- * **SGLang has no multimodal dummy profiling**, so vLLM's ``sitecustomize.py`` dummy-video
33
- patch has no counterpart and is not needed. The flip side is that SGLang never measures
34
- the ViT activation peak, so ``mem_fraction_static`` is a static guess that does not know
35
- about it — which is why that knob is exposed here.
36
-
37
- What *is* still needed is the mm-processor override: upstream's ``preprocess_video`` assumes
38
- a decord ``VideoReader`` and dies on our pre-computed dicts, and upstream also drops the video
39
- grid on this path, which silently costs the video tokens their 3D M-RoPE (see
40
- ``_sglang_patches/qwen_vl_processor_6ch.py``).
41
-
42
- Prompt timestamp precision is a per-run choice (``timestamp_format``, wired to
43
- ``--timestamp-format``), identical to the other two backends. ``.1f`` is this processor's
44
- native behaviour and reproduces the published reference numbers; ``.2f`` matches training. The
45
- class *rebind* that ``Qwen3VLProcessorOurs`` represents is never applied — measurement showed
46
- it cannot take effect — so ``.2f`` is delivered by patching the method instead; see
47
- :func:`rebind_transformers_processor` for that measurement and
48
- ``transvlm/models/processor_patch.py`` for the mechanism.
49
- """
50
-
51
- from __future__ import annotations
52
-
53
- import logging
54
- import os
55
- import sys
56
- import time
57
- from pathlib import Path
58
- from typing import Any
59
-
60
- from transvlm.inference.clip_engine import (
61
- DEFAULT_FPS,
62
- DEFAULT_PROMPT_FILE,
63
- DEFAULT_TIMESTAMP_FORMAT,
64
- SAMPLING_KWARGS,
65
- ClipResult,
66
- _rebased,
67
- build_messages,
68
- read_user_input,
69
- )
70
- from transvlm.inference.processor_inputs import (
71
- run_client_side_processor,
72
- video_metadata_for_processor as _video_metadata_for_processor,
73
- )
74
- from transvlm.models.processor_patch import (
75
- apply_timestamp_format,
76
- assert_prompt_timestamp_format,
77
- )
78
-
79
- from transvlm._paths import PACKAGE_PARENT
80
-
81
- logger = logging.getLogger(__name__)
82
-
83
- # SGLang's tokenizer_manager reads this package name from
84
- # SGLANG_EXTERNAL_MM_PROCESSOR_PACKAGE and scans it for processor subclasses.
85
- PATCH_PKG = 'transvlm.models._sglang_patches'
86
-
87
- # Engine kwargs chosen to mirror the vLLM engine's posture rather than to be fast.
88
- #
89
- # `context_length` matches vLLM's `max_model_len` (16384) exactly. The archived
90
- # implementation used 200000, which is a different engine configuration and would make any
91
- # comparison meaningless.
92
- #
93
- # `mem_fraction_static` has no vLLM equivalent worth copying: vLLM's
94
- # `gpu_memory_utilization=0.2` works because vLLM *measures* the ViT peak during profiling and
95
- # budgets around it, whereas this is a blind static split. Tunable because the historical
96
- # high-resolution failure was first misdiagnosed as pure OOM.
97
- #
98
- # 0.45 rather than the inherited 0.70, measured on this GPU 2026-07-30. At 0.70 the engine's
99
- # peak was 88.4 GiB of the 97 GiB card -- 91%, roughly twice vLLM's 45.0 GiB -- leaving 6.6 GiB
100
- # free, which is less than the ~6.1-6.9 GiB the GPU flow visualisation needs. 29 of 30 clips
101
- # therefore fell through `OnlineFlowComputer._fits_on_gpu` onto the CPU path and silently lost
102
- # the 19.5x speedup that vLLM was getting from the same code. Note 0.70 does not mean 68 GiB:
103
- # the fraction covers weights + KV cache only, and activations plus CUDA context stack on top.
104
- #
105
- # At 0.45 (and 0.30, identical in measurement) the fallback disappears: flow_to_image median
106
- # 4.257 s -> 0.218 s and the whole flow stage 227.7 s -> 95.7 s over 30 clips, against a ~5%
107
- # rise in generate (122.6 s -> 128.8 s) from the smaller KV cache. 0.45 is the larger of the
108
- # two working values, so it keeps the most KV headroom that still frees the visualisation.
109
- #
110
- # The 0.70 figure came from a 44 GiB card, where it was 31 GiB. Anything comparing against the
111
- # 2026-07-29 SGLang run should know that run was at 0.70.
112
- ENGINE_KWARGS: dict[str, Any] = {
113
- 'dtype': 'bfloat16',
114
- 'trust_remote_code': False,
115
- 'mem_fraction_static': 0.45,
116
- 'context_length': 16384,
117
- 'kv_cache_dtype': 'auto',
118
- 'log_level': 'info',
119
- # Skips graph capture. Kept off deliberately: capture buys throughput, not peak memory,
120
- # and window widths vary, so recapture per shape is a known source of surprises here.
121
- 'disable_cuda_graph': True,
122
- # Single sequential requests need no scheduler overlap, and overlap has been observed to
123
- # spin the Scheduler subprocess while the caller waits on a stalled IPC round-trip.
124
- 'disable_overlap_schedule': True,
125
- }
126
-
127
- # Sampling params. vLLM's `seed` / `top_k` / `stop_token_ids` have no per-request equivalent
128
- # in SGLang, so greedy is expressed the way SGLang expresses it. Every field is set
129
- # explicitly rather than left to a default, because a silent default change between versions
130
- # would look like a model regression.
131
- SAMPLING_KWARGS_SGLANG: dict[str, Any] = {
132
- 'temperature': 0.0,
133
- 'top_p': 1.0,
134
- 'top_k': -1,
135
- 'max_new_tokens': SAMPLING_KWARGS['max_tokens'],
136
- }
137
-
138
- # Env knobs for the diagnostic ladder, so a rung can be tried without editing code.
139
- ENV_MEM_FRACTION = 'TRANSVLM_SGLANG_MEM_FRACTION_STATIC'
140
- ENV_CONTEXT_LENGTH = 'TRANSVLM_SGLANG_CONTEXT_LENGTH'
141
- ENV_ATTENTION_BACKEND = 'TRANSVLM_SGLANG_ATTENTION_BACKEND'
142
- ENV_KV_CACHE_DTYPE = 'TRANSVLM_SGLANG_KV_CACHE_DTYPE'
143
-
144
- # `TRANSVLM_SGLANG_REBIND_PROCESSOR` used to gate the .2f timestamp patch here. It is gone:
145
- # the format is now the `timestamp_format` ctor argument, wired to `--timestamp-format` on the
146
- # runtime scripts, so all three backends express it the same way. An env knob on one backend
147
- # only was exactly the asymmetry that let the three drift apart unnoticed.
148
-
149
-
150
- def prepare_environment() -> None:
151
- """Install the mm-processor override and make it importable from subprocesses.
152
-
153
- Must run before ``sglang.Engine(...)``. SGLang calls
154
- ``mp.set_start_method('spawn', force=True)``, so a parent-process monkey-patch would
155
- fail **silently** in the Scheduler subprocesses — the repo root has to be on their
156
- ``PYTHONPATH``, not merely on this process's ``sys.path``.
157
- """
158
- os.environ['SGLANG_EXTERNAL_MM_PROCESSOR_PACKAGE'] = PATCH_PKG
159
-
160
- # PyTorch 2.9 renamed PYTORCH_CUDA_ALLOC_CONF to PYTORCH_ALLOC_CONF; set both so the
161
- # setting applies regardless of which name this build honours.
162
- os.environ.setdefault('PYTORCH_ALLOC_CONF', 'expandable_segments:True')
163
- os.environ.setdefault('PYTORCH_CUDA_ALLOC_CONF', 'expandable_segments:True')
164
-
165
- root = str(PACKAGE_PARENT)
166
- if root not in sys.path:
167
- sys.path.insert(0, root)
168
- existing = os.environ.get('PYTHONPATH', '')
169
- entries = existing.split(os.pathsep) if existing else []
170
- if root not in entries:
171
- os.environ['PYTHONPATH'] = os.pathsep.join([root, *entries]) if entries else root
172
-
173
-
174
- def rebind_transformers_processor() -> None:
175
- """Point ``transformers``' Qwen3VLProcessor at our 6-channel subclass. Idempotent.
176
-
177
- **Nothing calls this.** It is kept because its measurement below is the evidence for why
178
- the class rebind cannot deliver ``.2f`` timestamps — the conclusion that
179
- ``transvlm/models/processor_patch.py`` acts on by patching the *method* onto the
180
- original class instead. Timestamp format is now chosen per run via
181
- ``apply_timestamp_format``; the ``TRANSVLM_SGLANG_REBIND_PROCESSOR`` env knob this function
182
- used to sit behind is gone.
183
-
184
- vLLM's ``sitecustomize.py`` performs this same rebind and its docstring says the point is
185
- to move prompt timestamps from ``.1f`` to ``.2f``. Measured on a 10 s clip, that rebind
186
- **has no effect on what ``AutoProcessor.from_pretrained`` returns** — AutoProcessor
187
- resolves the class through its own registry, which already holds a direct reference to the
188
- original, so patching the module attribute afterwards is too late:
189
-
190
- no patch Qwen3VLProcessor 12,673 tokens stamps 0.0 0.1 0.2 0.3 0.3
191
- sitecustomize Qwen3VLProcessor 12,673 tokens stamps 0.0 0.1 0.2 0.3 0.3
192
- this rebind Qwen3VLProcessorOurs 12,798 tokens stamps 0.02 0.10 0.18 0.26
193
-
194
- And vLLM's returned ``prompt_token_ids`` for that clip is 12,673 with ``.1f`` stamps,
195
- matching the unpatched expansion token for token — so the reference results this project
196
- reproduces were produced with ``.1f``, duplicate stamps and all. Calling this function
197
- would therefore make SGLang diverge from vLLM rather than agree with it.
198
-
199
- Kept because it is the only way to test the ``.2f`` hypothesis, and because silently
200
- dropping it would hide the discrepancy from whoever reads the vLLM docstring next.
201
-
202
- The subclass lives under ``_vllm_patches/`` but is pure ``transformers`` code with no
203
- vLLM import, so sharing it across backends is deliberate, not an accident of layout.
204
- """
205
- import transformers # noqa: PLC0415
206
- import transformers.models.qwen3_vl.processing_qwen3_vl as _mod # noqa: PLC0415
207
-
208
- from transvlm.models._vllm_patches.qwen3vl_processor_ours import ( # noqa: PLC0415
209
- Qwen3VLProcessorOurs,
210
- )
211
-
212
- _mod.Qwen3VLProcessor = Qwen3VLProcessorOurs
213
- if hasattr(transformers, 'Qwen3VLProcessor'):
214
- transformers.Qwen3VLProcessor = Qwen3VLProcessorOurs
215
-
216
-
217
- def _warn_on_vllm_coexistence() -> None:
218
- """Warn if vllm is importable: the two pin incompatible flashinfer versions.
219
-
220
- ``pyproject.toml`` declares the groups mutually exclusive, so this state means uv's
221
- solver was bypassed and SGLang may die on a flashinfer symbol mismatch.
222
- """
223
- import importlib.util # noqa: PLC0415
224
-
225
- if importlib.util.find_spec('vllm') is not None:
226
- logger.warning(
227
- 'both vllm and sglang are importable in this environment; they pin different '
228
- 'flashinfer-python versions (0.5.3 vs 0.6.1) and sglang may crash on a symbol '
229
- 'mismatch. Use the dedicated SGLang venv described in the inference README.'
230
- )
231
-
232
-
233
- class SGLangClipInferenceEngine:
234
- """Owns the SGLang engine and the client-side processor for a process's lifetime."""
235
-
236
- def __init__(
237
- self,
238
- *,
239
- ckpt_dir: str | Path,
240
- prompt_file: str | Path = DEFAULT_PROMPT_FILE,
241
- fps: float = DEFAULT_FPS,
242
- max_new_tokens: int = SAMPLING_KWARGS['max_tokens'],
243
- no_prefix_caching: bool = False,
244
- timestamp_format: str = DEFAULT_TIMESTAMP_FORMAT,
245
- ) -> None:
246
- self.ckpt_dir = Path(ckpt_dir)
247
- self.prompt_file = Path(prompt_file)
248
- self.fps = fps
249
- self.max_new_tokens = max_new_tokens
250
- self.no_prefix_caching = no_prefix_caching
251
- self.timestamp_format = timestamp_format
252
- # Verified once, on the first clip: the failure this guards against is a patch that
253
- # looks installed and is not, which cost a full 872-clip run before it was spotted.
254
- self._timestamp_format_verified = False
255
- self.user_input = read_user_input(self.prompt_file)
256
-
257
- self.engine_kwargs: dict[str, Any] = dict(ENGINE_KWARGS, model_path=str(self.ckpt_dir))
258
- self.engine_kwargs['mem_fraction_static'] = float(
259
- os.environ.get(ENV_MEM_FRACTION, ENGINE_KWARGS['mem_fraction_static'])
260
- )
261
- self.engine_kwargs['context_length'] = int(os.environ.get(ENV_CONTEXT_LENGTH, ENGINE_KWARGS['context_length']))
262
- self.engine_kwargs['kv_cache_dtype'] = os.environ.get(ENV_KV_CACHE_DTYPE, ENGINE_KWARGS['kv_cache_dtype'])
263
- attention_backend = os.environ.get(ENV_ATTENTION_BACKEND)
264
- if attention_backend:
265
- self.engine_kwargs['attention_backend'] = attention_backend
266
- if no_prefix_caching:
267
- # SGLang's RadixCache is the counterpart of vLLM's prefix caching. vLLM's was
268
- # measured moving a boundary 1.98 -> 1.99 when the same request was resubmitted,
269
- # so any A/B comparison needs it off on both sides.
270
- self.engine_kwargs['disable_radix_cache'] = True
271
-
272
- self.sampling_kwargs: dict[str, Any] = dict(SAMPLING_KWARGS_SGLANG, max_new_tokens=max_new_tokens)
273
- self._engine: Any = None
274
- self._processor: Any = None
275
- self.model_load_sec = 0.0
276
-
277
- def load(self) -> None:
278
- """Build the engine. Idempotent."""
279
- if self._engine is not None:
280
- return
281
-
282
- _warn_on_vllm_coexistence()
283
- prepare_environment()
284
- # include_vllm=False: this backend runs the HF processor client-side, so the HF-level
285
- # patch is the whole story here. Importing vLLM's module would also pull vLLM into a
286
- # process that is deliberately kept free of it.
287
- apply_timestamp_format(self.timestamp_format, include_vllm=False)
288
-
289
- from transformers import AutoProcessor # noqa: PLC0415
290
-
291
- started = time.perf_counter()
292
- from transvlm.inference.backends import validate_ckpt_dir # noqa: PLC0415
293
-
294
- validate_ckpt_dir(self.ckpt_dir)
295
- self._processor = AutoProcessor.from_pretrained(str(self.ckpt_dir))
296
-
297
- # Imported only after the env vars are set, so tokenizer_manager picks up the
298
- # external mm-processor override.
299
- try:
300
- import sglang # noqa: PLC0415
301
- except ImportError as exc:
302
- from transvlm.inference.backends import missing_backend_exit # noqa: PLC0415
303
-
304
- raise missing_backend_exit('sglang', 'sglang', exc) from exc
305
-
306
- logger.info('sglang engine kwargs: %s', self.engine_kwargs)
307
- self._engine = sglang.Engine(**self.engine_kwargs)
308
- self.model_load_sec = time.perf_counter() - started
309
- logger.info('engine ready in %.1f s', self.model_load_sec)
310
-
311
- def infer(
312
- self,
313
- rgb_video: str | Path,
314
- flow_video: str | Path,
315
- *,
316
- video_start: float | None = None,
317
- video_end: float | None = None,
318
- rebase_frame_indices: bool = False,
319
- ) -> ClipResult:
320
- """Run one request. One request per generate call.
321
-
322
- Single requests are mandatory, not merely tidy: batching several videos into one
323
- ``Engine.generate`` on 0.5.8 raises ``Mismatch: More 'VIDEO' tokens found than
324
- corresponding data provided`` and returns empty text for all but the last request.
325
- It also matches the vLLM path, which sends one request for determinism.
326
-
327
- ``rebase_frame_indices`` has the same meaning as on the vLLM engine: a range-decoded
328
- window returns ABSOLUTE frame numbers, and prompt timestamps derive from that field,
329
- so without rebasing the model would see absolute video time where a pre-cut clip
330
- shows window-local time.
331
- """
332
- if self._engine is None:
333
- raise RuntimeError('call load() before infer()')
334
-
335
- # Routed through the loader so the decoder choice (torchcodec, else decord with a
336
- # warning) is settled before the vendored module captures it at import time.
337
- from transvlm.data.video_backend import load_process_vision_info # noqa: PLC0415
338
-
339
- process_vision_info = load_process_vision_info()
340
-
341
- pre_started = time.perf_counter()
342
- messages = build_messages(
343
- rgb_video,
344
- flow_video,
345
- self.user_input,
346
- self.fps,
347
- video_start=video_start,
348
- video_end=video_end,
349
- )
350
- text = self._processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
351
- _image_inputs, video_inputs, video_kwargs = process_vision_info(
352
- messages,
353
- image_patch_size=self._processor.image_processor.patch_size,
354
- return_video_kwargs=True,
355
- return_video_metadata=True,
356
- )
357
- if not video_inputs:
358
- raise RuntimeError('process_vision_info returned no video input')
359
- tensor, metadata = video_inputs[0]
360
- if rebase_frame_indices:
361
- metadata = _rebased(metadata)
362
-
363
- # Client-side processing, with EXACTLY the kwargs vLLM sends server-side:
364
- # `{'do_sample_frames': False}` and nothing else. In particular `do_resize` is left at
365
- # its default of True, so the processor applies its own resize on top of the one
366
- # `process_vision_info` already did. That double resize looks redundant but it is the
367
- # protocol the reference results were produced with, and skipping it is catastrophic
368
- # rather than merely different: measured on a 1152x640 clip,
369
- #
370
- # do_resize=False -> grid [125, 72, 40], 91,423 prompt tokens
371
- # do_resize=True -> grid [125, 26, 14], 12,798 prompt tokens (vLLM: 12,673)
372
- #
373
- # and `context_length` is 16384, so the first form overflows the window by 5.6x. The
374
- # earlier implementation passed do_resize=False, which is very likely the real cause
375
- # of the historical "valid but empty []" collapse at high resolution that was
376
- # attributed to an upstream vision-encoder regression.
377
- inputs = run_client_side_processor(self._processor, text, tensor, metadata, self.fps, video_kwargs)
378
- if not self._timestamp_format_verified:
379
- decimals = assert_prompt_timestamp_format(self._processor, inputs, self.timestamp_format)
380
- logger.info('prompt timestamps verified: %d decimal(s)', decimals)
381
- self._timestamp_format_verified = True
382
- input_ids = inputs['input_ids'][0].tolist()
383
- video_data = [
384
- {
385
- 'format': 'processor_output',
386
- 'pixel_values_videos': inputs['pixel_values_videos'],
387
- 'video_grid_thw': inputs['video_grid_thw'],
388
- }
389
- ]
390
- preprocess_sec = time.perf_counter() - pre_started
391
-
392
- gen_started = time.perf_counter()
393
- result = self._engine.generate(
394
- input_ids=input_ids,
395
- sampling_params=self.sampling_kwargs,
396
- video_data=video_data,
397
- )
398
- generate_sec = time.perf_counter() - gen_started
399
-
400
- if isinstance(result, list):
401
- if len(result) != 1:
402
- raise RuntimeError(f'expected exactly 1 output, got {len(result)}')
403
- result = result[0]
404
-
405
- meta_info = result.get('meta_info') or {}
406
- # An aborted request (context overflow, scheduler abort) comes back with empty text
407
- # and no exception, which would be written to the jsonl as a legitimate empty
408
- # prediction — indistinguishable from "the model saw no transition" and exactly the
409
- # signature of the historical silent-collapse. Refuse to report it as a result.
410
- finish_type = (meta_info.get('finish_reason') or {}).get('type')
411
- if finish_type not in (None, 'stop', 'length'):
412
- raise RuntimeError(f'sglang returned finish_reason={finish_type!r} for {rgb_video}')
413
- if not result.get('text') and finish_type != 'stop':
414
- raise RuntimeError(
415
- f'sglang returned empty text with finish_reason={finish_type!r} for {rgb_video}; '
416
- 'refusing to record it as an empty prediction'
417
- )
418
-
419
- return ClipResult(
420
- raw_text=result.get('text', ''),
421
- preprocess_sec=preprocess_sec,
422
- generate_sec=generate_sec,
423
- n_input_tokens=int(meta_info.get('prompt_tokens') or len(input_ids)),
424
- n_output_tokens=int(meta_info.get('completion_tokens') or 0),
425
- n_frames=int(tensor.shape[0]),
426
- height=int(tensor.shape[2]),
427
- width=int(tensor.shape[3]),
428
- )
429
-
430
- def close(self) -> None:
431
- """Shut the engine down so the Scheduler subprocesses actually exit.
432
-
433
- SGLang leaves ``sglang::scheduler`` processes holding tens of GiB of VRAM if the
434
- engine is merely dropped, and they survive a ``pkill`` aimed at the parent — so this
435
- calls ``shutdown()`` rather than just clearing the reference.
436
- """
437
- if self._engine is not None:
438
- shutdown = getattr(self._engine, 'shutdown', None)
439
- if shutdown is not None:
440
- try:
441
- shutdown()
442
- except Exception: # noqa: BLE001 -- teardown must not mask a real failure
443
- logger.warning('engine shutdown raised; scheduler may need killing by name')
444
- self._engine = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
transvlm/models/_sglang_patches/__init__.py DELETED
@@ -1,20 +0,0 @@
1
- # Copyright © 2026 HeyGen
2
- # SPDX-License-Identifier: Apache-2.0
3
-
4
- """SGLang 6-channel adapter patches — external multimodal processor package.
5
-
6
- When SGLang boots with the environment variable
7
- ``SGLANG_EXTERNAL_MM_PROCESSOR_PACKAGE=transvlm.models._sglang_patches``
8
- (set by ``transvlm.inference.sglang_clip_engine.prepare_environment``),
9
- ``sglang/srt/managers/multimodal_processor.py::import_processors`` scans this
10
- package for subclasses whose ``models`` class attribute lists the loaded
11
- ``hf_config.architectures`` entry, and uses them in place of the upstream
12
- processor.
13
- """
14
- # Import the subclass so SGLang's package walker picks it up during
15
- # ``import_processors(_PATCH_PKG, overwrite=True)``.
16
- from transvlm.models._sglang_patches.qwen_vl_processor_6ch import (
17
- QwenVLImageProcessorOurs,
18
- )
19
-
20
- __all__ = ['QwenVLImageProcessorOurs']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
transvlm/models/_sglang_patches/qwen_vl_processor_6ch.py DELETED
@@ -1,191 +0,0 @@
1
- """SGLang QwenVLImageProcessor subclass for pre-decoded 6-channel video inputs.
2
-
3
- Upstream ``sglang.srt.multimodal.processors.qwen_vl.QwenVLImageProcessor``
4
- iterates ``base_output.videos`` calling ``preprocess_video(video, video_config)``
5
- which requires a decord-style ``VideoReader`` (crashes on dicts with
6
- ``AttributeError: 'dict' object has no attribute 'get_avg_fps'``).
7
-
8
- For our 6-channel checkpoint we compute ``pixel_values_videos`` +
9
- ``video_grid_thw`` client-side using ``Qwen3VLProcessorOurs`` and pass them via
10
- ``video_data=[{'format': 'processor_output', 'pixel_values_videos': ...,
11
- 'video_grid_thw': ...}]``. These dict items are recognized by
12
- ``base_processor.load_mm_data`` (base_processor.py:409-420) and forwarded to
13
- ``base_output.videos`` as-is. Only the ``preprocess_video`` call above trips
14
- on them — this subclass skips it and lets
15
- ``process_and_combine_mm_data`` (base_processor.py:1023-1042) handle the
16
- dict items through its ``collect_mm_items_from_processor_output`` branch.
17
-
18
- Register the containing package via the environment variable
19
- ``SGLANG_EXTERNAL_MM_PROCESSOR_PACKAGE=transvlm.models._sglang_patches``
20
- BEFORE calling ``sglang.Engine(...)`` — SGLang's tokenizer_manager scans the
21
- package for subclasses whose ``models`` class attribute lists the loaded
22
- ``hf_config.architectures`` entry (see
23
- ``sglang/srt/managers/tokenizer_manager.py:267-269``).
24
- """
25
- from __future__ import annotations
26
-
27
- import logging
28
- import time
29
-
30
- import torch
31
-
32
- from sglang.srt.layers.rotary_embedding import MRotaryEmbedding
33
- from sglang.srt.multimodal.processors.qwen_vl import (
34
- QwenVLImageProcessor,
35
- preprocess_video,
36
- )
37
-
38
- logger = logging.getLogger(__name__)
39
-
40
-
41
- class QwenVLImageProcessorOurs(QwenVLImageProcessor):
42
- """QwenVLImageProcessor variant that tolerates pre-decoded dict videos."""
43
-
44
- # SGLang dispatches by hf_config.architectures name -> processor via the
45
- # ``models`` list; ``import_processors(pkg, overwrite=True)`` at
46
- # tokenizer_manager.py:267-269 overrides the upstream registration
47
- # for the same class names.
48
- models = QwenVLImageProcessor.models
49
-
50
- async def process_mm_data_async(
51
- self,
52
- image_data,
53
- input_text,
54
- request_obj,
55
- *args,
56
- **kwargs,
57
- ):
58
- entry_time = time.perf_counter()
59
- base_output = self.load_mm_data(
60
- prompt=input_text,
61
- image_data=image_data,
62
- video_data=request_obj.video_data,
63
- audio_data=request_obj.audio_data,
64
- multimodal_tokens=self.mm_tokens,
65
- )
66
- load_time = time.perf_counter()
67
- rid = getattr(request_obj, 'rid', 'anonymous_rid')
68
-
69
- video_metadata = None
70
- if base_output.videos:
71
- processed_videos: list = []
72
- processed_metadata: list = []
73
- for video in base_output.videos:
74
- if isinstance(video, dict):
75
- # processor_output / precomputed_embedding passthrough.
76
- # Downstream process_and_combine_mm_data recognizes it
77
- # via its dict-handling branch (base_processor.py:1023).
78
- processed_videos.append(video)
79
- else:
80
- v, m = await preprocess_video(video, video_config=self.video_config)
81
- processed_videos.append(v)
82
- processed_metadata.append(m)
83
- base_output.videos = processed_videos
84
- video_metadata = processed_metadata if processed_metadata else None
85
-
86
- preprocess_time = time.perf_counter()
87
-
88
- # Below is verbatim from the upstream Qwen3VL branch.
89
- if self.hf_config.model_type in ('qwen3_vl', 'qwen3_vl_moe'):
90
- mm_items, input_ids, ret = self.process_and_combine_mm_data(
91
- base_output,
92
- self.mm_tokens,
93
- video_metadata=video_metadata,
94
- do_sample_frames=False,
95
- )
96
- else:
97
- mm_items, input_ids, ret = self.process_and_combine_mm_data(
98
- base_output, self.mm_tokens,
99
- )
100
-
101
- audio_feature_lengths = None
102
- if self.model_type == 'qwen3_omni_moe':
103
- audio_item = next((mm for mm in mm_items if mm.is_audio()), None)
104
- if audio_item:
105
- audio_feature_lengths = torch.sum(
106
- audio_item.feature_attention_mask, dim=1,
107
- )
108
-
109
- second_per_grid_ts = getattr(ret, 'second_per_grid_ts', None)
110
- if second_per_grid_ts is None:
111
- second_per_grid_ts = getattr(ret, 'video_second_per_grid', None)
112
-
113
- process_time = time.perf_counter()
114
-
115
- input_ids = input_ids.flatten()
116
-
117
- image_grid_thw = None
118
- if hasattr(ret, 'image_grid_thw'):
119
- image_grid_thw = ret.image_grid_thw
120
- if image_grid_thw is None and image_data and isinstance(image_data[0], dict):
121
- image_grid_thw = image_data[0].get('image_grid_thw')
122
-
123
- video_grid_thw = None
124
- if hasattr(ret, 'video_grid_thw'):
125
- video_grid_thw = ret.video_grid_thw
126
- if video_grid_thw is None and request_obj.video_data:
127
- first_video = request_obj.video_data[0]
128
- if isinstance(first_video, dict):
129
- video_grid_thw = first_video.get('video_grid_thw')
130
-
131
- mrope_positions, mrope_position_delta = MRotaryEmbedding.get_rope_index(
132
- spatial_merge_size=self.hf_config.vision_config.spatial_merge_size,
133
- image_token_id=self.mm_tokens.image_token_id,
134
- video_token_id=self.mm_tokens.video_token_id,
135
- vision_start_token_id=self.vision_start_token_id,
136
- model_type=self.model_type,
137
- tokens_per_second=getattr(
138
- self.hf_config.vision_config, 'tokens_per_second', None,
139
- ),
140
- input_ids=input_ids.unsqueeze(0),
141
- # DELIBERATE DIVERGENCE FROM UPSTREAM — the one place this subclass is not a
142
- # faithful transcription, and it is a bug fix.
143
- #
144
- # Upstream passes `getattr(ret, 'video_grid_thw', None)` here. On the
145
- # processor_output path `ret` is None (process_and_combine_mm_data returns None
146
- # when there are no raw videos to process), so upstream hands get_rope_index
147
- # video_grid_thw=None. get_rope_index then falls through to its text branch and
148
- # assigns plain sequential positions — roughly 12k video tokens lose Qwen3-VL's
149
- # 3D M-RoPE entirely. The grids computed just above are upstream's own dead code:
150
- # correct values that are never passed anywhere.
151
- #
152
- # This is silent rather than loud: the timestamps are also present as
153
- # `<x.xx seconds>` text in the prompt, so output stays plausible while position
154
- # encoding is wrong. Nothing downstream recomputes it — mrope_positions rides
155
- # MultimodalInputs straight into the model forward.
156
- image_grid_thw=image_grid_thw,
157
- video_grid_thw=video_grid_thw,
158
- second_per_grid_ts=second_per_grid_ts,
159
- use_audio_in_video=False,
160
- audio_seqlens=audio_feature_lengths,
161
- audio_token_id=getattr(self.hf_config, 'audio_token_id', None),
162
- audio_start_token_id=self.audio_start_token_id,
163
- position_id_per_seconds=getattr(
164
- self.hf_config, 'position_id_per_seconds', None,
165
- ),
166
- )
167
- mrope_positions = mrope_positions.squeeze(1)
168
- get_rope_index_time = time.perf_counter()
169
-
170
- logger.debug(
171
- '[QwenVLProcessorOurs Perf] rid=%s '
172
- 'load=%.2fms preprocess=%.2fms process=%.2fms rope=%.2fms total=%.2fms',
173
- rid,
174
- (load_time - entry_time) * 1000,
175
- (preprocess_time - load_time) * 1000,
176
- (process_time - preprocess_time) * 1000,
177
- (get_rope_index_time - process_time) * 1000,
178
- (get_rope_index_time - entry_time) * 1000,
179
- )
180
-
181
- return {
182
- 'input_ids': input_ids.tolist(),
183
- 'mm_items': mm_items,
184
- 'im_start_id': self.vision_start_token_id,
185
- 'im_end_id': self.vision_end_token_id,
186
- 'im_token_id': self.mm_tokens.image_token_id,
187
- 'video_token_id': self.mm_tokens.video_token_id,
188
- 'audio_token_id': self.mm_tokens.audio_token_id,
189
- 'mrope_positions': mrope_positions,
190
- 'mrope_position_delta': mrope_position_delta,
191
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
transvlm/models/_vllm_patches/__init__.py DELETED
@@ -1,29 +0,0 @@
1
- # Copyright © 2026 HeyGen
2
- # SPDX-License-Identifier: Apache-2.0
3
-
4
- """Runtime patches applied when running the vLLM backend.
5
-
6
- Two patches, vendored from the reference implementation:
7
-
8
- 1. ``sitecustomize.py`` — auto-loaded by Python at interpreter startup **when
9
- this directory is on PYTHONPATH**. Overrides vLLM's Qwen3-VL dummy video
10
- builder to produce 6-channel torch tensors (default 3 channels would break
11
- profiling of our 6-channel patch embedding). Only activates when
12
- ``VLLM_QWEN3VL_6CH_DUMMY=1``.
13
- 2. ``qwen3vl_processor_ours.py`` — replaces
14
- ``transformers.models.qwen3_vl.processing_qwen3_vl.Qwen3VLProcessor`` with
15
- a variant that handles the bare ``<|video_pad|>`` token vLLM passes at
16
- inference time.
17
-
18
- Callers must put this directory on ``sys.path`` **and** on ``PYTHONPATH`` before
19
- the first ``import vllm``, then ``import sitecustomize`` explicitly (the
20
- interpreter has already booted by then). ``PYTHONPATH`` is the load-bearing one:
21
- vLLM spawns its EngineCore workers, and only the environment survives a spawn.
22
- See ``infer_clips.py``.
23
-
24
- Both files are byte-identical to the reference implementation as of
25
- 2026-07-29 (sha256 ``444e9224…`` / ``d1385233…``, verified against
26
- Do not edit them here -- re-vendor from upstream instead, so the
27
- two copies cannot drift apart silently.
28
- """
29
- from __future__ import annotations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
transvlm/models/_vllm_patches/qwen3vl_processor_ours.py DELETED
@@ -1,210 +0,0 @@
1
- """
2
- Lightweight Qwen3VLProcessorOurs class for runtime monkey-patching.
3
-
4
- This module contains ONLY the custom processor class with minimal dependencies
5
- (only transformers and numpy) to avoid pulling in heavy modules like vLLM,
6
- PIL, torch.distributed, etc. when loaded at Python startup via sitecustomize.
7
-
8
- This is functionally identical to the class defined in
9
- the reference implementation, but extracted here
10
- so that sitecustomize.py (and vLLM spawned child processes) can import it
11
- without triggering the entire models_vllm.py import chain.
12
- """
13
-
14
- import logging
15
- from typing import Union
16
-
17
- import numpy as np
18
-
19
- from transformers.models.qwen3_vl.processing_qwen3_vl import (
20
- Qwen3VLProcessor,
21
- Qwen3VLProcessorKwargs,
22
- ImageInput,
23
- VideoInput,
24
- TextInput,
25
- PreTokenizedInput,
26
- Unpack,
27
- BatchFeature,
28
- )
29
-
30
- logger = logging.getLogger(__name__)
31
-
32
- # Track whether the fps warning has been emitted to avoid spamming logs
33
- _fps_warning_emitted = False
34
-
35
-
36
- class Qwen3VLProcessorOurs(Qwen3VLProcessor):
37
- """
38
- Custom Qwen3VL processor that handles video timestamp processing
39
- correctly for vLLM.
40
-
41
- Key difference from the base Qwen3VLProcessor:
42
- When replacing video tokens with per-frame timestamp placeholders,
43
- this class checks for BOTH the wrapped pattern
44
- (``<|vision_start|><|video_pad|><|vision_end|>``) and the bare
45
- ``<|video_pad|>`` token. vLLM may pass the bare token directly,
46
- which the upstream implementation does not handle.
47
- """
48
-
49
- def __call__(
50
- self,
51
- images: ImageInput = None,
52
- text: Union[
53
- TextInput,
54
- PreTokenizedInput,
55
- list[TextInput],
56
- list[PreTokenizedInput],
57
- ] = None,
58
- videos: VideoInput = None,
59
- **kwargs: Unpack[Qwen3VLProcessorKwargs],
60
- ) -> BatchFeature:
61
- """
62
- Prepare one or several sequence(s) and image(s) / video(s) for the
63
- model. See the base-class docstring for full parameter descriptions.
64
- """
65
- global _fps_warning_emitted
66
-
67
- logger.debug("Qwen3VLProcessorOurs __call__")
68
- output_kwargs = self._merge_kwargs(
69
- Qwen3VLProcessorKwargs,
70
- tokenizer_init_kwargs=self.tokenizer.init_kwargs,
71
- **kwargs,
72
- )
73
-
74
- # ----- image processing -----
75
- if images is not None:
76
- image_inputs = self.image_processor(
77
- images=images, **output_kwargs["images_kwargs"]
78
- )
79
- image_grid_thw = image_inputs["image_grid_thw"]
80
- else:
81
- image_inputs = {}
82
- image_grid_thw = None
83
-
84
- # ----- video processing -----
85
- if videos is not None:
86
- videos_inputs = self.video_processor(
87
- videos=videos, **output_kwargs["videos_kwargs"]
88
- )
89
- video_grid_thw = videos_inputs["video_grid_thw"]
90
- # If user has not requested video metadata, pop it
91
- if "return_metadata" not in kwargs:
92
- video_metadata = videos_inputs.pop("video_metadata")
93
- else:
94
- video_metadata = videos_inputs["video_metadata"]
95
- video_grid_thw = videos_inputs["video_grid_thw"]
96
- else:
97
- videos_inputs = {}
98
- video_grid_thw = None
99
-
100
- # ----- text token replacement -----
101
- if not isinstance(text, list):
102
- text = [text]
103
-
104
- text = text.copy() # below lines change text in-place
105
-
106
- # Replace image tokens with the correct number of placeholders
107
- if image_grid_thw is not None:
108
- merge_length = self.image_processor.merge_size ** 2
109
- index = 0
110
- for i in range(len(text)):
111
- while self.image_token in text[i]:
112
- num_image_tokens = (
113
- image_grid_thw[index].prod() // merge_length
114
- )
115
- text[i] = text[i].replace(
116
- self.image_token,
117
- "<|placeholder|>" * num_image_tokens,
118
- 1,
119
- )
120
- index += 1
121
- text[i] = text[i].replace("<|placeholder|>", self.image_token)
122
-
123
- # Replace video tokens with per-frame timestamp placeholders
124
- if video_grid_thw is not None:
125
- merge_length = self.video_processor.merge_size ** 2
126
- index = 0
127
- for i in range(len(text)):
128
- while self.video_token in text[i]:
129
- metadata = video_metadata[index]
130
- if metadata.fps is None:
131
- if not _fps_warning_emitted:
132
- logger.warning(
133
- "Qwen3VL requires frame timestamps to "
134
- "construct prompts, but the `fps` of the "
135
- "input video could not be inferred. Probably "
136
- "`video_metadata` was missing from inputs and "
137
- "you passed pre-sampled frames. Defaulting to "
138
- "`fps=24`. Please provide `video_metadata` "
139
- "for more accurate results."
140
- )
141
- _fps_warning_emitted = True
142
- metadata.fps = (
143
- 24 if metadata.fps is None else metadata.fps
144
- )
145
-
146
- # Calculate per-frame timestamps
147
- curr_timestamp = self._calculate_timestamps(
148
- metadata.frames_indices,
149
- metadata.fps,
150
- self.video_processor.merge_size,
151
- )
152
-
153
- video_placeholder = ""
154
- frame_seqlen = (
155
- video_grid_thw[index][1:].prod() // merge_length
156
- )
157
- for frame_idx in range(video_grid_thw[index][0]):
158
- curr_time = curr_timestamp[frame_idx]
159
- video_placeholder += f"<{curr_time:.2f} seconds>"
160
- video_placeholder += (
161
- self.vision_start_token
162
- + "<|placeholder|>" * frame_seqlen
163
- + self.vision_end_token
164
- )
165
-
166
- # Check for the fully-wrapped pattern first
167
- wrapped_pattern = (
168
- f"{self.vision_start_token}"
169
- f"{self.video_token}"
170
- f"{self.vision_end_token}"
171
- )
172
- if wrapped_pattern in text[i]:
173
- text[i] = text[i].replace(
174
- wrapped_pattern,
175
- video_placeholder,
176
- 1,
177
- )
178
- else:
179
- # vLLM may input the video token directly (bare)
180
- text[i] = text[i].replace(
181
- self.video_token, video_placeholder, 1
182
- )
183
- index += 1
184
-
185
- text[i] = text[i].replace(
186
- "<|placeholder|>", self.video_token
187
- )
188
-
189
- # ----- tokenization -----
190
- return_tensors = output_kwargs["text_kwargs"].pop(
191
- "return_tensors", None
192
- )
193
- return_mm_token_type_ids = output_kwargs["text_kwargs"].pop(
194
- "return_mm_token_type_ids", None
195
- )
196
- text_inputs = self.tokenizer(text, **output_kwargs["text_kwargs"])
197
- self._check_special_mm_tokens(
198
- text, text_inputs, modalities=["image", "video"]
199
- )
200
-
201
- if return_mm_token_type_ids:
202
- array_ids = np.array(text_inputs["input_ids"])
203
- mm_token_type_ids = np.zeros_like(text_inputs["input_ids"])
204
- mm_token_type_ids[array_ids == self.image_token_id] = 1
205
- text_inputs["mm_token_type_ids"] = mm_token_type_ids.tolist()
206
-
207
- return BatchFeature(
208
- data={**text_inputs, **image_inputs, **videos_inputs},
209
- tensor_type=return_tensors,
210
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
transvlm/models/_vllm_patches/sitecustomize.py DELETED
@@ -1,149 +0,0 @@
1
- """
2
- Runtime patches for vLLM + custom Qwen3VL (6-channel, custom processor).
3
-
4
- This file is automatically imported by Python at startup when its parent
5
- directory is on PYTHONPATH. It is designed to work with ``spawn``
6
- multiprocessing so that both the main process and EngineCore child processes
7
- apply the patches.
8
-
9
- Patches applied (when VLLM_QWEN3VL_6CH_DUMMY=1):
10
- 1. **6-channel dummy video**: Override vLLM's Qwen3VL dummy video builder
11
- to produce torch.Tensor inputs with the correct number of channels
12
- (default 6) so that memory profiling does not crash on the modified
13
- Qwen3VL patch embedding layer.
14
- 2. **Processor monkey patch**: Replace the upstream
15
- ``transformers.models.qwen3_vl.processing_qwen3_vl.Qwen3VLProcessor``
16
- with ``Qwen3VLProcessorOurs`` which correctly handles video timestamp
17
- placeholders when vLLM passes bare video tokens.
18
-
19
- Activation:
20
- Set the environment variable VLLM_QWEN3VL_6CH_DUMMY=1 to enable both
21
- patches. Without this variable (or with any other value), no patching
22
- occurs.
23
-
24
- Usage:
25
- PYTHONPATH=/path/to/runtime_patches:$PYTHONPATH \\
26
- VLLM_QWEN3VL_6CH_DUMMY=1 \\
27
- python your_script.py
28
- """
29
-
30
- import os
31
- import sys
32
-
33
- _ENABLE_FLAG = os.environ.get("VLLM_QWEN3VL_6CH_DUMMY", "0")
34
-
35
- if _ENABLE_FLAG == "1":
36
- try:
37
- import torch
38
-
39
- from vllm.multimodal import MULTIMODAL_REGISTRY
40
- from vllm.model_executor.models.qwen3_vl import (
41
- Qwen3VLForConditionalGeneration,
42
- Qwen3VLMultiModalProcessor,
43
- Qwen3VLProcessingInfo,
44
- Qwen3VLDummyInputsBuilder,
45
- )
46
-
47
- # Number of input channels expected by the modified model
48
- _NUM_CHANNELS = int(os.environ.get("VLLM_QWEN3VL_NUM_CHANNELS", "6"))
49
-
50
- class Qwen3VL6ChDummyInputsBuilder(Qwen3VLDummyInputsBuilder):
51
- """
52
- Overrides dummy video generation to produce **torch.Tensor**
53
- dummy videos in channels-first layout (T, C, H, W) with the
54
- correct number of channels (default 6).
55
-
56
- Using torch.Tensor instead of np.ndarray avoids the Transformers
57
- ``infer_channel_dimension_format`` path which only accepts 1 or 3
58
- channels for numpy arrays, thereby preventing the ValueError
59
- during vLLM's memory profiling pass.
60
- """
61
-
62
- def _get_dummy_videos(
63
- self,
64
- *,
65
- width: int,
66
- height: int,
67
- num_frames: int,
68
- num_videos: int,
69
- ):
70
- # Build channels-first torch tensor: (T, C, H, W)
71
- # RGB channels filled with 1.0, remaining channels (e.g. OF)
72
- # filled with 0.0. Using float32 to match typical processor
73
- # output dtype.
74
- video = torch.zeros(
75
- (num_frames, _NUM_CHANNELS, height, width),
76
- dtype=torch.float32,
77
- )
78
- # Fill RGB channels (first 3) with white (1.0)
79
- video[:, 3:, :, :] = 255.0
80
-
81
- video_items = []
82
- for _ in range(num_videos):
83
- video_metadata = {
84
- "fps": 2.0,
85
- "duration": num_frames / 2.0,
86
- "total_num_frames": num_frames,
87
- "frames_indices": list(range(num_frames)),
88
- "video_backend": "opencv",
89
- "do_sample_frames": False,
90
- }
91
- video_items.append((video.clone(), video_metadata))
92
- return video_items
93
-
94
- # Re-register the processor factory on the model class, replacing
95
- # only the dummy_inputs builder while keeping info and processor
96
- # identical to the upstream defaults.
97
- MULTIMODAL_REGISTRY.register_processor(
98
- Qwen3VLMultiModalProcessor,
99
- info=Qwen3VLProcessingInfo,
100
- dummy_inputs=Qwen3VL6ChDummyInputsBuilder,
101
- )(Qwen3VLForConditionalGeneration)
102
-
103
- print(
104
- f"[sitecustomize] Qwen3VL 6-channel dummy patch applied "
105
- f"(channels={_NUM_CHANNELS}, dtype=torch.float32, "
106
- f"layout=channels-first).",
107
- file=sys.stderr,
108
- )
109
-
110
- except ImportError:
111
- # vLLM is not installed in this environment; skip silently.
112
- pass
113
- except Exception as exc:
114
- print(
115
- f"[sitecustomize] WARNING: Failed to apply Qwen3VL 6-channel "
116
- f"dummy patch: {exc}",
117
- file=sys.stderr,
118
- )
119
-
120
- # ------------------------------------------------------------------
121
- # Patch 2: Replace transformers Qwen3VLProcessor with our custom
122
- # version that handles bare video tokens from vLLM correctly.
123
- # ------------------------------------------------------------------
124
- try:
125
- from qwen3vl_processor_ours import ( # noqa: E402
126
- Qwen3VLProcessorOurs,
127
- )
128
- import transformers.models.qwen3_vl.processing_qwen3_vl as _qwen3_vl_mod
129
-
130
- _qwen3_vl_mod.Qwen3VLProcessor = Qwen3VLProcessorOurs
131
-
132
- print(
133
- f"[sitecustomize] Qwen3VLProcessor monkey patch applied "
134
- f"(replaced with {Qwen3VLProcessorOurs.__name__}).",
135
- file=sys.stderr,
136
- )
137
-
138
- except ImportError as exc:
139
- print(
140
- f"[sitecustomize] WARNING: Could not apply Qwen3VLProcessor "
141
- f"monkey patch (import error): {exc}",
142
- file=sys.stderr,
143
- )
144
- except Exception as exc:
145
- print(
146
- f"[sitecustomize] WARNING: Failed to apply Qwen3VLProcessor "
147
- f"monkey patch: {exc}",
148
- file=sys.stderr,
149
- )