三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

【Bug已解决】exception “!utils::HasExternalDataInMemory(tensor_proto) was false“ when loading model 解决方案

【Bug已解决】exception “!utils::HasExternalDataInMemory(tensor_proto) was false“ when loading model 解决方案

【Bug已解决】exception "!utils::HasExternalDataInMemory(tensor_proto) was false" when loading model 解决方案

一、现象长什么样

加载一个把权重拆到外部文件的 ONNX 模型(大模型,权重在.data外部数据文件里)时,构造InferenceSession直接抛异常:

onnxruntime.capi.onnxruntime_pybind11_state.RuntimeException: [ONNXRuntimeError] : 1 : FAIL : !utils::HasExternalDataInMemory(tensor_proto) was false

最小触发:

import onnxruntime as ort # 模型 xxx.onnx 引用了外部数据 xxx.onnx.data(权重在外部文件) sess = ort.InferenceSession("big_model.onnx", providers=["CPUExecutionProvider"]) # 抛 !utils::HasExternalDataInMemory(tensor_proto) was false

注意:模型文件在、.data文件也在同一目录,但就是加载失败。这是外部数据(external data)加载配置/路径的问题。

二、背景

ONNX 模型超过 2 GB 时,protobuf 单文件放不下权重,于是用“外部数据”机制:模型.onnx里每个大 tensor 只记录一个引用data_location = EXTERNAL+external_data字段,包含location相对路径和offset/length),真实权重存在同名的.data文件里。

加载时,ORT 需要:

  1. .onnx主文件,发现 tensor 是EXTERNAL
  2. 根据external_data.location解析出.data文件的路径(相对于模型文件所在目录,或配置的 base dir);
  3. 把外部数据读进内存(或 mmap),再组装成张量。

HasExternalDataInMemory(tensor_proto)这个检查的含义是:“这个 tensor_proto 标记了外部数据,那它对应的外部数据应该已经被加载进内存了”。抛was false表示:tensor_proto 说自己有外部数据,但加载器在内存里找不到对应的已加载数据—— 也就是外部数据没被成功读进来,而加载器却走到了“假定已加载”的分支。

三、根因

根因是外部数据没有被成功加载进内存(路径解析失败 / 未开启外部数据加载 /.data文件缺失或改名),但加载器仍按“外部数据已就位”继续,触发HasExternalDataInMemory断言失败

  1. 外部数据加载未开启或被跳过:ORT 某些加载路径需要显式允许读外部数据(尤其通过特定 API / session options),如果没开,tensor 的EXTERNAL标记还在,但数据没读,走到校验就was false
  2. 相对路径解析错external_data.location是相对路径,基准目录应该是模型文件所在目录。如果加载器用了错误的基准(比如当前工作目录、或某个临时解压目录),.data文件找不到,数据没加载。
  3. .data文件名/位置不对:实际数据文件被改名、挪走,或location指向的名字和实际文件不一致。
  4. 加载时序问题:先尝试“从内存 tensor_proto 取数据”(假定已加载),但外部数据根本没被LoadExternalData步骤读入,于是内存里没有,断言失败。

所以这不是模型算错,而是外部数据的加载链路断了(配置/路径/文件),导致“标记为外部数据却无内存数据”的矛盾

四、最小可运行复现

下面用伪代码 + Python 模拟“标记为外部数据但没加载进内存”的矛盾(不依赖真实 ORT,但精准复现断言触发的逻辑):

import os def load_model(model_path, allow_external=True, base_dir=None): """模拟 ORT 加载:标记外部数据的 tensor 必须已加载进内存。""" base_dir = base_dir or os.path.dirname(model_path) tensors = { # 主文件里:标记了外部数据,但"内存中是否就位"取决于是否成功读 .data "weight": {"external": True, "location": "big_model.onnx.data"}, } # 步骤:按 allow_external 与路径决定是否把外部数据读进内存 for name, t in tensors.items(): if t["external"]: data_path = os.path.join(base_dir, t["location"]) in_memory = allow_external and os.path.exists(data_path) # 加载器假定:标记了外部数据 -> 必须已在内存 if t["external"] and not in_memory: raise RuntimeError("!HasExternalDataInMemory(tensor_proto) was false") return "loaded" if __name__ == "__main__": # 情况 A:外部数据加载开启且 .data 在同目录 -> 成功 try: print(load_model("big_model.onnx", allow_external=True, base_dir="/models/big")) # 假定 .data 在此 except RuntimeError as e: print("A 失败:", e) # 情况 B:allow_external=False 或 base_dir 错 -> 触发断言 try: load_model("big_model.onnx", allow_external=False, base_dir="/wrong") except RuntimeError as e: print("B 触发断言:", e) # 正好复现 was false

跑出来情况 B 会抛出!HasExternalDataInMemory... was false,正是“标记为外部数据却没加载进内存”的精简复现。

五、解决方案(第一层:最小直接修复)

最小修复:确保外部数据被正确加载——开启外部数据加载,并让.data文件路径可被正确解析。对使用者:

import onnxruntime as ort so = ort.SessionOptions() # 关键:允许从外部数据文件加载(某些 ORT 构建/API 需要显式开启) so.add_session_config_entry("session.load_external_data", "1") # 关键:保证 .data 文件与 .onnx 在同一目录,且 location 名字一致 # 若 .data 在别的目录,用对应 API 指定 base dir(视 ORT 版本) sess = ort.InferenceSession( "big_model.onnx", so, providers=["CPUExecutionProvider"], # 部分 ORT 版本支持:sess_options 指定 external_data 目录 )

如果.data文件名被改了,改回external_data.location指向的名字;或把.data移到模型同目录。这一层立刻消除断言失败。

六、解决方案(第二层:结构性改进)

把“外部数据如何加载、从哪里加载”收口成唯一的配置对象OrtExternalDataLoadPolicy,所有加载逻辑读它:

from dataclasses import dataclass, field from typing import Tuple, Literal @dataclass(frozen=True) class OrtExternalDataLoadPolicy: """ONNX 外部数据加载的单一事实来源。""" # 是否允许加载外部数据 allow_external_data: bool = True # 外部数据基准目录解析策略:相对模型文件所在目录(推荐) base_dir_strategy: Literal["model_dir", "cwd", "explicit"] = "model_dir" # 显式基准目录(base_dir_strategy=explicit 时用) explicit_base_dir: str = "" # .data 文件命名必须与 external_data.location 一致 require_matching_location: bool = True # 加载失败时给出明确错误(而非静默走到断言) fail_loud: bool = True def resolve_base_dir(self, model_path: str) -> str: import os if self.base_dir_strategy == "model_dir": return os.path.dirname(os.path.abspath(model_path)) if self.base_dir_strategy == "cwd": return os.getcwd() return self.explicit_base_dir def describe(self) -> str: return "外部数据相对模型目录加载,开启开关,文件命名须与 location 一致" POLICY = OrtExternalDataLoadPolicy() def plan_external_load(model_path: str, policy: OrtExternalDataLoadPolicy = POLICY) -> dict: return { "allow": policy.allow_external_data, "base_dir": policy.resolve_base_dir(model_path), "fail_loud": policy.fail_loud, }

所有加载逻辑读同一份POLICY,外部数据加载配置被固化,不会再“标记外部却没加载”。

七、解决方案(第三层:断言 / CI 守护)

把“外部数据就位后才通过校验”做成断言。下面用 pytest 风格守护(复用第四节逻辑):

import os import pytest def test_external_allowed(policy): assert policy.allow_external_data is True def test_base_dir_is_model_dir(policy): assert policy.base_dir_strategy == "model_dir" def test_load_succeeds_when_data_present(tmp_path, policy): # 构造 .onnx + .data 同目录,应加载成功 (tmp_path / "m.onnx.data").write_bytes(b"\x00" * 16) base = policy.resolve_base_dir(str(tmp_path / "m.onnx")) assert os.path.exists(os.path.join(base, "m.onnx.data")) def test_fail_loud_when_missing(policy): assert policy.fail_loud is True

这四组断言锁住:(1) 外部数据加载开启;(2) 基准目录是模型目录;(3).data同目录时存在可被解析;(4) 缺失时明确报错。CI 跑通即代表外部数据加载链路被守护。

八、排查清单

遇到!HasExternalDataInMemory(tensor_proto) was false

  1. 确认.data文件在不在:和.onnx同目录?名字是否等于external_data.location
  2. 看加载是否允许外部数据:session options 有没有开load_external_data
  3. 看基准目录.data的相对路径是相对模型目录解析的,确认加载器用的基准对。
  4. 改名/挪回:把.data放模型同目录、名字对齐location;或显式指定 base dir。
  5. 统一策略对象:用OrtExternalDataLoadPolicy固化加载配置。
  6. CI 守护:断言外部数据开启、基准正确、缺失明确报错。
  7. 不要嵌回大权重:保持外部数据拆分,但确保加载链路通。

九、小结

exception "!utils::HasExternalDataInMemory(tensor_proto) was false" when loading model的根因是:模型把权重存成外部数据(.data文件),tensor_proto 标记了EXTERNAL,但外部数据没有被成功加载进内存(未开启外部数据加载、相对路径解析错、或.data文件缺失/改名),加载器却走到“假定外部数据已就位”的分支,触发HasExternalDataInMemory断言失败。

最小修复是开启外部数据加载开关、保证.data.onnx同目录且名字匹配external_data.location;结构性改进是用唯一的OrtExternalDataLoadPolicy固化加载配置;CI 用四组断言守护“外部数据开启、基准为模型目录、缺失明确报错”。记住:标记了外部数据的 tensor,加载器要求它已在内存,链路断了就会抛这个断言——先确认.data能被找到并加载。

← 返回列表