Prhub

#35028 config: one control-plane log for the process

原始 PR 作者 ch-wan 合并时间 2026-08-18 07:19 文件变更 8 提交数 1 评论 5 代码增减 +245 / -95

执行摘要

tokenizer 配置日志并入进程级 override,新增 config_leaf 读侧

PR body 明确指出旧实现的问题:tokenizer manager 维护了一份与 runtime context 形状相同的 post-startup 配置变更日志 (source, fields) with provenance,并在每次输出消息时反向扫描它。作者的目标是 "a process carries one log and one provenance format, and reading one field is an attribute read"——即整个进程只保留一份配置变更日志,读取单个字段退化为一次属性访问,同时让持有字段名而非 namespace 的调用者(readback 端点、control-plane handler)不必关心字段落在哪个 bag 里。

值得精读,重点看三个设计决策:一是 config_leaf() 作为 override() 读侧的对称设计——读写共用 namespace_of 映射,调用者持字段名即可解析;二是 model_path / served_model_name 留在 manager 而非写 bag 的跨进程权衡;三是 test_supplied_instance_exposure_ratchet.py 的 AST 静态审计如何把 record_config_updates 包装器与循环变量键纳入解析,这类「防漂移 ratchet」写法对大型 Python 代码库的配置机制很有借鉴价值。

讨论亮点

5 条 review 评论全部来自作者 ch-wan 的自审,整体结论是 "The incremental migration is correct",bugs 数为 0,核心争议集中在文档表述滞后:

  • resolved_server_args_dict() 的新 docstring 声称 "every post-publish change is in this one log",但 model_path / served_model_name 是 manager 属性、并不进 _overrides_log_update_model_path_info 只通过 overrideload_format;评论者担心读者会误以为该 dict 携带权重重载后的路径。后续版本已重写该段,明确 per-process log 语义并点名 ModelRunner.update_model_fields 的差异。
  • SKILL.md 前两轮 review 被指出只有早期 stack 的 "Four ways" 材料、完全没提 config_leaf 与单 log 契约;第三轮确认文档补上了三块说明,但新段落仍误述 path 字段位置;第四轮确认已修正。
  • test_tokenizer_config_updates.pytest_readbacks_go_through_the_manager 断言失败消息仍写 "the update lives on the TokenizerManager",与新的 bag 语义矛盾,第四轮 review 仍标记为遗留问题(open),建议改为指向 config_value() / bags 并点名 path 例外。
  • 一个 nit:get_context 应并入原有的 from sglang.srt.runtime_context import ... 块;head 版本已合并。

实现拆解

本 PR 是一次围绕 config 读写机制的收敛重构,按以下 4 步落地:

  1. 新增读侧 API(python/sglang/srt/runtime_context.py:在 RuntimeContext 上新增 config_leaf(name),作为 override() 的读侧。它复用 namespace_of(type(self._server_args)) 映射把扁平字段名解析到 namespace 路径,再沿 bag 树的 _subs 子节点逐级下沉,最终 getattr(bag, name) 返回叶子值。未发布 config(_config_bags is None)或字段名没有 NS namespace 时抛出 ValueError,与写侧 override() 的错误语义对称。
  2. 折叠写侧与读侧(python/sglang/srt/managers/tokenizer_manager.py:删除 _SERVER_ARGS_FIELDS 字段校验与 self._config_updates 列表;record_config_updates() 直接转发 get_context().override(source, **fields)config_value()_MANAGER_OWNED_FIELDSmodel_path / served_model_name)返回 manager 属性,其余字段走 get_context().config_leaf(name)resolved_config_dict() 委托 get_context().resolved_server_args_dict(base) 后叠加两个 path 字段。crash dump 的 config_updates 数据源也从 list(self._config_updates) 改为 get_context().overrides_log()
  3. 暴露审计同步(test/registered/unit/test_supplied_instance_exposure_ratchet.py_override_written_fields() 现在把 record_config_updates 视为 override 的命名包装器,扫描其调用点(跳过转发调用本体);_expanded_override_keys() 新增 loop_variable_values() 辅助函数,解析 dict 键为循环变量(parser detection 按循环逐字段记录)的静态取值;_OVERRIDDEN_AND_READ 新增 entrypoints/engine.pyreasoning_parser / tool_call_parser 两对 pinned 断言。
  4. 测试与文档配套test_tokenizer_config_updates.py 改为先 publish(server_args) 再通过 get_context().overrides_log() 断言 provenance;test_serving_chat.pyutils.py 的 mock 从 _config_updates 列表改为 _config_overrides 字典以模拟 bag 语义;test_server_info.py 移除 _config_updates 赋值;.claude/skills/sglang-runtime-context/SKILL.md 补充了 config_leaf、单 log 契约、_MANAGER_OWNED_FIELDS 例外以及 "Four ways a config sweep breaks" 的防护清单。
文件 模块 状态 重要度
python/sglang/srt/managers/tokenizer_manager.py 入口进程 modified 6.86
python/sglang/srt/runtime_context.py 配置层 modified 7.03
test/registered/unit/test_supplied_instance_exposure_ratchet.py 暴露审计 modified 6.19
test/registered/unit/managers/test_tokenizer_config_updates.py 配置测试 modified 6.26
test/registered/unit/entrypoints/openai/test_serving_chat.py 对话服务 modified 5.15
test/registered/unit/entrypoints/openai/utils.py 测试工具 modified 4.47
test/registered/unit/entrypoints/test_server_info.py 服务信息 modified 3.59
.claude/skills/sglang-runtime-context/SKILL.md 开发文档 modified 4.14

关键符号

RuntimeContext.config_leaf RuntimeContext.overrides_log RuntimeContext.resolved_server_args_dict TokenizerManager.record_config_updates TokenizerManager.config_value TokenizerManager.resolved_config_dict TokenizerManager._update_model_path_info TokenizerManager._dump_data_to_file _expanded_override_keys.loop_variable_values _override_written_fields

关键源码片段

python/sglang/srt/managers/tokenizer_manager.py core-logic

核心逻辑变更:删除 manager 私有 _config_updates 日志,record_config_updates 改为 get_context().override 包装,config_value / resolved_config_dict 改从 config bags 读取,crash dump 数据源切换为 overrides_log。

def record_config_updates(self, source: str, **fields) -> None:
    """记录一次 control-plane 配置变更:权重更新、从 chat template 解析出的
    parser、HiCache mirror 挂载等。    # 这些变更与其它 post-publish 变更一样写入 config bags,因此整个进程
    # 只有一份 log 统一携带 provenance——本方法只是 override() 的命名包装。
    """
    get_context().override(source, **fields)
​
​
def config_value(self, name: str):
    """返回某个配置字段当前生效的值。"""
    if name in _MANAGER_OWNED_FIELDS:
        # model_path / served_model_name 刻意留在 manager 上:写进 bag 不会
        # 到达读取它们的其它进程,readback 从这里叠加。
        return getattr(self, name)
    # 其余字段走 bag:单次属性读,替代旧实现对私有 log 的逐条反向扫描。
    return get_context().config_leaf(name)
​
​
def resolved_config_dict(self, base: Dict[str, Any]) -> Dict[str, Any]:
    """在 ``base``(序列化后的 ServerArgs)之上叠加 control-plane 变更。"""
    resolved = get_context().resolved_server_args_dict(base)
    for name in _MANAGER_OWNED_FIELDS:
        resolved[name] = getattr(self, name)
    return resolved
python/sglang/srt/runtime_context.py dependency-wiring

新增 config_leaf() 作为 override() 的读侧,复用同一 namespace_of 映射解析字段名,未发布或非叶子时抛 ValueError;同时修正 resolved_server_args_dict 的 docstring 以准确描述 per-process log 语义。

def config_leaf(self, name: str):
    """按字段名返回一个生效中的配置叶子值——`override()` 的读侧。    # 调用者手里只有字段名、不知道它落在哪个 namespace bag 时(如 readback
    # 端点、control-plane handler),用这里统一解析;读写两侧共用
    # namespace_of 映射,保证解析路径一致。
    """
    bags = self._config_bags
    if bags is None:
        # bag 未发布说明配置还没解析完成,此时无法读取。
        raise ValueError("config not published; cannot read a config leaf")
    from sglang.srt.arg_groups.arg_utils import namespace_of
​
    path = namespace_of(type(self._server_args)).get(name)
    if path is None:
        # 字段名没有对应的 NS namespace,说明它不是被投影的 config 叶子。
        raise ValueError(f"{name!r} is not a config leaf (no NS namespace)")
    parts = path.split(".")
    bag = self.config_bag(parts[0])
    for seg in parts[1:]:
        # 逐级下沉到子 bag;_subs 是 bag 树节点的私有子表。
        bag = object.__getattribute__(bag, "_subs").get(seg)
        if bag is None:
            raise ValueError(f"subgroup {seg!r} missing under {path!r}")
    return getattr(bag, name)
test/registered/unit/test_supplied_instance_exposure_ratchet.py test-coverage

暴露审计(ratchet)随新架构扩展:学会把 record_config_updates 解析为 override 包装器,新增 loop_variable_values 解析循环变量键,并新增 entrypoints/engine.py 两对 parser 的 pinned 断言,防止读侧漂移。

def loop_variable_values(name: str) -> set:
    """收集 `for name, ... in (<字面量元组>)` 循环为 name 绑定的取值。    # 按循环逐字段记录配置的 handler 会把字段名写进循环的字面量里,
    # 因此这些 key 在设计上仍是静态可解析的。
    """
    values = set()
    for node in ast.walk(tree):
        if not isinstance(node, ast.For):
            continue
        target = node.target
        names = (
            [target]
            if isinstance(target, ast.Name)
            else list(getattr(target, "elts", []))
        )
        # 只关心首元素与目标变量同名、且循环包含本次 override 调用的场景。
        if not names or not isinstance(names[0], ast.Name) or names[0].id != name:
            continue
        if not (node.lineno <= call.lineno <= (node.end_lineno or node.lineno)):
            continue
        for item in getattr(node.iter, "elts", []):
            first = (
                item.elts[0] if isinstance(item, ast.Tuple) and item.elts else item
            )
            if isinstance(first, ast.Constant) and isinstance(first.value, str):
                values.add(first.value)
    return values

评论区精华

get_context 应并入现有 runtime_context 导入块 style

ch-wan 指出 `get_context` 被单独放在第二个 `from sglang.srt.runtime_context import ...` 块中,与已存在的上下文访问器导入重复。

结论:已处理,head 版本中 `get_context` 已并入第一个 import 块。 · 已解决

resolved_server_args_dict docstring 误述 path 字段所在 documentation

ch-wan 指出新段落声称所有 post-publish 变更都在这一个 log,但 model_path / served_model_name 是 manager 属性、不进 _overrides_log,_update_model_path_info 只通过 override 写 load_format;读者会误以为该 dict 携带权重重载后的路径。

结论:后续版本已重写该段,明确 per-process log 语义,并点名 scheduler 侧 ModelRunner.update_model_fields 与 tokenizer 侧只记录 load_format 的差异。 · 已解决

SKILL.md 应记录 config_leaf 与单 log 契约 documentation

前三轮 review 均围绕 SKILL.md:前两轮指出只记录了早期 stack 的 "Four ways" 材料,未提及 config_leaf、单 log 折叠与 path 字段例外;第三轮确认文档已补上但新段落仍误述 path 字段位置。

结论:最终 SKILL.md 已补充 config_leaf、record_config_updates 包装器、_MANAGER_OWNED_FIELDS 三块说明。 · 已解决

test_serving_chat.py docstring 仍教旧的两 log 世界观 documentation

ch-wan 指出 test_parsers_follow_the_control_plane_overlay 的 docstring 仍说 template detection "records the parsers on the manager",而该字段此时已走 override 落在 bags,只有 path 对是 manager-owned。

结论:建议改为 records the parsers through override / the bags;最终测试文件已同步更新表述。 · 已解决

test_tokenizer_config_updates.py 断言文本滞后于新架构 documentation

模块 docstring 已改为落在 runtime context,但 test_readbacks_go_through_the_manager 的断言失败消息仍写 "the update lives on the TokenizerManager",会误导后续重构把新 readback 又放回 manager。

结论:第四轮 review 仍标记为遗留问题(open),建议将断言指向 config_value / bags 并点名 path 例外。 · 待处理

风险与影响

  1. publish 时序依赖config_value() / record_config_updates() 现在强依赖 get_context() 已发布 config(_config_bags is not None),否则 config_leaf() / override() 直接抛 ValueError。测试已通过 publish(server_args) 保证前置条件,但生产路径(如 tokenizer worker 单进程启动、in-process 启动)必须先执行 set_global_server_args_for_tokenizer 对应的发布逻辑,若未来调整初始化顺序会导致启动期崩溃。
  2. 错误语义变化record_config_updates() 原来对未知字段报 "not ServerArgs fields",现在由 override() 报 "not a resolved config leaf"。语义从「必须是一等 ServerArgs 字段」变为「必须是 NS namespace 投影的叶子」,两者覆盖范围基本一致,但错误信息变化可能影响依赖旧文案的排障脚本或测试。
  3. crash dump 内容扩大_dump_data_to_file()config_updates 现在来自 get_context().overrides_log(),包含进程中所有来源的 override(不只是 tokenizer 记录的),dump 的字段集合会随其他模块的 override 调用变化,对比历史 dump 时需注意。
  4. 测试 mock 与真实实现的语义偏差test_serving_chat.py / utils.py 的 mock 用 _config_overrides 字典模拟 bag 语义,没有真正走 config_leaf() 的 namespace 解析与错误路径,若未来 namespace_of 映射变化,mock 不会第一时间暴露问题。
  5. 性能正向影响:消除了每次输出消息对私有日志的反向扫描,config_value() 退化为单次属性读;收益真实但量级很小,不是本 PR 的主要价值。

影响范围集中在 tokenizer manager 与 config 读取路径,无用户可见的对外 API 变化:

  • 系统/server_info、crash dump、resolved_config_dict() 等 readback 数据源从「manager 私有日志 + 反向扫描」变为「进程级 overrides log + 属性读」,provenance 格式全局统一。
  • 团队:后续新增 post-publish 配置变更只需调用 override()(或 record_config_updates() 包装),读侧一律走 config_leaf() / bag 属性,不再需要维护第二套日志与扫描逻辑;暴露审计自动把包装器调用点纳入 override 面,防止读侧漂移。
  • 进程模型model_path / served_model_name 继续由 tokenizer manager 持有并叠加,避免了把路径写入本进程 bag 却无法广播到其他进程的假象。
依赖 publish 时序 配置读取路径变更 错误语义变化 测试 mock 语义独立

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论