LLM DAILYA field guide to language models

Day 55 / 12 min read

Tensor parallelism — how several GPUs cooperate on one Transformer layer

Yesterday we saw how ZeRO lets a training job shard persistent model state instead of storing a complete copy on every data-parallel worker.

But ZeRO leaves us with a different question:

What if a single Transformer layer is itself too large — or too slow — for one GPU?

Then we can split the computation inside the layer across multiple accelerators.

This is tensor parallelism.

The core idea is:

\[ \boxed{\text{split a large matrix multiplication into smaller matrix multiplications that different GPUs perform simultaneously}} \]

That sounds straightforward. The interesting part is deciding where to split the tensors so that the GPUs do useful local work before they have to communicate.

Start with one linear layer

A large fraction of Transformer computation is matrix multiplication.

Suppose a layer computes:

\[ Y=XW, \]

where:

\[ X\in\mathbb{R}^{B\times d_{\text{in}}}, \]
\[ W\in\mathbb{R}^{d_{\text{in}}\times d_{\text{out}}}, \]

and:

\[ Y\in\mathbb{R}^{B\times d_{\text{out}}}. \]

Here \(B\) can represent the collection of token positions being processed together.

If \(W\) is enormous, one GPU must both:

  1. store the entire matrix,
  2. perform the entire multiplication.

Tensor parallelism asks:

Can GPU 0 own part of \(W\), GPU 1 own another part, and both compute at the same time?

Yes.

But there are two especially useful ways to cut the matrix.

Column parallelism: split the outputs

Suppose we split \(W\) by columns:

\[ W= \begin{bmatrix} W_1 & W_2 \end{bmatrix}. \]

Then:

\[ Y = X \begin{bmatrix} W_1 & W_2 \end{bmatrix} = \begin{bmatrix} XW_1 & XW_2 \end{bmatrix}. \]

So two GPUs can compute:

\[ Y_1=XW_1 \]

and:

\[ Y_2=XW_2. \]

Conceptually:

                  ┌→ GPU 0: W1 → Y1
X on both GPUs ───┤
                  └→ GPU 1: W2 → Y2

final Y = [Y1 Y2]

Each GPU owns only half of the output features.

This is often called column-parallel linear algebra because we partition the columns of the weight matrix.

The useful property is that each GPU can compute its output shard independently once it has \(X\).

A concrete example

Suppose:

\[ X= \begin{bmatrix} 1 & 2 \end{bmatrix} \]

and:

\[ W= \begin{bmatrix} 1 & 10 & 100 & 1000\\ 2 & 20 & 200 & 2000 \end{bmatrix}. \]

Split the columns:

\[ W_1= \begin{bmatrix} 1 & 10\\ 2 & 20 \end{bmatrix}, \qquad W_2= \begin{bmatrix} 100 & 1000\\ 200 & 2000 \end{bmatrix}. \]

GPU 0 computes:

\[ Y_1=XW_1 = \begin{bmatrix} 5 & 50 \end{bmatrix}. \]

GPU 1 computes:

\[ Y_2=XW_2 = \begin{bmatrix} 500 & 5000 \end{bmatrix}. \]

Concatenate them:

\[ Y= \begin{bmatrix} 5 & 50 & 500 & 5000 \end{bmatrix}. \]

The result is exactly what one GPU would have computed with the full \(W\).

We changed the physical execution, not the mathematics.

Row parallelism: split the inputs

Now split \(W\) by rows:

\[ W= \begin{bmatrix} W_1\\ W_2 \end{bmatrix}. \]

To match that partition, split the input features:

\[ X= \begin{bmatrix} X_1 & X_2 \end{bmatrix}. \]

Then:

\[ Y = XW = X_1W_1+X_2W_2. \]

GPU 0 computes:

\[ Z_1=X_1W_1, \]

while GPU 1 computes:

\[ Z_2=X_2W_2. \]

But neither GPU has the final answer.

We must combine:

\[ Y=Z_1+Z_2. \]

Conceptually:

GPU 0: X1 × W1 → Z1 ─┐
                      ├→ SUM → Y
GPU 1: X2 × W2 → Z2 ─┘

That sum requires communication across the tensor-parallel group.

This is the central tension in tensor parallelism:

\[ \boxed{\text{sharding reduces local compute and memory, but creates communication}} \]

The jigsaw-puzzle mental model

Imagine two people assembling a giant picture.

A bad strategy is:

After placing every puzzle piece, stop and compare the entire puzzle.

They spend more time coordinating than assembling.

A better strategy is:

Give each person a large region they can work on independently, then synchronize only when the regions must connect.

Tensor parallelism works the same way.

The goal is not merely to divide arithmetic evenly.

The goal is to create large chunks of local computation between communication points.

That is why the exact tensor partition matters.

Transformers contain a natural column-then-row pattern

Consider a simplified feed-forward network:

\[ H=\phi(XW_1), \]
\[ Y=HW_2. \]

Typically \(W_1\) expands the hidden dimension and \(W_2\) projects it back down.

For example:

\[ d\rightarrow4d\rightarrow d. \]

A clever tensor-parallel layout can split \(W_1\) by columns.

