The problem
In plain data parallelism every worker keeps the full Adam state: and , the two running averages, for every parameter. With ZeRO (Zero Redundancy Optimizer), the parameters are divided into shards, one per worker, and worker keeps the Adam state of its shard only. You will simulate one training step with this split, and check that it gives exactly the result of ordinary Adam.
The step
- Reduce-scatter the gradients: worker receives the mean over all workers of the gradient, but only for the parameters of its shard.
- Each worker runs Adam on its shard only, with its own shard of and .
- All-gather the updated shards, so that every worker has the full, updated parameters again.
The shards
For parameters and workers, shard is np.array_split(np.arange(n), N)[i]: the -th of nearly equal slices of the parameter indices.
Example. 5 parameters, 2 workers:
np.array_split(np.arange(5), 2)is[array([0, 1, 2]), array([3, 4])]. Worker 0 owns parameters 0, 1, 2 and keeps and of length 3; worker 1 owns parameters 3, 4 and keeps and of length 2.
Example. One step with those shards. All 5 parameters are 1; the two workers computed the local gradients
[0.2, 0.4, 0.6, 0.8, 1.0]and[0.4, 0.0, 0.2, -0.8, 3.0].
- Reduce-scatter: worker 0 gets the mean of its slice,
[0.3, 0.2, 0.4]; worker 1 gets[0.0, 2.0].- Adam on each shard (first step,
lr = 0.1): every parameter with a non-zero gradient moves bylragainst it (section 3), so worker 0 gets[0.9, 0.9, 0.9]and worker 1 gets[1.0, 0.9](parameter 3 has a mean gradient of 0, and does not move).- All-gather: everyone has
[0.9, 0.9, 0.9, 1.0, 0.9], exactly what Adam on the full vector with the mean gradient gives.
What to implement
init_states(n_params, n_workers): a list of pairs(m_shard, v_shard)of zeros, one per worker, each the size of that worker's shard.zero_step(param, grads, states, t, lr=1e-2):paramis the full parameter vector (identical on every worker),gradsis the list of the workers' local gradients (full length),statescomes frominit_statesor a previous step, andtis the step number. Return(new_param, new_states).
adam_update is given: one Adam step on one array.
Requirements
- The result must equal ordinary Adam applied to the full vector with the mean gradient.
- Each worker's
mandvstay the size of its shard: that is the memory saving. - No Python loop over parameters (a loop over the workers is fine).