all_lessons/kubernetes_genai/06lesson 6 / 18

Part II - GPU platform

GPU Nodes as a Platform Layer

Lesson 05 got the model weights into the pod — the multi-gigabyte tensors are now sitting on a volume the container can read. But weights are inert without a chip to run them on, and a freshly provisioned GPU node does not announce itself to Kubernetes the way a CPU does. Plain Kubernetes counts CPU and memory natively; it has no built-in idea of what a GPU is, how many a node has, or whether one is already in use. This lesson is about the platform plumbing that turns a bare GPU box into a node the scheduler can target — and the sharing modes that decide whether one physical card serves one tenant or seven. Get this layer wrong and your pod lands on a GPU node, starts, and then fails the moment it asks for CUDA.

Source coverage
PDF Chapter 3: Kubernetes and GPUs; refreshed with current DRA status (Kubernetes v1.36 era, 2026).
Linear position
Prerequisite: Lesson 05 (Getting model weights into pods) — the weights are reachable inside the container, but nothing yet guarantees the container can see a GPU device. You also know basic Kubernetes scheduling: a pod requests resources, the scheduler finds a node that has them.
New capability: Understand how a GPU becomes a schedulable Kubernetes resource — the driver/discovery/plugin stack the platform must install first — and how to choose a sharing mode (whole-GPU, MIG, time-slicing, DRA) by trading isolation against utilization.
The plan
Five moves. (1) Establish the mental model: a GPU is not a number, it is a stack of layers that each must be present before a pod can use the chip. (2) Walk that stack as a pipeline — hardware to driver to discovery to plugin/DRA to scheduler to pod visibility — defining each component at first use and showing how the NVIDIA GPU Operator installs the whole thing as platform code. (3) Make the resource model concrete with a pod spec requesting nvidia.com/gpu: 1, and show exactly what information that single number throws away. (4) Compare the four sharing/isolation modes with a worked utilization example and an interactive packing widget. (5) Failure modes, a platform checklist, and the hand-off to multi-GPU topology.

1 · The GPU is a stack, not a count

When you write resources: { limits: { cpu: "2" } }, Kubernetes already understands CPU intimately — the kubelet measures it, cgroups enforce it, the scheduler bin-packs it. A GPU gets none of this for free. The kernel does not know how to talk to the card without a vendor driver; the container runtime does not know how to expose the device files; and the Kubernetes scheduler has no field called "GPU" at all. Every one of those gaps is filled by a separate piece of software, and if any layer is missing the symptom is the same maddening one: the pod schedules fine and then cannot find a GPU.

So the mental model to carry through this lesson is: a GPU is a stack. Driver, runtime, device plugin (or DRA driver), node labels, topology, isolation mode, telemetry, and scheduling policy — each is a distinct layer, and "the GPU works" means all of them are present and agree. The book's Chapter 3 walks this stack precisely because most "my GPU pod doesn't work" incidents are platform incidents, not model incidents. The fix lives below the model server, not in it.

Why "GPU: 1" is the wrong unit
A CPU core is roughly fungible — two cores on a node are interchangeable. A GPU is not. An H100 with 80 GB of HBM (High Bandwidth Memory — the on-package memory where model weights and the KV cache live) is a completely different resource from an L4 with 24 GB, yet a naive cluster advertises both as "1 GPU." Worse, two GPUs in the same node may or may not share a fast NVLink interconnect, which changes multi-GPU performance by an order of magnitude (lesson 07). Counting GPUs loses memory size, generation, isolation mode, and topology — all the things that actually determine whether your model fits and runs fast.

2 · The platform stack, layer by layer

Here is the full pipeline a request for a GPU traverses, from silicon to a process that can call CUDA. Each stage is a real component you install or rely on; the names matter because each is a separate failure point.

1 · HardwareThe physical GPU on a node — e.g. an NVIDIA H100. Present but invisible to software until a driver loads.
2 · Driver + runtimeThe kernel-mode NVIDIA driver, the CUDA libraries, and the container toolkit that injects device files (/dev/nvidia*) and libraries into containers. Without this the chip is dark.
3 · DiscoveryNode Feature Discovery (NFD) and GPU Feature Discovery (GFD) inspect the node and write Kubernetes labels advertising what is there: GPU type, count, driver version, MIG state, topology.
4 · Device plugin OR DRA driverThe piece that registers the GPU with the kubelet as an allocatable resource. The classic device plugin advertises a count like nvidia.com/gpu; the newer DRA driver exposes claim-based requests (move 4).
5 · Scheduler resource modelThe scheduler now sees a node offering nvidia.com/gpu: 8 (or named MIG profiles, or DRA device classes) and matches pod requests to nodes that have free units.
6 · Pod device visibilityThe pod lands, the runtime mounts the device, and the process inside finally sees a GPU when it calls nvidia-smi or initializes CUDA.

