Support the fused SwiGLU override with dist-GEMM TP overlap - #4151
Open
anijain2305 wants to merge 1 commit into
Open
Support the fused SwiGLU override with dist-GEMM TP overlap#4151anijain2305 wants to merge 1 commit into
anijain2305 wants to merge 1 commit into
Conversation
anijain2305
requested review from
fegin,
tianyu-l,
wconstab and
wwwjn
as code owners
August 14, 2026 19:39
anijain2305
added a commit
that referenced
this pull request
Aug 14, 2026
`overrides/fused_swiglu` fuses the SwiGLU gate and up projections into one `w13` parameter and runs a Triton `silu(gate) * up`. It targets `FeedForward.Config` without `exact=True`, so it also claimed the dist-GEMM FFN config and rewrote it into its own non-overlapping one. Stacking it on `tp_gemm_backend="dist_gemm"` therefore put the TP collectives back outside the GEMMs: the job still trained, and nothing in the logs said the overlap was gone. The two optimizations are orthogonal, so this makes them compose. `AllGatherFusedSwiGLU` subclasses `FusedSwiGLU` and overrides only `forward` with the overlapping schedule -- the `w13` parameter, its state_dict hooks and the Triton kernel are all inherited -- and the `fused_swiglu` factory dispatches on the incoming config so a dist-GEMM FFN maps to it. Because `w13` is a single weight this needs plain `AllGatherLinear` where the unfused `AllGatherFusedFeedForward` needs `AllGatherLinearMulti`. Worth stating since it motivated the question: that saves no collective. The multi-weight version already does one all-gather for both halves, and both do one collective in forward and two in backward. The only saving is the pair of `torch.cat` calls the multi version makes in dgrad. `AllGatherLinearMulti` therefore stays -- it is the primitive for any set of column-parallel projections sharing an input. Adding `exact=True` to the override was considered instead. It would stop the override claiming the dist-GEMM config, but then asking for both optimizations would silently get only one; the dispatch is what makes them compose. Everything stays in `overrides/`. An earlier attempt factored the `w13` layout into `models/common/feed_forward.py` as a shared base so the dist-GEMM FFN could subclass it, but nothing in core would have used it -- `w13` is an overrides-only concept today -- and it meant changing a `FeedForward` shared by five models to serve one override. The override borrows `_tp_group_from_context` and `_warn_once_unfused` from the dist-GEMM module so its TP-off fallback behaves identically; dependencies only need to run core -> overrides, not both ways. Two details worth noting for review. The gate/up split needs no `contiguous()`: folding `w13`'s leading axes yields a GEMM weight whose rows alternate gate, up, so the output columns alternate too and `view(-1, 2).unbind(-1)` recovers the halves as strided views, which the Triton kernel accepts (it is passed each input's strides). And `silu_and_mul_op` is called directly rather than through `_fused_silu_and_mul`, whose `local_map` declares the 3D (batch, seq, features) layout -- on this path the activations are already plain local 2D tensors with no DTensor to map over. Test Plan: New tests: composition and checkpoint-layout coverage in `tests/unit_tests/test_fused_swiglu.py`, and a 2-rank numerics test against the stock FFN in `tests/unit_tests/test_dist_gemm.py` (CUDA-guarded, since `DTensorTestBase` otherwise falls back to CPU/gloo where the Triton op is unregistered). The composition test asserts on the built module type, not the config type -- the failure mode is a working-but-unoverlapped FFN, which a config-type check would not catch. ``` python -m pytest tests/unit_tests/test_dist_gemm.py \ tests/unit_tests/test_distributed_linear.py \ tests/unit_tests/test_fused_swiglu.py \ tests/unit_tests/test_fused_swiglu_override.py \ tests/unit_tests/test_inference_moe.py -q ``` 38 passed. End to end on 4 GPUs, confirming the override log reports `AllGatherFusedFeedForward.Config -> AllGatherFusedSwiGLU.Config` for all six layers rather than the non-overlapping `FusedSwiGLU.Config`: ``` NGPU=4 ./run_train.sh --module llama3 --config llama3_debugmodel_dist_gemm \ --parallelism.tensor_parallel_degree 2 \ --override.imports torchtitan.overrides.fused_swiglu.fused_swiglu --training.steps 5 ``` Also run with `--compile.enable`, and both unchanged paths (dist-GEMM alone, and the override alone without dist-GEMM) re-run as regression checks. All train. Authored with the assistance of an AI agent (Claude Code). ghstack-source-id: 6d7ba9a Pull-Request: #4151
anijain2305
added a commit
that referenced
this pull request
Aug 14, 2026
Stack from [ghstack](https://github.com/ezyang/ghstack/tree/0.15.0) (oldest at bottom): * #4151 * __->__ #4044 Motivation Async-TP -- folding the TP all-gather and reduce-scatter into the adjacent GEMM rather than running them beside it -- is currently only reachable in torchtitan through a compiler: either torch.compile's micro-pipelining pass behind `--parallelism.enable_async_tensor_parallel`, or a GraphTrainer graph pass. That ties a parallelism strategy to a particular compilation path. This makes it a property of the model config instead, selected by a `gemm_backend` argument on `model_registry` exactly as `attn_backend` already selects the attention kernel. Async-TP then works under the eager trainer with no graph pass involved. Structure `torchtitan/distributed/linear.py` holds three reusable autograd Functions. `AllGatherLinear` is a column-parallel projection (all-gather the sequence shard, then GEMM) and `LinearReduceScatter` its row-parallel dual (GEMM, then reduce-scatter). `AllGatherLinearMulti` serves several column-parallel linears that share one input, so the SwiGLU pair w1/w3 costs one all-gather rather than two; its backward still needs only the same two collectives, because the sum over outputs is expressed as a single concatenated product. None of them is attention-specific; MoE projections could use the same pair. `torchtitan/models/common/dist_gemm.py` holds the modules. `AllGatherFusedQKVLinear`, `RowParallelLinear` and `AllGatherFusedFeedForward` are drop-in replacements for the stock QKV, output and SwiGLU projections, keeping the stock parameter layouts so checkpoints still interoperate. `RowParallelLinear` serves both attention's `wo` and the FFN's `w2`. Selection follows the `attn_backend` path one for one: `model_registry` -> flavor function -> `_build_llama3_layers` -> `make_gqa_config` / `make_ffn_config`, which is where `gemm_backend="dist_gemm"` picks the fused classes. Only llama3 is threaded; every other model keeps the default and can gain the argument when someone needs it. `llama3_debugmodel_dist_gemm` is the config-registry entry, and the h100 integration test runs it at TP=2. No attention or FFN subclass is needed beyond the projections themselves. That required one fix in shared code: `GQAttention.forward` captured `B, L` from its input and reused them to fold the attention output, which is only valid when the QKV preserves sequence length. `AllGatherFusedQKVLinear` gathers the SP shard, so it does not, and under spmd_types -- where a tensor's shape is its local shape -- that silently folded sequence into features. It now derives the fold from the tensor being folded. spmd_types only The fused modules take and return plain local tensors, which is the `spmd_types` contract; the DTensor backends are being deprecated and are rejected outright. Two preconditions are checked at parallelize time, because neither is detectable from inside a module once activations carry no placements: the backend must be `spmd_types`, and sequence parallelism must be enabled. The fused GEMMs *are* the SP collectives, so with SP disabled there is nothing to gather and `wo` would reduce-scatter where it must all-reduce. Because selection happens at config-construction time, `set_gqa_attention_sharding` and `set_dense_ffn_sharding` declare the fused blocks' contracts directly: no boundary all-gather, and a row-parallel output that emits its final Shard(1) rather than the Partial a stock rowwise linear produces. Both branches are transitional and collapse once redistribute collectives move inside the modules generally. Symmetric memory One workspace per process group serves every layer, and the symm_mem ops size it themselves: each computes its own requirement and `get_symm_mem_workspace` grows monotonically, so the cost is a max over layers rather than a sum. Nothing here reserves it up front. Growth re-rendezvouses, which is a collective and is rejected during CUDA graph capture, but capture is always preceded by a real warmup call (`graph_trainer/cudagraph.py`), and a warmup forward and backward runs every layer's collectives -- so all growth has happened before anything is recorded. An earlier revision pre-reserved from `parallelize` to make that explicit; it bought one growth instead of a few during warmup, at the cost of duplicating the sizing arithmetic, and is not what makes capture safe. One consequence worth keeping: both wgrad all-gathers leave `return_A` at its default. Passing False would select `_multimem_all_gather_matmul`, which reserves the full gathered buffer rather than a shard -- ranks times more symmetric memory for that one call -- and its heuristic evaluates to `K <= 2048` there, a dimension it was not tuned for. Leaving it defaulted also keeps forward and backward on the same schedule. Every op carves its buffers from offset 0 of that shared workspace, so the regions alias; safety comes from the ops being serialized rather than from disjointness. Each op also brackets itself with barriers, so ranks cannot run ahead of one another. Sequential module forwards and autograd backward are single-stream, which is how models actually run, so this holds today -- but deliberate cross-stream overlap would need distinct workspace offsets, not a barrier. Known gap `--debug.spmd_typechecking` does not pass. The autograd Functions are registered with `typecheck_forward` declaring their type transitions, which the checker requires, but it then rejects the QKV reshape that splits the TP-sharded feature dim into (n_kv, r_dim, head_dim) -- aligned in practice, since the shard is over n_kv. The stock `FusedQKVLinear` hits the same limitation and works around it with `spmd.local()` plus an explicit `PartitionSpec`, carrying a TODO to remove that once the checker handles aligned splits. Rather than add a second instance of a pattern already marked for deletion, this leaves typechecking unsupported until that lands upstream. Authored with assistance from Claude Code.
tianyu-l
reviewed
Aug 14, 2026
Comment on lines
+569
to
+572
| target = ( | ||
| AllGatherFusedSwiGLU.Config | ||
| if isinstance(cfg, AllGatherFusedFeedForward.Config) | ||
| else FusedSwiGLU.Config |
Contributor
There was a problem hiding this comment.
This sounds fine, but I didn't get
Adding exact=True to the override was considered instead. It would stop the
override claiming the dist-GEMM config, but then asking for both optimizations
would silently get only one;
Why could we do exact=True and then have two @override one for FeedForward and the other for AllGatherFusedFeedForward?
| # silently train with the collectives back outside the GEMMs. | ||
| target = ( | ||
| AllGatherFusedSwiGLU.Config | ||
| if isinstance(cfg, AllGatherFusedFeedForward.Config) |
Contributor
There was a problem hiding this comment.
maybe I didn't carefully read in last PR, by AllGatherFusedFeedForward sounds not a good name
- not only all-gather, but also reduce scatter
- "fused" all-gather coincide with this file's "fused" swiglu -- and after all our feedforward is swiglu
Probably should rename AllGatherFusedFeedForward to AsyncTPFeedForward / TPOverlappedFeedforward / DistGEMMFeedForward?
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.
Stack from ghstack (oldest at bottom):
overrides/fused_swiglufuses the SwiGLU gate and up projections into onew13parameter and runs a Triton
silu(gate) * up. It targetsFeedForward.Configwithout
exact=True, so it also claimed the dist-GEMM FFN config and rewrote itinto its own non-overlapping one. Stacking it on
tp_gemm_backend="dist_gemm"therefore put the TP collectives back outside the GEMMs: the job still trained,
and nothing in the logs said the overlap was gone.
The two optimizations are orthogonal, so this makes them compose.
AllGatherFusedSwiGLUsubclassesFusedSwiGLUand overrides onlyforwardwith the overlapping schedule -- the
w13parameter, its state_dict hooks andthe Triton kernel are all inherited -- and the
fused_swiglufactory dispatcheson the incoming config so a dist-GEMM FFN maps to it.
Because
w13is a single weight this needs plainAllGatherLinearwhere theunfused
AllGatherFusedFeedForwardneedsAllGatherLinearMulti. Worth statingsince it motivated the question: that saves no collective. The multi-weight
version already does one all-gather for both halves, and both do one collective
in forward and two in backward. The only saving is the pair of
torch.catcallsthe multi version makes in dgrad.
AllGatherLinearMultitherefore stays -- itis the primitive for any set of column-parallel projections sharing an input.
Adding
exact=Trueto the override was considered instead. It would stop theoverride claiming the dist-GEMM config, but then asking for both optimizations
would silently get only one; the dispatch is what makes them compose.
Everything stays in
overrides/. An earlier attempt factored thew13layoutinto
models/common/feed_forward.pyas a shared base so the dist-GEMM FFN couldsubclass it, but nothing in core would have used it --
w13is an overrides-onlyconcept today -- and it meant changing a
FeedForwardshared by five models toserve one override. The override borrows
_tp_group_from_contextand_warn_once_unfusedfrom the dist-GEMM module so its TP-off fallback behavesidentically; dependencies only need to run core -> overrides, not both ways.
Two details worth noting for review. The gate/up split needs no
contiguous():folding
w13's leading axes yields a GEMM weight whose rows alternate gate, up,so the output columns alternate too and
view(-1, 2).unbind(-1)recovers thehalves as strided views, which the Triton kernel accepts (it is passed each
input's strides). And
silu_and_mul_opis called directly rather than through_fused_silu_and_mul, whoselocal_mapdeclares the 3D (batch, seq, features)layout -- on this path the activations are already plain local 2D tensors with
no DTensor to map over.
Test Plan:
New tests: composition and checkpoint-layout coverage in
tests/unit_tests/test_fused_swiglu.py, and a 2-rank numerics test against thestock FFN in
tests/unit_tests/test_dist_gemm.py(CUDA-guarded, sinceDTensorTestBaseotherwise falls back to CPU/gloo where the Triton op isunregistered). The composition test asserts on the built module type, not the
config type -- the failure mode is a working-but-unoverlapped FFN, which a
config-type check would not catch.
38 passed. End to end on 4 GPUs, confirming the override log reports
AllGatherFusedFeedForward.Config -> AllGatherFusedSwiGLU.Configfor all sixlayers rather than the non-overlapping
FusedSwiGLU.Config:Also run with
--compile.enable, and both unchanged paths (dist-GEMM alone, andthe override alone without dist-GEMM) re-run as regression checks. All train.
Authored with the assistance of an AI agent (Claude Code).