The three mechanisms divide different things. Time-slicing divides time and caps nothing, MPS divides memory and thread ceilings through a userspace control daemon, and MIG divides the silicon, giving each instance its own crossbar ports, L2 cache banks, memory controllers and DRAM address busses. Two popular claims about them are wrong. MPS is not the safe middle on fault isolation: NVIDIA documents that a fatal GPU fault is reported to every client on the affected GPU without indicating which client caused it, and the MPS server moves from ACTIVE to FAULT, so on a single-card node the blast radius matches time-slicing. And MPS on MIG is not impossible: the Kubernetes device plugin refuses it at v0.20.0 while the MIG User Guide documents CUDA MPS on top of MIG with one control daemon per GPU instance. That plugin support has carried an experimental label since v0.15.0 in April 2024 and still carries it at v0.20.0, released 19 August 2026. Under both shared modes the split is only as good as your own arithmetic, because vLLM v0.27.1 computes its budget as total device memory times gpu_memory_utilization with a default of 0.92, so every replica sizes itself against the entire card. Set gpu_memory_utilization at or below 1 divided by the replica count on every pod that lands on a shared card, then print nvidia.com/gpu.sharing-strategy on every GPU node and check it against what you told your assessor.
The usual comparison of MIG vs MPS vs time-slicing puts the three on one axis, loosest to strictest, with MPS as the sensible middle. That framing is wrong in two specific places, and NVIDIA documents both.
MPS does not give you fault isolation. NVIDIA documents that a fatal GPU fault reaches every client on the affected GPUs without saying which one caused it. On a single-card node that is time-slicing's blast radius.
And MPS on MIG is not impossible: the device plugin refuses it at tag v0.20.0 while the MIG User Guide documents CUDA MPS on top of MIG with a worked example. Which layer says no changes what you do about it.
Three mechanisms, three different things being divided
Time-slicing divides time: each pod runs as many processes on the underlying GPU as it likes, and the GPU provides an equal share of time to all of them.
MPS divides space through a userspace control daemon that allows memory and compute resources to be explicitly partitioned, and enforces those limits per workload.
MIG divides the silicon: each instance's processors have separate and isolated paths through the entire memory system, with the on-chip crossbar ports, L2 cache banks, memory controllers and DRAM address busses assigned uniquely to one instance. NVIDIA calls that memory and fault isolation at the hardware layer, and never uses the phrase for MPS.
Its costs mirror that: fixed profiles, geometry changes that need the GPU free of user workloads, CAP_SYS_ADMIN and a driver-handle daemon stop to turn MIG mode on, and a per-card instance ceiling that varies by product, so 7 is not universal. Our RTX PRO 6000 vs H100 vs L40S comparison has those counts; isolating a workload from its host is a different control, priced in confidential GPU inference.
| Mechanism | Memory ceiling | Compute share | Fault domain, one card | Enforced by | Extra node privilege |
|---|---|---|---|---|---|
| Time-slicing | None, every replica sees the whole card | Equal share of time, no proportional guarantee | Shared, one crash takes them all | Nothing, CUDA interleaves contexts | None |
| MPS | Total memory over replicas, allocation past it fails | 100 over replicas, a cap not a reservation | Shared, the fault reaches every client, server goes ACTIVE to FAULT | The MPS control daemon, userspace | Privileged DaemonSet, hostPID true |
| MIG | Fixed by the profile | Fixed by the profile, dedicated SM slices | Isolated at the hardware layer | The silicon: crossbar ports, L2 banks, memory controllers, DRAM busses | None, but geometry changes need the card idle |
Time-slicing: one fault domain, and no proportional share
The README sentence to bring to a design review: nothing special is done to isolate workloads granted replicas from the same underlying GPU, each has access to the GPU memory, and they share a fault-domain, so if one crashes they all do.
The second misunderstanding is arithmetic. replicas multiplies a resource's advertised count; it does not divide capacity. A node with 8 GPUs at replicas: 10 advertises 80 nvidia.com/gpu, and a pod requesting two gets two scheduling tokens and no extra compute: more than one time-sliced GPU carries no proportional guarantee.
failRequestsGreaterThanOne turns that silent non-event into an UnexpectedAdmissionError: kubelet reports nvidia.com/gpu: 2 as too large because the maximum request size for shared resources is 1. NVIDIA recommends true and ships false for backwards compatibility, so a time-sliced node has it off unless you set it.
apiVersion: v1
kind: ConfigMap
metadata:
name: time-slicing-config
data:
any: |-
version: v1
flags:
migStrategy: none
sharing:
timeSlicing:
renameByDefault: true
failRequestsGreaterThanOne: true
resources:
- name: nvidia.com/gpu
replicas: 4Wiring it into the operator is a create and a patch:
kubectl create -n gpu-operator -f time-slicing-config.yaml
kubectl patch clusterpolicies.nvidia.com/cluster-policy \
-n gpu-operator --type merge \
-p '{"spec": {"devicePlugin": {"config": {"name": "time-slicing-config", "default": "any"}}}}'renameByDefault: true advertises nvidia.com/gpu.shared, which keeps whole-card workloads off a sliced node. Set it false and the product label is suffixed instead: nvidia.com/gpu.product = A100-SXM4-40GB-SHARED.
DCGM-Exporter also stops associating metrics to containers once time-slicing is enabled with the NVIDIA Kubernetes device plugin, and an equal share of time is not a service level objective, which puts a shared card and P95 TTFT targets in tension.
MPS: real caps, an equal-fraction default, and a fault domain people misread
apiVersion: v1
kind: ConfigMap
metadata:
name: mps-config
data:
any: |-
version: v1
sharing:
mps:
renameByDefault: true
resources:
- name: nvidia.com/gpu
replicas: 4nvidia-cuda-mps-control -d echo "set_default_device_pinned_mem_limit 0 24G" | nvidia-cuda-mps-control echo "set_default_active_thread_percentage 50" | nvidia-cuda-mps-control echo "ps" | nvidia-cuda-mps-control
echo "ps" | nvidia-cuda-mps-control # PID ID SERVER DEVICE NAMESPACE COMMAND echo "terminate_client <server-pid> <client-pid>" | nvidia-cuda-mps-control # returns 0, the client observes CUDA_ERROR_MPS_CLIENT_TERMINATED kill -9 <client-pid>
What the control daemon actually issues
The plugin's MPS control daemon at v0.20.0 sets compute mode with nvidia-smi -i <uuid> -c EXCLUSIVE_PROCESS, starts nvidia-cuda-mps-control -d, then pipes it set_default_device_pinned_mem_limit <index> <limit>M per device and set_default_active_thread_percentage <pct>. Both come from total memory over the replica count and integer 100 over it. Integer division does not round up, so replicas: 3 gives every client 33 percent and leaves 1 percent unassigned. Your config carries no memory figure and no percentage, only that replica count: The memory ceiling is real: allocating beyond the preset limit returns an out of memory error, and the limit accounts for CUDA's internal device allocations. The thread percentage is not: setting it reserves nothing, and kernels from different client contexts may execute on the same SM depending on load balancing. Being a ceiling, it need not track the memory limit's replica count. On a three-tenant 80 GB card the plugin hands out 33 percent and roughly 26 GB each; issuing the commands yourself buys a tighter memory ceiling with a looser compute one. Those defaults apply to clients of future servers. A process that should get less carries CUDA_MPS_PINNED_DEVICE_MEM_LIMIT in its environment, and a tenant's CUDA_MPS_ACTIVE_THREAD_PERCENTAGE only further constrains the daemon's limit. No tenant raises its own ceiling.
Error containment, read carefully
MPS client processes have fully isolated GPU address spaces, and NVIDIA calls what MPS offers a limited form of error containment. The next sentence decides your design: the fatal fault reaches every client on the affected GPUs without saying which one caused it, the server sits in FAULT until all of them exit, and new clients are rejected with CUDA_ERROR_MPS_SERVER_NOT_READY meanwhile. Containment is to a subset of GPUs; on one card that subset is the card.
Killing an MPS client is unsupported
Terminating an MPS client via CTRL-C or signals is not supported and will lead to undefined behavior, after which the server and all its clients must be restarted. Pod termination is a signal, so every rollout, eviction and OOMKill on an MPS node is that operation. The safe path is a control command a preStop hook has to imitate, with the PID translated into the control daemon's namespace.
Ceilings, users, and the experimental label
Two client ceilings exist: the plugin caps replicas per device at 48 where compute capability is at or above 7.5 and 16 below, while each MPS server supports up to 60 client CUDA contexts per device at the default CUDA_DEVICE_MAX_CONNECTIONS of 2. Only one user may have an active MPS server, other users' requests queueing into serialised exclusive access, and the -multiuser-server mode that lets different UIDs connect drops isolation between them. MPS in the plugin has been experimental since v0.15.0, published April 2024, and still is at v0.20.0, published 19 August 2026. Roughly 28 months.
| Key | Where it lives | Default | What it does |
|---|---|---|---|
sharing.timeSlicing.resources[].replicas | Plugin config or Operator ConfigMap | None, 2 or more | Multiplies the advertised count |
sharing.timeSlicing.renameByDefault | Same | false | Advertises <resource>.shared, product label unsuffixed |
failRequestsGreaterThanOne | Either sharing block | false under timeSlicing, true under mps | Fails a pod asking for more than one shared replica |
sharing.mps.resources[].replicas | Same | None, 2 or more, capped at 48 from compute capability 7.5 up | Sets the memory ceiling and thread percentage |
flags.migStrategy | Same | none | none, single or mixed; mixed exposes nvidia.com/mig-<Ng>.<M>gb |
mps.enableHostPID | Plugin Helm values | true | Runs the MPS daemon in the host PID namespace |
mig.strategy | GPU Operator v26.7.0 Helm values | single | How MIG devices are labelled cluster-wide |
MPS on MIG: what the plugin forbids and what the driver documents
The device plugin README at v0.20.0 has one line on this: sharing with MPS is currently not supported on devices with MIG enabled. That is a statement about the plugin, not the hardware. The MIG User Guide says CUDA MPS is supported on top of MIG, the 48-client maximum simply lowered proportionally to the Compute Instance size.
They agree once you notice what the plugin daemon does first. It sets EXCLUSIVE_PROCESS, and the MIG guide says that mode is not supported in MIG mode because multiple MPS servers are used, one per MIG GPU instance. NVIDIA does not state that as its reason, so treat it as two documents agreeing, not causation. Outside Kubernetes the combination is a loop over instance UUIDs, one control daemon and pipe directory each:
for mig_device in "${MIG_DEVICES[@]}"; do
export CUDA_MPS_PIPE_DIRECTORY=/tmp/$mig_device
mkdir -p "$CUDA_MPS_PIPE_DIRECTORY"
sudo CUDA_VISIBLE_DEVICES=$mig_device \
CUDA_MPS_PIPE_DIRECTORY=/tmp/$mig_device \
nvidia-cuda-mps-control -d
CUDA_MPS_PIPE_DIRECTORY=/tmp/$mig_device \
CUDA_VISIBLE_DEVICES=$mig_device \
my-cuda-app &
doneInside the plugin the pairing that works is mixed MIG strategy plus time-slicing: the supported resource types are nvidia.com/gpu and any emerging from the mixed strategy. A nvidia.com/mig-3g.40gb can be time-sliced. It cannot run MPS.
The constraint that breaks mixed clusters: sharing is a node property
From the same README: with both time-slicing and MPS, the same sharing method is applied to all GPUs on a node, and you cannot configure sharing on a per-GPU basis. They are also mutually exclusive, and the plugin picks MPS when both blocks appear. The isolation decision therefore lives in the node pool, not the pod spec, at a granularity of one config key per node: a multi-key ConfigMap plus the nvidia.com/device-plugin.config label. The devices selector does not help; the plugin logs that customising it is not yet supported and ignores it.
The MPS row ends arguments: all MPS client behavior will be attributed to the MPS server process by system monitoring and accounting tools, nvidia-smi and NVML named explicitly. Tell an assessor Department A's inference is separable from Department B's while the node label says mps or time-slicing, and NVIDIA's documentation contradicts you. On-premise there is no per-instance invoice either.
MPS also costs you node posture
In the plugin's Helm chart at v0.20.0 the MPS control daemon DaemonSet runs with hostPID: true (because mps.enableHostPID defaults to true) and both containers privileged: true, so the MPS server can find its own PID via /proc/self. The Kubernetes Baseline Pod Security Standard disallows both, as two controls each allowing only undefined or false. The plain device plugin DaemonSet has neither, so the ordering inverts: the loosest mechanism costs nothing in node posture, the middle one costs a Pod Security Admission exception on every GPU node.
What this changes on-premise
The decision only exists when you own the cards. A cloud tenant needing a different isolation posture rents a different instance type and gets a billing line as evidence. An on-premise team splits a fixed pool into node pools, one per posture, absorbs the stranded capacity in each, and grants that privileged hostPID exception in its own name on every GPU node. That is the GPU-layer form of the boundary argument in cloud vs on-premise AI security and cost, and the same silo-and-pool decision repeats at every layer, as in multi-tenant RAG isolation. A MIG geometry change is not a label edit either: MIG Manager requires that no user workloads run on the GPUs being configured, and terminates the node's GPU pods to get there. On a locked-down estate that is a change window escorted like the release path in our air-gapped vLLM deployment guide.
The evidence you lose when you share
What DRA changes
The DRA driver v0.5.0, published 19 August 2026, models sharing properly. A GpuConfig or MigDeviceConfig in the resource.nvidia.com/v1beta1 group carries a sharing block with a TimeSlicing or MPS strategy, and under MPS an mpsConfig with defaultActiveThreadPercentage, defaultPinnedDeviceMemoryLimit and multiUser. A ResourceClaim referencing that object does nothing on a default install: MPSSupport, TimeSlicingSettings, ConsumableShares, DynamicMIG, DeviceMetadata, PassthroughSupport and NVMLDeviceHealthCheck are all Alpha and disabled behind draDriver.featureGates, and a cluster runs either a GPUCluster for DRA or a ClusterPolicy for the device plugin, not both. Plan for DRA; do not write this year's isolation commitment against it.
| Mechanism | Per-container DCGM-Exporter metrics | Attribution in nvidia-smi and NVML | Node label recording the choice |
|---|---|---|---|
| Dedicated GPU | Yes | Yes | nvidia.com/gpu.sharing-strategy=none |
| Time-slicing | No, NVIDIA documents the limitation | Yes, processes are separate | nvidia.com/gpu.sharing-strategy=time-slicing plus nvidia.com/gpu.replicas |
| MPS | Not documented as supported | No, all of it lands on the MPS server process | nvidia.com/gpu.sharing-strategy=mps plus nvidia.com/mps.capable |
| MIG | Per instance | Per instance, with GI and CI columns | nvidia.com/mig.strategy plus nvidia.com/mig.config |
Sizing a vLLM worker against a share it cannot see
Here is the line that causes the outage. In vLLM v0.27.1 the worker computes its requested memory as the total memory in its initial snapshot times gpu_memory_utilization, rounded up. Total, not free. The snapshot reads the accelerator runtime directly, with no branch for time-slicing, MPS or a replica count, and the default is 0.92.
The docstring is unambiguous that the split is your job: the limit is per-instance, it does not matter whether another vLLM instance is running on the same GPU, and two instances on one GPU should each be set to 0.5. Nothing in the sharing layer does that for you, because the replica count never reaches the serving process. The second replica reads the whole card's total memory, asks for 92 percent of it, and hits one of two failures depending on timing.
ValueError: Free memory on device cuda:0 (12.4/79.1 GiB) on startup is less than desired GPU memory utilization (0.92, 72.8 GiB). Decrease GPU memory utilization or reduce GPU memory used by other processes. AssertionError: Error in memory profiling. Initial free memory 61.3 GiB, current free memory 68.9 GiB. This happens when other processes sharing the same container release GPU memory while vLLM is profiling during initialization. To fix this, ensure consistent GPU memory allocation or isolate vLLM in its own container.
The first is the polite one: the co-tenant already held memory at startup and the check caught it. The second fires the other way round, when a co-tenant releases memory mid-profile and free memory ends up higher than the worker measured. The rule that prevents both: gpu_memory_utilization at or below 1 over the replica count, minus headroom, on every pod.
# one of four replicas on an 80 GB H100: the 0.92 default would claim the whole card vllm serve /models/qwen3-8b \ --gpu-memory-utilization 0.22 \ --max-model-len 8192 # resources: # limits: # nvidia.com/gpu.shared: 1
The dedicated-card version of that memory equation is derived in full in debugging vLLM KV cache OOM.
Under MIG the instance is the device: nvidia-smi reports each MIG device's own memory total, so on an 80 GB H100 the card row reads 87MiB of 81559MiB while each 3g.40gb device row reads 44MiB of 40448MiB.
Memory slices are roughly eighths of the card and SM slices roughly sevenths, which is why the seven-instance geometry addresses 70 GB and leaves an eighth unassigned.
A 10 GB instance holds weights, activations and the KV cache, and the memory-versus-concurrency arithmetic in our Kimi K3 GPU sizing analysis shows how fast the cache side grows. Seven tenants each get a slice that will not serve a useful model; three 2g.20gb instances partition the card more honestly. Nor can you span slices: NCCL is not supported with MIG, and with driver R570 only P2P between MIG instances on the same GPU is supported. If prefill and decode contention is the real problem, split the phases across pools instead, as in prefill and decode disaggregation.
| Profile | Fraction of memory | Fraction of SMs | Instances available | Memory addressed at that count |
|---|---|---|---|---|
1g.10gb | 1/8 | 1/7 | 7 | 70 GB of 80 GB |
1g.20gb | 1/4 | 1/7 | 4 | 80 GB |
2g.20gb | 2/8 | 2/7 | 3 | 60 GB of 80 GB |
3g.40gb | 4/8 | 3/7 | 2 | 80 GB |
4g.40gb | 4/8 | 4/7 | 1 | 40 GB |
7g.80gb | Full | 7/7 | 1 | 80 GB |
The decision path, and the four lines you write down
Work backwards from the isolation requirement, never forwards from the pod count.
Write the same four lines down whichever row you land on, because none appear in a pod spec:
This week, print the four sharing-related labels on every GPU node and check them against what you have already told an assessor.
kubectl get nodes -o custom-columns=\ NODE:.metadata.name,\ SHARING:.metadata.labels.nvidia\\.com/gpu\\.sharing-strategy,\ REPLICAS:.metadata.labels.nvidia\\.com/gpu\\.replicas,\ MIG:.metadata.labels.nvidia\\.com/mig\\.strategy,\ MPS:.metadata.labels.nvidia\\.com/mps\\.capable
If that column reads time-slicing on a node where two departments run, fix the document before you move the workload. For the platform decisions around this one, start at our AI development tools pillar.
| What you need bounded | Turn on | Why the others are wrong here |
|---|---|---|
| A tenant must not read or crash another tenant's work | MIG | Time-slicing shares a fault domain, MPS reports a fatal fault to every client |
| A workload must not exhaust the card's memory, one trust boundary | MPS | Time-slicing has no memory ceiling, MIG needs a drain to change geometry |
| Bursty low-duty-cycle work in one team, notebooks and eval jobs | Time-slicing | MPS adds a privileged hostPID DaemonSet for caps you do not need, MIG strands slices |
| One large model that does not fit a slice | None, use the whole card | MIG cannot help: NCCL is not supported with MIG |
| Mixed requirements on one node | None as configured today | Sharing is node-wide, so split the pool |
FAQ
Quick answers to the questions this post tends to raise.



