Skip to content

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) - #1890

Merged
lusoris merged 1 commit into
masterfrom
feat/upstream-parity-guard
Oct 3, 2026
Merged

lusoris merged 1 commit into
masterfrom
feat/upstream-parity-guard

Conversation

@lusoris

@lusoris lusoris commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

make upstream-parity compares this tree's CPU extractors with Netflix/vmaf at the recorded parity head, every emitted value at %.17g, in the dev container image, and fails on a difference that no recorded deviation covers. It makes the rule of ADR-1487 enforceable: code inherited from Netflix evaluates as Netflix's source does, and a difference needs an ADR.

What it does:

  • Builds Netflix/vmaf at the pin (the heading in docs/development/known-upstream-bugs.md, read through scripts/ci/upstream_parity_pin.py) and this tree with the golden build profile, both with the same compilers, both in the dev container image (--container, what the make targets pass).
  • Runs one harness (scripts/dev/upstream_parity_harness.c) against each tree through the C API: 16 shared extractors, 169 option variants, 32 model requests, on 31 fixtures, at scalar and default dispatch (the full matrix adds AVX2).
  • Attributes every difference to one fragment under scripts/ci/upstream_parity.d/. Fails on a difference no fragment covers, on one above its fragment's bound, on a stale fragment, and on a crash of this tree's harness. Exit 2 means it could not compare; that is never a pass.

Why the dev image, and the heap check

  • Netflix's own values depend on the environment. Its ciede2000() calls powf(x, 2): on the host (glibc 2.44) that differs from the product on 119 of 327 frames (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); in the dev image (glibc 2.43) on none. So a comparison is evidence only in one recorded environment. Both result documents record the image id, the compilers and the C library; documents from two environments are not compared; outside the image the guard exits 2 unless --unpinned marks the verdict advisory.
  • Some upstream outputs are undefined. Repeating the full matrix on the host with the heap filled moved 48 upstream runs and twice took a difference above the bound it had then (ciede on odd sizes, float_motion scale-1 chroma: 25.2, then 61). --heap-check (in make upstream-parity-full) reruns every request with MALLOC_PERTURB_=170: an output of this tree that changes fails the guard; an upstream output that changes may only be covered with bound: inf. In the image: 1,675 upstream outputs in 45 runs change, none of this tree's.

Result on master

Dev image sha256:43ef1e32cb32 (GCC 15.2.0, glibc 2.43) on ryzen-4090-arc, master 96af5b34e with this PR against Netflix 9e48141b.

Probe set (make upstream-parity) Full matrix (make upstream-parity-full)
Runs per tree 1,848 5,349
Values compared 252,162 886,002
Identical 219,592 780,741
Differences covered by a deliberate fragment 19,958 70,826
Differences covered by a pending fragment 6,180 12,690
Not covered / above bound / stale 0 / 0 / 0 0 / 0 / 0
Heap check: upstream outputs that change / this tree's not run 1,675 in 45 runs / 0
Verdict PASS PASS

42 fragments: 37 deliberate (D1 to D15 of the audit, ADR-1494, and the harmonic-mean guard of ADR-1008) and 5 pending. Every fragment's evidence line is the measurement in the image; no bound had to move from the host measurement. ADR-1467's ciede product has no fragment: in the image there is no difference to cover.

A planted change fails it (in the image): 10 * changed to 10.000000000000002 * in one line of core/src/feature/float_psnr.c gave 376 differences not covered, exit 1.

