Skip to content

fix(dnn): wire the int8 redirect into --tiny-model and stop vmaf_tiny_v3.int8 double-scaling - #1320

Draft
lusoris wants to merge 9 commits into
masterfrom
feat/dnn-int8-redirect-and-sidecar-fixes
Draft

fix(dnn): wire the int8 redirect into --tiny-model and stop vmaf_tiny_v3.int8 double-scaling#1320
lusoris wants to merge 9 commits into
masterfrom
feat/dnn-int8-redirect-and-sidecar-fixes

Conversation

@lusoris

@lusoris lusoris commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Completes the int8 half of epic #1242. vmaf_use_tiny_model() — the entry point behind --tiny-model — ignored the sidecar's quant_mode and always opened the fp32 graph; it now mirrors vmaf_dnn_session_open(), redirecting to <stem>.int8.onnx and falling back to fp32 per ADR-1032 with a debug log line. model/tiny/vmaf_tiny_v3.int8.json shipped without "onnx_has_scaler": true although the graph bakes the StandardScaler, so libvmaf.c double-scaled the canonical-6 vector: on the Netflix src01_hrc00/hrc01_576x324 pair (48 frames, CPU) pooled vmaf_tiny_model was 16.020865 and is now 71.952113 against an fp32 baseline of 72.359458, with per-frame PLCC vs fp32 going 0.975443 to 0.999876. A scaler/sidecar consistency gate over every model/tiny/*.int8.onnx now runs in three places so this cannot recur. On the Python side, measure_quant_drop.py gained --fp32 / --int8 / --budget / --id so an uncommitted model can be gated without a registry entry (verified PLCC=0.999880 drop=0.000120 on the shipped v3 pair), and qat_train.py now picks its training loader from the rank of qat.input_shape — rank-4 configs like learned_filter_v1_qat.yaml previously could not train at all, because every config went to the tabular datamodule and the missing-cache branch silently downgraded the run to --smoke. The three docs/ai/ placeholders (#1242's last docs task) are replaced with the audited state of the tree rather than restated plans. Verification: meson dnn suite 13/13 with -Denable_dnn=enabled + ONNX Runtime in the container, Netflix golden gate 271 passed / 12 skipped / 0 failed, 40 Python tests green. Two things are deliberately not here and are recorded rather than half-wired: the loader still does not verify int8_sha256 (documented in docs/ai/quantization.md and pinned in core/src/dnn/AGENTS.md — a digest mismatch is a third outcome ADR-1032 does not define, so it needs its own ADR), and the discovery that the two gpu-full CI jobs can never be scheduled is filed as T-GPU-RUNNER-LABEL-MISMATCH-2026-09-05 in docs/state.md rather than fixed here.

Follow-up fix on this branch: the redirect must not fail an invocation that worked

The first cut of the redirect only fell back to fp32 when the int8 file was
missing or failed the op allowlist. That is not the only way an int8 graph can
be unusable: model/tiny/nr_metric_v1.json declares quant_mode: "dynamic",
so --tiny-model model/tiny/nr_metric_v1.onnx now redirected to
nr_metric_v1.int8.onnx — which clears the size cap and the allowlist and then
fails ORT session creation with -EIO and Could not find an implementation for ConvInteger(10) on any ONNX Runtime build without that kernel.
core/test/dnn/test_cli.sh caught it: it fails with this branch's loader and
passes with master's. Both twins now retry the fp32 baseline once when
vmaf_ort_open() fails on a redirected path (vmaf_ort_open() leaves its
out-parameter untouched on error, so the retry cannot leak the failed session).
meson test -C build-cpu --suite dnn is 13/13 again.

Type

  • fix — bug fix — int8 redirect missing from vmaf_use_tiny_model(), and a shipped sidecar that double-scaled its features.

