cone-distance / src /bench /eval_ncnn.py
Aryan Sethi
Phase 3: per-format benchmark with stage timing
0b910fa
Raw History Blame Contribute Delete
3.45 kB
"""Benchmark the NCNN export.
The one that matters for the Pi. NCNN is Tencent's ARM-targeted inference
framework, which is why it wins on a Cortex-A76 -- Ultralytics' published Pi 5
table has it at 93.9 ms against PyTorch's 524.8 ms, 5.6x, with mAP slightly
*up* rather than degraded. On x86 that advantage largely disappears, so a number
from this machine is a plumbing check, not a prediction of Pi performance.
`--weights` takes the exported *directory*, not a file: NCNN splits a model into
`model.ncnn.param` (the graph) and `model.ncnn.bin` (the weights).
python -m src.bench.eval_ncnn
A note on the preflight below. A mismatch between the pnnx that wrote the export
and the ncnn runtime loading it does not fail cleanly -- both files load, the
extractor is created, the input is accepted, and the process then segfaults
inside the forward pass. A segfault cannot be caught in-process, so a bad
combination would take the benchmark down with no diagnosis and no output. The
check therefore runs one forward pass in a child process first, where a crash is
just a return code.
"""
import subprocess
import sys
from pathlib import Path
from src.bench.common_cli import build_parser, run
PROBE = """
import ncnn, sys
net = ncnn.Net()
if net.load_param({param!r}) != 0 or net.load_model({bin!r}) != 0:
sys.exit(2)
ex = net.create_extractor()
mat = ncnn.Mat(64, 64, 3); mat.fill(0.0)
ex.input(net.input_names()[0], mat)
ex.extract(net.output_names()[0])
"""
def preflight(weights: str) -> None:
"""Run one forward pass in a child process and explain any crash."""
directory = Path(weights)
param, binary = directory / "model.ncnn.param", directory / "model.ncnn.bin"
if not param.exists() or not binary.exists():
raise SystemExit(f"{directory} is not an NCNN export "
f"(expected model.ncnn.param and model.ncnn.bin). "
f"Create it with conversions.py.")
result = subprocess.run(
[sys.executable, "-c", PROBE.format(param=str(param), bin=str(binary))],
capture_output=True)
if result.returncode == 0:
return
import ncnn
if result.returncode < 0:
raise SystemExit(
f"\nThe NCNN runtime crashed running this model "
f"(signal {-result.returncode}).\n\n"
f"The export loads -- both files parse and the graph builds -- and then\n"
f"dies in the forward pass. That is the signature of a version mismatch\n"
f"between the pnnx that wrote {directory}/ and the ncnn that is\n"
f"reading it: the weight layout changed between them, so ncnn walks off\n"
f"the end of the buffer.\n\n"
f" installed ncnn: {ncnn.__version__}\n\n"
f"Fixes, cheapest first:\n"
f" * re-export on the machine that will run it -- this is the usual\n"
f" answer for the Pi, where you install ncnn from its own toolchain\n"
f" * pin the ncnn wheel to the version pnnx targeted\n"
f" * benchmark ONNX instead on this box; it measures the same pipeline\n")
raise SystemExit(f"NCNN probe failed (exit {result.returncode}): "
f"{result.stderr.decode()[:400]}")
if __name__ == "__main__":
parser = build_parser(__doc__, default_weights="yolo11n_ncnn_model")
args = parser.parse_args()
preflight(args.weights)
run(args, label="NCNN", tag="ncnn")