Kimi K3 模型结构(10):RL 与服务,状态住在哪里
系统篇收尾。1M 上下文的 RL 和线上服务里,瓶颈从算力变成「状态放哪」:KV 块和 KDA 状态在 GPU 与 DRAM 池之间的 write-back,训练态让位到 NVMe,参考模型权重借梯度缓冲的槽位流进 GPU,沙箱的 pause / fork / snapshot,以及集群级的缓存亲和与预算准入。三篇里第三次出现双槽位模式。
目录7 节
对应论文 §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,隔离度和保真度都是容器给不了的。
灵活的生命周期。 底层支持增量 checkpoint 和恢复,只保存上次 checkpoint 之后被写脏的内存页,checkpoint 和 resume 的延迟分别低到 133 ms 和 49 ms。在此之上有三个高层操作。Pause / Resume:暂停的沙箱不占内存和 CPU,而 agent 等待模型推理结果的时间可以占沙箱生命周期的 98%,所以等待时直接暂停。Fork:从原沙箱的精确状态复制出一个新沙箱,原来的继续跑,用于无副作用的判分。Snapshot:定期快照,用于错误恢复。
高效率、高密度。 数万个沙箱、每个带一套不同的镜像,要在几秒内起来。镜像格式用 OverlayBD,配自研的 ublk 驱动、存储层共享和 P2P 传输,大规模下做到亚秒级启动;写时复制的内存和 page cache 优化让真实负载下的内存超售比达到 6.5 倍。整个 K3 的训练和评测一共创建了 51,219,741 个沙箱、1,505,678 个镜像。
集群级调度:预算与亲和
最后把视角从一个服务实例拉到整个集群池。§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 的 double buffer;KV 池为 KDA 加 MLA 的混合缓存付了一套对齐的生命周期。付账的方式反复是同一个:找到一段空闲,把代价藏进去;找到两个槽位,让它们轮转。
到这里 Kimi K3 的连载全部结束。本连载没有覆盖的部分:数据与预训练配方(§3)、post-training 的 SFT、RL 算法、多教师蒸馏(§4)、评测(§6)。