Skip to content

Commit 4ed31d3

Browse files
authored
Add benchmarking script (#20188) (#20188)
Summary: Adds a standalone microbenchmark for the ImageProcessor reuse APIs and a companion script to diff two runs, so kernel/pipeline changes (e.g. the NEON deinterleave switch) can be measured reproducibly. New directory xplat/executorch/extension/image/benchmark/: * image_processor_benchmark.cpp (cxx_binary) — times process_into (BGRA/RGBA) and process_yuv_into (NV12/NV21) over a sweep of common input sizes × target sizes. Per cell it runs variants covering execution path (CPU / GPU / size-default), resize mode (stretch / letterbox), orientation (upright + 90°), cropped ROI, and the allocating process() vs process_into(). Each row reports mean/median/p95/stddev over 100 iters (10 warmup) on a synthetic gradient input; a row that fails is reported as ERROR rather than timed. * Flags (all optional): --format=bgra|rgba|nv12|nv21, --unit=cpu|gpu|default (both default to all), --out=PATH (writes a clean results table; the input-size sweep and rotation always run). Output is grouped under === API-section banners with a column legend, and --- per-cell separators. * compare_benchmarks.py (python_binary, stdlib-only) — matches rows by (API section, input→target cell, variant) and prints per-row base / new speedup plus a summary bucketed by execution path (CPU / GPU / default). * README.md — usage, the build-mode caveat, and the capture→compare workflow. * BUCK / TARGETS / targets.bzl — build defs. Note: benchmark only with an optimized build (-c cxx.extra_cxxflags=-Os); the default buck2 run is -O0 and unrepresentative. Differential Revision: D108048181
1 parent e0be283 commit 4ed31d3

6 files changed

Lines changed: 819 additions & 0 deletions

File tree

extension/image/benchmark/BUCK

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
load(":targets.bzl", "define_common_targets")
2+
3+
oncall("executorch")
4+
5+
define_common_targets()
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# ImageProcessor benchmark
2+
3+
A microbenchmark for the `ImageProcessor` reuse APIs (`process_into` and
4+
`process_yuv_into`) plus a companion script to compare two runs.
5+
6+
## What it measures
7+
8+
`image_processor_benchmark` sweeps common input sizes × target sizes and, per
9+
cell, times a set of variants:
10+
11+
- **API**: `process_into` (BGRA/RGBA) and `process_yuv_into` (NV12/NV21)
12+
- **execution path**: CPU, GPU, and the size-threshold default
13+
- **resize mode**: stretch, letterbox
14+
- **orientation**: upright and 90° rotate
15+
- **other**: cropped ROI, and the allocating `process()` vs `process_into()`
16+
17+
Each row reports mean / median / p95 / stddev over 100 measured iterations
18+
(10 warmup).
19+
20+
## Build mode matters
21+
22+
Always benchmark an **optimized** build. The default `buck2 run` compiles at
23+
`-O0`, where the hand-written NEON kernels are unrepresentative. Pass `-c cxx.extra_cxxflags=-Os` to match
24+
how ExecuTorch ships:
25+
26+
```bash
27+
buck2 run -c cxx.extra_cxxflags=-Os \
28+
fbsource//xplat/executorch/extension/image/benchmark:image_processor_benchmark
29+
```
30+
31+
## Options
32+
33+
| Flag | Default | Meaning |
34+
|------|---------|---------|
35+
| `--format=bgra\|rgba\|nv12\|nv21` | all | restrict to one color / YUV format |
36+
| `--unit=cpu\|gpu\|default` | all | restrict to one execution path |
37+
| `--out=PATH` | stdout | write the results table to PATH |
38+
39+
The input-size sweep and the rotation variant always run. Writing with `--out`
40+
keeps the file free of buck build-log lines (which go to stderr).
41+
42+
## Comparing two runs
43+
44+
Capture a baseline and a candidate, then diff them:
45+
46+
```bash
47+
TARGET=fbsource//xplat/executorch/extension/image/benchmark:image_processor_benchmark
48+
buck2 run -c cxx.extra_cxxflags=-Os $TARGET -- --out=/tmp/base.txt
49+
# ... make your change ...
50+
buck2 run -c cxx.extra_cxxflags=-Os $TARGET -- --out=/tmp/new.txt
51+
52+
python3 xplat/executorch/extension/image/benchmark/compare_benchmarks.py \
53+
/tmp/base.txt /tmp/new.txt
54+
# or via buck:
55+
buck2 run fbsource//xplat/executorch/extension/image/benchmark:compare_benchmarks \
56+
-- /tmp/base.txt /tmp/new.txt
57+
```
58+
59+
`compare_benchmarks.py` matches rows by (API section, input→target cell, variant)
60+
and prints the per-row `base / new` speedup plus a summary bucketed by execution
61+
path (CPU / GPU / default). Cross-run and thermal drift shift all rows together,
62+
so compare the buckets against each other rather than reading any single ratio
63+
absolutely.
64+
65+
For a clean A/B, capture both files back-to-back on an otherwise idle machine.
66+
67+
## Files
68+
69+
- `image_processor_benchmark.cpp` — the benchmark binary; buck target
70+
`:image_processor_benchmark` (run with `buck2 run`)
71+
- `compare_benchmarks.py` — compares two result files (stdlib only); buck target
72+
`:compare_benchmarks` (run with `buck2 run …:compare_benchmarks -- BASE NEW`)
73+
- `BUCK` / `TARGETS` / `targets.bzl` — build definitions

extension/image/benchmark/TARGETS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
load(":targets.bzl", "define_common_targets")
2+
3+
oncall("executorch")
4+
5+
define_common_targets()
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under the BSD-style license found in the
6+
# LICENSE file in the root directory of this source tree.
7+
8+
"""Compare two image_processor_benchmark result files.
9+
10+
Each input is the output of `image_processor_benchmark --out=PATH` (or its
11+
stdout). Rows are matched by (API section, input->target cell, variant label)
12+
and the per-row speedup base/new is reported.
13+
14+
The summary buckets rows by execution path (CPU / GPU / default). Cross-run and
15+
thermal drift shift all rows together, so compare the buckets against each other
16+
rather than reading any single ratio absolutely.
17+
18+
Usage:
19+
compare_benchmarks.py BASE.txt NEW.txt [--metric=median|mean]
20+
"""
21+
22+
import argparse
23+
import re
24+
import statistics
25+
import sys
26+
27+
ROW_RE = re.compile(
28+
r"^(?P<label>.*?)\s+mean=\s*(?P<mean>[\d.]+) ms\s+"
29+
r"median=\s*(?P<median>[\d.]+) ms"
30+
)
31+
CELL_RE = re.compile(r"^\[(?P<cell>.+?)\]\s*$")
32+
33+
34+
def path_bucket(label):
35+
"""Bucket a variant by execution path for the summary, or None to skip."""
36+
if "GPU" in label:
37+
return "GPU"
38+
if "def" in label:
39+
return "Default"
40+
if "CPU" in label:
41+
return "CPU"
42+
return None
43+
44+
45+
def parse(path, metric):
46+
"""Return {(section, cell, label): value} for the chosen metric."""
47+
rows = {}
48+
section = None
49+
cell = None
50+
with open(path) as f:
51+
for line in f:
52+
stripped = line.strip()
53+
if "ImageProcessor::process_yuv_into" in stripped:
54+
section = "process_yuv_into"
55+
continue
56+
if "ImageProcessor::process_into" in stripped:
57+
section = "process_into"
58+
continue
59+
cell_m = CELL_RE.match(stripped)
60+
if cell_m and "->" in stripped:
61+
cell = cell_m.group("cell")
62+
continue
63+
row_m = ROW_RE.match(line)
64+
if row_m:
65+
key = (section, cell, row_m.group("label").strip())
66+
rows[key] = float(row_m.group(metric))
67+
return rows
68+
69+
70+
def main():
71+
ap = argparse.ArgumentParser(description=__doc__)
72+
ap.add_argument("base", help="baseline results file")
73+
ap.add_argument("new", help="new results file")
74+
ap.add_argument("--metric", choices=["median", "mean"], default="median")
75+
args = ap.parse_args()
76+
77+
base = parse(args.base, args.metric)
78+
new = parse(args.new, args.metric)
79+
80+
keys = [k for k in base if k in new]
81+
if not keys:
82+
print("no matching rows between the two files", file=sys.stderr)
83+
return 1
84+
only = set(base) ^ set(new)
85+
if only:
86+
print(f"note: {len(only)} row(s) present in only one file (ignored)\n")
87+
88+
buckets = {"CPU": [], "GPU": [], "Default": []}
89+
for section in ("process_into", "process_yuv_into"):
90+
sect_keys = [k for k in keys if k[0] == section]
91+
if not sect_keys:
92+
continue
93+
print(f"=== {section} ({args.metric}, speedup = base / new) ===")
94+
print(f"{'cell':<26}{'variant':<24}{'base':>9}{'new':>9}{'speedup':>9}")
95+
print("-" * 77)
96+
for k in sect_keys:
97+
_, cell, label = k
98+
b, n = base[k], new[k]
99+
sp = b / n if n else float("nan")
100+
bucket = path_bucket(label)
101+
if bucket is not None:
102+
buckets[bucket].append(sp)
103+
print(f"{cell:<26}{label:<24}{b:>9.3f}{n:>9.3f}{sp:>8.2f}x")
104+
print()
105+
106+
def summary(name, xs):
107+
if not xs:
108+
return
109+
print(
110+
f"{name:<14} n={len(xs):<4} "
111+
f"median={statistics.median(xs):.2f}x "
112+
f"min={min(xs):.2f}x max={max(xs):.2f}x"
113+
)
114+
115+
print("=== summary (speedup = base / new, by execution path) ===")
116+
for name in ("CPU", "GPU", "Default"):
117+
summary(f"{name} rows", buckets[name])
118+
return 0
119+
120+
121+
if __name__ == "__main__":
122+
sys.exit(main())

0 commit comments

Comments
 (0)