Skip to content

GPU Node Pool Architecture & Device Plugin

Tại sao quan trọng

GPU trong Kubernetes không phải là "resource như CPU/memory". Chúng yêu cầu:

  • Device driver cài sẵn trên node
  • Device plugin expose GPU resources cho Kubernetes
  • Taints/tolerations để chỉ GPU workloads được schedule
  • Custom scheduling logic để tránh fragmenting GPU memory

Nếu hiểu sai mental model, bạn sẽ gặp vấn đề: Pod pending dù có GPU available, driver mismatch, workloads không được isolate properly.


Cơ chế GPU Node Pools trong GKE

Node Pool creation và GPU attachment

Khi bạn tạo node pool với GPU:

bash
gcloud container node-pools create gpu-pool \
  --cluster=my-cluster \
  --machine-type=n1-highmem-8 \
  --accelerator=type=nvidia-tesla-a100,count=2 \
  --num-nodes=3

GKE thực hiện các bước sau:

  1. Validate quota: Kiểm tra bạn có đủ GPU quota cho region/zone (2 GPUs × 3 nodes = 6 GPUs)
  2. Create VMs: Provision 3 VMs với 2 A100 GPUs mỗi VM
  3. Install driver: Tùy theo cấu hình (mặc định: GKE tự động install NVIDIA driver)
  4. Tag nodes: Thêm label cloud.google.com/gke-accelerator=nvidia-tesla-a100
  5. Apply taints: Thêm taint nvidia.com/gpu=true:NoSchedule để chỉ GPU Pods được schedule

Taints and tolerations: Isolating GPU workloads

GKE tự động thêm taint:

yaml
taints:
- key: nvidia.com/gpu
  value: true
  effect: NoSchedule

Pod yêu cầu GPU phải tolerant taint này:

yaml
spec:
  tolerations:
  - key: nvidia.com/gpu
    operator: Equal
    value: "true"
    effect: NoSchedule
  containers:
  - name: training-job
    resources:
      limits:
        nvidia.com/gpu: 2  # Request 2 GPUs

Mental model: Taint là "bộ lọc cứng" — GPU node sẽ từ chối Pod không có tolerations, ngay cả nếu có GPU free.


NVIDIA Device Plugin: Resource exposure

Cơ chế hoạt động

NVIDIA device plugin là Daemonset chạy trên mỗi GPU node:

kubelet → device plugin grpc endpoint → enumerate GPUs

          expose nvidia.com/gpu resource type

          kubelet → resource available on node

Quá trình chi tiết:

  1. Device discovery (startup): Plugin quét node và tìm tất cả physical GPUs

    nvidia-smi → 2 GPUs (GPU 0: H100, GPU 1: H100)
  2. Capacity export (kubelet sync):

    yaml
    Node status:
    allocatable:
      nvidia.com/gpu: 2
  3. Pod admission:

    yaml
    Pod requests nvidia.com/gpu: 1
    kubelet → plugin: allocate GPU 0
    plugin → kubelet: device allocated, volume mounts /dev/nvidia0
  4. Container execution:

    bash
    # kubelet launches container with:
    # - /dev/nvidia0, /dev/nvidia1, /dev/nvidiactl mounted
    # - LD_LIBRARY_PATH=/usr/local/nvidia/lib64
    # - GPU driver libraries + CUDA runtime available

Device plugin lifecycle

Khi node khởi động:

1. kubelet starts → loads default device plugins
2. nvidia-device-plugin daemonset pod starts
3. plugin discovers GPUs → exports capacity
4. kubelet calls plugin's ListAndWatch endpoint
5. On GPU failure: plugin updates capacity (e.g., 2 → 1 GPU)
6. kubelet reschedules pending Pods

Key constraint: Device plugin là stateless. Nó chỉ track physical availability, không track actual usage (VRAM, compute utilization). Kubernetes scheduler không biết Pod A dùng 80% VRAM của GPU — từ perspective của kubelet, "GPU được allocated" và đó là tất cả thông tin nó có.


GPU Driver Management: Automatic vs Manual

Khi tạo GPU node pool, GKE tự động:

  1. Detect GPU type: Query Compute Engine metadata
  2. Select driver version: Dựa trên GPU model + node OS (COS)
  3. Download + install: From GKE bucket (network-local, fast)
  4. Monitor + auto-repair: NVIDIA DCGM on node monitors driver health
Node startup sequence:
1. Boot COS image
2. GKE agent detects: GPU H100 present
3. Install NVIDIA driver (latest stable for H100 + COS)
4. Start DCGM daemon (monitoring + error detection)
5. Notify kubelet: Ready

Advantage: Zero operational burden. GKE patches driver automatically.

Limitation: Driver version is fixed per GPU type. Bạn không thể force driver 550 nếu GKE phân phối 545.

NVIDIA GPU Operator (User-Managed)

Nếu bạn cần control driver lifecycle (multi-cloud consistency, custom CUDA versions):

bash
gcloud container node-pools create gpu-pool \
  --cluster=my-cluster \
  --enable-ip-alias \
  --accelerator=type=nvidia-tesla-h100,count=8 \
  --labels=gke-no-default-nvidia-gpu-device-plugin=true  # Disable GKE plugin

Sau đó install NVIDIA GPU Operator qua Helm:

bash
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm install gpu-operator nvidia/gpu-operator \
  --namespace gpu-operator-system \
  --create-namespace

GPU Operator quản lý:

  • NVIDIA driver installation + updates
  • NVIDIA device plugin (same as GKE's, but user-managed)
  • CUDA libraries
  • DCGM (monitoring)
  • NVIDIA MIG manager (if enabled)

Trade-off:

  • ✓ Control driver version
  • ✓ Consistent across clouds
  • ✗ More operational overhead (you manage updates)
  • ✗ Longer debugging chain if GPU issues (is it driver? operator? plugin?)

GPU Scheduling: Constraints and Fragmentation

Bin-packing behavior

Kubernetes scheduler không hiểu "GPU memory fragmentation". Nó chỉ track:

GPU count (nvidia.com/gpu: integer)

Scenario: Node có 2 H100 GPUs (80GB mỗi cái)

Pod A: requests nvidia.com/gpu: 1 → allocated GPU 0
Pod B: requests nvidia.com/gpu: 1 → allocated GPU 1

Now Pod C: requests nvidia.com/gpu: 1 → PENDING (no GPU)

Nhưng nếu Pod A chỉ dùng 30GB VRAM và Pod B dùng 20GB, vẫn có 110GB miễn phí tổng. Scheduler không thấy điều này. Đó là lý do cần MIG hoặc Time-Slicing (chương tiếp).

Zone pinning

GPU availability là zone-specific. Nếu bạn tạo GPU node pool chỉ ở zone us-central1-a:

Cluster có 3 zones: a, b, c
GPU nodes chỉ ở zone a

→ Tất cả GPU workloads được pinned vào zone a
→ Khi zone a resource pool saturated, pending Pod sẽ block (cluster autoscaler scale up nhưng chỉ ở zone a)

Mitigation:

  • Tạo GPU node pool ở tất cả zones (hoặc 3 node pools, mỗi cái 1 zone)
  • Hoặc sử dụng NodeAffinity để workload có thể move giữa zones

Device plugin failure modes

Scenario 1: Device plugin crash

GPU node running 4 Pods (each with GPU)
nvidia-device-plugin crashes (OOM, kernel panic, etc.)

kubelet notices device plugin dead → marks GPU capacity = 0
All 4 Pods still running (kubelet doesn't evict)
But no new GPU Pod can be scheduled on that node

Recovery: kubelet auto-restarts device plugin daemonset. Nếu DaemonSet crashed, manual intervention cần.

Scenario 2: Driver mismatch

Node A: Driver 550 + CUDA 12.0
Pod requests: cuda:11.8 (incompatible)

Container tries to load libcuda.so.11.8 → ENOENT
Pod crashes with OOM-like symptoms (vague error)

Prevention: Version-specific image tagging, nodeSelector ở CUDA version label (nếu track).


GPU Quota và Billing

Quota enforcement

Setiap region punya quota untuk GPU count:

project my-project:
  us-central1:
    nvidia-tesla-a100: 50 (limit)
                        8 (in-use)
                       42 (available)

Ketika membuat node pool:

bash
gcloud container node-pools create gpu-pool \
  --accelerator=type=nvidia-tesla-a100,count=8 \
  --num-nodes=10

# Total: 8 × 10 = 80 A100s
# But quota = 50 → CREATE FAILS

Quota request pattern: Most teams request in increments of 8, 16, 32 (matching popular batching).

Billing

GPU ditagih per-minute (seperti VM):

1 A100 GPU = $1.95/hour (us-central1, on-demand)

Node pool 10 nodes × 8 GPUs/node = 80 GPUs
Monthly cost: 80 × $1.95 × 730 hours = ~$114,120

Dengan 10% utilization (actual VRAM usage 8GB dari 80GB), cost-per-unit bisa meningkat 10x. Itulah mengapa resource efficiency (batching, model parallelism, Spot VMs) kritis.


GKE-specific GPU behaviors

Autopilot vs Standard modes

GKE Autopilot:

  • User hanya declare resource needs: nvidia.com/gpu: 4
  • GKE provision node automatically
  • GPU driver auto-installed + auto-patched
  • Pricing: per-Pod resource reservation (tidak per-node)

GKE Standard:

  • User create node pool + manage scaling
  • Full control over machine type, driver version, autoscaling
  • Pricing: per-node (GPU cost bundled with VM)

GPU node pool specifics in Autopilot: Pod CPU/memory request harus match GPU memory capacity roughly. Jika request 4 GPUs (320GB) tapi hanya 64GB CPU memory, kubelet reject (mismatch).

Graceful shutdown

When node drain (update, scale-down):

1. kubelet sends SIGTERM to GPU Pod
2. GPU code must checkpoint state (loss = restart training)
3. If no graceful shutdown hooks → Pod forcibly killed after 30s
4. New Pod schedule on another GPU node (potentially losing epochs)

For long-running training: implement PreStop hook to checkpoint state, atau pakai ProvisioningRequest (chapter 4) untuk gang-schedule resumable batches.


Mental Model Summary

GPU node pools dalam GKE:

  1. Device plugin: stateless inventory + resource export mechanism, does NOT track actual utilization
  2. Taints: hard filter ensuring non-GPU-tolerant workloads can't accidentally schedule
  3. Driver: usually GKE-managed (safe, automatic), optionally user-managed (GPU Operator)
  4. Fragmentation: scheduler bins by count, not by memory → underlies need for MIG/time-slicing
  5. Quota: regional, not project-global → potential bottleneck at specific region
  6. Graceful shutdown: workload must handle SIGTERM + checkpoint, else data loss

Production Anti-patterns

❌ Anti-pattern 1: No taint toleration

yaml
# ❌ WRONG: GPU Pod without toleration
spec:
  containers:
  - name: inference
    resources:
      limits:
        nvidia.com/gpu: 1
  # Missing: tolerations

GPU taint ngăn chặn Pod này → PENDING forever.

Fix:

yaml
tolerations:
- key: nvidia.com/gpu
  operator: Equal
  value: "true"
  effect: NoSchedule

❌ Anti-pattern 2: Ignoring fragmentation

Running 10 training Pods, mỗi cái request 1 GPU, trên cluster có 16 GPUs. Mỗi Pod dùng ~60GB VRAM → 6GB spare per GPU.

Pod ke-11 pending (no free GPU), nhưng tổng cluster có 96GB spare VRAM.

Solution: MIG/Time-Slicing (chapter 2), không scaling up more GPUs.

❌ Anti-pattern 3: Assuming driver version doesn't matter

dockerfile
FROM nvidia/cuda:11.8
COPY model.pth /app/
RUN python -m pip install torch==2.0

Node chạy CUDA 12.0 (GKE standard H100 default) → library mismatch → cryptic error.

Fix: Pin CUDA version trong image tag hay sử dụng base image matching node's driver. Atau track driver version ở node label + nodeSelector.


References