GPU sharing: running more GPU nodes than you have GPUs
Why local runs work and Kubernetes runs stall
Locally (LocalProcessEngine), every node is an OS subprocess on one machine.
Nothing arbitrates the GPU: all N processes open it concurrently and share it,
bounded only by VRAM. A 9-GPU-node graph runs fine on a single card if the models
fit.
On Kubernetes, each GPU replica requests nvidia.com/gpu — an integer extended
resource that cannot be overcommitted. The scheduler allocates whole devices
exclusively: a graph with N GPU replicas needs N allocatable devices. On a cluster
with fewer, some pods bind and the rest sit Pending
(Insufficient nvidia.com/gpu) forever. The failure is silent in both flow
modes, in different ways:
BATCH — the missing node’s input stream fills, backpressure blocks the producer, and the flow hangs making no progress. (Deploy’s wait loop now detects the unschedulable pod and aborts with an actionable error instead of hanging.)
REALTIME — producers never block; frames headed for the dead node are silently evicted and everything downstream of it produces nothing, while every running pod looks healthy. (Deploy now runs a bounded post-apply schedulability check and warns.)
videoflow explain my_flow.py prints a flow’s total GPU demand, and deploy’s
preflight compares demand against the cluster’s allocatable capacity before
applying anything (--strict-preflight makes a shortfall a hard error).
Three ways to close the gap
1. Reduce demand. Not every “GPU” stage needs one: trackers and light pose
models often run fine on CPU. Prefer solution-level device knobs (e.g. per-stage
device.tracker: cpu in a solution config) — the floor is the number of nodes
doing genuinely GPU-bound inference.
2. Share the physical GPU, keep scheduler accounting: device-plugin time-slicing. The NVIDIA device plugin can advertise each physical GPU as N schedulable units:
# nvidia-plugin-config.yaml (namespace kube-system)
apiVersion: v1
kind: ConfigMap
metadata:
name: nvidia-plugin-configs
namespace: kube-system
data:
config.yaml: |
version: v1
sharing:
timeSlicing:
renameByDefault: false
failRequestsGreaterThanOne: true
resources:
- name: nvidia.com/gpu
replicas: 4
Mount it into the device-plugin DaemonSet and point the plugin at it (CONFIG_FILE
env var), then kubectl -n kube-system rollout restart ds/<device-plugin>.
Allocatable nvidia.com/gpu flips from the physical count to replicas x
physical, and — because renameByDefault: false keeps the resource name —
videoflow manifests need no change at all. Caveats: time-slicing is round-robin
temporal multiplexing with no memory isolation and one shared fault domain; the
sum of all co-tenant models must fit in VRAM, and gpu_count > 1 is rejected
(failRequestsGreaterThanOne).
3. Skip scheduler accounting entirely: ``–gpu-mode shared``.
videoflow deploy my_flow.py --gpu-mode shared --gpu-runtime-class nvidia
Shared mode emits no GPU resource limit: every GPU pod schedules onto the GPU pool (the nodeSelector/toleration stay) and shares the physical devices through the NVIDIA container runtime — exactly the semantics of a local run, with VRAM as the only limit. This is a dev-cluster tool:
There is no scheduler accounting and no memory isolation; an OOM takes out whichever pod the driver picks.
On a multi-GPU node every shared pod sees all devices (the CUDA base images set
NVIDIA_VISIBLE_DEVICES=all) and frameworks default to device 0 — expect pile-up unless your node code picks a device.On clusters where the NVIDIA runtime is an opt-in RuntimeClass (k3s),
--gpu-runtime-classis required: with no resource limit, the runtime class is the only mechanism that injects the device, so deploy refuses shared mode without it.
Production clusters
Keep the default exclusive mode in production and size node pools to demand — the preflight numbers are exactly the capacity you need. Where sharing with real isolation is required:
MIG (A100/H100-class GPUs): hardware partitions exposed as their own resources. Point a node at a profile with
gpu_resource_name='nvidia.com/mig-1g.10gb'(or--gpu-resource-name); no other videoflow change.MPS (any Volta+ GPU, via the device plugin’s Helm chart): concurrent kernels with hard per-client memory caps of
total/replicas. Stronger isolation than time-slicing; the heaviest model bounds the replica count. Pod specs are unchanged.KEDA autoscaling excludes GPU nodes by default (each extra replica claims whole devices); opt in deliberately with
--gpu-autoscalingonce capacity math says it is safe.