§ ONNX Runtime for Engineers
9 lessons · every lesson has a Colab notebook

Profiling and debugging

Nine lessons on running trained models with ONNX Runtime. Each lesson links to a Colab notebook for hands-on practice. The core exercises use a CPU; GPU work is optional.

Before you begin

You should be comfortable with Python functions and arrays. Inference means using a trained model to calculate an answer. A graph describes the operations used in that calculation. An operator is one kind of operation, such as addition or matrix multiplication.

Read the plain-language introduction, try its question, then open the notebook. Keep your measured results separate from the illustrative simulations on this page.

Technical references: ONNX Runtime Python API and quantization guidance.

09 · Profiling and Debugging

Introduction

When inference is slow or incorrect, guessing from total latency is not enough. Runtime profiling records node timing and device placement, while severity-controlled logs expose loading, partitioning, and execution failures.

Learning goal

Collect an ONNX Runtime performance profile, read trace evidence, configure useful logs, and diagnose common failures.

Before you start

Inference sessions, graph nodes, files, timing, and basic JSON or browser developer tools.

Lesson plan

  1. Enable profiling around real session runs and locate the generated trace file.
  2. Inspect node durations and provider assignments in a Chrome-trace viewer.
  3. Set useful log severity and map common error messages to likely causes.

Move from symptom to evidence. If latency rises, first confirm input shapes and provider placement. Then inspect the trace for expensive nodes, copies, or repeated allocations. A profile is a timeline of measured events, not an automatic diagnosis.

Three tools replace guessing: the profiler, the log severity knob, and a mental catalog of error messages.

so = ort.SessionOptions()
so.enable_profiling = True
so.profile_file_prefix = "ortprof"
sess = ort.InferenceSession(model, so, providers=["CPUExecutionProvider"])

for _ in range(5):
    sess.run(None, {"X": x})

path = sess.end_profiling()      # Chrome-trace JSON: every kernel, with duration
                                 # opens in chrome://tracing, Perfetto, or in pandas

Read the profile like a budget: for a MatMul-heavy model, MatMul should dominate and elementwise ops should be noise. If a Cast chain, a Transpose pair, or a Memcpy tops the table, you have found your optimization target. On multi-EP setups, each event's provider argument tells you where the node actually ran — the tool for proving silent CPU fallback.

SeverityMeaning
0Verbose — includes provider/partition decisions
1Info
2Warning
3Error (default)
4Fatal only
The debugging loop that works
  1. Check the contract — input/output names, dtypes, shapes from get_inputs()
  2. Verify the exportnp.allclose against the training framework
  3. Profile — read the top of the kernel table
  4. Raise log severity — read the partition; confirm which EP ran what
  5. Audit the optimized graph — dump it, open it in Netron, check the fusion you expected
  6. Diff the versions — ORT, opset, IR, EP wheel. Version skew is the most common "impossible" bug
▶ Run notebook 09 in Colab 09-profiling-debugging.ipynb CPU runtime · profile parsing + a field guide of real errors