【Bug已解决】AddExternalInitializers copies device (GPU) OrtValues per session instead of using them in …

发布时间:2026/8/13 23:43:59
【Bug已解决】AddExternalInitializers copies device (GPU) OrtValues per session instead of using them in … 【Bug已解决】AddExternalInitializers copies device (GPU) OrtValues per session instead of using them in place 解决方案一、现象长什么样用AddExternalInitializers把一个已经常驻 GPU 的OrtValue作为外部初始化器加到计算图里常见于把预加载的大常量权重直接挂到图上时发现每个新建的 session 都会把这份 GPU 张量完整拷贝一份而不是原地复用in-place / 共享引用// 预加载一个大权重到 GPU得到 device OrtValue Ort::Value gpu_weight loadWeightToGpu(...); // 比如 1GB 的 embedding 表 // 把它作为外部初始化器加进图 // 期望session 直接引用 gpu_weight0 拷贝 // 实际每个 session 都 deep-copy 了一份 gpu_weight1GB x N session std::vectorOrtValue* initializers {gpu_weight}; // AddExternalInitializers 内部对 device OrtValue 做了拷贝最小信号GPU 显存随 session 数线性增长每个 session 多一份权重拷贝 预期所有 session 共享同一份 GPU 权重in-place注意这不是结果错而是显存浪费 拷贝开销——大权重本可跨 session 共享却被每份 session 复制。二、背景AddExternalInitializers的作用是把一个OrtValue通常是常量权重注册成图的初始化器这样图在构造时就“拥有”这个权重不用每次run()都从外部传入。当这个OrtValue已经在GPU 设备上时理想情况是 session直接持有它的引用或共享所有权多个 session 共享同一块 GPU 显存。但 ORT 的AddExternalInitializers实现里对 deviceGPUOrtValue走了“拷一份再挂到图上”的路径而不是“原地接管/共享引用”。原因通常是图的初始化器在内部用OrtValue的拷贝语义值语义默认AddExternalInitializers的契约是“把值加进图”实现者图省事直接拷贝。deviceOrtValue的拷贝涉及一次cudaMemcpy设备内或跨流既占显存又耗时。对于大模型几十 GB 权重每个 session 拷一份直接爆显存所以必须 in-place 共享。三、根因根因是AddExternalInitializers对 deviceGPUOrtValue走了深拷贝路径而不是 in-place 共享引用移动语义 / 共享所有权导致每个 session 都复制一份 GPU 权重默认值拷贝语义AddExternalInitializers内部把传入的OrtValue按值存入图的初始化器列表对 GPU 张量触发一次设备内拷贝。未用移动/共享没有判断“如果传入的是 device OrtValue就移动所有权或共享引用不拷贝”于是多 session 各自持有一份拷贝。显存线性增长N 个 session 就有 N 份权重拷贝大模型直接 OOM。不是结果错拷贝出的权重值一样推理正确只是浪费显存和拷贝时间。所以这不是数值错而是API 对 device OrtValue 的共享语义没实现导致本可共享的 GPU 权重被重复拷贝。四、最小可运行复现下面用 C 标准库模拟“AddExternalInitializers 拷贝 vs in-place 共享”的显存差异#include iostream #include vector #include memory // 模拟 device OrtValueGPU 张量记录被拷贝次数 struct DeviceOrtValue { size_t bytes; static int copy_count; DeviceOrtValue(size_t b) : bytes(b) {} DeviceOrtValue(const DeviceOrtValue o) : bytes(o.bytes) { copy_count; } }; int DeviceOrtValue::copy_count 0; // 有 bug 的实现拷贝 device OrtValue 进图 std::vectorDeviceOrtValue addExternalInitializers_copy(const DeviceOrtValue w) { std::vectorDeviceOrtValue graph; graph.push_back(w); // 拷贝 return graph; } int main() { DeviceOrtValue weight(1000000000); // 1GB // 建 4 个 session每个都 AddExternalInitializers for (int i 0; i 4; i) addExternalInitializers_copy(weight); std::cout GPU 权重拷贝次数: DeviceOrtValue::copy_count 每个 session 拷贝一次 - 浪费 4GB\n; // 修复应移动/共享引用拷贝次数 0 }跑出来copy_count 4即 4 个 session 各拷贝一份 1GB 权重共浪费 4GB。修复后应当用移动/共享引用copy_count 0。这复现了“device OrtValue 被重复拷贝”的机制。五、解决方案第一层最小直接修复最小修复让AddExternalInitializers对 deviceOrtValue走 in-place 共享移动所有权或共享引用不拷贝。对使用者临时规避是减少 session 数量全局单例 session让拷贝只发生一次// 全局只建一个持有外部初始化器的 session所有推理复用 // 这样 device OrtValue 只被拷贝或共享一次 static Ort::Session* g_session nullptr; Ort::Session getSession(Ort::Env env, Ort::SessionOptions so, const char* path) { if (!g_session) { // 把 GPU 权重 AddExternalInitializers 后建 session仅一次拷贝 g_session new Ort::Session(env, path, so); } return *g_session; }对 ORT 仓库侧修复是改AddExternalInitializers若传入的OrtValue位于设备内存使用std::move或共享OrtValue的智能指针引用计数接管而不是值拷贝多个 session 共享同一份 deviceOrtValue。这一层立刻消除重复拷贝。六、解决方案第二层结构性改进把“外部初始化器对 device OrtValue 必须 in-place 共享”收口成唯一的配置对象OrtAddExternalInitDeviceCopyPolicyAPI 实现读它from dataclasses import dataclass, field from typing import Tuple, Literal dataclass(frozenTrue) class OrtAddExternalInitDeviceCopyPolicy: AddExternalInitializers device OrtValue 共享语义的单一事实来源。 # device OrtValue 的处理方式in-place 共享不拷贝 device_value_policy: Literal[in_place_share, move, copy] in_place_share # 是否允许多 session 共享同一 device OrtValue allow_cross_session_share: bool True # 受影响 API affected_api: Tuple[str, ...] (AddExternalInitializers,) # 拷贝必须显式声明默认禁止隐式拷贝 forbid_implicit_copy: bool True def should_copy(self, is_device_value: bool) - bool: if not is_device_value: return False return self.device_value_policy copy def describe(self) - str: return device OrtValue 进 AddExternalInitializers 必须 in-place 共享禁止隐式拷贝 POLICY OrtAddExternalInitDeviceCopyPolicy() def plan_add(ort_value_is_device: bool, policy: OrtAddExternalInitDeviceCopyPolicy POLICY) - str: return copy if policy.should_copy(ort_value_is_device) else in_place_share所有 API 实现读同一份POLICYdevice OrtValue 默认 in-place 共享杜绝隐式拷贝。七、解决方案第三层断言 / CI 守护把“device OrtValue 不被隐式拷贝”做成断言。下面用 pytest 风格守护复用第四节逻辑import pytest def test_device_value_in_place(policy): assert policy.device_value_policy in_place_share assert policy.should_copy(True) is False # device 不拷贝 def test_cross_session_share_allowed(policy): assert policy.allow_cross_session_share is True def test_forbid_implicit_copy(policy): assert policy.forbid_implicit_copy is True def test_host_value_no_copy_either(policy): # 即便 host 值按策略也不拷贝直接用 assert policy.should_copy(False) is False这四组断言锁住(1) device 值 in-place 不拷贝(2) 允许跨 session 共享(3) 禁止隐式拷贝(4) host 值也不无故拷贝。CI 跑通即代表共享语义被守护。八、排查清单遇到 AddExternalInitializers 让 GPU 显存随 session 数增长确认是不是 device OrtValue传入的权重在 GPU 上、且每个 session 都多一份 - 锁定隐式拷贝。看 API 语义AddExternalInitializers是不是值拷贝没用移动/共享。查 session 数量是不是每请求一 session应改单例。临时规避全局单例 session只拷贝/共享一次。根本修复device OrtValue 走 in-place 共享移动/引用计数不拷贝。统一策略对象用OrtAddExternalInitDeviceCopyPolicy固化。CI 守护断言 device 值不拷贝、可跨 session 共享。九、小结AddExternalInitializers copies device (GPU) OrtValues per session instead of using them in place的根因是AddExternalInitializers对位于设备内存的OrtValue走了值拷贝路径而不是 in-place 共享引用移动语义/引用计数导致每个新建的 session 都把这份 GPU 权重完整拷贝一份显存随 session 数线性增长、还有拷贝开销。最小修复是让 API 对 device OrtValue 走 in-place 共享移动/共享所有权临时规避是全局单例 session 只共享一次结构性改进是用唯一的OrtAddExternalInitDeviceCopyPolicy固化“device 值必须 in-place 共享、禁止隐式拷贝”CI 用四组断言守护“device 不拷贝、可跨 session 共享、禁止隐式拷贝”。记住常驻 GPU 的大权重进图必须共享引用隐式拷贝会直接撑爆显存。