Two of these names deserve a sharper definition at first use. Node Feature Discovery (NFD) is a Kubernetes add-on that detects hardware and system features on each node and exposes them as node labels; GPU Feature Discovery (GFD) is the GPU-specific companion that adds labels like nvidia.com/gpu.product=H100, nvidia.com/gpu.memory=81920, and nvidia.com/mig.strategy. A device plugin is the kubelet extension mechanism (a gRPC service) by which a vendor advertises non-CPU/memory resources; NVIDIA's device plugin is what makes nvidia.com/gpu a thing the scheduler can count and allocate.

Installing and version-matching all six layers by hand on every node is exactly the "snowflake node" trap. The NVIDIA GPU Operator exists to collapse it: it is a Kubernetes Operator that installs and lifecycle-manages the driver, container toolkit, device plugin, NFD/GFD, and DCGM telemetry (move 5) from a single cluster-level configuration. The platform lesson is that this stack is code — a declarative policy you version and review — not a checklist someone runs once over SSH and forgets.

The whole stack must agree on versions
The most common silent breakage is a driver/runtime mismatch: the kernel driver is one CUDA generation, the container toolkit injects libraries from another, and the model server crashes on a cryptic CUDA error: forward compatibility. Because the GPU Operator pins these together, version skew is the strongest argument for letting it own the stack rather than assembling layers à la carte.

3 · The resource model — and what a single number hides

With the device plugin in place, requesting a GPU looks deceptively like requesting CPU. Here is the canonical whole-GPU pod spec:

apiVersion: v1
kind: Pod
metadata:
  name: llm-server
spec:
  containers:
  - name: vllm
    image: vllm/vllm-openai:latest
    resources:
      limits:
        nvidia.com/gpu: 1        # whole GPU; this pod owns the device

Note that GPUs are requested only as a limit, and the count is an integer — you cannot ask for "0.5 GPU" through the basic device plugin. The pod that gets nvidia.com/gpu: 1 owns the entire physical device with full memory and compute isolation. Simple, but the scheduler made its decision knowing only the integer 1. It did not know whether that GPU has 24 GB or 80 GB of HBM, nor whether the node's two GPUs are NVLink-connected. A 70B-parameter model in 16-bit needs roughly 70 × 2 = 140 GB of weights plus KV cache — it will not fit on one 80 GB card, but nvidia.com/gpu: 1 happily schedules it anyway, and the pod OOMs at load time. The number was the wrong unit (move 1) made operational.

Hardware partitioning gives the scheduler richer units. With MIG (Multi-Instance GPU) — NVIDIA's feature that splits one physical GPU into hardware-isolated slices — a single A100 can be carved into up to seven instances, each with its own dedicated memory and compute. Those slices are advertised as named resources, so a pod requests a profile:

    resources:
      limits:
        nvidia.com/mig-1g.10gb: 1   # one MIG slice: ~1/7 compute, 10 GB HBM

Now the resource name itself carries memory and compute size (1g.10gb = one compute slice, 10 GB), so the scheduler is no longer flying blind on capacity. That is the difference: nvidia.com/gpu: 1 says "a GPU, somewhere"; nvidia.com/mig-1g.10gb: 1 says "10 GB of isolated GPU." Topology — which GPUs are NVLink-adjacent — is still not in either request, which is the whole subject of lesson 07.

4 · Sharing modes — utilization against isolation

One H100 can cost on the order of $2–$4 / GPU-hour on a cloud, so leaving it 15% utilized by a small model is burning money. Sharing modes exist to raise utilization — but every form of sharing trades away some isolation, and isolation is the assumption your tenants are quietly relying on. The four modes, defined at first use:

