LMCache 的 K3s 构建基 CI Harness 架构:单节点 Kubernetes 之上的 GPU 测试集群设计与实现

原创2026-09-15 10:35:171,380 阅读
文章标签:人工智能大模型缓存抽象模型推理服务

LMCache 的 K3s 构建基 CI Harness 架构:单节点 Kubernetes 之上的 GPU 测试集群设计与实现

本文系统梳理 LMCache 项目中基于 K3s 的 Buildkite CI Harness(CI 基架)的整体架构:它如何在任意一台裸机 Linux GPU 机器上,通过一条脚本拉起一套完整的 CI 测试集群,并以"临时 Pod"方式执行每个测试任务。读完本文,你将掌握这套 CI 体系的核心设计决策(GPU Operator、agent-stack-k8s、本地基础镜像、共享卷)、节点初始化与 CI 执行的完整流程,以及 setup 脚本、环境准备脚本、GPU 监控与 teardown 的实操细节,可以直接复用到自己的 GPU CI 场景。

一、为什么 LMCache 需要一套 K3s CI Harness

LMCache 是一个面向 LLM 的 KV Cache 层,其 CI 需要频繁在真实 GPU 上跑 vLLM 集成测试、多进程测试与各类正确性验证。在引入这套 K3s 方案之前,传统的裸机 Buildkite agent 方式存在明显的痛点,ARCHITECTURE.md 总结了四点核心动机:

  1. 易于搭建(Ease of Setup):希望在任何一台裸机 Linux GPU 机器上,仅凭一个脚本(setup-cluster.sh)就能拉起一个功能完整的 CI 节点,避免手动配置依赖或维护常驻的 Buildkite agent。
  2. 与机器无关(Machine Agnostic):不再依赖机器专属脚本(例如自定义的 pick-free-gpu.sh)。任何带 NVIDIA GPU 和 Docker 的机器上,CI 都以相同的方式运行。
  3. 干净环境(Clean Environments):消除测试运行之间的状态泄漏——没有共享的 pip/uv 缓存,没有残留文件,每个任务都拿到一个全新的环境。
  4. 自动化资源管理(Automated Resource Management):借助标准 Kubernetes 原语完成 GPU 分配、卷挂载与清理,而不是靠手写脚本去锁 GPU。

二、五项核心设计决策

1. K3s + NVIDIA GPU Operator

CI 集群采用 K3s——一个轻量级、单节点的 Kubernetes 发行版。NVIDIA GPU Operator 自动配置容器运行时并把 GPU 暴露给 K8s,抽象掉底层硬件差异、自动完成 GPU 发现。从 setup-cluster.sh 可以看到,GPU Operator 通过 Helm 安装,并显式设置 driver.enabled=false(宿主机已装驱动)与 toolkit.enabled=true,随后等待 device-plugin daemonset 就绪,再用 kubectl get node -o jsonpath='{.items[0].status.allocatable.nvidia\.com/gpu}' 确认可分配的 GPU 数量。

2. 基于临时 Pod 的执行(agent-stack-k8s)

不使用常驻的 buildkite-agent 二进制,而是采用 Buildkite 官方的 agent-stack-k8s:一个 controller Pod 在 K3s 内轮询 Buildkite 队列,动态地为每个任务拉起一个临时的 K8s Pod 执行任务;任务结束后 Pod 被销毁。因此机器上不存在任何常驻 agent,也就没有 agent 生命周期管理、没有跨任务的环境污染。

3. 声明式 GPU 分配与自动并行

每个 pipeline step 在 Kubernetes Pod 规格中显式声明它需要的 GPU 数量:

resources:
  limits:
    nvidia.com/gpu: "2"  # 请求 2 张 GPU

由于 Kubernetes 负责原子化的资源调度,多 GPU 节点上的任务会自动并行。文档给出了一个直观的例子:若节点有 4 张 GPU,三个任务分别请求 1、2、1 张 GPU,K8s 会把它们同时调度运行;若再来一个请求 2 张 GPU 的任务,它会排队等待资源释放。这彻底消除了手动 GPU 锁机制(例如仓库中旧有的 pick-free-gpu.sh、pick-free-gpu-amd.sh 这类脚本)的需求。真实的 pipeline 用法可参考 unit/pipeline.yml:step 通过 kubernetes 插件指定 podSpec,在 limits.nvidia.com/gpu 声明 GPU 数量,并用 requests/limits 声明 CPU 与内存。

4. 本地基础镜像与临时环境准备

为避免对 Docker registry 的依赖,setup-cluster.sh 会自动检测宿主 GPU 的计算能力(compute capability),本地构建 lmcache/ci-base:latest 镜像,并直接导入 K3s 的 containerd。每个任务 Pod 使用该基础镜像,在运行时通过 setup-env.sh 动态安装 vLLM 与 LMCache。镜像本身不包含 vLLM/LMCache(见 ci-base.Dockerfile 的注释说明),只提供 CUDA 13.0.2 + Ubuntu 24.04 + Python 3.12 + uv + 构建依赖,并把 requirements/*.txt 中不常变的依赖预先装好。

5. 共享宿主机卷

像 HuggingFace 模型权重、数据集这类体积大、读多写少的目录,通过 hostPath 挂载进 Pod,避免重复下载、加速测试,同时任务环境本身保持无状态。在 unit/pipeline.yml 中可以看到三种典型卷:/data/huggingface 挂到 /root/.cache/huggingface(模型缓存)、/data/gds-scratch 挂到 /scratch(GDS 临时目录,注释特别说明不能用 subPath 否则会破坏 cuFile 的 fs-type 检测)、以及一个 16 GiB 的 tmpfs emptyDir 挂到 /dev/shm(规避 K8s 默认 64 MiB shm 导致 shm_allocator 测试 SIGBUS 的问题)。

三、整体架构流程

节点初始化(一次性 setup)

[ Raw Linux Host + NVIDIA GPU ]
          |
          | (run setup-cluster.sh)
          v
+-----------------------------------------------------+
|                     K3s Cluster                     |
|                                                     |
|  1. Install K3s                                     |
|  2. Install Helm -> Deploy NVIDIA GPU Operator      |
|  3. Build CI Base Image (lmcache/ci-base:latest)    |
|  4. Import Base Image to K3s containerd             |
+-----------------------------------------------------+
          |
          | (run install-agent-stack.sh)
          v
[ agent-stack-k8s Controller Pod (Watches queue) ]

对应的落地脚本是 setup-cluster.sh,它被设计为幂等(脚本头部注释明确 "Safe to re-run"):K3s 已运行则跳过安装;GPU Operator 已安装则跳过;基础镜像已导入 containerd 则跳过构建。关键细节包括:

  • 安装 K3s 时使用 --disable=traefik(CI 场景不需要 ingress)与 --write-kubeconfig-mode=644,并把 KUBECONFIG=/etc/rancher/k3s/k3s.yaml 持久化到 ~/.bashrc;
  • 通过 nvidia-smi --query-gpu=compute_cap 自动探测计算能力,生成 TORCH_CUDA_ARCH_LIST="${COMPUTE_CAP}+PTX" 作为 docker build 的 build-arg,确保镜像中的 torch/CUDA 扩展与宿主机 GPU 匹配;
  • 镜像构建后用 docker save | k3s ctr images import - 直接灌入 K3s containerd,全程不依赖外部 registry;
  • 末尾创建共享宿主机卷目录 /data/huggingface 与 /data/datasets,并打印集群就绪摘要(K3s 版本、GPU Operator 版本、GPU 数量与型号、arch list、镜像名)。

CI 执行流程

Buildkite Web UI
       |
       | 1. Job pushed to 'k8s' queue
       v
agent-stack-k8s Controller (in K3s)
       |
       | 2. Reads job, creates ephemeral Pod requesting GPUs
       v
+-------------------------------------------------------------+
| Job Pod (e.g., limits: nvidia.com/gpu: "1")                 |
|                                                             |
|  - Mounts /data/huggingface from host                       |
|  - Runs setup-env.sh (Installs vLLM, LMCache)               |
|  - Executes test script (e.g., pytest)                      |
+-------------------------------------------------------------+
       |
       | 3. Test finishes (Pass/Fail)
       v
agent-stack-k8s Controller
       |
       | 4. Reports result to Buildkite
       | 5. Destroys the Pod (wipes environment)
       v
Buildkite Web UI

四、与 Buildkite 的集成方式

README.md 详细说明了这套方案与传统裸机 agent 的区别:机器上没有常驻 agent。agent-stack-k8s 的 controller Pod 轮询 Buildkite 队列,有任务出现就创建 Pod,任务结束就删除 Pod。

在 Buildkite Web UI 侧需要准备三件事:

  1. 创建一个队列:进入 Organization Settings → Default cluster → Queues → New Queue,创建名为 k8s 的队列(名字可自选)。队列不需要任何配置,也不要注册任何 agent;
  2. 获取 agent token:从 cluster 设置页复制 agent token;
  3. 获取 GitHub token:创建对仓库有读写权限的 PAT(或 fine-grained token),用于 HTTPS checkout 以及向 benchmarks-main 分支推送 baseline。

然后执行 install-agent-stack.sh:

.buildkite/k3_harness/install-agent-stack.sh <BUILDKITE_AGENT_TOKEN> <GITHUB_TOKEN>

若想使用 k8s 以外的队列名,通过环境变量指定:

BUILDKITE_QUEUE=my-queue .buildkite/k3_harness/install-agent-stack.sh <AGENT_TOKEN> <GITHUB_TOKEN>

该脚本内部做了两件事:一是创建名为 buildkite-git-creds 的 K8s secret,包含两个键——.git-credentials(供 agent-stack-k8s 的 checkout 容器做 HTTPS clone,前缀点号对应 git-credential-store 的约定)与 GITHUB_TOKEN(注入任务容器供 git push 使用);二是通过 Helm 从 oci://ghcr.io/buildkite/helm/agent-stack-k8s 安装/升级 agent-stack-k8s(版本 0.38.0),设置 agentToken、config.queue,并通过 config.default-checkout-params.gitCredentialsSecret 让 checkout 走 HTTPS。

Pipeline step 通过如下方式指向该队列:

agents:
  queue: "k8s"   # must match the queue name

五、每个任务的环境准备:setup-env.sh 深度拆解

每个 CI 任务都会先 source setup-env.sh 来安装 vLLM 与 LMCache:

command: |
  source .buildkite/k3_harness/setup-env.sh
  bash .buildkite/scripts/my-test.sh

每个 Pod 拥有独立的临时文件系统,任务结束后被完全清空——没有共享 pip/uv 缓存,没有跨 Pod 争用。这个脚本远比"装两个包"复杂,它沉淀了大量真实 CI 踩坑经验,值得逐段理解:

  1. GPU 健康预检:脚本开头调用 helpers.sh 中的 check_gpu_health 80(见 helpers.sh)。若 Pod 分到的 GPU 空闲内存低于 80%(通常由宿主机上的残留进程导致),任务立即失败并给出明确信息,而不是在 setup 之后才撞上晦涩的 CUDA OOM。
  2. 运行时架构重探测:基础镜像可能在不同 GPU 主机上构建(例如 H200 的 sm_90 镜像无法在 A100 的 sm_80 上加载内核),因此脚本用 nvidia-smi 重新获取运行时 GPU 的计算能力并导出 TORCH_CUDA_ARCH_LIST,覆盖镜像构建时的值。
  3. 合并 PR 目标分支:调用 merge_pr_base_branch(helpers.sh 中定义),在任务 Pod 内把 PR 分支合并进目标分支再测试。
  4. vLLM 版本解析:source resolve-pinned-vllm.sh,按优先级解析要装的 vLLM nightly:显式 PINNED_VLLM_VERSION 环境变量 > 从 buildkite_latest_tested_vllm 分支拉取的 latest_tested_vllm.txt(canary 构建最近验证过的版本)> 空值回退到"最新 nightly"。pin 文件除了裸版本号,还携带 short_sha、full_sha、archive_index_url 等元数据,使安装侧无需额外 API 调用;USE_PINNED_VLLM=false 可跳过 pin(canary 构建自身使用,避免"自我确认")。
  5. 字节码/缓存驱逐:CI 中曾出现 ImportError: cannot import name 'GenerationConfig' from 'transformers'——即使已安装文件明确包含该符号,同一安装配方在全新 venv 中总能成功,说明故障与基础镜像文件系统状态(残留 __pycache__、overlayfs 部分升级)有关。因此脚本在安装前后各执行一次 find ... -name __pycache__ -exec rm -rf 和 uv cache clean。
  6. vLLM nightly 安装(钉死 cu130 索引):基础镜像是 nvidia/cuda:13.0.2-devel-ubuntu24.04(系统 nvcc 13)。vLLM 通用 nightly 索引可能随机解析到 cu128 或 cu130 的 torch wheel,当解析到 cu128 时 torch.utils.cpp_extension._check_cuda_version 会因 CUDA 版本不匹配(13.0 vs 12.8)中止 LMCache 的可编辑安装。因此脚本强制使用 --extra-index-url https://wheels.vllm.ai/nightly/cu130 与 https://download.pytorch.org/whl/cu130,并配合 --reinstall-package transformers/tokenizers/huggingface-hub/safetensors/vllm 强制重装,绕开基础镜像中的文件系统级不一致。若启用 pin,则优先使用 archive_index_url 指向的 commit 归档索引(vLLM nightly 索引只保留最新 wheel,一两天就滚动下线,但 wheels.vllm.ai/<full-commit-sha>/<cuda>/ 是永久保留的 PEP 503 索引);旧格式 pin 文件则通过 GitHub commits API 展开短 SHA。
  7. vLLM CLI 探测与自愈:通过子进程执行 vllm --help 来完整探测 vllm serve 的导入链(vllm.entrypoints.cli.main.main() 会在函数体内触发 from transformers import GenerationConfig, PretrainedConfig;仅 import vllm.entrypoints.cli.main 不会执行函数体,无法发现问题)。探测失败且为 ModuleNotFoundError 时,自动安装缺失模块(最多 5 次,如 vLLM nightly 中未声明的 pandas);其他错误则输出一份完整的 transformers 诊断 dump(包版本、目录、_import_structure、_class_to_module 等)并退出。
  8. torch/nvcc CUDA 版本一致性检查:python 片段对比 torch.version.cuda 与系统 nvcc 的主版本号,不匹配立即失败——否则这个错误会在 ninja 编译深处以晦涩的 cusparse.h: No such file or directory 出现。
  9. LMCache 从源码可编辑安装:设置 SETUPTOOLS_SCM_PRETEND_VERSION_FOR_LMCACHE=0.0.0+ci(仓库带有 nightly、nightly-cu13 等非 PEP-440 tag,会击穿较新的 vcs_versioning 后端);uv pip install -e . --no-build-isolation 后安装 requirements/proto.txt 并运行 lmcache/v1/multiprocess/transport/grpc_impl/_proto_gen/_generate.py 生成 gRPC 绑定;安装前后各 uv pip freeze | sort 一次并 diff,展示 LMCache 安装对依赖的改动。
  10. 安装后二次 CLI 探测:LMCache 可编辑安装可能为了满足 requirements/common.txt 的版本上限而降级传递依赖,破坏 vLLM CLI 导入链。脚本再次执行 probe_vllm_cli,失败则打印 traceback 与 uv pip freeze 直接退出,而不是让每个测试 harness 在 wait_for_server 超时 180 秒后才暴露问题。

最终脚本以 python -c "import vllm; import lmcache; ..." 验证环境就绪。此外,仓库还提供 setup-lmcache-only-env.sh(不需要 vLLM 的轻量任务,如单元测试)与 setup-sglang-env.sh、setup-blend-env.sh(特定任务场景)等变体。

六、共享卷与 GPU 分配细节

README 中给出了挂载进每个 Pod 的共享卷清单:

Host Container What
/data/huggingface /root/.cache/huggingface 模型权重(读密集、写一次)
/data/datasets /root/correctness 测试数据集(下载后只读)

GPU 分配方式为每个 pipeline step 显式声明:

plugins:
  - kubernetes:
      podSpec:
        containers:
          - resources:
              limits:
                nvidia.com/gpu: "2"  # 1 或 2

GPU 的原子化分配由 K8s device plugin 完成,不再需要 pick-free-gpu.sh 之类的脚本。

七、CI 基础镜像的构建与重建

ci-base.Dockerfile 基于 nvidia/cuda:13.0.2-devel-ubuntu24.04,安装 ccache、git、curl、jq、lsof、ffmpeg、libnuma1、libcudart12 等系统依赖,安装 uv,创建 /opt/venv,并预装 requirements/cuda.txt 与 requirements/build.txt。TORCH_CUDA_ARCH_LIST 作为 build-arg 在构建时写入环境变量,与 CI 机器的 GPU 匹配。

基础镜像默认由 setup-cluster.sh 自动构建并导入 K3s containerd,无需 registry。修改 requirements/*.txt 或 ci-base.Dockerfile 后需要强制重建:

REBUILD_IMAGE=1 .buildkite/k3_harness/setup-cluster.sh

八、集群验证、GPU 监控与 teardown

冒烟测试

smoke-test.sh 用于验证集群就绪:检查节点、检查可分配 GPU 数(少于 1 则失败),随后提交一个请求 nvidia.com/gpu: "1" 的 nvidia/cuda:12.8.0-base-ubuntu24.04 Pod 执行 nvidia-smi,等待其 Succeeded 后输出日志并清理。

双层 GPU 监控

这套体系对 GPU 健康做了双层防护:

  • 任务级:每个 CI 任务启动时通过 check_gpu_health(默认要求 80% 空闲内存)快速失败;
  • 宿主机级:gpu-monitor.sh 每 10 分钟扫描一次,通过遍历 /sys/fs/cgroup/*/kubepods* 的 cgroup.procs、以及 crictl ps/crictl inspect 收集 K8s 容器 PID,与 nvidia-smi --query-compute-apps=pid 的 GPU 进程集合做差集,找出不属于任何 K8s Pod 的残留 GPU 进程,并记录 PID、命令、GPU bus、显存占用与进程年龄(从 /proc/$pid/stat 的 start time 推算),输出到 /var/log/gpu-monitor.log。安装方式:
# 安装每 10 分钟检查一次残留进程的 cron 任务
sudo bash .buildkite/k3_harness/setup-gpu-monitor.sh

setup-gpu-monitor.sh 会同时配置 logrotate(每日轮转、保留 7 份、压缩)。查看日志与移除 cron:

# 查看监控日志
tail -f /var/log/gpu-monitor.log

# 移除 cron 任务
crontab -l 2>/dev/null | grep -v gpu-monitor.sh | crontab -

完整 teardown

teardown.sh 按序卸载 agent-stack-k8s、GPU Operator(含命名空间),最后调用 k3s-uninstall.sh 移除 K3s。/data/* 宿主数据卷被保留,便于下次重建集群后继续复用模型与数据集缓存。

九、k3_harness 目录速览

.buildkite/k3_harness/
├── ci-base.Dockerfile      # CI 基础镜像定义(CUDA 13 + Python 3.12 + uv,不含 vLLM/LMCache)
├── setup-cluster.sh        # 一次性:K3s + GPU Operator + 基础镜像构建与导入(幂等)
├── install-agent-stack.sh  # 一次性:安装 agent-stack-k8s(需要 agent token + GitHub token)
├── values.yaml             # 参考 Helm values(仅文档用途)
├── setup-env.sh            # 每任务:安装 vLLM + LMCache(含 GPU 健康检查与多级自检)
├── resolve-pinned-vllm.sh  # 解析 vLLM nightly pin 版本
├── setup-lmcache-only-env.sh / setup-sglang-env.sh / setup-blend-env.sh  # 场景化环境准备变体
├── smoke-test.sh           # 验证 GPU Pod 能在 K3s 中运行
├── gpu-monitor.sh          # 宿主机级:检测非 K8s 的残留 GPU 进程
├── setup-gpu-monitor.sh    # 将 gpu-monitor.sh 安装为 cron 任务
└── teardown.sh             # 卸载全部组件(保留 /data/*)

配合 k3_tests 目录下的各测试套件(unit、comprehensive、integration、multiprocess、correctness、blend、sglang、amd、musa、xpu 等)以及 pipelines 下的 clean.yml、comprehensive-tests.yml、end-to-end-tests.yml、multiprocessing-test.yml,这套 K3s harness 构成了 LMCache 在真实 GPU 上持续验证的核心基座:任何机器、一条脚本、干净的临时环境、由 Kubernetes 原生保证的 GPU 并行与隔离。

登录后查看全文
LMCache