限制执行中工作,还不够
如果生产者不断生成结果,消费者暂时不读取,结果放在哪里?有界队列把“已经生产、尚未消费”的数量也限制住。
import asyncio
async def _produce(outputs):
for value in (10, 11):
await outputs.put(value)
await outputs.put(None)
async def _next(outputs):
values = []
while True:
value = await outputs.get()
if value is None:
return values
values.append(value)
async def exercise():
outputs = asyncio.Queue(maxsize=1)
_, values = await asyncio.gather(_produce(outputs), _next(outputs))
print("received", values)
if __name__ == "__main__":
asyncio.run(exercise())
下载并运行本节 example.py。只使用 Python 标准库;推荐 Python 3.12,最低 Python 3.11。
运行结果:
received [10, 11]
这里 None 是实验约定的结束信号,不是 Queue 的内置协议。队列满时 await put() 等待;队列空时 await get() 等待。生产和消费因此通过同一条队列配合。
Slime 的结果队列
_GenerationActor._start() 建立 Queue(maxsize=1),生产者在 _produce() 中把结果放进去,消费者通过 _next() 取走。跟踪 self.outputs,就能把这三个方法连起来。
展开 Slime 源码:_GenerationActor._start
_GenerationActor._startslime/rollout/fully_async_distributed.py · L283–L292
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码
async def _start(self, rollout_id):
self.rollout_id = rollout_id
self.running = True
if self.producer is None or self.producer.done():
if self.producer is not None:
self.producer.result()
# Keep completed results bounded even when training stops consuming.
# At most one queued result plus one result per pool slot stays local.
self.outputs = asyncio.Queue(maxsize=1)
self.producer = asyncio.create_task(self._produce())展开 Slime 源码:_GenerationActor._produce
_GenerationActor._produceslime/rollout/fully_async_distributed.py · L319–L326
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码
async def _produce(self):
async def generate_slot(group):
while self.running or group is not None:
output = await self._generate_group(self.rollout_id, group)
group = None
if output is not None: # Aborted groups have been returned to the durable queue.
await self.outputs.put(output)
del output容量 1 表示最多 1 份结果已经入队。生产者本身还可能持有正在等待入队的结果,所以它不是整个系统的内存总量上限。
与 threading 队列区分
asyncio.Queue 为同一事件循环中的任务设计,不保证 OS 线程之间的安全访问。跨线程数据交接常用标准库 queue.Queue;它的阻塞等待也需要考虑运行位置。
如果使用 queue.join(),消费者必须为取出的工作调用相应的 task_done();本实验使用结束信号和 gather 等待退出,未采用 join 协议。
Slime 怎样取走“现在已经完成”的结果
AsyncRolloutWorker.get_completed_groups() 使用同步 queue.Queue。它调用 get_nowait():有结果就立刻取走,没有就抛出 queue.Empty,于是这一轮停止收集。它没有等待未来才会出现的结果。
| 同步队列的写法 | 本次调用的行为 |
|---|---|
put(value) | 入队;有界队列满时会阻塞调用线程 |
get() | 取走一项;空队列时会阻塞调用线程 |
get_nowait() | 立刻尝试取走;没有数据时抛 queue.Empty |
qsize() / empty() | 查看这一刻的状态;其他线程下一刻可能改变队列 |
不能先看 empty(),就保证随后 get() 一定不阻塞。要表达“没有就算了”,应直接尝试 get_nowait() 并处理 queue.Empty。qsize() 适合观察积压,不是预留结果的操作。
展开 Slime 源码:同步队列的非阻塞收集
AsyncRolloutWorker.get_completed_groups / queue_sizeslime/rollout/fully_async_rollout.py · L142–L159
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码
def get_completed_groups(self, limit: int | None = None) -> list[tuple[int, list[Sample]]]:
"""Pop up to ``limit`` completed groups (all of them when ``None``).
Callers that only need a fixed number of groups must pass ``limit`` —
anything popped beyond it would otherwise have to be thrown away, and
these groups are fully generated and reward-scored, with their prompts
already consumed from ``data_buffer``.
"""
completed: list[tuple[int, list[Sample]]] = []
while limit is None or len(completed) < limit:
try:
completed.append(self.output_queue.get_nowait())
except queue.Empty:
break
return completed
def queue_size(self) -> int:
return self.output_queue.qsize()对于本节的异步队列,empty() 同样只是状态观察;需要等待下一份结果时用 await get()。同步 Queue、异步 Queue、进程 Queue 的等待位置不同,进程版本在第 18 节继续讲。
语义核对:同步 queue.Queue。
复述检查
Semaphore 限制的是进入某段工作的许可,Queue 的 maxsize 限制的是已排队元素。二者可以同时存在,分别约束不同阶段。
语义核对:asyncio Queue。
本节对应的全部源码位置
下面把同类用法在 Slime 中的每个扫描位置接回本节讲解;点击文件名可查看完整源码快照。重复出现的 API 共用解释,具体输入与清理由所在函数决定。
展开本节 19 条源码记录
| 记录 / 讲解 | 源码位置 | 原代码 |
|---|---|---|
| occ-000642 | slime/backends/megatron_utils/update_weight/update_weight_from_disk_delta.py:6 <module> | import queue |
| occ-000646 | slime/backends/megatron_utils/update_weight/update_weight_from_disk_delta.py:234 UpdateWeightFromDiskDelta._encode_delta | free_q: queue.Queue = queue.Queue() |
| occ-000647 | slime/backends/megatron_utils/update_weight/update_weight_from_disk_delta.py:234 UpdateWeightFromDiskDelta._encode_delta | free_q: queue.Queue = queue.Queue() |
| occ-000648 有接收者线索 | slime/backends/megatron_utils/update_weight/update_weight_from_disk_delta.py:238 UpdateWeightFromDiskDelta._encode_delta | free_q.put(torch.empty(max_bytes, dtype=torch.uint8, pin_memory=True)) |
| occ-000649 有接收者线索 | slime/backends/megatron_utils/update_weight/update_weight_from_disk_delta.py:247 UpdateWeightFromDiskDelta._encode_delta.diff_and_compress | free_q.put(buf) |
| occ-000652 有接收者线索 | slime/backends/megatron_utils/update_weight/update_weight_from_disk_delta.py:280 UpdateWeightFromDiskDelta._encode_delta | buf = free_q.get() # blocks when all buffers are in flight -> backpressures the gather |
| occ-000942 | slime/rollout/fully_async_distributed.py:291 _GenerationActor._start | self.outputs = asyncio.Queue(maxsize=1) |
| occ-000955 有接收者线索 | slime/rollout/fully_async_distributed.py:306 _GenerationActor._next | take = asyncio.create_task(self.outputs.get()) |
| occ-000962 有接收者线索 | slime/rollout/fully_async_distributed.py:312 _GenerationActor._next | if not self.outputs.empty(): |
| occ-000971 有接收者线索 | slime/rollout/fully_async_distributed.py:325 _GenerationActor._produce.generate_slot | await self.outputs.put(output) |
| occ-001023 | slime/rollout/fully_async_rollout.py:33 <module> | import queue |
| occ-001028 | slime/rollout/fully_async_rollout.py:97 AsyncRolloutWorker.__init__ | self.output_queue: queue.Queue[tuple[int, list[Sample]]] = queue.Queue() |
| occ-001029 | slime/rollout/fully_async_rollout.py:97 AsyncRolloutWorker.__init__ | self.output_queue: queue.Queue[tuple[int, list[Sample]]] = queue.Queue() |
| occ-001035 有接收者线索 | slime/rollout/fully_async_rollout.py:133 AsyncRolloutWorker.load_state_dict | self.output_queue.put(result) |
| occ-001038 有接收者线索 | slime/rollout/fully_async_rollout.py:153 AsyncRolloutWorker.get_completed_groups | completed.append(self.output_queue.get_nowait()) |
| occ-001039 | slime/rollout/fully_async_rollout.py:154 AsyncRolloutWorker.get_completed_groups | except queue.Empty: |
| occ-001040 有接收者线索 | slime/rollout/fully_async_rollout.py:159 AsyncRolloutWorker.queue_size | return self.output_queue.qsize() |
| occ-001049 有接收者线索 | slime/rollout/fully_async_rollout.py:199 AsyncRolloutWorker._loop | and self.output_queue.qsize() < max_concurrent |
| occ-001061 有接收者线索 | slime/rollout/fully_async_rollout.py:263 AsyncRolloutWorker._make_done_cb._cb | self.output_queue.put((gid, result)) |
专题首页 · 上一节 · 下一节:08 · 线程池:把阻塞工作移出事件循环 · 源码与教程对照 · 全部用法