Each GPU produces only part of the expanded hidden representation:

                 ┌→ GPU 0: H0
X replicated ────┤
                 └→ GPU 1: H1

Now notice something important.

The activation function \(\phi\), such as GELU or SiLU, is elementwise.

Therefore:

\[ \phi([H_0,H_1]) = [\phi(H_0),\phi(H_1)]. \]

We do not need to gather the full \(H\) before applying the nonlinearity.

Each GPU can continue locally.

Then \(W_2\) can be row-partitioned so that each GPU consumes its local hidden shard:

GPU 0: H0 → local W2 shard → Z0 ─┐
                                 ├→ SUM → Y
GPU 1: H1 → local W2 shard → Z1 ─┘

We delayed communication until after substantial useful work.

That is much better than synchronizing after every operation.

This is why tensor-parallel design follows the computation graph

Suppose we partition a tensor, then immediately run an operation requiring the full tensor.

We would have to gather it right away.

The sharding accomplished little.

A good partition instead tries to preserve a tensor's distributed layout across several compatible operations.

Conceptually:

BAD:

split
 ↓
matmul
 ↓
GATHER
 ↓
elementwise op
 ↓
split again


BETTER:

split
 ↓
matmul
 ↓
elementwise op
 ↓
next sharded matmul
 ↓
REDUCE

The second design gets more computation per communication event.

This ratio is analogous to arithmetic intensity inside a GPU.

At cluster scale, we care about something like:

\[ \frac{\text{useful local computation}} {\text{bytes communicated}}. \]

Attention can also be tensor-parallel

Multi-head attention gives us another natural partition.

Suppose attention has:

\[ N_h=64 \]

query heads and we use 8 GPUs.

A simple strategy is to assign:

\[ \frac{64}{8}=8 \]

heads to each GPU.

Conceptually:

GPU 0 → heads  0–7
GPU 1 → heads  8–15
GPU 2 → heads 16–23
...
GPU 7 → heads 56–63

Many computations for those heads can happen locally.

This is appealing because attention heads are partly independent before their outputs are combined.

Again, model structure gives us a useful place to cut the computation.

GQA makes the partition more subtle

Recall grouped-query attention:

\[ N_{\text{KV}}<N_Q. \]

Suppose:

\[ N_Q=64, \qquad N_{\text{KV}}=8. \]

Across 8 tensor-parallel GPUs, we might naturally assign one K/V head and its associated query heads to each GPU.

But if:

\[ N_{\text{KV}}=4 \]

and we still want 8-way tensor parallelism, there are fewer K/V heads than GPUs.

Now the serving or training system may need to replicate some K/V information or choose a different partition.

So model architecture can constrain efficient parallelism.

This is a recurring theme:

