55from __future__ import annotations
66
77import logging
8+ from copy import deepcopy
89from dataclasses import dataclass
910from typing import TYPE_CHECKING , Callable
1011
@@ -123,30 +124,62 @@ def _quant_config_rank(qargs: dict[str, int | bool]) -> tuple[int, int, int]:
123124def normalize_qkv_quant_config (
124125 wrapper : ModelWrapper ,
125126 qcfg : OliveHfQuantizationConfig ,
126- module_names : set [str ] | None = None ,
127+ locked_modules : set [str ] | None = None ,
127128) -> OliveHfQuantizationConfig :
128129 """Promote split QKV projection overrides to one shared quantization config.
129130
130- When ``module_names`` is provided, QKV members not in the set (e.g., excluded from
131- quantization) are ignored when forming the group and choosing the promoted config.
131+ Groups span all attention input projections of a layer regardless of whether the current
132+ pass quantizes them; follow-up passes (e.g. RTN after AutoClip) will pick up the shared
133+ settings via the recorded overrides so downstream QKV fusion remains valid.
134+
135+ ``locked_modules`` are modules whose overrides must not be rewritten -- typically the
136+ pre-existing overrides of an already-quantized checkpoint. For a group containing a
137+ locked member, the shared config is forced to that locked member's config; if multiple
138+ locked members of one group disagree, the group is left untouched.
132139 """
133- for group in get_qkv_quantization_groups (wrapper , module_names ):
134- group_qargs = {module_name : qcfg .get_qlinear_init_args (module_name ) for module_name in group }
140+ locked_modules = locked_modules or set ()
141+ for group in get_qkv_quantization_groups (wrapper ):
142+ group_qargs = {name : qcfg .get_qlinear_init_args (name ) for name in group }
135143 if len ({tuple (qargs .items ()) for qargs in group_qargs .values ()}) == 1 :
136144 continue
137145
138- promoted_qargs = max (group_qargs .values (), key = _quant_config_rank )
146+ locked_in_group = [name for name in group if name in locked_modules ]
147+ locked_configs = {tuple (group_qargs [name ].items ()) for name in locked_in_group }
148+ if len (locked_configs ) > 1 :
149+ logger .debug (
150+ "QKV group %s contains already-quantized members with conflicting configs; "
151+ "skipping (downstream QKV fusion may be inhibited)." ,
152+ group ,
153+ )
154+ continue
155+ promoted_qargs = (
156+ group_qargs [locked_in_group [0 ]] if locked_in_group else max (group_qargs .values (), key = _quant_config_rank )
157+ )
158+
139159 logger .debug ("Promoting QKV group %s to shared quantization config %s" , group , promoted_qargs )
140- for module_name in group :
141- override = {key : value for key , value in promoted_qargs .items () if getattr (qcfg , key ) != value }
160+ for name in group :
161+ if name in locked_modules :
162+ continue
163+ override = {k : v for k , v in promoted_qargs .items () if getattr (qcfg , k ) != v }
142164 if override :
143- qcfg .overrides [module_name ] = OliveHfQuantizationOverrideConfig (** override )
165+ qcfg .overrides [name ] = OliveHfQuantizationOverrideConfig (** override )
144166 else :
145- qcfg .overrides .pop (module_name , None )
167+ qcfg .overrides .pop (name , None )
146168
147169 return qcfg
148170
149171
172+ def _collect_excluded_attn_inputs (wrapper : ModelWrapper ) -> set [torch .nn .Module ]:
173+ excluded : set [torch .nn .Module ] = set ()
174+ for layer_wrapper in wrapper .get_layer_wrappers ():
175+ attn_inputs , _ = layer_wrapper .get_attention_inputs ()
176+ if len (attn_inputs ) == 1 :
177+ excluded .add (attn_inputs [0 ])
178+ else :
179+ excluded .update (attn_inputs [:2 ])
180+ return excluded
181+
182+
150183def prepare_model (
151184 model : HfModelHandler ,
152185 config : type [BasePassConfig ],
@@ -168,56 +201,65 @@ def prepare_model(
168201 if existing_qcfg := getattr (model .get_hf_model_config (), "quantization_config" , None ):
169202 if not allow_quantized :
170203 raise ValueError ("Model is already quantized. Cannot quantize again using this pass." )
171- if not isinstance (existing_qcfg , dict ):
172- existing_qcfg = existing_qcfg .to_dict ()
204+ # Always work on a fresh copy: the underlying HF config holds the original object
205+ # (dict or dataclass) and we mutate ``existing_qcfg`` heavily below.
206+ existing_qcfg = deepcopy (existing_qcfg ) if isinstance (existing_qcfg , dict ) else existing_qcfg .to_dict ()
173207 if existing_qcfg .get ("quant_method" , None ) != OliveHfQuantizationMethod .OLIVE :
174208 raise ValueError ("Model has an existing quantization configuration that is not compatible with this pass." )
175209
176210 wrapper = ModelWrapper .from_model (load_hf_base_model (model ))
177211 wrapper .model .eval ()
178212
179- excluded_attn_inputs : set [torch .nn .Module ] = set ()
180- if exclude_attn_inputs :
181- for layer_wrapper in wrapper .get_layer_wrappers ():
182- attn_inputs , _ = layer_wrapper .get_attention_inputs ()
183- if len (attn_inputs ) == 1 :
184- excluded_attn_inputs .add (attn_inputs [0 ])
185- else :
186- excluded_attn_inputs .update (attn_inputs [:2 ])
187-
188- quantizable_attn_input_names : set [str ] | None = None
189- if excluded_attn_inputs :
190- module_to_name = {id (module ): name for name , module in wrapper .model .named_modules ()}
191- excluded_ids = {id (module ) for module in excluded_attn_inputs }
192- quantizable_attn_input_names = set ()
193- for layer_wrapper in wrapper .get_layer_wrappers ():
194- attn_inputs , _ = layer_wrapper .get_attention_inputs ()
195- for module in attn_inputs :
196- if id (module ) in excluded_ids :
197- continue
198- name = module_to_name .get (id (module ))
199- if name is not None :
200- quantizable_attn_input_names .add (name )
201-
202- qcfg = normalize_qkv_quant_config (wrapper , get_quant_config (model , config ), quantizable_attn_input_names )
213+ excluded_attn_inputs = _collect_excluded_attn_inputs (wrapper ) if exclude_attn_inputs else set ()
214+
215+ fresh_qcfg = normalize_qkv_quant_config (wrapper , get_quant_config (model , config ))
203216
204217 originally_tied_embeddings = wrapper .config .tie_word_embeddings
205- if qcfg .lm_head or qcfg .embeds :
218+ if fresh_qcfg .lm_head or fresh_qcfg .embeds :
206219 wrapper .maybe_untie_word_embeddings ()
207220
208221 lm_head_name = wrapper .get_lm_head ()[1 ]
209222 embeds_name = wrapper .get_embeds ()[1 ][0 ]
210- new_qargs : dict [str , dict [str , int | bool ]] = {}
211223
212224 def should_quantize (module : torch .nn .Module , name : str ) -> bool :
213225 if module in excluded_attn_inputs :
214226 return False
215227 if isinstance (module , torch .nn .Linear ):
216- return name != lm_head_name or qcfg .lm_head
217- if qcfg .embeds and isinstance (module , torch .nn .Embedding ):
228+ return name != lm_head_name or fresh_qcfg .lm_head
229+ if fresh_qcfg .embeds and isinstance (module , torch .nn .Embedding ):
218230 return name == embeds_name
219231 return False
220232
233+ # Pre-existing quantized weights are immutable. If we're merging with an existing
234+ # checkpoint, build the final qcfg first (merge fresh into existing, then renormalize
235+ # QKV with already-quantized modules locked) so that the quant_info we attach below
236+ # uses the same settings the on-disk fusion will require. Every module that is already
237+ # a QuantLinear/QuantEmbedding after load is on-disk-immutable, including those that
238+ # used the existing config's defaults (no explicit override entry).
239+ on_disk_overrides : set [str ] = set ()
240+ already_quantized : set [str ] = set ()
241+ if existing_qcfg :
242+ on_disk_overrides = set ((existing_qcfg .get ("overrides" ) or {}).keys ())
243+ already_quantized = {
244+ name for name , module in wrapper .model .named_modules () if isinstance (module , (QuantLinear , QuantEmbedding ))
245+ }
246+ fresh_names = {name for name , module in wrapper .model .named_modules () if should_quantize (module , name )}
247+ merged = existing_qcfg
248+ merged ["overrides" ] = existing_qcfg .get ("overrides" ) or {}
249+ for name in fresh_names :
250+ qargs = fresh_qcfg .get_qlinear_init_args (name )
251+ override = {k : v for k , v in qargs .items () if merged [k ] != v }
252+ if override :
253+ merged ["overrides" ][name ] = override
254+ merged ["lm_head" ] |= fresh_qcfg .lm_head
255+ merged ["embeds" ] |= fresh_qcfg .embeds
256+ qcfg = OliveHfQuantizationConfig (** merged )
257+ qcfg = normalize_qkv_quant_config (wrapper , qcfg , locked_modules = already_quantized )
258+ else :
259+ qcfg = fresh_qcfg
260+
261+ new_qargs : dict [str , dict [str , int | bool ]] = {}
262+
221263 def add_quant_info (module : torch .nn .Module , name : str ) -> torch .nn .Module :
222264 # TODO(jambayk): validate that the module and config are compatible
223265 qargs = qcfg .get_qlinear_init_args (name )
@@ -227,26 +269,15 @@ def add_quant_info(module: torch.nn.Module, name: str) -> torch.nn.Module:
227269
228270 replace_matching_submodules (wrapper .model , should_quantize , add_quant_info , description = "Preparing model" )
229271
230- # remove overrides for modules not being quantized
272+ # Drop overrides for modules that won't be quantized this pass. Pre-existing (on-disk)
273+ # overrides are preserved verbatim since they describe already-quantized weights.
274+ # QKV-group overrides for modules excluded from this pass are not kept: when the
275+ # follow-up pass runs, the quantized members in the group will be locked and pull the
276+ # remaining members back into the shared config via ``normalize_qkv_quant_config``.
231277 for name in list (qcfg .overrides or {}):
232- if name not in new_qargs :
278+ if name not in new_qargs and name not in on_disk_overrides :
233279 qcfg .overrides .pop (name )
234280
235- # merge the new_quant_settings into the existing quant_config
236- if existing_qcfg :
237- merged_qcfg_dict = existing_qcfg
238- merged_qcfg_dict ["overrides" ] = existing_qcfg .get ("overrides" ) or {}
239- for name , qargs in new_qargs .items ():
240- override = {k : v for k , v in qargs .items () if merged_qcfg_dict [k ] != v }
241- if override :
242- merged_qcfg_dict ["overrides" ][name ] = override
243- merged_qcfg_dict ["lm_head" ] |= qcfg .lm_head
244- merged_qcfg_dict ["embeds" ] |= qcfg .embeds
245- qcfg = OliveHfQuantizationConfig (** merged_qcfg_dict )
246- # Re-normalize: the pre-existing overrides we just merged in may violate QKV
247- # consistency on their own, which can break ModelBuilder's GQA fusion.
248- qcfg = normalize_qkv_quant_config (wrapper , qcfg , quantizable_attn_input_names )
249-
250281 word_embeddings_eligible_for_tieing = (
251282 originally_tied_embeddings
252283 and embeds_name in new_qargs
0 commit comments