最早跑CubeStudio单机版的时候,一台双卡机器加一块共享存储就能把模型开发、训推的流程全部兜住。但等团队多了、项目组多了、GPU卡不够分的时候,你很快会发现单机这摊事根本撑不住。要么排队排到天荒地老,要么不同项目的环境互相干扰,更别提GPU利用率低到让人心慌。我开始做多机扩容、多集群纳管和按项目组分算力,是被一张“四卡机只有一块卡在跑、另外三块空转”的资源监控截图刺激到的。花了两三周把架构重排了一遍,现在这套东西稳定跑了几十台GPU节点、多个K8s集群,各个项目组的配额、优先级、抢占行为都能收敛到可控范围内。这篇就把整个演进过程里真正踩过的坑、验证过的路径和可以直接照抄的配置全部捋一遍。
1. 先理清楚:从单机到多集群到底要解决哪几层问题
很多人一上来就急着装组件、配调度器,结果多机节点加了一堆,资源还是乱成一锅粥。我自己的经验是,先别碰任何命令,把“单机—多机—多集群—多资源组”这四层形态想明白,比什么都重要。
1.1 四个形态各自要突破什么瓶颈
我习惯用下面这张表把每个阶段的瓶颈点和核心动作拆开看,方便对号入座:
| 阶段形态 | 核心瓶颈 | 要解决的问题 | 关键动作 |
|---|---|---|---|
| 单机版 | 单机GPU数量有限,算力上限固定 | 把开发、训练、推理在一个节点内跑通 | 部署基础服务,打通存储与镜像拉取 |
| 多机版 | 单节点物理资源不满足多租户排队 | GPU利用率低,任务互相抢占 | 节点加入集群,统一GPU可观测与调度 |
| 多集群版 | 一个集群可纳管的节点和Namespace有限 | 业务隔离、故障域隔离、环境区分 | 控制面统一纳管,数据面分散部署 |
| 多资源组 | 所有项目共用同一份算力池,无法差异化保障 | 按项目组/团队划分配额、优先级与费用归属 | 资源组模型+调度策略+配额管理 |
这张表其实对应了路径依赖:你不能在单机没跑稳的前提下直接上多集群,因为你会把单机的故障域直接放大到整个集群。更合理的路线是,先把单机基线做扎实,然后“同构地”往外推:多机是在做一个更大集群的“单机”,多集群是在做“多个更大集群”的调度,而资源组则是在为前两层赋予业务语义。
1.2 组件角色要先划分清楚:控制面、数据面、存储面
在实操里,踩过的最大一个坑是把“所有组件堆在一个集群里”。CubeStudio这种平台天然包含三个角色面,混在一起会让后续扩容非常痛苦:
- 控制面:负责用户认证、项目组管理、资源组定义、任务调度决策。这一层需要高可用,但负载通常不高,用三台不带GPU的小机器就够。
- 数据面:负责真正跑GPU任务的节点组,一般由装有GPU卡的机器组成。这里只运行业务Pod和必要的DaemonSet,不要放控制面组件。
- 存储面:负责保存数据集、模型权重、训练日志。建议选用独立的分布式文件系统或对象存储,避免和控制面/数据面互相影响。
我最初把镜像仓库、MySQL、Redis全部塞在同一台GPU节点上,后来发现一旦训练任务把带宽和内存吃满,控制面接口直接卡成PPT,镜像拉取也变得极不稳定。后来把控制面完全抽出去,数据面只保留容器运行时、GPU Device Plugin和监控采集器,整个系统的稳定性才真正提升了一个档次。
这个思维模式很重要:**多机多集群的本质不是“把Pod放到更多机器上”,而是“把共享服务与计算资源解耦”。**想通了这一点,后面所有配置都会变得很顺。
2. 单机扩多机实操:把更多GPU卡变成同一个池子
第一阶段是把一台GPU机器扩展成多台GPU机器的集群。这里的核心工作不是装K8s,而是让新加入的机器立刻变成“同一个池子”里的资源,并且能够被CubeStudio正常调度和监控到。
2.1 新节点加入集群前的五项体检
如果把一台没有做任何预检的裸机直接加进集群,大概率会遇到版本不一致、GPU驱动不匹配、防火墙策略挡住通信等一连串问题。我的习惯是先做一轮快速体检,确认五项指标:
- 操作系统与内核版本是否与现有节点一致,尽量使用相同的大版本号,避免内核模块兼容性差异。
- 容器运行时版本是否一致,特别是containerd与K8s版本的兼容性矩阵,差一个小版本都可能出现节点NotReady。
- GPU驱动与CUDA版本是否满足最低要求,直接用nvidia-smi查看驱动版本,确认和集群内其他节点一致。
- 是否安装并配置好了GPU Device Plugin所需的软件包,比如NVIDIA Container Toolkit,保证容器内能访问GPU。
- 网络通信是否打通,特别是6443端口、Pod网段和Service网段之间的路由规则。
体检通过之后,再用标准的节点加入命令把机器接进集群。这一步不追求快,追求的是把每个节点的规格信息、GPU型号、显存大小都登记清楚,后面分配算力才有依据。
2.2 GPU池化和节点打标签:让调度器知道每张卡在哪
节点加入集群后,需要给节点和GPU打好标签,否则按项目组分算力的时候完全没法精准匹配到卡片。我的标签体系大概长这样:
- gpu-type: 按GPU型号标记,比如一个项目组如果专门做推理,可以限定只用某种推理卡。
- gpu-node-group: 标记节点属于哪个资源池,比如“项目A专用池”、“共享池”。
- storage-class: 标记该节点能够访问的存储类型,比如本地盘、高性能共享盘、普通共享盘。
- maintenance: 标记节点是否处于维护状态,避免调度器往上面派新任务。
标签不只是给人看的,更是给调度器和资源配额用的。后续在CubeStudio资源组配置里,可以直接通过nodeSelector来处理,也可以配合亲和性做更细的调度策略。这一步如果偷懒,后面项目组之间的算力隔离就是一句空话。
2.3 多机版反复踩坑的四个问题
节点加进来之后,不等于一切就自动好用了。实际用下来高频出问题的通常集中在以下几个方面:
- GPU卡在一台机器上只有一部分被识别。原因是节点上残留旧版驱动或Device Plugin版本不对,导致K8s上报的可用GPU卡数不准确。处理方法是卸载旧驱动,重装统一版本的驱动和插件,并重启kubelet。
- 不同节点GPU型号混用,任务被调度到不对的卡上。解决办法是给节点打gpu-type标签,并在任务模板里显式声明需要的GPU类型。
- 新节点一直陷入NotReady状态。多数原因是容器运行时和kubelet配置不一致,或者节点时间偏移过大。我遇到一次很诡异的情况是其中一台机器时钟慢了三分钟,直接导致证书校验失败。
- 共享存储没有挂到新节点上。很多GPU任务要读取同一个数据集目录,如果新节点没有挂载共享存储,训练一开始就会报“目录不存在”的错误。体检清单里必须加入存储挂载检查。
多机阶段能做到的事情是:算力池有了,多个项目可以同时提交任务,GPU利用率开始回升。但这个阶段还无法解决“多个项目组互相抢资源”的问题,所以下一步要上多集群。
3. 多集群纳管实操:一个控制面管住所有业务集群
多机解决的是“机器的横向扩展”,而多集群解决的是“故障域与租户隔离”。当不同项目组对稳定性、安全要求不一致时,把所有任务放同一个集群里风险太大。比如一个做对外API推理服务的组,如果和一个大量跑批训练实验的组混在一起,一个资源争抢或者一个故障都可能互相拖累。
3.1 中心管控集群与业务集群的分工
我的方案是把系统分成一个“中心管控集群”和若干个“业务集群”,管控层不跑GPU任务,只做三件事:
- 维护全局的用户、项目组、资源组定义。
- 维护与每个业务集群建立信任关系的配置信息。
- 把用户在界面上发起的算力申请,翻译成具体业务集群上的工作负载。
业务集群则专注于跑任务,每个集群有自己独立的K8s API Server、etcd和资源配额。这样的好处是,如果某个业务集群因为网络分区或者etcd抖动出问题,其他集群完全不受影响。
为了把这套关系落地,我在CubeStudio里为每个业务集群注册了一个“集群身份”,包括集群名称、API Server地址、验证用的证书或Token信息。注册完成后,还要为每个业务集群配置允许被纳管的Namespace范围,避免业务集群中出现不受管控的角落。
3.2 集群注册与统一视图的搭建步骤
如果你手里有一套现成的多套K8s集群,想要让CubeStudio把它们的资源统一汇聚起来,步骤大概是这样的:
- 在中心管控集群上安装多集群纳管组件,并生成一个用于身份认证的加密配置。
- 登录目标业务集群,把对应的只读或管理权限的授权配置安装进去,注意权限范围不要给到整个cluster-admin,最好限制到指定Namespace。
- 在CubeStudio的管理界面里填写业务集群的API Server地址和授权配置,测试连通性。
- 连通后,将业务集群里的节点、GPU卡、已分配资源等数据同步到统一视图里,让管理员在同一个面板上看到所有集群的实时状态。
- 为每个业务集群设置默认的资源组映射关系,说明该集群主要服务于哪些项目组。
这里有个非常关键的细节:授权配置中的证书与Token一定要设置有效期和自动轮换机制。我之前一次“多集群任务突然全部调度失败”的排障,最后发现是一个业务集群的访问Token过期了,而监控面板上没有报错,只显示“该集群不可用”。排查过程中浪费了大量时间去检查网络和调度器。自从加上自动轮换后,这个问题就再没出现过。
3.3 跨集群调度应该做到什么程度
很多人在多集群纳管后会立刻想“能不能把一个任务同时调度到多个集群”。我的建议是分阶段:第一阶段先做到“一个任务在一个集群里跑,但可以由管理员手动指定或系统自动选择集群”;第二阶段才去做跨集群的GPU池化调度,比如某个集群资源不足时自动把新任务调度到另一个集群。
第一阶段的实现方式比较简单,在任务模板里增加clusterSelector字段,CubeStudio会根据集群标签和当前负载来决定投递到哪个集群。第二阶段由于涉及数据分布、文件系统访问延迟、跨集群网络带宽等问题,复杂度高很多,如果你没有特别强的平台团队,不建议一开始就全量铺开。
我自己在第二阶段尝试了一段时间,发现跨集群调度最大的开销不是调度本身,而是数据同步。两个集群如果不在同一个网段,而且没有共享存储,跨集群跑任务时数据集要先拷贝过去,训练中产生的checkpoint还要定期同步回来,时间和带宽消耗都非常大。所以我的折衷方案是:**业务集群成对绑定一个共用的存储池,只有同一存储池内的集群之间才开启跨集群调度。**这样既避免了大规模数据搬迁,又能在单集群故障时快速切换。
多集群纳管解决的是“多个环境统一管理”的问题。但到了这一步,各个项目组虽然在物理和逻辑上被分到了不同集群,却缺少统一的“项目组—资源组—配额”的业务语义。下一步需要把按项目组分算力的规则落地。
4. 按项目组分算力实操:从“资源池”到“资源组”
这是整个改造里业务价值最直接、也最容易踩坑的一环。可以理解为:算力池是一大堆GPU卡的物理集合,但不同项目组不能无差别地从这个池子里领资源。资源组就是给这个池子画上“归属线”和“限额线”。
4.1 资源组建模:项目组、配额、优先级三层关系
我在设计资源组时,参考了K8s自带的Namespace + ResourceQuota + RBAC的经典组合,但在上面加了一层CubeStudio的资源组概念,来承载“按项目组分算力”的业务逻辑。三层关系大概是这样:
| 层级 | 对应实体 | 作用 |
|---|---|---|
| 项目组 | 比如算法一组、算法二组、推理服务组 | 确定用户属于哪个业务团队,有多少人、预算如何 |
| 资源组 | 一组GPU规格和数量的配额 | 确定该团队最多能占用多少算力,有哪些调度策略 |
| 调度策略 | 优先级、抢占、排队规则 | 确定繁忙时谁先谁后,哪些任务可以被挤掉 |
项目组和资源组之间是多对多的关系,一个项目组如果有多个资源组,可以分别用于“日常训练”、“紧急任务”、“高优推理”等不同场景。我自己的项目里为每个项目组分配了至少两个资源组:一个常规训练组,用比较便宜的共享GPU卡;一个高优任务组,用性能更强的卡,且优先级更高。
4.2 配额计算和设置方法
配额设计不能拍脑袋。每个项目组真正需要的不是“几张卡”,而是“几个并发任务”和“每个任务占用多少卡/显存”。我通常按下面的公式估算:
并发任务数 = 同时进行实验的算法工程师人数 × 每人的典型并行实验数
比如算法一组有8个人,平均每人会同时跑2个训练任务,那么并发任务数就是16。如果每个任务要用1块32GB显存的GPU卡,那么这个项目组的资源组配额就应该至少设置为16卡。实际配置时我会再留20~30%的余量,应对突发实验和抢卡高峰。
具体到K8s层面的落地,我用ResourceQuota来限定每个项目组的资源上限,用LimitRange来约束单Pod的资源大小,同时用PriorityClass标记任务优先级。这样配置完成后,高优任务会在资源紧张时优先调度,低优任务的Pod则可以被轻易抢占,保障核心业务不被实验任务拖垮。
4.3 项目组之间的亲和性与反亲和:避免互踩
按项目组划分算力之后,还有一个非常容易被忽略的问题:不同项目组的Pod被调度到了同一台物理机上,导致显存和带宽互相抢。比如A组和B组的任务都落在同一张GPU物理机上,A组跑一个大模型训练,显存占掉大半,B组其他任务就被挤到OOM边缘。
解决方案是给每个项目组的Pod配置反亲和性规则,让同一项目组的不同Pod尽量分散到不同节点,不同项目组的Pod尽量避开同一台GPU服务器。具体做法是通过Pod的反亲和和节点选择器来组合实现。但需要注意的是,反亲和规则不能配置得太死,否则在GPU资源紧张时,调度器可能找不出任何满足条件的节点,导致任务长时间处于Pending状态。
一个实用的折衷办法是:先把同一项目组的训练任务用软反亲和,能分散就分散,分散不了也能接受,用调度器自带的binpack策略兜底。
4.4 算力分配中的“优先级抢占”玩法
除了配额和亲和性,真正的算力保障还要靠优先级抢占。CubeStudio的任务调度器里上层有多个PriorityClass。我的配置是这样的:
- 高优推理服务:PriorityClass值最大,几乎不参与抢占。
- 紧急实验任务:优先级中等,排队时间超过一定阈值可以抢占低优任务。
- 普通批训练任务:优先级最低,适合跑超参数搜索、模型评估这类不需要即时返回的作业。
实际流量高峰期出现过几次“低优任务直接被杀掉”的事件,后来我在任务模板里增加了checkpoint和中断恢复的机制,保证被抢占的低优任务可以在换一台空闲机器后继续跑,不丢失关键轮次的状态。这个点对团队体感影响巨大——用户不在乎被抢占,在乎的是被抢占之后我的实验进度还能不能续上。
到这里,多机、多集群、多资源组的部署路径已经全部走通。下面进入验收和排障阶段,把整个系统压到真实负载下验证,并整理一套可以随时查“旧账”的记录册。
5. 部署后的验收清单、排障实录与最佳实践
部署完成后,不要急着向团队宣布“算力平台升级完毕”。多机的稳定性、多集群的可靠性、资源组的隔离性,都必须用任务打一打、用异常场景压一压,才能真正信得过。
5.1 验收清单:能不能跑通才算数
我自己做验收时会走一套固定清单,像体检一样跑一遍:
- 多机验收:分别在多台不同GPU节点上提交不同类型任务,确认调度器能把任务分布到不同节点,且GPU利用率有监控数据产出。
- 多集群验收:在管理中心发起一次“跨集群手动调度”,把一个训练任务从集群A指派到集群B,确认数据读取、任务启动、日志回流均正常。
- 资源组验收:为某个项目组设置一个极小的配额,提交超过配额的任务,确认超出部分被拒绝或排队,且有清晰的中文提示。
- 隔离性验收:模拟某个业务集群网络不可用,确认中心管控面板能快速标识该集群异常,其他集群任务不受影响。
- 存储验收:在某个集群上删除并重新创建一组任务,确认数据集和模型权重能从共享存储里稳定拉取,不会出现数据不一致。
验收过程中我最常看到的状态是“所有组件都正常,但任务就是跑不动”。这时候不要慌,先把时间花在检查调度器是不是因为某个资源组根本没有匹配的节点而无法调度。
5.2 高频问题排查速查表
把过去遇到的常见问题整理成了一张速查表,特别适合大家在排障时快速定位:
| 症状 | 大概率原因 | 快速处理 |
|---|---|---|
| 任务一直Pending | 资源组配额不足或节点标签不匹配调度条件 | 查看任务详情里的节点选择条件,先扩容或改标签 |
| Pod提示GPU不可用 | GPU Driver或Device Plugin版本不一致 | 检查节点GPU上报数,重装统一版本插件 |
| 跨集群调度失败 | 访问Token过期或网络策略拦截 | 检查集群身份授权有效期,刷新Token |
| 资源组配额不生效 | 项目组与资源组绑定关系没配好 | 核对项目组所属资源组,在管理界面重新绑定 |
| 节点显示NotReady | 容器运行时与kubelet配置不一致或时钟偏移 | 对比节点参数,同步时间和配置 |
不要指望一次排障就能根治所有问题。我建立了一个“问题日志记录本”,每次排障完把现象、根因、解决过程简要记录进去。后面再遇到类似情况,翻记录的效率远高于现场重新看日志。
5.3 三条最佳实践:从能用到好用
有几个经验属于“不做也行,但做了以后会省很多事”的级别,我单独拿出来讲一下。
第一,给每个资源组配置“容量水位告警”,不要等任务大批量排队之后才发现资源不够。我在监控面板配置了三个档位:资源使用率超过70%时提示后续任务可能排队,超过85%时发出通知,95%以上时触发紧急告警。这个做法能让你在用户抱怨之前就提前扩容。
第二,多集群场景下,优先保证“控制面可以一定范围内的不可用”,但绝不接受“数据面被打满导致互相拖累”。具体实操上,我把网络策略严格限制在“业务集群内部”和“业务集群到存储池”两个方向,禁止业务Pod之间跨集群互访,从规则层面杜绝了因为网络流量互相干扰引发的稳定性问题。
第三,存储上的目录规划要提前统一。很多团队在单机阶段并不在意目录结构,各自在本地盘上随意放数据。多机多集群后,数据如果分散在各节点本地盘,任务调度到其他节点就必然读不到数据。我最终定下的规范是:所有数据集、模型、日志统一放到共享存储,节点本地盘只放临时文件和缓存。这个规范在部署阶段没有强制推行,后来靠数据迁移和任务报错教育了所有人。
最后再分享一个经验
折腾完整个系统后,我最大的体会是:多机、多集群、多资源组,本质上是一套“算力供给系统”的三个切面,它们之间不能割裂考虑。很多时候问题出在“你先做了多机,等资源不够了才想到多集群,最后才做资源组”,结果各种组件东拼西凑,每个功能都是后来打补丁加上去的,权限关系和调度逻辑乱成一团。
如果重新来一次,我会先花一天把项目组边界、资源组配额关系、集群之间的信任模型全部画出来,哪怕简单画在纸上,也比直接上手部署要靠谱得多。后续真正的操作反而顺理成章,毕竟解决方案已经清晰了,剩下的不过是按图索骥。
如果你正在规划单机向多机多集群演进,我的建议是:不要贪多求全,先把“多机+资源组”做扎实,再在此基础上叠加“多集群”。每一步都确认稳定后再往下一个形态推进,你会省下大量返工的时间。