| |
| """Inspect an ONNX model and emit a machine-readable characterization. |
| |
| Reports the facts a production-readiness review needs about a checkpoint before and |
| after optimization: opset, node/initializer counts, the weight-dtype histogram, whether |
| external data is used, the exact I/O tensor contract (names, dtypes, shapes), and the |
| operator histogram. Pure `onnx` (no onnxruntime) so it also runs on graphs a given ORT |
| build cannot execute. |
| |
| Usage: |
| python inspect_onnx.py MODEL.onnx [--json OUT.json] |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import collections |
| import hashlib |
| import json |
| import os |
| import sys |
|
|
| import onnx |
| from onnx import TensorProto |
|
|
| |
| DTYPE_NAME = {v: k for k, v in TensorProto.DataType.items()} |
|
|
|
|
| def _elem_type_name(elem_type: int) -> str: |
| return DTYPE_NAME.get(elem_type, f"UNKNOWN({elem_type})") |
|
|
|
|
| def _shape_of(value_info) -> list: |
| dims = [] |
| for d in value_info.type.tensor_type.shape.dim: |
| if d.HasField("dim_value"): |
| dims.append(d.dim_value) |
| elif d.HasField("dim_param") and d.dim_param: |
| dims.append(d.dim_param) |
| else: |
| dims.append("dynamic") |
| return dims |
|
|
|
|
| def _io_spec(value_info) -> dict: |
| return { |
| "name": value_info.name, |
| "dtype": _elem_type_name(value_info.type.tensor_type.elem_type), |
| "shape": _shape_of(value_info), |
| } |
|
|
|
|
| def inspect(path: str) -> dict: |
| size_bytes = os.path.getsize(path) |
| with open(path, "rb") as fh: |
| raw = fh.read() |
| sha256 = hashlib.sha256(raw).hexdigest() |
|
|
| model = onnx.load(path, load_external_data=False) |
| graph = model.graph |
|
|
| |
| opset = {} |
| for entry in model.opset_import: |
| opset[entry.domain or "ai.onnx"] = entry.version |
|
|
| |
| op_hist = collections.Counter(node.op_type for node in graph.node) |
|
|
| |
| init_dtype_hist = collections.Counter() |
| init_bytes = collections.Counter() |
| uses_external_data = False |
| for init in graph.initializer: |
| dtype = _elem_type_name(init.data_type) |
| init_dtype_hist[dtype] += 1 |
| |
| if init.data_location == TensorProto.EXTERNAL: |
| uses_external_data = True |
| |
| init_bytes[dtype] += len(init.raw_data) |
|
|
| |
| external_files = [] |
| model_dir = os.path.dirname(os.path.abspath(path)) |
| for entry in os.listdir(model_dir) if os.path.isdir(model_dir) else []: |
| if entry.endswith(".onnx_data") or entry.endswith(".data") or entry.endswith(".bin"): |
| external_files.append(entry) |
|
|
| |
| init_names = {init.name for init in graph.initializer} |
| inputs = [_io_spec(v) for v in graph.input if v.name not in init_names] |
| outputs = [_io_spec(v) for v in graph.output] |
|
|
| checker_ok = True |
| checker_error = None |
| try: |
| onnx.checker.check_model(model) |
| except Exception as exc: |
| checker_ok = False |
| checker_error = str(exc) |
|
|
| return { |
| "path": os.path.abspath(path), |
| "size_bytes": size_bytes, |
| "size_mib": round(size_bytes / 2**20, 3), |
| "sha256": sha256, |
| "ir_version": model.ir_version, |
| "producer": f"{model.producer_name} {model.producer_version}".strip(), |
| "opset": opset, |
| "num_nodes": len(graph.node), |
| "num_initializers": len(graph.initializer), |
| "uses_external_data": uses_external_data, |
| "external_files_in_dir": external_files, |
| "initializer_dtype_histogram": dict(init_dtype_hist), |
| "initializer_bytes_by_dtype": dict(init_bytes), |
| "operator_histogram": dict(sorted(op_hist.items(), key=lambda kv: -kv[1])), |
| "num_distinct_ops": len(op_hist), |
| "inputs": inputs, |
| "outputs": outputs, |
| "onnx_checker_ok": checker_ok, |
| "onnx_checker_error": checker_error, |
| } |
|
|
|
|
| def main() -> int: |
| ap = argparse.ArgumentParser(description=__doc__) |
| ap.add_argument("model") |
| ap.add_argument("--json", dest="json_out", default=None) |
| args = ap.parse_args() |
|
|
| info = inspect(args.model) |
| text = json.dumps(info, indent=2) |
| print(text) |
| if args.json_out: |
| with open(args.json_out, "w") as fh: |
| fh.write(text) |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|