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
386 lines
13 KiB
Python
Executable File
386 lines
13 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.
|
|
|
|
"""Quantize a bundled PaddleOCR ONNX model.
|
|
|
|
Reads ``inference.onnx`` from ``--input-model-dir``, writes quantized ``inference.onnx`` to
|
|
``--output-model-dir``. When input and output directories differ, copies ``inference.yml`` from
|
|
source to destination after a successful quantize. In-place mode (same directory) only
|
|
replaces ``inference.onnx``.
|
|
|
|
Examples:
|
|
# Dynamic (weights int8); no calibration data
|
|
python quantize_onnx_model.py --input-model-dir ./PaddleOCRDemo/Models/det \\
|
|
--output-model-dir ./out/det_q --mode dynamic
|
|
|
|
# Static; one .npy file per calibration sample (float32, model input shape)
|
|
python quantize_onnx_model.py --input-model-dir ./Models/rec \\
|
|
--output-model-dir ./Models/rec_int8 --mode static \\
|
|
--calib-data-dir ./my_calib_npy
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
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
|
|
|
|
|
|
def _same_file(a: Path, b: Path) -> bool:
|
|
try:
|
|
return a.resolve() == b.resolve()
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def _run_ort_quant_pre_process(src_onnx: Path, work_dir: Path) -> Path:
|
|
"""Run ORT *quant_pre_process*; write a new ONNX with ``onnx.quant.pre_process`` metadata.
|
|
|
|
Tries, in order: default → ``skip_symbolic_shape`` (some det/NMS graphs fail symbolic infer)
|
|
→ also ``skip_optimization`` (last resort). Removes any failed output before retrying.
|
|
"""
|
|
try:
|
|
from onnxruntime.quantization import quant_pre_process
|
|
except ImportError as e:
|
|
die(
|
|
f"onnxruntime.quantization.quant_pre_process is required for --ort-preprocess: {e}"
|
|
)
|
|
|
|
fd, raw = tempfile.mkstemp(
|
|
suffix=".pre.onnx",
|
|
prefix="inference.ort_",
|
|
dir=str(work_dir),
|
|
)
|
|
os.close(fd)
|
|
out = Path(raw)
|
|
|
|
attempts: list[tuple[str, dict[str, bool]]] = [
|
|
(
|
|
"(full symbolic + ORT optimize)",
|
|
{"skip_symbolic_shape": False, "skip_optimization": False},
|
|
),
|
|
(
|
|
"(skip symbolic shape, keep ORT optimize)",
|
|
{"skip_symbolic_shape": True, "skip_optimization": False},
|
|
),
|
|
(
|
|
"(skip symbolic shape, skip ORT graph optimization)",
|
|
{"skip_symbolic_shape": True, "skip_optimization": True},
|
|
),
|
|
]
|
|
last_err: Exception | None = None
|
|
for label, kwargs in attempts:
|
|
out.unlink(missing_ok=True)
|
|
try:
|
|
quant_pre_process(
|
|
input_model=str(src_onnx),
|
|
output_model_path=str(out),
|
|
**kwargs,
|
|
)
|
|
except Exception as e:
|
|
last_err = e
|
|
continue
|
|
if label != attempts[0][0]:
|
|
print(
|
|
f"warning: ORT quant_pre_process succeeded with {label}.",
|
|
file=sys.stderr,
|
|
)
|
|
return out
|
|
|
|
out.unlink(missing_ok=True)
|
|
die(
|
|
f"ORT quant_pre_process failed after {len(attempts)} attempt(s). Last error: {last_err!r}. "
|
|
"You can try again with --no-ort-preprocess to quantize the original model only."
|
|
)
|
|
|
|
|
|
def _quantize_dynamic(src_onnx: Path, dst_onnx: Path, per_channel: bool) -> None:
|
|
from onnxruntime.quantization import QuantType, quantize_dynamic
|
|
|
|
quantize_dynamic(
|
|
model_input=str(src_onnx),
|
|
model_output=str(dst_onnx),
|
|
weight_type=QuantType.QInt8,
|
|
per_channel=per_channel,
|
|
)
|
|
|
|
|
|
def _quantize_static(
|
|
src_onnx: Path,
|
|
dst_onnx: Path,
|
|
calib_dir: Path,
|
|
per_channel: bool,
|
|
calibrate_method_name: str,
|
|
) -> None:
|
|
from onnxruntime.quantization import (
|
|
CalibrationMethod,
|
|
QuantFormat,
|
|
QuantType,
|
|
quantize_static,
|
|
)
|
|
|
|
reader = build_npy_dir_reader(src_onnx, calib_dir)
|
|
try:
|
|
method = getattr(CalibrationMethod, calibrate_method_name)
|
|
except AttributeError:
|
|
die(
|
|
f"unknown calibration method {calibrate_method_name!r}; "
|
|
f"valid names: {', '.join(CalibrationMethod.__members__)}"
|
|
)
|
|
quantize_static(
|
|
model_input=str(src_onnx),
|
|
model_output=str(dst_onnx),
|
|
calibration_data_reader=reader,
|
|
quant_format=QuantFormat.QDQ,
|
|
activation_type=QuantType.QUInt8,
|
|
weight_type=QuantType.QInt8,
|
|
per_channel=per_channel,
|
|
calibrate_method=method,
|
|
)
|
|
|
|
|
|
def _default_domain_onnx_opset(m: Any) -> int:
|
|
"""Max declared opset for default / ``ai.onnx`` imports."""
|
|
v = 0
|
|
for oi in m.opset_import:
|
|
dom = oi.domain or ""
|
|
if dom in ("", "ai.onnx"):
|
|
v = max(v, int(oi.version))
|
|
return v
|
|
|
|
|
|
def _try_onnx_opset_via_version_converter(path: Path, target_opset: int) -> str:
|
|
import onnx
|
|
from onnx import version_converter
|
|
|
|
m = onnx.load(str(path), load_external_data=True)
|
|
cur = _default_domain_onnx_opset(m)
|
|
if cur >= target_opset:
|
|
return "skipped_already_ge_target"
|
|
try:
|
|
m2 = version_converter.convert_version(m, target_opset)
|
|
except Exception as e:
|
|
print(
|
|
f"warning: onnx.version_converter.convert_version(..., {target_opset}) failed: {e!r}. "
|
|
"Full checker may still fail. Try a newer `onnx`, or use --no-verify if ORT loads the model.",
|
|
file=sys.stderr,
|
|
)
|
|
return "convert_failed"
|
|
try:
|
|
onnx.save(m2, str(path))
|
|
except Exception as e:
|
|
die(f"failed to write ONNX after version conversion: {e}")
|
|
return "converted"
|
|
|
|
|
|
def _verify_onnx_file(path: Path) -> None:
|
|
"""Validate the output with the ONNX checker (avoids ORT IR / build skew in the host venv)."""
|
|
import onnx
|
|
|
|
m = onnx.load(str(path), load_external_data=True)
|
|
try:
|
|
onnx.checker.check_model(m, full_check=True)
|
|
except Exception as e:
|
|
die(f"output model failed ONNX checker validation: {e}")
|
|
|
|
|
|
def _atomic_replace(src: Path, dst: Path) -> None:
|
|
os.replace(str(src), str(dst))
|
|
|
|
|
|
def main() -> None:
|
|
p = argparse.ArgumentParser(description="Quantize PaddleOCR ONNX model.")
|
|
p.add_argument(
|
|
"--input-model-dir",
|
|
required=True,
|
|
type=Path,
|
|
help="Input model directory",
|
|
)
|
|
p.add_argument(
|
|
"--output-model-dir",
|
|
required=True,
|
|
type=Path,
|
|
help="Output model directory",
|
|
)
|
|
p.add_argument(
|
|
"--mode",
|
|
required=True,
|
|
choices=("dynamic", "static"),
|
|
help="dynamic: weight-only (quantize_dynamic). static: QDQ (quantize_static, needs --calib-data-dir).",
|
|
)
|
|
p.add_argument(
|
|
"--calib-data-dir",
|
|
type=Path,
|
|
default=None,
|
|
help="Directory of float32 .npy calibration samples (static mode only; one tensor per file).",
|
|
)
|
|
p.add_argument(
|
|
"--per-channel",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=True,
|
|
help="Per-channel weight quantization (default: true).",
|
|
)
|
|
p.add_argument(
|
|
"--calibration-method",
|
|
default="MinMax",
|
|
help="ORT CalibrationMethod name (e.g. MinMax, Entropy, Percentile).",
|
|
)
|
|
p.add_argument(
|
|
"--no-verify",
|
|
action="store_true",
|
|
help="Skip ONNX checker validation of the output model after quantization.",
|
|
)
|
|
p.add_argument(
|
|
"--ort-preprocess",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=True,
|
|
help=(
|
|
"Before quantize_static / quantize_dynamic, run ORT quant_pre_process (shape infer + "
|
|
"optional graph optimization) and attach onnx.quant metadata."
|
|
),
|
|
)
|
|
p.add_argument(
|
|
"--onnx-opset-convert",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=True,
|
|
help=(
|
|
"After ORT writes the output, run onnx.version_converter when the graph's declared "
|
|
"default-domain opset is below --onnx-target-opset. Use --no-onnx-opset-convert to keep "
|
|
"raw ORT output."
|
|
),
|
|
)
|
|
p.add_argument(
|
|
"--onnx-target-opset",
|
|
type=int,
|
|
default=13,
|
|
metavar="N",
|
|
help=(
|
|
"Target ONNX opset for --onnx-opset-convert (default: 13). Ignored when conversion is off."
|
|
),
|
|
)
|
|
args = p.parse_args()
|
|
|
|
if args.mode == "static" and args.calib_data_dir is None:
|
|
die("static mode requires --calib-data-dir")
|
|
if args.mode == "dynamic" and args.calib_data_dir is not None:
|
|
print("warning: --calib-data-dir is ignored for dynamic mode", file=sys.stderr)
|
|
if args.onnx_target_opset < 1:
|
|
die("--onnx-target-opset must be >= 1")
|
|
|
|
input_model_dir: Path = args.input_model_dir
|
|
output_model_dir: Path = args.output_model_dir
|
|
if not input_model_dir.is_dir():
|
|
die(f"input directory does not exist: {input_model_dir}")
|
|
|
|
src_onnx = input_model_dir / "inference.onnx"
|
|
src_yml = input_model_dir / "inference.yml"
|
|
if not src_onnx.is_file():
|
|
die(f"missing {src_onnx}")
|
|
if not src_yml.is_file():
|
|
die(f"missing {src_yml} (expected alongside inference.onnx)")
|
|
|
|
out_onnx = output_model_dir / "inference.onnx"
|
|
in_place = _same_file(input_model_dir, output_model_dir)
|
|
if not in_place:
|
|
output_model_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
pre_onnx: Path | None = None
|
|
if args.ort_preprocess:
|
|
pre_onnx = _run_ort_quant_pre_process(src_onnx, input_model_dir)
|
|
quant_src = pre_onnx if pre_onnx is not None else src_onnx
|
|
|
|
try:
|
|
if in_place:
|
|
fd, tmp_name = tempfile.mkstemp(
|
|
prefix="inference.onnx.",
|
|
suffix=".tmp",
|
|
dir=str(input_model_dir),
|
|
)
|
|
os.close(fd)
|
|
tmp_path = Path(tmp_name)
|
|
try:
|
|
if args.mode == "dynamic":
|
|
_quantize_dynamic(quant_src, tmp_path, per_channel=args.per_channel)
|
|
else:
|
|
assert args.calib_data_dir is not None
|
|
_quantize_static(
|
|
quant_src,
|
|
tmp_path,
|
|
args.calib_data_dir,
|
|
per_channel=args.per_channel,
|
|
calibrate_method_name=args.calibration_method,
|
|
)
|
|
_atomic_replace(tmp_path, out_onnx)
|
|
finally:
|
|
if tmp_path.is_file() and not _same_file(tmp_path, out_onnx):
|
|
try:
|
|
tmp_path.unlink()
|
|
except OSError:
|
|
pass
|
|
else:
|
|
try:
|
|
if args.mode == "dynamic":
|
|
_quantize_dynamic(quant_src, out_onnx, per_channel=args.per_channel)
|
|
else:
|
|
assert args.calib_data_dir is not None
|
|
_quantize_static(
|
|
quant_src,
|
|
out_onnx,
|
|
args.calib_data_dir,
|
|
per_channel=args.per_channel,
|
|
calibrate_method_name=args.calibration_method,
|
|
)
|
|
shutil.copy2(src_yml, output_model_dir / "inference.yml")
|
|
except Exception:
|
|
if out_onnx.is_file():
|
|
try:
|
|
out_onnx.unlink()
|
|
except OSError:
|
|
pass
|
|
raise
|
|
finally:
|
|
if pre_onnx is not None and pre_onnx.is_file():
|
|
try:
|
|
pre_onnx.unlink()
|
|
except OSError:
|
|
pass
|
|
|
|
if args.onnx_opset_convert:
|
|
st = _try_onnx_opset_via_version_converter(out_onnx, args.onnx_target_opset)
|
|
if st == "converted":
|
|
print(
|
|
f"note: applied onnx.version_converter to opset {args.onnx_target_opset}.",
|
|
file=sys.stderr,
|
|
)
|
|
if not args.no_verify:
|
|
_verify_onnx_file(out_onnx)
|
|
|
|
extra = f" and copied inference.yml" if not in_place else ""
|
|
print(f"Wrote {out_onnx}{extra}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|