异步rl:pause/resume
vLLM文档:https://docs.vllm.ai/en/stable/training/async_rl/
Overview
在标准 RL 训练循环中,生成(generation)和训练(training)是顺序执行的:策略模型先生成 rollout 样本,然后在这些样本上进行训练,如此往复循环。在生成阶段,训练加速器处于空闲状态,反之亦然。
one-off pipelining 方法将生成阶段和训练阶段拆分为两个并行的协程,使模型能够一边生成新样本,一边对先前生成的数据进行训练。这可以带来更高的 GPU 利用率和更大的训练吞吐量。
然而,这种重叠引入了一个复杂性:推理引擎必须在运行过程中更新权重,而此时可能仍有正在处理的请求。
Pause 与 Resume API
为了在推理引擎运行时安全地更新权重,vLLM 提供了 pause_generation 和 resume_generation 方法。这让训练器能够协调出一个干净的窗口来进行权重同步,而不会丢失正在进行的任务。
pause_generation
await engine.pause_generation(mode="keep", clear_cache=True)
mode 参数控制如何处理正在进行的请求:
| Mode |
Behavior |
"abort" |
立即中止所有正在进行的请求并返回部分结果(默认) |
"wait" |
等待所有正在进行的请求完成后再暂停 |
"keep" |
冻结队列中的请求;调用 resume_generation 后它们会恢复执行 |
clear_cache 参数控制暂停后是否清空 KV 缓存和前缀缓存。
resume_generation
await engine.resume_generation()
暂停后恢复调度器。任何通过 mode="keep" 冻结的请求将继续生成。
HTTP 端点
使用 vLLM HTTP 服务器时,可以通过以下端点使用相同的功能:
POST /pause?mode=keep - 暂停生成
POST /resume - 恢复生成
注意:数据并行
当使用 vLLM 的内部负载均衡器(即 data_parallel_backend="ray")进行数据并行时,pause 和 resume 会在所有 DP rank 上自动处理——一次调用即可。当使用外部负载均衡器(即代理后的多个独立 vLLM 实例)时,必须在权重更新前后分别向每个引擎实例发送 pause 和 resume 请求。
典型异步 RL 流程
典型的带权重同步的异步 RL 循环如下:
关键洞察在于:使用 mode="keep" 暂停的请求,在暂停前产生的 token 来自旧权重,而在恢复后产生的 token 则来自新权重。clear_cache 参数控制在暂停期间是否使 KV 缓存失效:
- 当
clear_cache=True 时,之前缓存的 key-value 条目将被丢弃,因此恢复后生成的所有 token 都将完全使用新权重计算。
- 当
clear_cache=False 时,现有的 KV 缓存条目将被保留,这意味着上下文中的某些 token 可能仍然反映旧权重的状态(即过期的 KV 缓存)。
Partial rollout 就是clear_cache=False吗?
源码实现分析
调用链路
HTTP API (api_router.py)
→ AsyncLLM (async_llm.py)
→ EngineCoreClient (core_client.py) [跨进程 ZMQ 通信]
→ EngineCoreProc/DPEngineCoreProc (core.py)
→ Scheduler.set_pause_state() (scheduler.py)
→ Scheduler.schedule() / _process_input_queue() / idle callbacks
核心数据结构
PauseState 枚举 (vllm/v1/core/sched/interface.py:22-32)
class PauseState(enum.IntEnum):
UNPAUSED = 0 # 正常运行
PAUSED_NEW = 1 # 不调度新请求,但已有 running 请求继续执行
PAUSED_ALL = 2 # 不调度任何请求
PauseMode 类型 (vllm/v1/engine/__init__.py:26)
PauseMode = Literal["abort", "wait", "keep"]
pause_generation
pause_generation(mode="keep", clear_cache=True) 逐层实现
第一层:AsyncLLM (vllm/v1/engine/async_llm.py:750-793)
async def pause_generation(self, *, mode="abort", clear_cache=True):
if clear_cache:
await self.renderer.clear_mm_cache_async() # ① 清理多模态缓存
await self.engine_core.pause_scheduler_async(
mode=mode, clear_cache=clear_cache
) # ② 跨进程 RPC
await asyncio.sleep(0.02) # ③ 等待 in-flight 输出的最终返回
三步详解:
- 清理多模态缓存: 如果
clear_cache=True,清空 renderer 中缓存的 image/audio 等多模态编码特征
- 跨进程 RPC: 通过 ZMQ 将
("pause_scheduler", mode, clear_cache) 发送到 EngineCore 进程
- 20ms 延迟: 确保 in-flight 请求的最终输出在
pause_generation() 返回前被 output_handler 取出并推送给调用方。这不是正确性要求,只是为了让调用方的视角更直观(看到所有输出后才返回)
第二层:EngineCoreClient (vllm/v1/engine/core_client.py:1130-1133)
async def pause_scheduler_async(self, mode, clear_cache):
await self.call_utility_async("pause_scheduler", mode, clear_cache)
call_utility_async 将方法名和参数序列化为 EngineCoreRequestType.UTILITY 类型的消息,通过 ZMQ 发送到 EngineCore 进程。如果 engine core 返回了一个 Future,则等待该 Future 完成。
async def call_utility_async(self, method: str, *args) -> Any:
return await self._call_utility_async(method, *args, engine=self.core_engine)
async def _call_utility_async(
self, method: str, *args, engine: EngineIdentity
) -> Any:
call_id = uuid.uuid1().int >> 64
future = asyncio.get_running_loop().create_future()
self.utility_results[call_id] = future
message = (
EngineCoreRequestType.UTILITY.value,
*self.encoder.encode((self.client_index, call_id, method, args)),
)
await self._send_input_message(message, engine, args)
self._ensure_output_queue_task()
return await future
第三层:EngineCoreProc — 核心逻辑 (vllm/v1/engine/core.py:1665-1705)
def pause_scheduler(self, mode, clear_cache):
if mode == "abort":
# 立即终止所有请求,发送 ABORT 完成信号
aborted_reqs = self.scheduler.finish_requests(
None, RequestStatus.FINISHED_ABORTED
)
self._send_abort_outputs(aborted_reqs)
# mode="keep" → PAUSED_ALL(冻结一切)
# mode="abort"/"wait" → PAUSED_NEW(冻结新请求,running 继续)
pause_state = PauseState.PAUSED_ALL if mode == "keep" else PauseState.PAUSED_NEW
self.scheduler.set_pause_state(pause_state)
if self._pause_complete(): # 引擎已空闲 → 同步返回
if clear_cache:
self._reset_caches()
return None # Future 已完成,call_utility_async 直接返回
# 引擎还有工作在进行中 → 异步等待
future = Future()
self._idle_state_callbacks.append(
partial(engine_idle_callback, future=future)
)
return future # call_utility_async 会等待这个 Future
engine_idle_callback 的注册与触发
run_busy_loop()
│
├─ _process_input_queue()
│ │
│ ├─ has_work() → PAUSED_ALL 时返回 False
│ │ │
│ │ └─ 进入 while not has_work() 死循环
│ │ │
│ │ ├─ _notify_idle_state_callbacks() ← engine_idle_callback 在这里被调用
│ │ │ future.set_result(None) ← pause 的 RPC 返回
│ │ │
│ │ └─ input_queue.get(block=True) ← 阻塞等待新消息
│ │ │
│ │ └─ 收到 resume → _handle_client_request()
│ │ → resume_scheduler() → set_pause_state(UNPAUSED)
│ │ → has_work() 重新返回 True,退出内层循环
│ │
│ └─ 退出 _process_input_queue
│
└─ _process_engine_step() ← 恢复正常的调度+推理
当引擎进入空闲循环 (_process_input_queue → has_work() == False) 时触发:
def _process_input_queue(self):
while not self.has_work() and self.is_running():
self._notify_idle_state_callbacks() # ← 触发所有注册的回调
# ... 等待新输入 ...
def _notify_idle_state_callbacks(self):
while self._idle_state_callbacks:
callback = self._idle_state_callbacks.pop()
callback(self) # 调用 engine_idle_callback
def engine_idle_callback(engine, future):
if clear_cache:
engine._reset_caches() # 清空所有缓存
future.set_result(None) # Future 完成 → pause 调用返回
has_work() 的判断逻辑
def has_work(self):
return (
self.engines_running or
self.scheduler.has_requests() or
bool(self.batch_queue)
)
当 PAUSED_ALL 时,scheduler 的 get_num_unfinished_requests() 返回 0,所以 has_requests() 返回 False。如果也没有 batch_queue 工作,has_work() 返回 False,引擎进入空闲循环,触发 idle callbacks。
第四层:Scheduler — 暂停状态如何影响调度
schedule() 方法中的 token_budget 控制 (vllm/v1/core/sched/scheduler.py:409-411):
if self._pause_state == PauseState.PAUSED_ALL:
# 不调度任何请求
token_budget = 0
get_num_unfinished_requests() 对暂停状态的响应 (scheduler.py:2136-2145):
def get_num_unfinished_requests(self):
if self._pause_state == PauseState.PAUSED_ALL:
return 0 # 对外宣称没有未完成的请求 → has_work() 返回 False
if self._pause_state == PauseState.PAUSED_NEW:
return len(self.running) # 只算 running,不算 waiting
return len(self.waiting) + len(self.skipped_waiting) - ... + len(self.running)
schedule() 中新请求的处理 (scheduler.py:629):
# 只有在 UNPAUSED 状态才会从 waiting 队列取新请求
if not preempted_reqs and self._pause_state == PauseState.UNPAUSED:
step_skipped_waiting = create_request_queue(self.policy)
while (self.waiting or self.skipped_waiting) and token_budget > 0:
# ... 调度 waiting 中的请求 ...
这意味着在 PAUSED_NEW 或 PAUSED_ALL 状态下,新添加的请求会被放入 waiting 队列,但不会被调度执行。
_reset_caches() (core.py:706-719)
def _reset_caches(self):
self.reset_prefix_cache(
reset_running_requests=True, reset_connector=True
)
self.reset_mm_cache()
self.reset_encoder_cache()
清空三类缓存:
- Prefix Cache: 前缀匹配缓存(最关键的 KV 缓存)
- MM Cache: 多模态特征缓存
- Encoder Cache: 编码器输出缓存
resume_generation
第一层:AsyncLLM (vllm/v1/engine/async_llm.py:795-797)
async def resume_generation(self):
await self.engine_core.resume_scheduler_async()
第二层:EngineCoreClient (vllm/v1/engine/core_client.py:1135-1136)
async def resume_scheduler_async(self):
await self.call_utility_async("resume_scheduler")
第三层:EngineCore — 同步返回 (vllm/v1/engine/core.py:753-755)
def resume_scheduler(self):
self.scheduler.set_pause_state(PauseState.UNPAUSED)
只需将调度器状态切回 UNPAUSED。下一次 schedule() 调用时:
token_budget 恢复正常计算
waiting 队列中的请求重新被调度
- 之前被
mode="keep" 冻结的请求继续执行
DPEngineCoreProc — Data Parallel 场景 (vllm/v1/engine/core.py:1852-1876)
def resume_scheduler(self):
# 安全检查:确保 pause 已完成
if self.pending_pause or (self.engines_running and self.ignore_start_dp_wave):
raise RuntimeError(
"resume_scheduler called while pause is still in flight..."
)
if self.engines_running:
return # 已经在运行
super().resume_scheduler() # 设为 UNPAUSED
self.ignore_start_dp_wave = False
# Barrier: 通过 all-reduce 等待所有 DP rank 都完成 resume
has_global_unfinished = ParallelConfig.has_unfinished_dp(
self.dp_group, self.scheduler.has_unfinished_requests()
)
if has_global_unfinished:
self.engines_running = True # 启动 step 循环
DP 场景的 resume 需要额外的同步:
- 确保
pending_pause 已清除、ignore_start_dp_wave 已重置
- 调用父类的
resume_scheduler() 将状态设为 UNPAUSED
- 通过 all-reduce barrier 等待所有 DP rank 都完成 resume
- 如果全局有未完成的请求,启动 step 循环
完整时序:mode="keep" 的 pause → resume 流程
调用方 AsyncLLM EngineCore Scheduler
│ │ │ │
│ pause_generation(mode="keep") │ │ │
│───────────────────────────────>│ │ │
│ │ clear_mm_cache_async() │ │
│ │──────────────────────────>│ │
│ │ │ │
│ │ pause_scheduler_async │ │
│ │ ("keep", clear_cache) │ │
│ │──────────────────────────>│ │
│ │ │ set_pause_state(PAUSED_ALL) │
│ │ │────────────────────────────>│
│ │ │ │
│ │ │ [schedule() 中 token_budget=0]│
│ │ │ [get_num_unfinished 返回 0] │
│ │ │ │
│ │ │ [_process_input_queue 空闲循环]│
│ │ │──┐ │
│ │ │ │ _notify_idle_callbacks │
│ │ │<─┘ │
│ │ │ engine_idle_callback: │
│ │ │ _reset_caches() │
│ │ │ future.set_result(None) │
│ │ │ │
│ │ <── RPC 返回 ───────────│ │
│ │ │ │
│ │ await asyncio.sleep(0.02)│ │
│ <──── pause 完成 ─────────────│ │ │
│ │ │ │
│ [权重更新期间] │ │ │
│ │ │ │
│ resume_generation() │ │ │
│───────────────────────────────>│ │ │
│ │ resume_scheduler_async │ │
│ │──────────────────────────>│ │
│ │ │ set_pause_state(UNPAUSED) │
│ │ │────────────────────────────>│
│ │ <── RPC 返回 ────────────│ │
│ <──── resume 完成 ────────────│ │ │
│ │ │ │
│ [下一个 generate() 调用] │ │ [schedule() 恢复正常] │
│ │ │ [waiting 中请求恢复执行] │
mode 对比总结
| 方面 |
mode="abort" |
mode="wait" |
mode="keep" |
| 调度器状态 |
PAUSED_NEW |
PAUSED_NEW |
PAUSED_ALL |
| 正在运行的请求 |
立即终止,发送 ABORT |
继续执行直到完成 |
冻结保留(不终止不执行) |
| 新请求 |
排队但不调度 |
排队但不调度 |
排队但不调度 |
| KV Cache |
空闲后由 callback 清空 |
空闲后由 callback 清空 |
空闲后由 callback 清空 |
| pause 返回时机 |
等 aborted 输出发送完毕 |
等所有 running 请求完成 |
等引擎进入空闲循环 |
| resume 后 |
被 abort 的请求不可恢复 |
所有请求已完成 |
冻结的请求继续执行 |
| 适用场景 |
快速权重切换,丢弃旧结果 |
优雅暂停,保留所有结果 |
异步 RL 训练,保留请求上下文 |
异步rl:pause/resume
Overview
在标准 RL 训练循环中,生成(generation)和训练(training)是顺序执行的:策略模型先生成 rollout 样本,然后在这些样本上进行训练,如此往复循环。在生成阶段,训练加速器处于空闲状态,反之亦然。
one-off pipelining 方法将生成阶段和训练阶段拆分为两个并行的协程,使模型能够一边生成新样本,一边对先前生成的数据进行训练。这可以带来更高的 GPU 利用率和更大的训练吞吐量。
然而,这种重叠引入了一个复杂性:推理引擎必须在运行过程中更新权重,而此时可能仍有正在处理的请求。
Pause 与 Resume API
为了在推理引擎运行时安全地更新权重,vLLM 提供了
pause_generation和resume_generation方法。这让训练器能够协调出一个干净的窗口来进行权重同步,而不会丢失正在进行的任务。pause_generation
mode参数控制如何处理正在进行的请求:"abort""wait""keep"resume_generation后它们会恢复执行clear_cache参数控制暂停后是否清空 KV 缓存和前缀缓存。resume_generation
暂停后恢复调度器。任何通过
mode="keep"冻结的请求将继续生成。HTTP 端点
使用 vLLM HTTP 服务器时,可以通过以下端点使用相同的功能:
POST /pause?mode=keep- 暂停生成POST /resume- 恢复生成典型异步 RL 流程
典型的带权重同步的异步 RL 循环如下:
mode="keep"暂停生成关键洞察在于:使用
mode="keep"暂停的请求,在暂停前产生的 token 来自旧权重,而在恢复后产生的 token 则来自新权重。clear_cache参数控制在暂停期间是否使 KV 缓存失效:clear_cache=True时,之前缓存的 key-value 条目将被丢弃,因此恢复后生成的所有 token 都将完全使用新权重计算。clear_cache=False时,现有的 KV 缓存条目将被保留,这意味着上下文中的某些 token 可能仍然反映旧权重的状态(即过期的 KV 缓存)。Partial rollout 就是clear_cache=False吗?
源码实现分析
调用链路
核心数据结构
PauseState枚举 (vllm/v1/core/sched/interface.py:22-32)PauseMode类型 (vllm/v1/engine/__init__.py:26)pause_generation
pause_generation(mode="keep", clear_cache=True) 逐层实现
第一层:AsyncLLM (
vllm/v1/engine/async_llm.py:750-793)三步详解:
clear_cache=True,清空 renderer 中缓存的 image/audio 等多模态编码特征("pause_scheduler", mode, clear_cache)发送到 EngineCore 进程pause_generation()返回前被output_handler取出并推送给调用方。这不是正确性要求,只是为了让调用方的视角更直观(看到所有输出后才返回)第二层:EngineCoreClient (
vllm/v1/engine/core_client.py:1130-1133)call_utility_async将方法名和参数序列化为EngineCoreRequestType.UTILITY类型的消息,通过 ZMQ 发送到 EngineCore 进程。如果 engine core 返回了一个Future,则等待该 Future 完成。第三层:EngineCoreProc — 核心逻辑 (
vllm/v1/engine/core.py:1665-1705)engine_idle_callback的注册与触发当引擎进入空闲循环 (
_process_input_queue→has_work() == False) 时触发:has_work()的判断逻辑当
PAUSED_ALL时,scheduler 的get_num_unfinished_requests()返回 0,所以has_requests()返回 False。如果也没有 batch_queue 工作,has_work()返回 False,引擎进入空闲循环,触发 idle callbacks。第四层:Scheduler — 暂停状态如何影响调度
schedule()方法中的 token_budget 控制 (vllm/v1/core/sched/scheduler.py:409-411):get_num_unfinished_requests()对暂停状态的响应 (scheduler.py:2136-2145):schedule()中新请求的处理 (scheduler.py:629):这意味着在
PAUSED_NEW或PAUSED_ALL状态下,新添加的请求会被放入waiting队列,但不会被调度执行。_reset_caches()(core.py:706-719)清空三类缓存:
resume_generation
第一层:AsyncLLM (
vllm/v1/engine/async_llm.py:795-797)第二层:EngineCoreClient (
vllm/v1/engine/core_client.py:1135-1136)第三层:EngineCore — 同步返回 (
vllm/v1/engine/core.py:753-755)只需将调度器状态切回
UNPAUSED。下一次schedule()调用时:token_budget恢复正常计算waiting队列中的请求重新被调度mode="keep"冻结的请求继续执行DPEngineCoreProc — Data Parallel 场景 (
vllm/v1/engine/core.py:1852-1876)DP 场景的 resume 需要额外的同步:
pending_pause已清除、ignore_start_dp_wave已重置resume_scheduler()将状态设为UNPAUSED完整时序:mode="keep" 的 pause → resume 流程
mode 对比总结
mode="abort"mode="wait"mode="keep"PAUSED_NEWPAUSED_NEWPAUSED_ALL