Skip to content

Commit 0c86556

Browse files
authored
Merge pull request #2 from eaglstun/mps-lion-triton-8bit
Fix Lion (and Adagrad) decoupled weight decay in the Triton 8-bit blockwise kernel
2 parents 7aa17ce + 7333266 commit 0c86556

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

bitsandbytes/backends/triton/kernels_optim.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -917,8 +917,11 @@ def _optimizer_update_1state_8bit_blockwise_triton_kernel(
917917
s1 = dequant_8bit_blockwise_kernel_util(state1_ptr, offsets, qmap1_ptr, absmax1_ptr, mask, BLOCK_SIZE_N)
918918

919919
# 3. Optimizer-specific updates
920-
# LION
921-
if weight_decay > 0.0 and OPTIMIZER_ID == 2:
920+
# LION (id 4) uses decoupled weight decay: shrink the param directly, outside the
921+
# sign update (Chen et al. 2023). This was previously gated on OPTIMIZER_ID == 2,
922+
# which is ADAGRAD -- so Lion got coupled decay (corrupting its sign update) and
923+
# Adagrad got decoupled decay instead of the L2 fold it expects.
924+
if weight_decay > 0.0 and OPTIMIZER_ID == 4:
922925
p *= 1.0 - lr * weight_decay
923926
# Apply weight decay for momentum, rmsprop, adagrad
924927
elif weight_decay > 0.0:

tests/test_optim.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,52 @@ def test_lion32bit_weight_decay(dim1, dim2, gtype, device):
292292
p2.copy_(p1.data)
293293

294294

295+
# bf16 is excluded: its ~8-bit mantissa rounds the bug's per-element (2 * lr) update
296+
# difference away on param write-back, so coupled and decoupled Lion8bit are numerically
297+
# indistinguishable in bf16 regardless of correctness. fp32/fp16 resolve it cleanly.
298+
@pytest.mark.parametrize("gtype", [torch.float32, torch.float16], ids=describe_dtype)
299+
@pytest.mark.parametrize("dim1", [1024], ids=id_formatter("dim1"))
300+
@pytest.mark.parametrize("dim2", [32, 1024], ids=id_formatter("dim2"))
301+
@pytest.mark.parametrize("device", get_available_devices(), ids=id_formatter("device"))
302+
def test_lion8bit_blockwise_weight_decay(dim1, dim2, gtype, device):
303+
"""Lion8bit must also use *decoupled* weight decay, like the 32-bit path.
304+
305+
Companion to test_lion32bit_weight_decay, covering the 8-bit blockwise kernels. The
306+
Triton 1-state blockwise kernel gated its decoupled-decay branch on OPTIMIZER_ID == 2
307+
(ADAGRAD) rather than 4 (LION), so Lion fell through to the coupled (L2) fold and its
308+
sign update was computed from a decay-polluted gradient.
309+
310+
Params are *not* resynced between steps: the coupled-vs-decoupled difference is a
311+
small per-step signal that only becomes reliably measurable once it accumulates. The
312+
budgets sit well above the correct path's 8-bit quantization noise and far below the
313+
error the coupled-decay bug produces (measured ~1.3e-3 in fp32, ~1.5e-3 in fp16).
314+
"""
315+
if device == "mps":
316+
# The 8-bit blockwise optimizer op is not implemented for MPS (the existing
317+
# test_optimizer8bit hits the same gap); there is nothing to exercise here.
318+
pytest.skip("optimizer_update_8bit_blockwise is not implemented for the MPS device")
319+
320+
weight_decay = 0.1
321+
err_budget = 1e-4 if gtype == torch.float32 else 4e-4
322+
323+
p1 = torch.randn(dim1, dim2, device=device, dtype=gtype) * 0.1
324+
p2 = p1.clone()
325+
p1 = p1.float()
326+
327+
torch_optimizer = Lion([p1], weight_decay=weight_decay)
328+
bnb_optimizer = bnb.optim.Lion8bit([p2], weight_decay=weight_decay)
329+
330+
for i in range(k):
331+
g = torch.randn(dim1, dim2, device=device, dtype=gtype) * 0.01
332+
p1.grad = g.clone().float()
333+
p2.grad = g.clone()
334+
335+
torch_optimizer.step()
336+
bnb_optimizer.step()
337+
338+
assert (p1 - p2.float()).abs().mean().item() < err_budget
339+
340+
295341
@pytest.mark.parametrize("dim1", [1024], ids=id_formatter("dim1"))
296342
@pytest.mark.parametrize("dim2", [32, 1024, 4097], ids=id_formatter("dim2"))
297343
@pytest.mark.parametrize("gtype", [torch.float32, torch.float16], ids=describe_dtype)

0 commit comments

Comments
 (0)