Whole GPU
One pod owns the entire device. Full memory and compute isolation, predictable latency. Wastes headroom whenever the workload is smaller than the card. The safe default for large models and strict latency SLOs.
MIG (Multi-Instance GPU)
Hardware partitions one GPU into up to 7 isolated instances, each with dedicated memory and compute. Real isolation — a noisy slice cannot starve its neighbors. Limited to supported GPUs (A100/H100 class) and fixed profile sizes.
Time slicing
Software interleaving: multiple pods take turns on the same GPU. Raises utilization for bursty/low-load jobs and works on any GPU. No memory isolation — pods share HBM and can OOM each other; one heavy tenant adds latency jitter to the rest. Oversubscription, not partitioning.
DRA (Dynamic Resource Allocation)
A claim-based request model: a pod files a ResourceClaim describing the device it needs (type, memory, sharing) and a DRA driver allocates it. More expressive than fixed counts. Progressing through Kubernetes — beta/maturing in 2026, not yet a drop-in for every driver.

The sharpest distinction to internalize: MIG isolates, time slicing does not. MIG draws hardware fences — separate memory controllers, separate compute partitions — so a misbehaving tenant in one slice physically cannot touch another's memory or steal its cycles. Time slicing just rotates the GPU's attention between processes that all see the same memory; it improves average utilization but provides zero protection, so a single pod that allocates too much HBM can crash every other pod on the device. Use time slicing for cooperative, low-stakes, bursty workloads; never use it where one tenant's bug must not become another tenant's outage.

ModeIsolationBest forRisk
Whole GPUFull (own device)Large models, strict latency SLOsLow utilization on small jobs — paying for unused HBM/compute
MIGFull (hardware slices)Many medium jobs needing real isolation on A100/H100Fixed profiles can mismatch workload memory; not on all GPUs
Time slicingNone (shared memory)Bursty, cooperative, low-load inferenceNoisy neighbors, OOM contagion, latency jitter, fuzzy accounting
DRA claimsDepends on claimExpressive requests as the API maturesDriver/version readiness; operators must debug ResourceClaims
Worked number — packing small models onto one card
Say you serve a fleet of small 7B-parameter models, each needing about 7 × 2 = 14 GB of HBM at load plus a few GB of KV cache — call it ~16 GB working set. On one 80 GB H100: whole-GPU gives you 1 tenant at ~20% memory use (16 / 80 GB) — wasteful. MIG with 3g.40gb/2g.20gb profiles lets you place 3–4 isolated tenants, each guaranteed its own ~20 GB and protected from the others, lifting utilization past 70% with no contagion risk. Time slicing could pack even more pods onto the raw 80 GB, but they all share that pool — five 16 GB tenants total 80 GB with zero slack, so one model loading a slightly larger checkpoint OOM-kills a neighbor, and request bursts collide into latency spikes. Same hardware, three very different risk profiles: the right choice is set by your isolation requirement, not by the maximum pod count.
Packing tenants onto one 80 GB GPU — time slicing vs MIG
Pack N small-model tenants (each ~16 GB working set) onto one 80 GB H100. Time slicing drops them all into one shared 80 GB pool — go over capacity and they OOM each other, and contention rises with every extra tenant. MIG hands each tenant an isolated ~20 GB slice (max 4), so it refuses to overcommit and contention stays flat. Drag N and watch utilization climb while the isolation story diverges.
Time slicing — HBM used
Time slicing — status
MIG — tenants placed
MIG — isolation
Show the core JS
const HBM = 80, PER = 16, SLICE = 20, MIG_MAX = 4;
// time slicing: one shared pool, OOM if demand exceeds capacity
const tsMem = N * PER;
const tsOOM = tsMem > HBM;                 // no isolation -> overcommit kills pods
// MIG: hard slices, scheduler refuses beyond capacity
const migPlaced = Math.min(N, MIG_MAX);    // each tenant fenced into ~20 GB
// MIG tenants are always isolated; time-slice tenants never are.

DRA deserves an honest maturity note. DRA (Dynamic Resource Allocation) generalizes the rigid "count of named resources" model into ResourceClaims: a pod describes the device it wants (a GPU with at least 40 GB, or a specific sharing arrangement) and a DRA driver matches and allocates it, much like a PersistentVolumeClaim does for storage. It is the strategic direction for accelerators on Kubernetes, and the core API has graduated into stable Kubernetes releases (the v1.34–v1.36 line moved DRA forward substantially). But "the API is stable" is not "every vendor driver and every advanced sharing feature is production-ready in your cluster." In 2026 the practical readiness questions are still vendor driver support, scheduler integration, telemetry, and whether your on-call engineers know how to debug a ResourceClaim that fails to allocate. Adopt it deliberately; do not overstate it.

