Prhub

#6052 [fully_async] fix: avoid blocking ray.get inside async actor methods

原始 PR 作者 yxs 合并时间 2026-04-20 13:14 文件变更 2 提交数 1 评论 2 代码增减 +6 / -5

执行摘要

修复完全异步训练器中同步 Ray 调用阻塞事件循环的问题。

根据PR body描述,在异步Ray Actor的async def方法中使用同步ray.get会阻塞Actor的事件循环,导致Ray运行时在每个受影响位置打印警告“Using blocking ray.get inside async actor. This blocks the event loop...”。同时,message_queue.py中已标记同步助手为“deprecated, use instead”,且异步变体已实现。因此需要修复这些阻塞调用以消除警告并遵循异步最佳实践。

该PR值得精读,特别是对于使用Ray异步Actor的开发者。关注点包括:

1) 如何正确地将同步Ray调用迁移到异步以避免阻塞;
2) 使用asyncio.wrap_future处理Ray远程future的模式;
3) 讨论中关于None处理的设计权衡,展示了实际开发中边界条件处理的优先级决策。

讨论亮点

reviewer gemini-code-assist[bot] 指出在fully_async_trainer.py第251行,get_sample()方法可能在消息队列关闭且为空时返回None(标量),尝试解包Nonesample, queue_len会引发TypeError。建议在解包前安全处理潜在的None返回值以避免崩溃。作者yxs回复“out of scope”,认为此问题超出本PR范围,未采纳建议。

实现拆解

  1. 导入asyncio模块:在fully_async_trainer.py头部添加import asyncio,为后续使用asyncio.wrap_future提供支持。
  2. 替换消息队列同步调用为异步调用:在fully_async_trainer.py_get_samples_from_queue方法中,将self.message_queue_client.get_sample_sync()替换为await self.message_queue_client.get_sample();在fully_async_rollouter.py_should_pause_generationget_statistics方法中,将self.message_queue_client.get_statistics_sync()替换为await self.message_queue_client.get_statistics()
  3. 替换Ray远程调用的同步获取为异步获取:在fully_async_trainer.py_fit_update_weights方法中,将ray.get(self.rollouter.reset_staleness.remote())替换为await asyncio.wrap_future(self.rollouter.reset_staleness.remote().future());在_fit_validate方法中,将ray.get(val_future)替换为await asyncio.wrap_future(val_future.future())
  4. 测试验证:PR body提到在1×8 H200上使用Qwen3-4B-Instruct-2507进行了50步完全异步PPO冒烟测试,预补丁和后补丁均成功,且警告消失,性能指标在运行间噪声范围内。
文件 模块 状态 重要度
verl/experimental/fully_async_policy/fully_async_trainer.py 异步策略 modified 5.97
verl/experimental/fully_async_policy/fully_async_rollouter.py 异步策略 modified 5.23

关键符号

_get_samples_from_queue _fit_update_weights _fit_validate _should_pause_generation get_statistics

关键源码片段

verl/experimental/fully_async_policy/fully_async_trainer.py core-logic

修复 Trainer 异步 Actor 中多个阻塞调用,包括消息队列采样和 Ray 远程调用,是消除警告的核心文件。

async def _get_samples_from_queue(self) -> tuple[None, None] | tuple[int, Any]:
    """
    从消息队列获取样本并组成gen_batch_output
    使用循环持续收集样本直到足够数量
    """
    print(
        f"[FullyAsyncTrainer] Requesting {self.required_samples} samples from queue",
        flush=True,
    )
​
    # 使用简单循环调用 get_sample 收集样本
    consumer_start = time.time()
    queue_samples = []
    queue_len = 0
    while len(queue_samples) < self.required_samples:
        # 获取单个样本并等待直到有样本或收到 None
        # 修复:将同步调用 get_sample_sync() 替换为异步调用 get_sample(),避免阻塞事件循环
        sample, queue_len = await self.message_queue_client.get_sample()
​
        if sample is None:
            print(
                f"[FullyAsyncTrainer] Detected termination signal (None), stopping sample collection. "
                f"Collected {len(queue_samples)}/{self.required_samples} samples"
            )
            break
​
        queue_samples.append(sample)
​
        if len(queue_samples) % 64 == 0:
            print(
                f"[FullyAsyncTrainer] Collected {len(queue_samples)}/{self.required_samples} samples. "
                f"mq_len: {queue_len}"
            )
​
    consumer_end = time.time()
​
    if not queue_samples or len(queue_samples) < self.required_samples:
        print("[FullyAsyncTrainer] not enough samples collected after loop")
        return None, None

评论区精华

get_sample() 返回 None 时的解包错误处理 正确性

reviewer gemini-code-assist[bot] 指出 get_sample() 可能在消息队列关闭时返回 None 标量,直接解包会引发 TypeError,建议先检查返回值。作者 yxs 认为此问题超出本 PR 范围。

结论:作者未采纳建议,认为该边缘情况处理不属于本次修复范围。 · 已解决

风险与影响

  1. 回归风险:将同步调用改为异步可能引入竞态条件或死锁,但变更仅限于调用方式,不改变核心逻辑,风险较低。
  2. 正确性风险get_sample()返回None时的解包错误未被修复,在消息队列关闭场景下可能引发TypeError崩溃,但根据作者回复,此场景被视为边缘情况且可能由其他机制处理。
  3. 兼容性风险:依赖asyncio.wrap_future和Ray异步API,要求Ray版本支持这些特性,但鉴于项目已广泛使用异步Actor,风险可控。
  1. 对系统的影响:消除了阻塞事件循环的警告,提升了异步Actor的响应性和吞吐量,有助于完全异步训练流程的稳定性。
  2. 对用户的影响:用户将不再看到相关警告,但行为无变化,不影响训练结果。
  3. 对团队的影响:强化了异步编程规范,为后续完全异步功能开发提供了更清晰的范例。
异步编程风险 边缘情况未处理

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论