TL;DR Recommended Path for Your Environment
- 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)
| Option | MIG support | Hypervisor overhead | VM live migration | Verdict |
|---|---|---|---|---|
| Bare-metal OCP worker (recommended) | Full | None | N/A — bare metal | Use this |
| PCIe passthrough to OCP VM | Partial — some boards disable MIG under IOMMU | IOMMU + VFIO layers | Permanently broken for that VM | Avoid |
| vGPU (VMware/Proxmox MxGPU) | None — mutually exclusive with MIG | vGPU manager process | Limited | Avoid |
nvidia.com/gpu=present:NoSchedule so only GPU workloads land on it.
Joining the bare-metal node to OCP
# 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
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.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
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:
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>
mixed strategy on the A100mixed strategy lets each node advertise different slice sizes simultaneously. Start with this profile for your workload mix on an 80 GB A100: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
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.
| MIG profile | Count | VRAM | Workload | Why |
|---|---|---|---|---|
1g.10gb | 2 | 10 GB each | Live audio transcoding (Whisper / Riva) | Whisper large-v3 fits in 1.5 GB; the slice handles many concurrent short requests. Two slices = redundancy. |
2g.20gb | 1 | 20 GB | Small/mid LLM inference (7B–13B models) | Llama-3 8B fp16 ≈ 16 GB. Comfortable headroom for KV cache. |
3g.40gb | 1 | 40 GB | Larger LLM inference (30B–40B) or fine-tune eval | Mistral 22B, CodeLlama 34B fit here. Also useful for batch embedding jobs. |
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.
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.# 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
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:
# 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}
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.
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.| Path | Components | Notes |
|---|---|---|
| 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. |
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
gpu_uuid and mig_profile tags — you get per-slice VRAM, SM utilization, and temperature without any extra configuration.
Stack architecture
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
Fig Approach Diagrams
Four ways to share 8 GPUs. Each diagram shows where isolation lives, what controls scheduling, and what a workload actually sees.
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.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.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.
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
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.
# Apply to node to trigger reconfiguration nvidia.com/mig.config: "all-3g.40gb" # 2x MIG instances on A100-80GB
resources: limits: nvidia.com/mig-3g.40gb: 1
Common MIG Profiles — A100-80GB
| Profile | SMs | Memory | Instances / GPU |
|---|---|---|---|
1g.10gb | 1/7 | 10 GB | 7 |
2g.20gb | 2/7 | 20 GB | 3 |
3g.40gb | 3/7 | 40 GB | 2 |
7g.80gb | 7/7 | 80 GB | 1 |
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.
sharing: mps: replicas: 4
04 Scheduling Optimizations
Bin-Packing
Default kube-scheduler spreads workloads across nodes. Override with MostAllocated to pack GPUs full before using next node.
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
nodeSelector: nvidia.com/gpu.product: "A100-SXM4-80GB" nvidia.com/gpu.memory: "81920" nvidia.com/mig.capable: "true"
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.
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:
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"))
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.
| Solution | K8s version | Isolation | Status |
|---|---|---|---|
| DRA + NVIDIA DRA driver | ≥ 1.34 | Hardware | GA |
| HAMi | ≥ 1.26 | Software (CUDA shim) | CNCF Incubating |
| Custom device plugin | Any | Hardware | DIY / operator fork |
DRA vs. HAMi — when to use which
| DRA + NVIDIA driver | HAMi | |
|---|---|---|
| What it is | K8s core API (KEP-3063) | External project — CUDA shim + scheduler extender |
| Mechanism | Scheduler assigns specific GPU UUID via ResourceClaim | Mutating webhook injects libvgpu.so into every container |
| Per-slot GPU selection | Yes — CEL on productName, memory, arch, UUID | Yes — type filter + UUID annotation |
| Sub-GPU VRAM limits | No — pod gets the whole assigned device | Yes — per-pod VRAM cap enforced at CUDA call level |
| SM utilization limits | No | Yes — per-pod SM % cap |
| Isolation strength | Hardware — device file binding, no bypass possible | Software — CUDA interception; bypassed if code avoids CUDA |
| Multi-vendor | NVIDIA only (separate DRA driver per vendor) | NVIDIA, AMD, Ascend, Cambricon, Hygon |
| K8s version | ≥ 1.26 alpha → GA ~1.34 | ≥ 1.16 (essentially any) |
| Overhead | None | CUDA 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.
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.
| Time | Profile | Instances | Workload |
|---|---|---|---|
| Day | all-7g.80gb | 1× full A100 | Distributed training |
| Night | all-1g.10gb | 7× small slices | Batch inference |
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 profile | Instances per GPU | Deployments on 8× A100 |
|---|---|---|---|
| Embedding / <3B | 1g.10gb | 7 | 56 |
| 7B (Mistral, LLaMA-3-8B) | 2g.20gb | 3 | 24 |
| 13B | 3g.40gb | 2 | 16 |
| 70B (needs all 80 GB) | 7g.80gb | 1 | 8 (no MIG benefit) |
| 70B+ (multi-GPU TP) | whole GPUs, no MIG | — | depends on TP width |
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.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.
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.
| Approach | VRAM isolation | Strength | Hardware requirement |
|---|---|---|---|
| MIG | Hardware | Hard — GPU MMU enforces in silicon | NVIDIA Ampere+ (A100, H100, A30) |
| AMD NPS4 (MI300X) | Hardware | Hard — HBM physically partitioned | AMD MI300X / MI325X only |
| SR-IOV / VM passthrough | Hardware (IOMMU) | Hard — VMs only, not pods | Any GPU with SR-IOV + hypervisor |
| K8s exclusive GPU per pod | Hardware | Hard — pod owns entire device | Any GPU; not sub-GPU sharing |
| DRA (whole GPU) | Hardware | Hard — specific device assigned to pod | Any GPU; K8s ≥ 1.34; not sub-GPU splitting |
| NVIDIA MPS pinned limits | Daemon (userspace) | Soft-medium — MPS server enforces caps; shared fault domain | Volta+, CUDA 11.5+, NVIDIA only |
| HAMi | CUDA shim | Soft — bypassed if workload avoids CUDA calls | NVIDIA, AMD, Ascend (any K8s ≥ 1.16) |
dmem cgroup controller | Kernel | Hard — but no K8s/runc wiring yet | Intel xe (Linux ≥ 6.14), AMD (≥ 6.15); no NVIDIA proprietary |
| Time-slicing | None | — | — |
| MPS (no limits configured) | None | — | — |
| Triton concurrent models | None | — | — |
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.
# 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
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.
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.
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.
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
| vLLM | Triton | |
|---|---|---|
| Target | LLMs only | Any model type |
| LLM batching | Continuous batching + PagedAttention — best-in-class LLM throughput | Dynamic batching — good, but LLM-specific optimizations weaker without TRT-LLM backend |
| LLM backend | Built-in | TRT-LLM backend or vLLM backend |
| Multi-model serving | No — one model per instance | Yes — model repository, ensembles, multiple models concurrently |
| RAG pipelines | No | Yes — ensemble: embed → rerank → LLM in one server |
| Tensor parallelism | Native, first-class | Via TRT-LLM backend |
| K8s integration | Direct pod | Direct 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 slice | Triton model(s) | Use |
|---|---|---|
1g.10gb | embedding model | RAG retrieval |
1g.10gb | reranker | RAG reranking |
3g.40gb | LLM (TRT-LLM or vLLM backend) | generation |
2g.20gb | vision / classification | multimodal 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.
— 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
| Layer | Component | Role |
|---|---|---|
| Inference | Triton + vLLM backend | Serves models on MIG slices; OpenAI-compatible API |
| Orchestration | Turnstone | Agent loop, tool dispatch, multi-workstream routing |
| Tools | MCP servers | Shell, file ops, search, cluster APIs — exposed to agents |
| Infra | OCP + GPU Operator + MIG | Hardware isolation, scheduling, observability |
Key fit points
- OpenAI-compatible target — Turnstone calls any
/v1/chat/completionsendpoint; 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:
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
— 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
| GPU | MIG profile | Slices → pods | Workload |
|---|---|---|---|
| A100 #0 | 1g+1g+2g+2g | 2× 1g.10gb → 2 Triton pods | Whisper v3 ASR (gRPC, plain OCP Service) |
| A100 #0 | 1g+1g+2g+2g | 2× 2g.20gb → 2 vLLM pods | Llama-3 8B · Pool llm-small |
| A100 #1 | 3g+3g | 2× 3g.40gb → 2 vLLM pods | Mistral 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
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
| Concern | llm-d | Turnstone |
|---|---|---|
| LLM replica selection | KV-cache-aware, per-request routing across vLLM replicas | Routes to one pod per model tier; no replica-level awareness |
| Multi-step agent loops | Not in scope — single-request routing only | Core purpose: RAG, tool calls, multi-model chaining |
| Model protocol | OpenAI API passthrough | OpenAI-compatible, MCP tool dispatch |
| Audio / non-LLM | Out of scope; Whisper bypasses llm-d | Chains Whisper → LLM in one agent loop |
| OCP / RH support | Red Hat co-author, OCP-native Gateway API | Community OSS, no OCP-specific support |
| Maturity | KEP-4804 targeting GA in K8s 1.33+; pre-GA today | Stable 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
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
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
— 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
| Element | What it shows | Actual 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
K8s Value-Add vs Bare-Metal / Hypervisor vGPU
| Concern | K8s Solution |
|---|---|
| Dynamic MIG reconfiguration | GPU Operator + node label |
| Workload isolation | Namespaces + ResourceQuota per team |
| GPU bin-packing | Scheduler MostAllocated strategy |
| Observability | DCGM Exporter + Prometheus/Grafana |
| Autoscaling GPU nodes | Karpenter with GPU instance type constraints |
| Mixed workload priority | PriorityClass + preemption |
| License cost | No 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.
- 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
- 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.
apiVersion: kubevirt.io/v1 kind: VirtualMachine spec: template: spec: domain: devices: gpus: - deviceName: nvidia.com/mig-3g.40gb name: gpu0
Decision Matrix
| Scenario | Recommendation |
|---|---|
| Internal teams, trusted workloads | Bare metal K8s + MIG/time-slicing. Skip VMs. |
| Multi-org or compliance boundary | VMs (KubeVirt or hypervisor) + MIG passthrough |
| Cloud (EKS/GKE/AKS) | Already on VMs — GPU passthrough node pools, accept it |
| Windows GPU workloads | VMs required, no alternative |
| Mixed Linux multi-tenant inference | MIG + ResourceQuota sufficient, no VMs needed |
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.
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
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.
Ref References
Primary source
- Sharing NVIDIA GPUs at the System Level: Time-Sliced and MIG-Backed vGPUs — Colfax Research. Basis of this guide.
NVIDIA official documentation
- NVIDIA GPU Operator — GPU Operator overview, installation, MIG manager, MPS daemon configuration.
- NVIDIA MIG User Guide — MIG architecture, profiles, partitioning, and instance management on A100/H100/A30.
- NVIDIA Multi-Process Service (MPS) — MPS architecture, client/server model, use cases.
- MPS environment variables —
CUDA_MPS_PINNED_DEVICE_MEM_LIMITand related per-client VRAM cap configuration. - NVIDIA Triton Inference Server — model repository, instance groups, dynamic batching, ensemble pipelines, backend support.
- GPU Operator + KubeVirt — vGPU manager integration; VMs on K8s; DRA-mode mixed container/VM nodes.
Kubernetes GPU projects
- HAMi — Heterogeneous AI Computing Virtualization Middleware (CNCF Incubating) — CUDA shim, per-pod VRAM limits, GPU-type filtering, multi-vendor support.
- NVIDIA DRA Driver API reference —
ResourceClaimstructured attributes;MpsConfig.DefaultPinnedDeviceMemoryLimit. - k8s-device-plugin issue #764 — MPS pinned memory limits:
cudaMemGetInfostill reports full card; tracking issue. - Intel GPU Plugin for Kubernetes —
shared-dev-num, SR-IOV VF support, Intel dGPU scheduling. - HAMi AMD core — dmem cgroup evaluation — tracking issue for using the Linux
dmemcgroup as the AMD VRAM enforcement backend.
AMD
- AMD ROCm — Compute and Memory Partition Modes — SPX/CPX compute partitioning; NPS1/NPS4 memory partitioning on MI300X/MI325X.
- AMD GPU Operator — Applying Partition Profiles — Device Config Manager; NPS4 setup on OpenShift.
Kernel / hardware
- dmem cgroup controller — VRAM control in Linux — Phoronix coverage of the kernel-level GPU memory cgroup merged in Linux 6.14 for Intel xe.
- Linux 6.15 DRM fixes — amdgpu dmem cgroup support timeline.