Skip to content

[feature]reward_loop - #2

Open
doctorMcy wants to merge 2 commits into
mainfrom
feature_rl_reward_loop
Open

doctorMcy wants to merge 2 commits into
mainfrom
feature_rl_reward_loop

Conversation

@doctorMcy

@doctorMcy doctorMcy commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

PR type

  • Bug Fix
  • [ √ ] New Feature
  • Document Updates
  • More Models or Datasets Support

PR information

reward_loop 奖励流水线:流式采样与节点微批(原理 / 收益 / 验证)

本 PR 的 reward_loop(src/twinkle/reward_loop/)把"算奖励"从 RL 训练主循环解耦成一条可异步
提交 / 收集
的流水线,并配套两项能力:引擎级流式采样(序列边生成、奖励边算)与奖励节点
微批
(BENCH_MICRO_BATCH,把逐条提交自动合并成大请求)。原理见"功能与原理"一节,实验证据见
§1~§2。

收益总览

实测场景:RM 生成式 judge 打分,每步 32 条序列(数字为每步 t_collect,即采样结束后等待全部
奖励就绪的耗时):

场景 逐条提交(默认) 逐条 + 微批 16 整批提交
判词 8 token 粒度无关紧要(§2.2) 3.3s 3.6s
判词 200 token 85.5s 13.6s 8.9s
判词 200 token + 长尾生成(2048) 68.4s 7.10s 11.49s
  • 长判词 + 长尾生成是流式的收益场景:逐条 + 微批 7.10s 反超整批 11.49s,单步总耗时快 ≈10s
    (§2.7)——整批必须等全部序列生成完,流式把奖励藏进了采样窗口。
  • 提高奖励并发同样有效:workers 2→4(不开微批),逐条 85.5s → 45.8s(§2.4)。
  • 函数式奖励(毫秒级规则打分):两种采样方式端到端持平,7 组 B/A 落在 1.00~1.19×(§1.3)。
  • 正确性:流式 vs 整批 tokens / logprobs / rewards / advantages 位级一致(Level-1);60 个
    run-路径-步骤语义检查点全 True(§1.2)。

功能与原理

reward_loop 是 RL 训练中负责"算奖励"的独立组件:上层只做两件事——submit 一批待打分的样本、
collect 取回分数。异步化的目的就是让奖励计算与采样、训练重叠(RewardLoopMetrics.overlap_ratio
量化这个重叠),本报告回答的正是"这个重叠能换来多少墙钟收益、代价在哪里"。

数据流:采样出的序列 → 组装 RewardItem(item_id / solution_str / ground_truth / extra_info)
→ pipeline.submit(items) 返回 BatchHandle → worker 内的 manager 并发打分 → pipeline.collect(handle)
按 item_id 还原为与输入同序的 RewardResult → 交给 advantage 与训练。本报告对比的两条路径
(整批 A / 流式 B)差别只在什么时候 submit,打分组件本身完全相同。

模块 职责
data.py 契约与工具:RewardItem / RewardResult;split_items 按 worker 数分块,reorder_by_id + assemble_scores 还原输入顺序(缺失/重复 item_id 直接报错)
pipeline.py AsyncRewardPipeline:submit / collect,backlog 背压(满时阻塞或丢弃最旧)、on_backlog_full / on_error(报错或记 0 分);worker 是 Ray actor 时走 ray.get,否则走内置线程池(宽度 = num_workers)
worker.py RewardLoopWorker:持有 manager,compute_score_batch(items) = asyncio.run(manager.run_batch(items));支持 custom_reward_function_path 动态导入打分函数
reward_manager/ RewardManagerBase(异步 call_score / run_single / run_batch,normalize_score 归一为标量分数,max_concurrent 信号量)+ 注册表 register / get_reward_manager_cls;内置 naive / rate_limited(rpm / tpm / 超时兜底)/ remote / dapo(超长惩罚)/ gdpo,支持注册自定义 manager(本测的 batch_judge 即自定义)
config.py RewardLoopArgs:num_workers / manager_name / manager_source / backlog / mode / max_rpm / max_tpm / max_concurrent / timeout 等
metrics.py RewardLoopMetrics:提交/收集批次数、submit / collect 耗时、max_backlog、overlap_ratio = 1 − collect 等待 /(submit + reward)
default_score.py 默认规则打分分派:按 data_source 注册 scorer,未知来源按 unknown_rewards 告警或报错

最小用法:

from twinkle.reward_loop import AsyncRewardPipeline, RewardItem

pipeline = AsyncRewardPipeline(num_workers=2, backlog=32, worker_kwargs={'compute_score': my_score})
handle = pipeline.submit(items)      # 整批提交 = 路径 A;逐条提交 = 路径 B
results = pipeline.collect(handle)   # 与 items 同序的 RewardResult 列表

参考:cookbook/rl/reward_loop/bench_streaming_local.py。

能力一:引擎级流式采样(路径 B)

传统流程(路径 A)要等一步的全部序列生成完、算完奖励才能训练;流式(路径 B)用
vLLMSampler.sample_sequences_to_queue 在一次远程调用内并发调度全部序列(vLLM 保持合批,
不牺牲采样吞吐),任何一条序列先生成完就立刻回传事件、立刻提交打分。它的收益来源是重叠窗口:
序列完成得越分散(例如长短答案混合的长尾生成),越有机会在采样进行中把奖励算掉。正确性与整批
位级一致(§1.2),代价只是奖励侧需要合适的提交粒度(见能力二与 §2.4)。

能力二:奖励节点微批(BENCH_MICRO_BATCH)

流式的默认提交方式是逐条(保留逐条事件语义),但生成式 judge 的成本按请求次数计——一个请求
装 1 条与装 16 条耗时几乎相同(§2.4/§2.6),逐条提交等于把请求数放大 16 倍。微批在 worker 内把
多次 submit 进来的 item 先攒着,攒满 micro_batch_size 条(或等 micro_batch_timeout_ms 兜底)
后一次发给打分后端:上游提交方式完全不变,引擎侧请求数自动收敛。实现上 worker 通过
max_inflight_calls 告知 pipeline 放大在飞请求数,否则逐条提交最多 2 个请求在飞、攒不满批。
manager 侧只需覆写新增的 call_score_batch 批接口(默认实现保持逐条语义,完全向后兼容)。
收益:判词 200 token 时逐条 t_collect 85.5s → 13.6s(§2.5),长尾场景反超整批(§2.7)。

0. 口径与指标

  • 路径 A(整批):整批采样 → 整批提交奖励 → collect → 训练。
  • 路径 B(流式):引擎级逐序列流式采样(vLLMSampler.sample_sequences_to_queue,一次远程调用内
    并发调度全部序列并逐条回传事件);提交粒度默认逐条,也可配为攒批后提交(见 §2)。
  • 提交粒度(与路径正交的另一个维度):一次 pipeline.submit 装多少条——整批 = 全部序列;
    逐条 = 每完成一条就提交一条;每 2 条 = 攒够 2 条再提交。路径 A 恒为整批;路径 B 默认逐条。
    奖励节点最终发给打分后端的"请求"大小还受 worker 内微批影响(§2.5)。
  • worker:奖励流水线内真正执行打分的并发工作单元(本测 2 个),决定同一时刻最多几个打分
    请求在跑。
  • chunk:一次 submit 的内容按 worker 数切出的分块(块数 = min(worker 数, 提交条数)),每块
    交给一个 worker。
  • 请求(judge 调用):奖励节点向后端发起的一次打分调用,一次可携带多条 item;本报告的核心
    结论之一是成本按请求计(§2.4)。
  • 可重叠窗口:采样仍在进行、但已有序列完成、可以开始算奖励的时间段(≈ 采样结束 − 首条序列
    完成时刻);窗口越大,流式越有机会把奖励藏进采样时间里。
指标 定义(单位:秒)
t_sample 采样阶段墙钟
t_submit 提交阶段墙钟(本测中可忽略,表内省略)
t_collect 等待本步全部奖励就绪的墙钟(奖励尾部等待)
t_train forward_backward + clip_grad_and_step 墙钟
t_total 单步总墙钟 = 四者之和
seq_per_s 每步序列数 / t_total
reward_head_start 本步第一条奖励开始计算距采样开始的时长(B 中 < t_sample 表示与采样重叠)
reward_tail_after_sample 本步最后一条奖励就绪距采样结束的时长(负值 = 早于采样结束就绪)

1. 函数式奖励(GSM8K 规则打分)

1.1 环境与配置

项 值
机器 / 部署 训练机,本地 ray 模式(twinkle.initialize(mode='ray'),无 twinkle-server),昇腾 NPU ×4(ASCEND_RT_VISIBLE_DEVICES=2,3,4,5),model 1 卡 + sampler 1 卡
模型 默认 ms://Qwen/Qwen3.5-4B(可覆盖为本地路径)
数据集 GSM8K train(7473 行),本地 jsonl(messages + gold_answer),经 DatasetMeta(data=rows) 内存加载,完全离线
采样参数 temperature=1.0, top_p=0.95, logprobs=1, num_samples=1
每步序列数 batch4 × gen(base/tok/d200/d1000=16,gen2=8,gen8=32)
奖励 函数式规则打分(gsm8k_score),毫秒量级(延迟=0 时 t_collect ≈ 0,见 §1.3);昂贵奖励由 TWINKLE_REWARD_DELAY_MS 注入
奖励并发 TWINKLE_REWARD_NUM_WORKERS=2;路径 B 提交粒度 per-item(固定)
调度 单缓冲:本步采样完成 → collect 本步奖励 → 训练(避免双缓冲掩盖流式收益)

1.2 正确性

Level-1(确定性:temperature=0 + 固定 seed,不训练)

检查项 最终结果
样本总数一致(16=16) ✅
tokens 逐位一致 ✅
logprobs 逐位一致 ✅(max diff 0.0)
decoded 文本一致 ✅
reward 逐样本一致 ✅(真实 ground truth 下成立,非平凡)
优势(GRPOAdvantage)一致 ✅

Level-2(语义:随机采样)

检查项 A(批量) B(流式)
无丢样本 / 无重复 item_id ✅ ✅
handle 结果与 item_id 对齐 ✅ ✅
reward 函数确定性(重打分一致) ✅ ✅
优势组均值归零 ✅ ✅

60 个 run-路径-步骤检查点全部 True。

1.3 收益测量

基础配置分解(batch=4, gen=4, max_tokens=1024, delay=0, 6 步均值)

指标 A(批量) B(引擎流式)
t_sample 19.6 20.3
t_collect 0.00 0.00
t_train 3.7 3.6
t_total 24.4 24.6
seq/s 0.65 0.65
reward_head_start 19.7 ≈16
reward_tail_after_sample 0.00 -0.001

单变量扫描(每 run 4 步;单位秒)

run 变量 A t_total B t_total B/A
base — 24.35 24.64 1.01×
gen2 gen=2 20.90 21.31 1.02×
gen8 gen=8 32.74 32.73 1.00×
tok512 max_tokens=512 16.00 17.02 1.06×
tok2048 max_tokens=2048 44.98 45.50 1.01×
d200 delay=200ms 25.16 26.56 1.06×
d1000 delay=1000ms 25.98 30.96 1.19×

奖励尾部与重叠窗口

run A t_collect B t_collect
d200 0.205s 0.397~1.578s
d1000 1.005s 1.208~7.977s

B 的首条奖励相对 A 的提前量(= A 的 reward_head_start − B 的 reward_head_start,
A 的该值≈其采样结束点):

run base gen2 gen8 tok512 tok2048 d200 d1000
提前量(s) 1.2 1.3 5.5 -0.9(更晚) 22.0 4.6 4.2

机制(结论 3、4 的依据)

  • A 侧 t_collect ≈ delay(延迟=0 时 ≈0;d200 0.205~0.21s、d1000 1.005s,见上表):整批提交时
    单个 handle 内部由 run_batch 用 asyncio 并发全部 item(2 个 chunk × 8 并发),奖励耗时几乎
    完全被并行吸收。
  • B 侧逐条提交时每次调用的 item 并发为 1,实际并发上限 = AsyncRewardPipeline 线程池宽度
    (max_workers = num_workers,本测为 2):d1000 下 16 条 ≈ (16/2) × 1s ≈ 8s,与实测最大尾部
    7.977s 吻合;由于同批序列完成时刻集中(引擎级流式合批),奖励请求成簇到达,尾部最明显。
  • 在 Ray actor 部署(server 侧)中,逐条提交会撞上 actor 默认 max_concurrency=1 的串行
    (remote_class 不传该参数即串行),此时提高 worker 数无效——加大提交 chunk 是通用解法。

1.4 小结(函数式奖励)

  1. 正确性:两级检查全部通过;Level-1 在 greedy + 固定 seed 下达成 tokens / logprobs / decoded /
    rewards / advantages 位级等价,reward 一致性建立在非空 ground truth 上(非平凡)。
  2. 采样层面持平:路径 B 在一次远程调用内并发调度全部序列(vLLM 保持合批),没有引入
    逐序列调用的串行代价——t_sample 与 A 同量级(20.3s vs 19.6s),7 组 t_total 的 B/A 落在
    1.00~1.19×(唯一超过 1.06× 的档位是 d1000),同时保留了逐条完成事件语义
    (Level-1 / Level-2 检查通过)。
  3. 延迟=0 时流式重叠买不到墙钟:奖励本身耗时可忽略(延迟=0 时 A/B 的 t_collect ≈ 0),
    无可重叠成本;即使长输出下重叠窗口很大(tok2048 首条奖励提前 22.0s),t_total 仍持平。
  4. 延迟>0 时奖励侧吞吐成为瓶颈:reward_head_start 确实提前(gen8 5.5s、d200/d1000 约 4~5s),
    但 reward_tail_after_sample 同步变大(B 最大 8.0s vs A 1.0s),净墙钟变差(d1000 B/A = 1.19×)。
    残余代价来自提交粒度而非采样方式;修法为整批/小批提交(或 §2.5 的奖励节点攒批)。

2. 奖励模型(RM,生成式 judge)

2.1 配置

项 值
打分器 生成式 judge,独立 DeviceGroup('reward') 单卡(TWINKLE_REWARD_MODEL_ID,默认跟随 TWINKLE_MODEL_ID)
judge 形态 冻结权重的第二个 vLLMSampler(remote_group='reward');compute_score 四参数契约;输出 Correct/Incorrect 或 1/0 → 1.0/0.0
并发形态 AsyncRewardPipeline 按 chunk 并行调用 judge(多线程 → vLLM 请求合批),与"并行 API 型 RM"一致
矩阵 batch=2 × gen=2(每步 4 条)、max_tokens=512、3 步、TWINKLE_REWARD_JUDGE_MAX_TOKENS=8(判词 8 token)
粒度档 提交粒度三档(run 级固定):整批(1 handle)/ 每 2 条一批 / 逐条;定义见 §0

判词 = judge 为每条样本生成的判定文本(含推理与结论),长度由 TWINKLE_REWARD_JUDGE_MAX_TOKENS
限制;判词长度直接决定单请求成本(§2.6)。

2.2 提交粒度矩阵(取稳定步均值;首步含 judge 引擎一次性 warm-up,已剔除)

run 路径 粒度 t_sample t_collect t_total
rm-whole A 整批(1 handle) 8.5s 0.78s 14.1s
rm-b-whole B 整批 9.7s 0.57s 11.7s
rm-b-mini B 每 2 条一批 9.8s 0.94s 12.1s
rm-b-per B 逐条 9.8s 0.92s 12.2s

结论(RM 场景)

  1. 短判词下提交粒度无关紧要:真实 judge 评分在亚秒级(8 token 判词;t_collect 0.57~0.94s),
    引擎对并发请求合批(探针 C≈A,§2.6),三档 t_total 11.7~12.2s 等价。对照 §1.3 的 d1000
    (函数式奖励 + 注入 1s/条延迟、每步 16 条)中同样的逐条提交:尾部被放大到 7.9s(A 侧 1.0s),
    说明粒度差异只在"奖励贵到超出重叠窗口"时才出现。
  2. 采样路径(A vs B)在奖励侧一致:整批 A 与 B 的 t_collect 0.78s vs 0.57s;A 的 t_total
    偏高来自首步 warm-up 与采样侧(§4)。
  3. ⚠️ judge 判别质量限制:reward_mean=0.0000 且无解析失败告警——judge 全部判 Incorrect
    (4B 验证器判别失效)。延迟特征(真实推理耗时)有效,判别语义无效;性能对比结论不受影响,
    如需真实判别需换更强 judge 或判别式 RM(直接打分、不生成判词,因此也没有判词长度问题)。
  4. 实践含义:是否需要调整提交粒度,取决于奖励侧的串行总量(条数 × 单条延迟 ÷ 可用并发,
    本地 harness 下可用并发 = pipeline 线程池宽度 = num_workers = 2)相对可重叠窗口的大小,
    而非单看单条延迟:
    • 本矩阵:每步 4 条、judge 单条在亚秒级(≲1s)→ 串行总量 ≲ 4 × 1s ÷ 2 = 2s,远小于采样窗口
      8.5~9.3s → 三档无差别,粒度无关紧要;
    • §1.3 的 d1000:每步 16 条、注入 1s/条 → 串行总量 ≈ 16 × 1s ÷ 2 = 8s,超过可重叠窗口
      (首条奖励早于采样结束约 4.2s,见 §1.3)→ 实测尾部溢出到 1.2~8.0s,此时应改用整批/小批提交;
    • §2.3 的长判词边界是第二条独立机制:chunk 越大,单次调用内并发的条数越多,judge 侧合批
      收益越明显(与排队溢出无关,故其收益在串行总量不溢出时也会出现)。

2.3 边界验证:长尾生成 + 昂贵 judge(采样 max_tokens=2048 / judge 判词 200 token)

配置同 §2.1 的矩阵(每步 4 条序列)。各臂 3 步;B 各臂的最慢一步是 step2(该批序列生成偏长,
t_sample 32.5~36.1s),A 的最慢一步是 step0(首步 warm-up),故 t_total 同时给出剔除最慢
一步后的值。

run 路径 t_sample t_collect t_train t_total(3 步均值) 剔除最慢一步
rm-whole A 27.9s 6.30s 2.14s 40.6s 35.1s
rm-b-whole B 23.9s 5.93s 1.19s 31.5s 25.4s
rm-b-mini B 27.2s 7.88s 1.20s 36.9s 30.2s
rm-b-per B 27.0s 6.84s 1.20s 35.8s 31.9s

结论(边界)

  1. 奖励仍未成为关键路径:t_collect 5.9~7.9s,仍低于采样 24~28s;流式未因奖励变贵而反超。
    B 的 t_total 整体小于 A(31.5~36.9s vs 40.6s),差异来自采样侧(A 的非流式采样 27.9s vs B
    约 24~27s)与 A 的首步 warm-up,而非奖励侧。
  2. 提交粒度排序稳定但差距收窄:整批 5.93s < 逐条 6.84s < 每 2 条 7.88s(差 ≤1.3×)。机制同
    §2.4:整批请求数最少;小批(每 2 条)在 2 个 worker 下被切成 1 条/块,实际 chunk 与逐条相同。
  3. RM 模式下 semantic_ok 列不可用(reward_deterministic 检查拿 judge 分数与规则奖励比对,
    语义不成立),应忽略。奖励时间线已给 batch_judge 补上逐条 reward_start / reward_end 与
    每 chunk 的 judge_chunk / judge_call 事件,因此 §2.4 的排队与合批指标同样适用于 RM。

2.4 RM 压力档:32 条/步(判词 200 token)

配置:batch=4 × gen=8(32 条/步)、采样 max_tokens=512、判词 200 token
(TWINKLE_REWARD_JUDGE_MAX_TOKENS=200)、3 步;奖励并发 TWINKLE_REWARD_NUM_WORKERS=2;其余同 §2.1。

run 路径 粒度 t_sample t_collect(=尾部) judge 调用:次数 / 平均条数 / 平均耗时
rm-whole A 整批 11.0s 9.38s 6 / 16.0 / 9.28s
rm-b-whole B 整批 12.6s 8.93s 6 / 16.0 / 8.85s
rm-b-mini B 每 2 条 12.8s 85.55s 96 / 1.0 / 5.33s
rm-b-per B 逐条 13.0s 85.55s 96 / 1.0 / 5.35s

同一配置把并发改成 4(TWINKLE_REWARD_NUM_WORKERS=4)另跑一次作对照(逐条臂):t_collect
85.5s → 45.8s、chunk 级并发 4——提高并发有效(约线性)。

该跑采样侧与训练侧正常(t_sample 11.0~13.0s、t_train 6.9~7.7s),差异只在奖励侧。

结论(压力档)

  1. 长判词下提交粒度决定成败:逐条 85.5s vs 整批 8.9s,差 9.6×(§2.2 的短判词档两档仅差
    1.6×)。A 与 B 整批的奖励侧一致(9.38s vs 8.93s)。
  2. 开销按"请求"计,引擎对并发请求合批:单请求成本 ≈5.3s(1 条)/ 8.9s(16 条)——16 条一个
    请求只贵 1.7×(vLLM 合批,探针 C≈A,§2.6);32 个请求与 2 个请求的数量差(16×)才是主因,
    总耗时由请求个数决定。
  3. 提高并发有效(旧结论反转):workers 2→4,逐条 t_collect 85.5s → 45.8s(≈线性,因为引擎
    合批并发请求);微批放大在飞请求数同样有效(§2.5)。
  4. 每次提交会被 worker 数切块:AsyncRewardPipeline 按 worker 数轮询切分提交内容,实际 judge
    chunk = 提交条数 ÷ worker 数(2 worker → 16 条/块,4 worker → 8 条/块)。因此"每 2 条一批"在
    2 或 4 个 worker 下都会被切成 1 条/块,退化成逐条(表中 mini 与 per-item 的调用次数与平均条数
    完全相同)。要形成真正的小批,提交条数必须大于 worker 数(BENCH_MINI_BATCH_SIZE >
    TWINKLE_REWARD_NUM_WORKERS)。
  5. 本轮仍无可重叠窗口:B 的首条序列在采样结束前 0.05s(11.75s vs 11.80s)才完成,奖励工作量
    整体落在采样之后,B 相对 A 没有提前量可用——与 §1.3 现象一致(该结论仅对短输出成立;长尾下
    窗口真实存在,见 §2.7)。

2.5 奖励节点攒批(BENCH_MICRO_BATCH):上游逐条提交 + 引擎大请求

微批定义见"功能与原理 / 能力二"。配置同 §2.4(32 条/步、判词 200 token、
TWINKLE_REWARD_NUM_WORKERS=2),微批为 BENCH_MICRO_BATCH=16 / BENCH_MICRO_BATCH_TIMEOUT_MS=200
(攒满 16 条或等 200ms 即发起一次 judge 调用);"关"一行为逐条基线。另做 BENCH_MICRO_BATCH 大小
扫描,判词长度扫描见 §2.6。

微批与引擎侧修复的关系(重要):微批是为绕过 Ray actor 阻塞设计的调用方侧方案——早期训练机
状态上,同步阻塞的 sample 占住 asyncio actor 的事件循环,并发请求在进引擎前被串行化(探针
4 并发 = 4 × 单请求),逐条提交因此付出 9.6× 尾部代价。main 分支 commit c839a4e(modelscope#280)已在
引擎侧修复该问题:vLLMSampler.sample 声明 enable_continous_work=True,async companion 把
阻塞体挪进线程池,并发请求得以一起进引擎合批——本次探针已证实其在训练机生效(§2.6:C ≈ A)。
因此:在该修复生效的版本上,逐条提交的尾部随在飞请求数下降(workers 2→4:85.5s → 45.8s);
调大 worker 数或后续引擎优化都可能进一步替代微批
。微批的价值在于:不增加 worker 数、不改上游
提交方式,即把尾部压到接近整批(13.6s vs 8.9s),并保留逐条事件语义。

run 粒度 微批 judge 调用:次数 / 平均条数 / 平均耗时 t_collect(=尾部) t_total
rm-b-per 逐条 关 96 / 1.0 / 5.35s 85.5s 106.2s
rm-b-per 逐条 开(16 条 / 200ms) 8 / 12.0 / 6.69s 13.6s 37.8s
rm-b-mini 每 2 条 开(16 条 / 200ms) 14 / 6.9 / 7.00s 14.2s 38.2s
rm-b-whole 整批 — 6 / 16.0 / 8.85s 8.9s 29.1s
rm-whole / A 整批 — 6 / 16.0 / 9.28s 9.4s 32.4s

微批大小扫描(逐条臂、BENCH_MICRO_BATCH_TIMEOUT_MS=200)

BENCH_MICRO_BATCH judge 调用:次数 / 平均条数 / 平均耗时 t_collect
0(关) 96 / 1.0 / 5.35s 85.5s
4 24 / 4.0 / 5.97s 47.7s
8 13 / 7.4 / 5.89s 23.5s
16 8 / 12.0 / 6.69s 13.6s
整批(对照) 6 / 16.0 / 8.85s 8.9s

扫描只跑逐条臂(BENCH_RUNS=rm-b-per);末行"整批(对照)"为同配置的 rm-b-whole,作参考线。

结论(攒批)

  1. 上游逐条提交 + 节点攒批 ≈ 整批量级:提交方式不变,只开微批,t_collect 85.5s → 13.6s
    (6.3×);仍差整批 4.7s——整批的 2 个 16 条请求被引擎合批(每调用 8.85s),逐条的块按到达节奏
    串行(块 {1,15,16},未攒满)。
  2. 块大小由到达节奏与超时共同决定:三步块为 {1,15,16}(MB=16 时);完成节奏分散时 200ms 可能
    先于攒满触发。大小扫描单调改善(4→47.7s、8→23.5s、16→13.6s),MB=16 已接近整批,无需更大。
  3. 引擎对并发请求合批是前提:探针三形态均 C≈A(§2.6),说明在飞请求数只要够多就会被合批;
    提高 worker 数等效(§2.4 结论 3),微批(BENCH_MICRO_BATCH)与 TWINKLE_REWARD_NUM_WORKERS
    都能放大在飞请求数。
  4. mini 与微批组合不再差(14.2s ≈ 逐条 13.6s):引擎合批后小块的代价消失,旧结论反转;
    三档提交方式 + 微批都收敛到整批量级。

2.6 判词长度对 judge 调用成本的影响

探针(同 prompt 三形态:1 调用 1 条 / 1 调用 4 条 / 4 并发各 1 条)与压力档 bench(MB=16)
联合测量:

判词上限 1×1 1×4 4 并发×1 端到端 t_collect:逐条+MB16 / 整批
8 0.28s 0.39s 0.40s 3.3s / 3.6s
200 5.24s 5.49s 5.53s 13.6s / 8.9s
2000 12.90s 16.59s 16.31s 46.8s / 26.7s

"端到端"列的"整批"即 §2.5 的 rm-b-whole(非流式提交),作对照线;探针三列(1×1 / 1×4 /
4 并发×1)是引擎层单请求测量,与提交路径(流式/整批)无关。

结论(判词长度)

  1. 判词上限决定单请求成本:单请求 0.28s(8)→ 5.24s(200)→ 12.90s(2000),解码 ≈25ms/token、
    预填充固定 ≈0.3s。
  2. 上限超过自然长度后封顶:2000 档实测 12.90s,按 25ms/token 折算 ≈500 token——judge 在约
    500 token 处自然 EOS,判词上限再大也不再增加成本("不限制"最多就是这个量级,不会失控到分钟级)。
  3. 引擎对并发请求合批,各档位成立:C ≈ A(0.40 / 5.53 / 16.31 vs 0.28 / 5.24 / 12.90)——
    并发请求被合批处理(早期版本的"跨请求串行 C=4×A"与旧代码有关,§4)。
  4. 端到端:短判词逐条 ≈ 整批,长判词整批更优(8:3.3 vs 3.6;200:13.6 vs 8.9;800:46.8 vs
    26.7)——判词越长整批优势越大(2 个并发 16 条请求被合批,逐条块按到达节奏串行);判词上限决定
    绝对成本(8→800 约 15×)。

2.7 长尾 × 微批:流式首次反超整批

配置同 §2.4 但采样 max_tokens=2048(长尾生成);判词 200 token、32 条/步、2 worker。MB=0 为基线,
MB=16 分 200ms / 1000ms 两档超时。

run 微批 t_sample t_collect t_total head judge 调用:次数 / 平均条数 / 平均耗时
rm-b-per 0 42.6s 68.4s 121.5s 17.4s 96 / 1.0 / 5.81s
rm-b-per 16(200ms) 41.0s 7.10s 58.6s 18.2s 60 / 1.6 / 6.76s
rm-b-per 16(1000ms) 43.1s 7.27s 60.8s 19.8s 40 / 2.4 / 6.39s
rm-b-whole — 41.0s 11.49s 69.0s 41.0s 6 / 16.0 / 11.36s

MB=0 跑的整批臂 judge 调用偏慢(31.3s,首跑 warm-up),整批对照取后两跑(11.4~11.5s)。

结论(长尾 × 微批)

  1. 长尾下"无可重叠窗口"不成立:head 17~20s 远小于 t_sample 41~43s,奖励在采样进行中就开始
    计算(§2.4 结论 5 仅对短输出成立)。
  2. 逐条 + 微批首次优于整批:t_collect 7.10s < 整批 11.49s,t_total 58.6s < 69.0s(快 ≈10s)——
    整批必须等全部序列完成才开始(head = 41s),流式 + 微批把奖励藏进了采样窗口。
  3. 长尾下微批的主要收益是放大并发 + 利用窗口:块均值仅 1.6~2.4 条(完成节奏分散,
    200ms / 1000ms 都攒不满 16),真正起作用的是微批把在飞请求数从 2 放大到 10~16(conc),引擎
    对并发请求合批(探针 C≈A,§2.6);配合长尾的真实重叠窗口(结论 1),奖励被藏进采样时间。
  4. 超时档几乎无差(7.10 vs 7.27s):奖励已藏进采样窗口,尾部不再是瓶颈。

3. 总体结论

  1. 正确性:函数式奖励与 RM 两条路径均通过(Level-1 位级等价;Level-2 60 个检查点全 True)。
    RM 模式需忽略 semantic_ok(检查语义不适用于 judge)。
  2. 性能对比取决于判词与输出长度:函数式奖励下两条路径持平(7 组 B/A 落在 1.00~1.19×,唯一
    超过 1.06× 的档位是 d1000);RM 短判词下持平;长判词下整批提交更优(§2.4);长尾 × 微批下流式
    逐条反超整批(§2.7)。
  3. 流式不带来墙钟收益(当前调度下):延迟=0 时无可重叠成本;延迟>0 时奖励侧吞吐成为新瓶颈,
    尾部等待抵消提前开始的收益。扫描表也没有出现收益随变量增大而放大的趋势——delay 0→200→1000ms
    的 B/A 为 1.01→1.06→1.19×,max_tokens 512→1024→2048 为 1.06→1.01→1.01×,gen 2→4→8 为
    1.02→1.01→1.00×。长尾档(§2.3,2048 token)B 的 t_total 整体低于 A(31.5~36.9s vs 40.6s),
    但该差异在采样侧(A 的非流式采样更长、首步 warm-up),且 §1 同长度档(tok2048)A ≈ B
    (36.9s vs 36.8s),故不归为流式收益。真正的例外是长尾 × 微批(§2.7):序列完成分散带来
    真实重叠窗口,且引擎对并发请求合批,逐条 + 微批的 t_collect 7.10s 反超整批 11.49s、
    t_total 快 ≈10s——这是唯一一次流式获得实质墙钟收益。
  4. 提交粒度 / 在飞请求数是最有效的杠杆:奖励便宜(毫秒量级/条)时粒度无关;奖励昂贵时,引擎
    对并发请求合批(探针 C≈A,§2.6),但逐条提交受 pipeline 池宽限制(无微批时在飞请求数 =
    num_workers)——提高 worker 数(§2.4:2→4 使逐条 85.5s → 45.8s)或开启微批放大在飞数
    (§2.5:85.5s → 13.6s)都有效;短输出下整批仍最优(8.9s)。注意小批要真正生效,提交条数必须
    大于 worker 数(§2.4 结论 4)。

4. 数据可信度与边界

  • 每个配置 3~6 步(§2.2~§2.5 为 3 步,其余 4~6 步)。除 d1000(+19%~+20%)外,各组差异均在
    噪声量级(组内步间抖动可达 ±2%)。
  • reward_tail_after_sample 的小负值(如基础配置 B 侧的 −0.001s)表示最后一条奖励在采样结束打点前
    就绪,不是计时错误。
  • RM 场景的 judge 判别质量受限(见 §2.2 结论 3),延迟结论有效、判别语义无效。
  • RM 的单次 judge 调用耗时随判词长度变化:8 token 判词(§2.2 规模)每步总等待仅 1.0~1.3s、
    单请求 ≈0.3s,200 token 单请求 ≈5s,且上限超过约 500 token 后由 judge 的自然 EOS 封顶
    (§2.6)。跨批次比较前先核对 TWINKLE_REWARD_JUDGE_MAX_TOKENS(bench 启动日志会打印
    judge_max_tokens=)。
  • §2 使用 Qwen3-1.7B 做采样、Qwen3.5-4B 做 judge;judge 侧一致,采样模型只影响 t_sample /
    t_train,不影响 §2 的奖励侧结论。
  • RM 的并发行为随 twinkle 版本变化:早期版本对并发请求串行处理(探针 C ≈ 4×A),当前版本合批
    (C ≈ A;修复为 main 分支 commit c839a4e 的 async companion / enable_continous_work)。
    本文 §2 数字为当前代码重测所得,跨版本比较前先核对 TWINKLE_SAMPLER_MAX_CONCURRENCY
    与 async-companion 状态(探针头部会打印)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant