[TRTLLM-14778][perf] Add feature-mode encoder CUDA graphs for fixed-shape encoders (Whisper) - #17030
Conversation
27454f4 to
e0e0af0
Compare
An encoder that consumes fixed-shape per-request features emits the same number of positions for every request, so its graph key is the batch size alone and the token-shaped num_tokens / seq_lens buckets do not apply. encoder_cuda_graph_config becomes a discriminated union on mode so each encoder kind accepts only the buckets it has. Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
…ture encoders Whisper's encoder takes a 30 s-padded waveform per request, so the runner swaps its packed-token static tensors for an input_features buffer keyed on batch size. Capture goes through the shared two-pass warmup helper and runs on a dedicated stream, because encoder replay is device-concurrent with decoder replay. Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
Covers capture and replay across the configured encoder batch sizes, the eager fallback for an uncaptured size, the config/model mismatch branches, and encoder microbatch admission with the feature config enabled and declined. Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
e0e0af0 to
7618a71
Compare
…, not a new config Encoder-graph capture keyed on batch size alone applies to an encoder whose input is a fixed-shape per-request feature tensor, which is a property of the model rather than a choice the caller makes. Detect it from encoder_graph_spec() and drop the separate config type, so encoder_cuda_graph_config keeps its existing shape and the token buckets a feature encoder derives become optional. Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
…ranches Delete five unreachable branches, fold the feature and token capture setups into one parameterized capture epilogue, and route feature-mode warmup through the existing enc-dec driver, which captures on the worker owning runtime replay. Feature capture now uses capture_error_mode="thread_local" like the token path, and a feature model with no fitting batch size disables the runner outright. Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
…redown Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com> # Conflicts: # tests/unittest/_torch/executor/test_py_executor.py
Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com> # Conflicts: # tensorrt_llm/_torch/pyexecutor/cuda_graph_runner.py
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
…eights load `_TorchLLM._build_model` asks the model class the architecture resolves to, so a T5/BART config missing `num_tokens`/`seq_lens` now fails at `LLM(...)` rather than at engine init in the worker. Decoder-only models, unresolved architectures, and configs whose class the checkpoint loader picks defer to the engine, which keeps the last word. Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
Resolves the model_engine.py conflict: main removed the write-only _max_cuda_graph_seq_len as dead code, while this branch added the feature-mode encoder shape resolution immediately after it. Keep both changes -- the attribute has no readers on either side. Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
1 similar comment
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
Adds docstrings to the three functions this branch introduces -- copy_inputs, capture_h2d and _enc_dec_encoder_graph_forward_fn -- and a one-line summary to each touched test, stating what the case pins rather than restating its body. Pre-existing functions the diff merely touches are left alone. Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
Picks up NVIDIA#18263, which removes the unbound is_idle read that was failing this branch's Pre-commit Check. Signed-off-by: Pranav Shrestha <254760092+pranav-nvidia@users.noreply.github.com>
|
/bot run --disable-fail-fast |
|
PR_Github #69544 [ run ] triggered by Bot. Commit: |
|
PR_Github #69544 [ run ] completed with state
|
|
/bot run |
|
PR_Github #69786 [ run ] triggered by Bot. Commit: |
|
PR_Github #69786 [ run ] completed with state
|
|
/bot run |
|
PR_Github #70021 [ run ] triggered by Bot. Commit: |
|
PR_Github #70021 [ run ] completed with state |
Dev Engineer Review
encoder_graph_spec().encoder_cuda_graph_configvalues for fixed-shape feature encoders.QA Engineer Review
Test changes
Added or updated coverage for:
test_llm_api_pytorch_whisper.py.test_py_executor.py.test_pytorch_model_engine.py.test_pytorch_model_engine_warmup.py.test_llm_args.py.Whisper integration coverage is mapped in
tests/integration/test_lists/test-db/l0_l40s.yml. The unit-test changes are not explicitly mapped in a test-list file.Verdict: needs follow-up.
Description
Encoder-decoder encoder CUDA graphs already exist for packed-token encoders such as T5 and BART (#16706). This PR extends that machinery to encoders whose input is a fixed-shape per-request feature tensor, starting with Whisper.
The difference is the graph key. A token encoder's key depends on the packed token count and sequence lengths, so those buckets have to be configured. A feature encoder emits a fixed number of encoder positions per request whatever the input, so its key degenerates to the batch size and both bucket lists are derived from the model.
Models opt in by declaring
encoder_graph_spec()returning(feature_shape, dtype, fixed_seq_len)— the model selects the mode, not the config.encoder_cuda_graph_configtherefore acceptsbatch_sizeson its own.num_tokens/seq_lensare no longer required by config validation, which cannot tell the two kinds of encoder apart; they are checked at engine init instead, where the model is loaded and its encoder kind is known, and a token encoder that omits them still raises. TP > 1, models that do not declare a spec, and draft models stay on the eager path.Four decisions worth a reviewer's attention:
FEATURE_MIRROR_SLOTS, currently two) rather than owned per bucket, and consecutive encoder batches can be enqueued back to back, so a captured copy would read a mirror after the host had already rotated onto it. Replay issues an eager stream-ordered H2D guarded by per-mirror events instead.encoder_stream, device-concurrent with decoder replay, and torch's pool-sharing contract assumes replays from a shared pool are not concurrent. The capture stream half changes the token path (T5/BART) too: without an explicitstream=,torch.cuda.graphcaptures on a process-wide singleton stream shared with the decoder graphs, which couples the two graph sets through stream-keyed cuBLAS scratch. The pool half costs nothing —_cuda_graph_mem_poolisNonefor the engine's life, so both runners already allocated their own pool at first capture, and t5-small measures identical peaks either way (628.0 MiB reserved, 596.0 MiB allocated). The literalNonemakes non-sharing a requirement rather than a coincidence.Mixed encoder/decoder capture now takes the decoder prefix length from the input processor (Whisper forces 4 tokens) rather than the BART/T5 heuristic; a mismatch makes every mixed batch miss its graph silently. Feature capture uses
capture_error_mode="thread_local", matching the token path.Enable with:
Test Coverage
test_whisper_pytorch_feature_combinations[bf16-kv-v1-encoder-graphs-on-greedy]transcribes at batch 1 and 2 with exact pinned greedy token ids, and additionally assertsnum_feature_replays > len(encoder_runner.graphs). That baseline excludes the one replay each key gets during the capture pass, so it proves runtime replay rather than mere capture — without it, a silent fallback to the eager encoder would pass every output check. The case replaces the existing L40S pre-merge decoder-only Whisper case rather than adding an invocation, since encoder graphs exercise decoder graphs too; KV-v2 decoder coverage stays on H100.Unit coverage for the feature path is in
test_pytorch_model_engine.py(graph spec selection, bucket validation, batch-size capping against the encoder token budget, the 12.5% padding bound, captured-metadata hit and miss) andtest_py_executor.py(feature-mode microbatch admission, including that it never targets an uncaptured batch size).Local validation on SM120 (RTX PRO 6000 Blackwell), re-run on the tree as merged with current main:
tests/unittest/_torch/executor/-k encodertests/unittest/_torch/executor/test_py_executor.pytest_pytorch_model_engine.py+test_py_executor.py+test_pytorch_model_engine_warmup.pySkips are TP2 cases needing two devices in a single-GPU container. The T5 + BART run is what covers the capture-stream change on the token path.
An earlier revision of this branch was additionally built and run on a B200 (SM100): Whisper 9 passed / 1 skipped and the encoder unit tests green, with
bf16-kv-v1-encoder-graphs-on-greedypassing there too, so feature-mode capture and replay are not SM120-only. The B200 T5/BART sweep on that box fails on an NVRTC JIT include path (could not open source file "cuda.h") which reproduces identically on pristine main with CUDA graphs off, so it is a container configuration issue rather than anything in this PR.Standing against the legacy TensorRT backend
Measured 2026-08-26 on this branch as merged with main, plus #17531 — these are the two PRs benchmarked together, so the figures are the current standing of the Whisper PyTorch path, not an attribution of this PR alone. fp16, greedy, overlap scheduler on, encoder CUDA graphs on, product worker topology; block shape pinned to the legacy harness. Legacy denominators are prior measurements on the same harness and hosts.
large-v3 exceeds legacy at batch 8 and 32 on both platforms. whisper-tiny remains behind and is worst at batch 32; that gap is host-side Python in the executor loop, not encoder work, and is tracked separately. WER was identical to the reference in all 39 runs (tiny 0.1250, large-v3 0.0423), so nothing here is a speed-for-accuracy trade.
Caveats worth carrying: the B200 legacy column is a single run with no recorded range, and the B200 arm was measured on a shared node under foreign load (host loadavg median 15 vs SM120's 3.9), so the B200 percentages are softer than the SM120 ones — whisper-tiny at batch 32 especially, being the most host-bound cell in the matrix. SM120 whisper-tiny at batch 32 is a known bimodal cell; the figure quoted is the median over 10 rounds.
Follow-up:
PR Checklist
GitHub Bot Help
To see a list of available CI bot commands, please comment
/bot help.