\[ \boxed{\text{the best distributed-system layout depends on the model's tensor shapes}} \]

Imagine splitting one Transformer layer across 8 GPUs.

Every forward pass may require collective communication between those GPUs.

Then backward requires additional communication.

And this happens layer after layer:

layer 1:
compute → communicate

layer 2:
compute → communicate

layer 3:
compute → communicate

...

Unlike ZeRO parameter gathering, tensor-parallel communication sits directly inside the computation of each layer.

Latency and bandwidth therefore matter enormously.

This is why tensor-parallel groups are often placed across GPUs with very fast interconnects.

A rough performance model for a collective is:

\[ T_{\text{comm}} \approx T_{\text{latency}} + \frac{\text{bytes transferred}} {\text{effective bandwidth}}. \]

For large tensors, bandwidth dominates.

For many small collectives, latency can matter greatly.

More GPUs do not imply proportional speedup

Suppose one GPU takes:

\[ 100\text{ ms} \]

to compute a layer.

With two GPUs, perhaps each performs roughly half the arithmetic:

\[ 50\text{ ms}. \]

But now add:

\[ 15\text{ ms} \]

of communication.

The layer takes approximately:

\[ 65\text{ ms}, \]

not 50 ms.

Speedup is:

\[ \frac{100}{65} \approx1.54\times, \]

not:

\[ 2\times. \]

With still more GPUs, local compute per GPU shrinks while communication may not shrink proportionally.

Eventually:

\[ \text{communication cost} \]

can dominate:

\[ \text{local compute}. \]

That creates a practical limit to tensor-parallel degree.

Strong scaling eventually runs out

This is an example of strong scaling.

We keep the model and workload fixed while adding more processors.

Ideally:

\[ T_N=\frac{T_1}{N}. \]

In reality:

\[ T_N \approx \frac{T_{\text{compute}}}{N} + T_{\text{communication}}(N) + T_{\text{overhead}}(N). \]

As \(N\) increases, the first term falls.

The other terms do not necessarily fall.

At some point adding GPUs yields little benefit — or can even make execution slower.

This is why "use more GPUs" is not a complete performance strategy.

Tensor parallelism and ZeRO are fundamentally different

Yesterday's distinction is now worth making precise.

With ZeRO-3, parameters are sharded at rest, but a layer's parameters may be gathered so a worker can perform the layer computation.

Tensor parallelism says:

Do not reconstruct the whole layer on one worker. Keep the layer partitioned and have multiple GPUs compute it together.

Conceptually:

ZeRO-3:

distributed weights
      ↓
gather needed layer
      ↓
local layer computation


Tensor parallelism:

distributed weights
      ↓
distributed layer computation
      ↓
collective communication

They solve different bottlenecks.

ZeRO primarily attacks redundant training-state memory.

Tensor parallelism lets one layer's storage and arithmetic span multiple devices.

Tensor parallelism also matters for inference

Suppose a 400-billion-parameter model simply does not fit on one GPU.

We can shard its weight matrices across several GPUs and execute each inference layer cooperatively.

So tensor parallelism is useful in both:

\[ \text{training} \]

and:

\[ \text{inference}. \]

But the performance tradeoffs differ.

During inference decode, each step may involve relatively little arithmetic per request.

That can make communication especially painful.

A tensor-parallel decode path may repeatedly perform:

small local computation
      ↓
collective
      ↓
small local computation
      ↓
collective

This is another reason batching matters: more active sequences can increase local work between synchronization points.

Tensor parallelism changes the memory picture too

If a weight matrix contains:

\[ M \]

bytes and we shard it evenly across \(N\) GPUs, each GPU stores approximately:

\[ \frac{M}{N}. \]

So tensor parallelism reduces per-GPU parameter memory for the sharded layers.

But unlike ZeRO, that memory reduction is inseparable from distributed execution.

Every forward pass uses all members of the tensor-parallel group.

The GPUs are not independent replicas.

They collectively behave like one larger accelerator.

That gives us a useful mental distinction:

data-parallel GPUs:
multiple workers doing different examples

tensor-parallel GPUs:
multiple pieces of one logical worker

What happens when we combine them?

Suppose we have 32 GPUs.

We could form tensor-parallel groups of 4 GPUs:

TP group 0: GPUs  0–3
TP group 1: GPUs  4–7
TP group 2: GPUs  8–11
...
TP group 7: GPUs 28–31

Each group behaves like one logical model worker.

Then the 8 groups can process different data in parallel.

So:

\[ \text{tensor parallel degree}=4, \]
\[ \text{data parallel degree}=8, \]

and:

\[ 4\times8=32\text{ GPUs}. \]

We can then add ZeRO-style sharding across the data-parallel dimension.

Now the topology matters.

Fast intra-node links may be used for tensor parallelism, while slower inter-node networking handles less latency-sensitive data-parallel communication.

This is why physical cluster topology becomes part of the training algorithm.

Pipeline parallelism gives us another axis

If the model is extremely deep, we might also split layers into pipeline stages.

For example:

pipeline stage 0:
layers  1–20
uses 4-way tensor parallelism

pipeline stage 1:
layers 21–40
uses 4-way tensor parallelism

pipeline stage 2:
layers 41–60
uses 4-way tensor parallelism

Then replicate the entire pipeline for data parallelism.

A large training job can therefore have coordinates like:

\[ (\text{data},\text{pipeline},\text{tensor}). \]

A GPU's role is determined by its position in that multidimensional grid.

This is sometimes called 3D parallelism.

Additional techniques can introduce still more axes, such as sequence or context parallelism.

Why communication topology becomes algorithmic

Suppose 8 GPUs inside one server communicate through extremely fast links, while communication between servers is slower.

Then an efficient arrangement might choose:

tensor parallelism
→ mostly inside a server

data parallelism
→ across servers

because tensor parallelism communicates frequently inside every layer.

We want its collectives on the fastest links.

So two clusters with the same number and type of GPUs can have different training performance if their network topology differs.

The hardware graph matters:

which GPU connects to which GPU
through what link
at what bandwidth
with what latency

The logical parallelism should map intelligently onto that graph.

The deeper connection to yesterday

ZeRO and tensor parallelism both begin from the same observation:

\[ \boxed{\text{one GPU does not have to own the whole model}} \]

But they exploit that observation differently.

ZeRO says:

Partition state to remove redundant storage, then communicate when a worker needs missing state.

Tensor parallelism says:

Partition the operation itself, so several GPUs jointly execute one layer.

One is largely about where state resides.

The other is about where arithmetic happens.

At very large scale, those questions merge into a single systems problem.

The key mental model

Think of a giant matrix multiplication as moving a piano.

Data parallelism says:

Give several teams identical pianos and let each team move a different one.

ZeRO says:

Stop making every team permanently store an identical warehouse of equipment.

Tensor parallelism says:

This particular piano is too heavy for one team. Put several teams on the same piano.

Adding more people helps only if they can coordinate efficiently.

If everyone must stop every few seconds to negotiate who holds which corner, coordination overwhelms the benefit.

That gives us today's main principle:

\[ \boxed{ \text{tensor parallelism trades local model size and computation for tightly coupled communication} } \]

And that is why tensor parallelism is powerful but not free.

The art is not simply splitting a tensor across GPUs.

It is choosing splits that let each GPU perform as much independent, useful computation as possible before the next synchronization.

At large scale, the Transformer architecture, tensor shapes, collective algorithms, GPU topology, and network bandwidth all become parts of the same computation.

LLM Daily Last updated