这次从一份工作走到结果

不先阅读全仓文件清单。先跟随 _GenerationActor 的一条主线,把每个原生机制放回它承担的职责中。

顺序到这里做什么Python 机制读完返回哪里
1调用方要求开始生产跨线程提交、wrap_future_start()
2建立结果缓冲并启动生产者Queue、create_task_produce()
3获取样本、安排生成任务await、Task、gather各生成槽位
4发布结果并等待消费Queue.put 的异步等待消费者 _next()
5同时观察结果和生产者状态wait、FIRST_COMPLETED、result调用方
6关闭时处理剩余任务cancel、gather、关闭执行器退出生产流程

先走正常路径

_start 接收 rollout 编号,建立有界队列,再安排 _produce。生产者创建多个生成槽位;每个槽位取得结果后放入输出队列。如果消费者慢,put 的等待会把压力传回生产者。

调用方通过 _next 取结果。它不是简单地永远等待 Queue.get,还同时观察生产者:生产者失败时,消费者需要得知失败,而不是卡在一个不会再被填入的队列上。

展开 Slime 源码:_GenerationActor.start / _start / _next

_GenerationActor.start / _start / _next
slime/rollout/fully_async_distributed.py · L279–L317
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

    async def start(self, rollout_id):
        future = asyncio.run_coroutine_threadsafe(self._start(rollout_id), get_async_loop().loop)
        await asyncio.wrap_future(future)

    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())

    async def pause(self):
        future = asyncio.run_coroutine_threadsafe(self._pause(), get_async_loop().loop)
        await asyncio.wrap_future(future)

    async def _pause(self):
        self.running = False

    async def next(self):
        future = asyncio.run_coroutine_threadsafe(self._next(), get_async_loop().loop)
        return await asyncio.wrap_future(future)

    async def _next(self):
        take = asyncio.create_task(self.outputs.get())
        try:
            await asyncio.wait((take, self.producer), return_when=asyncio.FIRST_COMPLETED)
            if take.done():
                return take.result()
            self.producer.result()  # Propagate custom generation errors.
            if not self.outputs.empty():
                return await take
            return None
        finally:
            take.cancel()
            await asyncio.gather(take, return_exceptions=True)
展开 Slime 源码:_GenerationActor._produce

_GenerationActor._produce
slime/rollout/fully_async_distributed.py · L319–L342
快照 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

        tasks = []
        try:
            for offset in range(0, self.capacity, 64):
                if not self.running:
                    break
                groups = await self.data_source.get_samples_async(min(64, self.capacity - offset))
                tasks.extend(asyncio.create_task(generate_slot(group)) for group in groups)
                # Spread startup callbacks across loop turns to reduce asyncio
                # pressure. Each slot then pulls its own next group immediately.
                await asyncio.sleep(0.001)
            await asyncio.gather(*tasks)
        finally:
            for task in tasks:
                task.cancel()
            await asyncio.gather(*tasks, return_exceptions=True)
展开 Slime 源码:_GenerationActor.close / _close

_GenerationActor.close / _close
slime/rollout/fully_async_distributed.py · L434–L447
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

    async def close(self):
        future = asyncio.run_coroutine_threadsafe(self._close(), get_async_loop().loop)
        await asyncio.wrap_future(future)

    async def _close(self):
        self.running = False
        if self.producer is not None:
            self.producer.cancel()
            await asyncio.gather(self.producer, return_exceptions=True)
        if getattr(self, "_rebuffer_task", None) is not None:
            await self._rebuffer_task
        # to_thread writes can outlive cancelled awaiters. Drain them before
        # closing their files or killing this process.
        await asyncio.get_running_loop().shutdown_default_executor()

再补失败与清理

finally 取消并等待不再需要的取数任务;生产者的 finally 清理自己创建的任务。这里要逐层追踪所有权:一个函数创建了哪些后台任务,就要找到这些任务在退出时由谁收回。

遇到 to_thread 再去读数据读取或发布逻辑;遇到生成器再进入模型请求。完成一个职责后返回这里,继续跟踪结果,而不是同时展开所有业务模块。

用五个问题检查理解

  1. 去掉 Queue 的容量限制,慢消费者会给生产者带来什么影响?
  2. 如果只等 outputs.get(),生产者先异常退出会发生什么?
  3. take.cancel() 之后为什么还有 gather(take, return_exceptions=True)?
  4. 把阻塞读取直接放进 _produce(),其他任务会受什么影响?
  5. shield 保护写入时,调用方取消和工作线程退出分别由谁处理?
对照思路

分别检查积压结果、永远等待、取消收尾、事件循环阻塞和线程工作所有权。不要用“这是异步函数”替代对具体调用位置的分析。

下一步可到全部用法索引按 API 回查;那里保留固定版本的全量位置,本页只负责把任务流程读连贯。

本节对应的全部源码位置

下面把同类用法在 Slime 中的每个扫描位置接回本节讲解;点击文件名可查看完整源码快照。重复出现的 API 共用解释,具体输入与清理由所在函数决定。

展开本节 0 条源码记录
记录 / 讲解源码位置原代码

专题首页 · 上一节 · 下一节:Event 通知与异步互斥 · 源码与教程对照 · 全部用法