Files
wehub-resource-sync e904b667c6
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:59:26 +08:00

322 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""QDQ quantization debug: align with ONNX Runtime ``qdq_loss_debug`` (official example flow).
This mirrors the *Debugging* section of:
https://github.com/microsoft/onnxruntime-inference-examples/blob/main/quantization/image_classification/cpu/ReadMe.md
Workflow:
1. Augment float32 and QDQ models to expose intermediate tensors
(``modify_model_output_intermediate_tensors``).
2. Run both with the same calibration ``.npy`` inputs
(``collect_activations``), matching ``quantize_onnx_model`` / ``build_onnx_calib_npy`` data.
3. ``create_activation_matching`` + SQNR; ``create_weight_matching`` + per-weight SQNR.
4. Print a report and optionally write JSON (scalar metrics only).
*float* should be the **same** float graph you quantized (ideally the ORT
``quant_pre_process`` output if you used ``--ort-preprocess`` in ``quantize_onnx_model.py``),
or the graph is unlikely to line-tensor match the QDQ model.
Example:
python3 scripts/debug_onnx_qdq.py \\
--float-model /path/to/float_infer.onnx \\
--qdq-model /path/to/inference_int8.onnx \\
--calib-data-dir /path/to/calib_npy \\
--max-samples 3 \\
--json-report qdq_debug.json
"""
from __future__ import annotations
import argparse
import json
import logging
import sys
import tempfile
from pathlib import Path
from typing import Any
_script_dir = Path(__file__).resolve().parent
if str(_script_dir) not in sys.path:
sys.path.insert(0, str(_script_dir))
from utils import build_npy_dir_reader, die
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def _parse_op_types(s: str | None) -> list[str] | None:
if not s or not s.strip():
return None
return [t.strip() for t in s.split(",") if t.strip()]
def _safe_compute_weight_error(
wmatch: dict[str, dict[str, Any]],
err_func: Any,
) -> dict[str, float]:
"""Like ORT ``compute_weight_error``, but coerce scalars to 0-d arrays.
``create_weight_matching`` can set ``dequantized`` to a bare ``numpy.float32``; ORT
``compute_signal_to_quantization_noice_ratio`` then does ``len(ylist)`` on that scalar
and raises ``TypeError``.
"""
import numpy as np
out: dict[str, float] = {}
for name, m in wmatch.items():
try:
xf = np.asarray(m["float"])
yd = np.asarray(m["dequantized"])
except (KeyError, TypeError, ValueError) as e:
logger.warning("weight SQNR skip %r: %s", name, e)
continue
if xf.size == 0 and yd.size == 0:
continue
if xf.size != yd.size:
logger.warning(
"weight SQNR skip %r: size mismatch %s vs %s",
name,
getattr(xf, "shape", xf),
getattr(yd, "shape", yd),
)
continue
try:
out[name] = float(err_func(xf, yd))
except (TypeError, ValueError, RuntimeError) as e:
logger.warning("weight SQNR skip %r: %s", name, e)
return out
def _safe_compute_activation_error(
match: dict[str, dict[str, Any]],
err_func: Any,
) -> dict[str, dict[str, float]]:
"""Like ORT ``compute_activation_error``, but skip missing *float* entries (avoids KeyError)."""
result: dict[str, dict[str, float]] = {}
for name, m in match.items():
e: dict[str, float] = {
"qdq_err": float(err_func(m["pre_qdq"], m["post_qdq"])),
}
if m.get("float"):
e["xmodel_err"] = float(err_func(m["float"], m["post_qdq"]))
result[name] = e
return result
def _sqnr_key(entry: dict[str, float], prefer_xmodel: bool) -> float:
if prefer_xmodel and "xmodel_err" in entry:
return entry["xmodel_err"]
return entry.get("qdq_err", float("nan"))
def _run(
float_model: Path,
qdq_model: Path,
calib: Path,
max_samples: int,
op_types: list[str] | None,
json_report: Path | None,
top_n: int,
skip_weights: bool,
skip_activations: bool,
) -> None:
from onnxruntime.quantization.qdq_loss_debug import (
collect_activations,
compute_signal_to_quantization_noice_ratio,
create_activation_matching,
create_weight_matching,
modify_model_output_intermediate_tensors,
)
if not float_model.is_file():
die(f"not a file: {float_model}")
if not qdq_model.is_file():
die(f"not a file: {qdq_model}")
if not skip_activations:
if not calib.is_dir():
die(f"not a directory: {calib}")
out_weights: dict[str, float] = {}
out_act: dict[str, dict[str, float]] = {}
if not skip_weights:
logger.info("weight matching (no inference run)…")
try:
wmatch = create_weight_matching(str(float_model), str(qdq_model))
except Exception as e:
die(f"create_weight_matching failed: {e}")
out_weights = _safe_compute_weight_error(
wmatch, compute_signal_to_quantization_noice_ratio
)
print("\n--- Weight SQNR (dB, higher is better) ---\n")
for name in sorted(out_weights, key=lambda x: out_weights[x]):
print(f" {name}: {out_weights[name]:.4f}")
if not skip_activations:
with tempfile.TemporaryDirectory(prefix="ort.qdq.debug.") as td:
tmp = Path(td)
aug_f = tmp / "augmented_float.onnx"
aug_q = tmp / "augmented_qdq.onnx"
op_arg = op_types if op_types is not None else []
logger.info("augmenting float model → %s", aug_f)
try:
modify_model_output_intermediate_tensors(
str(float_model), str(aug_f), op_types_for_saving=op_arg
)
except Exception as e:
die(f"modify_model_output_intermediate_tensors (float) failed: {e}")
logger.info("augmenting QDQ model → %s", aug_q)
try:
modify_model_output_intermediate_tensors(
str(qdq_model), str(aug_q), op_types_for_saving=op_arg
)
except Exception as e:
die(f"modify_model_output_intermediate_tensors (qdq) failed: {e}")
r_float = build_npy_dir_reader(float_model, calib, max_samples=max_samples)
r_qdq = build_npy_dir_reader(qdq_model, calib, max_samples=max_samples)
try:
logger.info("collect_activations (float)…")
act_f = collect_activations(str(aug_f), r_float)
logger.info("collect_activations (QDQ)…")
act_q = collect_activations(str(aug_q), r_qdq)
except Exception as e:
die(f"collect_activations failed: {e}")
match = create_activation_matching(act_q, act_f)
if not match:
print(
"\nwarning: empty activation match — is the quantized model QDQ (not QOperator)?",
file=sys.stderr,
)
out_act = _safe_compute_activation_error(
match, compute_signal_to_quantization_noice_ratio
)
has_xm = any("xmodel_err" in v for v in out_act.values())
scored = sorted(
out_act.items(),
key=lambda kv: _sqnr_key(kv[1], prefer_xmodel=has_xm),
)[: top_n if top_n > 0 else len(out_act)]
which = "xmodel_err (float vs post_qdq)" if has_xm else "qdq_err (q dq pair)"
print(
f"\n--- Top activation SQNR (dB, {which}; lower = worse) — showing "
f"{len(scored)} of {len(out_act)}\n"
)
for name, m in scored:
if has_xm and "xmodel_err" in m:
print(
f" {name}: pre/post_qdq {m['qdq_err']:.4f} "
f"float vs post_qdq {m['xmodel_err']:.4f}"
)
else:
print(f" {name}: pre/post_qdq {m['qdq_err']:.4f}")
if json_report is not None:
payload: dict[str, Any] = {
"weight_sqnr_db": out_weights,
"activation_sqnr_db": out_act,
}
json_report = json_report.resolve()
json_report.parent.mkdir(parents=True, exist_ok=True)
with json_report.open("w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, sort_keys=True)
print(f"\nwrote {json_report}")
def main() -> None:
ap = argparse.ArgumentParser(
description="ORT-style QDQ debug: float vs QDQ activations/weights (same .npy calib as quantize).",
)
ap.add_argument(
"--float-model",
type=Path,
required=True,
help="Float32 ONNX (same topology as the graph you quantized; use ORT preprocessed model if you used it before quantize).",
)
ap.add_argument(
"--qdq-model",
type=Path,
required=True,
help="Statically quantized (QDQ) ONNX model, e.g. from quantize_onnx_model.py.",
)
ap.add_argument(
"--calib-data-dir",
type=Path,
default=None,
help="Directory of float32 .npy used for calibration (required unless --skip-activations).",
)
ap.add_argument(
"--max-samples",
type=int,
default=5,
help="Max number of .npy files to use (default 5; full runs can be slow and memory-heavy).",
)
ap.add_argument(
"--op-types",
type=str,
default="",
help="Comma-separated op types to save intermediates for (e.g. Conv,Add). Empty = all float tensors (very slow and large).",
)
ap.add_argument(
"--json-report",
type=Path,
default=None,
help="Write metrics-only JSON to this path.",
)
ap.add_argument(
"--top",
type=int,
default=40,
help="How many activations to print (sorted worst-first by SQNR). 0 = print all.",
)
ap.add_argument(
"--skip-weights",
action="store_true",
help="Only run activation path (faster if you only care about activations).",
)
ap.add_argument(
"--skip-activations",
action="store_true",
help="Only run weight matching (no collect_activations; ignores calib).",
)
args = ap.parse_args()
if args.skip_weights and args.skip_activations:
die("cannot set both --skip-weights and --skip-activations")
if not args.skip_activations:
if args.calib_data_dir is None:
die("activation path requires --calib-data-dir")
if args.max_samples < 1:
die("--max-samples must be >= 1 for activation path")
_run(
float_model=args.float_model,
qdq_model=args.qdq_model,
calib=args.calib_data_dir or Path(),
max_samples=args.max_samples,
op_types=_parse_op_types(args.op_types),
json_report=args.json_report,
top_n=args.top,
skip_weights=args.skip_weights,
skip_activations=args.skip_activations,
)
if __name__ == "__main__":
main()