Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
35 KiB
NVIDIA GPU Extension + Dynamic Pool Scaling — Design & Research
PeerTube transcoding worker cluster · design wave · 2026-10-09
Companion to the Intel QSV worker implementation landing in
infra/peertube-workers/ (the enrolled worker laptop). This document depends only on the
Intel worker's CONTRACT (official @peertube/peertube-runner, per-worker
admission controller, pluggable capability probe, status JSON, drain control,
fail-closed hardware rule), never on its internals.
Empirical anchors in this doc are marked [E] and were measured on this
machine on 2026-10-09: WSL2 host, 2× NVIDIA GeForce RTX 3090 (Ampere GA102),
Windows-side driver 595.58.03 / CUDA 13.2, Debian 12 ffmpeg 5.1.9
(0+deb12u1, libmfx build), 56-core Xeon E5-2690 v4, 128 GiB RAM. Numbers not
marked [E] are from NVIDIA primary documentation (Video Codec SDK 13.x
support matrix, NVENC help page, driver release notes, CUDA-on-WSL guide) as
recalled; no web access was available in this session, so treat
vendor-doc-derived figures as "verify against the live matrix when the first
NVIDIA box is onboarded" — every one of them is also re-derived at install
time by the probe (§5.3), so a stale number here degrades to a warning, never
to a wrong production decision.
0. Executive summary
- NVENC on GeForce is no longer meaningfully session-capped for our use case: the driver limit is 8 concurrent encode sessions per GPU since driver branch 536 (mid-2023); professional/datacenter boards have no driver cap. Our per-worker concurrency will be bounded by VRAM, NVDEC+filter throughput and bandwidth long before 8 sessions matter — but the cap must still be tracked, because PeerTube fans one job out to many resolution renditions and a 5-rendition job on one GPU is 5 sessions.
- The hardware rule generalizes cleanly: the same fail-closed contract that
keeps QSV from falling back to x264 keeps NVENC from falling back to x264.
The only NVIDIA-specific additions are device selection (
-gpu N), driver/kernel-module installation, session accounting, and VRAM floors. - Do not put NVIDIA workers on WSL2. [E] The API surface exists
(
h264_nvenc/hevc_nvencregistered,libnvidia-encode.so.595.58.03present) but the GPU is shared with the Windows host, NVML encoder queries are reduced, and there is no/dev/dri, no persistence-mode control and no isolation from Windows-side VRAM consumers. On this very box NVENC is currently unusable because two co-tenant Windows processes hold 23.8/24 GiB on each GPU. WSL2 is a dev/probe platform; production NVIDIA workers are native Linux. - Dynamic pool scaling does not require touching PeerTube's scheduler.
Runners register once and persist server-side; "joining the pool" is
systemctl start+ admission AVAILABLE, "leaving" is graceful drain + optionalsystemctl stop. The pool manager nudges which runners are eligible (and at what concurrency), PeerTube still decides job assignment. Mixed-vendor preference is expressed the same way: selective availability + concurrency, not scheduler surgery. - The one interface the Intel implementation must get right now: the
capability probe must be a mandatory, fail-closed, versioned plugin
interface, and the status JSON must be vendor-generic with a
gpu.vendordiscriminator from day one.
1. NVIDIA capability research (2026)
1.1 NVENC concurrent-session limits
| GPU class | Driver-imposed concurrent NVENC sessions | Notes |
|---|---|---|
| GeForce (consumer) | 8 per GPU | Raised 3→5 in 530.41.03/531.29 (2023-03), 5→8 in branch 536 (2023-07). Applies per GPU, across all processes; an ffmpeg process using -threads still counts as 1 session per encode context. |
| Professional / workstation (RTX A-series, RTX 5000/6000 Ada, RTX Pro Blackwell; ex-Quadro) | No driver cap | Bounded only by hardware/VRAM. |
| Datacenter media-class (T4, A2, L4, L40, L40S) | No driver cap | These are the right datacenter parts for transcoding. |
| Datacenter compute-class (A100, H100, H200, B100/B200) | 0 — no NVENC at all | Compute dies ship no encode engine (and Hopper/A100 have reduced/absent NVDEC). Never buy these for a transcode pool. |
Design consequences:
- The cap counts encode sessions, not processes and not ffmpeg "engines". A single PeerTube VOD job that produces 4 renditions from one decode can hold 4 encode sessions (PeerTube today typically runs one ffmpeg per rendition, so it holds 4 decode sessions too — see §3.4).
- Session pressure must be measured, not assumed from the table: NVML
exposes
nvmlDeviceGetEncoderCapacity/ encoder-session counts on bare-metal Linux, but [E] on WSL2 the capacity query returned a non-success code in our ctypes probe, so the probe design (§5.3) treats NVML as best-effort with a driver-class lookup table and our own accounting as ground truth.
1.2 NVENC encode support matrix by generation
Generational numbering below is the commonly used NVENC-generation scheme (1=Kepler … 7=Blackwell); NVIDIA's own docs speak of "Turing NVENC", "Ampere NVENC", etc. Quality-relevant jumps: Turing (large HEVC/H.264 quality+lookahead gains) and Ada (AV1 + HEVC 4:4:4).
| NVENC generation (arch) | Example parts | H.264 | HEVC 8-bit | HEVC 10-bit | HEVC B-frames | HEVC 4:4:4 | AV1 |
|---|---|---|---|---|---|---|---|
| Gen 1 (Kepler) | GTX 680 | ✓ | — | — | — | — | — |
| Gen 2 (Maxwell) | GTX 750Ti/970; GM206 (950/960) | ✓ | GM206 only | — | — | — | — |
| Gen 3 (Pascal) | GTX 10xx, Quadro P | ✓ | ✓ | ✓ | — | — | — |
| Gen 4 (Turing) | GTX 16xx, RTX 20xx, T4 | ✓ | ✓ | ✓ | ✓ | — | — |
| Gen 5 (Ampere) | RTX 30xx, A-series | ✓ | ✓ | ✓ | ✓ | ✓ (Pro/A-series) | — |
| Gen 6 (Ada) | RTX 40xx, L4, L40/L40S | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Gen 7 (Blackwell) | RTX 50xx, RTX Pro | ✓ | ✓ | ✓ | ✓ | ✓ + 4:2:2 | ✓ (+UHQ) |
[E] Consistency check on this box (RTX 3090 = Ampere = gen 5):
ffmpeg 5.1.9 lists h264_nvenc and hevc_nvenc and does not list
av1_nvenc; av1_cuvid decode is listed. Matches the matrix exactly
(Ampere: AV1 decode yes, AV1 encode no).
1.3 NVDEC decode support matrix
| NVDEC generation (arch) | H.264 | HEVC 8/10-bit | VP9 | HEVC 4:4:4 | AV1 |
|---|---|---|---|---|---|
| Maxwell | ✓ | GM206: 8-bit only | — | — | — |
| Pascal | ✓ | ✓ | ✓ | — | — |
| Turing | ✓ | ✓ | ✓ 10/12-bit | ✓ | — |
| Ampere | ✓ | ✓ | ✓ | ✓ | ✓ 8/10-bit |
| Ada | ✓ | ✓ | ✓ | ✓ | ✓ (incl. higher bit depths) |
| Blackwell | ✓ | ✓ (+4:2:2) | ✓ | ✓ | ✓ (+4:4:4/12-bit) |
Practical rule for the pool: any card we would actually buy (Turing or newer) decodes H.264/HEVC/VP9; AV1 decode needs Ampere+; AV1 encode needs Ada+. An Ampere pool (like this box) can ingest AV1 sources and emit H.264/HEVC, which fits the stated "H.264 primary, HEVC secondary, no AV1 output" policy with headroom.
1.4 Linux driver stack
- Open vs proprietary kernel modules: NVIDIA's open kernel modules
(
nvidia-open/ open-gpu-kernel-modules) are the default recommended path for Turing+ since the 560-series; proprietary remains for pre-Turing and a few feature gaps. All realistic pool hardware (Turing+) → use open modules. Blackwell effectively requires them. - 2026 driver branches: current feature branches run in the 5xx range;
[E] this host's Windows-side driver is 595.58.03 (CUDA 13.2). Debian
packages:
nvidia-open-kernel-dkms(or prebuiltlinux-image-amd64+ signed modules),nvidia-driver-libs,nvidia-smi,nvidia-utils; firmware fromnon-free-firmware. Keep driver version pinned per pool fleet and recorddriver + kernel + module flavorin the status JSON. - Userspace pieces that matter to us:
libnvidia-encode.so.1(NVENC), CUDA driver (libcuda.so.1) used by ffmpeg's nvenc/cuvid code paths,libnvidia-ml.so.1(NVML for nvidia-smi). ffmpeg needs no CUDA toolkit — only the driver libs. (A statically-built ffmpeg with--enable-ffnvcodec/--enable-cuda-nvccis only needed forscale_npp;scale_cudais covered by the header-only ffnvcodec path.) - Access control: user in
videogroup (or a dedicated worker user with a udev/tagged device ACL);nvidia-persistenced(orNVreg_PreserveVideoMemoryAllocations=1) to avoid multi-second GPU re-init between jobs on an idle pool box — relevant to dynamic scale-up latency (§3.7). - MIG/compute-mode: leave compute mode
DEFAULT[E]; nothing to configure, but note it in the probe report.
1.5 WSL2: CUDA/NVENC status
What works [E, this host]:
- CUDA user-mode stack present via
/usr/lib/x86_64-linux-gnu/libnvidia-*.so(WSL-provided, version-matched to the Windows driver). - ffmpeg registers
h264_nvenc,hevc_nvenc,*_cuviddecoders and thecudahwaccel;libnvidia-encode.so.595.58.03is installed. nvidia-smiworks (2 GPUs visible, temps, clocks, throttle counters).
What breaks it (all [E] on this host):
- No isolation from Windows GPU consumers. Two Windows processes hold
23 856 MiB of each 24 GB GPU → any CUDA context creation fails
(
CUDA_ERROR_OUT_OF_MEMORY), soh264_nvencinit fails with "No capable devices found" andh264_cuviddevice setup fails. This is the textbookGPU_UNAVAILABLEstate, and it is caused by another tenant, not by the driver. - Reduced NVML: encoder-capacity/session queries returned non-success
in a ctypes probe;
nvidia-smi -qshows Encoder Stats fields that cannot be validated. - No
/dev/dri[E: directory absent/empty] → no VAAPI/QSV fallback on the same box, nointel_gpu_top-style counters, no display-path controls. - No control of persistence mode, ECC, or power limits from inside WSL.
Verdict: transcoding in WSL2 is feasible in the "API present, demo works"
sense but is not a production worker platform — the GPU is a shared
resource with the Windows host, NVML is limited, and admission signals are
unreliable. Use WSL2 to develop the nvidia-nvenc probe; run production
workers on native Linux (or pass a whole GPU to a VM with VFIO if the box
must stay Windows-adjacent).
1.6 FFmpeg flags: the NVIDIA pipeline
The NVIDIA analogue of hw decode → hw frames → hw scale → hw encode:
# full hardware pipeline, frames never leave the GPU
ffmpeg -y -init_hw_device cuda=cu:0 -filter_hw_device cu \
-hwaccel cuda -hwaccel_output_format cuda -c:v h264_cuvid -i INPUT \
-vf scale_cuda=1280:720:format=yuv420p \
-c:v h264_nvenc -preset p5 -tune hq -rc vbr -cq 23 -b:v 0 \
-rc-lookahead 20 -spatial_aq 1 -temporal_aq 1 \
-bf 3 -b_ref_mode middle \
-c:a copy OUTPUT
Key flags (PeerTube-runner passes its own argv; these are what the capability probe and any wrapper/profile must emit):
| Purpose | Flag | Notes |
|---|---|---|
| HW decode | -hwaccel cuda -hwaccel_output_format cuda or explicit -c:v h264_cuvid/hevc_cuvid/av1_cuvid |
explicit *_cuvid gives named-decoder certainty; -hwaccel_output_format cuda keeps frames on-GPU |
| HW upload (when input is CPU-decoded) | -vf hwupload_cuda |
needed for e.g. audio-merge jobs with CPU-side frames |
| HW scale | scale_cuda=w:h (part of ffmpeg) or scale_npp (needs nvcc-built ffmpeg) |
scale_cuda is sufficient and dependency-light |
| Multi-GPU | -init_hw_device cuda=cu:1, encoder -gpu N |
probe must test the specific device index it will advertise |
| Encode | -c:v h264_nvenc / hevc_nvenc / av1_nvenc |
av1_nvenc exists only on Ada+ builds of ffmpeg ≥ 5.0 |
| Quality | -preset p1..p7 (p7 slowest/best), -tune hq|ll|ull, -multipass qres|fullres |
p4–p5 is the sweet spot for VOD |
| Rate control | -rc vbr -cq N -b:v 0 (quality-targeted VBR) or -rc constqp |
prefer -cq + -b:v 0 for PeerTube's CRF-style outputs |
| Lookahead/AQ | -rc-lookahead, -spatial_aq, -temporal_aq, -bf, -b_ref_mode middle (Turing+) |
the Turing+ quality levers |
| 10-bit HEVC | -profile:v main10 -pix_fmt p010le |
verify per generation (§1.2) |
| Session hygiene | -delay 0, and one encode context per session |
sessions are the scarce resource on GeForce (§1.1) |
| Verification | run under nvidia-smi dmon -s u (enc/dec columns), check encoder-session count delta |
this is the no-silent-CPU evidence, see §5.4 |
Minimal VRAM budget per 1080p hw pipeline ≈ 200–400 MiB (decode surfaces + scale + encode lookahead); 4K ≈ 0.8–1.5 GiB. The probe measures real usage per resolution class instead of trusting these numbers.
2. What an NVIDIA worker is (profile)
Same shape as the Intel worker — official runner, admission controller, status JSON, drain control — differing only in the probe module, deps, and concurrency inputs.
2.1 Differences for install-peertube-worker.sh
| Aspect | Intel QSV worker | NVIDIA worker |
|---|---|---|
| Kernel/driver deps | intel-media-va-driver-non-free, oneVPL runtime, /dev/dri/renderD128 + render/video groups |
nvidia-open kernel module + pinned driver branch, libnvidia-encode, nvidia-smi, nvidia-persistenced, user in video group |
| Device nodes | /dev/dri/* (always present) |
/dev/nvidia* (created by module; nvidia-modprobe/udev) |
| Probe module | intel-qsv |
nvidia-nvenc (new module, same interface) |
| Config additions | — | PT_WORKER_GPU_DEVICES=0 (or 0,1), PT_WORKER_SESSIONS_RESERVE=1 (headroom sessions), PT_WORKER_VRAM_FLOOR_MB=1024, PT_WORKER_DRIVER_MIN=550 |
| Self-test | vainfo + QSV transcode | nvidia-smi gates + per-device NVENC transcode + session-slot test + VRAM floor (§5.3) |
| Concurrency inputs | engine util, CPU/RAM | min(sessions_free, cpu_bound, ram_bound, vram_bound, disk_bound, bw_bound) |
| Runner invocation | as Intel contract | plus -gpu N device pinning in the ffmpeg argv and one runner instance per GPU when a box has >1 GPU (see §2.2) |
Everything else — systemd unit shape, service account, Tailscale posture, registration, VOD-only job types, drain semantics — is inherited unchanged.
2.2 Multi-GPU boxes
A box with K GPUs runs K runner instances (peertube-worker@gpu0…gpuK-1
template units), each pinned to one device, rather than one runner juggling
devices. Reasons: session accounting and VRAM floors are per-device;
admission states (GPU_UNAVAILABLE for one card after a co-tenant/ECC event)
should not drain healthy siblings; PeerTube sees N independent runners and
load-balances them natively. [E: this host has 2× RTX 3090 and would onboard
as …:gpu0 and …:gpu1.]
3. Dynamic pool design
3.1 Architecture
┌────────────────────────────────────────────┐
│ pool-manager (new, small, stateless-ish) │
│ signals → policy engine → per-worker │
│ desired state (AVAILABLE/DRAINED + │
│ concurrency hint + job-class eligibility) │
└───────┬───────────────────┬────────────────┘
read-only admin │ │ control channel (SSH over
API polls │ │ Tailscale → pt-worker CLI)
PeerTube ◄───────────┘ ▼
keeps THE job queue worker-1 (Intel, QSV) worker-2 (NV gpu0) …
and does assignment admission controller admission controller
+ peertube-runner + peertube-runner
The pool manager never talks to PeerTube as a scheduler. It only (a) observes queue pressure read-only, (b) starts/stops/drains worker services, (c) sets admission state and concurrency. Assignment remains PeerTube's, via the official runner request/accept cycle. This honors the "do not build a new job scheduler" constraint.
3.2 (a) Load signals
| Signal | Source | Cadence | Notes |
|---|---|---|---|
pending_depth |
PeerTube admin API: runner-jobs endpoint filtered by state=pending, per job type (vod-web-video / vod-hls / vod-audio-merge) | 15 s | the primary scale-up input |
oldest_pending_age_s |
same endpoint, createdAt of oldest pending job |
15 s | catches stall at low depth (e.g. one 4K job nobody can serve) |
completed_rate_ewma |
per-worker status JSON jobs_completed counters |
30 s | throughput of the currently-active set |
enc_util, dec_util |
NVIDIA: nvidia-smi --query-gpu=encoder.utilization.../NVML; Intel: media-engine counters per Intel contract |
10 s | saturation of the active set |
sessions_active/cap |
our own accounting + NVML where supported | per-job event | GeForce cap awareness (§1.1); NVML unsupported on WSL2 [E] |
vram_free_mb |
NVML | 10 s | hard eligibility floor |
recent_download_mbps / upload_mbps (EWMA) |
per-job transfer observations (Intel contract already defines these) | per-job | job-size-aware eligibility input |
realtime_factor_ewma |
completed jobs (encode wall-clock vs duration) | per-job | learned worker weight (§3.3) |
throttle_reasons, temp_c |
NVML throttle-cause bits / thermal zones | 10 s | THERMAL_THROTTLED mapping [E: counters visible on this host] |
Pressure index (normalized 0..1), computed over eligible workers only:
P = clamp01( max(
pending_depth / (2 * Σ active_capacity_slots),
oldest_pending_age_s / 900s
))
Σ active_capacity_slots = Σ over AVAILABLE/BUSY workers of their current
concurrency hint. The 2× makes P=1 mean "queue is two full waves deep".
3.3 (b) Policy engine
Pool-worker states (per worker; these are manager-side views layered on the worker's own admission states from the Intel contract):
| Manager state | Meaning | Transition in |
|---|---|---|
COLD |
powered off / suspended (WoL-able, §3.8) | manual or WoL policy |
SPARE |
service up, admission DRAINED (registered, idle) | default resting state |
WARM |
service up, DRAINED, persistence-mode on, probe recently green | pre-warm tier |
ACTIVE |
admission AVAILABLE at concurrency C | scale-up decision |
DRAINING |
admission DRAINING, in-flight jobs finishing | scale-down or fault |
COOLDOWN |
admission DRAINING after repeated job failures | failure policy |
Hysteresis (per-worker and pool-wide):
- Scale up when
P ≥ 0.6sustained ≥ 90 s (two consecutive 45 s windows) → promote the highest-weight SPARE/WARM worker to ACTIVE. - Scale down when
P ≤ 0.15sustained ≥ 20 min and worker has been idle ≥ 15 min → demote ACTIVE → SPARE (drain, then optional service stop). - Never demote below
min_active = 1(or 2 once multi-tenant), regardless of idle, to keep probe freshness and WoL-free response. - Promotions are staggered ≥ 30 s apart and probe-gated: a worker may only become ACTIVE if its last probe is < 5 min old and green. This prevents thundering-herd registration wake-ups and GPU re-init spikes.
- Failure fast-path bypasses all hysteresis: GPU_UNAVAILABLE, thermal, or a hardware-evidence-failed job → immediate DRAINING.
Per-worker learned weights (used for promotion order and drain order):
w_i = EWMA_λ=0.3 over jobs of ( output_pixel_rate_i / class_baseline )
shrunk toward class prior: ŵ = (n·w + k·w_prior) / (n + k), k=3
output_pixel_rate = output pixels/sec during encode phase only (excludes
download/upload), bucketed by job class so a worker isn't punished for
getting small jobs. Cold-start workers inherit the class prior, so a new
box's first promotion comes from queue pressure, not fabricated history.
Drain order prefers low ŵ and low recent_download_mbps (cheapest to
re-warm).
Job-size-aware eligibility. Job classes from PeerTube job metadata (source resolution × duration, known at pending time):
| Class | Definition (approx) | Requires |
|---|---|---|
| S | ≤1080p, ≤10 min | any ACTIVE worker |
| M | ≤1080p long, or 1440p | enc_util headroom ≥ 30% |
| L | 4K, or 4K-source→1080p | realtime_factor_ewma ≥ 3, recent_download_mbps ≥ 50, vram_free ≥ 4 GiB (NVIDIA) or equivalent QSV gate |
| XL | 4K60 long-form | L gates + ŵ ≥ 1.0 |
Enforcement is, again, selective availability: when an L/XL job is pending and no one satisfies the gates, the manager keeps the small workers at their normal concurrency but does not count them as capacity for that job (PeerTube will still let a small worker take it if it asks — acceptable fallback, not silent CPU fallback, and it still satisfies the hardware rule).
3.4 (c) Scale-up / scale-down mechanics
The constraint: PeerTube runner registration is static. A runner row
exists server-side from first register (with a registration token) until
an admin deletes it; the runner process merely polls. Therefore:
Joining the pool =
- box provisioned + runner registered once at install time
(
install-peertube-worker.sh --name w2:gpu0 …), - service exists but may be stopped or admission-DRAINED,
- manager:
systemctl start peertube-worker@…(if stopped) thenpt-worker enable→ admission AVAILABLE → runner starts requesting jobs on its normal poll cycle.
Leaving the pool (grace) =
pt-worker drain→ admission DRAINING; the controller stops offering the runner for new jobs (per Intel contract: runner paused/disabled, not killed),- wait for in-flight jobs:
active_jobs → 0, with a max-drain timeout (e.g. 2× p99 job wall-clock, floor 30 min), - then either leave at SPARE (service up, DRAINED — cheapest re-join) or
systemctl stopfor overnight-class quiescence, or suspend (§3.8).
Notes:
- Repeated start/stop never touches registration tokens; tokens live only in the install-time secret path on the worker.
- Because a drained runner is still registered, PeerTube's per-runner job-request rate is unaffected; there is no server-side cleanup needed.
- One job → many sessions: PeerTube runs separate ffmpeg instances per
output rendition, so a "concurrency 1" job can still consume 4+ NVENC
sessions. The admission controller must therefore count actual encode
sessions (its own children's NVENC contexts), not just jobs, when
enforcing
sessions_active < cap(§1.1, §5.3).
3.5 (d) Mixed-vendor scheduling
We cannot intercept PeerTube's runner→job assignment, so vendor preference is expressed as eligibility, decided at the moment a job class is pending:
| Job / condition | Prefer | Mechanism |
|---|---|---|
| AV1 source | NVIDIA Ampere+ (NVDEC AV1) or CPU-decode on QSV box | gate QSV workers out of the AV1-decode class via probe capability (av1_dec) |
| AV1 output | NVIDIA Ada+ only (av1_enc) |
same gate; policy says: not enabled in production anyway |
| 4K HEVC/H.264 outputs | NVIDIA (throughput, VRAM for lookahead) | weight-based promotion pulls NVENC boxes in first at L/XL classes |
| ≤1080p H.264 bulk | whichever has session/enc headroom; QSV is typically more power-efficient per 1080p job | neutral; capacity-driven |
| Session pressure (NVENC near cap, QSV idle) | QSV | temporarily demote NVENC availability for class S/M |
| One vendor fleet-wide drained (driver CVE, bad driver branch) | the other | vendor-level kill switch in manager config: vendor_disabled: [nvidia] |
Probe capability keys that drive this (identical names across vendors):
h264_enc, hevc_enc, av1_enc, h264_dec, hevc_dec, av1_dec, max10bit,
max_sessions, max_width/height. The Intel probe fills the same keys
(i5-1240P: h264_enc=1, hevc_enc=1, av1_enc=0, av1_dec=1), which is the
whole point of the pluggable module.
3.6 (e) Unified status/metrics schema
Extends the Intel status JSON with a versioned, vendor-generic gpu block.
Bump schema_version; additive changes only.
{
"schema_version": 2,
"hostname": "w2", "runner_name": "w2:gpu0",
"tailscale_ip": "100.x.y.z", "net_path": "DIRECT|DERP",
"state": "AVAILABLE", // Intel-contract states, unchanged
"registration_state": "registered",
"active_jobs": 1, "concurrency": 3, "concurrency_limiters": {
"cpu": 4, "ram": 4, "disk": 8, "bw": 3,
"gpu_sessions": 3, // NVIDIA only; null on Intel
"vram": 3
},
"gpu": { // NEW: vendor-generic
"vendor": "nvidia", // "intel" | "nvidia" | "none"
"probe_module": "nvidia-nvenc", // "intel-qsv" | "nvidia-nvenc"
"model": "RTX 3090", "generation": "ampere",
"nvenc_generation": 5,
"driver": "595.58.03", "kernel_module": "open",
"capabilities": { "h264_enc": 1, "hevc_enc": 1, "av1_enc": 0,
"h264_dec": 1, "hevc_dec": 1, "av1_dec": 1,
"max10bit": 1 },
"sessions": { "active": 2, "cap": 8,
"cap_source": "driver_class_table|nvml|probe" },
"devices": [ { "index": 0, "uuid": "GPU-2095…",
"vram_total_mb": 24576, "vram_free_mb": 22000,
"enc_util_pct": 71, "dec_util_pct": 40,
"temp_c": 66, "throttle_reasons": [] } ],
"self_test": { "ts": "2026-10-09T14:00:00Z", "ok": true,
"evidence": "enc_util_delta=+68%", "detail": "…" }
},
"hw_evidence_last_job": { "ok": true,
"method": "encoder_session_delta>=1", "ts": "…" },
"cpu": {"model": "…", "util_pct": 40}, "temps_c": {"cpu": 55},
"disk_free_gb": 180,
"recent_download_mbps": 92.4, "recent_upload_mbps": 88.1,
"jobs": {"completed": 214, "failed": 3, "hw_fallback_prevented": 0},
"last_job": {"duration_s": 412, "realtime_factor": 7.1,
"class": "M", "output_pixel_rate_mpx_s": 22.6},
"manager": {"desired": "ACTIVE", "weight": 1.14, "class_gates": "ok"}
}
Prometheus mapping is mechanical: one pt_worker_up, one
pt_worker_gpu_sessions{vendor,cap_source}, one
pt_worker_hw_evidence_ok (the no-silent-CPU invariant as an alertable
series), plus re-exports of the Intel contract's series.
3.7 (f) Future: bare-metal autoscaling hooks (WoL)
Brief by request. If worker boxes are wakeable:
- Manager keeps
wol_mac,wake_budget(max 2 concurrent wakes, ≥ 3 min apart), and a boot-readiness check = Tailscale SSH + agent status JSON with fresh probe (typical budget 3–6 min including GPU persistence). - Wake policy reuses the scale-up rule with an extra tier: only wake when
P ≥ 0.8sustained 5 min and all SPARE/WARM workers are ACTIVE (i.e. waking is the last resort, because boot latency is minutes not seconds). - Sleep path = graceful drain (§3.4) then
systemctl suspend; AC/battery rules from the Intel laptop policy apply first (never sleep a box on battery, never wake one). - Accounting: track
wakes/dayandwoken-minutes-savedper box; cap wakes (e.g. ≤ 6/day/box) to respect hardware and power-on cycles.
4. Rollout plan
4.1 Change the Intel admission controller NOW (small interface list)
Five things, all cheap while the code is fresh:
- Versioned status JSON: add
schema_version: 1today, and the vendor-genericgpublock shape (§3.6) with"vendor": "intel"— i.e.gpu.probe_module,gpu.capabilities{h264_enc,hevc_enc,av1_enc, h264_dec,hevc_dec,av1_dec},gpu.model/generation, per-device array (Intel: one entry,/dev/dri/renderD128,enc_utilfrom media-engine counters). Avoid leaking QSV-specific keys to the top level. - Probe interface contract (document, even if only one impl exists):
probe() → {ok, capabilities, maxSessions?, devices[], evidence}andverifyHardwareDidWork(jobSamples) → {ok, evidence}— both fail closed: any exception, timeout, or absent evidence ⇒ GPU_UNAVAILABLE + drain, never "assume CPU is fine". Selectable viaPT_WORKER_GPU_PROBE=intel-qsv|nvidia-nvenc. - Concurrency resolver as a pure function of
(probe, cpu, ram, disk, bw, [sessions], [vram])so NVIDIA is a new input set, not a new control path. Expose which limiter bound the choice (concurrency_limiters, §3.6) — this is what the pool manager consumes. - Hardware-evidence hook per job: after each completed job, sample the
GPU's encoder utilization delta (Intel: media-engine busy% delta during
the job; NVIDIA: NVML encoder-session/util delta) and record
hw_evidence_last_job. A job that completed without evidence must count as a failure for pool purposes even if PeerTube marks it OK — this is the enforcement teeth of the no-silent-CPU rule, and it is vendor-neutral. - Job-class metadata in per-job telemetry (source res/codec, output renditions, wall-clock, realtime factor) so the manager's weights and gates (§3.3) have data from day one on the Intel cluster.
4.2 Onboarding the first NVIDIA box (when one exists)
- Pick the platform: native Linux (Debian 12/13), Turing or newer; Ada+ if AV1 encode is ever wanted. Reject A100/H100-class compute cards (no NVENC, §1.1).
- Driver install, pinned branch (≥ 550; open kernel modules for Turing+),
nvidia-persistenced, user invideo, verifynvidia-smi+libnvidia-encode.so.1present. Reboot-verify. - ffmpeg: distro build is fine if it lists
h264_nvenc/hevc_nvencand*_cuvid[E: Debian 12's ffmpeg 5.1.9 does]; else pin a static BtbN/johnvansickle-style build ≥ 5.0 with ffnvcodec. Recordffmpeg --versionin status JSON. install-peertube-worker.sh --name <host>:gpu0 --gpu-probe nvidia-nvenc --gpu-devices 0 …— same script, new profile (§2.1). Per additional GPU on the box, repeat with:gpu1, … (§2.2).- Self-test gate (must pass before registration): see §5.3 — nvidia-smi
gates → per-device NVENC encode probe under
dmon→ session-slot test → VRAM floor. Fail ⇒ abort install withGPU_UNAVAILABLE, no runner registration. - Register runner (VOD job types only, per Intel contract), start service, run one real E2E job, and assert hardware evidence: encoder-session delta ≥ 1 per rendition and enc-util delta > 0 and CPU-user-time of the ffmpeg process ≪ CPU-encode baseline (belt and braces).
- Soak: 1 h at target concurrency; watch sessions, VRAM, temps, throttle
reasons, throughput; set the box's initial
concurrencyfrom measured end-to-end throughput, not single-job fps (Intel Phase 9 methodology, unchanged). - Add to pool manager inventory as
SPAREwith class-prior weight; first promotion happens on the next pressure event. - Only then consider multi-GPU boxes and WoL tiers (§3.7).
5. Appendices
5.1 Failure-mode → state mapping (NVIDIA additions)
| Failure | Detection | State | Action |
|---|---|---|---|
| Driver/module missing or unloaded | nvidia-smi nonzero, no /dev/nvidia* |
GPU_UNAVAILABLE | refuse work; re-probe 60 s |
| VRAM exhausted (co-tenant, leak) [E] | CUDA_ERROR_OUT_OF_MEMORY / "No capable devices found" in ffmpeg init; NVML vram_free below floor |
GPU_UNAVAILABLE (device-scoped) | drain that device's runner; sibling GPUs unaffected (§2.2) |
| Session cap hit | our accounting ≥ cap − reserve | BUSY (capacity) | no new jobs until sessions free; never queue extra ffmpeg per job |
| Thermal/power throttle | NVML throttle-cause bits, temp ≥ limit | THERMAL_THROTTLED | drain after in-flight; cooldown |
| Encoder produced no HW evidence | hw_evidence_last_job.ok=false |
GPU_UNAVAILABLE + job counted failed | page; never re-run on CPU |
| Driver/GPU fallen off bus (Xid errors in dmesg) | dmesg NVRM: Xid watch |
OFFLINE | drain + host-level alert |
| Network DERP/throughput collapse | Intel contract's EWMA + path change | NETWORK_DEGRADED | unchanged from Intel |
5.2 Config surface (NVIDIA profile)
PT_WORKER_GPU_PROBE=nvidia-nvenc
PT_WORKER_GPU_DEVICES=0 # one runner instance per device
PT_WORKER_SESSIONS_CAP=auto # nvml → driver_class_table → 8 (GeForce)
PT_WORKER_SESSIONS_RESERVE=1 # keep 1 session headroom
PT_WORKER_VRAM_FLOOR_MB=1024
PT_WORKER_DRIVER_MIN=550
PT_WORKER_FFMPEG_NVENC_PRESET=p5 # map PeerTube quality tier → preset
PT_WORKER_PROBE_INTERVAL=300 # full probe; 10 s counters in between
5.3 nvidia-nvenc probe / self-test design (ordered, fail-closed)
- Gate:
nvidia-smiexits 0; driver ≥ min; module flavor recorded;libnvidia-encode.so.1resolvable; ≥1 device withvram_free ≥ floor. - Capability enumeration: per advertised device, from NVML name →
generation → capability table (§1.2/§1.3), then verified empirically:
attempt a 1-second
h264_nvencencode on that exact device (-gpu N); attempthevc_nvenc; attemptav1_nvenconly if generation ≥ Ada. Absence of an encoder inffmpeg -encodersdowngrades capability without failing the probe (e.g. distro ffmpeg without av1_nvenc). - Pipeline probe: full hw chain (§1.6) at 1080p on the target device
under
nvidia-smi dmon -s u; requireenc>0anddec>0columns observed during the run, and record VRAM delta as the per-class budget. - Session-slot test: launch K parallel tiny
h264_nvencencodes (K = advertised cap) and count how many initialize successfully (No capable devices found/ init failure = slot unavailable). The largest K′ that succeeds becomessessions.capwithcap_source=probe. This is the ground truth the controller trusts — it automatically reflects driver-class caps and whatever the platform (e.g. WSL2 [E]) actually allows. - Decode matrix: one decode-only pass per codec family
(h264/hevc/av1
*_cuvid) on a synthetic stream, assertingdec>0. - Report: fill the §3.6
gpublock, includedmonexcerpts and VRAM deltas asevidence. Any step erroring or timing out (30 s/step) ⇒ok=false⇒ GPU_UNAVAILABLE. No step may conclude "use CPU instead".
5.4 Enforcing the no-silent-CPU rule on NVIDIA
Three independent tripwires, any one sufficient to fail a job:
- Argv gate: the wrapper/runner must be invoking
*_nvencfor video (grep the spawned argv); PeerTube's own fallback logic must be disabled/refused in config. - Evidence gate: NVML encoder-session count and encoder utilization must increase while the job's ffmpeg runs (sampled ≥ 1 Hz); a completed encode with flat counters = CPU fallback (or a no-op) ⇒ job marked failed, worker GPU_UNAVAILABLE, manager COOLDOWN.
- Plausibility gate: ffmpeg CPU user-time for the job must be far below the CPU-encode baseline for that resolution (measured once per box during install); a CPU-encode-shaped CPU profile ⇒ same treatment.
This is deliberately vendor-neutral (Intel uses media-engine busy% in place of NVML counters), which is exactly why it belongs in the shared admission controller rather than in the probe module.
5.5 Open questions for the Intel implementation (blocking-free)
- Exact admin endpoint + auth shape for pending runner-jobs depth (manager needs it read-only; a proxy via the worker's own credentials is acceptable if admin scope is unavailable).
- Whether the Intel drain implementation pauses the runner process or stops the unit — the manager wants to treat both as DRAINED and needs to know which to expect for start/stop idempotency.
- Whether per-rendition ffmpeg invocation is confirmed (drives session-counting assumptions, §3.4).