Prhub

#36300 config: the model-config cache keys on the path the record carried

原始 PR 作者 ch-wan 合并时间 2026-08-25 18:18 文件变更 2 提交数 1 评论 2 代码增减 +198 / -14

执行摘要

修复对象存储路径下模型配置缓存误失效导致启动崩溃

PR body 指出 test/registered/model_loading/test_runai_model_loader.py 是 2026-08-23 nightly 上 nightly-1-gpu-large 唯一失败的用例:所有 --load-format runai_streamer 启动都会在 scheduler 中崩溃,报错 server_args.model_config assigned after resolution; server_args is read-only。根因是 #35907 让 get_model_config() 在缓存对象报告的 model_path 与记录不一致时丢弃缓存,却把两种无关的路径移动合并了:resolution 移动记录的路径(GGUF/ModelScope 需要重建)与 ModelConfig 在构造函数内自移路径指向本地拉取目录(对象存储场景仍描述同一 checkpoint,不应重建)。

值得精读,特别是面向维护启动配置与参数解析的工程师。核心看点:(1) 如何区分"记录路径移动"与"配置自身路径移动",确定缓存失效键;(2) 只读保护如何通过 _CACHE_SLOTS 与缓存槽共存而不破坏"解析后配置不可变"的边界;(3) 测试如何用真实 ModelConfig 路径 + stub 下载锁定回归。

讨论亮点

PR 没有 review 评论;核心讨论体现在 PR body 与提交信息中。作者把两种路径移动区分开:resolution 会移动记录的路径(GGUF/ModelScope 场景,应重建),而 ModelConfig 在对象存储场景下自移 model_path(不应重建)。据此把缓存键定为"记录填充缓存时携带的路径"而非缓存对象自身的 model_path,既保留 #35907 的失效语义,又解决对象存储误重建。没有遗留的未解决疑虑。

实现拆解

  1. 缓存键语义调整(python/sglang/srt/server_args.py::get_model_config):新增私有属性 _model_config_built_from 记录填充缓存时的 self.model_path,命中判断从"缓存对象 model_path 是否等于记录路径"改为"记录自身路径自填充后是否移动":built_from is None or built_from == self.model_path 时直接返回缓存。built_from is None 兼容 fixture 注入的配置;重建时同时写回缓存对象和键。
  2. 只读保护放行缓存槽(python/sglang/srt/server_args.py::setattr 与 _CACHE_SLOTS):原保护按拼写分类,公共名称视为已解析配置,model_config 因此被误拦截。新增 _CACHE_SLOTS = frozenset({"model_config"}),在保护条件中加入 name not in _CACHE_SLOTS,使缓存槽在已解析记录上可写;下划线键(如 _model_config_built_from)本就豁免。同时修正 _underscore_field_names() 的 docstring。
  3. 回归测试(新增 test/registered/unit/server_args/test_model_config_cache.py,171 行):注册 CPU CI(register_cpu_ci(est_time=10, suite="base-a-test-cpu")),用真实 ModelConfig、is_runai_obj_uri、_maybe_pull_model_for_runai 路径,仅 stub 下载。四个用例覆盖对象存储保持缓存、GGUF/ModelScope 重建(#35907 失效语义不变)、已解析记录缓存 refill、fixture 配置直通,并处理 EnvField 与环境变量恢复。
文件 模块 状态 重要度
python/sglang/srt/server_args.py 配置解析 modified 6.38
test/registered/unit/server_args/test_model_config_cache.py 配置缓存 added 7.59

关键符号

get_model_config __setattr__ _underscore_field_names test_a_configuration_that_repoints_itself_stays_cached test_a_declared_model_path_rebuilds_the_configuration

关键源码片段

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

核心修复文件:get_model_config() 的缓存键改为记录填充时携带的路径(_model_config_built_from),并新增 _CACHE_SLOTS 让 __setattr__ 只读保护放行缓存槽写入,解决对象存储 URI 模型启动崩溃。

# python/sglang/srt/server_args.py —— get_model_config 缓存键语义
def get_model_config(self):
    # 延迟导入以避免循环依赖
    from sglang.srt.configs.model_config import ModelConfig
​
    memo = getattr(self, "model_config", None)
    if memo is not None:
        # 缓存键是填充缓存时记录携带的路径(_model_config_built_from)。
        # GGUF / ModelScope 处理器会声明不同的 model_path,配置构建于其前
        # 则描述另一份 checkpoint,需要重建;而对象存储 URI 场景下是
        # ModelConfig 自己在构造函数里把 model_path 指向本地拉取目录,
        # 记录的路径并未移动,缓存仍然有效。fixture 注入的配置没有键,直接返回。
        built_from = getattr(self, "_model_config_built_from", None)
        if built_from is None or built_from == self.model_path:
            return memo
​
    model_config = ModelConfig.from_server_args(self)
    self.model_config = model_config
    self._model_config_built_from = self.model_path
    if model_config.is_hybrid_swa:
        logger.info(
            "Hybrid SWA model detected. architectures=%s",
            model_config.hf_config.architectures,
        )
    return model_config
​
​
# 只读保护放行的缓存槽:记录从自身派生的值不属于已解析配置,
# 而可能使缓存失效的键必须能在已解析记录上写回。
_CACHE_SLOTS = frozenset({"model_config"})
​
​
def __setattr__(self, name, value):
    # 已物化后,字段是最终启动配置,默认只读;唯一例外是下划线字段
    # (记录自身簿记)以及 _CACHE_SLOTS 中列出的缓存槽。
    if (
        getattr(self, "_declarations_materialized", False)
        and not getattr(self, "_internal_write", False)
        and name not in _CACHE_SLOTS
        and (not name.startswith("_") or name in _underscore_field_names())
    ):
        raise AttributeError(
            f"server_args.{name} assigned after resolution; server_args is "
            "read-only -- use get_context().override(source, ...) to change "
            "resolved config; a value one runner owns travels as a "
            "constructor argument."
        )
    object.__setattr__(self, name, value)
test/registered/unit/server_args/test_model_config_cache.py test-coverage

新增的回归测试文件:用真实 ModelConfig 与对象存储拉取路径(仅 stub 下载)覆盖缓存键语义的四种形状,其中两个用例在未打补丁的 main 上会因 AttributeError 失败。

# test/registered/unit/server_args/test_model_config_cache.py —— 回归测试
class TestTheModelConfigCache(CustomTestCase):
    def setUp(self):
        # 解析过程会写环境变量并翻转 EnvField 的描述符标志;
        # os.environ 不携带后者,因此需要同时保存并恢复。
        fields = {}
        for klass in reversed(type(envs).__mro__):
            for name, field in vars(klass).items():
                if isinstance(field, EnvField):
                    fields[name] = field
        state = (
            dict(os.environ),
            {name: field._set_to_none for name, field in fields.items()},
        )
        self.addCleanup(self._restore, state)
​
    @staticmethod
    def _restore(state):
        saved_environ, saved_none_flags = state
        os.environ.clear()
        os.environ.update(saved_environ)
        for name, was_none in saved_none_flags.items():
            getattr(type(envs), name)._set_to_none = was_none
​
    def test_a_configuration_that_repoints_itself_stays_cached(self):
        # 对象存储形状:ModelConfig 自身把 model_path 指向本地拉取目录,
        # 记录仍保留 URI,缓存应保持命中。
        pulled = self._checkpoint()
        self._pulled_to(pulled)
​
        server_args = self._resolved(
            model_path=_OBJECT_STORE_URI, load_format="runai_streamer"
        )
        cached = server_args.__dict__["model_config"]
        self.assertIsInstance(cached, ModelConfig)
        self.assertEqual(server_args.model_path, _OBJECT_STORE_URI)
        self.assertEqual(cached.model_path, pulled)
        self.assertIs(server_args.get_model_config(), cached)

评论区精华

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

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

风险与影响

  1. 回归风险:失效条件改为 built_from != self.model_path 后,若未来有逻辑在构造配置前后修改 self.model_path 且不希望重建,可能被误判失效;测试覆盖了当前已知的 GGUF/ModelScope 与对象存储两条路径。
  2. 只读保护变宽:_CACHE_SLOTS 使 model_config 成为已解析记录上唯一可写的公共属性,其他代码若误写该属性不再被拦截(此前会抛 AttributeError)。这是有意豁免,但扩大了可变面。
  3. 影响范围:仅影响启动期配置构建,is_hybrid_swa 等决策读取新构建的配置,行为不变;不涉及推理路径,无性能与精度影响。
  4. 测试依赖:新测试 stub 了对象存储下载,注册为 CPU CI,若 runai_utils 接口变化需同步更新。

对用户:所有 --load-format runai_streamer 以及远程连接器(gs://、s3://、az://)的模型启动从崩溃恢复,Nightly CI 的 nightly-1-gpu-large 回归转绿。对系统:get_model_config() 在对象存储路径下不再重复重建 ModelConfig,减少启动期冗余解析;缓存失效语义更精确。对团队:新增的 4 个单元测试明确了缓存键边界,为后续 server_args 解析/只读机制重构提供回归保护。

对象存储启动路径 缓存键语义调整 只读保护豁免 新增回归测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论