执行摘要
- 一句话:清除最后进程级配置读取,迁移至命名空间访问器
- 推荐动作:值得精读。核心看点是两层:一是 AST 静态扫描作为架构护栏的做法——把"进程级配置读取只准减少"编译进 CI,用双向断言(多了报违例、少了逼降基线)驱动渐进迁移;二是"什么该留守全局"的判别框架(派生 API、config-intent 读取、无进程组上下文),对理解
ServerArgs 与命名空间访问器的边界很有帮助。另需关注作者自述的验证缺口:model / attention 路径的翻转读取依赖 GPU CI,合入后应留意 speculative 与 model 套件结果。
功能与动机
PR body 开门见山:get_server_args().<field> reads the startup record of one process——进程级全局配置本质上是一个进程的启动记录,凡是需要解析后值(含 post-publish override)或每个 runner / 实例独立值的读取,它都不是正确载体。body 明确说明两类迁移理由:9 处读取有命名空间归宿(如 attention_backend ×5 应读 get_exec().kernel、skip_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 是同一最终修订版的重新提交。
实现拆解
- 注意力后端读取迁往执行命名空间(5 处 + 1 处内核):
mem_cache/allocation.py 的 write_cache_indices 与 get_last_loc、layers/rotary_embedding/mrope.py 的 get_cos_sin_with_position、models/gpt_oss.py 的 sink dtype 选择、batch_overlap/two_batch_overlap.py 的 derive_fields_related_to_seq_len_for_two_chunk、layers/attention/attention_registry.py 统一由 get_server_args().attention_backend 改为 get_exec().kernel.attention_backend;kernels/ops/layernorm/mhc.py 的 chunked-prefill 大小读取改走 get_schedule()。这些位置处于 decode / extend 热路径,取值语义不变,但获得的是含 post-publish override 的解析值。
- 服务与模型命名空间迁移(4 处):
managers/mm_utils.py 的 wrap_shm_features / unwrap_shm_features 中 2 处 skip_tokenizer_init 改读 get_serving();_acknowledge_deferred_cuda_ipc_cache_hits 的 consumer_count 由 get_server_args().tp_size 改为已持有的 parallel.tp_size(实时拓扑);models/inkling_common/dense_mlp.py 的 _shared_scales 中 dummy 加载判定改读 get_model().load_format(draft-aware 版本)。
- 多模态设备选择下沉到实例(本 PR 唯一的行为修复):
base_processor.py 抽出 _fast_image_processor_device(processor) 方法,从 self.server_args 解析设备,process_mm_data 原有约 30 行内联分支收敛为一次调用;NPU 的 qwen-vl / GLM46V 补丁逻辑、分支顺序以及 Glm4vProcessor 不设 device 的语义保持不变。
- 新增 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_size、get_model_config()、enable_mamba_extra_buffer() 等),_CONFIG_INTENT_SIZES 白名单豁免 3 处必须留守的 config-intent 读取(dsa_indexer.pp_size、allocation.dcp_size、cuda_ipc_transport_utils.tp_size)。测试是双向护栏:读数变多列出违现场,读数变少要求下调基线锁住进度。
- 测试与文档配套:
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(模块 配置守卫;类别 test;类型 test-coverage;符号 _is_global_call, _collect, counted, _field_reads): 新增 AST 静态扫描护栏:全包扫描 get_server_args().field 直接读取与函数内别名读取,直接基线 0、别名基线 12,白名单豁免派生 API 与 config-intent 读取,是本 PR 的架构约束核心。
test/registered/unit/multimodal/test_processor_device_selection.py(模块 设备选择;类别 test;类型 test-coverage;符号 _Processor, _StubProcessor, process_mm_data_async, _make): 新增回归测试:钉死 fast image processor 设备必须来自实例自身 server_args,覆盖多 Engine 同进程、发布其他配置不漂移、RL / CPU / XPU / NPU 各分支。
python/sglang/srt/multimodal/processors/base_processor.py(模块 多模态;类别 source;类型 core-logic;符号 _fast_image_processor_device, process_mm_data): 核心逻辑变更:抽出 _fast_image_processor_device,设备选择从进程级 get_server_args() 改为实例 self.server_args,修复多 Engine 共享 tokenizer 进程时选错 GPU 的隐患。
python/sglang/srt/mem_cache/allocation.py(模块 内存缓存;类别 source;类型 dependency-wiring;符号 write_cache_indices, get_last_loc): 内存分配热路径两处 attention_backend 读取改走 get_exec().kernel(write_cache_indices 与 get_last_loc),是解码路径上最重要的迁移点。
python/sglang/srt/managers/mm_utils.py(模块 多模态;类别 source;类型 core-logic;符号 _acknowledge_deferred_cuda_ipc_cache_hits, wrap_shm_features, unwrap_shm_features): 多模态工具三处迁移:skip_tokenizer_init ×2 改读 get_serving(),tp_size 改读 live parallel.tp_size,消除进程级读取。
python/sglang/srt/models/inkling_common/dense_mlp.py(模块 模型层;类别 source;类型 data-contract;符号 _shared_scales): 共享专家 FP4 加载的 load_format 判定改读 get_model(),取 draft-aware 解析值而非进程启动记录。
python/sglang/srt/layers/rotary_embedding/mrope.py(模块 位置编码;类别 source;类型 dependency-wiring;符号 get_cos_sin_with_position): 旋转位置编码的 Triton 分支判定改读 get_exec().kernel.attention_backend,属于注意力相关热路径迁移。
python/sglang/srt/models/gpt_oss.py(模块 模型层;类别 source;类型 data-contract): sink 参数 dtype 选择(trtllm_mha 要求 float32)改读 get_exec().kernel.attention_backend,消除模型侧进程级读取。
python/sglang/srt/batch_overlap/two_batch_overlap.py(模块 批重叠;类别 source;类型 dependency-wiring;符号 derive_fields_related_to_seq_len_for_two_chunk): 两批 overlap 的 compute_position 调用改读 get_exec().kernel.attention_backend。
python/sglang/srt/layers/attention/attention_registry.py(模块 注意力;类别 source;类型 core-logic): 注意力后端注册表同步迁移配置读取,属于 attention_backend 命名空间迁移的一部分。
test/registered/unit/mem_cache/test_dllm_fdfo_kv_reuse.py(模块 内存缓存;类别 test;类型 test-coverage;符号 setUp, tearDown, test_alloc_for_extend_mixed_reuse_allocates_only_fresh_and_writes_rows): 测试改为发布真实配置(override_server_args + addCleanup),替代会随读者迁移而失效的 monkeypatch 桩,是本 PR 测试治理的代表。
python/sglang/kernels/ops/layernorm/mhc.py(模块 内核层;类别 infra;类型 infrastructure): 内核层 chunked-prefill 大小读取改走 get_schedule(),是本次迁移中唯一位于 sglang/kernels 的读取点。
.claude/skills/sglang-runtime-context/SKILL.md(模块 开发文档;类别 docs;类型 documentation): 运行时上下文访问指南同步更新,记录命名空间访问器用法,属于配套文档。
关键符号:_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
新增回归测试:钉死 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
核心逻辑变更:抽出 _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
评论区精华
本 PR 没有任何 review 评论(唯一的 issue 评论是 Gemini Code Assist 停服通知),作者在 body 中明确:全部 review 讨论与逐轮分诊都在前作 #33244 上,"the code here is identical to that PR's final revision"。可提炼的实质设计决策如下:
风险与影响
- 风险:
- GPU 路径未本地验证:翻转的读取集中在 model / attention 路径(
gpt_oss.py、dense_mlp.py、mrope.py、two_batch_overlap.py、attention_registry.py),作者明确说 speculative 与 model CI 才是真正的检验;若某调用时机早于命名空间发布,可能拿到默认值而非用户配置(作者认为投影自同一发布配置、value-preserving)。
- 实例契约变化:
_fast_image_processor_device 依赖 processor 构造时已持有 server_args,绕过构造器的创建路径会引入缺失属性错误;新测试用 __new__ 手工装配桩恰好钉住该契约。
- 护栏维护成本:别名形态基线 12 未归零,后续合法的新别名读取必须先降基线再改代码;全包 AST 扫描 + 基线绑定的模式会给并行开发造成轻微摩擦(新增读取即 CI 失败)。
- 进程级上下文覆盖:
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 路径未本地验证, 静态扫描基线约束, 进程级上下文覆盖
关联脉络
- PR #33244 config: retire the last process-global config field reads: 本 PR 前作:同一代码、同一最终修订版,因 GitHub 将 chained-base 系列视为 stack、阻塞 base retargeting 与合并路径而被关闭,全部 review 讨论留在该 PR(据 body 陈述)。
- PR #33294 test: stand up the config tiers two unit tests read from: 同一波"配置发布 / 真实配置"治理,同样改动了 managers/mm_utils.py 与配置类测试,二者共同把测试从"桩 + 全局替换"推向"发布真实配置"。
参与讨论