在标准 RL 训练循环中,生成(generation)和训练(training)顺序执行:策略模型先生成 rollout 样本,然后在这些样本上进行训练,往复循环。在生成阶段,训练侧处于空闲状态,反之亦然。
所谓 one-off pipelining 方法将生成阶段和训练阶段拆分为两个并行的协程,使模型能够一边生成新样本,一边对先前生成的数据进行训练,带来更高的 GPU 利用率和更大的训练吞吐量。
然而,这种重叠引入了一个复杂性:推理引擎必须在运行过程中更新权重,而此时可能仍有正在处理的请求。
Pause and Resume API
为了在推理引擎运行时安全地更新权重,vLLM 提供了 pause_generation 和 resume_generation 方法。这让训练侧能够协调出一个干净的窗口来进行权重同步,而不会丢失正在进行的生成任务。
pause_generation
1 | await engine.pause_generation(mode="keep", clear_cache=True) |
mode 参数控制如何处理正在进行的请求:
| Mode | Behavior |
|---|---|
"abort" |
立即中止所有正在进行的请求并返回部分结果(默认) |
"wait" |
等待所有正在进行的请求完成后再暂停 |
"keep" |
冻结队列中的请求;调用resume_generation 后它们会恢复执行 |
clear_cache 参数控制暂停后是否清空 KV 缓存和前缀缓存等。
resume_generation
1 | await engine.resume_generation() |
暂停后恢复调度器,通过 mode="keep" 冻结的请求将继续生成。
HTTP Endpoints
使用 vLLM HTTP server时,可以通过以下 endpoint 调用相同的功能:
POST /pause?mode=keep- 暂停生成POST /resume- 恢复生成
注意:数据并行
当使用 vLLM 的内部负载均衡器(即
data_parallel_backend="ray")进行数据并行时,pause 和 resume 会在所有 DP rank 上自动处理——一次调用即可。当使用外部负载均衡器(即代理后的多个独立 vLLM 实例)时,必须在权重更新前后分别向每个引擎实例发送 pause 和 resume 请求。
典型异步RL流程
典型的带权重同步的异步 RL 循环如下:
- 从当前策略开始生成 rollout 样本
- 当训练器有新权重需要更新时,使用
mode="keep"暂停生成 - 将更新后的权重从训练器同步到推理引擎(参见 Weight Transfer)
- 恢复生成——正在进行的请求将使用新权重继续执行
- 重复此过程
关键在于,使用 mode="keep" 暂停的请求,在暂停前产生的 token 来自旧权重,而在恢复后产生的 token 则来自新权重。clear_cache 参数控制在暂停期间是否使 KV 缓存失效:
- 当
clear_cache=True时,之前缓存的 key-value 条目将被丢弃,因此恢复后生成的所有 token 都将完全使用新权重计算。 - 当
clear_cache=False时,现有的 KV 缓存条目将被保留,这意味着上下文中的某些 token 可能仍然反映旧权重的状态(即过期的 KV 缓存)。
代码实现
整体架构
1 | HTTP API (api_router.py) |
核心数据结构
PauseState 状态机 - 调度器维护的一个三态枚举:
| 状态 | 含义 |
|---|---|
UNPAUSED |
正常调度 |
PAUSED_NEW |
不调度新请求,已在 running 的请求继续执行(用于abort/wait 模式) |
PAUSED_ALL |
完全冻结,token_budget 强制为 0(用于keep 模式) |
pause_generation
sequenceDiagram
participant Caller
participant AsyncLLM
participant ECP as EngineCoreProc
participant SC as Scheduler
Caller->>AsyncLLM: await pause_generation(mode, clear_cache)
AsyncLLM->>ECP: pause_scheduler(mode, clear_cache)
alt mode == abort
ECP->>SC: finish_requests(None, FINISHED_ABORTED)
Note over SC: running reqs aborted immediately
ECP->>SC: set_pause_state(PAUSED_NEW)
else mode == wait
ECP->>SC: set_pause_state(PAUSED_NEW)
Note over SC: PAUSED_NEW - waiting queue blocked,
running queue still scheduled
else mode == keep
ECP->>SC: set_pause_state(PAUSED_ALL)
Note over SC: PAUSED_ALL - token_budget=0,
nothing scheduled
end
rect rgb(245,245,245)
Note over SC: Inside schedule() - effect of pause_state
alt pause_state == PAUSED_ALL
SC->>SC: token_budget = 0, skip running and waiting loops
else pause_state == PAUSED_NEW
SC->>SC: running loop executes, waiting loop skipped
else pause_state == UNPAUSED
SC->>SC: both loops execute normally
end
end
ECP->>ECP: _pause_complete() = not has_work()
alt complete immediately
ECP->>ECP: _reset_caches() if clear_cache
ECP-->>AsyncLLM: Future resolved
else pending work
ECP->>ECP: register idle_state_callback, keep stepping
ECP-->>AsyncLLM: Future resolved once idle
end
AsyncLLM-->>Caller: return
pause_generation(mode=”keep”, clear_cache=True) 从用户 API 到调度器状态机的调用链路:
1 | AsyncLLM.pause_generation() |
EngineCoreProc.pause_scheduler() pause mode 对应行为:
| Mode | Behavior |
|---|---|
abort |
立即调用finish_requests(None, FINISHED_ABORTED) 中止所有请求,发送 abort 输出,设置 PAUSED_NEW |
wait |
设置PAUSED_NEW(不中止请求),等待 engine 自然 idle |
keep |
设置PAUSED_ALL(冻结所有调度),等待 output queue 排空 |
engine_idle_callback 的注册与触发
1 | run_busy_loop() |
resume_generation
sequenceDiagram
participant Caller as Trainer/RL Loop
participant AsyncLLM
participant Client as EngineCoreClient
participant Core as EngineCore Proc
participant Sched as Scheduler
participant MR as ModelRunner V1/V2
Caller->>AsyncLLM: resume_generation()
AsyncLLM->>Client: engine_core.resume_scheduler_async()
Client->>Core: resume_scheduler()
alt DP mode DPEngineCoreProc
Core->>Core: check pending_pause / ignore_start_dp_wave
Core->>Sched: set_pause_state(UNPAUSED)
Core->>Core: ignore_start_dp_wave = False
Note over Core: Barrier - all-reduce across DP ranks
to confirm all have resumed
Core->>Sched: has_unfinished_requests()
Core->>Core: engines_running = True if global unfinished
else single engine
Core->>Sched: set_pause_state(UNPAUSED)
end
Note over Sched: token_budget restored to max_num_scheduled_tokens
Core->>Sched: schedule() next step loop
alt requests were PAUSED_NEW abort/wait
Note over Sched: running reqs never dequeued, continue normally.
Queued adds now flushed in.
else requests were PAUSED_ALL mode=keep
Note over Sched: frozen running/waiting reqs now eligible,
previously-running reqs treated as resumed
Sched->>Sched: _make_cached_request_data(resumed_reqs=[...])
alt use_v2_model_runner == False V1
Sched-->>MR: CachedRequestData - resumed_req_ids + all_token_ids full history
MR->>MR: _update_states(scheduler_output)
Note over MR: V1 reconstructs full sequence state
from all_token_ids in cached data
else use_v2_model_runner == True V2
Sched-->>MR: NewRequestData - prefill_token_ids carries full token ids
Note over MR: V2 resumed request emitted as a new request,
all_token_ids skipped in cached_reqs
end
end
Sched-->>Core: SchedulerOutput
Core-->>Client: step results / outputs
Client-->>AsyncLLM: await completion
AsyncLLM-->>Caller: return
resume_generation 调用链路:
1 | AsyncLLM.resume_generation() |
只需将调度器状态切回 UNPAUSED。下一次 schedule() 调用时:
token_budget恢复正常计算waiting队列中的请求重新被调度- 之前被
mode="keep"冻结的请求继续执行
DP 场景的 resume 需要额外的同步:
- 确保
pending_pause已清除、ignore_start_dp_wave已重置 - 调用父类的
resume_scheduler()将状态设为UNPAUSED - 通过 all-reduce barrier 等待所有 DP rank 都完成 resume
- 如果全局有未完成的请求,启动 step 循环
vllm e2e用例设计
https://github.com/vllm-project/vllm/pull/52144 已合入
1、is_paused状态与幂等性:重复pause/resume,多次循环
2、invalid mode行为 - return status code 状态不变
3、keep/wait/abort对in-flight请求和新请求的行为,及resume之后的行为
4、clear_cache =true/false的行为 - golden 输出比对
1 | 优先级 用例 主要覆盖 |
1. 状态与幂等性 test_pause_resume_state_is_idempotent_across_cycles
1 | 初始 is_paused == false |
2. 非法 mode test_invalid_pause_mode_preserves_state
1 | 覆盖两个起始状态: |
3. 三种 mode 的请求生命周期
用一个参数化测试:
1 |
|
预期矩阵如下:
1 | mode pause 返回时 in-flight 请求 pause 后提交的新请求 resume 后 |
测试实现仍沿用 threading 风格,但做几点调整:
- in-flight 请求使用 streaming completion,并用“收到第一个 token”的 Event 确认它确实开始执行,替代固定的 sleep(0.3)。
- 新请求不使用短 timeout 后断言 not ok。这会混淆“正确排队”和“请求失败”。应保持线程连接,确认 pause 期间线程未结束,resume 后再 join 并断言 ok。
- 对 keep,可以记录 pause 返回时的 chunk 数,短暂等待后确认 chunk 数未增加,从而验证“冻结”而不仅是“尚未完成”。
4. clear_cache 与 golden 输出 test_clear_cache_preserves_output_and_controls_prefix_cache
服务增加:
1 | --enable-prefix-caching |
1 | 使用一个明显长于 cache block 的固定 prompt,temperature=0: |
速度与稳定性
使用 class/module scoped 的 server_url fixture,一次启动服务供所有测试复用,并在每个测试前后调用一次 resume() 恢复状态。cache 测试自行在开始时执行 clear_cache=True,避免共享服务造成缓存污染。
最终成本大致是:
- 1 次模型服务启动。
- 3 个 mode 各一个短 in-flight 请求。
- 4 次短 golden 请求。
- 状态和非法参数测试几乎没有额外推理成本。
不建议在这一层增加并发 pause/resume race、多请求批量、几十轮循环或 3 modes × 2 clear_cache 全组合;abort/wait/keep 和多请求行为在底层测试中已经有较完整覆盖,HTTP E2E保留上述关键映射即可。
RL e2e用例设计
examples/rl/rlhf_async_new_apis.py 展示了在异步 RL 训练中如何通过 pause/resume + weight transfer 在推理运行期间切换权重。
sequenceDiagram
participant D as Driver
participant TM as TrainModel (Ray actor)
participant LLM as MyLLM (AsyncLLM)
Note over D,LLM: Setup: init train model + inference engine on separate GPUs
D->>TM: TrainModel.remote(MODEL_NAME_V2)
D->>LLM: MyLLM.remote(**llm_kwargs)
D->>TM: get_master_address_and_port()
D->>LLM: init_weight_transfer_engine(NCCL init info)
D->>TM: init_weight_transfer_group(world_size)
Note over TM,LLM: NCCL process group established
rect rgb(245,245,245)
Note over D,LLM: Phase 1: concurrent generation with weight sync
loop for each prompt
D->>LLM: do_generate.remote(prompt_ids, sampling_params)
Note over LLM: streams tokens, sets
_request_pause_flag at threshold
end
D->>LLM: pause_after_n_tokens()
LLM->>LLM: await pause_generation(mode="keep")
Note over LLM: sleep(5), _generation_paused = True
D->>LLM: start_weight_update(is_checkpoint_format=True)
D->>LLM: update_weights(NCCLWeightTransferUpdateRequest)
D->>TM: broadcast_weights(packed=True)
TM-->>LLM: weights sent over NCCL
D->>LLM: finish_weight_update()
D->>LLM: resume_generation()
LLM-->>D: gen_futures resolve (output, pause_token_index)
end
Note over D,LLM: Phase 2: validation
D->>LLM: shutdown() / ray.kill(llm, train_model)
D->>LLM: fresh MyLLM.remote(MODEL_NAME_V2)
loop for each result
D->>LLM: do_generate(prompt+prefix tokens, max_tokens=n_after)
end
LLM-->>D: val_results
Note over D: compare post-swap tokens vs
fresh V2 output, assert pass_rate
概述:Trainer 占一个 GPU,vLLM 引擎占另一个 GPU,通过 NCCL 通道在 GPU 间直接传输权重。开启 batch_invariant 保证输出确定性,用于后续验证。
Phase 1: 并发生成 + 权重切换。核心流程:触发生成 → 暂停 → 切换权重 → 恢复
Phase 2: 正确性验证。
1 | 验证逻辑: |
model runner v2方案
V1 vs V2 的核心差异:被抢占/恢复请求的处理方式
flowchart TD
A["resume_scheduler() -> pause_state = UNPAUSED"] --> B["scheduler.schedule() next step"]
B --> C["RUNNING loop finds frozen/preempted request
status == PREEMPTED or WAITING"]
C --> D{"request.status == PREEMPTED?"}
D -->|"yes"| E["scheduled_resumed_reqs.append(request)"]
D -->|"no (WAITING, i.e. fresh)"| F["scheduled_new_reqs.append(request)"]
E --> G{"use_v2_model_runner?"}
F --> H["always in scheduled_new_reqs
(NewRequestData)"]
G -->|"V1: False"| I["stays in scheduled_resumed_reqs
-> _make_cached_request_data()
-> CachedRequestData
resumed_req_ids += req_id
all_token_ids[req_id] = full history"]
G -->|"V2: True"| J["merged into scheduled_new_reqs
-> NewRequestData.from_request(req, blocks, req._all_token_ids)
prefill_token_ids carries full history
all_token_ids NOT populated"]
I --> K["GPUModelRunner._update_states:
req_data = scheduled_cached_reqs
resumed_from_preemption = req_id in resumed_req_ids
req_index is None -> reqs_to_add
output_token_ids recovered from all_token_ids"]
J --> L["GPUModelRunner treats it like a brand-new request:
uses prefill_token_ids directly,
no need to reconstruct output_token_ids from all_token_ids"]
V1 Model Runner:resumed 请求走 CachedRequestData 路径
- 被恢复的请求放入
scheduled_cached_reqs.resumed_req_ids - 完整的 token 历史(prompt + 已生成 token)通过
all_token_ids字段传递 - 调度器需要维护
prev_step_scheduled_req_ids来判断哪些请求需要传递all_token_ids - Model runner 在
_update_states中处理 resumed 请求时,需要从all_token_ids恢复output_token_ids
V2 Model Runner:resumed 请求走 NewRequestData 路径
scheduled_resumed_reqs被合并进scheduled_new_reqs- 完整 token 历史通过
NewRequestData.prefill_token_ids传递(v2 专用字段) - V2 的
GPUModelRunner.add_requests()统一处理所有请求(包括恢复的请求),使用prefill_token_ids作为完整的 token 上下文 CachedRequestData.all_token_ids始终为空 ,不需要prev_step_scheduled_req_ids追踪
MRV2 设计原则:“Treat preemption as completion. On resume, re-add request data as fresh state.”
| 维度 | V1 Model Runner | V2 Model Runner |
|---|---|---|
| resumed 请求的调度输出位置 | scheduled_cached_reqs (resumed_req_ids) |
scheduled_new_reqs (prefill_token_ids) |
| token 历史传递方式 | CachedRequestData.all_token_ids |
NewRequestData.prefill_token_ids |
prev_step_scheduled_req_ids |
需要维护 | 不需要 |
| model runner 处理方式 | 特殊的 resumed 路径,需恢复 output_token_ids | 与新请求完全相同的路径,persistent batch 重新初始化 |
| 复杂度 | 需要区分 running/resumed/new 三种状态 | resumed 等同于 new,逻辑更简洁 |
异步 RL 的 pause/resume 功能直接复用了 vllm 的代码,vllm-ascend 没有自己实现生成层面的 pause/resume 逻辑,而是完全复用了上游 vllm 的基础设施。
具体复用方式:
API 层面:RL 示例直接调用上游 API
- rlhf_async_new_apis.py 继承 vllm.AsyncLLMEngine,调用 super().pause_generation(mode=”keep”)
- rlhf_http_hccl.py / rlhf_http_npu_ipc.py 调用 vllm API Server 的 /pause 和 /resume HTTP 端点
调度器层面:自定义调度器直接 import 上游的类和枚举
- PauseState 从 vllm.v1.core.sched.interface 导入
- AsyncScheduler 从 vllm.v1.core.sched.async_scheduler 导入
- AsyncRecomputeScheduler 继承自上游的 AsyncScheduler
- 调度循环中通过 self._pause_state == PauseState.PAUSED_ALL 判断是否暂停
vllm-ascend 源码中没有任何 pause_generation、resume_generation、/pause、/resume 或 PauseState 的定义
vllm-ascend 自己实现的部分(与 RL 相关但不属于 pause/resume)
1 | ┌──────────────┬───────────────────────────────────────────────────────────────────────────────────────────┐ |
RL侧接入方案(以vime为例)
1、RL侧暴露的控制平面接口
| 接口 | 语义 |
|---|---|
pause_generation |
暂停调度,对应 HTTP/pause,但保留现有 KV cache(mode=keep, clear_cache=false) |
flush_cache |
显式清理 prefix cache,为权重替换腾出/校正状态,与 pause 解耦为独立步骤 |
start_weight_update / finish_weight_update |
显式声明”进入/退出权重更新会话”的窗口,允许引擎内部做互斥锁定、缓冲区切换等准备 |
continue_generation |
恢复调度,对应 HTTP/resume |
pause_generation/continue_generation,
1 | def pause_generation(self): |
每个引擎 actor 应仅在”控制节点”(vime 中为 node_rank == 0)真正发出控制请求,其余节点直接 no-op 返回,避免对同一 server 重复请求。
vime 底层 /pause、/resume、/sleep、/wake_up、start_weight_update、finish_weight_update 等具体是 vLLM server 侧暴露的 HTTP 接口,这部分实现在 vLLM 本身。
2、时序图
sequenceDiagram
participant M as "MegatronTrainRayActor (rank0)"
participant V as "VLLMEngine (node_rank0)"
participant S as "vLLM server (/pause /resume)"
M->>V: "pause_generation() (Ray remote)"
alt "node_rank == 0"
V->>S: "POST /pause (mode=keep, clear_cache=false)"
S-->>V: "200 OK"
else "node_rank != 0"
V-->>M: "no-op, return None"
end
M->>V: "flush_cache()"
Note over M: "int4/fp4 pre-process (可选)"
Note over M,V: "dist.barrier(Gloo)"
M->>V: "start_weight_update(is_checkpoint_format=True)"
Note over M,V: "dist.barrier(Gloo)"
alt "NCCL, non-colocated (UpdateWeightFromDistributed)"
loop "For each weight bucket"
M->>M: "all_gather_params_async / convert_to_hf"
M->>V: "NCCL broadcast weights"
end
else "IPC, colocated (UpdateWeightFromTensor)"
loop "For each weight chunk (bounded in-flight)"
M->>M: "build CUDA IPC handles / all_gather_object(Gloo)"
M->>V: "update_weights_from_tensor (IPC ObjectRef)"
end
Note over M: "torch.cuda.ipc_collect()"
end
M->>V: "finish_weight_update()"
Note over M,V: "dist.barrier(Gloo)"
Note over M: "int4/fp4 post-process (可选)"
M->>V: "continue_generation() (Ray remote)"
alt "node_rank == 0"
V->>S: "POST /resume"
S-->>V: "200 OK"
else "node_rank != 0"
V-->>M: "no-op, return None"
end
Note over M,V: "dist.barrier(Gloo)"
pause_generation 向 /pause 端点发送带 mode=keep, clear_cache=false 参数的 POST 请求,暂停生成但保留 KV cache(由 flush_cache 单独控制清理);continue_generation 向 /resume 端点发送无参数 POST 请求以恢复推理。
两个方法都有 if self.node_rank != 0: return 的防护,因为在多节点跨主机部署的引擎中,只有 rank0(持有 vLLM server 控制端点)需要真正发请求,其余 headless worker 直接跳过。
3、model runner v1/v2
vLLM 的 pause/resume 特性,其对 RL 侧暴露的接口(pause_generation / resume_generation)与对应 HTTP 端点(/pause、/resume、/is_paused)在 model runner v2 引入后保持不变。
model runner v2 相较于 v1 的差异仅体现在调度器(Scheduler)内部向 model runner 传递请求数据的格式上,不影响 RL 侧调用方式,因此 RL 侧无需为适配 v2 做额外修改。