Skip to content

[None][fix] make bypass_processor_output_validation thread-safe - #14604

Closed
longlee0622 wants to merge 1 commit into
NVIDIA:mainfrom
longlee0622:fix/bypass-processor-validation-thread-safety
Closed

[None][fix] make bypass_processor_output_validation thread-safe#14604
longlee0622 wants to merge 1 commit into
NVIDIA:mainfrom
longlee0622:fix/bypass-processor-validation-thread-safety

Conversation

@longlee0622

@longlee0622 longlee0622 commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

bypass_processor_output_validation() in tensorrt_llm/_torch/models/modeling_multimodal_utils.py (introduced in #12829) is thread-unsafe. Preprocessing for multimodal serving is dispatched via asyncio.to_thread(self.llm.preprocess, ...), so multiple worker threads can sit inside this context manager concurrently. The original implementation captured each entering thread's view of validate_typed_dict as its "original" and installed a new wrapper on top — under high concurrency this stacks wrappers indefinitely, producing unbounded recursion and corrupting the restore on out-of-order exit.

The customer-reported symptom on Qwen3-VL / cosmos3-reasoner at 256-concurrency video serving was a recursion-limit traceback that returned HTTP 400 to the client; the traceback showed _filtered_validate repeating 968–975 times before reaching the real huggingface_hub.dataclasses.validate_typed_dict.

Fix

Reference-count the patch under a threading.Lock:

  • First entrant captures the true originals and installs the single shared wrapper.
  • Later entrants only bump a counter — no wrapper stacking, no re-snapshot of an already-patched function.
  • Last leaver restores. The wrapper itself is unchanged.

This also fixes the secondary "restore corruption" bug where Thread A's finally could restore the real function while Thread B's stale originals snapshot would then re-patch with a dangling wrapper.

Regression test

Added tests/unittest/_torch/modeling/test_bypass_processor_output_validation.py. Five tests, no GPU / no model weights needed:

  • test_originals_restored_after_exit — basic invariant.
  • test_filter_strips_output_keys — confirms _PROCESSOR_OUTPUT_KEYS are filtered before reaching the validator.
  • test_concurrent_entry_does_not_stack_wrappers — 64 threads enter behind a Barrier; all must observe the same wrapper object.
  • test_concurrent_entry_does_not_recurse — directly pins down the reporter's recursion symptom: walks sys._getframe() to count _filtered_validate frames; with the fix max(depths) == 1 regardless of concurrency.
  • test_sequential_reentry_restores_cleanly — guards the refcount state machine across repeated enter/exit.

Test plan

  • New unit tests added (tests/unittest/_torch/modeling/test_bypass_processor_output_validation.py)
  • Customer-reported repro: aiperf profile -m qwen3_vl --concurrency 256 ... --video-* against trtllm-serve no longer returns HTTP 400 with RecursionError
  • Existing test_modeling_multimodal.py paths that use bypass_processor_output_validation still pass

Background

Reported by Aswin Visva — fails the cosmos3-reasoner model at high video concurrency in the R16 TRT-LLM release. Suggest cherry-picking to the 1.3 release branch once merged.

Summary by CodeRabbit

  • Tests

    • Added regression test suite for validation handling, verifying correct behavior under concurrent usage, sequential re-entry, and multi-threading scenarios.
  • Improvements

    • Enhanced internal validation handling with improved thread-safety mechanisms to support concurrent operations reliably.

Review Change Stack

The context manager patches module-level validate_typed_dict on
transformers modules and restores on exit. Preprocessing is dispatched
via asyncio.to_thread, so multiple workers can sit inside this context
manager concurrently. Each entering thread captured the already-patched
function as its "original" and installed a new wrapper on top, producing
unbounded recursion (hundreds of nested _filtered_validate frames under
video concurrency) and corrupting the restore on out-of-order exit.

Reference-count the patch under a lock: the first entrant captures the
true originals and installs a single shared wrapper, later entrants only
bump a counter, and the last leaver restores. The wrapper itself is
unchanged.

Adds a unit test module exercising the concurrent-entry invariants
(shared wrapper identity, bounded recursion depth, clean restore).

Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

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: 8b65ad49-3354-4854-8208-8d1f27d3ec92

📥 Commits

Reviewing files that changed from the base of the PR and between c7e7fc5 and 82367d7.

📒 Files selected for processing (2)
  • tensorrt_llm/_torch/models/modeling_multimodal_utils.py
  • tests/unittest/_torch/modeling/test_bypass_processor_output_validation.py

📝 Walkthrough

Walkthrough

The bypass_processor_output_validation() context manager is updated from single-threaded operation to a thread-safe, reference-counted design using a module-level lock and depth counter. The wrapper function is installed and original validators saved only on the first entrant; subsequent concurrent entrants increment depth without re-installing. Original validators are restored only when depth returns to zero on exit. Comprehensive regression tests verify restoration, key filtering, lack of wrapper stacking, bounded recursion depth, and sequential re-entry correctness.

Changes

Thread-safe validator bypass mechanism

Layer / File(s) Summary
Thread-safe patching infrastructure
tensorrt_llm/_torch/models/modeling_multimodal_utils.py
threading import added; module-level _bypass_lock, _bypass_depth, and _bypass_saved_originals coordinate concurrent patch installation and restoration. Context manager docstring updated to describe reference-counted behavior. Core lock-protected logic installs wrapper and saves originals only on first entrant (depth 0→1), increments depth for nested/concurrent entries, and restores originals only when last entrant exits (depth→0).
Regression test suite
tests/unittest/_torch/modeling/test_bypass_processor_output_validation.py
Pytest module discovers transformers binder modules via _binders() helper and executes five regression tests: test_originals_restored_after_exit confirms validators are replaced inside the context and restored after; test_filter_strips_output_keys verifies _PROCESSOR_OUTPUT_KEYS are stripped from validator input; test_concurrent_entry_does_not_stack_wrappers ensures concurrent threads all observe the same wrapper object and originals are correctly restored; test_concurrent_entry_does_not_recurse asserts wrapper call-stack depth stays exactly 1 under concurrent threaded calls; test_sequential_reentry_restores_cleanly verifies clean restoration across multiple single-threaded enter/exit cycles.

Sequence Diagram

sequenceDiagram
  participant T1 as Thread 1
  participant T2 as Thread 2
  participant Lock as _bypass_lock
  participant State as Module State
  participant Binder

  T1->>Lock: acquire()
  Lock->>T1: locked
  T1->>State: check _bypass_depth == 0
  State->>T1: true
  T1->>Binder: save original validate_typed_dict
  T1->>State: save to _bypass_saved_originals
  T1->>Binder: install _filtered_validate wrapper
  T1->>State: set _bypass_depth = 1
  T1->>Lock: release()
  
  T2->>Lock: acquire()
  Lock->>T2: locked
  T2->>State: check _bypass_depth == 0
  State->>T2: false
  T2->>State: increment _bypass_depth to 2
  T2->>Lock: release()
  
  T1->>Binder: validator call (sees wrapper)
  T2->>Binder: validator call (same wrapper)
  
  T1->>Lock: acquire()
  Lock->>T1: locked
  T1->>State: decrement _bypass_depth to 1
  T1->>State: check if == 0
  State->>T1: false
  T1->>Lock: release()
  
  T2->>Lock: acquire()
  Lock->>T2: locked
  T2->>State: decrement _bypass_depth to 0
  T2->>State: check if == 0
  State->>T2: true
  T2->>Binder: restore original validate_typed_dict
  T2->>State: clear _bypass_saved_originals
  T2->>Lock: release()
Loading

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 clearly and specifically describes the main change: making the bypass_processor_output_validation function thread-safe, which is the core objective of this PR.
Description check ✅ Passed The PR description provides comprehensive context including issue summary, root cause, fix approach, regression tests, and test plan; it covers all template sections needed for a fix PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@longlee0622
longlee0622 marked this pull request as draft May 27, 2026 03:37
@longlee0622
longlee0622 deleted the fix/bypass-processor-validation-thread-safety branch July 18, 2026 00:35
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.

1 participant