GPU Sharing Reference
GitHub

TL;DR Recommended Path for Your Environment

Your constraints:
  • One AI server per DC — scarce, must maximize utilization
  • Proxmox + OpenShift clusters already running in those DCs
  • Workloads: live audio transcoding, LLM inference (stock + custom models)
0
Add a bare-metal GPU node to your OCP cluster
Your OCP clusters run as VMs on VMware/Proxmox. Do not pass the GPU through to a VM — add a dedicated bare-metal worker node with the AI server's GPU directly to the OCP cluster instead. The node joins OCP via the standard Machine Config / ignition flow and is scheduled like any other worker.
OptionMIG supportHypervisor overheadVM live migrationVerdict
Bare-metal OCP worker (recommended)FullNoneN/A — bare metalUse this
PCIe passthrough to OCP VMPartial — some boards disable MIG under IOMMUIOMMU + VFIO layersPermanently broken for that VMAvoid
vGPU (VMware/Proxmox MxGPU)None — mutually exclusive with MIGvGPU manager processLimitedAvoid
One node, same cluster The bare-metal node is just another OCP worker — same ArgoCD, same GPU Operator DaemonSet, same scheduling namespace. No separate cluster to manage. Taint it nvidia.com/gpu=present:NoSchedule so only GPU workloads land on it.

Joining the bare-metal node to OCP

bash — generate ignition config and boot the node
# 1. Generate a worker ignition config from the cluster
oc extract -n openshift-machine-api \
  secret/worker-user-data --to=- > worker.ign

# 2. Serve worker.ign over HTTP from a reachable host, then boot the bare-metal
#    node with the RHCOS ISO. At the boot prompt:
#    coreos.inst.install_dev=/dev/sda
#    coreos.inst.ignition_url=http://<bastion>:8080/worker.ign

# 3. Approve the CSRs that appear once the node contacts the API server
oc get csr -o name | xargs oc adm certificate approve

# 4. Confirm node joined and taint it
oc get nodes
oc adm taint node <gpu-node-name> nvidia.com/gpu=present:NoSchedule
Infrastructure
1
Install NVIDIA GPU Operator on OpenShift via Helm + ArgoCD
Avoid OperatorHub — manage GPU Operator as a Helm chart declared in ArgoCD. Add the NVIDIA Helm repo (helm.ngc.nvidia.com/nvidia), create an ArgoCD Application targeting the gpu-operator chart, and apply it to your OCP cluster. This keeps GPU infrastructure in GitOps alongside your other workloads, gives you version-pinned upgrades, and avoids OLM lifecycle complexity. NFD (Node Feature Discovery) should be a separate ArgoCD app — GPU Operator's bundled NFD can conflict with an existing cluster-wide NFD install.
yaml — ArgoCD Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: gpu-operator
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://helm.ngc.nvidia.com/nvidia
    chart: gpu-operator
    targetRevision: v24.9.2   # pin to a specific release
    helm:
      releaseName: gpu-operator
      values: |
        mig:
          strategy: mixed
        nfd:
          enabled: false   # disable if NFD already installed cluster-wide
        operator:
          defaultRuntime: crio  # OCP uses CRI-O
  destination:
    server: https://kubernetes.default.svc
    namespace: gpu-operator
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
OCP Security Context Constraints GPU Operator creates an nvidia-scc SCC, but pods won't use it unless their ServiceAccount is explicitly bound to it. Without this, GPU workloads fail with EPERM on device access. Bind before deploying any GPU pod:
bash — bind SCC to workload namespace
oc adm policy add-scc-to-user nvidia-scc \
  -z default -n <your-workload-namespace>

# or via ClusterRoleBinding for all service accounts in a namespace:
oc adm policy add-scc-to-group nvidia-scc \
  system:serviceaccounts:<your-workload-namespace>
Helm + ArgoCD
2
Enable MIG with mixed strategy on the A100
MIG is the only way to get hard VRAM and SM isolation on one physical GPU with multiple concurrent workloads. mixed strategy lets each node advertise different slice sizes simultaneously. Start with this profile for your workload mix on an 80 GB A100:
yaml — MigConfig ConfigMap (node label selects profile)
apiVersion: v1
kind: ConfigMap
metadata:
  name: default-mig-parted-config
  namespace: gpu-operator
data:
  config.yaml: |
    version: v1
    mig-configs:
      # profile name referenced by node label below
      audio-and-llm:
        - devices: all
          mig-enabled: true
          mig-devices:
            "1g.10gb": 2   # Whisper / live audio
            "2g.20gb": 1   # 7B-13B LLM
            "3g.40gb": 1   # 30B-40B LLM
---
# Label the GPU node to activate the profile:
# kubectl label node <gpu-node> nvidia.com/mig.config=audio-and-llm
MIG Manager applies the config automatically Once the node is labeled, GPU Operator's MIG Manager DaemonSet reads the ConfigMap, drains GPU workloads on that node, repartitions the device, and restarts. No manual nvidia-smi required. The node will advertise nvidia.com/mig-1g.10gb, nvidia.com/mig-2g.20gb, and nvidia.com/mig-3g.40gb resources to the scheduler.
Hardware isolation
MIG profileCountVRAMWorkloadWhy
1g.10gb210 GB eachLive audio transcoding (Whisper / Riva)Whisper large-v3 fits in 1.5 GB; the slice handles many concurrent short requests. Two slices = redundancy.
2g.20gb120 GBSmall/mid LLM inference (7B–13B models)Llama-3 8B fp16 ≈ 16 GB. Comfortable headroom for KV cache.
3g.40gb140 GBLarger LLM inference (30B–40B) or fine-tune evalMistral 22B, CodeLlama 34B fit here. Also useful for batch embedding jobs.
70B+ models A 70B model (fp16 ≈ 140 GB) does not fit any single A100-80GB MIG slice. Options: quantize to 4-bit (≈35 GB → fits in 3g.40gb), use the full GPU without MIG (reconfigure the node dynamically), or run across two GPUs with tensor parallelism. With one GPU per DC, quantization is the practical path.
3
Run Triton Inference Server as the serving layer
Triton sits on top of MIG and handles multi-model serving, dynamic batching, and the audio pipeline in one place. Deploy one Triton pod per MIG slice, each with its own model repository. For audio: use the whisper or Riva ASR backend. For LLMs: use the vllm backend (continuous batching + PagedAttention without managing a separate vLLM process). Clients use gRPC or HTTP — no GPU knowledge required at the application layer.
yaml — Triton pods targeting specific MIG slices
# Pod targeting 1g.10gb slice (Whisper / audio)
resources:
  limits:
    nvidia.com/mig-1g.10gb: "1"
---
# Pod targeting 2g.20gb slice (7B-13B LLM)
resources:
  limits:
    nvidia.com/mig-2g.20gb: "1"
---
# Pod targeting 3g.40gb slice (30B-40B LLM)
resources:
  limits:
    nvidia.com/mig-3g.40gb: "1"
---
# Triton env vars to pin to its assigned MIG slice
env:
  - name: NVIDIA_VISIBLE_DEVICES
    valueFrom:
      fieldRef: { fieldPath: status.containerStatuses[0].allocatedResources['nvidia.com/mig-1g.10gb'] }
  - name: CUDA_VISIBLE_DEVICES
    value: "MIG-GPU-..."  # injected automatically by device plugin
Device plugin handles CUDA_VISIBLE_DEVICES You only need to set the resources.limits key. The device plugin injects NVIDIA_VISIBLE_DEVICES and CUDA_VISIBLE_DEVICES pointing at the specific MIG UUID automatically — Triton (and any CUDA process in the container) sees only that slice. Each pod is fully isolated at the hardware level.

Model repository layout

Each Triton pod mounts its own model repo volume. Minimal layout for your two workload types:

text — Triton model repository structure
# Pod 1: 1g.10gb slice — audio transcoding
model-repo-audio/
  whisper-large-v3/
    config.pbtxt          # backend: "openai", model_type: whisper
    1/
      model.py            # faster-whisper Python backend

# Pod 2: 2g.20gb slice — small LLM (7B-13B)
model-repo-llm-small/
  llama3-8b/
    config.pbtxt          # backend: "vllm"
    1/
      model.json          # {"model": "meta-llama/Meta-Llama-3-8B-Instruct", "gpu_memory_utilization": 0.85}

# Pod 3: 3g.40gb slice — large LLM (30B-40B)
model-repo-llm-large/
  mistral-22b/
    config.pbtxt          # backend: "vllm"
    1/
      model.json          # {"model": "mistralai/Mistral-Small-3.1-22B-Instruct-2503", "gpu_memory_utilization": 0.90}
Custom models Point model.json at a local path ("model": "/models/my-fine-tuned") instead of a HuggingFace ID. Mount the weights via a PVC. No Triton config change needed — the vLLM backend handles the rest.
Serving layer
4
Use Proxmox for isolation-heavy or non-OCP workloads
Proxmox with SR-IOV passthrough is the right path when a workload needs a full GPU in a VM (e.g., a Windows transcoding app, a vendor appliance, or a team that can't use containers). MIG and SR-IOV are mutually exclusive on the same GPU — you can't do both simultaneously. If you need both VM and pod workloads on one GPU, use KubeVirt on OCP with the NVIDIA vGPU manager instead; Proxmox is better suited for workloads that are fully VM-native and don't need K8s scheduling.
Proxmox / VMs
5
Monitor and right-size — Datadog (preferred) or FOSS stack
GPU Operator deploys DCGM Exporter automatically. Watch DCGM_FI_DEV_MEM_USED and DCGM_FI_PROF_GR_ENGINE_ACTIVE per MIG slice. With one GPU per DC, saturation of any slice directly impacts all workloads — alert before adding tenants.
PathComponentsNotes
Datadog preferred Datadog Agent DaemonSet → Datadog cloud Agent collects GPU metrics via NVML/DCGM directly. MIG slice metrics appear as nvidia.gpu.* tagged by profile. Monitors + dashboards replace PrometheusRule + Grafana. Zero extra infra if Datadog already in your DC.
FOSS DCGM Exporter → Prometheus → Grafana Fully self-hosted, stays on-prem. Import NVIDIA dashboard ID 12239. Alert via PrometheusRule CR. Right choice if no Datadog license or data-residency requirements block SaaS.
yaml — Datadog Agent on GPU node (key config)
datadog:
  apiKey: <DD_API_KEY>
  clusterName: ocp-dc1
  tags:
    - "env:production"
    - "dc:dc1"
  nvidia:
    enabled: true       # enables NVML/DCGM GPU metrics
  kubelet:
    tlsVerify: false    # required for OCP kubelet cert
agents:
  tolerations:
    - key: nvidia.com/gpu
      operator: Exists  # ensure agent lands on GPU node
Datadog + MIG Datadog Agent >= 7.40 required for MIG-aware metrics. Each MIG slice surfaces as a separate device with gpu_uuid and mig_profile tags — you get per-slice VRAM, SM utilization, and temperature without any extra configuration.
Observability
Bottom line: OCP + GPU Operator + MIG (mixed) + Triton is the path that uses your existing infrastructure, gives hard per-workload VRAM isolation, handles both the audio and LLM cases, and scales to custom models without architecture changes. Proxmox stays as the VM layer for workloads that genuinely need it.

Stack architecture

OCP Cluster ArgoCD GPU Operator deploys manages Bare-metal GPU Worker (OCP worker node) NVIDIA A100-80GB 1g.10gb 10 GB VRAM audio #1 1g.10gb 10 GB VRAM audio #2 2g.20gb 20 GB VRAM llm-small 3g.40gb 40 GB VRAM llm-large device plugin assigns MIG UUID Triton whisper backend gRPC :8001 Triton whisper backend gRPC :8001 Triton vLLM backend gRPC :8001 Triton vLLM backend gRPC :8001 Whisper large-v3 live audio → text Whisper large-v3 live audio → text Llama-3 8B / 13B small LLM inference Mistral 22B / custom large LLM inference clients — gRPC / HTTP (Triton API) MIG slice (hardware isolated) Triton pod (OCP) GPU Operator component
TL;DR architecture: ArgoCD deploys GPU Operator to the bare-metal OCP worker. GPU Operator's MIG Manager partitions the A100-80GB into 4 slices. Each slice is claimed by exactly one Triton pod via a typed resource request (nvidia.com/mig-Xg.Ygb); the device plugin injects the MIG UUID. Clients call the Triton gRPC/HTTP API — no GPU knowledge required at the application layer.

00 Hardware Sharing Mechanisms

Time-Slicing
Any NVIDIA GPU
Isolation: None — memory shared across pods
Dev environments, low-utilization inference, burstable workloads
MIG
Ampere+ (A100, H100, A30)
Isolation: Hard — dedicated SMs, memory bandwidth, engines
Production inference, multi-tenant SLA, consistent QoS
MPS
Any NVIDIA GPU
Isolation: None — concurrent CUDA contexts
Batch decode, embedding generation, idle-SM workloads

Fig Approach Diagrams

Four ways to share 8 GPUs. Each diagram shows where isolation lives, what controls scheduling, and what a workload actually sees.

bare metal host · 8× A100 client client client HTTP vLLM TP=8 · continuous batching single process all GPUs on every req resp tensor parallel GPU 0 80 GB GPU 1 80 GB GPU 2 80 GB GPU 3 80 GB GPU 4 80 GB GPU 5 80 GB GPU 6 80 GB GPU 7 80 GB NVLink · 600 GB/s all-reduce
vLLM on bare metal. One vLLM process claims all 8 GPUs. Every inference request enters a single HTTP endpoint, is batched by vLLM's continuous batching scheduler, and executed with tensor parallelism — meaning all 8 GPUs collaborate on every request simultaneously. No isolation between users; throughput maximized by keeping all SMs busy. NVLink handles the all-reduce between GPU pairs each forward pass.
OCP / K8s cluster GPU Worker Node taint: nvidia.com/gpu=present:NoSchedule GPU Operator device plugin · MIG mgr advertises nvidia.com/gpu:8 to K8s API server Pod 0 gpu: 2 tolerates NoSchedule Pod 1 gpu: 2 tolerates NoSchedule Pod 2 gpu: 2 tolerates NoSchedule Pod 3 gpu: 2 tolerates NoSchedule allocates 2 GPUs GPU 0 pod 0 GPU 1 pod 0 GPU 2 pod 1 GPU 3 pod 1 GPU 4 pod 2 GPU 5 pod 2 GPU 6 pod 3 GPU 7 pod 3 device plugin: exclusive allocation per pod · no sharing within a GPU
OCP / K8s with NVIDIA GPU Operator. The node carries a taint (nvidia.com/gpu=present:NoSchedule) that blocks non-GPU pods from landing here. GPU Operator deploys the device plugin, which advertises 8 schedulable GPU resources to the API server. Pods declare a toleration and a resource request (nvidia.com/gpu: 2); the scheduler matches taint to toleration and places the pod; the device plugin allocates specific GPU device files. Each GPU is exclusively owned by one pod — no time-sharing at the GPU level. Node selectors (nvidia.com/gpu.product: A100) let pods target specific GPU hardware.
GPU Operator MIG Manager applies profile: all-1g.10gb A100-80GB · hardware MIG partitioned 1g.10gb 1/7 SMs 10 GB HBM 1g.10gb 1/7 SMs 10 GB HBM 1g.10gb 1/7 SMs 10 GB HBM 1g.10gb 1/7 SMs 10 GB HBM 1g.10gb 1/7 SMs 10 GB HBM 1g.10gb 1/7 SMs 10 GB HBM 1g.10gb 1/7 SMs 10 GB HBM ↑ hardware boundary — no cross-slice memory or SM access Pod 0 mig-1g.10gb Pod 1 mig-1g.10gb Pod 2 mig-1g.10gb Pod 3 mig-1g.10gb Pod 4 mig-1g.10gb Pod 5 mig-1g.10gb Pod 6 mig-1g.10gb
MIG partitioning on one A100. The GPU Operator's MIG Manager applies a profile (all-1g.10gb) that instructs the A100's firmware to partition the die into 7 independent instances. Each instance gets a dedicated 1/7 of the streaming multiprocessors and 10 GB of HBM — enforced in silicon, not software. The device plugin advertises each instance as a separate resource (nvidia.com/mig-1g.10gb). Pods claim one instance each and get guaranteed, noise-free compute. One pod cannot see or affect another's memory. Profiles can be mixed (2g.20gb, 3g.40gb) to match workload sizes.
physical host · KVM/QEMU GPU PF 0 PCIe PF GPU PF 1 PCIe PF GPU PF 2 PCIe PF GPU PF 3 PCIe PF GPU PF 4 PCIe PF GPU PF 5 PCIe PF GPU PF 6 PCIe PF GPU PF 7 PCIe PF SR-IOV creates Virtual Functions (VFs) — one VF per GPU, assigned via IOMMU VF 0 VF 1 VF 2 VF 3 VF 4 VF 5 VF 6 VF 7 Hypervisor — not in the GPU data path after VF assignment VM 0 VF 0 direct DMA vGPU drv VM 1 VF 1 direct DMA vGPU drv VM 2 VF 2 direct DMA vGPU drv VM 3 VF 3 direct DMA vGPU drv VM 4 VF 4 direct DMA vGPU drv VM 5 VF 5 direct DMA vGPU drv VM 6 VF 6 direct DMA vGPU drv VM 7 VF 7 direct DMA vGPU drv
VM + SR-IOV GPU passthrough. Each physical GPU exposes a PCIe Physical Function (PF). SR-IOV creates one or more Virtual Functions (VFs) per PF; each VF is a lightweight PCIe endpoint with its own BAR space. The hypervisor assigns VFs to VMs via IOMMU — after that initial assignment, GPU DMA goes directly between the VM's memory and the GPU without any hypervisor involvement in the data path. Each VM runs a guest vGPU driver and sees the GPU as a native device. Isolation is enforced by IOMMU page tables: a VM cannot address memory outside its assigned VF. Cost: requires NVIDIA vGPU license for the host driver stack.

01 Time-Slicing via NVIDIA Device Plugin

Simplest option. Device plugin advertises N virtual GPUs per physical GPU. Pods request nvidia.com/gpu: 1 normally.

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: time-slicing-config
  namespace: gpu-operator
data:
  any: |-
    version: v1
    sharing:
      timeSlicing:
        replicas: 4   # 4 pods share 1 physical GPU
Operational reality — no enforcement Time-slicing is a scheduler fiction. The device plugin creates N virtual GPU entries, but there are zero enforcement mechanisms between them. Any pod can allocate all 80 GB of VRAM and all SMs simultaneously — there is no budget, no cgroup equivalent, nothing. A greedy (or buggy) pod exhausts VRAM on the physical GPU and all co-tenants OOM regardless of how small their request was. The NVIDIA driver does not enforce per-slice limits. K8s resource requests only count GPU units, not memory within a unit. Marketing material describes "shared access"; production reality is noisy-neighbor resource starvation. Reserve time-slicing for trusted dev/test workloads where VRAM consumption per pod is known and bounded. ~20% context-switch overhead is a secondary concern.

02 MIG Partitioning (Ampere+)

GPU Operator's MIG Manager reconfigures profiles via node label. Handles drain/reconfigure/rejoin automatically — no manual nvidia-smi mig calls.

yaml
# Apply to node to trigger reconfiguration
nvidia.com/mig.config: "all-3g.40gb"   # 2x MIG instances on A100-80GB
yaml — pod resource request
resources:
  limits:
    nvidia.com/mig-3g.40gb: 1

Common MIG Profiles — A100-80GB

ProfileSMsMemoryInstances / GPU
1g.10gb1/710 GB7
2g.20gb2/720 GB3
3g.40gb3/740 GB2
7g.80gb7/780 GB1
What MIG actually enforces — in hardware The A100/H100 firmware partitions the die into independent GPC (Graphics Processing Cluster) slices and dedicated HBM regions. Each MIG instance gets a fixed fraction of SMs, L2 cache, and memory bandwidth — all enforced by the GPU's MMU and SM scheduler in silicon, not software. One MIG tenant cannot access or exhaust another's VRAM or SMs regardless of what its workload does. This is the only NVIDIA sharing mode with genuine multi-tenant isolation and is the right choice anywhere resource guarantees matter.

03 MPS — Multi-Process Service

Multiple CUDA contexts run simultaneously (not time-sliced). Lower latency, no ctx-switch overhead. Better throughput for concurrent small kernels; same lack of memory isolation as time-slicing.

yaml
sharing:
  mps:
    replicas: 4
Shared memory space — no isolation MPS runs all CUDA clients through a single context in a shared address space. A process that over-allocates VRAM starves all other MPS clients on that GPU. Fault propagation is also broader: if one client segfaults, the MPS server crashes and all co-tenant processes on that GPU die simultaneously. Use MPS where workloads are trusted and the goal is to eliminate context-switch latency, not to provide resource guardrails.

04 Scheduling Optimizations

Bin-Packing

Default kube-scheduler spreads workloads across nodes. Override with MostAllocated to pack GPUs full before using next node.

yaml — KubeSchedulerConfiguration
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: MostAllocated
        resources:
        - name: nvidia.com/gpu
          weight: 10
        - name: cpu
          weight: 1
        - name: memory
          weight: 1

GPU Feature Discovery Node Selectors

yaml
nodeSelector:
  nvidia.com/gpu.product: "A100-SXM4-80GB"
  nvidia.com/gpu.memory: "81920"
  nvidia.com/mig.capable: "true"
Mixed GPU types on one node — assignment is non-deterministic The GPU Operator device plugin exposes all GPUs on a node as a fungible pool of nvidia.com/gpu units. It does not distinguish between individual GPU slots by VRAM size or model when assigning devices to pods. If a node contains mixed GPU models — e.g. some 8 GB and some 16 GB cards — there is no built-in mechanism to guarantee a pod gets the right one. A pod requesting nvidia.com/gpu: 1 may land on either card. Node selectors using GFD labels (nvidia.com/gpu.product, nvidia.com/gpu.memory) target the node, not a specific slot within it. The operational rule: GPU nodes must be homogeneous. Each node pool should contain one GPU model only. Use separate node groups per GPU type and let taints + node selectors steer pods to the right pool.
Projects that solve per-GPU-slot selection within a heterogeneous node Three options exist today, from GA to DIY:

1 — Kubernetes DRA + NVIDIA DRA Driver (K8s ≥ 1.34, GA)
Dynamic Resource Allocation (KEP-3063) replaces count-based device plugin slots with a claim-based API. NVIDIA's DRA driver publishes a ResourceSlice per node listing every GPU with structured per-device attributes: product name, memory capacity, architecture, NVLink topology, UUID. A pod declares a ResourceClaim with a CEL selector that filters against those attributes — the scheduler resolves it against actual device inventory, so a claim requesting an A100 on a node that has both A10 and A100 slots gets the right card:
yaml — ResourceClaim (DRA)
spec:
  devices:
    requests:
    - name: gpu
      selectors:
      - cel:
          expression: |
            device.attributes["gpu.nvidia.com"].productName.startsWith("A100") &&
            device.capacity["gpu.nvidia.com"].memory.isGreaterThan(quantity("40Gi"))
Requires GPU Operator with DRA mode enabled (opt-in; device plugin mode remains the default). This is the correct long-term answer — hardware-enforced, no shim needed.

2 — HAMi — CNCF Incubating (K8s ≥ 1.26, works today)
HAMi (project-hami/hami) intercepts CUDA calls via an injected libvgpu.so shim and exposes virtual GPU resources with per-pod VRAM limits — something the stock device plugin cannot do. It also supports GPU-type filtering within a heterogeneous node via pod annotations, and UUID-level targeting for exact-slot assignment. Covers NVIDIA, AMD, and Ascend under one scheduler. Isolation is software-enforced (CUDA interception), not hardware, so it sits between time-slicing and MIG on the guarantee spectrum. Production-ready on clusters that can't upgrade to 1.34 yet.

3 — Custom device plugin (any K8s version, DIY)
A patched device plugin can advertise nvidia.com/gpu-a100 and nvidia.com/gpu-a10 as separate resource types on the same node. Pods request the specific name. Works without DRA or HAMi, but requires maintaining a plugin fork and manual slot→name mapping via node-local discovery. No upstream project ships this out of the box.
SolutionK8s versionIsolationStatus
DRA + NVIDIA DRA driver≥ 1.34HardwareGA
HAMi≥ 1.26Software (CUDA shim)CNCF Incubating
Custom device pluginAnyHardwareDIY / operator fork

DRA vs. HAMi — when to use which

DRA + NVIDIA driverHAMi
What it isK8s core API (KEP-3063)External project — CUDA shim + scheduler extender
MechanismScheduler assigns specific GPU UUID via ResourceClaimMutating webhook injects libvgpu.so into every container
Per-slot GPU selectionYes — CEL on productName, memory, arch, UUIDYes — type filter + UUID annotation
Sub-GPU VRAM limitsNo — pod gets the whole assigned deviceYes — per-pod VRAM cap enforced at CUDA call level
SM utilization limitsNoYes — per-pod SM % cap
Isolation strengthHardware — device file binding, no bypass possibleSoftware — CUDA interception; bypassed if code avoids CUDA
Multi-vendorNVIDIA only (separate DRA driver per vendor)NVIDIA, AMD, Ascend, Cambricon, Hygon
K8s version≥ 1.26 alpha → GA ~1.34≥ 1.16 (essentially any)
OverheadNoneCUDA interception latency (small)

Core split: DRA answers which GPU slot does this pod get — hardware binding, no sub-GPU VRAM enforcement beyond MIG. HAMi answers how much of a GPU slot can this pod consume — soft quotas, multiple pods per physical device. They solve adjacent problems and can run together (HAMi has experimental DRA integration). Practical today: HAMi if K8s < 1.34, multi-vendor, or VRAM quotas needed. DRA if K8s ≥ 1.34 and clean K8s-native semantics matter more than sub-GPU splitting.

Priority Classes + Preemption

High-priority inference pods preempt low-priority training jobs when GPU capacity is needed.

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: inference-high
value: 1000
preemptionPolicy: PreemptLowerPriority
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: batch-train-low
value: 100
preemptionPolicy: Never

05 Dynamic MIG Reconfiguration

Shift MIG profiles by time of day to match workload patterns. GPU Operator handles node drain/reconfigure/rejoin — no manual intervention.

TimeProfileInstancesWorkload
Dayall-7g.80gb1× full A100Distributed training
Nightall-1g.10gb7× small slicesBatch inference
yaml — CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
  name: mig-reconfigure-night
spec:
  schedule: "0 20 * * *"   # 8pm
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: reconfigure
            image: bitnami/kubectl
            command:
            - kubectl
            - label
            - node
            - <gpu-node>
            - nvidia.com/mig.config=all-1g.10gb

Hyb Hybrid Approaches

The mechanisms above compose. The most useful production hybrid is vLLM + MIG: partition each GPU into hardware-isolated slices, then run an independent vLLM instance per slice. You get K8s scheduling flexibility, MIG's hard resource guarantees, and vLLM's continuous-batching throughput — simultaneously, with no tradeoff between them.

vLLM + MIG — per-model isolation at scale

Pick a MIG profile that fits your model's VRAM footprint, schedule one vLLM pod per MIG slice. Each pod sees an isolated GPU device with dedicated SMs and VRAM — noisy-neighbor exhaustion is impossible.

Profile sizing guide (A100-80GB)

Model size (FP16)MIG profileInstances per GPUDeployments on 8× A100
Embedding / <3B1g.10gb756
7B (Mistral, LLaMA-3-8B)2g.20gb324
13B3g.40gb216
70B (needs all 80 GB)7g.80gb18 (no MIG benefit)
70B+ (multi-GPU TP)whole GPUs, no MIG—depends on TP width
physical host · 8× A100 (2 shown) A100-80GB · GPU 0 3g.40gb 40 GB · 3/7 SMs hardware isolated ↕ 3g.40gb 40 GB · 3/7 SMs hardware isolated ↕ A100-80GB · GPU 1 3g.40gb 40 GB · 3/7 SMs hardware isolated ↕ 3g.40gb 40 GB · 3/7 SMs hardware isolated ↕ ··· ×6 nvidia.com/mig-3g.40gb: 1 vLLM pod llama-3-13b TP=1 · 40 GB vLLM pod mistral-7b-instruct TP=1 · 40 GB vLLM pod codellama-13b TP=1 · 40 GB vLLM pod embedding model TP=1 · 40 GB
vLLM + MIG on 8× A100. Each GPU splits into 2× 3g.40gb slices = 16 isolated vLLM deployments across the host. Each vLLM pod runs TP=1 (one MIG slice only) and gets guaranteed 40 GB VRAM + 3/7 of the SMs. Different models can share the same physical node with zero cross-model interference. K8s scales each deployment independently.
NVLink boundary — TP=1 limit MIG instances cannot communicate over NVLink. Each vLLM runs tensor_parallel_size=1. A model that requires tensor parallelism across multiple GPUs — typically 70B+ at FP16, or any model that exceeds a single slice's VRAM — cannot be served by a MIG-sliced pod. For those models, use whole GPUs with tensor_parallel_size=N and MIG disabled on those nodes. You can run both modes across different node pools in the same cluster.

MIG + MPS within a slice

Once a MIG slice is assigned to a pod, that pod can itself start an MPS daemon to multiplex multiple CUDA processes within the slice — running concurrent kernels without serialization, without surrendering the slice's hard VRAM floor. This stacks two levels of isolation: hardware boundary between slices, MPS concurrency within a slice. Useful when a slice is large enough to run several small inference replicas simultaneously and context-switch latency matters.

Isolation hierarchy MIG enforces the outer boundary (slice cannot be exceeded by any workload). MPS adds concurrent execution within that boundary. The MIG guarantee holds regardless of MPS behavior: a runaway MPS client cannot consume another slice's VRAM.

ISO VRAM Isolation Landscape

Not all GPU sharing approaches provide VRAM isolation. This matters operationally: without it, a greedy or buggy workload can exhaust all GPU memory and take down every co-tenant on that device.

ApproachVRAM isolationStrengthHardware requirement
MIGHardwareHard — GPU MMU enforces in siliconNVIDIA Ampere+ (A100, H100, A30)
AMD NPS4 (MI300X)HardwareHard — HBM physically partitionedAMD MI300X / MI325X only
SR-IOV / VM passthroughHardware (IOMMU)Hard — VMs only, not podsAny GPU with SR-IOV + hypervisor
K8s exclusive GPU per podHardwareHard — pod owns entire deviceAny GPU; not sub-GPU sharing
DRA (whole GPU)HardwareHard — specific device assigned to podAny GPU; K8s ≥ 1.34; not sub-GPU splitting
NVIDIA MPS pinned limitsDaemon (userspace)Soft-medium — MPS server enforces caps; shared fault domainVolta+, CUDA 11.5+, NVIDIA only
HAMiCUDA shimSoft — bypassed if workload avoids CUDA callsNVIDIA, AMD, Ascend (any K8s ≥ 1.16)
dmem cgroup controllerKernelHard — but no K8s/runc wiring yetIntel xe (Linux ≥ 6.14), AMD (≥ 6.15); no NVIDIA proprietary
Time-slicingNone——
MPS (no limits configured)None——
Triton concurrent modelsNone——

Non-MIG options in detail

NVIDIA MPS pinned memory limits (Volta+)

Set per-client VRAM caps inside the MPS daemon. Already wired into the k8s-device-plugin MPS sharing mode (splits evenly across replicas) and the NVIDIA DRA driver (MpsConfig.DefaultPinnedDeviceMemoryLimit). GKE supports it natively.

bash — MPS daemon config
# per-client cap set in MPS daemon
echo "set_default_device_pinned_mem_limit 0 4096m" | nvidia-cuda-mps-control

# or via environment variable before launching client
CUDA_MPS_PINNED_DEVICE_MEM_LIMIT="0=4096m" python inference.py
Shared fault domain MPS clients share a CUDA context. A fatal fault (illegal memory access, driver crash) in one client kills all co-located MPS clients simultaneously. The VRAM cap is enforced by the MPS daemon process — if it crashes, caps are gone. Not equivalent to MIG's hardware boundary.

AMD MI300X — NPS4 memory partitioning

The MI300X and MI325X support Non-Uniform Memory Access Partitioning (NPS4), which physically splits the 192 GB HBM stack into 4 × 48 GB regions. Combined with CPX (Compute Partitioning), up to 64 hardware partitions per 8-GPU node are possible. The AMD GPU Operator's Device Config Manager applies profiles declaratively; supported on OpenShift. This is the AMD hardware equivalent of MIG, limited to MI300-class GPUs.

Production-ready on OCP AMD GPU Operator + Device Config Manager handles partition setup and exposes slices as schedulable K8s resources. Consumer and Radeon cards have no equivalent. AMD MxGPU SR-IOV exists but is VM-only.

dmem cgroup controller (Linux ≥ 6.14)

A kernel-level VRAM cgroup with min/low/max semantics identical to the memory cgroup. Merged for Intel xe in Linux 6.14; AMD amdgpu support targeting 6.15/6.16; nouveau patch on mailing list. Does not work with the NVIDIA proprietary driver. No runc, containerd, or K8s wiring exists yet — cgroup files must be set manually via an NRI plugin or privileged DaemonSet. Known gaps: hipMallocAsync pools spill uncapped into system RAM; lowering dmem.max does not reclaim already-allocated memory. HAMi is evaluating dmem as its AMD backend.

Experimental — no K8s integration Not usable in production today without significant operator work. Watch HAMi and the amdgpu driver for when this lands as a first-class K8s resource.

Inf Inference Server Layer — Triton vs. vLLM

Triton Inference Server and vLLM sit at the application layer, above K8s GPU sharing. They don't replace MIG, device plugin, or DRA — they run inside whatever GPU resource(s) K8s assigns to their pod.

Hardware: 8× A100 ↓ K8s GPU sharing layer # MIG / time-slicing / DRA / device plugin ↓ pod gets GPU resource(s) ↓ Application / inference server layer # Triton and vLLM live here ↓ Triton's own internal sharing # instance_group, concurrent execution, dynamic batching

What Triton adds at the application layer

  • instance_group — load N copies of a model spread across specific GPU indices the pod owns. K8s gave pod GPUs 0–3; Triton puts 2 instances on GPU 0, 2 on GPU 1.
  • Concurrent model execution — multiple different models share one GPU simultaneously through Triton's own scheduler. No VRAM isolation between them (same noisy-neighbor exposure as MPS — application-level, not hardware).
  • Dynamic batching — coalesces requests before GPU invocation. Reduces effective GPU calls. Complements K8s-level sharing.
  • Rate limiter — caps simultaneous requests per model, prevents one model starving others on the same GPU.
  • Multi-backend + ensembles — TensorRT, PyTorch, ONNX, TF, OpenVINO, Python, and a vLLM backend. One Triton instance can serve a CNN, reranker, and LLM together in a single ensemble pipeline — something three separate vLLM instances cannot do.

Triton vs. vLLM

vLLMTriton
TargetLLMs onlyAny model type
LLM batchingContinuous batching + PagedAttention — best-in-class LLM throughputDynamic batching — good, but LLM-specific optimizations weaker without TRT-LLM backend
LLM backendBuilt-inTRT-LLM backend or vLLM backend
Multi-model servingNo — one model per instanceYes — model repository, ensembles, multiple models concurrently
RAG pipelinesNoYes — ensemble: embed → rerank → LLM in one server
Tensor parallelismNative, first-classVia TRT-LLM backend
K8s integrationDirect podDirect pod, or via KServe

Triton + MIG — same pattern as vLLM + MIG

One Triton pod per MIG slice, each slice running different models, isolated at hardware level. For mixed-model workloads this beats running separate vLLM instances:

MIG sliceTriton model(s)Use
1g.10gbembedding modelRAG retrieval
1g.10gbrerankerRAG reranking
3g.40gbLLM (TRT-LLM or vLLM backend)generation
2g.20gbvision / classificationmultimodal pre-processing

Each slice is hardware-isolated — the embedding model cannot OOM the LLM slice. Triton's ensemble API then wires the pipeline together over gRPC without leaving the node. A single vLLM instance cannot express this topology.

Triton does not solve K8s-level isolation Concurrent model execution within a Triton instance has no VRAM isolation between models. A misbehaving or memory-leaking model inside Triton can OOM the entire server and kill all co-located models — the same noisy-neighbor problem as MPS. For hard per-tenant isolation, MIG is still required at the K8s layer beneath Triton.

— Agent Orchestration: Turnstone

Turnstone is a self-hosted, local-first agent orchestration layer (Apache 2.0, Python 3.13+). It sits above your inference stack — Triton and vLLM do the model serving; Turnstone drives the agent loop, routes tool calls, and manages multi-turn workstreams across your cluster.

Where it fits in your stack

LayerComponentRole
InferenceTriton + vLLM backendServes models on MIG slices; OpenAI-compatible API
OrchestrationTurnstoneAgent loop, tool dispatch, multi-workstream routing
ToolsMCP serversShell, file ops, search, cluster APIs — exposed to agents
InfraOCP + GPU Operator + MIGHardware isolation, scheduling, observability

Key fit points

  • OpenAI-compatible target — Turnstone calls any /v1/chat/completions endpoint; your Triton vLLM backend exposes exactly that. Point Turnstone at the pod's ClusterIP service.
  • Local-first / no telemetry — aligns with on-prem DC requirements. No phone-home, no SaaS dependency.
  • Cluster-native — tasks fan out across nodes; your OCP cluster with multiple MIG slices maps directly to its multi-machine model.
  • MCP tool servers — any MCP server (kubectl, custom APIs, S3, databases) plugs in as agent tools, same protocol Claude Code uses.
  • LLM risk judge — evaluates each tool call before execution. Useful when agents have kubectl or shell access in a production cluster.

Routing agents to MIG slices

Configure separate Turnstone endpoints per slice type so heavy reasoning steps land on the larger slice and lighter tool calls use the smaller one:

yaml — Turnstone model config (conceptual)
models:
  - name: llm-large          # reasoning, planning, summarization
    api_base: http://triton-llm-large.gpu-operator.svc.cluster.local:8000/v1
    model: mistral-22b
  - name: llm-small          # tool-call generation, classification
    api_base: http://triton-llm-small.gpu-operator.svc.cluster.local:8000/v1
    model: llama3-8b
  - name: audio-asr          # audio → text (Whisper via Triton HTTP)
    api_base: http://triton-audio.gpu-operator.svc.cluster.local:8000/v2
    model: whisper-large-v3
Turnstone is orchestration, not isolation Routing different agent steps to different Triton pods is a scheduling choice, not a security boundary. VRAM isolation is still provided by MIG at the hardware layer — Turnstone sits above all of that and has no visibility into GPU resource allocation.

— Alternative: llm-d + Dual A100

llm-d is a Kubernetes-native LLM inference routing layer, co-developed by Red Hat and Google, built on the Gateway API Inference Extension (KEP-4804, targeting GA in K8s 1.33+). It adds KV-cache-aware load balancing and InferencePool semantics on top of plain vLLM pods — no Triton, no Turnstone, no custom routing code.

As a major llm-d contributor, Red Hat ships it as a supported component on OCP. If you are already on OCP and want a Red Hat-supported LLM routing layer, this is the path to evaluate alongside the Triton + Turnstone approach.

Reference architecture — dual A100-80GB

GPUMIG profileSlices → podsWorkload
A100 #01g+1g+2g+2g2× 1g.10gb → 2 Triton podsWhisper v3 ASR (gRPC, plain OCP Service)
A100 #01g+1g+2g+2g2× 2g.20gb → 2 vLLM podsLlama-3 8B · Pool llm-small
A100 #13g+3g2× 3g.40gb → 2 vLLM podsMistral 22B · Pool llm-large

Audio bypasses llm-d entirely — it routes gRPC directly to the Whisper OCP Service. LLM traffic hits the llm-d Gateway, which routes by model name to the appropriate InferencePool.

llm-d request path

Client → llm-d Gateway (Envoy · POST /v1/chat/completions, model field read) ↓ Endpoint Picker (extension: KV-cache-aware replica selection) ↓ InferencePool (llm-small: 2× vLLM 8B | llm-large: 2× vLLM 22B) ↓ vLLM pod (one per MIG slice, typed resource request)

Key components

  • InferencePool — a K8s CRD grouping replicas that serve the same model. Replaces a plain Service for LLM traffic; exposes replica-level metadata (KV-cache fill, queue depth) to the endpoint picker.
  • InferenceModel — maps a model name (e.g. mistral-22b) to an InferencePool, with criticality tiers (critical / sheddable). The Gateway reads this to route without client-side logic.
  • Endpoint Picker (ext-proc) — an Envoy external processor that receives each request, queries live replica stats, and returns the best backend. Default picker: lowest KV-cache utilization. Pluggable via the picker API.
  • Gateway — standard Envoy Proxy (OCP's Gateway API implementation), extended with the ext-proc filter. No custom proxy fork needed.

llm-d vs. Turnstone — what each layer solves

Concernllm-dTurnstone
LLM replica selectionKV-cache-aware, per-request routing across vLLM replicasRoutes to one pod per model tier; no replica-level awareness
Multi-step agent loopsNot in scope — single-request routing onlyCore purpose: RAG, tool calls, multi-model chaining
Model protocolOpenAI API passthroughOpenAI-compatible, MCP tool dispatch
Audio / non-LLMOut of scope; Whisper bypasses llm-dChains Whisper → LLM in one agent loop
OCP / RH supportRed Hat co-author, OCP-native Gateway APICommunity OSS, no OCP-specific support
MaturityKEP-4804 targeting GA in K8s 1.33+; pre-GA todayStable release, active development

These are not mutually exclusive. llm-d handles replica selection beneath the LLM endpoint; Turnstone handles orchestration above it. You could run Turnstone pointing at the llm-d Gateway and get both KV-cache routing and agent orchestration.

llm-d quick-start on OCP

bash — install llm-d operator via Helm (OCP)
helm repo add llm-d https://llm-d.ai/helm-charts
helm repo update

helm install llm-d-operator llm-d/llm-d-operator \
  --namespace llm-d-system \
  --create-namespace
yaml — InferencePool + InferenceModel for llm-small
apiVersion: inference.networking.x-k8s.io/v1alpha2
kind: InferencePool
metadata:
  name: llm-small
spec:
  targetPortNumber: 8000
  selector:
    app: vllm-llama3-8b
  extensionRef:
    name: llm-d-epp   # endpoint picker deployment
---
apiVersion: inference.networking.x-k8s.io/v1alpha2
kind: InferenceModel
metadata:
  name: llama3-8b
spec:
  modelName: llama3-8b
  criticality: Critical
  poolRef:
    name: llm-small
Pre-GA caveat Gateway API Inference Extension (KEP-4804) is alpha/beta today. API shapes may change before K8s 1.33 GA. Evaluate against your OCP version's Gateway API support level before committing production workloads. Red Hat's OCP AI reference architecture docs are the authoritative source for supported versions.

— Live Simulation: What the Diagram Shows

The interactive simulation animates the full request path through this architecture — 10 concurrent clients, day/night MIG reconfiguration, QoS preemption, and Datadog observability. This section explains what each animated element represents and where the simulation simplifies reality.

What each element represents

ElementWhat it showsActual mechanism
Moving packet (ball) One client request in flight HTTP/gRPC call to OCP ClusterIP Service; K8s routes to pod owning the matching MIG slice UUID
Packet exits client box at y≈300–320 All packets originate from the two visible arrows Client dot glow identifies which of the 10 clients fired; packet travels the route that matches its type. The dot position is visual shorthand — in reality every client connects to the same Service endpoint.
Packet through Turnstone (LLM-lg + Batch) Request routed via agent orchestration layer LLM-lg: Turnstone runs a multi-step loop — RAG lookup (MCP tool call) → Mistral 22B completion → optional synthesis pass. Client sees one response.
Batch: Turnstone chains two model calls — Whisper on 1g.10gb for transcription, then LLM 8B or 22B for summarization/extraction. The diagram shows one packet; reality has two sequential model invocations.
Packet direct to OCP (Audio + LLM-sm) Single-model call, no orchestration Client hits the ClusterIP Service directly. K8s routes to the pod holding the appropriate MIG slice. No agent loop needed for simple transcription or single-turn LLM completions.
Utilization bar fills on Triton pod Concurrent request count vs. max for that slice Real concurrency is bounded by VRAM and the vLLM/Triton max-batch setting, not a simple count. The bar is a proxy for saturation pressure.
P3 preemption flash Batch request cancelled to free the 3g.40gb slice K8s PriorityClass preemption evicts the Batch pod and reschedules it once the P2 request completes. In-flight requests to that pod fail; clients must retry.
Day/night reconfiguration overlay MIG Manager repartitioning the A100 MIG Manager drains all pods on the GPU node, runs nvidia-smi mig commands to destroy and re-create slices, then restarts the device plugin. All in-flight requests to that node fail during the 2–5 min window.
Datadog Agent → Datadog cloud GPU telemetry pipeline Datadog Agent DaemonSet on the GPU node collects per-MIG-slice metrics via NVML/DCGM and ships them to Datadog. Monitors + alerts replace PrometheusRule CRs. Requires Agent ≥ 7.40 for MIG-aware tagging.

Caveats — what the simulation omits or simplifies

  • Batch is shown as one packet, but is two model calls. A real batch pipeline routes Whisper (audio → text) and then the LLM (text → summary) as two sequential requests. The diagram shows one packet to keep the animation readable.
  • Turnstone dwell time not shown. Packets transit Turnstone nearly instantaneously in the animation. In reality the orchestration layer adds latency for context assembly, tool call round-trips, and inter-model hops — commonly 200 ms–2 s depending on tool count and MCP server latency.
  • Concurrency model is simplified. The simulation caps each slice at max=1 or max=2 for visual clarity. Real Triton/vLLM instances support dynamic batching; actual saturation depends on request size, sequence length, and KV-cache pressure — not just count.
  • Preemption is instantaneous in the simulation. Real K8s preemption involves a graceful termination period (default 30 s), pod eviction, and rescheduling. Affected clients must handle 5xx errors and retry.
  • MIG reconfiguration window is 2.5 s in the sim. Production reconfiguration takes 2–5 minutes: pod drain, slice destroy/create, device plugin restart, new pod scheduling. Plan maintenance windows accordingly.
  • No network latency, no KV-cache state. The animation treats all hops as instant. Real token-streaming latency, KV-cache warm-up on first request, and network RTT between services are not represented.
  • Turnstone is optional. If workloads are simple single-turn completions with no tool use, clients hit OCP Service directly. The Audio (P1) and LLM-sm (P2) clients in the simulation already show this bypass pattern.

06 Recommended Stack

NVIDIA GPU Operator # manages drivers, device plugin, MIG manager, MPS daemon └── GPU Feature Discovery # labels nodes with GPU model, VRAM, MIG capability └── DCGM Exporter # Prometheus metrics: utilization, memory, errors, NVLink Scheduling: kube-scheduler # bin-packing config for GPU workloads Volcano / JobSet # gang scheduling for distributed training Sharing strategy by workload: Dev / experimentation → time-slicing (4–8× replicas, trusted workloads only) Production inference → MIG (A100/H100) — hardware-isolated, no noisy-neighbor Multiple models, one host → vLLM + MIG (one vLLM pod per MIG slice) Latency-sensitive, trusted → MPS (concurrent kernels, no ctx-switch) Distributed training → full GPU + gang scheduling (no sharing) 70B+ inference → whole GPUs, TP=8, no MIG

K8s Value-Add vs Bare-Metal / Hypervisor vGPU

ConcernK8s Solution
Dynamic MIG reconfigurationGPU Operator + node label
Workload isolationNamespaces + ResourceQuota per team
GPU bin-packingScheduler MostAllocated strategy
ObservabilityDCGM Exporter + Prometheus/Grafana
Autoscaling GPU nodesKarpenter with GPU instance type constraints
Mixed workload priorityPriorityClass + preemption
License costNo NVIDIA vGPU license needed for MIG/time-slicing in pods

07 VMs: Boon or Bust?

Mostly bust. The VM layer adds overhead and cost that K8s + MIG already solves for most workloads.

Where VMs Hurt
  • Performance: ~20% overhead per CUDA API call through vGPU driver stack, plus VM memory eats VRAM headroom
  • License cost: NVIDIA vGPU requires per-concurrent-user licensing; MIG + time-slicing in pods is free
  • Complexity: hypervisor + VM images + K8s + GPU Operator + vGPU driver versions must match host exactly
  • Startup latency: pods spin up in seconds; VMs take 30–90s, killing autoscaling responsiveness
Where VMs Earn Their Cost
  • Hard multi-tenant isolation: different orgs, compliance (HIPAA, FedRAMP), untrusted code — container escape doesn't become tenant escape
  • Mixed OS: Windows workloads needing GPU access (DirectML, DirectX) — no other path
  • Already on hypervisor: EKS, GKE, AKS GPU nodes are VMs; overhead is baked in

Middle Ground: KubeVirt

Run VMs as K8s workloads with GPU passthrough. Gets K8s-native lifecycle + VM isolation + MIG hardware partitioning. Loses pod startup speed; adds KubeVirt operator complexity.

yaml — KubeVirt VirtualMachine
apiVersion: kubevirt.io/v1
kind: VirtualMachine
spec:
  template:
    spec:
      domain:
        devices:
          gpus:
          - deviceName: nvidia.com/mig-3g.40gb
            name: gpu0

Decision Matrix

ScenarioRecommendation
Internal teams, trusted workloadsBare metal K8s + MIG/time-slicing. Skip VMs.
Multi-org or compliance boundaryVMs (KubeVirt or hypervisor) + MIG passthrough
Cloud (EKS/GKE/AKS)Already on VMs — GPU passthrough node pools, accept it
Windows GPU workloadsVMs required, no alternative
Mixed Linux multi-tenant inferenceMIG + ResourceQuota sufficient, no VMs needed
Default: bare metal K8s nodes with GPU Operator. VMs only when compliance or OS requirements force it.

08 GPU Operator Install Reference

For OpenShift with ArgoCD, use the GitOps path in TL;DR step 1 — it handles CRI-O runtime, NFD separation, and version pinning. The raw Helm commands below are a reference for non-OCP clusters or one-off installs.

bash — generic Helm install (non-OCP)
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

helm install gpu-operator nvidia/gpu-operator \
  --namespace gpu-operator \
  --create-namespace \
  --set mig.strategy=mixed \
  --set devicePlugin.config.name=time-slicing-config
OCP differences On OpenShift: set operator.defaultRuntime=crio, disable bundled NFD if already installed (nfd.enabled=false), and bind the nvidia-scc SCC to workload namespaces before deploying GPU pods. See TL;DR steps 0–1 for the full OCP-specific flow.

09 Next Steps

Ranked by usefulness. Each builds directly on what's here.

High
manifests/ directory
The YAML snippets above as actual deployable files — time-slicing ConfigMap, MIG CronJob, PriorityClasses, scheduler config. Right now copy-paste only; a kubectl apply -f manifests/ path makes this repo operationally useful.
High
GitHub Actions CI — manifest validation
Run kubeconform against manifests/ on every push. Keeps YAML examples valid against real Kubernetes schemas and catches drift when profiles or API versions change.
High
H100 MIG profiles table
H100 has different slice sizes and a new 3g.47gb profile not present on A100. Increasingly the default inference hardware; the current table is A100-only and will mislead H100 users.
Medium
Karpenter NodePool example
GPU autoscaling via Karpenter is the dominant cloud pattern (EKS, AKS). A NodePool + EC2NodeClass snippet targeting GPU instance families (p4d, p3, g5) rounds out the scheduling section.
Medium
values/ Helm overrides
Three GPU Operator Helm values files — one each for time-slicing, MIG mixed, and MPS — so helm install -f values/time-slicing.yaml works without flags. Lowers the bar for first-time operators.
Lower
DCGM Grafana dashboard reference
Link or embed the NVIDIA DCGM dashboard JSON (dashboard ID 12239 on Grafana.com). Gives operators an immediate observability starting point for the metrics DCGM Exporter surfaces.
Lower
Benchmarks section
Sourced latency and throughput numbers comparing time-slicing, MIG, and MPS under inference load. Would turn the qualitative trade-off table into a data-backed decision guide.

Ref References

Primary source

NVIDIA official documentation

Kubernetes GPU projects

AMD

Kernel / hardware