从一张四宫格图出发,讲清楚 on-policy、流式 off-policy、异步陈旧样本、Partial Rollout 这四种训练模式到底差在哪;再顺着一条样本的旅程,走完 Rollouter → Harbor 沙箱 → MessageQueue → Trainer → 权重同步的完整链路。
在 SWE-bench 这类代码 Agent 场景里做 RL,一条样本要在沙箱里跑几分钟到几十分钟,而且每条样本的耗时天差地别。传统训推一体(Colocate)架构下,GPU 会长时间空转等最慢的那条样本。全异步(Fully Async)把生成和训练拆到两组 GPU 上、用一个消息队列以「单条样本」为粒度流式对接,端到端吞吐提升 2.35×–2.67×。
而那张四宫格图,画的是同一套架构下的四档「异步程度」:从最保守的完全同步(a),一路放宽到最激进的「同步权重时把生成中的样本切断、同步完再接着写」(d)。越往后吞吐越高,代价是训练用的样本越「不新鲜」,算法上要付出额外的修正手段。
Colocate 模式下,Rollout(生成轨迹)和 Training(更新参数)共用同一组卡,串行执行:
Colocate:
|------ rollout 1(含长尾等待)------|-- train --|------ rollout 2 ------|-- train --|
↑
绝大多数样本早就跑完了,
整批在等最慢的那 1~2 条
一个厨师既炒菜又洗碗,而且规定「一桌 64 道菜全部出锅才能开始洗碗」。有 62 道菜 3 分钟就好了,剩下 2 道要炖 40 分钟——于是灶台空着 37 分钟,锅碗也堆着不洗。这就是长尾样本 + 训推串行的双重浪费。
全异步的做法是:请两个厨师,各占一个灶台——一个只管炒菜(Rollouter),一个只管洗碗(Trainer),中间放一个传菜口(MessageQueue)。菜一道一道地传,不必等整桌齐:
Fully Async:
Rollouter: |--- 持续生成 ---|--- 持续生成 ---|--- 持续生成 ---|
Trainer: |---- train 1 ----|---- train 2 ----|---- train 3 ----|
══════════════> 两条时间线重叠,端到端时间大幅压缩
┌──────────────────────────────────────────────────────────────┐
│ Fully Async Policy │
│ │
│ ┌───────────┐ 一条一条传 ┌──────────────┐ │
│ │ Rollouter │ ─────────────> │ MessageQueue │ │
│ │ (8 GPU) │ │ (Ray Actor) │ │
│ └───────────┘ └──────┬───────┘ │
│ ▲ │ 一条一条取 │
│ │ NCCL 广播权重 ▼ │
│ ┌────┴──────────────────┐ ┌──────────────┐ │
│ │ ParameterSynchronizer │ <──│ Trainer │ │
│ │ (checkpoint-engine) │ │ (8 GPU) │ │
│ └───────────────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
| 组件 | 身份 | 做什么 |
|---|---|---|
| Rollouter | 生产者 | 独占一组 GPU(如 8 卡),不停地跑 Agent 轨迹,每完成一条就立刻塞进队列。生成速度由 staleness 参数节流。 |
| MessageQueue | 缓冲区 | 一个 Ray Actor,内部就是 deque + asyncio.Condition 的生产者-消费者队列。 |
| Trainer | 消费者 | 独占另一组 GPU,从队列里逐条取样本,攒够 require_batches × ppo_mini_batch_size 条就训一步。 |
| ParameterSynchronizer | 搬运工 | 基于 NCCL(参考 checkpoint-engine),把 Trainer 的新权重高效广播给 Rollouter。 |
dequeasyncio.Condition 就是配套的「队列空了先睡、来货了叫醒」的通知机制。不是一个 batch,是单条轨迹。这一点是整个流式架构的地基——只有粒度足够细,Rollouter 才不用等齐一整批,Trainer 也才能边攒边算。
先花 30 秒把「怎么看」搞清楚,后面四张图就都能秒懂了。

mini-batch 方块 = 一次本地参数更新。wait first batch rollout finish、wait last batch train finish、wait active task finish——这些红框标的都是气泡(bubble),也就是这个模式无法消除的空转。看图时最该盯的就是红框:红框越少,流水线越满。
四种模式由三个开关组合而成,先把开关摆出来:
| 模式 | trigger_parameter_sync_step | staleness_threshold | partial_rollout | 一句话 |
|---|---|---|---|---|
| (a) On Policy | = 1 | 0 | — | 训一步同步一次,最保守 |
| (b) Stream Off Policy | > 1 | 0 | — | 训多步才同步一次 |
| (c) Async (stale) | ≥ 1 | > 0 | False | 允许旧样本,但同步时要等在跑的样本跑完 |
| (d) Async (partial) | ≥ 1 | > 0 | True | 允许旧样本,同步时直接把在跑的样本切断、之后续写 |

这是严格 on-policy:训练用的每一条样本,都是当前这版权重刚生成出来的。算法最干净,PPO 的重要性采样比值天然接近 1,不需要任何修正。但代价是流水线几乎没有重叠——图上黄线之间的空白就是纯浪费。
π_new / π_old(新旧策略给这个 token 的概率之比),把「旧分布下采到的样本」换算到新分布。比值越接近 1,说明新旧策略差别越小、修正越轻——这就是正文说 (a) 「比值天然接近 1」的意思。require_batches × ppo_mini_batch_size 条就做一次本地更新——图上 Trainer 泳道的一个绿方块就是一个 mini-batch。
wait first batch rollout finish —— 窗口开头,Trainer 得干等第一批样本攒够。wait last batch train finish —— 窗口结尾,Rollouter 已经把额度用完停下了,得等 Trainer 训完最后一步才能同步。trigger_parameter_sync_step > 1 的意思是:Trainer 本地更新 N 次,才和 Rollouter 同步一次权重。同步是有固定开销的(abort、释放 KV cache、建 NCCL 通信组、广播、唤醒),少同步几次自然更省。
但注意:staleness 仍然是 0。也就是说 Rollouter 在一个窗口内只被允许生成固定数量的样本,生成完就得停下等同步。所以图上窗口的头和尾各有一个气泡——这正是 (c) 要解决的问题。
因为一个窗口里训了 3 步。第 2、3 步用的样本,是窗口开始时那版权重生成的,而此时 Trainer 的参数已经更新过 1~2 次了。样本和参数已经对不齐——这就是 off-policy 的来源,只不过偏差还比较可控。

wait active task finish。staleness_threshold > 0:允许 Rollouter「超额生产」,多出来的样本留给下一个窗口用。关键洞察是:Rollouter 停机的根本原因,是「本窗口配额用完了」。那就把配额放宽——允许它提前生成下一窗口要用的样本。这些样本进入队列时是新鲜的(灰色),但被 Trainer 取走时权重已经换了一版,于是变成陈旧样本(粉色)。
厨师照着上一版菜谱已经把菜炒好了,端上桌时菜谱刚好更新了。菜还是能吃的,只是不完全符合新标准。staleness_threshold 就是在规定「一桌菜里,最多允许多大比例是照旧菜谱做的」。
这就是全异步真正开始赚钱的地方:Rollouter 的空转基本被填平了。代价是训练数据里混入了 off-policy 成分,需要在算法侧留心(见 §06 的 bypass_mode 与 Rollout IS)。

partial_rollout=True:同步权重时不再等待,直接 abort 所有在途请求;同步完成后 resume,从中断点接着生成。(c) 里最后那个气泡是「等长尾样本收尾」。而一条 SWE 轨迹可能要跑几十分钟,为它等待代价太大。Partial Rollout 的答案很直接:不等了,切断它。
你在写一篇长文,写到一半编辑部换了写作规范。你不用把整篇按旧规范写完再重写,而是停笔、换上新规范、从当前这句接着往下写。文章前半段是旧规范、后半段是新规范——这就是那个「半灰半粉」的格子。
实现上有两个漂亮的细节:
FullyAsyncLLMServerManager.generate() 内部:它检测到 stop_reason == "aborted" 就带着已生成的 token 再发一次请求,直到正常结束。Agent Loop 那一层从头到尾只看见一次 generate() 调用。request_id,负载均衡器的粘性会话会把它路由回同一个 vLLM replica,从而复用 Automatic Prefix Caching(APC)里已有的 KV cache,续写的 prefill 开销大幅降低。request_id轨迹会记录 min_global_steps / max_global_steps,标明它横跨了哪几个权重版本,供算法侧做修正。
| Rollouter 空转 | Trainer 空转 | 样本新鲜度 | 适合什么场景 | |
|---|---|---|---|---|
| (a) | 大 | 大 | 100% 新鲜 | 算法验证、小规模、对 off-policy 极敏感的任务 |
| (b) | 中(首尾各一个气泡) | 中 | 100% 新鲜 | 同步开销显著、rollout 耗时较均匀 |
| (c) | 小(只剩同步点等长尾) | 小 | 混入陈旧样本 | rollout 耗时中等、长尾不算极端 |
| (d) | ≈ 0 | ≈ 0 | 陈旧 + 跨版本轨迹 | 代码 Agent 这类超长尾场景 |
从 (a) 到 (d) 是逐个消灭气泡的过程:(b) 消灭「频繁同步的开销」,(c) 消灭「Rollouter 等配额」,(d) 消灭「同步时等长尾」。而每消灭一个气泡,就多欠算法一点 off-policy 的债——所以 staleness_threshold 建议设成小于 1 的值,别把债欠太狠。
trigger_parameter_sync_step:多久同步一次权重Trainer 做多少次本地更新后,才和 Rollouter 同步一次。两次同步之间,Trainer 一共消费:
trigger_parameter_sync_step × require_batches × ppo_mini_batch_size 条样本
想和 colocate 模式做公平的速度对比,就把它设成:
trigger_parameter_sync_step = data.train_batch_size / (require_batches × ppo_mini_batch_size)
staleness_threshold:允许多少「不新鲜」它是允许使用的过期样本最大比例。两次权重同步之间,Rollouter 最多生成:
staleness_threshold = 0(同步训练):
rollout_num = trigger_parameter_sync_step × require_batches × ppo_mini_batch_size
staleness_threshold > 0(异步训练):
rollout_num = (1 + staleness_threshold)
× (trigger_parameter_sync_step × require_batches × ppo_mini_batch_size)
− num_staleness_sample
其中 num_staleness_sample 是上一轮多生产、结转过来的样本数——所以这是个自动结算的额度机制:上轮超产多少,这轮就少产多少,总量守恒。
staleness_threshold = 1 基本等价于 one-step-off 策略。rollout_num——此时 staleness 是「用不上」的。partial_rollout:只在 staleness > 0 时才真正生效这一点容易踩坑:staleness_threshold = 0 时打开 partial_rollout 是没有意义的——因为窗口内本来就不允许有跨版本样本存在。
require_batches:一次训练喂多少理论上流式训练该设成 1(攒够一个 ppo_mini_batch_size 就训)。但实测发现:一次分发的样本太少,会因为数据分发的顺序问题导致训练不稳定、response 长度变长。require_batches 就是为此提供的一个调节量,用来控制每次真正参与训练的样本数。
use_rollout_log_probs 与 bypass_mode:一个容易被忽略的正确性问题PPO/GRPO/DAPO 计算重要性采样时,old_log_prob 必须和「生成这些 token 时所用的那一版参数」对应。在全异步模式下,样本可能是好几个版本以前生成的——如果让 Trainer 用当前权重去重算 log_prob,比值就错了。
bypass_mode = True(默认)
old_log_probs = rollout 侧返回的 log_probs ← 零额外计算,天然版本对齐
bypass_mode = False
old_log_probs = 用训练引擎(FSDP/Megatron)重新计算
同时启用 Rollout Importance Sampling 修正
在模式 (d) 下,这近似于 AReaL 的 Decoupled PPO
什么时候关掉 bypass?文档给的经验是:训练后期指标和 response 长度开始不稳定时,改用训练引擎算 log_prob 并叠加 Rollout IS 修正,能缓解这个问题。
old_log_probstaleness_threshold = 1 且 rollout 足够快时,系统会自然收敛到这个稳态。bypass_mode=False 走的就是这条路。从 Rollouter 拿到一条任务,到它在 Harbor 沙箱里跑完,中间要穿过六层并发限制。以 fully_async_2nodes.sh 的配置为例(rollout 8 GPU、gen_tp=4、num_workers=128、ppo_mini_batch_size=64、staleness_threshold=1.0):
max_required_samples = 64 × (1.0 + 1) × 1 = 128 # 一个同步窗口内最多生成
num_replicas = 8 / 4 = 2 # vLLM server 实例数
max_concurrent_samples = min(2 × 32, 128) = 64 # 同时在途的样本数上限
max_queue_size = max_required_samples = 128
asyncio.wait(FIRST_COMPLETED) 等一个完成再提交新的vLLM replica(硬件)→ active_tasks = 64(软件硬限)→ pending_queue = 128(缓冲)→ Worker 池 128(容量)→ K8s 集群资源(基础设施)。
换句话说:想提高并发,先看 GPU 和 K8s 有没有余量,再动 max_concurrent_samples。
asyncio.gather所有 AgentLoopWorker 共享一个全局 Ray Actor 做路由,规则只有两条:
request_id(= 同一条轨迹)的所有轮次,通过 LRU Cache 记录,始终打到同一个 replica。目的是让 vLLM 的 APC 复用前几轮的 KV Cache,省掉大量 prefill。inflight 计数最小的 replica,避免一个忙死一个闲着。请求结束时在 finally 里 fire-and-forget 地 release_server,把计数减回去。Partial Rollout 的续写之所以能复用缓存,正是因为它沿用了同一个 request_id,被粘性会话送回了原来那台 replica。
request_id → replica(上限 10000 条)——所以「命中 LRU」= 这条对话在路由表里还有记录,走粘性路径;「未命中」= 新对话或记录已被淘汰,重新挑一台。VeRL 原本的 ToolAgentLoop 是自己驱动状态机、自己在进程内执行工具的。但 SWE 场景下,Agent(如 OpenHands)跑在 K8s Pod 里,循环是 Harbor 控制的。所以适配的思路是:把一整次 Harbor Trial.run() 包装成 VeRL 的一次 run() 调用。
| ToolAgentLoop | BuiltinSWEAgentLoop | |
|---|---|---|
| 谁控制 Agent 循环 | VeRL(状态机驱动) | Harbor(Trial.run 内部) |
| 工具在哪执行 | VeRL 进程内 | Harbor 沙箱(K8s Pod / Docker) |
| 怎么调模型 | 直接 server_manager.generate() | 经 HTTP Proxy 间接触发 |
| 轨迹 token 哪来 | 状态机自己累积 | Proxy 会话捕获 |
| 怎么算 reward | VeRL RewardModel | Harbor Verifier(真跑测试套件,0/1) |
一次 Trial 内部分四个阶段,每个阶段都有独立计时并上报到 VeRL metrics:env_setup(起 Pod、跑 Dockerfile)→ agent_setup(装 Agent、把 LLM_BASE_URL 指向 Proxy)→ agent_execute(多轮「推理 ↔ 工具」循环)→ verify(跑测试算 reward)。
沙箱里的 Agent 用的是标准 LiteLLM/OpenAI 客户端,它不知道 VeRL 的存在。于是 VeRL 在自己进程里起了一个 aiohttp server,伪装成 OpenAI 兼容的 vLLM 端点,把 LLM_BASE_URL 塞给 Agent。Agent 一行代码都不用改。
Harbor Agent (K8s Pod) Proxy (进程内 aiohttp) VeRL
│ POST /sess/{id}/v1/chat/completions │ │
│ ───────────────────────────────────> │ │
│ ① 归一化 messages │ │
│ ② apply_chat_template │
│ ③ 算出 prompt_ids │ │
│ │ server_manager │
│ │ .generate() ───> │
│ │ <─── TokenOutput │
│ ④ 解析 tool_call(hermes / qwen3_coder)│
│ ⑤ 更新 session 轨迹状态 │
│ <──── OpenAI 格式 JSON ───────────── │ │
它做的事远不止转发——它是轨迹 token 的唯一采集点。因为 Agent 在沙箱里,VeRL 看不到对话历史,只能在 Proxy 这一层把每一轮的 token、mask、logprob 攒起来。
POST /v1/chat/completions,收 messages、返回 choices)的 HTTP 服务。它是 LLM 服务的事实标准——任何 OpenAI SDK / LiteLLM 客户端都能直连。vLLM 本身就长这样,所以 Proxy「装成 vLLM」毫无违和感。Trainer 需要精确知道每个 token 的身份(哪些是模型生成的、哪些是工具返回的)。Proxy 是增量累积的——每轮只 tokenize 新增的那几条消息。但这个增量结果,必须和「把完整对话一次性 apply_chat_template」的结果逐 token 完全一致,否则训练数据就是错位的。
把 10 样东西一件一件称重再加起来,必须和一次性称这 10 样东西的读数一模一样。听起来是废话,但 chat template 里有各种分隔符、系统提示、生成前缀,稍不留神就多一个或少一个 token。
Proxy 用三招守住这个不变式:
prompt_ids;之后每轮只对新增的 delta 消息 apply_chat_template(remove_system_prompt=True),追加进 traj_acc_ids。模型生成的 token 打 mask=1,工具/observation 打 mask=0。<|im_end|> 后面那个换行)。Proxy 预先算好这个「尾巴」,每次以 EOS 结束后手动补上。这一个换行符如果丢了,后面所有 token 就全错位了。此外 Proxy 还负责把 log_probs(作为 rollout_log_probs)、response_mask、routed_experts(MoE 的 Router Replay 用)一路传回 Trainer;并支持 max_consecutive_no_tool 早停,防止 Agent 陷入「只聊天不干活」的死循环。
<|im_start|> 之类的角色分隔符、工具描述、生成前缀。每家模型的模板都不一样,token 错位的坑大多埋在这里。response_masktool_calls 字段。routed_expertsrouted_experts 记录了生成时每个 token 实际走了哪些专家,训练时按记录回放(Router Replay),保证训推一致。FullyAsyncTrainer.fit() 是个无限循环,每轮 fit_step() 十个动作:
1. _fit_generate 从 MessageQueue 逐条取样本,攒够 required_samples 组装成 batch
2. _fit_compute_reward reward 评分(Harbor 侧其实已经算好了)
3. _fit_compute_log_prob old_log_prob(bypass_mode 决定是直接用还是重算)
4. _fit_compute_ref_log_prob Reference Policy 的 log prob
5. _fit_compute_critic Critic 估值
6. _fit_compute_advantage GAE 优势计算
7. _fit_update_critic 更新 Critic
8. _fit_update_actor PPO / GRPO / DAPO loss 更新 Actor
9. _fit_update_local_step local_trigger_step += 1,到达阈值则复位
10. _fit_update_weights 若 local_trigger_step == 1:
→ checkpoint_manager.update_weights() 同步权重
→ rollouter.reset_staleness() 重置陈旧度基准
第 9、10 步就是「多步本地训练 + 一次参数同步」的实现:计数器从 1 数到 trigger_parameter_sync_step,然后复位并把 current_param_version 加一。
CheckpointEngineManager.update_weights() 的八个步骤:
| # | 动作 | 为什么 |
|---|---|---|
| 1 | abort_all_requests() | 切断所有在途推理,返回 stop_reason="aborted"。这是 partial rollout 的起点 |
| 2 | 构建临时 RayWorkerGroup | 把所有 rollout replica 的 worker 收拢起来 |
| 3 | sleep replicas(可选) | 释放 vLLM 的 KV-cache 显存,给 NCCL 传输腾地方 |
| 4 | 建 NCCL 通信组 | Trainer rank 0 + 全部 rollout worker |
| 5 | 权重传输 | Trainer 侧 pack 进 CuPy 双缓冲后 NCCL Broadcast;元数据走 ZeroMQ PUB/SUB 并行发送,与 NCCL 传输重叠 |
| 6 | Finalize | 释放 NCCL bucket、销毁通信组 |
| 7 | wake up replicas | 恢复 KV-cache,新权重上 GPU |
| 8 | resume_generation() | 被中断的样本从断点继续生成——这就是 (d) 里那些橙色虚线条 |
class FullyAsyncLLMServerManager:
async def generate(self, ...):
final_output = TokenOutput(token_ids=[], ...)
while True:
# 每次都把 prompt + 已生成的 token 一起传进去
output = await super().generate(
prompt_ids=prompt_ids + final_output.token_ids, ...
)
final_output.token_ids.extend(output.token_ids)
final_output.log_probs.extend(output.log_probs)
if output.stop_reason in ("aborted", "abort") and partial_rollout:
continue # 被权重同步打断了 → 接着写
else:
break # 正常结束
return final_output
上层的 Agent Loop 完全感知不到这中间发生了权重切换——它只看到一次普通的 generate()。
async def reset_staleness(self):
async with self.lock:
# 在途任务 + 队列里等待的样本,都算作新版本下的样本
self.staleness_samples = len(self.active_tasks) + self.pending_queue.qsize()
self.paused = False
self._resume_event.set()
逻辑是:staleness_samples 要反映「当前参数版本之后已经生成了多少」。同步完成的那一刻,手上那些还没交付的样本,从记账角度就归到新版本名下——下一个窗口的生成配额会自动扣掉它们。这就是 §06 里那个「总量守恒」的实现。
| 术语 | 解释 |
|---|---|
| Colocate | 训推一体:Rollout 和 Training 共享同一组 GPU,只能轮流执行 |
| Fully Async | 全异步训推分离:Rollouter 和 Trainer 各占一组 GPU、完全解耦,靠队列流式对接 |
| Rollouter | 样本生成器:持续异步产出训练样本,每完成一条立刻入队 |
| MessageQueue | 基于 Ray Actor 的异步队列,以「单条样本」为粒度连接 Rollouter 和 Trainer |
| Rollout / 轨迹 | 让当前模型实际跑一遍任务产出的完整交互记录(多轮对话 + 工具调用),即一条训练样本 |
| Staleness(陈旧度) | 样本生成时的参数版本与当前训练参数版本的差距;staleness_threshold 限制陈旧样本的最大比例 |
| Partial Rollout | 中断恢复:同步权重时打断进行中的生成,同步后带着已生成的 token 从断点续写 |
| on-policy / off-policy | 训练样本是否由当前这版策略自己生成;不是,就是 off-policy,需要重要性采样等修正 |
| 重要性采样(IS) | 用概率比值 π_new/π_old 把旧策略采到的样本换算到新策略分布下的修正方法 |
| MIS | Multiple Importance Sampling,多步重要性采样:样本横跨多个参数版本时保证 π_old 一致性 |
| one-step-off | 训练样本恰好、且始终落后一个参数版本的稳态异步方案 |
| bypass_mode | 直接用 rollout 侧返回的 log_probs 当 old_log_probs——零额外计算且天然版本对齐 |
| 气泡(bubble) | 流水线里无法避免的 GPU 空转时段,即四宫格图里的红框 |
| 术语 | 解释 |
|---|---|
| vLLM | 主流的高吞吐 LLM 推理引擎,对外提供 OpenAI 兼容的 HTTP 端点 |
| replica(副本) | 一个完整、可独立服务的 vLLM 实例;dp2 tp4 = 2 个副本 × 每副本 4 卡张量并行 |
| KV Cache | 推理时缓存的每个已处理 token 的 Key/Value 向量,生成新 token 时免去重算前文 |
| prefill | 生成前把整段 prompt 过一遍模型、填好 KV Cache 的阶段,prompt 越长越贵 |
| APC | vLLM 的 Automatic Prefix Caching:相同前缀的请求直接复用已有 KV Cache,多轮对话复用的关键 |
| LRU | Least Recently Used(最近最少使用):缓存满时先淘汰最久没被访问的条目 |
| LRU Cache | 按 LRU 策略封顶的字典;LoadBalancer 用它存 request_id → replica 路由表(上限 10000) |
| 粘性会话 | Sticky Session:同一会话的所有请求固定路由到同一台服务器,为的是复用 KV Cache |
| 在途请求(inflight) | 已发出、尚未返回的请求数,衡量一台 replica 忙闲的实时指标 |
| request_id | 一条轨迹(整段多轮对话)的唯一标识,粘性路由与断点续写都以它为准 |
| 术语 | 解释 |
|---|---|
| Proxy | 进程内 aiohttp HTTP Server,伪装成 vLLM 端点,把 Harbor 的 OpenAI 请求转成 VeRL 的 server_manager 调用,并顺路采集轨迹 token |
| AgentLoop | VeRL 中封装「生成单条轨迹」逻辑的抽象单元 |
| OpenAI 兼容端点 | 实现 OpenAI API 格式(/v1/chat/completions)的 HTTP 服务,LLM 服务的事实标准接口 |
| LiteLLM | 统一各家 LLM API 的 Python 客户端库,Agent 框架对接模型服务的常用选择 |
| aiohttp | Python 异步 HTTP 库,与 VeRL 共用一条 asyncio 事件循环,高并发请求互不堵塞 |
| chat template | 把 messages 列表拼成模型实际输入文本的模板(角色分隔符、工具描述、生成前缀) |
| EOS | end-of-sequence,模型表示「说完了」的特殊 token;EOS 之后的模板尾巴需要 Proxy 手动回填 |
| response_mask | 0/1 序列:1 = 模型生成的 token(算 loss),0 = 工具返回等外部 token(不算) |
| Token-in-Token-out | 增量 tokenization 与一次性 tokenization 的结果必须逐 token 一致的不变式 |
| Router Replay | MoE 路由决策的复现:记录生成时每个 token 走过的专家(routed_experts),训练时按记录回放 |
| 术语 | 解释 |
|---|---|
| Ray / Ray Actor | Ray 是分布式计算框架;Actor 是常驻的远程服务对象,全集群可远程调用、共享其状态 |
| asyncio / 协程 | Python 单线程异步并发:大量任务在等 IO 时互相让出 CPU |
| NCCL | NVIDIA 的 GPU 集合通信库,多卡/多机显存间直接高速传输,权重同步的标配 |
| Broadcast | 集合通信操作:一个节点把同一份数据发给通信组内所有其他节点 |
| checkpoint-engine | 基于 NCCL 的高效分布式参数同步引擎(本架构权重同步的参考实现) |
| CuPy | GPU 版 numpy;权重同步时用它做双缓冲打包,让打包与传输重叠 |
| ZeroMQ | 轻量消息库;PUB/SUB 模式用于并行发送权重元数据,与 NCCL 大块传输互不干扰 |
| fire-and-forget | 调用发出后不等待结果返回,适合无关紧要的收尾操作 |
| FSDP / Megatron | 两种主流分布式训练引擎,即「训练侧」的模型载体,与推理侧的 vLLM 相对 |
如果只带走一句话:四宫格画的是同一条流水线在四种「松紧度」下的样子,红框标的就是浪费掉的 GPU 时间,而 (d) 是唯一一张没有红框的图。代价写在队列那一行——越往后,粉色和半粉的格子越多,off-policy 的债就越重。staleness_threshold 是你在吞吐和精度之间的那个刻度盘。