Files
ai-toolkit/toolkit/optimizers/test_optimizers.py
2026-06-08 08:59:06 -06:00

205 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Optimizer benchmark on a ~500M parameter transformer, run once per dtype
(float32, float16, bfloat16).
Compares speed (ms/step) and peak VRAM across:
- AdamW (torch, unfused — traditional Python loop)
- AdamW8bit (bitsandbytes)
- Adafactor
- Automagic v1
- Automagic v2 (fused-backward)
- Automagic v3 (fused-backward and traditional/unfused)
- Prodigy
"""
import contextlib
import gc
import io
import os
import sys
import time
import torch
import torch.nn as nn
import torch.nn.functional as F
# Allow running this file directly: `python test_optimizers.py` without setting PYTHONPATH.
# Toolkit imports happen inside main() so they pick this up.
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
# ---- model ---------------------------------------------------------------
class TransformerBlock(nn.Module):
def __init__(self, d_model: int, n_heads: int, d_ff: int):
super().__init__()
self.n_heads = n_heads
self.d_head = d_model // n_heads
self.ln1 = nn.LayerNorm(d_model)
self.q = nn.Linear(d_model, d_model, bias=False)
self.k = nn.Linear(d_model, d_model, bias=False)
self.v = nn.Linear(d_model, d_model, bias=False)
self.o = nn.Linear(d_model, d_model, bias=False)
self.ln2 = nn.LayerNorm(d_model)
self.ffn_up = nn.Linear(d_model, d_ff, bias=False)
self.ffn_down = nn.Linear(d_ff, d_model, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
B, S, D = x.shape
h = self.ln1(x)
q = self.q(h).view(B, S, self.n_heads, self.d_head).transpose(1, 2)
k = self.k(h).view(B, S, self.n_heads, self.d_head).transpose(1, 2)
v = self.v(h).view(B, S, self.n_heads, self.d_head).transpose(1, 2)
a = F.scaled_dot_product_attention(q, k, v, is_causal=True)
a = a.transpose(1, 2).contiguous().view(B, S, D)
x = x + self.o(a)
h = self.ln2(x)
x = x + self.ffn_down(F.gelu(self.ffn_up(h)))
return x
class Transformer(nn.Module):
def __init__(self, d_model=1024, n_heads=16, n_layers=40, d_ff=4096):
super().__init__()
self.blocks = nn.ModuleList([
TransformerBlock(d_model, n_heads, d_ff) for _ in range(n_layers)
])
self.norm = nn.LayerNorm(d_model)
def forward(self, x: torch.Tensor) -> torch.Tensor:
for b in self.blocks:
x = b(x)
return self.norm(x)
# ---- benchmark -----------------------------------------------------------
DEVICE = "cuda"
DTYPES = [torch.float32, torch.float16, torch.bfloat16]
D_MODEL = 1024
N_HEADS = 16
N_LAYERS = 40
D_FF = 4096
BATCH = 1
SEQ = 128
WARMUP = 3
ITERS = 10
def build_model(dtype):
torch.manual_seed(0)
return Transformer(D_MODEL, N_HEADS, N_LAYERS, D_FF).to(DEVICE, dtype=dtype)
def benchmark(results: list, label: str, opt_factory, dtype):
gc.collect()
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats()
model = build_model(dtype)
# Some optimizers print on construction; mute that so the final table is clean.
with contextlib.redirect_stdout(io.StringIO()):
opt = opt_factory(model.parameters())
x = torch.randn(BATCH, SEQ, D_MODEL, device=DEVICE, dtype=dtype)
print(f" running {label}...", flush=True)
try:
for _ in range(WARMUP):
opt.zero_grad(set_to_none=True)
model(x).sum().backward()
opt.step()
torch.cuda.synchronize()
t0 = time.perf_counter()
for _ in range(ITERS):
opt.zero_grad(set_to_none=True)
model(x).sum().backward()
opt.step()
torch.cuda.synchronize()
dt = (time.perf_counter() - t0) / ITERS * 1000
peak = torch.cuda.max_memory_allocated() / 1024**3
results.append({"label": label, "ms": dt, "peak": peak, "ok": True})
except torch.cuda.OutOfMemoryError:
results.append({"label": label, "ms": float("inf"), "peak": float("inf"), "ok": False, "note": "OOM"})
except Exception as e:
# An optimizer may not support a given dtype; record it and keep going.
print(f" {label} failed: {type(e).__name__}: {e}", flush=True)
results.append({"label": label, "ms": float("inf"), "peak": float("inf"), "ok": False, "note": "ERR"})
finally:
# Fused-backward optimizers register post-accumulate-grad hooks on the
# params; without removing them the hook closures keep this model alive
# past `del`, inflating the *next* fused optimizer's peak. Unregister so
# each run reports its true isolated peak regardless of run order.
for h in getattr(opt, "_hook_handles", []):
h.remove()
del opt, model
gc.collect()
torch.cuda.empty_cache()
def print_table(results: list, dtype_name: str):
results = sorted(results, key=lambda r: r["peak"])
headers = ["#", "Optimizer", "Peak VRAM", "Time/step"]
rows = []
for i, r in enumerate(results, 1):
if not r["ok"]:
rows.append([str(i), r["label"], r.get("note", "OOM"), "-"])
continue
rows.append([str(i), r["label"], f"{r['peak']:.2f} GB", f"{r['ms']:.1f} ms"])
widths = [max(len(str(row[c])) for row in [headers] + rows) for c in range(len(headers))]
def fmt(row, sep=""):
return sep.join(s.ljust(widths[c]) if c == 1 else s.rjust(widths[c]) for c, s in enumerate(row))
line_top = "" * (sum(widths) + 3 * (len(widths) - 1))
print()
print(f" dtype: {dtype_name}")
print(line_top)
print(fmt(headers))
print(line_top)
for row in rows:
print(fmt(row))
print(line_top)
def main():
n_params = sum(p.numel() for p in build_model(torch.float32).parameters())
print(f"Model: {N_LAYERS} blocks × d_model={D_MODEL} × d_ff={D_FF}")
print(f" {n_params/1e6:.1f}M params")
print(f"Step: batch={BATCH}, seq={SEQ}")
print(f"Timing: {WARMUP} warmup + {ITERS} timed iters")
print(f"Rounds: {', '.join(str(d).replace('torch.', '') for d in DTYPES)}")
from toolkit.optimizers.automagic import Automagic
from toolkit.optimizers.automagic2 import Automagic2
from toolkit.optimizers.automagic3 import Automagic3
from toolkit.optimizers.adafactor import Adafactor
from prodigyopt import Prodigy
import bitsandbytes as bnb
optimizers = [
("AdamW", lambda p: torch.optim.AdamW(p, lr=1e-4, eps=1e-6, foreach=False, fused=False)),
("AdamW8bit", lambda p: bnb.optim.AdamW8bit(p, lr=1e-4, eps=1e-6)),
("Adafactor", lambda p: Adafactor(p, lr=1e-4, scale_parameter=False, relative_step=False, warmup_init=False)),
("Automagic v1", lambda p: Automagic(p, lr=1e-4)),
("Automagic v2", lambda p: Automagic2(p, lr=1e-4)),
("Automagic v3 fused", lambda p: Automagic3(p, lr=1e-4, fused=True)),
("Automagic v3 unfused", lambda p: Automagic3(p, lr=1e-4, fused=False)),
("Prodigy", lambda p: Prodigy(p, lr=1.0, eps=1e-6)),
]
for dtype in DTYPES:
dtype_name = str(dtype).replace("torch.", "")
print(f"\n=== Round: {dtype_name} ===")
results: list = []
for label, factory in optimizers:
benchmark(results, label, factory, dtype)
print_table(results, dtype_name)
if __name__ == "__main__":
main()