hands-on labKubernetes in your browser

Multi-GPU-Type Targeting: nodeSelector, nodeAffinity & Tolerations

Your fleet has A100s for training, L40S for inference, and Tesla-K80s for dev.

time
35 min
checked steps
5
level
Intermediate
setup
None

$29.99a month with Pro

Pro opens this lab and every other one. Cancel any time.

The job

Production GPU clusters run a mix of hardware. A100s for training that needs HBM bandwidth, L40S for inference where price-per-token wins, T4s and K80s for development and CI. Random scheduling lands every workload anywhere, and now your training pod is running on a K80, your inference is on an A100 you're paying triple for, and the platform team is on fire. The fix is the four placement primitives you'll author in this lab, and reading whichever combination is right for your workload-to-hardware mapping.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    The multi-GPU node landscape: read what your fleet has

    This cluster has 5 nodes across 3 GPU types, the same shape you'll see in a real multi-tenant GPU production cluster:

    read this step below ↓

  2. 2

    nodeSelector: target a single GPU product

    nodeSelector is the simplest of the four placement primitives.

  3. 3

    nodeAffinity: OR semantics, Set membership, and Soft preference

    nodeAffinity is nodeSelector with three superpowers:

  4. 4

    Taints + tolerations: reserved-pool patterns

    nodeSelector / nodeAffinity are pod-side opt-INs.

  5. 5

    Triage Day: three placement failures

    Three pods are stuck Pending, one for each placement primitive you've worked through:

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: The multi-GPU node landscape: read what your fleet has

The multi-GPU node landscape

This cluster has 5 nodes across 3 GPU types — the same shape you'll see in a real multi-tenant GPU production cluster:

NodePoolGPU productGPUsTaints
mocloud-baseddefaultTesla-K804(none)
kwok-a100-1a100-poolNVIDIA-A100-SXM4-80GB2kwok.x-k8s.io/node=fake:NoSchedule
kwok-a100-2a100-poolNVIDIA-A100-SXM4-80GB2kwok.x-k8s.io/node=fake:NoSchedule
kwok-l40s-1l40s-poolNVIDIA-L40S2kwok.x-k8s.io/node=fake:NoSchedule
kwok-l40s-2l40s-poolNVIDIA-L40S2kwok.x-k8s.io/node=fake:NoSchedule

The Tesla-K80 node is the host's real node with execution; the four kwok-* nodes are KWOK virtual nodes (synthetic — pods scheduled there go Running but don't actually execute). For this lab the difference doesn't matter — we're teaching the placement primitives, and kubectl get pod -o wide shows you which node a pod landed on either way.

What gpu-feature-discovery wrote on each node

Every GPU node carries a set of nvidia.com/gpu.* labels written by gpu-feature-discovery (the GPU Operator's labeling daemon). The values come from the underlying hardware (or in our case, the topology ConfigMap):

nvidia.com/gpu.present     = true                          (every GPU node)
nvidia.com/gpu.product     = <model name>                  (e.g., NVIDIA-A100-SXM4-80GB)
nvidia.com/gpu.count       = <count of GPUs on the node>   (2 or 4 in this lab)
nvidia.com/gpu.memory      = <per-GPU memory in MiB>       (11441 for K80, 81920 for A100)
nvidia.com/gpu.compute.major / .minor = CUDA Compute Capability

These are the labels your nodeSelector / nodeAffinity will target. Run the step's command (or click Run) — it dumps every nvidia.com label on every node so you can see the full landscape.

What the taints mean

The four KWOK virtual nodes carry the taint:

kwok.x-k8s.io/node=fake:NoSchedule

NoSchedule means: pods that don't tolerate this taint will NOT be scheduled on this node. The taint is automatically applied to KWOK nodes by the KWOK admission webhook to prevent pods from accidentally landing on synthetic nodes. To use these nodes, your pod spec must include a matching toleration. We'll do this in step 4.

The Tesla-K80 node has no taint — pods schedule there freely.

Production parallels

In a real production GPU cluster, the layout looks similar — minus the KWOK fakery. Common patterns:

  • nvidia.com/gpu=present:NoSchedule taint on every GPU node — repels non-GPU workloads (cron jobs, monitoring sidecars) so they don't waste GPU-node capacity.
  • workload=training:NoSchedule taint on training-reserved nodes — only pods marked as training tolerate; inference / dev land elsewhere.
  • GPU-product-specific taints like nvidia.com/gpu.product=NVIDIA-A100:NoSchedule for "A100s are reserved for the team that authored this taint."

Once you can read kubectl get node -o jsonpath for both labels and taints, the placement decisions become trivial. The next four steps make that concrete.

Why this matters in practice: the four primitives — nodeSelector, nodeAffinity, taints, tolerations — are how every production GPU cluster encodes its workload-to-hardware policy. NCA-AIIO frames this as "given this fleet (5 nodes, 3 GPU types) and this workload (training, inference, dev), author the pod spec that lands it on the right hardware."

what you get

  • A Kubernetes cluster and terminal in your browser
  • A check at the end of every step
  • Hints and the full solution
  • Nothing to install or configure

before you start

  • Completed `nca-aiio-gpu-operator-chain` (or comfortable with gpu-feature-discovery labels)
  • Familiar with `kubectl describe pod` events and reading scheduler messages

Exam domains covered

AI Infrastructure & OperationsGPU Acceleration & Distributed Training

Part of the GPU and Kubernetes track

Lab 5 of 20. GPU scheduling on your own Kubernetes cluster, the GPU Operator, CUDA kernels and profiling.

Questions about this lab

When should I use nodeSelector vs nodeAffinity?

nodeSelector is the simplest case: a single key=value match (or a few). If your only condition is "node has label nvidia.com/gpu.product=NVIDIA-A100", nodeSelector is one line and reads cleanly. nodeAffinity is the richer form, it supports operators like In, NotIn, Exists, DoesNotExist, Gt, Lt, lets you OR multiple terms together, and crucially has both a requiredDuringSchedulingIgnoredDuringExecution mode (hard match, same as nodeSelector) AND a preferredDuringSchedulingIgnoredDuringExecution mode (soft preference, with weights). Use nodeAffinity when you need OR semantics ("A100 OR H100"), set membership, or the soft-preference fallback ("prefer A100, but L40S is acceptable"). Stick with nodeSelector for the trivial hard match.

What's the difference between taints and nodeSelector, they both block scheduling?

They run in opposite directions. nodeSelector is on the pod and says "this pod requires nodes with these labels"; pods opt-IN to specific nodes. taints are on the node and say "this node repels pods that don't have a matching toleration"; pods opt-IN by tolerating, but the default is to be excluded. The asymmetry matters in production: if your A100 nodes are expensive, you want the default to be "no random pod lands here", that's taints. If you need workloads to discriminate but the hardware is shared freely, that's nodeSelector. Most production fleets use BOTH: taint A100 nodes (only A100-tolerating pods land here) AND select with nodeSelector (only pods explicitly targeting A100 land here).

What does requiredDuringSchedulingIgnoredDuringExecution actually do?

It's a hard match enforced at scheduling time only. The "IgnoredDuringExecution" half means once the pod has been placed, label changes on the node DON'T cause the pod to be evicted, so if you re-label a node mid-experiment, your already-running pod stays. The hard match: if no node satisfies the affinity, the pod stays Pending forever (with FailedScheduling events). The mirror is preferredDuringSchedulingIgnoredDuringExecution, which is soft, the scheduler tries to match preferences but will fall back to ANY node if no match. The required version is what most NCA-AIIO content tests; the preferred version is the production-friendly "prefer this hardware but don't block on it." Note: there is no RequiredDuringExecution (it would mean live-evict on label change), Kubernetes doesn't ship that.

Why does my pod stay Pending with node(s) had untolerated taint?

The scheduler considered the node, but the node has a taint your pod doesn't tolerate. The fix: read the taint (kubectl get node <name> -o jsonpath='{.spec.taints}'), then add a matching toleration to your pod spec. Tolerations match on the same key+value+effect triple as the taint. Three operators: Equal (matches when key+value+effect all match), Exists (matches any value with that key+effect, useful for opt-in to ALL taints with a given key), and the special "no operator + no key" combo which tolerates ALL taints (use sparingly). Production GPU node pools commonly carry the taint nvidia.com/gpu=present:NoSchedule to repel non-GPU workloads; only pods with tolerations: [{key: nvidia.com/gpu, operator: Exists, effect: NoSchedule}] land there.

What you'll learn

The four primitives Kubernetes ships for placing pods on the right nodes are nodeSelector, nodeAffinity (required and preferred), node taints, and pod tolerations. Each has a different semantic, selector is a hard filter, affinity is also hard or soft, taints repel pods unless they tolerate. The combination you choose is determined by the workload-to-hardware mapping you're encoding: "this pod MUST run on A100" vs "this pod PREFERS A100 but L40S is fine" vs "this hardware is reserved for these pods only."

This lab walks all four with a real multi-pool cluster (Tesla-K80, A100-80GB, L40S simulated nodes). You'll author selectors that target one product line, nodeAffinity expressions that match a set, tolerations to opt into reserved hardware, and finish with a Triage Day where three pods are broken at three placement layers, selector typos, missing tolerations, over-strict required-affinity. Recognizing each in kubectl describe pod events is the platform-engineering muscle the NCA-AIIO exam tests under AI Infrastructure & Operations.