66
77"""
88
9+ import re
910from dataclasses import dataclass , field
1011from enum import StrEnum
11- from typing import Union
12+ from typing import Any , Optional , Union
1213
1314import torch
1415
@@ -121,6 +122,16 @@ class AutoBitQuantizer(Quantizer):
121122 dbf_threshold : float = 2.0
122123 dbf_iters : int = None # None → DBF default (600); set low (e.g. 10) for fast testing
123124
125+ # --- vLLM fused-layer constraints ---
126+ # Groups of module suffixes that vLLM fuses into a single linear
127+ fused_groups : list = field (
128+ default_factory = lambda : [
129+ ["self_attn.q_proj" , "self_attn.k_proj" , "self_attn.v_proj" ],
130+ ["mlp.gate_proj" , "mlp.up_proj" ],
131+ ]
132+ )
133+ enable_fused_groups : bool = False
134+
124135 # internal variables
125136 _module_to_quantizer : dict = field (default_factory = dict , repr = False , init = False )
126137 _name_to_quantizer : dict = field (default_factory = dict , repr = False , init = False )
@@ -130,14 +141,9 @@ def __post_init__(self):
130141 if not isinstance (self .assignment_strategy , AssignmentStrategy ):
131142 self .assignment_strategy = AssignmentStrategy (self .assignment_strategy )
132143 if not self .quantizers :
133- self . quantizers = self . _default_quantizers ( )
144+ raise ValueError ( " quantizers must be provided" )
134145 self ._sync_flags ()
135146
136- @staticmethod
137- def _default_quantizers ():
138- """GPTQ candidates for 2–8 bit (auto-generated when ``quantizers=[]``)."""
139- return [GPTQ (wbits = b ) for b in (2 , 3 , 4 , 5 , 6 , 7 , 8 )]
140-
141147 def validate_params (self ):
142148 """Validate AutoBitQuantizer parameters."""
143149 bad = []
@@ -187,6 +193,23 @@ def validate_params(self):
187193 except (ValueError , TypeError ) as e :
188194 bad .append (f"quantizers[{ i } ] ({ type (q ).__name__ } ): { e } " )
189195
196+ _VLLM_SUPPORTED_BITS = {2 , 3 , 4 , 8 }
197+ if self .enable_fused_groups :
198+ for i , q in enumerate (self .quantizers ):
199+ if not isinstance (q , GPTQ ):
200+ bad .append (
201+ f"quantizers[{ i } ] ({ type (q ).__name__ } ): "
202+ f"enable_fused_groups=True requires all quantizers to be GPTQ"
203+ )
204+ elif q .wbits not in _VLLM_SUPPORTED_BITS :
205+ bad .append (
206+ f"quantizers[{ i } ] (GPTQ wbits={ q .wbits } ): "
207+ f"vLLM only supports GPTQ bit-widths { sorted (_VLLM_SUPPORTED_BITS )} "
208+ )
209+
210+ if self .assignment_strategy == AssignmentStrategy .MANUAL :
211+ bad .extend (self ._validate_manual_fused_consistency ())
212+
190213 if bad :
191214 raise ValueError ("; " .join (bad ))
192215
@@ -331,6 +354,47 @@ def _sync_flags(self):
331354 self .flag_hessian = any (q .flag_hessian for q in self .quantizers )
332355 self .flag_xtx = any (q .flag_xtx for q in self .quantizers )
333356
357+ def _validate_manual_fused_consistency (self ):
358+ """Check that manual keyword rules don't split fused groups."""
359+ bad = []
360+ for group in self .fused_groups :
361+ bits_for_suffix = {}
362+ for suffix in group :
363+ for child_q in self .quantizers :
364+ if self ._suffix_matches_quantizer (suffix , child_q ):
365+ bits_for_suffix [suffix ] = getattr (
366+ child_q , "wbits" , getattr (child_q , "bits" , None )
367+ )
368+ break
369+ unique_bits = set (bits_for_suffix .values ())
370+ if len (unique_bits ) > 1 :
371+ detail = ", " .join (f"{ s } ={ b } bit" for s , b in bits_for_suffix .items ())
372+ bad .append (
373+ f"Fused group { group } : mixed bit-widths ({ detail } ). "
374+ f"vLLM requires identical bit-widths within each fused group"
375+ )
376+ return bad
377+
378+ @staticmethod
379+ def _suffix_matches_quantizer (suffix , quantizer ):
380+ """Return True if quantizer's keyword/name rules would match suffix."""
381+ include_names = getattr (quantizer , "include_layer_names" , None )
382+ include_kw = getattr (quantizer , "include_layer_keywords" , None )
383+ exclude_kw = getattr (quantizer , "exclude_layer_keywords" , None )
384+
385+ if include_names is not None :
386+ if not any (suffix in name for name in include_names ):
387+ return False
388+ elif include_kw is not None :
389+ if not any (kw in suffix for kw in include_kw ):
390+ return False
391+
392+ if exclude_kw is not None :
393+ if any (kw in suffix for kw in exclude_kw ):
394+ return False
395+
396+ return True
397+
334398 def _assign_layer (self , name , module , child_q ):
335399 self .module_to_name [module ] = name
336400 self ._module_to_quantizer [module ] = child_q
@@ -422,7 +486,15 @@ def execute_post_processing(self):
422486 self .module_to_name = {}
423487 self ._module_to_quantizer = {}
424488
489+ def _all_children_gptq (self ) -> bool :
490+ if not self ._name_to_quantizer :
491+ return False
492+ return all (isinstance (q , GPTQ ) for q in self ._name_to_quantizer .values ())
493+
425494 def get_quant_config (self ) -> dict :
495+ if self ._all_children_gptq ():
496+ return self ._get_mixed_gptq_config ()
497+
426498 child_configs = []
427499 for child_q in self .quantizers :
428500 try :
@@ -439,6 +511,86 @@ def get_quant_config(self) -> dict:
439511 "quantizers" : child_configs ,
440512 }
441513
514+ def _get_mixed_gptq_config (self ) -> dict :
515+ unique_quantizers = list ({id (q ): q for q in self ._name_to_quantizer .values ()}.values ())
516+ dominant_q : GPTQ = max (
517+ unique_quantizers ,
518+ key = lambda q : sum (1 for v in self ._name_to_quantizer .values () if v is q ),
519+ )
520+ result : dict = {
521+ "quant_method" : "mixed_gptq" ,
522+ "bits" : dominant_q .wbits ,
523+ "groupsize" : dominant_q .groupsize ,
524+ "actorder" : dominant_q .actorder ,
525+ "group_size" : dominant_q .groupsize ,
526+ "desc_act" : dominant_q .actorder ,
527+ "sym" : dominant_q .sym ,
528+ "checkpoint_format" : "gptq" ,
529+ }
530+ if dominant_q .mlp_wbits is not None :
531+ result ["mlp_wbits" ] = dominant_q .mlp_wbits
532+ if dominant_q .mlp_groupsize is not None :
533+ result ["mlp_groupsize" ] = dominant_q .mlp_groupsize
534+ if dominant_q .module_wbits :
535+ result ["module_wbits" ] = dict (dominant_q .module_wbits )
536+ return result
537+
538+ def _build_quantization_bits (
539+ self ,
540+ num_layers : int ,
541+ ) -> list [dict [str , Any ]]:
542+ _LAYER_RE = re .compile (r"\.layers\.(\d+)\.(.*)" )
543+
544+ skipped : list [str ] = []
545+ layer_modules : dict [int , dict [str , Any ]] = {}
546+ for name , child_q in self ._name_to_quantizer .items ():
547+ m = _LAYER_RE .search (name )
548+ if m is None :
549+ skipped .append (name )
550+ continue
551+ layer_idx = int (m .group (1 ))
552+ suffix = m .group (2 )
553+ layer_modules .setdefault (layer_idx , {})[suffix ] = {
554+ "bits" : child_q .wbits ,
555+ "method" : "gptq" ,
556+ "params" : {"group_size" : child_q .groupsize },
557+ }
558+
559+ if skipped :
560+ self .logger .debug (
561+ "Skipped %d module(s) not matching layer pattern: %s" ,
562+ len (skipped ),
563+ skipped ,
564+ )
565+
566+ if not layer_modules :
567+ return []
568+
569+ return [layer_modules .get (i , {}) for i in range (num_layers )]
570+
571+ def finalize_quant_config_for_save (
572+ self ,
573+ quant_config : dict [str , Any ],
574+ quantized_layer_names : list [str ],
575+ num_hidden_layers : Optional [int ] = None ,
576+ ) -> dict [str , Any ]:
577+ if quant_config .get ("quant_method" ) != "mixed_gptq" :
578+ return quant_config
579+
580+ if num_hidden_layers is None :
581+ raise ValueError (
582+ "num_hidden_layers is required for mixed_gptq quantization_bits "
583+ "(Runner passes model.config.num_hidden_layers)"
584+ )
585+
586+ quant_config ["quantization_bits" ] = self ._build_quantization_bits (
587+ num_layers = num_hidden_layers ,
588+ )
589+
590+ # TODO : DBF fallback is not supported yet
591+
592+ return quant_config
593+
442594 def create_inference_layer (self , result , linear_module , ** kwargs ):
443595 for name , stored_result in self .results .items ():
444596 if stored_result is result :
0 commit comments