Skip to content

[None][feat] Enable PyTorch profiler traces for VisualGen - #16814

Open
chang-l wants to merge 12 commits into
NVIDIA:mainfrom
chang-l:agent/visual-gen-torch-profiler
Open

[None][feat] Enable PyTorch profiler traces for VisualGen#16814
chang-l wants to merge 12 commits into
NVIDIA:mainfrom
chang-l:agent/visual-gen-torch-profiler

Conversation

@chang-l

@chang-l chang-l commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Dev Engineer Review

  • Added optional VisualGen PyTorch profiler trace capture when TLLM_TORCH_PROFILE_TRACE is set, gated/scoped by TLLM_PROFILE_VISUAL_GEN_START_STOP.
    • Supports all, predenoise, postdenoise, and numeric denoise-step range selections.
    • Warmup requests do not produce trace windows.
    • Exports per-rank (and per profiling window) Chrome trace files with CPU + available accelerator activity and record_shapes.
    • Ensures robust cleanup on inference exceptions, CUDA sync failures, and Chrome trace export failures (window closure and internal profiler state reset).
    • Multi-stage behavior: postdenoise is positioned to cover the correct denoise boundary for pipelines with multiple stages (may extend beyond VAE decode when later stages run).
  • Introduced VisualGenProfiler (tensorrt_llm/_torch/visual_gen/profiler.py) and refactored VisualGen pipeline profiling to use it.
    • BasePipeline.run_inference() now wraps non-warmup requests in request_scope() and centralizes denoise-step windowing via BasePipeline._profile_denoise_steps(...).
    • Removed previous env-var-driven / one-shot CUDA profiler start-stop window logic from tensorrt_llm/_torch/visual_gen/pipeline.py.
    • Denoise loops were updated to drive profiling window boundaries through self._profile_denoise_steps(timesteps) / step-range iterators.
  • Updated model pipelines to integrate the new profiling step-window wrapper:
    • Qwen-Image denoise loops (pipeline_qwen_image.py, pipeline_qwen_image_edit.py, pipeline_qwen_image_layered.py).
    • LTX-2 Stage 2 refinement denoise loop (pipeline_ltx2_two_stages.py) so Stage 2 is the one driving the profiling capture window(s).
  • Updated executor/test scaffolding and inference call sites to use pipeline.run_inference() rather than pipeline.infer().
    • Adjusted failure-path assertions and stub pipeline method availability accordingly.
  • Documentation updated (docs/source/developer-guide/perf-analysis.md) with VisualGen-specific instructions, including how TLLM_PROFILE_VISUAL_GEN_START_STOP maps to phases/ranges and trace export behavior (per-rank/per-window suffixing).

QA Engineer Review

Test files modified

  • tests/unittest/_torch/visual_gen/test_profiler.py
  • tests/unittest/_torch/visual_gen/test_qwen_image_pipeline.py
  • tests/unittest/_torch/visual_gen/test_visual_gen_params.py
  • tests/unittest/visual_gen/test_executor_shared_tensor_ipc.py
  • tests/integration/test_lists/test-db/l0_a10.yml

Test functions added/updated

tests/unittest/_torch/visual_gen/test_profiler.py

  • Added/expanded:
    • test_setup_torch_profiler
    • test_setup_torch_profiler_requires_profile_range
    • test_window_controls_torch_profiler
    • test_close_window_syncs_before_ending_capture
    • test_close_window_stops_collectors_when_sync_fails
    • test_close_window_is_a_noop_when_never_opened
    • test_each_window_uses_a_fresh_trace_file
    • test_cuda_gate_closes_when_trace_export_fails
    • test_request_scope_owns_phase_boundaries (parameterized)
    • test_request_scope_closes_window_when_inference_raises
    • test_single_shot_phases_do_not_rearm
    • test_step_windows_follow_numeric_ranges
    • test_numeric_window_does_not_outlive_a_short_denoise_loop
    • test_run_inference_skips_profiling_during_warmup
    • test_every_denoise_loop_is_instrumented (AST structural guard for denoise loops)
  • Coverage expectation: exercised in CI via l0_a10.yml (see below) and should validate trace export/gating and lifecycle semantics.

tests/unittest/_torch/visual_gen/test_qwen_image_pipeline.py

  • Added: test_forward_honors_profile_step_range

tests/unittest/_torch/visual_gen/test_visual_gen_params.py

  • Updated: error-origin expectations to align with pipeline.run_inference().

tests/unittest/visual_gen/test_executor_shared_tensor_ipc.py

  • Updated: stub pipeline gained run_inference(req) delegating to existing infer(req).

Test-list coverage

  • tests/integration/test_lists/test-db/l0_a10.yml
    • Under # visual_gen, includes unittest/_torch/visual_gen/test_profiler.py (added entry per PR change summary).

Verdict: sufficient — VisualGen profiler unit tests are registered in l0_a10.yml, so the added profiling lifecycle and trace-export behavior should be exercised in CI.

++ BrianLi23 for viz

Description

VisualGen already uses TLLM_PROFILE_VISUAL_GEN_START_STOP to gate Nsight Systems capture, but it could not emit PyTorch/Kineto traces like the LLM executor.

This change starts an optional torch.profiler.profile session on the same VisualGen ranges when TLLM_TORCH_PROFILE_TRACE is set:

  • all now covers the complete user request from text encoding through denoising and VAE decode.
  • predenoise covers request start through the denoise-loop boundary; postdenoise covers that boundary through request completion.
  • Numeric ranges continue to select denoise steps per request.
  • A shared BasePipeline.run_inference() wrapper owns request-level start/stop and exception cleanup. Base denoise and Qwen-Image's custom true-CFG loop call the same semantic phase/step hooks, so Qwen does not duplicate profiler setup or teardown.
  • Warmups are excluded. Each rank and repeated capture window writes a distinct Chrome trace file.
  • The trace records CPU plus the available accelerator activity and tensor shapes. There are no public API or dependency changes.

cudaProfilerStart()/cudaProfilerStop() remain capture gates for an attached Nsight collector; they do not start Nsight on their own. Torch and Nsight collectors should be run in separate invocations because both use CUPTI.

Test Coverage

  • Added profiler lifecycle coverage for setup, missing-range behavior, all/pre/post request boundaries, warmup deferral, repeated non-overwriting windows, and export-failure cleanup.
  • Added Qwen-Image numeric-range coverage and registered test_profiler.py in l0_a10.yml.
  • Ran pre-commit on all nine changed files; all hooks passed.
  • On a shared B200 node, focused pytest coverage passed: 13 passed, 38 warnings in 143.25s.
  • On the same B200, Wan 2.1 T2V 1.3B completed a 480x832, 17-frame request with exactly 5 denoise steps and error=None using TLLM_PROFILE_VISUAL_GEN_START_STOP=all.
  • The exported 47,756,268-byte trace is valid JSON with 128,681 events, including text-encoder embedding ops, 15,250 GPU kernels across the full request, and VAE-decode convolution ops after denoising.
  • A separate five-step Nsight Systems run also completed with exit code 0 and observed the same application capture start/stop gates.

PR Checklist

  • PR description clearly explains what and why.

  • PR follows the TRT-LLM coding guidelines.

  • Test cases are provided for the new code paths and are routed in the test database.

  • No public API changes or new dependencies.

  • Documentation is updated.

  • No CODEOWNERS or architecture-diagram change is required.

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, comment /bot help.

@chang-l
chang-l marked this pull request as ready for review July 23, 2026 23:31
@chang-l
chang-l requested review from a team as code owners July 23, 2026 23:31
@coderabbitai

coderabbitai Bot commented Jul 23, 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

Walkthrough

VisualGen profiling now supports environment-controlled PyTorch traces coordinated with CUDA profiler ranges, denoising-phase hooks, rank- and window-specific trace output, executor integration through run_inference, expanded tests, and updated profiling guidance.

Changes

VisualGen profiling and inference integration

Layer / File(s) Summary
Profiling configuration and initialization
tensorrt_llm/_torch/visual_gen/profiler.py
Adds profiling range parsing, optional torch.profiler setup, and rank- and window-specific trace paths.
Profiler lifecycle and denoising integration
tensorrt_llm/_torch/visual_gen/profiler.py, tensorrt_llm/_torch/visual_gen/pipeline.py, tensorrt_llm/_torch/visual_gen/models/...
Coordinates request, denoise-step, cleanup, Qwen Image, and LTX-2 Stage 2 profiling windows.
Inference entrypoint and profiling boundaries
tensorrt_llm/_torch/visual_gen/executor.py, tests/unittest/visual_gen/test_executor_shared_tensor_ipc.py, tests/unittest/_torch/visual_gen/test_visual_gen_params.py
Routes executor requests through run_inference and updates stubs and failure assertions.
Profiling validation and usage guidance
tests/unittest/_torch/visual_gen/test_profiler.py, tests/unittest/_torch/visual_gen/test_qwen_image_pipeline.py, tests/integration/test_lists/test-db/l0_a10.yml, docs/source/developer-guide/perf-analysis.md
Validates profiler setup, lifecycle ordering, cleanup, step ranges, warmup behavior, loop instrumentation, Qwen Image integration, test-list wiring, and documented usage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DiffusionExecutor
  participant BasePipeline
  participant VisualGenProfiler
  participant CUDAProfiler
  participant TorchProfiler
  DiffusionExecutor->>BasePipeline: Call run_inference(req)
  BasePipeline->>VisualGenProfiler: Enter request_scope()
  VisualGenProfiler->>CUDAProfiler: Start selected window
  VisualGenProfiler->>TorchProfiler: Start tracing
  BasePipeline->>VisualGenProfiler: Iterate denoise steps
  VisualGenProfiler->>CUDAProfiler: Stop profiling window
  VisualGenProfiler->>TorchProfiler: Export Chrome trace
  BasePipeline-->>DiffusionExecutor: Return PipelineOutput
