Part of Language AI Handbook
Explains how tensor parallelism splits weight matrices across GPUs using column and row strategies, enabling training of models too large for any single device.
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Tensor Parallelism
Training a large language model with hundreds of billions of parameters is not something a single GPU can do on its own. A single NVIDIA A100 has 80 GB of memory, and a model like GPT-3 requires roughly 350 GB just to store its weights in 16-bit precision, before accounting for optimizer states, gradients, and activations. The simplest response to this constraint is data parallelism, where each GPU holds a complete copy of the model but processes a different slice of the minibatch. But data parallelism fails precisely when the model itself does not fit on one device. You cannot copy what you cannot hold.
Tensor parallelism solves this differently. Instead of replicating the entire model across GPUs, it splits individual tensors across multiple devices. Each GPU holds only a shard of each weight matrix, performs computation on its shard, and then communicates with peer GPUs to assemble the full result. The model is not replicated; it is partitioned. This lets you train models whose individual layers would overflow any single device's memory.
The idea sounds straightforward, but realizing it efficiently requires care. A naive partitioning strategy might simply distribute layers to different GPUs (inter-layer parallelism), but this creates severe load-balancing problems and requires activations to travel from one device to another between every layer. Tensor parallelism takes a different approach: it operates within a single layer, distributing the matrix multiplications that dominate transformer computation. By splitting weight matrices in mathematically compatible ways, tensor parallelism allows each GPU to do useful work simultaneously on every layer, with communication happening at precisely defined synchronization points rather than at every layer boundary.
This chapter covers the mechanics of tensor parallelism in depth: how weight matrices are split along columns or rows, what communication is required to combine shards into correct outputs, how sequences of column-parallel and row-parallel layers are chained to minimize synchronization, and how the Megatron-LM system from NVIDIA Research formalized these ideas into a practical training framework used for some of the largest models in existence. We will build understanding from first principles, working through the math of why the splits are correct before looking at implementation.
As we discussed in earlier chapters on the transformer architecture, the dominant compute in a transformer layer comes from two sources: the multi-head attention mechanism and the two-layer feed-forward network (FFN). Tensor parallelism in Megatron style targets both of these components, splitting the weight matrices in carefully chosen directions to enable maximum parallelism with minimum communication.
Why Split Tensors?
Before diving into how tensor parallelism works, it helps to be precise about the problem it solves and why the solutions take the forms they do.
A standard linear layer computes , where is a batch of tokens each with hidden dimension , and is a weight matrix mapping to some output dimension . The output is dense: every output element depends on every input element. At first glance, this density suggests that any partitioning of the computation must involve data dependencies, and therefore communication, across devices.
But the density of the output does not mean the computation is inseparable. Matrix multiplication has a fundamental block structure that allows it to be decomposed in multiple ways. Understanding this structure is the conceptual key to tensor parallelism.
When and are large (e.g., for GPT-3's hidden size), the weight matrix can be enormous. A single linear layer in GPT-3's attention has four weight matrices for Q, K, V, and output projections, each of size , consuming roughly 576 MB each in fp16. Across 96 layers, the attention weights alone exceed 200 GB. The FFN layers add another 150 GB or more. No single device, regardless of generation, can hold this.
The key insight enabling tensor parallelism is that matrix multiplication is separable. Different columns of contribute to different columns of without any cross-dependency, and different rows of can each be multiplied with the corresponding input slice independently, with results accumulated at the end. This separability is the mathematical foundation for both column-parallel and row-parallel splitting strategies. The question is not whether the computation can be split, but how to split it such that the communication required to assemble final results is minimized and falls at convenient points in the computation graph.
The choice of splitting direction is not arbitrary. Different layers and positions in the transformer have different mathematical structures, and the clever design of tensor parallelism is that it exploits these structures to pair column splits with row splits in a way that eliminates intermediate communication. The column-parallel output of one layer is exactly the right input format for a row-parallel next layer, creating a natural pipeline that requires only a single all-reduce per sub-layer rather than a communication step between every layer.
Column Parallelism
Column parallelism splits a weight matrix along its output (column) dimension. Given GPUs with indices , we partition the weight matrix into column slices:
where each .
Each GPU stores its slice and computes:
where:
- is the full input, replicated on all GPUs
- is the column shard on GPU
- is the partial output on GPU
The partial outputs together form the full output: concatenating along the column dimension recovers the full . This is correct because matrix multiplication with a partitioned weight matrix produces a partitioned result:
The critical observation is that each GPU's computation is completely independent. No communication is needed during the forward pass to produce ; each device can compute its shard without knowledge of what the other devices are doing. The price is that the input must be fully available on every GPU before computation begins. In practice, this is handled by an all-gather operation at the start of the layer or by keeping the input in the replicated state (which happens automatically when tensor parallelism is paired with data parallelism).
Why is replicating the input acceptable? Because the input to each transformer layer is the residual stream, which after an all-reduce from the previous layer is identical on all GPUs. There is no communication cost to have a replicated input in steady-state operation, since the previous layer's row-parallel all-reduce already distributed it.
Column Parallelism for Multi-Head Attention
Column parallelism is natural for the query, key, and value projections in multi-head attention. Recall that multi-head attention splits the model's hidden dimension into heads, where each head operates on a dimensional subspace. When you have GPUs and split the weight matrix along the head dimension, each GPU naturally handles attention heads. The column-parallel split corresponds exactly to distributing heads across devices.
This alignment between tensor parallelism and multi-head attention is not a coincidence. The original Megatron-LM paper was designed with this correspondence in mind. Because attention heads are completely independent of one another during the self-attention computation (they attend to the full sequence in parallel subspaces without interacting), you can assign entire heads to individual GPUs and have each GPU compute attention for its assigned heads without ever communicating with other GPUs during the attention computation itself. The interaction between heads happens only when their outputs are projected back to the model's hidden dimension, and that projection is handled by the row-parallel output projection.
For an attention layer with hidden size and 32 heads on 4 GPUs, each GPU holds the Q, K, V projection weights for 8 heads: matrices of size rather than . Each GPU attends over its 8 heads independently and produces attention outputs of size . The output projection then acts on these 1024-dimensional features row-parallel, accumulating the full 4096-dimensional output across GPUs via all-reduce.
Column Parallelism for the Feed-Forward Network
The feed-forward network (FFN) also benefits from column parallelism. A standard transformer FFN applies two linear transformations with a nonlinearity between them:
where:
- is the input batch with tokens and hidden dimension
- is the expansion weight matrix, increasing the dimension by a factor of 4
- is the expansion bias
- is the activation function (GeLU in most modern transformers, ReLU in older ones)
- is the contraction weight matrix, reducing back to
- is the output bias
Column parallelism applied to splits the expansion across GPUs. GPU receives the full input and computes using its column shard (for a 4-way split of a -dimensional FFN). The activation function is then applied locally because GeLU and ReLU operate element-wise on each dimension independently. This is a subtle but important point: element-wise nonlinearities are trivially parallelizable because each output element depends on exactly one input element, which lies entirely within the local shard.
After the nonlinearity, each GPU holds . These shards together form the full intermediate activation , ready to be consumed by the row-parallel second layer.

Row Parallelism
Row parallelism splits a weight matrix along its input (row) dimension. Given the weight matrix , we partition along the row dimension into slices:
where each .
Each GPU stores its row shard and receives a corresponding slice of the input, then computes:
where:
- is the input shard on GPU
- is the row shard on GPU
- is the partial result on GPU
The full output requires summing the partial results:
This is an all-reduce operation: each device produces a partial sum over the full output dimension , and summing these partial results gives the correct final output. Unlike column parallelism, where the output shards can be consumed independently (each GPU's shard feeds into a corresponding downstream shard), row parallelism requires synchronization to assemble the full output.
The correctness of this decomposition follows directly from the block form of matrix multiplication. If we write the full input as (conceptually partitioned along the column dimension) and the weight matrix as stacked row blocks, then:
Each term is a partial contribution to the full output, and summing them gives the correct result. Note that each partial result has the full output shape rather than a shard of it. This is the defining feature of row parallelism: the partial results are over the full output space, and correctness requires their sum, not their concatenation.
Why Row Parallelism Follows Column Parallelism Naturally
The design of row parallelism makes most sense when you see it as the second half of a pair. The output of a column-parallel layer is a feature-sharded tensor, where GPU holds the columns of the full hidden state. This is precisely the input format that row parallelism expects: GPU should receive input shard corresponding to its row shard . When you place a row-parallel layer directly after a column-parallel layer, the output of the first layer is already distributed in exactly the right way for the second layer to consume without any additional communication.
This chaining property is what makes tensor parallelism efficient. Without it, you would need an all-gather after the column-parallel layer to reassemble the full activation before the row-parallel layer, adding an extra communication step. With careful layer pairing, the communication is deferred to the very end, where a single all-reduce assembles the final output.
Combining Column and Row Parallelism
The real power of tensor parallelism emerges from chaining column-parallel and row-parallel layers such that the output of one feeds naturally into the other, eliminating unnecessary communication.
Consider the two-layer FFN with (expansion) followed by (contraction):
-
Column-parallel : GPU receives the full input and computes where is the -th column shard of . After applying the nonlinearity, each GPU holds , a shard of the intermediate activations. No communication has occurred at this point.
-
Row-parallel : GPU holds the -th row shard of , where this partitioning is along the input dimension of (which is the same dimension along which is partitioned). Each GPU computes , a partial sum over the full output space.
-
All-reduce: The partial sums are summed across GPUs to produce the full output .
The key observation is that the output of column parallelism (sharded along the feature dimension) is exactly the right input format for row parallelism (which expects sharded inputs along the same dimension). This means no communication is needed between the two layers. The only communication happens at the end of the row-parallel layer, in the all-reduce that assembles the final output.
For a two-layer block (FFN or attention), this yields a total of one all-reduce in the forward pass. Without this careful pairing, a naive implementation would require an all-gather after the column-parallel layer (to reassemble the full activations before the second layer), costing an extra communication step. This design choice, seeming minor in the abstract, has a substantial impact on training throughput: every saved all-gather at model scale represents gigabytes of data that do not need to traverse the interconnect on every forward pass.
The Role of Nonlinearity Placement
A subtle but important aspect of the FFN chaining is where the nonlinearity fits. The GeLU (or ReLU) function is applied between the two linear layers. In column-parallel mode, each GPU applies GeLU locally to its activation shard . This works correctly because GeLU is an element-wise function: depends only on the scalar value , not on any other element in the tensor. There are no cross-feature dependencies that would require communication.
This would not be the case for operations like softmax or layer normalization, which are reduction operations over features. Softmax requires the full feature vector to compute the normalizing denominator, and layer normalization requires statistics (mean and variance) over the feature dimension. These operations cannot be applied to feature shards independently. This is why tensor parallelism requires careful attention to which operations are tensor-parallelizable and which require full-feature access, a consideration that motivated the sequence parallelism extension discussed later.
Attention with Column-Row Chaining
Multi-head attention can be decomposed similarly to the FFN. The QKV projections are column-parallel: each GPU handles a subset of attention heads independently. The output projection maps from the concatenated head outputs back to the model dimension, and it is row-parallel.
Why does this work? Because attention heads are completely independent of one another in standard self-attention. Each head attends to the full sequence but produces its output in a separate subspace of the representation. When you distribute heads across GPUs, each GPU attends over its subset of heads without needing to know what other GPUs' heads are doing. The interaction between heads happens only at the output projection layer, which aggregates all head outputs and is naturally handled by the row-parallel reduce.
Concretely, the QKV weight matrices are split column-wise across GPUs, with each GPU holding the Q, K, V weights for its assigned heads. Each GPU computes attention scores and applies them to values independently, since heads do not communicate during the attention computation. The attention output for each GPU's heads is then passed through the row-parallel output projection, which accumulates partial sums across GPUs in an all-reduce.
This design has a pleasing symmetry with the FFN decomposition: both sub-layers require exactly one all-reduce in the forward pass, giving two all-reduces total per transformer layer. The all-reduces happen at the output projection of attention and at of the FFN. Between the two sub-layers, residual connections are added to the accumulated results. Because every GPU holds the full output after each all-reduce, the residual stream is synchronized and identical on all devices before the next sub-layer begins, making sure that the replicated-input assumption for the next column-parallel layer holds automatically.
Communication Patterns
The communication overhead in tensor parallelism is dominated by two collectives: all-reduce and all-gather.
An all-reduce operation sums tensors held by all processes and distributes the result back to every process. If each of GPUs starts with a tensor of size , the all-reduce produces the sum across all GPUs and every GPU ends with an identical copy of that sum. In ring-based implementations, the total data volume transmitted is per GPU, approaching as grows large.
An all-gather concatenates tensors held by all processes and distributes the result to every process. If each of GPUs starts with a shard of size , the all-gather produces the full tensor of size on every GPU. The total data volume is per GPU.
A reduce-scatter is the inverse of all-gather: it sums tensors across all GPUs, then distributes one shard of the result to each GPU. Each GPU ends with a different -sized portion of the total sum. The data volume is the same as all-gather: per GPU.
In the Megatron-style column-row chaining, the forward pass uses one all-reduce per transformer sub-layer to combine partial sums from row-parallel layers. This is the dominant communication pattern during training. The backward pass mirrors this with a symmetric but inverted set of collectives, as discussed below.
The communication volume per transformer layer scales as where is batch size, is sequence length, and is hidden dimension. This is the volume of one activation tensor, and each all-reduce transmits this volume. With modern NVLink interconnects providing 600 GB/s of bidirectional bandwidth on an A100 DGX node, the communication overhead is manageable for moderate tensor parallelism degrees (typically 8 within a node), but becomes expensive for larger degrees that span nodes and are limited to slower InfiniBand links at roughly 25-50 GB/s.
The practical implication is that tensor parallelism is almost always limited to a single node. Within a node, NVLink provides sufficient bandwidth to keep communication overhead at a few percent of compute time. Across nodes, the bandwidth drop of 10-20x makes the communication overhead prohibitive. This is why production training systems for the largest models use tensor parallelism within a node (for intra-layer splitting) and pipeline parallelism across nodes (for inter-layer splitting), a combination known as 3D parallelism when combined with data parallelism across pipeline groups.
Forward and Backward Communication Symmetry
A subtle but important point about tensor parallelism is that the communication pattern in the backward pass is the dual of the forward pass. This symmetry is what makes the approach efficient and predictable.
In the forward pass:
- Column-parallel layers require no communication (full input available, partial output produced)
- Row-parallel layers require an all-reduce (partial outputs summed to full output)
In the backward pass, the gradients flow in the opposite direction through the same graph. For row-parallel layers, the gradient of the loss with respect to the full output (which every GPU has after the forward all-reduce) must be distributed back to the input shards. This requires a reduce-scatter: each GPU receives the gradient slice corresponding to its input shard. For column-parallel layers, the gradient with respect to the weight shard can be computed locally (since the full input was available on every GPU), and the gradient with respect to the full input requires an all-reduce of the partial gradients computed from each weight shard.
This duality means that the backward pass has exactly as many collective operations as the forward pass, and each one transmits roughly the same data volume. The total communication volume per training step (forward plus backward) is approximately times the per-layer all-reduce volume times the number of layers. For a model like GPT-3 with 96 layers, an activation size of roughly 50 MB per layer, and a tensor-parallel degree of 8, this amounts to several gigabytes of data transmitted per training step, well within the capacity of a DGX node's NVLink fabric at reasonable batch sizes.
The Reduce-Scatter, All-Gather Decomposition
An important optimization for memory efficiency comes from observing that an all-reduce is equivalent to a reduce-scatter followed by an all-gather. This decomposition matters for sequence parallelism, which is discussed in the next section, but it is worth understanding in the context of standard tensor parallelism as well.
In a naive all-reduce, every GPU accumulates the full-size sum and every GPU holds the full result, using memory on each device. In a reduce-scatter plus all-gather, after the reduce-scatter each GPU holds only of the sum, and the all-gather distributes it. During the time between reduce-scatter and all-gather, memory usage is reduced by a factor of . By exploiting this gap (inserting useful computation like layer normalization between the two primitives), sequence parallelism reduces peak activation memory while keeping the same total communication volume.
Megatron-LM Parallelism
Megatron-LM, developed by NVIDIA Research and described in the 2019 paper "Megatron-LM: Training Multi-Billion Parameter Language Models Using Model Parallelism" by Shoeybi et al., is the reference implementation of tensor parallelism for transformer models. It introduced the column-parallel and row-parallel chaining strategy described above and demonstrated that it could train models with billions of parameters efficiently on NVIDIA DGX clusters.
The paper was significant because it paired the technique with careful engineering validation. Shoeybi et al. trained models up to 8.3 billion parameters (at the time, the largest dense language model trained on a single cluster) and showed that their approach scaled without degradation in efficiency as the degree of parallelism increased from 1 to 8 GPUs per tensor-parallel group. This empirical validation was important because theoretical analyses of distributed training often miss hardware-specific bottlenecks that only appear at scale.
The key contributions of Megatron-LM are:
- Intra-layer model parallelism: Splitting individual weight matrices across devices, as opposed to inter-layer model parallelism where different layers go to different devices. Intra-layer splitting allows simultaneous computation across all GPUs on every layer, whereas inter-layer splitting creates idle GPUs whenever a layer is not the bottleneck.
- Minimal communication design: The column-row pairing ensures only one all-reduce per sub-layer in the forward pass and one dual collective in the backward pass, minimizing synchronization overhead.
- Fusion with data parallelism: Megatron-LM trains with both tensor parallelism (splitting layers within a node) and data parallelism (replicating groups across nodes), allowing scaling to thousands of GPUs with straightforward composition.
The Megatron Transformer Block
A Megatron-style transformer block processes input through both the self-attention and FFN sub-layers, each using the column-row chaining pattern. From the perspective of a single GPU in a tensor-parallel group of size :
For the self-attention sub-layer, the input is available on all GPUs in the group (after the residual connection from the previous layer, which is identical on all devices after the previous all-reduce). The QKV weight matrices are each column-partitioned: this GPU holds columns for its rank within the tensor-parallel group. The output projection is row-partitioned along its input dimension.
The computation within a single transformer layer flows as follows. Each GPU computes its column-parallel QKV projections independently, using only the locally stored weight shards. It then performs self-attention on its assigned subset of heads completely independently, since heads in standard self-attention do not interact during the attention computation. The attention output from those heads is then passed through the row-parallel output projection. Each GPU accumulates a partial sum of the full-dimension output, and then an all-reduce assembles the complete post-attention representation. This output is added to the residual stream (which is identical on all GPUs because both tensors are synchronized), and then the FFN applies the same column-row pattern.
The total communication per transformer layer under this design is two all-reduces in the forward pass (one after attention, one after FFN) and two dual collectives in the backward pass. For a 48-layer model with hidden dimension 8192 on 8 GPUs, this amounts to roughly 96 all-reduce operations per forward pass, each transmitting an activation tensor of about 64 MB at batch size 4 and sequence length 2048. The aggregate communication per forward pass is around 6 GB, manageable on NVLink but expensive on InfiniBand.
Sequence Parallelism Extension
An important extension to the Megatron design is sequence parallelism, described in "Reducing Activation Recomputation in Large Transformer Models" by Korthikanti et al. (2022). The observation is that some operations in a transformer layer, specifically layer normalization and dropout, are not tensor-parallelized in the basic Megatron design; they operate on the full activation tensor replicated on every GPU. This wastes memory proportional to the tensor-parallel degree, since every GPU stores the same copy of the full activation during these replicated operations.
Sequence parallelism addresses this by distributing the sequence dimension across GPUs during the replicated operations. The key insight is that layer normalization and dropout operate independently on each token, so tokens can be distributed across GPUs (each GPU handling tokens) without any cross-GPU dependencies. During the tensor-parallel regions (attention and FFN), the model needs the feature dimension partitioned, not the sequence dimension. The transition between these two parallel modes requires collective operations: an all-gather to expand from sequence-parallel (token-sharded) to tensor-parallel (feature-replicated) mode, and a reduce-scatter to transition back.
These two new collectives replace the single all-reduce at the end of each sub-layer. The net result is that the total communication volume is unchanged (an all-reduce equals a reduce-scatter plus an all-gather), but now the activation memory is sharded throughout the layer rather than being replicated during layer normalization and dropout. Each GPU holds only of the activation memory throughout the full layer, reducing peak activation memory by a factor of . For large models with high tensor-parallel degrees, this memory savings can be the difference between a model that fits in memory and one that does not.
The combination of tensor parallelism and sequence parallelism as described in Korthikanti et al. became the standard for training large models in the Megatron ecosystem, and its ideas influenced subsequent parallelism designs from other research groups.
Worked Example: Column-Parallel FFN
Let's trace through a specific numerical example to make the computation concrete. Suppose we have a feed-forward network with input dimension , hidden dimension , and we are using GPUs.
The full weight matrix is split column-wise:
where lives on GPU 0 and lives on GPU 1.
Given an input (batch of 2 tokens, each with 4 features), both GPUs have the full input. GPU 0 computes:
and GPU 1 computes:
After the GeLU nonlinearity applied locally on each GPU, the activations and together form the full intermediate representation . Neither GPU needed to communicate with the other to reach this point.
The second weight matrix is split row-wise:
where on GPU 0 and on GPU 1.
GPU 0 multiplies its activation shard by its weight shard: . GPU 1 multiplies its activation shard by its weight shard: .
The final output is obtained by all-reducing (summing) across GPUs:
This is identical to the full computation , but computed with weight matrices that fit on individual GPUs. The only communication is the final all-reduce that sums and .
What makes this example particularly illuminating is the absence of intermediate communication. Between the column-parallel step and the row-parallel step, GPUs 0 and 1 proceed completely independently. GPU 0 never needs to know what looks like. GPU 1 never needs to know what looks like. They only need to coordinate once, at the very end, to produce the final output. This one communication per two linear layers is the core efficiency of Megatron-style tensor parallelism.
Code Implementation
Let's implement tensor parallelism from scratch using PyTorch's distributed computing primitives. We'll simulate multiple GPUs using CPU processes, then show how the same logic applies on actual multi-GPU hardware.
Setting Up Distributed Process Groups
First, we set up the distributed environment. In real training, this is handled by the launcher (e.g., torchrun), but for this tutorial, we'll configure it explicitly.
def setup_distributed(rank: int, world_size: int) -> None:
"""Initialize the distributed process group."""
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = "12355"
dist.init_process_group(
backend="gloo", # Use "nccl" for actual GPUs
rank=rank,
world_size=world_size,
)
def cleanup_distributed() -> None:
"""Clean up the distributed process group."""
dist.destroy_process_group()The process group is the fundamental abstraction for collective communication in PyTorch. Each GPU (or CPU process in simulation) has a rank within the group, and collective operations like all-reduce act across all ranks in the group. Real Megatron training uses NCCL backend for GPU-to-GPU communication via NVLink or InfiniBand; we use Gloo here for CPU-based simulation.
Column-Parallel Linear Layer
Now let's implement a column-parallel linear layer. Each process holds a column shard of the weight matrix and computes on the full input.
import torch
import torch.nn as nn
import torch.nn.functional as F
class ColumnParallelLinear(nn.Module):
"""
Linear layer split along the output (column) dimension.
Each rank holds a shard of shape (in_features, out_features // world_size).
The input X is replicated on all ranks.
The output Y is sharded: each rank produces columns [rank * shard_size : (rank+1) * shard_size].
"""
def __init__(
self,
in_features: int,
out_features: int,
world_size: int,
rank: int,
bias: bool = True,
) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.world_size = world_size
self.rank = rank
# Each rank holds out_features / world_size output columns
assert out_features % world_size == 0, (
f"out_features ({out_features}) must be divisible by world_size ({world_size})"
)
self.shard_size = out_features // world_size
# Weight shard for this rank: shape (shard_size, in_features)
# F.linear expects weight of shape (out_features, in_features)
self.weight = nn.Parameter(
torch.randn(self.shard_size, in_features) * (1.0 / in_features**0.5)
)
if bias:
# Bias shard: each rank has the bias for its output columns
self.bias = nn.Parameter(torch.zeros(self.shard_size))
else:
self.bias = None
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: Full input tensor of shape (batch, seq_len, in_features),
replicated on all ranks.
Returns:
Sharded output of shape (batch, seq_len, shard_size),
each rank has a different column slice.
"""
# Local matrix multiply: X @ W_shard^T + b_shard
# F.linear computes x @ weight.T + bias, weight shape: (shard_size, in_features)
output = F.linear(x, self.weight, self.bias)
return outputNotice that the forward method contains no communication. This is the defining property of column parallelism: each rank can compute its output shard entirely from local data. The weight shard has shard_size rows (corresponding to the output columns assigned to this rank) and in_features columns (the full input dimension). F.linear computes x @ weight.T + bias, which for our weight shape gives x @ (shard_size, in_features).T = x @ (in_features, shard_size), producing the correct output shard of shape (batch, seq_len, shard_size).
Row-Parallel Linear Layer
The row-parallel layer receives a sharded input (output from a column-parallel layer) and produces a full output via all-reduce.
class RowParallelLinear(nn.Module):
"""
Linear layer split along the input (row) dimension.
Each rank holds a shard of shape (in_features // world_size, out_features).
The input X is sharded: each rank receives its corresponding columns.
The full output Y is produced by summing partial results via all-reduce.
"""
def __init__(
self,
in_features: int,
out_features: int,
world_size: int,
rank: int,
bias: bool = True,
) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.world_size = world_size
self.rank = rank
assert in_features % world_size == 0, (
f"in_features ({in_features}) must be divisible by world_size ({world_size})"
)
self.shard_size = in_features // world_size
# Weight shard for this rank: shape (out_features, shard_size)
# F.linear expects weight of shape (out_features, in_features)
self.weight = nn.Parameter(
torch.randn(out_features, self.shard_size)
* (1.0 / in_features**0.5)
)
# Bias lives on rank 0 only and is added after the all-reduce
if bias and rank == 0:
self.bias = nn.Parameter(torch.zeros(out_features))
else:
self.bias = None
def forward(self, x_shard: torch.Tensor) -> torch.Tensor:
"""
Args:
x_shard: Sharded input of shape (batch, seq_len, shard_size).
Each rank provides its input shard (columns from the previous layer).
Returns:
Full output of shape (batch, seq_len, out_features) on all ranks.
"""
# Local matrix multiply: partial_Y = X_shard @ W_shard^T
# Shape: (batch, seq_len, out_features)
partial_output = F.linear(x_shard, self.weight)
# All-reduce: sum partial outputs across all ranks
# After this, every rank has the full output
dist.all_reduce(partial_output, op=dist.ReduceOp.SUM)
# Add bias (only rank 0 has it; others contribute zero)
if self.bias is not None:
partial_output = partial_output + self.bias
return partial_outputThe all-reduce call is the only communication in the row-parallel forward pass. Notice the bias handling: only rank 0 holds the bias term, which is added after the all-reduce. This is correct because the bias is a full-output-dimensional vector (not split), and adding it once (on rank 0) is equivalent to adding it to the summed output. Adding it on every rank would multiply the bias by world_size, which would be incorrect.
Tensor-Parallel Feed-Forward Network
We can now assemble a complete tensor-parallel FFN by chaining column-parallel and row-parallel layers.
class TensorParallelFFN(nn.Module):
"""
Tensor-parallel feed-forward network using column-row chaining.
W1 is column-parallel: each rank holds (d_model, 4*d_model/world_size)
W2 is row-parallel: each rank holds (4*d_model/world_size, d_model)
Communication: one all-reduce in the forward pass (inside RowParallelLinear)
"""
def __init__(
self,
d_model: int,
d_ffn: int,
world_size: int,
rank: int,
) -> None:
super().__init__()
self.column_parallel = ColumnParallelLinear(
in_features=d_model,
out_features=d_ffn,
world_size=world_size,
rank=rank,
)
self.row_parallel = RowParallelLinear(
in_features=d_ffn,
out_features=d_model,
world_size=world_size,
rank=rank,
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: Full input tensor (batch, seq_len, d_model) on all ranks.
Returns:
Full output tensor (batch, seq_len, d_model) on all ranks.
"""
# Column-parallel: full input → sharded activations
# Each rank computes GeLU(X @ W1_shard)
hidden = F.gelu(self.column_parallel(x))
# Row-parallel: sharded activations → full output (with all-reduce)
output = self.row_parallel(hidden)
return outputThe elegance of this implementation is in what is absent: there is no explicit communication between the column-parallel and row-parallel layers. The sharded output of column_parallel flows directly into row_parallel as a sharded input, with no reshaping, no synchronization, and no data movement. The single dist.all_reduce inside row_parallel handles all necessary communication for the entire two-layer block.
Demonstrating Correctness
To verify our tensor-parallel implementation produces the same result as a standard sequential FFN, we simulate two-GPU behavior by running the full computation twice with rank-0 and rank-1 shards.
def demonstrate_column_parallel_correctness() -> dict:
"""
Verify that column-parallel + row-parallel chaining gives
the same result as a standard sequential linear layer.
This function simulates 2-GPU behavior without actual distributed setup.
"""
torch.manual_seed(42)
d_model = 8
d_ffn = 16
batch_size = 2
seq_len = 3
world_size = 2
# Create full weight matrices (the reference)
W1_full = torch.randn(d_model, d_ffn) * (1.0 / d_model**0.5)
W2_full = torch.randn(d_ffn, d_model) * (1.0 / d_ffn**0.5)
b1_full = torch.zeros(d_ffn)
b2_full = torch.zeros(d_model)
# Full input, replicated on both "GPUs"
x = torch.randn(batch_size, seq_len, d_model)
# Reference (non-parallel) computation
hidden_full = F.gelu(x @ W1_full + b1_full)
y_reference = hidden_full @ W2_full + b2_full
# Simulate rank 0
shard_size_ffn = d_ffn // world_size # = 8
W1_shard_0 = W1_full[:, :shard_size_ffn] # (8, 8)
W2_shard_0 = W2_full[:shard_size_ffn, :] # (8, 8)
b1_shard_0 = b1_full[:shard_size_ffn]
hidden_0 = F.gelu(x @ W1_shard_0 + b1_shard_0) # (2, 3, 8)
partial_y_0 = hidden_0 @ W2_shard_0 # (2, 3, 8)
# Simulate rank 1
W1_shard_1 = W1_full[:, shard_size_ffn:] # (8, 8)
W2_shard_1 = W2_full[shard_size_ffn:, :] # (8, 8)
b1_shard_1 = b1_full[shard_size_ffn:]
hidden_1 = F.gelu(x @ W1_shard_1 + b1_shard_1) # (2, 3, 8)
partial_y_1 = hidden_1 @ W2_shard_1 # (2, 3, 8)
# All-reduce: sum partial results + bias
y_parallel = partial_y_0 + partial_y_1 + b2_full
max_diff = (y_reference - y_parallel).abs().max().item()
mean_diff = (y_reference - y_parallel).abs().mean().item()
return {
"max_absolute_diff": max_diff,
"mean_absolute_diff": mean_diff,
"outputs_match": max_diff < 1e-5,
"y_reference_shape": list(y_reference.shape),
"y_parallel_shape": list(y_parallel.shape),
"d_model": d_model,
"d_ffn": d_ffn,
"world_size": world_size,
"shard_size": shard_size_ffn,
}
results = demonstrate_column_parallel_correctness()Tensor Parallelism Correctness Verification ================================================== Input shape: (batch=2, seq=3, d_model=8) Output shape: [2, 3, 8] FFN hidden dim: 16 split into 2 shards of 8 each Max absolute difference (reference vs parallel): 2.38e-07 Mean absolute difference: 3.34e-08 Outputs match (diff < 1e-5): True
The near-zero numerical difference confirms that the column-parallel and row-parallel chaining produces exactly the same result as the sequential computation. Any remaining difference is due to floating-point arithmetic order: when you sum the same values in a different sequence, rounding errors at the bit level can introduce tiny discrepancies that are entirely correct behavior. The correctness proof guarantees mathematical equivalence; the implementation delivers numerical equivalence up to floating-point precision.
Memory Savings Analysis
A key benefit of tensor parallelism is reduced memory per GPU. Let's compute how memory requirements scale with the tensor-parallel degree.
def compute_memory_savings(
d_model: int,
num_layers: int,
ffn_ratio: int = 4,
bytes_per_param: int = 2, # fp16
) -> dict:
"""
Compute weight memory per GPU for different tensor-parallel degrees.
"""
# Parameter counts for one transformer layer
# Attention: 4 weight matrices of shape (d_model, d_model) for Q, K, V, O
attn_params_per_layer = 4 * d_model * d_model
# FFN: W1 of (d_model, ffn_ratio*d_model), W2 of (ffn_ratio*d_model, d_model)
ffn_params_per_layer = 2 * d_model * (ffn_ratio * d_model)
total_params_per_layer = attn_params_per_layer + ffn_params_per_layer
total_params = total_params_per_layer * num_layers
total_bytes = total_params * bytes_per_param
total_gb = total_bytes / (1024**3)
memory_by_degree = {}
for degree in [1, 2, 4, 8, 16]:
params_per_gpu = total_params / degree
bytes_per_gpu = params_per_gpu * bytes_per_param
gb_per_gpu = bytes_per_gpu / (1024**3)
memory_by_degree[degree] = {
"params_per_gpu": int(params_per_gpu),
"gb_per_gpu": gb_per_gpu,
"reduction_factor": degree,
}
return {
"total_params": total_params,
"total_gb": total_gb,
"memory_by_degree": memory_by_degree,
"d_model": d_model,
"num_layers": num_layers,
}
# GPT-3 scale: d_model=12288, 96 layers
memory_info = compute_memory_savings(d_model=12288, num_layers=96)Model: d_model=12288, 96 layers
Total weight parameters: 173.9B
Total weight memory (fp16): 324.0 GB
TP Degree | Params/GPU | Weight GB/GPU
---------------------------------------------
1 | 173.9B | 324.0
2 | 87.0B | 162.0
4 | 43.5B | 81.0
8 | 21.7B | 40.5
16 | 10.9B | 20.2These numbers show why tensor parallelism is essential at GPT-3 scale. With a tensor-parallel degree of 8, each GPU holds roughly 40 GB of weight memory for GPT-3's attention and FFN layers combined, well within the 80 GB capacity of an A100. Without tensor parallelism, a single GPU would need to hold over 320 GB for those weights alone, far exceeding any available device memory. This estimate covers only the core weight matrices; embedding tables, optimizer states, and gradient buffers add substantially more to the total memory burden. The optimizer states for mixed-precision training (maintaining fp32 master weights alongside fp16 working weights) roughly triple the weight memory, which is why real training systems need tensor-parallel degrees of 8 or more even for models much smaller than GPT-3.

Communication Overhead Estimation
Understanding when tensor parallelism becomes the bottleneck requires estimating how much time the all-reduce operations consume relative to compute. Let's build this analysis.
def estimate_communication_overhead(
d_model: int,
seq_len: int,
batch_size: int,
num_layers: int,
num_gpus: int,
bandwidth_gb_per_s: float,
compute_tflops_per_gpu: float,
) -> dict:
"""
Estimate communication time relative to compute time for tensor parallelism.
Each transformer layer requires 2 all-reduces (one for attention, one for FFN).
Each all-reduce transmits one full activation tensor of shape (batch, seq, d_model).
"""
bytes_per_element = 2 # fp16
# Size of one activation tensor
activation_size_bytes = batch_size * seq_len * d_model * bytes_per_element
activation_size_gb = activation_size_bytes / (1024**3)
# In a ring all-reduce with N GPUs, each GPU sends and receives 2*(N-1)/N * data
# For communication time, use the effective bandwidth of the ring
all_reduce_volume_gb = 2 * (num_gpus - 1) / num_gpus * activation_size_gb
all_reduce_time_s = all_reduce_volume_gb / bandwidth_gb_per_s
# Total communication: 2 all-reduces per layer
total_comm_time_s = 2 * num_layers * all_reduce_time_s
# Compute time estimate for transformer
# Each transformer layer: ~24 * d_model^2 * batch * seq FLOPs
# (4 proj matrices in attention: 2 * 4 * d^2 * batch * seq = 8 * d^2 * batch * seq
# + FFN 2 linear layers: 2 * 2 * 4 * d^2 * batch * seq = 16 * d^2 * batch * seq)
flops_per_layer = 24 * (d_model**2) * batch_size * seq_len
total_flops = flops_per_layer * num_layers
# With TP degree = num_gpus, compute is distributed across GPUs
flops_per_gpu = total_flops / num_gpus
# A100 achieves ~60% of peak on typical transformer workloads
effective_tflops = compute_tflops_per_gpu * 0.6
compute_time_s = flops_per_gpu / (effective_tflops * 1e12)
comm_to_compute_ratio = total_comm_time_s / compute_time_s
return {
"activation_size_mb": activation_size_gb * 1024,
"all_reduce_volume_mb": all_reduce_volume_gb * 1024,
"all_reduce_time_ms": all_reduce_time_s * 1000,
"total_comm_time_ms": total_comm_time_s * 1000,
"compute_time_ms": compute_time_s * 1000,
"comm_to_compute_ratio": comm_to_compute_ratio,
"comm_overhead_pct": comm_to_compute_ratio * 100,
}
# NVLink (within node): ~600 GB/s bidirectional
nvlink_overhead = estimate_communication_overhead(
d_model=4096,
seq_len=2048,
batch_size=4,
num_layers=32,
num_gpus=8,
bandwidth_gb_per_s=300.0, # One-directional NVLink
compute_tflops_per_gpu=312.0, # A100 fp16
)
# InfiniBand (across nodes): ~25 GB/s
infiniband_overhead = estimate_communication_overhead(
d_model=4096,
seq_len=2048,
batch_size=4,
num_layers=32,
num_gpus=8,
bandwidth_gb_per_s=12.5, # One-directional InfiniBand
compute_tflops_per_gpu=312.0,
)Communication Overhead Analysis (8 GPUs, d_model=4096, 32 layers)
=================================================================
NVLink InfiniBand
-----------------------------------------------------------------
Activation size (MB) 64.0 64.0
All-reduce volume/op (MB) 112.0 112.0
All-reduce time (ms) 0.365 8.750
Total comm time (ms) 23.3 560.0
Compute time (ms) 70.5 70.5
Comm overhead (%) 33.1 794.5The contrast between NVLink and InfiniBand overhead illustrates why tensor parallelism is typically restricted to within a single DGX node (8 GPUs). NVLink's high bandwidth keeps communication below compute time through the typical within-node range, while InfiniBand communication already exceeds compute time at degree 2 in this simplified model. The analysis also shows why the minimal-communication design of Megatron's column-row chaining matters: at InfiniBand speeds, even the carefully optimized two-all-reduces-per-layer design is expensive. A naive design requiring four or six all-reduces per layer would be completely impractical.


Key Parameters
The key parameters for tensor parallelism configuration are:
- tensor_parallel_degree (tp): Number of GPUs across which each layer is split. Must divide all weight dimensions evenly. Typically 1, 2, 4, or 8 within a single node.
- pipeline_parallel_degree (pp): Number of stages in pipeline parallelism, used alongside tensor parallelism. A cluster of GPUs forms one complete model replica.
- data_parallel_degree (dp): Number of replicas used for data parallelism. Total GPUs .
- sequence_length: Affects activation memory proportionally. Longer sequences amplify the benefit of sequence parallelism.
- all_reduce_backend: Use
ncclfor NVIDIA GPU communication (exploits NVLink/InfiniBand),gloofor CPU.
Limitations and Practical Considerations
Tensor parallelism is powerful, but it comes with real constraints that practitioners must account for when designing training infrastructure.
Divisibility Requirements
The most immediate limitation is that every dimension you want to shard must be divisible by the tensor-parallel degree. This sounds like a minor implementation detail, but it shapes model architecture choices from the ground up. A model with a hidden dimension of 4096 and 32 attention heads can be tensor-parallelized with degrees 1, 2, 4, 8, 16, and 32 (all of which divide both 4096 and 32 evenly), but a model with a hidden dimension of 3840 and 30 heads is more constrained. If you want degree 8, you need all key dimensions to be divisible by 8.
Large research teams deploying Megatron-style training consistently choose hidden dimensions and head counts that are highly divisible: powers of 2 or multiples of 128 are standard. GPT-3's hidden dimension of 12288 is , divisible by 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, and more. This kind of divisibility is not accidental; it is designed to accommodate tensor parallelism at multiple degrees without architectural changes.
FFN dimensions are similarly constrained. The standard FFN expansion factor of 4 gives a hidden dimension of , and the product must be divisible by the tensor-parallel degree. In practice, since itself is divisible by 2 and 4, any hidden dimension divisible by 8 will work with degrees up to 8. At degree 16, you need to be divisible by 16, so must be divisible by 4, which is true for all powers of 2.
Communication Bandwidth Dependency
The second major limitation is communication bandwidth dependency. The all-reduce operations that make tensor parallelism correct are not free; they consume network bandwidth proportional to the activation tensor size at every layer. On modern DGX nodes, NVLink provides 600 GB/s bidirectional bandwidth, which is typically sufficient for tensor-parallel degrees up to 8 within a node. Crossing node boundaries over InfiniBand introduces a 10-20x bandwidth reduction, making tensor parallelism inefficient at those scales.
This is why Megatron-style systems typically combine tensor parallelism (within a node) with pipeline parallelism (across nodes). Pipeline parallelism communicates only at pipeline stage boundaries, transmitting only the activations for one microbatch at the boundary layer rather than all-reducing full activations at every layer. This reduces cross-node communication volume by orders of magnitude compared to applying tensor parallelism across nodes. The combination of intra-node tensor parallelism and inter-node pipeline parallelism became the standard architecture for large-scale training, codified in the Megatron v2 and v3 papers.
Tight Coupling Within Tensor-Parallel Groups
Tensor parallelism increases the degree of synchronization required within a group. With GPUs in a tensor-parallel group, all devices must synchronize at every all-reduce. If one GPU stalls due to a hardware issue, memory pressure, or kernel scheduling delay, all other GPUs in the group wait. This tight coupling means that hardware failures or stragglers have immediate impact on training throughput.
In contrast, data parallelism is much more loosely coupled: each GPU processes its own microbatch independently, and gradient averaging happens once per step rather than twice per transformer layer. Pipeline parallelism falls in between: stages must synchronize at pipeline boundaries, but each stage can compute independently otherwise. The tight synchronization of tensor parallelism is one reason it is restricted to within a single node, where communication is fast enough that synchronization delays are measured in microseconds rather than milliseconds.
Weight Initialization
Implementing tensor parallelism correctly requires careful attention to weight initialization. When a weight matrix is split across GPUs and each shard is initialized independently from a normal distribution, the effective initialization variance seen by the full layer computation differs from what a single-GPU initialization would produce.
Consider column parallelism: each GPU initializes its shard from . The full output has variance proportional to (by standard variance analysis of matrix-vector products). This is the same as if the full matrix had been initialized with variance on a single GPU. So far, so good.
For row parallelism, the situation is different. Each GPU computes a partial sum , and the all-reduce computes . The variance of is times the variance of a single (since the are independent). To maintain the correct variance, Megatron-LM scales the initialization of row-parallel weight shards by . This ensures that the aggregated output has the same distribution as the single-GPU case. Without this adjustment, deeper layers would see activations with inflated variance, potentially destabilizing training.
Practical Degree Selection
Choosing the right tensor-parallel degree in practice involves balancing several competing factors. The key considerations are:
- Memory capacity: Use a degree large enough that the model fits on each GPU with room for optimizer states and activations.
- Divisibility: All key dimensions (hidden size, number of heads, FFN intermediate size) must be divisible by the degree.
- Communication budget: On NVLink hardware, degrees 2-8 are typically efficient. Higher degrees diminish returns because the per-layer communication overhead grows while the per-GPU compute shrinks.
- Node boundary alignment: Keep tensor parallelism within a single node to exploit NVLink. Use pipeline or data parallelism for cross-node scaling.
The interaction between these factors means that choosing a tensor-parallel degree is fundamentally a hardware-aware decision. A model that trains efficiently with degree 8 on an A100 DGX node might train better with degree 4 on older hardware with slower NVLink, because the same communication volume represents a larger fraction of compute time.
Despite these limitations, tensor parallelism has proven essential for training the largest language models. Models like GPT-3, Megatron-Turing NLG (530 billion parameters), and subsequent systems from Google, Meta, and Anthropic all rely on tensor parallelism as a core component of their training infrastructure. The technique scales naturally with the number of high-bandwidth links available and integrates cleanly with data and pipeline parallelism to enable distributed training at scales that were previously considered impractical.
We will explore how tensor parallelism integrates with pipeline parallelism and data parallelism in the next chapter, which covers 3D parallelism strategies for training at extreme scale.
Summary
Tensor parallelism partitions individual weight matrices across multiple GPUs, enabling models to be trained whose layers would overflow the memory of any single device. The two fundamental splitting strategies are:
- Column parallelism: The weight matrix is split along the output dimension. Each GPU holds a column shard and computes on the full input independently, producing a shard of the output. No communication is needed during the forward pass; output shards are consumed directly by a downstream row-parallel layer.
- Row parallelism: The weight matrix is split along the input dimension. Each GPU holds a row shard and receives the corresponding input shard. Each GPU produces a full-size partial sum, and an all-reduce aggregates these partial sums into the correct final output.
Chaining column-parallel and row-parallel layers eliminates intermediate communication: the shard-format output of the column-parallel layer is precisely the input format the row-parallel layer expects, so no all-gather is needed between them. This enables a transformer's FFN and attention sub-layers to execute with only one all-reduce per sub-layer in the forward pass, minimizing synchronization overhead to two all-reduces per transformer layer.
The Megatron-LM framework from NVIDIA Research established this column-row chaining as the standard approach for intra-layer model parallelism. The design exploits the natural parallelism of multi-head attention (heads are independent and map cleanly to column-parallel splits) and the FFN structure (the expansion layer is column-parallel, the contraction layer is row-parallel). Extended with sequence parallelism (distributing sequence tokens across GPUs during non-tensor-parallel operations like layer normalization), the Megatron design achieves near-linear memory reduction proportional to the tensor-parallel degree while keeping communication overhead manageable on high-bandwidth NVLink interconnects.
Key takeaways:
- Tensor parallelism is complementary to data parallelism and pipeline parallelism, and production training systems combine all three in what is often called 3D parallelism.
- The optimal tensor-parallel degree depends on model size, layer dimensions, network bandwidth, and GPU memory capacity. Within a node, degrees of 2-8 are typical.
- Model architecture choices (hidden dimension, number of heads, FFN expansion ratio) should be divisible by likely tensor-parallel degrees; powers of 2 and multiples of 128 are standard choices.
- Crossing node boundaries with tensor parallelism is expensive due to InfiniBand's lower bandwidth relative to NVLink; pipeline parallelism is preferred for inter-node communication.
- Weight initialization must account for the tensor-parallel degree: row-parallel layers require a scaling factor to maintain correct output variance when shards are summed.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about tensor parallelism.
Reference
Citation details
Cite or share this article.
Continue with the full handbook
This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.
Explore Language AI HandbookStay up to date
Get articles, book updates, and news delivered to your inbox.
No spam, unsubscribe anytime.
Join the community
Sign in to remove popups, track your reading progress, and join the discussion.

Comments
No comments yet. Be the first to share your thoughts!