Checklist

  • Commits follow Conventional Commits.
  • make format && make lint is green locally (pre-commit on every touched file; clang-tidy -p core/build core/src/dnn/dnn_attach_api.c reports only the two modernize-use-nullptr warnings master already carries — the function-size, redundant-branch-condition and dead-store findings the first draft introduced were refactored away).
  • Unit tests: meson test -C /tmp/build-dnn --suite dnn 13/13 OK with -Denable_dnn=enabled; pytest ai/tests/test_measure_quant_drop_unit.py ai/tests/test_qat_train_loader.py 29 passed / 4 skipped; pytest python/test/model_registry_schema_test.py 11 passed / 1 xfailed; bash core/test/dnn/test_registry.sh and python ai/scripts/validate_model_registry.py both OK on 26 entries.
  • Docs in the same PR: docs/ai/quantization.md, docs/ai/inference.md, docs/ai/extractor-template.md, docs/ai/sidecar-online-training.md, docs/usage/cli.md.
  • SIMD/GPU, twins, new C sources, breaking change, ADR — no SIMD or GPU path touched (the DNN loader is backend-agnostic; execution-provider selection is unchanged). dnn_attach_api.c and dnn_api.c are twins and both are edited here: the redirect logic and the two-trigger fp32 fallback are identical in each, and core/src/dnn/AGENTS.md pins that they must stay so. No new C source files. No breaking change — the redirect only fires for sidecars declaring quant_mode != fp32, which no default model does. No ADR: ADR-1032's fallback semantics are applied to a failure mode it did not enumerate, not changed; the alternative (fail loudly on an unopenable int8 graph) would regress --tiny-model invocations that work on master today, so there is no second reasonable option to weigh.

Bug-status hygiene (ADR-0165)

  • docs/state.md — closed T-DNN-ATTACH-INT8-REDIRECT-MISSING-2026-09-04 and T-TINY-V3-INT8-SIDECAR-MISSING-ONNX-HAS-SCALER-2026-09-04 (Recently closed, with the measured before/after scores); opened T-GPU-RUNNER-LABEL-MISMATCH-2026-09-05 (Open bugs).

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: the digest for this work, docs/research/2029-int8-static-qat-readiness.md, already landed on master in PR docs(ai): int8 static PTQ / QAT readiness smoke for the 1.0.0 retrain #1293; this PR implements its findings (§5 gaps 1, 2, 4, 5) and adds no new research.
  • Decision matrixdocs/ai/quantization.md §Mode selection (redirect vs. explicit-path alternatives), backed by docs/research/2029-int8-static-qat-readiness.md §2 and §6 already on master.
  • AGENTS.md invariant notecore/src/dnn/AGENTS.md §"Invariant — int8 loader redirect and scaler declaration contract"
  • Reproducer / smoke-test command — below.
  • CHANGELOG fragmentchangelog.d/fixed/vmaf-tiny-v3-int8-scaler.md, changelog.d/fixed/dnn-attach-int8-redirect.md, changelog.d/added/measure-quant-drop-path-overrides.md, changelog.d/added/qat-train-4d-image-loader.md, changelog.d/changed/docs-ai-gaps-1242.md
  • Rebase notedocs/rebase-notes.md entry

Reproducer

# 1. The scaler fix, end to end. Requires ONNX Runtime, so run in the container.
#    A copy of the sidecar with "onnx_has_scaler" removed reproduces the defect.
docker exec -u 0 -w /workspace/.claude/worktrees/int8loader vmaf-dev-mcp bash -lc '
  D=/tmp/v3proof; mkdir -p $D/before $D/after
  cp model/tiny/vmaf_tiny_v3.int8.onnx $D/before/; cp model/tiny/vmaf_tiny_v3.int8.onnx $D/after/
  /opt/vmaf-venv/bin/python -c "
