1212import bitsandbytes .functional as F
1313from bitsandbytes .functional import QuantState
1414
15-
16- class Experts4bit (nn .Module ):
17- """4-bit quantized storage for fused Mixture-of-Experts expert weights.
15+ # Storage bit-width per supported quantization scheme. The 4-bit schemes pack two values per
16+ # byte via quantize_4bit; the 8-bit schemes store one codebook index per byte via
17+ # quantize_blockwise ("int8" = the blockwise dynamic map, NOT the LLM.int8() vectorwise
18+ # scheme; "fp8" = an e4m3 float codebook). The 16-bit entries are unquantized passthrough
19+ # storage — no codebook, no absmax — useful as a reference baseline or a per-layer opt-out.
20+ _SCHEME_BITS = {
21+ "nf4" : 4 ,
22+ "fp4" : 4 ,
23+ "int8" : 8 ,
24+ "fp8" : 8 ,
25+ "bf16" : 16 ,
26+ "fp16" : 16 ,
27+ }
28+
29+ _PASSTHROUGH_DTYPES = {"bf16" : torch .bfloat16 , "fp16" : torch .float16 }
30+
31+
32+ def _build_code (quant_type : str , device ) -> Optional [torch .Tensor ]:
33+ """The shared per-scheme codebook (`None` for 16-bit passthrough)."""
34+ if quant_type in ("nf4" , "fp4" ):
35+ return F .get_4bit_type (quant_type , device = device )
36+ if quant_type == "int8" :
37+ code = F .create_dynamic_map ()
38+ elif quant_type == "fp8" :
39+ code = F .create_fp8_map (signed = True , exponent_bits = 4 , precision_bits = 3 , total_bits = 8 )
40+ else :
41+ return None
42+ return code .to (device ) if device is not None else code
43+
44+
45+ class ExpertsNbit (nn .Module ):
46+ """Low-bit quantized storage for fused Mixture-of-Experts expert weights.
1847
1948 A growing number of models in the Hugging Face ecosystem store their MoE expert
2049 weights as a single 3D ``nn.Parameter`` of shape ``[num_experts, out_features,
2150 in_features]`` (e.g. ``OlmoeExperts``, ``Qwen3MoeExperts``) rather than as a
22- collection of ``nn.Linear`` layers. The default 4-bit quantization walker only
23- replaces ``nn.Linear`` modules, so these fused experts are silently skipped and
24- stay in full precision — the dominant contribution to the model's memory footprint
51+ collection of ``nn.Linear`` layers. The default quantization walkers only replace
52+ ``nn.Linear`` modules, so these fused experts are silently skipped and stay in full
53+ precision — the dominant contribution to the model's memory footprint
2554 (see https://github.com/bitsandbytes-foundation/bitsandbytes/issues/1849).
2655
27- ``Experts4bit`` holds the two expert projections (``gate_up_proj`` and ``down_proj``)
28- in 4-bit NF4/FP4 precision. Unlike :class:`Linear4bit`, the packed weights are kept
29- as plain ``nn.Parameter`` buffers and the per-expert quantization statistics
30- (``absmax``) live on the module as ordinary buffers. This avoids bending
31- :class:`Params4bit`'s tensor-subclass and device-movement machinery around a 3D
32- stack, and it means the module serializes through the standard ``state_dict``
33- mechanism with no custom save/load hooks.
56+ ``ExpertsNbit`` holds the two expert projections (``gate_up_proj`` and ``down_proj``)
57+ at a configurable storage precision selected by ``quant_type``:
58+
59+ * ``"nf4"`` / ``"fp4"`` — 4-bit blockwise (two values per byte, 4x compression).
60+ :class:`Experts4bit` is the fixed-4-bit specialization of this class.
61+ * ``"int8"`` / ``"fp8"`` — 8-bit blockwise (one codebook index per byte, 2x
62+ compression at substantially higher fidelity than 4-bit). ``"int8"`` uses the
63+ blockwise dynamic map (`quantize_blockwise`'s default); it is *not* the
64+ LLM.int8() vectorwise scheme. ``"fp8"`` uses an e4m3 float codebook.
65+ * ``"bf16"`` / ``"fp16"`` — unquantized passthrough storage (no compression). A
66+ reference baseline and a per-layer opt-out inside an otherwise-quantized model.
67+
68+ The packed weights are kept as plain ``nn.Parameter`` buffers and the per-expert
69+ quantization statistics (``absmax``) live on the module as ordinary buffers. This
70+ avoids bending :class:`Params4bit`'s tensor-subclass and device-movement machinery
71+ around a 3D stack, and it means the module serializes through the standard
72+ ``state_dict`` mechanism with no custom save/load hooks.
3473
3574 The forward pass dequantizes a single expert at a time (a per-expert loop), mirroring
3675 the reference fused-experts forward. Grouped-GEMM is intentionally left for future
@@ -51,17 +90,20 @@ class Experts4bit(nn.Module):
5190 to ``torch.nn.functional.silu`` (SwiGLU), matching OLMoE / Qwen3-MoE.
5291 compute_dtype (`torch.dtype`, *optional*): The dtype expert weights are
5392 dequantized to for the matmul. When `None`, the input's dtype is used.
54- quant_type (`str`, *optional*, defaults to `"nf4"`): The 4-bit data type, ``nf4``
55- or ``fp4``.
93+ quant_type (`str`, *optional*, defaults to `"nf4"`): The storage scheme — one of
94+ ``nf4``, ``fp4``, ``int8``, ``fp8``, ``bf16``, ``fp16 ``.
5695 blocksize (`int`, *optional*, defaults to `64`): The quantization block size.
96+ Ignored for the 16-bit passthrough schemes.
5797 device (*optional*): The device for the (empty) packed buffers.
5898
5999 Raises:
60- ValueError: If ``quant_type`` is invalid, or if ``hidden_dim`` / ``intermediate_dim``
61- is not divisible by ``blocksize`` (required so per-expert quantization blocks
62- never straddle an expert boundary).
100+ ValueError: If ``quant_type`` is invalid, or if a quantized scheme is selected and
101+ ``hidden_dim`` / ``intermediate_dim`` is not divisible by ``blocksize``
102+ (required so per-expert quantization blocks never straddle an expert boundary).
63103 """
64104
105+ _ALLOWED_QUANT_TYPES : tuple = tuple (_SCHEME_BITS )
106+
65107 def __init__ (
66108 self ,
67109 num_experts : int ,
@@ -76,21 +118,25 @@ def __init__(
76118 ):
77119 super ().__init__ ()
78120
79- if quant_type not in ("nf4" , "fp4" ):
80- raise ValueError (f"quant_type must be 'nf4' or 'fp4', got { quant_type !r} " )
121+ allowed = type (self )._ALLOWED_QUANT_TYPES
122+ if quant_type not in allowed :
123+ raise ValueError (f"quant_type must be one of { allowed } , got { quant_type !r} " )
124+
125+ self .bits = _SCHEME_BITS [quant_type ]
81126
82127 # Each expert is quantized independently, so an expert occupies a contiguous
83128 # `out_features * in_features` run of elements. Requiring the in_features dim to
84129 # be a multiple of the blocksize guarantees `out_features * in_features` is too,
85130 # so blocks tile each expert exactly and absmax reshapes cleanly to
86131 # [num_experts, blocks_per_expert]. (gate_up in_features is hidden_dim; down_proj
87- # in_features is intermediate_dim.)
88- for name , in_features in (("hidden_dim" , hidden_dim ), ("intermediate_dim" , intermediate_dim )):
89- if in_features % blocksize != 0 :
90- raise ValueError (
91- f"{ name } ({ in_features } ) must be divisible by blocksize ({ blocksize } ) "
92- "so per-expert quantization blocks align with expert boundaries"
93- )
132+ # in_features is intermediate_dim.) Passthrough storage has no blocks.
133+ if self .bits < 16 :
134+ for name , in_features in (("hidden_dim" , hidden_dim ), ("intermediate_dim" , intermediate_dim )):
135+ if in_features % blocksize != 0 :
136+ raise ValueError (
137+ f"{ name } ({ in_features } ) must be divisible by blocksize ({ blocksize } ) "
138+ "so per-expert quantization blocks align with expert boundaries"
139+ )
94140
95141 self .num_experts = num_experts
96142 self .hidden_dim = hidden_dim
@@ -108,29 +154,39 @@ def __init__(
108154 gate_up_numel = gate_up_out * hidden_dim
109155 down_numel = hidden_dim * intermediate_dim
110156
111- # Packed 4-bit weights as plain (frozen) parameters: two 4-bit values per byte.
112- self .gate_up_proj = nn .Parameter (
113- torch .empty (num_experts , gate_up_numel // 2 , dtype = torch .uint8 , device = device ),
114- requires_grad = False ,
115- )
116- self .down_proj = nn .Parameter (
117- torch .empty (num_experts , down_numel // 2 , dtype = torch .uint8 , device = device ),
118- requires_grad = False ,
119- )
120-
121- # Per-expert quantization scales.
122- self .register_buffer (
123- "gate_up_absmax" ,
124- torch .empty (num_experts , gate_up_numel // blocksize , dtype = torch .float32 , device = device ),
125- )
126- self .register_buffer (
127- "down_absmax" ,
128- torch .empty (num_experts , down_numel // blocksize , dtype = torch .float32 , device = device ),
129- )
157+ # Packed weights as plain (frozen) parameters. 4-bit: two values per byte; 8-bit:
158+ # one codebook index per byte; 16-bit: the weights themselves in the storage dtype.
159+ if self .bits == 16 :
160+ storage_dtype = _PASSTHROUGH_DTYPES [quant_type ]
161+ gate_up_storage = torch .empty (num_experts , gate_up_numel , dtype = storage_dtype , device = device )
162+ down_storage = torch .empty (num_experts , down_numel , dtype = storage_dtype , device = device )
163+ else :
164+ packed_per_value = 2 if self .bits == 4 else 1
165+ gate_up_storage = torch .empty (
166+ num_experts , gate_up_numel // packed_per_value , dtype = torch .uint8 , device = device
167+ )
168+ down_storage = torch .empty (num_experts , down_numel // packed_per_value , dtype = torch .uint8 , device = device )
169+ self .gate_up_proj = nn .Parameter (gate_up_storage , requires_grad = False )
170+ self .down_proj = nn .Parameter (down_storage , requires_grad = False )
171+
172+ # Per-expert quantization scales (absent for passthrough storage — registering None
173+ # keeps attribute access uniform while leaving state_dict free of the keys).
174+ if self .bits < 16 :
175+ self .register_buffer (
176+ "gate_up_absmax" ,
177+ torch .empty (num_experts , gate_up_numel // blocksize , dtype = torch .float32 , device = device ),
178+ )
179+ self .register_buffer (
180+ "down_absmax" ,
181+ torch .empty (num_experts , down_numel // blocksize , dtype = torch .float32 , device = device ),
182+ )
183+ else :
184+ self .register_buffer ("gate_up_absmax" , None )
185+ self .register_buffer ("down_absmax" , None )
130186
131- # The 4-bit codebook is identical for every expert and fully determined by
132- # quant_type, so it is reconstructed at init rather than serialized.
133- self .register_buffer ("code" , F . get_4bit_type (quant_type , device = device ), persistent = False )
187+ # The codebook is identical for every expert and fully determined by quant_type,
188+ # so it is reconstructed at init rather than serialized.
189+ self .register_buffer ("code" , _build_code (quant_type , device ), persistent = False )
134190
135191 @classmethod
136192 def from_float (
@@ -142,8 +198,8 @@ def from_float(
142198 compute_dtype : Optional [torch .dtype ] = None ,
143199 quant_type : str = "nf4" ,
144200 blocksize : int = 64 ,
145- ) -> "Experts4bit " :
146- """Build an :class:`Experts4bit ` by quantizing full-precision expert weights.
201+ ) -> "ExpertsNbit " :
202+ """Build an :class:`ExpertsNbit ` by quantizing full-precision expert weights.
147203
148204 Args:
149205 gate_up_proj (`torch.Tensor`): Shape ``[num_experts, gate_up_out, hidden_dim]``,
@@ -152,7 +208,7 @@ def from_float(
152208 down_proj (`torch.Tensor`): Shape ``[num_experts, hidden_dim, intermediate_dim]``.
153209
154210 Returns:
155- `Experts4bit `: A module holding the quantized weights on the inputs' device.
211+ `ExpertsNbit `: A module holding the quantized weights on the inputs' device.
156212 """
157213 if gate_up_proj .dim () != 3 or down_proj .dim () != 3 :
158214 raise ValueError ("gate_up_proj and down_proj must be 3D [num_experts, out, in] tensors" )
@@ -181,30 +237,55 @@ def from_float(
181237 module .down_absmax = down_absmax
182238 return module
183239
184- def _quantize_stack (self , weights : torch .Tensor ) -> tuple [torch .Tensor , torch .Tensor ]:
185- """Quantize a ``[num_experts, out, in]`` stack to packed bytes + per-expert absmax."""
240+ def _quantize_stack (self , weights : torch .Tensor ) -> tuple [torch .Tensor , Optional [torch .Tensor ]]:
241+ """Quantize a ``[num_experts, out, in]`` stack to packed storage + per-expert absmax.
242+
243+ For 16-bit passthrough the stack is stored as-is (flattened per expert) and the
244+ returned absmax is `None`.
245+ """
246+ if self .bits == 16 :
247+ storage_dtype = _PASSTHROUGH_DTYPES [self .quant_type ]
248+ return weights .reshape (weights .shape [0 ], - 1 ).to (storage_dtype ).contiguous (), None
249+
186250 packed = []
187251 absmax = []
188252 for e in range (weights .shape [0 ]):
189- q , state = F .quantize_4bit (
190- weights [e ].contiguous (),
191- blocksize = self .blocksize ,
192- compress_statistics = False ,
193- quant_type = self .quant_type ,
194- )
253+ if self .bits == 4 :
254+ q , state = F .quantize_4bit (
255+ weights [e ].contiguous (),
256+ blocksize = self .blocksize ,
257+ compress_statistics = False ,
258+ quant_type = self .quant_type ,
259+ )
260+ else :
261+ q , state = F .quantize_blockwise (
262+ weights [e ].contiguous (),
263+ code = self .code ,
264+ blocksize = self .blocksize ,
265+ )
195266 packed .append (q .reshape (- 1 ))
196267 absmax .append (state .absmax .reshape (- 1 ))
197268 return torch .stack (packed ), torch .stack (absmax )
198269
199270 def _dequantize_expert (
200271 self ,
201272 packed : torch .Tensor ,
202- absmax : torch .Tensor ,
273+ absmax : Optional [ torch .Tensor ] ,
203274 shape : tuple [int , int ],
204275 expert_idx : int ,
205276 dtype : torch .dtype ,
206277 ) -> torch .Tensor :
207278 """Dequantize a single expert's 2D weight ``[out, in]`` for the matmul."""
279+ if self .bits == 16 :
280+ return packed [expert_idx ].reshape (shape ).to (dtype )
281+ if self .bits == 8 :
282+ quant_state = QuantState (
283+ absmax = absmax [expert_idx ],
284+ code = self .code ,
285+ blocksize = self .blocksize ,
286+ dtype = dtype ,
287+ )
288+ return F .dequantize_blockwise (packed [expert_idx ].reshape (shape ), quant_state = quant_state )
208289 quant_state = QuantState (
209290 absmax = absmax [expert_idx ],
210291 shape = torch .Size (shape ),
@@ -217,6 +298,11 @@ def _dequantize_expert(
217298 # transpose back-compat shim — keyed on A.shape[0] == 1 — from firing).
218299 return F .dequantize_4bit (packed [expert_idx ].reshape (- 1 , 1 ), quant_state = quant_state )
219300
301+ def _project (self , packed , absmax , shape , expert_idx , x , compute_dtype ):
302+ """One expert projection: dequantize + ``linear``."""
303+ weight = self ._dequantize_expert (packed , absmax , shape , expert_idx , compute_dtype )
304+ return F_nn .linear (x , weight )
305+
220306 def forward (
221307 self ,
222308 hidden_states : torch .Tensor ,
@@ -238,21 +324,43 @@ def forward(
238324 top_k_pos , token_idx = torch .where (expert_mask [expert_idx ])
239325 current_state = hidden_states [token_idx ]
240326
241- gate_up_w = self ._dequantize_expert (
242- self .gate_up_proj , self .gate_up_absmax , self ._gate_up_shape , expert_idx , compute_dtype
327+ proj = self ._project (
328+ self .gate_up_proj ,
329+ self .gate_up_absmax ,
330+ self ._gate_up_shape ,
331+ expert_idx ,
332+ current_state ,
333+ compute_dtype ,
243334 )
244- proj = F_nn .linear (current_state , gate_up_w )
245335 if self .has_gate :
246336 gate , up = proj .chunk (2 , dim = - 1 )
247337 current_hidden = self .act_fn (gate ) * up
248338 else :
249339 current_hidden = self .act_fn (proj )
250340
251- down_w = self ._dequantize_expert (
252- self .down_proj , self .down_absmax , self ._down_shape , expert_idx , compute_dtype
341+ current_hidden = self ._project (
342+ self .down_proj ,
343+ self .down_absmax ,
344+ self ._down_shape ,
345+ expert_idx ,
346+ current_hidden ,
347+ compute_dtype ,
253348 )
254- current_hidden = F_nn .linear (current_hidden , down_w )
255349 current_hidden = current_hidden * top_k_weights [token_idx , top_k_pos , None ]
256350 final_hidden_states .index_add_ (0 , token_idx , current_hidden .to (final_hidden_states .dtype ))
257351
258352 return final_hidden_states .to (hidden_states .dtype )
353+
354+
355+ class Experts4bit (ExpertsNbit ):
356+ """4-bit quantized storage for fused Mixture-of-Experts expert weights.
357+
358+ The fixed-4-bit (``nf4`` / ``fp4``) specialization of :class:`ExpertsNbit` — see the
359+ parent class for the storage design, constructor arguments, and forward semantics.
360+ Kept as a distinct class so the 4-bit contract (two packed values per byte, 4x
361+ compression) has a stable name.
362+
363+ <Tip warning={true}>This feature is experimental and may change in future releases.</Tip>
364+ """
365+
366+ _ALLOWED_QUANT_TYPES = ("nf4" , "fp4" )
0 commit comments