news 2026/8/13 23:43:55

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【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_weight(0 拷贝) // 实际:每个 session 都 deep-copy 了一份 gpu_weight(1GB x N session) std::vector<OrtValue*> 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实现里,对 device(GPU)OrtValue走了“拷一份再挂到图上”的路径,而不是“原地接管/共享引用”。原因通常是:

  • 图的初始化器在内部用OrtValue的拷贝语义(值语义),默认AddExternalInitializers的契约是“把值加进图”,实现者图省事直接拷贝。
  • deviceOrtValue的拷贝涉及一次cudaMemcpy(设备内或跨流),既占显存又耗时。

对于大模型(几十 GB 权重),每个 session 拷一份直接爆显存,所以必须 in-place 共享。

三、根因

根因是AddExternalInitializers对 device(GPU)OrtValue走了深拷贝路径,而不是 in-place 共享引用(移动语义 / 共享所有权),导致每个 session 都复制一份 GPU 权重

  1. 默认值拷贝语义AddExternalInitializers内部把传入的OrtValue按值存入图的初始化器列表,对 GPU 张量触发一次设备内拷贝。
  2. 未用移动/共享:没有判断“如果传入的是 device OrtValue,就移动所有权或共享引用,不拷贝”,于是多 session 各自持有一份拷贝。
  3. 显存线性增长:N 个 session 就有 N 份权重拷贝,大模型直接 OOM。
  4. 不是结果错:拷贝出的权重值一样,推理正确,只是浪费显存和拷贝时间。

所以这不是数值错,而是API 对 device OrtValue 的共享语义没实现,导致本可共享的 GPU 权重被重复拷贝

四、最小可运行复现

下面用 C++ 标准库模拟“AddExternalInitializers 拷贝 vs in-place 共享”的显存差异:

#include <iostream> #include <vector> #include <memory> // 模拟 device OrtValue(GPU 张量),记录被拷贝次数 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::vector<DeviceOrtValue> addExternalInitializers_copy(const DeviceOrtValue& w) { std::vector<DeviceOrtValue> graph; graph.push_back(w); // 拷贝 return graph; } int main() { DeviceOrtValue weight(1'000'000'000); // 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 共享”收口成唯一的配置对象OrtAddExternalInitDeviceCopyPolicy,API 实现读它:

from dataclasses import dataclass, field from typing import Tuple, Literal @dataclass(frozen=True) 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 实现读同一份POLICY,device 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 数增长:

  1. 确认是不是 device OrtValue:传入的权重在 GPU 上、且每个 session 都多一份 -> 锁定隐式拷贝。
  2. 看 API 语义AddExternalInitializers是不是值拷贝,没用移动/共享。
  3. 查 session 数量:是不是每请求一 session(应改单例)。
  4. 临时规避:全局单例 session,只拷贝/共享一次。
  5. 根本修复:device OrtValue 走 in-place 共享(移动/引用计数),不拷贝。
  6. 统一策略对象:用OrtAddExternalInitDeviceCopyPolicy固化。
  7. 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 的大权重进图必须共享引用,隐式拷贝会直接撑爆显存。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/13 23:43:29

嵌入式工程师进阶指南:RTOS事件在工程中具体应用

普通的信号量存在两个弊端&#xff1a;一是无法避免优先级翻转问题&#xff0c;二是不具备实现一对多线程发送信号的能力。事件和信号量的设计初衷是一致的就是实现两个线程之间的同步&#xff0c;而事件比信号量更适合在多对多和多对一的场景上使用。现有一个要求如下&#xf…

作者头像 李华
网站建设 2026/8/13 23:43:17

数据安全审计系统架构设计与AI实践

1. 项目概述&#xff1a;数据安全审计的范式革命 去年参与某金融机构数据治理项目时&#xff0c;客户安全团队负责人向我展示过这样一组数据&#xff1a;他们每天需要处理来自网络设备、数据库、业务系统的审计日志超过200GB&#xff0c;但实际能有效分析的不足5%。这并非个例&…

作者头像 李华
网站建设 2026/8/13 23:42:11

SDC约束设计实战:从核心命令到工程避坑指南

1. 项目概述&#xff1a;SDC命令的江湖地位与核心价值 在数字芯片设计的江湖里&#xff0c;SDC&#xff08;Synopsys Design Constraints&#xff09;文件就是整个项目的“宪法”。它不写代码&#xff0c;却定义了芯片的“行为准则”——时钟怎么跑、信号怎么传、路径怎么约束。…

作者头像 李华
网站建设 2026/8/13 23:42:00

Python+Selenium自动化测试入门:从环境搭建到Page Object模式实战

1. 从“手动点点点”到“脚本自己跑”&#xff1a;为什么我们需要自动化测试&#xff1f;如果你是一名测试工程师&#xff0c;或者是一名需要对自己代码负责的开发&#xff0c;那么“自动化测试”这个词你一定不陌生。但很多时候&#xff0c;它就像一个挂在嘴边的“高级”概念&…

作者头像 李华
网站建设 2026/8/13 23:40:51

深入解析 OpenAI Node.js SDK 源码:架构设计与工程实践

1. 项目概述&#xff1a;为什么我们要读 openai-node 的源码&#xff1f; 如果你是一名 Node.js 或 TypeScript 开发者&#xff0c;并且正在或打算与 OpenAI 的 API 打交道&#xff0c;那么 openai-node 这个官方 SDK 大概率已经是你项目中的依赖项了。我们每天都在用 npm i…

作者头像 李华
网站建设 2026/8/13 23:40:20

运算放大器虚短虚断原理:从负反馈本质到电路分析实战

1. 从“玄学”到直觉&#xff1a;为什么运算放大器分析让你头疼&#xff1f;刚接触模拟电路&#xff0c;尤其是学到运算放大器这一章时&#xff0c;很多人都会在“虚短”和“虚断”这两个概念上卡壳。书上写的定义似乎都懂&#xff1a;理想运放下&#xff0c;同相端和反相端电压…

作者头像 李华