@@ -292,3 +292,58 @@ def test_experts4bit_lora_training_reduces_loss():
292292 # The frozen 4-bit base is bit-identical before and after training.
293293 assert torch .equal (base .gate_up_proj , gate_up_before )
294294 assert torch .equal (base .down_proj , down_before )
295+
296+
297+ # --- #1849 regression + shape coverage ------------------------------------------------------------
298+
299+
300+ def test_experts4bit_1849_regression_fused_experts_get_quantized ():
301+ """Regression for #1849. transformers v5 stores MoE experts as a single fused 3D ``nn.Parameter``
302+ (``[num_experts, out, in]``, e.g. ``Qwen3MoeExperts``), which the default 4-bit walker skips because
303+ there is no ``nn.Linear`` to replace — so the experts stay full-precision and dominate memory.
304+ ``Experts4bit`` is the fix: it actually 4-bit-quantizes the fused stack. Assert (a) the fused module
305+ exposes no ``nn.Linear`` for the walker to catch, and (b) ``from_float`` yields ``uint8``-packed 4-bit
306+ weights materially smaller than the fp16 originals."""
307+ num_experts , hidden , inter = 4 , 128 , 256
308+
309+ class FusedExperts (torch .nn .Module ): # mirrors OlmoeExperts / Qwen3MoeExperts from #1849
310+ def __init__ (self ):
311+ super ().__init__ ()
312+ self .gate_up_proj = torch .nn .Parameter (torch .randn (num_experts , 2 * inter , hidden ) * 0.1 )
313+ self .down_proj = torch .nn .Parameter (torch .randn (num_experts , hidden , inter ) * 0.1 )
314+
315+ fused = FusedExperts ()
316+ # (a) the walker's target type is absent -> a Linear4bit conversion would be a silent no-op here.
317+ assert not any (isinstance (m , torch .nn .Linear ) for m in fused .modules ())
318+ fp16_bytes = (fused .gate_up_proj .numel () + fused .down_proj .numel ()) * 2
319+
320+ # (b) Experts4bit quantizes the fused stack: uint8-packed 4-bit weights + small fp32 absmax.
321+ q = Experts4bit .from_float (
322+ fused .gate_up_proj .data .half (), fused .down_proj .data .half (), compute_dtype = torch .float16
323+ )
324+ assert q .gate_up_proj .dtype == torch .uint8 and q .down_proj .dtype == torch .uint8
325+ quantized_bytes = (
326+ q .gate_up_proj .numel () + q .down_proj .numel () # uint8 packed
327+ + (q .gate_up_absmax .numel () + q .down_absmax .numel ()) * 4 # fp32 absmax
328+ )
329+ assert quantized_bytes < fp16_bytes / 3 # ~4x on the weights, minus small absmax overhead
330+
331+
332+ @pytest .mark .parametrize ("device" , get_available_devices ())
333+ @pytest .mark .parametrize (
334+ "num_experts,hidden,inter" ,
335+ [(2 , 64 , 64 ), (8 , 128 , 256 ), (4 , 192 , 320 )],
336+ ids = ["e2_h64_i64" , "e8_h128_i256" , "e4_h192_i320" ],
337+ )
338+ def test_experts4bit_shapes (device , num_experts , hidden , inter ):
339+ """Forward is correct across a spread of MoE dims (all multiples of the blocksize)."""
340+ gate_up = torch .randn (num_experts , 2 * inter , hidden , dtype = torch .float32 , device = device ) * 0.1
341+ down = torch .randn (num_experts , hidden , inter , dtype = torch .float32 , device = device ) * 0.1
342+ module = Experts4bit .from_float (gate_up , down , compute_dtype = torch .float32 )
343+
344+ n_tok = 10
345+ hidden_states = torch .randn (n_tok , hidden , device = device )
346+ top_k_index = torch .randint (0 , num_experts , (n_tok , TOP_K ), device = device )
347+ top_k_weights = torch .softmax (torch .randn (n_tok , TOP_K , device = device ), dim = - 1 )
348+ out = module (hidden_states , top_k_index , top_k_weights )
349+ assert out .shape == (n_tok , hidden ) and torch .isfinite (out ).all ()
0 commit comments