onnx-mlir

Logo

Representation and Reference Lowering of ONNX Models in MLIR Compiler Infrastructure

View the Project on GitHub onnx/onnx-mlir

How-Tos

Inference Using Python
Inference Using C/C++
Inference Using Java

References

ONNX Dialect
OMTensor C99 Runtime API
OMTensorList C99 Runtime API
OMTensor Java Runtime API
OMTensorList Java Runtime API
Generate ONNX Dialect
About Documentation

Development

Add an Operation
Testing Guidelines
Error Handling
Command-line Options
Instrumentation
Performance Testing
Constant Propagation
Add an Accelerator

Tools

Tools

RunONNXModel.py
DocCheck

This project is maintained by onnx

Hosted on GitHub Pages — Theme by orderedlist

Performance Testing

onnx-mlir has two complementary ways to find out where time is going in a compiled model:

This page walks through the first workflow end to end: compile, run, collect a log, and turn that log into a report with make-report.py.

Step 1: Compile and run with RunONNXModel.py

Compile with --profile-ir=<stage> (Onnx, or ZHigh on an NNPA build) — via RunONNXModel.py, that goes in --compile-args. Add -w/-n for warmup/timed iteration counts and --write-runtime-log to capture the instrumentation output:

utils/RunONNXModel.py --model mymodel.onnx -w 2 -n 10  \
  --compile-args="-O3 --profile-ir=Onnx" \
  --write-runtime-log run.log

run.log will contain data from the warmup runs too — pass a matching -w 2 to make-report.py in the next step to exclude them.

Step 2: Aggregate stats with make-report.py

utils/make-report.py -r run.log -w 2

With only -r given, make-report.py defaults to --stats=perf: one line per op, with columns op-name, count, average-time, cumulative-time, percent-of-total:

Statistics start (all ops).
  onnx.Add, 112, 0.0001166, 0.0130570, 8.4%
  onnx.Cast, 105, 0.0000018, 0.0001860, 0.1%
  onnx.MatMul, 72, 0.0112630, 0.8109330, 52.3%

Useful flags at this stage:

Step 3: Per-shape timing with --profile-ir-with-sig

--profile-ir tells you which op is slow. It doesn’t tell you which instance of that op — the same op name can appear many times in a model with different tensor shapes, and shape is often the reason one instance is slow and another isn’t. --profile-ir-with-sig=<stage> adds each instrumented op’s input/output tensor shapes to the log alongside the timing:

utils/RunONNXModel.py --model mymodel.onnx \
  --compile-args="-O3 --profile-ir-with-sig=Onnx" \
  --write-runtime-log sig.log

(--profile-ir-with-sig takes the same stage values as --profile-irOnnx, or ZHigh on an NNPA build.)

Then report shape-signature statistics, focused on one op:

utils/make-report.py -r sig.log --stats=sig -l2 -f onnx.Transpose

Reading the printed shapes for a single op is often enough to answer shape questions without going back to an IR dump by hand. Here is a real report from profiling a granite-3.1-2b decoder model on an NNPA (z17) build, adding -w 2 to skip the run’s warmup iterations, -u ms for milliseconds, and -m 0.1 to hide ops below 0.1% of total time:

> make-report.py -r rz17-nnpa-fused-512.log -w 2 -u ms -m 0.1 -f onnx.Transpose -l2 --stats=sig
[...]
Statistics start all ops, 0.1%+ exec time ordered_by time, tot_time,  5.8831250
  onnx.Transpose, 81, 0.0668164, 5.4121250, 92.0%
    40, 0.1103594, 4.4143750, 81.6%, sig, 1x256x32x64xfloat, 1x32x256x64xfloat
    40, 0.0248719, 0.9948750, 18.4%, sig, 1x256x8x64xfloat, 1x8x256x64xfloat
    1, 0.0028750, 0.0028750, 0.1%, sig, 1x32x256xfloat, 1x256x32xfloat
Statistics end all ops, 0.1%+ exec time ordered_by time, tot_time,  5.8831250

onnx.Transpose alone accounts for 92% of total execution time here, and the -l2 breakdown shows it isn’t one uniform operation: it’s really three distinct shape instances hiding behind the same op name.

Without -l2, all of this would have collapsed into a single onnx.Transpose, 81, ... line — no way to tell that the vast majority of Transpose time comes from permuting a middle dimension rather than the last two, which matters directly on hardware with a dedicated transpose-matmul for last-two-dim transposes (e.g. NNPA on z17). The same shape listing makes a missing broadcast on some other op visible just as directly, since the mismatched dimensions show up side by side in the signature.

Other --stats views: par and simd

par and simd report compile-time decisions rather than runtime timing: whether each op was parallelized (par) or vectorized (simd), and why not when it wasn’t. These come from a separate compiler flag, --opt-report, and need a compile-time log (-c) in addition to a runtime log:

utils/RunONNXModel.py --model mymodel.onnx -w 2 -n 16 \
  --compile-args="-O3 --opt-report=Simd --profile-ir=Onnx" \
  --write-compile-log compile.log --write-runtime-log run.log
utils/make-report.py -c compile.log -r run.log -w 2
Statistics start (all ops).
  onnx.Add-simd, 112, 0.0130570
  onnx.Cast, 23, 0.0000650
  onnx.Gemm, 1, 0.0003570
  onnx.Gemm-simd, 72, 0.8109330

Ops that were successfully vectorized/parallelized are listed separately, with a -simd/-par suffix, from instances of the same op that weren’t. Combining -c and -r like this correlates the compile-time decision with the actual runtime cost of each case — e.g. above, onnx.Gemm-simd instances took far longer in total than plain onnx.Gemm, but that’s dominated by the fact there are 72 of them vs. 1.

Gathering compile-time info

Two independent ways to see what a model was actually compiled with:

The same embedded info is also picked up automatically by make-report.py: whenever it’s present in the log passed via -r/-c, make-report.py prints the compiler_version, compile_options, and accelerator info before the statistics table. This means a saved run.log is self-describing even without keeping a separate compile.log around.

Implementation notes

A few internals that explain some of the behavior above, in case you need to inspect a raw log directly or something doesn’t line up: