Skip to content

fix(gpu): mirror the CPU integer_adm option table on the CUDA, SYCL and HIP twins - #1324

Merged
lusoris merged 7 commits into
masterfrom
feat/gpu-adm-csf-mode-parity
Sep 5, 2026
Merged

lusoris merged 7 commits into
masterfrom
feat/gpu-adm-csf-mode-parity

Conversation

@lusoris

@lusoris lusoris commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

The CUDA, SYCL and HIP integer_adm twins declared only a drifted subset of the CPU option table — no adm_csf_mode, no adm_p_norm, aliases that disagreed with core/src/feature/integer_adm.c (cs/cds/ss0/sai instead of scf/scfd/ssz). That is not a cosmetic gap: vmaf_feature_name_from_options() builds the emitted feature key from the extractor's own options[], so a twin missing one VMAF_OPT_FLAG_FEATURE_PARAM entry answers under a shorter key than the CPU twin for the same opts dict and the model lookup finds nothing — silently. The fork's default model vmaf_v1.0.16_3d0h asks for VMAF_integer_feature_adm3_score under integer_adm3_csf_2_dlmw_0.7_egl_1_min_0.5_nw_0.02, so adm_csf_mode=2 was both ignored arithmetically and absent from the key. All four CSF models and adm_p_norm are now implemented on all three twins and every option table is an entry-for-entry mirror of the CPU table; three further CPU-parity defects found while verifying it are fixed too (adm_min_val clamped adm2 where the CPU floors adm3 alone, numden_limit used the scale-3 area instead of the full-frame area, and the SYCL/HIP twins emitted aim_score/adm3_score from a hard-coded aim_num = 0.0). Measured CPU-vs-GPU pooled means on the 576x324 Netflix pair over adm2 + aim + adm3 + scale0..3: CUDA max |delta| 0.0e+00 at defaults, 0.0e+00 at csf_mode=2, 1.0e-06 at csf_mode=3, 1.0e-06 at p_norm=2.0, 0.0e+00 under the full model dict; SYCL (adm2 + scale0..3) 1.0e-06 / 0.0e+00 / 1.0e-06 / 5.0e-06 / 1.0e-06. Netflix golden gate 271 passed, 12 skipped, 0 failed. Deferred and recorded as Open rows in docs/state.md, not silently dropped: the SYCL/HIP AIM device pass (T-GPU-ADM-AIM-DEVICE-PASS-MISSING-SYCL-HIP-2026-09-05 — both features now fall back to the CPU twin rather than being fabricated), the pre-existing HIP integer_adm GPU page fault that blocks hardware verification of the HIP half (T-HIP-INTEGER-ADM-GPU-PAGE-FAULT-2026-09-05), the meson fatbin header-dependency gap (T-CUDA-FATBIN-NO-HEADER-DEP-2026-09-05), the pre-existing Arc A380 adm2 parity gap (T-SYCL-ARC-ADM2-PARITY-1.1E-4-2026-09-05), the degenerate adm_csf_mode=1 on the CPU reference itself (T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05), and the icx golden-gate trap found while gating this branch (T-GOLDEN-GATE-ICX-FP-DRIFT-2026-09-05).

Type

  • fix — GPU integer_adm twins now honour the CPU option table and the feature-name key it produces.

Checklist

  • Commits follow Conventional Commits.
  • make format && make lint is green locally (pre-commit on every touched file: pre-commit run --files ./CHANGELOG.md ./changelog.d/fixed/gpu-adm-csf-mode-parity.md ./core/src/feature/AGENTS.md ./core/src/feature/cuda/integer_adm_cuda.{c,h} ./core/src/feature/hip/integer_adm_hip.{c,h} ./core/src/feature/sycl/integer_adm_sycl.cpp ./core/test/test_{cuda,sycl,hip}_adm_parity.c ./docs/backends/{cuda,sycl,hip}/overview.md ./docs/metrics/features.md ./docs/rebase-notes.md ./docs/state.md ./python/test/gpu_default_model_test.py — all hooks Passed; scripts/ci/twin-drift-check.sh, scripts/ci/assertion-density.sh and scripts/ci/check-copyright.sh all PASS).
  • Unit tests: meson test -C core/build test_cuda_adm_parity test_adm_csf test_barten_csf test_adm_csf_tools_coverage → 4 OK / 0 Fail. ./core/build/test/test_cuda_adm_parity 4/4 pass; ./core/build/test/test_sycl_adm_parity 5 run, 1 failed — the pre-existing Arc A380 test_adm_cpu_sycl_parity gap, reproduced unchanged against origin/master's integer_adm_sycl.cpp; ./core/build/test/test_hip_adm_parity 2 structural tests pass and the device test faults the GPU on origin/master too. python3 -m pytest python/test/gpu_default_model_test.py → 3 passed, 2 skipped (HIP), 3 subtests passed.
  • Docs in the same PR: docs/metrics/features.md, docs/backends/cuda/overview.md, docs/backends/sycl/overview.md, docs/backends/hip/overview.md.
  • SIMD/GPU, twins, new C sources, breaking change, ADR — GPU: all three integer_adm twins changed and cross-checked against the CPU reference on real content (table above) plus the C parity tests; twins: every twin of this extractor is updated in this PR, and the two that cannot compute AIM declare that instead of faking it; new C sources: none (one new Python test file, python/test/gpu_default_model_test.py, carries the Lusoris header); breaking change: none — every delta at default options is 0.0e+00 and the Netflix golden gate is unchanged; ADR: n/a, this restores conformance to decisions ADR-0487 / ADR-0530 / ADR-0746 already own rather than making a new one.

Bug-status hygiene (ADR-0165)

  • docs/state.md — Recently closed: T-GPU-ADM-CSF-MODE-NOT-PORTED-2026-09-05 with the per-twin delta table. Open: T-GPU-ADM-AIM-DEVICE-PASS-MISSING-SYCL-HIP-2026-09-05, T-HIP-INTEGER-ADM-GPU-PAGE-FAULT-2026-09-05, T-CUDA-FATBIN-NO-HEADER-DEP-2026-09-05, T-SYCL-ARC-ADM2-PARITY-1.1E-4-2026-09-05, T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05, T-GOLDEN-GATE-ICX-FP-DRIFT-2026-09-05.

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 — no digest needed: trivial — core/src/feature/integer_adm.c is the normative reference and the whole change is conformance to it.
  • Decision matrix — no alternatives: only-one-way fix — the twins either mirror the CPU option table and its arithmetic or they emit the wrong key and the wrong value.
  • AGENTS.md invariant note — core/src/feature/AGENTS.md, section "GPU-twin VmafOption tables mirror the CPU table (2026-09-05)".
  • Reproducer / smoke-test command — below.
  • CHANGELOG fragment — changelog.d/fixed/gpu-adm-csf-mode-parity.md
  • Rebase note — docs/rebase-notes.md entry

Reproducer

# 1. Feature-name-key identity + value parity, CPU vs each GPU twin, on the
#    default model's ADM option dict. This is the failure the PR fixes: before
#    it, the GPU twins answered under a key without `_csf_2`.
Y=python/test/resource/yuv
O='adm_csf_mode=2:adm_dlm_weight=0.7:adm_enhn_gain_limit=1.0:adm_min_val=0.5:adm_noise_weight=0.02'
for pair in "adm|--no_cuda --no_sycl --no_hip" "adm_cuda|--backend cuda" "adm_sycl|--backend sycl"; do
  fex=${pair%%|*}; flags=${pair#*|}
  ./core/build/tools/vmaf -r $Y/src01_hrc00_576x324.yuv -d $Y/src01_hrc01_576x324.yuv \
      -w 576 -h 324 -p 420 -b 8 --feature "$fex=$O" --no_prediction $flags \
      --json -o /tmp/$fex.json
done
python3 - <<'PY'
import json
cpu = json.load(open('/tmp/adm.json'))['pooled_metrics']
for be in ('adm_cuda', 'adm_sycl'):
    gpu = json.load(open(f'/tmp/{be}.json'))['pooled_metrics']
    for k in sorted(cpu):
        print(f'{be:9s} {k:60s} cpu={cpu[k]["mean"]!r} gpu={gpu.get(k, {}).get("mean")!r}')
PY
# Expected: every CPU key carries the `_csf_2_dlmw_0.7_egl_1_min_0.5_nw_0.02`
# suffix and starts with `integer_adm3` / `integer_adm2` / `integer_aim` /
# `integer_adm_scale0..3`. adm_cuda reproduces all seven bit-for-bit at the
# emitted precision (max |delta| 0.0e+00). adm_sycl reproduces the five it
# provides (max |delta| 1.0e-06) and prints `gpu=None` for
# `integer_aim_...` / `integer_adm3_...` — it has no AIM device pass, so those
# two route to the CPU twin through the ADR-0530 fallback instead of being
# fabricated.

# 2. C parity + option-table-mirror tests.
meson test -C core/build test_cuda_adm_parity   # 1/1 OK
./core/build/test/test_cuda_adm_parity          # 4 tests run, 4 passed
./core/build/test/test_hip_adm_parity           # first 2 (device-free) pass
source /opt/intel/oneapi/setvars.sh
./core/build/test/test_sycl_adm_parity          # 5 run, 1 failed: the
                                                # pre-existing Arc A380 gap

# 3. The GPU default-model key test (probes each backend, skips what is absent).
PYTHONPATH=$PWD/python python3 -m pytest python/test/gpu_default_model_test.py -v
# Expected: 3 passed, 2 skipped, 3 subtests passed

# 4. Netflix golden gate. MUST be run against a gcc build dir: a `core/build`
#    configured with oneAPI reports 11 unrelated float-motion FP-contraction
#    failures for any branch (T-GOLDEN-GATE-ICX-FP-DRIFT-2026-09-05).
CC=gcc CXX=g++ meson setup core/build-golden core \
    -Denable_cuda=false -Denable_sycl=false -Denable_hip=false -Denable_dnn=disabled
ninja -C core/build-golden
printf 'VMAFEXEC_PATH = "%s/core/build-golden/tools/vmaf"\n' "$PWD" > compat/python-vmaf/externals.py
CUDA_VISIBLE_DEVICES="" VMAF_FORCE_BACKEND=cpu PYTHONPATH=$PWD/python python3 -m pytest \
    python/test/quality_runner_test.py python/test/feature_extractor_test.py \
    python/test/vmafexec_test.py python/test/vmafexec_feature_extractor_test.py \
    python/test/result_test.py -m "not slow" --tb=short -q
rm -f compat/python-vmaf/externals.py
# Expected: 271 passed, 12 skipped

🤖 Generated with Claude Code

@lusoris lusoris added this to the 1.0.0 — First release milestone Sep 5, 2026
@lusoris
lusoris force-pushed the feat/gpu-adm-csf-mode-parity branch from 044ca45 to ee25ab1 Compare September 5, 2026 22:15
…IP twins

Three CPU-parity defects in the GPU integer_adm twins, all found while
verifying the adm_csf_mode / adm_p_norm option port against
core/src/feature/integer_adm.c.

1. adm_min_val clamped adm2. The CPU reference applies
   MAX(..., s->adm_min_val) to the adm3 expression alone; adm2 is emitted
   raw. The Netflix golden adm_min_val=0.98 case pins
   VMAF_integer_feature_adm2_min_0.98_score at 0.9345148541666667, below
   the floor -- a twin that clamps adm2 diverges the moment a caller sets
   the option. Removed on all three twins; ADR-0487's intent (honour the
   option) is preserved, its scope corrected to match the reference.

2. numden_limit used the scale-3 area. integer_compute_adm scales the
   precision floor with the full-frame area; all three twins read the
   loop variables after the four-scale loop had halved them four times,
   giving a floor 256x too small.

3. The SYCL and HIP twins emitted VMAF_integer_feature_aim_score and
   VMAF_integer_feature_adm3_score from a hard-coded aim_num = 0.0.
   Neither backend has an AIM contrast-measure device pass -- that is a
   second CM pass with the decouple_a / decouple_r roles swapped, which
   only the CUDA twin (ADR-0746) and integer_adm_metal implement. A
   fabricated score is worse than an absent feature: it stops the
   ADR-0530 name-based fallback from firing, so the model silently
   consumes the stand-in instead of the CPU twin's real value. Both
   features are now out of provided_features[] on SYCL and HIP, and
   adm_skip_aim -- which is not a feature param and so cannot affect the
   emitted key -- is out of their option tables rather than
   declared-and-ignored. adm_dlm_weight stays: it IS a feature param, so
   dropping it would make those twins emit integer_adm2_... where the CPU
   emits integer_adm2_dlmw_<v>_... for the same opts dict.

The two unused float factor1[4] / factor2[4] members of
AdmFixedParametersCuda / AdmFixedParametersHip are kept and commented.
They are dead in every kernel, but the struct is passed by value into
device code and meson's cu_ptx_target_* custom_target declares no header
dependency, so shrinking it pairs a new host layout with a stale fatbin
and silently corrupts every ADM score (measured: cpu=0.99048041
cuda=0.80872844). Filed as T-CUDA-FATBIN-NO-HEADER-DEP-2026-09-05.

Verified on the 576x324 Netflix pair, CPU vs CUDA pooled means over
adm2 + aim + adm3 + scale0..3: max |delta| 0.0e+00 at default options,
0.0e+00 at adm_csf_mode=2, 1.0e-06 at adm_csf_mode=3, 1.0e-06 at
adm_p_norm=2.0, 0.0e+00 under the full default-model option dict.
SYCL (adm2 + scale0..3) 1.0e-06 / 0.0e+00 / 1.0e-06 / 5.0e-06 / 1.0e-06.
HIP is not hardware-verified: integer_adm_hip faults the GPU on
origin/master too (T-HIP-INTEGER-ADM-GPU-PAGE-FAULT-2026-09-05).
…del's ADM key

The failure this covers is silent. vmaf_feature_name_from_options()
builds the emitted feature key from the extractor's OWN options[] table,
so a GPU twin missing one VMAF_OPT_FLAG_FEATURE_PARAM entry answers under
a shorter key than the CPU twin for the same opts dict and the model
lookup finds nothing -- no error, no warning, just an absent score. The
fork's default model vmaf_v1.0.16_3d0h looks ADM up under
integer_adm3_csf_2_dlmw_0.7_egl_1_min_0.5_nw_0.02.

New coverage, per backend:

- <backend>_option_table_mirrors_cpu walks the CPU `adm` table and fails
  on the first name / alias / type / feature-param-flag mismatch. Runs
  without a device, so it catches table drift on every machine and in CI.
- <backend>_does_not_claim_aim (SYCL, HIP) pins the absence of
  VMAF_integer_feature_aim_score / _adm3_score from provided_features[]:
  neither twin has an AIM device pass, and re-adding the names without
  the kernels would silence the ADR-0530 CPU fallback.
- <backend>_model_option_keys / _model_option_parity run both twins under
  the default model's option dict and compare every emitted key at
  places=4. Split in two on SYCL so the hardware-independent structural
  half stays green.
- python/test/gpu_default_model_test.py drives the same option dict
  through the CLI on the real 576x324 pair for all three backends,
  probing each and skipping when its device or runtime is unavailable.
  No hardcoded scores: the CPU twin is the reference in the same run.

test_adm_cpu_sycl_parity moves to the end of the SYCL file. mu_run_test
aborts the binary on the first failure, and that assertion is red on
Intel Arc A380 at 1.10e-04 against its 1e-4 gate -- reproduced unchanged
against origin/master's integer_adm_sycl.cpp, so it is not a regression
from this branch (T-SYCL-ARC-ADM2-PARITY-1.1E-4-2026-09-05). Ordering it
last stops a known-red legacy assertion from masking the new coverage.
The threshold is NOT loosened: an earlier draft on this branch raised it
to 2e-4 and that change is reverted here.

Results on this workstation:
  test_cuda_adm_parity   4 tests run, 4 passed
  test_sycl_adm_parity   5 tests run, 1 failed (the pre-existing Arc gap;
                         3 structural + the model-option key test pass)
  test_hip_adm_parity    2 structural tests pass; the device test faults
                         the GPU on origin/master too
                         (T-HIP-INTEGER-ADM-GPU-PAGE-FAULT-2026-09-05)
  gpu_default_model_test 3 passed, 2 skipped (HIP), 3 subtests passed
…tract

Every user-discoverable surface this branch touches, plus the invariant
the next agent needs.

docs/metrics/features.md: the ADM option table now says which backends
honour each option and, for the two whose scope is easy to get wrong,
what the scope actually is. adm_csf_scale / adm_csf_diag_scale are read
ONLY under adm_csf_mode=1 (Barten) -- they are arguments of barten_csf()
and modes 0, 2 and 3 ignore them, on the CPU too; the old text implied a
universal "scale / quant_step" multiplier. adm_csf_mode gains its four
model names and a note that the default model requests mode 2.
adm_p_norm is now documented as honoured on CUDA / SYCL / HIP rather than
"pin GPU sweeps to the default until a backend documents it". aim_score
and adm3_score move out of the "float_adm only" framing: the fixed-point
CPU, CUDA and Metal twins emit them, SYCL and HIP do not.

docs/backends/{cuda,sycl,hip}/overview.md: one section each naming the
default model's ADM request, the exact feature-name key it looks up, and
what that backend does with it. The CUDA page says the whole ADM family
runs on the device; the SYCL and HIP pages say adm2 and the per-scale
values run on the device while adm3 / aim fall back to the CPU twin, and
why.

core/src/feature/AGENTS.md: the rebase-sensitive invariant. A twin's
option table mirrors the CPU table because the emitted key is built from
it; an option is honoured or -- only when its arithmetic feeds a feature
the twin does not emit, exactly as adm_dlm_weight and adm_min_val do not
affect adm2 on the CPU either -- carried for key parity with a comment
saying so. A non-feature-param option has no key-parity excuse. Never
fabricate a feature to make a name resolve: leave it out of
provided_features[] and let the ADR-0530 fallback answer. Plus the two
scope facts the twins got wrong (adm_min_val floors adm3 only,
numden_limit scales with the full-frame area).

docs/state.md: T-GPU-ADM-CSF-MODE-NOT-PORTED-2026-09-05 closed with the
per-twin delta table. Five Open rows: the missing SYCL/HIP AIM device
pass, the pre-existing HIP integer_adm GPU page fault, the meson fatbin
header-dependency gap that lets a stale device struct corrupt scores
silently, the pre-existing Arc A380 adm2 parity gap, and adm_csf_mode=1
being degenerate on the CPU reference itself (NaN adm2 / aim / adm3).
Each says how it was reproduced and what closes it.

docs/rebase-notes.md + changelog.d/fixed/gpu-adm-csf-mode-parity.md.

No ADR: this restores conformance to an existing reference (ADR-0487,
ADR-0530, ADR-0746 already own the decisions) rather than making a new
choice. No research digest: core/src/feature/integer_adm.c defines the
semantics.
A core/build configured with the oneAPI compilers fails 11 Netflix golden
assertions that are green under gcc — six transform_score tests in
quality_runner_test.py and five in vmafexec_test.py, all on
VMAF_feature_motion_score / float-VIF, with 7.1e-05 and 8.2e-05 deltas
against a places=4 gate. icx relaxes FP contraction by default, so the
float convolutions contract differently. make test-netflix-golden pins
BUILD_DIR = core/build unconditionally, so on a workstation whose
core/build is the all-backends oneAPI build the gate reports the same 11
failures for any branch and says nothing about the change under test.

Found while gating this branch: the same tree scores 271 passed / 12
skipped / 0 failed against a gcc CPU-only build dir.
@lusoris
lusoris marked this pull request as ready for review September 5, 2026 22:36
@lusoris
lusoris force-pushed the feat/gpu-adm-csf-mode-parity branch from ee25ab1 to 03ecca1 Compare September 5, 2026 22:37
@lusoris
lusoris merged commit cd52f26 into master Sep 5, 2026
71 of 72 checks passed
@lusoris
lusoris deleted the feat/gpu-adm-csf-mode-parity branch September 5, 2026 23:03
lusoris added a commit that referenced this pull request Sep 5, 2026
…ge 1 (PR #1339)

Moves the row from '## Open bugs' to '## Recently closed', citing ADR-1191 and
PR #1339. Records what stage 1 fixed (the unrepresentable CSF configurations
now return -EINVAL on the CPU and on all three GPU twins) and what it did not
(the widening half, still tracked under
T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05). Corrects the row's stale 'no
cross-backend hole' claim, which #1324 invalidated by mirroring the CPU option
table onto the CUDA, SYCL and HIP twins. Updates the two cross-referencing
rows to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 5, 2026
…ge 1 (PR #1339)

Moves the row from '## Open bugs' to '## Recently closed', citing ADR-1191 and
PR #1339. Records what stage 1 fixed (the unrepresentable CSF configurations
now return -EINVAL on the CPU and on all three GPU twins) and what it did not
(the widening half, still tracked under
T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05). Corrects the row's stale 'no
cross-backend hole' claim, which #1324 invalidated by mirroring the CPU option
table onto the CUDA, SYCL and HIP twins. Updates the two cross-referencing
rows to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…195)

CLAUDE.md rule 15 says to rebuild the container when "its image predates the
last master sync". That is stated in terms of time, and time cannot answer the
question. A build run against a checkout that is behind master produces an
image newer than every commit in the repository and missing exactly the work
it was rebuilt for.

That happened today. The container was rebuilt specifically to pick up the GPU
default-model fixes (#1307, #1312, #1324) so the epic #1246 GPU smoke could run
against them. The build succeeded, the image was the newest thing on disk, and
it contained none of the three: the build context was 28 commits behind
origin/master. It was caught only because a test file added by one of those PRs
was missing. Had the smoke run instead, it would have reported green numbers
for code that was not in the image, and those numbers would have been cited as
a retrain gate.

ADR-1102's marker answers "did a container build this?". It cannot answer
"which code was in that container?", and that second question is the one that
was wrong.

dev/Containerfile now records /etc/vmafx-dev-source (source_rev, source_ref,
source_repo) from a VMAFX_SOURCE_REV build argument supplied by
dev/docker-compose.yml. It is written in the LAST stage, deliberately away from
the ADR-1102 marker in the first: the first stage is reused by every rebuild,
so a marker there would report the revision of whichever build first populated
the layer cache -- authoritative and stale, which is worse than absent.

scripts/dev/check-container-source.sh answers it in both directions.
--pre-build refuses a checkout that is behind the reference and lists the
commits under baked-in paths the image would be missing; --image reads the
marker out of an existing image and reports current, stale (naming what is
missing), or unverifiable. A build that never received the argument records
`unknown`, and `unknown` is reported as "cannot verify", not as a pass: an
image that cannot say what it holds is not evidence.

dev/scripts/container-build.sh makes the correct path the easy one -- check,
build with the verified revision, re-verify the result. --allow-behind exists
for local experiments and says so loudly.

Verified by scripts/ci/tests/test-check-container-source.sh: 8 assertions,
hermetic apart from one Docker case that skips when no daemon is reachable.
The stale-context case reproduces today's shape; ahead-of-master is
deliberately allowed, since a feature branch legitimately leads master.

no digest needed: trivial

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
… (ADR-1192)

Re-ran the Netflix benchmark suite on cd52f26 for epic #1245 items 1 and 5.
All three fixtures reproduce on CPU, CUDA and SYCL through the FFmpeg filter
path against a container-built current-master libvmaf, but every backend's
pooled score has drifted from testdata/netflix_benchmark_results.json (recorded
by PR #309 on 2026-05-02): CPU +2.83e-06, CUDA -1.07e-03, SYCL -1.40e-03 on the
576x324 pair. A rebuild of 5a08030 — the commit before the 2026-09-06 GPU
merges #1307/#1312/#1324 — shows the same drift, so none of it comes from
today's merges. The snapshot is deliberately NOT regenerated (ADR-1192) and no
throughput baseline is recorded, because the run also reproduced two
pre-existing GPU defects:

- vmaf --threads N aborts on every GPU backend (exit 234, "context could not be
  synchronized"); without --threads both CUDA and SYCL score correctly and are
  bit-stable over 10 runs. bench_all.sh hard-codes --threads 1.
- The libvmaf_cuda FFmpeg filter returns a wrong pooled score in 10 of 40 runs
  on master and 8 of 40 on 5a08030 — inside binomial noise of each other.

Harness fixes in the same change:

- bench_all.sh kept its stderr on /dev/null and relabelled every non-zero exit
  as "backend likely unavailable", which is how a hard abort passed for a
  missing device for months. It now captures stderr per row and prints FAIL
  with the exit code and the real last line. Its flag sets also drop
  --no_vulkan, unrecognized since ADR-0726 removed the Vulkan backend.
- benchmark_netflix.py hard-coded /home/kilian/dev/ffmpeg-8/ffmpeg (gone) and
  /dev/dri/renderD130 for the SYCL/QSV import (now the AMD iGPU on the bench
  host, so the SYCL rows failed outright). Both are environment overrides now,
  VMAF_FFMPEG and the new VMAF_SYCL_RENDER_NODE, per the ADR-0792 pattern.

No golden assertions touched; no snapshot regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…195)

CLAUDE.md rule 15 says to rebuild the container when "its image predates the
last master sync". That is stated in terms of time, and time cannot answer the
question. A build run against a checkout that is behind master produces an
image newer than every commit in the repository and missing exactly the work
it was rebuilt for.

That happened today. The container was rebuilt specifically to pick up the GPU
default-model fixes (#1307, #1312, #1324) so the epic #1246 GPU smoke could run
against them. The build succeeded, the image was the newest thing on disk, and
it contained none of the three: the build context was 28 commits behind
origin/master. It was caught only because a test file added by one of those PRs
was missing. Had the smoke run instead, it would have reported green numbers
for code that was not in the image, and those numbers would have been cited as
a retrain gate.

ADR-1102's marker answers "did a container build this?". It cannot answer
"which code was in that container?", and that second question is the one that
was wrong.

dev/Containerfile now records /etc/vmafx-dev-source (source_rev, source_ref,
source_repo) from a VMAFX_SOURCE_REV build argument supplied by
dev/docker-compose.yml. It is written in the LAST stage, deliberately away from
the ADR-1102 marker in the first: the first stage is reused by every rebuild,
so a marker there would report the revision of whichever build first populated
the layer cache -- authoritative and stale, which is worse than absent.

scripts/dev/check-container-source.sh answers it in both directions.
--pre-build refuses a checkout that is behind the reference and lists the
commits under baked-in paths the image would be missing; --image reads the
marker out of an existing image and reports current, stale (naming what is
missing), or unverifiable. A build that never received the argument records
`unknown`, and `unknown` is reported as "cannot verify", not as a pass: an
image that cannot say what it holds is not evidence.

dev/scripts/container-build.sh makes the correct path the easy one -- check,
build with the verified revision, re-verify the result. --allow-behind exists
for local experiments and says so loudly.

Verified by scripts/ci/tests/test-check-container-source.sh: 8 assertions,
hermetic apart from one Docker case that skips when no daemon is reachable.
The stale-context case reproduces today's shape; ahead-of-master is
deliberately allowed, since a feature branch legitimately leads master.

no digest needed: trivial

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…ge 1 (PR #1339)

Moves the row from '## Open bugs' to '## Recently closed', citing ADR-1191 and
PR #1339. Records what stage 1 fixed (the unrepresentable CSF configurations
now return -EINVAL on the CPU and on all three GPU twins) and what it did not
(the widening half, still tracked under
T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05). Corrects the row's stale 'no
cross-backend hole' claim, which #1324 invalidated by mirroring the CPU option
table onto the CUDA, SYCL and HIP twins. Updates the two cross-referencing
rows to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…195)

CLAUDE.md rule 15 says to rebuild the container when "its image predates the
last master sync". That is stated in terms of time, and time cannot answer the
question. A build run against a checkout that is behind master produces an
image newer than every commit in the repository and missing exactly the work
it was rebuilt for.

That happened today. The container was rebuilt specifically to pick up the GPU
default-model fixes (#1307, #1312, #1324) so the epic #1246 GPU smoke could run
against them. The build succeeded, the image was the newest thing on disk, and
it contained none of the three: the build context was 28 commits behind
origin/master. It was caught only because a test file added by one of those PRs
was missing. Had the smoke run instead, it would have reported green numbers
for code that was not in the image, and those numbers would have been cited as
a retrain gate.

ADR-1102's marker answers "did a container build this?". It cannot answer
"which code was in that container?", and that second question is the one that
was wrong.

dev/Containerfile now records /etc/vmafx-dev-source (source_rev, source_ref,
source_repo) from a VMAFX_SOURCE_REV build argument supplied by
dev/docker-compose.yml. It is written in the LAST stage, deliberately away from
the ADR-1102 marker in the first: the first stage is reused by every rebuild,
so a marker there would report the revision of whichever build first populated
the layer cache -- authoritative and stale, which is worse than absent.

scripts/dev/check-container-source.sh answers it in both directions.
--pre-build refuses a checkout that is behind the reference and lists the
commits under baked-in paths the image would be missing; --image reads the
marker out of an existing image and reports current, stale (naming what is
missing), or unverifiable. A build that never received the argument records
`unknown`, and `unknown` is reported as "cannot verify", not as a pass: an
image that cannot say what it holds is not evidence.

dev/scripts/container-build.sh makes the correct path the easy one -- check,
build with the verified revision, re-verify the result. --allow-behind exists
for local experiments and says so loudly.

Verified by scripts/ci/tests/test-check-container-source.sh: 8 assertions,
hermetic apart from one Docker case that skips when no daemon is reachable.
The stale-context case reproduces today's shape; ahead-of-master is
deliberately allowed, since a feature branch legitimately leads master.

no digest needed: trivial

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…195) (#1337)

* ci(dev): make the container say which source it was built from (ADR-1195)

CLAUDE.md rule 15 says to rebuild the container when "its image predates the
last master sync". That is stated in terms of time, and time cannot answer the
question. A build run against a checkout that is behind master produces an
image newer than every commit in the repository and missing exactly the work
it was rebuilt for.

That happened today. The container was rebuilt specifically to pick up the GPU
default-model fixes (#1307, #1312, #1324) so the epic #1246 GPU smoke could run
against them. The build succeeded, the image was the newest thing on disk, and
it contained none of the three: the build context was 28 commits behind
origin/master. It was caught only because a test file added by one of those PRs
was missing. Had the smoke run instead, it would have reported green numbers
for code that was not in the image, and those numbers would have been cited as
a retrain gate.

ADR-1102's marker answers "did a container build this?". It cannot answer
"which code was in that container?", and that second question is the one that
was wrong.

dev/Containerfile now records /etc/vmafx-dev-source (source_rev, source_ref,
source_repo) from a VMAFX_SOURCE_REV build argument supplied by
dev/docker-compose.yml. It is written in the LAST stage, deliberately away from
the ADR-1102 marker in the first: the first stage is reused by every rebuild,
so a marker there would report the revision of whichever build first populated
the layer cache -- authoritative and stale, which is worse than absent.

scripts/dev/check-container-source.sh answers it in both directions.
--pre-build refuses a checkout that is behind the reference and lists the
commits under baked-in paths the image would be missing; --image reads the
marker out of an existing image and reports current, stale (naming what is
missing), or unverifiable. A build that never received the argument records
`unknown`, and `unknown` is reported as "cannot verify", not as a pass: an
image that cannot say what it holds is not evidence.

dev/scripts/container-build.sh makes the correct path the easy one -- check,
build with the verified revision, re-verify the result. --allow-behind exists
for local experiments and says so loudly.

Verified by scripts/ci/tests/test-check-container-source.sh: 8 assertions,
hermetic apart from one Docker case that skips when no daemon is reachable.
The stale-context case reproduces today's shape; ahead-of-master is
deliberately allowed, since a feature branch legitimately leads master.

no digest needed: trivial

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(state): cite PR #1337 in the container-staleness row (ADR-0165 touch gate)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(state): drop the duplicate rows a keep-both rebase created

Each dropped row restates one origin/master already carries; master is the
authoritative record. Verified with scripts/ci/check-state-md-rows.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Lusoris <lusoris@pm.me>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
… (ADR-1192)

Re-ran the Netflix benchmark suite on cd52f26 for epic #1245 items 1 and 5.
All three fixtures reproduce on CPU, CUDA and SYCL through the FFmpeg filter
path against a container-built current-master libvmaf, but every backend's
pooled score has drifted from testdata/netflix_benchmark_results.json (recorded
by PR #309 on 2026-05-02): CPU +2.83e-06, CUDA -1.07e-03, SYCL -1.40e-03 on the
576x324 pair. A rebuild of 5a08030 — the commit before the 2026-09-06 GPU
merges #1307/#1312/#1324 — shows the same drift, so none of it comes from
today's merges. The snapshot is deliberately NOT regenerated (ADR-1192) and no
throughput baseline is recorded, because the run also reproduced two
pre-existing GPU defects:

- vmaf --threads N aborts on every GPU backend (exit 234, "context could not be
  synchronized"); without --threads both CUDA and SYCL score correctly and are
  bit-stable over 10 runs. bench_all.sh hard-codes --threads 1.
- The libvmaf_cuda FFmpeg filter returns a wrong pooled score in 10 of 40 runs
  on master and 8 of 40 on 5a08030 — inside binomial noise of each other.

Harness fixes in the same change:

- bench_all.sh kept its stderr on /dev/null and relabelled every non-zero exit
  as "backend likely unavailable", which is how a hard abort passed for a
  missing device for months. It now captures stderr per row and prints FAIL
  with the exit code and the real last line. Its flag sets also drop
  --no_vulkan, unrecognized since ADR-0726 removed the Vulkan backend.
- benchmark_netflix.py hard-coded /home/kilian/dev/ffmpeg-8/ffmpeg (gone) and
  /dev/dri/renderD130 for the SYCL/QSV import (now the AMD iGPU on the bench
  host, so the SYCL rows failed outright). Both are environment overrides now,
  VMAF_FFMPEG and the new VMAF_SYCL_RENDER_NODE, per the ADR-0792 pattern.

No golden assertions touched; no snapshot regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
… (ADR-1192) (#1334)

* fix(testdata): make the Netflix benchmark harness honest and portable (ADR-1192)

Re-ran the Netflix benchmark suite on cd52f26 for epic #1245 items 1 and 5.
All three fixtures reproduce on CPU, CUDA and SYCL through the FFmpeg filter
path against a container-built current-master libvmaf, but every backend's
pooled score has drifted from testdata/netflix_benchmark_results.json (recorded
by PR #309 on 2026-05-02): CPU +2.83e-06, CUDA -1.07e-03, SYCL -1.40e-03 on the
576x324 pair. A rebuild of 5a08030 — the commit before the 2026-09-06 GPU
merges #1307/#1312/#1324 — shows the same drift, so none of it comes from
today's merges. The snapshot is deliberately NOT regenerated (ADR-1192) and no
throughput baseline is recorded, because the run also reproduced two
pre-existing GPU defects:

- vmaf --threads N aborts on every GPU backend (exit 234, "context could not be
  synchronized"); without --threads both CUDA and SYCL score correctly and are
  bit-stable over 10 runs. bench_all.sh hard-codes --threads 1.
- The libvmaf_cuda FFmpeg filter returns a wrong pooled score in 10 of 40 runs
  on master and 8 of 40 on 5a08030 — inside binomial noise of each other.

Harness fixes in the same change:

- bench_all.sh kept its stderr on /dev/null and relabelled every non-zero exit
  as "backend likely unavailable", which is how a hard abort passed for a
  missing device for months. It now captures stderr per row and prints FAIL
  with the exit code and the real last line. Its flag sets also drop
  --no_vulkan, unrecognized since ADR-0726 removed the Vulkan backend.
- benchmark_netflix.py hard-coded /home/kilian/dev/ffmpeg-8/ffmpeg (gone) and
  /dev/dri/renderD130 for the SYCL/QSV import (now the AMD iGPU on the bench
  host, so the SYCL rows failed outright). Both are environment overrides now,
  VMAF_FFMPEG and the new VMAF_SYCL_RENDER_NODE, per the ADR-0792 pattern.

No golden assertions touched; no snapshot regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(state): drop the duplicate rows a keep-both rebase created

Each dropped row restates one origin/master already carries; master is the
authoritative record. Verified with scripts/ci/check-state-md-rows.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Lusoris <lusoris@pm.me>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…ge 1 (PR #1339)

Moves the row from '## Open bugs' to '## Recently closed', citing ADR-1191 and
PR #1339. Records what stage 1 fixed (the unrepresentable CSF configurations
now return -EINVAL on the CPU and on all three GPU twins) and what it did not
(the widening half, still tracked under
T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05). Corrects the row's stale 'no
cross-backend hole' claim, which #1324 invalidated by mirroring the CPU option
table onto the CUDA, SYCL and HIP twins. Updates the two cross-referencing
rows to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…ge 1 (PR #1339)

Moves the row from '## Open bugs' to '## Recently closed', citing ADR-1191 and
PR #1339. Records what stage 1 fixed (the unrepresentable CSF configurations
now return -EINVAL on the CPU and on all three GPU twins) and what it did not
(the widening half, still tracked under
T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05). Corrects the row's stale 'no
cross-backend hole' claim, which #1324 invalidated by mirroring the CPU option
table onto the CUDA, SYCL and HIP twins. Updates the two cross-referencing
rows to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…ge 1 (PR #1339)

Moves the row from '## Open bugs' to '## Recently closed', citing ADR-1191 and
PR #1339. Records what stage 1 fixed (the unrepresentable CSF configurations
now return -EINVAL on the CPU and on all three GPU twins) and what it did not
(the widening half, still tracked under
T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05). Corrects the row's stale 'no
cross-backend hole' claim, which #1324 invalidated by mirroring the CPU option
table onto the CUDA, SYCL and HIP twins. Updates the two cross-referencing
rows to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…ge 1 (PR #1339)

Moves the row from '## Open bugs' to '## Recently closed', citing ADR-1191 and
PR #1339. Records what stage 1 fixed (the unrepresentable CSF configurations
now return -EINVAL on the CPU and on all three GPU twins) and what it did not
(the widening half, still tracked under
T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05). Corrects the row's stale 'no
cross-backend hole' claim, which #1324 invalidated by mirroring the CPU option
table onto the CUDA, SYCL and HIP twins. Updates the two cross-referencing
rows to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…ge cannot represent, and fix the x86 DWT2 tail bound (#1339)

* fix(adm): reject integer-ADM CSF configurations the fixed-point storage cannot represent

The integer ADM pipeline stores each DWT scale's contrast-sensitivity weight
as a fixed-point integer -- `uint16_t` at scale 0 (h/v bands scaled by 2^21,
diagonal by 2^23) and `uint32_t` at scales 1-3 (scaled by 2^32). Those budgets
were sized for the Watson97 weights, which sit around 1e-2.

Two reachable configurations exceed them and were converted anyway:

- `adm_csf_mode=1` (Barten) at the default `adm_csf_scale=1.0` yields 1.2105
  at scale 0 and 26.98 at scale 3. The scale-0 conversions are 2538595 and
  10154382 -- 38x and 155x past 65535 -- and every scale-1..3 conversion is
  past 2^32. Scoring the 576x324 Netflix pair with `--feature
  adm=adm_csf_mode=1` emitted `integer_adm2_csf_1: null` and
  `integer_adm_scale0_csf_1: 0.030096` against a float reference of 0.9396.
- `adm_csf_mode=2` / `=3` at a viewing geometry the blended-CSF tables do not
  carry (e.g. `adm_ref_display_height=1200`) return `-EINVAL` *as a float*,
  which then reached an undefined negative-to-unsigned conversion.

Add `core/src/feature/adm_csf_fixed_point.h`, which owns the fixed-point
exponents, the storage bounds, the tabulated-fast-path predicate and the
scale-0 narrowing conversion. `integer_adm.c` evaluates the verdict once in
`init()`, caches it in `AdmState::csf_config_err`, and returns it from
`extract()` beside the pre-existing viewing-geometry guard -- the same place
and the same status `test_adm_coverage.c` already pins for an unsupported ADM
configuration. The CUDA, HIP and SYCL twins apply the identical bounds from
the same header so their accept/reject set matches the CPU reference, which
the ADR-1183 option / feature-name parity contract depends on.

The conversion is bit-exact with the code it replaces: the products stay
`double`-valued, exactly as `float * double` promotion already made them.
Configurations that fit are untouched -- `adm_csf_mode=1` with
`adm_csf_scale=0.002893` / `adm_csf_diag_scale=0.001586`, and `adm_csf_mode=2`
as requested by the default model `vmaf_v1.0.16_3d0h`, both still score.

Netflix golden gate: 271 passed, 12 skipped, 0 failed.
libvmaf fast suite: 115/115 OK.

Refs ADR-1191, Netflix/vmaf#1494.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(adr): route the ADR-1191 index row through _index_fragments (ADR-0221)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(state): close T-UPSTREAM-1494-ADM-CSF-MODE-IRFACTOR-OVERFLOW stage 1 (PR #1339)

Moves the row from '## Open bugs' to '## Recently closed', citing ADR-1191 and
PR #1339. Records what stage 1 fixed (the unrepresentable CSF configurations
now return -EINVAL on the CPU and on all three GPU twins) and what it did not
(the widening half, still tracked under
T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05). Corrects the row's stale 'no
cross-backend hole' claim, which #1324 invalidated by mirroring the CPU option
table onto the CUDA, SYCL and HIP twins. Updates the two cross-referencing
rows to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(feature/adm): fix x86 DWT2 tail bounds for AVX2 and AVX-512

(cherry picked from commit dd6b018)

* docs(state): record the x86 DWT2 tail-bound port and close T-UPSTREAM-1494 stage 1

Absorbs the unique half of PR #1303 (x86 DWT2 tail bound, cherry-picked in the
preceding commit) so that PR can be closed without losing work:

- docs/rebase-notes.md: adds the AVX2 / AVX-512 `half_w - 1 - ((half_w - 2) % N)`
  tail-bound invariant and the pinning test to this branch's entry. Upstream
  Netflix still carries the unguarded bound, so the guard must survive syncs.
- docs/state.md: the T-UPSTREAM-1564 Recently-closed row now records residual (3)
  (the x86 tail bound) as closed here; T-UPSTREAM-1494 stage 1 moves from Open to
  Recently closed with a marker comment pointing at the move, so the id appears
  exactly once as a row. Stage 2 stays open under
  T-ADM-CSF-MODE-1-BARTEN-DEGENERATE-2026-09-05.
- CHANGELOG.md re-rendered from changelog.d/ by
  scripts/release/concat-changelog-fragments.sh --write.

Verified: scripts/ci/check-state-md-rows.sh docs/state.md -> OK (361 rows, no
duplicate ids); meson test -C build-cpu test_adm_dwt2_x86 test_adm_csf_representable
test_adm_coverage test_integer_adm_simd -> Ok 4 / Fail 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(test): make the x86 ADM DWT2 parity test clang-tidy clean

The ported test carried 29 clang-tidy findings (function-size,
isolate-declaration, modernize-use-nullptr, unix.Malloc leak paths on the
early-return branch, cert-err33-c on fprintf), which ADR-0141 and the
ADR-1142 whole-tree ratchet do not allow a newly added file to introduce.

Restructure instead of suppressing:

- ref_src_indices() splits into a per-axis ref_mirror_indices() helper, which
  also removes the duplicated y/x mirror loops and brings both functions under
  the 60-line budget.
- The two near-identical test bodies collapse into one
  dwt2_kernel_matches_scalar(label, kernel) driver behind an adm_dwt2_8_fn
  function pointer, with a Dwt2Fixture that owns every buffer and a single
  fixture_free() cleanup path, so the analyzer no longer sees a leak on the
  mismatch branch.
- fprintf return values are (void)-discarded; declarations are isolated.
- modernize-use-nullptr is suppressed with the same NOLINTBEGIN block and the
  same ADR-1138 citation core/test/test_adm_csf_representable.c already uses
  for this C translation unit.

Behaviour is unchanged: the test still fails against the origin/master
kernels (AVX2 34x32 band_a[0][16] scalar 5589 != simd 6706) and passes with
the fixed tail bound. clang-tidy -p build-cpu reports 0 findings.

* docs(state): drop the duplicate rows a keep-both rebase created

Each dropped row restates one origin/master already carries; master is the
authoritative record. Verified with scripts/ci/check-state-md-rows.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Lusoris <lusoris@pm.me>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
The epic #1246 gate table listed G2 and G3 as FAIL against conditions that no
longer hold. G3 in particular blamed "PR #1307 & fix/cambi-cuda-context
unmerged" -- both merged on 2026-09-05/06.

Re-ran each gate rather than reasoning about it:

  G2  PASS  zero failing runs on master HEAD. The release-please failure the
            row was written for is gone; ADR-1171 made the missing release-bot
            credential warn-not-error on push and the workflow reports success.
  G3  PASS  #1307, #1312 and #1324 all on master; container rebuilt from
            cd52f26 and the default model verified on ALL FOUR backends --
            CPU 82.816062, CUDA 82.814062, SYCL 82.814061, HIP 82.816061, each
            exiting 0. The runbook only asked for CUDA; the others were checked
            because a model reaches the GPU twins through the model, not
            through --feature, so a CUDA-only check would not have covered them.
  G1  FAIL  12 epics open, listed by number, with the caveat that the epic
            bodies are snapshots and several of their items have already
            shipped -- the count overstates the work.
  G4  FAIL  still blocked on #1302, and now says why precisely: master's
            extract_k150k_features.py has no --vmaf-model flag (grep returns
            0), which is what §4.2's teacher_model assertion needs. #1302 has
            no failing check -- its only red mark is the aggregator's draft
            guard, and its ADR-0108 validator passes six of six. It needs
            promotion, not repair.

Adds a note that the table is a measurement and each row must be re-run rather
than carried forward, since stale-status drift is what it just corrected.

no digest needed: trivial

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris added a commit that referenced this pull request Sep 6, 2026
…tus (#1350)

* docs(ai): record measured retrain gate status, not authoring-time status

The epic #1246 gate table listed G2 and G3 as FAIL against conditions that no
longer hold. G3 in particular blamed "PR #1307 & fix/cambi-cuda-context
unmerged" -- both merged on 2026-09-05/06.

Re-ran each gate rather than reasoning about it:

  G2  PASS  zero failing runs on master HEAD. The release-please failure the
            row was written for is gone; ADR-1171 made the missing release-bot
            credential warn-not-error on push and the workflow reports success.
  G3  PASS  #1307, #1312 and #1324 all on master; container rebuilt from
            cd52f26 and the default model verified on ALL FOUR backends --
            CPU 82.816062, CUDA 82.814062, SYCL 82.814061, HIP 82.816061, each
            exiting 0. The runbook only asked for CUDA; the others were checked
            because a model reaches the GPU twins through the model, not
            through --feature, so a CUDA-only check would not have covered them.
  G1  FAIL  12 epics open, listed by number, with the caveat that the epic
            bodies are snapshots and several of their items have already
            shipped -- the count overstates the work.
  G4  FAIL  still blocked on #1302, and now says why precisely: master's
            extract_k150k_features.py has no --vmaf-model flag (grep returns
            0), which is what §4.2's teacher_model assertion needs. #1302 has
            no failing check -- its only red mark is the aggregator's draft
            guard, and its ADR-0108 validator passes six of six. It needs
            promotion, not repair.

Adds a note that the table is a measurement and each row must be re-run rather
than carried forward, since stale-status drift is what it just corrected.

no digest needed: trivial

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(ai): fix the K150K scores path in both the smoke and the production run

The runbook pointed --scores at .corpus/konvid-150k/scores.csv. That file does
not exist. KoNViD-150k splits its scores by part, and the corpus on this
workstation holds k150ka_scores.csv and k150kb_scores.csv (plus the matching
*_votes.csv and a manifest.csv).

The wrong path appeared TWICE: in the section 4 five-clip smoke and in the
section 5.1 multi-day production extraction. extract_k150k_features.py
validates the file and exits with 'error: scores CSV not found', so each would
have aborted on its first line -- the smoke immediately, and the ~105-110 hour
K150K run at its very start.

Corrected to k150ka_scores.csv, which is also the script's own argparse default
and the path in its module docstring example.

--clips-dir is deliberately left as clips/: a real directory of 153,841 files
and a superset of the script's default k150ka_extracted/ (152,265). Lookup is by
video_name, so either resolves. Both paths are now listed in a verify-first
command so an operator checks them before committing to the long run.

no digest needed: trivial

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(ai): correct four K150K command errors found by running the smoke

Ran the section 4 five-clip smoke end to end in the container. As written it
failed on its first line, then its second, then on every clip. Each fix below
comes from an actual run, not from reading.

1. --scores named scores.csv, which does not exist. KoNViD-150k splits its
   scores by part; the corpus holds k150ka_scores.csv (154,746 rows) and
   k150kb_scores.csv. k150ka_scores.csv is the script's own default.

2. --cpu-vmaf-bin was missing entirely. It is required, and its default
   /build/vmaf/core/build-cpu/tools/vmaf does not exist in the container, so the
   run aborts with 'error: cpu-vmaf-bin not found'. /usr/local/bin/vmaf serves
   both roles.

3. --clips-dir named clips/, which is unusable from inside the container. Its
   153,841 entries are symlinks to HOST absolute paths under
   /home/kilian/dev/vmaf/.workingdir2/konvid-150k/, which do not resolve in the
   container mount. Every clip failed with 'ffprobe ... returned non-zero exit
   status 1' -- which reads like corrupt media and is really a dangling link.
   The real files are in k150ka_extracted/ (152,265 files, ffprobe reports
   960x540), again the script's default.

All three appeared in BOTH the section 4 smoke and the section 5.1 multi-day
production extraction, so the ~105-110 hour K150K run would have aborted at its
very start.

With them fixed the pipeline runs clean: ok=5 fail=0 at 1.26 clip/s,
status complete, schema k150k-feature-extraction-manifest-v1, 5 parquet rows.

Of section 4.2's assertions, schema, status and stats.ok already pass on master.
Three do not, and all three come from #1302: teacher_model in the manifest, the
teacher_model parquet column, and adm3_mean (grep -c adm3 returns 0 on master's
extractor and 3 on #1302's). G4 is blocked on that PR alone -- corpus, binary
and pipeline are all verified working.

no digest needed: trivial

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Lusoris <lusoris@pm.me>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@lusoris lusoris added the type:bug Something isn't working label Sep 7, 2026
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