LLM DAILYA field guide to language models

Day 42 / 12 min read

Tensor parallelism — splitting one matrix multiplication across GPUs

Yesterday we saw fully sharded data parallelism (FSDP).

FSDP solves a storage problem:

A GPU does not need to permanently hold the entire model.

But during computation, FSDP typically gathers the parameters needed for a layer so that each worker can execute that layer on its local data.

That leaves another problem.

What if one layer itself is too large, or we want more compute than one GPU can provide for a single token stream?

Then we can split the layer's actual mathematics across GPUs.

That is tensor parallelism.

Its central idea is:

\[ \boxed{\text{split one large tensor operation into several smaller tensor operations}} \]

Instead of GPU 0 computing an entire matrix multiplication, several GPUs compute pieces of it simultaneously and communicate only when the pieces must be combined.

Start with the operation hiding inside almost every Transformer layer

Much of a Transformer is matrix multiplication.

For example:

\[ Y = XW. \]

Suppose:

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

and:

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

Then:

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

In an LLM, \(B\) here can represent many token positions flattened together.

If:

\[ d_{\text{in}}=8192 \]

and:

\[ d_{\text{out}}=32768, \]

then \(W\) contains:

\[ 8192\times32768 = 268{,}435{,}456 \]

parameters.

At BF16, that one matrix is roughly:

\[ 268{,}435{,}456\times2 \approx537\text{ MB}. \]

A single Transformer block contains several large matrices like this.

Tensor parallelism asks:

Why should one GPU own and multiply the whole matrix?

Split the matrix by columns

Suppose we have two GPUs.

Write:

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

Each half contains different output columns.

Then:

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

So GPU 0 can store \(W_1\) and compute:

\[ Y_1=XW_1. \]

GPU 1 stores \(W_2\) and computes:

\[ Y_2=XW_2. \]

Then:

\[ Y= \begin{bmatrix} Y_1 & Y_2 \end{bmatrix}. \]

Visually:

                    W

          ┌─────────┬─────────┐
          │   W1    │   W2    │
          └─────────┴─────────┘
               ↓         ↓
             GPU 0     GPU 1

X ───────────→ XW1      XW2

               ↓         ↓

              Y1         Y2

               └────┬────┘
                    ↓

              [ Y1 | Y2 ]

Each GPU stores only half of \(W\) and performs roughly half of the matrix multiplication.

This is column-parallel linear algebra.

A tiny numerical example

Let:

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

and:

\[ W= \begin{bmatrix} 3 & 4 & 5 & 6\\ 7 & 8 & 9 & 10 \end{bmatrix}. \]

The full multiplication is:

\[ XW = \begin{bmatrix} 17 & 20 & 23 & 26 \end{bmatrix}. \]

Now split \(W\) across two GPUs:

\[ W_1= \begin{bmatrix} 3 & 4\\ 7 & 8 \end{bmatrix}, \qquad W_2= \begin{bmatrix} 5 & 6\\ 9 & 10 \end{bmatrix}. \]

GPU 0 computes:

\[ XW_1 = \begin{bmatrix} 17 & 20 \end{bmatrix}. \]

GPU 1 computes:

\[ XW_2 = \begin{bmatrix} 23 & 26 \end{bmatrix}. \]

Concatenate them:

\[ \begin{bmatrix} 17 & 20 \end{bmatrix} \;\Vert\; \begin{bmatrix} 23 & 26 \end{bmatrix} = \begin{bmatrix} 17 & 20 & 23 & 26 \end{bmatrix}. \]

Exactly the same answer.

The mathematics has not changed.

Only the physical execution has.

But the next layer creates a problem

Suppose the next operation is:

\[ Z=YV. \]

Our \(Y\) is split:

GPU 0: Y1
GPU 1: Y2

One possibility is to gather the full \(Y\) onto both GPUs.

But if we gather after every matrix multiplication, communication can overwhelm the benefit.

A better trick is to choose the partitioning of the next matrix so that it naturally consumes the split output.

