# PR #36006 完整报告

- 仓库：`sgl-project/sglang`
- 标题：refactor(disagg): register SGLANG_ENCODER_MM_LOAD_WORKERS in Envs
- 合并时间：2026-08-23 12:47
- 原文链接：http://prhub.com.cn/sgl-project/sglang/pull/36006

---

# 执行摘要

- 一句话：将编码器 I/O 线程数环境变量注册进 Envs 描述符
- 推荐动作：值得快速浏览，不必精读。PR 本身改动极小且论证充分（等价性表格 + 静态检查），作为 disaggregation 系列清理的收尾没有惊喜；更值得关注的是其 PR body 中识别的 `SGLANG_VLM_CACHE_SIZE_MB` 40 倍默认值差异——这是一个真实存在的配置一致性隐患，建议跟进单独修复。可借鉴的设计习惯是：坚持所有 `SGLANG_*` 变量注册进 `Envs` 并用描述符读取，使变量对测试可见、可 override、可审计，裸读环境变量会让默认值漂移变得不可见。

# 功能与动机

PR body 指出 `EncoderPreprocessor.__init__` 中两个相邻线程池初始化语句，一个遵循 env-var 约定（`envs.SGLANG_ENCODER_PREPROC_WORKERS.get()`），一个没有（`int(os.environ.get("SGLANG_ENCODER_MM_LOAD_WORKERS", 4))`）。按项目约定，sglang 自有的 `SGLANG_*` 变量都应注册在 `Envs` 类并通过描述符读取；裸 `os.environ` 读取对描述符 API 不可见，无法用 `.get()` 读取，更实际的是测试无法 `.override()` 它。作者还专门核实两个 knob 不是重复配置——`preproc_executor` 是 CPU 预处理池，`io_executor` 是媒体加载 I/O 池——因此不做合并。

# 实现拆解

1. **注册环境变量 **（`python/sglang/srt/environ.py`）：在 `Envs` 类的 "Encoder pipeline and disaggregation" 区块中，紧挨 `SGLANG_ENCODER_PREPROC_WORKERS = EnvInt(8)` 之后新增 `SGLANG_ENCODER_MM_LOAD_WORKERS = EnvInt(4)`，默认值与原先内联默认一致，避免行为漂移。
2. **迁移读取点 **（`python/sglang/srt/disaggregation/encoder/preprocessor.py`）：`EncoderPreprocessor.__init__` 中 `io_executor` 的 `max_workers` 从 `int(os.environ.get("SGLANG_ENCODER_MM_LOAD_WORKERS", 4))` 改为 `envs.SGLANG_ENCODER_MM_LOAD_WORKERS.get()`。这是该文件对 `os` 的唯一使用，因此删除 `import os`（`envs` 此前已导入），同时清掉一个孤儿导入。
3. **等价性与静态检查验证**：PR 用表格论证三种可达状态行为完全一致——变量未设置时均得 `4`；设置为合法整数 `"16"` 时均得 `16`；`.override(3)` 是描述符新增能力（裸读不可达）。固定版本 `ruff`（F401/F821/UP037）、`black`、`isort` 全部通过，其中 F401 正是用于确认删除 `import os` 后无残留引用。
4. **测试与部署配套**：PR 明确不新增测试（无可测行为变化），也未提供 accuracy/speed 数据（不涉及模型前向）。编码路径未在本地验证（需要 GPU 与在线 encode server），由 CI 的 EPD 测试覆盖，重跑 `test/registered/disaggregation/test_epd_disaggregation.py` 通过。
5. **遗留问题识别（不随本 PR 修复）**：PR body 指出 `disaggregation/encoder/server.py` 裸读 `SGLANG_VLM_CACHE_SIZE_MB` 且内联默认值为 `4096`，而 `Envs` 中注册的默认值为 `100`——变量未设置时 encode server 的缓存尺寸是全系统其余部分的 40 倍。作者有意不在本 PR 修正，因为任何方向的修正都会改变真实行为，需要由当初设定该缓存大小的 owner 决策。

关键文件：
- `python/sglang/srt/disaggregation/encoder/preprocessor.py`（模块 编码器；类别 source；类型 dependency-wiring；符号 EncoderPreprocessor）: 核心变更点：io_executor 线程池大小从裸 os.environ 读取改为 Envs 描述符读取，并删除该文件唯一的 os 导入。
- `python/sglang/srt/environ.py`（模块 环境配置；类别 source；类型 core-logic；符号 SGLANG_ENCODER_MM_LOAD_WORKERS）: 项目 SGLANG_* 环境变量的唯一注册表，新增 SGLANG_ENCODER_MM_LOAD_WORKERS = EnvInt(4)，使变量对描述符 API 与测试可见。

关键符号：EncoderPreprocessor.__init__

## 关键源码片段

### `python/sglang/srt/disaggregation/encoder/preprocessor.py`

核心变更点：io_executor 线程池大小从裸 os.environ 读取改为 Envs 描述符读取，并删除该文件唯一的 os 导入。

```python
# python/sglang/srt/disaggregation/encoder/preprocessor.py
# CPU 侧多模态预处理模块，服务于 EPD 编码器；模块文档注明未来可由 Rust 实现替换。
# 本 PR 只改动线程池初始化的读取方式，其余逻辑不变。

import asyncio
import concurrent.futures
import functools
import logging
# import os 已删除 —— 该文件对 os 的唯一使用被描述符读取替换后成孤儿导入
from dataclasses import dataclass
from typing import Callable, List, Optional, Tuple, Union

from sglang.srt.environ import envs

class EncoderPreprocessor:
    # __init__ 中两个相邻线程池：一个早已遵循 Envs 约定，一个是本次迁移对象
    def __init__(self, ...):
        ...
        # CPU 预处理线程池：控制 config 校验、HF processor 调用等 CPU 工作并发度
        self.preproc_executor = concurrent.futures.ThreadPoolExecutor(
            max_workers=envs.SGLANG_ENCODER_PREPROC_WORKERS.get()
        )
        # 媒体加载 I/O 线程池（图片 / 视频 / 音频读取）：原为裸 os.environ 读取，
        # 现统一走 Envs 描述符 —— 默认值同为 4，且测试可通过
        # envs.SGLANG_ENCODER_MM_LOAD_WORKERS.override(n) 注入确定性数值
        self.io_executor = concurrent.futures.ThreadPoolExecutor(
            max_workers=envs.SGLANG_ENCODER_MM_LOAD_WORKERS.get()
        )

```

### `python/sglang/srt/environ.py`

项目 SGLANG_* 环境变量的唯一注册表，新增 SGLANG_ENCODER_MM_LOAD_WORKERS = EnvInt(4)，使变量对描述符 API 与测试可见。

```python
# python/sglang/srt/environ.py —— 项目 SGLANG_* 环境变量的唯一注册表。
# 约定：sglang 自有的每个变量都必须注册为 EnvField 描述符，统一经 .get() 读取。

class Envs:
    # ===================================================================
    # Encoder pipeline and disaggregation
    # ===================================================================
    SGLANG_ENCODER_GRPC_TIMEOUT_SECS = EnvInt(60)
    # Encoder receiver selection: http|grpc (used by EPD paths).
    SGLANG_ENCODER_MM_RECEIVER_MODE = EnvStr("http")
    SGLANG_ENCODER_RECV_TIMEOUT = EnvFloat(180.0)
    SGLANG_ENCODER_MAX_BATCH_SIZE = EnvInt(8)
    SGLANG_ENCODER_PREPROC_WORKERS = EnvInt(8)
    # 媒体加载 I/O 线程池大小；与 PREPROC_WORKERS（CPU 预处理池）是并列的两个 knob
    SGLANG_ENCODER_MM_LOAD_WORKERS = EnvInt(4)
    # EncoderBootstrapServer health-check tuning. Interval == 0 disables it.
    SGLANG_ENCODER_BOOTSTRAP_HEALTH_CHECK_INTERVAL = EnvFloat(10.0)
    ...

```

# 评论区精华

该 PR 无任何 review comments（0 条），技术交锋为零。值得记录的讨论信息集中在 issue 评论与 PR body：作者发起 `/rerun-test test/registered/disaggregation/test_epd_disaggregation.py`，CI 机器人在 4-gpu-h100 上确认该测试通过（PR Test Base/Extra 此前为 ❌ 状态，AMD ROCm 7.2 仍在跑）。PR body 中作者主动澄清了两点：一是两个 worker 配置名相似但职责不同（预处理 vs I/O），不做合并；二是 `SGLANG_TORCH_PROFILER_DIR`、`ASCEND_MF_*` 等裸读被有意保留（仓库级既有模式或供应商前缀变量，另案处理）。最有价值的发现是 `SGLANG_VLM_CACHE_SIZE_MB` 的 40 倍默认值差异，作者说明 "correcting it changes real behavior in either direction"，并主动提出可单独开 PR 处理。

- 暂无高价值评论线程

# 风险与影响

- 风险：技术风险很低，但仍有三处需要留意：一是行为等价性依赖 `EnvInt.get()` 语义——`EnvField.get()` 对无法解析的值仅告警并返回默认值，与 `int()` 直接抛 `ValueError` 存在理论差异，PR 仅验证了两种合法可达状态；二是编码路径未在本地运行（需 GPU 与在线 encode server），改动依赖 CI 覆盖，重跑的 `test_epd_disaggregation.py` 通过但覆盖深度有限；三是遗留的 `SGLANG_VLM_CACHE_SIZE_MB` 默认值不一致（encode server 4096 MB vs 系统其余 100 MB）未被本 PR 修复，属于真实存在的配置分歧。此外本次未新增任何测试文件，无行为变化前提下可接受，但后续若调整默认值或解析语义应补上描述符行为的单元测试。
- 影响：对用户与系统运行无感知影响：线程池大小在三种状态下与原实现完全一致，改动仅发生在 `EncoderPreprocessor` 构造路径，不触碰模型前向、内核或网络路径。对团队与工程治理而言收益明显——该变量从此可被测试通过 `envs.SGLANG_ENCODER_MM_LOAD_WORKERS.override(n)` 注入确定性数值，且所有 encoder 相关环境变量读取收敛到 `Envs` 描述符，与仓库 config 系列重构（#35904–#35910）的理念一致，降低后续迁移与审计成本。影响范围极小：2 个文件、净 +2/-2 行。
- 风险标记：编码路径未本地验证（需 GPU 与 encode server）, 无新增测试覆盖 , 行为等价性依赖 EnvInt 解析语义 , 遗留 VLM 缓存默认值 40 倍差异未修

# 关联脉络

- PR #35838 refactor(disagg): remove unreferenced dead code: 同系列 disaggregation 清理的开端，同一作者 ShangmingCai，PR body 中列为关联项。
- PR #35847 refactor(disagg): collapse duplicated branches in get_kv_class: 同系列同模块重构，同样以行为等价的脚本验证为核心手法。
- PR #35890 fix(disagg): PD transfer-failure injection was silently inert: 同系列中同样涉及 env 变量读取规范化与测试 override 的 PR，理念一致。
- PR #35980 refactor(disagg): hoist staging helper imports out of the bootstrap loops: 同系列清理的收尾 PR，PR body 相互引用。