| name | dash-faster-shampoo-optimizer |
| title | DASH: Faster Shampoo via Batched Block Preconditioning |
| version | 0.0.2 |
| engine | skillxiv-v0.0.2-claude-opus-4.6 |
| license | MIT |
| url | https://arxiv.org/abs/2602.02016 |
| keywords | ["Optimizer","Preconditioning","Matrix Operations","GPU Acceleration","Training Efficiency"] |
| description | Accelerate the Shampoo optimizer 4.8x using batched block-wise preconditioning and numerical approximations, enabling more frequent preconditioner updates without computational bottleneck. |
DASH: Faster Shampoo via Batched Block Preconditioning
Problem Context
The Shampoo optimizer achieves superior training performance (won MLCommons AlgoPerf competition) but suffers from significant computational overhead. Computing inverse matrix roots—its primary bottleneck—scales as O(n³), forcing infrequent preconditioner updates that degrade optimization quality. This limits practical adoption despite strong theoretical properties.
Core Concept
DASH introduces [batched block processing, numerical approximations, specialized GPU operations] to accelerate the most expensive components. By stacking preconditioner blocks into 3D tensors, DASH enables parallel GPU processing via batched operations, replacing sequential block computation with vectorized operations.
Architecture Overview
- Architectural optimization: Stack blocks into 3D tensors for batched processing
- Numerical improvements: Newton-Denman-Beavers (NDB) iteration and Chebyshev polynomial approximations as alternatives to eigenvalue decomposition
- Multi-Power-Iteration: Optimal matrix scaling with faster convergence
- Frequency boost: Enable more frequent updates without wall-clock slowdown
- Drop-in replacement: Compatible with existing Shampoo implementations
Implementation
Step 1: Organize weight matrices into blocks
Partition parameter matrices into blocks and stack them for batch processing.
class BlockOrganizer:
def __init__(self, block_size=256):
self.block_size = block_size
def partition_matrix(self, weight_matrix):
"""
Partition weight matrix into blocks of size block_size x block_size.
Returns list of blocks and metadata for reconstruction.
"""
h, w = weight_matrix.shape
blocks = []
block_info = []
for i in range(0, h, self.block_size):
for j in range(0, w, self.block_size):
block_h = min(self.block_size, h - i)
block_w = min(self.block_size, w - j)
block = weight_matrix[i:i+block_h, j:j+block_w]
blocks.append(block)
block_info.append({
'row_start': i, 'row_end': i + block_h,
'col_start': j, 'col_end': j + block_w,
'shape': block.shape
})
return blocks, block_info
def reconstruct_matrix(self, blocks, block_info, original_shape):
"""
Reconstruct original matrix from blocks.
"""
h, w = original_shape
reconstructed = torch.zeros(h, w, device=blocks[0].device)
for block, info in (blocks, block_info):
rs, re = info[], info[]
cs, info[], info[]
reconstructed[rs:re, cs:ce] = block
reconstructed
():
batches = []
i (, (blocks), batch_size):
batch = torch.stack(blocks[i:i+batch_size])
batches.append(batch)
batches
Step 2: Implement batched matrix root computation
Use batched GPU operations to compute inverse square roots in parallel.
def batched_matrix_inv_sqrt(matrices_batch, method='ndb', num_iterations=10):
"""
Compute (M^T M)^{-1/2} for a batch of matrices.
Args:
matrices_batch: Tensor of shape (batch_size, n, n)
method: 'ndb' (Newton-Denman-Beavers), 'eigen', or 'cheby'
num_iterations: Iterations for iterative methods
"""
batch_size = matrices_batch.shape[0]
n = matrices_batch.shape[1]
if method == 'ndb':
Y = matrices_batch.clone()
Z = torch.eye(n, device=matrices_batch.device).unsqueeze(0).expand(
batch_size, -1, -1
)
for _ in range(num_iterations):
Y_inv = torch.linalg.inv(Y)
Z_inv = torch.linalg.inv(Z)
Y_next = 0.5 * (Y + Z_inv)
Z_next = 0.5 * (Z + Y_inv)
Y = Y_next
Z = Z_next
inv_sqrt = Y
elif method == 'cheby':
evals = torch.linalg.eigvalsh(matrices_batch)
lambda_max = evals[..., -1]
lambda_min = evals[..., 0]
center = (lambda_max + lambda_min) / 2.0
half_width = (lambda_max - lambda_min) / 2.0
inv_sqrt = torch.zeros_like(matrices_batch)
i (num_iterations):
T_i = compute_chebyshev_polynomial(
i, (matrices_batch - center) / half_width
)
inv_sqrt += T_i
:
evals, evecs = torch.linalg.eigh(matrices_batch)
inv_sqrt = evecs @ torch.diag_embed( / torch.sqrt(evals)) @ evecs.transpose(-, -)
inv_sqrt
Step 3: Implement multi-power iteration for scaling
Optimize matrix scaling to balance numerical stability and convergence.
def multi_power_iteration_scaling(matrix, num_iterations=5):
"""
Compute optimal scaling for matrix using power iteration.
This stabilizes subsequent root computations.
"""
v = torch.randn(matrix.shape[0], 1, device=matrix.device)
v = v / torch.norm(v)
for _ in range(num_iterations):
v = matrix @ v
v = v / torch.norm(v)
lambda_max = (v.T @ matrix @ v) / (v.T @ v)
scaling = 1.0 / (lambda_max + 1e-8)
return scaling, lambda_max.item()
Step 4: Integrate into optimizer step
Create a drop-in replacement for Shampoo that uses DASH acceleration.
class DASHShampoo(torch.optim.Optimizer):
def __init__(
self,
params,
lr=1e-3,
eps=1e-10,
block_size=256,
update_freq=1,
matrix_root_method='ndb'
):
defaults = dict(
lr=lr, eps=eps, block_size=block_size,
update_freq=update_freq, matrix_root_method=matrix_root_method
)
super().__init__(params, defaults)
self.block_organizer = BlockOrganizer(block_size=block_size)
self.step_count = 0
def step(self, closure=None):
"""
Single optimization step using DASH-accelerated Shampoo.
"""
loss = None
if closure is not None:
loss = closure()
self.step_count += 1
for group in self.param_groups:
for p in group['params']:
if p.grad is None:
continue
grad = p.grad.data
state = self.state[p]
if len(state) == 0:
state[] =
state[] = torch.eye(
grad.shape[], device=grad.device
) (grad.shape) == grad.numel()
state[] +=
(grad.shape) == :
grad_norm = grad / (torch.norm(grad) + group[])
state[] += grad_norm @ grad_norm.T
state[] % group[] == :
blocks, block_info = .block_organizer.partition_matrix(
state[]
)
batch_blocks = .block_organizer.batch_blocks(
blocks, batch_size=
)
inv_sqrt_blocks = []
batch batch_blocks:
inv_sqrt_batch = batched_matrix_inv_sqrt(
batch, method=group[]
)
inv_sqrt_blocks.extend(inv_sqrt_batch)
H_inv_sqrt = .block_organizer.reconstruct_matrix(
inv_sqrt_blocks, block_info, state[].shape
)
p.data -= group[] * (grad @ H_inv_sqrt)
:
state[] += grad **
h_inv_sqrt = / torch.sqrt(state[] + group[])
p.data -= group[] * grad * h_inv_sqrt
loss
Step 5: Benchmark and validate
Compare DASH against standard Shampoo to verify speedup and convergence.
def benchmark_optimizer(
model, train_loader, optimizer_class, optimizer_kwargs,
num_epochs=5, device='cuda'
):
"""
Benchmark optimizer training speed and convergence.
"""
import time
model = model.to(device)
optimizer = optimizer_class(model.parameters(), **optimizer_kwargs)
criterion = torch.nn.CrossEntropyLoss()
wall_times = []
losses = []
for epoch in range(num_epochs):
epoch_start = time.time()
epoch_loss = 0.0
num_batches = 0
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
num_batches += 1
epoch_time = time.time() - epoch_start
wall_times.append(epoch_time)
avg_loss = epoch_loss / num_batches
losses.append(avg_loss)
print(f"Epoch {epoch + 1}: Loss={avg_loss:.4f}, Time={epoch_time:.2f}s")
return {
'wall_times': wall_times,
'losses': losses,
'total_time': sum(wall_times)
}
Practical Guidance
When to use: Large-scale model training where preconditioner computation is a bottleneck (1B+ parameters). Most beneficial for dense, fully-connected layers.
Hyperparameters:
- Block size: 256 (typical); balance between parallelism and computation per block
- Update frequency: 1 (update preconditioner every step); increase to 2-5 for larger savings
- Matrix root method: 'ndb' (recommended default for stability), 'cheby' (fast for well-conditioned)
- Learning rate: Same as standard Shampoo; no tuning needed
Key performance metrics:
- Speedup: 4-4.83x on standard Shampoo implementation
- Wall-clock improvement: ~40-50% overhead vs. SGD (vs. 90%+ for unoptimized Shampoo)
- Convergence: Often better than SGD due to improved preconditioner estimation
Common pitfalls:
- Block size too small → excessive overhead from block management
- Block size too large → reduces parallelism
- Forgetting to use batched operations → negates acceleration benefits
- Not validating numerical stability with NDB; eigenvalue decomposition safer but slower
Scaling: Benefits scale with parameter count. Minimal benefits for small models (<100M). Optimal for dense 1B-70B models.
Reference
Paper: https://arxiv.org/abs/2602.02016
Code: Available at author's repository
Related work: Shampoo optimizer, preconditioning, second-order optimization
Benchmarks: Llama-953M, perplexity metrics, training curves