Skip to content

Commit 04ef7d2

Browse files
jambaykhanbitmythsCopilot
authored
selective_mixed_precision: QKV-aware overrides, AUTO memory mode, MULTI_GPU dispatch (microsoft#2475)
## Describe your changes Based on microsoft#2473 ## Checklist before requesting a review - [ ] Add unit tests for this change. - [ ] Make sure all tests can pass. - [ ] Update documents if necessary. - [ ] Lint and apply fixes to your code by running `lintrunner -a` - [ ] Is this a user-facing change? If yes, give a description of this change to be included in the release notes. ## (Optional) Issue link --------- Co-authored-by: Sunghoon Choi <sunghcho@microsoft.com> Co-authored-by: Sunghoon Choi <35605090+hanbitmyths@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c9e4b40 commit 04ef7d2

4 files changed

Lines changed: 2341 additions & 227 deletions

File tree

olive/passes/pytorch/quant_utils.py

Lines changed: 147 additions & 30 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

@@ -14,6 +15,7 @@
1415
from olive.common.quant.hf_utils import (
1516
OliveHfQuantizationConfig,
1617
OliveHfQuantizationMethod,
18+
OliveHfQuantizationOverrideConfig,
1719
replace_matching_submodules,
1820
tie_quant_word_embeddings,
1921
)
@@ -78,6 +80,106 @@ def get_quantizer_config(allow_embeds: bool = False) -> dict[str, PassConfigPara
7880
}
7981

8082

83+
def get_qkv_quantization_groups(wrapper: ModelWrapper, module_names: set[str] | None = None) -> list[tuple[str, ...]]:
84+
"""Get attention input projection groups that must share quantization settings.
85+
86+
Names are resolved from ``wrapper.model.named_modules()`` to stay correct for any layer
87+
container (``ModuleList``, ``ModuleDict``, custom containers) and for unpacked QKV
88+
submodules. When ``module_names`` is provided, attention inputs not in the set are
89+
dropped from the group. Groups with fewer than two members are skipped.
90+
"""
91+
module_to_name = {id(module): name for name, module in wrapper.model.named_modules()}
92+
qkv_groups = []
93+
for layer_wrapper in wrapper.get_layer_wrappers():
94+
attn_inputs, _ = layer_wrapper.get_attention_inputs()
95+
group = tuple(
96+
name
97+
for name in (module_to_name.get(id(module)) for module in attn_inputs)
98+
if name is not None and (module_names is None or name in module_names)
99+
)
100+
if len(group) > 1:
101+
qkv_groups.append(group)
102+
return qkv_groups
103+
104+
105+
def _quant_config_rank(qargs: dict[str, int | bool]) -> tuple[int, int, int]:
106+
"""Rank quantization configs by precision; higher rank means more precise.
107+
108+
Ordering: higher ``bits`` wins; among equal bits, smaller positive ``group_size`` wins;
109+
per-channel (``-1``) wins over per-tensor (``0``) but loses to positive group sizes.
110+
``symmetric`` is intentionally not part of the ordering since it is a representation
111+
choice rather than a strict precision axis.
112+
"""
113+
bits = qargs["bits"].value if hasattr(qargs["bits"], "value") else qargs["bits"]
114+
group_size = qargs["group_size"]
115+
if group_size > 0:
116+
group_size_rank = (2, -group_size)
117+
elif group_size == -1:
118+
group_size_rank = (1, 0)
119+
else:
120+
group_size_rank = (0, 0)
121+
return bits, *group_size_rank
122+
123+
124+
def normalize_qkv_quant_config(
125+
wrapper: ModelWrapper,
126+
qcfg: OliveHfQuantizationConfig,
127+
locked_modules: set[str] | None = None,
128+
) -> OliveHfQuantizationConfig:
129+
"""Promote split QKV projection overrides to one shared quantization config.
130+
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.
139+
"""
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}
143+
if len({tuple(qargs.items()) for qargs in group_qargs.values()}) == 1:
144+
continue
145+
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+
159+
logger.debug("Promoting QKV group %s to shared quantization config %s", group, promoted_qargs)
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}
164+
if override:
165+
qcfg.overrides[name] = OliveHfQuantizationOverrideConfig(**override)
166+
else:
167+
qcfg.overrides.pop(name, None)
168+
169+
return qcfg
170+
171+
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+
81183
def prepare_model(
82184
model: HfModelHandler,
83185
config: type[BasePassConfig],
@@ -99,42 +201,65 @@ def prepare_model(
99201
if existing_qcfg := getattr(model.get_hf_model_config(), "quantization_config", None):
100202
if not allow_quantized:
101203
raise ValueError("Model is already quantized. Cannot quantize again using this pass.")
102-
if not isinstance(existing_qcfg, dict):
103-
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()
104207
if existing_qcfg.get("quant_method", None) != OliveHfQuantizationMethod.OLIVE:
105208
raise ValueError("Model has an existing quantization configuration that is not compatible with this pass.")
106209

107210
wrapper = ModelWrapper.from_model(load_hf_base_model(model))
108211
wrapper.model.eval()
109212

110-
qcfg = get_quant_config(model, config)
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))
111216

112217
originally_tied_embeddings = wrapper.config.tie_word_embeddings
113-
if qcfg.lm_head or qcfg.embeds:
218+
if fresh_qcfg.lm_head or fresh_qcfg.embeds:
114219
wrapper.maybe_untie_word_embeddings()
115220

116221
lm_head_name = wrapper.get_lm_head()[1]
117222
embeds_name = wrapper.get_embeds()[1][0]
118-
new_qargs: dict[str, dict[str, int | bool]] = {}
119-
120-
excluded_attn_inputs: set[torch.nn.Module] = set()
121-
if exclude_attn_inputs:
122-
for layer_wrapper in wrapper.get_layer_wrappers():
123-
attn_inputs, _ = layer_wrapper.get_attention_inputs()
124-
if len(attn_inputs) == 1:
125-
excluded_attn_inputs.add(attn_inputs[0])
126-
else:
127-
excluded_attn_inputs.update(attn_inputs[:2])
128223

129224
def should_quantize(module: torch.nn.Module, name: str) -> bool:
130225
if module in excluded_attn_inputs:
131226
return False
132227
if isinstance(module, torch.nn.Linear):
133-
return name != lm_head_name or qcfg.lm_head
134-
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):
135230
return name == embeds_name
136231
return False
137232

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+
138263
def add_quant_info(module: torch.nn.Module, name: str) -> torch.nn.Module:
139264
# TODO(jambayk): validate that the module and config are compatible
140265
qargs = qcfg.get_qlinear_init_args(name)
@@ -144,23 +269,15 @@ def add_quant_info(module: torch.nn.Module, name: str) -> torch.nn.Module:
144269

145270
replace_matching_submodules(wrapper.model, should_quantize, add_quant_info, description="Preparing model")
146271

147-
# 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``.
148277
for name in list(qcfg.overrides or {}):
149-
if name not in new_qargs:
278+
if name not in new_qargs and name not in on_disk_overrides:
150279
qcfg.overrides.pop(name)
151280

152-
# merge the new_quant_settings into the existing quant_config
153-
if existing_qcfg:
154-
merged_qcfg_dict = existing_qcfg
155-
merged_qcfg_dict["overrides"] = existing_qcfg.get("overrides") or {}
156-
for name, qargs in new_qargs.items():
157-
override = {k: v for k, v in qargs.items() if merged_qcfg_dict[k] != v}
158-
if override:
159-
merged_qcfg_dict["overrides"][name] = override
160-
merged_qcfg_dict["lm_head"] |= qcfg.lm_head
161-
merged_qcfg_dict["embeds"] |= qcfg.embeds
162-
qcfg = OliveHfQuantizationConfig(**merged_qcfg_dict)
163-
164281
word_embeddings_eligible_for_tieing = (
165282
originally_tied_embeddings
166283
and embeds_name in new_qargs

0 commit comments

Comments
 (0)