Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions changelog.d/fixed/neon-fma-safe-float-adm-dwt2-bitexact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
- **float-ADM NEON DWT2 is now bit-exact with the scalar reference on
aarch64**: completes the ADR-1057 follow-up. The dispatched NEON DWT2
kernel (`float_adm_dwt2_neon`) is FMA-free and compiled `-ffp-contract=off`,
but on aarch64 the scalar reference `adm_dwt2_s`/`adm_dwt2_lo_s` were being
FMA-contracted by the compiler default (`-ffp-contract=fast` on GCC,
`on` on Clang — both fuse `accum += a*b` into a single-rounded `fmadd`),
so the runtime scalar-fallback path diverged from the NEON path by ~1 ULP.
(This was the actual root cause behind the PR #685 -> #695 revert; the
divergence was the *scalar* side fusing, not only the NEON intrinsics.)
Fix: `adm_tools.c` now includes `config.h` (the prior `#ifdef HAVE_CONFIG_H`
guard was dead, so `ARCH_AARCH64` never reached the TU), and the two scalar
single-precision DWT2 functions carry an aarch64-only `-ffp-contract=off`
guard (`#pragma clang fp contract(off)` for Clang, the `optimize` function
attribute for GCC). x86 is byte-for-byte unaffected — `ARCH_AARCH64` is
undefined there and GCC already defaults to `-ffp-contract=off` on x86 — so
the Netflix golden gate is untouched. A new load-bearing parity test,
`test_float_adm_simd` (`test_float_adm_dwt2_bitexact`-equivalent), asserts
`float_adm_dwt2_neon == adm_dwt2_s` via `memcmp` across nine fixtures and is
registered in the `fast`/`simd` suites. Verified bit-exact on GCC 16.1 and
Clang 22 under `qemu-aarch64` across all nine cases (the original kernel
diverged on all nine — negative control confirmed). (ADR-1057)
53 changes: 50 additions & 3 deletions core/src/feature/adm_tools.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@
#include <stddef.h>
#include <string.h>

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
/* ADR-1057: include the generated config.h unconditionally so ARCH_AARCH64 is
* visible in this TU. The previous `#ifdef HAVE_CONFIG_H` guard was dead — the
* feature static library is not compiled with `-DHAVE_CONFIG_H`, so the macro
* never reached this file. Mirrors cpu.h, which includes config.h directly.
* The aarch64 FMA-contract guards on adm_dwt2_s / adm_dwt2_lo_s below depend on
* ARCH_AARCH64 being defined here. */
#include "config.h" // IWYU pragma: keep (provides ARCH_AARCH64 macro)

#include "mem.h"
#include "adm_options.h"
Expand Down Expand Up @@ -1078,6 +1082,32 @@ void dwt2_src_indices_filt_s(int **src_ind_y, int **src_ind_x, int w, int h)
}
}

/* aarch64 FMA-contract guard (ADR-1057).
*
* On AArch64 every core has a hardware fused multiply-add, so GCC
* (`-ffp-contract=fast`) and Clang (`-ffp-contract=on`) fuse the
* `accum += filter[k] * sN` accumulations below into a single-rounded
* `fmadd`. The dispatched NEON kernel `float_adm_dwt2_neon` is deliberately
* FMA-free (explicit `vmulq` + `vaddq`, two roundings) and its TU is built
* `-ffp-contract=off`. Without this guard the NEON path and this scalar
* fallback diverge by ~1 ULP on aarch64 — the exact gap that reverted
* PR #685 (see ADR-1057) and that `test_float_adm_simd` asserts against.
*
* x86 is intentionally left untouched: GCC defaults to `-ffp-contract=off`
* on x86 (no within-statement fusion of `a*b+c`), and float-ADM DWT2 is the
* scalar path on x86 (no AVX2 DWT2 dispatch), so this is the function the
* Netflix golden gate exercises. Gating the guard on `ARCH_AARCH64` keeps the
* x86 golden bytes byte-identical while making the aarch64 scalar fallback
* bit-exact with the NEON kernel. Clang on aarch64 honours the `#pragma`;
* GCC honours the function attribute. */
#if defined(ARCH_AARCH64) && ARCH_AARCH64
#if defined(__clang__)
#pragma clang fp contract(off)
#endif
#if defined(__GNUC__) && !defined(__clang__)
__attribute__((optimize("-ffp-contract=off")))
#endif
#endif
int adm_dwt2_s(const float *src, const adm_dwt_band_t_s *dst, int **ind_y, int **ind_x, int w,
int h, int src_stride, int dst_stride)
{
Expand Down Expand Up @@ -1175,6 +1205,16 @@ int adm_dwt2_s(const float *src, const adm_dwt_band_t_s *dst, int **ind_y, int *
return 0;
}

/* aarch64 FMA-contract guard (ADR-1057) — see adm_dwt2_s above. `adm_dwt2_lo`
* has no SIMD twin (always scalar), but keeping the low-pass-only DWT2
* FMA-free on aarch64 holds the scale-0 lo-pass numerics consistent with the
* full DWT2 and with x86. The GCC attribute is per-function; the Clang
* `#pragma` set at adm_dwt2_s is still in effect here. */
#if defined(ARCH_AARCH64) && ARCH_AARCH64
#if defined(__GNUC__) && !defined(__clang__)
__attribute__((optimize("-ffp-contract=off")))
#endif
#endif
int adm_dwt2_lo_s(const float *src, const adm_dwt_band_t_s *dst, int **ind_y, int **ind_x, int w,
int h, int src_stride, int dst_stride)
{
Expand Down Expand Up @@ -1233,6 +1273,13 @@ int adm_dwt2_lo_s(const float *src, const adm_dwt_band_t_s *dst, int **ind_y, in
return 0;
}

/* Restore Clang's default FP contraction for the remaining (double-precision
* DWT2 + buffer-copy) functions; the `contract(off)` region set at adm_dwt2_s
* applied only to the two single-precision DWT2 kernels on aarch64. */
#if defined(ARCH_AARCH64) && ARCH_AARCH64 && defined(__clang__)
#pragma clang fp contract(on)
#endif

int adm_dwt2_d(const double *src, const adm_dwt_band_t_d *dst, int **ind_y, int **ind_x, int w,
int h, int src_stride, int dst_stride)
{
Expand Down
1 change: 1 addition & 0 deletions core/src/feature/arm64/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ with the matching change to the other halves in the **same PR**:
| **SSIMULACRA 2 SIMD** (ADR-0161 / 0162 / 0163 / 0213 / 0252) | `ssimulacra2_neon.c` + `ssimulacra2_sve2.c` + `../x86/ssimulacra2_avx2.c` + `../x86/ssimulacra2_avx512.c` + `ssimulacra2_host_neon.c` + `../x86/ssimulacra2_host_avx2.c` + scalar `../ssimulacra2.c` + Vulkan host-path `../vulkan/ssimulacra2_vulkan.c` |
| **CAMBI calculate_c_values_row** (ADR-0452) | `cambi_neon.c` (`calculate_c_values_row_neon`) + `../x86/cambi_avx2.c` (`calculate_c_values_row_avx2`) + `../x86/cambi_avx512.c` (`calculate_c_values_row_avx512`) + scalar in `../cambi.c` (`calculate_c_values_row`). Every cambi inner-loop function ported to AVX2 **must** have AVX-512 + NEON siblings in the **same PR**. NEON uses vectorised mask-zero detection (vmaxvq_u16) + scalar per-pixel inner loop for bit-exact output (no gather instruction on NEON). Tested in `../../test/test_cambi_simd.c`. |
| **Motion v2 NEON** (ADR-0145) | `motion_v2_neon.c` uses **arithmetic** right-shift (`vshrq_n_s64(v, 16)` / `vshlq_s64(v, -(int64_t)bpc)`); matches scalar. Sister `../x86/motion_v2_avx2.c` uses `_mm256_srlv_epi64` (logical) — knowingly out-of-spec until the AVX2 audit. **Do NOT port the AVX2 logical pattern here.** 4-lane stride + scalar tails on both sides of the row are load-bearing for the x_conv edge-mirror contract. |
| **float-ADM DWT2** (ADR-1057) | `float_adm_dwt2_neon.c` (FMA-free: `vmulq_laneq_f32` + `vaddq_f32`, **never** `vmlaq*` / `vfmaq*`; meson lib `arm64_adm_dwt2_neon_lib` built `-ffp-contract=off`) + scalar `../adm_tools.c` (`adm_dwt2_s` / `adm_dwt2_lo_s`). The scalar half carries an **aarch64-only `-ffp-contract=off` guard** (`#pragma clang fp contract(off)` + GCC `optimize` attribute) — without it the compiler fuses the scalar `accum += a*b` into a single-rounded `fmadd` on aarch64 and the NEON path diverges by ~1 ULP. `adm_tools.c` must keep its unconditional `#include "config.h"` (provides `ARCH_AARCH64`; the old `#ifdef HAVE_CONFIG_H` was dead). x86 is inert (guard gated on `ARCH_AARCH64`; GCC x86 default is already `-ffp-contract=off`) so the golden gate is untouched. Bit-exactness asserted by `../../test/test_float_adm_simd.c`. |

The complete invariants live in [../AGENTS.md
§"Rebase-sensitive invariants"](../AGENTS.md).
Expand Down
28 changes: 28 additions & 0 deletions core/test/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,34 @@ test_ms_ssim_decimate = executable('test_ms_ssim_decimate',
)
endif

# float-ADM NEON DWT2 bit-exactness parity test (ADR-1057).
# Asserts float_adm_dwt2_neon == adm_dwt2_s byte-for-byte. The kernel lives in
# the `arm64_adm_dwt2_neon_lib` carve-out (compiled `-ffp-contract=off`) inside
# platform_specific_cpu_objects; the scalar reference is in the feature static
# lib. The test self-skips the NEON comparison on non-aarch64 (kernel absent),
# so it builds and runs on every arch. `_simd_strict_fp_args` keeps the test TU
# itself free of host FMA contraction.
test_float_adm_simd = executable('test_float_adm_simd',
['test.c', 'test_float_adm_simd.c',
'../src/mem.c', '../src/picture.c', '../src/ref.c',
'../src/dict.c', '../src/predict.c',
'../src/metadata_handler.cpp', '../src/thread_locale.c', '../src/gpu_picture_pool.cpp', dnn_sources],
include_directories : [libvmaf_inc, test_inc, include_directories('../src/'),
include_directories('../src/feature/'), dnn_inc],
c_args : vmaf_cflags_common + dnn_defines + _simd_strict_fp_args,
dependencies : [math_lib, stdatomic_dependency, pthread_dependency, thread_lib,
gpu_all_deps, dnn_deps],
objects : [
common_cuda_objects,
platform_specific_cpu_objects,
libvmaf_cpu_static_lib.extract_all_objects(recursive: true),
libvmaf_feature_static_lib.extract_all_objects(recursive: true),
log_cpp23_test_objects,
libsvm_static_lib.extract_all_objects(recursive: true),
] + wave8_opt_only_objects,
)
test('test_float_adm_simd', test_float_adm_simd, suite : ['fast', 'simd'])

test_locale_handling = executable('test_locale_handling',
['test.c', 'test_locale_handling.c'],
include_directories : [
Expand Down
Loading
Loading