尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Ascend for Volcano集成实战:异构算力调度与高可用配置

Ascend for Volcano集成实战:异构算力调度与高可用配置 开头部分华为Ascend和Volcano这两个名字放在一起很多做AI基建的同行应该不陌生。一个代表了国产AI算力的主流加速芯片另一个是Kubernetes生态里最常用的批量调度器。把两者集成起来解决的核心问题就是当你的集群里同时存在GPU、NPU、CPU这些异构资源而训练任务又需要大规模并行跑起来的时候怎么让调度器聪明地分配资源让每个节点把算力吃干榨净还能扛得住节点故障和任务失败。这篇文章是我整理自己在真实集群里做Ascend for Volcano集成的完整记录包括资源上报、调度策略、高可用配置和一堆踩坑实录。如果你是平台工程师、SRE或者正在搭AI训练平台这篇应该能直接当参考手册用。我不打算写那种泛泛的架构介绍而是直接把YAML、命令、参数和代码逻辑摊开来讲每一步都让你能自己动手验证。1. 项目背景与核心思路拆解1.1 为什么默认调度器搞不定异构算力先聊个最根本的问题Kubernetes自带的调度器kube-scheduler为什么没法直接拿来做AI训练调度核心原因是它的调度逻辑偏向“单容器、单资源、先到先得”但AI训练任务往往是多Pod协作的比如分布式训练里多个worker和ps同时启动而且对资源的需求是“一次性要够量”。打个比方kube-scheduler就像一个“一个一个安排座位的服务员”而AI训练任务就像一群一起进场看球赛的观众——要么所有人一起进去要么一个都别进去。如果你让单个Pod先调度其他Pod还在排队等资源那先调度的Pod就会一直空转等待浪费算力甚至引发死锁。这就是集群里常见的“资源碎片化”和“饿死”问题。再往深一点说异构资源本身也是个麻烦事。GPU有显存和算力NPU有AI Core和内存CPU有超标量核这些资源不只是“存在”它们的数量、规格、拓扑关系都需要被调度器感知。默认的调度器只会看nvidia.com/gpu这类简单的资源计数对华为Ascend这种需要额外设置tiling、融并策略的设备来说基本等于瞎的。所以选Volcano本质上是选了一套“批量感知”的调度模型。Volcano源自华为云的batch调度实践后来捐给了CNCF如今在AI、大数据场景用得很多。它引入了队列Queue、PodGroup、Job等概念支持“Gang调度”把一个作业的所有Pod视为一个整体要么全部调度成功要么全部等待从根上解决了资源碎片问题。1.2 华为Ascend和Volcano怎么定位彼此在集成之前先得把两个组件的边界理清楚不然后面看代码会晕。华为Ascend昇腾的硬件侧由CANN工具链和驱动负责而Kubernetes这边要做的是把Ascend NPU当作可调度的资源暴露出来。这一层主要由Device Plugin完成官方叫ascend-device-plugin它会上报类似huawei.com/Ascend910这样的资源类型。Volcano只管调度策略它不直接知道Ascend是什么但它可以知道“每个节点上有多少个huawei.com/Ascend910”。所以二者配合的模型是Device Plugin负责“数数”Volcano负责“分果果”。这个分离设计很干净也符合K8s的插件机制。有人会问那为什么还要同时用Node Affinity和Volcano的队列这是为了兼顾拓扑亲性和任务优先级。比如一个8卡训练任务最好把Pod调度到同一台物理机的8块NPU上减少跨机通信。这个需求光靠Volcano的默认配置做不到需要你在Pod声明里加nodeSelector或者affinity同时配合Volcano的PodGroup把调度单位圈起来。2. 环境准备与集成部署2.1 硬件与软件版本要求我实测的环境信息如下大家可以参考但不用完全一致关键是版本之间的兼容性要查清楚组件版本说明Kubernetes1.24建议1.26以上Device Plugin的API兼容性更好Volcano1.8.0支持Gang调度和拓扑调度Huawei Ascend驱动22.0.0及以上和CANN版本严格对应CANN Toolkit6.3.0提供npu-smi和AI Core操作库ascend-device-plugin1.0.0上报Ascend NPU资源这里有个特别重要的点CANN和驱动不是越新越好一定要查官方兼容性列表。我见过一次只升CANN不升驱动结果npu-smi显示正常但容器里调用aclrtSetDevice直接报错的情况后来发现是驱动接口版本不匹配。2.2 部署Volcano调度器Volcano的部署很简单用helm或者直接YAML我都试过。我推荐先从命令行快速起一套验证完再考虑helm管理版本# 添加Volcano官方helm仓库 helm repo add volcano https://volcano.sh/helm-charts # 安装这里我就直接指定版本并关闭部分特性 helm install volcano volcano/volcano --namespace volcano-system --create-namespace \ --set volcano-scheduler.image.repositoryvolcanosh/vc-scheduler \ --set controllers.image.repositoryvolcanosh/vc-controller-manager \ --setversion1.8.0装完之后检查三个关键Pod是否Runningvc-scheduler、vc-controller-manager、vc-webhook-manager。其中webhook特别重要它负责为Pod注入podgroup引用如果webhook没起来后面提交的Pod不会有调度组概念。2.3 接入Ascend Device PluginAscend Device Plugin需要以DaemonSet的方式部署确保每个网络可达的机器节点都可以上报NPU。从华为官方仓库拉取部署YAMLgit clone https://github.com/Ascend/ascend-device-plugin.git kubectl apply -f ascend-device-plugin/etc/device-plugin-ds.yaml这个DaemonSet会为每个节点创建一个ascend-device-plugin容器它通过Unix socket跟kubelet通信向API Server注册资源。完成后验证一下kubectl get nodes -o json | jq .items[].status.allocatable应该能看到类似huawei.com/Ascend910: 8这样的条目。如果没有优先检查节点上的CANN驱动是否装好用npu-smi info在宿主机上先跑一遍排错从硬件开始查挡在心头的第一个坎基本就是驱动没起来。3. 核心调度逻辑代码实战3.1 资源上报机制——Device Plugin怎么“数N卡”这个环节我拿代码讲。Device Plugin的思路是kubelet启动时通过/var/lib/kubelet/device-plugins/kubelet.sock找Device Plugin服务如果Plugin通过注册接口报了资源名kubelet就会以Allocatable属性对外展示。看一下ascend-device-plugin的关键逻辑它本质上是一个gRPC服务端// 消息里上报的资源数量就是NPU卡片的数量 message ListAndWatchResponse { repeated Device resources 1; } // 每次探测发现物理卡之后构造一个Device并且上报给kubelet func (h *ResourceManager) ListAndWatch(empty *api.Empty, stream api.DevicePlugin_ListAndWatchServer) error { devices : h.apiDeviceManager.GetDevices() rsp : api.ListAndWatchResponse{ Devices: devices, } err : stream.Send(rsp) ... }关键点在于GetDevices()需要只返回“可分配的”NPU那些被占用或者处于异常状态的比如温度过高导致的disabled必须通过ContainerHealthy字段标识状态。我在做容错时发现如果某张卡因为过热被驱动禁用Device Plugin很老实还会继续上报这会导致调度器把Pod调度到一张坏卡上。这个Bug让我排查了很久最后是在换卡时拉了npu-smi info才发现状态完全不对。顺带说一句Volcano调度器会在节点缓存里看资源视图它对资源的状态判断完全依赖Allocatable。所以建议定期跑一遍kubectl describe node确认huawei.com/Ascend910的数量和真实物理卡一致如果发现不一致说明这个插件或者驱动侧有异常要及时定位别等任务失败才折腾。3.2 用Volcano提交批量训练任务不直接起裸Pod而是用Volcano的vcjob资源来提交。如下面的YAML所示我圈了一个8卡训练任务apiVersion: batch.volcano.sh/v1alpha1 kind: Job metadata: name: ascend-bert-pretrain spec: minAvailable: 8 schedulerName: volcano policies: - event: PodFailed action: RestartJob tasks: - name: worker replicas: 8 template: spec: nodeSelector: huawei.com/Ascend910: true containers: - name: bert-worker image: ascend-bert-mpi:latest resources: limits: huawei.com/Ascend910: 1 env: - name: NPU_VISIBLE_DEVICES value: 0,1,2,3,4,5,6,7这段配置显示了Volcano的优势。minAvailable: 8是Gang调度的核心参数它告诉调度器这个Job里必须有8个Pod同时有资源如果不够整套作业不会调度也不会让部分Pod先跑起来。这种设计对多机多卡训练特别有效避免先启动的节点空转等后启动的节点。policies里定义了PodFailed之后怎么做我把它配置成了RestartJob意味着Pod失败后整个作业重来。这里有个经验重试次数太大的话会把故障Pod卡在脏状态后面把cluster的资源越吃越多。建议配合一个外部的容器内存/卡内存监控及时止损。3.3 高可用设计——节点故障与任务容错高可用这个词听起来高端实际上核心就是“怎么应对节点掉线和卡损坏”。我在这块做了三层防护。第一层是Volcano的队列管理。用一个队列专门放高优任务另一个放低优任务可以设置队内任务的priority和抢占关系。高可用场景下如果高优任务需要抢低优任务占着的NPUVolcano的preemptable参数就能激活抢占逻辑。但这个抢占是“不保证”的我在生产环境中看到过抢占后新任务起不来的情况原因是被抢占的Pod没有及时释放NPU驱动资源导致Device Plugin上报的数量和实际不一致。第二层是PodDisruptionBudgetPDB。Volcano的PodGroup天然支持PDB这样就算系统要主动驱逐Pod比如节点维护也不会一次性把所有Pod都驱走。我举个例子如果一个Job有8个Pod你给它配maxUnavailable: 2那么最多同时驱逐2个剩下的6个保证还在跑最大程度降低了训练中断的时间。第三层是节点故障检测。我在集群里装了一个简单的node-problem-detector它会识别npu-smi命令输出的错误码比如卡被禁、显存ECC错误、驱动崩溃。检测到后直接给节点打上污点taint让Volcano的调度器绕开它。这一步特别关键不然坏节点的NPU照样会被纳入可用资源新任务一上去就失败调度器还觉得资源很充足这就是典型的“假资源”。4. 常见问题与排查技巧实录4.1huawei.com/Ascend910不出现部署完Device Plugin节点上却没有这个资源名。我基本会按下面顺序排查步骤命令/操作判断标准1宿主机执行npu-smi info能看到物理卡信息说明驱动正常2查看Device Plugin日志kubectl logs -l componentascend-device-plugin -n kube-system看是否有gRPC注册失败的记录3检查kubelet日志journalctl -u kubelet -n 200确认设备插件是否被kubelet识别4查看Pod状态kubectl get ds ascend-device-plugin -n kube-system -o wide确保DaemonSet是Running最常见的坑是宿主机CANN环境变量没配。Device Plugin启动时会读取ASCEND_VISIBLE_DEVICES之类的环境变量如果这个变量没有指向正确的卡列表插件可能干脆一个卡都不上报。项目里我一开始只配了PATH忘了配CANN的export ASCEND_HOME/usr/local/Ascend结果就看到那个空报数的惨状。正确做法是把环境变量写进DaemonSet的env里而不是依赖宿主机全局配置。4.2 调度完成后容器内看不到NPU设备有时候Volcano调度是成功的Pod也Running了但容器里npu-smi info却看不到任何设备。这个问题的根源通常不在调度器而在Device Plugin的Allocate函数。Device Plugin的Allocate需要在容器启动前把NPU设备映射进容器。它有两种方式一种是通过环境变量比如NPU_VISIBLE_DEVICES告诉容器驱动的设置另一种是通过Linux的/dev/davinci*设备节点映射。如果映射没生效容器里自然看不到。我是这么查的kubectl exec -it pod -- ls /dev/ | grep davinci看设备节点是否存在。如果没有先确认Pod声明里的limits有没有正确指定huawei.com/Ascend910别只写到了requests。Volcano对这种资源有个有趣的行为如果只写requests它调度时也会分配但Device Plugin的Allocate只认limits所以分配了设备但没注入。这一条我真是踩了又踩希望大家别重蹈。4.3 多Pod同步启动时出现死锁再讲一个Volcano特有的问题。当一个Job的minAvailable很大但集群里能同时获得的节点资源不足时调度器会反复尝试。默认的podgroup处理方式是挂起所有Pod但如果你设置了错误的超时时间可能出现一种半拉状态所有Pod都被挂起谁也不让谁释放资源形成“调度死锁”。这个现象在测试环境非常常见特别是你同时跑几个大的Job的时候。解决方法有两个方向。一是用Volcano的队列策略在queue的spec里设置capability限制不同队列的资源让高优先级队列始终有资源可以抢二是打开preemption策略但一定测试好不然低优作业永远无法完成生产会骂娘。我在生产集群上最终采用了“队列亲和性”的组合规定大训练作业只能落在特定的NPU节点池测试作业用另一个池子。这样一来两个池子相对独立不会互相把资源吃死。4.4 性能优化经验——如何让算力跑得更满除了稳定性这块我还有一点独家体验。Volcano默认使用的调度策略是proportion它会按队列权重分配资源但对“硬件拓扑”感知很弱。如果你的NPU节点是两路CPU甚至四路NUMA架构把Pod调度得离内存太远性能会掉一截。这时候用Volcano的tiers配binpack和numaaware策略会好些。我的实测数据显示开了NUMA感知之后8卡训练任务的通信延迟平均降了12%整体吞吐提升约8%。这个结果看起来不大但在大集群里价值就突出了。不过要提醒一下numaaware策略目前对节点拓扑信息的依赖较强你要确保节点上有topologyManangerPolicy支持才行否则策略会静默无效。为了清晰记忆我直接贴一下Volcano scheduler配置里关键的两个参数spec: tiers: - plugins: - name: binpack arguments: binpack.weight: 0.2 - name: numaaware enabled: true5. 实战体验与扩展建议最后这部分我想多说一点真实操作时的体会。整个Ascend for Volcano集成项目的难度不在“跑通”而在“跑稳”。你可以在一个周末把Devices Plugin和Volcano部署好让一个简单的训练Job跑起来。但真正上生产面对的是GPU/NPU混布、突发故障、多租户抢占这些真问题。我个人经验是一定要把“资源上报和分配”这块做成可观测的。我在集群里给vc-scheduler和Device Plugin都挂了Prometheus指标专门盯两类数据一是节点实际NPU卡数和调度器记录的Allocatable数量之间的差值二是Job排队等待时间。这两组数据能提前暴露资源管理上的隐性Bug比我后来用的很多高级追踪工具都管用。另外如果有条件建议把CANN升级到6.3以上它的aclrtMalloc接口对动态shape的支持更好和Volcano的Gang调度搭配起来减少了很多由于显存预分配过大而导致的调度失败问题。这个项目还可以继续往下扩展的方向也很有价值比如把Volcano的队列模型和现有的配额系统做联动让每个业务线有自己独立的NPU预算或者把Ascend的MetalBank和Volcano的上报联动做更细粒度的显存调度。总之这套集成不是改完一两个YAML就完事它更像一个地基把地基打扎实了后面的Agent调度、任务编排、智能弹性就都有了承接的地方。
返回列表