Skip to content

Feature/qnnfusesupport#4174

Merged
wangzhaode merged 5 commits intoalibaba:masterfrom
jxt1234:feature/qnnfusesupport
Mar 9, 2026
Merged

Feature/qnnfusesupport#4174
wangzhaode merged 5 commits intoalibaba:masterfrom
jxt1234:feature/qnnfusesupport

Conversation

@jxt1234
Copy link
Collaborator

@jxt1234 jxt1234 commented Feb 23, 2026

  1. LLM QNN 后端支持设置 max_history_token ,此时 Attention 使用 QNN 实现
  2. 部分 QNN 算子 bug 修正

@wangzhaode wangzhaode self-assigned this Feb 23, 2026
jxt1234 added 2 commits March 7, 2026 22:50
QNN:Refractor: Use op name as qnn's graph op name as default
QNN:Bugfix: Fix bug for reshape with common nc4hw4 case
QNN:Bugfix: When output quant is nullptr, don't support convolution quant
QNN:Refractor: Opt the log when can't find input / output
QNN:Refractor: Transpose Q, K, V's matmul axis for easy to use kvcache

QNN:Feature: Support kv_cache for QNNAttention
@jxt1234 jxt1234 force-pushed the feature/qnnfusesupport branch from 26f7cc0 to c5d5bad Compare March 7, 2026 14:52
@jxt1234 jxt1234 force-pushed the feature/qnnfusesupport branch from c5d5bad to 32f9072 Compare March 7, 2026 14:56
Copy link
Collaborator

@wangzhaode wangzhaode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #4174: Feature/qnnfusesupport 代码审查

总体评估

本 PR 为 QNN 后端的 LLM Attention 增加了 KV Cache 支持(通过 max_history_token 配置),并修正了若干 QNN 算子 Bug。改动涉及 17 个文件,+971/-278 行。整体方向正确,但存在若干正确性、安全性和代码质量问题需要关注。

具体问题

