Kimi K3 模型结构(10):RL 与服务中的状态管理
系统篇收尾。1M 上下文的 RL 和线上服务里,瓶颈从算力变成「状态放哪」:KV 块和 KDA 状态在 GPU 与 DRAM 池之间的 write-back,训练状态腾到 NVMe,参考模型权重借用梯度缓冲流进 GPU,沙箱的 pause / fork / snapshot,以及集群级的缓存亲和与预算准入。
目录8 节
对应论文 §5.3 "Infra for 1M Agentic RL" 和 §5.4.3 "Fleet-Level Scheduling"。前两篇的时间尺度是一步训练,这一篇拉长到 RL 的一轮迭代和线上服务的一次会话。时间一拉长,问题就变了:不再是算力够不够,而是状态放在哪里、什么时候搬。这一篇沿着状态的生命周期走一遍,§5.4.1 的前缀缓存已经在第 7 篇里讲过,这里只放链接。
1M 上下文 RL 的资源约束
先看约束。K3 这么大的模型做 1M 上下文的 agentic RL,论文说资源效率是第一目标。两个前置选择继承自 K1.5 和 K2:co-located RL,训练和推理用同一批卡,让一个 1M 上下文的 RL 实验能装进几百张 GPU;partial rollout,超长轨迹分段生成,压掉尾延迟。
代价随之而来。rollout 阶段推理引擎要为长上下文长时间保留 KV cache,它和训练侧的权重、优化器状态、梯度缓冲抢的是同一批显存和 DRAM。而且 prefill 和 decode 都要高效的话,前缀管理和请求调度都得为 1M 专门设计。§5.3.1 的三个技巧对应三种状态:KV cache、训练态、参考模型权重。先看它们各自放在哪:
外部 KV 池:write-back,不是 write-through
1M 上下文的多步 rollout 里,一次前缀 KV miss 极贵:整个前缀要重新 prefill。三件事在放大 miss 的频率。partial rollout 让每轮迭代开头有一堆上一轮没跑完的长 prefill 请求同时到达;投机解码在固定的工具调用间隔里加快了请求周转,前缀块被换出换入更频繁;两者叠加会触发抢占,把命中率压下去,而命中率对长上下文 RL 是关键指标。
K3 的思路是:前缀要留着,但不一定留在 GPU 上。活跃解码的块留在 GPU 的 KV cache 里;闲置但可复用的前缀,只在被 GPU 驱逐时才写回 CPU DRAM 里的外部 KV 池,下次复用之前再预取回来。KDA 的状态和它对应的 MLA KV 块一起 offload、一起预取,生命周期对齐,这和第 7 篇里「两种页共用一套分配和驱逐逻辑」是同一个设计的延续。
write-back 和 write-through 的取舍是通用的缓存问题,见推理服务的缓存与调度。放在 K3 的场景里,结论是:只有离开活跃解码的前缀才占用 DRAM 空间和传输带宽,GPU 上仍然活跃的块不在 CPU 上多存一份。
DRAM 从哪来?和训练态抢。K3 的做法是时间上错开:训练迭代结束后把权重和优化器状态下到 NVMe,把 DRAM 让给 KV 池;rollout 迭代结束后释放 KV 池,让给训练。这是存储层次图里那条到 NVMe 的箭头。
rollout 自动节流
第二个问题是并发数。多步 rollout 里上下文随轨迹推进逐渐变长,按整条轨迹的平均长度设一个固定并发,早期估不准而且过于保守,卡空着;并发设高了,后期 KV 压力上来触发抢占。两头都不对,说明这个数不该是常数。
K3 在 LLM 请求调度层加了一个自动节流:用运行时信号,活跃请求数、排队请求数、KV cache 利用率,动态控制送进推理引擎的请求数。rollout 早期上下文短,并发自然高,卡能吃满;随着 KV 压力上升,并发自动降下来。卡不会闲着,也不会被挤爆,不需要手调。
参考模型:借用梯度缓冲
第三种状态是只做前向的非策略模型,比如参考模型。RL 的 loss 要用它,但它的权重太大,常驻 GPU 放不下。
K3 把它的权重放在 CPU 内存里,需要时才搬到 GPU 上,搬到的位置是策略模型的 FP32 梯度缓冲。这块显存本来就在,不用额外分配也不会产生碎片;安全性在于之后真正的梯度算出来时会覆盖它,只要参考模型的前向在那之前做完就行。
具体怎么流?回到第 9 篇的 Pipeline ZeRO-2:梯度分片放 CPU 之后,每张卡在 RL 训练里只保留两个 VPP chunk 的梯度缓冲。参考模型的权重就按 chunk 流进这两个槽:一个槽做当前 chunk 的前向,另一个槽预取下一个 chunk,拷贝的时间藏在计算后面,GPU 显存一点不涨。
沙箱:从容器到 microVM
agentic RL 的另一半状态不在模型里,在环境里。K3 用了多种沙箱运行时,容器的、GPU 的,以及一个新的基于 microVM 的运行时 AgentENV,它是这一节的主角,已经开源。三个设计目标,每个都对应一个具体的痛点。
更强的隔离和更真实的环境。 agent 越强、任务越难,探索就越激进,甚至会试图 reward hacking。早期用传统容器沙箱时,作者观察到过 agent 的意外操作直接造成内核 panic 和死锁。但反过来又不能限制探索,复杂任务需要接近真实的环境,agent 应该可以随意挂盘、跑容器、甚至开虚拟机。AgentENV 用 Firecracker 跑隔离的 microVM,既比容器隔离得彻底,又比容器更像一台真机。
灵活的生命周期。 三个高层操作各对应一个 RL 需求。Pause / Resume:部分 rollout 调度把一条轨迹搁置时,暂停它的环境,暂停的沙箱不占内存和 CPU。Fork:从原沙箱的精确状态复制出一个新沙箱,原来的继续跑,判分在副本里做,不会在原轨迹上留下副作用。Snapshot:定期快照,用于错误恢复。仓库里没有 RL 代码,它暴露 E2B 兼容的 HTTP API,由外部的 RL 循环驱动。
高效率、高密度。 数万个沙箱、每个带一套不同的镜像,要在几秒内起来。整个 K3 的训练和评测一共创建了 51,219,741 个沙箱、1,505,678 个镜像。
133 ms 和 49 ms 是怎么来的
先看一个数字:agent 等待模型推理结果的时间可以占一个沙箱生命周期的 98%。所以 checkpoint 和 resume 的速度直接决定整个沙箱池的成本,论文给的是 133 ms 和 49 ms,靠的是下面三个机制。
磁盘快照:封存再改名。 沙箱磁盘是 OverlayBD 分层镜像,只有一层可写的 upper layer。快照就是把这一层封存、改名,再在上面叠一层空的 upper。改名是常数时间;封存要先把还没写到磁盘的内容刷下去、扫一遍映射、追加一个紧凑索引,这部分随写过的映射数增长。
内存快照:不写中间文件。 先做一个只含 CPU 和设备状态、不含内存的快照。然后问 Firecracker 自上次 checkpoint 以来哪些页被写过,这个 /vm/dirty-memory-ranges 接口上游没有,是打了补丁的。再用 Linux 的 process_vm_readv 直接从运行中的 VM 进程读那些页,打包成一个 OverlayBD 层。没改过的页一个字节都不拷,中间也没有 mem.bin。仍保留了一条先写 mem.bin 再转换的后备路径。
共享内存设备:同一模板 fork 出的沙箱共用一份。 内存快照变成一个只读的 ublk 块设备,Firecracker 以写时复制的方式映射它。设备管理器按镜像做引用计数,N 个从同一个模板 fork 出来的沙箱共用一个设备和一份 page cache。加上 guest 侧的 balloon 和 DAMON 回收,真实负载下内存超售比达到 6.5 倍。
fork 用的是同一套零件:源沙箱暂停再恢复,N 个子沙箱从它的快照并发启动,各自独立成功或失败。判分不影响原轨迹,就是靠这个。
集群级调度:预算与亲和
最后把视角从一个服务实例拉到整个集群池。§5.4.3 说,到了这一层挑战从「每个请求的效率」变成「可预测性」:一次前缀缓存 miss 比 hit 贵几个数量级,一阵 1M 请求的突发可以把短请求饿死。两条策略对应这两个问题,它们背后的一般原理(缓存亲和、一致性哈希、按类别做预算)在推理服务的缓存与调度里,这里只写 K3 的数字和取舍。
缓存亲和调度。 1M 上下文下一个典型的编码请求带着 400K 的前缀,但只需要 prefill 4K 的增量。命中时只算 4K,miss 时算 400K,两个数量级。所以请求跟着缓存走,路由到持有这个会话前缀缓存的集群。K3 用一致性哈希给每个会话钉两个集群:主集群服务流量,备集群在主集群故障时接管并重新 prefill,备份均匀散在整个集群池上,单个集群故障的重 prefill 摊给很多集群。平时能保住缓存命中,出了故障影响也有限。
这和前面的双缓冲思路相近:两个位置,一个在用,一个待命。只不过前两次是一张卡上的两块缓冲,这次是两个集群。
预算准入。 K3 的生产流量里短请求不到 2K,长请求到 1M,单个请求的代价跨了三个数量级。请求大小差这么多,按「平均请求」规划容量就不管用了,典型故障是一阵长上下文请求吃满算力、后到的短请求首 token 延迟一起恶化。K3 给不同的请求类别分配各自的资源预算,长上下文的突发最多消耗自己那一份,不会拖垮其他类别的 SLO。
三篇系统篇的收尾
回头看这三篇各自的一张图:第 8 篇的时间线,标的是谁在等谁;第 9 篇的账本,标的是每一行放在哪、搬走的代价是什么;这一篇的存储层次,标的是状态什么时候往下搬、什么时候搬回来。三张图说的是同一件事的三个时间尺度:一次前向、一步训练、一轮迭代或一次会话。
再回头看第 7 篇的结论:系统那边的工作,都是结构选择带来的代价。这三篇把代价一项项列了出来:MoonEP 为 896 个 expert 的路由预留了 个冗余槽位;显存方面,为 2.8T 参数用上了 FP8、重算、offload 和两个 chunk 的双缓冲;KV 池为 KDA 加 MLA 的混合缓存设计了一套对齐的生命周期。处理这些代价的办法反复是两种:找一段空闲时间,把开销藏进去;准备两个缓冲区,轮流使用。
反过来,系统上省下的资源也用回了训练上。KDA 的固定状态让 1M 上下文的 RL 实验能装进几百张卡,几乎零开销的沙箱暂停让长时间的推理等待不再占资源。论文里 5100 万个沙箱上的 agentic RL,正是靠这些省下来的算力跑起来的。
到这里 Kimi K3 的连载全部结束。本连载没有覆盖的部分:数据与预训练配方(§3)、post-training 的 SFT、RL 算法、多教师蒸馏(§4)、评测(§6)。
评论