给 Qwen3.8-Flash-Next 配内存 KV 缓存

上一篇讲了怎么用 4 张 V100 跑 Qwen3.8-Flash-Next。这台机器主要给我的 Agent 用,会话动不动就是几十万 token。显存里能放约 110 万 token 的 KV 缓存,可会话一多,或者某个会话闲置一阵,它的缓存就会被挤出显存,下次再问只能从头重算。几十万 token 从头算,要好几分钟。

所以又加了一层主机内存缓存:显存放不下的 KV 先存到内存里,用到时再搬回显存。这篇记一下怎么设置,以及踩过的坑。

怎么设置

引擎还是 1Cat-vLLM,用它自带的 OffloadingConnector:

--enable-prefix-caching --mamba-cache-mode align --prefix-cache-retention-interval 0 \
--kv-transfer-config '{"kv_connector": "OffloadingConnector", "kv_role": "kv_both",
  "kv_connector_extra_config": {
    "spec_name": "TieringOffloadingSpec",
    "cpu_bytes_to_use": 68719476736,
    "eviction_policy": "lru",
    "demote_superseded_states": true,
    "mamba_state_slots_reference_tokens": 65536}}'

其中三个参数最关键:

  • cpu_bytes_to_use 决定给缓存多少内存,我给了 64GB。容器总共 160GiB 内存,跑起来以后还剩 15GB 左右。别给得太满,系统和多模态预处理也要用内存。
  • demote_superseded_states 让已经用完的旧状态优先被淘汰。不开的话,闲置的长会话很容易丢缓存(#738)。
  • mamba_state_slots_reference_tokens 决定内存怎么分配,最需要按场景调,下面细说。

为什么要分两种槽

Flash-Next 是混合模型:一部分层是普通注意力,一部分是线性注意力(GDN)。普通注意力的缓存逐个 token 存;线性注意力存的是一份“状态”,而且只在请求结尾处才有。

要从内存里恢复一个会话,两样都得有。只有 token 缓存、没有对应位置的状态,前面存的东西就全用不上,还是得从头算。所以内存被分成了两块:token 槽存普通注意力的 KV,很便宜;状态槽存线性注意力的状态,一个就顶好几个 token 槽,数量少得多。

粗略来说,每个能被恢复的会话大约要占 2 个状态槽。参考长度设得越小,状态槽越多,token 槽越少:

参考长度token 容量每组状态槽大约能保住的会话数
524288378 万168
262144368 万3015
131072351 万5427
65536321 万9648
32768275 万16281

(以上是 64GB 内存、4 张卡、7 个线性注意力层组时的数字。)

怎么选

看你的会话是“少而长”,还是“多而短”。如果只有几个长会话,比如就几个人在聊天,262144 就够了,token 容量最大。如果 Agent 会开很多子任务,就选 65536 甚至更小:token 容量只少了十几个百分点,能保住的会话却多了好几倍。

我一开始用的是 262144。后来 Agent 改成每读几页就开一个新的子会话,半小时能冒出几百个短会话,30 个状态槽很快就被挤满了。结果主会话只闲置了半小时,状态就被挤掉了,28 万 token 只能从头算。改成 65536 以后,同样的负载下,从内存命中的比例从 1%–3% 提高到约 6.5%,需要重算的比例从 11%–12% 降到约 8%。

用起来怎么样

一个 50 万 token 的会话闲置后回来,从内存恢复后首字只要 5.7 秒;没有这层缓存时要 265 秒。实际使用中,有个 30 万 token 的会话闲置了将近 4 小时,回来时 99% 都是从内存恢复的。连续跑 27 小时,92.5% 的输入 token 都命中了缓存:显存命中 86.5%,内存命中 6%。

踩过的坑

第一,光开内存缓存不够,客户端超时也要放宽。状态一旦丢了,几十万 token 的冷启动要好几分钟,期间一个字都不吐。很多客户端 5 分钟收不到数据就断开重试,而重试又是从头算,越试越慢。我给 pi-web 打了个补丁,把超时放宽到 30 分钟。

第二,Agent 的子任务别切得太碎。每个子会话都要占状态槽。把每轮的工作量从 2 页调到 4 页以后,会话数少了一半,每页反而还更快了。

第三,看监控,别凭感觉。vLLM 会分别报告显存命中(prefix_cache_hits)和内存命中(external_prefix_cache_hits)。如果内存命中一直接近 0,重算比例又偏高,多半是状态槽不够了。