Prhub

#53190 [Bugfix][EC Connector] Fall back when MADV_POPULATE_WRITE is unsupported

原始 PR 作者 wentian-byte 合并时间 2026-09-01 11:17 文件变更 2 提交数 2 评论 4 代码增减 +235 / -2

执行摘要

修复 EC 共享区在旧内核上的 madvise 崩溃

PR body 明确指出崩溃根因:MADV_POPULATE_WRITE 在 Linux 5.14 才引入,老内核上 mmap 模块没有该常量,getattr(mmap, "MADV_POPULATE_WRITE", 23) 的默认值 23 被传给不认识它的内核,madvise 返回 EINVAL,导致“Every code path that constructs an ECSharedRegion then aborts”。这是 #51116 在 SharedOffloadRegion(vllm/v1/kv_offload/cpu/shared_offload_region.py,merged ef2615c2e011,修复 #44888)中修复过的同一 bug,但 ec_shared_region.py 的 docstring 自称“Modeled after SharedOffloadRegion”,却是 #47423 单独引入、未同步该修复,因此 EC 连接器在 Linux < 5.14 上仍然崩溃。回退方案沿用 #51116 review 结论:用 |= 0 而非 = 0,避免破坏对等 worker 已写入共享 mmap 的 EC 数据。

值得精读,建议关注三点:(1) “单页探测 + 双路径回退”的内核特性兼容模式,是处理跨内核版本 API 差异的通用做法;(2) |= 0 读-改-写保留共享数据的技巧,以及对应的 mincore 页面驻留测试方法;(3) 该 PR 是典型的“重复实现未同步修复”教训——ec_shared_region.py 与 shared_offload_region.py 已两次出现同类 bug 不同步,值得跟进抽取公共 helper 的后续重构。整体改动小、测试扎实、无行为回归。

讨论亮点

整个 PR 的实质 review 交锋很少,核心只有一条:

  • omerpaz95(#47423 作者之一)评论:“Looks good. Please also add unit tests like in the original PR.”——这是唯一一次实质评审意见,作者随后在第二个 commit(b816fdd)补齐了与 #51116 对应的 5 个单元测试,该意见已解决。
  • wentian-byte 在 issue 评论中补充说明:vllm/distributed/ec_transfer/ 没有 CODEOWNERS,不会自动指派评审人,因此 @ 了 #47423 作者 orozery、omerpaz95 与 #51116 相关评审者,并请求打上 ready 标签跑完整 CI。
  • claude[bot] 提示:来自 fork 的 PR 自动评审被禁用,需要维护者手动触发 @claude review,最终未触发。
  • gty111 与 Isotr0py 均给出空 APPROVE,Isotr0py 以 /ci run 触发 Buildkite CI #86462 后合并。

实现拆解

变更入口是 vllm/distributed/ec_transfer/ec_connector/cpu/ec_shared_region.py,将原先 ECSharedRegion.init 内联的 getattr(mmap, "MADV_POPULATE_WRITE", 23) + madvise(...) 调用重构为模块级“探测 + 双路径回退”结构,具体分五步:

  1. 提升为模块级辅助函数:新增 _MADV_POPULATE_WRITE 常量与三个函数——_madvise_populate_write(原生 madvise 路径)、_fallback_populate_write(逐页触页回退)、_get_populate_write_fn(探测选择器);同时新增 errno、numpy、collections.abc.Callable 导入。模块级函数同时充当测试缝隙:Python 3.12+ 的 mmap.mmap 是不可变 C 类型,无法子类化或打补丁,独立函数可以直接被 monkeypatch 替换为 spy。

  2. 单页探测与选择逻辑:_get_populate_write_fn(mmap_obj) 先对一页(mmap.PAGESIZE)调用原生路径探测。仅当 OSError.errno == errno.EINVAL 时判定“内核不支持该 advice”,记录 warning 后返回 _fallback_populate_write;其他 OSError(如 EIO)原样抛出,避免把真实 I/O 故障误判为内核能力不足而静默掩盖。

  3. 回退实现:_fallback_populate_write 用 np.frombuffer 把 mmap 包装为 uint8 数组,对 [offset : offset + length : mmap.PAGESIZE] 切片执行 |= 0。逐页读-改-写会触发写时缺页使页面驻留,但字节内容不变——保证对等 worker 已写入的 EC 数据不会被清零,这是 #51116 review 中定下的决策。

  4. 调用点改造:init 中仅创建者(_is_creator)执行 populate_write_fn = _get_populate_write_fn(self._mmap_obj) 后对整个 region 调用一次预触页;非创建者既不探测也不触碰页面。内核 ≥ 5.14 时行为与原来完全一致。

  5. 测试配套:tests/v1/ec_connector/unit/cpu/test_ec_shared_region.py 新增 5 个测试与两个辅助设施(_region 上下文管理器、_page_residency 页面驻留探测)。测试通过 monkeypatch 把两个 helper 换成 spy 断言调用次数,并用 ctypes 调用 mincore 系统调用验证页面真实驻留状态。覆盖路径包括:原生路径 1 次探测 + 1 次全量预触页且 fallback 零调用;EINVAL 时对全量 region 走回退且 region 仍可读写;非创建者零调用;|= 0 保留已有字节且逐页驻留;非 EINVAL 的 OSError 向上传播(并手动清理 /dev/shm 残留文件)。

文件 模块 状态 重要度
vllm/distributed/ec_transfer/ec_connector/cpu/ec_shared_region.py EC 连接器 modified 7.5
tests/v1/ec_connector/unit/cpu/test_ec_shared_region.py EC 连接器 modified 7.16

关键符号

_get_populate_write_fn _madvise_populate_write _fallback_populate_write _page_residency test_madvise_success_selects_madvise_population test_madvise_einval_selects_fallback_for_whole_region test_non_creator_does_not_probe_or_populate test_fallback_populate_write_preserves_bytes_and_faults_pages test_madvise_unexpected_oserror_propagates

关键源码片段

vllm/distributed/ec_transfer/ec_connector/cpu/ec_shared_region.py core-logic

核心修复文件:将硬编码的 madvise(MADV_POPULATE_WRITE, advice=23) 调用重构为“单页探测 + 原生 / 逐页回退”双路径,修复 Linux < 5.14 上每次 ECSharedRegion 构造都因 EINVAL 崩溃的问题;回退用 |= 0 保留对等 worker 已写入的数据。

# vllm/distributed/ec_transfer/ec_connector/cpu/ec_shared_region.py
​
# Linux 5.14 才引入 MADV_POPULATE_WRITE(advice 值 23)。老内核上
# getattr 取默认值 23,内核不识别该 advice,madvise 直接返回 EINVAL
# —— 这正是本 PR 修复的崩溃根因。
_MADV_POPULATE_WRITE = getattr(mmap, "MADV_POPULATE_WRITE", 23)
​
​
def _madvise_populate_write(mmap_obj: mmap.mmap, offset: int, length: int) -> None:
    # 原生路径:请求内核一次性把 [offset, offset + length) 的页面预触到位。
    mmap_obj.madvise(_MADV_POPULATE_WRITE, offset, length)
​
​
def _fallback_populate_write(mmap_obj: mmap.mmap, offset: int, length: int) -> None:
    # 回退路径:逐页做一次读 - 改 - 写,触发写时缺页让页面驻留。
    # 用 |= 0 而不是直接清零是关键:对等 worker 可能已经向共享 mmap
    # 写入了 EC 数据,|= 0 不改变任何字节内容,只产生页面驻留效果。
    arr = np.frombuffer(mmap_obj, dtype=np.uint8)
    arr[offset : offset + length : mmap.PAGESIZE] |= 0
​
​
def _get_populate_write_fn(
    mmap_obj: mmap.mmap,
) -> Callable[[mmap.mmap, int, int], None]:
    # 单页探测一次,为整个 region 选定预触页方案。
    # 只把 EINVAL 当作“内核不支持”,其他 OSError(如 EIO)向上抛出,
    # 避免把真实 I/O 故障误判为内核能力不足而静默掩盖。
    try:
        _madvise_populate_write(mmap_obj, 0, mmap.PAGESIZE)
    except OSError as e:
        if e.errno != errno.EINVAL:
            raise
        logger.warning(
            "MADV_POPULATE_WRITE is not supported; falling back to per-page "
            "writes for EC mmap pre-population. Startup may be slower."
        )
        return _fallback_populate_write
    return _madvise_populate_write
# ECSharedRegion.__init__ 中的调用点替换为以下逻辑:
# 原来内联的 getattr + madvise 被拆成“探测一次 + 按结果选函数”,
# 且只由创建者执行;非创建者打开既有文件时不探测也不触碰页面。
if self._is_creator:
    populate_write_fn = _get_populate_write_fn(self._mmap_obj)
    populate_write_fn(self._mmap_obj, 0, total_size_bytes)
tests/v1/ec_connector/unit/cpu/test_ec_shared_region.py test-coverage

测试配套:新增 5 个单元测试覆盖原生路径、EINVAL 回退、非创建者零调用、|= 0 字节保留与异常传播;通过 spy 断言调用次数,并用 ctypes 调 mincore 验证页面真实驻留,是本次修复可信度的主要保障。

# tests/v1/ec_connector/unit/cpu/test_ec_shared_region.py
​
def _page_residency(mmap_obj: mmap.mmap, length: int) -> list[bool]:
    # 通过 mincore 系统调用读取 Linux 页面驻留位,用于验证预触页真的生效。
    # 之所以要观测真实内核状态,是因为 Python 3.12+ 的 mmap.mmap 是
    # 不可变 C 类型,无法子类化或打补丁,只能用行为证据来断言。
    try:
        libc = ctypes.CDLL(None, use_errno=True)
        mincore = libc.mincore
    except AttributeError:
        pytest.skip("mincore is unavailable")
​
    page_count = (length + mmap.PAGESIZE - 1) // mmap.PAGESIZE
    mincore.argtypes = [
        ctypes.c_void_p,
        ctypes.c_size_t,
        ctypes.POINTER(ctypes.c_ubyte),
    ]
    mincore.restype = ctypes.c_int
    vector = (ctypes.c_ubyte * page_count)()
    address = ctypes.addressof(ctypes.c_ubyte.from_buffer(mmap_obj))
    result = mincore(ctypes.c_void_p(address), length, vector)
    if result != 0:
        raise OSError(ctypes.get_errno())
    return [bool(value & 1) for value in vector]

评论区精华

要求补充单元测试 测试

omerpaz95(#47423 作者)在 PR 上评论:“Looks good. Please also add unit tests like in the original PR.”,要求像 #51116 那样补齐测试。

结论:作者在第二个 commit(b816fdd)补齐 5 个测试,覆盖原生路径 spy、EINVAL 全量回退、非创建者零调用、字节保留与异常传播,意见已解决。 · 已解决

维护者指派与 CI 触发 other

wentian-byte 说明 vllm/distributed/ec_transfer/ 无 CODEOWNERS、不会自动指派评审人,@ 了 #47423 作者与 #51116 相关评审者,并请求打上 ready 标签跑完整 CI;Isotr0py 随后 /ci run。

结论:Isotr0py 触发 Buildkite CI #86462,gty111 与 Isotr0py 空 APPROVE 后合并。 · 已解决

来自 fork 的自动 review 被禁用 other

claude[bot] 提示:该 PR 来自 fork,自动评审被禁用,需维护者手动评论 @claude review 触发一次性评审。

结论:未触发额外自动评审,维持人工 review 流程。 · 已解决

风险与影响

技术风险主要集中在新引入的回退路径与测试方式上:

  • 启动性能降级:回退路径需要逐页读写整个共享 region(arr[...::PAGESIZE] |= 0),大 region 时预触页耗时明显高于一次 madvise。仅影响 Linux < 5.14 的老内核,且已有 warning 日志提示“Startup may be slower”。
  • 共享数据保真约束:回退依赖 |= 0 的不动字节语义,一旦未来被“顺手优化”成 = 0 或清零赋值,会直接破坏对等 worker 已写入的 EC 数据。测试 test_fallback_populate_write_preserves_bytes_and_faults_pages 已用 MADV_DONTNEED + mincore 锁定该语义,但该测试在无法丢弃匿名页的内核上会 skip。
  • 重复实现漂移:ec_shared_region.py 与 shared_offload_region.py 是两份近似重复的实现,本次已是第二次出现“一份修了、另一份没修”的同类 bug,缺少机制性保障。
  • 异常传播的副作用:探测阶段的非 EINVAL 错误会在 init 中抛出,此时已创建的 /dev/shm/vllm_ec_{engine_id}.mmap 文件不会被 cleanup 回收(测试里需手动 os.unlink),生产环境出现 EIO 时可能残留临时文件。
  • 用户/系统:修复 Linux < 5.14(如 5.4 内核的发行版)上所有 ECSharedRegion 构造路径的启动崩溃,EC 连接器(encoder cache)在这些平台恢复可用;内核 ≥ 5.14 平台行为完全不变,零回归面。
  • 可观测性:回退发生时输出 MADV_POPULATE_WRITE is not supported warning,帮助用户区分“启动变慢”与“功能异常”。
  • 团队/工程:为 ec_shared_region 引入了与 shared_offload_region 一致的测试缝隙(模块级 helper + monkeypatch spy),两份实现口径首次对齐;但重复实现带来的漂移问题未被根治,后续可考虑抽取公共 helper。
回退路径启动变慢 共享数据保真约束 重复实现漂移风险 mincore 测试可能 skip

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论