Skip to content

feat(rust): predict the vmaf_v1.0.16 models in Rust, bit-identical to the C predictor - #2085

Merged
lusoris merged 5 commits into
masterfrom
rc4/predict-twin
Oct 8, 2026
Merged

lusoris merged 5 commits into
masterfrom
rc4/predict-twin

Conversation

@lusoris

@lusoris lusoris commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

RC4 lane P of #1723: the prediction step of the vmaf_v1.0.16* models in Rust. With -Denable_rust_features=true and VMAF_FEATURE_IMPL=rust, vmaf_predict_score_at_index() runs normalisation, the chroma correction, the nu-SVR (RBF, plus linear and polynomial kernels), denormalisation, the finite checks, the polynomial and piecewise-linear score transform and the clip in the new vmafx-predict crate. The C predictor stays the default. Model loading, name resolution, the score gather from the collector and the append of the prediction stay in C. Lands bottom-up per Q-083 after #2086 (the framework, ADR-1713).

Landed base: #2086 (framework, ADR-1713) and #2199 are on master; this branch was rebased onto master 2da09c2a9 with the framework's commits dropped, retargeted to master, and queued with the other twins (Q-083). The cpu tidy baseline took master's side at each rebase and was re-measured in the dev container (4 TUs, 0 findings). Re-gate on that head: default build 0 warnings, --suite=fast 392 OK, golden 280 passed, msvcism pass; Rust build 0 warnings, --suite=fast --suite=rust 397 OK, 0 fail; model cells --models on netflix, checker1, checker10: 24 of 24 EQUAL.

Result: bit-identical to the C predictor on every model cell measured on the rebased head, 0 diffs (details below).

