|
| 1 | +# Copyright 2025 Google LLC |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +"""Functions for gradient accumulation (GA)""" |
| 16 | + |
| 17 | +import jax |
| 18 | +import jax.numpy as jnp |
| 19 | + |
| 20 | + |
| 21 | +def gradient_accumulation_loss_and_grad( |
| 22 | + _loss_fn, |
| 23 | + config, |
| 24 | + model, |
| 25 | + params, |
| 26 | + params_shardings, |
| 27 | + data, |
| 28 | + dropout_rng, |
| 29 | + extra_dpo_args, |
| 30 | +): |
| 31 | + """ |
| 32 | + Calculates gradients using gradient accumulation. |
| 33 | +
|
| 34 | + This function computes the gradient of `_loss_fn` over multiple microbatches |
| 35 | + and accumulates them before returning a single, averaged gradient. It uses |
| 36 | + `jax.lax.scan` for efficient accumulation on device. |
| 37 | +
|
| 38 | + It also supports a `shard_optimizer_over_data` mode (e.g., ZeRO-1) where |
| 39 | + parameters are cast to bf16 and sharded *before* the accumulation loop |
| 40 | + to perform the all-gather in lower precision. |
| 41 | +
|
| 42 | + Args: |
| 43 | + _loss_fn: The loss function to differentiate. Its signature is expected |
| 44 | + to be: `(model, config, data, dropout_rng, params, *extra_args, is_train=True)`. |
| 45 | + config: Model and training configuration object. Must contain |
| 46 | + `gradient_accumulation_steps` and `shard_optimizer_over_data`. |
| 47 | + model: The model module. |
| 48 | + params: The model parameters (PyTree). |
| 49 | + params_shardings: The sharding constraints for the parameters (PyTree). |
| 50 | + data: A PyTree of batched data. The leading dimension is assumed |
| 51 | + to be the total batch size (microbatch_size * num_accumulations). |
| 52 | + dropout_rng: JAX PRNGKey for dropout. |
| 53 | + extra_dpo_args: A tuple of extra arguments to pass to the loss function. |
| 54 | +
|
| 55 | + Returns: |
| 56 | + A tuple containing: |
| 57 | + - total_loss (Array): The mean loss, averaged over all microbatches. |
| 58 | + - final_aux (PyTree): Auxiliary outputs, summed across microbatches. |
| 59 | + - raw_grads (PyTree): The accumulated and averaged gradients. |
| 60 | + """ |
| 61 | + # When using Zero-1 optimizer sharding, cast params to lower precision and apply sharding constraints |
| 62 | + # so that all-gather is done once in the lower precision before the gradient accumulation loop |
| 63 | + if config.shard_optimizer_over_data: |
| 64 | + |
| 65 | + def convert_to_bf16(param): |
| 66 | + if param.dtype == jnp.float32: |
| 67 | + return param.astype(jnp.bfloat16) |
| 68 | + return param |
| 69 | + |
| 70 | + ga_params = jax.tree_util.tree_map(convert_to_bf16, params) |
| 71 | + ga_params = jax.tree.map(jax.lax.with_sharding_constraint, ga_params, params_shardings) |
| 72 | + else: |
| 73 | + ga_params = params |
| 74 | + |
| 75 | + grad_func = jax.value_and_grad(_loss_fn, argnums=4, has_aux=True) |
| 76 | + |
| 77 | + def accumulate_gradient(acc_grad_and_loss, data): |
| 78 | + ga_params = acc_grad_and_loss["ga_params"] |
| 79 | + |
| 80 | + (_, aux), cur_batch_gradient = grad_func(model, config, data, dropout_rng, ga_params, *extra_dpo_args, is_train=True) |
| 81 | + acc_grad_and_loss["loss"] += aux["total_loss"] |
| 82 | + acc_grad_and_loss["moe_lb_loss"] += aux["moe_lb_loss"] |
| 83 | + acc_grad_and_loss["mtp_loss"] += aux["mtp_loss"] |
| 84 | + acc_grad_and_loss["grad"] = jax.tree_util.tree_map(lambda x, y: x + y, cur_batch_gradient, acc_grad_and_loss["grad"]) |
| 85 | + acc_grad_and_loss["total_weights"] += aux["total_weights"] |
| 86 | + return acc_grad_and_loss, aux |
| 87 | + |
| 88 | + def reshape_to_microbatch_accumulations(batch_arr): |
| 89 | + """Reshape global batch to microbatches, assuming batch axis is leading.""" |
| 90 | + num_microbatches = config.gradient_accumulation_steps |
| 91 | + microbatch_shape = (batch_arr.shape[0] // num_microbatches, num_microbatches) + batch_arr.shape[1:] |
| 92 | + reshaped_batch_arr = jnp.reshape(batch_arr, microbatch_shape) |
| 93 | + return jnp.swapaxes(reshaped_batch_arr, 0, 1) |
| 94 | + |
| 95 | + data = jax.tree_util.tree_map(reshape_to_microbatch_accumulations, data) |
| 96 | + init_grad = jax.tree_util.tree_map(jnp.zeros_like, ga_params) |
| 97 | + init_grad = jax.tree.map(jax.lax.with_sharding_constraint, init_grad, params_shardings) |
| 98 | + init_grad_and_loss = { |
| 99 | + "loss": 0.0, |
| 100 | + "grad": init_grad, |
| 101 | + "total_weights": 0, |
| 102 | + "moe_lb_loss": 0.0, |
| 103 | + "mtp_loss": 0.0, |
| 104 | + "ga_params": ga_params, |
| 105 | + } |
| 106 | + |
| 107 | + grad_and_loss, aux = jax.lax.scan( |
| 108 | + accumulate_gradient, init_grad_and_loss, data, length=config.gradient_accumulation_steps |
| 109 | + ) |
| 110 | + loss = ( |
| 111 | + grad_and_loss["loss"] / grad_and_loss["total_weights"] |
| 112 | + + grad_and_loss["moe_lb_loss"] / config.gradient_accumulation_steps |
| 113 | + + grad_and_loss["mtp_loss"] / config.gradient_accumulation_steps |
| 114 | + ) |
| 115 | + raw_grads = grad_and_loss["grad"] |
| 116 | + if config.shard_optimizer_over_data: |
| 117 | + raw_grads = jax.tree.map(jax.lax.with_sharding_constraint, raw_grads, params_shardings) |
| 118 | + raw_grads = jax.tree_util.tree_map(lambda arr: arr / grad_and_loss["total_weights"], raw_grads) |
| 119 | + aux = jax.tree.map(lambda x: jnp.sum(x, axis=0), aux) # pytype: disable=module-attr |
| 120 | + |
| 121 | + return loss, aux, raw_grads |
0 commit comments