Repository navigation
fix(float_adm): refuse frames below 17x17 instead of reading outside the bands - #1770
Merged
Merged
Conversation
lusoris
force-pushed
the
fix/float-adm-min-frame
branch
from
October 1, 2026 21:08
95b6fcf to
6b3ed9f
Compare
lusoris
marked this pull request as ready for review
October 1, 2026 21:08
…1754) * fix(cuda): download every plane of device input for CPU extractors translate_picture_device() copied the luma plane of a device picture into the host picture only. With device-resident input and an extractor on the CPU the chroma planes stayed uninitialised, and psnr_cb came out as the 60 dB cap where the CPU gives 12.54 dB, with no error (Netflix/vmaf#1613). The download now takes every plane the picture has, as the upload already does. test_cuda_device_input_host_extractors compares psnr y/cb/cr of device input with a CPU-only run at n_threads 0 and 4; it fails without the fix. * docs: regenerate the indexes and the citation map after rebasing
… changes (#1764) * fix(build): rebuild SYCL translation units when a header they include changes meson compiles every SYCL source through a custom target that declared its source file and nothing else, so Ninja knew none of its headers. After an edit to a header such as feature/sycl/sycl_exact_fp.h, ninja reported nothing to do and the library kept the kernels compiled from the old text. A clean build was never affected. The sycl_common and sycl_feature targets now pass -MD -MF to icpx and declare the depfile. Not on Windows, where the depfile of the icpx driver is unverified; that lane builds from clean. Found while measuring float_adm_sycl's differences one at a time: six header variants scored as the unchanged twin until the source was touched. ADR-1320 fixed the same defect for the CUDA fatbins and HIP HSACOs. Closes T-SYCL-TU-HEADER-DEPS-UNTRACKED-2026-10-01. * docs: regenerate the indexes and the citation map after rebasing
…the bands (#1770) * fix(float_adm): refuse frames below 17x17 instead of reading outside the bands float_adm decomposes a frame into four DWT levels. Below 17 pixels the scale-3 bands have one sample and adm_tools.c reads outside them: adm_cm_thresh3x3_s() mirrors the sample before the first one to index 1, and dwt2_src_indices_1d_s() gives a one-sample input a fourth tap at index -1. The second is a heap read before the band buffer: an AddressSanitizer build reports heap-buffer-overflow, READ of size 4 in adm_dwt2_vert_pass_s, on an 8x8 pair. From 9 to 16 pixels the read stays inside the buffer and returns a sample of another scale (a random 8x8 pair scored adm_scale3 = 1.05). float_adm.c::init() now calls adm_frame_size_check(), the fixed-point extractor's 17x17 floor, and float_adm_cuda does the same before it claims a device resource. Both log "<extractor> requires width >= 17 and height >= 17 (got WxH)" and return -EINVAL. 17x17 is accepted and identical on the two. Not changed, and recorded why: the debug ratio stays under the unsuffixed key `adm`. Listing `adm` in provided_features (it lists `adm_scale0`, as upstream does) would suffix the key and let two debug instances coexist, but the Netflix golden tests read VMAF_feature_adm_score under non-default options (T-FLOAT-ADM-DEBUG-KEY-UNSUFFIXED-2026-10-01, now deferred). The SYCL, HIP and Metal float twins still accept such frames (T-GPU-FLOAT-ADM-TINY-FRAME-FLOOR-2026-10-01). * docs: regenerate the indexes and the citation map after rebasing
lusoris
force-pushed
the
fix/float-adm-min-frame
branch
from
October 1, 2026 21:41
6b3ed9f to
953cf6e
Compare
This was referenced Oct 1, 2026
fix(sycl): compute float_adm in the CPU's arithmetic without fp64 so the twin is bit-identical
#1787
Merged
17 of 26 tasks
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 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
float_admandfloat_adm_cudanow refuse frames smaller than 17x17, as the fixed-pointadmextractor does. Before, the CPU extractor accepted them and read outside its coarsest bands; at 8 pixels or fewer that is a heap read before a buffer.#1734 recorded the defect while making the CUDA twin exact; this is the fix.
The defect
float_admdecomposes a frame into four DWT levels. Below 17 pixels the scale-3 bands have one sample, andcore/src/feature/adm_tools.creads outside them in two places:adm_cm_thresh3x3_s()mirrors the sample before the first one to index 1, which a one-sample band does not have (frames of 9 to 16 pixels);dwt2_src_indices_1d_s()gives a one-sample input a fourth tap at index -1, andadm_dwt2_s()readssrc[-stride + j], one row before the band buffer (frames of 8 pixels or fewer).An AddressSanitizer build of master
e955b2fe6on a random 8x8 pair:At 12x9 and 16x16 the read stays inside the buffer and returns a sample of another scale, so the scores are not meaningful (a random 8x8 pair scored
adm_scale3 = 1.05). Upstream Netflix has the same code and no check.What changed
core/src/feature/float_adm.c::init()callsadm_frame_size_check("float_adm", w, h)before it allocates. That is the fixed-point extractor's check (adm_csf_fixed_point.h), not a second implementation.core/src/feature/cuda/float_adm_cuda.c::init_fex_cuda()calls it before it claims a device resource. The twin clamped the two indices and so scored frames the CPU cannot.<extractor> requires width >= 17 and height >= 17 (got WxH)and return-EINVAL. 17x17 is accepted and bit-identical on the two.What did not change, and why
The row
T-FLOAT-ADM-DEBUG-KEY-UNSUFFIXED-2026-10-01proposed a second fix for this PR: listadminstead ofadm_scale0inprovided_features, so that the debug ratio gets its option suffix and twofloat_admdebug instances can run together. I tried it and withdrew it. It renames the key of every non-default instance fromadmtoadm_<suffix>, and the Python harness then reportsVMAF_feature_adm_<suffix>_score, where two Netflix golden tests readresults[0]["VMAF_feature_adm_score"]under non-default options (test_run_vmaf_fextractor_with_feature_overloads,test_run_vmaf_fextractor_with_adm_skip_scale0inpython/test/feature_extractor_test.py). Those tests are not to be changed, so the unsuffixed key is a contract. The row moves to deferred with that finding and the two options left.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; clang-tidy reports 0 in the touched files.)python3 scripts/ci/run_meson_test.py -- -C build. (--suite=fast: 222 of 222 on a CPU build, 286 of 286 on a CUDA build with an RTX 4090.)/cross-backend-diffand the worst ULP is ≤ 2. (No kernel changes; 17x17 is identical on CPU and CUDA,test_cuda_float_adm_parity17 of 17.).c/.cpp/.cu/.h/.hpp, it has the appropriate license header (seeCONTRIBUTING.md). (None added.)!orBREAKING CHANGE:and the migration path is documented below. (Frames below 17x17 now fail at start instead of returning scores that were not meaningful; no API changes.)docs/adr/_index_fragments/<NNNN-slug>.md. (No ADR: see the decision matrix item.)Bug-status hygiene (ADR-0165)
docs/state.mdupdated in this PR:T-FLOAT-ADM-TINY-FRAME-BAND-READS-2026-10-01moved to Recently closed with the sanitizer evidence;T-GPU-FLOAT-ADM-TINY-FRAME-FLOOR-2026-10-01opened for the SYCL, HIP and Metal twins;T-FLOAT-ADM-DEBUG-KEY-UNSUFFIXED-2026-10-01rewritten and moved to deferred.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests.make test-netflix-goldenwas not run for this PR.)Cross-backend numerical results
Performance (if
perforfeat)Not a performance change: one comparison in
init().Deep-dive deliverables (ADR-0108)
AGENTS.mdinvariant note —core/src/feature/AGENTS.md(the floor is fork-local, keep it on an upstream sync; the debug key stays unsuffixed).changelog.d/fixed/float-adm-min-frame.md.docs/rebase-notes.md, "float_adm refuses frames below 17x17".Reproducer
Known follow-ups
float_adm_sycl,float_adm_hip,float_adm_metalstill accept frames below 17x17 (read from source, not run):T-GPU-FLOAT-ADM-TINY-FRAME-FLOOR-2026-10-01.T-FLOAT-ADM-DEBUG-KEY-UNSUFFIXED-2026-10-01, deferred, with the reason the proposed fix cannot be applied.