Prhub

#33012 runtime_context: record the publishing process role

原始 PR 作者 ch-wan 合并时间 2026-08-01 06:06 文件变更 12 提交数 1 评论 5 代码增减 +166 / -45

执行摘要

runtime_context 记录进程角色,全进程入口迁移至角色化 publish

PR body 明确说明:role is provenance for now; per-role namespace projection and fail-closed enforcement come later。运行时存在多种角色进程(tokenizer / scheduler / encoder / expert_backup / weight_cache_daemon / launcher / test)共享同一套 runtime context 配置管道,但此前 set_global_server_args_for_schedulerset_global_server_args_for_tokenizer 是同一函数别名,进程语义被抹平;同时 _ConfigBag 依赖 __getattr__ 动态查找,在 torch.compile 编译的模型 forward 中读取配置(如 embedding 层的 get_exec().comm.enable_symm_mem)会造成 graph-break。本 PR 要为后续 per-role 配置投影和 fail-closed 强制校验建立角色记录基础(RFC #30696)。

值得精读。核心设计决策:(1) _ConfigBag 的"记账映射 + 真实属性"双写模式,解决了编译模型前向中配置读取的 dynamo 可追踪性问题,是一种可复制的模式;(2) _ServerArgsOverride 从"恢复发布对象"升级为"快照整个生命周期",避免嵌套作用域重投影导致的语义丢失;(3) legacy shim 刻意去别名,承认 tokenizer 与 scheduler 进程语义不同;(4) 角色先作 provenance、后做投影与 fail-closed 的分阶段落地思路务实。另外 TYPE_CHECKING 导入陷阱的修复过程(isort 挪导入 → 运行时 NameError → 无条件导入 + AST 扫描)也是对 Python 代码库的通用警示。

讨论亮点

Codex 机器人提出两条有效问题,作者均确认为真实缺陷并修复:

  1. P1(correctness)ray/scheduler_actor.pypublish 的导入被 isort 挪进 TYPE_CHECKING 块,Ray actor 运行时直接 NameError: name 'publish' is not defined,所有 Ray scheduler actor 初始化必挂;py_compile 无法捕获这种运行时名字错误。作者回应:修复为无条件模块级导入(用 stub ray 实际导入模块验证 publish 已绑定),并做全树 AST 扫描确认没有其他运行时使用的名字被 TYPE_CHECKING 守卫。
  2. P2(correctness):直接调用 get_context().set_server_args(...)(测试 override、draft worker 构建等合法路径)后,publish_role() 仍返回旧角色,provenance 与实际安装的生命周期不一致。作者回应:set_server_args 现在清除角色(直接安装即无角色),publish 仅在安装成功返回后赋值角色,并补回归测试 test_direct_install_clears_role

实现拆解

  1. 角色发布 API 与进程入口迁移:在 python/sglang/srt/runtime_context.py 新增模块级 publish(server_args, *, role=...)publish_role()RuntimeContext 增加 _publish_role 槽位;re-publish 语义为 last-publish-wins(重新投影配置袋、重置 provenance、覆盖角色)。随后逐入口迁移:ray/scheduler_actor.pySchedulerActor.__init__managers/scheduler.pyrun_scheduler_processmanagers/data_parallel_controller.pyrun_data_parallel_controller_processrole="scheduler" / role="dp_controller" 发布;disaggregation/encode_server.pyMMEncoder.__init__role="encoder" 发布(此前误用 scheduler setter);elastic_ep/expert_backup_manager.pyweight_cache/daemon.py 分别以 expert_backupweight_cache_daemon 发布。每个入口的注释都点明动机:Scheduler 构造会先于模型 worker 的 publish 读取配置命名空间。
  2. _ConfigBag 可追踪化改造:去掉 __slots__,让 _set / _set_sub 同时写入记账映射(_fields / _subs)与真实实例属性(__dict__),override 作用域管理器统一改走 _set_build_config_bags 改走 _set_sub__getattr__ 退化为兜底。效果是 bag.leaf / bag.sub 变成 dynamo 可追踪的普通属性加载,同时记账映射仍承担 override 路由、成员判断与作用域恢复的权威职责。
  3. 生命周期快照修复_ServerArgsOverride.install 从只快照 server_args 与 capture 开关,扩展为快照整个生命周期(_server_args_config_bags_overrides_log_publish_roleparallel._config、capture 开关),restore 原样复原而非重投影,保证嵌套作用域逐字恢复;preserve_config 同步快照/恢复 _publish_rolebuild_draft_tp_workerpreserve_config 作用域内发布 draft 配置副本,使 draft 层解析自己的配置且 target 的 post-publish override 可在恢复后存活;overrides_log() 改为返回逐条拷贝,防止外部调用方就地污染 provenance 记录。
  4. legacy shim 去别名与 ratchetpython/sglang/srt/server_args.pyset_global_server_args_for_tokenizer 不再别名到 scheduler shim,而是独立函数以 role="tokenizer" 发布;scheduler shim 显式 role="scheduler"。配套 test/registered/unit/test_legacy_global_ratchet.py 把 legacy setter 调用计数 ratchet 从 5 下调到 4,体现进程入口迁移减少调用点。
  5. 测试配套test/registered/unit/test_runtime_context_override.py 新增 test_publish_records_roletest_legacy_shims_record_rolestest_reset_clears_roletest_direct_install_clears_roletest/registered/unit/test_runtime_context.pytest_tokenizer_alias_is_same_function(断言 alias)反转为 test_tokenizer_alias_is_distinct_role_shim(断言 assertIsNot)。
文件 模块 状态 重要度
python/sglang/srt/runtime_context.py 配置上下文 modified 8.29
python/sglang/srt/server_args.py 启动参数 modified 6.58
python/sglang/srt/ray/scheduler_actor.py 调度器 modified 6.15
test/registered/unit/test_runtime_context_override.py 配置测试 modified 6.49
python/sglang/srt/managers/scheduler.py 调度器 modified 5.54
python/sglang/srt/weight_cache/daemon.py 权重缓存 modified 5.07
python/sglang/srt/disaggregation/encode_server.py 编码器 modified 5.07
python/sglang/srt/elastic_ep/expert_backup_manager.py 弹性 EP modified 5.07
python/sglang/srt/managers/data_parallel_controller.py DP 控制器 modified 4.99
test/registered/unit/test_runtime_context.py 配置测试 modified 4.77
python/sglang/srt/speculative/draft_worker_common.py 投机解码 modified 4.52
test/registered/unit/test_legacy_global_ratchet.py 配置测试 modified 3.45

关键符号

publish publish_role _ConfigBag._set _ConfigBag._set_sub _ServerArgsOverride.install _ServerArgsOverride.restore RuntimeContext.preserve_config RuntimeContext.overrides_log set_global_server_args_for_scheduler set_global_server_args_for_tokenizer run_scheduler_process SchedulerActor.__init__ MMEncoder.__init__ run_expert_backup_manager_process run_data_parallel_controller_process

关键源码片段

python/sglang/srt/runtime_context.py core-logic

核心变更文件:新增 publish/publish_role 角色发布与查询;_ConfigBag 从 __slots__ 迁移到真实实例属性以支持 torch.compile 追踪;_ServerArgsOverride 快照整个生命周期;overrides_log 返回副本。

class _ConfigBag:
    """已解析配置的命名空间袋。    数值在 ``publish`` 时从 ``server_args`` 快照而来,之后本袋是这些字段的
    **唯一事实来源**。读取是普通属性访问;裸赋值只读。    Leaf 与 sub-bag 存放为 **真实实例属性**(在 ``__dict__`` 中),因此
    ``bag.leaf`` / ``bag.sub`` 是普通属性加载,``torch.compile`` / dynamo
    可以追踪——编译后的模型 forward 中的配置读取(例如 embedding 层的
    ``get_exec().comm.enable_symm_mem``)不能 graph-break。
    ``_fields`` / ``_subs`` 仍是权威的 name→value 映射,负责 override 路由、
    成员判断与作用域恢复;``__getattr__`` 只兜底真正缺失的名字。
    刻意不用 ``__slots__``:leaf 是动态的,``__dict__`` 正是可追踪读取的来源。
    """
​
    def __init__(self, path: str):
        object.__setattr__(self, "_path", path)
        object.__setattr__(self, "_fields", {}) # {leaf: value} 记账映射
        object.__setattr__(self, "_subs", {}) # {subname: _ConfigBag} 记账映射
​
    def __getattr__(self, name: str) -> Any:
        # 仅兜底:真实 leaf / sub-bag 会先经 __dict__ 命中。这里用
        # object.__getattribute__(而非 self._fields)保证 __init__ 之前
        # 被调用也安全。
        fields = object.__getattribute__(self, "_fields")
        if name in fields:
            return fields[name]
        subs = object.__getattribute__(self, "_subs")
        if name in subs:
            return subs[name]
        path = object.__getattribute__(self, "_path")
        raise AttributeError(f"config namespace {path!r} has no leaf/subgroup {name!r}")
​
    def __setattr__(self, name: str, value: Any) -> None:
        # 对外只读:所有写入必须走 publish / override 通道。
        raise AttributeError(
            f"config namespace {self._path!r} is read-only; write via "
            "get_context().override(source, ...) or the scoped .override(**kw)"
        )
​
    def _set(self, name: str, value: Any) -> None:
        """内部写入(publish + override),绕过只读守卫。        同时更新记账映射和真实属性(可追踪读取),保证两处永远一致。"""
        object.__getattribute__(self, "_fields")[name] = value
        object.__setattr__(self, name, value)
​
    def _set_sub(self, name: str, sub: _ConfigBag) -> None:
        """注册嵌套 bag:同时进入记账映射与真实属性。        这样 ``bag.sub`` 是普通、可追踪的属性加载。"""
        object.__getattribute__(self, "_subs")[name] = sub
        object.__setattr__(self, name, sub)
​
    def __contains__(self, name: str) -> bool:
        return name in object.__getattribute__(self, "_fields")
​
    @contextmanager
    def override(self, **kwargs):
        """作用域化、事务性的测试专用 override(写前校验 key,退出时恢复)。"""
        fields = object.__getattribute__(self, "_fields")
        unknown = set(kwargs) - set(fields)
        if unknown:
            path = object.__getattribute__(self, "_path")
            raise ValueError(f"unknown config leaf for {path!r}: {sorted(unknown)}")
        saved = {name: fields[name] for name in kwargs}
        for name, value in kwargs.items():
            self._set(name, value)
        try:
            yield self
        finally:
            for name, value in saved.items():
                self._set(name, value)
python/sglang/srt/server_args.py core-logic

legacy setter 从别名关系拆分为两个带角色的独立 shim,是本 PR 角色语义的关键载体,同时受 ratchet 测试约束。

# NOTE: 进程级 ServerArgs 由 runtime context(sglang.srt.runtime_context)持有。
# 下面两个函数是 LEGACY shim,保留给既有调用方;它们按引用发布 / 读取同一个
# 活对象。不要新增调用方——调用次数由
# test/registered/unit/test_legacy_global_ratchet.py 以 ratchet(只减不增)约束。
# 函数内 import 是为了让两个模块在 import 期保持无环。
def set_global_server_args_for_scheduler(server_args: ServerArgs):
    """Legacy publish shim(role=scheduler)——新代码请改用
    ``runtime_context.publish(server_args, role=...)``。"""
    from sglang.srt.runtime_context import publish
​
    publish(server_args, role="scheduler")
​
​
def set_global_server_args_for_tokenizer(server_args: ServerArgs):
    """Legacy publish shim(role=tokenizer)。刻意不与 scheduler shim 做别名:
    两个进程的角色不同,去别名后角色记录才能反映真实进程语义。"""
    from sglang.srt.runtime_context import publish
​
    publish(server_args, role="tokenizer")
​
​
def get_global_server_args() -> ServerArgs:
    """Legacy accessor shim——新代码请改用
    ``get_server_args()``(来自 ``sglang.srt.runtime_context``)。"""
    from sglang.srt.runtime_context import get_context
​
    return get_context().server_args
python/sglang/srt/ray/scheduler_actor.py dependency-wiring

P1 讨论点所在文件:publish 导入曾被 isort 移入 TYPE_CHECKING 块导致运行时 NameError,修复为无条件模块级导入并在 actor 构造前以 scheduler 角色发布。

# 顶部无条件导入(不能放在 TYPE_CHECKING 下):Ray actor 运行时需要调用
# publish,若被 import 期跳过,会在 __init__ 中触发 NameError,且 py_compile
# 无法捕获这类运行时名字错误(isort 曾把该导入挪进 TYPE_CHECKING 块)。
from sglang.srt.runtime_context import publish
from sglang.srt.server_args import PortArgs, ServerArgs
​
​
@ray.remote
class SchedulerActor:
    """Ray actor wrapper for SGLang Scheduler.    Each actor manages one GPU and runs the Scheduler + TpModelWorker stack.
    Ray is used for process lifecycle; ZMQ handles request/response communication.
    """
​
    def __init__(
        self,
        server_args: ServerArgs,
        port_args: PortArgs,
        gpu_id: int,
        tp_rank: int,
        attn_cp_rank: int,
        moe_dp_rank: int,
        moe_ep_rank: int,
        pp_rank: int,
        dp_rank: Optional[int],
        dist_init_addr: Optional[str] = None,
    ):
        ...
        # 该 actor 直接构造 Scheduler(不走 run_scheduler_process),而
        # Scheduler.__init__ 会在 model worker 自己 publish 之前读取配置
        # 命名空间——因此必须先以 scheduler 角色发布解析后的配置。
        publish(server_args, role="scheduler")
​
        self.scheduler = Scheduler(
            server_args=server_args,
            port_args=port_args,
            gpu_id=actual_gpu_id,
            tp_rank=tp_rank,
            moe_ep_rank=moe_ep_rank,
            pp_rank=pp_rank,
            attn_cp_rank=attn_cp_rank,
            moe_dp_rank=moe_dp_rank,
            dp_rank=dp_rank,
        )
        ...

评论区精华

publish 导入被 isort 移入 TYPE_CHECKING 导致 Ray actor 运行时 NameError 正确性

Codex P1:SchedulerActor 中 publish 的导入位于 TYPE_CHECKING 块内,运行时永不执行,所有 Ray scheduler actor 初始化都会在构造 Scheduler 前抛 NameError。作者回应:isort 把顶层导入挪进了既有 TYPE_CHECKING 块,py_compile 无法捕获运行时 NameError;已修复为无条件模块级导入,用 stub ray 实际导入模块验证 publish 绑定,并做全树 AST 扫描确认无其他 runtime 使用的名字被 TYPE_CHECKING 守卫。

结论:publish 改为无条件模块级导入;补充 AST 全树扫描确认同类问题不存在。 · 已解决

直接 set_server_args 后 publish_role 返回陈旧角色(P2) 正确性

Codex P2:直接调用 get_context().set_server_args(...)(如测试 override、draft worker 构建)后,publish_role() 仍返回先前角色,与文档中 "None for a legacy set" 语义矛盾,provenance 不再描述当前安装的生命周期。作者确认属实,修复为 set_server_args 清除角色(直接安装即无角色),publish 仅在安装成功返回后赋值角色,并新增回归测试 test_direct_install_clears_role。

结论:set_server_args 清除角色,publish 在安装成功后赋值;补回归测试。 · 已解决

风险与影响

  1. 配置容器内部表示变更runtime_context.py_ConfigBag__slots__ 改为 __dict__ 动态属性,_set / _set_sub 的双写路径是把双刃剑——一旦记账映射与真实属性失同步,__getattr__ 兜底与 __dict__ 读取结果会分歧;当前 override/restore 全部收敛到 _set 单点写入,一致性依赖这条路径不被后续改动绕过。
  2. 进程入口大范围迁移:scheduler、DP controller、encoder、expert backup、weight cache daemon 六个进程入口同时改发布方式,任何一个入口漏发布或角色填错,在后续 fail-closed 阶段会被放大为启动失败;weight_cache/daemon.py 是首次引入角色(此前无 setter 调用语义),需要额外验证。
  3. 导入路径陷阱TYPE_CHECKING 守卫导入的教训说明静态检查无法拦截运行时 NameError,未来新增 runtime 使用的名字仍有被 isort 等工具挪进守卫块的风险;本次只做了单次全树 AST 扫描,没有形成持续校验机制。
  4. draft worker 构建路径build_draft_tp_workerpreserve_config 作用域内发布 draft 副本,若恢复逻辑遗漏某个生命周期字段(如 parallel._config),target 的 post-publish override 会丢失,影响投机解码配置的正确性。
  5. ratchet 行为约束server_args.py 的 legacy setter 计数 ratchet 从 5 降到 4,新代码若再调用 legacy setter 会 CI 失败——这是设计意图,但对不熟悉该约束的贡献者构成隐性行为限制。

对用户与外部 API 无可见变化,publish 角色是内部 provenance,不影响请求/响应语义。对系统而言,运行时配置从"无角色全局对象"走向"带角色语义的过程级配置",为 RFC #30696 的 per-role 命名空间投影与 fail-closed 校验奠定数据基础。对性能有正面影响:_ConfigBag 读取从 __getattr__ 动态查找变为 __dict__ 普通属性加载,配合 torch.compile 可减少编译前向中的 graph-break。对团队开发流程:新代码应优先使用 publish(server_args, role=...),legacy setter 受 ratchet 约束只能减少不能新增,overrides_log() 返回副本也杜绝了外部篡改 provenance 的隐患。

进程入口大范围迁移 配置容器内部表示变更 导入路径陷阱 role 语义约束

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论