跳转到主要内容
deploy/k8s/ 是一套 kustomize overlay,覆盖命名空间、配置、密钥、RBAC、Deployment、 Service、Ingress 和两个 PVC。

它会创建什么

apply 之前先把镜像指向你的仓库:
deploy/k8s/kustomization.yaml

真正要紧的是那个存储卷

ReadWriteMany 不是可选项,而 RWO 是静默失败的。 RWO 的 claim 不会报错—— Kubernetes 会给每个 Pod 各自一个卷。多节点作业于是把 checkpoint 分片散落在不同 Pod 上, 这次 run 再也续不了,而日志里没有任何东西说明原因。执行器的健康检查会把 TUNEPLANE_K8S_STORAGE_SHARED 和 claim 的实际 accessModes 核对。 首次部署后去读那个结果,别想当然。
控制台把这个 claim 挂在 TUNEPLANE_STORAGE_ROOT 指定的那个路径上—— 它往 runs/<user>/<exp>/<run_id>/work 里放作业目录、测量磁盘压力、执行回收。 训练 Pod 挂载同一个 claim,所以控制台写的路径就是作业打开的路径。 容量按「权重缓存 + 数据集缓存 + 所有 run 目录」估。2Ti 是一个起点而不是建议值; 光是权重缓存,每个基座模型就是几十 GB。

推荐配置

deploy/k8s/configmap.yaml
其中四项值得理解而不是照抄:
训练 Pod 往它回传。用外部 ingress 地址会先出集群再绕回来;服务 DNS 名不会。 绝不能填 127.0.0.1——那是 worker 自己的回环。
不一致的症状是「集群起来了但 worker 注册不上」,而唯一的线索是一行很容易淹没在启动输出里的版本警告。
Ray 的 object store 就在 /dev/shm 里。用默认值的话,稍大一点的 batch 立刻 OOM。 它由内存支撑,所以它加上内存上限不能超过物理内存。
没有它,拉不到的镜像或者没有节点满足的 nodeSelector 会让作业无限期 Pending,同时一直占着队列位置。

密钥

secret.example.yaml 只是用来说明结构。上生产前请换成真实来源—— kubectl create secret、Sealed Secrets 或 External Secrets Operator—— 并把它从 kustomization.yaml 里移除。 至少要有:TUNEPLANE_WEB_JWT_SECRET、TUNEPLANE_DB_URL、TUNEPLANE_REDIS_URL、 TUNEPLANE_S3_SECRET_KEY、TUNEPLANE_SECRET_ENC_KEY,以及 OIDC 的 client secret。

副本数

API 和 worker 必须共享 PostgreSQL、Redis 6.2 或更高版本、固定的 JWT 密钥和存储。 Helm 中启用 worker.enabled=true 后,会关闭 API 内的后台循环,并创建一个可扩容的 worker Deployment。每个 worker 都运行全部协调循环(调度、状态对账、看门狗), 各循环通过 leader lease 保证同一时刻只有一个副本执行,其余副本热备;每增加一个 worker 也会增加消费能力。设置 worker.replicas=3 即为三个 worker。 Kustomize 只需 worker.yaml,不再有单独的 controller。
所有 worker 使用相同启动命令,自动领取任务。不同 run 的后台动作、诊断、Hub 导入, 以及导入到不同 Volume 的任务可以并行。GPU 分配由持有调度 lease 的那个 worker 负责; 共享维护任务保留协调约束,增加副本不会拆分单次传输。 TUNEPLANE_WORKER_CONCURRENCY 控制每个进程的在途任务数(默认 4)。 TUNEPLANE_WORKER_IO_CONCURRENCY 控制整个 worker 池的导入和 Volume 测量并发上限 (默认 4),所有副本应设置相同值。仅增加副本不会提高存储并发预算,调整时还应计入 数据库连接数和存储带宽。 对象存储必须支持条件写入,浏览器上传的 CORS 配置还需允许 If-None-Match 请求头。 API、消费者、SDK 和网页应一起升级:Volume 文件上传后 必须完成发布才能被读取。托管 Volume 使用不可变代次读取,不兼容旧的可变数据目录。

确认成功

然后看执行器自己的视角——它才能告诉你存储卷是不是真的 RWX: