ADR-1359: The CLI maps --feature <cpu-name> to the explicit --backend's twin¶
- Status: Accepted
- Date: 2026-09-29
- Deciders: Lusoris
- Tags: cli, api, gpu, dispatch, output-schema, fork-local
Context¶
vmaf --backend sycl --feature ciede initialised the SYCL device and then computed ciede2000 with the CPU ciede extractor. register_cli_feature() in core/tools/vmaf.cpp passed the name to vmaf_use_feature(), which selects an extractor by exact name. Only a model's features reach a device twin: vmaf_use_features_from_model() looks each feature up with vmaf_get_feature_extractor_by_feature_name(name, fex_flags), keeps the twin only when it honours the model's options (ADR-1183, ADR-1316), and swaps in the CPU extractor at the first picture when the twin cannot run its geometry (ADR-1324). On a 4K 8-bit 4:2:0 clip, --feature ciede --backend sycl took 1714 ms per frame on an Arc B580, the same as the serial CPU extractor, against 8.4 ms for --feature ciede_sycl (Research-2120). The JSON still said "backend_used": "sycl", because the CLI reported the backend it had initialised, not the one that computed anything. Any per-feature GPU benchmark written as --feature <cpu-name> --backend <gpu> measured the CPU (T-CLI-FEATURE-NAME-BYPASSES-GPU-BACKEND-2026-09-29).
Three places could resolve the name: the CLI, vmaf_use_feature() (which also changes the C API and the FFmpeg filters), or nowhere, with only the receipt corrected. The maintainer chose the CLI.
The CLI cannot do the lookup on its own. It links the shared libvmaf, which is built with -fvisibility=hidden (ADR-0379), so the extractor registry, vmaf_get_feature_extractor_by_feature_name() and vmaf_feature_extractor_honours_options() are not reachable from it. Twin names follow no pattern the CLI could reconstruct: the twin of float_ssim is float_ssim_cuda, of ssim integer_ssim_cuda, of motion motion_sycl, of ciede on Metal integer_ciede_metal.
Decision¶
With an explicit device --backend (cuda, sycl, hip or metal), the CLI resolves every --feature <name>[=options] whose name is a CPU extractor through a new, additive libvmaf entry point, vmaf_feature_backend_twin(), and registers what it returns.
- Pairing is the model-dispatch lookup, not a second table. The CPU extractor's provided features are looked up in order with
vmaf_get_feature_extractor_by_feature_name()under the context's extractor flags (compute_fex_flags(), so a non-zerogpumaskdisables it as it does for models); the first result that carries the backend's flag is the twin (vmaf_get_feature_extractor_twin()infeature_extractor.cpp). - Acceptance is the model-dispatch fallback applied up front. The twin must honour every option in the
--featurestring (ADR-1183 / ADR-1316) and, for the input's width, height, bit depth and pixel format, pass the ADR-1324 first-picture check, which runs on a scratch context that is never initialised. - Fallback: when there is no twin, the twin cannot honour an option or the geometry, or the backend's extractors are disabled by
--gpumask, the CLI registers the CPU extractor and prints one line to stderr that names the feature and the reason, for examplevmaf: warning: --feature brisque: the sycl backend has no twin of this extractor; computing it on the CPU. The run continues; nothing fails that worked before. - Unchanged: a twin-suffixed name (
--feature ciede_sycl) keeps its exact registration and its ADR-0543 exit-100 gate;--backend cpu,--backend autoand no--backendkeep exact-name registration and never call the lookup;vmaf_use_feature()and the FFmpeg filters keep selecting by exact name. - Receipt:
backend_usednames the backend the registered extractors ran on, read after the final flush through a second additive entry point,vmaf_registered_feature_extractor(). It is the device backend of the first extractor that ran on a device, orcpuwhen every extractor ran on the CPU, including a run where a device was initialised and nothing was dispatched to it. Its value set is unchanged (cpu,cuda,sycl,hip,metal). A new top-level array,feature_backends, lists every registered extractor in registration order as{"extractor": <registry name>, "backend": <backend>}. A run that mixes device twins and CPU extractors is one whosebackend_usednames a device and whosefeature_backendsholds at least onecpuentry. The receipt is written for every JSON output, whatever--backendsays.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Resolve in the CLI through a libvmaf query (chosen) | Fixes the CLI foot-gun; one pairing implementation shared with model dispatch; vmaf_use_feature() and FFmpeg unchanged | Two new exported symbols | Maintainer decision; the smallest change that gives the CLI the model-dispatch answer |
Resolve inside vmaf_use_feature() | Every caller, FFmpeg included, gets the twin | Changes the documented exact-name contract of a public function and the FFmpeg filters' behaviour; needs the FFmpeg patch series updated | Maintainer chose CLI-only |
Keep exact names, only fix backend_used | No routing change | The foot-gun stays: --feature ciede --backend sycl still measures the CPU | Does not fix the reported problem |
CLI-side <name>_<backend> name table | No library change | A second pairing implementation (HISS-19); names do not follow the pattern (ssim → integer_ssim_cuda, ciede → integer_ciede_metal); the option and geometry gates are unreachable from the CLI | Wrong for several extractors and cannot apply ADR-1183 |
Export vmaf_get_feature_extractor_by_feature_name() and the option helpers | Reuses them verbatim | Puts the internal VmafFeatureExtractor and VmafDictionary layouts into the ABI | Undoes ADR-0379 for three internal symbols |
| Fail with exit 100 when a feature has no twin | Strictest; no silent CPU work | Breaks invocations that work today (--backend sycl --feature brisque with a model); CPU-only extractors would need a separate run | The warning plus the feature_backends receipt already make the CPU work visible |
Report a mixed run as "backend_used": "mixed" | One field to read | Adds a value that consumers comparing against a backend name (dev/scripts/smoke-probe-loop.sh, the RC1 tester report, both MCP servers) do not expect | HISS-14: a new field instead of a new value |
| Require the twin to provide every feature the CPU extractor lists | Output columns identical to the CPU run | provided_features lists are not emission contracts; it would reject the ADM, float ADM, float motion and MS-SSIM twins that model dispatch uses every run | Rejects working twins |
Consequences¶
- Positive:
--feature ciede --backend syclrunsciede_sycl, with the same scores as--feature ciede_sycl. A benchmark written with CPU names now measures the device it names, or says on stderr and in the receipt that it did not.feature_backendsis the per-extractor dispatch evidence that ADR-1342 point 11 saysbackend_usedalone cannot give. - Negative: The twin's scores differ from the CPU extractor's within the cross-backend tolerances of ADR-0214, so the same command line now gives slightly different numbers with an explicit device backend. A twin emits the metrics it implements, so auxiliary columns can differ: with 576x324 input the CPU
motionextractor reportsVMAF_integer_feature_motion_sad_score,integer_motion2andinteger_motion3, andmotion_syclreportsinteger_motion,integer_motion2andinteger_motion3.--backend cpurestores the CPU output.backend_usednow readscpufor a run that initialised a device but ran nothing on it, where it used to name the device. - Neutral / follow-ups: The receipt's pairing is verified on a SYCL device here; CUDA, HIP and Metal use the same lookup and are covered by the device-free unit tests and the
test_vmaf_feature_backend_<backend>device runs on hosts that have those GPUs.
References¶
- Popup, 2026-09-29: "CLI picks the twin (Recommended)" (maintainer answer to where
--feature <cpu-name>with an explicit GPU--backendshould be resolved to that backend's twin). - Research-2120 (the measurement that found the bypass) and Research-2121 (the pairing and receipt evidence for this decision).
docs/state.mdrowT-CLI-FEATURE-NAME-BYPASSES-GPU-BACKEND-2026-09-29.- ADR-0379, ADR-0498, ADR-0530, ADR-0543, ADR-0804, ADR-1183, ADR-1316, ADR-1324.