docs/transformers/llm.md
基于MNN开发的LLM推理引擎,支持目前主流的开源LLM模型。该功能分为2部分:
此步骤是将原始的 PyTorch 模型(如 Qwen2 系列)转换为 MNN 引擎可以加载和推理的格式。
安装依赖: 进入导出工具目录并安装必要的 Python 包。
cd ./transformers/llm/export
pip install -r requirements.txt
准备原始模型:
将需要部署的开源 LLM 模型(例如 Qwen2-0.5B-Instruct)克隆到本地。务必确保 git lfs 已安装,以下载完整的模型文件。
git lfs install
git clone https://www.modelscope.cn/qwen/Qwen2-0.5B-Instruct.git
执行导出命令:
运行 llmexport.py 脚本,将模型、Tokenizer、Embedding 等导出为 MNN 格式。
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 等文件的模型目录。(可选)高级功能:
--quant_bit 4 和 --quant_block 128 等参数可以调节量化的Bits数,默认为4 bit , block size 64。通过 --hqq 或 --awq 或 --omni 可以启用对应算法以提升量化后的模型精度,一般建议增加--hqq--lora_path 合并或分离 LoRA 权重。Tie-Embeding技术,默认不会导出embeddings_bf16.bin,而是复用llm.mnn.weight中的lm权重,需要提升embed精度可以设置 --seperate_embed 分离出embeddings_bf16.bin。--gptq_path 应用预量化好的 GPTQ 权重。mnn 失败,或者需要fp16/fp32精度的模型,可先导出 onnx,再用 MNNConvert 工具手动转换。此步骤是编译 MNN 的 C++ 推理引擎,使其支持 LLM 推理功能。
配置编译选项:
在标准的 MNN 编译命令中,必须添加 -DMNN_BUILD_LLM=true 以启用 LLM 支持。
-DMNN_BUILD_LLM_OMNI=ON。-DMNN_AVX512=true 以利用 AVX512 指令集加速。-DMNN_OPENCL=true 以利用 GPU 加速。-DMNN_METAL=ON 以利用 GPU 加速。emcmake 并配置 -DMNN_FORBID_MULTI_THREAD=ON 等特定选项。执行编译: 以 Linux/Mac 为例:
mkdir build && cd build
cmake .. -DMNN_BUILD_LLM=true -DMNN_AVX512=true # 根据平台调整选项
make -j16
编译完成后,会生成核心库文件(如 libMNN.so, libllm.so)。
此步骤是配置模型运行参数并启动推理。
准备模型目录:
将第一步导出的所有文件(llm.mnn, llm.mnn.weight, tokenizer.mtok, embeddings_bf16.bin, llm_config.json)放在同一个文件夹下。
配置 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 (内存分块) 等。{
"backend_type": "cpu",
"thread_num": 4,
"precision": "low",
"sampler_type": "mixed",
"temperature": 0.7,
"topP": 0.9,
"reuse_kv": true
}
运行推理 Demo:
使用编译好的 llm_demo 工具进行推理。
./llm_demo /path/to/model_dir/config.json
./llm_demo /path/to/model_dir/config.json /path/to/prompt.txt
<audio> 标签。(可选)性能基准测试:
使用 llm_bench 工具对不同后端、线程数、Prompt 长度等配置进行性能压测,以找到最优配置。
./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 进行推理
llmexportllmexport是一个llm模型导出工具,能够将llm模型导出为onnx和mnn模型。
cd ./transformers/llm/export
pip install -r requirements.txt
git lfs install
git clone https://www.modelscope.cn/qwen/Qwen2-0.5B-Instruct.git
clone 后检查一下模型大小,有可能因为lfs没安装导致下载的是空模型
llmexport.py导出模型cd ./transformers/llm/export
# 导出模型,tokenizer和embedding,并导出对应的mnn模型
python llmexport.py \
--path /path/to/Qwen2-0.5B-Instruct \
--export mnn
config.json: 模型运行时的配置,可手动修改;embeddings_bf16.bin: 模型的embedding权重二进制文件,推理时使用;llm.mnn: 模型的mnn文件,推理时使用;llm.mnn.json: mnn模型对应的json文件,apply_lora或gptq量化权重时使用;llm.mnn.weight: 模型的mnn权重,推理时使用;llm.onnx: 模型的onnx文件,不包含权重,推理时不使用;llm_config.json: 模型的配置信息,推理时使用;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
--export mnn,注意,你需要先安装pymnn或者通过--mnnconvert选项指定MNNConvert工具的地址,两种条件必须满足其中一个。如果没有安装pymnn并且没有通过--mnnconvert指定MNNConvert工具的地址,那么llmexport.py脚本会在目录"../../../build/"下寻找MNNConvert工具,需保证该目录下存在MNNConvert文件。此方案目前支持导出4bit和8bit模型--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_path--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 文件读取权重的工具,可以降低内存需求,提高转换速度。使用方法如下:
https://modelscope.cn/organization/MNN
pip install MNN
mnnconvert -f MNN --modelFile model/llm.mnn --JsonFile model/llm.mnn.json
使用 safetensors2mnn.py 读取权重:
python3 safetensors2mnn.py --path /Users/xtjiang/.cache/modelscope/hub/Qwen/Qwen2___5-0___5B-Instruct --mnn_dir model
safetensors2mnn.py 支持设定量化参数,和 llmexport.py 一致
使用 gguf2mnn.py 读取 gguf 文件
python3 gguf2mnn.py --gguf ~/third/llama.cpp/build/ggml-model-Q4_K.gguf --mnn_dir model
目前本方案不支持多模态的模型转换。
从源码编译 在原有编译过程中增加llm开关即可:
-DMNN_BUILD_LLM=ON
若需要开启Omni功能(支持图像/音频输入),增加MNN_BUILD_LLM_OMNI选项
-DMNN_BUILD_LLM=ON -D MNN_BUILD_LLM_OMNI=ON
以 mac / linux 为例 :
make build
cd build
cmake ../ -DMNN_BUILD_LLM=true
make -j16
x86架构额外加 MNN_AVX512 的宏:
make build
cd build
cmake ../ -DMNN_BUILD_LLM=true -DMNN_AVX512=true
make -j16
MNN_OPENCL的宏cd project/android
mkdir build_64
../build_64.sh -DMNN_BUILD_LLM=true -DMNN_OPENCL=true -DMNN_USE_LOGCAT=true
高通设备部分视觉模型支持NPU功能,可增加MNN_QNN宏启用QNN功能。QNN运行分2种模式:
MNN_WITH_PLUGIN宏。cd project/android
mkdir build_64
../build_64.sh -DMNN_BUILD_LLM=true -DMNN_OPENCL=true -DMNN_QNN=true -DMNN_WITH_PLUGIN=true -DMNN_USE_LOGCAT=true
sh package_scripts/ios/buildiOS.sh -DMNN_BUILD_LLM=true
环境配置参考 https://mnn-docs.readthedocs.io/en/latest/compile/engine.html#web
libMNN.a,libMNN_Express.a,libllm.amkdir buildweb
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release -DMNN_FORBID_MULTI_THREAD=ON -DMNN_USE_THREAD_POOL=OFF -DMNN_USE_SSE=OFF -DMNN_BUILD_LLM=true
make -j16
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
配置文件支持以下配置:
模型文件信息
llm_config.json的实际名称路径为base_dir + llm_config,默认为base_dir + 'config.json'llm.mnn的实际名称路径为base_dir + llm_model,默认为base_dir + 'llm.mnn'llm.mnn.weight的实际名称路径为base_dir + llm_weight,默认为base_dir + 'llm.mnn.weight'block_{idx}.mnn的实际路径为base_dir + block_model,默认为base_dir + 'block_{idx}.mnn'lm.mnn的实际路径为base_dir + lm_model,默认为base_dir + 'lm.mnn'base_dir + embedding_model,默认为base_dir + 'embedding.mnn'base_dir + embedding_file,默认为base_dir + 'embeddings_bf16.bin'tokenizer.mtok的实际名称路径为base_dir + tokenizer_file,默认为base_dir + 'tokenizer.mtok'base_dir + visual_model,默认为base_dir + 'visual.mnn'、base_dir + audio_model,默认为base_dir + 'audio.mnn'base_dir + talker_model,默认为base_dir + 'talker.mnn'base_dir + talker_weight,默认为base_dir + 'talker.mnn.weight'base_dir + talker_embedding_file,默认为base_dir + 'talker_embeddings_bf16.bin'base_dir + predit_model,默认为base_dir + 'predit.mnn'base_dir + dit_model,默认为base_dir + 'dit.mnn'base_dir + bigvgan_model,默认为base_dir + 'bigvgan.mnn'base_dir + spk_dict,默认为base_dir + 'spk_dict.txt'base_dir + context_file,默认base_dir + 'context.json',内容格式为json格式的上下文信息,包含:如tools,enable_thinking等信息。推理配置
512kv cache,默认为false.attention_modeattention_mode = flash_attention * 8 + kv_quant_mode,默认为80, 8, 16,默认为8,目前仅支持Metal后端,含义如下:
NSString *tempDirectory = NSTemporaryDirectory();llm->set_config("{\"tmp_path\":\"" + std::string([tempDirectory UTF8String]) + "\"}")硬件配置
"cpu"4; OpenCL推理时使用68(不是传统意义的线程数,代表的是opencl buffer存储和tuning wide模式)"low",尽量使用fp16"low",开启运行时量化与CPU动态量化相关的配置,提升精度、性能
0, 1, 2, 8, 9, 10,默认是0,含义如下:
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。
配置参数
mixed。可选值见上表。sampler_type为mixed时有效,默认为["topK", "tfs", "typical", "topP", "min_p", "temperature"],模型计算得到的logits会依次经过这些采样器处理。temperature/topP/minP/tfs/typical采样中的softmax计算,默认为1.0。值越大输出越随机,值越小输出越确定。top_k或topK两种写法,优先读取top_k)top_p或topP两种写法,优先读取top_p)min_p或minP两种写法,优先读取min_p)tfs_z或tfsZ两种写法,优先读取tfs_z)penalty字段。N * frequency_penalty。默认为0.0。penalty选中时生效,默认为8。penalty模式下施加完惩罚项后的最终采样策略,可选"greedy"或"temperature",默认"greedy"。{"token_id": bias_value}的JSON对象,正值增加生成概率,负值降低。默认为空。token_id可通过tokenizer获取,示例:
{
"logit_bias": {
"198": -100.0,
"151643": 5.0
}
}
"banned_tokens": [198, 151643]投机解码配置项
lookahead(使用外接知识库/输入prompt信息去生成草稿做投机验证),通常需要较完备的知识库或者输入prompt与输出重合度较高的场景(例如:代码编辑、文本总结)才有较明显加速。low、medium、high。通常严格程度越高,草稿接受率越高,但是启用并行验证概率也越低。默认为low,该参数仅lookahead模式设置有效。freqxlen(出现频率与匹配长度最高者)和fcfs(最先匹配者)。默认freqxlen,该参数仅lookahead模式设置有效。lookahead模式设置有效。lookup_file.txt,该参数仅lookahead模式设置有效。false,该参数仅lookahead模式设置有效。Omni语音生成配置
2048["Chelsie", "Ethan"]5, 建议设置为5~10, 越大语音质量越高计算耗时越高;1, 4,默认为1使用一阶欧拉法;4表示四阶龙格库塔法,效果略好但耗时增加4倍;config.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
{
"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
{
"tools": [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "获取当前时间"
}
}
],
"enable_thinking": false
}
C++ 多轮对话使用 ChatMessage 类型,定义为 std::pair<std::string, std::string>:
"system", "user", "assistant", "tool"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 模板:
// 普通消息
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
https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg</img>介绍一下图片里的内容
# 指定图片大小
<hw>280, 420</hw>https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg</img>介绍一下图片里的内容
<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_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
在build目录下运行
./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
rollback_demo提供了多Prompt场景下自行选择复用部分kvcache的示例代码。
./rollback_demo /path/to/model_dir/config.json /path/to/prompt.txt <cache_prefix_in_disk> <max_token_number>
其中,prompt.txt需要包含至少三组prompt。
需要使用GPTQ权重,可以在导出模型时,使用--gptq_path PATH来指定的路径,使用如下:
# 导出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权重有两使用方式:1. 合并LoRA权重到原始模型;2. LoRA模型单独导出。
第一种模式速度更快,使用更简单但是不支持运行时切换;第二种略微增加一些内存和计算开销,但是更加灵活,支持运行时切换LoRA,适合多LoRA场景。
将LoRA权重合并到原始模型中导出,在模型导出时指定--lora_path PATH参数,默认使用合并方式导出,使用如下:
# 导出LoRA合并的模型
python llmexport.py --path /path/to/Qwen2.5-0.5B-Instruct --lora_path /path/to/lora --export mnn
融合LoRA模型使用与原始模型使用方法完全一样。
将LoRA单独导出为一个模型,支持运行时切换,在模型导出时指定--lora_path PATH参数,并指定--lora_split,就会将LoRA分离导出,使用如下:
python llmexport.py --path /path/to/Qwen2.5-0.5B-Instruct --lora_path /path/to/lora --lora_split --export mnn
导出后模型文件夹内除了原始模型外,还会增加lora.mnn,这个就是lora模型文件。
运行时创建lora模型
// 创建并加载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在文本生成结束后调用,示例如下:
#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;
}
#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;
}
参考 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推理,需要特定的导出参数,并针对目标设备转换出相应的模型。目前支持使用高通芯片和MTK芯片的NPU进行推理。一般流程是:LLM模型导出->转换成对应设备NPU模型->推到目标设备运行
NPU运行LLM需要特定的量化格式,需要按如下参数以导出 mnn llmexport脚本导出在NPU上运行的模型时,必须使用的选项有:
--act_sym,即该选项可视情况加或者不加;--act_bit=16;--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
可通过以下步骤获取依赖:
/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
在模型转换器编译时,增加-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 模型
eg:
cd ${MNN_ROOT}
cd transformers/llm/export
python3 npu/generate_llm_qnn.py --model model --soc_id=57 --dsp_arch=v75
目标设备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 不再需要,可以删除以减少文件总大小
-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
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
~/.bashrc,添加环境变量,eg:export NEURON_SDK=/home/xiaying/third/mtk/neuropilot-sdk-basic-7.0.8-build20240807/neuron_sdk
MLDA 是 MTK 的 NPU 推理引擎,需要把 MNN 模型转成 MLDA 模型才可在其NPU上运行
-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可以删除
-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