Skip to content

speed: fuse scalar downsampling and add AArch64 NEON covariance - #1653

Merged
kylophone merged 3 commits into
Netflix:masterfrom
dantrapp:perf/speed-neon
Oct 2, 2026
Merged

kylophone merged 3 commits into
Netflix:masterfrom
dantrapp:perf/speed-neon

Conversation

@dantrapp

@dantrapp dantrapp commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

SpEED's scalar antialias filter computes a full image before decimation retains one sample per 16 rows and columns. This computes the vertical pass only at retained rows and the horizontal pass only at retained columns, preserving the scalar accumulation order and edge reflection. The existing x86 filtering path is unchanged.

The AArch64 covariance kernel now uses NEON with double-precision accumulation and scalar handling of the final odd sample. Tests cover short widths, vector tails, unaligned inputs, padded strides, and constant and near-mean inputs. The fused filter is checked byte-for-byte against full filtering followed by decimation.

Performance

Apple M4, macOS 15.6.1, Apple Clang 17, release builds, one thread. Medians of seven runs with alternating build order and one warmup per build; baseline is 6ec23e8, candidate is 3632e81.

1080p workload Baseline This branch Speedup
SpEED chroma 0.819 s 0.095 s 8.65×
SpEED temporal 1.608 s 0.194 s 8.30×
VMAF v1.0.16, 3d0h model 1.649 s 0.935 s 1.76×

Inputs are the src01_hrc00_1920x1080_5frames.yuv and src01_hrc01_1920x1080_5frames.yuv clips from Netflix/vmaf_resource, each repeated ten times to make a 50-frame sequence. Timings include the CLI process and file reads. These measurements use repeated content on one machine.

Validation

  • Release: 24/24 tests pass.
  • AddressSanitizer and UndefinedBehaviorSanitizer: 24/24 pass.
  • Assembly disabled: 23/23 pass.
  • Floating-point features enabled: 26/26 pass.
  • All 3,743 feature and VMAF values compared through the public API are bit-for-bit identical across 15 cases. Inputs cover two upstream 8-bit clips and 10/12/16-bit variants made by widening and shifting the SD samples.

Validation was performed on Apple M4; x86 execution was not tested locally.

Compute the vertical pass only at retained rows and the horizontal pass only at retained columns. Preserve the scalar filter's accumulation order and edge reflection. Keep the existing x86 convolution path.

Add exact-output tests covering filter widths, image patterns, unaligned inputs, and padded strides.
@kylophone

Copy link
Copy Markdown
Collaborator

Hi @dantrapp. These changes look promising, thank you. Can you please also add a checkasm test for this?

@dantrapp

dantrapp commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look, Kyle! Added in da332c2. The new checkasm test compares the fused filter against the original filter-and-decimate path and benchmarks both. Release and sanitizer checks pass.

@kylophone
kylophone merged commit cea2b4d into Netflix:master Oct 2, 2026
@lusoris

lusoris commented Oct 2, 2026

Copy link
Copy Markdown

Tested #1653 at cea2b4d on x86-64 and on aarch64 under QEMU; no defect found. The description says x86 execution was not tested locally, so this covers that side.

Builds and tests (-Denable_float=true -Denable_checkasm=true, 8e7a1ac vs cea2b4d):

Build 8e7a1ac cea2b4d
x86-64 release, GCC 16.2.1 24/24 25/25
x86-64 release, Clang 22.1.8 24/24 25/25
aarch64-linux-gnu-gcc 16.1.0 release, qemu-aarch64 11.1.1 (default max CPU, NEON emulated) 25/25 26/26

The extra test is test_speed_filter. checkasm --test=speed --repeat=50 passes on x86 (both compilers) and under QEMU (NEON and C variants). With ASan/UBSan the only failures are test_predict and test_pic_preallocation (leaks) and the ADM checkasm, and they fail identically on 8e7a1ac. Under GCC ASan the AVX-512 compute_cov_kernel check also reports "missing vzeroupper", on both revisions.

Fused filter against filter-then-decimate. To run the fused path on x86 I changed #if ARCH_X86 in filter_and_downscale() to #if 0 locally (not part of the PR) and compared speed_chroma and speed_temporal through the C API at %.17g, with the default CPU mask and with SIMD masked off. Three Netflix pairs (576x324 x 48 frames, 1920x1080 x 3 frames twice) plus synthetic 4:2:0 frames from 160x160 to 1921x1081, including sizes that are not multiples of 16 or 2 (161x161, 255x257, 321x241, 335x199, 417x333, 577x325, 1279x719), with speed_kernelscale 0.5, 1, 1.5, 2, 3, 1.0667, 0.7111 (and 0.74074 on x86). That is 304 comparisons and 1888 frames on x86: every score is bit-identical, with and without AVX2. The AVX2 convolution and the scalar fused loop agree exactly on all of them. vif_filter1d_dec16_s() called on exactly-sized heap buffers under ASan/UBSan against vif_filter1d_s() + vif_dec16_s(): 18630 size/padding/kernelscale combinations, no mismatch, no report.

aarch64. Same inputs, 8e7a1ac vs cea2b4d under QEMU, 130 configurations, 856 frames, 3304 scores: all bit-identical, with NEON on (fused filter and NEON kernel) and with NEON masked off (fused filter only). New build with NEON on vs masked off: also identical.

NEON covariance kernel. Against compute_cov_kernel_scalar it is not bit-exact: for 23629 random, constant, near-mean, denormal and large-magnitude inputs (widths 1..70 and up to 4099, strides w..w+3, unaligned starts) 5729 sums differ in the last bits, max relative difference 3.3e-13, which is within the 1e-10 checkasm tolerance and does not change any float score above. A guard-page run (rows ending on an unmapped page, 640 shape/stride combinations) shows no over-read.

Timings under QEMU are not meaningful and I did not measure any.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants