Skip to content

fix(sycl): return the CPU's psnr_hvs scores bit for bit on SYCL and HIP - #1689

Merged
lusoris merged 1 commit into
masterfrom
fix/psnr-hvs-sycl-hip-exact-sum
Oct 1, 2026
Merged

lusoris merged 1 commit into
masterfrom
fix/psnr-hvs-sycl-hip-exact-sum

Conversation

@lusoris

@lusoris lusoris commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

psnr_hvs_sycl and psnr_hvs_hip now return the CPU extractor's scores bit for bit, as ADR-1397 decided for the GPU twins and #1666 did for psnr_hvs_cuda. Before, both summed each block on the device and were up to 1.66e-2 dB from --backend cpu at 3840x2160, beyond the ADR-1361 tolerance (3.34e-3 dB). Closes T-SYCL-PSNR-HVS-EXACT-SUM-2026-10-01 and T-HIP-PSNR-HVS-EXACT-SUM-2026-10-01.

Based on master after #1666 merged (the branch was developed on top of #1666 and rebased once it landed); it reuses core/src/feature/psnr_hvs_score.c from it.

Both kernels store the 64 terms of every block in the CPU's arithmetic and the hosts call psnr_hvs_score.c, which adds them in the CPU's order. The HIP kernel takes the CUDA kernel's form (masking table and threshold in double, module built with -ffp-contract=off and correctly rounded division). The SYCL kernel has no fp64 (ADR-0220): its masking table is a compile-time constant, and the CPU's sqrt((double)s_mask * s_gvar) comes from a new sqrt_prod_rn() in sycl_exact_fp.h, an integer product of the two significands and an integer square root rounded to nearest. That equals the CPU's two roundings because the root of a 48-bit integer is never within a double rounding of the midpoint of two float values (ADR-1401, Research-1401).

This costs throughput (numbers below), which the ADR-1397 decision accepts: correctness first, tuning afterwards.

Type

  • feat — new feature
  • fix — bug fix
  • perf — performance improvement
  • refactor — no behavior change
  • docs — documentation only
  • test — test-only
  • build / ci — tooling / infra
  • port — cherry-pick from upstream Netflix/vmaf
  • sycl / cuda / simd — backend-specific

Checklist

  • Commits follow Conventional Commits (the commit-msg hook enforces this).
  • make format && make lint is green locally. The pre-commit hook set passed on the whole diff. The clang-tidy ratchet was measured on every touched translation unit in its lane (--only): integer_psnr_hvs_sycl.cpp, test_sycl_psnr_hvs_parity.c, test_sycl_fp_arith_contract.c and test_sycl_fp_arith_probe.cpp (SYCL), integer_psnr_hvs_hip.c and test_hip_psnr_hvs_parity.c (HIP), test_cuda_psnr_hvs_parity.c (CUDA), with sycl_exact_fp.h and psnr_hvs_twin_parity.h through them: 0 findings in the touched files. test_hip_psnr_hvs_parity.c went from 18 to 0, so tidy-baseline-hip.json is tightened (make tidy-ratchet-write, scoped). The whole-tree lint was not run.
  • Unit tests pass: python3 scripts/ci/run_meson_test.py -- -C build --suite=fast on a CPU build and on a CPU + CUDA build (RTX 4090); on the SYCL build (Arc A380) test_sycl_psnr_hvs_parity, _simd32, _large, test_sycl_fp_arith_contract, test_sycl_shared_planes, test_sycl_kernel_scratch; on the HIP build (gfx1036) test_hip_psnr_hvs_parity, _large; test_psnr_hvs_twin_exact_sum_contract and test_psnr_hvs_score everywhere.
  • If I touched any SIMD/GPU code path, I ran /cross-backend-diff and the worst ULP is ≤ 2. The worst difference is 0: every output of every frame is bit-identical to the CPU (table below).
  • If I touched a feature extractor with SIMD/GPU twins, I either updated every twin or listed the gap under "Known follow-ups" below.
  • If I added a new .c / .cpp / .cu / .h / .hpp, it has the appropriate license header (see CONTRIBUTING.md).
  • If this is a breaking change, the commit message uses ! or BREAKING CHANGE: and the migration path is documented below.
  • If this PR adds an ADR, the ADR row lives in docs/adr/_index_fragments/<NNNN-slug>.md and the slug is appended to docs/adr/_index_fragments/_order.txt — do not edit docs/adr/README.md directly (regenerated by scripts/docs/concat-adr-index.sh; see ADR-0221).

Bug-status hygiene (ADR-0165)

  • docs/state.md updated in this PR: T-SYCL-PSNR-HVS-EXACT-SUM-2026-10-01 and T-HIP-PSNR-HVS-EXACT-SUM-2026-10-01 moved to Recently closed; T-SYCL-HIP-PSNR-HVS-EXACT-SUM-THROUGHPUT-2026-10-01 (RC3), T-SYCL-HIP-PSNR-HVS-YUV400-REFUSED-2026-10-01 (RC3) and T-HIP-GFX1036-SDMA-READ-FAULT-2026-10-01 (deferred) opened.

Netflix golden-data gate (ADR-0024)

  • I did not modify any assertAlmostEqual(...) score in the Netflix golden Python tests.
  • If I believe a golden value must change, I have explained why below AND pinged @lusoris for a CODEOWNERS exception.

No CPU extractor code changes.

Cross-backend numerical results

--precision max, psnr_hvs on --backend cpu against the twin of the same binary. Arc A380 (dg2-g11, xe driver, compute runtime 26.35.39758.10, IGC 2.41.5, oneAPI 2026.0.0, JIT) and gfx1036 (ROCm 7.2.4). Largest absolute difference in dB over psnr_hvs, psnr_hvs_y, psnr_hvs_cb and psnr_hvs_cr; "before" is origin/master c66d28b2f with #1666 applied.

fixture (frames)                         SYCL before   HIP before   after, both twins
Netflix 576x324 8-bit (48)               8.37e-5       8.37e-5      0, bit-identical
Netflix 576x324 10-bit (3)               4.87e-5       4.87e-5      0, bit-identical
Netflix 576x324 12-bit (3)               3.48e-5       3.48e-5      0, bit-identical
Netflix 576x324 4:2:2 10-bit (48)        7.41e-5       7.41e-5      0, bit-identical
checkerboard 1 px 1920x1080 8-bit (3)    1.71e-3       1.71e-3      0, bit-identical
checkerboard 10 px 1920x1080 8-bit (3)   7.41e-4       7.41e-4      0, bit-identical
checkerboard 1 px 1920x1080 10-bit (3)   2.95e-4       2.95e-4      0, bit-identical
checkerboard 10 px 1920x1080 10-bit (3)  4.50e-4       4.50e-4      0, bit-identical
BBB 1920x1080 8-bit (24)                 1.38e-3       1.38e-3      0, bit-identical
BBB 1920x1080 10-bit (24)                2.22e-3       2.22e-3      0, bit-identical
BBB 3840x2160 8-bit (24)                 1.10e-2       1.10e-2      0, bit-identical
BBB 3840x2160 10-bit (24)                1.66e-2       1.66e-2      0, bit-identical

Identical frames: 210 of 210 on each twin (0 of 210 before). The parity gate on the Netflix pair and on all 200 frames of BBB 3840x2160 reports tol=0.0e+00 (exact:ADR-1397) max_abs_diff=0.000e+00 OK for cpu ↔ sycl and for cpu ↔ hip. The 10-bit 1920x1080 and 3840x2160 fixtures are ffmpeg conversions of the 8-bit ones whose distorted side carries noise=alls=6:allf=t; BBB 1920x1080 is a bicubic downscale.

Further checks:

  • The SYCL kernel uses no scratch memory on the A380: private_mem_size 0 and spill_memory_size 0 at SIMD16 and with IGC_ForceOCLSIMDWidth=32.
  • sqrt_prod_rn() has 0 mismatches against the host's (float)sqrt((double)a * (double)b) on 1 312 917 operand pairs on the A380 (test_sycl_fp_arith_contract: random, boundary, and the k (k + 1) pairs nearest a rounding midpoint) and on 450 million on the host. A float product and root differ for 34 % of random pairs.
  • With the sum in place, restoring one arithmetic difference on the A380 still breaks identity: a float product and root in the threshold on 3 of the 48 Netflix frames (4.4e-7 dB), a float masking table on 9 of them (7.4e-7 dB).
  • The HIP twin was run 125 more times on the 3840x2160 10-bit fixture: 3000 of 3000 frames equal the CPU's.
  • The comparison is inside one binary. The dB value uses the host's log10: the SYCL build (icx, Intel libimf) and a gcc build (glibc) differ by one unit in the last place on 3 of the 48 Netflix frames, on the CPU extractor and on the twin alike.

Performance (if perf or feat)

