Repository navigation
Conversation
a36b05d to
62cd901
Compare
d2d00fc to
3ddbf51
Compare
… decouple kernels adm_decouple() stores MIN(rst * gain, t) or MAX(rst * gain, t), a double, in an integer, which truncates toward zero. The AVX2 scale 0 kernel, the AVX-512 scale 0 kernel and the AVX-512 scale 1-3 kernel converted the product with _mm256_cvtpd_epi32, _mm512_cvtpd_epi32 and _mm512_cvtpd_epi64, which round to nearest, so they differ from the scalar kernel by one in every limited sample whose product has a fraction of one half or more. That needs a fractional adm_enhn_gain_limit: at 1.0 and at the default 100 the product is integral and all three paths agree. With a limit of 1.2 or 1.5, integer_adm_scale0 of the AVX2 and AVX-512 paths differs from the scalar path by up to 1.2e-6 per frame on the Netflix 576x324 pair and 3.4e-5 on a checkerboard pair. Use the truncating conversions. t is an integer, so truncating the product and then taking the integer minimum or maximum gives the scalar's sample. The AVX2 scale 1-3 kernel already truncates (it casts in C). The scale 0 angle test of both kernels forms the dot product and the squared magnitudes with madd_epi16. The sum of two products of -32768 * -32768 is 2^31, which does not fit int32 and reads INT32_MIN, so a distorted sample with h = v = -32768 sets the angle flag where the scalar kernel, which sums in int64, clears it (the reference side forms the same sum). Read the squared magnitudes as unsigned and map an INT32_MIN dot product back to 2^31. No decoded picture reaches this (scale 0 coefficients stay within 22916), but checkasm and the new test do. test_adm_decouple_gain compares the scale 0 kernels and the AVX-512 scale 1-3 kernel with the scalar kernels at limits of 1, 1.2, 1.5 and 100 on bands that take the limit, and the scale 0 kernels on bands that reach -32768. It fails on the unpatched tree and passes with this change. check_adm's decouple test keeps the sizes, patterns and gains of the AArch64 NEON cases and adds a pattern whose distorted samples are scaled copies of the reference (independent random bands almost never pass the angle test), runs it on x86 too, adds the limits 1.2 and 1.5 to every architecture, and generates div_lookup, which was all zeros in that translation unit, so the restored signal was always 0 and the NEON limited-sample branch never ran. The AVX2 scale 1-3 kernel is not compared in the new test: it passes lanes below 2^15 to get_best15_from32, a negative shift under UBSan.
3ddbf51 to
372ca14
Compare
|
Rebased onto acdd937; the new head is 372ca14. The x86 kernels did not change upstream, so the fix is the same. The conflict was libvmaf/test/checkasm/check_adm.c, in check_adm_decouple_s123(): upstream's NEON commits added aarch64 sizes, gains (1.0, 1.1, 100) and value patterns to that test, which this PR also edits. I kept upstream's structure and its aarch64 cases and added to it: the gains 1.2 and 1.5 (so rst * gain is fractional) on every architecture, a pattern 5 whose distorted bands are scaled copies of the reference (so the angle test passes and the limit is taken), run on x86 as well, and the div_lookup generator. Release build with checkasm: all 28 meson tests pass, test_adm_decouple_gain passes, and |
With a fractional
adm_enhn_gain_limitthe AVX2 and AVX-512 integer ADM paths return a differentinteger_adm_scale0from the scalar path. The default limit (100) and the limit of 1 that thevmaf_v1.0.16models use are not affected, because the product is integral there.Cause
adm_decouple()(libvmaf/src/feature/integer_adm.c) storesMIN(rst * adm_enhn_gain_limit, t)orMAX(...), adouble, in an integer. That conversion truncates toward zero. Three vector kernels convert the product with an instruction that rounds to nearest:adm_decouple_avx2()(scale 0):_mm256_cvtpd_epi32adm_decouple_avx512()(scale 0):_mm512_cvtpd_epi32adm_decouple_s123_avx512()(scales 1 to 3):_mm512_cvtpd_epi64They differ from the scalar kernel by one in every limited sample whose product has a fraction of one half or more.
adm_decouple_s123_avx2()casts in C and already truncates.A second defect sits in the same two scale 0 kernels. The angle test forms the dot product and the squared magnitudes with
madd_epi16. The sum of two products of-32768 * -32768is 2^31, which does not fitint32and readsINT32_MIN, so a distorted sample withh = v = -32768sets the angle flag where the scalar kernel, which sums inint64, clears it (the reference side forms the same sum). Decoded pictures cannot reach this: scale 0 coefficients stay within 22916.check_admand any other caller that fills the bands directly can.Reproducer
Master
9e48141b(reproduced there; the x86 code is unchanged since 6ec23e8), release build, AVX-512 host (--cpumask 16is AVX2,-1is scalar). Checkerboard pair,integer_adm_scale0of the three frames:-1)16)0)The CLI prints six digits, so the full comparison below used a throwaway copy of the tree with
%.6freplaced by%.17ginoutput.c(not part of this change). Per-frameinteger_adm_scale0,integer_adm_scale1to3andinteger_adm2, AVX2 and AVX-512 against scalar, frames that differ / frames, and the largest absolute difference:src01576x324integer_adm_scale1to3do not differ on these decoded pairs even with the AVX-512 scale 1 to 3 kernel; the new test shows that kernel's difference on bands that take the limit (below).Fix
The three conversions become
_mm256_cvttpd_epi32,_mm512_cvttpd_epi32and_mm512_cvttpd_epi64.tis an integer, so truncating the product and then taking the integer minimum or maximum gives the scalar's sample. In the scale 0 angle test the squared magnitudes are read as unsigned (uint32_tlanes in AVX2,_mm512_cvtepu32_psin AVX-512) and a dot product ofINT32_MINis mapped back to 2^31.Tests
test_adm_decouple_gain(new,libvmaf/test/) runs the scale 0 kernels and the AVX-512 scale 1 to 3 kernel against the scalar kernels at limits 1, 1.2, 1.5 and 100, on bands in which three samples in four are a scaled copy of the reference (so the angle test passes and the limit is taken), and the scale 0 kernels again on bands that reach-32768. The first case uses the scale 0 coefficient bound of 22916 so that it isolates the rounding. On unpatched master it fails: AVX2 differs in 710 to 2470 samples per frame at limit 1.2 and AVX-512 in 492 to 2138 (scale 0), 562 to 2248 (scale 1 to 3). With only the conversions fixed, the-32768case still fails (36 samples at 64x48, limit 1.2). The AVX2 scale 1 to 3 kernel is not compared: it passes lanes below 2^15 toget_best15_from32, a negative shift under UBSan.check_adm's decouple test keeps the sizes, patterns and gains that adm: add NEON scale-zero decoupling #1656 added for the AArch64 NEON kernel and adds, on every architecture, the limits 1.2 and 1.5 and a pattern (5) whose distorted bands are derived from the reference (independent random bands almost never pass the angle test); x86 runs patterns 0 and 5. It also callsdiv_lookup_generator():div_lookupis astaticarray ininteger_adm.h, and onlyadm_buffer_alloc()ininteger_adm.cfills its own copy, so the copy incheck_adm.cwas never filled. With master'sadm_avx2.candadm_avx512.cand this test,checkasm --test=admfails 50 to 72 of 415 tests on each of 20 seeds. On this branch it passes on the same 20 seeds (415 tests).adm_decouple_neon()now runs incheckasm; with the zero lookup the reconstruction ratio was zero and it never did. On an aarch64 cross build under qemu-usercheckasm --test=admpasses with upstream's kernel (507 tests); a kernel that adds 1 to the limited sample fails it (55 of 507), and one that rounds the reconstruction instead of truncating fails 114 of 507. A shifted angle threshold (x1.0000001) or>for>= 0in the angle test is not caught; the NEON kernel itself is unchanged by this PR.Validation
Rebased on master 9e48141 (2026-10-02). x86-64 Linux, GCC 16.2.1, on master
9e48141b. Thecheckasm --benchcycle counts in this description were measured on 6ec23e8 and not repeated; the%.17gcomparison table, the test counts,checkasm --test=admand the reference-pair comparison were re-run on 9e48141.meson testwith-Denable_checkasm=true: 25/25 on master, 26/26 here (the new test), includingcheckasm.-Db_sanitize=address,undefinedbuild: 22 pass on master, 23 pass here (checkasmis one of the three that abort, in the existingadm_dwt2_16test).test_predictandtest_pic_preallocationabort on LeakSanitizer reports on both; this change does not touch them..engagement/golden_compare.py): identical per-frame and pooled output between master and this branch, default dispatch and--cpumask -1. Also identical at%.17gfor--cpumask 0,16and-1. No golden assertion changes.checkasm --bench, limit 100, cycles per call, median of 7 alternating runs (Ryzen 9 9950X3D): within run-to-run noise. The unchanged scalar kernel moves by up to 13% between the two builds, the vector kernels by 0.3% to 10%, none consistently in one direction (adm_decouple_64x48: AVX2 14019 to 13860, AVX-512 11766 to 11726;adm_decouple_s123_64x48: AVX2 28056 to 27345, AVX-512 18596 to 18618).Overlap: #1602 and #1600 also touch
adm_avx2.c,adm_avx512.c,check_adm.candlibvmaf/test/meson.build.git merge-treeagainst both branches reports no conflict.The workflow run on this PR needs a maintainer's approval.