Skip to content

Commit ec28cc9

Browse files
kalwaltclaude
andauthored
perf(kpm): box-filter Pyramid downsample — criterion benchmark + SIMD (#211)
* perf(kpm): add criterion benchmark for Pyramid downsample (#131) Establish the scalar baseline for the pyramid downsample trilogy so the SIMD path (#132) and any chunked-layout work (#133) can prove speedups against stable numbers. - Promote the private `downsample` to `pub fn downsample_scalar` and add a `pub fn downsample` dispatcher (scalar-only for now; #132 wires in the SIMD paths). Both are `pub` so the criterion bench (a separate crate) can measure them directly; `Pyramid::build` is the stable API. - Add `crates/core/benches/pyramid_bench.rs` benchmarking `downsample_scalar` and `Pyramid::build` (num_levels=4) at 640x480, 1280x720 and 1920x1080 with a fixed-seed PRNG input. - Wire `[[bench]] name = "pyramid_bench"` into Cargo.toml. Baseline (median, this machine): downsample 640x480 ~116 us | 1280x720 ~341 us | 1920x1080 ~884 us pyramid_build 640x480 ~159 us | 1280x720 ~597 us | 1920x1080 ~1.87 ms Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(kpm): SIMD path for Pyramid downsample (AVX2/SSE4.1/wasm32) (#132) Add runtime-dispatched SIMD implementations of the pyramid downsample, each byte-identical to the scalar baseline established in #131. - `downsample_avx2`: 32 output cols/iter via 256-bit lanes (`maddubs` horizontal pairwise add + `packus` + `permute4x64` to undo the per-lane interleave). - `downsample_sse41`: 16 output cols/iter via 128-bit lanes. - `downsample_wasm`: 16 output cols/iter via `simd128` (`extadd_pairwise_u8x16` + `narrow`). - `downsample` dispatches AVX2 -> SSE4.1 -> scalar on x86_64 (runtime `is_x86_feature_detected!`) and simd128 on wasm32; scalar remains the fallback. Each `unsafe` site carries a `// SAFETY:` comment. - Shared `downsample_dims` + `downsample_row_tail_scalar` helpers define the rounding in one place; the SIMD main loops handle full blocks and defer the per-row remainder to the scalar tail. - Block counts are sized so loads/stores stay in bounds for any dimension (incl. odd / sub-block widths). Correctness: property tests compare each SIMD path against scalar over 16 sizes (odd dims, tiny images, block boundaries) plus a dispatcher-parity test. `cargo clippy --all-targets --all-features -- -D warnings` clean. Speedup (downsample, median ns -> us, this machine): 640x480 scalar 85.8 | sse41 7.4 (11.6x) | avx2 5.8 (14.7x) 1280x720 scalar 251 | sse41 25.0 (10.0x) | avx2 18.6 (13.5x) 1920x1080 scalar 773 | sse41 96.1 (8.0x) | avx2 75.9 (10.2x) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(kpm): exhaustive width-sweep parity for SIMD downsample (#132) Add a 2..=160 column sweep (x4 row counts) comparing both the AVX2 and SSE4.1 paths against scalar, covering every remainder case of the 16- and 32-wide main loops. Strengthens confidence beyond the hand-picked sizes that each SIMD path is byte-identical to scalar for all dimensions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent db70e09 commit ec28cc9

3 files changed

Lines changed: 578 additions & 10 deletions

File tree

crates/core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ name = "feature_map_bench"
154154
harness = false
155155
required-features = ["log-helpers"]
156156

157+
[[bench]]
158+
name = "pyramid_bench"
159+
harness = false
160+
157161
[[bench]]
158162
name = "gaussian_pyramid_bench"
159163
harness = false
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* 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 `kpm::freak::pyramid` downsampling (#131).
38+
//!
39+
//! Establishes the scalar baseline so the SIMD path (#132) and any
40+
//! chunked-layout work (#133) can prove their speedup against stable
41+
//! numbers. Two groups are measured at typical AR input resolutions
42+
//! (640×480, 1280×720, 1920×1080):
43+
//!
44+
//! - `downsample`: one 2×2 box-filter decimation step (the hot inner loop).
45+
//! - `pyramid_build`: end-to-end `Pyramid::build` with `num_levels = 4`.
46+
//!
47+
//! Inputs are filled deterministically with a fixed-seed PRNG so runs are
48+
//! comparable across machines and across PRs in the trilogy.
49+
50+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
51+
use purecv::core::Matrix;
52+
use rand::rngs::StdRng;
53+
use rand::{Rng, SeedableRng};
54+
use std::hint::black_box;
55+
use webarkitlib_rs::kpm::freak::pyramid::{downsample_scalar, Pyramid};
56+
57+
#[cfg(all(target_arch = "x86_64", feature = "simd-x86-sse41"))]
58+
use webarkitlib_rs::kpm::freak::pyramid::downsample_sse41;
59+
60+
#[cfg(all(target_arch = "x86_64", feature = "simd-x86-avx2"))]
61+
use webarkitlib_rs::kpm::freak::pyramid::downsample_avx2;
62+
63+
/// Typical AR camera input resolutions as `(width, height)`.
64+
const SIZES: &[(usize, usize)] = &[(640, 480), (1280, 720), (1920, 1080)];
65+
66+
/// Build a deterministic grayscale `Matrix<u8>` of `rows × cols` using a
67+
/// fixed-seed PRNG, so every benchmark run sees identical pixel data.
68+
fn random_gray(rows: usize, cols: usize) -> Matrix<u8> {
69+
let mut rng = StdRng::seed_from_u64(0xA12B_C3D4);
70+
let data: Vec<u8> = (0..rows * cols).map(|_| rng.random::<u8>()).collect();
71+
Matrix::<u8>::from_vec(rows, cols, 1, data)
72+
}
73+
74+
fn bench_downsample(c: &mut Criterion) {
75+
let mut group = c.benchmark_group("downsample");
76+
for &(w, h) in SIZES {
77+
let src = random_gray(h, w);
78+
// One output pixel per 2×2 source block; throughput is measured in
79+
// source pixels touched.
80+
group.throughput(Throughput::Elements((w * h) as u64));
81+
group.bench_with_input(
82+
BenchmarkId::new("scalar", format!("{w}x{h}")),
83+
&src,
84+
|bencher, src| bencher.iter(|| downsample_scalar(black_box(src))),
85+
);
86+
87+
#[cfg(all(target_arch = "x86_64", feature = "simd-x86-sse41"))]
88+
if is_x86_feature_detected!("sse4.1") {
89+
group.bench_with_input(
90+
BenchmarkId::new("sse41", format!("{w}x{h}")),
91+
&src,
92+
// SAFETY: sse4.1 confirmed available by the runtime check above.
93+
|bencher, src| bencher.iter(|| unsafe { downsample_sse41(black_box(src)) }),
94+
);
95+
}
96+
97+
#[cfg(all(target_arch = "x86_64", feature = "simd-x86-avx2"))]
98+
if is_x86_feature_detected!("avx2") {
99+
group.bench_with_input(
100+
BenchmarkId::new("avx2", format!("{w}x{h}")),
101+
&src,
102+
// SAFETY: avx2 confirmed available by the runtime check above.
103+
|bencher, src| bencher.iter(|| unsafe { downsample_avx2(black_box(src)) }),
104+
);
105+
}
106+
}
107+
group.finish();
108+
}
109+
110+
fn bench_pyramid_build(c: &mut Criterion) {
111+
let mut group = c.benchmark_group("pyramid_build");
112+
for &(w, h) in SIZES {
113+
let src = random_gray(h, w);
114+
group.throughput(Throughput::Elements((w * h) as u64));
115+
group.bench_with_input(
116+
BenchmarkId::new("levels4", format!("{w}x{h}")),
117+
&src,
118+
|bencher, src| {
119+
bencher.iter(|| {
120+
let mut p = Pyramid::new(4, 2.0);
121+
p.build(black_box(src)).expect("build should succeed");
122+
p
123+
})
124+
},
125+
);
126+
}
127+
group.finish();
128+
}
129+
130+
criterion_group!(benches, bench_downsample, bench_pyramid_build);
131+
criterion_main!(benches);

0 commit comments

Comments
 (0)