1. 从一张GPU账单说起为什么异构算力调度成了AI平台的生死线去年帮一个做多模态训练的团队看他们的算力账单发现一个很典型的现象8张A100的集群GPU利用率长期在35%上下浮动但训练任务排队时间却经常超过两小时。运维同学很委屈——任务提交上来的时候确实没卡但跑着跑着就发现有的卡在等数据加载有的卡在等CPU预处理还有的卡被一个只用了2GB显存的小推理任务占着剩下70多GB全空着。这就是典型的异构算力资源碎片化问题GPU、CPU、内存、磁盘这四类资源各自为政调度器只看GPU数量完全不管CPU和内存的配比是否合理。AllData数据中台集成Crater这个开源项目要解决的就是这件事。Crater的定位很明确——AI训推一体化算力平台把训练和推理两种截然不同的负载放在同一套资源池里统一调度。训练任务吃GPU显存和NVLink带宽推理任务吃CPU和内存带宽两者对磁盘IO的要求也完全不同。如果还是用传统的Kubernetes默认调度器按GPU数量做整数分配那资源浪费是必然的。我先把这篇要讲的东西说清楚Crater怎么接入AllData中台、异构资源怎么抽象、GPU/CPU/内存/磁盘四类资源怎么做联合调度、训推混部时有哪些坑、以及我实际跑下来觉得最值得抄的几个配置。适合正在搭建AI平台、被算力利用率折磨的运维和算法同学看也适合想了解异构调度底层逻辑的开发者。提示本文涉及的所有配置和参数都基于Crater社区版的实际部署经验不同版本字段可能有差异建议对照官方文档核对。2. Crater在AllData中台里的角色定位与接入方式2.1 为什么不是直接上Kubernetes默认调度器很多人第一反应是Kubernetes不是有Device Plugin机制吗装个NVIDIA的插件就能调度GPU了为什么还要引入Crater这个问题我一开始也问过。实际跑下来发现K8s默认调度器有三个硬伤第一GPU分配是整数级别的。一张80GB的A100要么整张分出去要么不分。但实际场景里一个推理任务可能只需要10GB显存一个微调任务需要30GB剩下的40GB完全可以再塞一个任务。K8s原生做不到显存切分除非上MIG或者vGPU方案但MIG的配置粒度是固定的不够灵活。第二调度维度太单一。K8s调度器主要看CPU和内存的request/limitGPU只是作为一个扩展资源计数。它不会去判断“这个节点虽然还有1张GPU空闲但CPU已经跑到90%了再塞任务进去数据加载会成为瓶颈”。Crater的做法是把GPU、CPU、内存、磁盘IOPS四个维度做成联合打分任何一个维度不满足阈值就不调度。第三训推混部缺乏优先级和抢占机制。训练任务通常跑几个小时甚至几天推理任务是毫秒级响应。如果两者混在一起推理任务被训练任务堵住就是灾难。Crater内置了优先级队列和抢占策略高优先级的推理任务可以抢占低优先级训练任务的资源被抢占的训练任务会自动checkpoint然后重新排队。2.2 AllData中台与Crater的集成架构AllData中台本身是一个数据资产管理平台Crater作为算力调度层嵌入进去整体架构分三层资源抽象层Crater Agent跑在每个计算节点上负责采集GPU显存、GPU利用率、CPU核数、内存容量、磁盘读写带宽和IOPS。这些指标每5秒上报一次到Crater Scheduler。调度决策层Crater Scheduler维护一个全局资源视图收到任务请求后先做资源过滤哪些节点满足最低要求再做打分排序哪个节点综合得分最高最后做绑定。任务执行层任务通过AllData中台的API提交Crater把任务转成K8s的PodSpec但扩展了自定义的ResourceClaim字段用来声明显存大小、CPU核数、内存量、磁盘IOPS需求。集成的关键点在于资源声明格式的统一。AllData中台对外暴露的接口是JSON格式Crater内部用的是protobuf中间需要一个转换层。我们当时踩的坑是中台传过来的显存单位是MBCrater默认是MiB差了一个换算系数导致任务实际拿到的显存比申请的多了一点点虽然不影响运行但资源核算对不上。后来在转换层加了一个单位归一化模块才解决。2.3 部署Crater Agent时最容易忽略的三个细节第一个细节是GPU指标采集的权限。Crater Agent需要读取nvidia-smi的输出但在容器里跑的时候默认没有权限访问GPU设备。必须在DaemonSet的securityContext里加上privileged: true或者至少挂载/dev/nvidia*设备。我们一开始只挂了/dev/nvidia0结果多卡节点上只能看到第一张卡。第二个细节是磁盘IOPS的测量方式。Crater默认用fio做基准测试但fio跑一次要几十秒频繁跑会影响业务。我们的做法是改成读取/proc/diskstats的增量每5秒算一次IOPS精度虽然不如fio但足够调度用了。第三个细节是Agent的心跳超时时间。默认是30秒但在网络抖动的时候容易误判节点离线。我们调到了90秒同时加了重试机制。这个参数在crater-agent.yaml的heartbeatInterval和heartbeatTimeout字段里改。3. GPU/CPU/内存/磁盘四类资源的联合调度逻辑拆解3.1 GPU显存切分不是简单的除法Crater的显存切分用的是动态分配硬隔离的方案。每个任务申请显存时Crater会在节点上创建一个CUDA Context通过cudaMalloc预留指定大小的显存然后把这个Context绑定到任务的进程组。这样即使任务内部有内存泄漏也不会影响到其他任务的显存。但这里有个坑CUDA Context本身有开销。每个Context大约占用300MB左右的显存如果一张卡上切了10个任务光Context就吃掉3GB。所以Crater默认限制单卡最多切8个任务超过8个就排队。这个限制可以在crater-scheduler-config的maxTasksPerGPU字段调整但不建议调太高。另一个坑是显存碎片。假设一张80GB的卡先分配了30GB再分配了20GB释放了30GB这时候虽然总空闲显存是60GB但最大连续块可能只有30GB。Crater的做法是定期做显存整理把碎片化的空闲块合并。整理频率默认是每小时一次可以在配置里改成更频繁但整理本身有开销太频繁反而影响性能。3.2 CPU与内存的配比约束训练任务和推理任务对CPU/内存的需求完全不同。训练任务通常需要大量CPU做数据预处理CPU:GPU比例大概在8:1到16:1之间推理任务CPU需求少但内存需求大因为要缓存模型权重和中间结果。Crater在调度时会检查CPU:GPU配比和内存:GPU配比两个约束。如果节点上GPU还有空闲但CPU已经用了80%以上调度器会给这个节点打低分优先选其他节点。这个阈值可以在crater-scheduler-config的cpuThreshold和memoryThreshold字段配置默认都是80%。我们实际跑下来把cpuThreshold调到70%效果更好。因为CPU跑到80%的时候数据加载的延迟已经开始明显上升了虽然还没到瓶颈但训练速度已经受影响。提前在70%就停止调度新任务整体吞吐反而更高。3.3 磁盘IOPS的隔离与限流磁盘是最容易被忽略的资源。训练任务读数据集的时候顺序读带宽能跑满NVMe推理任务加载模型的时候随机读IOPS很高。两者混在一起磁盘成为瓶颈所有任务都变慢。Crater的做法是给每个任务设置IOPS上限和带宽上限通过cgroup的blkio子系统实现。训练任务默认给高带宽、低IOPS顺序读为主推理任务给低带宽、高IOPS随机读为主。这个分类是自动的——Crater会根据任务的镜像和启动命令判断是训练还是推理也可以手动在任务描述里指定workloadType: training或workloadType: inference。注意blkio限流对NVMe的效果不如对SATA盘明显因为NVMe的并发队列太深了。如果用的是NVMe建议同时开启Crater的IO调度器它会在应用层做请求合并和排序。3.4 联合打分的计算公式Crater的调度打分公式大致是这样的score w_gpu * (1 - gpu_util) w_cpu * (1 - cpu_util) w_mem * (1 - mem_util) w_disk * (1 - disk_util)四个权重默认都是0.25但可以根据集群的实际情况调整。比如GPU密集型集群可以把w_gpu调到0.4CPU密集型集群把w_cpu调到0.4。权重配置在crater-scheduler-config的scoreWeights字段。我们集群的配置是w_gpu0.35, w_cpu0.25, w_mem0.25, w_disk0.15。调这个权重的依据是GPU是最贵的资源优先保证GPU不空闲磁盘相对便宜权重最低。调完之后GPU利用率从35%涨到了62%效果很明显。4. 训推一体化场景下的资源争抢与优先级设计4.1 训练任务和推理任务的资源画像差异先看一张对比表这是我们在生产环境跑了三个月总结出来的维度训练任务推理任务GPU显存大通常20GB以上小通常4-16GBGPU计算持续高负载利用率70-95%波动大峰值高但均值低CPU高数据预处理吃核低主要是请求调度内存中等主要是数据缓存高模型权重常驻磁盘顺序读为主带宽敏感随机读为主IOPS敏感运行时长小时到天级别毫秒到秒级别容错性可checkpoint可重启要求高可用不能中断这张表是设计调度策略的基础。训练任务可以容忍被抢占只要有checkpoint推理任务不能容忍中断。所以Crater的优先级设计是推理任务默认高优先级训练任务默认低优先级但训练任务可以声明“不可抢占”。4.2 抢占与Checkpoint的配合机制Crater的抢占流程是这样的当高优先级任务需要资源但集群没有空闲资源时调度器会找一个低优先级任务给它发送SIGTERM信号。任务收到信号后有30秒的时间做checkpoint。30秒后如果还没退出发SIGKILL强制杀掉。这里的关键是checkpoint的自动化。我们要求所有训练任务必须集成Crater的checkpoint SDK在收到SIGTERM时自动保存模型状态和优化器状态到持久化存储。没有集成SDK的任务被抢占后只能从头开始跑非常浪费。实际跑下来30秒的checkpoint窗口对大多数模型够用但如果是百亿参数以上的大模型保存一次要几分钟。这种情况可以在任务描述里设置gracePeriod: 300把窗口延长到5分钟。但窗口越长高优先级任务等待的时间也越长需要权衡。4.3 推理任务的弹性伸缩与冷启动优化推理任务的特点是流量波动大白天高峰、晚上低谷。Crater支持基于QPS的弹性伸缩当某个推理服务的QPS超过阈值时自动扩容副本数低于阈值时自动缩容。但推理任务有个致命问题冷启动慢。一个7B的模型从拉镜像到加载权重到能响应请求大概要2-3分钟。如果流量突然涨上来扩容的副本还没起来请求就已经超时了。我们的优化方案是预热池保持一定数量的“热”副本这些副本已经加载好模型但不接收流量。当需要扩容时直接把热副本加入负载均衡秒级生效。预热池的大小根据历史流量曲线动态调整白天保持3个热副本晚上保持1个。这个功能Crater原生支持配置在crater-inference-config的warmPoolSize字段。但要注意热副本也占GPU显存会降低整体利用率。我们的做法是把热副本调度到GPU利用率较低的节点上用Crater的preferLowUtilization策略。5. 实际部署中踩过的五个坑与排查过程5.1 坑一GPU显存分配成功但CUDA初始化失败现象任务申请了20GB显存Crater显示分配成功但任务启动时报CUDA_ERROR_OUT_OF_MEMORY。排查过程先看nvidia-smi发现显存确实被占用了20GB但任务就是跑不起来。后来用cuda-memcheck跑了一个最小复现发现是CUDA Context创建失败。原因是Crater在分配显存时只做了cudaMalloc但没有创建Context。任务启动时创建Context需要额外的显存而这时候显存已经被cudaMalloc占满了。修复方案在Crater的显存分配逻辑里先创建Context再cudaMalloc。Context的显存开销约300MB要算在任务申请的量里面。这个修复在Crater v0.8.2版本里已经合入。5.2 坑二CPU限流导致数据加载线程被饿死现象训练任务跑着跑着速度突然掉一半看GPU利用率从80%掉到30%。排查过程用pidstat看任务的CPU使用发现数据加载进程的CPU时间被压缩得很厉害。查Crater的cgroup配置发现CPU quota设置得太紧。Crater默认给训练任务分配request级别的CPU但limit设成了request的1.5倍。数据加载是突发性的1.5倍不够用。修复方案把训练任务的CPU limit调到request的3倍同时开启CPU burst功能。配置在任务描述里的cpuLimitMultiplier字段。调完之后GPU利用率稳定在75%以上。5.3 坑三磁盘IOPS限流误伤了模型加载现象推理任务启动时加载模型特别慢从正常的30秒变成3分钟。排查过程查Crater的IO限流配置发现推理任务的IOPS上限设的是500。但模型加载是随机读7B的模型有几百个文件每个文件都要随机读500 IOPS根本不够。Crater自动分类把推理任务归到了“低IOPS”类别但实际上模型加载阶段需要高IOPS。修复方案在任务描述里手动指定ioProfile: model-loading这个profile给的是高IOPS、低带宽。或者更简单的做法把模型文件打包成一个大文件顺序读加载这样用低IOPS高带宽的profile就够了。我们后来把所有模型都转成了safetensors格式的单文件加载速度恢复了正常。5.4 坑四节点心跳丢失导致任务被误迁移现象任务跑了一半突然被迁移到另一个节点从头开始跑。排查过程查Crater Scheduler日志发现原节点的心跳超时了。但节点本身没挂只是网络抖动了几秒。Crater默认30秒没收到心跳就认为节点离线触发任务迁移。修复方案把heartbeatTimeout从30秒调到90秒同时加了心跳重试。另外对于已经运行超过10分钟的任务即使节点心跳丢失也不立即迁移而是先尝试重连。这个逻辑在Crater v0.9.0里加了配置项migrationPolicy: conservative。5.5 坑五多卡训练的NCCL通信被CPU限流影响现象多卡训练时NCCL的all-reduce操作特别慢8卡训练比4卡还慢。排查过程用NCCL的调试日志看发现all-reduce的延迟很高。查CPU使用发现NCCL的通信线程被CPU限流了。NCCL做all-reduce的时候需要CPU参与协调如果CPU被限流通信效率直线下降。修复方案给多卡训练任务单独设置CPU策略保证NCCL线程有足够的CPU时间。具体做法是在任务描述里加ncclCpuPriority: highCrater会给NCCL相关线程设置更高的CPU调度优先级。另外把cpuLimitMultiplier调到4倍以上。6. 把GPU利用率从35%拉到70%几个立竿见影的调优动作6.1 显存超卖与安全水位Crater支持显存超卖也就是说节点上所有任务申请的显存总和可以超过物理显存。这听起来很危险但实际上大多数任务的显存使用是波动的峰值只占申请量的70%左右。Crater的做法是设置一个安全水位默认是物理显存的90%。当实际使用超过90%时触发显存回收或者任务迁移。我们集群的配置是overcommitRatio: 1.2也就是允许申请120%的显存。同时把安全水位调到85%留更多缓冲。跑下来没有出现过OOMGPU利用率从35%涨到了55%。6.2 训练任务的Checkpoint频率优化Checkpoint太频繁会影响训练速度太稀疏又会导致被抢占时损失太多。我们的经验值是每30分钟一次全量checkpoint每5分钟一次增量checkpoint。增量checkpoint只保存优化器状态的变化量速度快占用存储少。全量checkpoint保存完整模型用于恢复。Crater的checkpoint SDK支持增量checkpoint配置在任务描述里的checkpointInterval和incrementalInterval字段。我们跑下来增量checkpoint的开销只有全量的十分之一对训练速度的影响可以忽略。6.3 推理任务的批处理与显存复用推理任务如果一次只处理一个请求GPU利用率会很低。Crater支持动态批处理把多个请求攒在一起凑成一个batch再送进GPU。这样GPU的计算单元利用率更高显存也能复用。批处理的大小是动态调整的根据请求的延迟要求来定。延迟要求高的服务batch size小一点延迟要求低的batch size大一点。配置在crater-inference-config的maxBatchSize和batchTimeout字段。我们设置的是maxBatchSize: 32, batchTimeout: 10msGPU利用率从20%涨到了45%。6.4 节点亲和性与数据本地化训练任务读数据集的时候如果数据在远程存储上网络带宽会成为瓶颈。Crater支持数据本地化调度如果某个节点上已经缓存了任务需要的数据集调度器会给这个节点加分优先把任务调度过去。数据本地化的配置在任务描述里的dataLocality字段可以设置preferred或required。我们设置的是preferred因为required太严格容易导致任务排队。配合Crater的分布式缓存数据集在多个节点上有副本调度器选最近的节点。6.5 监控与告警的闭环调优不是一次性的需要持续监控。我们搭了一套监控看板核心指标包括GPU利用率、显存使用率、CPU使用率、内存使用率、磁盘IOPS、任务排队时间、任务平均运行时长。每个指标都设了告警阈值比如GPU利用率连续10分钟低于30%就告警说明调度有问题。Crater自带Prometheus指标导出配置在crater-monitor-config里。我们用的是Grafana做可视化告警走的是Alertmanager。这套监控帮我们发现了不少问题比如某个节点的GPU驱动版本不一致导致调度失败就是通过监控看出来的。7. 写在最后一些个人体会这套东西跑了大半年最大的感受是异构算力调度不是技术问题是平衡问题。GPU利用率、任务等待时间、任务成功率、运维复杂度这四个指标互相制约。你把利用率拉高了等待时间就长了你把等待时间压短了利用率就下来了。Crater给了一套可调的参数但最终怎么调取决于你的业务更看重什么。我们集群的最终配置是GPU利用率目标65%任务平均等待时间不超过5分钟任务成功率99%以上。为了达到这个平衡我们牺牲了一些利用率换来了更稳定的任务体验。如果你的业务对等待时间不敏感可以把overcommitRatio调高利用率还能再涨。另外Crater的社区版功能已经够用了但如果你需要更细粒度的显存隔离比如按MB级别切分或者需要跨集群调度那得考虑企业版或者自己二次开发。我们目前是自己改了一部分调度逻辑主要是加了自定义的打分插件适配我们自己的业务优先级。最后分享一个小技巧Crater的调度日志非常详细但默认只保留7天。建议把日志导到ELK或者Loki里保留至少30天。我们有一次排查一个偶发的调度失败就是翻了20天前的日志才找到原因。日志这东西平时觉得没用出问题的时候就是救命稻草。
