最近一段时间,我们在阿里PPU ZW810E上,对DeepSeek V4 Flash做了几轮适配和调优。测试服务器配有8张96GBPPU,采用TP8部署,推理框架为vLLM,并接入LMCache管理外部KV缓存。
这项工作从Spec Coding的实际使用出发。随着用户增加、代码上下文变长,响应速度和缓存空间都需要考虑。我们也想看看,现有设备还能支撑多少用户,是否还有进一步优化的空间。
几轮调试下来,应用侧的调整带来了一些明显改善,底层适配和持续运行中也陆续碰到了问题。这里把这段时间的经历整理出来,供同学们参考。
先做应用层调优:低成本挖出并发空间
我们的调优对象是Spec Coding代码助手场景。项目最初,环境大致能支撑10-15个用户并发。
项目组从应用侧入手:先梳理提示词和工具调用流程,优化系统提示词,调整工具调用顺序,尽量让可以复用的前缀保持稳定。
经过几轮整改,效果立竿见影:
**●**能够支撑的并发用户从10-15个提升到20-25个
**●**缓存命中率从个位数提高到90%以上
这一步给了我们一点启发:同样的业务意图,提示词、工具定义和代码上下文的组织方式不同,缓存复用效果也会有明显差别,后面需要重复计算的内容也随之变化。这些地方值得细抠。应用侧调整后,20-25个用户的使用基本稳定下来,我们才接着往底层看,还有多少空间可以挖。
再往上增加并发,KV Cache的空间就成了一个制约。Spec Coding请求往往带着较长的代码上下文,多轮执行下来,上下文还会继续增长。在当时的负载和配置下,显存中能够保留的KV有限,继续增加并发就比较吃力了。
一个思路随之产生:能不能把一部分KV缓存卸载到主机内存里,留待后续请求复用,为显存腾出一些空间?于是我们开始了PPU平台上KV Cache卸载的探索。
KV缓存卸载:GPU成熟方案,在PPU上需要重新验证
KV Cache内存卸载在NVIDIA GPU上已有较多实践,资料和工具相对好找。到了PPU ZW810E,能直接参考的社区经验就少了很多。
查资料时,我们了解到SmartX、GPUStack在PPU上开展适配的公开介绍,心里多了一些底,也给了项目组继续尝试的信心。不过,具体到我们使用的模型、框架版本和缓存方案,能不能走通,还得动手实测验证。
我们先尝试了vLLM的CPU KV卸载:模型可以正常加载,普通请求也能正常返回,但请求触发KV搬运时,底层报错:
hgapiMemcpyBatchAsync:functionnotimplemented
随后KV搬运失败,相关进程也退出了。
踩坑提示:模型能正常服务,缓存卸载也未必能用。
缓存搬运、请求抢占、跨进程缓存共享这类冷路径,不会在普通推理中触发,必须单独做专项测试。
排查故障时,要追溯最早的异常日志。底层出错后,通信、Worker和引擎可能接连退出,不要只盯着最后报错,否则很容易被连锁报错误导,走很多弯路。
落地LMCache:环境依赖是第一道坎
尝试过vLLM原生卸载和FlexKV后,我们把后续调试的重点转到了LMCache。
安装时,得先留意已经适配好的PPU环境。PyTorch、vLLM和运行时之间有依赖关系,新增组件可能连带更新原有依赖,所以要检查安装前后的版本变化。否则,缓存还没开始测,原来能跑的推理环境可能先出了问题。
编译LMCache时,我们还在LMCACHE_CUDA_MAJOR环境变量的设置上踩了两次坑,才把它调到适合PPU环境的配置。
LMCache启动后,还得确认缓存有没有真正用起来。配置了主机内存,不代表已经存入有效KV;日志里有注册信息,后续搬运也未必正常。我们便顺着缓存写入、查询、读取,再到主机向设备传输的过程,逐项检查。
当时考虑的是:代码助手和Agent会反复带上系统提示、工具定义和部分代码上下文。如果能把这些相同前缀的KV留下来,后续请求就有机会少做一些Prefill计算。不过,留下来的内容还会不会用到,要看实际请求,并不是缓存配置越大,命中率就越高。
三层缓存,各解决什么问题
后续我们逐步搭建起三级分层缓存架构:
**●**L0在PPU显存中,保存运行所需的KV,也供后续请求复用相同前缀;
**●**L1使用主机内存,让更多历史KV能够留下来;
**●**L2使用本地文件系统,部分KV从内存中淘汰后,还可以尝试从磁盘取回。
本次配置:L1容量上限设为300 GiB,L2容量上限设为2048 GB。运行后能看到内存池完成预分配,磁盘占用也随着使用逐渐增加。
不过,调试时也遇到过内存池已经分配,L1 usage却仍为零的情况。这时候需要注意,分配了内存,还得看里面有没有存入有效KV。磁盘目录里有了文件,也要继续确认后续请求是否真正读取过。
⚠️重点提醒:内存/磁盘完成分配 ≠ 存入有效KV。
中间因为其他事情,适配调优的事暂停一周。国庆假期前后时间宽裕些,又继续推进。10月4日,日志里已经能看到L1的实际使用和L2读取记录。观测区间内,L1使用峰值约为245 GiB,累计有946次请求从L2磁盘预取数据,分层缓存链路正式跑通。
看到这些记录,我们心里踏实了不少,分层缓存确实用起来了。至于它带来了多少吞吐提升、减少了多少延迟,还得继续做对照测试进一步评估。其中查询、预取和传输也要花时间,这些开销都得算进去。
70并发压测:得到阶段性实测结果
经过几轮调整,随后做了多轮70并发测试。测试覆盖了多种配方:
**●**关闭投机解码
**●**使用其他推测解码替代
**●**开启投机解码
**●**关闭异步调度等
上述组合逐一进行验证。基于vLLM+LMCache内存+磁盘分层缓存,单机8卡环境,70并发压测,132分钟完成9800条请求,成功率100%,无失败、无超时。主要指标如下:

本轮压测完成后,可以进一步投入到真实业务场景开展检验。需要注意的是,实际业务中请求的输入输出长度、模型思考过程以及任务类型各不相同,都会对最终完成时间产生影响。
显存还有余量,性能也可能下降
调试过程中我们遇到过一个值得深思的现象:即便KV Cache占用不高,系统吞吐却已经降下来了。明明显存还有余量,推理为什么仍然变慢?
这里要厘清一个关键点:缓存容量和推理速度得分开看。KV缓存空间只能决定能保留多少上下文内容,而请求处理得快不快,还要看模型计算、调度、缓存查询和数据传输。只看缓存占用,很难判断慢在哪里。
此前几轮70并发测试中,有一轮出现过持续数小时的性能退化,后续又自行逐步恢复。设定并发没有变,前后表现却相差不少,单靠并发指标完全无法解释该现象。
当时正处于集中代码编写阶段,请求内容和前缀复用情况可能都在动态变化。日志里能看到缓存命中、查询延迟和吞吐一起波动,但它们之间是什么关系,还需要进一步深挖定位。
排查这类问题,需要沿着单条请求完整链路拆解分析:输入多长,命中多少,需要重算多少,取缓存花了多久,生成又用了多久。把这些记录与请求日志一一对应上,才更容易找到该调整的地方。
从模拟压测走向GienCoder业务验证
后续又做了两段验证,一段用模拟压测,另一段用源启GienCoder智能软件工厂中的编码智能体GienCoder Agent,两段之间后端没有重启。
**●**模拟压测:平均首token时间为2.94秒,平均TPOT为86.21毫秒;
**●**GienCoder Agent:平均首token时间为2.84秒,平均TPOT为87.10毫秒。
两者平均表现比较接近,但慢请求的耗时有所增加,端到端P99从约310秒增加到了约391秒。
Agent在执行任务时会调用工具,也会带着增长的上下文再次请求模型。一个用户任务,背后可能有多次模型调用,中间还夹着工具执行和等待。因此,即使并发数相同,模型实际受到的负载也未必一样,分析性能时必须要结合调用记录来看。
两段运行都观察到了L2回补。整个观测窗口内,来自L2的保留key占比约为9%,部分时段占比更高。有些预取记录中,大部分key都来自L2,说明磁盘缓存确实为部分请求补齐了所需的KV。
到第二段测试结束,后端进程已经连续运行62小时,但有效业务测试为5小时27分钟,负载动态变化,不能等同于62小时满载稳定性验证。
持续运行,和跑分同样费功夫
早期调试中,我们还遇到过LMCache停止响应、进程崩溃,以及文件描述符和HGGC共享对象随运行不断增加的情况。
为了弄清这些问题,我们增加了内存、线程、文件描述符、容器资源和设备状态的持续采集,再结合调用栈,查看进程具体等在哪里。
资源增加以后,还要跟踪它什么时候回收、是否正常回收。单纯提高文件描述符上限,只能临时缓解上限不足的问题,但资源回收环节是否存在异常,仍需要进一步排查定位。
还有些问题,得通过混沌测试来确认:当缓存服务失去响应后,推理服务会表现出怎样的行为?待缓存服务恢复后,推理服务能否接着工作,还是需要一起重启?这些场景都关系到生产环境的可用性,我们把它们放到了AI混沌测试的验证范畴。
回顾整个调优过程,从提示词、工具调用流程的调整优化,到KV卸载和分层缓存落地,项目组一步步迭代验证,单机8卡环境也完成了多轮70-100并发压力测试。
拿到阶段性结果,也积累了一些实践经验,我们心里多了一些底,但仍有一些问题待深挖。后续我们将开展多芯片对比测试、长周期稳定性以及故障恢复能力验证,有新的实践进展,再和各位同学们分享交流。