Prhub

#33338 config: retire the last process-global config field reads

原始 PR 作者 ch-wan 合并时间 2026-08-03 12:24 文件变更 13 提交数 1 评论 1 代码增减 +307 / -57

执行摘要

清除最后进程级配置读取,迁移至命名空间访问器

PR body 开门见山:get_server_args().<field> reads the startup record of one process——进程级全局配置本质上是一个进程的启动记录,凡是需要解析后值(含 post-publish override)或每个 runner / 实例独立值的读取,它都不是正确载体。body 明确说明两类迁移理由:9 处读取有命名空间归宿(如 attention_backend ×5 应读 get_exec().kernelskip_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 Engines can share a tokenizer process"。前作 #33244 因 GitHub 将 chained-base 系列视为 stack、阻塞合并而被关闭,本 PR 是同一最终修订版的重新提交。

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

讨论亮点

本 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. 注意力后端读取迁往执行命名空间(5 处 + 1 处内核)mem_cache/allocation.pywrite_cache_indicesget_last_loclayers/rotary_embedding/mrope.pyget_cos_sin_with_positionmodels/gpt_oss.py 的 sink dtype 选择、batch_overlap/two_batch_overlap.pyderive_fields_related_to_seq_len_for_two_chunklayers/attention/attention_registry.py 统一由 get_server_args().attention_backend 改为 get_exec().kernel.attention_backendkernels/ops/layernorm/mhc.py 的 chunked-prefill 大小读取改走 get_schedule()。这些位置处于 decode / extend 热路径,取值语义不变,但获得的是含 post-publish override 的解析值。
  2. 服务与模型命名空间迁移(4 处)managers/mm_utils.pywrap_shm_features / unwrap_shm_features 中 2 处 skip_tokenizer_init 改读 get_serving()_acknowledge_deferred_cuda_ipc_cache_hitsconsumer_countget_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_sizeget_model_config()enable_mamba_extra_buffer() 等),_CONFIG_INTENT_SIZES 白名单豁免 3 处必须留守的 config-intent 读取(dsa_indexer.pp_sizeallocation.dcp_sizecuda_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 配置守卫 added 7.62
test/registered/unit/multimodal/test_processor_device_selection.py 设备选择 added 7.09
python/sglang/srt/multimodal/processors/base_processor.py 多模态 modified 7.19
python/sglang/srt/mem_cache/allocation.py 内存缓存 modified 5.52
python/sglang/srt/managers/mm_utils.py 多模态 modified 5.4
python/sglang/srt/models/inkling_common/dense_mlp.py 模型层 modified 5.04
python/sglang/srt/layers/rotary_embedding/mrope.py 位置编码 modified 4.9
python/sglang/srt/models/gpt_oss.py 模型层 modified 4.8
python/sglang/srt/batch_overlap/two_batch_overlap.py 批重叠 modified 4.67
python/sglang/srt/layers/attention/attention_registry.py 注意力 modified 4.5
test/registered/unit/mem_cache/test_dllm_fdfo_kv_reuse.py 内存缓存 modified 4.75
python/sglang/kernels/ops/layernorm/mhc.py 内核层 modified 3.29
.claude/skills/sglang-runtime-context/SKILL.md 开发文档 modified 2.27

关键符号

_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 test-coverage

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

# 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 core-logic

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

# 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

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. GPU 路径未本地验证:翻转的读取集中在 model / attention 路径(gpt_oss.pydense_mlp.pymrope.pytwo_batch_overlap.pyattention_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 路径未本地验证 静态扫描基线约束 进程级上下文覆盖

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论