import json
d=json.load(open(\"model/tiny/vmaf_tiny_v3.int8.json\"))
b=dict(d); b.pop(\"onnx_has_scaler\")
json.dump(b, open(\"/tmp/v3proof/before/vmaf_tiny_v3.int8.json\",\"w\"))
json.dump(d, open(\"/tmp/v3proof/after/vmaf_tiny_v3.int8.json\",\"w\"))"
  for v in before after; do
    /usr/local/bin/vmaf --backend cpu \
      -r python/test/resource/yuv/src01_hrc00_576x324.yuv \
      -d python/test/resource/yuv/src01_hrc01_576x324.yuv \
      -w 576 -h 324 -p 420 -b 8 -m path=model/vmaf_float_v0.6.1.json \
      --tiny-model $D/$v/vmaf_tiny_v3.int8.onnx --tiny-device cpu \
      -o $D/$v.json --json >/dev/null 2>&1
    echo -n "$v: "
    /opt/vmaf-venv/bin/python -c "
import json,sys; print(json.load(open(sys.argv[1]))[\"pooled_metrics\"][\"vmaf_tiny_model\"][\"mean\"])" $D/$v.json
  done'
# before: 16.020865
# after:  71.952113        (fp32 vmaf_tiny_v3.onnx baseline: 72.359458)

# 2. C loader + registry gate.
docker exec -u 0 -w /workspace/.claude/worktrees/int8loader vmaf-dev-mcp bash -lc '
  meson setup /tmp/build-dnn /workspace/.claude/worktrees/int8loader/core \
      -Denable_cuda=false -Denable_sycl=false -Denable_dnn=enabled -Denable_tools=true
  ninja -C /tmp/build-dnn && meson test -C /tmp/build-dnn --suite dnn'
# Ok: 13   Fail: 0   (includes test_vmaf_use_tiny_model and test_registry)

# 3. Registry consistency gate, host-side, no ONNX Runtime needed.
TINY_DIR=model/tiny bash core/test/dnn/test_registry.sh
python ai/scripts/validate_model_registry.py
python -m pytest python/test/model_registry_schema_test.py -q
# OK: 26 registry entries verified
# OK: 26 registry entries valid against registry.schema.json
# 11 passed, 1 xfailed

# 4. The two Python-script gaps.
python -m pytest ai/tests/test_measure_quant_drop_unit.py ai/tests/test_qat_train_loader.py -q
# 29 passed, 4 skipped   (2 further failures on a host without onnxruntime are pre-existing)
docker exec -u 0 -w /workspace/.claude/worktrees/int8loader vmaf-dev-mcp \
  /opt/vmaf-venv/bin/python ai/scripts/measure_quant_drop.py \
    --fp32 model/tiny/vmaf_tiny_v3.onnx --int8 model/tiny/vmaf_tiny_v3.int8.onnx --budget 0.002
# [PASS] vmaf_tiny_v3   mode=override PLCC=0.999880  drop=0.000120  budget=0.0020  worst_abs=2.2058

# 5. Netflix golden gate (CPU, -Denable_dnn=disabled — the CI configuration).
make test-netflix-golden
# 271 passed, 12 skipped

Known follow-ups

  • The loader does not verify int8_sha256. Documented in docs/ai/quantization.md and pinned as an invariant in core/src/dnn/AGENTS.md; adding it needs an ADR because a digest mismatch is a third outcome ADR-1032's fp32-fallback semantics do not define, and it would have to land in both loader twins at once.
  • T-GPU-RUNNER-LABEL-MISMATCH-2026-09-05 (docs/state.md, Open bugs): the Coverage GPU (advisory) and SYCL float_ssim Parity jobs target runs-on: [self-hosted, linux, gpu-full], but the only registered runner is labelled sycl-arc and GPU_COVERAGE_ENABLED is unset, so neither can be scheduled. No CI job covers tiny-AI cross-device parity at all. docs/ai/inference.md now records this instead of "planned: self-hosted runner"; the CI fix is a separate change.
  • Research-2029 §5 gaps 3 and 6 are untouched by this PR: pinning quant_format=QuantFormat.QDQ in ptq_static.py / qat_train.py (tracked as T-AI-PTQ-STATIC-QUANT-FORMAT-UNPINNED-2026-09-03), and the end-to-end VMAF delta gate.

🤖 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/dnn-int8-redirect-and-sidecar-fixes branch 6 times, most recently from 4919f64 to bc9d6ad Compare September 6, 2026 09:53
lusoris pushed a commit that referenced this pull request Sep 6, 2026
The int8 redirect added earlier on this branch turned a working
`--tiny-model model/tiny/nr_metric_v1.onnx` invocation into a hard
`-EIO`: the sidecar declares `quant_mode: dynamic`, so the loader
redirected to `nr_metric_v1.int8.onnx`, which clears the size cap and
the op allowlist but fails ORT session creation on any ONNX Runtime
build without a `ConvInteger` kernel ("Could not find an implementation
for ConvInteger(10)"). `core/test/dnn/test_cli.sh` failed on exactly
that; it passes against master's loader.

ADR-1032's "better degraded than dead" rule covers this for the same
reason it covers a missing int8 file, so both loader twins now retry the
fp32 baseline once when `vmaf_ort_open()` fails on a redirected path.
`vmaf_ort_open()` leaves the out-parameter untouched on error, so the
retry cannot leak the failed session. Kept in both `dnn_api.c` and
`dnn_attach_api.c`, and the AGENTS.md invariant now pins that the
fallback has two triggers.

Also:

- `core/test/dnn/test_vmaf_use_tiny_model.c`: split the new redirect
  test into `stage_redirect_triple()` / `expect_tiny_attach()` so no
  function exceeds the `readability-function-size` branch budget, and
  bracket the added block with the tree's cited
  `NOLINTBEGIN(modernize-use-nullptr)` (ADR-1138) so the file stays at
  its ADR-1142 ratchet baseline of 51 rather than growing to 62.
- `docs/usage/cli.md`, `docs/ai/quantization.md`: document the second
  fallback trigger, and correct the claim that the CLI has a
  `--log-level debug` flag — it does not; the binary runs at
  `VMAF_LOG_LEVEL_INFO` and only API callers can see the fallback line.
- `docs/state.md`: the two closed rows and the opened row now cite
  `PR #1320` instead of the branch name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lusoris
lusoris force-pushed the feat/dnn-int8-redirect-and-sidecar-fixes branch from bc9d6ad to 335850f Compare September 6, 2026 18:15
lusoris pushed a commit that referenced this pull request Sep 6, 2026
The int8 redirect added earlier on this branch turned a working
`--tiny-model model/tiny/nr_metric_v1.onnx` invocation into a hard
`-EIO`: the sidecar declares `quant_mode: dynamic`, so the loader
redirected to `nr_metric_v1.int8.onnx`, which clears the size cap and
the op allowlist but fails ORT session creation on any ONNX Runtime
build without a `ConvInteger` kernel ("Could not find an implementation
for ConvInteger(10)"). `core/test/dnn/test_cli.sh` failed on exactly
that; it passes against master's loader.

ADR-1032's "better degraded than dead" rule covers this for the same
reason it covers a missing int8 file, so both loader twins now retry the
fp32 baseline once when `vmaf_ort_open()` fails on a redirected path.
`vmaf_ort_open()` leaves the out-parameter untouched on error, so the
retry cannot leak the failed session. Kept in both `dnn_api.c` and
`dnn_attach_api.c`, and the AGENTS.md invariant now pins that the
fallback has two triggers.

Also:

- `core/test/dnn/test_vmaf_use_tiny_model.c`: split the new redirect
  test into `stage_redirect_triple()` / `expect_tiny_attach()` so no
  function exceeds the `readability-function-size` branch budget, and
  bracket the added block with the tree's cited
  `NOLINTBEGIN(modernize-use-nullptr)` (ADR-1138) so the file stays at
  its ADR-1142 ratchet baseline of 51 rather than growing to 62.
- `docs/usage/cli.md`, `docs/ai/quantization.md`: document the second
  fallback trigger, and correct the claim that the CLI has a
  `--log-level debug` flag — it does not; the binary runs at
  `VMAF_LOG_LEVEL_INFO` and only API callers can see the fallback line.
- `docs/state.md`: the two closed rows and the opened row now cite
  `PR #1320` instead of the branch name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lusoris
lusoris force-pushed the feat/dnn-int8-redirect-and-sidecar-fixes branch from 335850f to e921ff8 Compare September 7, 2026 06:55
lusoris pushed a commit that referenced this pull request Sep 7, 2026
The int8 redirect added earlier on this branch turned a working
`--tiny-model model/tiny/nr_metric_v1.onnx` invocation into a hard
`-EIO`: the sidecar declares `quant_mode: dynamic`, so the loader
redirected to `nr_metric_v1.int8.onnx`, which clears the size cap and
the op allowlist but fails ORT session creation on any ONNX Runtime
build without a `ConvInteger` kernel ("Could not find an implementation
for ConvInteger(10)"). `core/test/dnn/test_cli.sh` failed on exactly
that; it passes against master's loader.

ADR-1032's "better degraded than dead" rule covers this for the same
reason it covers a missing int8 file, so both loader twins now retry the
fp32 baseline once when `vmaf_ort_open()` fails on a redirected path.
`vmaf_ort_open()` leaves the out-parameter untouched on error, so the
retry cannot leak the failed session. Kept in both `dnn_api.c` and
`dnn_attach_api.c`, and the AGENTS.md invariant now pins that the
fallback has two triggers.

Also:

- `core/test/dnn/test_vmaf_use_tiny_model.c`: split the new redirect
  test into `stage_redirect_triple()` / `expect_tiny_attach()` so no
  function exceeds the `readability-function-size` branch budget, and
  bracket the added block with the tree's cited
  `NOLINTBEGIN(modernize-use-nullptr)` (ADR-1138) so the file stays at
  its ADR-1142 ratchet baseline of 51 rather than growing to 62.
- `docs/usage/cli.md`, `docs/ai/quantization.md`: document the second
  fallback trigger, and correct the claim that the CLI has a
  `--log-level debug` flag — it does not; the binary runs at
  `VMAF_LOG_LEVEL_INFO` and only API callers can see the fallback line.
- `docs/state.md`: the two closed rows and the opened row now cite
  `PR #1320` instead of the branch name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris pushed a commit that referenced this pull request Sep 7, 2026
The int8 redirect added earlier on this branch turned a working
`--tiny-model model/tiny/nr_metric_v1.onnx` invocation into a hard
`-EIO`: the sidecar declares `quant_mode: dynamic`, so the loader
redirected to `nr_metric_v1.int8.onnx`, which clears the size cap and
the op allowlist but fails ORT session creation on any ONNX Runtime
build without a `ConvInteger` kernel ("Could not find an implementation
for ConvInteger(10)"). `core/test/dnn/test_cli.sh` failed on exactly
that; it passes against master's loader.

ADR-1032's "better degraded than dead" rule covers this for the same
reason it covers a missing int8 file, so both loader twins now retry the
fp32 baseline once when `vmaf_ort_open()` fails on a redirected path.
`vmaf_ort_open()` leaves the out-parameter untouched on error, so the
retry cannot leak the failed session. Kept in both `dnn_api.c` and
`dnn_attach_api.c`, and the AGENTS.md invariant now pins that the
fallback has two triggers.

Also:

- `core/test/dnn/test_vmaf_use_tiny_model.c`: split the new redirect
  test into `stage_redirect_triple()` / `expect_tiny_attach()` so no
  function exceeds the `readability-function-size` branch budget, and
  bracket the added block with the tree's cited
  `NOLINTBEGIN(modernize-use-nullptr)` (ADR-1138) so the file stays at
  its ADR-1142 ratchet baseline of 51 rather than growing to 62.
- `docs/usage/cli.md`, `docs/ai/quantization.md`: document the second
  fallback trigger, and correct the claim that the CLI has a
  `--log-level debug` flag — it does not; the binary runs at
  `VMAF_LOG_LEVEL_INFO` and only API callers can see the fallback line.
- `docs/state.md`: the two closed rows and the opened row now cite
  `PR #1320` instead of the branch name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lusoris
lusoris force-pushed the feat/dnn-int8-redirect-and-sidecar-fixes branch from e921ff8 to 328b1f3 Compare September 7, 2026 10:19
@lusoris
lusoris marked this pull request as ready for review September 7, 2026 13:17
@lusoris
lusoris enabled auto-merge (squash) September 7, 2026 13:17
lusoris pushed a commit that referenced this pull request Sep 7, 2026
The rebase onto master produced two Recently-closed rows for each of
T-TINY-V3-INT8-SIDECAR-MISSING-ONNX-HAS-SCALER-2026-09-04 and
T-DNN-ATTACH-INT8-REDIRECT-MISSING-2026-09-04, which failed the ADR-0165
uniqueness gate in Pre-Commit.

The two copies of each differed only in the reference column: one cited the
branch name, the other cited PR #1320. Kept the PR citation -- a branch name
is precisely the placeholder reference that state-md-touch-check.sh's
placeholder-ref hardening rejects, and it stops meaning anything once the
branch is deleted.

check-state-md-rows: OK (373 rows, no duplicate ids).

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

lusoris commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Cleared three of the four failures; the fourth needs a test I'd rather flag than guess at.

Fixed and pushed:

  1. Pre-Commit (f14afde..) — a keep-both rebase left duplicate state.md rows for T-TINY-V3-INT8-SIDECAR-MISSING-ONNX-HAS-SCALER and T-DNN-ATTACH-INT8-REDIRECT-MISSING. The two copies differed only in the reference column; kept the PR #1320 citation over the branch-name one, since a branch name is the placeholder state-md-touch-check.sh rejects and stops meaning anything once the branch is deleted.
  2. Tiny AIgraph_bakes_scaler() guarded onnx.load() against the library being absent but not the file being unparseable, so test_consistency_check_int8_sha_mismatch (which writes b"int8payload" on purpose) got a DecodeError instead of the mismatch it asserts. Worse, the exception aborted _consistency_check and discarded every other finding. Now degrades to the same byte scan the no-onnx path uses.
  3. Tidy Ratchet — exit 3, the good kind: this PR took dnn_attach_api.c from 1 warning to 0. Applied exactly CI's measured delta (total 2950 → 2949) rather than re-running the ratchet locally, since a host build resolves enable_dnn=auto differently from the CI cpu lane and would rebaseline unrelated files.

Still failing — Coverage Gate, and it is this PR's own new code:

critical: core/src/dnn/dnn_api.c — 82.0000% (min 83%)
  FAIL: security-critical file below 83%

Every other critical file passes. The 1-point miss is the int8→fp32 retry branch this PR adds to dnn_api.c:

if (rc < 0 && load_path != onnx_path) {
    load_path = onnx_path;
    rc = vmaf_ort_open(&s->ort, onnx_path, cfg);
}

Why the existing test does not cover it. test_use_tiny_model_int8_redirect_and_fallback covers the missing-file fallback — it unlinks the int8 and re-attaches. This branch is the session-open-failure fallback: the int8 file exists and clears both the size cap and the op allowlist, but vmaf_ort_open fails anyway (the "Could not find an implementation for ConvInteger" case the comment cites). Different branch, and it is reached through the session API rather than vmaf_use_tiny_model.

The mechanism to test it already exists: core/test/dnn/test_ort_error_injection.c compiles ort_backend.c with a mock OrtApi and a scenario enum that steers which call returns a non-NULL OrtStatus. Covering this branch means making CreateSession fail for the first open and succeed for the retry — the scenario global is currently all-or-nothing, so it needs a call-count-scoped scenario.

Holding this PR in the merge train meanwhile so it stops blocking the window — it has held it for about an hour across four CI rounds.

@lusoris
lusoris marked this pull request as draft September 7, 2026 14:25
auto-merge was automatically disabled September 7, 2026 14:25

Pull request was converted to draft

@lusoris
lusoris force-pushed the feat/dnn-int8-redirect-and-sidecar-fixes branch from f14afde to df68949 Compare September 7, 2026 17:00
lusoris pushed a commit that referenced this pull request Sep 7, 2026
The int8 redirect added earlier on this branch turned a working
`--tiny-model model/tiny/nr_metric_v1.onnx` invocation into a hard
`-EIO`: the sidecar declares `quant_mode: dynamic`, so the loader
redirected to `nr_metric_v1.int8.onnx`, which clears the size cap and
the op allowlist but fails ORT session creation on any ONNX Runtime
build without a `ConvInteger` kernel ("Could not find an implementation
for ConvInteger(10)"). `core/test/dnn/test_cli.sh` failed on exactly
that; it passes against master's loader.

ADR-1032's "better degraded than dead" rule covers this for the same
reason it covers a missing int8 file, so both loader twins now retry the
fp32 baseline once when `vmaf_ort_open()` fails on a redirected path.
`vmaf_ort_open()` leaves the out-parameter untouched on error, so the
retry cannot leak the failed session. Kept in both `dnn_api.c` and
`dnn_attach_api.c`, and the AGENTS.md invariant now pins that the
fallback has two triggers.

Also:

- `core/test/dnn/test_vmaf_use_tiny_model.c`: split the new redirect
  test into `stage_redirect_triple()` / `expect_tiny_attach()` so no
  function exceeds the `readability-function-size` branch budget, and
  bracket the added block with the tree's cited
  `NOLINTBEGIN(modernize-use-nullptr)` (ADR-1138) so the file stays at
  its ADR-1142 ratchet baseline of 51 rather than growing to 62.
- `docs/usage/cli.md`, `docs/ai/quantization.md`: document the second
  fallback trigger, and correct the claim that the CLI has a
  `--log-level debug` flag — it does not; the binary runs at
  `VMAF_LOG_LEVEL_INFO` and only API callers can see the fallback line.
- `docs/state.md`: the two closed rows and the opened row now cite
  `PR #1320` instead of the branch name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lusoris added 7 commits September 7, 2026 20:33
`model/tiny/vmaf_tiny_v3.int8.onnx` bakes the StandardScaler into the
graph as `Sub` / `Div` Constant nodes, but its sidecar omitted
`"onnx_has_scaler": true`. `core/src/libvmaf.c` therefore normalised the
canonical-6 feature vector a second time before inference.

Measured on the Netflix `src01_hrc00/hrc01_576x324` pair (48 frames, CPU
backend): pooled `vmaf_tiny_model` mean 16.020865 without the field vs
71.952113 with it, against an fp32 `vmaf_tiny_v3.onnx` baseline of
72.359458. Per-frame PLCC vs fp32 goes 0.975443 -> 0.999876 (drop
0.000124, inside the sidecar's declared 0.01 budget).

Adds a consistency gate over every `model/tiny/*.int8.onnx`: a graph
containing both `Sub` and `Div` must have a companion sidecar declaring
`"onnx_has_scaler": true`. Enforced in `core/test/dnn/test_registry.sh`,
`python/test/model_registry_schema_test.py`, and
`ai/scripts/validate_model_registry.py`; detection prefers the `onnx`
parser and falls back to a protobuf byte scan on legs without it.

Refs #1242
`--all` and the positional form both resolve through
`model/tiny/registry.json`: the positional path must live under
`model/tiny/`, and the PLCC-drop budget comes from that entry's
`quant_accuracy_budget_plcc`. A model that is not committed yet — PTQ or
QAT scratch output, a CI smoke artifact, a release candidate — has
neither, which is Research-2029 gap 5.

`--fp32 PATH --int8 PATH` measures an explicit pair and never loads the
registry; `--budget` supplies the drop budget (default 0.01, the same
registry-wide default) and `--id` labels the row. The overrides are
rejected alongside `--all` or a positional path (exit 2); exit codes are
otherwise unchanged.

Verified against the shipped v3 pair in the vmaf-dev-mcp container:

    measure_quant_drop.py --fp32 model/tiny/vmaf_tiny_v3.onnx \
        --int8 model/tiny/vmaf_tiny_v3.int8.onnx --budget 0.002
    [PASS] vmaf_tiny_v3 mode=override PLCC=0.999880 drop=0.000120 \
        budget=0.0020 worst_abs=2.2058

Refs #1242
`_build_train_loader_factory` handed every config to
`VmafTrainDataModule`, which only materialises rank-2 tabular rows (the
canonical-6 feature vector plus a scalar MOS). A 2D CNN — `learned_filter`,
`nr_metric` — needs image batches and fails inside its first `Conv2d`.
`ai/configs/learned_filter_v1_qat.yaml` only appeared to work because its
parquet cache is uncommitted, so the missing-cache branch silently
downgraded the run to `--smoke` and trained nothing. Research-2029 §5 gap 4.

The loader is now selected by the rank of `qat.input_shape` — the same
shape `_build_example_inputs` traces with, so the loader and the FX trace
cannot disagree. Rank 2 keeps the datamodule; rank 4 reads an `.npz` with
`x` (N, C, H, W) and `y`; any other rank downgrades to smoke mode with a
message on stderr rather than failing deep in a forward pass. The archive
is validated when the loader is built, so a malformed cache fails before
the fp32 warm-start burns an epoch.

Refs #1242
Each placeholder is replaced with the audited state of the tree rather
than a restated plan.

sidecar-online-training.md: the "no stability gate ... planned per
Research-0733 §3.4" line becomes a Checkpoint quarantine section that
tabulates §3.4 element by element. Atomic checkpoint writes and the
`.sha256` file exist (`ai/sidecar/sgd_ema.py`, `online_trainer.py`); the
stability gate, the fixture set, the `unstable` tag, `spec.versionPolicy`,
`stability_plcc_delta`, automatic rollback, and node-side digest
verification do not — `cmd/vmafx-node/` contains no SHA-256 check at all.
Every committed checkpoint is picked up unvetted.

extractor-template.md: the large-N row called `feature_transnet_v2.c`
"planned". It shipped, as `core/src/feature/transnet_v2.c`. The new
subsection documents the 100-slot ring buffer, the
`[1, 100, 3, 27, 48]` tensor, the fact that the suggested strided
submission was not taken (the network runs once per frame), the ~50-frame
warm-up, and that per-shot aggregation is backlog-only.

inference.md: "planned: self-hosted runner" was wrong in both directions.
Two `gpu-full` jobs exist, but the only registered runner is labelled
`sycl-arc` and `GPU_COVERAGE_ENABLED` is unset, so neither can be
scheduled; and neither would cover the cross-device tiny-AI bounds this
page quotes. Opened as T-GPU-RUNNER-LABEL-MISMATCH-2026-09-05 in
docs/state.md.

Refs #1242
The fp32 sidecar carries an `int8_sha256` for its quantised sibling, but
neither `vmaf_dnn_session_open` nor `vmaf_use_tiny_model` parses it — the
only load-time gates are the 50 MB size cap and the op allowlist. Integrity
of the shipped artefacts is enforced by the registry gates and by
`--tiny-model-verify`, not by the loader.

Adding a digest check would introduce a third outcome (mismatch) that
ADR-1032's fp32-fallback semantics do not define, and would have to land in
both twins at once, so it needs its own ADR. Documented in
docs/ai/quantization.md and pinned as an invariant in core/src/dnn/AGENTS.md
rather than left as an unstated gap.

Refs #1242
Wiring the int8 redirect inline pushed `vmaf_use_tiny_model` to 106 lines
and introduced two clang-tidy findings the earlier commit did not clear:
`bugprone-redundant-branch-condition` on an `if (have_meta)` nested inside
a branch already guarded by `have_meta &&`, and a
`clang-analyzer-deadcode.DeadStores` on the `rc = 0` that reset the
fallback path.

The redirect is now `resolve_quantised_load_path()` (out-param for the
resolved path, so the sentinel `NULL` return goes away too), the sidecar
load is `load_optional_sidecar()`, and the shape query plus
`vmaf_ctx_dnn_attach` are `attach_opened_session()`. The entry point is 54
lines — under both the 60-line threshold and master's pre-existing 63.

clang-tidy on the file now reports only the two `modernize-use-nullptr`
warnings that master already carries. Behaviour is unchanged: the 13-test
meson `dnn` suite passes with `-Denable_dnn=enabled` and ONNX Runtime in
the container, `test_vmaf_use_tiny_model` included. All three helpers sit
inside the `#if VMAF_HAVE_DNN` guard, so the ADR-0374 disabled-build stub
is untouched.

Refs #1242
Lusoris and others added 2 commits September 7, 2026 20:33
The int8 redirect added earlier on this branch turned a working
`--tiny-model model/tiny/nr_metric_v1.onnx` invocation into a hard
`-EIO`: the sidecar declares `quant_mode: dynamic`, so the loader
redirected to `nr_metric_v1.int8.onnx`, which clears the size cap and
the op allowlist but fails ORT session creation on any ONNX Runtime
build without a `ConvInteger` kernel ("Could not find an implementation
for ConvInteger(10)"). `core/test/dnn/test_cli.sh` failed on exactly
that; it passes against master's loader.

ADR-1032's "better degraded than dead" rule covers this for the same
reason it covers a missing int8 file, so both loader twins now retry the
fp32 baseline once when `vmaf_ort_open()` fails on a redirected path.
`vmaf_ort_open()` leaves the out-parameter untouched on error, so the
retry cannot leak the failed session. Kept in both `dnn_api.c` and
`dnn_attach_api.c`, and the AGENTS.md invariant now pins that the
fallback has two triggers.

Also:

- `core/test/dnn/test_vmaf_use_tiny_model.c`: split the new redirect
  test into `stage_redirect_triple()` / `expect_tiny_attach()` so no
  function exceeds the `readability-function-size` branch budget, and
  bracket the added block with the tree's cited
  `NOLINTBEGIN(modernize-use-nullptr)` (ADR-1138) so the file stays at
  its ADR-1142 ratchet baseline of 51 rather than growing to 62.
- `docs/usage/cli.md`, `docs/ai/quantization.md`: document the second
  fallback trigger, and correct the claim that the CLI has a
  `--log-level debug` flag — it does not; the binary runs at
  `VMAF_LOG_LEVEL_INFO` and only API callers can see the fallback line.
- `docs/state.md`: the two closed rows and the opened row now cite
  `PR #1320` instead of the branch name.

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

graph_bakes_scaler() guarded onnx.load() against the library being absent but
not against the file being unparseable. On a corrupt or non-ONNX blob
onnx.load raises google.protobuf.message.DecodeError, which propagated out of
_consistency_check() and aborted the whole validation run.

That is the wrong failure mode twice over. A validator exists to report
problems, not to raise on them; and raising here discards every other finding
it had collected -- including the sha256 mismatch that is usually the actual
reason the file is bad. test_consistency_check_int8_sha_mismatch writes
b"int8payload" precisely to assert that mismatch, and got a DecodeError
instead.

Degrade to the same length-prefixed byte scan the no-onnx-installed path
already uses. On arbitrary bytes it finds no Sub/Div markers and answers
False, so _consistency_check finishes and reports the real error.

Verified both paths: ai/tests/test_validate_model_registry_unit.py is 22/22,
and with a stub onnx module whose load() raises DecodeError (the CI condition,
which cannot be reproduced in a venv without onnx installed) the helper
returns False rather than propagating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lusoris
lusoris force-pushed the feat/dnn-int8-redirect-and-sidecar-fixes branch from df68949 to 4aa0b92 Compare September 7, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant