Megatron training is often introduced through launcher flags: tensor parallelism, pipeline parallelism, sequence parallelism, distributed optimizer, recomputation, and process groups. That is useful when operating the framework, but it is a poor way to learn what the framework is doing.
I wanted to understand one smaller question first:
How can two ranks reproduce the forward pass, backward pass, and parameter update of one dense Transformer feed-forward sublayer without either rank storing the full intermediate width?
So I rebuilt that path with ordinary PyTorch and two CPU processes. The block is not a complete Transformer. It contains the part under test: pre-RMSNorm, a fused gate/up projection, SwiGLU, a down projection, and a residual connection.
The finished two-rank step matched the dense reference to within 1.27e-07 in the forward pass and 1.49e-08 after one SGD update. More importantly, two deliberately broken versions failed in different places. Removing the forward reduction changed the output immediately. Removing the backward reduction left the output and loss correct, but corrupted the gradient entering RMSNorm.
That difference is the useful lesson. Tensor parallelism is not merely a way to divide weights. It is a way to divide one algebraic operation while restoring the missing sums at the exact boundaries where the dense computation requires them.
Runnable source: README, implementation, and tests. The commands below run from the cloned module directory.
Start with the dense block
Here is the dense computation the two ranks must reproduce:
| |
Stacking is not required for tensor parallelism; separate gate and up weights would also work if both used the same shard boundaries. Here, the leading dimension selects gate or up, while slicing the shared ffn_size dimension gives each rank matching channels from both projections. Those matching local channels can then be multiplied without communication in SiLU(gate) * up.
Initialization side note. The class above is a schematic module definition.
torch.emptyallocates storage without initializing its values, so a real module must initialize those weights before the first forward pass. The executable fixture uses the equivalent functional computation and creates deterministic tensors directly: the dense path receives the full tensors, while each sharded path receives the corresponding slices. The class above shows a zero-initialized down-projection bias, but the fixture uses a small deterministic nonzero bias replicated across ranks. Some production Transformer variants intentionally zero-initialize the residual branch’s output projection so the block starts asx + 0; that can stabilize very deep models, but it is a separate design choice from this equivalence fixture.
The executable fixture uses:
- sequence length
S = 3; - batch size
B = 2; - model width
H = 8; - feed-forward width
F = 12; - tensor-parallel size
P = 2.
The dense shape trace is:
| |
The equations are:
\[ N = \operatorname{RMSNorm}(X), \]\[ G = N W_g^\top, \qquad U = N W_u^\top, \]\[ A = \operatorname{SiLU}(G) \odot U, \]\[ Y = X + A W_d^\top + b_d. \]The gate and up matrices each have shape [F, H]. The down matrix has shape [H, F]. That shared feed-forward dimension is the axis we will partition.
The equivalence fixture starts from dense tensors and derives each rank’s local parameters with one shared FFN interval:
| |
The shared slice keeps the corresponding FFN channels together:
| |
With F = 12 and P = 2, rank 0 owns FFN channels 0:6 and rank 1 owns channels 6:12. The same interval selects the gate/up output channels and the down-projection input channels, so each rank can pass its local SwiGLU result directly into its local down projection. The input X, RMSNorm weight, residual, and down-projection bias remain replicated. The fixture clones these slices into leaf tensors for gradient comparison; a production tensor-parallel module would usually allocate only the local parameter shapes from the start.
Split the expansion
The first change is to divide the gate and up projections by output feature. Each rank receives the full normalized token representation but computes only half of the feed-forward channels.
For two ranks:
\[ W_g = \begin{bmatrix} W_g^{(0)} \\ W_g^{(1)} \end{bmatrix}, \qquad W_u = \begin{bmatrix} W_u^{(0)} \\ W_u^{(1)} \end{bmatrix}. \]Rank r computes:
The nonlinear operation remains local because each gate feature is paired with the corresponding up feature on the same rank.
The normalized input is wrapped at the column-parallel boundary before either local projection uses it:
| |
CopyToTensorParallelRegion is an identity in forward, so nothing needs to be summed yet. Rank 0 owns one set of feed-forward features; rank 1 owns the other. Concatenating those slices would recover the dense SwiGLU activation. The wrapper’s backward behavior becomes necessary after we trace the contraction and its gradients.
The shape correspondence is easier to see in one table:
| Tensor | Dense shape | Shape on each rank | Partition |
|---|---|---|---|
Input X | [3, 2, 8] | [3, 2, 8] | replicated |
RMSNorm output N | [3, 2, 8] | [3, 2, 8] | replicated |
| Gate weight | [12, 8] | [6, 8] | output features |
| Up weight | [12, 8] | [6, 8] | output features |
| Local SwiGLU output | [3, 2, 12] | [3, 2, 6] | feed-forward features |
| Down weight | [8, 12] | [8, 6] | input features |
| Partial down output | — | [3, 2, 8] | partial sum |
| Reduced output | [3, 2, 8] | [3, 2, 8] | replicated |
In Megatron terminology, this expansion is the role played by a column-parallel linear layer: each rank owns a different slice of the projection’s output features.
Split the contraction
Now partition the down projection along its input feature dimension. Each rank consumes the SwiGLU slice it already owns:
\[ W_d = \begin{bmatrix} W_d^{(0)} & W_d^{(1)} \end{bmatrix}. \]Each rank computes a full-width but incomplete output:
\[ Z^{(r)} = A^{(r)}\left(W_d^{(r)}\right)^\top. \]The dense result is the sum of those partial products:
\[ Z = \sum_{r=0}^{P-1} Z^{(r)}. \]That equation tells us exactly where the first collective belongs.
| |
ReduceFromTensorParallelRegion performs an all-reduce in the forward pass:
| |
The backward pass through this operation is an identity. Every rank receives the same gradient for the replicated output, and each local down-projection shard uses that gradient to compute its own weight and activation gradients.
This is the row-parallel half of the pair: split the input features, compute partial outputs, and sum them.
Side by side, the sharded forward path performs the same channelwise work as the dense path. SiLU remains local to each rank, and the all-reduce restores the dense contraction:
| |
Put the other collective in backward
The expansion layer has the opposite communication pattern.
Its forward pass needed no reduction because the output features were intentionally partitioned. But during backpropagation, every rank computes only the contribution from its local gate and up features to the gradient of the replicated normalized input.
For the dense input gradient:
$$ \begin{aligned} \frac{\partial L}{\partial N} &= \sum_{r=0}^{P-1} \left(\frac{\partial L}{\partial N}\right)_r . \end{aligned} $$That sum must happen before the gradient continues through RMSNorm and into the residual input. CopyToTensorParallelRegion encodes exactly that rule: it is an identity in forward and an all-reduce in backward.
| |
This operation marks the tensor-parallel boundary for the replicated normalized input. Without it, the forward pass still looks correct because every rank receives the same normed tensor. The error appears only during backpropagation: each rank would pass only its local gate/up contribution into RMSNorm instead of the sum required by the dense computation.
The two communication rules are now paired:
| Logical layer | Forward | Backward |
|---|---|---|
| Column-parallel gate/up projection | keep local feature slices | sum replicated-input gradient |
| Row-parallel down projection | sum partial outputs | keep local shard gradients |
This maps to the core Megatron pattern:
| Reconstruction | Megatron-style concept | Why it exists |
|---|---|---|
| Fused local gate/up weights | ColumnParallelLinear role | partition expansion outputs |
| Local down-projection weights | RowParallelLinear role | consume partitioned hidden features |
| Identity forward, reduce backward | copy-to-tensor-parallel region | combine contributions to replicated input gradients |
| Reduce forward, identity backward | reduce-from-tensor-parallel region | reconstruct the dense contraction output |
The fixture implements these semantics directly with torch.distributed; it does not import Megatron Core classes.
Reproduce one dense training step
First inspect the actual environment, block, and tensor shapes:
| |
| |
The launcher’s Gloo connection lines are omitted from this embedded excerpt; the complete retained output is in data/terminal-01-inspect.txt. The environment record is in data/environment.json.
Now compare one complete sharded training step with the dense reference initialized from the same tensors:
| |
| |
The complete transcript is data/terminal-02-equivalence.txt.
| Check | Maximum absolute difference |
|---|---|
| Forward output | 1.27e-07 |
| Loss | 0.00 |
| Input gradient | 2.98e-08 |
| RMSNorm weight gradient | 1.49e-08 |
| Fused gate/up gradient | 4.47e-08 |
| Down-projection gradient | 2.24e-08 |
| Down-projection bias gradient | 2.24e-08 |
| Parameters after one SGD step | 1.49e-08 |
The differences are consistent with floating-point operation ordering. Within this CPU fixture, the two-rank path reproduces the dense forward pass, backward pass, and parameter update.
Remove each collective
An equivalence result is useful only if the test can reject plausible wrong implementations. The two broken versions below are designed to fail at different points.
Remove the forward reduction
Prediction: each rank will treat a partial down-projection result as the complete MLP output, so the first mismatch should appear in the forward output.
| |
| |
The retained output is data/terminal-03-missing-forward.txt.
The prediction holds. The first mismatched signal is the model output, with a maximum error of 1.10. Once the loss is computed from the wrong output, every downstream gradient is also wrong.
Remove the backward reduction
Prediction: the forward output and loss will still match because the row-parallel forward sum remains intact. The first mismatch should appear only when gradients from the two local expansion shards need to be combined before RMSNorm.
| |
| |
The retained output is data/terminal-04-missing-backward.txt.
This is the more revealing failure. The forward output still agrees to 1.27e-07, and the loss is identical. The local gate/up and down-projection gradients also match. Yet the input gradient differs by 0.139, the RMSNorm weight gradient differs by 0.202, and one optimizer step moves the parameters apart by 0.0101.
A forward-only test would approve this broken training graph.
The distributed test suite checks all four cases:
| |
| |
The retained test output is data/terminal-05-tests.txt. The complete implementation is megatron_tp_mlp.py, and the direct tests are test_megatron_tp_mlp.py.
What this reconstruction proves—and what it does not
The small implementation establishes one precise result: for this pre-RMSNorm SwiGLU feed-forward block, partitioning the expansion by output features and the contraction by input features reproduces dense training when the partial contraction outputs are summed in forward and the partial replicated-input gradients are summed in backward.
It also shows why the two reductions need separate tests. A missing forward collective is visible immediately. A missing backward collective can survive output and loss checks while silently corrupting upstream learning.
The fixture does not execute Megatron Core. It does not test:
- self-attention or a complete Transformer block;
- sequence parallelism;
- CUDA or NCCL collective behavior;
- BF16 or FP8 arithmetic;
- fused RMSNorm, SwiGLU, or linear kernels;
- asynchronous communication overlap;
- gradient accumulation;
- pipeline parallelism;
- distributed optimizer state;
- more than two tensor-parallel ranks.
Those are not cosmetic differences. For example, sequence parallelism changes which activations are replicated, fused kernels change numerical ordering, and larger process groups change communication cost and failure behavior.
Still, the reconstruction changes how I read Megatron code. I no longer start with “which tensors are split?” I start with the dense equation and ask two questions:
- Which sum has been replaced by independent local products?
- At what point must those products be combined so forward and backward remain equivalent?
For the feed-forward block, those questions lead directly to the matched column-parallel and row-parallel projections—and to one collective in each direction.
The next reconstruction applies the same discipline to attention rather than the MLP: tensor-parallel grouped-query attention keeps complete query/KV head groups together, then tests the separate forward and backward communication obligations.
References
- NVIDIA, Megatron Core User Guide.
- NVIDIA, Megatron Core parallelism strategies.
- NVIDIA, Tensor-parallel layers API.