[PyTorch] [torch.compile] torch.compile support for GroupedLinear - #32
Draft
pggPL wants to merge 6 commits into
Draft
[PyTorch] [torch.compile] torch.compile support for GroupedLinear#32pggPL wants to merge 6 commits into
pggPL wants to merge 6 commits into
Conversation
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
pggPL
force-pushed
the
grouped_linear_compile
branch
from
August 13, 2026 10:27
926952e to
45a2c8b
Compare
pggPL
changed the base branch from
linear_compile_on_main
to
linear_torch_compile_final_attempt
August 13, 2026 10:27
… framework Additive extensions needed by ops taking per-GEMM lists (GroupedLinear): - _TensorListAdapter, _TensorOrQuantizedListAdapter, _QuantizerListAdapter - _SymIntListAdapter: List[int] as a SymInt[] slot, so host-side ints Dynamo marks dynamic (e.g. m_splits) stay compilable in one symbolic graph - list-aware grad plumbing: per-target (slot, is_list) resolution, flat packing/slicing of list-shaped grads, tensor-list-only no-grad defaults - wrapper-op subclass flattening for entries inside list slot groups Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Register _GroupedLinear's forward/backward as a torch.library custom op so
torch.compile(fullgraph=True) traces the module as one graph node:
- GroupedLinearFwdArgs/BwdArgs dataclasses; _grouped_linear_forward_impl /
_backward_impl extracted from the autograd.Function (which is now a thin
wrapper; the fused GroupedTensor path is unchanged and gated to eager)
- save_original_input flips and recipe-derived scalars resolved in the module
forward, so the impl and the compile-time fake see the same config
- saved layout (inputmat_full, *inputmats, *weights_fp8, *saved_weights,
*biases) with per-slot alias tags; the plain path saves one full cast
buffer and re-splits in backward (op returns may not alias)
- compiled path disables split-quantize bulk allocation and uses per-expert
wgrad buffers (returned tensors may not alias each other)
- m_splits crosses as a SymInt[] op input: static splits reuse the graph,
a changed distribution recompiles once into a symbolic graph
- compile_unsupported_reason gate with explicit graph_break(msg=...);
grouped cuBLAS workspace pre-allocated in reset_parameters
- grouped check_gemm_dims variant in utils.py
- tests: all recipes x {default, reduce-overhead}, primary-FP8 weights,
microbatch weight-workspace caching, m_splits changes, save_original_input,
fallbacks, and a toy-op unit test for tensor-list gradients
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…c _ListAdapter One lift of any scalar field adapter to its List[...] analog: per-element delegation to the inner adapter's to_slots/from_slots, with slot-type aggregation (Tensor? -> Tensor[], Tensor[] -> concatenated payloads + counts, bundle -> bundle of per-element bundles). Drops _TensorListAdapter, _TensorOrQuantizedListAdapter and _QuantizerListAdapter; any future scalar adapter becomes list-capable for free. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
… path
Second custom op (grouped_linear_fused) for
NVTE_GROUPED_LINEAR_USE_FUSED_GROUPED_GEMM=1: m_splits stays a device tensor
(no host sync), so changing the split distribution in place reuses one graph
with zero recompiles.
- GroupedLinearFusedFwdArgs/BwdArgs; impls mirror
_forward_grouped_tensor/_backward_grouped_tensor with the saved grouped
input expressed as its flat 10-slot payload + static metadata; the
GroupedTensorStorage is rebuilt inside the backward op (first_dims /
tensor_offsets recomputed from m_splits_tensor rather than saved)
- recipes: bf16/fp16 and FP8 per-tensor current scaling; MXFP8 / NVFP4 /
block scaling fall back to eager (their grouped storages carry per-split
host scale offsets that cannot cross the op boundary)
- per-expert wgrad / dbias buffers on the compiled path (no packed aliases)
- test: fused fullgraph x {default, reduce-overhead} x {bf16, current
scaling}, with in-place m_splits changes asserted recompile-free
(Hopper/Blackwell only; skipped elsewhere)
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
tex.get_cublasLt_version() is an untraceable pybind call; the fused-path dispatch in GroupedLinear.forward hit it under torch.compile and graph-broke before reaching the gate. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Follow-up to NVIDIA#3053 (
linear_torch_compile_final_attempt, torch.compile custom-op support forLinear). This PR adds the same torch.compile custom-op support tote.pytorch.GroupedLinear:torch.compile(fullgraph=True)traces the module as a single custom-op graph node under every built-in recipe, in default andreduce-overhead(CUDA-graph trees) modes.Type of change
Changes
Framework extensions in
dynamo/custom_op.pyGroupedLinear passes
num_gemmsweights / biases / cached FP8 workspaces and 6 quantizer lists; the custom-op framework only supported scalar fields. Added (all additive; the Linear op registration is untouched):_ListAdapter— a generic lift of any scalar field adapter to itsList[...]analog. The schema is fixed at registration while list lengths vary per call, so the inner adapter's slots are aggregated instead of replicated:Tensor/Tensor?→Tensor[](one entry per element,Nonerides the existing 0-element complex32 sentinel),Tensor[]→Tensor[](elements' payloads concatenated in order, per-element counts in the bundle slot), value bundle → one bundle of per-element bundles.to_slots/from_slotsdelegate per element to the scalar adapter, so kind tagging / storage flattening / value-opaque checks apply unchanged. CoversList[Tensor],List[Tensor | QuantizedTensorStorage | None](weights, workspaces) andList[Quantizer]; any future scalar adapter becomes list-capable for free._SymIntListAdapter—List[int]as aSymInt[]slot: host-side ints that Dynamo may mark dynamic (e.g.m_splits) cross as op inputs instead of being baked into a value bundle, so one symbolic graph serves every split distribution._resolve_grad_targetsreturns(slot, is_list)targets;_pack_bwd_resultflattens a list-shaped grad to one slot per element;_autograd_backwardslices the flat bwd return back per target (with a count check);fwd_tensor_list_lengthsnow records only genuine tensor lists (aSymInt[]slot takes a plainNonegrad).register_torch_dispatchpath rewritesFloat8Tensor-style entries inside a list group into the storage layout (splicing inner tensors into the concatenated slot and rebuilding the meta bundle).grouped_linear.pyrefactor + op registrationMirrors the
linear.pystructure:GroupedLinearFwdArgs/GroupedLinearBwdArgsdataclasses; thesave_original_inputruntime flips (delayed-scaling / unsafe-requantization warnings) andbackward_overrideare resolved in the module forward, before the args are built, so the impl and the compile-time fake always see one final flag. Recipe-derived scalars (fprop/dgrad/wgrad split accumulators, native-bgrad support) are computed there too — the backward no longer reads the recipe object (_split_quantize_and_biastakes a precomputed bool instead ofrecipe)._grouped_linear_forward_impl/_grouped_linear_backward_implextracted from theautograd.Functionstaticmethods;_GroupedLinearis now a thin wrapper (the fused GroupedTensor path dispatches before the impl and is unchanged)._grouped_linear_setup_ctxpopulates the backward args and rebuilds alias-deduped save slots;_grouped_linear_eager(@no_torch_dynamo) is the eager entry.(inputmat_full, *inputmats, *weights_fp8, *saved_weights, *biases)(1 + 4N slots) with per-slot name/index alias tags — a custom op may not return aliasing tensors, and everyweights_fp8[i]aliases an input or another return (cache hit →weight_workspaces[i], miss →new_workspaces[i], primary FP8 →weights[i]). The plain (non-quantized) path saves one full cast buffer (or aliasesinpfor a no-op cast) and backward re-splits it — the Ntorch.splitviews can't be returned from an op.compiled_op=True): split-quantize bulk allocation is disabled (bulk storages alias each other) and backward allocates per-expert wgrad buffers instead of one packed buffer, for the same reason;clear_tensor_dataon saved inputs is skipped (in-place storage resize of op inputs)._grouped_linear_forward_fake/_grouped_linear_backward_fake—TensorSpectwins mirroring the impl's quantizerset_usage/update_usageretargeting and the alias tags exactly.compile_unsupported_reason()gate (explicittorch._dynamo.graph_break(msg=...)thenwarn_compile_eager_fallback, as in Linear): debug, distributed weights,m_splitspassed as a device tensor, userout/dgrad_outbuffers (returned alias), fused GroupedTensor path,single_grouped_weight/bias, CPU offloading, delayed wgrad,fuse_wgrad_accumulation,fp8_calibration(calibration inside the op would mutate quantizers rebuilt from graph constants), mixedrequires_gradacross the weights list, non-value-opaque quantizers.GroupedLinear.forwarddrops@no_torch_dynamo(); the compiled path requires host-sidem_splits(list/tuple — a device tensor falls back), runs a groupedcheck_gemm_dimsvariant (check_grouped_gemm_dimsinutils.py,torch._checkguards incl. per-split FP8 divisibility), and reassemblesnew_workspacesfrom the op outputs to refresh_fp8_workspaces.GroupedLinear.reset_parameterseagerly pre-allocates the grouped cuBLAS workspace (get_cublas_workspace(dev, False, True)), mirroringLinear.reset_parameters— underreduce-overheadthe first grouped GEMM can run inside CUDA-graph capture.Deviations from the original plan
m_splitsare supported, not deferred: instead of baking the splits into theOpaqueValueBundle(graph-per-distribution, and a hard Dynamo error once automatic-dynamic marks the ints symbolic),m_splitscrosses as aSymInt[]op input. Static splits behave as before; once a distribution changes, Dynamo recompiles once to a symbolic graph that then serves all further distributions.fp8_calibrationresolved to a gate entry (open question 2):calibrate()inside the op body would update amax on quantizers rebuilt from__fx_repr__constants and the updates would be lost.requires_gradacross the weights list is asserted in the gate (open question 3).Tests (
tests/pytorch/test_torch_compile.py)test_te_grouped_linear_compiles— fullgraph, all recipes + bf16 × {default, reduce-overhead}, bit-exact eager-vs-compiled forward / dgrad / per-expert wgrads + bias grads, cudagraph-skip counter guard.test_te_grouped_linear_compile_with_quantized_fp8_weight— primary-FP8 weights (subclass flattening inside aTensor[]slot group).test_te_grouped_linear_compile_is_first_microbatch— per-expert FP8 workspace caching across a[True, False, False]schedule with cache-identity checks, vs a separate eager reference module.test_te_grouped_linear_m_splits_change— same splits: no recompile; changed distributions: still the compiled path (symbolic graph), bit-exact.test_te_grouped_linear_compile_save_original_input— input saved by alias, re-split/re-quantized in backward.test_te_grouped_linear_compile_train_eval_switch— grouped counterpart of the Linear xfail (currently xpasses).test_te_grouped_linear_compile_m_splits_tensor_falls_back/..._delayed_scaling_falls_back— graph-break fallbacks; withfullgraph=Truethe error names the reason.test_custom_op_tensor_list_grads— framework unit test: a toy op with aList[Optional[Tensor]]input (incl. aNoneentry riding the sentinel) compiles fullgraph and routes one gradient per list element, eager and compiled.Test results (RTX Ada workstation)
test_torch_compile.py: 69 passed, 24 skipped, 1 xfailed, 1 xpassedtest_grouped_linear.py(eager): 717 passed, 416 skipped — the refactor is a numerical no-op for eagerKnown limitations / follow-ups
NVTE_GROUPED_LINEAR_USE_FUSED_GROUPED_GEMM=1) stays eager (gate); it keepsm_splitson device and is the natural follow-up now thatSymInt[]splits work.fuse_wgrad_accumulation, delayed wgrad (wgrad_store), CPU offloading, debug andout=/dgrad_out=buffers fall back to eager, same as (or analogous to) Linear.Checklist: