# PR #33338 完整报告

- 仓库：`sgl-project/sglang`
- 标题：config: retire the last process-global config field reads
- 合并时间：2026-08-03 12:24
- 原文链接：http://prhub.com.cn/sgl-project/sglang/pull/33338

---

# 执行摘要

- 一句话：清除最后进程级配置读取，迁移至命名空间访问器
- 推荐动作：值得精读。核心看点是两层：一是 AST 静态扫描作为架构护栏的做法——把 " 进程级配置读取只准减少 " 编译进 CI，用双向断言（多了报违例、少了逼降基线）驱动渐进迁移；二是 " 什么该留守全局 " 的判别框架（派生 API、config-intent 读取、无进程组上下文），对理解 `ServerArgs` 与命名空间访问器的边界很有帮助。另需关注作者自述的验证缺口：model / attention 路径的翻转读取依赖 GPU CI，合入后应留意 speculative 与 model 套件结果。

# 功能与动机

PR body 开门见山：`get_server_args().<field>` reads the startup record of one process——进程级全局配置本质上是一个进程的启动记录，凡是需要解析后值（含 post-publish override）或每个 runner / 实例独立值的读取，它都不是正确载体。body 明确说明两类迁移理由：9 处读取有命名空间归宿（如 `attention_backend` ×5 应读 `get_exec().kernel`、`skip_tokenizer_init` ×2 应读 `get_serving()`、draft-aware 的 `load_format` 应读 `get_model()`、PP 大小应读 `get_parallel()`），2 处必须下沉到实例："the encode-server DP workers each specialise their own copy — so no process-global value can stand in for it, and several `Engine`s can share a tokenizer process"。前作 #33244 因 GitHub 将 chained-base 系列视为 stack、阻塞合并而被关闭，本 PR 是同一最终修订版的重新提交。

# 实现拆解

1. **注意力后端读取迁往执行命名空间（5 处 + 1 处内核）**：`mem_cache/allocation.py` 的 `write_cache_indices` 与 `get_last_loc`、`layers/rotary_embedding/mrope.py` 的 `get_cos_sin_with_position`、`models/gpt_oss.py` 的 sink dtype 选择、`batch_overlap/two_batch_overlap.py` 的 `derive_fields_related_to_seq_len_for_two_chunk`、`layers/attention/attention_registry.py` 统一由 `get_server_args().attention_backend` 改为 `get_exec().kernel.attention_backend`；`kernels/ops/layernorm/mhc.py` 的 chunked-prefill 大小读取改走 `get_schedule()`。这些位置处于 decode / extend 热路径，取值语义不变，但获得的是含 post-publish override 的解析值。
2. **服务与模型命名空间迁移（4 处）**：`managers/mm_utils.py` 的 `wrap_shm_features` / `unwrap_shm_features` 中 2 处 `skip_tokenizer_init` 改读 `get_serving()`；`_acknowledge_deferred_cuda_ipc_cache_hits` 的 `consumer_count` 由 `get_server_args().tp_size` 改为已持有的 `parallel.tp_size`（实时拓扑）；`models/inkling_common/dense_mlp.py` 的 `_shared_scales` 中 dummy 加载判定改读 `get_model().load_format`（draft-aware 版本）。
3. **多模态设备选择下沉到实例（本 PR 唯一的行为修复）**：`base_processor.py` 抽出 `_fast_image_processor_device(processor)` 方法，从 `self.server_args` 解析设备，`process_mm_data` 原有约 30 行内联分支收敛为一次调用；NPU 的 qwen-vl / GLM46V 补丁逻辑、分支顺序以及 Glm4vProcessor 不设 device 的语义保持不变。
4. **新增 AST 护栏测试**：`test_global_config_read_ratchet.py` 遍历整个 `sglang` 包，识别 " 直接 `get_server_args().field`"（基线 0）与 " 同函数内 `sa = get_server_args()` 后 `sa.field`"（基线 12）两种形态；`_DERIVED_MEMBERS` 白名单豁免派生 API（`mamba_cache_chunk_size`、`get_model_config()`、`enable_mamba_extra_buffer()` 等），`_CONFIG_INTENT_SIZES` 白名单豁免 3 处必须留守的 config-intent 读取（`dsa_indexer.pp_size`、`allocation.dcp_size`、`cuda_ipc_transport_utils.tp_size`）。测试是双向护栏：读数变多列出违现场，读数变少要求下调基线锁住进度。
5. **测试与文档配套**：`test_dllm_fdfo_kv_reuse.py` 放弃对 `allocation.get_server_args` 的 monkeypatch（读者一迁移桩就失效，本 PR 正是如此），改由 `get_context().override_server_args()` 发布真实配置并以 `addCleanup` 恢复；新增 `test_processor_device_selection.py` 用 6 个用例钉死实例级设备解析；`.claude/skills/sglang-runtime-context/SKILL.md` 同步更新访问指南。验证层面，作者跑了各组单元套件与 16 分区 CPU 全量，7 个分支专属失败重跑后全绿，无新增失败。

