AMD GPU Operator on Kubernetes — DaemonSets, device plugin, multi-tenant patterns
How the AMD GPU Operator builds on the NVIDIA GPU Operator playbook, what's different (cert-manager hard-required, KMM-based driver, no MIG on MI300X), how to install via helm, validate, and operate at scale.
help for the full list, or solutions for copy-paste fix recipes.If you have run the NVIDIA GPU Operator on RKE2 / EKS / GKE / OpenShift in production, the AMD GPU Operator will feel like the second-system effect that learned a few lessons. AMD shipped its operator in 2024, several years after NVIDIA's, and the architecture reflects what AMD watched everyone else trip over: cert-manager is a hard prerequisite (no half-installs), the kernel module is managed via the upstream KMM operator (not a custom DaemonSet), and the DeviceConfig CRD gives you the "one chart, many node configurations" pattern out of the box.
It is also younger software. ROCm 7 plus AMD GPU Operator 1.4+ are getting close to NVIDIA-Operator-grade reliability, but you should expect more rough edges than on the NVIDIA side. This page covers what's there, how to install, how to validate, and the multi-tenant patterns that work today.
What the operator deploys
| Component | NVIDIA equivalent | AMD component / DaemonSet | What it does |
|---|---|---|---|
| Driver / kernel module | nvidia-driver-daemonset | KMM-managed Module CR (amdgpu) | Loads amdgpu on each node |
| Device plugin | nvidia-device-plugin-daemonset | amd-gpu-device-plugin | Advertises amd.com/gpu to kubelet |
| Node labeller / GFD | gpu-feature-discovery | amd-gpu-node-labeller | Labels nodes with GPU model/family |
| Container runtime hooks | nvidia-container-toolkit-daemonset | (none — relies on standard --device mounts) | Different model — see below |
| Metrics | nvidia-dcgm-exporter | amd-device-metrics-exporter | Prometheus metrics |
| Validator | nvidia-operator-validator | (validation pod per DeviceConfig) | Checks the stack works end-to-end |
| MIG manager | nvidia-mig-manager | (none — MI300X has no MIG; CDNA 4 partition manager planned) | n/a yet |
The biggest architectural divergence is how the kernel module gets onto the node. NVIDIA's operator ships its own privileged DaemonSet that runs make modules_install inside a container. AMD reuses KMM (Kernel Module Management), a Red Hat–originated upstream Kubernetes operator that handles kernel module lifecycle generically. KMM is more flexible (you can sign modules, you can target specific kernel versions, you can blacklist conflicting in-tree modules) but has more moving parts.
The other divergence: there is no AMD container-toolkit equivalent. NVIDIA injects a runtime hook into containerd that automatically mounts /dev/nvidia* and the userspace libraries. AMD relies on the device plugin advertising /dev/kfd and /dev/dri/render* and you mount the userspace as a normal volume (or bake it into your image). Less magic, more explicit.
Prerequisites
| Requirement | Version | Why |
|---|---|---|
| Kubernetes | v1.29+ | DeviceConfig CRD uses v1alpha1, requires recent k8s |
| Helm | v3.2+ | Chart format |
| cert-manager | v1.13+ | Mandatory. Operator's webhook needs TLS |
| Node Feature Discovery (NFD) | v0.15+ | Auto-installed unless you set --set node-feature-discovery.enabled=false |
| Kernel Module Management (KMM) | v2.0+ | Auto-installed unless --set kmm.enabled=false |
| Container runtime | containerd 1.7+ or CRI-O | Standard k8s runtimes; no AMD-specific hooks needed |
| Underlying nodes | Ubuntu 22.04 / 24.04, RHEL 9, kernel 5.14+ / 6.x | See ROCm for full matrix |
The cert-manager requirement is non-negotiable. Don't skip it; the operator's admission webhook silently fails closed without TLS.
# Install cert-manager first
helm repo add jetstack https://charts.jetstack.io --force-update
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.15.1 \
--set crds.enabled=true
# Wait for cert-manager to be ready
kubectl wait --for=condition=Ready --timeout=120s pod \
-n cert-manager -l app.kubernetes.io/instance=cert-manager
Two driver modes (same trade-off as NVIDIA)
Like the NVIDIA operator, the AMD operator supports two driver modes:
Mode A: pre-installed driver (spec.driver.enable: false)
The host already has amdgpu-dkms installed and amdgpu is loaded. The operator skips driver management and only deploys device-plugin / labeller / metrics.
This is the right choice for:
- Bare-metal fleets where you bake drivers into the OS image.
- Air-gapped clusters where pulling a driver-container image is awkward.
- Mixed-vendor clusters where you want clear ownership boundaries.
Mode B: operator-managed out-of-tree driver (spec.driver.enable: true)
The operator (via KMM) loads the kernel module from a container image at pod startup. The host kernel doesn't need amdgpu-dkms installed.
This is the right choice for:
- Greenfield clusters where you want declarative driver management.
- Heterogeneous kernel versions across nodes that you don't want to handle manually.
- Frequent ROCm version churn where bumping the operator chart is cleaner than re-imaging.
For most established fleets I've seen, Mode A (pre-installed) wins. You get more control, fewer surprises during outages, and the OS-level drift management you already have (Ansible / Puppet / OS image rebuilds) handles the driver consistently.
Helm install: the canonical path
# Add the AMD operator chart repo
helm repo add rocm https://rocm.github.io/gpu-operator
helm repo update
# Install
helm install amd-gpu-operator rocm/gpu-operator-charts \
--namespace kube-amd-gpu \
--create-namespace \
--version=v1.4.1
That installs the operator itself. It does not yet do anything to your nodes — the operator is just a control plane. You also need to apply a DeviceConfig Custom Resource to tell it which nodes to target.
DeviceConfig: the per-pool configuration
DeviceConfig is the AMD operator's central CRD. One per "pool of nodes that share configuration" — usually one for the whole cluster, but you can have multiple if you have heterogeneous nodes.
Example: pre-installed driver, default plugin, metrics on
apiVersion: amd.com/v1alpha1
kind: DeviceConfig
metadata:
name: production-gpus
namespace: kube-amd-gpu
spec:
driver:
enable: false # host has amdgpu-dkms already
devicePlugin:
devicePluginImage: rocm/k8s-device-plugin:latest
nodeLabellerImage: rocm/k8s-device-plugin:labeller-latest
metricsExporter:
enable: true
serviceType: "ClusterIP" # or NodePort if scraping from outside
image: docker.io/rocm/device-metrics-exporter:v1.4.1
rbacConfig:
enable: true # service account + RBAC for the exporter
selector:
feature.node.kubernetes.io/amd-gpu: "true"
Apply it:
kubectl apply -f deviceconfig.yaml
The selector says: target only nodes that NFD has labelled amd-gpu: "true". If NFD isn't installed (you set --set node-feature-discovery.enabled=false), use a manual label:
kubectl label node mynode amd.com/gpu-node=true
And update the selector accordingly.
Example: operator-managed driver
apiVersion: amd.com/v1alpha1
kind: DeviceConfig
metadata:
name: greenfield-gpus
namespace: kube-amd-gpu
spec:
driver:
enable: true
blacklist: true # blacklist any in-tree amdgpu the host might load
image: docker.io/rocm/amdgpu-driver:30.20.1-22.04
version: "30.20.1"
devicePlugin:
devicePluginImage: rocm/k8s-device-plugin:latest
nodeLabellerImage: rocm/k8s-device-plugin:labeller-latest
metricsExporter:
enable: true
serviceType: "ClusterIP"
image: docker.io/rocm/device-metrics-exporter:v1.4.1
selector:
feature.node.kubernetes.io/amd-gpu: "true"
The blacklist: true is important — without it, on a node that already has the in-tree amdgpu loaded by systemd-udev, the operator will fail to load its own version. Blacklist forces a clean slate.
Validation: end-to-end
Same flow as NVIDIA: install, validate, run a smoke pod.
# 1. Operator pods running?
$ kubectl get pods -n kube-amd-gpu
NAME READY STATUS RESTARTS AGE
amd-gpu-operator-controller-manager-xxx 2/2 Running 0 2m
amd-gpu-operator-device-plugin-xxx 1/1 Running 0 1m
amd-gpu-operator-node-labeller-xxx 1/1 Running 0 1m
amd-gpu-operator-metrics-exporter-xxx 1/1 Running 0 1m
# 2. Nodes have amd.com/gpu allocatable?
$ kubectl describe node my-mi300x-node | grep -i amd.com/gpu
amd.com/gpu: 8
# 3. Or as a custom-columns sweep:
$ kubectl get nodes -o custom-columns=NAME:.metadata.name,GPU:'.status.allocatable.amd\.com/gpu'
NAME GPU
my-mi300x-node-0 8
my-mi300x-node-1 8
my-mi300x-node-2 8
# 4. DeviceConfig status
$ kubectl get deviceconfigs -n kube-amd-gpu -o yaml | grep -A 10 status:
# 5. Run a smoke pod
$ kubectl run amd-smi-test --rm -it --restart=Never \
--image=docker.io/rocm/pytorch:latest \
--overrides='{"spec":{"containers":[{"name":"amd-smi-test","image":"docker.io/rocm/pytorch:latest","command":["bash","-c","amd-smi monitor -ptu && rocminfo | grep gfx"],"resources":{"limits":{"amd.com/gpu":"1"}}}]}}'
If the smoke pod sees one GPU at the right gfx target, you are end-to-end functional.
The full smoke-pod manifest
apiVersion: v1
kind: Pod
metadata:
name: amd-rccl-test
namespace: default
spec:
restartPolicy: Never
containers:
- name: amd-rccl-test
image: docker.io/rocm/pytorch:latest
command: ["/bin/bash", "-c"]
args:
- |
set -e
echo "=== rocm-smi ==="
rocm-smi --showid
echo "=== rocminfo gfx ==="
rocminfo | grep -E "Name:.*gfx"
echo "=== xGMI topo ==="
rocm-smi --showtopo
echo "=== PyTorch sanity ==="
python -c "import torch; print('CUDA available (RCCL via NCCL backend):', torch.cuda.is_available()); print('Device count:', torch.cuda.device_count()); print('Arch list:', torch.cuda.get_arch_list())"
resources:
limits:
amd.com/gpu: 8
requests:
amd.com/gpu: 8
For multi-node validation, run an MPI-launched rccl-tests job using Volcano or the Kubeflow MPIOperator — the same pattern that runs nccl-tests on NVIDIA fleets, just with the AMD container image and the amd.com/gpu resource type.
Resource model: what amd.com/gpu actually means
A pod requests amd.com/gpu: 1 and gets:
- One GPU, exposed via
/dev/dri/renderNplus/dev/kfd. - Read/write group permissions configured by the device plugin.
- HSA-visible to the runtime;
rocminfoandrocm-smiwill show only the assigned GPU.
The Kubernetes scheduler treats it as opaque-extended-resource — same model as nvidia.com/gpu. No fractional resources, no priority among consumers within a node.
What it does not do (yet) on MI300X:
- No fractional GPUs. You cannot request
amd.com/gpu: 0.5. This is the same constraint NVIDIA had pre-MIG. - No GPU isolation between containers. Two containers requesting
amd.com/gpu: 1each on a 2-GPU node will get one each, but if one of them setsHIP_VISIBLE_DEVICES=0,1it can see and use both. The device plugin restricts what the kernel exposes; it does not enforce HSA-level isolation.
Multi-tenant patterns: what works on MI300X today
The most-asked question coming from NVIDIA: "I'm used to slicing my H100 into 7 MIG instances for cheap inference. How do I do that on MI300X?"
Short answer: you don't, on MI300X. CDNA 3 has no MIG equivalent. The MI300X is a single GPU, atomically. You cannot slice it.
Slightly longer answer: CDNA 4 (MI350X / MI355X) has partitioning modes, but they are coarser than MIG and the operator support is younger. Specifically:
| Mode | What it does | Equivalent in NVIDIA terms |
|---|---|---|
| SPX (Single Partition) | One GPU = one partition. The default. | Default H100 mode |
| CPX (Compute Partition) | Two compute partitions per OAM module. | (rough) MIG 1g.40gb-style, 2 instances |
| TPX (Triple) | Three compute partitions | (rough) more aggressive slicing |
| QPX (Quad) | Four compute partitions | (rough) MIG 1g.10gb / 1g.20gb |
These are runtime-configurable on CDNA 4 silicon (a rocm-smi call to the partition controller, then a node reboot). The AMD GPU Operator is gaining a "GPU Partitioning Manager" component analogous to NVIDIA's nvidia-mig-manager — at time of writing this is in early access.
For MI300X today, the practical multi-tenant patterns are:
Pattern 1: One container = one whole GPU
The simplest model. Workloads request amd.com/gpu: 1 and get a whole GPU. Inference services that don't need 192 GB of HBM still consume a whole GPU's worth of scheduler slot. This is fine for high-throughput, low-tenant-count clusters (one model per GPU, batch size large).
Pattern 2: Time-sliced via Triton / vLLM / TGI
If you're serving multiple inference models on one GPU, use a per-GPU multi-tenant inference server like vLLM with multi-LoRA, or NVIDIA Triton (yes, it works on AMD), or TGI (HuggingFace) with per-request routing. The Kubernetes scheduler still sees one amd.com/gpu, but the inference server multiplexes inside.
This is the pattern most production AMD inference deployments use today. It's how Microsoft Azure runs Llama models on MI300X SKUs at scale.
Pattern 3: One node, many containers, careful HIP_VISIBLE_DEVICES
If you have an 8-GPU node and want to run 8 different small inference services, pin each via HIP_VISIBLE_DEVICES=N and request amd.com/gpu: 1 per pod. The scheduler will place 8 pods on the node; each gets a different GPU. This works but requires the workload to behave when it sees only its assigned GPU.
Pattern 4: Wait for MI355X partitioning
For genuine MIG-style slicing — multiple isolated tenants on one GPU — wait for CDNA 4 partition support to mature in the operator. As of mid-2026, basic CPX support is shipping; TPX/QPX needs more bake time.
Metrics and observability
The AMD device-metrics-exporter is the analog of dcgm-exporter. Same Prometheus exposition format, similar label conventions:
# A typical metric line
amd_gpu_temperature_celsius{gpu="0",hostname="my-mi300x-node-0",model="MI300X"} 38.0
amd_gpu_power_watts{gpu="0",hostname="my-mi300x-node-0",model="MI300X"} 142.0
amd_gpu_memory_used_bytes{gpu="0",hostname="my-mi300x-node-0",model="MI300X"} 5.36e+08
amd_gpu_memory_total_bytes{gpu="0",hostname="my-mi300x-node-0",model="MI300X"} 2.06e+11
amd_gpu_xgmi_link_error_count{gpu="0",link="0",hostname="my-mi300x-node-0"} 0
To hook into Prometheus:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: amd-device-metrics-exporter
namespace: kube-amd-gpu
spec:
selector:
matchLabels:
app: amd-device-metrics-exporter
endpoints:
- port: metrics
interval: 15s
The metrics worth alerting on:
| Metric | Alert when |
|---|---|
amd_gpu_uncorrectable_errors_total | > 0 (any UE → drain node) |
amd_gpu_temperature_celsius | > 95 sustained (thermal limit approaching) |
amd_gpu_throttle_status | THERM or PROCHOT for > 60s |
amd_gpu_xgmi_link_error_count | rate-of-change > 0 (link degrading) |
amd_gpu_memory_used_bytes / total_bytes | > 0.95 (OOM risk for the workload) |
amd_gpu_power_watts | < 100 W during workload (GPU idle when expected busy) |
Anyone running NVIDIA DCGM dashboards can port them with a sed-script — the metric names are different but the conceptual list is identical.
Operational patterns at scale
Node draining for maintenance
# Same as any k8s drain, but be aware that GPU pods may have to spin up large model weights
# from network storage on a different node — drain windows are longer than CPU-only
kubectl drain my-mi300x-node-3 --ignore-daemonsets --delete-emptydir-data
# When done:
kubectl uncordon my-mi300x-node-3
Driver upgrade via the operator
In Mode B (operator-managed driver), bumping the ROCm version is:
spec:
driver:
enable: true
image: docker.io/rocm/amdgpu-driver:30.20.2-22.04
version: "30.20.2"
The operator will roll the driver one node at a time (controlled by KMM's update strategy). Pods on a node draining for driver update will be evicted; if your training job spans the whole cluster, you need to coordinate the upgrade with the application.
In Mode A (pre-installed driver), the upgrade flow is the OS image / DKMS path — totally outside the operator. See ROCm.
Heterogeneous fleets (mixed AMD models)
If you have a mix of MI250X (gfx90a), MI300X (gfx942), and MI355X (gfx950) in the same cluster, you need:
- The node-labeller component, which adds
amd.com/gpu.product=MI300Xstyle labels. - Workload selectors that match the right model:
spec:
nodeSelector:
amd.com/gpu.family: "Instinct"
amd.com/gpu.product: "MI300X"
- Container images compiled for the right gfx targets, OR a multi-arch image with
PYTORCH_ROCM_ARCH=gfx90a;gfx942;gfx950.
Mixing AMD and NVIDIA in one cluster
Yes, you can do this. They're independent resources (nvidia.com/gpu vs amd.com/gpu), independent operators, independent device plugins. The risks:
- Both operators install NFD and KMM. Coordinate
--set node-feature-discovery.enabled=falseand--set kmm.enabled=falseon whichever you install second. - Some monitoring stacks assume NVIDIA-only metric names. Verify your Grafana dashboards work with both.
- Workload schedulers (Volcano, Kueue, MCAD) need to be configured to understand both resource types.
This is increasingly common in mixed-vendor clouds. It works.
What's missing today (the honest list)
Things the AMD GPU Operator does not have, that the NVIDIA Operator does:
- Mature MIG-equivalent. CDNA 3 has nothing; CDNA 4 partitioning is shipping but rough.
- Confidential computing flow. NVIDIA has GPU TEE / Confidential VMs in production. AMD's equivalent (SEV-SNP + GPU TEE) is in earlier stages on the operator side.
- GPU Direct Storage operator integration. NVIDIA bundles GDS configuration; AMD users do it manually.
- Fabric-manager component. Not strictly needed (no NVSwitch), but the lack of a single-pane-of-glass for IF link health is felt — you query each node individually.
- Time-slicing config in the operator. NVIDIA supports
replicas: Nto oversubscribe a GPU; AMD's analog is in the device plugin but not as well-integrated into the operator chart.
These will close over time. None are hard blockers for production today; they're papercuts.
See also
- AMD GPU stack overview — what the underlying hardware is
- ROCm stack — the user-mode and kernel pieces this operator manages
- RCCL vs NCCL — multi-GPU collectives inside this operator's pods
- AMD troubleshooting — debug failures that surface as Pod errors
- NVIDIA GPU Operator — the analogous NVIDIA chart
- ArgoCD — GitOps deployment patterns apply identically
- GPUDirect RDMA — fabric integration is configured per-node, same as NVIDIA