Skip to content

Commit bc4d900

Browse files
feat: log quantization progress with linear ETA
Add QuantizationProgressTracker and wire it through calibration, chunked calibration, multi-GPU phase 2, QEP general and arch-aware paths. Runner gains quantization_progress flag (default on). Includes unit tests for ETA formatting and thread-safe stepping. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6526c0f commit bc4d900

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
@@ -314,27 +321,19 @@ def _exclude_moe_router_if_needed(self):
314321
config = self.model_config.load_config()
315322
num_experts = (
316323
getattr(config, "num_experts", 0)
317-
or getattr(
318-
getattr(config, "text_config", None), "num_experts", 0
319-
) or
320-
0
324+
or getattr(getattr(config, "text_config", None), "num_experts", 0)
325+
or 0
321326
)
322327
if num_experts == 0:
323328
return
324329

325330
keyword = "router"
326-
target_quantizers = (
327-
self.quantizers
328-
if self.quantizers is not None
329-
else [self.quantizer]
330-
)
331+
target_quantizers = self.quantizers if self.quantizers is not None else [self.quantizer]
331332
for q in target_quantizers:
332333
if q.exclude_layer_keywords is None:
333334
q.exclude_layer_keywords = [keyword]
334335
elif keyword not in q.exclude_layer_keywords:
335-
q.exclude_layer_keywords = list(q.exclude_layer_keywords) + [
336-
keyword
337-
]
336+
q.exclude_layer_keywords = list(q.exclude_layer_keywords) + [keyword]
338337

339338
self.logger.info(
340339
"MoE model (num_experts=%d): excluding '%s' layers from "
@@ -511,12 +510,9 @@ def auto_run(
511510
uniform_bit = max(valid_wbits)
512511
if save_dir == "auto":
513512
model_name = model_id.rstrip("/").split("/")[-1]
514-
save_dir = (
515-
f"{model_name}-gptq-{uniform_bit}bit"
516-
)
513+
save_dir = f"{model_name}-gptq-{uniform_bit}bit"
517514
logger.warning(
518-
"Gemma 4 detected → falling back to uniform GPTQ %d-bit "
519-
"(target wbits=%.2f)",
515+
"Gemma 4 detected → falling back to uniform GPTQ %d-bit " "(target wbits=%.2f)",
520516
uniform_bit,
521517
wbits,
522518
)
@@ -527,6 +523,7 @@ def auto_run(
527523
save_dir = f"{model_name}-autobit-{wbits}bit"
528524

529525
from .quantizer.autobit import AutoBitQuantizer
526+
530527
candidate_quantizers = [
531528
GPTQ(wbits=b, groupsize=groupsize, **kwargs) for b in candidate_bits
532529
]
@@ -595,8 +592,30 @@ def quantize_with_calibration(self):
595592

596593
# Register hooks to all linear layers
597594
handles = []
595+
progress = None
596+
if self.quantization_progress:
597+
# pylint: disable-next=import-outside-toplevel
598+
from .utils.quantization_progress import QuantizationProgressTracker
599+
600+
progress = QuantizationProgressTracker(
601+
logger,
602+
len(self.quantizer.module_to_name),
603+
"Calibration quantization layers",
604+
)
605+
606+
if progress:
607+
quantize_bound = self.quantizer.quantize
608+
609+
def _quantize_hook(module, input, output): # pylint: disable=redefined-builtin
610+
quantize_bound(module, input, output)
611+
progress.step_complete(self.quantizer.module_to_name[module])
612+
613+
hook_fn = _quantize_hook
614+
else:
615+
hook_fn = self.quantizer.quantize
616+
598617
for module in self.quantizer.module_to_name.keys():
599-
handle = module.register_forward_hook(self.quantizer.quantize)
618+
handle = module.register_forward_hook(hook_fn)
600619
handles.append(handle)
601620

602621
logger.info("Quantizing the model using %s", self.quantizer.name)
@@ -636,6 +655,7 @@ def quantize_with_calibration_chunked(self):
636655
model_config=self.model_config,
637656
quantizers=self.quantizers if self.quantizers is not None else [self.quantizer],
638657
calibration_config=self.calibration_config,
658+
quantization_progress=self.quantization_progress,
639659
)
640660

641661
def quantize_with_calibration_on_multi_gpu(self):
@@ -664,6 +684,7 @@ def quantize_with_calibration_on_multi_gpu(self):
664684
quantizer=self.quantizer,
665685
calibration_config=self.calibration_config,
666686
gpu_ids=self.gpu_ids,
687+
quantization_progress=self.quantization_progress,
667688
)
668689

669690
# Store results in quantizer.results
@@ -690,8 +711,20 @@ def quantize_without_calibration(self):
690711
"Quantizing the model without calibration using %s",
691712
self.quantizer.name,
692713
)
714+
progress = None
715+
if self.quantization_progress:
716+
# pylint: disable-next=import-outside-toplevel
717+
from .utils.quantization_progress import QuantizationProgressTracker
718+
719+
progress = QuantizationProgressTracker(
720+
logger,
721+
len(self.quantizer.module_to_name),
722+
"Quantization without calibration (layers)",
723+
)
693724
for module in self.quantizer.module_to_name.keys():
694725
self.quantizer.quantize(module, None, None)
726+
if progress:
727+
progress.step_complete(self.quantizer.module_to_name[module])
695728

696729
self.quantizer.execute_post_processing()
697730

@@ -712,6 +745,7 @@ def quantize_with_qep(self):
712745
quantizer=self.quantizer,
713746
qep_config=self.qep_config,
714747
calibration_config=self.calibration_config,
748+
quantization_progress=self.quantization_progress,
715749
)
716750

717751
if self.qep_config.general:
@@ -859,7 +893,7 @@ def prepare_calibration_dataset(self, device, model=None):
859893
860894
Args:
861895
device (torch.device): Device to place tensors on (CPU or GPU)
862-
model: Model instance (optional). Add model-specific fields
896+
model: Model instance (optional). Add model-specific fields
863897
(e.g. mm_token_type_ids for Gemma 4).
864898
865899
Returns:
@@ -983,10 +1017,7 @@ def save_quantization_statistics(self, path: str, quantizer=None):
9831017

9841018
logger.info("Saving the quantization statistics to %s", path)
9851019

986-
statistics = {
987-
key: result.get_statistics()
988-
for key, result in quantizer.results.items()
989-
}
1020+
statistics = {key: result.get_statistics() for key, result in quantizer.results.items()}
9901021

9911022
with open(path, "w", encoding="utf-8") as f:
9921023
json.dump(statistics, f, indent=4)
@@ -1665,15 +1696,10 @@ def create_quantized_model(self, pack_weights: bool = True, quantizer=None, use_
16651696
# cf) https://docs.vllm.ai/en/stable/features/quantization/#implementing-a-quantized-moe-method
16661697
num_experts = (
16671698
getattr(model.config, "num_experts", None)
1668-
or getattr(
1669-
getattr(model.config, "text_config", None), "num_experts", None
1670-
)
1699+
or getattr(getattr(model.config, "text_config", None), "num_experts", None)
16711700
or 0
16721701
)
1673-
if (
1674-
quant_config.get("quant_method") == "gptq"
1675-
and num_experts > 0
1676-
):
1702+
if quant_config.get("quant_method") == "gptq" and num_experts > 0:
16771703
quant_config["quant_method"] = "mixed_gptq"
16781704
self.logger.info(
16791705
"MoE model detected (num_experts=%d): "
@@ -1697,15 +1723,13 @@ def _patch_k_eq_v_for_vllm(self, model, quant_config: dict) -> None:
16971723
Gemma4 full-attention layers with attention_k_eq_v=True have no
16981724
v_proj weight — the model reuses key states as value states.
16991725
vLLM fuses q/k/v into a single qkv_proj and requires all shards
1700-
to share the same quantization status.
1726+
to share the same quantization status.
17011727
"""
17021728
text_cfg = getattr(model.config, "text_config", None)
17031729
if text_cfg is None or not getattr(text_cfg, "attention_k_eq_v", False):
17041730
return
17051731
layer_types = getattr(text_cfg, "layer_types", [])
1706-
k_eq_v_indices = {
1707-
i for i, lt in enumerate(layer_types) if lt == "full_attention"
1708-
}
1732+
k_eq_v_indices = {i for i, lt in enumerate(layer_types) if lt == "full_attention"}
17091733
if not k_eq_v_indices:
17101734
return
17111735

@@ -1743,9 +1767,7 @@ def _patch_k_eq_v_for_vllm(self, model, quant_config: dict) -> None:
17431767
and "self_attn.k_proj" in layer_cfg
17441768
and "self_attn.v_proj" not in layer_cfg
17451769
):
1746-
layer_cfg["self_attn.v_proj"] = copy.deepcopy(
1747-
layer_cfg["self_attn.k_proj"]
1748-
)
1770+
layer_cfg["self_attn.v_proj"] = copy.deepcopy(layer_cfg["self_attn.k_proj"])
17491771

17501772
for key in ("modules_in_block_to_quantize", "quantized_layer_names"):
17511773
names = quant_config.get(key, [])
@@ -1816,6 +1838,7 @@ def save_quantized_model(self, save_directory: str, pack_weights: bool = True):
18161838
if src_dir and not os.path.isdir(src_dir):
18171839
# when the model_id is specified, the path is modifed to the local directory
18181840
from huggingface_hub import snapshot_download
1841+
18191842
src_dir = snapshot_download(src_dir, local_files_only=True)
18201843
if src_dir and os.path.isdir(src_dir):
18211844
for fname in ("processor_config.json", "preprocessor_config.json"):

0 commit comments

Comments
 (0)