限制执行中工作,还不够

如果生产者不断生成结果,消费者暂时不读取,结果放在哪里?有界队列把“已经生产、尚未消费”的数量也限制住。

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._start
slime/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._produce
slime/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_size
slime/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-000642slime/backends/megatron_utils/update_weight/update_weight_from_disk_delta.py:6
<module>
import queue
occ-000646slime/backends/megatron_utils/update_weight/update_weight_from_disk_delta.py:234
UpdateWeightFromDiskDelta._encode_delta
free_q: queue.Queue = queue.Queue()
occ-000647slime/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-000942slime/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-001023slime/rollout/fully_async_rollout.py:33
<module>
import queue
occ-001028slime/rollout/fully_async_rollout.py:97
AsyncRolloutWorker.__init__
self.output_queue: queue.Queue[tuple[int, list[Sample]]] = queue.Queue()
occ-001029slime/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-001039slime/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 · 线程池:把阻塞工作移出事件循环 · 源码与教程对照 · 全部用法