Prhub

#43464 Fix RunAI streamer tensor buffer reuse during weight loading

原始 PR 作者 bbartels 合并时间 2026-05-28 10:16 文件变更 2 提交数 2 评论 5 代码增减 +24 / -1

执行摘要

修复 RunAI streamer 张量缓冲区重用导致的数据损坏

Issue #43163 报告 GLM-5.1-FP8 模型使用 RunAI streamer 加载时输出乱码。经 bisect 定位到 ac3dac545 提交(索引器 upcast 融合)改变了权重加载流程,暴露了 streamer 缓冲区重用问题。

值得快速合并到 v0.22.0 milestone。代码量小、逻辑清晰、测试覆盖良好。可作为「流式迭代器内存安全」的经典案例。

讨论亮点

Reviewer noa-neria 指出初始测试设计缺陷:如果 Streamer 内部缓冲区足够大,不会触发重用,测试即使没有 clone 也会通过。解决方案是设置环境变量 RUNAI_STREAMER_MEMORY_LIMIT=0 强制缓冲区立即重用。最终测试按此建议实现。

实现拆解

  1. 源码变更vllm/model_executor/model_loader/weight_utils.py):在 runai_safetensors_weights_iterator 中将 yield from tensor_iter 改为 for name, tensor in tensor_iter: yield name, tensor.clone()。这一行改动确保每次迭代返回的张量是 streamer 内部缓冲区的独立副本,避免后续代码持有引用导致数据被覆盖。
  2. 测试配套tests/model_executor/model_loader/runai_streamer_loader/test_weight_utils.py):新增 test_runai_safetensors_weights_iterator_clones_reused_buffers 测试函数。该测试使用 monkeypatch.setenv("RUNAI_STREAMER_MEMORY_LIMIT", "0") 强制 streamer 重用内部缓冲区,然后构造两个小型 safetensors 张量并调用迭代器收集为字典。断言:键匹配、两个张量的 data_ptr() 不同(即非同一内存)、数值相等。该测试能可靠地检测缺少 clone 的回归。
  3. 旧测试保留:原有的 test_runai_model_loader 保持不变,继续验证 RunAI 加载结果与 HF safetensors 加载一致。
文件 模块 状态 重要度
vllm/model_executor/model_loader/weight_utils.py 权重加载器 modified 5.6
tests/model_executor/model_loader/runai_streamer_loader/test_weight_utils.py 测试 modified 5.53

关键符号

runai_safetensors_weights_iterator

关键源码片段

tests/model_executor/model_loader/runai_streamer_loader/test_weight_utils.py test-coverage

新增回归测试,强制 streamer 内存限制为 0 以触发缓冲区重用,验证 tensor 内存独立且数值正确。

# tests/model_executor/model_loader/runai_streamer_loader/test_weight_utils.py
# 新增测试:强制 streamer 重用缓冲区,验证 clone 后的 tensor 内存独立。
def test_runai_safetensors_weights_iterator_clones_reused_buffers(
    tmp_path, monkeypatch
):
    # 设置 RUNAI_STREAMER_MEMORY_LIMIT=0 强制 streamer 重用内部缓冲区
    monkeypatch.setenv("RUNAI_STREAMER_MEMORY_LIMIT", "0")
    weights_file = tmp_path / "model.safetensors"
    expected_tensors = {
        "first": torch.tensor([1.0, 2.0]),
        "second": torch.tensor([3.0, 4.0]),
    }
    save_file(expected_tensors, weights_file)
​
    actual_tensors = dict(
        runai_safetensors_weights_iterator([str(weights_file)], False)
    )
​
    assert actual_tensors.keys() == expected_tensors.keys()
    # 关键断言:两个 tensor 的数据指针不同,确认不是同一块内存
    assert actual_tensors["first"].data_ptr() != actual_tensors["second"].data_ptr()
    for name, expected_tensor in expected_tensors.items():
        assert torch.equal(actual_tensors[name], expected_tensor)

评论区精华

测试设计:如何强制缓冲区重用以验证 clone 测试

noa-neria 指出最初测试可能无法触发缓冲区重用,建议设置 RUNAI_STREAMER_MEMORY_LIMIT=0。

结论:采用 monkeypatch 设置环境变量 RUNAI_STREAMER_MEMORY_LIMIT=0 来强制内部缓冲区立即重用。 · 已解决

风险与影响

核心风险极低:仅改动一行代码(for 循环 + clone),不改变接口签名或逻辑结构。clone 引入了极小的内存和计算开销(约 O(参数数量) 的拷贝),但权重加载本身已是 I/O 与计算密集型,此开销可忽略。潜在回归:如果未来 RunAI streamer 内部行为改变(例如不再重用缓冲区),clone 仍是安全的(数据独立),不会破坏正确性。测试通过强制内存限制确保持续捕获回归。

影响范围:仅 RunAI streamer 加载路径的用户。修复了特定模型(如 GLM-5.1-FP8)在使用 RunAI streamer 时输出乱码的问题。与其他加载器(HF safetensors、fastsafetensors)无关。影响程度:重要 bugfix,解决数据损坏导致的功能不可用。

低风险 已覆盖测试

关联 Issue

#43163 [Bug]: GLM-5.1-FP8 produces gibberish with RunAI streamer after ac3dac545

完整报告

参与讨论