关键文件：
- `test/registered/unit/test_global_config_read_ratchet.py`（模块 配置守卫；类别 test；类型 test-coverage；符号 _is_global_call, _collect, counted, _field_reads）: 新增 AST 静态扫描护栏：全包扫描 `get_server_args().field` 直接读取与函数内别名读取，直接基线 0、别名基线 12，白名单豁免派生 API 与 config-intent 读取，是本 PR 的架构约束核心。
- `test/registered/unit/multimodal/test_processor_device_selection.py`（模块 设备选择；类别 test；类型 test-coverage；符号 _Processor, _StubProcessor, process_mm_data_async, _make）: 新增回归测试：钉死 fast image processor 设备必须来自实例自身 `server_args`，覆盖多 Engine 同进程、发布其他配置不漂移、RL / CPU / XPU / NPU 各分支。
- `python/sglang/srt/multimodal/processors/base_processor.py`（模块 多模态；类别 source；类型 core-logic；符号 _fast_image_processor_device, process_mm_data）: 核心逻辑变更：抽出 `_fast_image_processor_device`，设备选择从进程级 `get_server_args()` 改为实例 `self.server_args`，修复多 Engine 共享 tokenizer 进程时选错 GPU 的隐患。
- `python/sglang/srt/mem_cache/allocation.py`（模块 内存缓存；类别 source；类型 dependency-wiring；符号 write_cache_indices, get_last_loc）: 内存分配热路径两处 attention_backend 读取改走 get_exec().kernel（write_cache_indices 与 get_last_loc），是解码路径上最重要的迁移点。
- `python/sglang/srt/managers/mm_utils.py`（模块 多模态；类别 source；类型 core-logic；符号 _acknowledge_deferred_cuda_ipc_cache_hits, wrap_shm_features, unwrap_shm_features）: 多模态工具三处迁移：skip_tokenizer_init ×2 改读 get_serving()，tp_size 改读 live parallel.tp_size，消除进程级读取。
- `python/sglang/srt/models/inkling_common/dense_mlp.py`（模块 模型层；类别 source；类型 data-contract；符号 _shared_scales）: 共享专家 FP4 加载的 load_format 判定改读 get_model()，取 draft-aware 解析值而非进程启动记录。
- `python/sglang/srt/layers/rotary_embedding/mrope.py`（模块 位置编码；类别 source；类型 dependency-wiring；符号 get_cos_sin_with_position）: 旋转位置编码的 Triton 分支判定改读 get_exec().kernel.attention_backend，属于注意力相关热路径迁移。
- `python/sglang/srt/models/gpt_oss.py`（模块 模型层；类别 source；类型 data-contract）: sink 参数 dtype 选择（trtllm_mha 要求 float32）改读 get_exec().kernel.attention_backend，消除模型侧进程级读取。
- `python/sglang/srt/batch_overlap/two_batch_overlap.py`（模块 批重叠；类别 source；类型 dependency-wiring；符号 derive_fields_related_to_seq_len_for_two_chunk）: 两批 overlap 的 compute_position 调用改读 get_exec().kernel.attention_backend。
- `python/sglang/srt/layers/attention/attention_registry.py`（模块 注意力；类别 source；类型 core-logic）: 注意力后端注册表同步迁移配置读取，属于 attention_backend 命名空间迁移的一部分。
- `test/registered/unit/mem_cache/test_dllm_fdfo_kv_reuse.py`（模块 内存缓存；类别 test；类型 test-coverage；符号 setUp, tearDown, test_alloc_for_extend_mixed_reuse_allocates_only_fresh_and_writes_rows）: 测试改为发布真实配置（override_server_args + addCleanup），替代会随读者迁移而失效的 monkeypatch 桩，是本 PR 测试治理的代表。
- `python/sglang/kernels/ops/layernorm/mhc.py`（模块 内核层；类别 infra；类型 infrastructure）: 内核层 chunked-prefill 大小读取改走 get_schedule()，是本次迁移中唯一位于 sglang/kernels 的读取点。
- `.claude/skills/sglang-runtime-context/SKILL.md`（模块 开发文档；类别 docs；类型 documentation）: 运行时上下文访问指南同步更新，记录命名空间访问器用法，属于配套文档。

关键符号：_fast_image_processor_device, _collect, _field_reads, _is_global_call, _check, test_global_field_reads_match_the_baseline, write_cache_indices, get_last_loc, _acknowledge_deferred_cuda_ipc_cache_hits, wrap_shm_features, unwrap_shm_features, _shared_scales, get_cos_sin_with_position, derive_fields_related_to_seq_len_for_two_chunk, process_mm_data

## 关键源码片段

### `test/registered/unit/multimodal/test_processor_device_selection.py`

新增回归测试：钉死 fast image processor 设备必须来自实例自身 `server_args`，覆盖多 Engine 同进程、发布其他配置不漂移、RL / CPU / XPU / NPU 各分支。

```python
# test/registered/unit/multimodal/test_processor_device_selection.py（新增）
# 回归测试：fast image processor 的设备必须来自 processor 自身的 ServerArgs，
# 而不是“最后发布者获胜”的进程级全局配置。

class _StubProcessor(BaseMultimodalProcessor):
    # 只用于承载 server_args 的最小桩：绕过 __init__ 直接构造实例
    async def process_mm_data_async(self, *args, **kwargs):
        raise NotImplementedError


def _make(**fields):
    processor = _StubProcessor.__new__(_StubProcessor)
    processor.server_args = ServerArgs(model_path="dummy", **fields)
    return processor


class TestFastImageProcessorDevice(CustomTestCase):
    def _device(self, processor, **platform):
        # patch.multiple 模拟 CPU / XPU / NPU 平台标志，隔离硬件探测
        flags = {"_is_cpu": False, "_is_xpu": False, "_is_npu": False}
        flags.update(platform)
        with patch.multiple(BASE, **flags):
            return processor._fast_image_processor_device(_Processor())

    def test_device_follows_the_instance_base_gpu_id(self):
        self.assertEqual(self._device(_make(base_gpu_id=3)), "cuda:3")

    def test_engines_in_one_process_keep_their_own_device(self):
        # 同一进程内两个 Engine 各自持有 base_gpu_id，设备必须各自独立
        first, second = _make(base_gpu_id=0), _make(base_gpu_id=5)
        self.assertEqual(self._device(first), "cuda:0")
        self.assertEqual(self._device(second), "cuda:5")

    def test_publishing_another_config_does_not_move_the_device(self):
        # 关键回归：发布另一个全局配置（base_gpu_id=7）不得影响已有实例
        from sglang.srt.runtime_context import get_context

        processor = _make(base_gpu_id=2)
        override = get_context().override_server_args(base_gpu_id=7)
        override.install()
        self.addCleanup(override.restore)
        self.assertEqual(self._device(processor), "cuda:2")

    def test_rl_on_policy_target_forces_cpu(self):
        processor = _make(base_gpu_id=3, rl_on_policy_target="fsdp")
        self.assertEqual(self._device(processor), "cpu")

    def test_cpu_and_xpu_platforms_win_over_base_gpu_id(self):
        processor = _make(base_gpu_id=3)
        self.assertEqual(self._device(processor, _is_cpu=True), "cpu")
        self.assertEqual(self._device(processor, _is_xpu=True), "xpu")

    def test_npu_glm4v_leaves_the_device_unset(self):
        # NPU 下 Glm4vProcessor 维持原语义：不设置 device
        class Glm4vProcessor:
            pass

        processor = _make(base_gpu_id=3)
        with patch.multiple(BASE, _is_cpu=False, _is_xpu=False, _is_npu=True):
            device = processor._fast_image_processor_device(Glm4vProcessor())
        self.assertIsNone(device)

```

### `python/sglang/srt/multimodal/processors/base_processor.py`

核心逻辑变更：抽出 `_fast_image_processor_device`，设备选择从进程级 `get_server_args()` 改为实例 `self.server_args`，修复多 Engine 共享 tokenizer 进程时选错 GPU 的隐患。

```python
# python/sglang/srt/multimodal/processors/base_processor.py
# 变更前 process_mm_data 内联读取进程级 get_server_args()；变更后抽成独立方法，
# 设备决策改用实例自身携带的 server_args，调用处只关心是否要写 device。

def _fast_image_processor_device(self, processor) -> Optional[str]:
    """决定 fast image processor 的运行设备，返回 None 表示不设置 device。

    设备信息取自该 processor 实例自己持有的 server_args：多个 Engine 可以
    共享同一个 tokenizer 进程，而每个 Engine 的 base_gpu_id 各不相同，
    读进程级全局配置（last-publish-wins）会让一个 Engine 的图片预处理落到
    另一个 Engine 的 GPU 上。
    """
    server_args = self.server_args
    # RL on-policy 训练目标或纯 CPU 环境：一律落到 CPU
    if _is_cpu or server_args.rl_on_policy_target is not None:
        return "cpu"
    if _is_xpu:
        return "xpu"
    if not _is_npu:
        # 常规 CUDA 路径：跟随本实例的 base_gpu_id
        return f"cuda:{server_args.base_gpu_id}"
    # NPU 分支：qwen-vl 受 Ascend 维度限制有 reshape 问题，需先打补丁再选 npu
    if processor.__class__.__name__ not in {"Glm4vProcessor", "Glm46VProcessor"}:
        from sglang.srt.hardware_backend.npu.modules.qwen_vl_processor import (
            npu_apply_qwen_image_preprocess_patch,
        )

        npu_apply_qwen_image_preprocess_patch()
        return "npu"
    if processor.__class__.__name__ == "Glm46VProcessor":
        from sglang.srt.hardware_backend.npu.modules.glm46v_processor import (
            npu_apply_glm46v_image_preprocess_patch,
        )

        npu_apply_glm46v_image_preprocess_patch()
        return "npu"
    # 其余 NPU 处理器（如 Glm4vProcessor）保持 device 不设置，沿用原有分支顺序
    return None

# process_mm_data 中的调用处：30 行内联分支收敛为一次方法调用
if (
    hasattr(processor, "image_processor")
    and isinstance(processor.image_processor, BaseImageProcessor)
    and not self.disable_fast_image_processor
):
    device = self._fast_image_processor_device(processor)
    if device is not None:
        kwargs["device"] = device

```

# 评论区精华

本 PR 没有任何 review 评论（唯一的 issue 评论是 Gemini Code Assist 停服通知），作者在 body 中明确：全部 review 讨论与逐轮分诊都在前作 #33244 上，"the code here is identical to that PR's final revision"。可提炼的实质设计决策如下：

- 迁移原则："Nine reads move to the namespace accessors — the value they want is the resolved one, including post-publish overrides."
- 留守原则："What stays is the derived API … computed from several fields plus the HF config, so they are not namespace leaves and `ServerArgs` is their only home."
- 护栏 docstring 为 3 处 config-intent 读取逐一举证：`dsa_indexer.pp_size` 的短路求值是关键（PP 关闭时绝不触碰 `get_pp_group()`，这让 `Indexer` 能在分布式初始化前被构造）；`allocation.dcp_size` 问的是 " 是否配置了 DCP"，而 live property 会去读仅在 DCP 开启时才安装的 group；`cuda_ipc_transport_utils.tp_size` 运行在没有进程组的 tokenizer 进程里（调用点已守卫 not published yet）。

- 暂无高价值评论线程

# 风险与影响

- 风险：
 1. **GPU 路径未本地验证**：翻转的读取集中在 model / attention 路径（`gpt_oss.py`、`dense_mlp.py`、`mrope.py`、`two_batch_overlap.py`、`attention_registry.py`），作者明确说 speculative 与 model CI 才是真正的检验；若某调用时机早于命名空间发布，可能拿到默认值而非用户配置（作者认为投影自同一发布配置、value-preserving）。
 2. **实例契约变化**：`_fast_image_processor_device` 依赖 processor 构造时已持有 `server_args`，绕过构造器的创建路径会引入缺失属性错误；新测试用 `__new__` 手工装配桩恰好钉住该契约。
 3. **护栏维护成本**：别名形态基线 12 未归零，后续合法的新别名读取必须先降基线再改代码；全包 AST 扫描 + 基线绑定的模式会给并行开发造成轻微摩擦（新增读取即 CI 失败）。
 4. **进程级上下文覆盖**：`test_dllm_fdfo_kv_reuse.py` 现在会向进程级上下文安装 override，`addCleanup` 保证恢复，但同进程并行测试的隔离性仍需留意。
 - 影响：对库的使用者无 API 变化；对内部开发者是新的硬约束——配置决策必须读命名空间访问器（`get_exec()` / `get_serving()` / `get_model()` / `get_parallel()` / `get_schedule()`）或所属 runner / 实例，违者会被 CPU CI `base-a-test-cpu` 套件拦截。多 Engine 共享 tokenizer 进程的部署场景（encode-server DP）会因设备选择修复而行为变化：图片预处理不再跟随 " 最后发布的全局配置 "。该 PR 覆盖 13 个文件、横跨内存分配、注意力、多模态、模型加载与内核层，属配置架构迁移的收尾，后续新代码必须遵循同一模式。
 - 风险标记：跨模块配置迁移 , 核心路径变更 , GPU 路径未本地验证 , 静态扫描基线约束 , 进程级上下文覆盖

# 关联脉络

- PR #33244 config: retire the last process-global config field reads: 本 PR 前作：同一代码、同一最终修订版，因 GitHub 将 chained-base 系列视为 stack、阻塞 base retargeting 与合并路径而被关闭，全部 review 讨论留在该 PR（据 body 陈述）。
- PR #33294 test: stand up the config tiers two unit tests read from: 同一波 " 配置发布 / 真实配置 " 治理，同样改动了 managers/mm_utils.py 与配置类测试，二者共同把测试从 " 桩 + 全局替换 " 推向 " 发布真实配置 "。