跳转至

实验:搭建 nvkind 集群并追踪 vLLM 部署

本实验在一台 Ubuntu 24.04 裸机上选择一张未启用 MIG 的 A100 80 GB,创建包含 control-plane 和 GPU worker 的 nvkind 集群,再部署带 PVC 与 Service 的 vLLM。bootstrap-host.sh 准备裸机,setup-course.sh 安装集群组件并部署应用,verify-course.sh 验证端到端结果,trace-vllm-deployment.sh 重建 Deployment 并收集组件证据。

验收范围包括 worker 的 nvidia.com/gpu Capacity/Allocatable 为 1/1、同一 GPU UUID 的容器注入、Bound PVC、ready Endpoint、集群内 Service DNS 与 ClusterIP 请求、/v1/models 模型列表,以及一次 OpenAI-compatible Chat Completions 请求。每次运行的结果以验证脚本退出码和 artifacts/chapter-01/ 下的证据目录为准。

教学环境边界

nvkind 使用 Docker 容器模拟 Kubernetes Node,CSI Hostpath Driver 是测试驱动。单卡 Device Plugin 清单用于多 GPU 裸机上的教学实验,不构成安全隔离方案。生产环境需要使用受支持的节点、存储和 GPU 隔离方案;实验 GPU 应保持空闲。

实验拓扑

下图展示了裸机、nvkind 节点和 vLLM 工作负载之间的关系。

flowchart TB
    subgraph HOST["Ubuntu 24.04 裸机"]
        DRIVER["NVIDIA Driver"]
        DOCKER["Docker + NVIDIA Container Toolkit"]
        GPU["1 × A100 80 GB\nMIG Disabled"]

        subgraph KIND["nvkind/Kind"]
            CP["control-plane\nAPI Server · etcd · Controller · Scheduler"]
            WK["worker\nKubelet · containerd"]
            CALICO["Calico CNI"]
            CSI["CSI Hostpath"]
            DEVICE["NVIDIA Device Plugin"]
            VLLM["Deployment\nvllm-demo\nQwen3-0.6B"]
            CP <--> WK
            WK --> CALICO
            WK --> CSI
            WK --> DEVICE
            CALICO -->|"Pod 网络"| VLLM
            CSI -->|"挂载 PVC"| VLLM
            DEVICE -->|"分配 GPU"| VLLM
        end

        DRIVER --> DOCKER
        GPU --> DOCKER
        DOCKER --> WK
    end

实验包含两层容器运行时。宿主机 Docker 运行 Kind Node 容器,worker 内的 containerd 通过 CRI 运行 Kubernetes 容器。NVIDIA Driver 和外层 Container Toolkit 由裸机负责,GPU Operator 运行在集群内部,宿主机内核模块仍由裸机安装流程管理。

版本与机器要求

examples/chapter-01/versions.env 固定了本章的组件版本,所有镜像都使用明确版本。修改其中任一版本后,需要重新执行安装、验证和追踪流程。

项目 本章基线
操作系统 Ubuntu 24.04 LTS,x86_64
GPU 一张空闲的 NVIDIA A100 80 GB,MIG Disabled
Kubernetes v1.36.1
Kind/nvkind Kind v0.32.0;nvkind 固定提交
网络 Calico v3.32.1,Calico Nftables + kube-proxy nftables
存储 CSI Hostpath Driver v1.18.0
GPU 软件 Driver R580 或更新版本(无可用 Driver 时安装 R580);Container Toolkit 1.20.0-1;GPU Operator 26.3.3;Device Plugin 0.19.3
Workload vllm/vllm-openai:v0.27.1,Qwen/Qwen3-0.6B

裸机需要访问 Docker Hub、GitHub、Kubernetes、NVIDIA、Calico、Helm 和 Hugging Face。首次启动 vLLM 会拉取镜像与模型,后续重建 Deployment 会复用 PVC 中的模型文件。Qwen/Qwen3-0.6B 是公开模型,通过 Hugging Face 匿名下载。

选择 GPU

以下命令列出 GPU、MIG 状态和正在运行的计算进程:

nvidia-smi --query-gpu=index,name,uuid,memory.total,driver_version,mig.mode.current \
  --format=csv,noheader

nvidia-smi --query-compute-apps=gpu_uuid,pid,process_name,used_gpu_memory \
  --format=csv,noheader

选择一张空闲的 A100 80 GB 并记录 GPU index。后续命令以 index 2 为例,实际运行时替换为目标卡。脚本接受单个数字 index,并要求该设备处于 MIG Disabled 状态。

固定的 nvkind 提交将数字 selector 作为 NVIDIA Runtime 的 NVML index,并在清理设备节点时将同一个数字用于 /dev/nvidiaN。setup-course.sh 读取 /proc/driver/nvidia/gpus/*/information,校验 GPU index 与 Linux device minor;两者不一致时,脚本退出并要求重新选卡。

准备裸机

在仓库根目录运行:

./examples/chapter-01/13-03-scripts/bootstrap-host.sh

脚本检查 Ubuntu 与 CPU 架构,安装或验证 NVIDIA Driver,再安装 Docker、NVIDIA Container Toolkit、kubectl、Kind、Helm、Go 和 nvkind。安装结束前,脚本分别验证普通 NVIDIA Runtime 和 nvkind 使用的特殊 volume-mount 入口。

已加载的 Driver 可以正常运行且版本不低于 R580 时,脚本保留现有版本;2026 年 8 月 20 日的实机验证使用 R590。已加载版本低于 R580 时,脚本退出并要求显式升级。裸机没有可用 Driver 时,脚本安装固定的 R580 分支,再根据内核模块状态判断是否需要重启。

Driver 加载与重启判断

Driver 安装完成后,脚本尝试加载 nvidia 和 nvidia_uvm 内核模块,再运行 nvidia-smi -L 检查内核模块与用户态工具:

  • 如果模块能正常加载,脚本继续执行,并明确打印无需重启。
  • 如果 nouveau 仍在使用设备、新旧 NVIDIA 模块无法替换、Secure Boot 阻止模块加载,或者 nvidia-smi 仍失败,脚本以退出码 20 停止并要求重启。
  • 重启后再次运行同一脚本,脚本从现有 Driver 状态继续检查。

NVIDIA Ubuntu 安装指南采用重启这一通用流程,本实验脚本则以当前内核模块和 nvidia-smi 的结果决定是否重启。退出码 20 表示 Driver 尚未生效,应完成模块或 Secure Boot 处理后重新运行脚本。

nvkind 的 GPU 透传

kind create cluster 创建普通的 Node 容器。nvkind 在 Kind 流程外增加两项 GPU 配置:

  1. 宿主机 NVIDIA Runtime 识别 /var/run/nvidia-container-devices/<index> 这个特殊挂载请求,把选中的 GPU 设备与 Driver 依赖注入 worker Node 容器。
  2. nvkind 进入 worker,安装 NVIDIA Container Toolkit,配置内层 containerd 的 NVIDIA runtime 与 CDI,并注册 Kubernetes RuntimeClass。

课程模板中的关键配置如下:

examples/chapter-01/13-01-nvkind/nvkind-config-template.yaml
nodes:
  - role: worker
    labels:
      nvidia.com/gpu.present: "true"
    extraMounts:
      - hostPath: /dev/null
        containerPath: /var/run/nvidia-container-devices/{{ $.gpuDevice }}

hostPath: /dev/null 是特殊挂载请求的占位路径。bootstrap-host.sh 为 Docker 开启 accept-nvidia-visible-devices-as-volume-mounts 后,NVIDIA Runtime 会将 containerPath 解释成设备选择请求。nvkind 将 GPU 接入 Node 容器,NVIDIA Device Plugin 再向 Kubelet 注册 nvidia.com/gpu。

创建集群并部署 vLLM

把前面选好的 GPU index 交给安装脚本:

NVKIND_GPU_DEVICE=2 \
  ./examples/chapter-01/13-03-scripts/setup-course.sh

下图展示了 setup-course.sh 的安装顺序。

flowchart LR
    A["校验目标 GPU"] --> B["nvkind 创建集群"]
    B --> C["安装 Calico"]
    C --> D["安装 CSI Hostpath"]
    D --> E["安装精简 GPU Operator"]
    E --> F["安装单卡 Device Plugin"]
    F --> G["应用 PVC、Service、Deployment"]
    G --> H["等待 /v1/models 返回目标模型"]

脚本创建一个 control-plane 和一个 GPU worker,并关闭 Kind 默认 CNI。Calico Nftables 提供 Pod 网络,kube-proxy nftables 提供 Service 数据面,CSI Hostpath 动态创建模型缓存卷。

裸机脚本负责宿主机 Driver,nvkind 配置 worker 内层 Toolkit。GPU Operator 保留 Operator 和验证流程,并关闭 Driver、Toolkit 与内建 Device Plugin;固定版本的单卡 Device Plugin DaemonSet 负责发布资源,使 worker 可见设备、nvidia.com/gpu 资源和 CDI 设备保持同一张 GPU。

已有同名 Kind 集群时,脚本复用集群并重新收敛插件与 Workload;NVKIND_GPU_DEVICE 仅在新建集群时读取。现有集群需要包含本章模板配置的 API Audit 和组件日志级别,追踪脚本会在这些条件缺失时退出并要求重建集群。

vLLM Deployment 使用以下清单:

examples/chapter-01/12-vllm/02-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-demo
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-demo
  template:
    metadata:
      labels:
        app: vllm-demo
    spec:
      runtimeClassName: nvidia
      containers:
        - name: vllm
          image: vllm/vllm-openai:v0.27.1
          command: ["vllm", "serve"]
          args: ["Qwen/Qwen3-0.6B", "--port", "8000"]
          ports:
            - containerPort: 8000
          resources:
            limits:
              nvidia.com/gpu: "1"
          volumeMounts:
            - name: model-cache
              mountPath: /root/.cache/huggingface
      volumes:
        - name: model-cache
          persistentVolumeClaim:
            claimName: vllm-model-cache

清单省略更新策略,API Server 将其默认设置为 RollingUpdate。重新部署时,setup-course.sh 和追踪脚本先删除现有 Deployment,再创建新对象,避免新旧 Pod 同时申请唯一一张 GPU;PVC 模型缓存和 Service 保持不变。

验证端到端结果

安装脚本会持续请求 /v1/models,直到返回目标模型。随后运行独立验证:

./examples/chapter-01/13-03-scripts/verify-course.sh

验证脚本检查以下对象、数据面和应用结果:

  • 所有 Node Ready,Calico 与 kube-proxy 都使用本章规定的 nftables 模式;
  • CSI Driver 可用,vllm-model-cache PVC 已经 Bound;
  • ClusterPolicy=ready,worker 的 GPU Capacity/Allocatable 都等于 1;
  • Deployment、ReplicaSet 与 Pod 的 UID 链存在,Pod 绑定到 GPU worker;
  • vLLM 容器内的 nvidia-smi 返回一张 GPU,Service 有 ready Endpoint,集群内 Service DNS 与 ClusterIP 请求成功;
  • Audit、etcd、Controller、Scheduler、Kubelet、containerd、CNI、CSI、Device Plugin 与应用日志均可读取;
  • /v1/models 发布 Qwen/Qwen3-0.6B,一次 Chat Completions 请求返回有效结果。

脚本默认临时使用本机端口 18000。如端口已被占用,可以显式改成另一个值:

VLLM_LOCAL_PORT=18080 \
  ./examples/chapter-01/13-03-scripts/verify-course.sh

全部检查通过后,脚本输出 Course verification completed successfully. 并以退出码 0 结束。

追踪 vLLM Deployment

验证成功后,运行追踪脚本:

./examples/chapter-01/13-03-scripts/trace-vllm-deployment.sh

追踪脚本会重建 Deployment

脚本先删除 default/vllm-demo Deployment 以及它拥有的 ReplicaSet 和 Pod,然后在日志采集启动后重新创建 Deployment。PVC、已下载的模型缓存和 Service 会保留。不要把同名对象用于其他工作。

脚本在创建 Deployment 前读取 etcd 当前 revision,并同时启动以下证据流:

环节 主要证据
API 请求 API Audit 中的 user、verb、resource、response code 与 authorization decision
持久化 Deployment、ReplicaSet、Pod 对应 etcd key 的 Watch revision
控制循环 Controller Manager 中 Deployment 与 ReplicaSet 的 reconcile 日志
调度 Scheduler 对同一 Pod 的过滤、选择与 binding 日志
节点执行 Kubelet、containerd、CRI sandbox/container inspect
网络 Calico CNI 日志、Pod IP、EndpointSlice 与集群内 Service 响应
存储 CSI 日志、PVC/PV 对象与 UID
GPU Device Plugin 日志、Pod allocatedResourcesStatus、Kubelet checkpoint、CRI/CDI 与具体 /dev/nvidiaN
应用 vLLM 启动日志与 Chat Completions 响应

这一步在端到端验证之后执行,PVC 中已经保存模型文件。追踪过程聚焦 Deployment 对象创建、容器启动和权重加载。

证据目录

每次运行都会创建一个带 UTC 时间戳的目录,主要文件如下:

artifacts/chapter-01/<trace-id>/
├── README.md
├── combined.log
├── streams/
│   ├── api-audit.log
│   ├── kube-apiserver.log
│   ├── etcd-deployment-watch.log
│   ├── etcd-replicaset-watch.log
│   ├── etcd-pod-watch.log
│   ├── controller-manager.log
│   ├── scheduler.log
│   ├── kubelet.log
│   ├── containerd.log
│   ├── calico-cni.log
│   ├── csi.log
│   ├── device-plugin.log
│   ├── events.log
│   ├── mainline.log
│   └── vllm.log
├── objects/
│   ├── deployment.json
│   ├── replicaset.json
│   ├── pod.json
│   ├── pvc.json
│   ├── pv.json
│   ├── service.json
│   ├── endpointslices.json
│   ├── events-correlated.json
│   ├── cri-sandboxes.json
│   ├── cri-containers.json
│   ├── cri-sandbox-inspect.json
│   ├── cri-container-inspect.json
│   ├── kubelet-device-checkpoint.json
│   ├── service-models.json
│   └── chat-completion.json
└── etcd/
    ├── deployment-current.json
    ├── replicaset-current.json
    ├── pod-current.json
    ├── pvc-current.json
    └── service-current.json

打开目录中的 README.md,记录 Deployment、ReplicaSet、Pod 的 UID 和 GPU UUID;再从 combined.log 中的第一条 Deployment API 请求开始读取。采集器为每行添加 UTC 时间与组件名。多路日志存在缓冲、时钟偏差和异步控制循环,UID、对象名与 etcd revision 是主要关联依据,采集时间用于辅助排序。

证据目录可能含有内部地址、对象名和日志内容,分享前应按所在环境的要求检查一次。

required evidence: complete 表示每个必需检查点都有匹配证据。退出码 2 表示 Workload 主线可能已经成功,但至少一项必需日志或对象证据缺失。脚本保留本次目录,便于根据缺失项继续分析。

GPU UUID 关联

追踪结果从 Pod Status、Kubelet checkpoint 和 CRI inspect 三处核对 GPU UUID:

trace_dir="$(find artifacts/chapter-01 -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1)"

jq '.status.containerStatuses[].allocatedResourcesStatus' \
  "${trace_dir}/objects/pod.json"

jq '.Data.PodDeviceEntries' \
  "${trace_dir}/objects/kubelet-device-checkpoint.json"

jq '{
  podUID: .status.labels["io.kubernetes.pod.uid"],
  cdi: (.status.annotations | with_entries(select(.key | startswith("cdi.k8s.io/")))),
  devices: [.info.runtimeSpec.linux.devices[].path]
}' "${trace_dir}/objects/cri-container-inspect.json"

Pod Status 记录 Kubelet 上报的资源,checkpoint 保存 Device Manager 的分配,CRI inspect 包含最终的 CDI annotation 与 /dev/nvidiaN。三处记录与 Device Plugin 看到的 UUID 一致时,可以把 Pod 的整数资源申请关联到容器取得的具体 GPU。

常见停点

bootstrap-host.sh 退出码 20

退出码 20 表示新 Driver 模块尚未生效。脚本输出会指出 nouveau、Secure Boot 或模块加载失败;完成重启或模块签名处理后,再运行同一脚本。

Worker 持续 NotReady

新集群创建后、Calico 安装前短暂 NotReady 是正常的。若 setup-course.sh 已经安装 Calico 仍未恢复,检查:

kubectl get tigerastatus
kubectl get pods -n calico-system -o wide
kubectl describe node k8s-ai-infra-worker

Pod 持续 Pending

检查 Pod 的 spec.nodeName:

kubectl get pod -n default \
  -l app=vllm-demo \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\n"}{end}'

kubectl describe pod -n default \
  -l app=vllm-demo

spec.nodeName 为空时,检查 Scheduler、nvidia.com/gpu Capacity/Allocatable、污点和 PVC Event。spec.nodeName 已写入时,继续检查 Kubelet、卷、镜像和容器运行时。

Pod 停在 ContainerCreating

按节点侧顺序读取 Event、Kubelet、containerd、CSI 与 Device Plugin:

pod_name="$(kubectl get pod -n default \
  -l app=vllm-demo \
  -o jsonpath='{.items[0].metadata.name}')"
kubectl get events -n default \
  --field-selector "involvedObject.name=${pod_name}" \
  --sort-by=.metadata.creationTimestamp
docker exec k8s-ai-infra-worker journalctl -u kubelet --no-pager -n 200
docker exec k8s-ai-infra-worker journalctl -u containerd --no-pager -n 200
kubectl logs daemonset/csi-hostpathplugin --all-containers=true --tail=200
kubectl logs -n nvidia-device-plugin \
  daemonset/single-gpu-device-plugin --all-containers=true --tail=200

Pod Ready 但 vLLM API 未就绪

基础清单未配置 Probe,Pod Ready 可能早于模型加载完成。检查模型下载、CUDA 初始化和 /v1/models:

pod_name="$(kubectl get pod -n default \
  -l app=vllm-demo \
  -o jsonpath='{.items[0].metadata.name}')"
kubectl logs -n default deployment/vllm-demo --tail=200
kubectl describe pod -n default \
  -l app=vllm-demo
kubectl get endpointslice -n default \
  -l kubernetes.io/service-name=vllm -o yaml
kubectl exec -n default "$pod_name" -- \
  python3 -c 'import urllib.request; print(urllib.request.urlopen("http://127.0.0.1:8000/v1/models", timeout=10).read().decode())'

vLLM 日志可以区分模型下载、CUDA 初始化和进程异常退出。Pod 已绑定节点并启动容器时,排查重点位于应用启动阶段。

Hugging Face HTTP 429

HTTP 429 表示当前出口 IP 已达到 Hugging Face 匿名 API 配额。等待配额恢复后重新执行 setup-course.sh,PVC 中已有的模型文件会继续保留。

清理

整个 Kind 集群都不再需要时运行:

./examples/chapter-01/13-03-scripts/delete-cluster.sh

该命令删除名为 k8s-ai-infra 的 Kind 集群及其中的 PVC 和 Hostpath 测试数据。宿主机 Driver、Docker、Container Toolkit 和本地追踪目录保留;需要长期保存实验结果时,提前备份 artifacts/chapter-01/。

相关资料