Skip to content

[QNN] 使用 QNN 后端转换 Qwen3-VL-2B 时,compilefornpu 发生段错误。 #4716

Description

@yydhYYDH

问题描述

使用 QNN 后端转换 Qwen3-VL-2B 的 llm.mnn 时,compilefornpu 发生段错误。

在没有任何本地源码修改的 MNN origin/master 上复现:

Commit: e1b8a9cbb1fc05d60a609901dc4ccbc2baa82fe3

进程退出码为 139。崩溃发生在 QNNStridedSlice::onEncode() 请求一个尚未注册到 QnnBackend::mTensorMap 的 tensor 时。QnnBackend::getTensorIdx() 将该 tensor 当作静态 tensor 处理,但它的 host buffer 是空指针,最终该空指针被传给 QnnFloatToHalf() 并引发段错误。

运行环境

操作系统: Linux 4.18.0-147.5.2.15.h1109.eulerosv2r10.x86_64
架构: x86_64
CMake: 4.0.3
GCC: 11.4.0
MNN Commit: e1b8a9cbb1fc05d60a609901dc4ccbc2baa82fe3
QNN SDK: 2.46.0.260424
构建类型: RelWithDebInfo

目标 Android 设备

SoC: Qualcomm Snapdragon 8 Elite
QNN SoC ID: 69
Hexagon DSP 架构: v79

段错误发生在 x86_64 Linux 主机运行 compilefornpu 的模型转换阶段,还没有执行到模型部署或手机端推理。

编译命令

export MNN_ROOT=/path/to/MNN
export QNN_SDK_ROOT=/path/to/qairt/2.46.0.260424

cmake -S "$MNN_ROOT" -B "$MNN_ROOT/build_qnn_repro" \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DMNN_BUILD_LLM=ON \
  -DMNN_LOW_MEMORY=ON \
  -DMNN_BUILD_LLM_OMNI=ON \
  -DMNN_QNN=ON \
  -DMNN_QNN_CONVERT_MODE=ON \
  -DMNN_QNN_ONLINE_FINALIZE=ON \
  -DMNN_WITH_PLUGIN=OFF \
  -DMNN_BUILD_TOOLS=ON \
  -DMNN_BUILD_CONVERTER=ON \
  -DMNN_SUPPORT_TRANSFORMER_FUSE=ON \
  -DQNN_SDK_ROOT="$QNN_SDK_ROOT"

cmake --build "$MNN_ROOT/build_qnn_repro" \
  --target compilefornpu \
  -j16

以上编译可以正常完成。

官方导出流程

最初是按照 MNN LLM 文档中推荐的 generate_llm_qnn.py 流程触发的

首先进入 MNN 的 LLM 导出目录并激活 Python 环境:

export MNN_ROOT=/path/to/MNN
export BUILD_ROOT="$MNN_ROOT/build_qnn_repro"
export MODEL_DIR=/path/to/Qwen3-VL-2B-MNN
export QNN_SDK_ROOT=/path/to/qairt/2.46.0.260424

export LD_LIBRARY_PATH="$QNN_SDK_ROOT/lib/x86_64-linux-clang:${LD_LIBRARY_PATH:-}"

cd "$MNN_ROOT/transformers/llm/export"
conda activate mnn

然后使用官方脚本转换 LLM:

python npu/generate_llm_qnn.py \
  --model "$MODEL_DIR" \
  --soc_id 69 \
  --dsp_arch v79 \
  --vtcm_mb 8 \
  --mnn_path "$BUILD_ROOT" \
  --chunk_size 128 \
  --max_history_token 1024

脚本首先成功生成了三组固定 shape 的测试输入和输出:

Successfully generate .../testdir/0/input.mnn and .../testdir/0/output.mnn.
Successfully generate .../testdir/1/input.mnn and .../testdir/1/output.mnn.
Successfully generate .../testdir/2/input.mnn and .../testdir/2/output.mnn.
Cost: 10.889739036560059 s

随后在 Step 2 内部调用 compilefornpu 时发生段错误:

Step2: Seperate Model
Segmentation fault (core dumped)
Traceback (most recent call last):
  File "npu/generate_llm_qnn.py", line 425, in <module>
    main()
  File "npu/generate_llm_qnn.py", line 421, in main
    convert(args)
  File "npu/generate_llm_qnn.py", line 373, in convert
    convert_llm(args)
  File "npu/generate_llm_qnn.py", line 352, in convert_llm
    convert_qnn(args, 'llm.mnn', inputjson, external_file,
                list(range(shape_count)))
  File "npu/generate_llm_qnn.py", line 288, in convert_qnn
    raise RuntimeError("compilefornpu failed")
RuntimeError: compilefornpu failed

因此,用户侧的完整复现入口是官方 generate_llm_qnn.py。下面直接调用 compilefornpu 的步骤只是为了隔离 Step 2、确认崩溃位置并采集 gdb 调用栈。

Step 2 的输入配置

输入模型是由 Qwen3-VL-2B 导出的语言模型 llm.mnn

传给 compilefornpuqnn.json 如下:

{
  "type": "QNN",
  "skips": [],
  "testdir": [
    "/path/to/testdir/0",
    "/path/to/testdir/1",
    "/path/to/testdir/2"
  ],
  "KVCACHE_SIZE_LIMIT": 1024,
  "cache": "qnn"
}

每个 testdir 目录中都包含由 MNN LLM QNN 导出流程生成的一组输入、输出及 shape 配置。

Step 2 最小化复现命令

compilefornpugenerate_llm_qnn.py 在 Step 2 内部调用的 MNN 转换工具,不是本次实验最初采用的用户侧导出入口。

为了排除 Python 脚本和其他导出步骤的影响,复用官方流程 Step 1 已成功生成的三组 testdir,并使用 clean upstream MNN 构建出的 compilefornpu 单独重跑 Step 2:

MNN_ROOT=/path/to/MNN
BUILD_ROOT="$MNN_ROOT/build_qnn_repro"
QNN_SDK_ROOT=/path/to/qairt/2.46.0.260424
MODEL=/path/to/Qwen3-VL-2B-MNN/llm.mnn
REPRO_DIR=/path/to/reproduction-directory

export LD_LIBRARY_PATH="$QNN_SDK_ROOT/lib/x86_64-linux-clang:$BUILD_ROOT:$BUILD_ROOT/express:${LD_LIBRARY_PATH:-}"

cd "$REPRO_DIR"
"$BUILD_ROOT/compilefornpu" \
  "$MODEL" \
  qnn/llm.mnn \
  qnn.json

echo "$?"

实际行为

Segmentation fault (core dumped)
139

完整的 generate_llm_qnn.py 流程在 Step2: Seperate Model 失败,并抛出 RuntimeError: compilefornpu failed。单独运行 Step 2 时,compilefornpu 退出码为 139;使用 gdb 执行相同的 Step 2 命令也可以复现同一处崩溃。

预期行为

compilefornpu 应当:

  1. 正常生成 QNN 模型;或者
  2. 如果当前图或输入配置不受支持,返回明确的错误信息。

不应解引用空的 host buffer,也不应以 SIGSEGV 终止。

gdb 调用栈

Program received signal SIGSEGV, Segmentation fault.
MNN::QNN::QnnFloatToHalf (
    src=src@entry=0x0,
    dst=0x5555570d5e80,
    size=size@entry=262144)
    at source/backend/qnn/backend/QNNUtils.cpp:26

#0  MNN::QNN::QnnFloatToHalf(src=0x0, size=262144)
    at source/backend/qnn/backend/QNNUtils.cpp:26
#1  MNN::QNN::QNNTensorWrapper::createStaticFloatTensor(
        name="QnnTensor_2",
        dataType=QNN_DATATYPE_FLOAT_16,
        dimensions={1, 128, 2048},
        buffer=0x0)
    at source/backend/qnn/backend/QNNWrapper.cpp:56
#2  MNN::QNN::QnnBackend::getTensorIdx(...)
    at source/backend/qnn/backend/QNNBackend.cpp
#3  MNN::QNN::QnnBackend::getNativeTensor(...)
    at source/backend/qnn/backend/QNNBackend.cpp:2090
#4  MNN::QNN::QNNStridedSlice::onEncode(...)
    at source/backend/qnn/execution/QNNStridedSlice.cpp
#5  MNN::QNN::QNNCommonExecution::onResize(...)
    at source/backend/qnn/execution/QNNCommonExecution.cpp:26

gdb 记录的关键局部变量:

tName = "QnnTensor_2"
tDims = {1, 128, 2048}
buffer = 0x0

inputShape = {1, 128, 2048}
beginRaw = {0, 0, 0}
endRaw = {1, 128, 2048}
strideRaw = {1, 1, 1}

相关代码路径

QnnBackend::getTensorIdx() 中,如果 tensor 不在 mTensorMap 中,就会进入创建静态 tensor 的路径:

if (iter == mTensorMap.end()) {
    std::string tName = "QnnTensor_" + std::to_string(mTensorCounter);
    MNN_ASSERT(
        TensorUtils::getDescribe(tensor)->usage ==
        Tensor::InsideDescribe::Usage::CONSTANT
    );

    // ...

    qnnTensorWrapper = QNNTensorWrapper::createStaticFloatTensor(
        tName,
        tDataType,
        tDims,
        tensor->host<float>()
    );
}

createStaticFloatTensor() 中也存在针对 buffer 的断言:

MNN_ASSERT(!name.empty() && !dimensions.empty() && buffer != nullptr);

但在启用了 NDEBUGRelWithDebInfo 构建中,这些断言不会阻止程序继续执行。空 buffer 最终进入:

FLOAT_TO_HALF(buffer, (int16_t*)dst, numElement);

随后在 QnnFloatToHalf() 中解引用空的 src 并崩溃。

初步分析

以下事实已经由 gdb 调用栈确认:

  1. QNNStridedSlice::onEncode() 请求了一个尚未注册到 mTensorMap 的 tensor。
  2. 该 tensor 的 shape 是 [1, 128, 2048]
  3. 该 tensor 的 host buffer 是空指针。
  4. getTensorIdx() 在 map miss 后进入了静态 tensor 创建路径。
  5. 空指针被传入 QnnFloatToHalf(),最终引发段错误。

目前的推测是:这个 tensor 实际是图输入或中间输入,本应在 QNNStridedSlice::onEncode() 执行前注册为 QNN graph tensor,而不应仅因为它没有出现在 mTensorMap 中就被当作常量处理。

增加空指针检查可以避免段错误,但可能只是把崩溃变成错误返回,并不一定构成完整修复。将该 tensor 注册为 QNN graph input 时,还可能需要同步保证生成的 wrapper 输入名称与 QNN context 中的实际输入 tensor 一致。因此目前尚未得到经过验证的完整修复方案。

可复现性说明

  • 问题最初由官方 generate_llm_qnn.py 完整导出流程触发。
  • 官方流程的 Step 1 可以正常生成三组测试输入/输出,问题发生在 Step 2。
  • 已在未修改的 upstream MNN worktree 上复现。
  • 复现使用的 worktree 中不存在本地 QNN 后端补丁。
  • 使用 Step 1 的原始产物直接重跑 Step 2,普通运行和 gdb 运行均到达相同的空指针崩溃位置。
  • 不使用 gdb 时,进程退出码为 139

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions