Skip to content
jxt1234 edited this page Oct 14, 2024 · 5 revisions

大语言模型

基于MNN开发的LLM推理引擎,支持目前主流的开源LLM模型。该功能分为2部分:

  • 模型导出:将torch模型导出为onnx,然后转换为mnn模型;导出tokenizer文件,embedding等文件;
  • 模型推理:支持导出的模型推理,支持LLM模型的文本生成;

模型导出

llmexport是一个llm模型导出工具,能够将llm模型导出为onnx和mnn模型。

用法

  1. 将需要导出的LLM项目clone到本地,如:Qwen2-0.5B-Instruct
git clone https://www.modelscope.cn/qwen/Qwen2-0.5B-Instruct.git
  1. 执行llmexport.py导出模型
cd ./transformers/llm/export
# 导出模型,tokenizer和embedding,并导出对应的mnn模型
python llmexport.py \
        --path /path/to/Qwen2-0.5B-Instruct \
        --export mnn
  1. 导出产物 导出产物为:
  2. config.json: 模型运行时的配置,可手动修改;
  3. embeddings_bf16.bin: 模型的embedding权重二进制文件,推理时使用;
  4. llm.mnn: 模型的mnn文件,推理时使用;
  5. llm.mnn.json: mnn模型对应的json文件,apply_lora或者gptq量化权重时使用;
  6. llm.mnn.weight: 模型的mnn权重,推理时使用;
  7. llm.onnx: 模型的onnx文件,不包含权重,推理时不使用;
  8. llm_config.json: 模型的配置信息,推理时使用;
  9. tokenizer.txt: 模型的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.txt

功能

  • 将模型先转为onnx模型,使用--export onnx,然后使用./MNNConvert工具将onnx模型转为mnn模型: ./MNNConvert --modelFile ../transformers/llm/export/model/onnx/llm.onnx --MNNModel llm.mnn --keepInputFormat --weightQuantBits=4 -f ONNX --transformerFuse=1 --allowCustomOp
  • 更快的方式:直接转为mnn模型,使用--export mnn,注意,你需要先安装pymnn或者通过--mnnconvert选项指定MNNConvert工具的地址,两种条件必须满足其中一个。如果没有安装pymnn并且没有通过--mnnconvert指定MNNConvert工具的地址,那么llmexport.py脚本会在目录"../../../build/"下寻找MNNConvert工具,需保证该目录下存在MNNConvert文件。
  • 支持对模型进行对话测试,使用--test $query会返回llm的回复内容
  • 默认会使用onnx-slim对onnx模型进行优化,跳过该步骤使用--skip_slim
  • 支持合并lora权重后导出,指定lora权重的目录使用--lora_path
  • 制定量化bit数使用--quant_bit;量化的block大小使用--quant_block
  • 使用--lm_quant_bit来制定lm_head层权重的量化bit数,不指定则使用--quant_bit的量化bit数
  • 支持使用自己编译的MNNConvert,使用--mnnconvert

参数

usage: llmexport.py [-h] --path PATH [--type TYPE] [--lora_path LORA_PATH] [--dst_path DST_PATH] [--test TEST] [--export EXPORT]
                    [--skip_slim] [--quant_bit QUANT_BIT] [--quant_block QUANT_BLOCK] [--lm_quant_bit LM_QUANT_BIT]
                    [--mnnconvert MNNCONVERT]

llm_exporter

options:
  -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.
  --lora_path LORA_PATH
                        lora path, defaut is `None` mean not apply lora.
  --dst_path DST_PATH   export onnx/mnn model to path, defaut is `./model`.
  --test TEST           test model inference with query `TEST`.
  --export EXPORT       export model to an onnx/mnn model.
  --skip_slim           Whether or not to skip onnx-slim.
  --quant_bit QUANT_BIT
                        mnn quant bit, 4 or 8, default is 4.
  --quant_block QUANT_BLOCK
                        mnn quant block, default is 0 mean channle-wise.
  --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.

模型推理

编译

从源码编译 在原有编译过程中增加必需编译宏即可: -DMNN_LOW_MEMORY=true -DMNN_CPU_WEIGHT_DEQUANT_GEMM=true -DMNN_BUILD_LLM=true -DMNN_SUPPORT_TRANSFORMER_FUSE=true

  • mac / linux / windows

以 mac / linux 为例 :

make build
cd build
cmake ../ -DMNN_LOW_MEMORY=true -DMNN_CPU_WEIGHT_DEQUANT_GEMM=true -DMNN_BUILD_LLM=true -DMNN_SUPPORT_TRANSFORMER_FUSE=true
make -j16

