Teaching a network to place a GPT on four GPUs

We rebuilt the 2017 device-placement paper from scratch on a 49M-parameter GPT and four T4 GPUs. A reinforcement-learning placer, trained only on measured step times, beat every hand-made placement in two of three settings. We also found out why it wins, and where it doesn't.

Dr. Rajat Dandekar

Vizuara AI Labs · paper re-implementation

The learning loop: a placer network assigns each operation of the model to one of four GPUs, one real training step is timed, and faster placements become more likely.
The whole method in one loop. A small network proposes which GPU each part of the model should run on. We run one real training step with that placement and time it. Placements that were faster than their batch-mates become more likely next time. Nobody tells the placer anything about GPUs, copies or memory.
−7%
step time vs the best hand-made placement, plain training (A)
−11%
vs the only human split that fits when memory is tight (C)
4 / 4
independent runs found the same placement idea
+5%
slower than the human pipeline split with micro-batches (B)
Start here

Where should each layer live?

When one GPU is too small or too slow for a model, you spread the model over several. Then you have to decide which layer goes on which card. Put two layers that talk a lot on different cards and every step pays for copying data between them. Pile everything on one card and the others sit idle. People usually make this call by hand, with rules of thumb.

In 2017, Azalia Mirhoseini and colleagues at Google Brain asked whether a network could learn this decision instead. Their placer reads the model's computation graph and writes down a device for every operation. The only feedback it gets is how long one training step took. They showed it could find placements faster than expert-designed ones on Inception-V3, a recurrent language model and a translation model. Mirhoseini later co-led AlphaChip and co-founded Ricursive Intelligence, a lab built on the same instinct: let AI design the hardware side of AI.

This post rebuilds that experiment small enough to run in an afternoon: a 49M-parameter GPT, four NVIDIA T4s on Modal, and roughly $20 of GPU time. Every number below comes from real runs and was re-measured in a separate head-to-head test.

The setup

The game: 40 decisions, 4 GPUs

PyTorch has no fixed graph to place, so we wrote the GPT as an explicit chain of 40 placeable nodes: the embedding, six transformer blocks of six nodes each (layer norm, attention, residual add, layer norm, MLP, residual add), then the final norm, the output head and the loss. The model has 49M parameters. 39M of them sit in just two nodes, the embedding and the output head, because the vocabulary has 50,257 tokens.

A placement is one GPU index per node, so there are 440 of them. The environment runs a real training step under the placement (forward, backward and an AdamW update on a batch of 16 × 256 tokens), copying activations whenever an edge crosses GPUs. Its reward is the median wall-clock time of 5 steps after 2 warm-up steps. A placement that runs out of memory is charged 10 seconds.

Two placements of the same chain: split in two with one copy, and scattered across four GPUs with a copy at every hop.
What a placement costs. Split the chain in two and each step pays for one copy between GPUs. Scatter it and every hop is a copy. On our hardware a scattered placement takes about 635 ms per step, almost three times the 224 ms of the fastest single GPU.
The method

The placer and REINFORCE

The placer follows the paper. Each node is described by three things: its operation type, the shape of its output and which nodes it connects to. An encoder LSTM reads the nodes in order. A decoder LSTM then walks the same sequence and picks a GPU for each node. At every step it looks back over all the encoder states (attention) and sees which GPU it chose for the previous node.

Sequence-to-sequence placer: operation cards feed an encoder; a decoder with attention emits a probability over four GPUs for each operation, feeding its previous choice forward.
The placer. One decision per node, made left to right. The probability of a whole placement is the product of the 40 per-node probabilities, which is what makes the REINFORCE update below possible.

Training is REINFORCE, as in the paper. Each update samples 8 placements and times all of them. Each placement's cost is the square root of its step time, which squashes the huge gap between terrible and decent placements. The update raises the probability of placements that cost less than a baseline and lowers the rest:

loss = mean over the 8 samples of ( √runtime − baseline ) × log p(placement)

The paper's baseline is a moving average of past costs. That is the version we started with.

First attempt

It learns in ten updates, then stops

The first run was encouraging for about thirty seconds of reading. The mean step time of the sampled placements fell from 530 ms to 230 ms in eight updates. By update sixteen, all eight samples in every batch were the same placement: everything on one GPU. When every sample is identical, every sample gets the same score, the update is zero, and learning is over for good.

