The problem
When a layer is too large for one GPU (graphics processing unit), its matrices can be cut into pieces, one per GPU. Megatron-LM does this for the MLP (multilayer perceptron) of a transformer,
with a single communication at the end. Here holds the token vectors, of shape widens them to numbers, GeLU (Gaussian error linear unit) is the nonlinearity, applied entry by entry, and of shape narrows them back. You will simulate the split on GPUs and check that it gives the same result.
The split
- is split by columns into . GPU computes on its own: each column of needs only one column of , and GeLU works entry by entry, so no other GPU is needed.
- is split by rows into the matching : the rows of that multiply the columns of this GPU computed. GPU computes the partial output , already of the full output shape.
- One all-reduce adds up the partial outputs: .
Example. One token , , , GPUs, with =
[[1, 0, -1, 2], [0, 1, 1, -1]]and =[[1, 0], [0, 1], [1, 1], [2, 0]].
- Unsplit: , , and .
- GPU 0 has columns 0–1 of and rows 0–1 of : , .
- GPU 1 has columns 2–3 of and rows 2–3 of : , .
- All-reduce: , the same as unsplit.
Why not split by rows? Then each GPU would hold part of a sum that must be completed before the GeLU, and GeLU of a sum is not the sum of the GeLUs: , but . That would need a second communication.
GeLU
GPT-2 (Generative Pre-trained Transformer 2) uses this approximation of GeLU:
where is one entry and is a helper value. For example and .
What to implement
With divisible by :
gelu(x): the approximation above, elementwise.shard_mlp(A, B, n): return the list of pairs(A_i, B_i).tp_mlp(X, shards): compute each GPU's partial output from its own(A_i, B_i)only, then sum them (the simulated all-reduce).
Requirements
- GPU gets columns to of and the same range of rows of .
- The result must equal the unsplit .