Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions changelog.d/fixed/0498-vmaf-tune-adr-0498-followup7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
- `vmaf-tune encode` x264/x265/libvpx-vp9 encoder capability is now
detected from the `ffmpeg -version` configure summary (`--enable-*`
flags) and surfaced through the new `EncoderInfo.codec_detected` bool
field returned by `probe_encoder_info`. The existing `libx264` and
`libsvtav1` patterns are joined by `libx265` and `libvpx-vp9`
(ADR-0498 follow-up #7).
- `build_pass1_stats_command` had a duplicate `fallback_duration`
assignment (dead first write left by the #1266 refactor). The
duplicate is removed; the duration-s clamp still works correctly
(ADR-0498 follow-up #7, Bug #V8-A cleanup).
- `vmaf-tune fast` TPE proxy-encode trials now score on the same GPU
backend as the mandatory verify pass. Previously all 30 TPE probe
scores ran on CPU even when a GPU backend was available; the backend
selected by `score_backend.select_backend` is now forwarded to
`_build_production_sample_extractor` (ADR-0498 follow-up #7).
- `codec_adapters.parse_available_codecs` parses `ffmpeg -hide_banner
-encoders` output into a `frozenset` of available codec names.
Callers can gate stats-capture or hardware-path logic on runtime
codec availability rather than compile-time assumptions
(ADR-0498 follow-up #7).
22 changes: 22 additions & 0 deletions docs/rebase-notes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
<!-- markdownlint-disable MD001 MD003 MD004 MD007 MD013 MD018 MD022 MD024 MD025 MD026 MD028 MD029 MD031 MD032 MD033 MD036 MD037 MD038 MD040 MD041 MD046 MD049 MD050 MD051 MD052 MD053 MD055 MD056 MD058 MD059 -->
# Rebase notes

## feat(vmaf-tune): ADR-0498 follow-up #7 — encoder stats, x264 detection, backend dispatch, codec-list parser

**Files touched:**
`tools/vmaf-tune/src/vmaftune/encode.py`,
`tools/vmaf-tune/src/vmaftune/fast.py`,
`tools/vmaf-tune/src/vmaftune/codec_adapters/__init__.py`,
`tools/vmaf-tune/tests/test_encode_dispatcher_per_adapter.py`,
`tools/vmaf-tune/tests/test_adr_0498_followup7.py`

**Rebase impact:** None. All changed files are fork-local to
`tools/vmaf-tune/`; no upstream Netflix/vmaf files are touched.
The `_VERSION_PROBE_PATTERNS` dict is additive (new keys only).
The `parse_available_codecs` function is new; no existing symbol
is renamed or removed. The `_build_production_sample_extractor`
signature change (new `backend=None` kwarg) is backward-compatible.
The `test_encode_dispatcher_per_adapter.py` fix (capture first call
only) resolves a test fragility introduced by the probe-cache
expansion; no merge conflict expected against Netflix upstream
since that test is fork-added.

---

## fix/cuda-duplicate-csf-r-definitions (2026-06-03)

**Files touched:**
Expand Down
20 changes: 20 additions & 0 deletions tools/vmaf-tune/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,26 @@ scripts for those local corpora.
`"<encoder>-enabled"` when the encoder is compiled in; an empty
string lets the caller keep its `"unknown"` placeholder so
existing tests that pin that exact value still pass.
`_VERSION_PROBE_PATTERNS` now covers `libx264`, `libsvtav1`,
`libx265`, and `libvpx-vp9`; tests for any of these codecs that
use a fake runner and don't return `--enable-*` text in stdout
must capture only the first subprocess call (the encode argv),
not the last, since the probe fires a second `ffmpeg -version`
call when the encoder banner is absent from the encode stderr.
`encode.probe_encoder_info(ffmpeg_bin, encoder)` returns
`EncoderInfo(encoder, codec_detected, version_label)` — callers
should use this rather than re-parsing the version string.
- **`fast._build_production_sample_extractor` accepts a `backend` kwarg
(ADR-0498 follow-up #7).** Pass `backend=select_backend(...)` so
TPE proxy trials score on GPU rather than always defaulting to CPU.
`_build_prod_predictor` and `fast_recommend` forward the selected
backend automatically; test seams that inject a custom
`sample_extractor` callable are unaffected.
- **`codec_adapters.parse_available_codecs(stdout, *, restrict_to_known)`
(ADR-0498 follow-up #7).** Parses `ffmpeg -hide_banner -encoders`
output into a frozenset of codec names. Set `restrict_to_known=False`
to get the full ffmpeg encoder list; the default restricts to the
adapter registry so callers can intersect with `known_codecs()`.
- **`_maybe_decode_reference` scales the reference YUV to the rung
target on cross-resolution sweeps (ADR-0501, Bug #V4-B).** When
`CorpusJob.src_width / src_height` differs from `width / height`,
Expand Down
77 changes: 71 additions & 6 deletions tools/vmaf-tune/src/vmaftune/codec_adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,14 @@ class CodecAdapter(Protocol):
# ADR-0332: opt-in to the pass-1 stats-file capture path. True
# iff the encoder writes a parseable per-frame stats file under
# ``-pass 1 -passlogfile <prefix>``. Software encoders that
# share x264-family rate-distortion tracking (libx264, libx265,
# libvpx) set True; hardware encoders (NVENC / AMF / QSV /
# VideoToolbox) and any encoder without a stats-file surface
# set False. v1 of the parser only handles x264's text format;
# libx265 / libvpx flip the flag but their format-specific
# parser arrives in a follow-up PR.
# share x264-family rate-distortion tracking (libx264, libx265)
# set True; hardware encoders (NVENC / AMF / QSV / VideoToolbox)
# and any encoder without a text stats-file surface set False.
# libvpx-vp9 uses a binary packet layout (``vpx_codec_pkt_t``
# / ``VPX_CODEC_STATS_PKT``) and sets False until a binary
# packet parser is contributed. The ``encoder_stats`` module
# normalises both x264 and x265 text formats via their field
# aliases (``q-aq``, ``icu``, ``pcu``, ``scu``).
supports_encoder_stats: bool

# Phase F (ADR-0333). Adapters that opt into 2-pass encoding set
Expand Down Expand Up @@ -175,6 +177,68 @@ def known_codecs() -> tuple[str, ...]:
return tuple(sorted(_REGISTRY))


def parse_available_codecs(
ffmpeg_encoders_stdout: str,
*,
restrict_to_known: bool = True,
) -> frozenset[str]:
"""Parse the output of ``ffmpeg -hide_banner -encoders`` into a frozenset.

ADR-0498 follow-up #7: this is the codec-list parser that was deferred
from the initial ``codec_adapters`` scaffolding (line 97: "codec-list
parser arrives in a follow-up"). The parser turns the encoder table
into a set of available codec names so callers can gate
``supports_encoder_stats`` capture and other codec-specific paths on
runtime availability rather than compile-time assumptions.

Each non-header line in ``ffmpeg -encoders`` output has the form::

" V..... libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"

where the first column is a capability-flags string (``V`` for video,
``.`` for unset flags) and the second is the encoder name. Header
lines (``"Encoders:"``, ``" ------"``, blank) are skipped.

Parameters
----------
ffmpeg_encoders_stdout:
Raw stdout from ``ffmpeg -hide_banner -encoders``.
restrict_to_known:
When ``True`` (the default) only names that appear in the
codec-adapter registry (:func:`known_codecs`) are returned.
Set to ``False`` to get the full set reported by ffmpeg.

Returns
-------
frozenset[str]
Codec names available in this ffmpeg build.

Examples
--------
>>> out = subprocess.check_output(["ffmpeg", "-hide_banner", "-encoders"], text=True)
>>> available = parse_available_codecs(out)
>>> "libx264" in available
True
"""
found: set[str] = set()
for raw in ffmpeg_encoders_stdout.splitlines():
line = raw.strip()
if not line or "------" in line or line.startswith("Encoders"):
continue
tokens = line.split()
# Encoder lines: first token is capability flags (e.g. ``V.....``),
# second is the encoder name. Skip non-encoder header lines.
if len(tokens) < 2:
continue
flags, name = tokens[0], tokens[1]
if len(flags) >= 1 and flags[0] in ("V", "A", "S"):
found.add(name)
Comment on lines +233 to +235
if restrict_to_known:
known = set(known_codecs())
return frozenset(found & known)
return frozenset(found)


__all__ = [
"AV1AMFAdapter",
"Av1NvencAdapter",
Expand All @@ -201,4 +265,5 @@ def known_codecs() -> tuple[str, ...]:
"X265Adapter",
"get_adapter",
"known_codecs",
"parse_available_codecs",
]
82 changes: 63 additions & 19 deletions tools/vmaf-tune/src/vmaftune/encode.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,34 +422,59 @@ def _tail(text: str, n: int) -> str:
# fresh probe. ADR-0498 follow-up #7 (BBB e2e v2).
_PROBE_CACHE: dict[tuple[str, str], str] = {}

# Map encoder names to (regex, prefix) for parsing the configure-line
# / banner that ``ffmpeg -version`` prints. The configure line typically
# looks like::
# Map encoder names to configure-line patterns for parsing the
# ``ffmpeg -version`` output. The configure summary looks like::
#
# configuration: --prefix=/usr ... --enable-libx264 --enable-libsvtav1 ...
#
# which carries no version. The libavcodec banner that follows::
#
# libavcodec 60. 31.102 / 60. 31.102
#
# also carries no encoder version. For libx264 / libsvtav1 the version
# is in the per-encoder banner that ffmpeg dumps on init; when that
# banner is suppressed we settle for an "enabled" marker so consumers
# at least know the encoder was compiled in.
# which carries no per-encoder version. The libavcodec banner that
# follows also carries no encoder version. For these codecs we settle
# for an ``"<encoder>-enabled"`` marker so consumers at least know the
# encoder was compiled in. ADR-0498 follow-up #7 extends this set to
# cover x265 and libvpx so the ``EncoderInfo.codec_detected`` field is
# populated for all three software encoder families.
_VERSION_PROBE_PATTERNS: dict[str, re.Pattern] = {
"libx264": re.compile(r"--enable-libx264"),
"libsvtav1": re.compile(r"--enable-libsvtav1"),
"libx265": re.compile(r"--enable-libx265"),
"libvpx-vp9": re.compile(r"--enable-libvpx"),
}


@dataclasses.dataclass(frozen=True)
class EncoderInfo:
"""Structured encoder availability record from ``ffmpeg -version``.

``encoder`` is the FFmpeg codec name (e.g. ``libx264``).
``codec_detected`` is ``True`` when the configure summary confirms
the codec was compiled into the ffmpeg binary (``--enable-<codec>``
present in the ``configuration:`` line). ``version_label`` carries
the human-readable token returned by
:func:`_probe_encoder_version_from_ffmpeg` (e.g. ``"libx264-enabled"``).

ADR-0498 follow-up #7: this dataclass replaces the bare string
return from the probe so callers can gate codec-stats capture and
report generation on ``codec_detected`` without re-parsing the
version string.
"""

encoder: str
codec_detected: bool
version_label: str


def _probe_encoder_version_from_ffmpeg(ffmpeg_bin: str, encoder: str, runner_fn: object) -> str:
"""Return a best-effort version label, or ``""`` when nothing parseable.

The CLI returns a short stable label (``libx264-enabled`` /
``libsvtav1-enabled``) when ``ffmpeg -version``'s configuration
line confirms the encoder is compiled in. Empty string keeps the
caller's previous ``"unknown"`` placeholder so existing tests that
pin that exact value still pass.
The function returns a short stable label (``libx264-enabled`` /
``libsvtav1-enabled`` / ``libx265-enabled`` / ``libvpx-vp9-enabled``)
when ``ffmpeg -version``'s configuration line confirms the encoder is
compiled in. Empty string keeps the caller's previous ``"unknown"``
placeholder so existing tests that pin that exact value still pass.

See also :func:`probe_encoder_info` for a structured
:class:`EncoderInfo` return when callers need the ``codec_detected``
boolean without reparsing the label string.
"""
pattern = _VERSION_PROBE_PATTERNS.get(encoder)
if pattern is None:
Expand All @@ -473,6 +498,28 @@ def _probe_encoder_version_from_ffmpeg(ffmpeg_bin: str, encoder: str, runner_fn:
return label


def probe_encoder_info(
ffmpeg_bin: str, encoder: str, runner_fn: object | None = None
) -> EncoderInfo:
"""Return structured encoder availability info from ``ffmpeg -version``.

Wraps :func:`_probe_encoder_version_from_ffmpeg` and returns an
:class:`EncoderInfo` with ``codec_detected = True`` when the
configure summary confirms the encoder is compiled in. Returns
``codec_detected = False`` for unknown encoders (not in
``_VERSION_PROBE_PATTERNS``) or when the configure line does not
include the ``--enable-<codec>`` flag.

ADR-0498 follow-up #7: callers that previously checked
``version_label != "unknown"`` can now use the boolean
``codec_detected`` field directly.
"""
_runner = runner_fn if runner_fn is not None else subprocess.run
label = _probe_encoder_version_from_ffmpeg(ffmpeg_bin, encoder, _runner)
detected = bool(label)
return EncoderInfo(encoder=encoder, codec_detected=detected, version_label=label or "unknown")


def build_pass1_stats_command(
req: EncodeRequest, stats_prefix: Path, ffmpeg_bin: str = "ffmpeg"
) -> list[str]:
Expand Down Expand Up @@ -503,9 +550,6 @@ def build_pass1_stats_command(
float(req.duration_s) if req.sample_clip_seconds <= 0.0 and req.duration_s > 0.0 else 0.0
)
cmd = [ffmpeg_bin, "-y", "-hide_banner", "-loglevel", "info"]
fallback_duration = (
float(req.duration_s) if req.sample_clip_seconds <= 0.0 and req.duration_s > 0.0 else 0.0
)
if req.source_is_container:
if req.sample_clip_seconds > 0.0:
cmd.extend(["-ss", f"{req.sample_clip_start_s}"])
Expand Down
43 changes: 33 additions & 10 deletions tools/vmaf-tune/src/vmaftune/fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,20 +200,23 @@ def _build_prod_predictor(
encoder: str,
crf_range: tuple[int, int],
sample_extractor: Callable[[Path, int, str], tuple[list[float], float]] | None,
backend: str | None = None,
) -> Callable[[int], TrialSample]:
"""Construct a CRF→TrialSample predictor backed by the v2 proxy.

``sample_extractor`` is the seam Phase B/C share for "encode a short
chunk + extract canonical-6 + observe bitrate". Tests inject a fake;
production callers leave it default and the harness builds it from
the existing :mod:`vmaftune.encode` + libvmaf feature pipeline. When
``sample_extractor`` is ``None`` we raise — the production-loop
encode-extract integration ships in a same-PR follow-up that wires
the existing :mod:`vmaftune.score_backend` GPU path; until then the
test-injection path is the only callable seam.
the existing :mod:`vmaftune.encode` + libvmaf feature pipeline.

``backend`` is forwarded to :func:`_build_production_sample_extractor`
so each TPE trial scores its probe clip on the same GPU backend used
by the verify pass. ADR-0498 follow-up #7: wires the
:mod:`vmaftune.score_backend` GPU path through to the proxy-encode
extractor so all 30 TPE trial scores run on GPU when available.
"""
if sample_extractor is None:
sample_extractor = _build_production_sample_extractor()
sample_extractor = _build_production_sample_extractor(backend=backend)

crf_lo, crf_hi = crf_range
crf_span = max(crf_hi - crf_lo, 1)
Expand Down Expand Up @@ -283,8 +286,7 @@ def _gpu_verify(
if encode_runner is None:
encode_runner = _build_production_encode_runner()

backend = score_backend_select(prefer="auto") # advisory; runner consumes
_ = backend # kept for the diagnostic hook a follow-up adds
backend = score_backend_select(prefer="auto")
_kbps, vmaf = encode_runner(src, encoder, crf, backend)
return float(vmaf)

Expand Down Expand Up @@ -341,6 +343,7 @@ def _build_production_sample_extractor(
vmaf_bin: str = "vmaf",
pix_fmt: str = "yuv420p",
preset: str = "medium",
backend: str | None = None,
) -> Callable[[Path, int, str], tuple[list[float], float]]:
"""Return a ``(src, crf, encoder) → (canonical_6, kbps)`` callable.

Expand All @@ -352,6 +355,14 @@ def _build_production_sample_extractor(
libvmaf CLI to extract the canonical-6 feature means.
4. Returns ``(canonical_6_features, observed_kbps)``.

``backend`` selects the libvmaf scoring backend (``cpu`` / ``cuda``
/ ``sycl`` / ``hip`` / ``auto``). When ``None`` or ``"auto"`` the
libvmaf CLI picks the fastest available backend. ADR-0498 follow-up
#7: previously the sample extractor ignored the backend selected by
:func:`vmaftune.score_backend.select_backend` so all 30 TPE trials
scored on CPU even when a GPU was available; the ``backend`` kwarg
wires the GPU path through to each probe-encode score call.

The returned callable is stateless: parallel TPE trials can call it
concurrently (each gets its own tempdir).
"""
Expand All @@ -364,6 +375,7 @@ class _Cfg:
ffprobe_bin: str = "ffprobe"

cfg = _Cfg()
_score_backend: str | None = None if (backend is None or backend == "auto") else backend

def _extract(src: Path, crf: int, encoder: str) -> tuple[list[float], float]:
with tempfile.TemporaryDirectory(prefix="vmaftune-fast-sample-") as td:
Expand Down Expand Up @@ -411,7 +423,7 @@ def _extract(src: Path, crf: int, encoder: str) -> tuple[list[float], float]:
),
frame_cnt=int(duration_s * fps),
)
score_result = run_score(score_req, vmaf_bin=vmaf_bin)
score_result = run_score(score_req, vmaf_bin=vmaf_bin, backend=_score_backend)
if score_result.exit_status != 0:
raise RuntimeError(
f"fast sample_extractor: score failed: {score_result.stderr_tail[-300:]}"
Expand Down Expand Up @@ -620,14 +632,25 @@ def fast_recommend(
"Use smoke=True for the synthetic pipeline."
)

# Select the scoring backend once; forward it to both the TPE proxy
# extractor and the GPU verify pass so all scoring — proxy trials +
# the final verify encode — uses the same backend. ADR-0498 follow-
# up #7: previously the sample extractor always defaulted to CPU
# even when a GPU was available.
from vmaftune.score_backend import select_backend as _select_backend # noqa: PLC0415

_prod_backend = _select_backend(prefer="auto")

if predictor is None:
# Build the v2-proxy-backed predictor from the production
# encode-extract sample seam.
# encode-extract sample seam, forwarding the selected backend
# so each TPE trial scores on GPU when available.
predictor = _build_prod_predictor(
src=src,
encoder=encoder,
crf_range=crf_range,
sample_extractor=sample_extractor,
backend=_prod_backend,
)
Comment on lines 652 to 654

recommended_crf, predicted_vmaf, predicted_kbps, completed_trials = _run_tpe(
Expand Down
Loading
Loading