Two panels: policy entropy over updates and the number of distinct placements per batch, for five variants.
Every variant collapses; the question is onto what. Left: how random the placer still is, as a share of the maximum. Right: how many of the 8 samples in a batch differ. An entropy bonus of 0.01 (amber) keeps the placer near random and it never commits. A bonus of 0.001 delays the collapse a little. Comparing each sample with the mean of the other seven (leave-one-out, dashed) collapses fastest of all. Co-location groups (magenta, next section) keep exploring for about 100 updates.

We tried three standard fixes before understanding the real problem:

The real cause is in the placements themselves. "Put this node on the same GPU as the one before it" is right almost everywhere, so the placer learns it at all 40 decisions at once and becomes nearly deterministic. From there, a better placement needs several decisions to change together. The clearest case is the output head and the loss: moving only one of them forces the 823 MB logits tensor (16 × 256 × 50,257 floats) across the PCIe bus, which is much slower. One-node-at-a-time exploration can't climb over that wall.

The fix

The paper's own fix: co-location groups

The paper does not place every operation separately. It first merges operations that obviously belong together and places the groups. We used the simplest rule of that kind: an operation whose output has exactly one consumer joins its consumer's group. On our GPT this turns 40 nodes into 13 groups: the embedding, two groups per transformer block, and a final group that holds the last MLP, the final norm, the output head and the loss.

The wall disappears, because head and loss now move together. The search space shrinks from 440 to 413, and even the very first random batch averages 247 ms instead of 530 ms. The placer still settles eventually, but it explores for about a hundred updates first, and that is long enough.

Experiment A learning curves: whole run on the left, zoom on the paper setup with baselines on the right.
Experiment A, the paper setup (13 groups, moving-average baseline). Left: both versions drop quickly from random. Right: the mean of the sampled placements (solid) settles around 234 ms, above the best single GPU. The fastest placement measured so far (dashed) drops below every human baseline within 15 updates. As in the paper, the output of the method is the best placement the search finds, so the next section re-measures it fairly.
Results

Results: two wins and a loss

A single timing of a multi-GPU placement can be off by 5 to 15 ms, and the fastest timing out of two thousand is always flattered by luck. So every result below comes from a separate head-to-head test on one machine. In 4 shuffled selection rounds we pick the best RL placement (from the training run's top 5 and its final favourite), the best single GPU (out of 4) and the best two-GPU human split (on three GPU pairs). Then 8 fresh confirmation rounds time only those picks and the fixed baselines. Only confirmation numbers are reported, and every side gets exactly one pick.

We ran three settings. A is a plain training step. B splits each batch into 4 micro-batches, so GPUs can work on different micro-batches at once, like an assembly line. C caps every GPU at 24% of its memory, so the model no longer fits on one card, the situation the paper was written for.

Bar charts of step time for each placement in experiments A, B and C.
Head-to-head, confirmation rounds only (mean ± sd of 8 rounds). In A and C the RL placer is the fastest placement anyone tried. In B the human two-GPU pipeline split wins.
PlacementA · plainB · micro-batchesC · memory capped
RL placer207.4 ± 5.5205.0 ± 7.1229.2 ± 12.1
Human expert, 2 GPUs (best pair)227.5 ± 13.8195.3 ± 5.2out of memory
Human expert, 4 GPUs253.1 ± 3.9206.3 ± 3.7256.3 ± 4.0
Best single GPU223.9 ± 0.5236.2 ± 0.6out of memory
Round robin, 4 GPUs625.6598.7627.6
Random, 4 GPUs635.3598.7631.1
RL vs best alternative−7.4%+5.0%−10.6%

Step time in ms, mean ± standard deviation over 8 confirmation rounds of 10 timed steps each. The standard error of each mean is about a third of the standard deviation shown, so the A and C gaps are well outside the noise (about 8 and 6 standard errors). B is a clear loss.

Is A a lucky seed?

We trained three more A placers with different seeds and settings and gave each the same head-to-head test, on a different machine each time. All four beat every hand-made placement:

A runRL placerBest single GPUBest human splitGain
13 groups, moving average (paper setup)207.4223.9227.5−7.4%
13 groups, leave-one-out, seed 0213.7232.7223.9−4.6%
13 groups, leave-one-out, seed 1215.5233.3234.8−7.6%
40 nodes, plain REINFORCE (the run that collapsed)213.1229.8235.1−7.3%

Even the run that collapsed onto a single GPU had sampled the winning idea on its way down, which is why the paper reports the best placement found, not the policy's last guess.

What it found

Why the winning placement wins

All four A runs found the same idea independently: keep the embedding and the output head away from the transformer body. Three of them use three GPUs (embedding, body, head). The fourth puts embedding and head together on one GPU and the body on another.

Placement maps: for each experiment, the human expert placement and the RL placement, 40 cells coloured by GPU.
Each row is one placement, read left to right through the 40 nodes; colour is the GPU. A: the placer gives the embedding its own GPU, the body a second and the head a third. B: it builds a three-stage pipeline where the human's two-stage one is faster. C: it keeps most blocks on one GPU and moves just enough of them elsewhere to fit in memory, with the head alone on GPU 1.

A GPT is a strict chain, so the forward and backward passes can't overlap across GPUs. So where does a 7% gain come from? We timed the three phases of a step separately, on one machine:

PlacementForwardBackwardOptimizer stepTotal
Fastest single GPU70.1135.917.1223.1
RL placer (A)67.9136.98.0212.9

Milliseconds, median of 12 steps, with every GPU synchronised between phases (so totals differ slightly from the head-to-head numbers).

The trick

The AdamW update is independent for every parameter, so it is the one part of a training step that can run on several GPUs at once. Putting the two vocabulary-sized layers on their own GPUs splits the 49M-parameter update three ways and more than halves its time, while forward and backward stay on the fastest card. The only extra traffic is two 6 MB activation copies each way.

None of the hand-made placements in this study does this, but once you see it, it makes sense. That may be the real value of the method: a search that turns up an idea a person can then understand and reuse. A human engineer would probably get the same speed-up today by sharding the optimizer state directly.

B: where the human wins

With 4 micro-batches, a clean two-stage pipeline lets GPU 3 work on micro-batch two while GPU 1 finishes micro-batch one. The human split (layers 1–3 on one GPU, layers 4–6 and the head on another) does that at 195 ms. The placer converged on a three-stage pipeline at 205 ms, about as fast as the human four-stage split (206 ms) but not the two-stage one. Its fastest single training measurement was 187 ms, which shows how much luck a best-of-2,000 number contains.

Experiment B learning curves with baselines.
Experiment B. The co-location placer starts near 225 ms and wanders down to about 205 ms before settling. Its dashed "fastest so far" line dips well below the human split, but those were lucky single measurements: re-timed fairly, the human wins.

C: when the model doesn't fit

With every GPU capped at 24% of its memory, a single GPU and every two-GPU split run out of memory. The only hand-made placement that fits is the four-way layer split, at 256 ms. At first three in four sampled placements ran out of memory. The placer learned to avoid that and found a placement at 229 ms that keeps most of the model on one card and moves only what it must.

Experiment C: share of samples out of memory, and step time of placements that fit.
Experiment C. Left: the share of sampled placements that ran out of memory falls from three quarters to about one in ten. Right: among placements that fit, the placer quickly found ones well under the human four-GPU split. The run stopped at update 124 (see below).
Read this before quoting a number

What to distrust

Reproduce it

Reproduce it

The code, every run log and the figure script are on GitHub under the MIT license. Everything runs on Modal (4 × T4 per job). A training run of 250 updates takes about an hour. A head-to-head evaluation takes about 12 minutes. All the runs in this post cost roughly $20.

git clone https://github.com/RajatDandekar/device-placement-rl && cd device-placement-rl
pip install modal && modal setup
# tests: placer, REINFORCE and infrastructure (CPU container)
modal run modal_app.py --task test
# baselines on 4x T4
modal run modal_app.py --task smoke --config tiny
# experiment A, paper setup: 13 co-location groups, moving-average baseline
modal run --detach modal_app.py --task train --n-iters 250 --lr 1e-3 --colocate
# B and C used --baseline leave_one_out, plus --n-micro 4 (B) or --mem-cap 0.24 (C)
# fair head-to-head (select 4 rounds, confirm 8), then every figure
modal run modal_app.py --task evaluate --run tiny_b16_m1_coloc_lr0.001_s0
modal run modal_app.py --task figures

Nothing on this page is hand-drawn from numbers. The three diagrams are concept sketches with no data in them. Every chart and table is generated by figures.py from the run logs, and one command regenerates them all.

Then break it

Cite the original

@inproceedings{mirhoseini2017device, title = {Device Placement Optimization with Reinforcement Learning}, author = {Mirhoseini, Azalia and Pham, Hieu and Le, Quoc V. and Steiner, Benoit and Larsen, Rasmus and Zhou, Yuefeng and Kumar, Naveen and Norouzi, Mohammad and Bengio, Samy and Dean, Jeff}, booktitle = {International Conference on Machine Learning (ICML)}, year = {2017} }