问题:收到了结果,工作进程就已经退出了吗?

第 12 讲用 Pool.starmap 收集返回值。本讲把这一层拆开:一个生产者把题目送进队列,一个工作进程生成答案,父进程收集结果。最后还要确认两者退出。随后换成外部命令,观察超时以后谁负责清理。

阅读顺序是:创建进程 → Queue 交接 → 完成消息 → 等待退出 → 超时升级清理。这里不启动模型、Ray 或网络服务;实验只创建自己的短命子进程。

1. 先创建执行边界,再启动工作

Slime 的 launch_server_process 为模型服务选择 spawn,创建 multiprocessing.Process,调用 start(),然后检查服务健康。在健康检查中,is_alive() 只能回答“进程是否还活着”,不能回答“HTTP 服务已经可用”。

源码入口:launch_server_process 的启动和健康检查交接

launch_server_process
slime/backends/sglang_utils/sglang_engine.py · L49–L63
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

    multiprocessing.set_start_method("spawn", force=True)
    server_args.host = server_args.host.strip("[]")
    p = multiprocessing.Process(target=_launch_server, args=(server_args, os.getpid()))
    p.start()

    if getattr(server_args, "node_rank", 0) != 0:
        return p

    _wait_server_healthy(
        base_url=server_args.url(),
        api_key=server_args.api_key,
        is_process_alive=lambda: p.is_alive(),
    )

    return p

教学程序使用同一种启动方式,但把模型服务换成生成固定答案的 worker_process:

ctx = multiprocessing.get_context("spawn")
task_queue, done_queue = ctx.Queue(maxsize=4), ctx.Queue(maxsize=4)
process = ctx.Process(
    target=worker_process,
    args=(task_queue, done_queue, query_single_turn, get_rule_based_math_reward, None, {}),
)
process.start()

Process(...) 先建立进程对象;start() 才安排子进程执行 target(*args)。spawn 会启动新解释器,因而把入口放在 if __name__ == "__main__": 下,worker 放在模块顶层,传递可序列化的参数。子进程不会共享父进程里的普通列表。

这里 multiprocessing.get_context("spawn") 返回具有 Process、Queue、Pool 工厂的上下文,相关对象统一从它创建。源码用 multiprocessing.set_start_method("spawn", force=True) 改全局选择;库式代码通常更适合局部上下文。不要在任意导入位置反复设置全局启动方式,也不要混用不同上下文创建的同步对象。Python 3.12 启动方式

创建者负责清理自己创建的进程。multiprocessing.active_children() 返回当前进程仍活着的 multiprocessing 子进程,不是全系统进程表。Slime 在启动前后取差集记录新路由器;实验只检查已经握有的 worker 是否在其中,不遍历并杀掉其他任务的子进程。

源码入口:ServingCluster 保存本轮启动的子进程

ServingCluster.__init__:新子进程归属
slime/ray/serving.py · L57–L78
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

        existing_children = {process.pid for process in multiprocessing.active_children()}
        try:
            # Constructor failure must release partial topology too. A detached
            # actor can fail before any driver obtains a usable deployment.
            self.placements = create_placement_groups(args)
            try:
                self.servers, handles = (
                    ({}, []) if args.debug_train_only else start_rollout_servers(args, self.placements["rollout"])
                )
            finally:
                self.router_processes = [
                    process for process in multiprocessing.active_children() if process.pid not in existing_children
                ]
            ray.get(handles)
            self.lock = Lock.options(num_cpus=0, num_gpus=0).remote()
        except BaseException:
            try:
                self.dispose()
            except Exception:
                logging.getLogger(__name__).exception("Failed to dispose partial serving deployment")
            raise

2. 从题目队列走到完成消息

输入交给 task_queue.put(item),worker 用 task_queue.get() 取出。计算结束后,把含 messages、reward、instance_id、extra_info 的结果交给 done_queue.put(output_item)。这是真实生成器使用的字段和交接方向。

def worker_process(task_queue, done_queue, rollout_func, reward_func, client, sampling_params):
    for item in iter(task_queue.get, "STOP"):
        messages, finish_reason = rollout_func(client, item["prompt"], sampling_params)
        item["messages"] = messages
        output_item = {
            "messages": messages,
            "reward": reward_func(item),
            "instance_id": item["instance_id"],
            "extra_info": {"finish_reason": finish_reason},
        }
        done_queue.put(output_item)
    done_queue.put("COMPLETE")

这是教学裁剪:保留原 worker_process 的签名、两条 Queue 和 STOP/COMPLETE 协议;只启动一个 worker,固定返回答案 4,删去 HTTP 调用、UUID、采样统计、JSON 字符串输入和结果上报。extra_info 只保留一个字段,真实实现放入更多题目与采样信息。生产者 read_data_into_queue 从原来的局部函数移到模块顶层,显式接收队列,以便用 spawn 导入;真实实现还读文件、打乱数据并重复投递。

源码入口:worker_process 接收题目、生成结果、发送 COMPLETE

worker_process
slime_plugins/rollout_buffer/generator/base_generator.py · L108–L140
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

def worker_process(task_queue, done_queue, rollout_func, reward_func, client, sampling_params):

    for line in iter(task_queue.get, "STOP"):
        if isinstance(line, str):
            item = json.loads(line)
        else:
            item = line

        # try:
        messages, finish_reason = rollout_func(client, item["prompt"], sampling_params)

        item["uid"] = str(uuid.uuid4())
        item["messages"] = messages
        reward = reward_func(item)
        item["rollout_index"] = 1
        item["reward"] = reward
        item["extra_info"] = {}
        item.update(sampling_params)
        item["timestamp"] = str(time.time())
        item["round_number"] = len([_ for _ in item["messages"] if _["role"] == "assistant"])
        item["finish_reason"] = finish_reason

        output_item = {
            "uid": item.pop("uid"),
            "messages": messages,
            "reward": reward,
            "instance_id": item.pop("instance_id"),
            "extra_info": item,
        }

        done_queue.put(output_item)

    done_queue.put("COMPLETE")
源码入口:BaseGenerator.run 创建 Queue、投递 STOP、启动进程并消费结果

BaseGenerator.run
slime_plugins/rollout_buffer/generator/base_generator.py · L192–L265
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

    def run(self, input_file, rollout_func, reward_func):
        task_queue, done_queue = Queue(maxsize=self.queue_size), Queue(maxsize=self.queue_size)

        def read_data_into_queue():
            cnt = 0
            items = []
            skipped_count = 0
            with open(input_file) as f:
                for i, line in enumerate(f):
                    item = json.loads(line)
                    if "instance_id" not in item:
                        item["instance_id"] = i
                    items.append(item)
            random.shuffle(items)

            for _ in range(self.num_repeats):

                for item in items:
                    for _ in range(self.num_repeat_per_sample):
                        item_repeat = copy.deepcopy(item)

                        if "uid" not in item_repeat:
                            item_repeat["uid"] = str(uuid.uuid4())

                        # Check if instance_id should be skipped
                        if self.skip_instance_ids is not None and item_repeat["instance_id"] in self.skip_instance_ids:
                            print(f"Skipping instance_id: {item_repeat['instance_id']}")
                            # Remove from skip list to handle potential duplicates in multiple epochs
                            self.skip_instance_ids.remove(item_repeat["instance_id"])
                            skipped_count += 1
                            continue

                        task_queue.put(item_repeat)
                    cnt += 1
                time.sleep(300)

            if skipped_count > 0:
                remaining_skip_count = len(self.skip_instance_ids) if self.skip_instance_ids is not None else 0
                print(
                    f"Rollout summary: skipped {skipped_count} instance_ids, {remaining_skip_count} still in skip list"
                )

            for _ in range(self.num_process):
                task_queue.put("STOP")

        processes = []
        SAMPLING_PARAMS["max_tokens"] = self.max_tokens

        for _ in range(self.num_process):
            process = Process(
                target=partial(worker_process, client=self.client, sampling_params=SAMPLING_PARAMS),
                args=(task_queue, done_queue, rollout_func, reward_func),
            )
            process.start()
            processes.append(process)

        process = Process(target=read_data_into_queue)
        process.start()

        progress_bar = tqdm()
        num_finished = 0
        while num_finished < self.num_process:
            item = done_queue.get()
            if item == "COMPLETE":
                num_finished += 1
            else:
                assert "reward" in item, f"reward not in item: {item}"
                assert "instance_id" in item, f"instance_id not in item: {item}"
                self.send_data_to_buffer(item)
                progress_bar.update(1)

        progress_bar.close()

        return "finished"

每个 worker 需要自己的停止消息。一个 STOP 只能被一个消费者取走;父进程也要收齐 worker 数量的 COMPLETE,不能因为暂时没收到结果就判断整个生成任务完成。

multiprocessing.Queue.empty() 和 qsize() 只适合诊断,不能拿来决定“接下来 get() 一定不会阻塞”或“所有生产者已经结束”。put() 后还有后台线程把数据送进管道,观察值也会被其他进程立即改变;qsize() 在 macOS 等平台还可能不可用。实验用明确的结束消息,父进程用 done_queue.get(timeout=10) 给异常路径留出口,超时抛出标准库 queue.Empty。Queue 的传输与限制

3. COMPLETE 以后,还要 join

运行 example.py:

python3 -W error example.py

Python 3.12 输出:

owned worker True
results [(10, 1), (11, 1)]
joined True
join timed out True
process reaped True
pool [4, 9]

前半段先消费结果,再对生产者和 worker 分别 join(timeout=5)。收到 COMPLETE 是业务协议;join 等待的是操作系统进程结束。join(timeout=...) 的返回值不表示成功与否,要再看 is_alive() 和 exitcode。退出后的 Process.close() 释放进程对象的句柄,不能用它代替停止仍活着的进程。

后半段刻意创建一个睡眠 60 秒的进程,join(timeout=0.01) 返回时它仍活着,随后按照 Slime 路由器的顺序处理:

if process.is_alive():
    process.terminate()
    process.join(timeout=5)
if process.is_alive():
    process.kill()
    process.join()

terminate() 在 POSIX 发送 SIGTERM,kill() 在 POSIX 使用 SIGKILL;Windows 的行为不同,不能把它们都理解成可执行 Python 清理逻辑的通知。它们也不自动终止后代进程。源码把等待限制在剩余清理预算内;教学例子只处理自己的单个无后代睡眠进程,因此最后的 join() 不另设置预算。Process 生命周期

源码入口:ServingCluster.dispose 对路由器 terminate → join → kill → join

ServingCluster.dispose:路由器回收
slime/ray/serving.py · L317–L323
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

            for router in self.router_processes:
                if router.is_alive():
                    cleanup.run("terminate router", router.terminate)
                    cleanup.run("join router", router.join, timeout=min(10, cleanup.remaining / 2))
                    if router.is_alive():
                        cleanup.run("kill router", router.kill)
                        cleanup.run("reap router", router.join, timeout=cleanup.remaining)

强制终止正在写 Queue 或持锁的进程,可能留下损坏的管道或让其他进程永久等锁。正常路径优先 STOP;异常路径停止本轮进程后,放弃本轮通信对象。实验最后对两条 Queue 调用 close()、join_thread(),释放本进程的队列资源并等待发送线程完成。不要在消费者还没取走大量结果时先 join 生产者,否则生产者可能卡在刷新队列,父进程又卡在等它退出。进程与 Queue 的清理注意事项

4. Pool 替你分派任务,仍需要关闭边界

现在回到第 12 讲:ctx.Pool(...) 管理多个 worker,pool.starmap(conversion_worker, ...) 分派参数元组并等待返回值。本讲例子把生命周期显式写出:成功后 pool.close() 停止接收新任务;随后 pool.join() 等待 worker 退出;失败时改为 pool.terminate() 放弃尚未完成的工作,再 join()。

真实转换工具使用 with ctx.Pool(num_workers) as pool:。要注意,Pool 的上下文退出调用 terminate();不是所有上下文管理器都承诺等完未完成工作。此处 starmap 已经同步返回,结果才进入后续权重汇总。如果改成后台提交后立刻离开 with,就不能沿用“所有任务都做完了”的推断。Pool 关闭合同

源码入口:转换工具在 Pool 内收集结果,退出后汇总

转换进程池
tools/convert_torch_dist_to_hf_parallel.py · L511–L528
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

    ctx = multiprocessing.get_context("spawn")
    with ctx.Pool(num_workers) as pool:
        results = pool.starmap(
            conversion_worker,
            [
                (
                    i,
                    key_chunks[i],
                    args,
                    megatron_args,
                    args.model_name,
                    state_dict_metadata_dict,
                    load_max_workers,
                    save_max_workers,
                )
                for i in range(num_workers)
            ],
        )

5. 外部命令超时:异常发生了,进程还活着

Process 执行 Python callable;subprocess.Popen 启动命令并立即交回控制对象。Slime 的 SubprocessInterpreter.run_file 用 Popen 跑文件,再用 communicate(timeout=...) 收集输出并等待退出。超过期限会抛 subprocess.TimeoutExpired,但这一调用不会自动杀进程。

try:
    stdout, stderr = proc.communicate(timeout=0.05)
except subprocess.TimeoutExpired:
    print("timeout still running", proc.poll() is None)
    proc.kill()
    stdout, stderr = proc.communicate()

第二次 communicate() 才完成输出读取、管道收尾和退出等待。不能只记一条“超时”日志就丢掉 proc。这是本例直接保留的源码顺序;命令替换成当前 Python 解释器的 time.sleep(60),没有调用真实模型或工具服务。communicate 超时合同

源码入口:SubprocessInterpreter.run_file 的超时回收

SubprocessInterpreter.run_file:超时回收
examples/strands_sglang/subprocess_interpreter.py · L227–L244
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

        try:
            proc = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                env=env,
                shell=False,  # Never use shell=True for security
            )
            # Add timeout to prevent hanging processes
            stdout, stderr = proc.communicate(timeout=self.execution_timeout)
            return_code = proc.returncode
        except subprocess.TimeoutExpired:
            proc.kill()
            stdout, stderr = proc.communicate()
            return_code = proc.returncode
            timeout_msg = f"Process timed out after {self.execution_timeout} seconds and was terminated."
            stderr = f"{stderr}\n{timeout_msg}"

proc.poll() 不阻塞:仍运行返回 None,结束返回退出码。proc.wait(timeout=...) 等待退出但不读取输出。给 stdout/stderr 配了 PIPE 却一直只 wait,孩子可能写满管道后等父进程读取;父进程同时等孩子退出。本例因此对捕获输出的进程使用 communicate,对丢弃输出的进程才直接 terminate()、wait(),超时再 kill()、wait()。Popen 等待与终止

源码入口:GPU 命令包装器检查运行状态并等待退出

_run_command:检查与等待子进程
tests/ci/gpu_lock_exec.py · L38–L65
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

