Skip to content
Merged
6 changes: 3 additions & 3 deletions .standards-baseline.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"generated_at": "2026-09-20T08:29:42Z",
"generated_at": "2026-09-20T09:43:13Z",
"repository": "",
"commit_sha": "",
"total_infractions": 1411,
Expand Down Expand Up @@ -7979,10 +7979,10 @@
{
"rule_id": "HISS-04",
"file_path": "core/test/test_hip_float_adm_parity.c",
"line_number": 161,
"line_number": 164,
"symbol": "{",
"message": "Function '{' (71 LOC) exceeds HISS-04 / NASA Rule 4 limit of 60 LOC",
"fingerprint": "core/test/test_hip_float_adm_parity.c:161:HISS-04"
"fingerprint": "core/test/test_hip_float_adm_parity.c:164:HISS-04"
},
{
"rule_id": "HISS-04",
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22929,6 +22929,20 @@ always returned so feature availability was never affected.
the DWT2 vertical-pass SIMD chains from the scalar reference (F3 fix, ADR-0844).


- **`float_adm` GPU twins applied `adm_csf_scale` / `adm_csf_diag_scale` in
Watson mode, and named the options differently from the CPU.** In the only
CSF mode the CUDA/SYCL/HIP/Metal twins support, the CPU reference
(`adm_tools.c::adm_csf_rfactor_s`) computes `rfactor = 1 / quant_step` and
never reads those two options — they belong to the Barten branch — but every
twin multiplied them in, so `adm_csf_scale=2.0` doubled the GPU's CSF weights
while the CPU ignored it. CUDA, SYCL and HIP also aliased the options `cs` /
`cds` (max 100) where the CPU uses `scf` / `scfd` (max 50); because feature
names derive from aliases, the same request produced `adm2_scf_2` on the CPU
and `adm2_cs_2` on the GPU. Both fixed to match the CPU; each backend's
`float_adm` parity test gains a variant that sets the options and reads back
under the derived key.


### `float_ansnr`: restore `enable_chroma` option clobbered by PR #1067

PR #1067 (bootstrap-name-builder dedup refactor) inadvertently replaced the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- **`float_adm` GPU twins applied `adm_csf_scale` / `adm_csf_diag_scale` in
Watson mode, and named the options differently from the CPU.** In the only
CSF mode the CUDA/SYCL/HIP/Metal twins support, the CPU reference
(`adm_tools.c::adm_csf_rfactor_s`) computes `rfactor = 1 / quant_step` and
never reads those two options — they belong to the Barten branch — but every
twin multiplied them in, so `adm_csf_scale=2.0` doubled the GPU's CSF weights
while the CPU ignored it. CUDA, SYCL and HIP also aliased the options `cs` /
`cds` (max 100) where the CPU uses `scf` / `scfd` (max 50); because feature
names derive from aliases, the same request produced `adm2_scf_2` on the CPU
and `adm2_cs_2` on the GPU. Both fixed to match the CPU; each backend's
`float_adm` parity test gains a variant that sets the options and reads back
under the derived key.
9 changes: 9 additions & 0 deletions core/src/feature/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1697,3 +1697,12 @@ Concretely, when kernel promotes `float` inputs to `double`, do promotion
**before** arithmetic, not after. `(double)a - (double)b` is exact for two
floats; `(double)(a - b)` is not, and mixing two between vector body and
its scalar tail makes result depend on vector width.

## Twin option tables mirror the CPU's aliases and semantics (ADR-1214)

When a GPU twin copies an option from the CPU extractor, copy the `alias` and
range too: ADR-1183 builds the emitted feature name from the alias and value of
every non-default option, so `cs` on the twin and `scf` on the CPU means two
different keys for one feature. And copy the *semantics* from the branch the
twin actually implements — `adm_csf_scale` is a Barten-mode argument, so in the
Watson-only twins it must be a no-op exactly as it is on the CPU.
18 changes: 18 additions & 0 deletions core/test/test_cuda_float_adm_parity.c
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ static const char *const ADM_FEATURES_APN[NUM_ADM_FEATURES] = {
static const char *const ADM_FEATURES_BCM[NUM_ADM_FEATURES] = {
"adm2_bcm_1", "adm_scale0_bcm_1", "adm_scale1_bcm_1", "adm_scale2_bcm_1", "adm_scale3_bcm_1",
};
static const char *const ADM_FEATURES_SCF[NUM_ADM_FEATURES] = {
"adm2_scf_2", "adm_scale0_scf_2", "adm_scale1_scf_2", "adm_scale2_scf_2", "adm_scale3_scf_2",
};

/* Build the option dictionary for a variant, or leave it NULL for defaults. */
static int adm_opts_build(VmafFeatureDictionary **opts, const char *name, const char *val)
Expand Down Expand Up @@ -310,10 +313,25 @@ static char *test_float_adm_bypass_cm_reaches_kernel(void)
return assert_opt_parity("adm_bypass_cm", "1", ADM_FEATURES_BCM, "bcm=1");
}

/* ADR-1214 — adm_csf_scale must be a no-op in the Watson-97 mode this twin
* implements, exactly as it is on the CPU (`adm_tools.c::adm_csf_rfactor_s`
* consults it only in Barten mode). The twins used to multiply it into every
* CSF rfactor, and declared it under the alias `cs` where the CPU says `scf`,
* so the same request produced a different feature key as well as a different
* score. The fix landed as 64ea351be without this regression test.
*
* The key suffix follows ADR-1183: the alias base plus `_<alias>_<%g value>`,
* so `adm_csf_scale=2.0` files the scores under `_scf_2`. */
static char *test_float_adm_csf_scale_is_a_watson_mode_noop(void)
{
return assert_opt_parity("adm_csf_scale", "2.0", ADM_FEATURES_SCF, "scf=2.0");
}

char *run_tests(void)
{
mu_run_test(test_float_adm_cpu_cuda_parity);
mu_run_test(test_float_adm_p_norm_reaches_kernel);
mu_run_test(test_float_adm_bypass_cm_reaches_kernel);
mu_run_test(test_float_adm_csf_scale_is_a_watson_mode_noop);
return NULL;
}
43 changes: 43 additions & 0 deletions core/test/test_hip_float_adm_parity.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ static const char *const kAdmFeatures[] = {
static const char *const kAdmFeaturesApn[] = {
"adm2_apn_2", "adm_scale0_apn_2", "adm_scale1_apn_2", "adm_scale2_apn_2", "adm_scale3_apn_2",
};
static const char *const kAdmFeaturesScf[NUM_ADM_FEATURES] = {
"adm2_scf_2", "adm_scale0_scf_2", "adm_scale1_scf_2", "adm_scale2_scf_2", "adm_scale3_scf_2",
};

/* Build the option dictionary for a variant, or leave it NULL for defaults. */
static int adm_opts_build(VmafFeatureDictionary **opts, const char *name, const char *val)
Expand Down Expand Up @@ -301,10 +304,50 @@ static char *test_float_adm_p_norm_reaches_kernel(void)
return NULL;
}

/* ADR-1214 — adm_csf_scale must be a no-op in the Watson-97 mode this twin
* implements, exactly as it is on the CPU (`adm_tools.c::adm_csf_rfactor_s`
* consults it only in Barten mode). The twins used to multiply it into every
* CSF rfactor, and declared it under the alias `cs` where the CPU says `scf`,
* so the same request produced a different feature key as well as a different
* score. The fix landed as 64ea351be without this regression test.
*
* The key suffix follows ADR-1183: the alias base plus `_<alias>_<%g value>`,
* so `adm_csf_scale=2.0` files the scores under `_scf_2`. */
static char *test_float_adm_csf_scale_is_a_watson_mode_noop(void)
{
double cpu_scores[NUM_ADM_FEATURES] = {0};
double hip_scores[NUM_ADM_FEATURES] = {0};
int skipped = 0;

char *msg = run_cpu_float_adm("adm_csf_scale", "2.0", kAdmFeaturesScf, cpu_scores);
if (msg)
return msg;
msg = run_hip_float_adm("adm_csf_scale", "2.0", kAdmFeaturesScf, hip_scores, &skipped);
if (msg)
return msg;
if (skipped)
return NULL;
for (size_t f = 0; f < NUM_ADM_FEATURES; f++) {
if (isnan(hip_scores[f]))
return NULL;
const double d = fabs(cpu_scores[f] - hip_scores[f]);
if (d > PARITY_TOL) {
(void)fprintf(stderr,
"\nfloat_adm scf=2.0 parity FAIL: %s cpu=%.8f hip=%.8f delta=%.2e "
"tol=%.2e\n",
kAdmFeaturesScf[f], cpu_scores[f], hip_scores[f], d, PARITY_TOL);
}
mu_assert("float_adm applies adm_csf_scale in Watson mode where the CPU ignores it",
d <= PARITY_TOL);
}
return NULL;
}

char *run_tests(void)
{
mu_run_test(test_float_adm_hip_registered);
mu_run_test(test_float_adm_cpu_hip_parity);
mu_run_test(test_float_adm_p_norm_reaches_kernel);
mu_run_test(test_float_adm_csf_scale_is_a_watson_mode_noop);
return NULL;
}
39 changes: 39 additions & 0 deletions core/test/test_sycl_float_adm_parity.c
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ static const char *const kAdmFeatures[NUM_ADM_FEATURES] = {
static const char *const kAdmFeaturesApn[NUM_ADM_FEATURES] = {
"adm2_apn_2", "adm_scale0_apn_2", "adm_scale1_apn_2", "adm_scale2_apn_2", "adm_scale3_apn_2",
};
static const char *const kAdmFeaturesScf[NUM_ADM_FEATURES] = {
"adm2_scf_2", "adm_scale0_scf_2", "adm_scale1_scf_2", "adm_scale2_scf_2", "adm_scale3_scf_2",
};

/* Build the option dictionary for a variant, or leave it NULL for defaults. */
static int adm_opts_build(VmafFeatureDictionary **opts, const char *name, const char *val)
Expand Down Expand Up @@ -254,11 +257,47 @@ static char *test_float_adm_p_norm_reaches_kernel(void)
return NULL;
}

/* ADR-1214 — adm_csf_scale must be a no-op in the Watson-97 mode this twin
* implements, exactly as it is on the CPU (`adm_tools.c::adm_csf_rfactor_s`
* consults it only in Barten mode). The twins used to multiply it into every
* CSF rfactor, and declared it under the alias `cs` where the CPU says `scf`,
* so the same request produced a different feature key as well as a different
* score. The fix landed as 64ea351be without this regression test.
*
* The key suffix follows ADR-1183: the alias base plus `_<alias>_<%g value>`,
* so `adm_csf_scale=2.0` files the scores under `_scf_2`. */
static char *test_float_adm_csf_scale_is_a_watson_mode_noop(void)
{
double cpu_scores[NUM_ADM_FEATURES] = {0};
double sycl_scores[NUM_ADM_FEATURES] = {0};
char *msg = run_cpu("adm_csf_scale", "2.0", kAdmFeaturesScf, cpu_scores);
if (msg)
return msg;
msg = run_sycl("adm_csf_scale", "2.0", kAdmFeaturesScf, sycl_scores);
if (msg)
return msg;
if (isnan(sycl_scores[0]))
return NULL;
for (unsigned m = 0; m < NUM_ADM_FEATURES; m++) {
const double delta = fabs(cpu_scores[m] - sycl_scores[m]);
if (delta > PARITY_TOL) {
(void)fprintf(stderr,
"\nfloat_adm scf=2.0 parity FAIL: %s cpu=%.8f sycl=%.8f delta=%.2e "
"tol=%.2e\n",
kAdmFeaturesScf[m], cpu_scores[m], sycl_scores[m], delta, PARITY_TOL);
}
mu_assert("float_adm applies adm_csf_scale in Watson mode where the CPU ignores it",
delta <= PARITY_TOL);
}
return NULL;
}

char *run_tests(void)
{
mu_run_test(test_float_adm_sycl_registered);
mu_run_test(test_float_adm_cpu_sycl_parity);
mu_run_test(test_float_adm_p_norm_reaches_kernel);
mu_run_test(test_float_adm_csf_scale_is_a_watson_mode_noop);
return NULL;
}

Expand Down
92 changes: 92 additions & 0 deletions docs/adr/1214-float-adm-csf-scale-watson-mode-and-aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<!-- markdownlint-disable MD013 MD041 MD060 -->

# ADR-1214: The float-ADM GPU twins ignore `adm_csf_scale` in Watson mode and share the CPU's option aliases

- **Status**: Proposed
- **Date**: 2026-09-07
- **Deciders**: Lusoris
- **Tags**: cuda, sycl, hip, metal, correctness, feature-extractor, options

## Context

Two related drifts in the CUDA, SYCL, HIP and Metal `float_adm` twins, both
found by the twin-drift sweep and confirmed against the source before any code
was touched.

**Semantics.** The only CSF mode the twins support is `adm_csf_mode == 0`
(Watson-97); every other mode is rejected at init. In that mode the CPU
reference, `core/src/feature/adm_tools.c::adm_csf_rfactor_s`, sets
`rfactor = 1 / dwt_quant_step(...)` and never reads `adm_csf_scale` or
`adm_csf_diag_scale` — those two options are arguments of the *Barten* branch
(mode 1) only. All four twins multiplied them into every rfactor anyway:

```c
s->rfactor[scale * 3 + 0] = (float)s->adm_csf_scale / f1; /* twin */
factor1 = 1.0f / dwt_quant_step(...); /* CPU, mode 0 */
```

The CUDA comment beside it claimed this "matches the CPU Watson-mode path
where rfactor = scale * (1/quant_step)", which is the opposite of what
`adm_tools.c` does. Net effect: `--feature float_adm_cuda=adm_csf_scale=2.0`
doubled every h/v CSF coefficient on the GPU while the CPU ignored the option.

**Naming.** The CUDA, SYCL and HIP option tables declared the two options with
aliases `cs` / `cds` and `max = 100`, where the CPU `float_adm` (and Metal)
use `scf` / `scfd` and `max = 50`. ADR-1183 derives a feature's name from its
alias plus every non-default option's *alias* and value, so for one request the
CPU emitted `adm2_scf_2` and the GPU emitted `adm2_cs_2` — two different keys
for the same feature, which also breaks feature-name parity between backends.

## Decision

We will make the four twins compute the Watson-mode rfactor exactly as the CPU
does (`1 / f`, ignoring the two scale options), and align the CUDA/SYCL/HIP
option aliases and ranges with the CPU (`scf` / `scfd`, `max = 50`). The
options stay advertised: the CPU advertises them too and treats them as no-ops
in this mode, so a model that sets them still selects the twin and gets the
CPU's behaviour and the CPU's feature key.

Each backend's `float_adm` parity test gains a variant that sets
`adm_csf_scale=2.0, adm_csf_diag_scale=0.5` and reads the scores back under
the derived key `adm2_scfd_0.5_scf_2` (options sorted by name, `%g` values),
so both the arithmetic and the naming are gated.

## Alternatives considered

| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Match the CPU: ignore the options in mode 0, align aliases (chosen) | Bit-for-bit the CPU contract; keeps the feature-name derivation identical; four small edits | The options become documented no-ops on the twins, as they already are on the CPU in this mode | — |
| Keep applying the scale on the GPU and change the CPU to match | Arguably a more useful option | Changes the CPU reference and every published float-ADM score for a non-default option; the CPU is the golden side | Rejected |
| Drop the two options from the twins' tables | Cannot be mis-applied | ADR-1183 would then route any model that sets them to the CPU, silently disabling the GPU path for an option the CPU itself ignores | Rejected |
| Implement the Barten branch on the twins so the options mean something | Feature-complete | A separate feature (Barten CSF port), not a parity fix; tracked under the CSF-mode work | Out of scope |

## Note added 2026-09-20 — the code landed before this ADR

The implementation merged as `64ea351be` while this ADR was still on its branch,
so `master` carried the fix without the decision record, the
`core/src/feature/AGENTS.md` invariant, the `docs/state.md` row, or a regression
test. That is the gap the same-PR rules exist to prevent. This PR closes it and
adds the missing test: `test_float_adm_csf_scale_is_a_watson_mode_noop` in the
CUDA, HIP and SYCL float-ADM parity tests asserts that setting
`adm_csf_scale=2.0` leaves the twin's scores equal to the CPU's, which is only
true once the twin stops consulting a Barten-mode argument in Watson mode.

## Consequences

- **Positive**: with `adm_csf_scale=2.0` the CUDA, SYCL and HIP twins now
report the same value as with the default (0.962085756 / 0.962090577 on the
Netflix 576x324 pair, unchanged from their default-path values) under the
same key as the CPU (`adm2_scf_2`). The new parity variants pass on an RTX
4090, an Arc A380 and a gfx1030.
- **Negative**: anyone who relied on `cs=` / `cds=` in a model file for a GPU
run gets an unknown-option error now; those aliases never matched the CPU.
- **Neutral / follow-ups**: Metal received the semantic fix but is unverified
here (no Apple hardware); its aliases were already correct.

## References

- CPU reference: `core/src/feature/adm_tools.c::adm_csf_rfactor_s`,
`core/src/feature/float_adm.c` option table.
- [ADR-1183](1183-model-options-gate-gpu-twin-selection.md) — option-honouring
extractor selection and alias-derived feature names.
- Source: `req` — user direction to fix bugs found by the twin-drift sweep.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1081,4 +1081,5 @@ ADRs may exist there for local session continuity, but the tracked
| [ADR-1262](1262-cli-input-read-error-exit-code.md) | A failed input read exits with the dedicated code 102 instead of 0. `run_frame_loop()` returned only a frame count, so every stop reason — clean end of stream, a reader error, a failed `vmaf_read_pictures()` — looked identical to `main()`, and `vmaf` exited 0 over a truncated prefix while writing a full report. Two failed reads were additionally classified as "both streams ended", because `ret1 && ret2` was tested before `ret1 < 0 \|\| ret2 < 0` and both `-1` values satisfy it, so no diagnostic was printed either; upstream carries the same ordering and left it unfixed in Netflix/vmaf#1604. A stream that legitimately ends earlier than its partner stays a warning and exit 0. The three Netflix golden pairs are byte-identical. | Proposed | cli, tools, exit-codes, fork-local |
| [ADR-1257](1257-retire-darwin-adm-dwt2-legacy-dispatch.md) | Retire the Darwin three-tap integer-ADM DWT2 compatibility dispatch: Apple AArch64 dispatches the universal four-tap `adm_dwt2_8_neon()`, `adm_dwt2_8_neon_apple_legacy()` is deleted, and the three akiyo assertions drop their Darwin branch for the platform-independent `88.030463` (Netflix/vmaf `cba9343ed` fixed the same dropped tap upstream). Supersedes ADR-1057's Darwin integer-compatibility contract. | Accepted | simd, neon, integer-adm, darwin, testing, upstream-sync |
| [ADR-1265](1265-clang-tidy-header-filter-absolute-paths.md) | `.clang-tidy`'s `HeaderFilterRegex` was anchored `^core/…`, but clang-tidy matches it against the absolute path it saw, which never starts with `core/` — so every in-repo header was dropped as "non-user code" and the ADR-1142 whole-tree ratchet had never counted a header (one TU: `Suppressed 540 warnings (480 in non-user code)`; tree-wide 60 headers carry 313 findings, `adm_tools.h` 142). The regex gains a `(^|/)` prefix and every lane's baseline is re-recorded, the CPU one from CI's own `tidy-ratchet-cpu` artifact. | Proposed | ci, clang-tidy, lint, ratchet, fork-local |
| [ADR-1214](1214-float-adm-csf-scale-watson-mode-and-aliases.md) | The float-ADM GPU twins ignore `adm_csf_scale` in Watson mode and share the CPU's option aliases | Proposed | cuda, sycl, hip, metal, correctness, feature-extractor, options |
| [ADR-1247](1247-scorecard-exact-head-gates.md) | Bind Scorecard gates to their measured source and scope | Accepted | ci, security, supply-chain |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| [ADR-1214](1214-float-adm-csf-scale-watson-mode-and-aliases.md) | The float-ADM GPU twins ignore `adm_csf_scale` in Watson mode and share the CPU's option aliases | Proposed | cuda, sycl, hip, metal, correctness, feature-extractor, options |
1 change: 1 addition & 0 deletions docs/adr/_index_fragments/_order.txt
Original file line number Diff line number Diff line change
Expand Up @@ -990,3 +990,4 @@
1262-cli-input-read-error-exit-code
1257-retire-darwin-adm-dwt2-legacy-dispatch
1265-clang-tidy-header-filter-absolute-paths
1214-float-adm-csf-scale-watson-mode-and-aliases
3 changes: 2 additions & 1 deletion docs/adr/by-tag/correctness.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Auto-generated by `scripts/docs/generate-adr-by-tag.sh`. Edit ADR `Tags:` lines to update.

87 ADR(s) carry this tag.
88 ADR(s) carry this tag.

| ID | Title |
|----|-------|
Expand Down Expand Up @@ -86,6 +86,7 @@ Auto-generated by `scripts/docs/generate-adr-by-tag.sh`. Edit ADR `Tags:` lines
| [ADR-1211](../1211-hip-integer-adm-picture-staging.md) | `integer_adm_hip` stages the luma plane onto the device before launching |
| [ADR-1212](../1212-gpu-moment-bit-depth-normalisation.md) | The GPU `float_moment` twins normalise by the bit-depth scaler on the host |
| [ADR-1213](../1213-hip-ciede-chroma-ceil-dimensions.md) | `ciede_hip` sizes its chroma staging with the picture's ceil dimensions |
| [ADR-1214](../1214-float-adm-csf-scale-watson-mode-and-aliases.md) | The float-ADM GPU twins ignore `adm_csf_scale` in Watson mode and share the CPU's option aliases |
| [ADR-1215](../1215-cuda-psnr-16bpc-plane-argument.md) | The 16-bpc CUDA PSNR kernel takes the plane index the host has always passed |
| [ADR-1216](../1216-gpu-motion3-fps-weight-applied-once.md) | The GPU motion3 twins apply `motion_fps_weight` exactly once |
| [ADR-1217](../1217-gpu-float-vif-options-reach-kernel.md) | The GPU float-VIF kernels read `vif_sigma_nsq` and `vif_enhn_gain_limit` from their options |
Expand Down
Loading
Loading