Spaces:
Running on Zero
Running on Zero
drop unused vllm/sglang paths and stale example clips
Browse files- examples/autoshot_test.mp4 +0 -3
- examples/fast_cut.mp4 +0 -3
- examples/movieshots2.mp4 +0 -3
- examples/vdb_clip.mp4 +0 -3
- transvlm/inference/sglang_clip_engine.py +0 -444
- transvlm/models/_sglang_patches/__init__.py +0 -20
- transvlm/models/_sglang_patches/qwen_vl_processor_6ch.py +0 -191
- transvlm/models/_vllm_patches/__init__.py +0 -29
- transvlm/models/_vllm_patches/qwen3vl_processor_ours.py +0 -210
- transvlm/models/_vllm_patches/sitecustomize.py +0 -149
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 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|