已关闭
[Bug-Report|缺陷反馈]: TensorDesc 超出 int64 范围的维度被 ctypes 静默截断 #638
廖氏如神创建于  27 天前关闭于  8 天前
廖氏如神
廖氏如神
27 天前 创建

问题描述与影响

TensorDesc([2**64+7]) 传给实际 C API 边界的维度为 7,Shape 仍保留原值;构造、set_shape 与 set_origin_shape 都经过同一无范围校验。

影响:描述符携带与调用方不同的实际维度,形状计算和原生推导可能错误。仅确认边界数值截断,不声称已复现设备越界。

基线 cann/ge:develop,提交 ba12e592e8f84bc59709464bd3ad6ca010498d8b。任务直接从刷新后的上游建立。

环境与复现

本地 Windows / Python 3.12.14;测试执行实际生产 Python 实现。实际 enum 类、ctypes 缓冲区和 Python getter/setter/Shape/TensorDesc;只在 graph C API 边界使用局部替身。未验证真实 ABI 或设备,未分配巨大张量。

  1. 使用未修复基线,并仅加入本 PR 对应回归文件,保留原生产实现。
  2. 从仓库根运行下面的测试命令;回归给出实际断言失败。
  3. 应用修复后运行同一用例,并核对正常路径控制项。

触发示例/数据说明(完整构造与边界隔离见回归文件):

# 调用实际 TensorDesc([2**64 + 7]) 或 set_shape/set_origin_shape。
# 原版 ctypes.c_int64(2**64 + 7).value == 7,原生入口观察到错误维度 7。
# 预期在调用原生入口之前抛 ValueError,不需要分配巨大张量。
python -m pytest -q -p no:cacheprovider tests/ge/ut/ge/graph/pyge_tests/tensor_desc_dimension_range_cpu_test.py

预期行为

原生 int64 无法表示的维度在构造 ctypes 数组前抛 ValueError,设备入口不调用;int64 边界、0 和现有动态维度 -1/-2 保留。

根因与修改范围

实现 api/python/ge/ge/graph/tensor_desc.py。

_normalize_dims 只检查 Python int 类型,ctypes.c_int64 接受大整数并按 64 位截断。

共享维度归一化加入有符号 64 位范围校验,覆盖 Shape/TensorDesc 的构造和两个 shape setter。

验证证据

11 项:构造、set_shape、set_origin_shape 三入口各覆盖过大/过小/回绕为合法正数的维度,Shape 入口拒绝及合法 signed-int64/动态标记控制。核对实际 ctypes 缓冲区。

同一最终回归在未修复生产实现:10 failed, 1 passed in 0.13s;应用修复:11 passed in 0.07s。依赖/收集失败没有计作缺陷复现。

本地组合验证:26 passed in 0.18s;组合对象:!5294: DataType write dispatch。未验证真实 CANN/NPU、全量 C++ UT/ST。远端完整 CI 以当前 PR 提交对应 run 的实际终态为准,本地通过不代表远端门禁通过。

等级建议、重复核查与关联

建议 L2,依据上述实际功能影响提出,最终请维护者/SIG 评定,不预先认定活动奖励等级。

核查全部作者、全部状态 Issue/PR 的标题正文,并读取涉及四个仓库的 443 个开放 PR 实际文件差异。

修复 PR:https://gitcode.com/cann/ge/pull/5299,目标 develop。

AI 辅助说明

由 Codex 辅助源码核查、最小复现、修复、回归及文案整理;所有本地结果来自执行记录。未声称人工复核、真实设备或远端 CI 已通过。