def _run_command(args):
    cmd = args.cmd
    if cmd[0] == "--":
        cmd = cmd[1:]

    child = subprocess.Popen(cmd, start_new_session=True)

    def terminate_child(signum, _frame):
        if child.poll() is None:
            child.terminate()
            try:
                child.wait(timeout=5)
                sys.exit(128 + signum)
            except subprocess.TimeoutExpired:
                pass

            try:
                os.killpg(child.pid, signum)
            except ProcessLookupError:
                pass
            try:
                child.wait(timeout=5)
            except subprocess.TimeoutExpired:
                try:
                    os.killpg(child.pid, signal.SIGKILL)
                except ProcessLookupError:
                    pass
                child.wait()

6. 输出送到哪里,决定父进程怎样等待

运行 subprocess_example.py,依次观察合并输出、超时回收、停止睡眠进程和检查命令结果:

merged ['out', 'err']
timeout still running True
killed and reaped True
terminated and waited True
command failed 7
check_call 0
check_output probe

subprocess.PIPE 建立父子进程间的管道;stderr=subprocess.STDOUT 把错误输出合并进标准输出,此时 communicate() 返回的第二项为 None;subprocess.DEVNULL 丢弃相应输出。示例用 -u 消除子 Python 的输出缓冲影响,先写 out 再写 err。subprocess 输出参数

Slime 的流式训练测试用 STDOUT 合并日志,边读 proc.stdout 边显示,再 wait();启动训练前探测 ray status 则把输出丢到 DEVNULL,只检查状态码。两者都是同步代码,管道存在不表示正在使用 asyncio。

源码入口:合并训练日志,读完再 wait