Rebase and adaptation. The lane's commits are squashed into one commit and replayed onto the rebased framework head (5962a1a51). The lane's stand-in wiring (shim compiled into the predict_c archive, #ifdef HAVE_RUST_FEATURES in predict.c and model.c, its own env reader) is replaced by the design the RC4 contract fixes for the predictor (request P-1):

  • predict.c keeps a struct VmafRustPredictOps table (create, predict, destroy, declared in predict.h), set with vmaf_predict_install_rust_ops(); NULL means the C predictor. predict.c, model.c and model_lifetime.c contain no Rust symbol, no Rust include and no HAVE_RUST_FEATURES branch, so every binary that links the predict_c archive without libvmaf still links without the Rust archive.
  • core/src/rust/shim/rust_predict.c builds the flat model view, defines the table on vmafx_rs_model_new / _predict / _free and vmaf_rust_predict_install(); it is listed in rust_shim_sources (a libvmaf source), and vmaf_ctx_subsystems_init() calls the installer next to vmaf_rust_twins_install().
  • The mode comes from the framework's one reader, vmaf_feature_impl_rust_requested(). With VMAF_FEATURE_IMPL=rust and no table (a build without Rust), or a model the Rust predictor refuses, the C predictor runs and a WARNING names the model (no silent fallback); a value other than c / rust fails the prediction with -EINVAL.
  • vmaf_model_destroy() (now in model_lifetime.c on master) frees the Rust handle through the table and the raw-score scratch.
  • New core/test/test_predict_rust_ops.c (every build, a counting stand-in table) covers the routing, both WARNING fallbacks, error propagation, one handle per model and the free on destroy; test_rust_predict gains a check that vmaf_init() installs the table.
  • core/src/rust/shim/rust_predict.c and core/test/test_rust_predict.c build only with -Denable_rust_features=true and join the tidy-coverage exceptions next to the framework's Rust-only units (same reason, expiry 2026-12-31). test_predict_rust_ops.c is built in every configuration and is measured by the cpu lane.

Rust ABI header check (found here, fixed here). With the pinned cbindgen 0.29.4, scripts/dev/rust-abi-header.sh --check (the Rust workflow's step, ADR-1713) failed on the framework head: the clang-format commit hook had reformatted core/src/rust/include/vmafx_rs.h when it was committed. The clang-format hooks and make format now skip core/src/rust/include/, both generated headers (vmafx_rs.h, this lane's vmafx_rs_predict.h) are cbindgen's bytes (whitespace-only diff), and scripts/ci/tests/test_rust_abi_header_verbatim.py fails when a formatter selects them or the check fails (all three cases fail on the previous head). State row T-RUST-ABI-HEADER-CHECK-FORMATTED-2026-10-07.

Re-gate after the framework's second rebase (onto #2173, framework head e8886a95a): default build 0 warnings, --suite=fast 381 OK, golden 280 passed, msvcism pass, affected tooling 2164 passed; Rust build 0 warnings, --suite=fast --suite=rust 386 OK, 0 fail; model cells rust_twin_diff.py --models on five fixtures 38 EQUAL, 2 REFUSED by both sides (the two 3d0h_2160 models on sparks10), 0 other; the tidy baseline was re-measured in the container (4 TUs, 0 findings) after master's history entry for #2173.

Type

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

Checklist

  • Commits follow Conventional Commits (the commit-msg hook enforces this).
  • make format && make lint is green locally. (Commit hooks pass; Rust: cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings clean.)
  • Unit tests pass: on the rebased head (fb2dd3c07), CPU only, -j4, both builds with $(bash scripts/ci/werror-args.sh true) and 0 compiler warnings:
    • Rust build (-Denable_rust_features=true -Db_lto=false): python3 scripts/ci/run_meson_test.py -- -C build-rs --suite=fast --suite=rust → 382 OK, 0 fail (incl. test_rust_predict, test_predict_rust_ops, test_rust_abi_layout, test_rust_twin_registry, test_rust_twin_harness_netflix).
    • Default build (no Rust): --suite=fast → 377 OK, 0 fail (incl. test_predict_rust_ops, test_predict); every binary that links the predict_c archive links without the Rust archive.
    • make test-netflix-golden → 280 passed, 3 skipped. Affected Python suites: 2149 passed, 0 failed.
    • cargo fmt --all --check, cargo clippy --offline --workspace --all-targets -- -D warnings (0 warnings), cargo test -p vmafx-predict → 10 passed (--manifest-path core/src/rust/Cargo.toml).
    • scripts/dev/preflight.sh --stage msvcism → pass.
    • tidy: cpu core/src/predict.c, core/src/libvmaf.c, core/src/model_lifetime.c, core/test/test_predict_rust_ops.c 0 findings (scripts/dev/tidy-lane.sh --write --only ... cpu, clang-tidy 22.1.8 in the dev image; test_predict_rust_ops.c added to the cpu lane's measured sources).
    • Failing first: with the vmaf_rust_predict_install() call removed from libvmaf.c, test_rust_predict fails ("vmaf_init() installed the Rust predictor"); with the store in vmaf_predict_install_rust_ops() removed, test_predict_rust_ops fails ("the table's result is the score"). Both restored.
  • If I touched any SIMD/GPU code path, I ran /cross-backend-diff and the worst ULP is ≤ 2. — no SIMD/GPU code touched (CPU prediction only).
  • If I touched a feature extractor with SIMD/GPU twins, I either updated every twin or listed the gap under "Known follow-ups" below. — no feature extractor touched.
  • If I added a new .c / .cpp / .cu / .h / .hpp, it has the appropriate license header (see CONTRIBUTING.md). (EUPL-1.2 for fork-authored files; the Rust files that port Netflix code statement by statement carry BSD-2-Clause-Patent and name the C file they mirror.)
  • If this is a breaking change, the commit message uses ! or BREAKING CHANGE: and the migration path is documented below. — not breaking; struct VmafModel is internal.
  • If this PR adds an ADR, the ADR row lives in docs/adr/_index_fragments/<NNNN-slug>.md and the slug is appended to docs/adr/_index_fragments/_order.txt — no new ADR; the decisions are ADR-1713's (the predictor table is the contract's section 4). The one choice this PR makes (a model the Rust predictor refuses runs on the C predictor with a WARNING; sigmoid kernel refused because vmafx_fex::libm has no tanh and no v1 model uses it) is recorded in core/src/AGENTS.d/rust-model-prediction.md.

Bug-status hygiene (ADR-0165)

  • docs/state.md updated in this PR with a row in the appropriate section, OR no state delta: REASON. — no state delta: no bug opened, closed or ruled out; no C defect found while porting.

Netflix golden-data gate (ADR-0024)

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

Cross-backend numerical results

Rust predictor vs C predictor through scripts/ci/rust_twin_diff.py --models --fixtures netflix,checker1,checker10,sparks10,bbb4k --threads 0 on the rebased head: the same build-rs/tools/vmaf run with VMAF_FEATURE_IMPL=c and =rust, --precision max --json, every per-frame metric including vmaf compared as IEEE doubles. Every model logs model vmaf: Rust prediction on the Rust side and no such line on the C side (checked per model on netflix). 40 cells: 38 EQUAL, 2 REFUSED, 0 other.

model                         netflix   checker1   checker10   sparks10   bbb4k
vmaf_v1.0.16_1d5h_2160        48/48     3/3        3/3         5/5        200/200
vmaf_v1.0.16_3d0h             48/48     3/3        3/3         5/5        200/200
vmaf_v1.0.16_3d0h_2160        48/48     3/3        3/3         REFUSED    200/200
vmaf_v1.0.16_5d0h             48/48     3/3        3/3         5/5        200/200
vmaf_v1.0.16_hfr_1d5h_2160    48/48     3/3        3/3         5/5        200/200
vmaf_v1.0.16_hfr_3d0h         48/48     3/3        3/3         5/5        200/200
vmaf_v1.0.16_hfr_3d0h_2160    48/48     3/3        3/3         REFUSED    200/200
vmaf_v1.0.16_hfr_5d0h         48/48     3/3        3/3         5/5        200/200

Identical frames / total; max abs diff 0 in every EQUAL cell. REFUSED: both sides exit 234 on the same input (SpEED: image too small, operating width or height is 0: the 2160 prescale of 480x270), which the harness counts as parity.

test_rust_predict adds 48 random frames x 4 flag sets per model on the 8 models, edited copies (piecewise knots with out_lte_in / out_gte_in, p1 only, no transform and no clip, normalisation off, chroma off), the chroma sentinel and NaN input (both fail with -EINVAL, output untouched). On the lane branch, mutating the Rust (denormalise by a reciprocal product, a one-ulp change of the p1 term) failed both the test and the harness.

Performance

Speed is not a gate in RC4 (measured on the lane branch before the rebase; the arithmetic is unchanged). Single thread, --threads 1, median of 3, default model vmaf_v1.0.16_3d0h, host load average 20 to 30:

end to end            C ms/frame   Rust ms/frame
576x324 (48 frames)   2.36         2.02
4K (40 frames)        79.75        107.93   (runs 77-85 vs 104-163: load noise; the prediction is 4 features)
prediction stage only (micro benchmark, 200000 calls, 3 reps)
C incl. collector gather   1.88 to 1.95 us/frame
Rust, scores in hand       2.33 to 2.43 us/frame

The prediction is about 0.002 ms of a 2 ms (576x324) or 80 ms (4K) frame, so the end-to-end numbers measure the host load, not the predictor. The Rust predictor is about 25 percent slower than the C one per call (215 support vectors, one exp each, bounds-checked slices); left for RC7.

Deep-dive deliverables (ADR-0108)

  • Research digest — no digest needed: a statement-by-statement port with no design question; the hazards are listed in the AGENTS page and the framework's digest covers the design.
  • Decision matrix — no alternatives: only-one-way fix; the split of C and Rust at the score gather and the installed table follow the RC4 contract (section 4, "Prediction"), decided in ADR-1713.
  • AGENTS.md invariant note — core/src/AGENTS.d/rust-model-prediction.md (bit identity with predict.c; only libvmaf links the Rust archive, predict.c / model.c / model_lifetime.c reach Rust only through VmafRustPredictOps).
  • Reproducer / smoke-test command — pasted below under "Reproducer".
  • CHANGELOG fragment — changelog.d/added/rust-model-prediction.md.
  • Rebase note — docs/rebase-notes.d/rust-model-prediction.md (fragment, ADR-2197), "Rust model prediction (RC4 lane P, RC4 — Rust metric, zero-copy import, the VMAFx API and the cloud-native platform #1723)".

Reproducer

meson setup build-rs core -Denable_rust_features=true -Db_lto=false
ninja -C build-rs -j4
python3 scripts/ci/run_meson_test.py -- -C build-rs --suite rust
LD_LIBRARY_PATH=build-rs/src python3 scripts/ci/rust_twin_diff.py --vmaf build-rs/tools/vmaf --models \
  --fixtures netflix,checker1,checker10,sparks10,bbb4k --threads 0
VMAF_FEATURE_IMPL=rust build-rs/tools/vmaf --reference python/test/resource/yuv/src01_hrc00_576x324.yuv \
  --distorted python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --pixel_format 420 \
  --bitdepth 8 --model path=model/vmaf_v1.0.16/vmaf_v1.0.16_3d0h.json --precision max --json --output /dev/stdout

Known follow-ups

  • Bootstrap collections reach the Rust predictor through the same per-model entry; their statistics (mean, stddev, percentile) stay in C.
  • Sigmoid kernel, classification SVMs and a precomputed kernel stay on the C predictor (with the WARNING).
  • scripts/dev/rust-abi-header.sh --check regenerates vmafx_rs_predict.h only where cbindgen is installed; test_rust_abi_layout and test_rust_predict guard the ABI meanwhile.
  • The tidy-coverage exceptions for the Rust-only units expire 2026-12-31, with the Rust toolchain in the dev image (ADR-1713 follow-up).

@lusoris lusoris added the rc4 RC4: the vmaf_v1.0.16_3d0h path in Rust; lands after the v1.0.0-rc.3 tag label Oct 5, 2026
@github-actions github-actions Bot added the type:feature New feature or request label Oct 5, 2026
@lusoris
lusoris force-pushed the rc4/rust-extractor-framework branch from e99e0f4 to 5962a1a Compare October 7, 2026 14:26
@lusoris
lusoris force-pushed the rc4/rust-extractor-framework branch from 5962a1a to e8886a9 Compare October 7, 2026 15:19
@lusoris
lusoris force-pushed the rc4/rust-extractor-framework branch from e8886a9 to ccf7ccf Compare October 7, 2026 16:43
Base automatically changed from rc4/rust-extractor-framework to master October 7, 2026 16:44
@lusoris
lusoris marked this pull request as ready for review October 7, 2026 17:51
/* What the table was handed for frame 1. */
static char *check_routed_arguments(const Fixture *fx)
{
mu_assert("the table gets the raw score", fake.first_raw == raw_score(0, 1));
fixture_close(&fx);
vmaf_predict_install_rust_ops(NULL);
mu_assert("an unsupported model scores", err == 0 && again == 0);
mu_assert("an unsupported model runs the C predictor", state == 2 && score == expected);
…2548 landed twice (#2560)

* fix(test): drop the second MALLOC_PERTURB_ entry and state row that #2548 landed twice

#2548 landed the merge train's squash of its first revision, which still carried its own MALLOC_PERTURB_=0 entry for test_gpu_picture_pool_uaf and a state row for the same bug; #2547 had already landed both. core/test/meson.build now lists the entry once (#2547's), docs/state.md keeps T-GPU-POOL-UAF-TEST-FILLS-HOST-MEMORY-2026-10-08 only, and the #2548 changelog and rebase-note fragments no longer claim that fix.

Signed-off-by: Lusoris <lusoris@proton.me>
… (ADR-2199) (#2423)

* ci(hooks): run actionlint under a deadline so a deadlock fails loudly (ADR-2199)

actionlint v1.7.12 writes the script of a `run:` block to shellcheck's stdin
pipe before it starts shellcheck, so it deadlocks when the script is larger
than the pipe. A user over fs.pipe-user-pages-soft gets pipes of 8 KiB, which
made the pre-push hook hang on about every other push. scripts/ci/run_actionlint.py
gives the run 90 seconds, terminates the process group, names the cause and
exits 124; it never reports a pass for a run that did not finish. The hook and
`make lint-actions` both go through it. The defect is upstream
(rhysd/actionlint#702). Decision Q-078.

Restore SIGQUIT for actionlint and signal only it, so the hang dump survives
a background start where non-interactive shells ignore SIGQUIT by default.

Signed-off-by: Lusoris <lusoris@proton.me>
#2096)

* feat(rust): port the integer motion extractor to Rust (motion_rust)
…m extractor (#2099)

* feat(rust): add adm_rust, a bit-identical Rust twin of the integer adm extractor

Signed-off-by: Lusoris <lusoris@proton.me>
… the C predictor (#2085)

* feat(rust): predict the vmaf_v1.0.16 models in Rust, bit-identical to the C predictor

Add the vmafx-predict crate (normalise, chroma correction, nu-SVR, denormalise,
finite checks, polynomial and piecewise score transform, clip) with its C ABI
and a flat model view built by core/src/rust/shim/rust_predict.c. predict.c
splits the post-gather steps into predict_compute_c() and predict_compute_rust();
VMAF_FEATURE_IMPL=rust selects the Rust predictor, which runs only for models it
implements and says so otherwise.

predict.c and model_lifetime.c reach Rust only through the struct
VmafRustPredictOps table declared in predict.h and installed with
vmaf_predict_install_rust_ops(). The shim defines the table and
vmaf_rust_predict_install(); it is a libvmaf source (rust_shim_sources), and
vmaf_ctx_subsystems_init() calls the installer next to
vmaf_rust_twins_install(). Binaries that link the predict_c archive without
libvmaf never need the Rust archive. With VMAF_FEATURE_IMPL=rust and no table
installed, or for a model the Rust predictor refuses, the C predictor runs and
a WARNING names the model; vmaf_model_destroy() frees the handle through the
table.

test_predict_rust_ops (every build, a stand-in table) covers the routing, the
fallback warnings, error propagation and the free on destroy;
test_rust_predict compares both predictors bit for bit on the v1.0.16 models
and edited variants and checks that vmaf_init() installs the table.

* fix(rust): keep the cbindgen headers byte for byte so the Rust ABI header check passes

rust-abi-header.sh --check compares core/src/rust/include/*.h with a fresh cbindgen 0.29.4 run, but the clang-format commit hook had reformatted vmafx_rs.h, so the Rust workflow's check failed on every head. The clang-format hooks and make format skip the directory, both headers are regenerated verbatim (whitespace only), and a tooling test fails when a formatter selects them or the check fails.

* ci(tidy): measure the translation units of the Rust predictor's C side in the cpu lane

The clang-tidy coverage rule on master requires every tracked translation
unit to be read by a lane. The new and touched units of this pull request
were measured in the dev container (scripts/dev/tidy-lane.sh --write
--only ... cpu, clang-tidy 22.1.8): 0 findings, 0 uncited NOLINT; they join
the cpu lane's measured sources.

* docs(agents): write the Rust twin's agent page in the internal register (praetor caveman lint)

* docs(rust): move the rebase note to a fragment and leave the rendered files to the landing render (ADR-2197)

Signed-off-by: Lusoris <lusoris@proton.me>
@lusoris
lusoris merged commit 059f018 into master Oct 8, 2026
11 of 35 checks passed
@lusoris
lusoris deleted the rc4/predict-twin branch October 8, 2026 02:20
lusoris added a commit that referenced this pull request Oct 8, 2026
…maf_test_link

test_predict_rust_ops, test_rust_predict and test_rust_cambi_kernels came to
master with #2085 and #2090 and link libvmaf.get_static_lib(). After the
library split libvmaf.a is the compat library: a white-box test links
vmaf_test_link (libvmaf.a with libvmafx.a, or both shared libraries), as every
other white-box test of this PR does.

Signed-off-by: Lusoris <lusoris@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rc4 RC4: the vmaf_v1.0.16_3d0h path in Rust; lands after the v1.0.0-rc.3 tag type:feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants