[TRTLLM-12427][perf] Qwen2.5/3/3.5-VL Performance Optimization#11943
Conversation
87e587d to
3489cf2
Compare
|
/bot run |
|
PR_Github #49687 [ run ] triggered by Bot. Commit: |
|
PR_Github #49687 [ run ] completed with state
|
3489cf2 to
124f649
Compare
|
/bot run |
|
PR_Github #50284 [ run ] triggered by Bot. Commit: |
|
PR_Github #50284 [ run ] completed with state
|
da59f84 to
657bb10
Compare
|
/bot run |
|
PR_Github #50450 [ run ] triggered by Bot. Commit: |
|
PR_Github #50450 [ run ] completed with state
|
Python 3's `/` is true division and always returns float, so `float(...) / float(...)` was redundant. Same effective values, less noise. Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Extract the ``_build_temporal_block`` step as a classmethod hook on ``Qwen2VLInputProcessorBase`` so Qwen3-VL's only meaningful difference (per-frame timestamps vs. ``tokens_per_second`` scaling) can be expressed as a one-line override. ``Qwen3VLInputProcessorBase`` now subclasses ``Qwen2VLInputProcessorBase``, overrides ``__init__`` (dtype source), ``get_rope_index`` (``repeat_interleave`` of ``video_grid_thw`` before super), and ``_build_temporal_block`` (plain ``np.indices``). Drops ~95% of the duplicated tokenizer / processor / mrope / call logic and the matching unused imports. Also dispatch ``get_mrope_config``'s ``get_rope_index`` call via ``type(self)`` so the subclass override is actually used, and condense the ``bypass_processor_output_validation`` rationale comment. Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank <161688079+yechank-nvidia@users.noreply.github.com>
|
/bot run |
|
PR_Github #53929 [ run ] triggered by Bot. Commit: |
|
PR_Github #53929 [ run ] completed with state
|
|
/bot run |
|
PR_Github #54018 [ run ] triggered by Bot. Commit: |
|
PR_Github #54018 [ run ] completed with state
|
|
/bot run |
|
PR_Github #54047 [ run ] triggered by Bot. Commit: |
|
PR_Github #54047 [ run ] completed with state |
…puts on non-spec / non-mrope path PR NVIDIA#11943 added per-request Python LOAD_ATTR overhead in the generation-input prep path. PR NVIDIA#15633 added three unconditional _wait_for_model_engine_input_copy() syncs in PyExecutor's run loop. For dense non-multimodal / non-spec-decode workloads (e.g. gpt-oss-120b disagg dep2 at con=2048), the aggregate host-side ticks explain a ~6% throughput regression on GB200. Changes (Python-only, no runtime behavior change on the affected path): * Snapshot request.py_batch_idx and request.max_beam_num_tokens once per generation-request iteration in _prepare_tp_inputs; read sites now pay LOAD_FAST instead of LOAD_ATTR. The write-site `request.py_batch_idx = request.py_seq_slot` still fires after the reads, so snapshot-before-write ordering is preserved. * Skip get_runtime_tokens_per_gen_step() call when self.enable_spec_decode is False (callee is `lambda _: 1` when spec_config is None). * Gate 3 _wait_for_model_engine_input_copy() calls in PyExecutor on self._is_kv_manager_v2. Pre-NVIDIA#15633 the legacy V1 scheduler ran without any such sync; the sync is only needed for V2's host page-table. Verified on Lyris GB200 in the auto-perf-fix-pro workflow, 3-rep median: test: disagg-e2e-gb200_gpt-oss-120b-fp4_1k1k_con2048_ctx1_tp1_gen1_dep2_eplb0_mtp0_ccb-UCX good median (15d06c0): 105905.45 tok/s bad median (c25fa74): 99217.72 tok/s ToT median (a0c406f): 98526.45 tok/s Fix median (this diff): 103082.72 tok/s (+4.62% over ToT, +3.90% over bad) Signed-off-by: Chenfei Zhang <chenfeiz@nvidia.com>
…e update to model device
Text-only startup of the Qwen3.6-27B dense VL checkpoint crashes during
KV-cache capacity estimation in the shared Qwen-VL mRoPE path
(`_prepare_qwen_vl_mrope_config`):
mrope_position_deltas_cache.index_copy_(0, seq_slots, deltas)
RuntimeError: Expected all tensors to be on the same device, but got
source is on cpu, different from other tensors on cuda:0
Root cause: checkpoints carrying `language_model_only: false` with the
dense `Qwen3_5ForConditionalGeneration` arch are routed to
`QwenImageBenchModel`, whose `multimodal_data_device_paths` was missing
the `mrope_config.mrope_position_ids` / `mrope_config.mrope_position_deltas`
entries that its sibling `_Qwen3_5VLModel` lists. The engine's H2D move
(`MultimodalParams.to_device` with `target_keywords`) therefore skips the
mRoPE tensors, leaving the deltas CPU-resident when the GPU seq-slot cache
write (introduced in NVIDIA#11943) consumes them. MoE VL checkpoints
(`Qwen3_5MoeForConditionalGeneration`) never match the image-bench route
and are unaffected.
Fix both layers:
- `QwenImageBenchModel.multimodal_data_device_paths`: add the two
`mrope_config.*` entries, matching `_Qwen3_5VLModel`, so mRoPE tensors
ride the engine's pinned async H2D move like every other Qwen-VL model.
- `_prepare_qwen_vl_mrope_config`: normalize `deltas` onto the cache
device before `index_copy_` as a defensive backstop for any path that
still reaches the write branch with CPU tensors (no-op when devices
already match).
Signed-off-by: nv-guomingz <137257613+nv-guomingz@users.noreply.github.com>

Summary
This PR reworks the Qwen2-VL / Qwen2.5-VL / Qwen3-VL PyTorch-backend implementations to cut host (CPU) overhead in the vision tower and input processors, enables piecewise CUDA graph for these models, and fixes several correctness issues around mRoPE and vision-block RoPE. The optimizations target the high-concurrency serving regime, where launch overhead and host↔device syncs dominate.
What changed
Vision tower / rotary embedding
cos/sininto init-time buffers so the forward path no longer calls.cos()/.sin()per step.Host-overhead reduction
async_tensor_h2dhelper and route all Qwen2.5/3-VL H2D copies (visionpos_ids,window_index,rope_position_ids) through it.maybe_pin_memorywhen the input is already pinned.CUDA graph
Refactor
Performance
Model:
Qwen3VLForConditionalGeneration(FP8 weights + KV cache, bf16 vision encoder). Hardware: H200 ×1. Workload: image+text, ISL=1000, OSL=1000, 512×512 image, KV block reuse off, 3-run mean. Comparison is against upstreammainwith identical serving config (max_batch_size=256,max_num_tokens=8192,num_postprocess_workers=4,cuda_graph_config.enable_padding=true, chunked prefill on).System output-token throughput (tok/s)
Other metrics
Gains are concentrated at high concurrency, where the host-side savings (fewer CPU launches, async H2D, higher CUDA-graph hit ratio) and the lower-overhead mRoPE path matter most. Low-concurrency TTFT also improves substantially (c=1 nearly halved) from the text fast path and removed vision-encoder syncs.
Summary by CodeRabbit
Release Notes
Bug Fixes
Performance
Tests