1161 lines
54 KiB
Markdown
1161 lines
54 KiB
Markdown
# 大语言模型
|
||
|
||
基于MNN开发的LLM推理引擎,支持目前主流的开源LLM模型。该功能分为2部分:
|
||
- 模型导出:将torch模型导出为onnx,然后转换为mnn模型;导出tokenizer文件,embedding等文件;
|
||
- 模型推理:支持导出的模型推理,支持LLM模型的文本生成;
|
||
|
||
## 快速开始
|
||
|
||
### **第一步:模型导出 (Export)**
|
||
|
||
此步骤是将原始的 PyTorch 模型(如 Qwen2 系列)转换为 MNN 引擎可以加载和推理的格式。
|
||
|
||
1. **安装依赖**:
|
||
进入导出工具目录并安装必要的 Python 包。
|
||
```bash
|
||
cd ./transformers/llm/export
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
2. **准备原始模型**:
|
||
将需要部署的开源 LLM 模型(例如 `Qwen2-0.5B-Instruct`)克隆到本地。**务必确保 `git lfs` 已安装,以下载完整的模型文件**。
|
||
```bash
|
||
git lfs install
|
||
git clone https://www.modelscope.cn/qwen/Qwen2-0.5B-Instruct.git
|
||
```
|
||
|
||
3. **执行导出命令**:
|
||
运行 `llmexport.py` 脚本,将模型、Tokenizer、Embedding 等导出为 MNN 格式。
|
||
```bash
|
||
python llmexport.py \
|
||
--path /path/to/Qwen2-0.5B-Instruct \
|
||
--export mnn --hqq
|
||
```
|
||
* **关键产物**:脚本会生成一个包含 `llm.mnn`, `llm.mnn.weight`, `tokenizer.mtok`, `embeddings_bf16.bin`【可能存在】, `llm_config.json`, `config.json` 等文件的模型目录。
|
||
|
||
4. **(可选)高级功能**:
|
||
* **量化**:通过 `--quant_bit 4` 和 `--quant_block 128` 等参数可以调节量化的Bits数,默认为`4 bit , block size 64`。通过 `--hqq` 或 `--awq` 或 `--omni` 可以启用对应算法以提升量化后的模型精度,一般建议增加`--hqq`
|
||
* **LoRA**:通过 `--lora_path` 合并或分离 LoRA 权重。
|
||
* **Embeding**:对于目前主流的8b以下模型,采用了`Tie-Embeding`技术,默认不会导出`embeddings_bf16.bin`,而是复用`llm.mnn.weight`中的`lm`权重,需要提升embed精度可以设置 `--seperate_embed` 分离出`embeddings_bf16.bin`。
|
||
* **GPTQ**:通过 `--gptq_path` 应用预量化好的 GPTQ 权重。
|
||
* **MNNConvert 工具**:推荐优先使用本地编译的 `MNNConvert`(在 MNN 根目录下 `mkdir build && cd build && cmake .. -DMNN_BUILD_CONVERTER=ON && make -j16`),`llmexport.py` 默认会到 `../../../build/` 下查找,也可通过 `--mnnconvert` 显式指定路径。
|
||
* **手动转换**:如果直接导出 `mnn` 失败,或者需要fp16/fp32精度的模型,可先导出 `onnx`,再用 `MNNConvert` 工具手动转换。
|
||
|
||
---
|
||
|
||
### **第二步:引擎编译 (Compile)**
|
||
|
||
此步骤是编译 MNN 的 C++ 推理引擎,使其支持 LLM 推理功能。
|
||
|
||
1. **配置编译选项**:
|
||
在标准的 MNN 编译命令中,**必须添加 `-DMNN_BUILD_LLM=ON`** 以启用 LLM 支持。
|
||
* **Omni 模型**:如果需要支持图像/音频输入,还需添加 `-DMNN_BUILD_LLM_OMNI=ON`。
|
||
* **平台优化**:
|
||
* **x86 (Mac/Linux)**:可添加 `-DMNN_AVX512=ON` 以利用 AVX512 指令集加速。
|
||
* **Android**:可添加 `-DMNN_OPENCL=ON` 以利用 GPU 加速。
|
||
* **iOS**:可添加 `-DMNN_METAL=ON` 以利用 GPU 加速。
|
||
* **Web (WASM)**:使用 `emcmake` 并配置 `-DMNN_FORBID_MULTI_THREAD=ON` 等特定选项。
|
||
|
||
2. **执行编译**:
|
||
以 Linux/Mac 为例:
|
||
```bash
|
||
mkdir build && cd build
|
||
cmake .. -DMNN_BUILD_LLM=ON -DMNN_AVX512=ON # 根据平台调整选项
|
||
make -j16
|
||
```
|
||
编译完成后,会生成核心库文件(如 `libMNN.so`, `libllm.so`)。
|
||
|
||
---
|
||
|
||
### **第三步:运行时配置与推理 (Inference)**
|
||
|
||
此步骤是配置模型运行参数并启动推理。
|
||
|
||
1. **准备模型目录**:
|
||
将第一步导出的所有文件(`llm.mnn`, `llm.mnn.weight`, `tokenizer.mtok`, `embeddings_bf16.bin`, `llm_config.json`)放在同一个文件夹下。
|
||
|
||
2. **配置 `config.json`**:
|
||
编辑或使用自动生成的 `config.json` 文件,根据你的硬件和需求调整参数:
|
||
* **硬件**:设置 `backend_type` (如 `"cpu"`, `"opencl"`) 和 `thread_num`。
|
||
* **性能**:设置 `precision` (如 `"low"` for fp16) 和 `memory` (如 `"low"` for runtime quant)。
|
||
* **生成**:设置 `max_new_tokens`, `sampler_type` (默认 `"mixed"`), `temperature`, `top_k`, `top_p`, `repetition_penalty` 等。
|
||
* **高级**:设置 `reuse_kv` (多轮对话), `chunk` (内存分块) 等。
|
||
* **示例**:
|
||
```json
|
||
{
|
||
"backend_type": "cpu",
|
||
"thread_num": 4,
|
||
"precision": "low",
|
||
"sampler_type": "mixed",
|
||
"temperature": 0.7,
|
||
"topP": 0.9,
|
||
"reuse_kv": true
|
||
}
|
||
```
|
||
|
||
3. **运行推理 Demo**:
|
||
使用编译好的 `llm_demo` 工具进行推理。
|
||
* **交互式聊天**:
|
||
```bash
|
||
./llm_demo /path/to/model_dir/config.json
|
||
```
|
||
* **批量处理 Prompt**:
|
||
```bash
|
||
./llm_demo /path/to/model_dir/config.json /path/to/prompt.txt
|
||
```
|
||
* **多模态输入** (Omni 模型):在 Prompt 中嵌入 `<img>` 或 `<audio>` 标签。
|
||
|
||
4. **(可选)性能基准测试**:
|
||
使用 `llm_bench` 工具对不同后端、线程数、Prompt 长度等配置进行性能压测,以找到最优配置。
|
||
```bash
|
||
./llm_bench -m ./model/config.json -a cpu,opencl -t 4,8 -p 32,64 -n 32 -rep 3
|
||
```
|
||
|
||
---
|
||
|
||
**总结流程图**:
|
||
`准备PyTorch模型` -> `使用 llmexport.py 导出为 MNN 格式` -> `编译 MNN 引擎 (启用 LLM)` -> `配置 config.json` -> `使用 llm_demo 进行推理`
|
||
|
||
|
||
|
||
## 模型导出工具`llmexport`
|
||
|
||
|
||
`llmexport`是一个llm模型导出工具,能够将llm模型导出为onnx和mnn模型。
|
||
|
||
### 依赖安装
|
||
```
|
||
cd ./transformers/llm/export
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
### 用法
|
||
1. 将需要导出的LLM项目clone到本地,如:Qwen2-0.5B-Instruct
|
||
```sh
|
||
git lfs install
|
||
git clone https://www.modelscope.cn/qwen/Qwen2-0.5B-Instruct.git
|
||
```
|
||
|
||
***clone 后检查一下模型大小,有可能因为lfs没安装导致下载的是空模型***
|
||
|
||
3. 执行`llmexport.py`导出模型
|
||
```sh
|
||
cd ./transformers/llm/export
|
||
# 导出模型,tokenizer和embedding,并导出对应的mnn模型
|
||
python llmexport.py \
|
||
--path /path/to/Qwen2-0.5B-Instruct \
|
||
--export mnn
|
||
```
|
||
4. 导出产物
|
||
导出产物为:
|
||
1. `config.json`: 模型运行时的配置,可手动修改;
|
||
2. `embeddings_bf16.bin`: 模型的embedding权重二进制文件,推理时使用;
|
||
3. `llm.mnn`: 模型的mnn文件,推理时使用;
|
||
4. `llm.mnn.json`: mnn模型对应的json文件,`apply_lora`或gptq量化权重时使用;
|
||
5. `llm.mnn.weight`: 模型的mnn权重,推理时使用;
|
||
6. `llm.onnx`: 模型的onnx文件,不包含权重,推理时不使用;
|
||
7. `llm_config.json`: 模型的配置信息,推理时使用;
|
||
8. `tokenizer.mtok`: 模型的tokenzier文件,推理时使用;
|
||
目录结构如下所示:
|
||
```
|
||
.
|
||
└── model
|
||
├── config.json
|
||
├── embeddings_bf16.bin
|
||
├── llm.mnn
|
||
├── llm.mnn.json
|
||
├── llm.mnn.weight
|
||
├── onnx/
|
||
├──llm.onnx
|
||
├──llm.onnx.data
|
||
├── llm_config.json
|
||
└── tokenizer.mtok
|
||
```
|
||
|
||
### 功能
|
||
- 直接转为mnn模型,使用`--export mnn`。**推荐优先使用本地编译的 MNNConvert 工具**:在 MNN 根目录下创建 `build/` 目录并执行编译,编译时需打开 `-DMNN_BUILD_CONVERTER=ON`,例如:
|
||
```bash
|
||
mkdir build && cd build
|
||
cmake .. -DMNN_BUILD_CONVERTER=ON && make -j16
|
||
```
|
||
编译完成后 `build/` 目录下会生成 `MNNConvert` 可执行文件,`llmexport.py` 默认会在 `../../../build/` 下查找该工具;也可以通过 `--mnnconvert` 选项显式指定 MNNConvert 路径。若未提供本地 MNNConvert,脚本会回退到 pymnn(需先安装 `pip install MNN`)。此方案目前支持导出4bit和8bit模型。
|
||
- 如果直接转为mnn模型遇到问题,或者需要其他bits数的量化(如5bit/6bit),可以先将模型先转为onnx模型,使用`--export onnx`,然后使用./MNNConvert工具将onnx模型转为mnn模型:
|
||
|
||
```
|
||
./MNNConvert --modelFile ../transformers/llm/export/model/onnx/llm.onnx --MNNModel llm.mnn --keepInputFormat --weightQuantBits=4 --weightQuantBlock=128 -f ONNX --transformerFuse=1 --allowCustomOp --saveExternalData
|
||
```
|
||
|
||
- 支持对模型进行对话测试,使用`--test $query`会返回llm的回复内容
|
||
- 支持合并lora权重后导出,指定lora权重的目录使用`--lora_path`
|
||
- 制定量化bit数使用`--quant_bit`;量化的block大小使用`--quant_block`
|
||
- 使用`--lm_quant_bit`来制定lm_head层权重的量化bit数,不指定则使用`--quant_bit`的量化bit数
|
||
|
||
### 参数
|
||
执行 `python llmexport.py -h` 可查看参数:
|
||
```
|
||
usage: llmexport.py [-h] --path PATH [--type TYPE] [--tokenizer_path TOKENIZER_PATH] [--lora_path LORA_PATH]
|
||
[--gptq_path GPTQ_PATH] [--dst_path DST_PATH] [--verbose] [--test TEST] [--export EXPORT]
|
||
[--onnx_slim] [--quant_bit QUANT_BIT] [--quant_block QUANT_BLOCK]
|
||
[--lm_quant_bit LM_QUANT_BIT] [--mnnconvert MNNCONVERT] [--ppl] [--awq] [--omni] [--sym] [--seperate_embed]
|
||
[--lora_split]
|
||
|
||
llm_exporter
|
||
|
||
optional arguments:
|
||
-h, --help show this help message and exit
|
||
--path PATH path(`str` or `os.PathLike`):
|
||
Can be either:
|
||
- A string, the *model id* of a pretrained model like `THUDM/chatglm-6b`. [TODO]
|
||
- A path to a *directory* clone from repo like `../chatglm-6b`.
|
||
--type TYPE type(`str`, *optional*):
|
||
The pretrain llm model type.
|
||
--tokenizer_path TOKENIZER_PATH
|
||
tokenizer path, defaut is `None` mean using `--path` value.
|
||
--lora_path LORA_PATH
|
||
lora path, defaut is `None` mean not apply lora.
|
||
--gptq_path GPTQ_PATH
|
||
gptq path, defaut is `None` mean not apply gptq.
|
||
--dst_path DST_PATH export onnx/mnn model to path, defaut is `./model`.
|
||
--verbose Whether or not to print verbose.
|
||
--test TEST test model inference with query `TEST`.
|
||
--export EXPORT export model to an onnx/mnn model.
|
||
--onnx_slim Whether or not to use onnx-slim.
|
||
--quant_bit QUANT_BIT
|
||
mnn quant bit, 4 or 8, default is 4.
|
||
--quant_block QUANT_BLOCK
|
||
mnn quant block, 0 mean channle-wise, default is 128.
|
||
--visual_quant_bit VISUAL_QUANT_BIT
|
||
mnn visual model quant bit, 4 or 8, default is setting in utils/vision.py by different vit model.
|
||
--visual_quant_block VISUAL_QUANT_BLOCK
|
||
mnn visual model quant block, 0 mean channle-wise, default is setting in utils/vision.py by different vit model.
|
||
--lm_quant_bit LM_QUANT_BIT
|
||
mnn lm_head quant bit, 4 or 8, default is `quant_bit`.
|
||
--mnnconvert MNNCONVERT
|
||
local mnnconvert path, if invalid, using pymnn.
|
||
--ppl Whether or not to get all logits of input tokens.
|
||
--awq Whether or not to use awq quant.
|
||
--sym Whether or not to using symmetric quant (without zeropoint), defualt is False.
|
||
--visual_sym Whether or not to using symmetric quant (without zeropoint) for visual model, defualt is False.
|
||
--seperate_embed For lm and embed shared model, whether or not to sepearte embed to avoid quant, defualt is False, if True, embed weight will be seperate to embeddingbf16.bin.
|
||
--lora_split Whether or not export lora split, defualt is False.
|
||
```
|
||
|
||
|
||
### 权重读取
|
||
llmexport.py 同时支持 LLM 的验证功能,有较多的依赖。在没有相应环境的情况下,MNN-LLM也提供由 safetensors 或 gguf 文件读取权重的工具,可以降低内存需求,提高转换速度。使用方法如下:
|
||
|
||
#### 权重读取前置工作
|
||
1. 下载模型结构:在如下地址找到对应的MNN模型并下载(建文件夹 model,单独下载4个文件: llm.mnn , llm_config.json, tokenizer.mtok , config.json)
|
||
```
|
||
https://modelscope.cn/organization/MNN
|
||
```
|
||
|
||
2. 安装 pymnn ,并把 llm.mnn 转换成 llm.mnn.json
|
||
```
|
||
pip install MNN
|
||
mnnconvert -f MNN --modelFile model/llm.mnn --JsonFile model/llm.mnn.json
|
||
```
|
||
|
||
#### safetensors 转 mnn
|
||
|
||
使用 safetensors2mnn.py 读取权重:
|
||
|
||
```
|
||
python3 safetensors2mnn.py --path /Users/xtjiang/.cache/modelscope/hub/Qwen/Qwen2___5-0___5B-Instruct --mnn_dir model
|
||
```
|
||
|
||
safetensors2mnn.py 支持设定量化参数,和 llmexport.py 一致
|
||
|
||
#### gguf 转 mnn
|
||
使用 gguf2mnn.py 读取 gguf 文件
|
||
|
||
```
|
||
python3 gguf2mnn.py --gguf ~/third/llama.cpp/build/ggml-model-Q4_K.gguf --mnn_dir model
|
||
```
|
||
|
||
目前本方案不支持多模态的模型转换。
|
||
|
||
|
||
## 模型推理
|
||
|
||
### 编译
|
||
|
||
[从源码编译](../compile/other.html#id4)
|
||
在原有编译过程中增加llm开关即可:
|
||
```
|
||
-DMNN_BUILD_LLM=ON
|
||
```
|
||
|
||
若需要开启Omni功能(支持图像/音频输入),增加`MNN_BUILD_LLM_OMNI`选项
|
||
```
|
||
-DMNN_BUILD_LLM=ON -DMNN_BUILD_LLM_OMNI=ON
|
||
```
|
||
|
||
#### mac / linux / windows
|
||
|
||
以 mac / linux 为例 :
|
||
```
|
||
make build
|
||
cd build
|
||
cmake ../ -DMNN_BUILD_LLM=ON
|
||
make -j16
|
||
```
|
||
|
||
x86架构额外加 `MNN_AVX512` 的宏:
|
||
```
|
||
make build
|
||
cd build
|
||
cmake ../ -DMNN_BUILD_LLM=ON -DMNN_AVX512=ON
|
||
make -j16
|
||
```
|
||
|
||
#### Android:额外增加`MNN_OPENCL`的宏
|
||
```
|
||
cd project/android
|
||
mkdir build_64
|
||
../build_64.sh -DMNN_BUILD_LLM=ON -DMNN_OPENCL=ON -DMNN_USE_LOGCAT=ON
|
||
```
|
||
高通设备部分视觉模型支持NPU功能,可增加`MNN_QNN`宏启用QNN功能。QNN运行分2种模式:
|
||
- 在线编译QNN模型:运行其它后端统一的mnn模型,运行时进行编译构图,通过需要较长的构图启动时间,主要用于功能正确性验证。
|
||
- 离线编译QNN模型:使用MNN2QNNModel转换工具将统一的mnn模型离线编译转换成含有Plugin算子的mnn模型以及QNN模型,运行时直接运行编译好的QNN模型,用于生产部署情况。此时需要开启`MNN_WITH_PLUGIN`宏。
|
||
```
|
||
cd project/android
|
||
mkdir build_64
|
||
../build_64.sh -DMNN_BUILD_LLM=ON -DMNN_OPENCL=ON -DMNN_QNN=ON -DMNN_WITH_PLUGIN=ON -DMNN_USE_LOGCAT=ON
|
||
```
|
||
|
||
#### iOS: 参考 transformers/llm/engine/ios/README.md
|
||
```
|
||
sh package_scripts/ios/buildiOS.sh -DMNN_BUILD_LLM=ON
|
||
```
|
||
|
||
#### Web
|
||
环境配置参考 https://mnn-docs.readthedocs.io/en/latest/compile/engine.html#web
|
||
|
||
- 编译库,产出 `libMNN.a`,`libMNN_Express.a`,`libllm.a`
|
||
|
||
```
|
||
mkdir buildweb
|
||
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release -DMNN_FORBID_MULTI_THREAD=ON -DMNN_USE_THREAD_POOL=OFF -DMNN_USE_SSE=OFF -DMNN_BUILD_LLM=ON
|
||
make -j16
|
||
```
|
||
|
||
- Demo 编译
|
||
|
||
```
|
||
emcc ../transformers/llm/engine/demo/llm_demo.cpp -I ../include -I ../transformers/llm/engine/include libMNN.a libllm.a express/libMNN_Express.a -o llm_demo.js --preload-file ~/qwen2.0_1.5b/ -s ALLOW_MEMORY_GROWTH=1 -o llm_demo.js
|
||
```
|
||
|
||
使用如下命令测试:
|
||
```
|
||
node llm_demo.js ~/qwen2.0_1.5b/config.json ~/qwen2.0_1.5b/prompt.txt
|
||
```
|
||
|
||
### 使用
|
||
#### 运行时配置
|
||
|
||
##### 运行时文件
|
||
将导出产物中用于模型推理的部分置于同一个文件夹下,添加一个配置文件`config.json`来描述模型名称与推理参数,目录如下:
|
||
```
|
||
.
|
||
└── model_dir
|
||
├── config.json
|
||
├── embeddings_bf16.bin
|
||
├── llm_config.json
|
||
├── llm.mnn
|
||
├── llm.mnn.weight
|
||
└── tokenizer.mtok
|
||
```
|
||
|
||
##### 配置项
|
||
配置文件支持以下配置:
|
||
- 模型文件信息
|
||
- base_dir: 模型文件加载的文件夹目录,默认为config.json的所在目录,或模型所在目录;
|
||
- llm_config: `llm_config.json`的实际名称路径为`base_dir + llm_config`,默认为`base_dir + 'config.json'`
|
||
- llm_model: `llm.mnn`的实际名称路径为`base_dir + llm_model`,默认为`base_dir + 'llm.mnn'`
|
||
- llm_weight: `llm.mnn.weight`的实际名称路径为`base_dir + llm_weight`,默认为`base_dir + 'llm.mnn.weight'`
|
||
- block_model: 分段模型时`block_{idx}.mnn`的实际路径为`base_dir + block_model`,默认为`base_dir + 'block_{idx}.mnn'`
|
||
- lm_model: 分段模型时`lm.mnn`的实际路径为`base_dir + lm_model`,默认为`base_dir + 'lm.mnn'`
|
||
- embedding_model: 当embedding使用模型时,embedding的实际路径为`base_dir + embedding_model`,默认为`base_dir + 'embedding.mnn'`
|
||
- embedding_file: 当embedding使用二进制时,embedding的实际路径为`base_dir + embedding_file`,默认为`base_dir + 'embeddings_bf16.bin'`
|
||
- tokenizer_file: `tokenizer.mtok`的实际名称路径为`base_dir + tokenizer_file`,默认为`base_dir + 'tokenizer.mtok'`
|
||
- visual_model: 当使用VL模型时,visual_model的实际路径为`base_dir + visual_model`,默认为`base_dir + 'visual.mnn'`、
|
||
- audio_model: 当使用Audio模型时,audio_model的实际路径为`base_dir + audio_model`,默认为`base_dir + 'audio.mnn'`
|
||
- Omni模型文件信息
|
||
- talker_model: 当使用Omni模型时,talker_model的实际路径为`base_dir + talker_model`,默认为`base_dir + 'talker.mnn'`
|
||
- talker_weight: 当使用Omni模型时,talker_weight的实际路径为`base_dir + talker_weight`,默认为`base_dir + 'talker.mnn.weight'`
|
||
- talker_embedding_file: 当使用Omni模型时,talker_embedding_file的实际路径为`base_dir + talker_embedding_file`,默认为`base_dir + 'talker_embeddings_bf16.bin'`
|
||
- predit_model: 当使用Omni模型时,predit_model的实际路径为`base_dir + predit_model`,默认为`base_dir + 'predit.mnn'`
|
||
- dit_model: 当使用Omni模型时,dit_model的实际路径为`base_dir + dit_model`,默认为`base_dir + 'dit.mnn'`
|
||
- bigvgan_model: 当使用Omni模型时,bigvgan_model的实际路径为`base_dir + bigvgan_model`,默认为`base_dir + 'bigvgan.mnn'`
|
||
- spk_dict: 当使用Omni模型时,spk_dict的实际路径为`base_dir + spk_dict`,默认为`base_dir + 'spk_dict.txt'`
|
||
- context_file: 配置上下文信息文件路径,实际路径为`base_dir + context_file`,默认`base_dir + 'context.json'`,内容格式为json格式的上下文信息,包含:如tools,enable_thinking等信息。
|
||
- 推理配置
|
||
- max_new_tokens: 生成时最大token数,默认为`512`
|
||
- reuse_kv: 多轮对话时是否复用之前对话的`kv cache`,默认为`false`.
|
||
- quant_qkv: 选项废弃,请使用 `attention_mode`
|
||
- attention_mode:
|
||
- CPU attention 算子中KV Cache量化方式和FlashAttention开关,编码规则为 `attention_mode = flash_attention * 8 + kv_quant_mode`,默认为`8`
|
||
- KV Cache量化模式(attention_mode % 8):
|
||
- 0: 不量化,key和value均为fp16
|
||
- 1: key使用int8量化,value不量化
|
||
- 2: key和value均使用int8量化
|
||
- 3: key使用TQ3(3-bit)量化,value不量化
|
||
- 4: key和value均使用TQ3(3-bit)量化
|
||
- 5: key使用TQ4(4-bit)量化,value不量化
|
||
- 6: key和value均使用TQ4(4-bit)量化
|
||
- FlashAttention(attention_mode / 8):
|
||
- 0: 不使用FlashAttention
|
||
- 1: 使用FlashAttention
|
||
- 常用配置:
|
||
- 8: FlashAttention,不量化(默认推荐)
|
||
- 10: FlashAttention + KV-INT8(精度几乎无损)
|
||
- 14: FlashAttention + KV-TQ4(4-bit量化,内存节省>30%,推荐4B+模型)
|
||
- 12: FlashAttention + KV-TQ3(3-bit量化,极致压缩,推荐4B+模型)
|
||
- 注意:TQ3/TQ4基于TurboQuant算法(WHT旋转+Lloyd-Max码本),建议在4B及以上参数模型上使用,小模型(<1B)精度损失较大
|
||
- GPU attention 算子中是否使用Flash Attention,可选为:`0, 8, 16`,默认为`8`,目前仅支持Metal后端,含义如下:
|
||
- 0: 运行时不使用Flash Attention, 朴素Attention实现,上下文较长时不推荐内存占用高
|
||
- 8: 运行时使用Flash Attention, 在算子层面分步实现,性能接近设为0,内存占用比设为0小
|
||
- 16: 运行时使用Flash Attention, 在算子层面单算子融合实现,内存占用最小,性能比设为8稍慢一些
|
||
- use_mmap: 是否使用mmap方式,在内存不足时将权重写入磁盘,避免溢出,默认为false,手机上建议设成true
|
||
- chunk: 限制每次最大处理的token数,高于此值将分块运行,以减少内存占用,eg: chunk: 128
|
||
- chunk_limits: 限制每次处理的token数,不在此范围内将分拆或者补零处理,eg: chunk_limits: [128, 1] , 存在 chunk_limits 时,chunk 配置无效
|
||
- kvcache_mmap: 是否使用mmap方式,在内存不足时将在KV Cache 写入磁盘,避免溢出,默认为false
|
||
- tmp_path: 启用 mmap 相关功能时,写入磁盘的缓存目录
|
||
- iOS 上可用如下语句创建临时目录并设置:`NSString *tempDirectory = NSTemporaryDirectory();llm->set_config("{\"tmp_path\":\"" + std::string([tempDirectory UTF8String]) + "\"}")`
|
||
- 硬件配置
|
||
- backend_type: 推理使用硬件后端类型,默认为:`"cpu"`
|
||
- thread_num: CPU推理使用硬件线程数,默认为:`4`; OpenCL推理时使用`68`(不是传统意义的线程数,代表的是opencl buffer存储和tuning wide模式)
|
||
- precision: 推理使用精度策略,默认为:`"low"`,尽量使用`fp16`
|
||
- memory: 推理使用内存策略,默认为:`"low"`,开启运行时量化
|
||
- 与CPU动态量化相关的配置,提升精度、性能
|
||
- dynamic_option: 推理时是否对feature map分blocksize/group进行量化。可选为:`0, 1, 2, 8, 9, 10`,默认是`0`,含义如下:
|
||
- 0: feature map数据使用per channel量化
|
||
- 1: feature map数据使用per tensor量化
|
||
- 2: feature map数据用per block量化,blocksize等于权重量化时的blocksize,如果权重量化时没有使用per block量化,即使设置2,也不会对feature map做per block量化
|
||
- 8+n(n=0,1,2): 该选项是为了加速LLM 推理时Decode性能。但是当prompt长度小于300时,Prefill速度会显著变慢。当prompt长度高于300时,Prefill速度不会变慢。
|
||
- cpu_sme2_neon_division_ratio: 为了提高Arm SME后端多线程推理时性能,可根据模型、线程数定制化设置该参数。参数计算方式: Prefill阶段单个SME核和NEON核的工作量比例x:1,Decode阶段工作量比例y:1,
|
||
则参数设置为8*x+y,x和y均是不大于7的正整数。41、49和33是常见的参数设置. 可以通过观察单线程推理时,SME后端相较于NEON后端的加速比来决定该参数的取值。默认是`41`.
|
||
- Sampler配置
|
||
|
||
MNN-LLM 采用pipeline架构的采样器,模型输出的logits依次经过各采样步骤处理,最终选出一个token。支持以下9种采样器及`mixed`混合模式:
|
||
|
||
**采样器类型说明**
|
||
|
||
| 采样器 | 别名 | 说明 |
|
||
|--------|------|------|
|
||
| `greedy` | - | 贪心采样,直接选择logit最大的token,输出完全确定性,不受temperature等参数影响 |
|
||
| `temperature` | - | 温度采样,将logits除以temperature后做softmax得到概率分布,再按概率随机采样。temperature越高输出越随机,越低越确定 |
|
||
| `topK` | `top_k` | 仅保留logit值最大的K个候选token,丢弃其余token,缩小采样范围后再采样 |
|
||
| `topP` | `top_p` | 核采样(Nucleus Sampling),将token按概率从高到低排序,保留累积概率刚好超过P的最小token集合,丢弃长尾低概率token |
|
||
| `minP` | `min_p` | 最小概率采样,丢弃概率低于阈值P的token。与topP不同,minP是绝对阈值而非累积阈值 |
|
||
| `tfs` | - | 尾部自由采样(Tail Free Sampling),通过计算概率分布的二阶导数来定位分布的"尾部",裁剪掉尾部的低概率token。参数Z控制裁剪程度,Z=1.0表示不裁剪 |
|
||
| `typical` | - | 典型采样(Typical Sampling),保留信息量(-log(p))最接近分布熵的token,丢弃信息量异常高或低的token。参数P控制保留的累积概率 |
|
||
| `penalty` | - | 重复惩罚,对已生成的token施加惩罚以降低重复。支持三种惩罚方式:乘性的repetition_penalty、加性的presence_penalty和频率相关的frequency_penalty |
|
||
| `mixed` | - | 混合模式,按`mixed_samplers`列表中的顺序依次执行多个采样器。logit_bias和banned_tokens会在其他步骤之前执行,penalty会被移到最前面 |
|
||
|
||
> **名称兼容性说明**:`topK`/`top_k`、`topP`/`top_p`、`minP`/`min_p` 在采样器名称和配置参数中均可互换使用。配置参数中同时支持 snake_case 和 camelCase 写法(如 `top_k` 与 `topK`),优先读取 snake_case 版本。旧配置中的 `penalty` 字段会自动映射为 `repetition_penalty`。
|
||
|
||
**配置参数**
|
||
|
||
- sampler_type: 使用的采样器种类,默认为`mixed`。可选值见上表。
|
||
- mixed_samplers: 当`sampler_type`为`mixed`时有效,默认为`["topK", "tfs", "typical", "topP", "min_p", "temperature"]`,模型计算得到的logits会依次经过这些采样器处理。
|
||
- temperature: 温度参数,用于`temperature`/`topP`/`minP`/`tfs`/`typical`采样中的softmax计算,默认为1.0。值越大输出越随机,值越小输出越确定。
|
||
- top_k/topK: Top-K采样的K值,保留概率最大的K个token,默认为40。(支持`top_k`或`topK`两种写法,优先读取`top_k`)
|
||
- top_p/topP: Top-P采样的P值,保留累积概率达到P的最小token集合,默认为0.9。(支持`top_p`或`topP`两种写法,优先读取`top_p`)
|
||
- min_p/minP: Min-P采样的P值,丢弃概率低于P的token,默认为0.1。(支持`min_p`或`minP`两种写法,优先读取`min_p`)
|
||
- tfs_z/tfsZ: TFS采样的Z值,控制尾部裁剪程度,默认为1.0(即不裁剪)。值越小裁剪越激进。(支持`tfs_z`或`tfsZ`两种写法,优先读取`tfs_z`)
|
||
- typical: Typical采样的P值,控制保留的累积概率,默认为1.0(即不过滤)。推荐值0.9~0.99。
|
||
- repetition_penalty: 重复惩罚系数(乘性),对已出现的token,正logit除以该值、负logit乘以该值,使其概率降低。默认为1.0(不惩罚),推荐值1.05~1.5。向后兼容旧配置中的`penalty`字段。
|
||
- presence_penalty: 存在惩罚(加性),对已出现过的每个token的logit减去该值,不论出现几次惩罚相同。默认为0.0。
|
||
- frequency_penalty: 频率惩罚(加性),对已出现的token按出现次数成比例扣减logit,出现N次则减去`N * frequency_penalty`。默认为0.0。
|
||
- penalty_window: 惩罚窗口大小,仅对最近N个token施加惩罚。0表示对全部历史token施加惩罚,默认为0。
|
||
- n_gram: 最大存储的ngram大小,超过此大小的重复ngram将被施加更强惩罚,仅在`penalty`选中时生效,默认为8。
|
||
- ngram_factor: 对重复ngram (n>1) 的额外惩罚倍率,匹配越长惩罚越强(逐级乘以ngram_factor)。默认为1.0(无额外惩罚)。
|
||
- penalty_sampler: `penalty`模式下施加完惩罚项后的最终采样策略,可选`"greedy"`或`"temperature"`,默认`"greedy"`。
|
||
- logit_bias: 对指定token的logit施加偏置,格式为`{"token_id": bias_value}`的JSON对象,正值增加生成概率,负值降低。默认为空。token_id可通过tokenizer获取,示例:
|
||
```json
|
||
{
|
||
"logit_bias": {
|
||
"198": -100.0,
|
||
"151643": 5.0
|
||
}
|
||
}
|
||
```
|
||
上例中,token 198 (如换行符) 的logit减少100(几乎禁止生成),token 151643 的logit增加5(提高生成概率)。
|
||
- banned_tokens: 禁止生成的token id列表,这些token的logit会被设为负无穷。默认为空。示例:`"banned_tokens": [198, 151643]`
|
||
- 投机解码配置项
|
||
- speculative_type: 投机解码算法设置,当前仅支持配置为`lookahead`(使用外接知识库/输入prompt信息去生成草稿做投机验证),通常需要较完备的知识库或者输入prompt与输出重合度较高的场景(例如:代码编辑、文本总结)才有较明显加速。
|
||
- draft_predict_length: 草稿长度,通常设置2-8之间,默认为4。
|
||
- draft_match_strictness: 草稿匹配的严格程度,当有草稿时,是否选取该草稿去做并行验证。可以设置`low`、`medium`、`high`。通常严格程度越高,草稿接受率越高,但是启用并行验证概率也越低。默认为`low`,该参数仅`lookahead`模式设置有效。
|
||
- draft_selection_rule: 草稿选择规则,当有多个草稿时,选取的规则设置。支持`freqxlen`(出现频率与匹配长度最高者)和`fcfs`(最先匹配者)。默认`freqxlen`,该参数仅`lookahead`模式设置有效。
|
||
- ngram_match_maxlen: ngram匹配历史token最长值,默认为4,该参数仅`lookahead`模式设置有效。
|
||
- lookup_file: 用户外接知识库文件路径,默认为`lookup_file.txt`,该参数仅`lookahead`模式设置有效。
|
||
- ngram_update: 是否解码过程实时添加更新ngram信息,默认为`false`,该参数仅`lookahead`模式设置有效。
|
||
- Omni语音生成配置
|
||
- talker_max_new_tokens: 生成时最大语音token数,在Qwen2.5-Omni中50个语音token对应1秒语音,默认为`2048`
|
||
- talker_speaker: 生成语音的音色,Qwen2.5-Omni中支持的音色为:`["Chelsie", "Ethan"]`
|
||
- dit_steps: 生成语音时扩散模型迭代次数,默认为`5`, 建议设置为`5~10`, 越大语音质量越高计算耗时越高;
|
||
- dit_solver: 生成语音时扩散模型求解算法阶数,支持`1, 4`,默认为`1`使用一阶欧拉法;`4`表示四阶龙格库塔法,效果略好但耗时增加4倍;
|
||
|
||
##### 配置文件示例
|
||
- `config.json`
|
||
```json
|
||
{
|
||
"llm_model": "qwen2-1.5b-int4.mnn",
|
||
"llm_weight": "qwen2-1.5b-int4.mnn.weight",
|
||
|
||
"backend_type": "cpu",
|
||
"thread_num": 4,
|
||
"precision": "low",
|
||
"memory": "low",
|
||
"sampler_type": "mixed",
|
||
"mixed_samplers": ["topK", "tfs", "typical", "topP", "min_p", "temperature"],
|
||
"temperature": 0.8,
|
||
"top_k": 40,
|
||
"top_p": 0.9,
|
||
"min_p": 0.05,
|
||
"tfs_z": 1.0,
|
||
"typical": 0.95,
|
||
"repetition_penalty": 1.0,
|
||
"reuse_kv": true
|
||
}
|
||
```
|
||
- `llm_config.json`
|
||
```json
|
||
{
|
||
"hidden_size": 1536,
|
||
"layer_nums": 28,
|
||
"attention_mask": "float",
|
||
"key_value_shape": [
|
||
2,
|
||
1,
|
||
0,
|
||
2,
|
||
128
|
||
],
|
||
"prompt_template": "<|im_start|>user\n%s<|im_end|>\n<|im_start|>assistant\n",
|
||
"is_visual": false,
|
||
"is_single": true
|
||
}
|
||
```
|
||
- `context.json`
|
||
```json
|
||
{
|
||
"tools": [
|
||
{
|
||
"type": "function",
|
||
"function": {
|
||
"name": "get_current_time",
|
||
"description": "获取当前时间"
|
||
}
|
||
}
|
||
],
|
||
"enable_thinking": false
|
||
}
|
||
```
|
||
|
||
#### ChatMessage 多轮对话接口
|
||
|
||
C++ 多轮对话使用 `ChatMessage` 类型,定义为 `std::pair<std::string, std::string>`:
|
||
|
||
- **first**: 消息角色,如 `"system"`, `"user"`, `"assistant"`, `"tool"`
|
||
- **second**: 消息内容,普通文本字符串
|
||
|
||
```cpp
|
||
ChatMessages messages;
|
||
messages.emplace_back("system", "You are a helpful assistant.");
|
||
messages.emplace_back("user", "你好");
|
||
llm->response(messages, &std::cout);
|
||
// 保存assistant回复
|
||
messages.emplace_back("assistant", assistant_output);
|
||
// 继续对话
|
||
messages.emplace_back("user", "介绍一下北京");
|
||
llm->response(messages, &std::cout);
|
||
```
|
||
|
||
**传递复杂消息(tool_calls等)**:当消息包含 `tool_calls`、`reasoning_content` 等额外字段时,将 `first` 设为 `"json"`,`second` 设为完整的 JSON 对象字符串。`apply_chat_template` 会自动将其解析为 JSON 对象传给 Jinja 模板:
|
||
|
||
```cpp
|
||
// 普通消息
|
||
messages.emplace_back("user", "What's the weather in NYC?");
|
||
// 带 tool_calls 的 assistant 消息:first="json", second=完整JSON
|
||
messages.emplace_back("json", R"({"role":"assistant","content":"","tool_calls":[{"id":"call_1","type":"function","function":{"name":"get_weather","arguments":"{\"location\":\"NYC\"}"}}]})");
|
||
// tool 回复
|
||
messages.emplace_back("tool", R"({"temperature": 72, "condition": "sunny"})");
|
||
```
|
||
|
||
#### 推理用法
|
||
`llm_demo`的用法如下:
|
||
```
|
||
# 使用config.json
|
||
## 交互式聊天
|
||
./llm_demo model_dir/config.json
|
||
## 针对prompt中的每行进行回复
|
||
./llm_demo model_dir/config.json prompt.txt
|
||
|
||
# 不使用config.json, 使用默认配置
|
||
## 交互式聊天
|
||
./llm_demo model_dir/llm.mnn
|
||
## 针对prompt中的每行进行回复
|
||
./llm_demo model_dir/llm.mnn prompt.txt
|
||
```
|
||
|
||
- 对于视觉大模型,在prompt中嵌入图片输入
|
||
```
|
||
<img>https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg</img>介绍一下图片里的内容
|
||
# 指定图片大小
|
||
<img><hw>280, 420</hw>https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg</img>介绍一下图片里的内容
|
||
```
|
||
- 对于音频大模型,在prompt中嵌入音频输入
|
||
```
|
||
<audio>https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen2-Audio/audio/translate_to_chinese.wav</audio>介绍一下音频里的内容
|
||
```
|
||
|
||
#### 单个模型对话性能测评
|
||
建议使用config.json, 可以自行配置运行后端、线程数、输出token数限制等选项。
|
||
```
|
||
## 注意:当选择opencl后端时,thread_num需设为68。
|
||
## 注意:测评opencl后端性能时,由于第一次运行会tuning生成缓存文件(性能较慢),因此需要运行第二次(已经有缓存文件)来看性能数据。
|
||
|
||
./llm_demo model_dir/config.json prompt.txt
|
||
```
|
||
|
||
#### LLM Benchmark工具使用
|
||
使用llm_bench可以比较不同模型在不同配置下的性能差异。
|
||
|
||
##### llm_bench参数列表
|
||
```
|
||
usage: ./llm_bench [options]
|
||
|
||
options:
|
||
-h, --help
|
||
-m, --model <filename> (default: ./Qwen2.5-1.5B-Instruct)
|
||
-a, --backends <cpu,opencl,metal> (default: cpu)
|
||
-c, --precision <n> (default: 2) | Note: (0:Normal(for cpu bakend, 'Nornal' is 'High'),1:High,2:Low)
|
||
-t, --threads <n> (default: 4)
|
||
-p, --n-prompt <n> (default: 512)
|
||
-n, --n-gen <n> (default: 128)
|
||
-pg <pp,tg> (default: 512,128)
|
||
-mmp, --mmap <0|1> (default: 0)
|
||
-rep, --n-repeat <n> (default: 5)
|
||
-kv, --kv-cache <true|false> (default: false) | Note: if true: Every time the LLM model generates a new word, it utilizes the cached KV-cache
|
||
-fp, --file-print <stdout|filename> (default: stdout) | If not 'stdout', all test results will be written to the specified file.
|
||
--profile Enable operator-level profiling to print detailed timing statistics
|
||
```
|
||
|
||
##### llm_bench 参数解释
|
||
- '-m | --model': llm.mnn和llm.mnn.weight文件所在的文件夹中config.json文件的路径,而不是文件夹的路径或者mnn/mnn.weight文件的路径。 可以填写多个模型的config.json文件地址,使用英文逗号分隔;
|
||
- '-a | --backends': 指定运行LLM模型的后端,目前MNN仅支持CPU/METAL/OPENCL后端。可以填写多个后端,后端名称均使用英文小写字母,使用英文逗号分隔;
|
||
- '-t | --threads': 指定CPU后端推理时采用的线程数。对于OPENCL后端,该字段表示的不是线程数,而是GPU MODE,当前LLM推理时OpenCL均采取Buffer模式推理,线程数设置为4时性能较优。对于METAL后端对性能的影响较小。可以填写多个线程数,使用英文逗号分隔;
|
||
- '-p | --n-prompt': 指定推理时处理的prompt长度,可填写多个长度,使用英文逗号分隔;测试结果表示LLM模型的首字符响应速度;
|
||
- '-n | --n-gen': 指定推理时生成字符的长度,可填写多个长度,使用英文逗号分隔;测试结果表示LLM模型在不考虑历史KV信息时生成一个字符的速度,即Attention算子中past_kv_length=0;
|
||
- '-pg': 指定prompt长度和生成字符数量,测试中该项的耗时是前两项('-p'和'-n')耗时的总和,处理字符的数量是prompt长度和生成字符数量之和;
|
||
- '-mmp | --mmap': 指定模型加载时是否使用mmap技术,只能填写一个候选项,0或1;该项对模型推理性能无影响;
|
||
- '-rep | --n-repeat': 每一个测试实例重复的次数,最终结果取平均数,并计算性能的标准差;
|
||
- '-kv | --kv-cache': 当设置为true时,测试时在LLM模型decode阶段会考虑历史KV信息,即测试方法和运行'llm_demo'程序一致;
|
||
- '-fp | --file-print': 默认输出到屏幕上;如果指定了输出文件,最终的测试结果会以追加的方式以markdown格式写入到文件中,不会删除文件中已有的内容;文件不存在会自动创建。
|
||
|
||
##### 命令行运行llm_bench
|
||
在build目录下运行
|
||
```bash
|
||
./llm_bench -m ./Qwen2.5-1.5B-Instruct/config.json,./Qwen2.5-0.5B-Instruct/config.json -a cpu,opencl,metal -c 1,2 -t 8,12 -p 16,32 -n 10,20 -pg 8,16 -mmp 0 -rep 4 -kv true -fp ./test_result
|
||
```
|
||
|
||
#### 多Prompt场景下KVCache选择性复用
|
||
rollback_demo提供了多Prompt场景下自行选择复用部分kvcache的示例代码。
|
||
```bash
|
||
./rollback_demo /path/to/model_dir/config.json /path/to/prompt.txt <cache_prefix_in_disk> <max_token_number>
|
||
```
|
||
其中,prompt.txt需要包含至少三组prompt。
|
||
- cache_prefix_in_disk需要设置为0或1。
|
||
- cache_prefix_in_disk 设置1表示:第一段Prompt是后续Prompt的公共前缀Prompt,第二、三段Prompt分别是基于第一段Prompt后续的文本内容。第一次启动会将前缀Prompt的KVCache缓存在磁盘文件中。第二次启动会跳过公共前缀Prompt的Prefill,直接在磁盘中加载,提升Prefill速度。。
|
||
- cache_prefix_in_disk 设置0表示:在多段Prompt下,如何删除不需要的KVCache,仅保留关联性的KVCache示例。
|
||
|
||
#### GPTQ权重
|
||
需要使用GPTQ权重,可以在导出模型时,使用`--gptq_path PATH`来指定的路径,使用如下:
|
||
```bash
|
||
# 导出GPTQ量化的模型
|
||
python llmexport.py --path /path/to/Qwen2.5-0.5B-Instruct --gptq_path /path/to/Qwen2.5-0.5B-Instruct-GPTQ-Int4 --export mnn
|
||
```
|
||
|
||
#### LoRA权重
|
||
LoRA权重有两使用方式:1. 合并LoRA权重到原始模型;2. LoRA模型单独导出。
|
||
|
||
第一种模式速度更快,使用更简单但是不支持运行时切换;第二种略微增加一些内存和计算开销,但是更加灵活,支持运行时切换LoRA,适合多LoRA场景。
|
||
##### 融合LoRA
|
||
|
||
将LoRA权重合并到原始模型中导出,在模型导出时指定`--lora_path PATH`参数,默认使用合并方式导出,使用如下:
|
||
```bash
|
||
# 导出LoRA合并的模型
|
||
python llmexport.py --path /path/to/Qwen2.5-0.5B-Instruct --lora_path /path/to/lora --export mnn
|
||
```
|
||
|
||
融合LoRA模型使用与原始模型使用方法完全一样。
|
||
|
||
##### 分离LoRA
|
||
|
||
将LoRA单独导出为一个模型,支持运行时切换,在模型导出时指定`--lora_path PATH`参数,并指定`--lora_split`,就会将LoRA分离导出,使用如下:
|
||
```bash
|
||
python llmexport.py --path /path/to/Qwen2.5-0.5B-Instruct --lora_path /path/to/lora --lora_split --export mnn
|
||
```
|
||
导出后模型文件夹内除了原始模型外,还会增加`lora.mnn`,这个就是lora模型文件。
|
||
|
||
运行时创建lora模型
|
||
```cpp
|
||
// 创建并加载base模型
|
||
std::unique_ptr<Llm> llm(Llm::createLLM(config_path));
|
||
llm->load();
|
||
// 创建lora模型,支持多个lora模型并存,支持并发
|
||
{
|
||
std::mutex creat_mutex;
|
||
auto chat = [&](const std::string& lora_name) {
|
||
MNN::BackendConfig bnConfig;
|
||
auto newExe = Executor::newExecutor(MNN_FORWARD_CPU, bnConfig, 1);
|
||
ExecutorScope scope(newExe);
|
||
Llm* current_llm = nullptr;
|
||
{
|
||
std::lock_guard<std::mutex> guard(creat_mutex);
|
||
current_llm = llm->create_lora(lora_name);
|
||
}
|
||
current_llm->response("Hello");
|
||
};
|
||
std::thread thread1(chat, "lora_1.mnn");
|
||
std::thread thread2(chat, "lora_2.mnn");
|
||
thread1.join();
|
||
thread2.join();
|
||
}
|
||
```
|
||
|
||
#### 获取语音输出
|
||
使用Omni模型时,可以使用接口`setWavformCallback`获取语音输出,使用接口`generateWavform`开始输出语音。
|
||
注意`setWavformCallback`需要在文本生成前调用, `generateWavform`在文本生成结束后调用,示例如下:
|
||
|
||
1. 保存语音到文件中
|
||
```cpp
|
||
#include <audio/audio.hpp>
|
||
int main() {
|
||
// save wavform to file for debug
|
||
std::vector<float> waveform;
|
||
llm->setWavformCallback([&](const float* ptr, size_t size, bool last_chunk) {
|
||
waveform.reserve(waveform.size() + size);
|
||
waveform.insert(waveform.end(), ptr, ptr + size);
|
||
if (last_chunk) {
|
||
auto waveform_var = MNN::Express::_Const(waveform.data(), {(int)waveform.size()}, MNN::Express::NCHW, halide_type_of<float>());
|
||
MNN::AUDIO::save("output.wav", waveform_var, 24000);
|
||
waveform.clear();
|
||
}
|
||
return true;
|
||
});
|
||
llm->response("Hello");
|
||
// generate wavform
|
||
llm->generateWavform();
|
||
return 0;
|
||
}
|
||
|
||
```
|
||
2. 流式播放语音(Mac/iOS为例)
|
||
```cpp
|
||
#include <thread>
|
||
#include <AudioToolbox/AudioToolbox.h>
|
||
|
||
struct AudioPlayer {
|
||
AudioStreamBasicDescription format;
|
||
std::vector<float> audioBuffer;
|
||
std::mutex bufferMutex;
|
||
std::condition_variable bufferCondVar;
|
||
bool doneGenerating = false;
|
||
std::thread playThread;
|
||
AudioPlayer() {
|
||
format.mSampleRate = 24000;
|
||
format.mFormatID = kAudioFormatLinearPCM;
|
||
format.mFormatFlags = kLinearPCMFormatFlagIsFloat;
|
||
format.mBytesPerPacket = sizeof(float);
|
||
format.mFramesPerPacket = 1;
|
||
format.mBytesPerFrame = sizeof(float);
|
||
format.mChannelsPerFrame = 1;
|
||
format.mBitsPerChannel = sizeof(float) * 8;
|
||
}
|
||
bool play(const float* ptr, size_t size, bool last_chunk);
|
||
};
|
||
|
||
void AudioQueueCallback(void* userData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
|
||
AudioPlayer* context = static_cast<AudioPlayer*>(userData);
|
||
std::unique_lock<std::mutex> lock(context->bufferMutex);
|
||
int samplesToCopy = inBuffer->mAudioDataBytesCapacity / sizeof(float);
|
||
while (context->audioBuffer.size() < samplesToCopy) {
|
||
if (context->doneGenerating) { break; }
|
||
context->bufferCondVar.wait(lock);
|
||
}
|
||
if (context->audioBuffer.size() < samplesToCopy) {
|
||
samplesToCopy = context->audioBuffer.size();
|
||
}
|
||
memcpy(inBuffer->mAudioData, context->audioBuffer.data(), samplesToCopy * sizeof(float));
|
||
context->audioBuffer.erase(context->audioBuffer.begin(), context->audioBuffer.begin() + samplesToCopy);
|
||
inBuffer->mAudioDataByteSize = samplesToCopy * sizeof(float);
|
||
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, nullptr);
|
||
}
|
||
|
||
void playAudioData(AudioPlayer* context) {
|
||
AudioQueueRef queue;
|
||
AudioQueueNewOutput(&context->format, AudioQueueCallback, context, nullptr, nullptr, 0, &queue);
|
||
AudioQueueBufferRef buffers[3];
|
||
UInt32 bufferSize = 1024 * sizeof(float);
|
||
for (int i = 0; i < 3; ++i) {
|
||
AudioQueueAllocateBuffer(queue, bufferSize, &buffers[i]);
|
||
AudioQueueCallback(context, queue, buffers[i]);
|
||
}
|
||
AudioQueueStart(queue, nullptr);
|
||
while (true) {
|
||
{
|
||
std::lock_guard<std::mutex> lock(context->bufferMutex);
|
||
if (context->doneGenerating && context->audioBuffer.empty())
|
||
break;
|
||
}
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||
}
|
||
AudioQueueStop(queue, true);
|
||
for (int i = 0; i < 3; ++i) {
|
||
AudioQueueFreeBuffer(queue, buffers[i]);
|
||
}
|
||
AudioQueueDispose(queue, true);
|
||
}
|
||
|
||
bool AudioPlayer::play(const float* ptr, size_t size, bool last_chunk) {
|
||
{
|
||
std::lock_guard<std::mutex> lock(bufferMutex);
|
||
audioBuffer.reserve(audioBuffer.size() + size);
|
||
audioBuffer.insert(audioBuffer.end(), ptr, ptr + size);
|
||
}
|
||
if (playThread.joinable()) {
|
||
bufferCondVar.notify_all();
|
||
} else {
|
||
playThread = std::thread(playAudioData, this);
|
||
printf(">>>>>>>> PLAY START\n");
|
||
}
|
||
if (last_chunk) {
|
||
doneGenerating = true;
|
||
bufferCondVar.notify_all();
|
||
if (playThread.joinable()) {
|
||
playThread.join();
|
||
printf(">>>>>>>> PLAY END\n");
|
||
}
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
int main() {
|
||
//....
|
||
AudioPlayer audio_player;
|
||
llm->setWavformCallback([&](const float* ptr, size_t size, bool last_chunk) {
|
||
return audio_player.play(ptr, size, last_chunk);
|
||
});
|
||
//....
|
||
llm->response("Hello");
|
||
// generate wavform
|
||
llm->generateWavform();
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
### Python 中使用
|
||
参考 `pymnn/examples/MNNLlm` 下面的 demo 使用
|
||
|
||
```
|
||
import MNN.llm as llm
|
||
import sys
|
||
|
||
if len(sys.argv) < 2:
|
||
print('usage: python llm_example.py <path_to_model_config>')
|
||
exit(1)
|
||
|
||
config_path = sys.argv[1]
|
||
# create model
|
||
qwen = llm.create(config_path)
|
||
# load model
|
||
qwen.load()
|
||
|
||
# response stream
|
||
out = qwen.response('你好', True)
|
||
print(out)
|
||
|
||
out_ids = qwen.generate([151644, 872, 198, 108386, 151645, 198, 151644, 77091])
|
||
print(out_ids)
|
||
```
|
||
|
||
## NPU 推理 LLM
|
||
|
||
使用NPU推理,需要特定的导出参数,并针对目标设备转换出相应的模型。目前支持使用高通芯片和MTK芯片的NPU进行推理。一般流程是:LLM模型导出->转换成对应设备NPU模型->推到目标设备运行
|
||
|
||
### LLM 模型导出
|
||
NPU运行LLM需要特定的量化格式,需要按如下参数以导出 mnn
|
||
llmexport脚本导出在NPU上运行的模型时,必须使用的选项有:
|
||
- --generate_for_npu: 导出在NPU上运行的模型
|
||
- --seperate_embed: NPU必须使用embedding层和lm层分开存储
|
||
- --sym: 目前NPU仅支持权重对称量化
|
||
用于提高量化精度可以使用的选项,选择其一即可,不可以同时使用:
|
||
- --smooth 使用Smooth量化算法提高精度
|
||
- --omni:使用Omni量化算法提高精度
|
||
部分选项说明:
|
||
- QNN已经支持了feature map使用非对称量化,转模型时可以不使用`--act_sym`,即该选项可视情况加或者不加;
|
||
- NPU目前仅支持feature map使用16bit量化以提高模型精度,所以转模型时加上选项`--act_bit=16`;
|
||
- 经过测试,截止2026年1月,仅仅在高通8Gen5芯片上使用QNN推理时,权重是4bit量化且group=64时,模型性能会比权重8bit量化,group=0时更好。
|
||
- 如果是要转换出在QNN上运行的LLM模型,LM层也会量化,该层的权重量化参数和其他Linear层一致
|
||
- 模型用于量化的校准数据集来源于HuggingFace的wikitext数据集,如果你想要使用指定的多个prompt作为校准数据集,可以使用`--calib_data`选项
|
||
|
||
`--smooth --act_bit=16 --quant_block=0 --lm_quant_bit=16 --quant_bit=4 --seperate_embed --sym --act_sym`
|
||
|
||
eg:
|
||
```
|
||
python3 llmexport.py --path /YouPath/Dowload/models/Qwen/Qwen3-4B --export mnn --smooth --act_bit=16 --quant_block=0 --lm_quant_bit=16 --seperate_embed --quant_bit=4 --sym --act_sym
|
||
```
|
||
或者你也可以自定义校准数据集,并使用Omni算法提高量化精度:
|
||
```
|
||
python llmexport.py --path /YouPath/Dowload/models/Qwen/Qwen3-0.6B --export mnn --quant_block 64 --quant_bit 4 --generate_for_npu --seperate_embed --act_bit=16 --sym --omni --hqq --calib_data /Your/prompt.txt
|
||
```
|
||
|
||
### QNN LLM
|
||
|
||
#### 获得QNN依赖
|
||
|
||
可通过以下步骤获取依赖:
|
||
- [注册高通账号](https://myaccount.qualcomm.com/signup)
|
||
- 访问Qualcomm AI Engine Direct SDK(即QNN SDK),下载SDK,并解压。比如`/home/xiaying/third/qnn/qairt/2.38.0.250901`
|
||
- 修改`~/.bashrc` ,增加SDK路径到环境变量, 然后运行 `source ~/.bashrc` 或者重启终端。eg:
|
||
|
||
```
|
||
export QNN_SDK_ROOT=/home/xiaying/third/qnn/qairt/2.38.0.250901
|
||
export QNN_ROOT=/home/xiaying/third/qnn/qairt/2.38.0.250901
|
||
export HEXAGON_SDK_ROOT=/home/xiaying/third/qnn/qairt/2.38.0.250901
|
||
```
|
||
|
||
#### 构建 QNN 模型
|
||
|
||
在模型转换器编译时,增加`-DMNN_QNN=ON -DMNN_QNN_CONVERT_MODE=ON`,eg:
|
||
|
||
```
|
||
cd ${MNN_ROOT}
|
||
mkdir build && cd build
|
||
cmake .. -DMNN_QNN=ON -DMNN_QNN_CONVERT_MODE=ON -DMNN_BUILD_TOOLS=ON -DMNN_BUILD_LLM=ON
|
||
make -j16
|
||
```
|
||
|
||
|
||
使用 `npu/generate_llm_qnn.py` 构建 qnn 模型。该脚本支持三种使用模式:转换 LLM 语言模型、转换 Visual 视觉模型、以及通过自定义 `input_json` 转换任意模型。
|
||
|
||
##### 参数说明
|
||
|
||
| 参数 | 类型 | 默认值 | 说明 |
|
||
| :--- | :--- | :----- | :--- |
|
||
| `--model` | str | (必填) | MNN 模型所在目录路径 |
|
||
| `--soc_id` | int | (必填) | 目标设备的 SOC ID,如 8Gen3 为 57 |
|
||
| `--dsp_arch` | str | (必填) | 目标设备的 DSP 架构,如 8Gen3 为 v75 |
|
||
| `--model_name` | str | `llm.mnn` | 要转换的模型文件名,如 `llm.mnn` 或 `visual.mnn` |
|
||
| `--image_sizes` | str | `512x512` | 视觉模型的输入图片尺寸,支持多尺寸,如 `"224x224,384x384,512x512"` |
|
||
| `--input_json` | str | `""` | 自定义输入 shape 的 JSON 文件路径,非空时使用自定义模式 |
|
||
| `--external_file` | str | `""` | 外部权重文件名(相对于 `--model` 目录),配合 `--input_json` 使用 |
|
||
| `--mnn_path` | str | `../../../build/` | MNN 编译产物路径 |
|
||
| `--cache_path` | str | `tmp` | 转换过程中的临时缓存目录 |
|
||
| `--chunk_size` | int | `128` | NPU 的 chunk 大小 |
|
||
| `--max_history_token` | int | `0` | 最大历史 token 数,0 表示不限制 |
|
||
|
||
##### 用法一:转换 LLM 语言模型
|
||
|
||
默认模式,将 `llm.mnn` 转换为 QNN 模型。脚本会自动从模型目录下的 `llm_config.json` 读取 `hidden_size` 等配置信息,生成对应的输入描述并完成转换。
|
||
|
||
```bash
|
||
cd ${MNN_ROOT}
|
||
cd transformers/llm/export
|
||
python3 npu/generate_llm_qnn.py \
|
||
--model /path/to/Qwen3.5-2B-MNN/ \
|
||
--soc_id=57 \
|
||
--dsp_arch=v75
|
||
```
|
||
|
||
转换完成后,会在模型目录下生成 `qnn/` 子目录和 `config_qnn.json` 配置文件。
|
||
|
||
##### 用法二:转换 Visual 视觉模型
|
||
|
||
通过指定 `--model_name visual.mnn` 进入视觉模型转换模式。需要通过 `--image_sizes` 指定支持的输入图片尺寸(格式为 `WxH`,多个尺寸用逗号分隔)。目前支持 Qwen2.5-VL、Qwen3-VL、Qwen3.5-VL 和 FastVLM 系列视觉模型。
|
||
|
||
```bash
|
||
cd ${MNN_ROOT}
|
||
cd transformers/llm/export
|
||
python3 npu/generate_llm_qnn.py \
|
||
--model /path/to/Qwen2.5-VL-3B-MNN/ \
|
||
--soc_id=57 \
|
||
--dsp_arch=v75 \
|
||
--image_sizes 256x256 \
|
||
--model_name visual.mnn
|
||
```
|
||
|
||
支持多个图片尺寸:
|
||
```bash
|
||
python3 npu/generate_llm_qnn.py \
|
||
--model /path/to/Qwen2.5-VL-3B-MNN/ \
|
||
--soc_id=57 \
|
||
--dsp_arch=v75 \
|
||
--image_sizes "224x224,384x384,512x512" \
|
||
--model_name visual.mnn
|
||
```
|
||
|
||
转换完成后,会在模型目录下生成 `qnn/` 子目录和 `config_qnn.json`(其中 `visual_model` 字段指向转换后的 QNN 视觉模型)。
|
||
|
||
##### 用法三:使用自定义 input_json 转换任意模型
|
||
|
||
当需要转换非标准模型或自定义输入 shape 时,可以通过 `--input_json` 指定一个 JSON 文件来描述模型的输入输出信息。此模式下需要同时指定 `--model_name`(模型文件名)和 `--external_file`(权重文件名)。
|
||
|
||
input_json 文件格式示例:
|
||
```json
|
||
{
|
||
"configs": [
|
||
{
|
||
"inputs": [
|
||
{"name": "input_0", "shape": [1, 3, 224, 224]},
|
||
{"name": "input_1", "shape": [1, 10], "type": "int"}
|
||
],
|
||
"outputs": ["output_0"]
|
||
},
|
||
{
|
||
"inputs": [
|
||
{"name": "input_0", "shape": [1, 3, 384, 384]},
|
||
{"name": "input_1", "shape": [1, 20], "type": "int"}
|
||
],
|
||
"outputs": ["output_0"]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
其中 `configs` 数组中的每个元素代表一组输入 shape 配置,脚本会为每组配置生成对应的 QNN 模型。`type` 字段可选,默认为 float,支持 `"int"` 等类型。
|
||
|
||
使用示例:
|
||
```bash
|
||
cd ${MNN_ROOT}
|
||
cd transformers/llm/export
|
||
python3 npu/generate_llm_qnn.py \
|
||
--model /path/to/MyModel-MNN/ \
|
||
--soc_id=57 \
|
||
--dsp_arch=v75 \
|
||
--input_json /path/to/input.json \
|
||
--model_name my_model.mnn \
|
||
--external_file my_model.mnn.weight
|
||
```
|
||
|
||
> **注意**:使用 `--input_json` 模式时,脚本不会自动生成 `config_qnn.json`,需要用户自行配置运行时的配置文件。
|
||
|
||
目标设备`soc_id` 和 `dsp_arch` 可在高通官方查询,如下为一些设备的参考
|
||
|
||
| 硬件 | SOC ID | HEXAGON ARCH |
|
||
| :------ | :----- | :----------- |
|
||
| 8 Gen 1 | 36 | 69 |
|
||
| 8 Gen 2 | 43 | 73 |
|
||
| 8 Gen 3 | 57 | 75 |
|
||
| 8 Elite | 69 | 79 |
|
||
|
||
|
||
***执行成功后,会在 model 目录下产出 config_qnn.json 及 model/qnn 目录***
|
||
|
||
***构建完成后,model 目录下的 llm.mnn 及 llm.mnn.weight 不再需要,可以删除以减少文件总大小***
|
||
|
||
#### Android设备上运行QNN LLM
|
||
|
||
- 编译 MNN Android 库并推送到目标设备,编译时需要增加 `-DMNN_QNN=ON -DMNN_WITH_PLUGIN=ON`,eg:
|
||
|
||
```
|
||
cd ${MNN_ROOT}
|
||
cd project/android
|
||
mkdir build_64 && cd build_64
|
||
../build_64.sh -DMNN_QNN=ON -DMNN_WITH_PLUGIN=ON -DMNN_BUILD_LLM=ON -DMNN_LOW_MEMORY=ON
|
||
../updateTest.sh
|
||
```
|
||
|
||
- 参考如下脚本把 QNN 相关 so 放到 Android 对应测试目录中
|
||
|
||
```
|
||
ANDROID_WORKING_DIR=/data/local/tmp/MNN/
|
||
HEXAGON_ARCH=75
|
||
adb push ${QNN_SDK_ROOT}/lib/aarch64-android/libQnnHtp.so ${ANDROID_WORKING_DIR}
|
||
adb push ${QNN_SDK_ROOT}/lib/aarch64-android/libQnnHtpV${HEXAGON_ARCH}Stub.so ${ANDROID_WORKING_DIR}
|
||
adb push ${QNN_SDK_ROOT}/lib/hexagon-v${HEXAGON_ARCH}/unsigned/libQnnHtpV${HEXAGON_ARCH}Skel.so ${ANDROID_WORKING_DIR}
|
||
adb push ${QNN_SDK_ROOT}/lib/aarch64-android/libQnnSystem.so ${ANDROID_WORKING_DIR}
|
||
```
|
||
|
||
- 推送模型并执行
|
||
|
||
推送模型:
|
||
```
|
||
cd ${MNN_ROOT}
|
||
cd transformers/llm/export
|
||
adb push model /data/local/tmp/MNN/model
|
||
```
|
||
|
||
运行:
|
||
```
|
||
cd ${MNN_ROOT}
|
||
project/android/testCommon.sh ./llm_demo model/config_qnn.json
|
||
```
|
||
|
||
### MTK LLM
|
||
#### 获得 MTK SDK
|
||
- 目前MTK没有开放SDK获得方案,需自行联系MTK取得支持,获得对应的SDK
|
||
- 获取后,修改`~/.bashrc`,添加环境变量,eg:
|
||
|
||
```
|
||
export NEURON_SDK=/home/xiaying/third/mtk/neuropilot-sdk-basic-7.0.8-build20240807/neuron_sdk
|
||
```
|
||
|
||
#### 构建 MLDA 模型
|
||
MLDA 是 MTK 的 NPU 推理引擎,需要把 MNN 模型转成 MLDA 模型才可在其NPU上运行
|
||
|
||
- 增加MNN对应的预转换后端配置 `-DMNN_NEUROPILOT=ON` ,eg:
|
||
|
||
```
|
||
cd ${MNN_ROOT}
|
||
mkdir build && cd build
|
||
cmake ../ -DMNN_BUILD_CONVERTER=ON -DMNN_BUILD_LLM=ON -DMNN_NEUROPILOT=ON
|
||
make -j4
|
||
```
|
||
|
||
- 确定设备的`mlda`版本号和编译选项,并修改`source/backend/neuropilot/npu_convert.py`的`archoptions`,当前默认配置为`--arch=mdla5.1 --l1-size-kb=7168 --num-mdla=4`,支持天玑9300的NPU编译
|
||
|
||
- 使用 `npu/generate_llm_mlda.py` 构建 MLDA 模型
|
||
|
||
```
|
||
cd ${MNN_ROOT}
|
||
cd transformers/llm/export
|
||
python3 npu/generate_llm_mlda.py --model model
|
||
```
|
||
|
||
执行成功后,会在 model 目录下产出`config_mlda.json`与`mlda`目录。
|
||
|
||
***生成后,原先的llm.mnn和llm.mnn.weight可以删除***
|
||
|
||
#### Android设备上运行 MLDA LLM
|
||
|
||
- 增加`-DMNN_NEUROPILOT=ON -DMNN_WITH_PLUGIN=ON`编译 MNN Android 库
|
||
|
||
```
|
||
cd ${MNN_ROOT}
|
||
cd project/android/
|
||
mkdir build_64
|
||
cd build_64
|
||
../build_64.sh -DMNN_NEUROPILOT=ON -DMNN_WITH_PLUGIN=ON -DMNN_BUILD_LLM=ON
|
||
../updateTest.sh
|
||
```
|
||
|
||
- 推送模型并执行
|
||
|
||
推送模型:
|
||
```
|
||
cd ${MNN_ROOT}
|
||
cd transformers/llm/export
|
||
adb push model /data/local/tmp/MNN/model
|
||
```
|
||
|
||
运行:
|
||
```
|
||
cd ${MNN_ROOT}
|
||
project/android/testCommon.sh ./llm_demo model/config_mlda.json
|
||
``` |