Raju Shingadiya Raju Shingadiya
All posts

Netron: the model viewer I open before every on-device deployment

A quantized model that loads without an error is not the same as a quantized model that's actually quantized. Netron is how I check.

A circuit board pattern forming the shape of a brain
Photo by Steve A Johnson on Unsplash

The first time I shipped an on-device model into an Android app, I did everything the guide told me to. Ran post-training quantization, got a .tflite file that was roughly a quarter the size of the original, dropped it in assets/, wired up the interpreter, and it worked. Inference time barely moved. I spent two days assuming the phone was thermally throttling.

It wasn't. Half the graph had silently stayed in float32, because a couple of ops in the middle didn't have integer kernels and the converter had quietly inserted quantize/dequantize pairs around them. Nothing failed. Nothing warned me. The file was smaller and the compute was the same.

I found it by opening the model in Netron and looking at it. Ten minutes. Since then it's the first thing I do with any model that's going on a device.

What Netron is

Netron is a viewer for neural network and machine learning models, written by Lutz Roeder. You give it a model file, it renders the graph — every node, every tensor, every attribute — and lets you click through it.

That's it. It doesn't train, convert, optimise or run anything. It's a read-only window into a binary you'd otherwise be treating as a black box, and that turns out to be enormously useful when the thing you're debugging is the model itself rather than your code around it.

The format coverage is what makes it practical. It handles ONNX, TensorFlow Lite, PyTorch, torch.export, ExecuTorch, TorchScript, TensorFlow, Core ML, OpenVINO, Keras, Caffe, Darknet, Safetensors and NumPy, with experimental support for MLIR, JAX, GGUF, RKNN, ncnn, MNN, PaddlePaddle and scikit-learn.

For on-device work that list is almost exactly the set of formats you touch. TFLite and ExecuTorch for Android, Core ML for iOS, ONNX as the interchange format everything passes through, GGUF if you're putting an LLM on the phone, and ncnn / MNN / RKNN if you're on a vendor runtime.

Getting it running

Four ways, and I use three of them regularly.

Browsernetron.app. Drag the file in. Fastest option for something quick. I don't use it for client models on principle, and neither should you if the model isn't yours to share.

Desktop app — what I actually use day to day:

# macOS
brew install --cask netron

# Windows
winget install -s winget netron

Linux gets a .deb or .rpm from the releases page.

Python — this is the one worth knowing about, because it hooks into a conversion script:

pip install netron

# from the shell
netron model.tflite

Or from inside the script that just produced the model:

import netron
netron.start('model_int8.tflite')

I put that behind a flag at the end of my conversion pipeline. Convert, then look at what came out, immediately, before it gets committed. Catching a bad conversion at the moment it happens is much cheaper than catching it after the APK is built.

Colourful source code filling a computer screen at an angle
Photo by Ali Shah Lakhani on Unsplash

Where it earns its keep: quantization

This is the whole reason I care about the tool.

Quantization is a lossy transformation applied by a converter that makes a lot of decisions on your behalf and reports almost none of them. You hand it a float model and a representative dataset, it hands you back a file. Whether that file is actually integer end to end, what the ranges look like, whether ops got fused the way you expected — none of that is in the output log. It's in the graph.

Is the model actually quantized

Click any tensor in Netron and the properties panel shows its type. In a properly full-integer TFLite model, the weights and activations show as int8 or uint8, and the quantization parameters — scale and zero point — are listed alongside them.

What you're looking for is what I missed the first time: a run of QUANTIZE and DEQUANTIZE nodes sitting in the middle of the graph. Every one of those is a conversion back to float and out again, which means the op between them is running in float, which means you paid the accuracy cost of quantization and got none of the speed.

A handful at the input and output boundary is normal and expected. A cluster in the middle of your backbone is a bug.

Reading scale and zero point

These two numbers are how an int8 value maps back to a real one:

real_value = (int8_value - zero_point) * scale

Netron shows them per tensor, and for weights it'll show per-channel scales where the converter used them. Two things I check:

Per-tensor vs per-channel matters. Weight quantization per output channel keeps far more accuracy than a single scale across the whole tensor, especially for depthwise convolutions. Netron shows you which one you got. If you asked for per-channel and see one scale value, your converter didn't do what you asked.

Did the fusion happen

Conversion is supposed to fold BatchNorm into the preceding convolution and fuse activations into the op. When it works, you see a single CONV_2D node with fused_activation_function: RELU6 in its attributes. When it doesn't, you see conv, then a separate mul, then an add, then a relu — four dispatches where there should be one.

This usually means something upstream in the export was unusual. It's a five-second check in Netron and a long afternoon to discover by profiling.

Where it earns its keep: getting a model onto a device

Knowing your input and output signature

Before I write a single line of inference code I open the model and note down, for every input and output: name, shape, dtype, and quantization params.

NHWC or NCHW. Whether the batch dimension is 1 or dynamic. Whether the model expects [0,255] uint8 or normalised float. How many outputs there actually are and in what order — detection models in particular often have four output tensors and the ordering is not always what the docs claim.

Reading it off the graph takes two minutes and removes all the guesswork from the Kotlin side.

Checking delegate and accelerator compatibility

This is the one that most directly affects whether your model runs fast on a phone. GPU delegates, NNAPI, NPU runtimes — they all support a subset of operators, and anything outside that subset falls back to CPU.

The fallback isn't just slower. If the unsupported op sits in the middle of the graph, execution has to bounce off the accelerator, run on CPU, and come back, and the transfer cost can make the whole thing slower than pure CPU would have been.

So I list the operators in the graph and compare against the delegate's supported set. If there's one odd op — a custom layer, an exotic resize mode, a gather nobody needed — it's often possible to replace it at the model level and unlock the accelerator for the entire network. Netron is how you find that op.

Sizing before shipping

The properties panel shows weight tensor shapes and types, which tells you where the file size actually lives. Nine times out of ten it's one or two large dense layers at the head, and it's worth knowing that before you argue with someone about the APK size budget.

Working with GGUF and on-device LLMs

The GGUF support is experimental but good enough for what I need it for: checking which quantization scheme a downloaded model actually uses. There's a real difference between Q4_K_M and Q8_0 in both memory footprint and quality, and filenames on model hubs are not always accurate. Open it, look at the tensor types, believe the file rather than its name.

Comparing before and after

My most common actual workflow is two windows side by side. Original float model on the left, converted or quantized model on the right. Walk the graph and see what changed — what got fused, what got split, what got inserted, what disappeared.

Every conversion tool transforms the graph in ways it doesn't document. Seeing both versions at once is the only reliable way I've found to know what a converter actually did to my model.

What it won't do

Being fair about the limits, because I've seen people expect too much of it:

None of that bothers me much, because it does the one thing I need it to do and does it instantly.

The short version

If you're putting models on phones, the gap between "the converter ran" and "the model is correct and fast" is wider than the tutorials suggest, and almost everything in that gap is visible in the graph. Netron is free, installs in one command, and turns a binary you were trusting on faith into something you can actually check.

Convert your model, open it, and look at it before you ship it. That's the whole habit.


github.com/lutzroeder/netron · netron.app

← All posts