_run:合并日志并等待
tests/test_glm52_6layer_deterministic_e2e.py · L402–L409
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

    proc = subprocess.Popen(cmd, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    lines = []
    for line in proc.stdout:
        lines.append(line)
        sys.stdout.write(line)
        sys.stdout.flush()
    proc.wait()
    return proc.returncode, "".join(lines)
源码入口:只关心 ray status 的返回码

execute_train:探测 Ray 状态
slime/utils/external_utils/command_utils.py · L111–L121
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

    # Successful training cleans up its own resources; failures retain them.
    if not external_ray:
        # A manual trainer restart must leave the Ray head and its detached
        # rollout session alive. A stale address file alone is not proof of a
        # running cluster, so query the head before deciding to start one.
        ray_running = (
            subprocess.run(
                ["ray", "status"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30
            ).returncode
            == 0
        )

7. 命令失败和命令超时是两条分支

Slime 的 exec_command 用 subprocess.run(..., check=True),非零退出时收到 subprocess.CalledProcessError;异常携带 returncode,捕获了输出时还可检查 stdout/stderr。check_call() 同样在非零退出时抛异常,成功返回 0;check_output() 成功返回捕获的 stdout。这些接口已完成等待,返回值合同不同于立即返回控制对象的 Popen。检查退出状态

源码入口:exec_command 保留命令失败的退出码与输出

exec_command
slime/utils/misc.py · L79–L97
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

def exec_command(cmd: str, capture_output: bool = False) -> str | None:
    print(f"EXEC: {cmd}", flush=True)

    try:
        result = subprocess.run(
            ["bash", "-c", cmd],
            shell=False,
            check=True,
            capture_output=capture_output,
            **(dict(text=True) if capture_output else {}),
        )
    except subprocess.CalledProcessError as e:
        if capture_output:
            print(f"{e.stdout=} {e.stderr=}")
        raise

    if capture_output:
        print(f"Captured stdout={result.stdout} stderr={result.stderr}")
        return result.stdout
源码入口:check_call 检查命令是否存在

SubprocessInterpreter._is_command_available
examples/strands_sglang/subprocess_interpreter.py · L425–L437
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

        else:  # Unix-like systems
            # On Unix-like systems, use which to find the command
            try:
                with open(os.devnull, "w") as devnull:
                    subprocess.check_call(
                        ["which", command],
                        stdout=devnull,
                        stderr=devnull,
                        shell=False,
                    )
                return True
            except subprocess.CalledProcessError:
                return False
源码入口:check_output 获取网卡查询结果

_get_bond_ipv4:check_output
tests/test_qwen3_4B_external_pd.py · L45–L56
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

    if not net_root.exists():
        return None

    bond_ifaces = [
        path.name for path in net_root.iterdir() if path.name.startswith("bond") and path.name[4:].isdigit()
    ]
    bond_ifaces.sort(key=lambda name: int(name[4:]))
    for iface in bond_ifaces:
        try:
            output = subprocess.check_output(["ip", "-o", "-4", "addr", "show", "dev", iface], text=True)
        except (OSError, subprocess.CalledProcessError):
            continue

TimeoutExpired 表示等待期限耗尽;CalledProcessError 表示受检查的命令已经以非零状态结束。普通 Popen 收到非零退出码不会自动抛后者。run(timeout=...) 超时会先杀死并等待直接子进程,再重新抛出异常;Popen.communicate(timeout=...) 则把后续清理留给调用方,不能仅凭参数都叫 timeout 就套用同一段代码。run 的超时合同

《流畅的 Python》核对:相似接口与不同执行边界

书上怎么说:Process 的基本用法与 Thread 相似。

我的判断(本书未覆盖):本讲的 Slime 完成协议和失败清理顺序来自上述固定源码;Python 3.12 的具体方法合同以链接中的官方文档为准,不能从接口相似推导出共享内存或相同取消行为。

依据:第 19 章,spinner-with-processes。英文 corpus/en/ch19.md:245–294;中文 corpus/zh/ch19.md:233–282。书籍为第 2 版、基于 Python 3.10;示例验证环境为 Python 3.12.3。

原文:“Basic usage of the Process class is similar to Thread.”

对照:“❹ Process 类的基本用法与 Thread 相似。”

复述与小实验

先说明结果消息、进程退出、资源释放分别由哪个动作确认,再动手:把生产者的 STOP 暂时去掉,观察父进程怎样从带期限的 get() 进入清理;随后恢复。不要用 empty() 替代结束协议。

第二个问题:若一个服务进程又启动了其他进程,为什么对直接孩子调用一次 kill() 还不能证明服务已全部退出?下一讲沿这个问题进入进程组与信号。

展开本节完整可运行实验
"""Teaching cut: STOP/COMPLETE protocol, then process and Pool cleanup."""

import multiprocessing
import time


def query_single_turn(client, messages, sampling_params):
    return messages + [{"role": "assistant", "content": "4"}], "stop"


def get_rule_based_math_reward(item):
    return int(item["messages"][-1]["content"] == item["label"])


def worker_process(task_queue, done_queue, rollout_func, reward_func, client, sampling_params):
    for item in iter(task_queue.get, "STOP"):
        messages, finish_reason = rollout_func(client, item["prompt"], sampling_params)
        item["messages"] = messages
        output_item = {
            "messages": messages,
            "reward": reward_func(item),
            "instance_id": item["instance_id"],
            "extra_info": {"finish_reason": finish_reason},
        }
        done_queue.put(output_item)
    done_queue.put("COMPLETE")


def read_data_into_queue(task_queue):
    for instance_id in (10, 11):
        task_queue.put({"instance_id": instance_id, "prompt": [], "label": "4"})
    task_queue.put("STOP")


def conversion_worker(value):
    return value * value


def main():
    ctx = multiprocessing.get_context("spawn")
    task_queue, done_queue = ctx.Queue(maxsize=4), ctx.Queue(maxsize=4)
    processes = []
    try:
        process = ctx.Process(
            target=worker_process,
            args=(task_queue, done_queue, query_single_turn, get_rule_based_math_reward, None, {}),
        )
        process.start()
        processes.append(process)
        print("owned worker", process.pid in {child.pid for child in multiprocessing.active_children()})

        producer = ctx.Process(target=read_data_into_queue, args=(task_queue,))
        producer.start()
        processes.append(producer)
        results = []
        while True:
            item = done_queue.get(timeout=10)
            if item == "COMPLETE":
                break
            results.append((item["instance_id"], item["reward"]))
        print("results", sorted(results))
        for process in processes:
            process.join(timeout=5)
            if process.is_alive() or process.exitcode != 0:
                raise RuntimeError("worker did not exit successfully")
        print("joined", all(not process.is_alive() for process in processes))
    finally:
        for process in processes:
            if process.is_alive():
                process.terminate()
                process.join(timeout=5)
            if process.is_alive():
                process.kill()
                process.join()
            process.close()
        for queue in (task_queue, done_queue):
            queue.close()
            queue.join_thread()

    process = ctx.Process(target=time.sleep, args=(60,))
    process.start()
    try:
        process.join(timeout=0.01)
        print("join timed out", process.is_alive())
    finally:
        if process.is_alive():
            process.terminate()
            process.join(timeout=5)
        if process.is_alive():
            process.kill()
            process.join()
        print("process reaped", not process.is_alive())
        process.close()

    pool = ctx.Pool(2)
    try:
        print("pool", pool.starmap(conversion_worker, [(2,), (3,)]))
        pool.close()
    except BaseException:
        pool.terminate()
        raise
    finally:
        pool.join()


if __name__ == "__main__":
    main()

本节对应的全部源码位置

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

展开本节 113 条源码记录
记录 / 讲解源码位置原代码
occ-000205examples/retool/tool_sandbox.py:14
<module>
import subprocess
occ-000210examples/retool/tool_sandbox.py:267
PythonSandbox.execute_code
process = subprocess.Popen(
occ-000211examples/retool/tool_sandbox.py:269
PythonSandbox.execute_code
stdout=subprocess.PIPE,
occ-000212examples/retool/tool_sandbox.py:270
PythonSandbox.execute_code
stderr=subprocess.PIPE,
occ-000213
有接收者线索
examples/retool/tool_sandbox.py:278
PythonSandbox.execute_code
stdout, stderr = process.communicate(timeout=self.timeout)
occ-000214examples/retool/tool_sandbox.py:285
PythonSandbox.execute_code
except subprocess.TimeoutExpired:
occ-000215
有接收者线索
examples/retool/tool_sandbox.py:286
PythonSandbox.execute_code
process.kill()
occ-000274examples/strands_sglang/subprocess_interpreter.py:27
<module>
import subprocess
occ-000276examples/strands_sglang/subprocess_interpreter.py:228
SubprocessInterpreter.run_file
proc = subprocess.Popen(
occ-000277examples/strands_sglang/subprocess_interpreter.py:230
SubprocessInterpreter.run_file
stdout=subprocess.PIPE,
occ-000278examples/strands_sglang/subprocess_interpreter.py:231
SubprocessInterpreter.run_file
stderr=subprocess.PIPE,
occ-000279
有接收者线索
examples/strands_sglang/subprocess_interpreter.py:237
SubprocessInterpreter.run_file
stdout, stderr = proc.communicate(timeout=self.execution_timeout)
occ-000280examples/strands_sglang/subprocess_interpreter.py:239
SubprocessInterpreter.run_file
except subprocess.TimeoutExpired:
occ-000281
有接收者线索
examples/strands_sglang/subprocess_interpreter.py:240
SubprocessInterpreter.run_file
proc.kill()
occ-000282
有接收者线索
examples/strands_sglang/subprocess_interpreter.py:241
SubprocessInterpreter.run_file
stdout, stderr = proc.communicate()
occ-000285examples/strands_sglang/subprocess_interpreter.py:416
SubprocessInterpreter._is_command_available
subprocess.check_call(
occ-000286examples/strands_sglang/subprocess_interpreter.py:423
SubprocessInterpreter._is_command_available
except subprocess.CalledProcessError:
occ-000287examples/strands_sglang/subprocess_interpreter.py:429
SubprocessInterpreter._is_command_available
subprocess.check_call(
occ-000288examples/strands_sglang/subprocess_interpreter.py:436
SubprocessInterpreter._is_command_available
except subprocess.CalledProcessError:
occ-000289examples/strands_sglang/subprocess_interpreter.py:467
SubprocessInterpreter.execute_command
proc = subprocess.Popen(
occ-000290examples/strands_sglang/subprocess_interpreter.py:469
SubprocessInterpreter.execute_command
stdout=subprocess.PIPE,
occ-000291examples/strands_sglang/subprocess_interpreter.py:470
SubprocessInterpreter.execute_command
stderr=subprocess.PIPE,
occ-000292
有接收者线索
examples/strands_sglang/subprocess_interpreter.py:476
SubprocessInterpreter.execute_command
stdout, stderr = proc.communicate(timeout=self.execution_timeout)
occ-000662slime/backends/sglang_utils/deployment.py:4
<module>
import multiprocessing
occ-000664slime/backends/sglang_utils/deployment.py:53
_start_router
process = multiprocessing.Process(
occ-000665
有接收者线索
slime/backends/sglang_utils/deployment.py:58
_start_router
process.start()
occ-000667
有接收者线索
slime/backends/sglang_utils/deployment.py:60
_start_router
assert process.is_alive()
occ-000710slime/backends/sglang_utils/sglang_engine.py:4
<module>
import multiprocessing
occ-000713slime/backends/sglang_utils/sglang_engine.py:32
<module>
def launch_server_process(server_args: ServerArgs) -> multiprocessing.Process:
occ-000714slime/backends/sglang_utils/sglang_engine.py:49
launch_server_process
multiprocessing.set_start_method("spawn", force=True)
occ-000715slime/backends/sglang_utils/sglang_engine.py:51
launch_server_process
p = multiprocessing.Process(target=_launch_server, args=(server_args, os.getpid()))
occ-000717
有接收者线索
slime/backends/sglang_utils/sglang_engine.py:52
launch_server_process
p.start()
occ-000718
有接收者线索
slime/backends/sglang_utils/sglang_engine.py:60
launch_server_process.<lambda>
is_process_alive=lambda: p.is_alive(),
occ-000901slime/ray/serving.py:10
<module>
import multiprocessing
occ-000902slime/ray/serving.py:57
ServingCluster.__init__
existing_children = {process.pid for process in multiprocessing.active_children()}
occ-000903slime/ray/serving.py:68
ServingCluster.__init__
process for process in multiprocessing.active_children() if process.pid not in existing_children
occ-001091slime/rollout/rm_hub/ifbench.py:6
<module>
import subprocess
occ-001092slime/rollout/rm_hub/ifbench.py:27
_ensure_ifbench_repo
subprocess.run(clone_cmd, check=True, capture_output=True)
occ-001093slime/rollout/rm_hub/ifbench.py:62
_ensure_ifbench_dependencies
subprocess.run(install_cmd, check=True)
occ-001214slime/utils/external_utils/command_utils.py:9
<module>
import subprocess
occ-001215slime/utils/external_utils/command_utils.py:117
execute_train
subprocess.run(
occ-001216slime/utils/external_utils/command_utils.py:118
execute_train
["ray", "status"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30
occ-001217slime/utils/external_utils/command_utils.py:118
execute_train
["ray", "status"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30
occ-001272slime/utils/misc.py:2
<module>
import subprocess
occ-001273slime/utils/misc.py:83
exec_command
result = subprocess.run(
occ-001274slime/utils/misc.py:90
exec_command
except subprocess.CalledProcessError as e:
occ-001329slime_plugins/rollout_buffer/generator/base_generator.py:7
<module>
from multiprocessing import Process, Queue
occ-001330slime_plugins/rollout_buffer/generator/base_generator.py:7
<module>
from multiprocessing import Process, Queue
occ-001335slime_plugins/rollout_buffer/generator/base_generator.py:193
BaseGenerator.run
task_queue, done_queue = Queue(maxsize=self.queue_size), Queue(maxsize=self.queue_size)
occ-001336slime_plugins/rollout_buffer/generator/base_generator.py:193
BaseGenerator.run
task_queue, done_queue = Queue(maxsize=self.queue_size), Queue(maxsize=self.queue_size)
occ-001340slime_plugins/rollout_buffer/generator/base_generator.py:241
BaseGenerator.run
process = Process(
occ-001341
有接收者线索
slime_plugins/rollout_buffer/generator/base_generator.py:245
BaseGenerator.run
process.start()
occ-001342slime_plugins/rollout_buffer/generator/base_generator.py:248
BaseGenerator.run
process = Process(target=read_data_into_queue)
occ-001343
有接收者线索
slime_plugins/rollout_buffer/generator/base_generator.py:249
BaseGenerator.run
process.start()
occ-001364tests/ci/gpu_lock_exec.py:7
<module>
import subprocess
occ-001368tests/ci/gpu_lock_exec.py:43
_run_command
child = subprocess.Popen(cmd, start_new_session=True)
occ-001369
有接收者线索
tests/ci/gpu_lock_exec.py:46
_run_command.terminate_child
if child.poll() is None:
occ-001370
有接收者线索
tests/ci/gpu_lock_exec.py:47
_run_command.terminate_child
child.terminate()
occ-001371
有接收者线索
tests/ci/gpu_lock_exec.py:49
_run_command.terminate_child
child.wait(timeout=5)
occ-001372tests/ci/gpu_lock_exec.py:51
_run_command.terminate_child
except subprocess.TimeoutExpired:
occ-001374
有接收者线索
tests/ci/gpu_lock_exec.py:59
_run_command.terminate_child
child.wait(timeout=5)
occ-001375tests/ci/gpu_lock_exec.py:60
_run_command.terminate_child
except subprocess.TimeoutExpired:
occ-001378
有接收者线索
tests/ci/gpu_lock_exec.py:65
_run_command.terminate_child
child.wait()
occ-001384
有接收者线索
tests/ci/gpu_lock_exec.py:75
_run_command
returncode = child.poll()
occ-001847tests/test_glm52_6layer_deterministic_e2e.py:31
<module>
import subprocess
occ-001848tests/test_glm52_6layer_deterministic_e2e.py:72
_iface_ipv4
out = subprocess.run(["ip", "-o", "-4", "addr", "show", ifname], capture_output=True, text=True).stdout.split()
occ-001849tests/test_glm52_6layer_deterministic_e2e.py:124
_skip_reason
visible = subprocess.run(
occ-001850tests/test_glm52_6layer_deterministic_e2e.py:132
_skip_reason
probe = subprocess.run([sys.executable, "-c", _PREREQ_PROBE], capture_output=True, text=True, env=probe_env)
occ-001851tests/test_glm52_6layer_deterministic_e2e.py:153
_download_ci_asset
subprocess.run(command, check=True)
occ-001853tests/test_glm52_6layer_deterministic_e2e.py:398
_run
r = subprocess.run(cmd, env=env, cwd=cwd, capture_output=True, text=True)
occ-001854tests/test_glm52_6layer_deterministic_e2e.py:402
_run
proc = subprocess.Popen(cmd, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
occ-001855tests/test_glm52_6layer_deterministic_e2e.py:402
_run
proc = subprocess.Popen(cmd, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
occ-001856tests/test_glm52_6layer_deterministic_e2e.py:402
_run
proc = subprocess.Popen(cmd, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
occ-001857
有接收者线索
tests/test_glm52_6layer_deterministic_e2e.py:408
_run
proc.wait()
occ-001859tests/test_optional_straw.py:4
<module>
import subprocess
occ-001861tests/test_optional_straw.py:144
test_default_rollout_and_training_work_without_straw
subprocess.run(
occ-001872tests/test_qwen2.5_0.5B_opd_sglang.py:2
<module>
import subprocess
occ-001875tests/test_qwen2.5_0.5B_opd_sglang.py:39
_launch_teacher_server
process = subprocess.Popen(
occ-001876tests/test_qwen2.5_0.5B_opd_sglang.py:57
_launch_teacher_server
stderr=subprocess.STDOUT,
occ-001877
有接收者线索
tests/test_qwen2.5_0.5B_opd_sglang.py:64
_launch_teacher_server
if process.poll() is not None:
occ-001879
有接收者线索
tests/test_qwen2.5_0.5B_opd_sglang.py:75
_launch_teacher_server
process.kill()
occ-001883tests/test_qwen2.5_0.5B_rollout_health.py:7
<module>
import subprocess
occ-001884tests/test_qwen2.5_0.5B_rollout_health.py:82
execute
subprocess.run(["ray", "stop", "--force"], check=True)
occ-001886tests/test_qwen2.5_0.5B_training_recovery.py:7
<module>
import subprocess
occ-001889tests/test_qwen2.5_0.5B_training_recovery.py:169
execute
subprocess.run(
occ-001895tests/test_qwen2.5_0.5B_training_recovery.py:374
execute
subprocess.run(["ray", "stop", "--force"], check=True)
occ-001896tests/test_qwen3_4B_external_pd.py:21
<module>
import subprocess
occ-001898tests/test_qwen3_4B_external_pd.py:54
_get_bond_ipv4
output = subprocess.check_output(["ip", "-o", "-4", "addr", "show", "dev", iface], text=True)
occ-001899tests/test_qwen3_4B_external_pd.py:55
_get_bond_ipv4
except (OSError, subprocess.CalledProcessError):
occ-001902tests/test_qwen3_4B_external_pd.py:157
<module>
) -> subprocess.Popen:
occ-001904tests/test_qwen3_4B_external_pd.py:190
_launch_sglang_server
process = subprocess.Popen(cmd, env=env, stdout=log_file, stderr=subprocess.STDOUT)
occ-001905tests/test_qwen3_4B_external_pd.py:190
_launch_sglang_server
process = subprocess.Popen(cmd, env=env, stdout=log_file, stderr=subprocess.STDOUT)
occ-001906
有接收者线索
tests/test_qwen3_4B_external_pd.py:201
_launch_sglang_server
if process.poll() is not None:
occ-001908
有接收者线索
tests/test_qwen3_4B_external_pd.py:212
_launch_sglang_server
process.kill()
occ-001909tests/test_qwen3_4B_external_pd.py:222
execute
processes: list[subprocess.Popen] = []
occ-001948tests/test_rollout_straw_transport.py:3
<module>
import subprocess
occ-001949tests/test_rollout_straw_transport.py:102
test_cpu_rollout_imports_do_not_require_sglang
subprocess.run(
occ-001950tests/test_rollout_straw_transport.py:261
test_payload_reference_is_small_and_readable_by_independent_process
subprocess.run(
occ-001972tests/test_rollout_straw_transport.py:1418
test_checkpoint_lock_released_when_controller_process_is_killed
process = subprocess.Popen(
occ-001973tests/test_rollout_straw_transport.py:1432
test_checkpoint_lock_released_when_controller_process_is_killed
stdin=subprocess.PIPE,
occ-001974tests/test_rollout_straw_transport.py:1433
test_checkpoint_lock_released_when_controller_process_is_killed
stdout=subprocess.PIPE,
occ-001975tests/test_rollout_straw_transport.py:1434
test_checkpoint_lock_released_when_controller_process_is_killed
stderr=subprocess.PIPE,
occ-001978
有接收者线索
tests/test_rollout_straw_transport.py:1446
test_checkpoint_lock_released_when_controller_process_is_killed
process.kill()
occ-001979
有接收者线索
tests/test_rollout_straw_transport.py:1447
test_checkpoint_lock_released_when_controller_process_is_killed
process.wait(timeout=30)
occ-002059tests/test_straw_fully_async_recovery.py:13
<module>
import subprocess
occ-002085tests/test_straw_fully_async_recovery.py:257
test_fully_async_sigkill_and_restart_preserves_results_and_prefixes
process = subprocess.Popen(
occ-002086tests/test_straw_fully_async_recovery.py:261
test_fully_async_sigkill_and_restart_preserves_results_and_prefixes
stderr=subprocess.STDOUT,
occ-002087
有接收者线索
tests/test_straw_fully_async_recovery.py:268
test_fully_async_sigkill_and_restart_preserves_results_and_prefixes
assert process.poll() is None, log_path.read_text()[-12000:]
occ-002090
有接收者线索
tests/test_straw_fully_async_recovery.py:278
test_fully_async_sigkill_and_restart_preserves_results_and_prefixes
assert process.wait(timeout=180) == 0, log_path.read_text()[-12000:]
occ-002229tests/test_training_recovery.py:291
test_recovery_e2e_contacts_dashboard_without_proxy
import subprocess
occ-002232tests/test_training_recovery.py:326
test_recovery_e2e_contacts_dashboard_without_proxy
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: started.append(dict(os.environ)))
occ-002261tools/convert_torch_dist_to_hf_parallel.py:3
<module>
import multiprocessing
occ-002281tools/convert_torch_dist_to_hf_parallel.py:511
<module>
ctx = multiprocessing.get_context("spawn")

专题首页 · 上一节 · 下一节:19 · 信号、进程组与文件锁:退出以后谁释放资源? · 源码与教程对照 · 全部用法