ADR-1417: Integer AIM stays the unclipped ratio upstream defines; float AIM stays clipped at 1¶
- Status: Accepted
- Date: 2026-10-01
- Deciders: lusoris
- Tags: adm, aim, numerics, netflix-compat, docs, fork-local
Context¶
While the integer ADM masking threshold was being fixed (ADR-1402), a flat grey 64x64 reference against the same picture with isolated patches scored integer_aim 3.1755853876204676 where float_adm reports an aim of exactly 1, before and after that fix. AIM is documented as lying in [0, 1], so the question was whether the integer extractor is wrong on such content (T-ADM-INTEGER-AIM-ABOVE-ONE-2026-10-01).
AIM is the additive impairment that survives contrast masking, divided by the DLM denominator, which measures the reference's own detail. The two pipelines were compared stage by stage on that picture (scalar path, --precision max):
| Scale | Integer AIM numerator | Float AIM numerator | Denominator (both) |
|---|---|---|---|
| 0 | 12.072382 | 12.0720234 | 8.71317863 |
| 1 | 17.7911568 | 17.7916965 | 5.48895884 |
| 2 | 25.354557 | 25.3548603 | 3.77976322 |
| 3 | 9.44635677 | 9.44600391 | 2.38110161 |
| Sum | 64.66445255279541 | 64.664584159851074 | 20.363002300262451 |
The numerators agree to four significant digits (fixed point against float) and the denominator is the same number: with a flat reference it is the noise floor term alone. The ratios are 3.17558539 and 3.17559185. The only difference is the last line of each extractor, and both lines are upstream's (Netflix/vmaf 6ec23e8f2):
/* libvmaf/src/feature/integer_adm.c:3006-3007 */
// normalize AIM score by the DLM denominator
*score_aim = aim_num / den;
/* libvmaf/src/feature/adm.c:322-323 */
// normalize AIM score by the DLM denominator and clip values larger than 1
*score_aim = MIN(aim_num / aim_den, 1.0f);
Upstream master, built from that commit, prints integer_aim 3.175585 and aim 1.000000 for the same two files. The fork mirrors both (vmaf_adm_scale_ratios() for the integer extractor, vmaf_adm_finalize_scores() for the float one, in core/src/feature/adm_score.h).
The value reaches a model. adm3 is max(w * adm2 + (1 - w) * (1 - aim), adm_min_val), and the shipped vmaf_v1.0.16 models read VMAF_integer_feature_adm3_score with w = 0.7 and adm_min_val = 0.5. Measured at 576x324 with the default model vmaf_v1.0.16_3d0h, the fork against upstream master running the same model file, and against a scratch build of the fork with the clip added:
| Flat grey reference against | Integer AIM (fork = upstream) | adm3, unclipped (fork = upstream) | adm3 with a clip | VMAF, fork | VMAF, upstream | VMAF with a clip |
|---|---|---|---|---|---|---|
| isolated patches every 16 px | 2.623164 | 0.5 | 0.7 | 0 | 0.0 | 0 |
| uniform noise of +/-24 | 1.328354 | 0.601494 | 0.7 | 0 | 0.0 | 0 |
| uniform noise of +/-16 | 0.896739 | 0.730978 | 0.730978 | 5.766443 | 5.766443 | 5.766443 |
| uniform noise of +/-12 | 0.681863 | 0.795441 | 0.795441 | 24.880717 | 24.880717 | 24.880717 |
So the clip changes the model's ADM input once AIM passes 1, and on these pictures the model's score has already reached 0 by then. No picture with an integer AIM above 1 and a non-zero default-model score was found; none was ruled out either.
Decision¶
The integer extractor keeps the unclipped ratio and the float extractor keeps its clip. The fork does not unify them. The difference is documented in docs/metrics/features.md, recorded as an upstream inconsistency in docs/development/known-upstream-bugs.md, and pinned by test_integer_adm_aim_unclipped, which fails if either extractor changes sides.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Clip integer AIM at 1, as the float extractor does | One documented range for both extractors; adm3 could not fall below w * adm2; the default model's score did not move on any picture tried | Changes VMAF_integer_feature_aim_score and VMAF_integer_feature_adm3_score away from upstream libvmaf's on every frame with an integer AIM above 1 (adm3 0.5 to 0.7 on the patch picture), for the shipped vmaf_v1.0.16 models and for anyone reading the features; no Netflix golden assertion covers the case, so nothing would flag a later drift; whether the models were fitted to the clipped or the unclipped feature is not known here | The integer extractor is what upstream ships and what the models consume; the fork follows it until upstream changes it |
| Remove the clip from the float extractor | Also one behaviour for both | Departs from upstream's float reference; float aim and adm3 would change for every user of float_adm | Same reason, in the other direction |
| Leave the documentation saying [0, 1] for both | No change | The range is wrong for the integer extractor, and the next reader runs the same investigation | The documented range was the defect |
| Add an option that selects the clip | Lets a user choose | A new knob on a model feature that no model would set; a second way to compute a score upstream computes one way | No user asked for it |
Consequences¶
- Positive:
- The documented range of
aim_scoreand the definition ofadm3_scoreare correct for both extractors. - A test pins the integer value against upstream's printed 3.175585 and the float clip at 1, so a change to either is deliberate.
- Negative:
admandfloat_admkeep disagreeing on content whose additive impairment exceeds the reference's own detail: flat or very smooth references with added noise, grain, dither or isolated artefacts. Theiradm3disagrees there too.- Neutral / follow-ups:
- If upstream adds the clip to
integer_adm.c, port it and re-run the golden gate;test_integer_adm_aim_unclippedthen needs its integer expectations changed in the same PR. - The CUDA and SYCL twins of the integer extractor return the CPU's value on the patch picture (identical at
--precision max); the HIP twin has no AIM pass; the Metal twin could not be run. - When the denominator is exactly 0, upstream leaves
score_aimuninitialised in both extractors; the fork reports 1 for a 0 / 0 ratio and fails the frame for a non-zero numerator over 0 (unchanged here).
References¶
- Task brief (rc3 worker assignment, 2026-10-01): "integer_aim is 3.18 on the 64x64 flat-plus-patches picture [...] where float_adm's aim is 1, before and after #1700. Determine whether integer aim is wrong there [...] or whether the two legitimately differ on that content. [...] If not: document why in docs/metrics and close it with evidence."
- Netflix/vmaf
6ec23e8f2:libvmaf/src/feature/integer_adm.c:3007,libvmaf/src/feature/adm.c:323; the clip enteredadm.cwith4dcc2f7ce(2026-04-27), ten days after966be8d59(2026-04-17) gave the integer extractor its AIM. - Research-1417: the stage numbers, the upstream build, the device twins.
- ADR-1362 (the AIM pass and its bit-exact contract), ADR-0746, ADR-1402, ADR-0024.
docs/state.md:T-ADM-INTEGER-AIM-ABOVE-ONE-2026-10-01.