Skip to content

vmafx-node: worker node image

vmafx-node is the VMAFX worker binary and its container image. A node pointed at a controller (VMAFX_CONTROLLER_ADDR) pulls scoring jobs from its queue, scores them and reports the scores back; every node also serves direct VmafxScoring calls. The image carries ffmpeg for the encoder probe. This page shows how to build and check the image first, then covers the ffmpeg setup, codec matrix and environment variables.

See ADR-0709 (Phase 4b umbrella) and ADR-0717 (ffmpeg version policy) for the decision record.

Quick start

# Build the CPU-only node image
docker buildx build --target node-cpu \
  -f docker/Dockerfile.node \
  -t vmafx-node:dev .

# Verify ffmpeg version
docker run --rm --entrypoint /usr/local/bin/ffmpeg \
  vmafx-node:dev -version | head -1
# → ffmpeg version n9.0.2 ...

# Verify codec inventory
docker run --rm --entrypoint /usr/local/bin/ffmpeg \
  vmafx-node:dev -encoders \
  | grep -E 'libx264|libx265|libsvtav1|libvpx'

Image variants

Target GPU scoring runtime FFmpeg encoders Use case
node-cpu none software only Development, CI, low-volume workloads
node-cuda NVIDIA (CUDA runtime from the digest-pinned CUDA_RUNTIME image) software only GPU-accelerated VMAF scoring on NVIDIA pools
node-rocm AMD (ROCm 10.0.0) software only GPU-accelerated VMAF scoring on AMD pools
node-sycl Intel (oneAPI runtime 2026.1) software only GPU-accelerated VMAF scoring on Intel Arc / Xe pools

The runtime versions and image digests come from build-config.env (CUDA_RUNTIME, ROCM_VERSION, ONEAPI_VERSION); docker/Dockerfile.node carries mirrored ARG defaults that make base-images-sync keeps in step. Edit the config, not the Dockerfile.

All four variants carry the same ffmpeg binary (built in the shared ffmpeg-builder-cpu stage). The CUDA / ROCm / SYCL variants differ only in the additional GPU runtime libraries copied into the final stage. That shared FFmpeg build does not enable NVENC, AMF, or QSV encoders; the GPU runtimes accelerate VMAF scoring, not video encoding.

ffmpeg version policy

The node image pins ffmpeg to the latest stable tagged release (FFMPEG_TAG=n9.0.2 as of 2026-09-20). The tag is a Docker build argument:

# Override to test against a specific release
docker buildx build --target node-cpu \
  --build-arg FFMPEG_TAG=n9.0.2 \
  -f docker/Dockerfile.node \
  -t vmafx-node:n9.0.2-test .

Update cadence: FFMPEG_TAG is updated in the same PR that bumps ffmpeg-patches/README.md after a patch-series refresh. The dev Containerfile (dev/Containerfile) and the node Dockerfile are updated together so both environments track the same base.

See ADR-0717 for the rationale behind pinning to a tag rather than a rolling release branch.

ffmpeg-patches

The node image applies the fork's full 20-patch series from ffmpeg-patches/ during the ffmpeg-builder-cpu stage. The patches:

  • Carry the fork's libvmaf selector/filter integrations (SYCL, CUDA, HIP and Metal selectors, the SYCL and Metal filters) plus vmaf_pre and libvmaf_tune; the node's shared FFmpeg build enables only CPU libvmaf.
  • Add the vmaf-tune qpfile AVOption to libx264 / libsvtav1.
  • Wire the -pass-autotune and -vmaf-profile CLI glue.
  • Read the pelorus sidedata (0017) and honour the retryable libvmaf close ownership (0020).
  • Patches 0004 and 0006 are no-op compatibility shims for the removed Vulkan backend (ADR-0726, ADR-0860); they stay in the series so later patches apply cleanly.
  • Keep the pinned FFmpeg sources warning-clean under GCC 14 and GCC 16 without removing VVC or another codec and without compiler-warning suppressions.
  • Resolve annotated release tags through the shared peeled-commit checkout helper; direct shallow clones emit dependency-setup warnings in BuildKit.

If a patch fails to apply against a new ffmpeg tag, the build fails at the git am step with a message naming the offending patch. Fix the patch before bumping the tag.

The apply command mirrors the dev container approach:

git am --3way /src/ffmpeg-patches/<patch>.patch

See ffmpeg-patches/README.md for the full verification gate and invariants.

Codec matrix

Codec Direction Library Notes
H.264 encode libx264 ubiquitous SW encoder
H.265 / HEVC encode libx265 SW encoder
VP9 encode libvpx Google VP9 SW encoder
AV1 encode libsvtav1 Intel/Netflix SVT-AV1 (production AV1 lane)
AV1 decode libdav1d Fast AV1 decoder

libaom is excluded: ffmpeg-patches/0007 references aom_roi_map_t fields not present in any released libaom. SVT-AV1 covers the AV1 production lane. See dev/Containerfile §NOTE:libaom for the full rationale.

Encoder startup probe

The vmafx-node binary runs ffmpeg -encoders at startup and caches the result. The inventory is logged at INFO level:

{"level":"INFO","msg":"encoder inventory","count":42,"available":["libx264","libx265",...]}

Missing expected software codecs are logged at WARN:

{"level":"WARN","msg":"expected codec missing from ffmpeg -encoders","codec":"libsvtav1","ffmpeg":"/usr/local/bin/ffmpeg"}

The probe is non-fatal — a node with a degraded codec matrix still starts and accepts jobs; jobs requesting unavailable codecs fail at dispatch time with a clear error message.

Environment variables

Every variable the node reads is in the node's environment table, generated from the platform definition. The node image sets VMAFX_FFMPEG_BIN=/usr/local/bin/ffmpeg, and the node-cuda, node-rocm and node-sycl images set VMAFX_BACKEND to their backend. The image has rclone but no FUSE helper, so VMAFX_STORAGE_MODE=auto resolves to http-serve.

VMAFX_NODE_ADDR was removed (ADR-1119); use VMAFX_GRPC_LISTEN.

Building locally

Build time is about 10 to 15 minutes on a standard developer machine, dominated by the ffmpeg compile. Use --cache-from or the BuildKit layer cache to speed up later builds.

  1. The CPU variant (no GPU SDK required) is the build in the Quick start.

  2. Build the CUDA variant (takes the CUDA runtime from the pinned CUDA_RUNTIME image):

    docker buildx build --target node-cuda \
      -f docker/Dockerfile.node \
      -t vmafx-node:local-cuda .
    

Image build internals

These details matter when you change the Dockerfile.

  • libvmaf stage. It installs both xxd and make. xxd embeds the default models; without it Meson can complete while producing a library with no built-in models. GCC uses make to execute the configured LTO partitions in parallel. The stage preserves the libvmaf.so SONAME link chain and carries Meson's generated libvmaf.pc, whose version is the library interface (3.0.0), not the image or product tag. The root Go-server image uses the same staging contract.
  • Warning scan. Every maintained FFmpeg build runs configure with --fatal-warnings, captures the compiler log and fails if any GCC/Clang/NVCC warning diagnostic remains.
  • Coverage: the root CUDA image, Dockerfile.ffmpeg, the dev container, the node image, the hosted FFmpeg integration matrix and the patch smoke harness.
  • The ordinary hosted matrix applies only patch 0019, which keeps its stock-surface compatibility purpose without compiling the known-warning source.
  • A stable-tag or toolchain update must fix a new warning at its root; it must not weaken the scan, add a suppression or disable the affected codec.
  • Library closure. The image build derives FFmpeg's non-glibc shared-library closure from ldd inside the native build stage. This avoids architecture-specific /usr/lib/x86_64-linux-gnu copies: an arm64 build resolves and stages its aarch64-linux-gnu libraries automatically.

Smoke tests

After building:

# Confirm the node binary carries the release/build version without starting
# its long-running gRPC service
docker run --rm vmafx-node:local --version

# Confirm ffmpeg version
docker run --rm --entrypoint /usr/local/bin/ffmpeg \
  vmafx-node:local -version | head -1

# Confirm codec inventory includes expected software encoders
docker run --rm --entrypoint /usr/local/bin/ffmpeg \
  vmafx-node:local -hide_banner -encoders 2>/dev/null \
  | grep -E 'libx264|libx265|libsvtav1|libvpx'

# Confirm libvmaf and all node runtime libraries resolve
docker run --rm --entrypoint /usr/local/bin/vmaf \
  vmafx-node:local --version

The release workflow runs the node's --version, vmaf --version and ffmpeg -version in the published image without a success-masking fallback. Release builds inject the published tag into pkg/version.version; dev identifies a non-release build.

The full smoke-test sequence from the task brief (including Netflix golden scoring) requires the python/test/resource/yuv/ fixtures to be mounted:

docker run --rm \
  -v "$PWD/python/test/resource:/data:ro" \
  --entrypoint /usr/local/bin/vmaf \
  vmafx-node:local \
    --reference /data/yuv/src01_hrc00_576x324.yuv \
    --distorted  /data/yuv/src01_hrc01_576x324.yuv \
    --width 576 --height 324 \
    --pixel_format 420 --bitdepth 8
# Expected pooled VMAF score with the default model: about 82.82
# (the golden 76.66783 belongs to --model version=vmaf_v0.6.1)

Relationship to dev container

The dev container (dev/Containerfile) also builds ffmpeg (currently n9.0.2). The two builds are intentionally separate:

  • Dev container: full workbench with CUDA toolchain, oneAPI, MCP server, Python environment. Not a delivery artifact.
  • Node image: lean production runtime based on distroless cc-debian13. Ships the vmafx-node binary + ffmpeg + libvmaf only.

When the patch series base is bumped, both the dev container's FFMPEG_TAG ARG and the node Dockerfile's FFMPEG_TAG ARG are updated in the same PR.

See dev-mcp.md for the dev container operator guide.