Skip to content

Commit db70e09

Browse files
kalwaltclaude
andauthored
perf(kpm): Gaussian scale-space pyramid — benchmark + rayon parallelization (#209)
* perf(kpm): criterion benchmark for Gaussian scale-space pyramid (#200) Establish the scalar baseline for the Gaussian pyramid SIMD series (#200 -> #201 -> #202). Unlike the BoxFilterPyramid8u trilogy (#131-133, dead code per #203), GaussianScaleSpacePyramid is the pyramid the KPM/FREAK detector actually uses via DoGScaleInvariantDetector. - Promote the three hot helpers to `pub` with scalar-only dispatchers, following the `pyramid::downsample` precedent from #131: binomial_4th_order_u8_to_f32{,_scalar} binomial_4th_order_f32_to_f32{,_scalar} downsample_bilinear_f32{,_scalar} `build` calls the dispatchers, so #201/#202 only fill in SIMD variants and wire dispatch. Behavior unchanged (dual-mode C++ parity test still green). - Add crates/core/benches/gaussian_pyramid_bench.rs benchmarking each scalar helper plus end-to-end `build` (num_octaves from num_octaves_for(w, h, 8), matching rust_backend) at 640x480, 1280x720 and 1920x1080 with fixed-seed PRNG input. - Wire `[[bench]] name = "gaussian_pyramid_bench"` into Cargo.toml. Baseline (median, this machine): binomial_f32 640x480 710 us | 1280x720 2.04 ms | 1920x1080 5.26 ms binomial_u8 640x480 546 us | 1280x720 1.60 ms | 1920x1080 3.38 ms bilinear 640x480 138 us | 1280x720 381 us | 1920x1080 1.16 ms build (e2e) 640x480 3.11 ms | 1280x720 9.95 ms | 1920x1080 26.0 ms Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(kpm): parallelize Gaussian pyramid filter passes with rayon (#207) (#208) The binomial filter and bilinear downsample of GaussianScaleSpacePyramid are memory-bandwidth bound, so the SIMD attempt (#201/#206) gave ~1.0× — a single core can't saturate DRAM bandwidth. Multiple cores can, so parallelize over rows with rayon (native; wasm stays single-threaded). - Per-row helpers (binomial_{h,v}_row_{f32,u8}, bilinear_row_f32) are the single source of truth for the arithmetic; serial and rayon paths both call them, so output is bit-for-bit identical regardless of threading (rows are independent). Validated by serial-vs-rayon parity tests + the existing dual-mode C++ parity test (scalar == C++, rayon == scalar). - Added *_rayon variants (par_chunks_mut over rows) for both binomial variants and the bilinear downsample, cfg(not(wasm32)). - Dispatchers pick rayon above PARALLEL_MIN_PIXELS (600k px), else serial; wasm always serial. The threshold keeps <=480p serial (where thread overhead regressed the binomial filter) and parallelizes 720p+. - Bench extended with a `rayon` arm per group. Speedup (scalar -> rayon, same-run medians, this machine): binomial_f32 720p 2.75->1.71 ms (1.61x) | 1080p 4.33->2.84 ms (1.52x) binomial_u8 720p 1.69->1.42 ms (1.19x) | 1080p 3.55->2.36 ms (1.50x) bilinear 720p 0.40->0.17 ms (2.36x) | 1080p 1.01->0.57 ms (1.77x) (<=480p stays serial by design.) Closes #207. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * test(kpm): skip large rayon-parity tests under Miri; drop redundant h alias The #207 rayon parity tests build pyramids at up to 800×800 — astronomically slow under Miri's interpreter (the run was stuck for 2h). #207 adds no unsafe (rayon is safe), so Miri has nothing to validate there. Annotate the four large-image tests with #[cfg_attr(miri, ignore)], matching the existing #194 idiom; they still run natively. Also replace the redundant `let h = height;` alias in binomial_v_row_{f32,u8} with `height` directly — the elided binding showed as 2 uncovered lines in the tarpaulin/codecov patch report. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 97d6bfb commit db70e09

3 files changed

Lines changed: 627 additions & 147 deletions

File tree

crates/core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,7 @@ required-features = ["simd-x86-sse41"]
153153
name = "feature_map_bench"
154154
harness = false
155155
required-features = ["log-helpers"]
156+
157+
[[bench]]
158+
name = "gaussian_pyramid_bench"
159+
harness = false
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* gaussian_pyramid_bench.rs
3+
* WebARKitLib-rs
4+
*
5+
* This file is part of WebARKitLib-rs - WebARKit.
6+
*
7+
* WebARKitLib-rs is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Lesser General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* WebARKitLib-rs is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Lesser General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with WebARKitLib-rs. If not, see <http://www.gnu.org/licenses/>.
19+
*
20+
* As a special exception, the copyright holders of this library give you
21+
* permission to link this library with independent modules to produce an
22+
* executable, regardless of the license terms of these independent modules, and to
23+
* copy and distribute the resulting executable under terms of your choice,
24+
* provided that you also meet, for each linked independent module, the terms and
25+
* conditions of the license of that module. An independent module is a module
26+
* which is neither derived from nor based on this library. If you modify this
27+
* library, you may extend this exception to your version of the library, but you
28+
* are not obligated to do so. If you do not wish to do so, delete this exception
29+
* statement from your version.
30+
*
31+
* Copyright 2026 WebARKit.
32+
*
33+
* Author(s): Walter Perdan @kalwalt https://github.com/kalwalt
34+
*
35+
*/
36+
37+
//! Criterion baseline for the **Gaussian scale-space pyramid** (#200) — the
38+
//! pyramid the KPM/FREAK detector actually uses (via
39+
//! `DoGScaleInvariantDetector`).
40+
//!
41+
//! Establishes the scalar baseline so the SIMD work can prove its speedup:
42+
//! the binomial 5-tap filter (#201) and the bilinear downsample (#202).
43+
//!
44+
//! Groups, each at 640×480, 1280×720 and 1920×1080:
45+
//!
46+
//! - `gaussian_binomial_u8`: `binomial_4th_order_u8_to_f32_scalar` (octave 0).
47+
//! - `gaussian_binomial_f32`: `binomial_4th_order_f32_to_f32_scalar` (the
48+
//! dominant per-octave cost — runs 3× per octave).
49+
//! - `gaussian_bilinear`: `downsample_bilinear_f32_scalar` (between octaves).
50+
//! - `gaussian_build`: end-to-end `GaussianScaleSpacePyramid::build` with
51+
//! `num_octaves` from `num_octaves_for(w, h, 8)`, matching the real KPM
52+
//! backend (`rust_backend.rs`).
53+
//!
54+
//! Inputs are filled deterministically with a fixed-seed PRNG so runs are
55+
//! comparable across machines and across PRs in the series.
56+
57+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
58+
use purecv::core::Matrix;
59+
use rand::rngs::StdRng;
60+
use rand::{Rng, SeedableRng};
61+
use std::hint::black_box;
62+
use webarkitlib_rs::kpm::freak::gaussian_pyramid::{
63+
binomial_4th_order_f32_to_f32_scalar, binomial_4th_order_u8_to_f32_scalar,
64+
downsample_bilinear_f32_scalar,
65+
};
66+
use webarkitlib_rs::kpm::freak::{num_octaves_for, GaussianScaleSpacePyramid};
67+
68+
#[cfg(not(target_arch = "wasm32"))]
69+
use webarkitlib_rs::kpm::freak::gaussian_pyramid::{
70+
binomial_4th_order_f32_to_f32_rayon, binomial_4th_order_u8_to_f32_rayon,
71+
downsample_bilinear_f32_rayon,
72+
};
73+
74+
/// Typical AR camera input resolutions as `(width, height)`.
75+
const SIZES: &[(usize, usize)] = &[(640, 480), (1280, 720), (1920, 1080)];
76+
77+
/// `min_size` for `num_octaves_for`, matching the real KPM backend.
78+
const MIN_COARSE_SIZE: usize = 8;
79+
80+
/// Deterministic grayscale `Matrix<u8>` (`rows × cols`), fixed-seed PRNG.
81+
fn random_u8(rows: usize, cols: usize) -> Matrix<u8> {
82+
let mut rng = StdRng::seed_from_u64(0x6A05_51A2);
83+
let data: Vec<u8> = (0..rows * cols).map(|_| rng.random::<u8>()).collect();
84+
Matrix::<u8>::from_vec(rows, cols, 1, data)
85+
}
86+
87+
/// Deterministic `Matrix<f32>` with values in `[0, 256)`, fixed-seed PRNG.
88+
fn random_f32(rows: usize, cols: usize) -> Matrix<f32> {
89+
let mut rng = StdRng::seed_from_u64(0x6A05_51A3);
90+
let data: Vec<f32> = (0..rows * cols)
91+
.map(|_| rng.random::<u8>() as f32)
92+
.collect();
93+
Matrix::<f32>::from_vec(rows, cols, 1, data)
94+
}
95+
96+
fn bench_binomial_u8(c: &mut Criterion) {
97+
let mut group = c.benchmark_group("gaussian_binomial_u8");
98+
for &(w, h) in SIZES {
99+
let src = random_u8(h, w);
100+
group.throughput(Throughput::Elements((w * h) as u64));
101+
group.bench_with_input(
102+
BenchmarkId::new("scalar", format!("{w}x{h}")),
103+
&src,
104+
|bencher, src| bencher.iter(|| binomial_4th_order_u8_to_f32_scalar(black_box(src))),
105+
);
106+
107+
#[cfg(not(target_arch = "wasm32"))]
108+
group.bench_with_input(
109+
BenchmarkId::new("rayon", format!("{w}x{h}")),
110+
&src,
111+
|bencher, src| bencher.iter(|| binomial_4th_order_u8_to_f32_rayon(black_box(src))),
112+
);
113+
}
114+
group.finish();
115+
}
116+
117+
fn bench_binomial_f32(c: &mut Criterion) {
118+
let mut group = c.benchmark_group("gaussian_binomial_f32");
119+
for &(w, h) in SIZES {
120+
let src = random_f32(h, w);
121+
group.throughput(Throughput::Elements((w * h) as u64));
122+
group.bench_with_input(
123+
BenchmarkId::new("scalar", format!("{w}x{h}")),
124+
&src,
125+
|bencher, src| bencher.iter(|| binomial_4th_order_f32_to_f32_scalar(black_box(src))),
126+
);
127+
128+
#[cfg(not(target_arch = "wasm32"))]
129+
group.bench_with_input(
130+
BenchmarkId::new("rayon", format!("{w}x{h}")),
131+
&src,
132+
|bencher, src| bencher.iter(|| binomial_4th_order_f32_to_f32_rayon(black_box(src))),
133+
);
134+
}
135+
group.finish();
136+
}
137+
138+
fn bench_bilinear(c: &mut Criterion) {
139+
let mut group = c.benchmark_group("gaussian_bilinear");
140+
for &(w, h) in SIZES {
141+
let src = random_f32(h, w);
142+
group.throughput(Throughput::Elements((w * h) as u64));
143+
group.bench_with_input(
144+
BenchmarkId::new("scalar", format!("{w}x{h}")),
145+
&src,
146+
|bencher, src| bencher.iter(|| downsample_bilinear_f32_scalar(black_box(src))),
147+
);
148+
149+
#[cfg(not(target_arch = "wasm32"))]
150+
group.bench_with_input(
151+
BenchmarkId::new("rayon", format!("{w}x{h}")),
152+
&src,
153+
|bencher, src| bencher.iter(|| downsample_bilinear_f32_rayon(black_box(src))),
154+
);
155+
}
156+
group.finish();
157+
}
158+
159+
fn bench_build(c: &mut Criterion) {
160+
let mut group = c.benchmark_group("gaussian_build");
161+
for &(w, h) in SIZES {
162+
let src = random_u8(h, w);
163+
let n_oct = num_octaves_for(w, h, MIN_COARSE_SIZE);
164+
group.throughput(Throughput::Elements((w * h) as u64));
165+
group.bench_with_input(
166+
BenchmarkId::new("scalar", format!("{w}x{h}")),
167+
&src,
168+
|bencher, src| {
169+
bencher.iter(|| {
170+
let mut p = GaussianScaleSpacePyramid::new(n_oct);
171+
p.build(black_box(src)).expect("build should succeed");
172+
p
173+
})
174+
},
175+
);
176+
}
177+
group.finish();
178+
}
179+
180+
criterion_group!(
181+
benches,
182+
bench_binomial_u8,
183+
bench_binomial_f32,
184+
bench_bilinear,
185+
bench_build
186+
);
187+
criterion_main!(benches);

0 commit comments

Comments
 (0)