# PR #2170 完整报告

- 仓库：`THUDM/slime`
- 标题：Fix placement group crash for external engines under debug_rollout_only
- 合并时间：2026-08-12 13:44
- 原文链接：http://prhub.com.cn/THUDM/slime/pull/2170

---

# 执行摘要

- 一句话：修复外部引擎 debug_rollout_only 下 placement group 崩溃
- 推荐动作：该 PR 属于针对特定调试模式的 bugfix，建议合入。虽然代码变更简单，但涉及 placement group 布局，可能影响训练资源分配，值得快速审查。可关注后续是否有类似组合的其他布局问题。

# 功能与动机

PR body 明确指出，当使用 `--rollout-external-engine-addrs` 和 `--debug-rollout-only` 时，`rollout_num_gpus` 会被远程引擎的 GPU 数量覆盖（覆盖了本地 actor 维度），但 `_get_placement_group_layout` 对此组合返回 `(0, 0)`，导致空 placement group 与非零的 actor world_size 不匹配，并在此处崩溃：https://github.com/THUDM/slime/blob/23464705f48d0dcb8beb7e983b60859929dd856f/slime/ray/actor_group.py#L114。因此需要修复该组合下的 placement group 布局逻辑。

# 实现拆解

本 PR 的修复分为两个主要步骤：
1. **参数校验逻辑调整 **（`slime/utils/arguments.py`）：在 `slime_validate_args` 函数的 `debug_rollout_only` 分支中，如果 `args.rollout_external` 为真，则跳过 actor 维度覆盖逻辑（`pass`），否则保留原有覆盖逻辑。这样避免了在外部引擎场景下错误地修改 `actor_num_gpus_per_node` 和 `actor_num_nodes`。
2. **placement group 布局修复 **（`slime/ray/placement_group.py`）：在 `_get_placement_group_layout` 函数中，将 `rollout_external` 且 `debug_rollout_only` 分支的返回值从 `(0, 0)` 改为 `(actor_num_gpus, 0)`，确保 placement group 中包含占位训练 actor 所需的 GPU 数。
3. **单元测试更新 **（`tests/test_placement_group.py`）：将 `external_debug_rollout` 用例的期望值从 `(0, 0)` 更新为 `(16, 0)`，以反映新的布局行为。

测试变更与源码变更联动，确保新逻辑被验证。

关键文件：
- `slime/ray/placement_group.py`（模块 调度器；类别 source；类型 core-logic；符号 _get_placement_group_layout）: 核心修复：修改 `_get_placement_group_layout` 返回值，解决空 placement group 崩溃问题。
- `slime/utils/arguments.py`（模块 参数校验；类别 source；类型 core-logic；符号 slime_validate_args）: 参数校验逻辑调整，避免外部引擎场景下 actor 维度被覆盖。
- `tests/test_placement_group.py`（模块 测试；类别 test；类型 test-coverage；符号 test_placement_group_layout）: 更新单元测试期望值，与修复逻辑保持一致。

关键符号：_get_placement_group_layout, slime_validate_args

## 关键源码片段

### `slime/ray/placement_group.py`

核心修复：修改 `_get_placement_group_layout` 返回值，解决空 placement group 崩溃问题。

```python
# slime/ray/placement_group.py
# 计算并返回 placement group 的布局 ( 总 GPU 数 , rollout 偏移量 )
def _get_placement_group_layout(args) -> tuple[int, int]:
    # 根据 actor 节点数与每节点 GPU 数计算 actor 所需 GPU 总数
    actor_num_gpus = args.actor_num_nodes * args.actor_num_gpus_per_node

    # 仅训练调试模式：不需要 rollout，只创建 actor 的 PG
    if args.debug_train_only:
        return actor_num_gpus, 0

    # 使用外部引擎（无本地 rollout）
    if args.rollout_external:
        # 调试 rollout 模式：跳过 rollout 引擎，但仍需为占位训练 actor 分配 GPU
        if args.debug_rollout_only:
            return actor_num_gpus, 0
        # 正常外部引擎模式：actor 与 rollout 共享同一 PG
        return actor_num_gpus, actor_num_gpus

    # 其他调试 / 常规分支逻辑不变
    if args.debug_rollout_only:
        return args.rollout_num_gpus, 0

    if args.colocate:
        return max(actor_num_gpus, args.rollout_num_gpus), 0

    return actor_num_gpus + args.rollout_num_gpus, actor_num_gpus

```

### `slime/utils/arguments.py`

参数校验逻辑调整，避免外部引擎场景下 actor 维度被覆盖。

```python
# slime/utils/arguments.py - 位于 slime_validate_args 函数中
    if args.debug_rollout_only:
        # 若使用外部引擎，跳过本地 actor 维度覆盖，保留原始 actor 配置
        if args.rollout_external:
            pass
        # 常规 colocate 场景：rollout_num_gpus 未显式设置时，沿用 actor 配置
        elif args.colocate and args.rollout_num_gpus is None:
            args.rollout_num_gpus = args.actor_num_gpus_per_node * args.actor_num_nodes
        elif args.rollout_num_gpus == 0:
            args.actor_num_gpus_per_node = 0
            args.actor_num_nodes = 0
        else:
            args.actor_num_gpus_per_node = min(8, args.rollout_num_gpus)
            args.actor_num_nodes = args.rollout_num_gpus // args.actor_num_gpus_per_node
        args.colocate = False
        args.offload_train = args.offload_rollout = False

```

### `tests/test_placement_group.py`

更新单元测试期望值，与修复逻辑保持一致。

```python
# tests/test_placement_group.py
@pytest.mark.parametrize(
    ("overrides", "expected"),
    [
        # 其他用例 ...
        # 外部引擎 + 调试 rollout 模式：应返回 actor_num_gpus (16) 和 Offset 0
        pytest.param({"rollout_external": True, "debug_rollout_only": True}, (16, 0), id="external_debug_rollout"),
    ],
)
def test_placement_group_layout(overrides, expected):
    assert _get_placement_group_layout(_args(**overrides)) == expected

```

# 评论区精华

该 PR 没有 review 评论或讨论线程。

- 暂无高价值评论线程

# 风险与影响

- 风险：本次变更属于小范围修复，风险较低。但需要注意，PR body 提到该修复假设节点上有可用的 GPU 来创建占位训练 actor。如果实际环境不满足该假设，仍可能遇到资源不足问题。另外，修改 `_get_placement_group_layout` 的返回值可能影响 `create_placement_groups` 中 rollout bundle 的切片逻辑（基于 `rollout_offset`），但此处偏移为 0，影响有限。建议确认 debug_rollout_only 场景下 actor 进程能正常使用 placement group 的 GPU。
- 影响：影响范围限定在使用 `--debug-rollout-only` 且配合外部引擎（`--rollout-external-engine-addrs`）的开发调试场景。修复后这些场景可以正常创建 placement group，避免崩溃。对正常训练流程无影响，因为该分支仅在 debug 模式激活。测试更新保持了与源码同步，团队可安全合并。
- 风险标记：假设节点有可用 GPU, 外部引擎调试场景

# 关联脉络

- PR #2208 Support reloading the default process group: 涉及 Ray 资源管理，与 placement group 相关联。
- PR #2181 [3/n] Disaggregated rollout: engine-side /pull_weights: 涉及外部引擎与 rollout 解耦，可能影响外部引擎参数处理。