OpenTranscode/opentranscode/gpu_profiles.py

238 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""GPU capability profiles (v4.8.0) — combined card generations.
Cards within the same hardware-encoder generation are functionally
identical for transcoding, so the dropdown lists CAPABILITY CLASSES,
not individual SKUs: one Pascal entry covers the GTX 10-series, Tesla
P40/P4/P100 and mobile chips; one Turing entry covers RTX 20-series,
GTX 16-series, the Tesla T4 and the crypto-era CMP 30/40/50HX cards.
Oddballs are included with their real capabilities:
- CMP 90HX is GA102-based (Ampere NVENC), but CMP 170HX is GA100-based
and has NO NVENC at all (like A100/V100/H100 compute boards).
- Intel Arc (QSV) and AMD RDNA (VAAPI) cover the rest of the trending
list; RDNA 3 added AV1 encode, RDNA 1/2 and GCN can only encode
H.264/HEVC.
Pure data + pure functions: no I/O, safe to import anywhere.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
# ──────────────────────────────────────────────
# GPU PROFILES
# ──────────────────────────────────────────────
@dataclass(frozen=True)
class GpuProfile:
key: str # stable id (CLI/UI)
label: str # dropdown entry
vendor: str # nvidia | amd | intel | none
api: str # nvenc | vaapi | qsv | none
# codec family → ffmpeg encoder name
encoders: dict[str, str]
# case-insensitive substrings matched against the detected GPU name
# (nvidia-smi / lspci) for auto-detection. First match wins; lists
# are ordered most-specific first.
match: tuple[str, ...] = ()
notes: str = ""
# extra args that must come BEFORE -i (hardware device init)
hw_device_args: tuple[str, ...] = ()
# filter-chain fragment required before the encoder (vaapi hwupload)
filter_tail: tuple[str, ...] = ()
# NVENC encoders by generation class. Quality control: -rc vbr -cq N
# (-b:v 0). 8-bit yuv420p everywhere — Pascal 10-bit HEVC runs at half
# speed and the archival targets here are 8-bit sources.
_NV = {"h264": "h264_nvenc", "hevc": "hevc_nvenc"}
GPU_PROFILES: list[GpuProfile] = [
GpuProfile(
key="nv-kepler-maxwell",
label="NVIDIA Kepler / Maxwell 1.0 (GTX 600/700/800M) — H.264 only",
vendor="nvidia", api="nvenc",
encoders={"h264": "h264_nvenc"},
match=("GTX 6", "GTX 7", "GT 7", "GTX 8", "GT 8", "840M", "860M", "750"),
notes="First NVENC generations: H.264 only, no HEVC.",
),
GpuProfile(
key="nv-pascal",
label="NVIDIA Pascal (GTX 10-series, TITAN Xp, Tesla P40/P4/P100) — H.264 + HEVC 8/10-bit",
vendor="nvidia", api="nvenc",
encoders=dict(_NV),
match=("GTX 10", "1070", "1080", "1060", "1050", "TITAN Xp",
"Tesla P40", "Tesla P4", "P100", "Quadro P"),
notes="Pascal NVENC: HEVC Main/Main10. 10-bit runs at ~half speed.",
),
GpuProfile(
key="nv-turing",
label="NVIDIA Turing (RTX 20-series, GTX 16-series, Tesla T4, CMP 30/40/50HX) — H.264 + HEVC + B-frames",
vendor="nvidia", api="nvenc",
encoders=dict(_NV),
match=("RTX 20", "GTX 16", "2060", "2070", "2080", "1660", "1650",
"Tesla T4", "CMP 30", "CMP 40", "CMP 50"),
notes="Turing NVENC: first gen with HEVC B-frames; big quality jump.",
),
GpuProfile(
key="nv-compute",
label="NVIDIA data-center compute (V100/A100/H100, CMP 170HX) — no NVENC (CPU path)",
vendor="nvidia", api="none",
encoders={},
match=("V100", "A100", "H100", "B200", "GB200", "CMP 170"),
notes="Compute boards ship without NVENC silicon. CMP 170HX is "
"GA100-based — the fastest mining card that cannot hardware-encode.",
),
GpuProfile(
key="nv-ampere",
label="NVIDIA Ampere (RTX 30-series, A10/A40/A2, CMP 90HX) — H.264 + HEVC (no AV1 encode)",
vendor="nvidia", api="nvenc",
encoders=dict(_NV),
match=("RTX 30", "3090", "3080", "3070", "3060", "3050",
"A10", "A40", "CMP 90"),
notes="Ampere added AV1 DECODE but not encode — AV1 stays on CPU.",
),
GpuProfile(
key="nv-ada",
label="NVIDIA Ada / Blackwell (RTX 40/50-series, L4/L40) — H.264 + HEVC + AV1 10-bit",
vendor="nvidia", api="nvenc",
encoders={"h264": "h264_nvenc", "hevc": "hevc_nvenc", "av1": "av1_nvenc"},
match=("RTX 40", "RTX 50", "4090", "4080", "4070", "4060",
"5090", "5080", "5070", "5060", "L4", "L40"),
notes="Ada introduced AV1 NVENC; Blackwell doubles AV1 throughput.",
),
GpuProfile(
key="intel-arc",
label="Intel Arc (Alchemist A-series, Battlemage B-series) — QSV: H.264 + HEVC + AV1",
vendor="intel", api="qsv",
encoders={"h264": "h264_qsv", "hevc": "hevc_qsv", "av1": "av1_qsv"},
match=("Arc A", "Arc B", "A380", "A750", "A770", "B570", "B580"),
notes="Arc media engines encode AV1 8/10-bit — best value encode card.",
),
GpuProfile(
key="intel-xe",
label="Intel Iris / UHD integrated (Gen9–Xe) — QSV: H.264 + HEVC",
vendor="intel", api="qsv",
encoders={"h264": "h264_qsv", "hevc": "hevc_qsv"},
match=("Iris", "UHD", "HD Graphics"),
notes="Integrated media engines; HEVC 8/10-bit, no AV1 encode.",
),
GpuProfile(
key="amd-rdna3",
label="AMD RDNA 3 (RX 7000-series) — VAAPI: H.264 + HEVC + AV1",
vendor="amd", api="vaapi",
encoders={"h264": "h264_vaapi", "hevc": "hevc_vaapi", "av1": "av1_vaapi"},
match=("RX 7", "7900", "7800", "7700", "7600"),
notes="RDNA 3 VCN: first AMD generation with AV1 encode.",
),
GpuProfile(
key="amd-rdna12",
label="AMD RDNA 1/2 (RX 5000/6000-series) — VAAPI: H.264 + HEVC (AV1 decode only)",
vendor="amd", api="vaapi",
encoders={"h264": "h264_vaapi", "hevc": "hevc_vaapi"},
match=("RX 5", "RX 6", "5700", "5600", "6800", "6700", "6600", "6500"),
notes="RDNA 2 has AV1 decode only — AV1 encode stays on CPU.",
),
GpuProfile(
key="amd-gcn",
label="AMD GCN 4/5 / Vega (RX 400/500, Vega 56/64) — VAAPI: H.264 + HEVC",
vendor="amd", api="vaapi",
encoders={"h264": "h264_vaapi", "hevc": "hevc_vaapi"},
match=("RX 4", "RX 5", "Vega", "580", "570", "480", "470", "64", "56"),
notes="The classic crypto-era mining cards (Polaris/Vega).",
),
GpuProfile(
key="cpu",
label="None (CPU-only encode)",
vendor="none", api="none",
encoders={},
),
]
_GPU_PROFILES_BY_KEY: dict[str, GpuProfile] = {p.key: p for p in GPU_PROFILES}
def gpu_profile_by_key(key: str | None) -> GpuProfile | None:
if not key:
return None
return _GPU_PROFILES_BY_KEY.get(key)
def match_gpu_profile(gpu_name: str) -> GpuProfile | None:
"""Best-effort auto-detection from a GPU name string (nvidia-smi or
lspci output). Case-insensitive; first matching profile wins (the
match lists are ordered most-specific first, and the compute boards
are matched before the consumer generations they share names with —
e.g. 'CMP 170HX' must not hit the Ampere 'A10' style entries)."""
if not gpu_name:
return None
name = gpu_name.lower()
for profile in GPU_PROFILES:
for frag in profile.match:
if frag.lower() in name:
return profile
return None
# ──────────────────────────────────────────────
# FFMPEG ARG HELPERS (per hardware API)
# ──────────────────────────────────────────────
def encoder_for_family(profile: GpuProfile | None, family: str) -> str | None:
"""Hardware encoder name for a codec family on this profile, or None."""
if not profile or profile.api == "none":
return None
return profile.encoders.get(family)
def resolve_vaapi_device() -> str:
"""First render node, or the classic fallback path. (Best-effort I/O —
callers that need purity pass the result into encoder_pre_args.)"""
import glob
nodes = sorted(glob.glob("/dev/dri/renderD*"))
return nodes[0] if nodes else "/dev/dri/renderD128"
def encoder_pre_args(profile: GpuProfile, vaapi_device: str | None = None) -> list[str]:
"""Args that must precede -i (hardware device initialisation)."""
if profile.api == "vaapi":
dev = vaapi_device or resolve_vaapi_device()
return ["-vaapi_device", dev]
if profile.api == "qsv":
return ["-init_hw_device", "qsv=hw"]
return []
def encoder_filter_chain(profile: GpuProfile) -> list[str]:
"""Filter args that upload software frames to the hardware surface
format (VAAPI encoders only accept hw frames; -vaapi_device makes
its device the default for hwupload)."""
if profile.api == "vaapi":
return ["-vf", "format=nv12,hwupload"]
return []
def encoder_quality_args(api: str, encoder: str, crf: int, preset: int) -> list[str]:
"""Constant-quality args for a hardware encoder. NVENC maps the CPU
preset tiers to p-presets; QSV uses very_fast/medium; VAAPI uses CQP
rate mode which has no preset knob."""
if api == "nvenc":
if preset <= 6:
p = "p7"
elif preset <= 8:
p = "p5"
else:
p = "p4"
return ["-preset", p, "-tune", "hq", "-rc", "vbr",
"-cq", str(crf), "-b:v", "0", "-pix_fmt", "yuv420p",
"-g", "240"]
if api == "qsv":
p = "veryslow" if preset <= 6 else ("medium" if preset <= 8 else "very_fast")
return ["-preset", p, "-global_quality", str(crf),
"-pix_fmt", "yuv420p", "-g", "240"]
if api == "vaapi":
return ["-rc_mode", "CQP", "-qp", str(crf), "-g", "240"]
return []