The pending fragments end as designed. Four reverts landed while this PR was in review (#1891, #1892, #1894, #1895); their seven fragments went stale and are removed here. With the last one, the SpEED revert, applied in a scratch copy, the full matrix has 795,000 identical values of 886,002, nothing outside the allowlist, and exactly its 3 pending-revert fragments stale.

Pending fragments and who removes them

Branch Fragments
fix/speed-upstream-double-math speed_chroma.float-math, speed_temporal.float-math, model.speed-float-math
port/upstream-motion-five-frame-window motion.five-frame-window, model.hfr-five-frame-window

Whichever of this PR and one of those branches lands second deletes the fragments.

ADR-1494

adm and float_adm refuse frames below 17x17 (fork PRs #1473, #1770), which no ADR recorded. Measured in the image on the 16x16, 12x9 and 8x8 crops: upstream's integer ADM ends in signal 11 on all three; its float_adm returns values, which at 12x9 and 8x8 change with the heap's contents (adm2 3.94 or 3.38; 1.42 or 3.6e-11). The two error fragments cite it.

Other changes

  • testdata/bench_upstream_ab.py builds upstream through the guard and takes its score verdict from the guard's comparison (marked advisory when run on the host). --max-score-delta is gone; the default upstream is the recorded head, not v3.2.0.
  • scripts/ci/cross_backend_calibration.py: the fragment line parser is shared (parse_fragment_fields(), check_fragment_adrs()); exact_twins.d loads as before.
  • scripts/ci/setup-golden-build.sh honours GOLDEN_NINJA_JOBS.

CI

Not wired, by decision of ADR-1487: a hosted job would need the same image, and the bounds are measured in that image on one host so far. T-UPSTREAM-PARITY-GUARD-HOSTED-JOB-2026-10-02 records what a nightly job needs. The script's tests need no build and run in a pre-commit hook.

Type

  • feat — new feature
  • build / ci — tooling / infra

Checklist

  • Commits follow Conventional Commits.
  • Unit tests pass: python3 -B scripts/ci/tests/test_upstream_parity_allowlist.py (25), python3 -B scripts/dev/tests/test_upstream_parity.py (52), pytest scripts/ci/test_calibration.py scripts/ci/test_cross_backend_parity_gate.py (109).
  • New .c file carries the licence header (scripts/dev/upstream_parity_harness.c, EUPL-1.2).
  • ADR rows live in docs/adr/_index_fragments/; docs/adr/README.md is regenerated.
  • Cross-backend diff: not applicable, no extractor, SIMD or GPU source changes.

Bug-status hygiene (ADR-0165)

  • docs/state.md updated: T-UPSTREAM-PARITY-PENDING-FRAGMENTS-2026-10-02 and T-UPSTREAM-PARITY-GUARD-HOSTED-JOB-2026-10-02 opened.

Netflix golden-data gate (ADR-0024)

  • I did not modify any assertAlmostEqual(...) score in the Netflix golden Python tests.

Deep-dive deliverables (ADR-0108)

  • Research digest — docs/research/1487-upstream-parity-audit.md.
  • Decision matrix — docs/adr/1487-upstream-parity-policy-and-guard.md and docs/adr/1494-adm-refuses-frames-below-17.md, ## Alternatives considered.
  • AGENTS.md invariant note — scripts/ci/AGENTS.d/upstream-parity.md.
  • Reproducer / smoke-test command — below.
  • CHANGELOG fragment — changelog.d/added/upstream-parity-guard.md, changelog.d/changed/bench-upstream-ab-parity-guard.md.
  • Rebase note — docs/rebase-notes.md, "Upstream parity guard and its allowlist".

Reproducer

docker compose -f dev/docker-compose.yml build dev-mcp   # the pinned image
scripts/test/fetch-test-yuvs.sh           # the three golden pairs
make upstream-parity                      # probe set: PASS, 0 not covered, 0 stale
make upstream-parity-full                 # full matrix with the heap check
python3 -B scripts/ci/tests/test_upstream_parity_allowlist.py
python3 -B scripts/dev/tests/test_upstream_parity.py
scripts/dev/upstream_parity.py --mode probe   # on the host: exit 2, not in the pinned environment
# a planted difference fails: in core/src/feature/float_psnr.c write
#   MIN(10.000000000000002 * log10(...)) and run
#   scripts/dev/upstream_parity.py --container --only F.float_psnr   -> exit 1, 376 not covered

Known follow-ups

  • Hosted nightly job in the same image: T-UPSTREAM-PARITY-GUARD-HOSTED-JOB-2026-10-02.
  • Where upstream's value is undefined (ciede.odd-size-chroma, float_motion.scale1-stride, speed_temporal.prescale-above-one) the bound is inf; this tree's values there are held by the tests of ADR-1483, ADR-1486 and ADR-1480.
  • Not compared: aarch64, clang / icx / MSVC, any C library but the image's, more than one thread, the extractors only this tree has.

@github-actions github-actions Bot added the type:feature New feature or request label Oct 2, 2026
@lusoris
lusoris force-pushed the docs/adr-deliberate-upstream-deviations branch from d6719ba to 79bdacd Compare October 2, 2026 21:50
@lusoris
lusoris force-pushed the feat/upstream-parity-guard branch from 08f4f81 to dde77ae Compare October 2, 2026 22:13
@lusoris
lusoris force-pushed the docs/adr-deliberate-upstream-deviations branch from 79bdacd to 7c2c559 Compare October 2, 2026 22:29
Base automatically changed from docs/adr-deliberate-upstream-deviations to master October 2, 2026 22:30
@lusoris
lusoris force-pushed the feat/upstream-parity-guard branch from dde77ae to af68c66 Compare October 2, 2026 23:56
@lusoris lusoris changed the title feat(parity): compare every CPU value with Netflix/vmaf at the recorded head and fail on a difference no ADR covers (ADR-1487) 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) Oct 2, 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
@lusoris
lusoris force-pushed the feat/upstream-parity-guard branch from af68c66 to 623b11c Compare October 3, 2026 00:26
@lusoris
lusoris merged commit 623b11c into master Oct 3, 2026
1 of 38 checks passed
@lusoris
lusoris deleted the feat/upstream-parity-guard branch October 3, 2026 00:26
lusoris added a commit that referenced this pull request Oct 3, 2026
…ion port made stale

The five-frame motion port (#1887) landed before the upstream parity
guard (#1890), so master fails make upstream-parity-full with
motion.five-frame-window and model.hfr-five-frame-window stale: the
port is exact and nothing is attributed to them any more.

Both fragments go. In the dev image (43ef1e32cb32: GCC 15.2.0, glibc
2.43) against Netflix 9e48141b the full matrix with the heap check then
passes: 5,349 runs per tree, 114 more of them complete on both trees
than before the port, 911,802 values, 803,151 identical, 86,492
differences all covered by the 40 remaining fragments, none above a
bound, none stale; 1,675 upstream outputs depend on the heap, none of
this tree's. The probe set passes too.

The guide's result table and the pending-fragments ledger row follow.
lusoris added a commit that referenced this pull request Oct 3, 2026
…ion port made stale (#1900)

* fix(parity): remove the two pending-port fragments the five-frame motion port made stale

The five-frame motion port (#1887) landed before the upstream parity
guard (#1890), so master fails make upstream-parity-full with
motion.five-frame-window and model.hfr-five-frame-window stale: the
port is exact and nothing is attributed to them any more.

Both fragments go. In the dev image (43ef1e32cb32: GCC 15.2.0, glibc
2.43) against Netflix 9e48141b the full matrix with the heap check then
passes: 5,349 runs per tree, 114 more of them complete on both trees
than before the port, 911,802 values, 803,151 identical, 86,492
differences all covered by the 40 remaining fragments, none above a
bound, none stale; 1,675 upstream outputs depend on the heap, none of
this tree's. The probe set passes too.

The guide's result table and the pending-fragments ledger row follow.

This branch was successfully deployed

1 active deployment
github-pages — 623b11ce Deployed Oct 3, 2026 by lusoris via deploy #4125
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant