blob: e8832bdaf9a00a9a4198256bd777500111a9a58b [file]
#-------------------------------------------------------------
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#-------------------------------------------------------------
/*
* Shampoo optimizer.
*
* Implementation of the Shampoo optimizer as described in:
*
* Gupta et al., "Shampoo: Preconditioned Stochastic Tensor Optimization"
* https://arxiv.org/abs/1802.09568
*
* Shampoo is a second-order optimization method that preconditions
* gradients using estimates of the row- and column-wise covariance
* of the gradients. Compared to first-order optimizers (SGD, Adam),
* Shampoo can converge faster but is significantly more memory-intensive.
*
* This implementation supports:
* - Full-matrix Shampoo (exact preconditioning)
* - Diagonal Shampoo (memory-efficient approximation)
*
* The choice between the two modes is determined by the shape of X
* and the preconditioner initialization.
*/
update = function(matrix[double] X, matrix[double] dX, double lr,
matrix[double] preconL, matrix[double] preconR, boolean useDiag)
return(matrix[double] X, matrix[double] preconL, matrix[double] preconR){
/*
* Performs one optimization step using the Shampoo update rule.
*
*
* Inputs:
* - X: Parameter matrix to be updated (n × m)
* - dX: Gradient of the loss w.r.t. X (n × m)
* - lr: Learning rate.
* - preconL: Left (row) preconditioner
* - Full: (n × n)
* - Diagonal: (n × 1)
* - preconR: Right (column) preconditioner
* - Full: (m × m)
* - Diagonal: (1 × m)
* - useDiag: Boolean flag indicating whether diagonal Shampoo is used
*
* Outputs:
* - X: Updated parameter matrix (n × m)
* - preconL: Updated left preconditioner
* - Full: (n × n)
* - Diagonal: (n × 1)
* - preconR: Updated right preconditioner
* - Full: (m × m)
* - Diagonal: (1 × m)
*/
# Full-matrix Shampoo:
# Only used if both dimensions are small enough
if(useDiag==FALSE){
preconL = preconL + dX %*% t(dX)
preconR = preconR + t(dX) %*% dX
[LEigenvalue, LEigenvector] = eigen(preconL)
preconLInvPowerRoot = LEigenvector %*% diag(LEigenvalue^(-0.25)) %*% t(LEigenvector)
[REigenvalue, REigenvector] = eigen(preconR)
preconRInvPowerRoot = REigenvector %*% diag(REigenvalue^(-0.25)) %*% t(REigenvector)
X = X - lr * preconLInvPowerRoot %*% dX %*% preconRInvPowerRoot
# Diagonal Shampoo:
# Memory-efficient approximation for large parameter matrices
} else{
n = nrow(dX)
m = ncol(dX)
preconL = preconL + rowSums(dX^2)
preconR = preconR + colSums(dX^2)
preconLScale = preconL^(-0.25)
preconRScale = preconR^(-0.25)
preconLMatrix = preconLScale %*% matrix(1, rows=1, cols=m)
preconRMatrix = matrix(1, rows=n, cols=1) %*% preconRScale
scaledGrad = dX * preconLMatrix;
scaledGrad = scaledGrad * preconRMatrix;
X = X - lr * scaledGrad;
}
}
init = function(matrix[double] X, double epsilon, int useDiagThreshold)
return (matrix[double] preconL, matrix[double] preconR, boolean useDiag) {
/*
* Initializes the Shampoo preconditioners for a given parameter matrix.
*
* Depending on the size of X, this function initializes either:
* - Full identity matrices (exact Shampoo), or
* - Diagonal vectors (approximate Shampoo)
*
* This threshold is crucial to avoid excessive memory usage,
* as full Shampoo requires O(n^2 + m^2) memory per parameter matrix.
*
* Inputs:
* - X: Parameter matrix to be optimized (n, m)
* - epsilon: Numerical stability constant
* - useDiagThreshold: Dimension threshold above which diagonal
* preconditioning is used
*
* Outputs:
* - preconL: Initial left preconditioner
* - Full: (n × n) identity scaled by epsilon
* - Diagonal: (n × 1) filled with epsilon
* - preconR: Initial right preconditioner
* - Full: (m × m) identity scaled by epsilon
* - Diagonal: (1 × m) filled with epsilon
* - useDiag: Boolean flag indicating whether diagonal Shampoo is used
*/
# Use diagonal Shampoo if parameter matrix is too large
if((nrow(X) > useDiagThreshold) | (ncol(X) > useDiagThreshold)){
preconL = matrix(epsilon, rows=nrow(X), cols=1);
preconR = matrix(epsilon, rows=1, cols=ncol(X));
useDiag = TRUE
# Use full Shampoo if parameter matrix is small enough
} else {
preconL = matrix(0, rows=nrow(X), cols=nrow(X));
index = 1;
while (index <= nrow(X)){
preconL[index, index] = epsilon * 1
index = index + 1
}
preconR = matrix(0, rows=ncol(X), cols=ncol(X));
index = 1;
while (index <= ncol(X)){
preconR[index, index] = epsilon * 1
index = index + 1
}
useDiag = FALSE
}
}
update_momentum = function(matrix[double] X, matrix[double] dX, double lr,
matrix[double] preconL, matrix[double] preconR,
matrix[double] momentum, boolean useDiag)
return(matrix[double] X, matrix[double] preconL, matrix[double] preconR,
matrix[double] momentum){
/*
* Performs one optimization step using the Shampoo update rule, while using momentum.
*
*
* Inputs:
* - X: Parameter matrix to be updated (n × m)
* - dX: Gradient of the loss w.r.t. X (n × m)
* - lr: Learning rate.
* - preconL: Left (row) preconditioner
* - Full: (n × n)
* - Diagonal: (n × 1)
* - preconR: Right (column) preconditioner
* - Full: (m × m)
* - Diagonal: (1 × m)
* - momentum: momentum (n × m)
* - useDiag: Boolean flag indicating whether diagonal Shampoo is used
*
* Outputs:
* - X: Updated parameter matrix (n × m)
* - preconL: Updated left preconditioner
* - Full: (n × n)
* - Diagonal: (n × 1)
* - preconR: Updated right preconditioner
* - Full: (m × m)
* - Diagonal: (1 × m)
* - momentum: Updated momentum (n × m)
*/
# calculating the updated momentum
momentum = 0.9 * momentum + (0.1)*dX
# Full-matrix Shampoo:
# Only used if both dimensions are small enough
if(useDiag==FALSE){
preconL = preconL + dX %*% t(dX)
preconR = preconR + t(dX) %*% dX
[LEigenvalue, LEigenvector] = eigen(preconL)
preconLInvPowerRoot = LEigenvector %*% diag(LEigenvalue^(-0.25)) %*% t(LEigenvector)
[REigenvalue, REigenvector] = eigen(preconR)
preconRInvPowerRoot = REigenvector %*% diag(REigenvalue^(-0.25)) %*% t(REigenvector)
X = X - lr * preconLInvPowerRoot %*% momentum %*% preconRInvPowerRoot
# Diagonal Shampoo:
# Memory-efficient approximation for large parameter matrices
} else{
n = nrow(dX)
m = ncol(dX)
preconL = preconL + rowSums(dX ^ 2)
preconR = preconR + colSums(dX ^ 2)
preconLScale = preconL^(-0.25)
preconRScale = preconR^(-0.25)
preconLMatrix = preconLScale %*% matrix(1, rows=1, cols=m)
preconRMatrix = matrix(1, rows=n, cols=1) %*% preconRScale
scaledGrad = momentum * preconLMatrix
scaledGrad = scaledGrad * preconRMatrix
X = X - lr * scaledGrad
}
}
init_momentum = function(matrix[double] X, double epsilon, int useDiagThreshold)
return (matrix[double] preconL, matrix[double] preconR,
matrix[double] momentum, boolean useDiag) {
/*
* Initializes the Shampoo preconditioners and momentum for a given parameter matrix.
*
* Depending on the size of X, this function initializes either:
* - Full identity matrices (exact Shampoo), or
* - Diagonal vectors (approximate Shampoo)
*
* This threshold is crucial to avoid excessive memory usage,
* as full Shampoo requires O(n² + m²) memory per parameter matrix.
*
* Inputs:
* - X: Parameter matrix to be optimized (n, m)
* - epsilon: Numerical stability constant
* - useDiagThreshold: Dimension threshold above which diagonal
* preconditioning is used
*
* Outputs:
* - preconL: Initial left preconditioner
* - Full: (n × n) identity scaled by epsilon
* - Diagonal: (n × 1) filled with epsilon
* - preconR: Initial right preconditioner
* - Full: (m × m) identity scaled by epsilon
* - Diagonal: (1 × m) filled with epsilon
* - momentum: Initial momentum (n × m), initialized to zeros
* - useDiag: Boolean flag indicating whether diagonal Shampoo is used
*/
# Use diagonal Shampoo if parameter matrix is too large
if((nrow(X) > useDiagThreshold) | (ncol(X) > useDiagThreshold)){
preconL = matrix(epsilon, rows=nrow(X), cols=1);
preconR = matrix(epsilon, rows=1, cols=ncol(X));
useDiag = TRUE
# Use full Shampoo if parameter matrix is small enough
} else {
preconL = matrix(0, rows=nrow(X), cols=nrow(X));
index = 1;
while (index <= nrow(X)){
preconL[index, index] = epsilon * 1
index = index + 1
}
preconR = matrix(0, rows=ncol(X), cols=ncol(X));
index = 1;
while (index <= ncol(X)){
preconR[index, index] = epsilon * 1
index = index + 1
}
useDiag = FALSE
}
momentum = X * 0
}
update_heuristic = function(matrix[double] X, matrix[double] dX, double lr,
matrix[double] preconL, matrix[double] preconR, matrix[double] momentum,
int stepCounter, int rootEvery, int preconEvery, matrix[double] bufferL,
matrix[double] bufferR, matrix[double] preconLInvPowerRoot,
matrix[double] preconRInvPowerRoot, boolean useDiag)
return (matrix[double] X, matrix[double] preconL, matrix[double] preconR,
matrix[double] momentum, int stepCounter, matrix[double] bufferL,
matrix[double] bufferR, matrix[double] preconLInvPowerRoot,
matrix[double] preconRInvPowerRoot){
/*
* Performs one optimization step using the Shampoo update rule, while using momentum
* and a heuristic for runtime improvements.
*
*
* Inputs:
* - X: Parameter matrix to be updated (n × m)
* - dX: Gradient of the loss w.r.t. X (n × m)
* - lr: Learning rate.
* - preconL: Left (row) preconditioner
* - Full: (n × n)
* - Diagonal: (n × 1)
* - preconR: Right (column) preconditioner
* - Full: (m × m)
* - Diagonal: (1 × m)
* - momentum: momentum (n × m)
* - stepCounter: Step counter (int), incremented each call
* - rootEvery: Frequency for recomputing inverse roots (int)
* - preconEvery: Frequency for applying buffered updates to preconditioners (int)
* - bufferL: Buffer accumulating left curvature updates
* - Full: (n × n)
* - Diagonal: (n × 1)
* - bufferR: Buffer accumulating right curvature updates
* - Full: (m × m)
* - Diagonal: (1 × m)
* - preconLInvPowerRoot: Cached preconL^{-1/4}
* - Full: (n × n)
* - Diagonal: (n × 1)
* - preconRInvPowerRoot: Cached preconR^{-1/4}
* - Full: (m × m)
* - Diagonal: (1 × m)
* - useDiag: Boolean flag indicating whether diagonal Shampoo is used
*
* Outputs:
* - X: Updated parameter matrix (n × m)
* - preconL: Updated left preconditioner
* - Full: (n × n)
* - Diagonal: (n × 1)
* - preconR: Updated right preconditioner
* - Full: (m × m)
* - Diagonal: (1 × m)
* - momentum: momentum (n × m)
* - stepCounter: Updated step counter (int)
* - bufferL: Updated bufferL (reset to 0 when applied)
* - Full: (n × n)
* - Diagonal: (n × 1)
* - bufferR: Updated bufferR (reset to 0 when applied)
* - Full: (m × m)
* - Diagonal: (1 × m)
* - preconLInvPowerRoot: Updated cached inverse root (when recomputed)
* - Full: (n × n)
* - Diagonal: (n × 1)
* - preconRInvPowerRoot: Updated cached inverse root (when recomputed)
* - Full: (m × m)
* - Diagonal: (1 × m)
*/
# calculating the updated momentum
momentum = 0.9 * momentum + (0.1)*dX
# Full-matrix Shampoo:
# Only used if both dimensions are small enough
if(useDiag==FALSE){
bufferL = bufferL + (dX %*% t(dX))
bufferR = bufferR + (t(dX) %*% dX)
if ((stepCounter > 0) & (stepCounter %% preconEvery == 0)){
preconL = preconL + bufferL
preconR = preconR + bufferR
bufferL = bufferL * 0
bufferR = bufferR * 0
}
if ((stepCounter > 0) & (stepCounter %% rootEvery == 0)){
[LEigenvalue, LEigenvector] = eigen(preconL)
preconLInvPowerRoot = LEigenvector %*% diag(LEigenvalue^(-0.25)) %*% t(LEigenvector)
[REigenvalue, REigenvector] = eigen(preconR)
preconRInvPowerRoot = REigenvector %*% diag(REigenvalue^(-0.25)) %*% t(REigenvector)
}
X = X - lr * preconLInvPowerRoot %*% momentum %*% preconRInvPowerRoot
stepCounter = stepCounter + 1
# Diagonal Shampoo:
# Memory-efficient approximation for large parameter matrices
} else{
n = nrow(dX)
m = ncol(dX)
bufferL = bufferL + rowSums(dX ^ 2)
bufferR = bufferR + colSums(dX ^ 2)
if ((stepCounter > 0) & (stepCounter %% preconEvery == 0)){
preconL = preconL + bufferL
preconR = preconR + bufferR
bufferL = bufferL * 0
bufferR = bufferR * 0
}
if ((stepCounter > 0) & (stepCounter %% rootEvery == 0)){
preconLInvPowerRoot = (preconL^(-0.25))
preconRInvPowerRoot = (preconR^(-0.25))
}
preconLMatrix = preconLInvPowerRoot %*% matrix(1, rows=1, cols=m)
preconRMatrix = matrix(1, rows=n, cols=1) %*% preconRInvPowerRoot
scaledGrad = momentum * preconLMatrix
scaledGrad = scaledGrad * preconRMatrix
X = X - lr * scaledGrad
stepCounter = stepCounter + 1
}
}
init_heuristic = function(matrix[double] X, double epsilon, int useDiagThreshold)
return (matrix[double] preconL, matrix[double] preconR, int stepCounter,
matrix[double] bufferL, matrix[double] bufferR, matrix[double] momentum,
matrix[double] preconLInvPowerRoot, matrix[double] preconRInvPowerRoot,
boolean useDiag) {
/*
* Initializes Shampoo preconditioners, buffers, cached inverse roots,
* and momentum for the heuristic variant.
*
* Depending on the size of X, this function initializes either:
* - Full identity matrices (exact Shampoo), or
* - Diagonal vectors (approximate Shampoo)
*
* This threshold is crucial to avoid excessive memory usage,
* as full Shampoo requires O(n^2 + m^2) memory per parameter matrix.
*
* Inputs:
* - X: Parameter matrix to be optimized (n, m)
* - epsilon: Numerical stability constant
* - useDiagThreshold: Dimension threshold above which diagonal
* preconditioning is used
*
* Outputs:
* - preconL: Initial left preconditioner (n × n) or (n × 1)
* - preconR: Initial right preconditioner (m × m) or (1 × m)
* - stepCounter: Initialized to 0
* - bufferL: Initialized to zeros, same shape as preconL
* - bufferR: Initialized to zeros, same shape as preconR
* - momentum: Initialized to zeros, same shape as X (n × m)
* - preconLInvPowerRoot: Cached inverse fourth root of preconL
* - Full: initialized to epsilon^{-1/4} * I (n × n)
* - Diagonal: initialized to preconL^{-1/4} (n × 1)
* - preconRInvPowerRoot: Cached inverse fourth root of preconR
* - Full: initialized to epsilon^{-1/4} * I (m × m)
* - Diagonal: initialized to preconR^{-1/4} (1 × m)
* - useDiag: Boolean flag indicating whether diagonal Shampoo is used
*/
# Use diagonal Shampoo if parameter matrix is too large
if((nrow(X) > useDiagThreshold) | (ncol(X) > useDiagThreshold)){
preconL = matrix(epsilon, rows=nrow(X), cols=1);
preconR = matrix(epsilon, rows=1, cols=ncol(X));
preconLInvPowerRoot = preconL^(-0.25)
preconRInvPowerRoot = preconR^(-0.25)
useDiag = TRUE
# Use full Shampoo if parameter matrix is small enough
} else {
preconL = matrix(0, rows=nrow(X), cols=nrow(X));
index = 1;
while (index <= nrow(X)){
preconL[index, index] = epsilon * 1
index = index + 1
}
preconR = matrix(0, rows=ncol(X), cols=ncol(X));
index = 1;
while (index <= ncol(X)){
preconR[index, index] = epsilon * 1
index = index + 1
}
preconLInvPowerRoot = preconL
i = 1
while(i <= nrow(preconLInvPowerRoot)) {
preconLInvPowerRoot[i,i] = epsilon^(-0.25)
i = i + 1
}
preconRInvPowerRoot = preconR
j = 1
while(j <= nrow(preconRInvPowerRoot)) {
preconRInvPowerRoot[j,j] = epsilon^(-0.25)
j = j + 1
}
useDiag = FALSE
}
bufferR = preconR * 0
bufferL = preconL * 0
stepCounter = 0
momentum = X * 0
}