Repository navigation
fix(adm): compute the CSF weights of float ADM in float, as Netflix does (ADR-1489) - #1894
Merged
Merged
Conversation
lusoris
force-pushed
the
fix/float-adm-barten-upstream-float
branch
2 times, most recently
from
October 2, 2026 23:24
b2f8b5f to
dc309e1
Compare
lusoris
marked this pull request as ready for review
October 2, 2026 23:24
…oes (ADR-1489) (#1894) * fix(adm): compute the CSF weights of float ADM in float, as Netflix does (ADR-1489) Two inherited routines compute the contrast-sensitivity weights of float ADM, and fork commits had widened both from float to double to quiet a static-analysis finding (cpp/integer-multiplication-cast-to-long), without a decision: - dwt_quant_step() in adm_tools.h (Watson): #552 cast an operand of the exponent k * temp * temp, #760 then kept r, temp and Q in double. 32 of 40 probed steps were not Netflix's bits. - barten_csf_tools.h (Barten, adm_csf_mode=1): #44 promoted one operand of six float products and quotients. 138 of 144 probed weight sets were not Netflix's bits. Both evaluate Netflix's arithmetic again. adm_tools.h has Netflix's three statements, with a suppression comment for the finding. The Barten header forms each product and quotient in float and writes the promotion of the result out: the SYCL and Metal twins of integer ADM compile it as C++, where pow(float, float) and exp(float) are the float functions, so Netflix's implicit form returns other weights there (135 of 144 probed sets) than in C. The Metal copy of the step (float_adm_metal.mm) follows; the CUDA, HIP and SYCL twins of float_adm call the CPU's routine. ADM_OPT_RECIP_DIVISION stays undefined (ADR-1442). Against Netflix/vmaf cea2b4d8 built with the plain quotient, float_adm is identical on every measured value (8225 of 8225 default-run values, 71280 of 71280 over 36 option variants, 1764 of 1764 frame scores of seven float models; 658 frames, scalar, AVX2 and AVX-512 dispatch): the division is the only difference left. Against Netflix as built on x86, adm2 is identical on 464 of 658 frames (30 before). Scores move: float_adm adm2 by at most 1.14e-7, the float models by at most 2.7e-5 on a frame, fixed-point adm with adm_csf_mode=1 by at most 1.6e-7. Fixed-point adm in its default mode, the default models and the testdata snapshots do not move. Twins on this host stay bit-identical to the CPU: float_adm and adm on CUDA (RTX 4090), HIP (gfx1036) and SYCL (Arc A380), parity tests and the gate's cells at tolerance 0, and integer adm in Barten mode on each twin. Metal: source changed, not run (no device). Snapshots: no CPU snapshot moves (they hold fixed-point features in Watson mode). testdata/scores_sycl_a380_{576,640,720,1080,4k}.json are re-recorded, because the files of 2026-09-26 no longer described the twin: they differ from the new ones on 101 to 139 of 576 shared values by up to 1.0e-4 and lack three metrics. Recorded with testdata/run_sycl_scores.py a380 inside the dev container (image sha256:43ef1e32cb32b148a076ed6dff73b72d7a6566ca3882bf90954b8a34a74761fc, icx 2026.1.1, glibc 2.43, /dev/dri passed through, Arc A380, compute runtime 26.35.39758), from this change as it stood at 539e99261 (no file under core/ differs from this commit). The image has no ocloc, so the binary was built with -Dsycl_icpx_aot_targets= (kernels compiled at run time). The recordings equal the CPU snapshots on 3599 of 3600 values; the one is vmaf of frame 26 at 1280x720 (88.435637 for 88.435634), an icx build's math library (T-ICX-LIBIMF-HOST-MATH-2026-10-01). The B580 and UHD 770 recordings stay stale: T-SYCL-SNAPSHOTS-STALE-2026-10-02. Closes T-ADM-CSF-EXPONENT-NOT-UPSTREAM-2026-10-01.
lusoris
force-pushed
the
fix/float-adm-barten-upstream-float
branch
from
October 2, 2026 23:30
dc309e1 to
96af5b3
Compare
lusoris
added a commit
that referenced
this pull request
Oct 2, 2026
…age and fail on a difference no ADR covers (ADR-1487, ADR-1494) Code inherited from Netflix/vmaf evaluates as Netflix's source does; a difference needs an ADR. The upstream parity audit of 2026-10-02 found 170,344 of 348,132 values different, six causes of them unintended, and no gate that would have seen any of them. This makes the rule checkable. scripts/dev/upstream_parity.py (make upstream-parity, make upstream-parity-full) builds Netflix/vmaf at the recorded parity head (read through scripts/ci/upstream_parity_pin.py) and this tree with the golden build profile, with the same compilers, runs one C API harness against each and compares every per-frame value, aggregate and pool at %.17g: 16 shared extractors, their option variants and the shipped models, on the Netflix pairs and clips derived from them, at scalar and default dispatch (AVX2 too in the full matrix). Both trees are built and run in the dev container image (--container): Netflix's own ciede values differ between glibc 2.43 and 2.44, so a comparison is evidence only in one recorded environment. The documents record the image id, compilers and C library; outside the image the guard refuses to measure unless --unpinned marks the verdict advisory. --heap-check (in the full target) reruns every request with MALLOC_PERTURB_=170: an output of this tree that changes fails, and an upstream output that changes is undefined and may only be covered with bound inf (ciede on odd sizes and float_motion's scale-1 chroma, which went flaky on the host). Every difference is attributed to one fragment under scripts/ci/upstream_parity.d/ (the exact_twins.d pattern; the line parser is now shared): 37 deliberate deviations with their ADRs and bounds measured in the image, 5 pending (the SpEED revert and the five-frame motion port). The guard fails on a difference no fragment covers, on one above its bound, on a stale fragment and on a crash of this tree's harness; exit 2 means it could not compare. The seven fragments of the reverts that landed meanwhile (fork PRs #1891, #1892, #1894, #1895) are removed. ADR-1494 records the ADM extractors' refusal of frames below 17x17 (fork PRs #1473, #1770): upstream's integer ADM ends in signal 11 there, its float ADM returns values that at 12x9 and 8x8 depend on the heap. On master 96af5b3 in the image (GCC 15.2.0, glibc 2.43) against Netflix 9e48141b: full matrix 886,002 values, 780,741 identical, 83,516 differences all covered, none stale; 1,675 upstream outputs depend on the heap, none of this tree's. Probe set 252,162 values, pass. With the SpEED revert applied in a scratch copy, exactly its 3 fragments go stale. A one-ulp change planted in float_psnr.c fails the guard. testdata/bench_upstream_ab.py builds upstream through the guard and takes its score verdict from it (advisory on the host); --max-score-delta is gone and the default upstream is the recorded head. Not a required check yet (T-UPSTREAM-PARITY-GUARD-HOSTED-JOB-2026-10-02).
14 of 15 tasks
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
Follows #1891 (the integer half, ADR-1475, on master as ad0326f); this branch is rebased onto master and holds one commit.
float_admcomputed its contrast-sensitivity weights with intermediates Netflix does not use. Two inherited routines were widened fromfloattodoubleby fork commits that answered a static-analysis finding (cpp/integer-multiplication-cast-to-long) and record no decision:dwt_quant_step()inadm_tools.h(the Watson step): chore(ci): refresh stale 'scaffold' headers in fuzz.yml + libvmaf-build-matrix.yml #552 (9ce9ab86a) cast an operand of the exponentk * temp * temp, and fix(test): include internal picture.h for vmaf_picture_ref decl (macOS Clang + TSan) #760 (6435a88f4) then keptr,tempandQindouble. 32 of 40 probed steps were not Netflix's bits.barten_csf_tools.h(the Barten CSF,adm_csf_mode=1): refactor(core): C++23 pilot Wave 2 — fex_ctx_vector.c → .cpp (ADR-0723) #44 (d06dd6cfc) promoted one operand of sixfloatproducts and quotients. Its message calls the casts semantics-preserving on the evidence of the default model's score, a run that never callsbarten_csf(). 138 of 144 probed weight sets were not Netflix's bits.This PR puts Netflix's arithmetic back (ADR-1489), under the rule that inherited code evaluates as Netflix's source does unless an ADR records a deviation.
ADM_OPT_RECIP_DIVISIONstays undefined (ADR-1442, deliberate): after this PR that division is the only arithmetic difference between the fork'sfloat_admand Netflix's on x86, which the third column below shows.What changed
core/src/feature/adm_tools.h:float r,float temp,float Q = 2.0 * params->a * pow(10.0, params->k * temp * temp) / ..., aslibvmaf/src/feature/adm_tools.hhas them; the finding is answered by a suppression comment that cites the ADR.core/src/feature/barten_csf_tools.h(its 18 locals that never change areconstnow, which clears the header's clang-tidy findings as C++): each product and quotient is formed infloat, as Netflix forms it, and the promotion of the result is written out (pow((double)(p_0 * spatial_frequency), (double)p_1),exp((double)(-barten_mtf_params_b[i] * spatial_frequency)), ...). The reason for the explicit form: the SYCL and Metal twins of integer ADM compile this header as C++, wherepow(float, float)andexp(float)are thefloatfunctions. Netflix's text compiled as C++ returns other weights than compiled as C on 135 of 144 probed argument sets; with the promotions written out the header returns the same bits in both languages, and an explicit conversion is not what the static-analysis query reports.linear_interpolate()is Netflix's text unchanged.core/src/feature/metal/float_adm_metal.mm: its copy of the step forms the exponent in a namedfloat. The CUDA, HIP and SYCL twins offloat_admcall the CPU'sadm_csf_rfactor_s()and follow by rebuild.core/test/test_float_adm_csf_upstream.c(new, withbarten_csf_cxx.cpp): the step equals thefloatform on 40 steps and thedoubleform is another number;linear_interpolate()equals thefloatform and thedoubleslope is another number;barten_csf()compiled as C++ returns the bits of the C one on 168 weights; on glibc the 40 steps and the 168 weights have the bits of a Netflixcea2b4d8build. It fails against master's headers.core/test/test_float_adm_csf_upstream_contract.py(new): reads the two headers and the Metal copy; rejects adoublelocal, a promoted operand, and an implicit promotion in the Barten header, each with a planted regression.Against Netflix master
Harness of #1891: C API, every collector value at
%.17g, 658 frames in the 28 fixturesfloat_admaccepts,float_admby default, withdebug=trueand under 36 option variants, seven models that readfloat_adm. Three trees: Netflixcea2b4d8; this branch; and Netflix with the plain quotient, which iscea2b4d8withADM_OPT_RECIP_DIVISIONundefined and the#ifdef __SSE2__ofadm_tools.cforced to its#elsebranch (DIVS(n, d) ((n) / (d)), what Netflix compiles on ARM; with the macro alone undefined the file has noDIVSon x86 and does not compile). Values identical / values compared; the counts are the same at scalar (cpumask63), AVX2 (48) and AVX-512 (0) dispatch:adm2, 658 framesdebug=truerunsvmaf_float_v0.6.1score, 252 framesvmaf_float_v0.6.1negscoreThe residual against Netflix is therefore exactly the division of ADR-1442. Outside the table: the three fixtures below 17x17, which the fork refuses (#1770) where Netflix reads outside the band.
What moves in the fork's own output (tree before against tree after):
float_admadm2float_admper-scale scoresfloat_admadm2,adm_csf_mode=1adm,integer_adm2,adm_csf_mode=1admper-scale scores,adm_csf_mode=1vmaf_float_v0.6.1frame scorevmaf_float_v0.6.1negframe scoreInteger
admin Barten mode moves because it reads the same header (U3). ADR-1472's weight limits and ADR-1325's normalisation are unchanged and its tests pass (test_adm_csf_representable7 of 7,test_barten_csf13 of 13,test_barten_csf_coverage); against Netflix that mode stays apart by design, since Netflix's Barten weights wrap in their fixed-point conversion. Integeradmoutside Barten mode and the 11 models that do not readfloat_admdo not move: 82 710 of 82 710 values identical.Golden gate, SIMD, twins
The harness tables, the aarch64 golden gate and the cpu, cuda, hip and arm64 tidy lanes were measured at 38b7a737d (this change on top of #1891 before both were rebased; the changed lines of every file are the same as now). After the rebase onto master f60ad15 the CPU build and suite, the float ADM and Barten tests, the x86 golden gate, all device tests and gate cells below, and the sycl tidy lane were run again on the pushed head.
test_float_adm_csf_upstream,test_float_adm_dwt2_neonandtest_feature_isa_invarianceamong them.test_float_adm_x86(the test feat(adm): make the x86 float ADM wavelet and CSF kernels exact and dispatch them (ADR-1473) #1874 added) passes: the AVX2 and AVX-512 wavelet and CSF kernels return the reverted scalar code's bits. The harness counts above are identical at the three dispatch levels.test_cuda_float_adm_parity17 of 17,_parity_large17 of 17,test_cuda_exact_twins2 of 2,test_cuda_adm_parity11 of 11,_parity_large11 of 11,_small_border,_tiny_frames(8),_wide_rounding,_dwt2_rows(6).test_hip_float_adm_parity19 of 19,_parity_large19 of 19,test_hip_float_adm_math3 of 3,test_hip_exact_twins2 of 2,test_hip_adm_exact9 of 9,test_hip_adm_parity3,_parity_large3,_small_border,_tiny_frames(8),_wide_rounding,_dwt2_rows(5),_init_unwind, bothfirst_frame_cleartests.test_sycl_float_adm_parity21 of 21,_parity_large21 of 21,test_sycl_float_adm_math7 of 7,test_sycl_exact_twins2 of 2,test_sycl_adm_parity7 of 7,_parity_large7 of 7,_tiny_frames8 of 8.float_admandadmcells, tolerance 0 (no fragment underscripts/ci/exact_twins.d/touched): max difference 0 on the Netflix pair, both checkerboard pairs and a 1920x1080 clip, for each of the three backends (24 cells).admwithadm_csf_mode=1,debug=true, each twin against the CPU extractor of the same build at--precision max: 0 of 864 values differ on the Netflix pair and 0 of 864 on the 1920x1080 clip, per backend. For SYCL this compares the Barten weights of the C++ translation unit with those of the C one.test_float_adm_csf_upstreampasses, bit tables included.Snapshots
testdata/scores_cpu_*.jsonhold the features ofvmaf_v0.6.1(fixed-point, Watson mode): the default-model output of this branch equals that of fix(adm): form the integer ADM quantisation step's exponent in float, as Netflix does (ADR-1475) #1891 value for value. No snapshot holds afloat_admor a Barten-mode value.testdata/scores_sycl_a380_{576,640,720,1080,4k}.jsonare re-recorded here (requested in review of fix(adm): form the integer ADM quantisation step's exponent in float, as Netflix does (ADR-1475) #1891; the device is on this host). They were stale before either PR: the files of 2026-09-26 differ from the new ones on 139, 123, 108, 101 and 107 of 576 shared values by up to 1.0e-4 and lack three metrics the twins emit now. Recorded withtestdata/run_sycl_scores.py a380inside the dev container (imagesha256:43ef1e32cb32b148a076ed6dff73b72d7a6566ca3882bf90954b8a34a74761fc, icx 2026.1.1, glibc 2.43, the worktree copied in,/dev/dripassed through, compute runtime 26.35.39758), under the device lock. The image has noocloc, so the binary was built with-Dsycl_icpx_aot_targets=(kernels compiled at run time), not with the image recipe's ahead-of-time list. The recordings equal the CPU snapshots on 3599 of 3600 values; the one isvmafof frame 26 at 1280x720 (88.435637 for 88.435634), which is the icx build's math library (T-ICX-LIBIMF-HOST-MATH-2026-10-01). At%.17gthe twin equals the CPU extractor of its own build on every value of the five clips, and a second recording run repeats the first.T-SYCL-SNAPSHOTS-STALE-2026-10-02now says what reads each recording (nothing gates on any of them;testdata/compare_combined.py, run by hand, reads the A380 ones) and what the office box has to record.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, the commit hooks,scripts/dev/preflight.sh --stage msvcism. clang-tidy 22.1.8 lanes measured in the dev container withscripts/dev/tidy-lane.sh: cpu 70 = baseline 70; sycl 154 for this PR's files, with the baseline tightened from 172 in this PR (barten_csf_tools.h18 to 0: the header's 18misc-const-correctnessfindings as C++ are fixed, and the three new test sources are listed); cuda 620 = 620; hip 505 against 504, arm64 116 against 115 and, after the rebase, sycl 155 against 154, where the one finding above baseline in each isreadability-function-sizeatcore/test/test_iqa_ssim_sub_window.c:281, a file master gained with fix(ssim): give the SIMD kernels the number of windows so float_ssim on a frame smaller than its window is 0, not garbage #1882 (2efce88) and this PR does not touch.)python3 scripts/ci/run_meson_test.py -- -C build. (CPU build: 267 of 267, 2 skipped.)/cross-backend-diffand the worst ULP is ≤ 2. (0 on CUDA, HIP and SYCL: the parity gate'sfloat_admandadmcells at tolerance 0.).c/.cpp/.cu/.h/.hpp, it has the appropriate license header (seeCONTRIBUTING.md). (test_float_adm_csf_upstream.c,barten_csf_cxx.cpp,barten_csf_cxx.h: EUPL-1.2.)!orBREAKING CHANGE:and the migration path is documented below. (Not breaking: no public symbol or option changes. Scores move in the seventh decimal forfloat_adm, the fifth for the float models.)docs/adr/_index_fragments/<NNNN-slug>.md. (1489-float-adm-barten-upstream-float.md.)Bug-status hygiene (ADR-0165)
docs/state.mdupdated in this PR.T-ADM-CSF-EXPONENT-NOT-UPSTREAM-2026-10-01moved to Recently closed (both halves) and taken off the RC3 list;T-SYCL-SNAPSHOTS-STALE-2026-10-02rewritten (consumers, A380 re-recorded, B580 and UHD 770 still stale).Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests.Deep-dive deliverables (ADR-0108)
docs/research/1489-float-adm-barten-upstream-float.md.AGENTS.mdinvariant note —core/src/feature/AGENTS.d/adm-csf-weights-float.md(new),core/src/feature/metal/AGENTS.md(the copy), and an entry indocs/development/rebase-sensitive-invariants.md.changelog.d/fixed/float-adm-barten-upstream-float.md.docs/rebase-notes.md, top entry: the statements are upstream's again and a sync takes upstream's side; for the Barten header, upstream's arithmetic with the promotions kept explicit.User documentation:
docs/metrics/features.md, new section "float_admuses Netflix's contrast-sensitivity weights".Reproducer
Known follow-ups
core/src/feature/AGENTS.d/float-adm.md.T-GPU-FLOAT-ADM-CPU-ARITHMETIC-2026-10-01(open) covers the rest of the Metalfloat_admtwin's arithmetic; this PR changes its quantisation step only.