1. 部署一个vLLM服务
前一段手上拿到一批昇腾 910C,考虑到可以给大家部署一些vLLM服务来用,本来以为就是按照vLLM的官方教程一步一步走完就行了,事实上也确实是这样的,一步一步走完了流程,但是能用和好用确实是两回事情,后面为了vLLM好用踩了不少坑。
先说一下我们用到的模型的情况,经过对kimi-k3、glm-5.2、glm-5.3-flash、deepseek-v4-flash、deepseek-vision等多个模型的测试,在推理性能、占用资源数、模型能力之间取了一个平衡,最终选择部署deepseek-v4.1-flash(dsv4.1-flash),上下文选择了一个比较适中的范围:600k,同时开启了dspark加速。
2. 多实例负载均衡
最先遇到的问题是单实例服务能力不够,最直接的想法就是搞多实例然后在new-api层做负载均衡,单手后期运维起来由于new-api没有做联动,有点运维难度,且负载均衡导致缓存命中降低,所以选择在vLLM和new-api之间加了一层router来做负载均衡。
最早的router是配置文件形态:引擎列表写死在配置里,加减引擎要改配置、重载。引擎少的时候忍忍也就过去了。
后来换了新一代动态router:引擎主动注册,自带cache-aware调度(前缀亲和,同一前缀尽量打到同一引擎)、失败重试、熔断摘除。和看门狗配合之后形成两层防御:
- 看门狗管"引擎挂死":真实推理探针,连续失败整组重启;
- Router 管"坏引擎别接流量":探活失败摘出路由,恢复后自动回归。
3. KV缓存问题
影响vLLM的服务质量TTFT的最大因素就是KV缓存,由于我想部署多个实例,导致留给KV的空间就剩下很少了,所以就需要考虑把KV卸载到DRAM或者SSD上,最开始用到的就是MemCache来做KV缓存管理。共享缓存的第一步是 MemCache:vllm-ascend的AscendStoreConnector,在本机内存里开一块池,把前缀 KV 存出去,下一个请求来了先查池。
但是在实际上线服务的时候遇到很多奇怪的问题:
- 存的日志一切正常,块块都写进去了;
- 审计计数器显示查询也在跑,exist请求源源不断,还能查到一部分key;
- 但命中恒为 0,TTFT 纹丝不动。
当时给它起了个名字叫"只存不取",一度怀疑池子根本没接上。真正的转机是把日志开到DEBUG重启,再加一个受控探针:灌入约50k的前缀token → 用流量把池驱逐干净 → 原样重发。
- 池侧逐组核对,exists全中:96/96、5/386……每个key都在;
- 紧接着一行
final hit=0; - 请求全量冷预填。
所以不是"只存不取",是查到了也算零命中。卡点在"查到之后怎么算命中长度"。
对着源码读才发现是粒度基不一致:池里存的是32-token粒度的块哈希,而vLLM core的契约写得明明白白——查命中时调用方必须传"对齐粒度 = 哈希粒度"(源码注释原话:caller passes alignment_tokens = hash_block_size)。我们部署分支的协调器传的却是引擎块大小128。128对32,取出来的哈希和池里存的根本不是同一批值,第一块就miss,后面全部作废。
先试了一行补丁:把128改成32,第一组立刻命中12288——成功了一组。但滑窗组不支持细粒度命中(直接断言),压缩器状态组的粒度又不一样,池侧还有一层按错误粒度截断的覆盖上限。修到最后确认:这个分支(vLLM 0.23)的命中路径就是个半成品,补丁救不完。
真正的解法是上游 v0.30.0rc1 把这条路径正面重写了:查询集收窄到"可前缀缓存的组",统一调度侧和 worker 侧的命中语义。换镜像,命中日志直接出现,一行补丁都没打。顺手还赚了个惊喜:新版本把查找表挪到了主机内存,腾出来的显存全给了KV池——池容量6.88倍(98万 → 678万token),600K长文加16并发在容量上从此无障碍。
4. 共享KV池
MemCache修好之后,后续的问题也逐渐暴露出来了:池是"每节点一份"的孤立池(元服务绑在本机回环地址上),跨节点从来没有真正通过;引擎一重启,池就空。用户的文档就那几份,却要每个引擎各存一份、重启全丢、再各预填一遍。
4.1 MemCache方案
把各节点的元服务指到同一个中心点:段注册、挂载、全局rank偏移,所有前置步骤全绿。然后跨节点put,100%失败(error -1),重试的put风暴反手把引擎拖死。关掉fabric内存直传的开关复测,依旧。
4.2 Mooncake方案
换分布式KV store:master起来了、几十个段全部挂载成功、对端引擎也能连上——万事俱备。然后put计数恒为 0,设备侧内存拷贝全部超时(EE1002)。想退回普通TCP传输:vllm-ascend硬编码只认昇腾直连协议,NotImplementedError,此路不通。
4.3 问题定位
上述两个方案都是经过单实例验证过的,但是最后都卡死在KV传输上,可奇怪的是这批节点正常训练都NPU传输数据是完全没问题的,凭什么 KV 不行?
答案是两者走的是完全不同的两条路——训练走 HCCL,自带连接建立、内存注册和传输协议,自成一体;KV直传走的是驱动层的"统一内存地址"通道,对固件和组网有隐含要求。
4.4 换方案
直传不通,那就把 KV 写到共享文件系统里,谁来要谁来读。昇腾的统一缓存管理器(UCM)提供这条路径。UCM 0.8.1版本接上之后,机械指标漂亮得不像话:三个实例100%命中,跨实例 TTFT 从几十秒塌到两秒级。
但还没来得及高兴呢,发现冒烟测试没通过,TTFT指标一切正常,但是命中请求的输出是乱码,冷请求完全正常。
又是一顿排查发现UCM 0.8.1的模型适配列表里根本没有dsv4.1-flash——它的专用连接器只认上一代的压缩布局,而新模型是压缩比混排的,天然不满足。UCM 0.9.0rc2 的release notes里明明白白写着支持。换版本,删掉全部补丁,零修改:冷热对照逐字一致、杀进程重启后从盘上纯回读逐字一致、A实例写B实例新进程读逐字一致,全部通过。
4.5 NFS的账,和共享盘的坑
UCM方案的瓶颈是多个实例之间的共享盘的带宽。2k token的前缀,盘读30多秒,而引擎热机重算只要5秒——短文档命中反而更慢。好消息是命中耗时几乎不随长度增长(4.4s → 6.7s),冷算却近似线性(7.5s → 27.2s),文档越长越赚:
于是把NFS换成更大的共享盘。但是又又又又遇到了坑:编排层提供的"共享盘挂载"选项,实测是个假动作——实际绑定到的是节点本地盘,既不共享,还往系统盘里写。改成宿主路径直挂共享卷,才真正打通。
6. 小结
- 能跑 ≠ 能用。 这次以为是一个很简单的vLLM部署任务,前后测试了一个多月,到目前的状态还是有很多坑仍然在解决…….
- 版本配套是第一生产力。 中间遇到了多次版本不匹配导致的问题,让AI帮忙修复,他们老想着打补丁,实际上是不靠谱的。
目前这套服务稳定跑着,下一步是把投机解码的接受率再往上拱一拱。至于还会遇到什么新的坑——按这一个多月的光荣传统,肯定是会有的。