No history yet

Scaling and Parallelism

Transcript

Beau

Okay, Jo. So we've built this thing. We have our Transformer block, we've got the training loop humming with AMP and a fancy learning rate scheduler, we can even generate text with a KV-cache. But... it's all running on one GPU. It feels like we built a high-performance engine and put it in a go-kart.

Jo

That's the perfect analogy. And this is the step where we move from the go-kart to the Formula 1 chassis. Because a single GPU, no matter how powerful, has a hard memory ceiling. You simply can't fit a 70-billion-parameter model into 80 gigs of VRAM.

Beau

Right. So... let's talk about the first, most obvious step. Distributed Data Parallel, or DDP. I've used it before, but what's actually happening under the hood in PyTorch?

Jo

DDP is conceptually the simplest. Imagine you have, say, eight GPUs. PyTorch, via DDP, will clone your entire model—every single weight, bias, everything—onto each of those eight GPUs. They are perfect replicas.

Beau

And the data? The batch gets split, right?

Jo

Exactly. If you have a global batch size of 256, each of the eight GPUs gets a micro-batch of 32. Each GPU then does a full forward and backward pass on its own little slice of data, completely independently.

Beau

Okay, but then you have eight different sets of gradients. If they all just update their local model, they'll diverge immediately. How do they sync up?

Jo

That's the magic trick. It's an operation called `all_reduce`. After the gradients are computed on each GPU, they all talk to each other over the network interconnect—like NVLink—and average their gradients together. So every GPU ends up with the exact same averaged gradient tensor.

Beau

And since they all started with the same weights and they're all applying the same averaged gradient update... they stay perfectly in sync. Okay, that makes sense. But the limitation is obvious: my model still has to fit on one GPU.

Jo

Precisely. You've hit the wall. DDP helps you train faster by processing more data in parallel, but it doesn't help you train a bigger model. For that, we need to shard the model itself. And that brings us to FSDP—Fully Sharded Data Parallel.

Beau

Okay, so 'sharding'. Instead of cloning the whole model, we're breaking it up? Like... giving Layer 1 to GPU 0, Layer 2 to GPU 1...?

Jo

That's one way, called pipeline parallelism, but FSDP is more clever. Imagine your model parameters—the weights—are a giant single vector. FSDP splits that vector across all your GPUs. So GPU 0 might hold the first 1/8th of the model's parameters, GPU 1 holds the second 1/8th, and so on.

Beau

But a GPU needs all the parameters for a given layer to do the forward pass, right? You can't do a matrix multiplication with only 1/8th of the matrix.

Jo

You're exactly right. So there's a communication step. When GPU 0 is ready to compute Layer 1, it sends out a request: 'Hey everyone, I need the full weights for Layer 1.' All the other GPUs send over their shards of the Layer 1 weights. GPU 0 assembles the full layer, does the forward pass, and then... this is the critical part... it immediately discards the weights it just gathered.

Beau

So it's this just-in-time, gather-compute-discard process, layer by layer. The peak memory is just the size of the largest single layer, not the whole model.

Jo

Exactly. And it does this not just for the model weights, but for the gradients and the optimizer states too. Remember, for every parameter, AdamW stores two moving averages. So the optimizer state is often twice the size of the model weights. Sharding that is a massive memory win.

Beau

The communication overhead must be astronomical, though. It sounds like the GPUs are constantly just talking to each other.

Jo

It is, which is why optimizing this is a huge area of research. PyTorch FSDP tries to be smart by overlapping the communication for the next layer with the computation of the current layer. So while the CPU is orchestrating the gather for layer N+1, the GPU is busy computing layer N. But yes, you're now trading memory for network bandwidth.

Beau

Okay, that covers the sharding. But even within a single forward pass on one GPU, we talked about that attention mechanism being O(N^2) in both compute and memory. For long sequences, that N-by-N attention matrix is the real killer, right?

Jo

Absolutely. If you have a sequence length of 32,000 tokens, that matrix is 32k by 32k. You can't store that. This is where something like FlashAttention comes in. It's a fundamental change to the algorithm.

Beau

How so? I thought QK-transpose was non-negotiable.

Jo

The math is the same, but the implementation is completely different. Instead of computing the whole N-by-N matrix, FlashAttention uses tiling. It breaks the Q, K, and V matrices into smaller blocks that can fit into the GPU's super-fast SRAM. It loads a block of Q and a block of K, computes their little sub-matrix, does the softmax, multiplies by the corresponding block of V, and writes the result to HBM, which is the main GPU memory.

Beau

And it does this iteratively for all the blocks... so the full N-by-N matrix never actually exists in memory at any one time.

Jo

Precisely. It fuses all those operations—the matmul, the softmax, the second matmul—into one CUDA kernel, which drastically reduces the number of slow reads and writes to and from HBM. The memory usage becomes linear, O(N), instead of quadratic. It's not an approximation; it's a numerically stable, IO-aware reimplementation of the exact same attention logic.

Beau

Which means in PyTorch, you basically just import `flash_attn` and replace your `scaled_dot_product_attention` call with it, and suddenly you can handle much longer sequences.

Jo

Essentially, yes. You combine FSDP to handle a model that's too big for one GPU's memory, and you use FlashAttention to handle sequences that are too long for the standard attention implementation. They solve two different, but related, scaling bottlenecks.

Beau

So now we have all these moving parts. How do you actually debug this? If one GPU out of 128 is acting weird, how do you even start to profile that?

Jo

That's where tools like the PyTorch Profiler become non-negotiable. You can run it on your training script, and it will give you a full trace showing exactly what every GPU is doing at any given microsecond. You can see if one GPU is lagging, or if you're spending 90% of your time waiting on network communication instead of doing math.

Beau

You can literally see the 'gather' operation from FSDP in the trace and see if it's overlapping correctly with the compute kernel.

Jo

Exactly. You look for big gaps of idle time on the GPU utilization chart. That tells you your GPU is data-starved or waiting on something else. It's no longer just about optimizing a single piece of code; it's about orchestrating a complex dance between compute, memory, and network across a whole cluster.

Beau

It really shifts the perspective from just writing model code to designing a distributed system where the 'service' is a matrix multiplication.