Over the last few lessons, we have learned to cut a Transformer along several axes.
Data parallelism cuts the batch.
Tensor parallelism cuts the hidden dimensions and matrix operations.
Pipeline parallelism cuts the layers.
FSDP cuts the stored model state.
Today we will cut another dimension that becomes increasingly important as context windows grow:
This family of techniques is usually called sequence parallelism or, for attention specifically, context parallelism.
The intuition is simple:
If a request contains 128,000 tokens, why should every GPU hold the activations for all 128,000 positions?
Instead, different GPUs can own different stretches of the sequence.
The catch is that attention allows tokens in one stretch to interact with tokens in another. So splitting the sequence saves memory, but creates a new communication problem.
Start with the activation tensor¶
A Transformer layer typically receives something shaped like:
Here:
- \(B\) is batch size,
- \(T\) is sequence length,
- \(d_{\text{model}}\) is hidden width.
Suppose:
In BF16, the raw hidden-state tensor alone contains:
values.
At 2 bytes each:
And that is only one hidden-state tensor.
Training may need many intermediate activations across many layers.
Long context therefore puts enormous pressure on activation memory.
Split the sequence across four GPUs¶
Instead of every GPU holding all \(128{,}000\) token positions, divide them:
GPU 0 → tokens 0–31,999
GPU 1 → tokens 32,000–63,999
GPU 2 → tokens 64,000–95,999
GPU 3 → tokens 96,000–127,999
Each GPU now owns:
positions.
For tensors whose computation is independent across token positions, this is easy.
Suppose we apply RMSNorm:
The normalization for token \(t\) depends on that token's hidden dimensions, not on other token positions.
So GPU 0 can normalize its 32,000 tokens while GPU 1 normalizes its own 32,000 tokens.
No token-to-token communication is required.
The same is true for many elementwise operations.
Why this pairs naturally with tensor parallelism¶
Yesterday's tensor parallelism often split the hidden dimension.
Imagine four GPUs holding:
GPU 0 → hidden channels 0–2047
GPU 1 → hidden channels 2048–4095
GPU 2 → hidden channels 4096–6143
GPU 3 → hidden channels 6144–8191
Each GPU may therefore hold all tokens but only part of each token.
For some operations, that is awkward.
Layer normalization or RMSNorm wants all hidden channels belonging to a token.
Sequence parallelism can effectively transpose the ownership pattern:
tensor-parallel layout:
GPU 0 → all tokens, hidden shard 0
GPU 1 → all tokens, hidden shard 1
GPU 2 → all tokens, hidden shard 2
GPU 3 → all tokens, hidden shard 3
↓ redistribute
sequence-parallel layout:
GPU 0 → token shard 0, full hidden width
GPU 1 → token shard 1, full hidden width
GPU 2 → token shard 2, full hidden width
GPU 3 → token shard 3, full hidden width
Now each GPU can perform token-local operations independently.
So sequence parallelism is often less about making the model mathematically different and more about choosing a better layout for intermediate activations.
A tiny matrix mental model¶
Imagine the activation matrix for one batch item:
Draw it as:
hidden dimension d
┌─────────────────────────────┐
token 1 │ │
token 2 │ │
token 3 │ │
... │ │
token T │ │
└─────────────────────────────┘
Tensor parallelism often cuts vertically:
┌───────┬───────┬───────┬───────┐
│ GPU 0 │ GPU 1 │ GPU 2 │ GPU 3 │
│ │ │ │ │
│ │ │ │ │
└───────┴───────┴───────┴───────┘
Sequence parallelism cuts horizontally:
┌─────────────────────────────┐
GPU 0 │ │
├─────────────────────────────┤
GPU 1 │ │
├─────────────────────────────┤
GPU 2 │ │
├─────────────────────────────┤
GPU 3 │ │
└─────────────────────────────┘
Same tensor.
Different cut.
This is a recurring theme in distributed ML:
But attention breaks the easy story¶
For an MLP or normalization, token positions can often be processed independently.
Self-attention explicitly couples them.
Recall:
Attention computes:
Suppose GPU 3 owns the final 32,000 tokens.
Its queries may need to attend to keys and values from earlier tokens owned by:
GPU 0
GPU 1
GPU 2
For causal attention, a query near token 120,000 may depend on nearly every previous token.
So simply saying:
GPU 3 owns token 120,000
does not mean GPU 3 has everything needed to compute that token's attention.
This is the central challenge of context parallelism.
Why long context makes this worth solving¶
Ignoring optimized kernels for a moment, the attention score matrix has shape:
If:
then:
possible query-key score positions.
Modern kernels such as FlashAttention avoid materializing that entire matrix in HBM, which is crucial.
But even with memory-efficient attention, the model must conceptually process interactions between distant queries and keys.
Long context therefore creates two separate scaling pressures:
activation storage
+
attention computation / communication
Sequence and context parallelism help distribute both across GPUs.
One strategy: move K and V around the ring¶
Suppose four GPUs each own a sequence chunk:
GPU 0 → Q0, K0, V0
GPU 1 → Q1, K1, V1
GPU 2 → Q2, K2, V2
GPU 3 → Q3, K3, V3
GPU 3's queries \(Q_3\) need earlier keys and values.
Instead of gathering every \(K\) and \(V\) onto every GPU at once, the GPUs can circulate K/V blocks.
Conceptually:
round 1:
GPU 0 uses K0,V0
GPU 1 uses K1,V1
GPU 2 uses K2,V2
GPU 3 uses K3,V3
↓ rotate K,V blocks
round 2:
GPU 0 receives another K,V block
GPU 1 receives another K,V block
GPU 2 receives another K,V block
GPU 3 receives another K,V block
↓ compute partial attention
↓ rotate again
After enough rounds, each query block has interacted with every key/value block it is allowed to attend to.
This family of ideas is often called ring attention.
The key insight is:
But softmax seems to require all scores at once¶
There is a subtle problem.
For one query, attention probabilities are:
If the scores \(s_j\) are spread across GPUs or arrive block by block, how can we compute the denominator without storing every score?
The answer is an online softmax.
Suppose we have processed some score block and maintain:
- current maximum \(m\),
- current exponential sum \(\ell\),
- current weighted value accumulator \(o\).
When a new block arrives with maximum \(m_{\text{new}}\), we can update the running statistics using a new global maximum:
The old normalization contribution can be rescaled:
The weighted value accumulator can be updated similarly.
This means attention can process K/V blocks incrementally without materializing all scores simultaneously.
This same mathematical trick is one of the foundations behind FlashAttention.
A concrete four-chunk example¶
Suppose a sequence has only eight tokens:
A B C D E F G H
Split it across four GPUs:
GPU 0 → A B
GPU 1 → C D
GPU 2 → E F
GPU 3 → G H
Consider the query for token H.
Because attention is causal, H may attend to:
A B C D E F G H
GPU 3 begins with local keys:
G H
It computes the partial attention contribution from those tokens.
Then it receives:
E F
and updates its online softmax statistics.
Then:
C D
and finally:
A B.
At no point does GPU 3 necessarily need one giant score vector stored in memory.
It streams through blocks and maintains the sufficient statistics needed to produce the exact attention output.
That is a useful mental model for distributed long-context attention:
The query stays home; the relevant memory pages travel past it.
Causal masking creates useful asymmetry¶
For bidirectional attention, every query may need every K/V block.
For causal language modeling, token \(t\) only attends to positions:
That means early sequence chunks need less information than late chunks.
For example:
GPU 0 queries → only chunk 0
GPU 1 queries → chunks 0,1
GPU 2 queries → chunks 0,1,2
GPU 3 queries → chunks 0,1,2,3
A good context-parallel implementation can exploit this triangular structure rather than performing useless work on masked future tokens.
The exact scheduling becomes a systems optimization problem.
Sequence parallelism versus context parallelism¶
The terminology can be confusing because different libraries use these names somewhat differently.
A useful conceptual distinction is:
Sequence parallelism often refers to sharding token-position activations for operations that are naturally token-local, frequently in conjunction with tensor parallelism.
Context parallelism usually emphasizes distributing the sequence dimension through attention itself, where tokens on one GPU must interact with K/V information owned by other GPUs.
So:
sequence parallelism
→ "split activation positions"
context parallelism
→ "make attention work across those split positions"
The exact implementation vocabulary varies, but this mental model is robust.
Training memory improves¶
Suppose an activation tensor costs:
bytes when all \(T\) positions are local.
With sequence parallel degree \(N_{\text{SP}}\), the sharded part ideally falls toward:
If a long-context training run has 40 GB of sequence-dependent activations per GPU, four-way sequence sharding might reduce the sharded portion toward:
Not every activation or temporary buffer scales perfectly this way, so real savings differ.
But the principle is powerful:
This becomes increasingly important as context windows grow.
Inference has a related problem: the KV cache¶
During autoregressive inference, we store keys and values from previous tokens.
For one layer, a simplified KV-cache size scales like:
The factor of 2 is for keys and values.
The crucial term is:
Double context length and, all else equal, KV-cache memory roughly doubles.
So distributing sequence-related state can also matter enormously for inference serving.
Long context is not merely a compute problem.
It is a memory-placement problem.
Why sequence parallelism is not free¶
As always, splitting something creates a boundary.
Without sequence sharding:
token activations
→ local
With sequence sharding:
token activations
→ distributed
Whenever an operation needs information across sequence shards, communication appears.
So the trade becomes:
This should feel familiar.
Activation checkpointing traded memory for compute.
FSDP traded memory for parameter communication.
Tensor parallelism traded large local compute for frequent partial-result communication.
Sequence parallelism trades sequence-local memory for redistribution and cross-context communication.
Put all the cuts on one tensor¶
Consider again:
We now recognize three obvious axes:
Different parallelism strategies can attack each:
And the Transformer stack itself has another axis:
So a useful abstraction is:
That is not the whole story—parameters, experts, optimizer state, and other structures add more axes—but it captures a surprising amount.
A 1,024-GPU thought experiment¶
Suppose a training job uses:
and:
Then:
What are those GPUs doing?
16 data-parallel groups
→ different training examples
within each replica:
8 pipeline stages
→ different layer ranges
within each stage:
4 tensor-parallel shards
→ different hidden/matrix pieces
within those groups:
2 context-parallel shards
→ different sequence regions
A single logical training example may therefore involve many GPUs simultaneously.
Yet mathematically we still describe the model simply as:
This gap between the simplicity of the mathematical definition and the complexity of the physical execution is one of the defining features of modern LLM systems.
The deeper mental model: tensors have geography¶
Early in learning neural networks, we think about tensors mostly in terms of:
shape
dtype
values
At large scale, a tensor has another property:
For:
you should increasingly imagine a map:
batch dimension → which replica?
sequence dimension → which context shard?
hidden dimension → which tensor-parallel rank?
layer dimension → which pipeline stage?
Two mathematically adjacent values may live on different GPUs.
An operation is cheap when the values it needs are already colocated.
An operation becomes expensive when the required values cross machine boundaries.
That gives us today's main principle:
The algebra tells us which values must interact.
The distributed system decides where those values live and how they meet.
And long-context LLMs make this especially visible:
When the sequence becomes too large to live comfortably in one place, the context itself becomes a distributed object.