Repository navigation
feat(adm): make the x86 float ADM wavelet and CSF kernels exact and dispatch them (ADR-1473) - #1874
Merged
Merged
Conversation
…#1863) * fix(ci): repair three failures of the first full hosted run on master The first complete hosted run on master since 2026-09-30 (513d2a6, with the merge train paused for it) passed 76 checks and failed 11. Three small ones are fixed here: - Windows ARM64 MSVC, fast suite: test_c_cxx_enum_definition_contract keyed a dictionary by str(path.relative_to(root)), which has backslashes on Windows. It uses as_posix() now, and the msvcism preflight check also sees a dictionary-comprehension key. - Sanitizers (undefined): test_speed_filter (1620 cases) hit meson's default 30 s under UBSan on a hosted runner; it gets 180 s. - gosec: fuse_mount.go G204. A context deadline added by #1762 sat between the #nosec comment and the exec call it annotates; the comment is back on the call. T-CI-MASTER-FIRST-FULL-RUN-2026-10-02 lists all seven real failures and stays open for the other four (clang ciede test, SPDX lines on 31 files, tidy baselines, cppcheck). Not verified here: a Windows run and a hosted UBSan run.
…pers (ADR-1142) (#1873) * refactor(python): assemble the harness command builders from pure helpers (ADR-1142) compat/python-vmaf/__init__.py was the last of the files the SPDX backfill (#1739) could not touch: call_vmafexec() (118 lines) and call_vmafexec_multi_features() (77 lines) were baselined HISS-04 rows. Both keep their signatures. The command text comes from module-level helpers, one per part of the command: _vmafexec_base_command, _vmafexec_feature_flags, _vmafexec_model_flags, _vmafexec_model_overloads, _vmafexec_run_flags, _multi_features_run_arguments and _feature_argument. Fixed on the way: with motion_force_zero=True and two models, call_vmafexec() raised AssertionError, because its loop over the models replaced the argument with the string "true" and then failed its own type check on the second model (Netflix upstream has the same statement). The overload suffix is now built once per model by a helper that does not modify its arguments. The commit hook refuses any edit to this file while the two functions are oversized, so the fix cannot land separately. Verified: old module against new over 191 236 argument combinations of the three builders: 190 340 produce the same command or the same exception; the other 896 are the two-model case. Four new tests pin both command texts, the type check and the two-model case (the last fails on the old module). Netflix golden gate on x86-64 GCC: 271 passed, 12 skipped. HISS baseline 242 to 240. SPDX-License-Identifier: BSD-2-Clause-Patent (ADR-1250). Closes T-PYTHON-CALL-VMAFEXEC-FORCE-ZERO-SECOND-MODEL-2026-10-02. * docs: regenerate the indexes and the citation map after rebasing
…ispatch them (ADR-1473) (#1874) * feat(adm): make the x86 float ADM wavelet and CSF kernels exact and dispatch them (ADR-1473) float_adm_avx2.c and float_adm_avx512.c held four kernels that nothing called and no test linked (ADR-1057 reverted the dispatch), and each differed from the scalar code: - wavelet: started a four-tap sum at the first product where adm_dwt2_s() starts at +0, so -0 for +0 on frames of signed zeros - CSF: multiplied FLOAT_ONE_BY_30 * |dst| in float where the scalar multiplies in double (0.9 % of values differ in the last bit) - denominator reduction and sum of cubes: double lane sums where the reference adds fp32 in column order; compute_adm() never calls a sum of cubes Maintainer decision (popup): make them exact, test and wire them. Wavelet and CSF kernels now return the scalar bits: every four-tap sum starts at +0 and multiplies before it adds, the CSF filter is a double product narrowed to float, and the wavelet returns -ENOMEM like adm_dwt2_s(). The AVX2 wavelet filters the horizontal pass eight outputs at a time. adm.c selects the kernel per call from vmaf_get_cpu_flags(): adm_dwt2_dispatch() gains x86 branches, and the CSF stage takes its band kernel through adm_csf_planes_s(), whose default is the scalar adm_csf_plane_s() (upstream's element loop, verbatim). --cpumask 63 / 48 / 0 selects scalar / AVX2 / AVX-512. The two reduction kernels are removed. The denominator's bits are a sequence of fp32 additions; a kernel that keeps the order is bound by the same chain, and the stage is 2 % of the extractor. Bits: every recorded adm / float_adm output and model score equals the base's (1695 x86 cases, 1066 aarch64 cases); 305 float_adm fixture and option groups and three float models on 35 fixtures are identical across scalar, AVX2 and AVX-512. Netflix golden gate 271 passed, 12 skipped on x86 GCC and aarch64 GCC. test_float_adm_x86 compares the kernels with adm_dwt2_s() and adm_csf_plane_s() byte for byte (stride padding included; picture, fractional, 16-bit, signed-zero and special-value input; widths 17 to 576) and compute_adm() across the three dispatch levels. 17 planted changes fail it. Time per frame, one thread, scalar to AVX-512: 576x324 1.45 to 1.11 ms, 1920x1080 17.9 to 15.1 ms, 3840x2160 76.2 to 62.9 ms. Twins: float_adm gate cells 0 on CUDA (RTX 4090), HIP (gfx1036) and SYCL (Arc A380). * docs: regenerate the indexes and the citation map after rebasing
lusoris
force-pushed
the
feat/float-adm-x86-simd-exact
branch
from
October 2, 2026 17:48
ddc5c7d to
0970c56
Compare
This was referenced Oct 2, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The x86 float ADM kernels existed, were called by nothing and tested by nothing, and differed from the scalar code (found in #1864). Maintainer decision (popup): make them exact, test them and wire them. This PR does that for the wavelet and the CSF stage, and removes the two reduction kernels, which cannot return the reference's bits (ADR-1473).
float_admis 13 to 24 % faster with AVX2 or AVX-512, and no score moves.What each kernel was, and is
float_adm_dwt2_avx2/_avx512adm_dwt2_s()+0. Identical on picture data,-0for+0on 2.5 % of the outputs of a frame of signed zerosfloat_adm_csf_avx2/_avx512adm_csf_s()FLOAT_ONE_BY_30 * fabsf(dst)in float; the scalar multiplies in double because the constant is a double literal. 0.9 % of values differ in the last bitfloat_adm_csf_den_scale_avx2/_avx512adm_csf_den_scale_s()float_adm_sum_cube_avx2/_avx512adm_sum_cube_s()compute_adm()never calls a sum of cubesWhat the scalar does (the brief asks):
adm_dwt2_s()and its two pass helpers carry a contraction guard (GCCoptimize("-ffp-contract=off"), Clang pragma; ADR-1057), and the whole tree builds with strict floating point since #1829. So the scalar multiplies, then adds; nothing is fused. The kernels do the same:_mm256_add_ps(accum, _mm256_mul_ps(c, s))from a zero vector, nofmadd.Why the reductions go: the denominator's bits are a sequence of
floatadditions in column order (inner[k] += val * val * val, folded per row;adm_fold3_s()says these accumulators are the golden-gated arithmetic). A kernel that keeps that order adds one value per step and is bound by the same addition chain as the scalar loop; one that does not changes the bits. The stage is 1.9 % of the extractor at 3840x2160. RC7 rowT-FLOAT-ADM-X86-SCALAR-STAGES-2026-10-02holds it together with the decouple and the contrast masking, which never had kernels.Wiring
adm.cchooses the kernel per call fromvmaf_get_cpu_flags(), asfloat_motion,float_ssim,float_psnrand the others choose theirs:adm_dwt2_dispatch()(it already carried the NEON branch) gains AVX-512 and AVX2 branches;adm_csf_planes_s(..., plane).adm_csf_s()is that function with the scalaradm_csf_plane_s(), which is upstream's element loop with its three statements verbatim.--cpumask 63runs the scalar code,48AVX2,0AVX-512 (perfshowsfloat_adm_dwt2_avx2/float_adm_csf_avx2and the_avx512pair under the two settings). The wavelet kernels now return-ENOMEMlikeadm_dwt2_s(); they used to return nothing and leave the bands unwritten. The AVX2 wavelet also filters its horizontal pass eight outputs at a time (it was scalar).Bits
Base: master
b34732ae1. Everything at--precision max.adm/float_admoutput (debug=true, 21 option sets) and the default andvmaf_v0.6.1scores, 35 fixtures, scalar / AVX2 / AVX-512374e4342a); on this head 1030 identical to the record taken before #1872, and the 36 that differ are the integeradm_csf_mode=1set, which #1872 changedfloat_admalone: fixture x option-set groups compared across the three dispatch levelsvmaf_float_v0.6.1,vmaf_float_v0.6.1neg,vmaf_float_4k_v0.6.1on 35 fixtures, per dispatch levelFixtures: Netflix 576x324 at 8, 10, 12 and 16 bits, both 1920x1080 checkerboards, BBB 3840x2160 (50 frames), 14 sizes from 17x17 to 129x65 at 8 and 10 bits. Against the x86 record taken before #1872, 1635 cases are identical and the 60 that differ are the integer
adm_csf_mode=1set.Test
core/test/test_float_adm_x86.c(new, suitesfastandsimd), 9 tests:adm_dwt2_s(): every band compared byte for byte with the stride padding (both sides start from one poison pattern, so a store outside the plane shows); 29 widths from 17 to 576 (around the 8- and 16-wide loops of both passes) at five heights from 17; picture, fractional and 16-bit data; frames of signed zeros; NaN, infinities and denormals;adm_csf_plane_s(): every width from 1 to 70, six input classes, five weights including one that overflows;compute_adm()on six sizes from 576x324 to 17x17 with the kernels masked off, with AVX2 and with everything: the twelve doubles it returns have the same bits.One limit: where two different NaNs meet in a sum, scalar and vector code can return NaNs of different sign or payload (the result is the first operand's, and a compiler may commute an addition). For the special-value class the test compares where the NaNs are, not their payload. A NaN in a frame fails the frame anyway (ADR-1302).
17 planted changes fail the test: a sum started at the first product (vector and scalar, both instruction sets), a fused multiply-add, two taps added in the other order, a horizontal loop one column too far, a wrong deinterleave, a horizontal tap from the wrong elements, the CSF filter as a float product (both kernels, and in the reference), a CSF loop that writes past the plane, swapped index tables in the dispatch. An 18th, the AVX-512 CSF falling back to the scalar function, passes by construction: the bits are the same.
Golden, twins, timing
Netflix golden gate: 271 passed, 12 skipped on x86-64 GCC and on aarch64 GCC under qemu, on this head.
x86
--suite=fast: 246 of 246. The three GPU contract tests that pin the reference's text (test_sycl_float_adm_exact_contract,test_cuda_float_adm_exact_contract,test_hip_float_adm_exact_contract) pass:adm_csf_plane_s()keeps upstream's statement verbatim.Twins. The CPU's values do not move, so the twins stay exact. Measured:
float_adm, cpu against devicefloat_adm_cudatest_cuda_float_adm_parity,_parity_large: 17 of 17 eachtest_hip_float_adm_parity,_parity_large(19 each),test_hip_float_adm_math(3), first-frame testtest_sycl_float_adm_parity,_parity_large(21 each),test_sycl_float_adm_math(7)HIP and SYCL were run on the tree before the rebase onto
b34732ae1(same sources in every file this PR touches).Timing. Whole
vmaf --feature float_admrun (file reading and picture copy included), one thread, median of five, Ryzen 9 9950X3D, load average 22 to 28:--cpumask 63)48)0)Before the change all three rows of a size run the same scalar code; their spread (and the scalar row's before / after difference) is the noise of a loaded host. Profile at 3840x2160: the wavelet goes from 23 % to 11 %; the decouple (27 %) and the contrast masking (20 %) now lead.
NEON
float_adm_dwt2_neon()is dispatched (adm_dwt2_dispatch(), since ADR-1057's update) and does not have the signed-zero defect: every sum starts atvdupq_n_f32(0.0f), andtest_float_adm_dwt2_neonhas a signed-zero case (it passed under qemu before and after #1865). Nothing to fix.The other three NEON kernels (
float_adm_csf_neon,float_adm_csf_den_scale_neon,float_adm_sum_cube_neon) are built and not dispatched, and by construction have the differences listed above for their x86 counterparts; their unit test compares them with a reference of its own, not with the scalar functions. I did not change them (not measured on aarch64; recorded in the RC7 row).Type
feat— new featurefix— bug fixperf— performance improvementrefactor— no behavior changedocs— documentation onlytest— test-onlybuild/ci— tooling / infraport— cherry-pick from upstream Netflix/vmafsycl/cuda/simd— backend-specificChecklist
make format && make lintis green locally. (clang-format and the commit hooks; HISS baseline unchanged at 242. clang-tidy: the host's version became 23.1.1 during this work, so the lanes could not be measured against their 22.1.8 baselines. Under 23.1.1 the two kernel files measure 0, and the new test,adm.candadm_tools.cshow only what that version reports tree-wide:modernize-redundant-void-argonf(void)andbugprone-signed-bitwiseonflags & VMAF_X86_CPU_FLAG_*, the spelling every extractor uses.)python3 scripts/ci/run_meson_test.py -- -C build. (--suite=faston a CPU build: 246 of 246.)/cross-backend-diffand the worst ULP is ≤ 2. (0: scalar, AVX2 and AVX-512 are bit-identical; the GPU twins equal the CPU.).c/.cpp/.cu/.h/.hpp, it has the appropriate license header (seeCONTRIBUTING.md). (test_float_adm_x86.c: EUPL-1.2.)!orBREAKING CHANGE:and the migration path is documented below. (Not breaking: no public symbol changes; the removed kernels were internal and had no caller.)docs/adr/_index_fragments/<NNNN-slug>.md. (1473-float-adm-x86-simd-exact-and-dispatched.md.)Bug-status hygiene (ADR-0165)
docs/state.mdupdated in this PR.T-FLOAT-ADM-X86-KERNELS-NOT-EXACT-NOT-DISPATCHED-2026-10-02opened and closed;T-FLOAT-ADM-X86-SCALAR-STAGES-2026-10-02opened for RC7.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests.Deep-dive deliverables (ADR-0108)
AGENTS.mdinvariant note —core/src/feature/x86/AGENTS.d/float-adm.md(rewritten: what each kernel must match and the rules that keep it exact) andcore/src/feature/AGENTS.d/float-adm.md(where the dispatch lives, what stays scalar, where an upstream hunk inadm_csf_s()goes).changelog.d/changed/float-adm-x86-simd-exact-dispatched.md.docs/rebase-notes.md, "x86 float ADM: wavelet and CSF kernels are exact and dispatched; the reduction kernels are gone". The kernel files are fork files (upstream has no float ADM SIMD);adm_tools.c,adm_tools.handadm.care upstream mirrors, and the note says where an upstream hunk goes.User documentation:
docs/metrics/features.md, new section "float_admuses AVX2 and AVX-512, with the same scores".Reproducer
Known follow-ups
T-FLOAT-ADM-X86-SCALAR-STAGES-2026-10-02(RC7): the decouple, the denominator reduction and the contrast masking have no SIMD form; the three undispatched NEON kernels.measured_sourcesdo not list the new test; I did not write baselines from this host after its clang-tidy changed version.