Prhub

#34267 config: pin the supplied-instance surface that a raw record would change

原始 PR 作者 ch-wan 合并时间 2026-08-15 15:40 文件变更 2 提交数 2 评论 55 代码增减 +1269 / -0

执行摘要

新增配置暴露面钉测与 EPD 扳机,护航 step-12 迁移

step-12 会让 ServerArgs 实例停留在用户原始输入,resolution 填充的字段不再出现在实例上。PR body 强调「Measured, not guessed: 297 distinct file/field pairs read one of the 125 fields resolution can write」;第二轴则钉住发布后经 get_context().override(...) 写入、supplied instance 仍然读取的字段——这类读取从 override 落地起就用启动值作答。目的不是列出 bug,而是把决定「resolved 值从哪来」的时机锁定在读取被写下的那一刻,而不是迁移翻转期间。

值得精读,尤其对正在做大型配置或数据契约迁移的团队:这是「用可执行测量钉住迁移前沿」的高质量示范。重点看三点:_MATRIX 驱动的 written set 派生与矩阵完备性规则(passed-in 字段排除、每个条目必须让 resolution 做决策);may-write 静态语义如何取代机制枚举;EPD tripwire 用真实解析而非固定 double 来保证翻转时必然报警。后续动作:step-12 flip 落地时逐项消化 _EXPOSED 清单,并把 chain 拼写(model_runner.server_args.field)扩展为独立 pin step。

讨论亮点

这是一次典型的「自动化 review 找洞、作者逐轮封堵」迭代,最有价值的交锋集中在 census 覆盖面与测试真实性上:

Codex(P2):getattr(server_args, 'field', default) 的读取被忽略,embedding_model_spec.pycuda_graph_config 等未入册。
ch-wan:Right — 收集器现在也数 getattr 字面量形式,三个例子全部 pin 进 _EXPOSED,计数 1121→1153 读、37→40 对;反向验证探针读取会失败。

Codex(P2):MMEncoder 把对象 park 成 self.server_argsencode_server.pyself.server_args.encoder_transfer_backend 未被统计。
ch-wan:census 现在跟踪同 class 内的 self.x = server_args 赋值并统计 self.x.field,新增 9 读 / 5 对;parking 到其他对象、容器或计算名仍不可见,docstring 已写明。

ch-wan(self-review):机制枚举每轮 review 都漏(DFLASH hook、MODEL_OVERRIDES 声明式 registry、alias 归一化循环),syntax 静默跳过还会伪装成表面收缩。
作者裁决:written set 采纳 may-write 语义 整体收集,解析失败 fail loudly,BOM 文件当场暴露;_EXPOSED 拆成 _EXPOSED + _EXPOSED_CUDA_ONLY,杜绝「共享精确列表无法表达 CUDA-only 写入」的死结。

ch-wan 最终 re-review(clean):Prior bugs and suggestions addressed on this head… Ratchet + EPD tripwire look solid. No residual findings.

实现拆解

  1. 新增双轴暴露面钉测主体test/registered/unit/test_supplied_instance_exposure_ratchet.py(+1165 行)中 TestSuppliedInstanceExposure 同时注册 CPU(base-a-test-cpu,估时 20 秒)与 CUDA(base-b1-gpu-small)套件。类覆写 _callTestMethod 让测试 body 只执行一次——ratchet 不匹配是确定性的,CI 重试会在 cleanup 栈半展开的状态下重入。
  2. 推导 resolution 写集合(轴 1)_MATRIX 每条目代表一个解析家族(EAGLE、DP、DWDP、HiCache、disaggregation、CP、LoRA、FP8 page_size、MIS 等),真实构造 ServerArgs 后与 dataclass 默认做 construct-and-diff;条目显式传入的字段被排除,因此 DWDP、MIS 这类自写字段必须有专属条目。每个条目解析前恢复 pristine 进程状态(environ + EnvField._set_to_none),SGLANG_IS_IN_CI 作为显式环境条目。declare_late_resolution 的写入者按关键字静态收集,动态 **detected 站点用 _LATE_RESOLUTION_DYNAMIC_SITES 表防漂移。此后 written set 又统一为 may-write 语义:把 hook 赋值(arg_groups/)、记录自身方法赋值(mooncake layout 重写、deepseek-EP 模式、random_seed 填充)、声明式 override registry(MODEL_OVERRIDES 与 provider 注册)、alias 归一化元组全部静态收集后与矩阵并集。
  3. 读取 census 与双向 pin:扫描全仓 AST,识别三种读取拼写——参数属性 server_args.field、字面量 getattr(server_args, 'field', default)、parked 形式 self.x = server_args 后的 self.x.field;owners(server_args.pyruntime_context.pyarg_groups/)豁免;source= 不计入字段。found = reads ∩ written_EXPOSED 双向比对:新 pair 失败、消失 pair 失败。解析失败的模块直接让测试失败(顺带发现 models/qwen2_classification.py 的 UTF-8 BOM,扫描改用 utf-8-sig)。精确 pin 按主机拆分:_EXPOSED 处处断言,_EXPOSED_CUDA_ONLY 仅 CUDA 断言(当前为空),AMD 有意不注册并写入 header 理由。
  4. 轴 2:post-publish override 收集:对 get_context().override(...) 做静态收集,解析 **kwargs 展开(dict 字面量、条件表达式、名称绑定 + 常量下标存储的 HiCache attach 形态),不可解析则失败;update_server_args 按名豁免(它的键集属于调用方)。该轴钉住 75 对 / 18 个字段。
  5. EPD tripwiretest_kimi_k3_encoder_mode.py 新增 test_epd_rejection_reads_the_resolved_transfer_backend,用迷你 Kimi-K3 配置真实解析一个 language-only TP2 启动,断言 encoder_transfer_backend == 'zmq_to_tokenizer'_reject_missing_dispatched_encoder_embedding 返回 503。测试前后恢复 environ 与 EnvField flags;mamba_radix_cache_strategy='no_buffer' 规避 CPU CI runner 无 GPU 栈的混合缓存分支。
文件 模块 状态 重要度
test/registered/unit/test_supplied_instance_exposure_ratchet.py 暴露面钉测 added 7.76
test/registered/unit/disaggregation/test_kimi_k3_encoder_mode.py 编码器模式 modified 6.48

关键符号

TestSuppliedInstanceExposure._callTestMethod TestSuppliedInstanceExposure.setUp _restore_process_state env_field_flags test_epd_rejection_reads_the_resolved_transfer_backend

关键源码片段

test/registered/unit/test_supplied_instance_exposure_ratchet.py test-coverage

PR 主体:双轴暴露面双向钉测,1165 行纯新增。包含 _MATRIX 解析派生 written set、三种读取拼写的全仓 census、_EXPOSED/_EXPOSED_CUDA_ONLY 双向 pin、进程状态隔离与 CPU/CUDA 双注册。

# test/registered/unit/test_supplied_instance_exposure_ratchet.py
# 双轴暴露面钉测:step-12 之后 ServerArgs 实例会保持用户原始输入,
# 凡 callee 从参数 server_args 读取「resolution 会填充」的字段,
# 都会从有效值变成 CLI 默认值。双向 pin:新增 pair 就是新的 step-12
# 工作,消失 pair 则须删除条目——清单是测量,不是记忆。# 每个矩阵条目只让 resolution 替它做决策;显式传入的字段会被排除。
# 因此 DWDP / MIS 这类「自己写字段」的家族必须专门设条目,
# 否则它们的写入永远进不了 written set。
_MATRIX = (
    {}, # 纯默认
    {
        'speculative_algorithm': 'EAGLE',
        'speculative_num_steps': 3,
        'speculative_eagle_topk': 1,
        'speculative_num_draft_tokens': 4,
    },
    {'dp_size': 2, 'tp_size': 2, 'enable_dp_attention': True},
    # DWDP 自己写 dp_size / enable_dp_attention / ep_size;上面的 DP 条目
    # 显式传入了这些字段,所以必须让 DWDP 条目自行解析,才能 pin 住
    # DP/EP 拓扑家族共 37 个读取对(launcher、控制器、tokenizer、spec workers)。
    {'tp_size': 2, 'dwdp_size': 2},
    {'enable_hierarchical_cache': True, 'hicache_ratio': 2.0},
    {'disaggregation_mode': 'prefill'},
    {'tp_size': 2, 'attn_cp_size': 2},
    {'enable_lora': True, 'max_lora_rank': 16},
    {'kv_cache_dtype': 'fp8_e4m3', 'page_size': 64},
    # MIS 自己写 disable_radix_cache;attention_backend 显式传入,
    # 因为 handler 只断言 flashinfer 而不是切换它。
    {'enable_mis': True, 'attention_backend': 'flashinfer'},
)# 只有真正的构造入参才豁免;tokenizer_path / served_model_name 总是由
# resolution 从 model_path 填充,它们的读取者与普通 pair 一样是暴露面。
_PASSED = frozenset({'model_path', 'device', 'random_seed'})# 双向精确清单(节选):新 pair 与消失 pair 都会让测试失败,
# 迫使「resolved 值从哪来」的决定发生在读取被写下的时刻。
_EXPOSED = {
    ('configs/embedding_model_spec.py', 'chunked_prefill_size'),
    ('configs/embedding_model_spec.py', 'cuda_graph_config'),
    ('configs/model_config.py', 'dtype'),
    ('configs/model_config.py', 'quantization'),
    ('disaggregation/encode_receiver.py', 'encoder_transfer_backend'),
    ('disaggregation/encode_server.py', 'tokenizer_path'),
    ('distributed/bootstrap.py', 'moe_a2a_backend'),
    ('entrypoints/engine.py', 'dp_size'),
    ('lora/marlin_lora_temp/policy.py', 'lora_paths'),
    ('managers/data_parallel_controller.py', 'soft_watchdog_timeout'),
    ('managers/tokenizer_manager.py', 'mm_feature_transport'), # parked 形式
}

评论区精华

getattr 字面量读取未计入 census 正确性

Codex(P2)指出收集器只识别 ast.Attribute,`getattr(server_args, 'field', default)` 的读取被忽略,例如 configs/embedding_model_spec.py 读 chunked_prefill_size 与 cuda_graph_config、BaseMultimodalProcessor 读 mm_feature_transport 均不在 _EXPOSED。

结论:作者扩展 census 覆盖 getattr 字面量形式,三个例子全部 pin 进 _EXPOSED,计数 1121→1153 读、37→40 对,并用探针读取反向验证 ratchet 会精确报出 pair。 · 已解决

parked 形式 self.x = server_args 与继承边界 正确性

Codex 先指出 MMEncoder.__init__ 把对象存为 self.server_args、encode_server.py 读 self.server_args.encoder_transfer_backend 未被统计;随后又给出 _Accumulator 基类 park、子类 _StatAccumulator 读取的继承场景。

结论:同 class 内的 parked 读取已入 census(新增 9 读 / 5 对,含 managers/tokenizer_manager.py 的 mm_feature_transport);继承场景作为已知边界处理,docstring 明确 parking 到其他对象、容器或计算名仍不可见。 · 已解决

矩阵完备性:DWDP / NGRAM / MIS 必须让 resolution 自己做决策 测试

三则 Codex 评论分别指出:DWDP 的 _handle_dwdp 自写 dp_size 等字段但没有条目触发;NGRAM 的 _handle_ngram 填充三个 speculative 字段;MIS 的 _handle_multi_item_scoring 写 disable_radix_cache。矩阵条目如果显式传入这些字段,就会被 written set 排除,导致对应读取永不 pin。

结论:作者补 {tp_size: 2, dwdp_size: 2}(DP/EP 拓扑家族 37 对,含 TokenizerControlMixin.init_communicators)与 {enable_mis, attention_backend: 'flashinfer'}(mem_cache/kv_cache_builder.py 等),并确立规则:条目必须让 resolution 做出它要测的那个决策。 · 已解决

post-publish override 的 **kwargs 展开 正确性

Codex 指出 get_context().override(..., **hicache_fields) 的 AST keyword 没有字面键,收集器会静默丢弃 HiCache attach 写入的 hicache_storage_prefetch_policy、hicache_write_policy 等字段。

结论:收集器现在静态解析 dict 字面量展开(字面量、条件之间的字面量、函数内绑定的名称加常量下标存储),无法解析则 fail loudly;update_server_args 按名豁免。由此 pin 住 6 个 HiCache 写 / 读对。 · 已解决

写集合采用 may-write 语义而非机制枚举 设计

作者记录:机制枚举每轮 review 都漏——DFLASH hook 洞、MODEL_OVERRIDES 声明式 registry(setattr 施加 dtype 等)、alias 归一化循环——每个机制都是赋值扫描看不见的。

结论:采纳 maintainer 裁决:written set 静态收集所有可写机制(hook 赋值、记录自身方法赋值、声明式 override registry、drift 守卫的 alias 元组、late-resolution 关键字),与矩阵并集后 pin 到 297 对 / 125 字段;静态收集到但永不触发的写点是上游该删的死分支,而不是缩小 census 的理由。 · 已解决

EPD tripwire 必须来自真实解析,且原始值应为 auto 测试

两轮 Codex 评论:初始版本把原始值当作 None,但 auto-resolved 启动的原始值是 ENCODER_TRANSFER_BACKEND_CHOICES[0] 即 'auto';随后又指出 fixed double(SimpleNamespace)在 step-12 翻转后依然返回 resolved 值,测试永远不红。

结论:tripwire 改用真实 Kimi-K3 TP2 迷你配置解析出 record,断言守卫以 503 拒绝且 encoder_transfer_backend == 'zmq_to_tokenizer';step-12 后同一启动传给守卫 'auto',拒绝静默停止,测试失败即信号。 · 已解决

进程状态隔离与 CI 重试 测试

作者 self-review 发现:矩阵连续 resolve 会留下 sticky 进程状态(如 SGLANG_SCHEDULER_SKIP_ALL_GATHER、multimodal transport 字段);EPD tripwire 只恢复 os.environ 会泄漏 EnvField 描述符标志;CustomTestCase 的 CI 重试会在 cleanup 半展开时重入。

结论:矩阵每个条目解析前恢复 pristine 状态(environ + MRO 遍历的 _set_to_none 标志);tripwire 同样快照恢复;类覆写 _callTestMethod 只跑一次 body。 · 已解决

单一精确 pin 无法表达 capability-gated 写入 设计

作者指出:CUDA-only 写入的读取对放进共享 _EXPOSED 会让 CPU 以 gone 失败,不放则 CUDA 以 new 失败;一个共享精确列表根本表达不了这种 pair。

结论:拆为 _EXPOSED(处处断言)与 _EXPOSED_CUDA_ONLY(仅 CUDA 断言,当前为空);AMD 有意不注册,理由写入 header(is_hip() 门控写会移动精确集合,任何 pinning host 都无法验证)。 · 已解决

census 剩余边界:chain 拼写、asdict、config_value、方法调用 question

Codex 提出 model_runner.server_args.field 的 chain 拼写(约 150 读)、dataclasses.asdict(server_args) 批量序列化、TokenizerManager.config_value('load_format') 动态读、get_attention_backends() 方法内字段读取均不在 census 内。

结论:chain 拼写已被 docstring 点名并作为独立 step;asdict / config_value / 方法调用属于已知边界,未在本 PR 内 pin。step-12 翻转时这些读取会静默看到默认值,是本 ratchet 的 false-negative 空洞。 · acknowledged

风险与影响

  1. census 已知盲区成为 step-12 切换时的未钉暴露面:chain 拼写 model_runner.server_args.field(约 150 读)、继承类 parked 读取、dataclasses.asdict(server_args) 批量序列化、config_value('load_format') 动态读、get_attention_backends() 方法内字段读取都不在 census 内,raw 实例下会静默看到默认值且本测试不会失败。
  2. written set 依赖矩阵与静态收集的完备性:任何未入 _MATRIX 的新解析家族、或声明式 registry 中非字面键的写入,仍可能漏出;hook may-write 收集缓解了机制枚举问题,但 update_server_args 按名豁免的面积取决于调用方键集。
  3. 主机差异:CPU 与 CUDA 的 written set 不同,_EXPOSED_CUDA_ONLY 当前为空;AMD 上 is_hip() 门控写入完全不在 pin 范围内(有意为之,但 ROCm 解析面没有等价精确 pin)。
  4. 测试基建风险:真实解析 + 全仓 AST 扫描,CPU 估时 20 秒;CUDA runner 不可用则 capability 面覆盖缺失;若新增 sticky 的 EnvField 状态而恢复逻辑未同步,会污染同 worker 后续文件。
  5. 开发摩擦:双向 pin 意味着任何新增的 server_args 读取 resolution 字段必须先入册,否则 CI 红;这是有意设计,但在 step-12 之前会给功能开发增加显式入册成本。

对用户与运行时零影响(纯测试)。对系统而言,它为 step-12「raw-input config」转换提供了精确到 (file, field) 的工作清单与回归防线,297 + 75 对覆盖 disaggregation、eplb、lora、managers、distributed、layers、entrypoints 等数十个模块。对团队而言,所有涉及 ServerArgs 字段读取的 PR 都会经过此 ratchet 的评审,配置一致性测试家族(与 test_resolution_is_reproducible.py 等)进一步成形;CI 新增 CPU(base-a-test-cpu)与 CUDA(base-b,1-gpu-small)双注册,扩展了覆盖面积。

纯测试无生产代码变更 census 已知盲区(chain 拼写等) 依赖真实解析矩阵与双 runner 双向 pin 增加开发摩擦

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论