[TRTLLM-11958][perf] reduce @torch.library.custom_op host overhead - #13149
Conversation
|
/bot run --disable-fail-fast |
📝 WalkthroughWalkthroughRefactored custom torch operation registration for Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tensorrt_llm/_torch/custom_ops/torch_custom_ops.py (1)
2367-2374: Schema lacks default values present in Python function.The Python function
tunable_fp4_quantizehas default values (scaling_vector_size=16,is_sf_swizzled_layout=False), but the schema definition doesn't include them. This means callers usingtorch.ops.trtllm.tunable_fp4_quantizemust always provide all four arguments.From the relevant code snippet in
linear.py, all arguments are passed explicitly, so this works. However, for API consistency with the Python function and other ops in this file, consider adding defaults to the schema:_trtllm_tunable_fp4_quantize_lib.define( - "tunable_fp4_quantize(Tensor input, Tensor input_scale, " - "int scaling_vector_size, bool is_sf_swizzled_layout) -> Tensor[]") + "tunable_fp4_quantize(Tensor input, Tensor input_scale, " + "int scaling_vector_size=16, bool is_sf_swizzled_layout=False) -> Tensor[]")If all call sites explicitly pass these arguments, this is a minor inconsistency rather than a functional issue.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tensorrt_llm/_torch/custom_ops/torch_custom_ops.py` around lines 2367 - 2374, The schema registered via _trtllm_tunable_fp4_quantize_lib.define for the op tunable_fp4_quantize currently omits the Python defaults (scaling_vector_size=16, is_sf_swizzled_layout=False); update the schema string passed to _trtllm_tunable_fp4_quantize_lib.define to include those default values so torch.ops.trtllm.tunable_fp4_quantize supports the same optional arguments as the Python function (i.e., add "=16" to scaling_vector_size and "=False" to is_sf_swizzled_layout in the define call).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@tensorrt_llm/_torch/custom_ops/torch_custom_ops.py`:
- Around line 2367-2374: The schema registered via
_trtllm_tunable_fp4_quantize_lib.define for the op tunable_fp4_quantize
currently omits the Python defaults (scaling_vector_size=16,
is_sf_swizzled_layout=False); update the schema string passed to
_trtllm_tunable_fp4_quantize_lib.define to include those default values so
torch.ops.trtllm.tunable_fp4_quantize supports the same optional arguments as
the Python function (i.e., add "=16" to scaling_vector_size and "=False" to
is_sf_swizzled_layout in the define call).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 0abef18f-78dc-4cb9-b5cf-e0cc434bc0f6
📒 Files selected for processing (1)
tensorrt_llm/_torch/custom_ops/torch_custom_ops.py
|
PR_Github #44012 [ run ] triggered by Bot. Commit: |
|
PR_Github #44012 [ run ] completed with state |
hyukn
left a comment
There was a problem hiding this comment.
This is a good idea for surgically eliminating the overhead caused by torch.custom_op wrapper system. But it sacrifices the original Python-level friendly developer guardrails provided by @custom_op (like automatic type inference), and it may require more constraints for the op author.
Another way is to apply a well-defined rule periodically for the existing ops, "translate" them into the way you did for these two ops, and guarantee the correctness. This may be safer.
Thanks. @hyukn Could you please take a look at my newest commit to see if it addressed your concern? |
Switch trtllm::nvfp4_gemm and trtllm::tunable_fp4_quantize from the @torch.library.custom_op decorator to the low-level torch.library.Library.define + impl API. The high-level decorator carries a hidden ~12us per-call dispatcher tax (visible on Python-heavy, host-bound iterations); the low-level API avoids it while preserving torch.compile support via register_fake. LTX2 dense transformer issues ~1260 tunable_fp4_quantize and ~840 nvfp4_gemm calls per step. At ~12us/call that is ~15ms/step and ~10ms/step of pure CPU dispatcher cost respectively, which amplifies on multi-GPU due to NCCL synchronization. Signed-off-by: Yiyun Lu <55233584+luyiyun1021@users.noreply.github.com>
Extract the low-level torch.library.Library.define+impl pattern into a @fast_custom_op decorator that keeps the @torch.library.custom_op developer experience (schema inferred from Python type hints via infer_schema, same .register_fake method on the returned op) while bypassing the ~7us/call Python dispatcher tax. Switch trtllm::nvfp4_gemm and trtllm::tunable_fp4_quantize from the manual Library.define+impl form to @fast_custom_op. This: - Keeps type-hint-driven schema inference (no hand-written schema strings) - Restores @op.register_fake ergonomics - Serves as a template for migrating more @custom_op sites mechanically Microbenchmark (B200, PyTorch 2.10, 20k-iter tight loop, x.clone() kernel): @torch.library.custom_op 11.67us/call (7.02us dispatcher tax) Manual Library.define+impl 6.28us/call (1.63us tax, -5.39us) @fast_custom_op via torch.ops 6.28us/call (1.63us tax, -5.38us) @fast_custom_op via proxy call 6.09us/call (1.44us tax, -5.57us) The helper is zero-cost on the hot path (torch.ops.trtllm.<name>(...) goes through the C++ dispatcher directly, same as the manual form). Signed-off-by: Yiyun Lu <55233584+luyiyun1021@users.noreply.github.com>
Revert fake function names back to _ to match the rest of the file (e.g. nvfp4_gemm_cublaslt, fp8_rowwise_gemm) and the original @custom_op idiom. The named identifiers were only needed during the intermediate manual Library.define+impl step; with @fast_custom_op the decorator form no longer needs named identifiers. No behavior change. Signed-off-by: Yiyun Lu <55233584+luyiyun1021@users.noreply.github.com>
dc3ca46 to
bc09821
Compare
|
/bot run --disable-fail-fast |
|
PR_Github #44962 [ run ] triggered by Bot. Commit: |
|
/bot run --disable-fail-fast |
|
PR_Github #45063 [ run ] triggered by Bot. Commit: |
hyukn
left a comment
There was a problem hiding this comment.
LGTM. This is more modular-designed. Let us see if CI reports any potential issues.
|
/bot run --disable-fail-fast |
|
PR_Github #45210 [ run ] triggered by Bot. Commit: |
|
PR_Github #45210 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #45299 [ run ] triggered by Bot. Commit: |
|
PR_Github #45299 [ run ] completed with state |
…VIDIA#13149) Signed-off-by: Yiyun Lu <55233584+luyiyun1021@users.noreply.github.com>
@coderabbitai summary
Description
@torch.library.custom_opwraps every call in Python-level wrappers registered on several dispatch keys (Autograd, optionally ADInplaceOrView, and the backend key), which imposes a ~7us per-call dispatcher tax on top of the actual kernel launch. On host-heavy iterations (LTX-2 dense transformer issues ~2100 such calls per step for the two ops in this PR) the tax becomes a measurable fraction of the per-step wall time.This PR:
fast_custom_opthat registers an op directly through the low-leveltorch.library.Library.define + implAPI. The helper preserves the@custom_opdeveloper experience — schema is inferred from Python type hints viatorch.library.infer_schema, and the returned object exposes.register_fake— while bypassing the multi-layer Python wrappers that@custom_opinstalls.trtllm::nvfp4_gemmandtrtllm::tunable_fp4_quantize) to@fast_custom_op.Approach — usage
The helper uses
torch.library.infer_schemainternally so the schema is still driven by Python type hints — no hand-written schema strings.FRAGMENTmode is used under the hood because thetrtllmnamespace is already declared by C++ viaTORCH_LIBRARY_FRAGMENT.Why
@custom_opis expensive — code-level analysisLooking at
torch/_library/custom_ops.py::CustomOpDef._register_to_dispatcher(L607-675),@custom_opregisters several Python-level kernels on multiple dispatch keys. Each one is executed on every call:1. Autograd key (always registered) —
torch/_library/autograd.py::autograd_impl(L108):Every call pays:
is_grad_enabled()+_any_requires_grad(*args)tensor iteration +Metadatadataclass construction +_AutoDispatchBelowAutogradcontext manager +op.redispatch(...)(a second dispatch trip).2. ADInplaceOrView key —
adinplaceorview_impl(L654). Registered only when the schema is mutable or a view op. Bumps version counters on mutated args and routes throughcall_boxed. Not a cost for the two ops in this PR (both are pure,mutates_args=()), so schema is non-mutable and this wrapper is not installed.3. CUDA backend key —
backend_implwrapper fromregister_kernel(L346-362):Every call pays an aliasing-constraint check against
self._opoverload._schema(iterates inputs/outputs, compares storage pointers) and a closure construction.4.
CustomOpDef.__call__— L697. One extra Python frame when the op is invoked by the decorated name (e.g.nvfp4_gemm(x)in-module). Call sites that go throughtorch.ops.trtllm.nvfp4_gemm(x)bypass this frame.What the low-level
Library.define + implpath doesrequires_grad=Truefalls through to the dispatcher's C++-level "no autograd kernel" path (same user-facing error as before), no Python wrapper runs on the common inference path where no tensor needs grad.backend_implwrapper, no aliasing check on each call.Call path comparison (same
torch.ops.trtllm.my_op(x)invocation)@custom_oppath (pure, non-mutating op):Library.define + implpath:Two fewer Python frames + no aliasing check + no re-dispatch.
Microbenchmark
Isolated per-call cost on B200 + PyTorch 2.10, 20k-iter tight loop with
x.clone()as a minimal kernel (tmp/bench_custom_op_overhead_v2.py):@custom_op@torch.library.custom_op(baseline)Library.define + impl@fast_custom_op(viatorch.ops)@fast_custom_op(via proxy__call__)*Pure dispatcher tax = total per-call minus the plain-Python-fn kernel floor.
Key observations:
@fast_custom_op's hot path (torch.ops.trtllm.<name>(...)) is byte-identical to manualLibrary.define + implat runtime: both resolve to the sameOpOverloadand skip all the Python wrappers. No wrapper overhead from the helper itself.__call__path is marginally cheaper because theOpOverloadobject is cached at decoration time.Estimated contribution on LTX-2 at baseline (
@custom_op, 12us/call gross):trtllm::tunable_fp4_quantizetrtllm::nvfp4_gemmEnd-to-end measurements
Per-step host-time attribution, 1 GPU non-cuda-graph vs compile modes (from nsys,
denoise_stepNVTX range, baseline@custom_opstate):@custom_opcontribution (12us × 2100 ≈ 25.2 ms)The E2E win scales with how much other host overhead (kernel launch APIs) has already been absorbed by
torch.compile+ CUDA Graph — ontorch.compile + cuda_graphthe@custom_opwrapper becomes a large fraction of what's left.When to keep
@custom_opmutates_args=("out",)trtllm::bmm_out)setup_context+backwardtorch.compile— only fires whenmutates_argsis non-emptymutates_args=()makes this a no-opKeep
@torch.library.custom_opfor ops that (a) have non-emptymutates_args, (b) need autograd, or (c) are under active development and benefit from the richer Python-side error messages.Functional equivalence
After the switch,
torch.ops.trtllm.nvfp4_gemmandtorch.ops.trtllm.tunable_fp4_quantizeresolve to the sameOpOverloadobjects with the same schema string as before — so eager Python,torch.compile, Dynamo, FX passes, and any downstream FX pattern matcher (e.g.ar_residual_normthat looks fortorch.ops.trtllm.nvfp4_gemm.default) all see an identical op. The only difference is the dispatch path, which is shorter.register_fakestill provides the same FakeTensor/meta shape inference used bytorch.compiletracing.Side-effect validation
Verified parity with
@custom_opbaseline via 13 targeted tests: numerical correctness,torch.compile(fullgraph + dynamic), FakeTensor/meta propagation,inference_mode/no_grad, mutation-safety (args not mutated, output not aliased to inputs), error cases (wrong dtype/device) raising identicalRuntimeError, CPU-tensor dispatch failing identically, autograd-path withrequires_grad=Trueraising the same error (both ops are non-differentiable by design).E2E smoke test: 10-step LTX-2 1 GPU run with
torch_compile=trueusing the@fast_custom_opform — exit code 0, steady-state 0.75s/step matches the manual-Library form.Test Coverage
tests/unittest/_torch/thop/test_nvfp4_gemm.pycoversnvfp4_gemm— passes unchanged.tunable_fp4_quantizeis exercised by all NVFP4 Linear / MoE tests and the LTX-2 integration.PR Checklist
PR description clearly explains what and why.
PR Follows TRT-LLM CODING GUIDELINES.
Test cases are provided for new code paths.
Any new dependencies have been scanned for license and vulnerabilities.
CODEOWNERS updated if ownership changes.
Documentation updated as needed.
Update tava architecture diagram if significant design change.
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, please comment
/bot help.