Skip to content

Commit 77da402

Browse files
authored
Merge pull request #13 from sotanengel/feature/quantization-progress-eta
feat: quantization progress logs with ETA
2 parents c7f98a2 + bc4d900 commit 77da402

7 files changed

Lines changed: 335 additions & 49 deletions

File tree

onecomp/qep/_quantize_with_qep.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def run_quantize_with_qep(
3636
quantizer: Quantizer,
3737
qep_config: QEPConfig,
3838
calibration_config: CalibrationConfig,
39+
*,
40+
quantization_progress: bool = True,
3941
):
4042
"""Run quantization with Quantization Error Propagation (QEP).
4143
@@ -51,6 +53,7 @@ def run_quantize_with_qep(
5153
qep_config (QEPConfig): Configuration for QEP
5254
(percdamp, perccorr, exclude_layer_keywords).
5355
calibration_config (CalibrationConfig): Calibration parameters.
56+
quantization_progress (bool): When True, log ``[progress]`` with ETA per layer.
5457
5558
"""
5659
model = model_config.load_model()
@@ -80,6 +83,17 @@ def run_quantize_with_qep(
8083

8184
logger.info("Quantizing the model using %s", quantizer.name)
8285

86+
progress = None
87+
if quantization_progress:
88+
# pylint: disable-next=import-outside-toplevel
89+
from onecomp.utils.quantization_progress import QuantizationProgressTracker
90+
91+
progress = QuantizationProgressTracker(
92+
logger,
93+
len(quantizer.module_to_name),
94+
"QEP quantization (general, per layer)",
95+
)
96+
8397
# 2. For each target layer, perform the following sequentially
8498
for module, name in quantizer.module_to_name.items():
8599

@@ -114,6 +128,8 @@ def run_quantize_with_qep(
114128

115129
# 2-4. Free memory
116130
del quant_input_activation
131+
if progress is not None:
132+
progress.step_complete(name)
117133

118134
del original_input_activations
119135
quantizer.execute_post_processing()

onecomp/qep/_quantize_with_qep_arch.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ def compute_hessian_and_crossterm(
143143
def make_hook(name):
144144
def hook(module, inp, out):
145145
dest[name] = inp[0] if isinstance(inp, tuple) else inp
146+
146147
return hook
147148

148149
handlers = [
@@ -213,6 +214,7 @@ def _compute_per_module_hessians(
213214
def _make_hook(key):
214215
def hook(_, inp, __):
215216
dest[key] = inp[0] if isinstance(inp, tuple) else inp
217+
216218
return hook
217219

218220
handlers = [m.register_forward_hook(_make_hook(i)) for i, m in enumerate(modules)]
@@ -251,10 +253,7 @@ def hook(_, inp, __):
251253
for h in handlers:
252254
h.remove()
253255

254-
return {
255-
modules[i]: (hessians[i] if nsamples[i] > 0 else None)
256-
for i in range(len(modules))
257-
}
256+
return {modules[i]: (hessians[i] if nsamples[i] > 0 else None) for i in range(len(modules))}
258257

259258

260259
@torch.no_grad()
@@ -263,6 +262,8 @@ def run_quantize_with_qep_arch(
263262
quantizer: Quantizer,
264263
qep_config: QEPConfig,
265264
calibration_config: CalibrationConfig,
265+
*,
266+
quantization_progress: bool = True,
266267
):
267268
"""Run architecture-aware quantization with QEP.
268269
@@ -279,6 +280,7 @@ def run_quantize_with_qep_arch(
279280
qep_config (QEPConfig): Configuration for QEP
280281
(percdamp, perccorr, exclude_layer_keywords).
281282
calibration_config (CalibrationConfig): Calibration parameters.
283+
quantization_progress (bool): When True, log ``[progress]`` with ETA per target layer.
282284
283285
"""
284286

@@ -318,6 +320,17 @@ def run_quantize_with_qep_arch(
318320
name for module, name in quantizer.module_to_name.items() if module in block_modules
319321
}
320322

323+
progress = None
324+
if quantization_progress:
325+
# pylint: disable-next=import-outside-toplevel
326+
from onecomp.utils.quantization_progress import QuantizationProgressTracker
327+
328+
progress = QuantizationProgressTracker(
329+
logger,
330+
len(remaining_targets),
331+
"QEP quantization (architecture-aware)",
332+
)
333+
321334
# 2. For each target transformer block, perform the following sequentially
322335
for block_idx, block in enumerate(blocks):
323336

@@ -365,9 +378,7 @@ def run_quantize_with_qep_arch(
365378
targets = [m for m in group_q if m in quantizer.module_to_name]
366379
if not targets:
367380
continue
368-
is_expert = any(
369-
".experts." in quantizer.module_to_name[m] for m in targets
370-
)
381+
is_expert = any(".experts." in quantizer.module_to_name[m] for m in targets)
371382
if is_expert:
372383
expert_modules_q.extend(targets)
373384
else:
@@ -442,6 +453,8 @@ def run_quantize_with_qep_arch(
442453
name,
443454
)
444455
remaining_targets.discard(name)
456+
if progress is not None:
457+
progress.step_complete(name)
445458

446459
# 4. Process MoE expert layers with per-module Hessians (no cross-term)
447460
if expert_modules_q:
@@ -451,7 +464,12 @@ def run_quantize_with_qep_arch(
451464
len(expert_modules_q),
452465
)
453466
expert_hessians = _compute_per_module_hessians(
454-
block_q, expert_modules_q, inps_q, kwargs, batch_size, device,
467+
block_q,
468+
expert_modules_q,
469+
inps_q,
470+
kwargs,
471+
batch_size,
472+
device,
455473
)
456474
for module_q in expert_modules_q:
457475
name = quantizer.module_to_name[module_q]
@@ -462,6 +480,8 @@ def run_quantize_with_qep_arch(
462480
name,
463481
)
464482
remaining_targets.discard(name)
483+
if progress is not None:
484+
progress.step_complete(f"{name} (skipped, no tokens)")
465485
continue
466486

467487
logger.info(
@@ -489,6 +509,8 @@ def run_quantize_with_qep_arch(
489509
name,
490510
)
491511
remaining_targets.discard(name)
512+
if progress is not None:
513+
progress.step_complete(name)
492514

493515
# forward input to the next block
494516
inps_q = forward_input(inps_q, block_q, kwargs, batch_size, device)

onecomp/runner.py

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def __init__(
8686
multi_gpu=False,
8787
gpu_ids=None,
8888
post_processes=None,
89+
quantization_progress=True,
8990
):
9091
"""__init__ method
9192
@@ -130,6 +131,11 @@ def __init__(
130131
a quantized model on CPU (built via
131132
``create_quantized_model``) and may modify it in-place.
132133
Processes are executed in order. Default is None.
134+
quantization_progress (bool):
135+
When ``True`` (default), emit ``[progress]`` log lines with
136+
completed steps, elapsed time, and a linear ETA estimate
137+
during long quantization (calibration, chunked, multi-GPU,
138+
QEP). Set to ``False`` for quiet runs (e.g. CI).
133139
134140
Note:
135141
For zero-config quantization (VRAM auto-estimation +
@@ -215,6 +221,7 @@ def __init__(
215221
self.lpcd_config = None
216222
if lpcd:
217223
self.lpcd_config = lpcd_config if lpcd_config is not None else LPCDConfig()
224+
self.quantization_progress = quantization_progress
218225

219226
def check(self):
220227
"""Check the settings
@@ -323,27 +330,19 @@ def _exclude_moe_router_if_needed(self):
323330
config = self.model_config.load_config()
324331
num_experts = (
325332
getattr(config, "num_experts", 0)
326-
or getattr(
327-
getattr(config, "text_config", None), "num_experts", 0
328-
) or
329-
0
333+
or getattr(getattr(config, "text_config", None), "num_experts", 0)
334+
or 0
330335
)
331336
if num_experts == 0:
332337
return
333338

334339
keyword = "router"
335-
target_quantizers = (
336-
self.quantizers
337-
if self.quantizers is not None
338-
else [self.quantizer]
339-
)
340+
target_quantizers = self.quantizers if self.quantizers is not None else [self.quantizer]
340341
for q in target_quantizers:
341342
if q.exclude_layer_keywords is None:
342343
q.exclude_layer_keywords = [keyword]
343344
elif keyword not in q.exclude_layer_keywords:
344-
q.exclude_layer_keywords = list(q.exclude_layer_keywords) + [
345-
keyword
346-
]
345+
q.exclude_layer_keywords = list(q.exclude_layer_keywords) + [keyword]
347346

348347
self.logger.info(
349348
"MoE model (num_experts=%d): excluding '%s' layers from "
@@ -520,12 +519,9 @@ def auto_run(
520519
uniform_bit = max(valid_wbits)
521520
if save_dir == "auto":
522521
model_name = model_id.rstrip("/").split("/")[-1]
523-
save_dir = (
524-
f"{model_name}-gptq-{uniform_bit}bit"
525-
)
522+
save_dir = f"{model_name}-gptq-{uniform_bit}bit"
526523
logger.warning(
527-
"Gemma 4 detected → falling back to uniform GPTQ %d-bit "
528-
"(target wbits=%.2f)",
524+
"Gemma 4 detected → falling back to uniform GPTQ %d-bit " "(target wbits=%.2f)",
529525
uniform_bit,
530526
wbits,
531527
)
@@ -536,6 +532,7 @@ def auto_run(
536532
save_dir = f"{model_name}-autobit-{wbits}bit"
537533

538534
from .quantizer.autobit import AutoBitQuantizer
535+
539536
candidate_quantizers = [
540537
GPTQ(wbits=b, groupsize=groupsize, **kwargs) for b in candidate_bits
541538
]
@@ -604,8 +601,30 @@ def quantize_with_calibration(self):
604601

605602
# Register hooks to all linear layers
606603
handles = []
604+
progress = None
605+
if self.quantization_progress:
606+
# pylint: disable-next=import-outside-toplevel
607+
from .utils.quantization_progress import QuantizationProgressTracker
608+
609+
progress = QuantizationProgressTracker(
610+
logger,
611+
len(self.quantizer.module_to_name),
612+
"Calibration quantization layers",
613+
)
614+
615+
if progress:
616+
quantize_bound = self.quantizer.quantize
617+
618+
def _quantize_hook(module, input, output): # pylint: disable=redefined-builtin
619+
quantize_bound(module, input, output)
620+
progress.step_complete(self.quantizer.module_to_name[module])
621+
622+
hook_fn = _quantize_hook
623+
else:
624+
hook_fn = self.quantizer.quantize
625+
607626
for module in self.quantizer.module_to_name.keys():
608-
handle = module.register_forward_hook(self.quantizer.quantize)
627+
handle = module.register_forward_hook(hook_fn)
609628
handles.append(handle)
610629

611630
logger.info("Quantizing the model using %s", self.quantizer.name)
@@ -645,6 +664,7 @@ def quantize_with_calibration_chunked(self):
645664
model_config=self.model_config,
646665
quantizers=self.quantizers if self.quantizers is not None else [self.quantizer],
647666
calibration_config=self.calibration_config,
667+
quantization_progress=self.quantization_progress,
648668
)
649669

650670
def quantize_with_calibration_on_multi_gpu(self):
@@ -673,6 +693,7 @@ def quantize_with_calibration_on_multi_gpu(self):
673693
quantizer=self.quantizer,
674694
calibration_config=self.calibration_config,
675695
gpu_ids=self.gpu_ids,
696+
quantization_progress=self.quantization_progress,
676697
)
677698

678699
# Store results in quantizer.results
@@ -699,8 +720,20 @@ def quantize_without_calibration(self):
699720
"Quantizing the model without calibration using %s",
700721
self.quantizer.name,
701722
)
723+
progress = None
724+
if self.quantization_progress:
725+
# pylint: disable-next=import-outside-toplevel
726+
from .utils.quantization_progress import QuantizationProgressTracker
727+
728+
progress = QuantizationProgressTracker(
729+
logger,
730+
len(self.quantizer.module_to_name),
731+
"Quantization without calibration (layers)",
732+
)
702733
for module in self.quantizer.module_to_name.keys():
703734
self.quantizer.quantize(module, None, None)
735+
if progress:
736+
progress.step_complete(self.quantizer.module_to_name[module])
704737

705738
self.quantizer.execute_post_processing()
706739

@@ -721,6 +754,7 @@ def quantize_with_qep(self):
721754
quantizer=self.quantizer,
722755
qep_config=self.qep_config,
723756
calibration_config=self.calibration_config,
757+
quantization_progress=self.quantization_progress,
724758
)
725759

726760
if self.qep_config.general:
@@ -868,7 +902,7 @@ def prepare_calibration_dataset(self, device, model=None):
868902
869903
Args:
870904
device (torch.device): Device to place tensors on (CPU or GPU)
871-
model: Model instance (optional). Add model-specific fields
905+
model: Model instance (optional). Add model-specific fields
872906
(e.g. mm_token_type_ids for Gemma 4).
873907
874908
Returns:
@@ -992,10 +1026,7 @@ def save_quantization_statistics(self, path: str, quantizer=None):
9921026

9931027
logger.info("Saving the quantization statistics to %s", path)
9941028

995-
statistics = {
996-
key: result.get_statistics()
997-
for key, result in quantizer.results.items()
998-
}
1029+
statistics = {key: result.get_statistics() for key, result in quantizer.results.items()}
9991030

10001031
with open(path, "w", encoding="utf-8") as f:
10011032
json.dump(statistics, f, indent=4)
@@ -1674,15 +1705,10 @@ def create_quantized_model(self, pack_weights: bool = True, quantizer=None, use_
16741705
# cf) https://docs.vllm.ai/en/stable/features/quantization/#implementing-a-quantized-moe-method
16751706
num_experts = (
16761707
getattr(model.config, "num_experts", None)
1677-
or getattr(
1678-
getattr(model.config, "text_config", None), "num_experts", None
1679-
)
1708+
or getattr(getattr(model.config, "text_config", None), "num_experts", None)
16801709
or 0
16811710
)
1682-
if (
1683-
quant_config.get("quant_method") == "gptq"
1684-
and num_experts > 0
1685-
):
1711+
if quant_config.get("quant_method") == "gptq" and num_experts > 0:
16861712
quant_config["quant_method"] = "mixed_gptq"
16871713
self.logger.info(
16881714
"MoE model detected (num_experts=%d): "
@@ -1706,15 +1732,13 @@ def _patch_k_eq_v_for_vllm(self, model, quant_config: dict) -> None:
17061732
Gemma4 full-attention layers with attention_k_eq_v=True have no
17071733
v_proj weight — the model reuses key states as value states.
17081734
vLLM fuses q/k/v into a single qkv_proj and requires all shards
1709-
to share the same quantization status.
1735+
to share the same quantization status.
17101736
"""
17111737
text_cfg = getattr(model.config, "text_config", None)
17121738
if text_cfg is None or not getattr(text_cfg, "attention_k_eq_v", False):
17131739
return
17141740
layer_types = getattr(text_cfg, "layer_types", [])
1715-
k_eq_v_indices = {
1716-
i for i, lt in enumerate(layer_types) if lt == "full_attention"
1717-
}
1741+
k_eq_v_indices = {i for i, lt in enumerate(layer_types) if lt == "full_attention"}
17181742
if not k_eq_v_indices:
17191743
return
17201744

@@ -1752,9 +1776,7 @@ def _patch_k_eq_v_for_vllm(self, model, quant_config: dict) -> None:
17521776
and "self_attn.k_proj" in layer_cfg
17531777
and "self_attn.v_proj" not in layer_cfg
17541778
):
1755-
layer_cfg["self_attn.v_proj"] = copy.deepcopy(
1756-
layer_cfg["self_attn.k_proj"]
1757-
)
1779+
layer_cfg["self_attn.v_proj"] = copy.deepcopy(layer_cfg["self_attn.k_proj"])
17581780

17591781
for key in ("modules_in_block_to_quantize", "quantized_layer_names"):
17601782
names = quant_config.get(key, [])
@@ -1825,6 +1847,7 @@ def save_quantized_model(self, save_directory: str, pack_weights: bool = True):
18251847
if src_dir and not os.path.isdir(src_dir):
18261848
# when the model_id is specified, the path is modifed to the local directory
18271849
from huggingface_hub import snapshot_download
1850+
18281851
src_dir = snapshot_download(src_dir, local_files_only=True)
18291852
if src_dir and os.path.isdir(src_dir):
18301853
for fname in ("processor_config.json", "preprocessor_config.json"):

0 commit comments

Comments
 (0)