1. Condense 给 RL 训练带来的新问题

Condense 解决了 rollout 能不能跑完的问题,但对训练而言却引入了一个根本性矛盾:

一个任务在 rollout 端是一条完整的轨迹,但在训练端被 condense 切成了 N 段彼此独立的 prompt-response 样本。

具体困难有三:

  1. 训练样本碎片化:condense 后每段的 prompt 都是"压缩过的新起点",trainer 看到的是 N 个互不相关的 (prompt, response) pair,原本属于同一条轨迹的因果关系被切断。
  2. Reward 信号位置错位:reward 只能在最终答案那一段算出来(前面段没有最终输出,无法 verify)。如果按传统 PPO/GRPO 每段独立算 advantage,前面 N-1 段的所有 token 拿到的 advantage 都是 0,等于浪费了 80%+ 的 rollout 算力。
  3. GAE 边界不连续:标准 GAE 在每段末尾把 nextvalues = 0(视为 terminal state),但在 long-horizon agent 中,段尾并不是"任务结束",而是"被 condense 打断"。把它当 terminal 会让 value function 学到错误的边界,导致 critic 永远低估前面段的价值。

维度 drop(ToolFIFOCondenser) summarize(LLMSummary / Claude /compact
log_prob 可复现 ✓ 训练时跟 rollout 时完全一致 ✗ summary 文本本身有随机性
段边界确定性 ✓ FIFO 是确定性操作,段边界 token 序列固定 ✗ summary 内容/长度都不固定,GAE 拼接难做
行为可解释 ✓ 直观可见丢了哪条 ✗ 出错难以 debug
生产延迟 ✓ 零额外开销 ✗ 多一次 LLM call
信息保真 ✗ drop 即真丢 △ 取决于 summary 质量
跟现有 mask 机制契合 ✓ 复用 loss_mask ✗ 需要新增 “summary token 区分”

代价是信息损失更彻底,但 Infini RL 通过 GAE 跨段拼接把这部分信息以 reward 信号的形式补回来:被丢掉的 observation 对应的 assistant token 仍然能拿到非零 advantage(来自后续段的 reward 经 γλ 衰减反传)。整体在数学上闭合、在工程上可复现,是 production-grade RL 训练的合理取舍。

挑战

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
═══════════════════════════════════════════════════════════════════════════
            Condense 之后,原本一条 trajectory 变成什么样子
═══════════════════════════════════════════════════════════════════════════

  逻辑 trajectory (跑了 12 轮的 SearchCI 任务):
                                                                            
   sys | user_q | a₁ t₁ a₂ t₂ a₃ t₃ a₄ t₄ a₅ t₅ a₆ t₆ a₇ t₇ ... a₁₂ t₁₂ 
                                                                          
                                                                          
                                                                       reward=1
                                                                       (任务成功)

  ── 物理上喂给 trainer 的样子(经过 2 次 condense):

  ┌─ 段 0 (inv_iter=2, 最早) ─────────────────────────────────────────┐
  │  sys | user_q | a₁ t₁ a₂ t₂ a₃ t₃                                  │
  │                                       reward = 0  (中间段无 reward)│
  └───────────────────────────────────────────────────────────────────┘
                          
                          ▼ (第 1 次 condense, 老内容被 drop)
                          
  ┌─ 段 1 (inv_iter=1) ───────────────────────────────────────────────┐
  │  sys | user_q | t₃ | a₄ t₄ a₅ t₅ a₆ t₆ a₇ t₇                       │
  │              ↑                       reward = 0                    │
  │      上一段被 drop 后留的骨架                                       │
  └───────────────────────────────────────────────────────────────────┘
                          
                          ▼ (第 2 次 condense)

  ┌─ 段 2 (inv_iter=0, 最新) ─────────────────────────────────────────┐
  │  sys | user_q | t₇ | a₈ t₈ a₉ t₉ a₁₀ t₁₀ a₁₁ t₁₁ a₁₂ t₁₂          │
  │                                                                    │
  │                                       reward = 1  ← 任务成功 reward│
  └───────────────────────────────────────────────────────────────────┘

═══════════════════════════════════════════════════════════════════════════

D1. Reward 归属:reward 只产生在最后一段,前面段怎么训?

任务成功/失败的 reward 信号只在 inv_iter=0 这段产生(agent 答对/答错的那一刻)。但前面 N-1 段的 action 才是把 trajectory 引向成功的真正原因。如果只用最后一段算 loss,前 N-1 段的 assistant 输出就完全得不到 credit assignment —— 模型学不到"前期搜索策略要怎么选才好"。

核心矛盾:reward 时空上集中在末尾,但需要把信号"反传"到前面所有段。

D2. Advantage 估计断裂:标准 GAE 跨不过段边界

GAE 的递推公式是:

$$ δ_t = r_t + γ·V (s_{t+1}) − V (s_t) $$

$$ A_t = δ_t + γλ·A_{t+1} $$ 它本质是从后往前沿着 token 序列递推。如果把每段当独立 trajectory 算:

段 2 GAE: A_t = δ_t + γλ·A_{t+1}, …, 直到段 2 第一个 token,链断。

段 1 GAE: A_t 完全独立计算,从段 1 末尾倒推

段 0 GAE: 同上

→ 段 2 的 reward 信号根本无法反传到段 1/段 0 的 action。这是 D 1 的技术表现。

D3. 段间 prompt “不连续”

段 1 开头的 prompt 是 [sys, user_q, t₃](被 condense 后的骨架),跟段 0 末尾的 prompt [sys, user_q, a₁ t₁ a₂ t₂ a₃ t₃] 不是延续关系。这意味着:

  • 不能简单 concat 所有段的 token 来算"一条长 trajectory 的 GAE"
  • 段 1 起点的 V(s) 估计跟段 0 终点的 V(s) 估计是两个 state 上的值,不能直接接

D4. Importance Sampling 的 prompt 复现

PPO 的核心是 ratio = π_new(a|s) / π_old(a|s),训练时必须用当时的 prompt + 当时的 response 重算 π_new(a|s)。Condense 是 deterministic 的(Drop 路线 OK),但训练 worker 必须能完全复现 rollout worker 当时看到的 prompt —— 这要求把 condense 的产物(哪段 message 被丢了、剩了哪些)准确序列化到 DataProto 里随 batch 传过来。

D5. Sequence Length 维度的爆炸

如果朴素地把整条逻辑 trajectory 拼到一起塞给训练(不切段),单条 sample 的 sequence length 可能是几十万 token,GPU 装不下。但如果按段切(每段 ≤ max_prompt_length + max_response_length),又面临 1-4 的问题。

→ 既要段化 fit 进 GPU,又要保留段间信号传递。

D6. 重复内容的 loss 重复计算

每段 prompt 的开头都重复包含 [sys, user_q, ...还有上次 condense 留的骨架]。如果不做特殊处理,PPO loss 会在多段里反复对同一段 token 算 loss:

  • assistant 段 0 输出的 a₃ 既出现在段 0 的 response 区域,也可能出现在段 1 的 prompt 区域(如果 condense 没把它丢掉的话)
  • 还有 sys、user_q 这些静态 prompt 永远在每段重复

→ 必须明确:哪段的哪些 token 才参与 loss 计算?

3. InfiniRL 的核心 idea

InfiniRL 的整体设计可以用一句话概括:

物理上分段、逻辑上连续 —— 每段当独立 sample 喂给 trainer(解决 D5),但通过 inv_iter + loss_mask + 跨段 GAE 三个机制把段间的信号通路重新接上(解决 D1-D4、D6)。

═══════════════════════════════════════════════════════════════════════════
                       InfiniRL 三件套
═══════════════════════════════════════════════════════════════════════════

   ┌───────────────────────────────────────────────────────────────────┐
   │  机制 1: inv_iter  ──  给每段标号                                  │
   │  ─────────────────────────────                                   │
   │  最末段 inv_iter=0, 倒数第二段 inv_iter=1, ...                     │
   │  在 rollout 端 (swalm_handler.py:1602-1605) 注入到 DataProto      │
   │  trainer 端 (ppo.py:445-523) 用它决定 GAE 处理顺序                │
   └───────────────────────────────────────────────────────────────────┘

   ┌───────────────────────────────────────────────────────────────────┐
   │  机制 2: loss_mask  ──  谁的 token 算 loss                         │
   │  ───────────────────────────────────                              │
   │  新生成的 assistant token: loss_mask = 1                          │
   │  被 condense 进 prompt 的老 token: loss_mask = 0                  │
   │  → 每段 loss 只对"本段刚 generate 出来的"算                        │
   │  → 重复出现的 sys/user_q/骨架不参与 loss                          │
   └───────────────────────────────────────────────────────────────────┘

   ┌───────────────────────────────────────────────────────────────────┐
   │  机制 3: 跨段 GAE  ──  reward 信号反传链                            │
   │  ──────────────────────────────────                              │
   │  按 inv_iter 从大到小处理: inv_iter=N → N-1 → ... → 1 → 0         │
   │  跨段传递 (lastgaelam, critic_lastgaelam, nextvalues)             │
   │  → 末段的 reward 沿着 GAE 链反传到所有段的 action 上               │
   └───────────────────────────────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════════════