Skip to content

Commit 085e522

Browse files
jambaykCopilot
andcommitted
selective_mixed_precision: holistic QKV-aware refactor
- Refactor scoring algorithms into per-algorithm strategy classes that own module stats collection, group aggregation, and scoring. - Make QKV-aware grouping the default behavior (no opt-in config). - Rewrite prepare_model / normalize_qkv_quant_config in quant_utils with defensive deepcopy of existing quantization configs and locked-override semantics for already-quantized members. - Drop overrides for modules that won't be quantized; preserve pre-existing on-disk overrides verbatim. - Split tests: move quant_utils tests into test_quant_utils.py; keep SMP behavior tests in test_selective_mixed_precision.py. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8e98a92 commit 085e522

4 files changed

Lines changed: 1849 additions & 900 deletions

File tree

olive/passes/pytorch/quant_utils.py

Lines changed: 88 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from __future__ import annotations
66

77
import logging
8+
from copy import deepcopy
89
from dataclasses import dataclass
910
from typing import TYPE_CHECKING, Callable
1011

@@ -123,30 +124,62 @@ def _quant_config_rank(qargs: dict[str, int | bool]) -> tuple[int, int, int]:
123124
def 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+
150183
def 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

Comments
 (0)