English (unofficial) translations of posts at kexue.fm
Source

Analysis of the AdaFactor Optimizer (with Open Source Implementation)

Translated by DeepSeek V4 Pro. Translations can be inaccurate, please refer to the original post for important stuff.

Since the popularity of pre-trained models like GPT and BERT, a clear trend has emerged: models are getting larger and larger. This is because larger models, combined with more sufficient pre-training, can usually climb leaderboards more effectively. However, while ideals can be infinite, reality is often constrained. Sometimes models become so large that even if you possess GPUs or TPUs with massive VRAM, you still feel a sense of despair. For example, the largest version of GPT-2 has 1.5 billion parameters, and the largest version of the T5 model even reaches 11 billion parameters. For models of this scale, it is impossible to run large batch sizes even on TPU clusters.

In such cases, one usually starts by optimizing the training process. For instance, using mixed-precision training (TensorFlow also supports a new floating-point format called bfloat16) can save VRAM and accelerate training. Alternatively, one can use more memory-efficient optimizers; for example, RMSProp is more memory-efficient than Adam. This article introduces AdaFactor, a new optimizer proposed by Google, first appearing in the paper "Adafactor: Adaptive Learning Rates with Sublinear Memory Cost". AdaFactor features adaptive learning rates but is even more memory-efficient than RMSProp, and it specifically addresses some of Adam’s deficiencies.

Adam

First, let’s review the update process of the commonly used Adam optimizer. Let t be the iteration step, \alpha_t be the current learning rate, L(\theta) be the loss function, \theta be the parameters to be optimized, and \epsilon be a small positive number to prevent division by zero. The Adam update process is: \left\{\begin{aligned} &g_t = \nabla_{\theta} L(\theta_{t-1})\\ &m_t = \beta_1 m_{t-1} + \left(1 - \beta_1\right) g_t\\ &v_t = \beta_2 v_{t-1} + \left(1 - \beta_2\right) g_t^2\\ &\hat{m}_t = m_t / \left(1 - \beta_1^t\right)\\ &\hat{v}_t = v_t / \left(1 - \beta_2^t\right)\\ &\theta_t = \theta_{t-1} - \alpha_t \hat{m}_t / \left(\sqrt{\hat{v}_t} + \epsilon\right) \end{aligned}\right.

To save VRAM, we must first know where the VRAM is being spent. First, the bulk of the computation and VRAM is certainly \nabla_{\theta} L(\theta_{t-1}). That is, calculating the gradient is very resource-intensive; this is why "although ALBERT has significantly fewer parameters than BERT, its training speed is not noticeably faster." Besides this, the main VRAM consumption comes from m and v. We need to maintain two sets of buffer variables to calculate the moving averages of the first two moments of the gradient (i.e., m and v). Each of these sets of variables is as large as the training parameters themselves. Therefore, for models with many parameters, the VRAM consumed by these two sets of buffer variables is substantial.

AdaFactor

In this section, we will introduce the AdaFactor optimizer in relative detail, involving several formulas and derivations. Readers who only seek a general understanding may skip some of the mathematical content.

Discarding Momentum

We know that in Computer Vision (CV), "SGD + Momentum" is often required to achieve optimal results, and adaptive learning rate optimizers usually do not yield the best performance. However, for NLP models, the situation is somewhat reversed; adaptive learning rates appear more important, and it is rare to hear of cases where NLP models are tuned purely with SGD. Therefore, as the first step to save VRAM, we can discard the momentum in Adam. This removes one set of buffer parameters, naturally saving VRAM: \left\{\begin{aligned} &g_t = \nabla_{\theta} L(\theta_{t-1})\\ &v_t = \beta_2 v_{t-1} + \left(1 - \beta_2\right) g_t^2\\ &\hat{v}_t = v_t / \left(1 - \beta_2^t\right)\\ &\theta_t = \theta_{t-1} - \alpha_t g_t / \sqrt{\hat{v}_t + \epsilon} \end{aligned}\right. This is essentially a variant of RMSProp, with the additional step \hat{v}_t = v_t / \left(1 - \beta_2^t\right).

Low-Rank Decomposition

After removing m, the buffer variables are reduced by half, but AdaFactor is not yet satisfied. It hopes to retain the adaptive learning rate functionality while further compressing the parameter count of the buffer variable v. This time, it utilizes low-rank matrix decomposition.

Generalized KL Divergence

In SGD, all parameters share a single scalar learning rate. In Adam, every parameter has its own learning rate \alpha_t / \left(\sqrt{\hat{v}_t} + \epsilon\right). We know that through fine-tuning the learning rate, SGD can actually achieve good results. This suggests that the fact that "every parameter has its own learning rate" is not particularly critical, or in other words, "fine-tuning every single parameter’s individual learning rate" is not vital.

This inspires us to replace \hat{v}_t with an approximation that uses fewer parameters. For an approximation with fewer parameters, low-rank decomposition naturally comes to mind. For an m \times n matrix C, we hope to find an m \times k matrix A and a k \times n matrix B such that: AB \approx C When k is sufficiently small, the total number of parameters in A and B is less than that of C. To save as much as possible, AdaFactor directly sets k=1, seeking \{a_i\}_{i=1}^m and \{b_j\}_{j=1}^n such that: a_i b_j \approx c_{i,j} Since we are approximating, we need a metric. A common metric is the Euclidean distance: \sum_{i,j} (a_i b_j - c_{i,j})^2 However, under this distance, a_i, b_j do not have an analytical solution. Furthermore, during the optimization process, c_{i,j} (i.e., \hat{v}_t) is non-negative, but a_i b_j optimized via the above objective cannot be guaranteed to be non-negative, which might disrupt the optimization process.

The authors of the original paper cleverly switched to a different metric that allows for an analytical solution for a_i, b_j. Specifically, they used the "Generalized KL Divergence," also known as "I-Divergence" (I-Divergence), which takes the form: l = \sum_{i,j} c_{i,j}\log \frac{c_{i,j}}{a_i b_j} - c_{i,j} + a_i b_j \label{eq:i-div} This metric originates from the inequality x \log x \geq x - 1 (\forall x > 0), where equality holds if and only if x=1. By substituting x = p/q (p, q > 0) and multiplying both sides by q, we get: p \log \frac{p}{q} - p + q \geq 0 Equality holds if and only if p=q. If p, q have multiple components, summing the results for all components yields the metric in Eq. [eq:i-div].

Clearly, Generalized KL Divergence is a natural extension of the KL divergence for probabilities, but it does not require c_{i,j} and a_i b_j to be normalized; it only requires them to be non-negative, which perfectly fits the AdaFactor scenario. Ingeniously, under this objective, there is an analytical solution: a_i = \sum_{j}c_{i,j},\quad b_j = \frac{\sum_{i}c_{i,j}}{\sum_{i,j}c_{i,j}}\label{eq:aibj} This analytical solution is quite intuitive: sum the rows and columns respectively, multiply them, and then divide by the total sum.

Derivation Process

Taking the partial derivatives of Eq. [eq:i-div] and setting them to zero: \left\{\begin{aligned} &\frac{\partial l}{\partial a_i}=\sum_j -\frac{c_{i,j}}{a_i} + b_j = 0\\ &\frac{\partial l}{\partial b_j}=\sum_i -\frac{c_{i,j}}{b_j} + a_i = 0 \end{aligned}\right. Rearranging gives: \left\{\begin{aligned} &a_i \sum_{j} b_j = \sum_j c_{i,j}\\ &b_j \sum_{i} a_i = \sum_i c_{i,j} \end{aligned}\right. Note that if (a_i, b_j) is an optimal solution, then (\lambda a_i, b_j/\lambda) is also one. Simply put, if all a_i are multiplied by a constant and all b_j are divided by the same constant, a_i b_j remains unchanged. Thus, we can arbitrarily specify \sum_i a_i or \sum_j b_j, as they are merely scaling scalars. Without loss of generality, if we specify \sum_j b_j = 1, we obtain Eq. [eq:aibj].

Intuitive Understanding

We can also understand the result in Eq. [eq:aibj] from another perspective. Since c_{i,j} is non-negative, we can normalize it to have the properties of a probability distribution, i.e., \hat{c}_{i,j} = \frac{c_{i,j}}{\sum_{i,j}c_{i,j}}. We then attempt the decomposition \hat{c}_{i,j} \approx \hat{a}_i \hat{b}_j. Since \hat{c}_{i,j} now acts as a bivariate joint probability distribution, \hat{a}_i and \hat{b}_j correspond to their marginal distributions: \hat{a}_i = \sum_j \hat{c}_{i,j} = \frac{\sum_{j}c_{i,j}}{\sum_{i,j} c_{i,j}},\quad \hat{b}_j = \sum_i \hat{c}_{i,j} = \frac{\sum_{i}c_{i,j}}{\sum_{i,j}c_{i,j}} To get from \hat{c}_{i,j} back to c_{i,j}, we need to multiply by \sum_{i,j}c_{i,j}. We can multiply this into either \hat{a}_i or \hat{b}_j. Without loss of generality, assuming we multiply it into \hat{a}_i, we obtain Eq. [eq:aibj].

AdaFactor Prototype

With the result in Eq. [eq:aibj], we can construct a more memory-efficient optimizer, which is the prototype of AdaFactor. Simply put, when the parameter \theta is a standard 1D vector, the optimization process remains unchanged. However, when \theta is an m \times n matrix, the calculated gradient g_t is also a matrix, and thus g_t^2 is a matrix. We then perform low-rank decomposition on g_t^2 and maintain two sets of buffer variables v^{(r)}_t \in \mathbb{R}^m and v^{(c)}_t \in \mathbb{R}^n, which are moving averages of the low-rank decomposition results. Finally, v^{(r)}_t and v^{(c)}_t are used together to adjust the learning rate: \left\{\begin{aligned} &g_{i,j;t} = \nabla_{\theta} L(\theta_{i,j;t-1})\\ &v^{(r)}_{i;t} = \beta_2 v^{(r)}_{t-1;i} + \left(1 - \beta_2\right) \sum_{j}\left(g_{i,j;t}^2+\epsilon\right)\\ &v^{(c)}_{j;t} = \beta_2 v^{(c)}_{t-1;j} + \left(1 - \beta_2\right) \sum_{i}\left(g_{i,j;t}^2+\epsilon\right)\\ &v_{i,j;t} = v^{(r)}_{i;t} v^{(c)}_{j;t} / \sum_{j}v^{(c)}_{j;t}\\ &\hat{v}_t = v_t / \left(1 - \beta_2^t\right)\\ &\theta_t = \theta_{t-1} - \alpha_t g_t / \sqrt{\hat{v}_t} \end{aligned}\right. (Adding \epsilon to g_t^2 rather than \hat{v}_t is the form chosen by AdaFactor, not a mistake by the author.)

Sliding Weights

In Adam and the AdaFactor prototype above, the sliding weight \beta_2 is a constant. AdaFactor points out that this is unscientific and proposes a new strategy.

Equivalent Form

To recognize this, let’s rewrite the update process for \hat{v}_t in Adam: \begin{aligned} \hat{v}_t &= v_t / \left(1 - \beta_2^t\right)\\ &= \frac{\beta_2 v_{t-1} + (1-\beta_2) g_t^2}{1 - \beta_2^t}\\ &= \frac{\beta_2 \hat{v}_{t-1}\left(1 - \beta_2^{t-1}\right) + (1-\beta_2) g_t^2}{1 - \beta_2^t}\\ &= \beta_2\frac{1 - \beta_2^{t-1}}{1 - \beta_2^t}\hat{v}_{t-1} + \left(1 - \beta_2\frac{1 - \beta_2^{t-1}}{1 - \beta_2^t}\right)g_t^2 \end{aligned} So if we set \hat{\beta}_{2,t} = \beta_2\frac{1 - \beta_2^{t-1}}{1 - \beta_2^t}, the update formula becomes: \hat{v}_t = \hat{\beta}_{2,t}\hat{v}_{t-1} + \left(1 - \hat{\beta}_{2,t}\right)g_t^2 The question is whether this \hat{\beta}_{2,t} is reasonable. The answer is likely not. When t=1, \hat{\beta}_{2,t}=0, and \hat{v}_t is simply g_t^2, meaning the real-time gradient is used to correct the learning rate, which is the maximum correction strength. As t \to \infty, \hat{\beta}_{2,t} \to \beta_2. Here, v_t is a weighted average of the accumulated squared gradients and the current squared gradient. Since \beta_2 < 1, the weight of the current gradient 1 - \beta_2 is non-zero. This may lead to training instability because, in the later stages of training, gradients become smaller and training itself tends to stabilize; the significance of correcting the learning rate diminishes. Therefore, the correction strength should decrease, and as t \to \infty, the learning rate should ideally become constant (equivalent to decaying into SGD). This requires \hat{\beta}_{2,t} \to 1 as t \to \infty.

New Decay Strategy

To achieve this, AdaFactor adopts the following decay strategy: \hat{\beta}_{2,t} = 1 - \frac{1}{t^c} \label{eq:beta2} It satisfies \hat{\beta}_{2,1}=0 and \lim_{t\to\infty} \hat{\beta}_{2,t}=1. However, not just any c is suitable; it must be 0 < c < 1. While c > 0 is easy to understand, why c < 1? The original paper contains an analysis of this, which readers can explore, but I find the derivation in the paper somewhat obscure, so I will provide my own interpretation.

First, for \hat{v}_t, a simple idea is to use the average of all squared gradients: \hat{v}_t = \frac{1}{t}\sum_{i=1}^t g_i^2 = \frac{t-1}{t}\hat{v}_{t-1} + \frac{1}{t}g_t^2 This is equivalent to setting \hat{\beta}_{2,t} = 1 - \frac{1}{t}. The downside of this scheme is that every step’s gradient is weighted equally, which is counter-intuitive because older gradients should generally be less important. Thus, the weight of the historical part should be appropriately reduced. When c < 1, 1 - \frac{1}{t^c} < 1 - \frac{1}{t}. Therefore, a concise solution is to take c < 1 in Eq. [eq:beta2]. AdaFactor’s default c is 0.8.

Layer Adaptation

Finally, we can further correct the update amount based on the norm of the parameters. This idea comes from the LAMB optimizer. Simply put, it normalizes the final update amount and then multiplies it by the parameter norm. In other words, no matter how you calculate the update, I only take your direction, while the magnitude is determined by the parameter’s own norm and a pre-set learning rate. This ensures that the relative change of all parameters in all layers remains consistent.

Full AdaFactor Version

At this point, we can finally write the update process for the full version of AdaFactor: \left\{\begin{aligned} &g_{i,j;t} = \nabla_{\theta} L(\theta_{i,j;t-1})\\ &\hat{\beta}_{2,t} = 1 - t^{-c}\\ &v^{(r)}_{i;t} = \hat{\beta}_{2,t} v^{(r)}_{t-1;i} + \left(1 - \hat{\beta}_{2,t}\right) \sum_{j}\left(g_{i,j;t}^2+\epsilon_1\right)\\ &v^{(c)}_{j;t} = \hat{\beta}_{2,t} v^{(c)}_{t-1;j} + \left(1 - \hat{\beta}_{2,t}\right) \sum_{i}\left(g_{i,j;t}^2+\epsilon_1\right)\\ &\hat{v}_{i,j;t} = v^{(r)}_{i;t} v^{(c)}_{j;t} / \sum_{j}v^{(c)}_{j;t}\\ &u_t = g_t / \sqrt{\hat{v}_t}\\ &\hat{u}_t = u_t / \max\left(1, RMS(u_t) / d\right) \times \max\left(\epsilon_2, RMS(\theta_{t-1})\right)\\ &\theta_t = \theta_{t-1} - \alpha_t \hat{u}_t \end{aligned}\right. Where RMS(x) = \sqrt{\frac{1}{n}\sum_{i=1}^n x_i^2} is a variant of the norm, and the step \max\left(1, RMS(u_t) / d\right) acts as a truncation, performing normalization only when RMS(u_t) > d. The default parameters in the original paper are:

\epsilon_1 10^{-30}
\epsilon_2 10^{-3}
d 1
\hat{\beta}_{2,t} 1 - t^{-0.8}

If the parameter is a 1D vector rather than a matrix, \hat{v}_t uses the standard update formula \hat{v}_t = \hat{\beta}_{2,t} v_{t-1} + \left(1 - \hat{\beta}_{2,t}\right) \left(g_t^2+\epsilon_1\right). Additionally, the paper suggests that if no learning rate is provided, a default of \alpha_t = \min(10^{-2}, 1/\sqrt{t}) can be used, though I noticed in the source code that this default is rarely used; usually, a learning rate must be passed manually.

Open Source Implementation

To facilitate usage, I have open-sourced my own implementation of AdaFactor:

GitHub Address: https://github.com/bojone/adafactor

The repository includes a pure Keras version and a tf.keras version. The usage is the same as standard Keras optimizers, and the tf.keras version can be used as a regular TensorFlow optimizer. The implementation refers to the mesh_tensorflow source code, for which I am grateful. The optimizer is also built into bert4keras for easy calling.

It is worth noting that when using AdaFactor, it is best to use a larger batch_size. This is because the low-rank decomposition itself introduces error, and if the batch_size is too small, the gradient estimation itself will also have large errors; the combination of both might lead to non-convergence. For pre-trained models, the batch_size is usually very large, which is why many pre-trained models have started using the AdaFactor optimizer. For ordinary downstream tasks, AdaFactor can be tried, but it may require more "alchemy" (tuning) to outperform the standard Adam. Also, a reminder: when using AdaFactor, the learning rate should be set larger, around the 10^{-3} level, even during the fine-tuning phase.

Summary

This article introduced the AdaFactor optimizer proposed by Google, which aims to reduce VRAM usage and specifically analyzes and solves some of Adam’s deficiencies. I believe that AdaFactor’s analysis of Adam is quite classic and worth careful study. For readers interested in optimization problems, it is a rare and valuable case study.

Of course, there is no absolutely effective method; there is only:

Even if a method is good, to make it actually effective, one must still practice the art of "alchemy" diligently.

When reposting, please include the original address: https://kexue.fm/archives/7302

For more details on reposting, please refer to: "Scientific Space FAQ"