likedislike
廖氏如神廖氏如神
27 天前 添加了label:bug-report
廖氏如神廖氏如神
27 天前 关联了pull request:fix(python): reject TensorDesc dimensions outside the native range (#638)
廖氏如神廖氏如神
27 天前 修改了issue 的描述
廖氏如神
廖氏如神
27 天前 评论:

/assign

likedislike
CANN-robotCANN-robot成员
27 天前 将 gcw_Z57idahf 设为负责人
KKenChow成员
15 天前 将 m0_50621083 设为负责人,移除负责人 gcw_Z57idahf
洪跃城
洪跃城成员
14 天前 评论:

已收到您的反馈,我们将会核实,请关注issue状态。

likedislike
洪跃城
洪跃城成员
10 天前 评论:

感谢反馈,也感谢您直接给出源码级定位、11 项回归用例和修复 PR。

结论:问题属实。这是 GE Python API(pyge)TensorDesc/Shape 维度归一化缺少 signed int64 范围校验导致的静默截断缺陷,属本仓(GE)责任田。我们在 master 225dd7adb 上逐条复核,与您在 develop ba12e592 基线上的判断一致。

先给一个便于对齐的事实:api/python/ge/ge/graph/tensor_desc.py 在这两个提交之间没有任何差异(git diff ba12e592 225dd7adb -- api/python/ge/ge/graph/tensor_desc.py 输出为空),因此下文行号在您的基线上同样适用。

核查证据

源码位置基于 main/ge(cann/ge master 225dd7adb)。

  1. 归一化只做类型校验,不做范围校验 —— _normalize_dims [main/ge/api/python/ge/ge/graph/tensor_desc.py:75]

    if not isinstance(dims, list) or not all(isinstance(dim, int) for dim in dims):
        raise TypeError(f"{arg_name} must be a list of integers")   # :79-80,仅类型
    return list(dims)                                               # :81,原值透传
    
  2. 三个入口共用同一条无校验路径,且都是先校验、后构造 ctypes 数组 —— 与您的描述一致:

    • TensorDesc.__init__:_normalize_dims [:119] → (ctypes.c_int64 * dims_num)(*dims) [:121] → GeApiWrapper_TensorDesc_Create [:122]
    • set_shape [:195] → _set_dims [:333]:_normalize_dims [:334] → (ctypes.c_int64 * dims_num)(*normalized) [:336]
    • set_origin_shape [:222] → 同一个 _set_dims [:333]
  3. 截断发生在 Python 侧,C 侧无从校验 —— GeApiWrapper_TensorDesc_Create [main/ge/api/python/ge/ge_api_c_wrapper/c_tensor_desc.cc:38] 与 GeApiWrapper_TensorDesc_SetShape [同文件:59] 的形参已经是 const int64_t *dims,BuildShape [同文件:21] 只判空指针后逐元素 emplace_back,拿到的就是截断后的值。所以范围校验只能加在 Python 构造 ctypes 数组之前,您的修复落点判断正确。

  4. Shape 保留原值、描述符携带截断值,两者确实会分歧 —— Shape.__init__ 直接 super().__init__(_normalize_dims(dims, "dims")) [:49],是纯 Python list,不经 ctypes,因此 Shape([2**64+7])[0] 仍是 2**64+7;而 desc.shape 走 _get_shape [:322] 从原生句柄读回,得到的是 7。同一份维度在 Python 对象与原生描述符上不一致,就是您说的"描述符携带与调用方不同的实际维度"。

不依赖 NPU 的最小复核

在仓库根目录执行,只从生产文件里取出 _normalize_dims 本体,不加载 libgraph_wrapper.so、不分配任何张量(本机已实测):

python3 - <<'EOF'
import ast, ctypes
from typing import List, Optional

src = open("api/python/ge/ge/graph/tensor_desc.py").read()
fn = next(n for n in ast.parse(src).body
          if isinstance(n, ast.FunctionDef) and n.name == "_normalize_dims")
ns = {"ctypes": ctypes, "List": List, "Optional": Optional}
exec(compile(ast.Module(body=[fn], type_ignores=[]), "<_normalize_dims>", "exec"), ns)
_normalize_dims = ns["_normalize_dims"]

for dims in ([2**64 + 7], [-2**63 - 1], [2**63 - 1], [-2**63], [0], [-1, -2]):
    norm = _normalize_dims(dims, "shape")        # 生产实现:无范围校验
    arr = (ctypes.c_int64 * len(norm))(*norm)    # 与 :121 / :336 同一构造
    print(f"caller={str(dims):22} native={str(list(arr)):22} "
          f"{'TRUNCATED' if list(arr) != norm else 'ok'}")
EOF

实测输出:

caller=[18446744073709551623]  native=[7]                     TRUNCATED
caller=[-9223372036854775809]  native=[9223372036854775807]   TRUNCATED
caller=[9223372036854775807]   native=[9223372036854775807]   ok
caller=[-9223372036854775808]  native=[-9223372036854775808]  ok
caller=[0]                     native=[0]                     ok
caller=[-1, -2]                native=[-1, -2]                ok

第 2 行值得单独强调:-2**63-1 回绕成 2**63-1,是一个完全合法的巨大正维度,原生侧不会报错,只会在后续 shape 计算里体现,比截断成 7 更难被发现。您把"回绕为合法正数"单列一类用例是必要的。后四行也确认了您给出的预期行为边界:int64 两端边界值、0、动态维度 -1/-2 都必须继续放行。

影响范围

同类"只校验 isinstance(dim, int)、随后直接构造 c_int64 数组"的写法在 pyge 里不止 tensor_desc.py 一处,PR #5299 未覆盖的至少还有:

  • Tensor.__init__ 的 shape 校验 [main/ge/api/python/ge/ge/graph/tensor.py:56] → c_int64 数组 [:119]、[:131]
  • GeUtils.infer_shape:_normalize_input_shapes 只判类型 [main/ge/api/python/ge/ge/utils/ge_utils.py:92] → flat_dims 数组 [:43]
  • TensorHolder.set_shape [main/ge/api/python/ge/ge/es/tensor_holder.py:186] → [:190]
  • GraphBuilder 建图入口的 shape [main/ge/api/python/ge/ge/es/graph_builder.py:210] → [:221],以及常量张量的 dims/values [:303]、[:351]、[:398]、[:445]、[:492]、[:520]

这些都不在本 issue 的复现路径上,是否随本次一并收敛由维护方评估,不需要您扩大 PR 范围。

另外确认了现有 UT 为何没拦住:test_shape_invalid [main/ge/tests/ge/ut/ge/graph/pyge_tests/tensor_desc_test.py:88]、test_set_shape_invalid [:146]、test_constructor_invalid [:153] 的参数集都是 "invalid" / [1, "invalid"] / [1.0, 2.0],断言的都是 TypeError,完全没有范围维度的用例。

后续

修复方向(在共享的维度归一化里加入 signed int64 范围校验,覆盖 Shape/TensorDesc 构造与两个 shape setter)我们认可,已关联 PR #5299,由维护团队按流程评审。评审时会一并关注三点:

  • 异常类型取舍:您选的是 ValueError。现有实现对类型错误抛 TypeError,而 Python 生态对数值溢出更常用 OverflowError(struct.pack、array 均如此)。两者都站得住,希望在 PR 里写明取舍理由,并保持 pyge 内部一致。
  • 边界放行清单:2**63-1、-2**63、0、-1、-2 必须继续通过;Shape.get_shape_size [:51] 与 is_unknown_shape [:66] 依赖 -1/-2 的动态维度语义,不能被新校验误伤。
  • 回归用例落点:建议并入 tests/ge/ut/ge/graph/pyge_tests/tensor_desc_test.py,与既有 test_shape_invalid / test_set_shape_invalid 同层参数化,避免新增独立文件后与现有用例重复覆盖。

改动影响性评估、同类入口是否统一收敛、回归验证均由维护方承担。合入时间与版本由维护团队统一评估,有进展会在本 issue 更新。

likedislike
CANN-robotCANN-robot成员
8 天前 关闭了 issue
CANN-robotCANN-robot成员
7 天前 添加了label:resolved