Roadmap / Distributed Training

✓

Tensor-Parallel MLP (Megatron)

mediumdistributedtransformer

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,

Y=GeLU(XA) BY = \mathrm{GeLU}(XA)\,B

with a single communication at the end. Here XX holds the token vectors, AA of shape (D,F)(D, F) widens them to FF numbers, GeLU (Gaussian error linear unit) is the nonlinearity, applied entry by entry, and BB of shape (F,D)(F, D) narrows them back. You will simulate the split on NN GPUs and check that it gives the same result.

The split

  • AA is split by columns into A1,…,ANA_1, \dots, A_N. GPU ii computes Hi=GeLU(XAi)H_i = \mathrm{GeLU}(XA_i) on its own: each column of XAXA needs only one column of AA, and GeLU works entry by entry, so no other GPU is needed.
  • BB is split by rows into the matching B1,…,BNB_1, \dots, B_N: the rows of BB that multiply the columns of HH this GPU computed. GPU ii computes the partial output Yi=HiBiY_i = H_i B_i, already of the full output shape.
  • One all-reduce adds up the partial outputs: Y=∑iYiY = \sum_i Y_i.

Example. One token X=(1,2)X = (1, 2), D=2D = 2, F=4F = 4, N=2N = 2 GPUs, with AA = [[1, 0, -1, 2], [0, 1, 1, -1]] and BB = [[1, 0], [0, 1], [1, 1], [2, 0]].

  • Unsplit: XA=(1,2,1,0)XA = (1, 2, 1, 0), GeLU(XA)≈(0.841,1.955,0.841,0)\mathrm{GeLU}(XA) \approx (0.841, 1.955, 0.841, 0), and Y≈(1.682,2.796)Y \approx (1.682, 2.796).
  • GPU 0 has columns 0–1 of AA and rows 0–1 of BB: H0≈(0.841,1.955)H_0 \approx (0.841, 1.955), Y0≈(0.841,1.955)Y_0 \approx (0.841, 1.955).
  • GPU 1 has columns 2–3 of AA and rows 2–3 of BB: H1≈(0.841,0)H_1 \approx (0.841, 0), Y1≈(0.841,0.841)Y_1 \approx (0.841, 0.841).
  • All-reduce: Y0+Y1≈(1.682,2.796)Y_0 + Y_1 \approx (1.682, 2.796), the same as unsplit.

Why not split AA 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: GeLU(1+1)≈1.955\mathrm{GeLU}(1 + 1) \approx 1.955, but GeLU(1)+GeLU(1)≈1.682\mathrm{GeLU}(1) + \mathrm{GeLU}(1) \approx 1.682. That would need a second communication.

GeLU

GPT-2 (Generative Pre-trained Transformer 2) uses this approximation of GeLU:

GeLU(x)≈12x (1+tanh⁡u),u=2/π (x+0.044715 x3)\begin{aligned} \mathrm{GeLU}(x) \approx {} & \tfrac{1}{2}x\,\big(1 + \tanh u\big), \\ u = {} & \sqrt{2/\pi}\,(x + 0.044715\,x^3) \end{aligned}

where xx is one entry and uu is a helper value. For example GeLU(1)≈0.841\mathrm{GeLU}(1) \approx 0.841 and GeLU(0)=0\mathrm{GeLU}(0) = 0.

What to implement

With FF divisible by NN:

  • gelu(x): the approximation above, elementwise.
  • shard_mlp(A, B, n): return the list of nn 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 ii gets columns iF/niF/n to (i+1)F/n−1(i+1)F/n - 1 of AA and the same range of rows of BB.
  • The result must equal the unsplit GeLU(XA) B\mathrm{GeLU}(XA)\,B.
solution.py
Python idle

Press Run (⌘/Ctrl + Enter) to test your code.