Repository navigation
fix(ciede): form ciede2000()'s two products in float as upstream does and mirror them in the CUDA, SYCL and HIP twins (ADR-1476) - #1892
Merged
Conversation
…each with its size and the upstream change that ends it (ADR-1479 to ADR-1486) (#1884) * docs(adr): record eight deliberate deviations from Netflix's source, each with its size and the upstream change that ends it (ADR-1479 to ADR-1486) The reference for code inherited from Netflix/vmaf is Netflix's source; a difference needs an ADR. The upstream parity audit of 2026-10-02 found deliberate differences that had none of their own, or whose ADR (ADR-1033) names neither upstream's behaviour nor the size: - ADR-1479 ciede on 4:2:2: chroma flags (fork PR #1050); 0.153 on 48 of 48 frames; Netflix/vmaf#1611. - ADR-1480 speed_temporal buffers at speed_prescale above 1 (#1643); up to 195, upstream segfaults on two fixtures; Netflix/vmaf#1627. - ADR-1481 a failing extractor fails the run (#871); status only, 78 probe runs where upstream is silent and 88 where it crashes. - ADR-1482 integer adm on frames of 17 to 32 pixels (#1473, #1507); scale 3 up to 0.23; Netflix/vmaf#1599, #1600. - ADR-1483 odd-sized chroma planes round up (4f08d32); psnr_cb / cr up to 0.684 / 0.826 dB, ciede 0.198. - ADR-1484 float_ms_ssim magnitude before pow() (#641, ADR-1033 item 2); NaN upstream on the 10 px checkerboard; Netflix/vmaf#1665. - ADR-1485 apsnr of a plane without error (#641, item 1); 114 against 60 dB; Netflix/vmaf#1666. - ADR-1486 float_motion scale-1 stride (#641, item 9); up to 25.1; Netflix/vmaf#1667. Each ADR gives upstream's file and line at Netflix 9e48141b, the fork's lines, the reason found in the fork's pull request, commit or code, and the measured size from the audit. Documentation only. * fix(test): give test_cuda_motion_tiny_frames 120s timeout On an unloaded RTX 4090 test_cuda_motion_tiny_frames runs 106 cases across 9 geometries and 3 bit depths in ~24s. Under parallel host load during merge-train validation the default 30s Meson test timeout was exceeded at 30.04s (SIGTERM). Set explicit timeout to 120s matching sibling CUDA parity suites. * docs: regenerate the indexes and the citation map after rebasing
… and mirror them in the CUDA, SYCL and HIP twins (ADR-1476) (#1892) * fix(ciede): form ciede2000()'s two products in float as upstream does and mirror them in the CUDA, SYCL and HIP twins (ADR-1476) Netflix's ciede2000() writes sqrt(c_prime_1 * c_prime_2) and + r_sub_t * chroma * hue with float operands, so both products are rounded to float before the double expression widens them (libvmaf/src/feature/ciede.c:224-225, :235-236 at 9e48141b). PR #552, a CodeQL sweep, cast the first operand of each to double, which keeps the products exact and changes the score. The upstream parity audit of 2026-10-02 found it (U4); the golden gate does not see it. ciede.c carries upstream's expressions again; the squares stay products (ADR-1467). cuda/integer_ciede/ciede_device.h and ciede_ff_math.h (SYCL and HIP) form the same float products in place of the exact ones. Against Netflix master (C API, %.17g, 327 frames from 8x8 to 3840x2160, GCC 16.2.1, glibc 2.44), scalar and default dispatch: 153 frames identical, 7 before. The rest: 119 frames up to 2.2e-11 are ADR-1467's product where upstream calls powf(x, 2) (272 of 327 with that call put back), 48 are 4:2:2 input (chroma flag fix, PR #1050), 7 are odd sizes (chroma planes rounded up). The CPU score moves by up to 1.3e-9 on frames of 160x90 and larger. Twins, 153 frames at --precision max, largest difference from the CPU before and after: CUDA (RTX 4090) 8.4e-13, 2.1e-12; SYCL (Arc A380) 7.3e-13, 2.1e-12; HIP (gfx1036) 7.3e-13, 2.1e-12; bound 1e-9. test_ciede_upstream_products replays the CUDA header's ciede_delta_e() with float and with widened products over 20 000 colour pairs (193 tell them apart); test_ciede_device_math holds the header to the CPU extractor bit for bit. The SYCL and CUDA source contracts pin upstream's statements. Netflix golden gate: 271 passed, 12 skipped on x86-64 and aarch64. * docs: regenerate the indexes and the citation map after rebasing
lusoris
force-pushed
the
fix/ciede-upstream-expression
branch
from
October 2, 2026 22:29
b2f8063 to
cfc1cd2
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
lusoris
added a commit
that referenced
this pull request
Oct 3, 2026
…age and fail on a difference no ADR covers (ADR-1487, ADR-1494) (#1890) * feat(parity): compare every CPU value with Netflix/vmaf in the dev image 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, 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). * docs: regenerate the indexes and the citation map after rebasing
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
ciede2000()forms two products infloatagain, as Netflix's source does. A CodeQL sweep (PR #552) had widened their first operand todouble, which changed the value. The CUDA, SYCL and HIP twins form the samefloatproducts. Decision: ADR-1476.Opens and closes
T-CIEDE-PRODUCTS-NOT-UPSTREAM-2026-10-02(finding U4 of the upstream parity audit of 2026-10-02).Land after #1886: under clang with link-time optimisation on an AVX-512 host,
test_ciede_device_mathneeds that fix (see "The hostedUbuntu clangfailure" below).What changes
Netflix
libvmaf/src/feature/ciede.cat9e48141b, lines 224-225 and 235-236:Both products have
floatoperands, so C rounds them tofloatbefore thedoubleexpression widens them. The fork had(double)c_prime_1 * c_prime_2and(double)r_sub_t * chroma * hue.core/src/feature/ciede.ccore/src/feature/cuda/integer_ciede/ciede_device.hchroma_productandrotationarefloatproducts.core/src/feature/ciede_ff_math.h(SYCL and HIP)from_float(c_prime_1 * c_prime_2)androtation * chroma * huein place oftwo_prod()forms. No constant or table changes, sogen_sycl_ff_math.py --checkis unchanged.core/src/feature/metal/integer_ciede.metalfloatthroughout and already forms both products infloat. Not run: no device.Upstream comparison
Audit harness (C API,
%.17g, 31 fixtures from 8x8 to 3840x2160 at 8 to 16 bits, 327 frames with aciede2000on both sides) against Netflix master9e48141bbuilt with GCC 16.2.1 and glibc 2.44. Scalar (cpumask63) and default dispatch give the same figures.39929960fpowf(degrees, 2)put back (experiment, not shipped)AVX2 only (279 frames; upstream has no 3840x2160 run at that mask): 7 before, 153 after.
The 174 frames that still differ, each with its recorded cause:
degrees * degreeswhere upstream callspowf(degrees, 2)T-BUGHUNT-FEATURE-CPU-2026-06-27The brief expected 272 of 327 with the last two rows as the only rest. That is the third line of the first table: the audit ran on
b34732ae1, before ADR-1467 (#1862) landed. With ADR-1467 on master the count is 153, and the 119 extra frames are exactly the ones that become identical when the call is put back.The 119-frame class is a property of glibc 2.44's
powf. In the dev container (glibc 2.43) a GCC 15.2 build and a clang 22.1.8 build of Netflix master both return this branch's values on all 96 frames of the Netflix 576x324 pair and Big Buck Bunny at 1920x1080; on the host 49 of those 96 differ.How far scores move
ciede2000of a CPU build moves on 319 of 327 measured frames: by at most 1.3e-9 on frames of 160x90 and larger, by up to 1.0e-8 on frames of 24x24 and smaller. No file undertestdata/storesciede.Compilers
Upstream's expression does not depend on the compiler. Built in the dev container and run on the Netflix 576x324 pair (48 frames,
--precision max): x86-64 GCC 15.2, x86-64 clang 22.1.8, aarch64 GCC 15.2 and aarch64 clang 22.1.8 (both underqemu-aarch64) return the same 48 values.test_ciede,test_ciede_upstream_productsandtest_ciede_device_mathpass in all four builds;test_ciede_neonpasses in both aarch64 builds andtest_ciede_simd_parityin both x86-64 builds.The hosted
Ubuntu clangfailure oftest_ciede_device_mathThe hosted line is
96x64 8-bit fmt 1: cpu=35.418753523280415 replay=45 delta=9.581e+00. Reproduced with the hosted compiler (clang 22.1.8,-O3 -flto) in the dev container on master and on this branch.Neither
ciedenor the test is compiler-dependent.vmaf_init_cpu()runs one 512-bit instruction as inline assembly on AVX-512 hosts and declared onlyzmm0clobbered; clang drops that clobber in a function not compiled for AVX-512. The link-time-optimised build inlined it into the test between the product and the sum of45 - 20 * log10(x), and the value inxmm0came back as zero. The job passes on runners without AVX-512, which is why it looked intermittent.That is fixed in #1886 (
T-CPU-AVX512-WARMUP-CLOBBER-2026-10-02), which also updates item 4 ofT-CI-MASTER-FIRST-FULL-RUN-2026-10-02. Results for this branch under clang 22.1.8-O3 -fltoon an AVX-512 host:test_ciede_device_mathcpu=35.418753526663913 replay=45Golden gate
make test-netflix-golden(x86-64): 271 passed, 12 skipped.make test-netflix-golden-arm64(aarch64 GCC cross build underqemu-aarch64): 271 passed, 12 skipped.No assertion is edited.
SIMD
The SIMD paths of
ciedeonly convert samples tofloat. Scalar, AVX2-only and default dispatch return the same 327 values on this branch.test_ciede_simd_parity(x86-64) andtest_ciede_neon(aarch64, qemu) pass.Twins
Not exact twins:
LIBM_TWINS["ciede"] = 1e-9. Against the GCC CPU extractor at--precision maxon 153 frames (the Netflix 576x324 pair at 8 bits, at 10 bits and as 10-bit 4:2:2, both 1920x1080 checkerboard pairs, 48 frames of Big Buck Bunny at 3840x2160):ciede_cudaciede_syclxe)ciede_hipAfter the change every differing frame is a 3840x2160 one. The parity gate cell (
libm:ADR-1426, tolerance 1e-9, Netflix pair) reports a largest difference of 0 on all three devices.Device tests, each under its device lock:
test_cuda_ciede_parity,test_cuda_ciede_parity_large;test_sycl_ciede_math,test_sycl_ciede_parity,test_sycl_ciede_parity_large,test_sycl_kernel_scratch;test_hip_ciede_math,test_hip_ciede_parity,test_hip_ciede_parity_large,test_hip_first_frame_clear_ciede_hip. All pass.VMAF_SYCL_AOT_JOBS=8 meson test --suite sycl-aot: pass (every SYCL translation unit for the 19 default targets).Tests
test_ciede_upstream_products: replaysciede_delta_e()of the CUDA header with the header's own helpers, once withfloatproducts and once with widened ones, over 20 000 colour pairs. The header must return thefloatform on every pair, and the pairs must tell the two forms apart (193 do). Against the header as it was on master the test fails on those 193 pairs.test_sycl_ciede_exact_contract.py,test_cuda_ciede_exact_contract.py: upstream's statements are pinned inciede.c(comment cites the upstream file and lines) and their mirrors in the two headers, with the widened forms as planted regressions.test_ciede_device_mathis unchanged: it replays the CUDA header against the CPU extractor bit for bit. With the two together, a cast put back inciede.calone failstest_ciede_device_math, and a cast in both files fails the new test, whatever the math library. (A test that includesciede.citself is not allowed incore/test:check-no-non-header-includes.)Lint
scripts/dev/tidy-lane.sh all, clang-tidy 22.1.8):cpu,cuda,hip,syclandarm64each exit 0, the baseline matches (70, 620, 504, 172 and 115 warnings). No baseline changes.ciede_device.hkeeps its 12 baselined findings in thecudalane andciede_ff_math.hits one in thehiplane: the kernel lint debt belongs to the standards batches, and a header's count only moves with a full rewrite of the lane.scripts/dev/preflight.sh --stage msvcism: pass.ciede.ccarry a citedNOLINTforperformance-type-promotion-in-math-fn(the file's convention for upstream's promotions) and acodeql[...]comment: CodeQL will reportcpp/integer-multiplication-cast-to-longon them again, and the alert is to be dismissed as upstream arithmetic.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. Not run as one target:clang-formatis clean on the touched files and the clang-tidy lanes were measured in the dev container (see Lint).python3 scripts/ci/run_meson_test.py -- -C build./cross-backend-diffand the worst ULP is ≤ 2. Measured per backend above (largest difference 2.1e-12, bound 1e-9)..c/.cpp/.cu/.h/.hpp, it has the appropriate license header (seeCONTRIBUTING.md).!orBREAKING CHANGE:and the migration path is documented below. Not a breaking change.docs/adr/_index_fragments/<NNNN-slug>.mdand the slug is appended todocs/adr/_index_fragments/_order.txt.Bug-status hygiene (ADR-0165)
docs/state.mdupdated in this PR with a row in the appropriate section (Open / Recently closed / Confirmed not-affected / Deferred), ORno state delta: REASON.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests.Cross-backend numerical results
Deep-dive deliverables (ADR-0108)
docs/state.mdrow.docs/adr/1476-ciede-upstream-expression.md,## Alternatives considered.AGENTS.mdinvariant note —core/src/AGENTS.d/ciede-squares-are-products.md,core/src/feature/cuda/AGENTS.d/ciede.md,core/src/feature/sycl/AGENTS.d/ciede.md, and the ciede entry ofdocs/development/rebase-sensitive-invariants.md.changelog.d/changed/ciede-upstream-products.md.docs/rebase-notes.md.Reproducer
python3 scripts/ci/run_meson_test.py -- -C build test_ciede_upstream_products test_ciede_device_math \ test_ciede test_sycl_ciede_exact_contract test_cuda_ciede_exact_contract # a twin on its device (replace cuda by sycl or hip): python3 scripts/dev/speed_gpu_parity.py --backend cuda --feature ciede --no-timing \ --max-abs-diff 1e-9 --vmaf "$PWD/build-cuda/tools/vmaf"Known follow-ups
no ffmpeg-patches update needed: no public surface changes.
ciedetwin: not run (no device); it was not edited.Breaking changes / migration
None.
ciede2000values at--precision maxchange in their last digits (at most 1.3e-9 on frames of 160x90 and larger).