问题描述
使用 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。
传给 compilefornpu 的 qnn.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 最小化复现命令
compilefornpu 是 generate_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 应当:
- 正常生成 QNN 模型;或者
- 如果当前图或输入配置不受支持,返回明确的错误信息。
不应解引用空的 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);
但在启用了 NDEBUG 的 RelWithDebInfo 构建中,这些断言不会阻止程序继续执行。空 buffer 最终进入:
FLOAT_TO_HALF(buffer, (int16_t*)dst, numElement);
随后在 QnnFloatToHalf() 中解引用空的 src 并崩溃。
初步分析
以下事实已经由 gdb 调用栈确认:
QNNStridedSlice::onEncode() 请求了一个尚未注册到 mTensorMap 的 tensor。
- 该 tensor 的 shape 是
[1, 128, 2048]。
- 该 tensor 的 host buffer 是空指针。
getTensorIdx() 在 map miss 后进入了静态 tensor 创建路径。
- 空指针被传入
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。
问题描述
使用 QNN 后端转换 Qwen3-VL-2B 的
llm.mnn时,compilefornpu发生段错误。在没有任何本地源码修改的 MNN
origin/master上复现:进程退出码为
139。崩溃发生在QNNStridedSlice::onEncode()请求一个尚未注册到QnnBackend::mTensorMap的 tensor 时。QnnBackend::getTensorIdx()将该 tensor 当作静态 tensor 处理,但它的 host buffer 是空指针,最终该空指针被传给QnnFloatToHalf()并引发段错误。运行环境
目标 Android 设备
段错误发生在 x86_64 Linux 主机运行
compilefornpu的模型转换阶段,还没有执行到模型部署或手机端推理。编译命令
以上编译可以正常完成。
官方导出流程
最初是按照 MNN LLM 文档中推荐的
generate_llm_qnn.py流程触发的首先进入 MNN 的 LLM 导出目录并激活 Python 环境:
然后使用官方脚本转换 LLM:
脚本首先成功生成了三组固定 shape 的测试输入和输出:
随后在 Step 2 内部调用
compilefornpu时发生段错误:因此,用户侧的完整复现入口是官方
generate_llm_qnn.py。下面直接调用compilefornpu的步骤只是为了隔离 Step 2、确认崩溃位置并采集 gdb 调用栈。Step 2 的输入配置
输入模型是由 Qwen3-VL-2B 导出的语言模型
llm.mnn。传给
compilefornpu的qnn.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 最小化复现命令
compilefornpu是generate_llm_qnn.py在 Step 2 内部调用的 MNN 转换工具,不是本次实验最初采用的用户侧导出入口。为了排除 Python 脚本和其他导出步骤的影响,复用官方流程 Step 1 已成功生成的三组
testdir,并使用 clean upstream MNN 构建出的compilefornpu单独重跑 Step 2:实际行为
完整的
generate_llm_qnn.py流程在Step2: Seperate Model失败,并抛出RuntimeError: compilefornpu failed。单独运行 Step 2 时,compilefornpu退出码为139;使用 gdb 执行相同的 Step 2 命令也可以复现同一处崩溃。预期行为
compilefornpu应当:不应解引用空的 host buffer,也不应以
SIGSEGV终止。gdb 调用栈
gdb 记录的关键局部变量:
相关代码路径
在
QnnBackend::getTensorIdx()中,如果 tensor 不在mTensorMap中,就会进入创建静态 tensor 的路径:createStaticFloatTensor()中也存在针对 buffer 的断言:但在启用了
NDEBUG的RelWithDebInfo构建中,这些断言不会阻止程序继续执行。空 buffer 最终进入:随后在
QnnFloatToHalf()中解引用空的src并崩溃。初步分析
以下事实已经由 gdb 调用栈确认:
QNNStridedSlice::onEncode()请求了一个尚未注册到mTensorMap的 tensor。[1, 128, 2048]。getTensorIdx()在 map miss 后进入了静态 tensor 创建路径。QnnFloatToHalf(),最终引发段错误。目前的推测是:这个 tensor 实际是图输入或中间输入,本应在
QNNStridedSlice::onEncode()执行前注册为 QNN graph tensor,而不应仅因为它没有出现在mTensorMap中就被当作常量处理。增加空指针检查可以避免段错误,但可能只是把崩溃变成错误返回,并不一定构成完整修复。将该 tensor 注册为 QNN graph input 时,还可能需要同步保证生成的 wrapper 输入名称与 QNN context 中的实际输入 tensor 一致。因此目前尚未得到经过验证的完整修复方案。
可复现性说明
generate_llm_qnn.py完整导出流程触发。139。