@@ -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