fix(gpu): make HIP and Metal CAMBI use the shared TVI bisection and the CPU border rules - #1378
Draft
lusoris wants to merge 1 commit into
Draft
fix(gpu): make HIP and Metal CAMBI use the shared TVI bisection and the CPU border rules#1378lusoris wants to merge 1 commit into
lusoris wants to merge 1 commit into
Conversation
lusoris
force-pushed
the
fix/gpu-cambi-tvi-bisection
branch
6 times, most recently
from
September 7, 2026 13:15
f47062d to
12c2c82
Compare
…he CPU border rules
CAMBI on the HIP backend returned exactly 0.0 on banding content that the CPU
scores at 5.85. Three divergences, each independently sufficient to move the
score, and a parity fixture that could not see any of them.
1. TVI table. cambi.c::get_tvi_for_diff bisects tvi_hard_threshold_condition
between luma_range.foot (64) and luma_range.head - diff - 1. The HIP and
Metal twins each hand-rolled a search over the NEGATED predicate, seeded from
luma 0 instead of foot, and derived vlt_luma as the largest luma below the
visibility threshold where the CPU takes the smallest at or above it.
Replicating both at the default max_log_contrast = 2:
diff CPU hand-rolled
1 182 1026
2 309 1025
3 436 1024
4 563 4
Since v_band_size = tvi_for_diff[num_diffs-1] + 1 - v_band_base, the scored
luma band collapses from 564 entries to a handful and calculate_c_values()
discards almost every pixel as out-of-band. Both twins now call
vmaf_cambi_init_tvi_and_vlt(), the CPU's own bisection, which the SYCL twin
has always used.
2. filter_mode border rows. cambi.c writes its vertical pass back under
`if (i > 1)`, covering output rows 1..height-2, so rows 0 and height-1 keep
the ORIGINAL unfiltered pixels -- the horizontal results for those rows live
only in the 3-row ring buffer. CUDA and SYCL already carried the matching
guard; HIP and Metal filtered those rows.
3. Mask box sum. get_spatial_mask_for_index() accumulates a ZERO-PADDED
summed-area table, so an out-of-frame tap adds nothing. The HIP kernel
clamped each of the 49 taps to the border pixel, counting its zero-derivative
flag up to three extra times per axis and flipping box_sum > mask_index on a
band of border pixels. CUDA and Metal already zero-pad.
Measured on gfx1030 with a 640x480 10-bit banding gradient:
before cpu = 5.84615400 hip = 0.00000000
after cpu = 5.8461540042 hip = 5.8461540042 (bit-exact)
Fixing only the TVI table left a 6.15e-3 residual against the 1e-3 gate --
close enough to be waved through as float noise. Running the CUDA twin on the
identical fixture gave cpu == gpu bit-exact, which proved the residual was
HIP-specific and led to defects 2 and 3.
Nothing caught any of this because both the HIP and Metal CAMBI parity fixtures
were 8-bit gradients stepping 32 code levels every 32 columns. CAMBI counts
neighbour differences of 1..num_diffs (4 at the default), so a 32-level step is
an edge, not banding: those fixtures scored exactly 0.0 on the CPU as well and
the parity assertion was 0 == 0. Both now use a 10-bit gradient of one code
level every two columns held inside the TVI band, and assert the CPU score is
non-degenerate before comparing.
The Metal twin gets fixes 1 and 2 by construction -- there is no Apple hardware
here to measure it on. CUDA and SYCL were unaffected and stay bit-exact.
ADR-1219.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
force-pushed
the
fix/gpu-cambi-tvi-bisection
branch
from
September 7, 2026 16:37
12c2c82 to
f3ac7ab
Compare
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
CAMBI on the HIP backend returned exactly
0.0on banding content that theCPU scores at
5.85. Not a tolerance drift — a total collapse, on defaultoptions, on any input CAMBI is meant to detect. Three independent divergences,
behind a parity fixture that asserted
0 == 0.1. The TVI table (HIP + Metal)
cambi.c::get_tvi_for_diff()bisectstvi_hard_threshold_conditionbetweenluma_range.foot(64) andluma_range.head - diff - 1. Both twins hand-rolled asearch over the negated predicate, seeded from luma
0instead offoot,and derived
vlt_lumaas the largest luma below the visibility threshold wherethe CPU takes the smallest at or above it. Replicating both against the same
luminance model at the default
max_log_contrast = 2:diffv_band_size = tvi_for_diff[num_diffs-1] + 1 - v_band_base, so the scored lumaband collapses from 564 entries to a handful and
calculate_c_values()discardsalmost every pixel as out-of-band. Both twins now call
vmaf_cambi_init_tvi_and_vlt()— the CPU's own bisection, which the SYCL twinhas always used.
2.
filter_modeborder rows (HIP + Metal)cambi.cwrites its vertical pass back underif (i > 1), covering output rows1 .. height-2. Rows0andheight-1keep the original unfiltered pixels;their horizontal results live only in the 3-row ring buffer. CUDA and SYCL
already carried the matching guard.
3. The 7x7 mask box sum (HIP only)
get_spatial_mask_for_index()accumulates a zero-padded summed-area table,so an out-of-frame tap adds nothing. The HIP kernel clamped each of the 49
taps to the border pixel, counting its zero-derivative flag up to three extra
times per axis. CUDA and Metal already zero-pad.
Reproducer
meson setup build-hip core -Denable_hip=true -Denable_hipcc=true -Denable_cuda=false -Denable_sycl=false -Db_lto=false ninja -C build-hip meson test -C build-hip test_hip_cambi_parity --print-errorlogs -vMeasured on gfx1030, 640x480 10-bit banding gradient:
Revert
core/src/feature/hip/and the test fails with that 5.85 delta.How the three were separated
Fixing only the TVI table left a
6.15e-3residual against the1e-3gate —close enough to look like GPU float noise and be waved through with a wider
tolerance. Running the CUDA twin on the identical fixture gave
cpu = gpu = 5.8461540042, bit-exact, which proved the residual wasHIP-specific rather than a general GPU/CPU property; diffing the HIP kernels
against the CUDA ones then located defects 2 and 3 directly. No tolerance was
widened.
Why nothing caught it
Both the HIP and Metal CAMBI parity fixtures were 8-bit gradients stepping 32
code levels every 32 columns. CAMBI counts neighbour differences of
1 .. num_diffs(4 at the default), so a 32-level step is an edge, notbanding: those fixtures scored exactly
0.0on the CPU as well, and theparity assertion compared zero to zero. Both now use a 10-bit gradient of one
code level every two columns held inside the TVI band (
200..900), and assertcpu > 1.0before comparing so the gate cannot rot back.Scope
Metal gets fixes 1 and 2 by construction — there is no Apple hardware here to
measure it on; that is stated in the ADR rather than implied. CUDA and SYCL were
unaffected and stay bit-exact on the same fixture.
Deep-dive deliverables (ADR-0108)
docs/research/2036-cambi-twin-drift-and-vacuous-fixtures.md: all three defects, the TVI table comparison, and how a second exact twin separated them without touching the tolerance.docs/adr/1219-gpu-cambi-tvi-shared-bisection.md## Alternatives considered(four options; the runner-up "port the CPU bisection into each twin" re-creates the duplication that caused this).core/src/feature/hip/AGENTS.mdandcore/src/feature/metal/AGENTS.mdcovering all three traps plus the fixture requirement.changelog.d/fixed/1219-gpu-cambi-tvi-and-borders.md.docs/rebase-notes.md— entryADR-1219 — CAMBI TVI bisection and border rules on the HIP/Metal twins (2026-09-07).Docs (rule 10)
docs/metrics/cambi.mdgains a prominent note under## GPU support: what HIP returned, why, which fixes landed, and theinstruction to re-measure any CAMBI score taken on the HIP backend.
Bug status (rule 13 / ADR-0165)
docs/state.md— closesT-GPU-CAMBI-HIP-SCORE-COLLAPSE-2026-09-07.🤖 Generated with Claude Code