Add variable precision adam convrot optimizer with stochastic rounding and accumulation.
This commit is contained in:
1 parent
bbb1fda457
commit
7690ea62c8
3 files changed
+703
-2
No files matched your search
@@ -106,6 +106,11 @@ def get_optimizer(
|
|||||||
elif lower_type == 'automagicexperiment':
|
elif lower_type == 'automagicexperiment':
|
||||||
from toolkit.optimizers.automagicEXPERIMENT import AutomagicEXPERIMENT
|
from toolkit.optimizers.automagicEXPERIMENT import AutomagicEXPERIMENT
|
||||||
optimizer = AutomagicEXPERIMENT(params, lr=float(learning_rate), **optimizer_params)
|
optimizer = AutomagicEXPERIMENT(params, lr=float(learning_rate), **optimizer_params)
|
||||||
|
elif lower_type == 'adamconvrot':
|
||||||
|
from toolkit.optimizers.adamconvrot import AdamConvRot
|
||||||
|
if 'eps' not in optimizer_params:
|
||||||
|
optimizer_params['eps'] = 1e-6
|
||||||
|
optimizer = AdamConvRot(params, lr=float(learning_rate), **optimizer_params)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unknown optimizer type {optimizer_type}')
|
raise ValueError(f'Unknown optimizer type {optimizer_type}')
|
||||||
return optimizer
|
return optimizer
|
||||||
@@ -0,0 +1,687 @@
|
|||||||
|
"""
|
||||||
|
AdamConvRot: Adam / AdamW with both moments stored in ConvRot n-bit codes.
|
||||||
|
|
||||||
|
qtype picks the storage width: "convrot8" or "convrotint2".."convrotint8"
|
||||||
|
(the convrot_quant names; 8 = plain int8 codes, everything else is the
|
||||||
|
bit-packed n-bit format of pack_intn_rows). adamw=True is decoupled weight
|
||||||
|
decay (AdamW), adamw=False is classic L2-in-the-gradient Adam.
|
||||||
|
|
||||||
|
Storage. Each moment is viewed flat and cut into 256-element blocks; a block
|
||||||
|
is rotated with the ConvRot regular-Hadamard transform (rotate(x, 256) from
|
||||||
|
convrot_quant -- spreads a block's outliers so one symmetric scale covers the
|
||||||
|
block without clipping the rest) and stored as n-bit symmetric codes with one
|
||||||
|
fp32 scale per block. The second moment is kept in the SQRT domain (what the
|
||||||
|
update divides by; half the dynamic range). Cost: bits/4 bytes per element
|
||||||
|
for both moments plus 1/32 byte of scales -- 2.03 B/elem at 8 bit, 1.03 at 4,
|
||||||
|
0.53 at 2.
|
||||||
|
|
||||||
|
Requantization is NOT dequantize -> EMA -> re-round. That scheme is dead on
|
||||||
|
arrival for the slow second moment: with beta2 = 0.999 the per-step change
|
||||||
|
of sqrt(v) is ~0.05%, far below half an int8 step, so nearest rounding
|
||||||
|
freezes the state (it only ever ratchets up on spikes and never decays), and
|
||||||
|
per-step stochastic rounding is unbiased but injects one code of noise every
|
||||||
|
step that the EMA only damps at rate (1 - beta2): ~9 codes rms at 8 bit,
|
||||||
|
more than the whole 4-bit grid. Instead the uniform decay of an EMA is
|
||||||
|
applied EXACTLY to the block scale (scale *= beta each step, an fp32
|
||||||
|
multiply), and only the increment -- (1-beta1)*g for m, the exact
|
||||||
|
sqrt-domain increment for s -- is rotated and added to the codes with
|
||||||
|
stochastic rounding, so an element with no gradient decays exactly and the
|
||||||
|
rounding noise is Poisson in the accumulated increment instead of one code
|
||||||
|
per step. A block is re-rounded (new scale from its exact fp32 values,
|
||||||
|
stochastic) only when a code would leave the grid; the new scale leaves
|
||||||
|
1.25x headroom so growth does not retrigger it every step. A dead block
|
||||||
|
(scale 0) restarts from its increment on the first non-zero gradient.
|
||||||
|
|
||||||
|
The update uses the exact fp32 moments of the current step (before their
|
||||||
|
requantization), bias-corrected as in torch Adam; parameters are updated in
|
||||||
|
fp32 and written back with stochastic rounding for bf16 / fp16.
|
||||||
|
|
||||||
|
Everything for one parameter -- unpack, 2 rotations back, EMA, update,
|
||||||
|
parameter write, 2 rotations forward, stochastic requant, pack -- is one
|
||||||
|
triton kernel launch, one pass over memory, no fp32 temporaries (the
|
||||||
|
rotation is two batched 16x16 tensor-core dots, exact on integer codes).
|
||||||
|
CPU / non-triton devices run the identical algorithm in torch. Tensors
|
||||||
|
below min_quant_numel (or 1D) keep plain fp32 moments.
|
||||||
|
|
||||||
|
fused=True updates each parameter inside the backward pass (a
|
||||||
|
post-accumulate-grad hook) and frees its gradient at once, so the peak never
|
||||||
|
holds a full set of gradients; .step() then only runs the closure. That
|
||||||
|
bypasses the trainer's grad clipping / non-finite skip, so non-finite grads
|
||||||
|
are zeroed in the hook, and it is incompatible with multi-backward gradient
|
||||||
|
accumulation. Default is the traditional unfused step, where low-precision
|
||||||
|
(bf16 / fp16) parameters accumulate their micro-batch grads through a
|
||||||
|
post-accumulate-grad hook that sums in fp32 and stochastically rounds back
|
||||||
|
into the param's dtype (optimizer_utils.stochastic_grad_accummulation), so
|
||||||
|
small per-micro-batch grads are not lost to round-to-nearest; .step()
|
||||||
|
consumes that buffer. Note the trainer's grad clipping does not see it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from toolkit.optimizers.optimizer_utils import (
|
||||||
|
copy_stochastic,
|
||||||
|
stochastic_grad_accummulation,
|
||||||
|
)
|
||||||
|
from toolkit.util.convrot_quant import (
|
||||||
|
_import_triton,
|
||||||
|
_triton_available,
|
||||||
|
pack_intn_rows,
|
||||||
|
regular_hadamard,
|
||||||
|
rotate,
|
||||||
|
unpack_intn_rows,
|
||||||
|
)
|
||||||
|
|
||||||
|
BLOCK = 256 # rotation block == scale group
|
||||||
|
HEADROOM = 1.25 # rescale puts the block max at qmax / HEADROOM
|
||||||
|
TINY = 1e-30
|
||||||
|
|
||||||
|
|
||||||
|
def _bits_for_qtype(qtype: str) -> int:
|
||||||
|
if qtype == "convrot8":
|
||||||
|
return 8
|
||||||
|
if qtype.startswith("convrotint"):
|
||||||
|
try:
|
||||||
|
bits = int(qtype[len("convrotint") :])
|
||||||
|
except ValueError:
|
||||||
|
bits = 0
|
||||||
|
if 2 <= bits <= 8:
|
||||||
|
return bits
|
||||||
|
raise ValueError(
|
||||||
|
f"AdamConvRot: unsupported qtype {qtype!r}; use convrot8 or convrotint2..convrotint8"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- triton path
|
||||||
|
|
||||||
|
_kernel = None
|
||||||
|
_h16_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _h16(device):
|
||||||
|
key = str(device)
|
||||||
|
h = _h16_cache.get(key)
|
||||||
|
if h is None:
|
||||||
|
# regular_hadamard(256) == kron(H16, H16): the 256-rotation is H16 over
|
||||||
|
# the high base-16 digit and H16 over the low one
|
||||||
|
h = regular_hadamard(16, device, torch.float32).contiguous()
|
||||||
|
_h16_cache[key] = h
|
||||||
|
return h
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kernel():
|
||||||
|
global _kernel
|
||||||
|
if _kernel is not None:
|
||||||
|
return _kernel
|
||||||
|
triton, tl = _import_triton()
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _rot256(x, hb, R: tl.constexpr):
|
||||||
|
# tf32 dots are exact for integer codes: |c| <= 127 and H16 = +-1/4 keep
|
||||||
|
# every partial sum within 11 mantissa bits
|
||||||
|
x3 = tl.reshape(x, (R, 16, 16))
|
||||||
|
z = tl.dot(x3, hb, input_precision="tf32")
|
||||||
|
y3 = tl.dot(hb, z, input_precision="tf32")
|
||||||
|
return tl.reshape(y3, (R, 256))
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _unpack256(
|
||||||
|
c_ptr,
|
||||||
|
b,
|
||||||
|
bm,
|
||||||
|
BITS: tl.constexpr,
|
||||||
|
BPOW: tl.constexpr,
|
||||||
|
QMAX: tl.constexpr,
|
||||||
|
R: tl.constexpr,
|
||||||
|
):
|
||||||
|
# pack_intn_rows layout: 8 codes -> one BITS-byte little-endian word
|
||||||
|
gi = tl.arange(0, 32)
|
||||||
|
bi = tl.arange(0, BPOW)
|
||||||
|
j = tl.arange(0, 8)
|
||||||
|
ptrs = (
|
||||||
|
c_ptr
|
||||||
|
+ b[:, None, None] * (32 * BITS)
|
||||||
|
+ gi[None, :, None] * BITS
|
||||||
|
+ bi[None, None, :]
|
||||||
|
)
|
||||||
|
msk = bm[:, None, None] & (bi < BITS)[None, None, :]
|
||||||
|
by = tl.load(ptrs, mask=msk, other=0).to(tl.int64)
|
||||||
|
word = tl.sum(by << (8 * bi)[None, None, :].to(tl.int64), axis=2)
|
||||||
|
v = (word[:, :, None] >> (BITS * j)[None, None, :].to(tl.int64)) & (
|
||||||
|
(1 << BITS) - 1
|
||||||
|
)
|
||||||
|
return tl.reshape((v - QMAX).to(tl.float32), (R, 256))
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _pack256(
|
||||||
|
c_ptr,
|
||||||
|
b,
|
||||||
|
bm,
|
||||||
|
q,
|
||||||
|
BITS: tl.constexpr,
|
||||||
|
BPOW: tl.constexpr,
|
||||||
|
QMAX: tl.constexpr,
|
||||||
|
R: tl.constexpr,
|
||||||
|
):
|
||||||
|
u = tl.reshape(q + QMAX, (R, 32, 8)).to(tl.int64)
|
||||||
|
j = tl.arange(0, 8)
|
||||||
|
word = tl.sum(u << (BITS * j)[None, None, :].to(tl.int64), axis=2)
|
||||||
|
gi = tl.arange(0, 32)
|
||||||
|
bi = tl.arange(0, BPOW)
|
||||||
|
by = ((word[:, :, None] >> (8 * bi)[None, None, :].to(tl.int64)) & 0xFF).to(
|
||||||
|
tl.uint8
|
||||||
|
)
|
||||||
|
ptrs = (
|
||||||
|
c_ptr
|
||||||
|
+ b[:, None, None] * (32 * BITS)
|
||||||
|
+ gi[None, :, None] * BITS
|
||||||
|
+ bi[None, None, :]
|
||||||
|
)
|
||||||
|
msk = bm[:, None, None] & (bi < BITS)[None, None, :]
|
||||||
|
tl.store(ptrs, by, mask=msk)
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _requant256(
|
||||||
|
c_ptr,
|
||||||
|
s_ptr,
|
||||||
|
b,
|
||||||
|
bm,
|
||||||
|
offs,
|
||||||
|
codes,
|
||||||
|
scale,
|
||||||
|
decay,
|
||||||
|
inc_rot,
|
||||||
|
rqmax,
|
||||||
|
tiny,
|
||||||
|
seed_a,
|
||||||
|
BITS: tl.constexpr,
|
||||||
|
BPOW: tl.constexpr,
|
||||||
|
QMAX: tl.constexpr,
|
||||||
|
SR: tl.constexpr,
|
||||||
|
R: tl.constexpr,
|
||||||
|
):
|
||||||
|
s_dec = scale * decay
|
||||||
|
values = codes * s_dec[:, None] + inc_rot # exact fp32 block values
|
||||||
|
if SR:
|
||||||
|
u = tl.rand(seed_a, offs)
|
||||||
|
else:
|
||||||
|
u = tl.full((R, 256), 0.5, tl.float32)
|
||||||
|
q = tl.floor(values / tl.maximum(s_dec, tiny)[:, None] + u)
|
||||||
|
over = tl.max(tl.abs(q), axis=1) > QMAX
|
||||||
|
# rescale path: new scale with headroom, same random draw (only the
|
||||||
|
# triggering element's rounding is conditioned on it)
|
||||||
|
s_out = tl.where(over, tl.max(tl.abs(values), axis=1) / rqmax, s_dec)
|
||||||
|
q = tl.floor(values / tl.maximum(s_out, tiny)[:, None] + u)
|
||||||
|
q = tl.minimum(tl.maximum(q, -1.0 * QMAX), 1.0 * QMAX)
|
||||||
|
tl.store(s_ptr + b, s_out, mask=bm)
|
||||||
|
_pack256(c_ptr, b, bm, q.to(tl.int32), BITS, BPOW, QMAX, R)
|
||||||
|
return s_out
|
||||||
|
|
||||||
|
@triton.jit(do_not_specialize=["numel", "n_blocks", "seed"])
|
||||||
|
def adam_convrot_kernel(
|
||||||
|
p_ptr,
|
||||||
|
g_ptr,
|
||||||
|
cm_ptr,
|
||||||
|
sm_ptr,
|
||||||
|
cs_ptr,
|
||||||
|
ss_ptr,
|
||||||
|
h_ptr,
|
||||||
|
numel,
|
||||||
|
n_blocks,
|
||||||
|
lr,
|
||||||
|
wd,
|
||||||
|
beta1,
|
||||||
|
beta2,
|
||||||
|
eps,
|
||||||
|
inv_bc1,
|
||||||
|
inv_sqrt_bc2,
|
||||||
|
sqrt_beta2,
|
||||||
|
rqmax,
|
||||||
|
tiny,
|
||||||
|
seed,
|
||||||
|
BITS: tl.constexpr,
|
||||||
|
BPOW: tl.constexpr,
|
||||||
|
QMAX: tl.constexpr,
|
||||||
|
ADAMW: tl.constexpr,
|
||||||
|
P_KIND: tl.constexpr,
|
||||||
|
SR: tl.constexpr,
|
||||||
|
SANITIZE: tl.constexpr,
|
||||||
|
R: tl.constexpr,
|
||||||
|
):
|
||||||
|
pid = tl.program_id(0)
|
||||||
|
b = pid * R + tl.arange(0, R)
|
||||||
|
bm = b < n_blocks
|
||||||
|
e = tl.arange(0, 256)
|
||||||
|
offs = b[:, None] * 256 + e[None, :]
|
||||||
|
em = bm[:, None] & (offs < numel)
|
||||||
|
r = tl.arange(0, 16)
|
||||||
|
hb = tl.load(
|
||||||
|
h_ptr
|
||||||
|
+ tl.zeros((R, 1, 1), tl.int32)
|
||||||
|
+ r[None, :, None] * 16
|
||||||
|
+ r[None, None, :]
|
||||||
|
)
|
||||||
|
|
||||||
|
g = tl.load(g_ptr + offs, mask=em, other=0.0).to(tl.float32)
|
||||||
|
if SANITIZE:
|
||||||
|
g = tl.where((g == g) & (tl.abs(g) < 3.0e38), g, 0.0)
|
||||||
|
if not ADAMW:
|
||||||
|
p0 = tl.load(p_ptr + offs, mask=em, other=0.0).to(tl.float32)
|
||||||
|
g = g + wd * p0
|
||||||
|
|
||||||
|
# second moment (sqrt domain) first: only its reciprocal stays live
|
||||||
|
ss = tl.load(ss_ptr + b, mask=bm, other=0.0)
|
||||||
|
cs = _unpack256(cs_ptr, b, bm, BITS, BPOW, QMAX, R)
|
||||||
|
s_old = _rot256(cs, hb, R) * ss[:, None]
|
||||||
|
# increment from the rectified value: quant noise can leave s negative,
|
||||||
|
# and an increment against a negative s would grow with the noise
|
||||||
|
s_pos = tl.maximum(s_old, 0.0)
|
||||||
|
s_new = tl.sqrt(beta2 * s_pos * s_pos + (1.0 - beta2) * g * g)
|
||||||
|
inc_s = _rot256(s_new - sqrt_beta2 * s_pos, hb, R)
|
||||||
|
ss_new = _requant256(
|
||||||
|
cs_ptr,
|
||||||
|
ss_ptr,
|
||||||
|
b,
|
||||||
|
bm,
|
||||||
|
offs,
|
||||||
|
cs,
|
||||||
|
ss,
|
||||||
|
sqrt_beta2,
|
||||||
|
inc_s,
|
||||||
|
rqmax,
|
||||||
|
tiny,
|
||||||
|
seed + 2,
|
||||||
|
BITS,
|
||||||
|
BPOW,
|
||||||
|
QMAX,
|
||||||
|
SR,
|
||||||
|
R,
|
||||||
|
)
|
||||||
|
# denominator floored at the block's code resolution -- a stored
|
||||||
|
# sqrt(v) below one code step is unresolved, not small
|
||||||
|
den = 1.0 / (tl.maximum(s_new, ss_new[:, None]) * inv_sqrt_bc2 + eps)
|
||||||
|
|
||||||
|
# first moment and the parameter update
|
||||||
|
sm = tl.load(sm_ptr + b, mask=bm, other=0.0)
|
||||||
|
cm = _unpack256(cm_ptr, b, bm, BITS, BPOW, QMAX, R)
|
||||||
|
m_new = beta1 * (_rot256(cm, hb, R) * sm[:, None]) + (1.0 - beta1) * g
|
||||||
|
upd = m_new * inv_bc1 * den
|
||||||
|
p = tl.load(p_ptr + offs, mask=em, other=0.0).to(tl.float32)
|
||||||
|
if ADAMW:
|
||||||
|
p_new = p - lr * (upd + wd * p)
|
||||||
|
else:
|
||||||
|
p_new = p - lr * upd
|
||||||
|
inc_m = _rot256(g * (1.0 - beta1), hb, R)
|
||||||
|
_requant256(
|
||||||
|
cm_ptr,
|
||||||
|
sm_ptr,
|
||||||
|
b,
|
||||||
|
bm,
|
||||||
|
offs,
|
||||||
|
cm,
|
||||||
|
sm,
|
||||||
|
beta1,
|
||||||
|
inc_m,
|
||||||
|
rqmax,
|
||||||
|
tiny,
|
||||||
|
seed,
|
||||||
|
BITS,
|
||||||
|
BPOW,
|
||||||
|
QMAX,
|
||||||
|
SR,
|
||||||
|
R,
|
||||||
|
)
|
||||||
|
|
||||||
|
if P_KIND == 0:
|
||||||
|
tl.store(p_ptr + offs, p_new, mask=em)
|
||||||
|
elif P_KIND == 1:
|
||||||
|
if SR:
|
||||||
|
# copy_stochastic_bf16: random 16-bit add, truncate
|
||||||
|
xi = p_new.to(tl.int32, bitcast=True)
|
||||||
|
r16 = (tl.randint(seed + 4, offs) & 0xFFFF).to(tl.int32)
|
||||||
|
xi = (xi + r16) & -65536
|
||||||
|
out = xi.to(tl.float32, bitcast=True).to(tl.bfloat16)
|
||||||
|
else:
|
||||||
|
out = p_new.to(tl.bfloat16)
|
||||||
|
tl.store(p_ptr + offs, out, mask=em)
|
||||||
|
else:
|
||||||
|
if SR:
|
||||||
|
# fp16 ulp from the fp32 exponent (subnormals below 2^-14)
|
||||||
|
ex = (p_new.to(tl.int32, bitcast=True) >> 23) & 0xFF
|
||||||
|
ulp = tl.exp2((tl.maximum(ex - 127, -14) - 10).to(tl.float32))
|
||||||
|
u = tl.rand(seed + 4, offs)
|
||||||
|
out = (tl.floor(p_new / ulp + u) * ulp).to(tl.float16)
|
||||||
|
else:
|
||||||
|
out = p_new.to(tl.float16)
|
||||||
|
tl.store(p_ptr + offs, out, mask=em)
|
||||||
|
|
||||||
|
_kernel = adam_convrot_kernel
|
||||||
|
return _kernel
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ optimizer
|
||||||
|
|
||||||
|
|
||||||
|
class AdamConvRot(torch.optim.Optimizer):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
params,
|
||||||
|
lr=1e-3,
|
||||||
|
betas=(0.9, 0.999),
|
||||||
|
eps=1e-8,
|
||||||
|
weight_decay=0.0,
|
||||||
|
qtype="convrot8",
|
||||||
|
adamw=True,
|
||||||
|
min_quant_numel=4096,
|
||||||
|
fused=False,
|
||||||
|
blocks_per_program=8, # 8x4 warps: no register spills, ~1.1 ms per 64M elems on a 5090
|
||||||
|
num_warps=4,
|
||||||
|
):
|
||||||
|
if lr < 0.0:
|
||||||
|
raise ValueError(f"Invalid learning rate: {lr}")
|
||||||
|
if not 0.0 <= betas[0] < 1.0 or not 0.0 <= betas[1] < 1.0:
|
||||||
|
raise ValueError(f"Invalid betas: {betas}")
|
||||||
|
self.bits = _bits_for_qtype(qtype)
|
||||||
|
self.qtype = qtype
|
||||||
|
self.adamw = bool(adamw)
|
||||||
|
self.min_quant_numel = int(min_quant_numel)
|
||||||
|
self.blocks_per_program = int(blocks_per_program)
|
||||||
|
self.num_warps = int(num_warps)
|
||||||
|
# off only for validation (nearest rounding, kernel == torch path)
|
||||||
|
self.stochastic_rounding = True
|
||||||
|
defaults = {"lr": lr, "betas": betas, "eps": eps, "weight_decay": weight_decay}
|
||||||
|
super().__init__(params, defaults)
|
||||||
|
|
||||||
|
self.fused = bool(fused)
|
||||||
|
self._hook_handles = []
|
||||||
|
self._rebuild_group_index()
|
||||||
|
for group in self.param_groups:
|
||||||
|
for p in group["params"]:
|
||||||
|
if not p.requires_grad:
|
||||||
|
continue
|
||||||
|
if self.fused:
|
||||||
|
self._hook_handles.append(
|
||||||
|
p.register_post_accumulate_grad_hook(self._fused_hook)
|
||||||
|
)
|
||||||
|
elif p.dtype != torch.float32:
|
||||||
|
# stochastic bf16/fp16 grad accumulation across micro-batches
|
||||||
|
self._hook_handles.append(
|
||||||
|
p.register_post_accumulate_grad_hook(
|
||||||
|
stochastic_grad_accummulation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _rebuild_group_index(self):
|
||||||
|
# the hooks cannot rely on group-dict identity: load_state_dict
|
||||||
|
# replaces the group dicts
|
||||||
|
self._param_group_index = {
|
||||||
|
p: gi for gi, group in enumerate(self.param_groups) for p in group["params"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def _fused_hook(self, p):
|
||||||
|
gi = self._param_group_index.get(p)
|
||||||
|
if gi is None:
|
||||||
|
self._rebuild_group_index()
|
||||||
|
gi = self._param_group_index.get(p, 0)
|
||||||
|
seed = int(torch.randint(0, 2**31 - 1, (1,)).item())
|
||||||
|
self._update_param(p, self.param_groups[gi], seed, sanitize=True)
|
||||||
|
p.grad = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def qmax(self) -> int:
|
||||||
|
return (1 << (self.bits - 1)) - 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rescale_qmax(self) -> float:
|
||||||
|
return max(self.qmax / HEADROOM, 1.0)
|
||||||
|
|
||||||
|
def _quantizes(self, p: torch.Tensor) -> bool:
|
||||||
|
return p.dim() >= 2 and p.numel() >= self.min_quant_numel
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ state
|
||||||
|
|
||||||
|
def _init_state(self, p: torch.Tensor):
|
||||||
|
state = self.state[p]
|
||||||
|
state["step"] = 0
|
||||||
|
if self._quantizes(p):
|
||||||
|
n_blocks = -(-p.numel() // BLOCK)
|
||||||
|
zero_codes = pack_intn_rows(
|
||||||
|
torch.zeros(n_blocks, BLOCK, dtype=torch.int8, device=p.device),
|
||||||
|
self.bits,
|
||||||
|
)
|
||||||
|
for key in ("exp_avg", "exp_avg_sq"):
|
||||||
|
state[key] = zero_codes.clone()
|
||||||
|
# fp32 scales as a uint8 view: the parent load_state_dict casts
|
||||||
|
# floating state to the param dtype
|
||||||
|
state[key + "_scale"] = torch.zeros(
|
||||||
|
n_blocks, dtype=torch.float32, device=p.device
|
||||||
|
).view(torch.uint8)
|
||||||
|
else:
|
||||||
|
state["exp_avg"] = torch.zeros_like(p, dtype=torch.float32)
|
||||||
|
state["exp_avg_sq"] = torch.zeros_like(p, dtype=torch.float32)
|
||||||
|
|
||||||
|
def _state_ok(self, p: torch.Tensor, state) -> bool:
|
||||||
|
if "exp_avg" not in state or "step" not in state:
|
||||||
|
return False
|
||||||
|
if self._quantizes(p):
|
||||||
|
n_blocks = -(-p.numel() // BLOCK)
|
||||||
|
return (
|
||||||
|
"exp_avg_scale" in state
|
||||||
|
and tuple(state["exp_avg"].shape) == (n_blocks, 32 * self.bits)
|
||||||
|
and state["exp_avg"].dtype == torch.uint8
|
||||||
|
)
|
||||||
|
return state["exp_avg"].shape == p.shape
|
||||||
|
|
||||||
|
def load_state_dict(self, state_dict):
|
||||||
|
# the parent casts every state tensor to the param dtype, which would
|
||||||
|
# turn the uint8 code / scale buffers (and fp32 moments) into bf16
|
||||||
|
super().load_state_dict(state_dict)
|
||||||
|
saved_ids = [pid for g in state_dict["param_groups"] for pid in g["params"]]
|
||||||
|
cur_params = [p for g in self.param_groups for p in g["params"]]
|
||||||
|
id_map = dict(zip(saved_ids, cur_params))
|
||||||
|
for pid, saved in state_dict["state"].items():
|
||||||
|
p = id_map.get(pid)
|
||||||
|
if p is None:
|
||||||
|
continue
|
||||||
|
for k, v in saved.items():
|
||||||
|
if k != "step" and torch.is_tensor(v):
|
||||||
|
self.state[p][k] = v.to(p.device, copy=True)
|
||||||
|
self._rebuild_group_index()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- step
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def step(self, closure=None):
|
||||||
|
loss = None
|
||||||
|
if closure is not None:
|
||||||
|
with torch.enable_grad():
|
||||||
|
loss = closure()
|
||||||
|
|
||||||
|
if self.fused:
|
||||||
|
return loss # every param was updated inside backward
|
||||||
|
|
||||||
|
step_seed = int(torch.randint(0, 2**31 - 1, (1,)).item())
|
||||||
|
param_index = 0
|
||||||
|
for group in self.param_groups:
|
||||||
|
for p in group["params"]:
|
||||||
|
param_index += 1
|
||||||
|
accum = getattr(p, "_accum_grad", None)
|
||||||
|
if accum is not None:
|
||||||
|
p.grad = accum
|
||||||
|
del p._accum_grad
|
||||||
|
if p.grad is None:
|
||||||
|
continue
|
||||||
|
seed = (step_seed ^ (param_index * 0x9E3779B1)) & 0x7FFFFFFF
|
||||||
|
self._update_param(p, group, seed, sanitize=False)
|
||||||
|
return loss
|
||||||
|
|
||||||
|
def _update_param(self, p, group, seed, sanitize):
|
||||||
|
if p.grad.is_sparse:
|
||||||
|
raise RuntimeError("AdamConvRot does not support sparse gradients")
|
||||||
|
beta1, beta2 = group["betas"]
|
||||||
|
state = self.state[p]
|
||||||
|
if not self._state_ok(p, state):
|
||||||
|
if len(state) > 0:
|
||||||
|
print(
|
||||||
|
"WARNING: AdamConvRot state does not match the parameter "
|
||||||
|
"or qtype; re-initializing its moments."
|
||||||
|
)
|
||||||
|
self._init_state(p)
|
||||||
|
state["step"] += 1
|
||||||
|
t = state["step"]
|
||||||
|
bc1 = 1.0 - beta1**t
|
||||||
|
bc2 = 1.0 - beta2**t
|
||||||
|
if self._quantizes(p):
|
||||||
|
self._step_quant(p, state, group, bc1, bc2, seed, sanitize)
|
||||||
|
else:
|
||||||
|
self._step_fp32(p, state, group, bc1, bc2, sanitize)
|
||||||
|
|
||||||
|
def _step_fp32(self, p, state, group, bc1, bc2, sanitize=False):
|
||||||
|
beta1, beta2 = group["betas"]
|
||||||
|
lr, wd, eps = group["lr"], group["weight_decay"], group["eps"]
|
||||||
|
g = p.grad.to(torch.float32)
|
||||||
|
if sanitize:
|
||||||
|
g = g.nan_to_num(nan=0.0, posinf=0.0, neginf=0.0)
|
||||||
|
p32 = p.to(torch.float32)
|
||||||
|
if not self.adamw and wd != 0.0:
|
||||||
|
g = g.add(p32, alpha=wd)
|
||||||
|
m, v = state["exp_avg"], state["exp_avg_sq"]
|
||||||
|
m.mul_(beta1).add_(g, alpha=1.0 - beta1)
|
||||||
|
v.mul_(beta2).addcmul_(g, g, value=1.0 - beta2)
|
||||||
|
upd = (m / bc1) / ((v / bc2).sqrt_().add_(eps))
|
||||||
|
if self.adamw and wd != 0.0:
|
||||||
|
upd.add_(p32, alpha=wd)
|
||||||
|
if p.dtype == torch.float32:
|
||||||
|
p.add_(upd, alpha=-lr)
|
||||||
|
return
|
||||||
|
p32.add_(upd, alpha=-lr)
|
||||||
|
if self.stochastic_rounding and p.is_cuda:
|
||||||
|
copy_stochastic(p, p32)
|
||||||
|
else:
|
||||||
|
p.copy_(p32)
|
||||||
|
|
||||||
|
def _step_quant(self, p, state, group, bc1, bc2, seed, sanitize=False):
|
||||||
|
use_triton = (
|
||||||
|
p.is_cuda
|
||||||
|
and p.grad.is_cuda
|
||||||
|
and p.is_contiguous()
|
||||||
|
and p.dtype in (torch.float32, torch.bfloat16, torch.float16)
|
||||||
|
and _triton_available()
|
||||||
|
)
|
||||||
|
if use_triton:
|
||||||
|
self._step_quant_triton(p, state, group, bc1, bc2, seed, sanitize)
|
||||||
|
else:
|
||||||
|
self._step_quant_torch(p, state, group, bc1, bc2, sanitize)
|
||||||
|
|
||||||
|
def _step_quant_triton(self, p, state, group, bc1, bc2, seed, sanitize=False):
|
||||||
|
beta1, beta2 = group["betas"]
|
||||||
|
g = p.grad
|
||||||
|
if not g.is_contiguous():
|
||||||
|
g = g.contiguous()
|
||||||
|
n_blocks = state["exp_avg"].shape[0]
|
||||||
|
R = self.blocks_per_program
|
||||||
|
kernel = _get_kernel()
|
||||||
|
p_kind = {torch.float32: 0, torch.bfloat16: 1, torch.float16: 2}[p.dtype]
|
||||||
|
kernel[(-(-n_blocks // R),)](
|
||||||
|
p,
|
||||||
|
g,
|
||||||
|
state["exp_avg"],
|
||||||
|
state["exp_avg_scale"].view(torch.float32),
|
||||||
|
state["exp_avg_sq"],
|
||||||
|
state["exp_avg_sq_scale"].view(torch.float32),
|
||||||
|
_h16(p.device),
|
||||||
|
p.numel(),
|
||||||
|
n_blocks,
|
||||||
|
float(group["lr"]),
|
||||||
|
float(group["weight_decay"]),
|
||||||
|
float(beta1),
|
||||||
|
float(beta2),
|
||||||
|
float(group["eps"]),
|
||||||
|
1.0 / bc1,
|
||||||
|
1.0 / math.sqrt(bc2),
|
||||||
|
math.sqrt(beta2),
|
||||||
|
float(self.rescale_qmax),
|
||||||
|
TINY,
|
||||||
|
int(seed),
|
||||||
|
BITS=self.bits,
|
||||||
|
BPOW=max(2, 1 << (self.bits - 1).bit_length()),
|
||||||
|
QMAX=self.qmax,
|
||||||
|
ADAMW=self.adamw,
|
||||||
|
P_KIND=p_kind,
|
||||||
|
SR=self.stochastic_rounding,
|
||||||
|
SANITIZE=bool(sanitize),
|
||||||
|
R=R,
|
||||||
|
num_warps=self.num_warps,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ torch path
|
||||||
|
|
||||||
|
def _dequant(self, codes, scale_u8, n_blocks):
|
||||||
|
c = unpack_intn_rows(codes, self.bits, n_blocks, BLOCK).to(torch.float32)
|
||||||
|
return (rotate(c, BLOCK) * scale_u8.view(torch.float32)[:, None]).reshape(-1)
|
||||||
|
|
||||||
|
def _requant(self, codes, scale_u8, decay, inc_rot):
|
||||||
|
qmax = self.qmax
|
||||||
|
scale = scale_u8.view(torch.float32)
|
||||||
|
n_blocks = codes.shape[0]
|
||||||
|
c = unpack_intn_rows(codes, self.bits, n_blocks, BLOCK).to(torch.float32)
|
||||||
|
s_dec = scale * decay
|
||||||
|
values = c * s_dec[:, None] + inc_rot
|
||||||
|
u = torch.rand_like(inc_rot) if self.stochastic_rounding else 0.5
|
||||||
|
q = torch.floor(values / s_dec.clamp_min(TINY)[:, None] + u)
|
||||||
|
over = q.abs().amax(dim=1) > qmax
|
||||||
|
s_out = torch.where(over, values.abs().amax(dim=1) / self.rescale_qmax, s_dec)
|
||||||
|
q = torch.floor(values / s_out.clamp_min(TINY)[:, None] + u).clamp_(-qmax, qmax)
|
||||||
|
codes.copy_(pack_intn_rows(q.to(torch.int8), self.bits))
|
||||||
|
scale.copy_(s_out)
|
||||||
|
return scale
|
||||||
|
|
||||||
|
def _step_quant_torch(self, p, state, group, bc1, bc2, sanitize=False):
|
||||||
|
beta1, beta2 = group["betas"]
|
||||||
|
lr, wd, eps = group["lr"], group["weight_decay"], group["eps"]
|
||||||
|
numel = p.numel()
|
||||||
|
n_blocks = state["exp_avg"].shape[0]
|
||||||
|
n_pad = n_blocks * BLOCK
|
||||||
|
g = torch.zeros(n_pad, dtype=torch.float32, device=p.device)
|
||||||
|
g[:numel] = p.grad.reshape(-1).to(torch.float32)
|
||||||
|
if sanitize:
|
||||||
|
g.nan_to_num_(nan=0.0, posinf=0.0, neginf=0.0)
|
||||||
|
p32 = torch.zeros(n_pad, dtype=torch.float32, device=p.device)
|
||||||
|
p32[:numel] = p.reshape(-1).to(torch.float32)
|
||||||
|
if not self.adamw:
|
||||||
|
g.add_(p32, alpha=wd)
|
||||||
|
|
||||||
|
m_old = self._dequant(state["exp_avg"], state["exp_avg_scale"], n_blocks)
|
||||||
|
m_new = beta1 * m_old + (1.0 - beta1) * g
|
||||||
|
self._requant(
|
||||||
|
state["exp_avg"],
|
||||||
|
state["exp_avg_scale"],
|
||||||
|
beta1,
|
||||||
|
rotate((g * (1.0 - beta1)).view(n_blocks, BLOCK), BLOCK),
|
||||||
|
)
|
||||||
|
s_old = self._dequant(state["exp_avg_sq"], state["exp_avg_sq_scale"], n_blocks)
|
||||||
|
s_pos = s_old.clamp_min(0.0)
|
||||||
|
s_new = torch.sqrt(beta2 * s_pos * s_pos + (1.0 - beta2) * g * g)
|
||||||
|
sqrt_beta2 = math.sqrt(beta2)
|
||||||
|
ss_new = self._requant(
|
||||||
|
state["exp_avg_sq"],
|
||||||
|
state["exp_avg_sq_scale"],
|
||||||
|
sqrt_beta2,
|
||||||
|
rotate((s_new - sqrt_beta2 * s_pos).view(n_blocks, BLOCK), BLOCK),
|
||||||
|
)
|
||||||
|
|
||||||
|
# denominator floored at the block's code resolution (see the kernel)
|
||||||
|
s_eff = torch.maximum(s_new, ss_new.repeat_interleave(BLOCK))
|
||||||
|
upd = (m_new / bc1) / (s_eff / math.sqrt(bc2) + eps)
|
||||||
|
if self.adamw:
|
||||||
|
upd.add_(p32, alpha=wd)
|
||||||
|
p32.add_(upd, alpha=-lr)
|
||||||
|
new = p32[:numel].view(p.shape)
|
||||||
|
if p.dtype == torch.float32:
|
||||||
|
p.copy_(new)
|
||||||
|
elif self.stochastic_rounding and p.is_cuda:
|
||||||
|
copy_stochastic(p, new)
|
||||||
|
else:
|
||||||
|
p.copy_(new)
|
||||||
@@ -10,6 +10,8 @@ Compares speed (ms/step) and peak VRAM across:
|
|||||||
- Automagic v2 (fused-backward)
|
- Automagic v2 (fused-backward)
|
||||||
- Automagic v3 (fused-backward and traditional/unfused)
|
- Automagic v3 (fused-backward and traditional/unfused)
|
||||||
- Automagic AdamW (fused-backward and traditional/unfused)
|
- Automagic AdamW (fused-backward and traditional/unfused)
|
||||||
|
- AdamConvRot (AdamW with ConvRot-quantized moments; 8, 4 and 2 bit,
|
||||||
|
traditional/unfused and fused-backward)
|
||||||
- Prodigy
|
- Prodigy
|
||||||
|
|
||||||
Pass optimizer keys as CLI args to run a subset (all when none passed), e.g.:
|
Pass optimizer keys as CLI args to run a subset (all when none passed), e.g.:
|
||||||
@@ -181,6 +183,7 @@ def main():
|
|||||||
from toolkit.optimizers.automagic2 import Automagic2
|
from toolkit.optimizers.automagic2 import Automagic2
|
||||||
from toolkit.optimizers.automagic3 import Automagic3
|
from toolkit.optimizers.automagic3 import Automagic3
|
||||||
from toolkit.optimizers.automagicEXPERIMENT import AutomagicEXPERIMENT
|
from toolkit.optimizers.automagicEXPERIMENT import AutomagicEXPERIMENT
|
||||||
|
from toolkit.optimizers.adamconvrot import AdamConvRot
|
||||||
from toolkit.optimizers.adafactor import Adafactor
|
from toolkit.optimizers.adafactor import Adafactor
|
||||||
from prodigyopt import Prodigy
|
from prodigyopt import Prodigy
|
||||||
import bitsandbytes as bnb
|
import bitsandbytes as bnb
|
||||||
@@ -195,18 +198,24 @@ def main():
|
|||||||
("automagic3", "Automagic v3 unfused", lambda p: Automagic3(p, lr=1e-4, fused=False)),
|
("automagic3", "Automagic v3 unfused", lambda p: Automagic3(p, lr=1e-4, fused=False)),
|
||||||
("automagicEXPERIMENT", "AutomagicEXPERIMENT", lambda p: AutomagicEXPERIMENT(p, lr=1e-4, fused=True)),
|
("automagicEXPERIMENT", "AutomagicEXPERIMENT", lambda p: AutomagicEXPERIMENT(p, lr=1e-4, fused=True)),
|
||||||
("automagicEXPERIMENT", "AutomagicEXPERIMENT", lambda p: AutomagicEXPERIMENT(p, lr=1e-4, fused=False)),
|
("automagicEXPERIMENT", "AutomagicEXPERIMENT", lambda p: AutomagicEXPERIMENT(p, lr=1e-4, fused=False)),
|
||||||
|
("adamconvrot", "AdamConvRot 8bit", lambda p: AdamConvRot(p, lr=1e-4, eps=1e-6, qtype="convrot8")),
|
||||||
|
("adamconvrot", "AdamConvRot 4bit", lambda p: AdamConvRot(p, lr=1e-4, eps=1e-6, qtype="convrotint4")),
|
||||||
|
("adamconvrot", "AdamConvRot 2bit", lambda p: AdamConvRot(p, lr=1e-4, eps=1e-6, qtype="convrotint2")),
|
||||||
|
("adamconvrot", "AdamConvRot 8bit fused", lambda p: AdamConvRot(p, lr=1e-4, eps=1e-6, qtype="convrot8", fused=True)),
|
||||||
|
("adamconvrot", "AdamConvRot 4bit fused", lambda p: AdamConvRot(p, lr=1e-4, eps=1e-6, qtype="convrotint4", fused=True)),
|
||||||
|
("adamconvrot", "AdamConvRot 2bit fused", lambda p: AdamConvRot(p, lr=1e-4, eps=1e-6, qtype="convrotint2", fused=True)),
|
||||||
("prodigy", "Prodigy", lambda p: Prodigy(p, lr=1.0, eps=1e-6)),
|
("prodigy", "Prodigy", lambda p: Prodigy(p, lr=1.0, eps=1e-6)),
|
||||||
]
|
]
|
||||||
|
|
||||||
selected = [a.lower() for a in sys.argv[1:]]
|
selected = [a.lower() for a in sys.argv[1:]]
|
||||||
if selected:
|
if selected:
|
||||||
valid = {key for key, _, _ in optimizers}
|
valid = {key.lower() for key, _, _ in optimizers}
|
||||||
unknown = [a for a in selected if a not in valid]
|
unknown = [a for a in selected if a not in valid]
|
||||||
if unknown:
|
if unknown:
|
||||||
print(f"Unknown optimizer keys: {', '.join(unknown)}")
|
print(f"Unknown optimizer keys: {', '.join(unknown)}")
|
||||||
print(f"Valid keys: {', '.join(sorted(valid))}")
|
print(f"Valid keys: {', '.join(sorted(valid))}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
optimizers = [o for o in optimizers if o[0] in selected]
|
optimizers = [o for o in optimizers if o[0].lower() in selected]
|
||||||
|
|
||||||
for dtype in DTYPES:
|
for dtype in DTYPES:
|
||||||
dtype_name = str(dtype).replace("torch.", "")
|
dtype_name = str(dtype).replace("torch.", "")
|
||||||
|
|||||||
Reference in new issue
Block a user