1. CMakeLists.txt: glob 模式过于宽泛

  • 文件: source/backend/qnn/CMakeLists.txt, 第 4-5 行
  • file(GLOB BACKEND_SRCS ${CMAKE_CURRENT_LIST_DIR}/backend/*) 将匹配所有文件,包括 .h.md.txt 等非源文件。新增了 dsprpc_interface.cc 文件(非 .cpp 后缀),这才是改为 * 的原因,但更好的做法是:
file(GLOB BACKEND_SRCS ${CMAKE_CURRENT_LIST_DIR}/backend/*.cpp ${CMAKE_CURRENT_LIST_DIR}/backend/*.cc)

2. 全局可变静态变量 gExtraIoPrefix 线程安全问题

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, 第 25 行
  • static std::string gExtraIoPrefix = "_mnn"; 是一个非 const 的全局 static std::string。如果它不会被修改,应该声明为 static const std::string 或者使用 static constexpr const char*

3. RPCBuffer 析构函数中未检查 nullptr

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, RPCBuffer::~RPCBuffer()
  • 析构函数直接调用 rpcmem_free(mPtr),但如果 mPtr 为 nullptr(虽然 alloc 失败时不会创建对象,但需考虑其他路径),行为取决于底层 rpcmem_free 实现。建议添加 nullptr 检查以增强防御性。

4. _findInput_findOutput 中引用了未定义的变量 inputs[i]

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, _findInput_findOutput 函数
  • #ifdef QNN_VERBOSE 块中的 MNN_PRINT("input name: %s %s\n", inputs[i].second.c_str(), dstT.v1.name);,此处 inputsi 在函数作用域内不存在(函数参数是 nameindex)。虽然在 QNN_VERBOSE 未定义时不会编译,但一旦启用就会导致编译错误。应改为 name.c_str()

5. rpcmem_init() 调用缺少对应的 rpcmem_deinit()

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, registerQNNRuntimeCreator
  • rpcmem_init() 在注册时调用,但在整个代码中没有看到对应的 rpcmem_deinit() 调用,存在资源泄漏。

6. setupStatemask->setToTensor 返回值未检查

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, setupState 函数
  • mask->setToTensor 可能返回 falsememRegister 失败),但调用处未检查返回值。类似地,statesInputs[i]->setToTensorstatesOutput[i]->setToTensor 的返回值也被忽略了。

7. _loadState 中缺少 mStateMaxSize 为 0 的边界检查

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, _loadState 函数
  • stateNumber > 0mStateMaxSize == 0 时,会执行 RPCBuffer::alloc(0),可能产生未定义行为。

8. compute 函数中 KV Cache 状态更新可能越界

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, PluginExecuteRaw::compute
  • mStateCurrent += meta->add 累加后,没有检查 mStateCurrent 是否超过 mStateMaxSize。如果累计 token 数超过 mStateMaxSizemaskPtr[i+mStateCurrent] 将越界写入,导致内存损坏。

9. mStateCurrent -= meta->remove 可能导致负值

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, PluginExecuteRaw::compute
  • 如果 meta->remove > mStateCurrentmStateCurrent 会下溢(类型为 int,变为负数),后续 maskPtr[i+mStateCurrent] 访问将越界。

10. invokModel 函数名拼写错误

  • 文件: source/backend/qnn/backend/QNNBackend.cpp
  • invokModel 应为 invokeModel。虽然原代码就存在此拼写错误,但既然重构了函数签名,这是修正的好时机。

11. createStaticFloatTensor 的 coef 维度与数据不匹配

  • 文件: source/backend/qnn/execution/QNNAttention.cpp
  • coef 的维度被改为 {1, 1, 1, 1},但 scaleVec 大小为 totalSize = batch * headNum * seqLenQ * headDim。这意味着只有第一个元素会被使用,其余的 scaleVec 数据被忽略。如果 QNN 支持广播,这可能是有意的优化,但值得在注释中说明。

12. dsprpc_interface.ccload_fn 未处理 lib == nullptr 的情况

  • 文件: source/backend/qnn/backend/dsprpc_interface.cc, load_fn 函数
  • load_lib() 返回 nullptr 时(dlopen 失败),load_fn 仍然调用 dlsym(lib, fn_name),此时 dlsym(nullptr, ...) 的行为是搜索默认符号表,这可能不是预期行为。

13. llm.cpp 缩进不一致

  • 文件: transformers/llm/engine/src/llm.cpp
  • 修改后的代码块使用了 3 空格缩进(if (mConfig->backend_type() == "cpu" && mValidBlockSize.empty())),而周围代码使用 4 空格缩进或 tab。这可能是因为在 diff 中混合了制表符和空格。

14. positionIds 维度变更可能引入兼容性问题

  • 文件: transformers/llm/engine/src/llm.cpptransformers/llm/engine/tools/generateLlmIO.cpp
  • positionIds{seqLen} 改为 {1, seqLen},这是一个影响模型输入格式的变更。需确认所有使用 positionIds 的模型和前处理代码都兼容新的 2D 格式。

15. ModuleBasic.cpp 中 KVMeta 结构体重复定义

  • 文件: tools/cpp/ModuleBasic.cpp
  • KVMeta 结构体在此文件中重新定义。如果 QNNBackend.cpp 中的 KVMeta 也存在(通过 meta = (KVMeta*)(ctx->backend()->getMetaPtr())),说明这个结构体需要在公共头文件中定义一次,而非在多处重复定义。结构体不一致将导致内存布局错误。

16. addStaticTensorToGraphaddStageTensorToGraph 合并后丢失断言

  • 文件: source/backend/qnn/backend/QNNBackend.hpp.cpp
  • 原来的 addStaticTensorToGraph 有断言 MNN_ASSERT(staticTensor->v1.type == QNN_TENSOR_TYPE_STATIC)addStageTensorToGraph 有断言 MNN_ASSERT(stageTensor->v1.type == QNN_TENSOR_TYPE_NATIVE)。合并为 addTensor 后这些断言被移除,降低了调试时发现类型错误的能力。

17. 注释中残留的调试代码

  • 文件: source/backend/qnn/backend/QNNBackend.cpp
  • 多处保留了注释掉的 MNN_PRINT 调试语句(如 // MNN_PRINT("All Inputs Begin\n")// inputs[i]->printShape())。提交前应清理。

亮点

  • QNNAttention 中基于索引的 mTempTensorWrappers[N] 访问方式重构为命名变量(如 Query_perm, scaleQ, QK 等),大幅提升了可读性。
  • dsprpc_interface 使用 dlopen/dlsym 动态加载方式,避免了对 QNN SDK 库的硬链接依赖。
  • QNNCommonExecutioncreate* 方法改为返回 shared_ptr,使调用方无需依赖全局索引数组。
  • setNodeName 增加了对 op->name() 的支持,优先使用算子的原始名称。

总结

问题 8,9(越界)是 blocker,必须修;4 和 15 修复成本低但隐患大,强烈建议一并修复

Reviewed by claude-opus-4-6

@wangzhaode
Copy link
Collaborator

PR #4174: Feature/qnnfusesupport 代码审查

总体评估

本 PR 为 QNN 后端的 LLM Attention 增加了 KV Cache 支持(通过 max_history_token 配置),并修正了若干 QNN 算子 Bug。改动涉及 17 个文件,+971/-278 行。整体方向正确,但存在若干正确性、安全性和代码质量问题需要关注。

具体问题

1. CMakeLists.txt: glob 模式过于宽泛

  • 文件: source/backend/qnn/CMakeLists.txt, 第 4-5 行
  • file(GLOB BACKEND_SRCS ${CMAKE_CURRENT_LIST_DIR}/backend/*) 将匹配所有文件,包括 .h.md.txt 等非源文件。新增了 dsprpc_interface.cc 文件(非 .cpp 后缀),这才是改为 * 的原因,但更好的做法是:
file(GLOB BACKEND_SRCS ${CMAKE_CURRENT_LIST_DIR}/backend/*.cpp ${CMAKE_CURRENT_LIST_DIR}/backend/*.cc)

2. 全局可变静态变量 gExtraIoPrefix 线程安全问题

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, 第 25 行
  • static std::string gExtraIoPrefix = "_mnn"; 是一个非 const 的全局 static std::string。如果它不会被修改,应该声明为 static const std::string 或者使用 static constexpr const char*

3. RPCBuffer 析构函数中未检查 nullptr

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, RPCBuffer::~RPCBuffer()
  • 析构函数直接调用 rpcmem_free(mPtr),但如果 mPtr 为 nullptr(虽然 alloc 失败时不会创建对象,但需考虑其他路径),行为取决于底层 rpcmem_free 实现。建议添加 nullptr 检查以增强防御性。

4. _findInput_findOutput 中引用了未定义的变量 inputs[i]

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, _findInput_findOutput 函数
  • #ifdef QNN_VERBOSE 块中的 MNN_PRINT("input name: %s %s\n", inputs[i].second.c_str(), dstT.v1.name);,此处 inputsi 在函数作用域内不存在(函数参数是 nameindex)。虽然在 QNN_VERBOSE 未定义时不会编译,但一旦启用就会导致编译错误。应改为 name.c_str()

5. rpcmem_init() 调用缺少对应的 rpcmem_deinit()

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, registerQNNRuntimeCreator
  • rpcmem_init() 在注册时调用,但在整个代码中没有看到对应的 rpcmem_deinit() 调用,存在资源泄漏。

6. setupStatemask->setToTensor 返回值未检查

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, setupState 函数
  • mask->setToTensor 可能返回 falsememRegister 失败),但调用处未检查返回值。类似地,statesInputs[i]->setToTensorstatesOutput[i]->setToTensor 的返回值也被忽略了。

7. _loadState 中缺少 mStateMaxSize 为 0 的边界检查

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, _loadState 函数
  • stateNumber > 0mStateMaxSize == 0 时,会执行 RPCBuffer::alloc(0),可能产生未定义行为。

8. compute 函数中 KV Cache 状态更新可能越界

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, PluginExecuteRaw::compute
  • mStateCurrent += meta->add 累加后,没有检查 mStateCurrent 是否超过 mStateMaxSize。如果累计 token 数超过 mStateMaxSizemaskPtr[i+mStateCurrent] 将越界写入,导致内存损坏。

9. mStateCurrent -= meta->remove 可能导致负值

  • 文件: source/backend/qnn/backend/QNNBackend.cpp, PluginExecuteRaw::compute
  • 如果 meta->remove > mStateCurrentmStateCurrent 会下溢(类型为 int,变为负数),后续 maskPtr[i+mStateCurrent] 访问将越界。

10. invokModel 函数名拼写错误

  • 文件: source/backend/qnn/backend/QNNBackend.cpp
  • invokModel 应为 invokeModel。虽然原代码就存在此拼写错误,但既然重构了函数签名,这是修正的好时机。

11. createStaticFloatTensor 的 coef 维度与数据不匹配

  • 文件: source/backend/qnn/execution/QNNAttention.cpp
  • coef 的维度被改为 {1, 1, 1, 1},但 scaleVec 大小为 totalSize = batch * headNum * seqLenQ * headDim。这意味着只有第一个元素会被使用,其余的 scaleVec 数据被忽略。如果 QNN 支持广播,这可能是有意的优化,但值得在注释中说明。

12. dsprpc_interface.ccload_fn 未处理 lib == nullptr 的情况

  • 文件: source/backend/qnn/backend/dsprpc_interface.cc, load_fn 函数
  • load_lib() 返回 nullptr 时(dlopen 失败),load_fn 仍然调用 dlsym(lib, fn_name),此时 dlsym(nullptr, ...) 的行为是搜索默认符号表,这可能不是预期行为。

13. llm.cpp 缩进不一致

  • 文件: transformers/llm/engine/src/llm.cpp
  • 修改后的代码块使用了 3 空格缩进(if (mConfig->backend_type() == "cpu" && mValidBlockSize.empty())),而周围代码使用 4 空格缩进或 tab。这可能是因为在 diff 中混合了制表符和空格。

14. positionIds 维度变更可能引入兼容性问题

  • 文件: transformers/llm/engine/src/llm.cpptransformers/llm/engine/tools/generateLlmIO.cpp
  • positionIds{seqLen} 改为 {1, seqLen},这是一个影响模型输入格式的变更。需确认所有使用 positionIds 的模型和前处理代码都兼容新的 2D 格式。

15. ModuleBasic.cpp 中 KVMeta 结构体重复定义

  • 文件: tools/cpp/ModuleBasic.cpp
  • KVMeta 结构体在此文件中重新定义。如果 QNNBackend.cpp 中的 KVMeta 也存在(通过 meta = (KVMeta*)(ctx->backend()->getMetaPtr())),说明这个结构体需要在公共头文件中定义一次,而非在多处重复定义。结构体不一致将导致内存布局错误。

16. addStaticTensorToGraphaddStageTensorToGraph 合并后丢失断言

  • 文件: source/backend/qnn/backend/QNNBackend.hpp.cpp
  • 原来的 addStaticTensorToGraph 有断言 MNN_ASSERT(staticTensor->v1.type == QNN_TENSOR_TYPE_STATIC)addStageTensorToGraph 有断言 MNN_ASSERT(stageTensor->v1.type == QNN_TENSOR_TYPE_NATIVE)。合并为 addTensor 后这些断言被移除,降低了调试时发现类型错误的能力。

17. 注释中残留的调试代码

  • 文件: source/backend/qnn/backend/QNNBackend.cpp
  • 多处保留了注释掉的 MNN_PRINT 调试语句(如 // MNN_PRINT("All Inputs Begin\n")// inputs[i]->printShape())。提交前应清理。

亮点

  • QNNAttention 中基于索引的 mTempTensorWrappers[N] 访问方式重构为命名变量(如 Query_perm, scaleQ, QK 等),大幅提升了可读性。
  • dsprpc_interface 使用 dlopen/dlsym 动态加载方式,避免了对 QNN SDK 库的硬链接依赖。
  • QNNCommonExecutioncreate* 方法改为返回 shared_ptr,使调用方无需依赖全局索引数组。
  • setNodeName 增加了对 op->name() 的支持,优先使用算子的原始名称。

总结

问题 8,9(越界)是 blocker,必须修;4 和 15 修复成本低但隐患大,强烈建议一并修复

Reviewed by claude-opus-4-6

@jxt1234 8,9要不增加一下判断保护一下吧

@wangzhaode wangzhaode merged commit 080bb31 into alibaba:master Mar 9, 2026
7 checks passed
EricMoin pushed a commit to EricMoin/MNN that referenced this pull request Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants