20
6
1
DCU技术达人
新人贴
给 vLLM 打保命补丁:海光 DCU 上多模态批量崩溃的源码级排查与修复
2026-09-30 08:55:02
10
在[海光 DCU](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E6%B5%B7%E5%85%89+DCU&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLmtbflhYkgRENVIiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MjgyMjQ2MDAzLCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjEsInpkX3Rva2VuIjpudWxsfQ.YLLwAO5myuWT7qWayxQqDQVotiKYFbkmOVFG09QlR0w&zhida_source=entity) 八卡机上部署多模态模型 [MiniCPM-V 4.6](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=MiniCPM-V+4.6&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiJNaW5pQ1BNLVYgNC42IiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MjgyMjQ2MDAzLCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjEsInpkX3Rva2VuIjpudWxsfQ.ttlMNOgZwPGyR_1jcYS1mc0zyaYqBgsSS-SurZnjRro&zhida_source=entity) 的集群后,我们撞上了一个最凶险级别的 bug:**并发图片请求时,[vLLM](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=vLLM&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiJ2TExNIiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MjgyMjQ2MDAzLCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjEsInpkX3Rva2VuIjpudWxsfQ.mJfT--We2KvEJKdyvCAl9eSCmI6mWAzDZhDg_IBDvk4&zhida_source=entity) 实例会随机崩溃,一崩就是整张卡的服务全灭**。[串行请求](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E4%B8%B2%E8%A1%8C%E8%AF%B7%E6%B1%82&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLkuLLooYzor7fmsYIiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.IxqxpmTYt8mL82F5MQ7ebISLmnHcAO2avByrl-_KYXg&zhida_source=entity)永远不崩、自测永远全绿,只在真实批量流量下偶发——时序相关、不可稳定复现,这类 bug 的排查难度是最高的。 更麻烦的是约束:出问题的是 DCU 定制版 vLLM(`0.20.0+das.dtk2604`),**不能 pip 升级**——升到上游新版会丢掉 [HIP 适配](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=HIP+%E9%80%82%E9%85%8D&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiJISVAg6YCC6YWNIiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MjgyMjQ2MDAzLCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjEsInpkX3Rva2VuIjpudWxsfQ.feDRTeuoEXzGkf3w61iDKsXNRJQoNUExgpo_IQE3FkQ&zhida_source=entity),直接跑不起来。而上游修复在 v0.22 才合入。等厂商适配是唯一「正规」出路,但生产等不起。 这篇记录完整的排查链路:从一条 ValueError 日志出发,读进 vLLM V1 的源码,搞清楚两个数字(327 和 303)各自怎么来的,最后给出双层修复——一层规避、一层源码补丁。核心内容是那个补丁的思路:**当不能修好它时,如何让它死不了**。 ## 症状:一崩就是一张卡 集群形态是 8 个 vLLM 实例各占一张 DCU(端口 8002–8009),前置[反向代理](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLlj43lkJHku6PnkIYiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.1Mh2v1-S7Bpr8y8Rh7dyWX0BDEykyEl6ZzEE3wlJhkQ&zhida_source=entity)。上线压测时发现:并发 ≥12 张图片的请求打进来,某些实例会崩在这样一行日志上: ```text ValueError: Attempted to assign 303 = 303 multimodal tokens to 327 placeholders ``` 然后 EngineCore 子进程退出,整个实例宕机。特征归纳: | 特征 | 表现 | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | 触发条件 | 多个图片请求落在同一个 scheduler step 里批量处理 | | 串行/单请求 | 从不崩 | | 复现稳定性 | 偶发,时序相关——同样的请求序列重放不一定崩 | | [爆炸半径](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E7%88%86%E7%82%B8%E5%8D%8A%E5%BE%84&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLniIbngrjljYrlvoQiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.GiOxwmFnHUq4caMhPj9CXCNoCKQxt16ruuYamFGfThI&zhida_source=entity) | 一个 raise 杀死 EngineCore → 整张卡的实例消失 | 「偶发 + 并发相关 + 进程死亡」这个组合已经足够画出嫌疑画像:**批处理路径上有个尺寸对不齐的检查,而且这个检查失败的方式是自杀**。 ## 排查:两个数字各是谁算的 顺着报错信息 grep vLLM 源码,找到 raise 的位置,再往上追调用链,落在 vLLM V1 引擎的 `gpu_model_runner.py`(`_gather_mm_embeddings` 一带)。多模态请求的执行流程里有两个关键数据,分别由**两条完全不同的路径**算出来: 1. **占位符数(327)**:文本 prompt 进 chat-template 后,图片被展开成一串占位 token,掩码 `is_mm_embed` 里 true 的数量。对 MiniCPM-V 4.6,这个数由模型侧的 `_compute_visual_tokens`**纯数学公式**算出(图片宽高 × 切片方案 → 预估 token 数); 2. **编码器实际输出(303)**:图片真正过一遍 vision encoder 后,实际产出的 embedding [向量](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E5%90%91%E9%87%8F&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLlkJHph48iLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.-aienxqUE7dDB1cHgVLiXDtwbVT1Jf_PDJELeGzRvpI&zhida_source=entity)个数。 两条路径在单请求时「碰巧」总是相等的——但 MiniCPM-V 4.6 有个特性:**每张图的 token 数是动态的**(`max_slice_nums=9`,按图片长宽比动态切片),且 chat-template 展开时会混入 ``、换行、列分隔等**结构 token**。vLLM 0.20 的占位符替换逻辑没有把这两边对齐:单 sequence 的 scatter 蒙对了,**多个 sequence 在同一个 scheduler step 里批量 scatter 时,只要任何一个 sequence 的计数不齐,就抛异常**。 而 vLLM V1 的[进程模型](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E8%BF%9B%E7%A8%8B%E6%A8%A1%E5%9E%8B&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLov5vnqIvmqKHlnosiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.yL9ZjsPVjbZSuk0YqbThBE1MKasDefaq4VupbrJWV6Q&zhida_source=entity)决定了这个 raise 的代价:EngineCore 是独立进程,engine loop 里未捕获的异常 = 进程退出 = 整张卡的服务蒸发。**一个请求的数据问题,惩罚是一张卡的[宕机](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=2&q=%E5%AE%95%E6%9C%BA&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLlrpXmnLoiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MiwiemRfdG9rZW4iOm51bGx9.7CIXTDMY0ePswM2tXIQYLMwsSvfoDJuTDte7tq_nNBk&zhida_source=entity)**——这个错误处理的设计,比 bug 本身更值得批评。 (后来确认上游已有人踩过同一坑,修复在 vLLM v0.22 合入,对应 PR #43213——我们的定位和上游修的方向一致,这在「不敢相信自己结论」的时刻是很重要的外部印证。) ## 约束:为什么不能等官方、也不能升级 修复路径理论上有三条,先排除两条: * **pip 升级 vLLM**:不可行。DCU 版 vLLM 带厂商补丁(HIP 适配、算子注册),升级到上游版 = 丢适配 = 服务起不来。这是[国产卡](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E5%9B%BD%E4%BA%A7%E5%8D%A1&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLlm73kuqfljaEiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.HPaj385MVJtRc6DTOK5IdnBE2FjWWh70NFsNpSJDRK0&zhida_source=entity)生态的常态约束:**你的版本坐标系是「上游版本 × 厂商适配」的二维组合,单看上游版本号没有意义**; * **等厂商出带修复的 DCU 版**:已提需求,但排期不可控,生产在跑的每一天都在承担风险。 所以只剩下:**在自己这套不能动的二进制栈里,把伤害降到最低**。 ## 修复 A:让 bug 没有触发条件(规避层) [根因](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E6%A0%B9%E5%9B%A0&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLmoLnlm6AiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.5RrVR2s1-q3KP5p1gXdvVs2F5mQcFDBRNb041B3nqz0&zhida_source=entity)的触发条件是「多 sequence 同一 step 批量 scatter」。那就让这个条件永不成立: ```text --max-num-seqs 1 --mm-processor-cache-gb 0 ``` 每个实例一次只处理一个图片请求,scatter 退化为单 sequence——就是那个「碰巧总是对的」路径,**确定性不触发**。并发行不行?行,8 张卡 × 1 = 集群级并发 8,由反向代理调度承担。 代价是吞吐:每卡并发被钉死在 1。实测 8 卡集群短回答 \~21 张/s、详细描述 \~1.16 张/s——够当时业务用,但天花板明显。这个方案作为**默认配置**上线:先保证不死,再谈快。 ## 修复 B:把 raise 改成降级(保命层) 规避层的问题是它靠「不给 bug 触发机会」活着,一旦有人改了配置、或者某个流量形态绕过了假设,就没有退路。所以需要第二层——直接改 vLLM 源码里的那个 raise。 位置在 site-packages 的 `vllm/model_executor/models/utils.py`,`_merge_multimodal_embeddings` 函数的异常分支。原逻辑是计数不齐就 `raise ValueError`;补丁改成: ```text # 原版:raise ValueError(f"Attempted to assign {n} ... to {m} placeholders") # 补丁版:不匹配时降级,保住 EngineCore 进程 if actual < expected: logger.warning("mm placeholder mismatch: pad %d zeros", expected - actual) padding = torch.zeros(expected - actual, *actual_embeds.shape[1:], dtype=actual_embeds.dtype, device=actual_embeds.device) merged = torch.cat([actual_embeds, padding], dim=0) elif actual > expected: logger.warning("mm placeholder mismatch: truncate %d -> %d", actual, expected) merged = actual_embeds[:expected] else: merged = actual_embeds # 计数相等:走原路径,行为零变化 ``` 设计要点: 1. **正常请求零影响**。计数相等时走的分支和原版完全一致——补丁只在不匹配这个「本来就要死」的分支上生效。这是给[生产系统](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E7%94%9F%E4%BA%A7%E7%B3%BB%E7%BB%9F&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLnlJ_kuqfns7vnu58iLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.WDm2f7JBe93eqCy1PJf97g2Wxy45QJCxAwAnsgnk8Mw&zhida_source=entity)打补丁的第一原则:**只在故障路径上改变行为,happy path 一个比特都不动**; 2. **降级的代价是局部而明确的**:少了 24 个 embedding 就补 24 个[零向量](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E9%9B%B6%E5%90%91%E9%87%8F&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLpm7blkJHph48iLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.JBV5xT41KYFditGx2szs7reMgW69gOlXlifNHroJxx4&zhida_source=entity),对应请求里尾部的图片 token 拿到零 embedding,该请求输出质量轻微降级——**一个请求变笨,好过一个实例死亡**。质量换可用性,在风控识图这类「宁可答得糙、不能没得答」的场景里是笔划算的交易; 3. **留了 warning 日志**:补丁不是修复,是[缓期执行](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E7%BC%93%E6%9C%9F%E6%89%A7%E8%A1%8C&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLnvJPmnJ_miafooYwiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.mWe9RZAYGa_YQITgDBMhP9YqHonsIhOrfMbywRU3m7I&zhida_source=entity)。每次触发都记下来,触发率就是一个「上游 bug 在我们流量上的真实发病率」仪表盘,也是日后向厂商申请升级的依据。 打完双层补丁后压测:并发 12/16/24 图片请求全部正确、8/8 实例零崩溃。之后还试过把 `MAX_SEQS` 开到 4(每卡 4 并发、集群 32),安全网兜住了偶发的不匹配——吞吐上去了,代价是极少数请求轻微降级,这在我们的业务里可接受。 ## 在 site-packages 里改源码的工程纪律 直接改生产环境依赖的源码是危险动作,需要纪律配套: * **可逆**:补丁尽量小而局部(只动异常分支),原代码注释掉而不是删除,`git diff` 式的改动记录存在部署文档里; * **可发现**:在部署 README 顶部显著标注「本环境 site-packages 含本地补丁 N 处,升级 vLLM 时必须重放或确认已包含官方修复」——半年后没人记得改过什么,文档是唯一的记忆; * **有退役条件**:补丁的存在是为了桥接官方修复到位之前的[空窗](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E7%A9%BA%E7%AA%97&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLnqbrnqpciLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.pbPUCrIxr4XO4MjbgO1lgRarWMUfc53qxnuonxD8Aro&zhida_source=entity)。拿到带上游修复的 DCU 版 vLLM 那天,第一件事就是撤掉它,让环境回归「纯官方」状态。 顺带一提,这个补丁能打、打得干净,得益于我们此前把服务从容器搬到了[裸机](https://zhida.zhihu.com/search?content_id=282246003&content_type=Article&match_order=1&q=%E8%A3%B8%E6%9C%BA&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3OTA5MDI0NjEsInEiOiLoo7jmnLoiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyODIyNDYwMDMsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.jS9akmif1rd_MnN5odkAq-Z9N9shjNd9HkKfaqMeXjc&zhida_source=entity) conda 环境——**环境是你自己的,你才有资格对它负责**。在容器黑盒里,这类「改一行源码救命」的操作连可能性都不存在。 ## 小结 这次排查的价值超出单个 bug:它示范了在国产卡生态里面对「上游已修、本地不能升」困境的标准动作——**读源码定位根因(两条路径算出的两个数字)→ 规避层让 bug 失去触发条件(max-num-seqs=1)→ 保命层把死亡改成降级(raise → 补零/截断)→ 纪律层保证补丁可发现、可退役**。bug 是厂商的,但止血是自己的:等待官方修复的窗口期里,服务一天都没停。 --- *环境信息:海光 K100\_AI × 8 / vLLM 0.20.0+das.dtk2604(DCU 定制版,不可 pip 升级)/ MiniCPM-V 4.6 多模态集群 / 上游对应修复:vLLM PR #43213(v0.22)。*
全部评论(0)
只看楼主
暂无数据
--
楼主
Level.2
0 帖子
楼主热帖
暂无数据