5 · Where this breaks and what to install

Failure modes

  • Pod schedules but cannot see the device. The driver stack is incomplete — driver, toolkit, or device plugin missing or version-skewed — so the pod starts and then fails on CUDA init. Signal: the pod is Running but nvidia-smi inside it errors, or the model server crashes with a CUDA forward-compatibility message while the node label says a GPU exists.
  • Sharing breaks tenant isolation. You switch on time slicing to lift utilization, and now one tenant's oversized checkpoint OOM-kills another's pod, or a batch job adds latency jitter to an interactive one. Signal: correlated OOMKills or p99 latency spikes across unrelated pods on the same GPU node — the dependency is the shared, un-isolated device.
  • Scheduler sees only nvidia.com/gpu: 1. The integer hides HBM size and topology, so a model that needs 140 GB schedules onto an 80 GB card, or tensor-parallel ranks land on GPUs with no NVLink between them. Signal: OOM at model load, or multi-GPU throughput far below spec because the placement ignored topology (lesson 07).
  • Snowflake node setup. Drivers installed by hand drift across nodes; a reimaged node comes back missing a layer. Signal: a workload works on some GPU nodes and not others with no code difference — the difference is undocumented node state.

Implementation checklist

  • Install discovery (NFD + GFD) so every node advertises accelerator type, count, driver version, MIG state, and topology as labels — you cannot schedule on what the cluster cannot see.
  • Choose the sharing mode per workload from isolation + utilization needs: whole-GPU for large/strict, MIG for many isolated medium jobs, time slicing only for cooperative bursty load, DRA where the driver is genuinely ready.
  • Treat GPU Operator configuration as platform code — versioned, reviewed, applied declaratively — not a one-off SSH session that produces snowflake nodes.
  • Validate DCGM (Data Center GPU Manager — NVIDIA's GPU telemetry) metrics and in-container runtime visibility before admitting tenant workloads, so you confirm the device is both healthy and visible end to end.
  • When a GPU workload fails, debug bottom-up: node sees device → driver matches runtime → discovery labeled it → plugin/DRA advertised it → scheduler placed it on a compatible node → container sees the device file. Debug the model server last.

Checkpoint exercise

Try it
You have a cluster of 80 GB H100 nodes and two tenants: Tenant A runs a latency-sensitive 70B chat model; Tenant B runs a fleet of small 7B classifiers that are idle most of the day. Decide the sharing mode for each, write the resources: block each pod would request, and name the one node label you would require discovery to expose to keep A off a card it cannot fit on. Then state what breaks if you put B on time slicing and B's checkpoint quietly grows by 5 GB.

Where this points next

This lesson made a single GPU schedulable and decided how many tenants share it. But the resource model still hides the thing that dominates large-model performance: how multiple GPUs are wired together. A 70B model that does not fit on one card must be split across several, and whether those cards share NVLink or only PCIe changes throughput by an order of magnitude — yet nvidia.com/gpu: 2 says nothing about it. Lesson 07, Multi-GPU Inference and Topology, adds the machinery the scheduler needs to place co-dependent GPUs correctly: tensor parallelism across a node, topology-aware scheduling, and gang scheduling so all the ranks of one model land together or not at all.

Takeaway
A GPU is not a resource count; it is a stack — driver, runtime, container toolkit, discovery (NFD/GFD), device plugin or DRA driver, scheduler resource model, and pod visibility — and "the GPU works" means every layer is present and version-agreed. The NVIDIA GPU Operator installs and manages that stack as platform code, which is the cure for snowflake nodes and version skew. The basic request nvidia.com/gpu: 1 grants a whole device with full isolation but throws away HBM size and topology; MIG advertises richer, hardware-isolated slices (nvidia.com/mig-1g.10gb), time slicing oversubscribes one card with no memory isolation, and DRA is the maturing claim-based future — choose among them by trading utilization against isolation, never just by maximum pod count. Validate DCGM telemetry and in-container device visibility before tenants arrive, and when GPU pods fail, debug the stack bottom-up before ever touching the model.

Interview prompts