Loading

Possibly related PRs

Suggested labels: api-compatible

Suggested reviewers: qijune

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.93% 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 is concise and accurately summarizes the main change: enabling PyTorch profiler traces for VisualGen.
Description check ✅ Passed The description follows the template and includes clear sections for the change, test coverage, and checklist, with mostly complete details.
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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tensorrt_llm/_torch/visual_gen/pipeline.py (1)

191-212: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Annotate profiler lifecycle methods.

Add -> None to both methods.

-    def _cuda_profiler_start(self):
+    def _cuda_profiler_start(self) -> None:
...
-    def _cuda_profiler_stop(self):
+    def _cuda_profiler_stop(self) -> None:

As per coding guidelines, “Annotate every function, use None for non-returning functions.”

🤖 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/visual_gen/pipeline.py` around lines 191 - 212, Add the
`-> None` return annotation to both `_cuda_profiler_start` and
`_cuda_profiler_stop`, preserving their existing profiler lifecycle behavior.

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 `@tensorrt_llm/_torch/visual_gen/pipeline.py`:
- Around line 178-179: Update the trace path construction around
self._torch_profile_trace_path so each profiling window receives a unique output
filename instead of reusing the same rank-based path. Add and maintain a window
index across repeated A-B,C-D profiling stops, incorporating it into the
exported trace path while preserving the existing rank and extension components.

In `@tests/unittest/_torch/visual_gen/test_profiler.py`:
- Around line 17-88: The profiler tests cover only a single start/stop window;
add a test for multiple configured ranges such as 0-1 and 3-4, asserting the
corresponding profiling behavior across both windows. Also register
tests/unittest/_torch/visual_gen/test_profiler.py in the relevant test-db and QA
test-list files, preserving their existing list format.

---

Nitpick comments:
In `@tensorrt_llm/_torch/visual_gen/pipeline.py`:
- Around line 191-212: Add the `-> None` return annotation to both
`_cuda_profiler_start` and `_cuda_profiler_stop`, preserving their existing
profiler lifecycle behavior.
🪄 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: ce070dc7-3ebc-4306-babd-3cbb37cc95e9

📥 Commits

Reviewing files that changed from the base of the PR and between 80b4eb3 and d787ad9.

📒 Files selected for processing (3)
  • docs/source/developer-guide/perf-analysis.md
  • tensorrt_llm/_torch/visual_gen/pipeline.py
  • tests/unittest/_torch/visual_gen/test_profiler.py

Comment thread tensorrt_llm/_torch/visual_gen/pipeline.py Outdated
Comment thread tests/unittest/_torch/visual_gen/test_profiler.py Outdated

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tensorrt_llm/_torch/visual_gen/pipeline.py (2)

181-190: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Select the profiler activity at runtime. torch.profiler.profile() should not be given both CUDA and XPU unconditionally; on builds that only support one accelerator backend, profiler setup can fail. Build the list from the supported activities or branch on the active device so the trace requests only CPU + the matching accelerator.

🤖 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/visual_gen/pipeline.py` around lines 181 - 190, Update
the profiler setup around self._torch_profiler and activities to select only the
accelerator activity supported by the active device or build, always retaining
CPU while excluding the incompatible CUDA/XPU activity. Pass the resulting
CPU-plus-matching-accelerator list to torch.profiler.profile.

189-189: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

with_modules=True won’t record module hierarchy here — VisualGen uses eager nn.Modules, so the profiler trace won’t include module info unless this path is scripted/traced or you add explicit instrumentation.

🤖 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/visual_gen/pipeline.py` at line 189, The profiler
configuration using with_modules=True does not capture hierarchy for VisualGen’s
eager nn.Module execution. Remove this ineffective option or replace it with
explicit module instrumentation, ensuring module hierarchy is recorded only
through a supported mechanism.
🧹 Nitpick comments (1)
tests/unittest/_torch/visual_gen/test_qwen_image_pipeline.py (1)

226-253: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Expand profiler lifecycle coverage

test_forward_honors_profile_step_range only covers the 0-1 path. Add focused cases for predenoise, all, postdenoise, warmup gating, and the final inactive state; the postdenoise case should assert that tracing stops and exports.

Coverage summary: added test_forward_honors_profile_step_range; listed in tests/integration/test_lists/test-db/l0_a10.yml; no matching entry in tests/integration/test_lists/qa/. Coverage verdict: needs follow-up.

🤖 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/visual_gen/test_qwen_image_pipeline.py` around lines
226 - 253, Expand test_forward_honors_profile_step_range with focused profiler
lifecycle cases covering predenoise, all, postdenoise, warmup gating, and the
final inactive state. Configure each profile range and assert the corresponding
CUDA and torch profiler calls, especially that postdenoise stops tracing and
exports the configured trace. Keep the tests isolated with the existing pipeline
test doubles and mocks.

Source: Path instructions

🤖 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 `@tensorrt_llm/_torch/visual_gen/models/qwen_image/pipeline_qwen_image.py`:
- Around line 528-530: Update the forward path around
_start_postdenoise_profile() so post-denoise profiling is stopped and exported
after _decode_latents() completes. Use a finally block to call the corresponding
stop operation even when decoding raises, ensuring profiler state is not left
active and the Chrome trace is exported.
- Around line 488-494: Move the `_start_predenoise_profile()` call earlier in
the request flow, before timestep creation and scheduler preparation, so
pre-loop scheduler refresh work is included in profiling. Keep
`_start_denoise_profile()` immediately before the denoising loop.

---

Outside diff comments:
In `@tensorrt_llm/_torch/visual_gen/pipeline.py`:
- Around line 181-190: Update the profiler setup around self._torch_profiler and
activities to select only the accelerator activity supported by the active
device or build, always retaining CPU while excluding the incompatible CUDA/XPU
activity. Pass the resulting CPU-plus-matching-accelerator list to
torch.profiler.profile.
- Line 189: The profiler configuration using with_modules=True does not capture
hierarchy for VisualGen’s eager nn.Module execution. Remove this ineffective
option or replace it with explicit module instrumentation, ensuring module
hierarchy is recorded only through a supported mechanism.

---

Nitpick comments:
In `@tests/unittest/_torch/visual_gen/test_qwen_image_pipeline.py`:
- Around line 226-253: Expand test_forward_honors_profile_step_range with
focused profiler lifecycle cases covering predenoise, all, postdenoise, warmup
gating, and the final inactive state. Configure each profile range and assert
the corresponding CUDA and torch profiler calls, especially that postdenoise
stops tracing and exports the configured trace. Keep the tests isolated with the
existing pipeline test doubles and mocks.
🪄 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: dbb8a4b9-421d-4cb2-bc91-1fad5661b0e9

📥 Commits

Reviewing files that changed from the base of the PR and between d787ad9 and f930d1a.

📒 Files selected for processing (4)
  • docs/source/developer-guide/perf-analysis.md
  • tensorrt_llm/_torch/visual_gen/models/qwen_image/pipeline_qwen_image.py
  • tensorrt_llm/_torch/visual_gen/pipeline.py
  • tests/unittest/_torch/visual_gen/test_qwen_image_pipeline.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/source/developer-guide/perf-analysis.md

Comment thread tensorrt_llm/_torch/visual_gen/models/qwen_image/pipeline_qwen_image.py Outdated
Comment thread tensorrt_llm/_torch/visual_gen/models/qwen_image/pipeline_qwen_image.py Outdated
@chang-l
chang-l requested review from a team as code owners July 24, 2026 06:35
@chang-l
chang-l marked this pull request as draft July 24, 2026 16:38
chang-l added 8 commits July 27, 2026 11:09
Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
Profiling state and lifecycle lived directly on BasePipeline: five
attributes and six methods on a class that is already 1300+ lines. Every
test double had to reproduce that state (the Qwen-Image doubles carried
eight profiler attributes just to call forward()), and the profiler tests
could only reach the logic by rebinding unbound methods onto
SimpleNamespace fakes.

Move it all to VisualGenProfiler in a new profiler.py. BasePipeline keeps
one attribute and thin hooks that gate on warmup and delegate. Three
changes ride along:

* Close each capture window on an idle device. A collector may stop
  collecting -- or end the process, as nsys --capture-range-end=stop-shutdown
  does -- the moment the range closes, so torch.cuda.synchronize() must run
  first. PyExecutor.profile_step() already does this; VisualGen did not.

* Name the hooks for what they do. _profile_denoise_start() stopped the
  profiler and _profile_denoise_end() started it, which reads backwards at
  the call site. They are now _close_predenoise_window() and
  _open_postdenoise_window(), matching _open_step_window() /
  _close_step_window().

* Test the profiler directly. No SimpleNamespace fakes, no MethodType
  rebinding; new coverage for the pre-stop sync, the no-op close, request
  scopes that unwind on an exception, and single-shot phases not re-arming.

No behavior change beyond the added synchronize().

Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
visual_gen has three denoise loops: BasePipeline.denoise(), Qwen-Image's
true-CFG loop, and LTX-2 two-stage's _refinement_denoise(). Only the first
two were instrumented, so on LTX2TwoStagesPipeline a numeric
TLLM_PROFILE_VISUAL_GEN_START_STOP range captured stage 1 and silently
dropped stage 2. That is worse than the Qwen-Image gap it mirrors: Qwen
produced no trace at all, which is obvious, whereas this produces a
plausible trace missing half the denoising.

Add the same hooks to the stage 2 loop, and add a source-level test that
finds every loop in visual_gen stepping a transformer and asserts it calls
them -- the next hand-written loop fails the test instead of quietly
producing a short trace.

Also document what multi-stage pipelines mean for the per-loop modes:
numeric ranges now emit one trace file per stage, and postdenoise still
opens at the end of stage 1, so its window spans the later stages too.

Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
A pipeline with its own denoise loop had to place four calls in the right
four spots -- close_predenoise before the loop, open/close_step_window
around each step, open_postdenoise after. Placing three of four still
compiles and still produces a trace, just a wrong one, which is the failure
mode that hid the Qwen-Image and LTX-2 stage 2 gaps.

Fold all four into the iterator the loop already needs:

    for i, t in self._profile_denoise_steps(timesteps):
        ...

The generator closes the pre-denoise window before yielding the first step,
toggles step windows on the indices a numeric range names, and opens the
post-denoise window after the last one. Partial instrumentation is no
longer expressible, and the loop bodies keep their indentation. Breaking
out early or raising skips the post-denoise arm, as before -- request_scope
still closes any window left open.

All three denoise loops now differ from a plain enumerate() by one call,
and the structural test asserts exactly that.

Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
steps() only closed a numeric window on the exact stop index, so a range
extending past a loop's last step never closed inside the loop --
request_scope() closed it at request exit instead. LTX-2 stage 2 runs three
steps, so 0-4 opened on its step 0 and stayed open through VAE decode:

    stage2 step0 -> open -> step1 -> step2 -> [still open] -> decode -> close

The stage-2 trace therefore contained decode, contradicting the documented
one-trace-per-stage behavior, and on a pipeline with a third stage the
window would have swallowed that too.

Numeric ranges select denoise steps, so close any window still open when
the iterator exhausts. Regression test drives two stages with the second
shorter than the selected range. Keyword modes are untouched: the new close
is guarded on the range being numeric, so predenoise/postdenoise/all keep
their existing boundaries.

Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
The synchronize() added when the profiler was extracted sat outside
close_window()'s try/finally. A sticky CUDA error from an earlier kernel
surfaces at exactly that call, and when it did:

  - cudaProfilerStop() was never attempted, so an attached Nsight collector
    kept its capture range open
  - _active stayed True, so every later open_window() short-circuited and
    every later close_window() re-raised -- profiling wedged for the rest
    of the process
  - the torch.profiler session stayed attached and recording

Verified all three against the previous commit before fixing.

Move the sync inside an outer try whose finally always runs the shutdown:
stop the torch profiler, export its trace, stop the CUDA gate, clear
_active. The CUDA error still propagates -- it should -- but it no longer
takes the profiler down with it.

Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
@chang-l
chang-l force-pushed the agent/visual-gen-torch-profiler branch from 310ef21 to 62f7864 Compare July 27, 2026 18:25
@chang-l
chang-l marked this pull request as ready for review July 27, 2026 23:50

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unittest/_torch/visual_gen/test_profiler.py (1)

1-437: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Coverage summary: tests/integration/test_lists/test-db/l0_a10.yml already lists unittest/_torch/visual_gen/test_profiler.py. Verdict: insufficient — add a steps() test for a multi-pair range like 0-1,3-4; test_each_window_uses_a_fresh_trace_file only covers repeated open_window()/close_window() calls.

🤖 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/visual_gen/test_profiler.py` around lines 1 - 437, Add
a focused test for VisualGenProfiler.steps using a multi-pair numeric range such
as “0-1,3-4”, verifying events open and close at both configured windows while
skipping the gap. Keep the existing test_step_windows_follow_numeric_ranges
coverage unchanged and use _recording_profiler to assert the complete event
sequence.

Source: Path instructions

🧹 Nitpick comments (2)
tests/unittest/_torch/visual_gen/test_profiler.py (1)

135-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No symmetric test for open_window()'s failure-cleanup path.

close_window()'s failure cleanup is covered twice (sync failure, export failure), but open_window()'s except RuntimeError branch (stopping cudaProfilerStop() and clearing _torch_profiler when torch_profiler.start()/cudaProfilerStart() raises) has no test.

Also applies to: 218-238

🤖 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/visual_gen/test_profiler.py` around lines 135 - 169,
Add a unit test alongside test_close_window_stops_collectors_when_sync_fails
covering open_window() failure cleanup: make torch_profiler.start() or
cudaProfilerStart() raise RuntimeError, assert the error propagates,
cudaProfilerStop() is called, profiler._torch_profiler is cleared, and
profiler.active is false. Reuse the existing _profiler_with_torch_trace setup
and mocking patterns.
tests/unittest/_torch/visual_gen/test_qwen_image_pipeline.py (1)

221-254: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a true-CFG profiler case true_cfg_scale=1.0 with no negative_prompt stays on the non-CFG path, so this test doesn’t exercise Qwen-Image’s dual cond/uncond loop. Use true_cfg_scale > 1.0 with a negative prompt, or add a second case, to cover the custom CFG path and its step window.

🤖 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/visual_gen/test_qwen_image_pipeline.py` around lines
221 - 254, Update test_forward_honors_profile_step_range to exercise the
true-CFG path by using true_cfg_scale greater than 1.0 and providing a
negative_prompt, while preserving the existing profiler step-window assertions.
Alternatively, add a separate test case covering the dual
conditional/unconditional loop and its profiler start/stop 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.

Inline comments:
In `@tensorrt_llm/_torch/visual_gen/profiler.py`:
- Around line 165-197: Update the postdenoise handling in steps and its callers
so _postdenoise_pending is opened only after the final denoise loop, not after
each completed loop. Have BasePipeline.denoise and _refinement_denoise
communicate whether another denoise loop follows, preserving the existing
single-stage behavior while keeping Stage 2 denoising outside the
postdenoise/VAE-only window.
- Around line 203-260: Guard the CUDA runtime calls in open_window and
_shutdown_collectors with torch.cuda.is_available(), matching the existing guard
in close_window. Only call cudart().cudaProfilerStart() or cudaProfilerStop()
when CUDA is available, while preserving profiler startup, shutdown, and
state-reset behavior for XPU-only builds.

---

Outside diff comments:
In `@tests/unittest/_torch/visual_gen/test_profiler.py`:
- Around line 1-437: Add a focused test for VisualGenProfiler.steps using a
multi-pair numeric range such as “0-1,3-4”, verifying events open and close at
both configured windows while skipping the gap. Keep the existing
test_step_windows_follow_numeric_ranges coverage unchanged and use
_recording_profiler to assert the complete event sequence.

---

Nitpick comments:
In `@tests/unittest/_torch/visual_gen/test_profiler.py`:
- Around line 135-169: Add a unit test alongside
test_close_window_stops_collectors_when_sync_fails covering open_window()
failure cleanup: make torch_profiler.start() or cudaProfilerStart() raise
RuntimeError, assert the error propagates, cudaProfilerStop() is called,
profiler._torch_profiler is cleared, and profiler.active is false. Reuse the
existing _profiler_with_torch_trace setup and mocking patterns.

In `@tests/unittest/_torch/visual_gen/test_qwen_image_pipeline.py`:
- Around line 221-254: Update test_forward_honors_profile_step_range to exercise
the true-CFG path by using true_cfg_scale greater than 1.0 and providing a
negative_prompt, while preserving the existing profiler step-window assertions.
Alternatively, add a separate test case covering the dual
conditional/unconditional loop and its profiler start/stop behavior.
🪄 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: 6ac9098a-4d8b-4aba-8553-73c461096f4c

📥 Commits

Reviewing files that changed from the base of the PR and between 5db6e01 and 62f7864.

📒 Files selected for processing (10)
  • docs/source/developer-guide/perf-analysis.md
  • tensorrt_llm/_torch/visual_gen/executor.py
  • tensorrt_llm/_torch/visual_gen/models/ltx2/pipeline_ltx2_two_stages.py
  • tensorrt_llm/_torch/visual_gen/models/qwen_image/pipeline_qwen_image.py
  • tensorrt_llm/_torch/visual_gen/pipeline.py
  • tensorrt_llm/_torch/visual_gen/profiler.py
  • tests/integration/test_lists/test-db/l0_a10.yml
  • tests/unittest/_torch/visual_gen/test_profiler.py
  • tests/unittest/_torch/visual_gen/test_qwen_image_pipeline.py
  • tests/unittest/_torch/visual_gen/test_visual_gen_params.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • docs/source/developer-guide/perf-analysis.md
  • tensorrt_llm/_torch/visual_gen/executor.py
  • tests/unittest/_torch/visual_gen/test_visual_gen_params.py
  • tests/integration/test_lists/test-db/l0_a10.yml

Comment on lines +165 to +197
def steps(self, timesteps: Iterable[Any]) -> Iterator[Tuple[int, Any]]:
"""Enumerate one denoise loop's steps, driving every window it owns.

Wrapping the iterator rather than exposing separate
before-loop/per-step/after-loop hooks means a pipeline cannot
instrument half a loop: the phase boundaries ride along with the
enumeration a denoise loop already needs.
"""
if self.range == "predenoise":
self.close_window()

starts, stops = self.range if isinstance(self.range, tuple) else (frozenset(), frozenset())
for i, t in enumerate(timesteps):
if i in starts:
self.open_window()
yield i, t
if i in stops:
self.close_window()

# Everything below is reached only when the loop runs to completion.
# A loop that raised or broke out early has no post-denoise work
# worth capturing, and ``request_scope`` closes whatever window is
# still open.
if starts:
# A numeric range selects denoise steps, so it must not outlive
# the loop. Without this, a range whose stop index is past the
# last step -- 0-4 against LTX-2 stage 2's three steps -- would
# stay open through VAE decode and, on a multi-stage pipeline,
# swallow the following stage as well.
self.close_window()
if self._postdenoise_pending:
self.open_window()
self._postdenoise_pending = False

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

postdenoise opens after the first denoise loop, not the last, on multi-stage pipelines.

_postdenoise_pending fires as soon as any steps() call runs to completion (Line 195-197). For a single-stage pipeline that's correct, but LTX2TwoStagesPipeline runs Stage 1 through BasePipeline.denoise() (which uses this same steps()) and then Stage 2 through its own loop in pipeline_ltx2_two_stages.py (also wired to _profile_denoise_steps). With postdenoise mode, the single-shot window opens right after Stage 1 finishes — swallowing all of Stage 2's refinement denoising into what's documented as a "VAE decode only" window (line 55-56: "profile from the end of the last denoise loop to request completion, covering VAE decode"). Contrast with the numeric-range case, which the docstring explicitly calls out as needing per-stage handling (lines 70-74), but postdenoise has no equivalent multi-stage guard.

🐛 Possible direction
-        if self._postdenoise_pending:
-            self.open_window()
-            self._postdenoise_pending = False
+        if self._postdenoise_pending and is_final_loop:
+            self.open_window()
+            self._postdenoise_pending = False

This would need callers (BasePipeline.denoise() for stage 1, _refinement_denoise() for stage 2) to tell steps() whether more denoise loops follow the current one.

🤖 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/visual_gen/profiler.py` around lines 165 - 197, Update
the postdenoise handling in steps and its callers so _postdenoise_pending is
opened only after the final denoise loop, not after each completed loop. Have
BasePipeline.denoise and _refinement_denoise communicate whether another denoise
loop follows, preserving the existing single-stage behavior while keeping Stage
2 denoising outside the postdenoise/VAE-only window.

Comment thread tensorrt_llm/_torch/visual_gen/profiler.py
@jieli-matrix
jieli-matrix requested review from yingguo-trt and removed request for jieli-matrix July 28, 2026 01:52
chang-l added 3 commits July 29, 2026 16:39
The parse_profile_range docstring claimed postdenoise ran "from the end of
the last denoise loop", but it is single-shot and arms at the end of the
*first* one. On LTX-2 two-stage that window is a superset of VAE decode --
it also holds the spatial upsample and all of stage 2 -- which contradicted
the docstring while the perf-analysis guide described it correctly.

Keep the behavior and state the limitation in both places. It is a superset,
not a gap: decode is always in the trace, just not alone. No mode isolates
decode on that pipeline; doing so needs the profiler to know which loop is
last, which the base denoise() cannot tell it.

Add a test pinning the window contents so the behavior cannot drift without
the docs being revisited.

Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
…rch-profiler

Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>

# Conflicts:
#	tensorrt_llm/_torch/visual_gen/pipeline.py
Merging main brought in Qwen-Image-Edit (NVIDIA#16095) and Qwen-Image-Layered,
each with its own denoise loop that bypasses BasePipeline.denoise(). Both
were invisible to TLLM_PROFILE_VISUAL_GEN_START_STOP, the same gap this
branch fixed for Qwen-Image and LTX-2 stage 2.

test_every_denoise_loop_is_instrumented caught them on the merge, which is
what it was written for. Route both through _profile_denoise_steps().

Also relax that test's exact loop count to a lower bound: a hard count fails
on every newly added pipeline rather than on the thing that matters, which is
whether each detected loop is instrumented.

Signed-off-by: Chang Liu <9713593+chang-l@users.noreply.github.com>
@chang-l
chang-l requested a review from a team as a code owner July 29, 2026 23:52
@chang-l
chang-l requested a review from jieli-matrix July 29, 2026 23:52

@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

♻️ Duplicate comments (1)
tensorrt_llm/_torch/visual_gen/profiler.py (1)

214-220: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard CUDA runtime calls on non-CUDA builds.

XPU tracing is explicitly supported in _create_torch_profiler(), but these unconditional torch.cuda.cudart() calls fail before startup or during cleanup on XPU-only builds. Gate both start/stop calls with torch.cuda.is_available() while still starting/stopping the torch profiler.

Also applies to: 260-263

🤖 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/visual_gen/profiler.py` around lines 214 - 220, Update
the profiler start and cleanup logic around the CUDA runtime calls to check
torch.cuda.is_available() before invoking torch.cuda.cudart(),
cudaProfilerStart(), or cudaProfilerStop(). Keep _torch_profiler.start() and its
corresponding stop behavior active regardless of CUDA availability so XPU-only
tracing continues to work.
🧹 Nitpick comments (1)
tensorrt_llm/_torch/visual_gen/profiler.py (1)

27-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use parameterized Python 3.10+ annotations.

ProfileRange leaves its endpoint types unknown and uses legacy Union/Tuple/Optional forms. Use str | tuple[frozenset[int], frozenset[int]] | None and str | None.

Also applies to: 133-133

🤖 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/visual_gen/profiler.py` around lines 27 - 38, Update the
ProfileRange alias to use Python 3.10 union and built-in generic syntax,
explicitly typing both range endpoints as frozenset[int] and allowing None. Also
replace the separately referenced Optional annotation at the indicated usage
with str | None, removing legacy Union, Tuple, and Optional imports if no longer
needed.

Sources: Coding guidelines, Learnings

🤖 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 `@tensorrt_llm/_torch/visual_gen/profiler.py`:
- Around line 49-59: Replace every en dash character in the profiler mode
docstring entries with Ruff-compliant punctuation, such as a hyphen or
equivalent ASCII separator, while preserving the documented meanings and
formatting of the A-B, range, predenoise, postdenoise, all, and unset options.

---

Duplicate comments:
In `@tensorrt_llm/_torch/visual_gen/profiler.py`:
- Around line 214-220: Update the profiler start and cleanup logic around the
CUDA runtime calls to check torch.cuda.is_available() before invoking
torch.cuda.cudart(), cudaProfilerStart(), or cudaProfilerStop(). Keep
_torch_profiler.start() and its corresponding stop behavior active regardless of
CUDA availability so XPU-only tracing continues to work.

---

Nitpick comments:
In `@tensorrt_llm/_torch/visual_gen/profiler.py`:
- Around line 27-38: Update the ProfileRange alias to use Python 3.10 union and
built-in generic syntax, explicitly typing both range endpoints as
frozenset[int] and allowing None. Also replace the separately referenced
Optional annotation at the indicated usage with str | None, removing legacy
Union, Tuple, and Optional imports if no longer needed.
🪄 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: da9a2e31-3c51-4289-972c-59c6e8e9c49d

📥 Commits

Reviewing files that changed from the base of the PR and between 62f7864 and cd2081d.

📒 Files selected for processing (7)
  • docs/source/developer-guide/perf-analysis.md
  • tensorrt_llm/_torch/visual_gen/models/ltx2/pipeline_ltx2_two_stages.py
  • tensorrt_llm/_torch/visual_gen/models/qwen_image/pipeline_qwen_image_edit.py
  • tensorrt_llm/_torch/visual_gen/models/qwen_image_layered/pipeline_qwen_image_layered.py
  • tensorrt_llm/_torch/visual_gen/pipeline.py
  • tensorrt_llm/_torch/visual_gen/profiler.py
  • tests/integration/test_lists/test-db/l0_a10.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/source/developer-guide/perf-analysis.md

Comment thread tensorrt_llm/_torch/visual_gen/profiler.py

@fredricz-20070104 fredricz-20070104 left a comment

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.

Review summary - Approve

Reviewed the full diff; no blocking or major issues found.

Minor, non-blocking notes:

  • tensorrt_llm/_torch/visual_gen/profiler.py: CUDA runtime gate calls are unconditional while torch profiler supports XPU
  • tensorrt_llm/_torch/visual_gen/profiler.py: Legacy typing forms / missing parameterization

Automated review by NVCortex Lite, run by @fredricz-20070104.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants