@_avichawla: 代理强化学习(Agentic RL)的一个有趣新方法:研究人员找到了让 AI 代理训练速度提升近 2 倍的方式。代理强化学习通常……

X AI KOLs Timeline 论文

摘要

研究人员提出了 KV-streams,一种通过直接修剪运行中的 KV 缓存而非重新预填充来压缩代理 RL rollout 的方法,在 SWE-bench Verified 上几乎将 RL 训练速度提升一倍,同时使训练与生成的 token 分布几乎完全一致。

代理强化学习(Agentic RL)的一个有趣新方法: 研究人员找到了让 AI 代理训练速度提升近 2 倍的方式。 代理 RL 通常会产生很长的 rollout,当 rollout 达到上下文预算时,压缩策略会移除较早的轮次,只保留提示词、最近的轮次或摘要。 大多数系统采用重新预填充(re-prefill)压缩方式。 它们生成一个更短的序列,并让每个保留的 token 重新经过一遍模型。这意味着每次压缩后都要重建 KV 缓存,同样的 token 可能会被重复处理多次。 KV-streams 就是为了解决这个问题而提出的新方法。 KV-streams 不重建较短的序列,而是直接从运行中的 KV 缓存中移除被丢弃的轮次,生成过程则从存活下来的条目继续。 但在实践中,不能直接照搬复用这些条目,需要做两处修正。 > 第一,RoPE 已经根据每个缓存 Key 的原始 token 位置对其进行了旋转。压缩后,某个 Key 可能从物理槽位 1,000 移到了槽位 500,但模型仍必须将其视为位置 1,000。 KV-streams 将物理存储与逻辑位置分离开来。保留的 Key 保留其旋转状态,新的 Query 和 Key 则从原始位置计数继续递增。 > 第二,vLLM 并不会独立存储每个 KV 条目,而是将它们分组成固定的 16-token 块,并通过块表(block table)在 GPU 内存中定位这些块。 KV-streams 不能在块表中留下空槽位,否则注意力内核可能读取到错误的内存。因此它会移除完整的块,并将剩余的块引用拼接在一起。 这又引出了另一个问题。 一个对话轮次可能在 16-token 块的中间结束。在他们的 vLLM 配置下,这个不完整的块不会提交到前缀缓存,当下一轮到来时必须重新计算。 如果压缩先于这一步发生,这些 token 原本所关注的上下文可能已经不存在了。 针对 vLLM 实验,研究人员将每个补全结果补齐到 16 个 token 的整数倍。这样在下一个轮次或压缩事件发生前,每个生成的 token 都会提交到缓存。SGLang 配置使用单 token 块,无需补齐。 此外,研究人员发现,token 级别的逐条驱逐会产生退化文本,并需要大量监督微调。 他们怀疑移除消息边界 token 破坏了训练中学到的聊天格式。因此,代理实验移除的是完整的轮次,包括其边界标签。 推理既然按这种方式运行,训练也必须遵循同样的流程。 普通训练器要么处理完整的 rollout,要么在压缩后重建较短的序列。这两种做法产生的 KV 条目都与生成时使用的不同。 KV-streams 会记录生成过程中的每次压缩事件,训练器随后复现这段历史:保留的 token 保持压缩前形成的状态,而后续 token 则被阻止关注推理阶段已经移除的片段。 在代码仓库的验证运行中,训练与生成之间的差异 KL 保持在 0.001 左右,说明两条路径产生的 token 概率几乎完全一致。 在 SWE-bench Verified 上,使用 KV-streams 的 Markovian Thinker 配置约 32 小时就达到了峰值性能。 采用相同压缩策略但使用重新预填充的方式则大约需要 60 小时,而两者最终得分相当。 这种加速主要体现在 RL 训练上,因为一次 rollout 可能会被压缩多次。 在生产推理场景中,批处理可以掩盖部分预填充成本,而且为不活跃用户保留缓存会降低服务容量。 还有一个细节。 保留下来的 KV 条目是在被移除的轮次仍然可见时创建的。后续 token 无法直接关注那些轮次,但其中一些影响可能仍残留在未被驱逐的条目中。 在一项受控测试中,RL 在原始消息被移除后恢复出了相应的任务分配内容。当缓存保留足够多时,召回率达到了 100%。 KV-streams 并不决定模型应该遗忘什么,而是通过保留压缩策略选择保留的 KV 状态,让现有的压缩策略以更低的成本进行训练。 仓库地址:https://github.com/Emilianopp/KV-streams 如果你想更深入地了解更广泛的 KV 缓存工程体系,我写过一篇文章,涵盖了驱逐(eviction)、分页(paging)、量化(quantization)、卸载(offloading)以及架构级压缩等内容。 请在下方阅读。
查看原文
查看缓存全文

缓存时间: 2026/10/03 06:53

一种有趣的智能体 RL 新方法:

研究人员找到了一种让 AI 智能体训练速度提升近 2 倍的方式。

Agentic RL(智能体强化学习)通常会产生很长的 rollout,当 rollout 超出上下文预算时,压缩策略(compaction policy)会移除较早的轮次,保留 prompt、最近的轮次或摘要。

大多数系统使用 re-prefill(重预填充)压缩。

它们先构造一个更短的序列,然后让所有保留的 token 再次经过模型。这会在每次压缩后重建 KV cache,因此相同的 token 可能被反复处理。

KV-streams 是解决这一问题的新方法。

它不重建较短的序列,而是直接从活跃的 KV cache 中删除被丢弃的轮次。生成操作从存活下来的条目处继续进行。

在实践中,这些条目并不能原样复用,需要两处修正。

首先,RoPE 已经使用原始 token 位置旋转了每个缓存的 Key。压缩后某个 Key 可能从物理槽位 1,000 移到槽位 500,但模型仍必须将其视为位置 1,000。

KV-streams 将物理存储与逻辑位置分离。保留下来的 Key 保持原有的旋转,新的 Query 和 Key 则从原始位置计数继续。

其次,vLLM 并不独立存储每条 KV 条目,而是将其组织成固定的 16-token 块,并用块表(block table)在 GPU 内存中定位这些块。

KV-streams 不能在该表中留下空槽位,否则注意力内核可能读取到错误的内存。因此它按完整块进行删除,并将剩余的块引用拼接在一起。

这就引出了另一个问题。

对话轮次可能在一个 16-token 块的中途结束。在他们的 vLLM 设置中,这个不完整的块不会被提交到前缀缓存(prefix cache),下一轮到来时必须重新计算。

如果压缩先发生,这些 token 最初所注意到的上下文可能已经不存在了。

对于 vLLM 实验,研究人员将每个补全填充到 16 token 的整数倍。这样在下一轮或压缩事件发生之前,每个生成的 token 都已提交到缓存中。SGLang 设置使用单 token 块,无需填充。

此外,研究人员还发现,token 级别的淘汰会产生退化的文本,并需要大量的监督微调。

他们怀疑,删除消息边界 token 破坏了训练中学习到的聊天格式。因此智能体实验删除的是完整轮次,包括其边界标签。

一旦推理按这种方式工作,训练也必须遵循同样的流程。

常规训练器要么处理完整的 rollout,要么在压缩后重建较短的序列。这两种方式产生的 KV 条目都与生成阶段使用的不同。

KV-streams 会记录生成过程中发生的每一次压缩事件。训练器随后复现这段历史:保留的 token 维持压缩前形成的状态,而后续 token 则被阻止注意到推理阶段已经移除的片段。

在仓库的验证运行中,训练与生成之间的差异始终保持在约 0.001 的失配 KL(mismatch KL),表明两条路径产生了几乎完全相同的 token 概率。

在 SWE-bench Verified 上,使用 KV-streams 的 Markovian Thinker 设置在约 32 小时内达到了峰值性能。

采用相同压缩策略但使用 re-prefill 的方法大约需要 60 小时,而两者的最终得分相当。

这种加速主要适用于 RL 训练,因为一次 rollout 可能被多次压缩。

在生产推理阶段,批处理可以掩盖部分预填充成本,而为不活跃用户保留缓存则会降低服务容量。

还有一个细节值得注意。

被保留的 KV 条目是在被删除的轮次仍然可见时创建的。后续 token 无法直接注意到这些轮次,但它们的部分影响可能仍残留在未被淘汰的条目中。

在一项受控测试中,RL 智能体在原始消息被删除后仍恢复了一个指派。当保留的缓存足够多时,召回率达到了 100%。

KV-streams 并不决定模型应该忘记什么,而是通过保留压缩策略所选择的 KV 状态,让现有的压缩策略训练成本更低。

仓库:https://github.com/Emilianopp/KV-streams…

如果你想深入了解更广泛的 KV-cache 工程技术栈,我写了一篇文章,涵盖了淘汰(eviction)、分页(paging)、量化(quantization)、卸载(offloading)以及架构级压缩等内容。

请在下方阅读。


Emilianopp/KV-streams

来源:https://github.com/Emilianopp/KV-streams

KV-streams

面向 RL 训练的原生 vLLM KV cache 压缩。这是 vLLM 0.19 的一个 fork,集成了调度器级的块级淘汰(block-level eviction)。

当某个请求的 KV 长度超过 --compaction-window-size 时,调度器会淘汰最旧的 prompt 之后的块(每次淘汰 --compaction-stride)。该请求的物理长度变短——注意力计算更快,无需重新计算 KV。

环境搭建

前置要求

  • 配备 A100(或更新型号)GPU 的 CUDA 12.8 环境
  • Python 3.12
  • uv(https://docs.astral.astral.io/uv/)包管理器
  • 建议但不强制使用容器运行时。一个合适的基础镜像是 docker.io/novaskyai/skyrl-train-ray-2.51.1-py3.12-cu12.8,其中捆绑了 CUDA 12.8、Python 3.12 和 uv。

1. 克隆仓库

bash git clone --recursive https://github.com/Emilianopp/KV-streams.git cd KV-streams

vllm/(kv-streams-vllm (https://github.com/Emilianopp/kv-streams-vllm))和 prime-rl/(kv-streams-prime-rl (https://github.com/Emilianopp/kv-streams-prime-rl))是子模块。如果你克隆时没有加 --recursive:

bash git submodule update --init

2. 安装

``bash

一键安装(创建虚拟环境、以可编辑模式安装 vLLM + 全部依赖)

bash setup.sh ``

或者手动安装(适用于开发场景):

``bash uv venv .venv –python python3.12 –clear source .venv/bin/activate

以可编辑模式安装启用了压缩功能的 vLLM fork。VLLM_USE_PRECOMPILED=1

会下载 vLLM CI 构建的 .so 产物并将其符号链接到源码树中,

这样对 vllm/vllm/v1/core/compaction/ 下 Python 代码的修改

无需重新完整编译 C++/CUDA 即可生效。

VLLM_USE_PRECOMPILED=1 uv pip install -e ./vllm –no-build-isolation

kv-eviction 集成层

uv pip install -e . ``

3. 验证安装

bash source .venv/bin/activate python -c " from vllm.v1.core.compaction import CompactingKVCacheManager, CompactionEvent print('Compaction imports OK') import vllm; print(f'vLLM {vllm.__version__}') "

推理

快速开始——启用压缩的 vLLM server

``bash source .venv/bin/activate

启动启用了压缩功能的 vLLM。

window=4096:KV 超过 4096 个 token 时触发淘汰

stride=512:每次压缩事件淘汰 512 个 token(32 个块)

block_size 默认为 16,stride 必须是它的整数倍

async_scheduling=False 是必需的(见下方“约束条件“)

python -m vllm.entrypoints.openai.api_server
–model Qwen/Qwen3-4B
–max-model-len 16384
–enable-prefix-caching false
–async-scheduling false
–compaction-window-size 4096
–compaction-stride 512
–tensor-parallel-size 1
–port 8000 ``

查询服务

``bash

标准的 OpenAI 兼容 API——压缩过程完全透明

curl -s http://localhost:8000/v1/chat/completions
-H “Content-Type: application/json”
-d ‘{ “model”: “Qwen/Qwen3-4B”, “messages”: [{“role”: “user”, “content”: “请分步求解:1234 * 5678 等于多少?”}], “max_tokens”: 8192, “temperature”: 0.7 }’ | python -m json.tool ``

Python 客户端

``python from openai import OpenAI

client = OpenAI(base_url=“http://localhost:8000/v1”, api_key=“unused”)

response = client.chat.completions.create( model=“Qwen/Qwen3-4B”, messages=[{“role”: “user”, “content”: “写一篇关于 AI 安全性的长文。”}], max_tokens=8192, temperature=0.7, ) print(response.choices[0].message.content) ``

离线推理(无需 server)

``python from vllm import LLM, SamplingParams

llm = LLM( model=“Qwen/Qwen3-4B”, max_model_len=16384, enable_prefix_caching=False, async_scheduling=False, # 使用压缩时必需(见“约束条件“) # 压缩相关参数 compaction_window_size=4096, compaction_stride=512, )

outputs = llm.generate( [“请详细介绍量子计算。”], SamplingParams(max_tokens=8192, temperature=0.7), ) print(outputs[0].outputs[0].text) ``

工作原理

`` 请求正在生成 token… [prompt: 200 个 token] [gen: 3800 个 token] KV 总量 = 4000(低于窗口)

生成下一个 token 后: [prompt: 200 个 token] [gen: 3801 个 token] KV 总量 = 4001(仍然低于窗口)

…又过了 96 个 token: [prompt: 200 个 token] [gen: 3897 个 token] KV 总量 = 4097(超过窗口!)

压缩触发:

  1. 从 KV cache 中淘汰最旧的 512 个生成 token(32 个块)
  2. 相应地裁剪 token ID
  3. 将 num_computed_tokens 减少 512
  4. 添加 position_offset += 512 以确保 RoPE 正确
  5. 标记该请求,让模型执行器重建

压缩之后: [prompt: 200 个 token] [gen: 3385 个 token] KV 总量 = 3585(回到窗口以内) 物理 seq_len = 3585(注意力计算更快!) RoPE 位置 = 物理位置 + 512(绝对位置仍然正确) ``

该请求会像什么都没发生过一样继续生成。注意力开销由窗口大小界定,而不再随生成长度线性增长。

配置

参数默认值说明
--compaction-window-size0(关闭)触发淘汰的 KV token 数量
--compaction-stride0每次事件淘汰的 token 数(必须是 block_size 的整数倍)

约束条件

  • 必须设置 --enable-prefix-caching false(压缩会对块进行拼接)
  • 必须设置 --async-scheduling false。在异步模式下,vLLM 会在处理当前步输出之前就预先调度下一步,因此当 update_from_output 执行时,num_output_placeholders 始终不为零。而压缩触发条件被限定为 num_output_placeholders == 0,因此在异步调度下压缩永远不会触发,运行会悄无声息地退化为全上下文模式。调度器在初始化时会对此进行断言检查。
  • 不支持流水线并行(--pipeline-parallel-size > 1)
  • stride 必须是 block_size(默认 16)的整数倍
  • window_size 必须大于 stride

推荐设置

使用场景窗口大小步长说明
长文本生成4096512速度与上下文的良好平衡
超长文本生成81921024保留更多上下文
激进压缩2048512极致速度,上下文较少

RL 训练(TextWorld)

configs/textworld/ 目录保存了论文中 TextWorld 实验结果背后的配置:在单节点 8 卡 H100(4 张用于推理 + 4 张用于训练)上运行 Qwen3-4B-Instruct-2507,batch 大小 512,每个 prompt 生成 8 条 rollout,共 500 步,预算 32k,每 10 个轮次压缩一次。每个文件都是独立完整的,并已对照其产生的运行的 W&B 配置进行过核验。

策略KV-streamsRe-prefill 压缩
全上下文(参照组)full_context.toml—
滑动窗口kv_streams_sliding_window.tomlreprefill_sliding_window.toml
Markovian Thinkerkv_streams_markovian_thinker.tomlreprefill_markovian_thinker.toml
摘要kv_streams_summary.tomlreprefill_summary.toml

sft/ 目录保存了 SFT 预热 + RL 运行(论文 6.1 节)的配置:与上面相同的 KV-streams 滑动窗口和 Markovian Thinker 配置,从 ppEmiliano/qwen3-4b-textworld-sft-eviction-mixed (https://huggingface.co/ppEmiliano/qwen3-4b-textworld-sft-eviction-mixed) 初始化。

``bash

构建数据集(见 reproducing.md),然后替换其路径:

sed “s|TEXTWORLD_DATASET|$PWD/data/textworld_all4_total60000_train59744_eval256_seed20260719|g”
configs/textworld/kv_streams_sliding_window.toml > /tmp/run.toml uv run rl @ /tmp/run.toml ``

KV-streams 实验组设置 kv_mode = "kv-eviction";re-prefill 实验组则设置 markovian_thinker.enabled = true 并关闭 kv_eviction。添加 --dry-run 可在不启动运行的情况下进行校验并导出解析后的配置。

训练过程中需要关注的指标

  • progress/reward/mean —— 应呈上升趋势。
  • loss/mismatch_kl_mean —— 训练器与推理的 logprob 一致性。当训练和推理看到相同的 KV 时,该值处于内核精度下限(约 1e-3);任何上升都意味着正确性 bug。
  • progress/entropy/mean —— 保持平稳或轻微下降属正常;熵值塌缩则表明发生了模式塌缩(mode collapse)。

项目结构

KV-streams/ vllm/ # 子模块:kv-streams-vllm(vLLM 0.19 fork,含 KV 淘汰) vllm/v1/core/compaction/ prime-rl/ # 子模块:kv-streams-prime-rl(训练器,淘汰机制接入代码) src/kv_eviction/ # 集成层(环境补丁、填充、分段前向传播) configs/textworld/ # 论文 RL 配置,每个实验组一个文件 plans/ # 代码注释中引用的设计笔记 experiments/ textworld_env/ # TextWorld 环境包、数据集生成、评估 scripts/ # 分析 / 基准测试工具 tests/ setup.sh # 一键安装

相似文章