x86架构额外加 MNN_AVX512 的宏:

make build
cd build
cmake ../ -DMNN_LOW_MEMORY=true -DMNN_CPU_WEIGHT_DEQUANT_GEMM=true -DMNN_BUILD_LLM=true -DMNN_SUPPORT_TRANSFORMER_FUSE=true -DMNN_AVX512=true
make -j16
  • Android:额外增加 MNN_ARM82 的宏
cd project/android
mkdir build_64
../build_64.sh "-DMNN_LOW_MEMORY=true -DMNN_CPU_WEIGHT_DEQUANT_GEMM=true -DMNN_BUILD_LLM=true -DMNN_SUPPORT_TRANSFORMER_FUSE=true -DMNN_ARM82=true"
  • iOS: 参考 transformers/llm/engine/ios/README.md
sh package_scripts/ios/buildiOS.sh "-DMNN_ARM82=true -DMNN_LOW_MEMORY=true -DMNN_SUPPORT_TRANSFORMER_FUSE=true -DMNN_BUILD_LLM=true -DMNN_CPU_WEIGHT_DEQUANT_GEMM=true"

使用

运行时配置

运行时文件

将导出产物中用于模型推理的部分置于同一个文件夹下,添加一个配置文件config.json来描述模型名称与推理参数,目录如下:

.
└── model_dir
     ├── config.json
     ├── embeddings_bf16.bin
     ├── llm_config.json
     ├── llm.mnn
     ├── llm.mnn.weight
     └── tokenizer.txt
配置项

配置文件支持以下配置:

  • 模型文件信息
    • 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.txt的实际名称路径为base_dir + tokenizer_file,默认为base_dir + 'tokenizer.txt'
    • visual_model: 当使用VL模型时,visual_model的实际路径为base_dir + visual_model,默认为base_dir + 'visual.mnn'
  • 推理配置
    • max_new_tokens: 生成时最大token数,默认为512
    • reuse_kv: 多轮对话时是否复用之前对话的kv cache,默认为false
    • quant_qkv: CPU attention 算子中query, key, value是否量化,可选为:0, 1, 2, 3, 4,默认为0,含义如下:
      • 0: key和value都不量化
      • 1: 使用非对称8bit量化存储key
      • 2: 使用fp8格式量化存储value
      • 3: 使用非对称8bit量化存储key,使用fp8格式量化存储value
      • 4: 量化kv的同时使用非对称8bit量化query,并使用int8矩阵乘计算Q*K
    • use_mmap: 是否使用mmap方式,在内存不足时将权重写入磁盘,避免溢出,默认为false,手机上建议设成true
    • 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
    • precision: 推理使用精度策略,默认为:"low",尽量使用fp16
    • memory: 推理使用内存策略,默认为:"low",开启运行时量化
配置文件示例
  • 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"
    }
  • 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
    }

推理用法

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

GPTQ权重加载

  • 使用脚本生成GPTQ模型权重,用法参考: apply_gptq.py
  • 创建gptq.json配置文件
    {
        "llm_model": "model.mnn",
        "llm_weight": "gptq.mnn.weight",
    }

LoRA权重加载

  • 使用脚本生成lora模型,用法参考: apply_lora.py
  • lora模型使用
    • 直接加载lora模型使用,创建lora.json配置文件
    {
        "llm_model": "lora.mnn",
        "llm_weight": "base.mnn.weight",
    }
    • 运行时选择并切换lora模型
    // 创建并加载base模型
    std::unique_ptr<Llm> llm(Llm::createLLM(config_path));
    llm->load();
    // 使用同一个对象,在多个lora模型之间选择性使用,不可以并发使用
    {
        // 在基础模型的基础上添加`lora_1`模型,模型的索引为`lora_1_idx`
        size_t lora_1_idx = llm->apply_lora("lora_1.mnn");
        llm->response("Hello lora1"); // 使用`lora_1`模型推理
        // 添加`lora_2`模型,并使用
        size_t lora_2_idx = llm->apply_lora("lora_2.mnn");
        llm->response("Hello lora2"); // 使用`lora_2`模型推理
        // 通过索引选择`lora_1`作为llm对象当前使用的模型
        llm->select_module(lora_1_idx);
        llm->response("Hello lora1"); // 使用`lora_1`模型推理
        // 释放加载的lora模型
        llm->release_module(lora_1_idx);
        llm->release_module(lora_2_idx);
        // 选择使用基础模型
        llm->select_module(0);
        llm->response("Hello base"); // 使用`base`模型推理
    }
    // 使用多个对象,可以并发的加载使用多个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();
    }
Clone this wiki locally