This is where tensor parallelism becomes more interesting than simply "cut every matrix in half."

Split the next matrix by rows

Since:

\[ Y= \begin{bmatrix} Y_1 & Y_2 \end{bmatrix}, \]

partition \(V\) by rows:

\[ V= \begin{bmatrix} V_1\\ V_2 \end{bmatrix}. \]

Then:

\[ YV = \begin{bmatrix} Y_1 & Y_2 \end{bmatrix} \begin{bmatrix} V_1\\ V_2 \end{bmatrix}. \]

Multiplying gives:

\[ Z = Y_1V_1 + Y_2V_2. \]

Now GPU 0 computes:

\[ Z_1=Y_1V_1 \]

while GPU 1 computes:

\[ Z_2=Y_2V_2. \]

Then the GPUs only need to sum:

\[ Z=Z_1+Z_2. \]

That sum can be implemented with a collective such as an all-reduce.

The pattern is:

column-parallel layer

GPU 0: XW1 → Y1
GPU 1: XW2 → Y2

          ↓

row-parallel layer

GPU 0: Y1V1 → Z1
GPU 1: Y2V2 → Z2

          ↓

      all-reduce

          ↓

       Z1 + Z2

Notice what we avoided:

gather full Y

between the two large matrix multiplications.

This maps beautifully onto the Transformer MLP

Recall the simplified feed-forward network:

\[ \operatorname{MLP}(x) = W_2\,\sigma(W_1x). \]

Ignoring notation orientation, the first projection expands the hidden dimension:

\[ d_{\text{model}} \rightarrow d_{\text{ff}}. \]

The second projection contracts it:

\[ d_{\text{ff}} \rightarrow d_{\text{model}}. \]

Suppose:

\[ d_{\text{model}}=8192 \]

and:

\[ d_{\text{ff}}=32768. \]

With four tensor-parallel GPUs, the expanded hidden dimension can be split:

GPU 0 → FFN channels 0–8191
GPU 1 → FFN channels 8192–16383
GPU 2 → FFN channels 16384–24575
GPU 3 → FFN channels 24576–32767

Each GPU performs the activation function on its own channels.

Then the second projection consumes those local channels and produces a partial contribution to the final hidden state.

Those partial contributions are summed.

Conceptually:

             x
             │
     ┌───────┼───────┬────────┐
     ↓       ↓       ↓        ↓
   GPU0    GPU1    GPU2     GPU3
   W1_0    W1_1    W1_2     W1_3
     ↓       ↓       ↓        ↓
    h0      h1      h2       h3
     ↓       ↓       ↓        ↓
   GELU    GELU    GELU     GELU
     ↓       ↓       ↓        ↓
   W2_0    W2_1    W2_2     W2_3
     ↓       ↓       ↓        ↓
    z0      z1      z2       z3
     └───────┼───────┴────────┘
             ↓
          all-reduce
             ↓
       z0+z1+z2+z3

The large MLP computation is physically spread across four accelerators.

Attention can be split too

Multi-head attention gives us another convenient dimension.

Suppose a Transformer has:

\[ 64 \]

attention heads.

With eight tensor-parallel GPUs, we can assign:

\[ 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

Each GPU can compute Q, K, V and attention for its local heads.

The outputs eventually need to participate in the output projection, where another carefully chosen partition and collective reconstruct the correct result.

This is one reason the multi-head structure of Transformers is friendly to parallel hardware.

The restaurant-kitchen mental model

Imagine a restaurant receives one enormous order.

Data parallelism is like opening four identical kitchens and sending a different customer's order to each kitchen.

Kitchen 0 → customer A
Kitchen 1 → customer B
Kitchen 2 → customer C
Kitchen 3 → customer D

Tensor parallelism is different.

One customer's order is too large for one kitchen.

So:

Kitchen 0 → appetizers
Kitchen 1 → pasta
Kitchen 2 → vegetables
Kitchen 3 → desserts

They are all working on the same order.

At certain points, their outputs must be combined before service can continue.

That synchronization is the price of splitting one computation across kitchens.

Tensor parallelism reduces parameter memory per GPU

If a weight matrix contains:

\[ P \]

parameters and we split it evenly across \(N\) tensor-parallel GPUs, each stores roughly:

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

For our 537 MB matrix across four GPUs:

\[ \frac{537}{4} \approx134\text{ MB/GPU}. \]

More importantly, the matrix multiplication itself is also divided.

Each GPU performs roughly one quarter of the relevant FLOPs.

So tensor parallelism can provide both:

\[ \text{lower per-GPU parameter memory} \]

and:

\[ \text{more aggregate compute}. \]

But again, there is a price.

Communication happens inside every Transformer block

With ordinary data parallelism, GPUs can independently execute almost an entire forward/backward pass before synchronizing gradients.

Tensor-parallel GPUs cannot.

They cooperate on the same layer.

That means communication may occur repeatedly:

attention projection
↓
collective
↓
attention output
↓
collective
↓
MLP projection
↓
collective
↓
next layer

The exact pattern depends on the architecture and implementation, but the principle is:

\[ \boxed{\text{tensor parallelism puts communication on the layer-by-layer critical path}} \]

That makes network latency and bandwidth extremely important.

Why tensor-parallel groups are often kept local

Suppose eight GPUs inside one server have extremely fast links.

Communication between servers is slower.

Because tensor parallelism communicates so frequently, it is often attractive to keep a tensor-parallel group within the fastest local fabric:

server 0:

GPU 0
GPU 1
GPU 2
GPU 3
GPU 4
GPU 5
GPU 6
GPU 7

← high-bandwidth tensor-parallel communication →

Then slower inter-server links can be used for parallelism patterns that communicate less frequently.

This is not a universal rule, but it illustrates a major design principle:

\[ \boxed{\text{map the most communication-intensive parallelism onto the fastest links}} \]

The physical topology of the cluster influences the logical topology of the model.

Tensor parallelism is not free scaling

Suppose one GPU takes:

\[ 100\text{ ms} \]

to execute a large layer.

With two GPUs, the arithmetic might ideally fall toward:

\[ 50\text{ ms}. \]

But suppose combining partial results costs:

\[ 15\text{ ms}. \]

Then total time becomes roughly:

\[ 65\text{ ms}. \]

The speedup is:

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

not \(2\times\).

With more GPUs, each GPU gets less arithmetic while communication remains.

Eventually:

\[ \text{communication cost} \]

can dominate:

\[ \text{compute saved}. \]

So there is usually a practical tensor-parallel degree rather than "use as many GPUs as possible."

Small matrix pieces can also hurt GPU efficiency

GPUs love large matrix multiplications.

They can keep many compute units busy and amortize overhead.

If we split a matrix across too many GPUs, each local multiplication becomes smaller.

At some point:

huge GEMM
→ excellent GPU utilization

turns into:

many smaller GEMMs
→ worse utilization

So increasing tensor parallelism creates two possible penalties:

\[ \text{more communication} \]

and:

\[ \text{less efficient local compute}. \]

This is another reason parallelism degree must be tuned.

Tensor parallelism during inference

Tensor parallelism is not just a training technique.

It is extremely useful for inference.

Suppose a model's weights require:

\[ 320\text{ GB}. \]

With 80 GB GPUs, the model cannot fit on one accelerator.

With four-way tensor parallelism:

\[ \frac{320}{4} = 80\text{ GB/GPU} \]

before accounting for KV cache and overhead.

The model's layers can execute across the four GPUs as one logical inference engine.

During autoregressive decoding, however, each generated token repeatedly traverses those tensor-parallel collectives.

So inference latency becomes sensitive to interconnect performance too.

This is why putting tensor-parallel inference across distant machines is usually much less attractive than keeping the group tightly connected.

Compare FSDP and tensor parallelism carefully

Yesterday's FSDP and today's tensor parallelism both shard parameters.

But they answer different questions.

FSDP asks:

Where should the parameters live when they are not being used?

Tensor parallelism asks:

How should the layer's actual computation be divided while it is running?

A useful simplified comparison is:

FSDP

persistent:
GPU 0 → W0
GPU 1 → W1

compute:
gather W
GPU 0 → full layer on data A
GPU 1 → full layer on data B

versus:

TENSOR PARALLELISM

persistent:
GPU 0 → W0
GPU 1 → W1

compute:
GPU 0 → part of layer
GPU 1 → part of layer
        ↓
combine partial results

So:

\[ \boxed{\text{FSDP primarily shards state; tensor parallelism shards the operation itself}} \]

Combine tensor and data parallelism

Suppose we have:

\[ 32 \]

GPUs.

We could organize them into eight groups of four:

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

Within each group:

\[ \text{tensor parallel size}=4. \]

The four GPUs cooperate to execute one model replica.

Across the eight groups:

\[ \text{data parallel size}=8. \]

Each group processes different training data.

Conceptually:

                    different data
                         ↓

DP replica 0 → [GPU GPU GPU GPU]
DP replica 1 → [GPU GPU GPU GPU]
DP replica 2 → [GPU GPU GPU GPU]
...
DP replica 7 → [GPU GPU GPU GPU]

                 ↔ tensor parallel ↔

So total GPU count is:

\[ N_{\text{GPU}} = N_{\text{DP}} \times N_{\text{TP}}. \]

Here:

\[ 32=8\times4. \]

This is the beginning of multi-dimensional parallelism.

Add yesterday's FSDP idea

Now imagine the data-parallel replicas also shard optimizer state and perhaps parameters using FSDP.

We now have several overlapping decompositions:

tensor parallelism
→ split one layer's computation

data parallelism
→ split training examples

FSDP
→ split persistent training state

gradient accumulation
→ split global batch across time

activation checkpointing
→ split activation lifetime across time

Each solves a different bottleneck.

Modern large-model training stacks combine them because no single dimension scales indefinitely.

The deeper mental model: choose where the equals sign happens

Return to:

\[ Z = Y_1V_1 + Y_2V_2. \]

GPU 0 can independently compute:

\[ Y_1V_1. \]

GPU 1 can independently compute:

\[ Y_2V_2. \]

The GPUs only need to communicate at the point where mathematics says:

\[ + \]

must happen.

This gives a powerful way to think about distributed tensor computation:

Keep operations local for as long as algebra permits, and communicate only when the mathematical pieces must meet.

Good tensor-parallel layouts are therefore partly exercises in algebra.

We partition matrices so that consecutive operations can consume local pieces without constantly reconstructing full tensors.

The best partition is not merely the one that divides memory evenly.

It is the one that minimizes expensive synchronization while preserving large, efficient local matrix multiplications.

The progression so far

We can now see a hierarchy of increasingly aggressive decomposition.

Ordinary data parallelism:

whole model
different data

FSDP:

sharded model storage
temporarily reconstructed layers
different data

Tensor parallelism:

sharded model storage
sharded layer computation
same token stream crosses multiple GPUs

The conceptual shift is important.

With data parallelism, a GPU can mostly pretend the others do not exist until synchronization.

With tensor parallelism, the GPUs are parts of a single virtual accelerator.

A token moving through the Transformer is effectively moving through a machine assembled from several physical GPUs.

That gives us today's main principle:

\[ \boxed{ \text{tensor parallelism turns multiple GPUs into one wider logical GPU} } \]

But that illusion only works if the GPUs can exchange partial results fast enough.

So once again, the limiting resource moves.

We started with a model too large for one GPU.

We divided the computation.

Now the challenge becomes moving the boundaries between those pieces efficiently.

That is the recurring story of large-scale LLM systems:

Every time we split something to solve a memory or compute problem, we create a communication problem at the cut.

LLM Daily Last updated