Slower, as expected. ms per frame of vmaf --backend <b> --no_prediction --feature psnr_hvs, (t(N) − t(2)) / (N − 2), N = 960 at 576x324 (the Netflix pair concatenated 20 times) and 102 at the larger sizes; each cell is the median over four passes (three for the 10-bit row) of the median of three runs, before and after binaries back to back. Other sessions shared the host (load average 12 to 29); each device was held under its lock.

frame size          SYCL A380 before   after    HIP gfx1036 before   after    CPU, 16 threads
576x324             0.41               0.61     0.39                 0.65     0.16
1920x1080           5.4                9.2      3.9                  8.7      1.7
3840x2160           22.4               35.9     18.3                 37.9     6.7
3840x2160 10-bit    17.6               25.2     28.8                 46.0     9.4

The SYCL passes agree within 0.8 ms at 3840x2160. The HIP figures are noisy: the integrated GPU shares memory with the busy host, and its 3840x2160 passes spread from 17.6 to 28.7 ms before and from 30.6 to 44.9 ms after. Both twins were slower than sixteen CPU threads before this change as well. The increase is the readback (64.8 MB per 3840x2160 frame instead of 1.0 MB) and the sequential host sum; T-SYCL-HIP-PSNR-HVS-EXACT-SUM-THROUGHPUT-2026-10-01 tracks the tuning, with the candidates of the CUDA row.

Deep-dive deliverables (ADR-0108)

  • Research digest — docs/research/1401-psnr-hvs-sycl-hip-exact-twins.md.
  • Decision matrix — captured in ADR-1401 ## Alternatives considered.
  • AGENTS.md invariant note — core/src/feature/sycl/AGENTS.md (the exact-sum contract and sqrt_prod_rn()), core/src/feature/hip/AGENTS.md, core/src/feature/AGENTS.md (psnr_hvs_score.c callers, the log10 note), core/src/feature/cuda/AGENTS.md (shared test header), scripts/ci/AGENTS.md (EXACT_TWINS), and the index entry in docs/development/rebase-sensitive-invariants.md.
  • Reproducer / smoke-test command — pasted below under "Reproducer".
  • CHANGELOG fragment — changelog.d/fixed/sycl-hip-psnr-hvs-cpu-float-sum.md.
  • Rebase note — docs/rebase-notes.md, "ADR-1401 — psnr_hvs_sycl and psnr_hvs_hip return the CPU's scores bit for bit".

Reproducer

# SYCL build (Intel GPU) and HIP build (AMD GPU); device-free guard last.
ninja -C build test/test_sycl_psnr_hvs_parity test/test_sycl_fp_arith_contract
./build/test/test_sycl_psnr_hvs_parity && ./build/test/test_sycl_fp_arith_contract
ninja -C build-hip test/test_hip_psnr_hvs_parity && ./build-hip/test/test_hip_psnr_hvs_parity
python3 core/test/test_psnr_hvs_twin_exact_sum_contract.py
python3 scripts/ci/cross_backend_parity_gate.py --vmaf-binary build/tools/vmaf \
    --reference python/test/resource/yuv/src01_hrc00_576x324.yuv \
    --distorted python/test/resource/yuv/src01_hrc01_576x324.yuv \
    --width 576 --height 324 --features psnr_hvs --backends cpu sycl

On the previous twins test_sycl_psnr_hvs_parity and test_hip_psnr_hvs_parity fail in test_psnr_hvs_cpu_*_identical (3.8e-6 dB at 256x144), the contract test reports 27 failures, and the gate reports max_abs_diff=8.370e-05 FAIL under the new contract.

Known follow-ups

  • T-SYCL-HIP-PSNR-HVS-EXACT-SUM-THROUGHPUT-2026-10-01: both exact twins are slower than sixteen CPU threads at every size measured.
  • T-SYCL-HIP-PSNR-HVS-YUV400-REFUSED-2026-10-01: both twins refuse 4:0:0 input, which the CPU extractor and psnr_hvs_cuda score on luma. Found adding the shared parity cases; docs/metrics/psnr-hvs.md claimed luma-only scoring for all three twins and is corrected here.
  • T-HIP-GFX1036-SDMA-READ-FAULT-2026-10-01: one run of the new HIP twin on 24 frames of the 3840x2160 10-bit fixture was killed by a GPU memory access fault that the kernel log attributes to the copy engine (SDMA0, a read). 131 further runs of that command and 126 runs of the previous twin did not fault. The cause is not established; the twin now moves 65 times more data through that engine per frame.
  • The Metal twin is outside the parity gate's backend list and unchanged.
  • Not fixed here: scripts/ci/test_cross_backend_feature_names.py fails on master (3 tests still expect the vulkan backend removed by ADR-0726); T-CI-PARITY-GATE-STALE-METRIC-KEYS-2026-09-29 already tracks it.
  • Corrected on the way: the (ADR-ADRNUM) placeholder perf(hip): upload native samples and convert on device in psnr_hvs #1658 left in psnr_hvs_score.hip; a GPU parity test without a device now exits 77 (skipped) instead of 0 for the three psnr_hvs twin tests.

Breaking changes / migration

None in the API. psnr_hvs_sycl and psnr_hvs_hip scores change in their last digits (by up to 8.4e-5 dB at 576x324 and 1.7e-2 dB at 3840x2160) to the values --backend cpu returns.

@lusoris
lusoris force-pushed the fix/psnr-hvs-sycl-hip-exact-sum branch from af99885 to 05a2421 Compare October 1, 2026 12:54
@lusoris
lusoris force-pushed the fix/psnr-hvs-sycl-hip-exact-sum branch 3 times, most recently from 31c7bc7 to fa073b5 Compare October 1, 2026 14:25
psnr_hvs_sycl and psnr_hvs_hip now return the CPU extractor's scores bit
for bit, as ADR-1397 decided for the GPU twins and psnr_hvs_cuda already
did. Before, both summed each block on the device and were up to 1.7e-2 dB
from --backend cpu at 3840x2160, beyond the ADR-1361 parity tolerance
(3.34e-3 dB).

- Both kernels store the 64 terms of every block, computed in the CPU's
  arithmetic, and the hosts call psnr_hvs_score.c, which adds them in the
  CPU's order.
- HIP: masking table and threshold in double, integer coefficient
  difference, module built with -ffp-contract=off and correctly rounded
  fp32 division.
- SYCL: the kernel stays fp64-free (ADR-0220). The masking table is a
  compile-time constant. The CPU's sqrt((double)s_mask * s_gvar) comes
  from sqrt_prod_rn() (new in sycl_exact_fp.h): an integer product of the
  two significands and an integer square root, rounded to nearest. It
  equals the CPU's two roundings because the root of a 48-bit integer is
  never within a double rounding of the midpoint of two floats. The kernel
  uses no scratch memory: private_mem_size and spill_memory_size are 0 on
  an Arc A380 at SIMD16 and SIMD32.
- The parity gates list sycl and hip in EXACT_TWINS, so every psnr_hvs
  cell is compared with tolerance 0 at --precision max.
- test_sycl_psnr_hvs_parity and test_hip_psnr_hvs_parity assert equality
  on all four outputs, 3840x2160 included; they share
  core/test/psnr_hvs_twin_parity.h with the CUDA test.
  test_sycl_fp_arith_contract checks sqrt_prod_rn() on the device against
  the host's double expression. test_psnr_hvs_twin_exact_sum_contract.py
  covers the three twins without a device.

Measured on an Arc A380 (xe driver) and a gfx1036 at --precision max:
every frame of the Netflix 576x324 pairs (8, 10, 12 bits, 4:2:2), the
1920x1080 checkerboard pairs and BBB 1920x1080 / 3840x2160 (8 and 10
bits) equals the CPU extractor of the same binary. CPU scores are
unchanged. The dB value uses the host's log10, which differs by one unit
in the last place between an icx and a gcc build, so a twin is compared
with the CPU extractor of its own binary.

The exact sum costs throughput: 35.9 ms per 3840x2160 frame instead of
22.4 ms on the A380 and about 38 ms instead of 18 ms on the gfx1036, and
256 bytes of readback per block. Tuning is tracked as
T-SYCL-HIP-PSNR-HVS-EXACT-SUM-THROUGHPUT-2026-10-01. Two more rows are
opened: both twins refuse 4:0:0 input
(T-SYCL-HIP-PSNR-HVS-YUV400-REFUSED-2026-10-01), and one of 132 runs of
the HIP twin at 3840x2160 10-bit ended in a GPU memory access fault that
did not recur (T-HIP-GFX1036-SDMA-READ-FAULT-2026-10-01).

Closes T-SYCL-PSNR-HVS-EXACT-SUM-2026-10-01 and
T-HIP-PSNR-HVS-EXACT-SUM-2026-10-01. ADR-1401, Research-1401.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant