Skip to content

[None][fix] Do not synthesize mrope_config for text-only prompts - #16874

Draft
lingjiew wants to merge 2 commits into
NVIDIA:mainfrom
lingjiew:user/lingjiew/fix-textonly-mrope-ipc-leak
Draft

[None][fix] Do not synthesize mrope_config for text-only prompts#16874
lingjiew wants to merge 2 commits into
NVIDIA:mainfrom
lingjiew:user/lingjiew/fix-textonly-mrope-ipc-leak

Conversation

@lingjiew

@lingjiew lingjiew commented Jul 26, 2026

Copy link
Copy Markdown

[None][fix] Do not synthesize mrope_config for text-only prompts

Summary

Qwen2VLInputProcessorBase.call_with_text_prompt() has a text-only fast path that
skips the multimodal HF processor but still calls get_mrope_config(), reasoning
that "the LM is M-RoPE". For a prompt with no images or video that work is
redundant, and it has an unbounded side effect under disaggregated serving: the
context worker permanently strands one (3, 1, prompt_len) int64 tensor per
scheduled context request
.

On Qwen3.5-397B-A17B over long-context agentic traces this is 24 bytes per prompt
token per scheduling event — 16.6 MiB/s of unrecoverable device memory, and the
context worker exhausts a GB300 in 2,300–3,500 s depending on KV-pool headroom.

Why it is redundant

PyTorchModelEngine._prepare_tp_inputs already establishes the text-only answer
itself:

# Broadcast [N] to [3,1,N]: default for text-only tokens.
self.mrope_position_ids_cuda[:, :, :total_num_tokens].copy_(
    self.position_ids_cuda[:total_num_tokens].view(1, 1, -1).expand(3, 1, -1), ...)
# Overwrite multimodal spans with per-axis MRoPE positions.
for start_idx, end_idx, segment in mrope_position_ids:
    ...

The per-axis positions only ever overwrite multimodal spans. With no such spans
there is nothing to overwrite, so the synthesized mrope_config reproduces a
value the engine has already written.

Verified numerically rather than argued — get_rope_index() on text-only input,
across four prompt lengths:

assertion result
three axes identical pass
equals arange(L) broadcast to [3,1,L] pass
mrope_position_deltas == 0 pass
dtype int64 (24 B/token) pass

deltas == 0 also covers the generation phase: with no mrope_position_delta
the engine falls back to past_seen_token_num, which is exactly
past_seen_token_num + 0.

Why it leaks

Populating multimodal_data makes MultimodalParams.has_content() true, which
opens the EPD mRoPE re-registration for context-only requests in
model_engine.py:

if (request.is_context_only_request and _use_mrope and
        "mrope_config" in multimodal_params.multimodal_data):
    request.py_result.set_mrope_position(_mrope_position_ids.clone(),
                                         _mrope_position_deltas.clone())

set_mrope_position wraps both clones in SharedTensorContainer.from_tensor(),
i.e. reduce_tensor()storage._share_cuda_(), which replaces the storage's
DataPtr with a CudaIPCSentData owning the original. The block does not return
to the caching allocator when the Python tensor dies; it enters
CudaIPCSentDataLimbo and is freed only once a consumer opens and drops the
handle. shared_tensor.py states the contract directly:

Whenever you call reduce_tensor, you must call the corresponding rebuild method
at consumer process(es), otherwise, the producer process cannot release the
memory to caching allocator as the inner refcount never reaches zero.

Under trtllm-serve disaggregation no consumer can exist: the serve-layer
DisaggregatedParams carries no mRoPE handle fields — see the existing
TODO(TRTLLM-12407) in serve/openai_protocol.py. The handle is dropped at the
orchestrator, so every export is a permanent pin.

Evidence

CUDA allocator snapshots (torch.cuda.memory._record_memory_history) taken 900 s
apart on the context worker, before and after this change. Same image, same
workload, same config.

before after
device growth over the 900 s window +10,037 MiB +10 MiB
live blocks at the mrope_position_ids clone 828 → 4,247 0 → 0
live blocks at the mrope_position_deltas clone 828 → 4,247 0 → 0
live blocks, whole process 4,631 → 11,470 2,974 → 2,971
steady-state slope 16.6 MiB/s ~0
context-worker OOM yes no

The two clone sites accounted for 100% of the process's live-block growth
(+6,838 of +6,839) in exact 1:1 pairing, with block sizes spread over 1,474
distinct values from 0.007 to 6.740 MiB — i.e. 24 bytes × prompt_len, mean
2.44 MiB.

Throughput, prefix-cache hit rate and startup KV-pool sizing are unchanged
(8.33 req/s, 95.6%, 168.20 GiB); the benchmark completed with zero errors.

With the fix the affected configuration also reproduces its pre-regression
baseline. A full 3600 s disaggregated run at the same operating point as a known
good build from before this path started firing — same parallelism, batch sizes,
sequence lengths, KV-pool fraction and client concurrency:

known good build this build + fix
request throughput 8.17 req/s 8.13 req/s
request error rate 0.02% 0.01%
requests completed 29,666 29,507
TTFT mean 2,247 ms 2,210 ms
theoretical prefix cache hit 95.64% 95.64%

Every metric is within run-to-run noise, and the context worker no longer runs
out of memory. Without the fix the same configuration cannot finish the window.

Scope

This does not close the underlying defect for genuine multimodal traffic: a
request that really carries images over trtllm-serve disaggregation would still
export mRoPE handles that nothing can rebuild. That needs the protocol fields
tracked by TRTLLM-12407. This change removes the text-only case, which is the one
that fires on 100% of requests for a text workload.

Test coverage

tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py (new).

test_text_only_prompt_returns_no_extra_processed_inputs asserts that
call_with_text_prompt returns extra_processed_inputs is None and that
get_mrope_config and _preprocess are both never called. It is parametrized
over the three falsy shapes of multi_modal_data (key absent / None / {})
and over the three processor classes that inherit this fast path
(Qwen2VLInputProcessorBase, Qwen2_5VLInputProcessorBase,
Qwen3VLInputProcessorBase), so a subclass reintroducing the export is caught
too.

test_multimodal_prompt_still_takes_the_full_path is the complement: with
non-empty multi_modal_data the full path still runs and still emits
mrope_config, so the fast path cannot be widened into the multimodal case.
It passes both before and after this change and is not the regression guard.

Both are CPU-only and need no model weights or network; the processor is built
with object.__new__ plus a fake tokenizer, the same technique the neighbouring
test_qwen3vl_disagg_prompt.py already uses. The file needs no test-list entry
because tests/integration/test_lists/test-db/l0_h100.yml collects
unittest/_torch/multimodal as a directory.

Against a revert of this PR's diff, both guard assertions in the first test fail,
in all nine parametrizations.

Accuracy has not been re-measured on non-disaggregated text-only inference for
other Qwen-VL models sharing this processor; happy to run whatever the reviewers
consider sufficient.

Dev Engineer Review

  • Updated tensorrt_llm/_torch/models/modeling_qwen2vl.py (Qwen2VLInputProcessorBase.call_with_text_prompt()): for the text-only fast path (multi_modal_data empty/falsy), the implementation now avoids computing/synthesizing mrope_config and does not return it via extra["multimodal_data"]. It directly tokenizes the prompt and returns only input_ids with no processed multimodal inputs produced.
  • Maintains correctness/performance rationale: prevents disaggregated-serving CUDA IPC/context-only retention triggered by synthesized mRoPE state; aligned with PyTorchModelEngine._prepare_tp_inputs, which already supplies default 3-axis M-RoPE position IDs for text-only inputs.
  • Preserves multimodal behavior: when multimodal inputs are present, the function continues through the full multimodal preprocessing path and extra["multimodal_data"]["mrope_config"] is still produced.
  • Scope note: change is intentionally for text-only/context-only requests; broader multimodal protocol requirements remain tracked separately (e.g., TRTLLM-12407).

QA Engineer Review

  • Added unit tests in tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py:
    • New helpers: _FakeTokenizer, _make_processor(processor_cls)
    • New tests:
      • test_text_only_prompt_returns_no_extra_processed_inputs(processor_cls, inputs) covering text-only handling across Qwen2VLInputProcessorBase, Qwen2_5VLInputProcessorBase, Qwen3VLInputProcessorBase with falsy multi_modal_data (missing, None, {}), asserting:
        • extra is None
        • returned token_ids match expected prompt token IDs
        • get_mrope_config and _preprocess are not called
      • test_multimodal_prompt_still_takes_the_full_path() verifying that non-empty multimodal inputs still run the full preprocessing path and produce extra["multimodal_data"]["mrope_config"] (including expected mrope_position_ids / mrope_position_deltas tensor shapes).
  • Integration/manual test list coverage:
    • Searched tests/integration/test_lists for test_qwen2vl_text_only_prompt / qwen2vl_text_only_prompt; no matching entries found (no test-list updates).
  • Verdict: needs follow-up

@lingjiew
lingjiew marked this pull request as ready for review July 27, 2026 02:56
@lingjiew
lingjiew requested a review from a team as a code owner July 27, 2026 02:56
@lingjiew
lingjiew requested a review from yechank-nvidia July 27, 2026 02:56
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fd10fe0c-38c5-4237-9fc1-81c9b6f7bf28

📥 Commits

Reviewing files that changed from the base of the PR and between 3b23dcd and 2945612.

📒 Files selected for processing (2)
  • tensorrt_llm/_torch/models/modeling_qwen2vl.py
  • tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tensorrt_llm/_torch/models/modeling_qwen2vl.py
  • tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py

Walkthrough

The Qwen2VL text-only prompt path now returns tokenized input IDs with no processed multimodal metadata. New tests cover absent, null, and empty multimodal inputs across Qwen VL processors and verify multimodal prompts retain their existing processing.

Changes

Qwen2VL text-only prompt handling

Layer / File(s) Summary
Prompt path handling
tensorrt_llm/_torch/models/modeling_qwen2vl.py
Text-only prompts return tokenized input_ids and None without generating attention masks or mRoPE configuration.
Prompt path validation
tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py
Tests cover absent, None, and empty multimodal data across Qwen VL processors, and verify that multimodal processing produces the expected outputs and mRoPE tensor shapes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: yechank-nvidia

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the required template and clearly describes the main change: skipping mrope_config synthesis for text-only prompts.
Description check ✅ Passed The description covers the issue, solution, and test coverage in detail and is broadly aligned with the repository template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tensorrt_llm/_torch/models/modeling_qwen2vl.py (1)

869-879: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a regression test for the text-only fast path.

Please verify that call_with_text_prompt() returns (input_ids, None) and does not call get_mrope_config() when multi_modal_data is empty. The supplied downstream test only verifies that None is tolerated, not that this memory-retention fix remains in place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tensorrt_llm/_torch/models/modeling_qwen2vl.py` around lines 869 - 879, Add a
regression test covering call_with_text_prompt() when multi_modal_data is empty:
assert it returns the tokenized input_ids with None for multimodal
configuration, and mock or spy on get_mrope_config() to assert it is not called.
Keep the test focused on preserving the text-only fast path and its
memory-retention behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tensorrt_llm/_torch/models/modeling_qwen2vl.py`:
- Around line 869-879: Add a regression test covering call_with_text_prompt()
when multi_modal_data is empty: assert it returns the tokenized input_ids with
None for multimodal configuration, and mock or spy on get_mrope_config() to
assert it is not called. Keep the test focused on preserving the text-only fast
path and its memory-retention behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1dec3717-ab8b-4ab7-8135-8c52717af6a9

📥 Commits

Reviewing files that changed from the base of the PR and between cf44a1c and ff636be.

📒 Files selected for processing (1)
  • tensorrt_llm/_torch/models/modeling_qwen2vl.py

@lingjiew
lingjiew force-pushed the user/lingjiew/fix-textonly-mrope-ipc-leak branch from 7ad0136 to d0fdde8 Compare July 27, 2026 04:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py (1)

20-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the required function annotations.

_FakeTokenizer methods, _make_processor, and both tests are unannotated. Add precise parameter and return annotations, including -> None for the tests. As per coding guidelines, “Annotate every function.”

Also applies to: 28-35, 55-55, 82-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py` around
lines 20 - 25, Annotate every function in this test module, including the
_FakeTokenizer constructor and __call__, _make_processor, and both test
functions. Add precise parameter and return types based on their existing usage,
and explicitly use -> None for the test methods.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py`:
- Around line 38-114: Add the relevant test-list entry for
test_text_only_prompt_returns_no_extra_processed_inputs and
test_multimodal_prompt_still_takes_the_full_path in the appropriate
tests/integration/test_lists/test-db or QA list, ensuring these Qwen VL
text-only and multimodal coverage tests are included in CI or manual QA
execution.

---

Nitpick comments:
In `@tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py`:
- Around line 20-25: Annotate every function in this test module, including the
_FakeTokenizer constructor and __call__, _make_processor, and both test
functions. Add precise parameter and return types based on their existing usage,
and explicitly use -> None for the test methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fae816ae-0c42-4b2a-9905-ab6a94bf0365

📥 Commits

Reviewing files that changed from the base of the PR and between ff636be and 7ad0136.

📒 Files selected for processing (1)
  • tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py

Comment thread tests/unittest/_torch/multimodal/test_qwen2vl_text_only_prompt.py
@lingjiew
lingjiew force-pushed the user/lingjiew/fix-textonly-mrope-ipc-leak branch from d0fdde8 to 3b23dcd Compare July 27, 2026 04:04
lingjiew added 2 commits July 27, 2026 12:09
Qwen2VLInputProcessorBase.call_with_text_prompt has a text-only fast path that
skips the multimodal HF processor but still calls get_mrope_config(), on the
reasoning that the LM is M-RoPE. For a prompt with no images or video that work
is redundant: PyTorchModelEngine._prepare_tp_inputs already establishes the
text-only answer itself by broadcasting position_ids to [3, 1, N], and the
per-axis positions only ever overwrite multimodal spans. get_rope_index() on
text-only input returns exactly that broadcast, with a delta of 0.

It is also unbounded under disaggregated serving. Populating multimodal_data
makes MultimodalParams.has_content() true, which opens the EPD mRoPE
re-registration for context-only requests. That path exports both tensors via
SharedTensorContainer.from_tensor() -> reduce_tensor() -> _share_cuda_(), after
which the block sits in CudaIPCSentDataLimbo until a consumer opens and drops
the handle. Over trtllm-serve disaggregation no consumer exists -- the
serve-layer DisaggregatedParams has no mRoPE handle fields, see the existing
TODO(TRTLLM-12407) -- so every context request permanently strands one
(3, 1, prompt_len) int64 tensor.

Measured on Qwen3.5-397B-A17B over long-context agentic traces: 24 bytes per
prompt token per scheduled context request, 16.6 MiB/s of unrecoverable growth,
context worker OOM in 2,300-3,500 s. CUDA allocator snapshots 900 s apart show
the two clone sites going from 828 -> 4,247 live blocks each (100% of the
process's live-block growth) to 0 -> 0 with this change, device growth over the
window falling from +10,037 MiB to +10 MiB, with throughput, prefix-cache hit
rate and startup KV-pool sizing unchanged.

This does not close the defect for genuine multimodal traffic over trtllm-serve,
which still needs the protocol fields tracked by TRTLLM-12407; it removes the
text-only case, which fires on every request of a text workload.

Signed-off-by: lingjiew <lingjiew@nvidia.com>
Assert that call_with_text_prompt emits no extra processed inputs and never
synthesizes an mrope_config when a prompt carries no multimodal data, across
the three falsy shapes of multi_modal_data and all three processor classes
that inherit the fast path.

Regressing this is silent: every functional test still passes, and the only
symptom is the CUDA-IPC mRoPE export for context-only requests pinning device
memory until the context worker runs out under disaggregated serving.

Signed-off-by: lingjiew <lingjiew@nvidia.com>
@lingjiew
lingjiew force-pushed the user/lingjiew/fix-textonly-mrope-ipc-leak branch from 3b23dcd to 2945612 Compare July 27, 2026 04:10
@lingjiew
lingjiew marked this pull request as draft July 27, 2026 05:50
@@ -866,18 +866,17 @@ def call_with_text_prompt(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked offline. Need to consider mixed modality requests(e.g. text request following with multimodal requests)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants