1. RKLLM简介
1.1 RKLLM工具链介绍
1.1.1 RKLLM-Toolkit功能介绍
RKLLM-Toolkit 是为用户提供在计算机上进行大语言模型的量化、转换的开发套件。通过该 工具提供的 Python 接口可以便捷地完成以下功能:
(1)模型转换:支持将 Hugging Face 和 GGUF 格式的大语言模型(Large Language Model, LLM) 转换为 RKLLM 模型,目前支持的模型包括 LLaMA, Qwen, Qwen2, Phi-2, Phi-3, ChatGLM3, Gemma, Gemma2, InternLM2, MiniCPM 和 MiniCPM3,转换后的 RKLLM 模型能够在 Rockchip NPU 平台上加载使用。
(2)量化功能:支持将浮点模型量化为定点模型,目前支持的量化类型包括 w4a16 、wa4a16分组量化(支持的分组数为32,64,128)、w8a8、w8a8分组量化(支持的分组数为128,256,512)。
1.1.2 RKLLM Runtime功能介绍
RKLLM Runtime 主 要 负 责 加 载 RKLLM-Toolkit 转换得到的 RKLLM 模型,并在RK3576/RK3588 板端通过调用 NPU 驱动在 Rockchip NPU 上实现 RKLLM 模型的推理。在推理 RKLLM 模型时,用户可以自行定义 RKLLM 模型的推理参数设置,定义不同的文本生成方式, 并通过预先定义的回调函数不断获得模型的推理结果。
1.2 RKLLM开发流程介绍
(1)模型转换:
在这一阶段,用户提供的 Hugging Face 格式的大语言模型将会被转换为 RKLLM 格式,以便在 Rockchip NPU 平台上进行高效的推理。这一步骤包括:
获取原始模型:1、开源的 Hugging Face 格式的大语言模型;2、自行训练得到的大语 言模型,要求模型保存的结构与 Hugging Face 平台上的模型结构一致;3、GGUF 模型,目前 仅支持 q4_0 和 fp16 类型模型;
模型加载:通过 rkllm.load_huggingface()函数加载 huggingface 格式模型,通过rkllm.load_gguf()函数加载 GGUF 模型;
模型量化配置:通过 rkllm.build() 函数构建 RKLLM 模型,在构建过程中可选择是否进行模型量化来提高模型部署在硬件上的性能,以及选择不同的优化等级和量化类型。
模型导出:通过 rkllm.export_rkllm() 函数将 RKLLM 模型导出为一个.rkllm 格式文件, 用于后续的部署。
(2)板端部署运行:
这个阶段涵盖了模型的实际部署和运行。它通常包括以下步骤:
模型初始化:加载 RKLLM 模型到 Rockchip NPU 平台,进行相应的模型参数设置来定义所需的文本生成方式,并提前定义用于接受实时推理结果的回调函数,进行推理前准备。
模型推理:执行推理操作,将输入数据传递给模型并运行模型推理,用户可以通过预先定义的回调函数不断获取推理结果。
模型释放:在完成推理流程后,释放模型资源,以便其他任务继续使用 NPU 的计算资源。以上这两个步骤构成了完整的 RKLLM 开发流程,确保大语言模型能够成功转换、调试,并 最终在 Rockchip NPU 上实现高效部署。
1.3 资料下载
模型文件、模型转换与部署代码的百度网盘下载链接(比较大,可以选择来下载):https://pan.baidu.com/s/13CHxaF-Cyp4tYxXpksD8LA?pwd=1234 (提取码:1234 )
1.4 开发环境搭建
1.4.1 RKLLM-Toolkit安装
本节主要说明如何通过 pip 方式来安装 RKLLM-Toolkit,用户可以参考以下的具体流程说明完成 RKLLM-Toolkit 工具链的安装。
工具安装包链接: https://pan.baidu.com/s/1y5ZN5sl4e3HJI5d9Imt4pg?pwd=1234(提取码: 1234)。
1.4.1.1 安装miniforge3工具
为防止系统对多个不同版本的 Python 环境的需求,建议使用 miniforge3 管理 Python 环境。 检查是否安装 miniforge3 和 conda 版本信息,若已安装则可省略此小节步骤。
下载 miniforge3 安装包:
wget -c https://mirrors.bfsu.edu.cn/github-release/conda-forge/miniforge/LatestRelease/Miniforge3-Linux-x86_64.sh
安装miniforge3:
chmod 777 Miniforge3-Linux-x86_64.sh bash Miniforge3-Linux-x86_64.sh
1.4.1.2 创建 RKLLM-Toolkit Conda 环境
进入 Conda base 环境:
source ~/miniforge3/bin/activate
创建一个 Python3.8 版本(建议版本)名为 RKLLM-Toolkit 的 Conda 环境:
conda create -n RKLLM-Toolkit python=3.8
进入 RKLLM-Toolkit Conda 环境:
conda activate RKLLM-Toolkit
1.4.1.3 安装RKLLM-Toolkit
在 RKLLM-Toolkit Conda 环境下使用 pip 工具直接安装所提供的工具链 whl 包,在安装过程 中,安装工具会自动下载 RKLLM-Toolkit 工具所需要的相关依赖包。
pip3 install nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl pip3 install torch-2.1.0-cp38-cp38-manylinux1_x86_64.whl pip3 install rkllm_toolkit-1.1.4-cp38-cp38-linux_x86_64.whl
若在安装的过程中,某些文件安装很慢,可以登录python官网单独下载:
https://pypi.org/
执行以下命令没有报错,则安装成功。
1.5 AI模型转换
本章主要说明如何实现Hugging Face格式的大语言模型(Large Language Model, LLM)
如何转换为RKLLM模型,目前支持的模型包括 LLaMA, Qwen, Qwen2, Phi-2, Phi-3, ChatGLM3, Gemma, InternLM2 和 MiniCPM。
1.5.1 模型下载
本节提供两种大模型文件,Hugging face的原始模型和转换完成的NPU模型。
下载链接: https://pan.baidu.com/s/14BDYQBOMTTK7jtsSa8evHw?pwd=1234 (提取码: 1234)。
1.5.2 模型转换
下载完成后模型和脚本放到同一个目录:
在RKLLM-Toolkit环境,执行以下指令进行模型转换:
至此模型转换成功,生成Qwen.rkllm NPU化的大模型文件:
test.py转换脚本如下所示, 用于转换Qwen-1_8B-Chat模型:
from rkllm.api import RKLLM from datasets import load_dataset from transformers import AutoTokenizer from tqdm import tqdm import torch from torch import nn import os # os.environ['CUDA_VISIBLE_DEVICES']='1' ''' https://huggingface.co/Qwen/Qwen-1_8B-Chat 从上面网址中下载Qwen模型 ''' modelpath = '/home/developer/RKLLM-Toolkit/Qwen-1_8B-Chat' # modelpath = "./path/to/Qwen-1.8B-F16.gguf" llm = RKLLM() # Load model # Use 'export CUDA_VISIBLE_DEVICES=2' to specify GPU device # options ['cpu', 'cuda'] ret = llm.load_huggingface(model=modelpath, model_lora = None, device='cpu') # ret = llm.load_gguf(model = modelpath) if ret != 0: print('Load model failed!') exit(ret) # Build model dataset = "./data_quant.json" # Json file format, please note to add prompt in the input,like this: # [{"input":"Human: 你好!nAssistant: ", "target": "你好!我是人工智能助手KK!"},...] qparams = None # qparams = 'gdq.qparams' # Use extra_qparams ret = llm.build(do_quantization=True, optimization_level=1, quantized_dtype='w4a16', quantized_algorithm='normal', target_platform='rk3576', num_npu_core=2, extra_qparams=qparams, dataset=None) if ret != 0: print('Build model failed!') exit(ret) # Evaluate Accuracy def eval_wikitext(llm): seqlen = 512 tokenizer = AutoTokenizer.from_pretrained( modelpath, trust_remote_code=True) # Dataset download link: # https://huggingface.co/datasets/Salesforce/wikitext/tree/main/wikitext-2-raw-v1 testenc = load_dataset( "parquet", data_files='./wikitext/wikitext-2-raw-1/test-00000-of-00001.parquet', split='train') testenc = tokenizer("nn".join( testenc['text']), return_tensors="pt").input_ids nsamples = testenc.numel() // seqlen nlls = [] for i in tqdm(range(nsamples), desc="eval_wikitext: "): batch = testenc[:, (i * seqlen): ((i + 1) * seqlen)] inputs = {"input_ids": batch} lm_logits = llm.get_logits(inputs) if lm_logits is None: print("get logits failed!") return shift_logits = lm_logits[:, :-1, :] shift_labels = batch[:, 1:].to(lm_logits.device) loss_fct = nn.CrossEntropyLoss().to(lm_logits.device) loss = loss_fct( shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) neg_log_likelihood = loss.float() * seqlen nlls.append(neg_log_likelihood) ppl = torch.exp(torch.stack(nlls).sum() / (nsamples * seqlen)) print(f'wikitext-2-raw-1-test ppl: {round(ppl.item(), 2)}') # eval_wikitext(llm) # Chat with model messages = "<|im_start| >system You are a helpful assistant.<|im_end| ><|im_start| >user你好!n<|im_end| ><|im_start| >assistant" kwargs = {"max_length": 128, "top_k": 1, "top_p": 0.8, "temperature": 0.8, "do_sample": True, "repetition_penalty": 1.1} # print(llm.chat_model(messages, kwargs)) # Export rkllm model ret = llm.export_rkllm("./Qwen_w4a16.rkllm") if ret != 0: print('Export model failed!') exit(ret)
1.6 模型转换API说明
1.6.1 RKLLM 初始化
在这一部分,用户需要先初始化 RKLLM 对象,这是整个工作流的第一步。在示例代码中使用 RKLLM()构造函数来初始化 RKLLM 对象:
rkllm = RKLLM()
1.6.2 模型加载
在 RKLLM 初始化完成后,用户需要调用 rkllm.load_huggingface()函数来传入模型的具体路径,RKLLM-Toolkit 即可根据对应路径顺利加载 Hugging Face 或 GGUF 格式的大语言模型,从而顺利完成后续的转换、量化操作,具体的函数定义如下:
表 1 load_huggingface 函数接口说明
函数名 | load_huggingface |
描述 | 用于加载开源的 Hugging Face 格式的大语言模型。 |
参数 | model: LLM 模型的文件路径,用于加载模型进行后续的转换、量化; model_lora: lora 权重的文件路径,转换时 model 必须指向相应的 base model 路径; |
返回值 | 0 表示模型加载正常;-1 表示模型加载失败; |
示例代码如下:
ret = rkllm.load_huggingface( model = './huggingface_model_dir', model_lora = './huggingface_lora_model_dir' ) if ret != 0: print('Load model failed!')
表 2 load_gguf 函数接口说明
函数名 | load_gguf |
描述 | 用于加载开源的 GGUF 格式的大语言模型,所支持的数值类型为 q4_0 和 fp16 两种,gguf 格式的 lora 模型也可以通过此接口加载转换为 rkllm 模型。 |
参数 | model: GGUF 模型文件路径; |
返回值 | 0 表示模型加载正常;-1 表示模型加载失败; |
示例代码如下:
ret = rkllm.load_gguf(model = './model-Q4_0.gguf') if ret != 0: print('Load model failed!')
1.6.3 RKLLM 模型的量化构建
用户在通过 rkllm.load_huggingface()函数完成原始模型的加载后,下一步就是通过 rkllm.build()函数实现对 RKLLM 模型的构建。构建模型时,用户可以选择是否进行量化,量化有助于减小模型的大小和提高在 Rockchip NPU 上的推理性能。rkllm.build()函数的具体定义如下:
表 3 build 函数接口说明
函数名 | build |
描述 | 用于构建得到 RKLLM 模型,并在转换过程中定义具体的量化操作。 |
参数 | do_quantization: 该参数控制是否对模型进行量化操作,建议设置为 True; optimization_level: 该参数用于设置是否进行量化精度优化,可选择的设置为{0,1},0 表示不做任何优化,1 表示进行精度优化,精度优化可能造成模型推理性能下降; quantized_dtype: 该参数用于设置量化的具体类型,目前支持的量化类型包括“ w4a16 ” , “ w4a16_g32 ” , “ w4a16_g64 ” , “ w4a16_g128 ” , “ w8a8 ” ,“w8a8_g128”,“w8a8_g256”,“w8a8_g512”,“w4a16”表示对权重进行 4bit 量化而对激活值不进行量化;“w4a16_g64”表示对权重进行 4bit 分组量化(groupsize=64)而对激活值不进行量化;“w8a8”表示对权重和激活值均进行 8bit 量化;“w8a8_g128”表示对权重和激活值均进行 8bit 分组量化(group size=128);目前rk3576 平台支持“w4a16”,“w4a16_g32”,“w4a16_g64”,“w4a16_g128”和“w8a8”五种量化类型,rk3588 支持“w8a8”,“w8a8_g128”,“w8a8_g256”,“ w8a8_g512 ” 四种量化类型; GGUF 模型的 q4_0 对应的量化类型为“w4a16_g32”;注意group size 应能被线性层的输出维度整除,否则会量化失败! quantized_algorithm: 量化精度优化算法, 可选择的设置包括“normal”或“gdq”,所有量化类型均可选择 normal,而 gdq 算法只支持 w4a16 及 w4a16 分组量化,且 gdq 对算力要求高,必须使用 GPU 进行加速运算; num_npu_core: 模型推理需要使用的 npu 核心数,“rk3576”可选项为[1,2],“rk3588”可选项为[1,2,3]; extra_qparams: 使用 gdq 算法会生成 gdq.qparams 量化权重缓存文件,将此参数设置为 gdq.qparams 路径,可以重复进行模型导出; dataset: 用于量化校正数据集,格式为 json,内容示例如下,input 为问题,需要加上提示词,target 为回答,多条数据以{}字典形式保存在列表中:[{"input":"今天天气怎么样?","target":"今天天气晴。"},....] hybrid_rate: 分组和不分组混合量化比率(∈[0,1)),当量化类型为 w4a16/w8a8 时,会按比率分别混合 w4a16 分组/w8a8 分组类型来提高精度,当量化类型为 w4a16分组/w8a8 分组类型时,会按比率分别混合 w4a16/w8a8 类型来提高推理性能,当hybrid_rate 值为 0 时,不进行混合量化; target_platform: 模型运行的硬件平台, 可选择的设置包括“rk3576”或“rk3588”; |
返回值 | 0 表示模型转换、量化正常;-1 表示模型转换失败; |
示例代码如下:
ret = rkllm.build( do_quantization=True, optimization_level=1, quantized_dtype='w8a8', quantized_algorithm="normal", num_npu_core=3, extra_qparams=None, dataset="quant_data.json", hybrid_rate=0, target_platform='rk3588') if ret != 0: print('Build model failed!')
1.6.4 导出 RKLLM 模型
用户在通过 rkllm.build()函数构建了 RKLLM 模型后,可以通过 rkllm.export_rkllm()函数将RKNN 模型保存为一个.rkllm 文件,以便后续模型的部署。rkllm.export_rkllm()函数的具体参数定义如下:
表 4 export_rkllm 函数接口说明
函数名 | export_rkllm |
描述 | 用于保存转换、量化后的 RKLLM 模型,用于后续的推理调用。 |
参数 | export_path: 导出 RKLLM 模型文件的保存路径,lora 模型会自动保存为带_lora 后缀的 rkllm 模型; |
返回值 | 0 表示模型成功导出保存;-1 表示模型导出失败; |
示例代码如下:
ret = rkllm.export_rkllm(export_path = './model.rkllm') if ret != 0: print('Export model failed!')
1.6.5 仿真精度评估
用户在通过 rkllm.build()函数构建了 RKLLM 模型后,可以通过 rkllm.get_logits()函数在 PC端进行仿真精度评估,rkllm.get_logits()函数的具体参数定义如下:
函数名 | get_logits |
函数名 | 用于 PC 端仿真精度评估。 |
参数 | inputs: 仿真输入格式与 huggingface 模型推理一样,示例如下:{“input_ids":"","top_k":1,...} |
返回值 | 返回模型推理出的 logits 值; |
使用此函数进行 wikitext 数据集 ppl 测试示例代码如下:
ef eval_wikitext(llm): seqlen = 512 tokenizer = AutoTokenizer.from_pretrained( modelpath, trust_remote_code=True ) #Dataset download link: #https://huggingface.co/datasets/Salesforce/wikitext/tree/main/wiki text-2-raw-v1 testenc = load_dataset("parquet", data_files='./wikitext/wikitext- 2-raw-1/test-00000-of-00001.parquet', split='train') testenc = tokenizer( "nn".join(testenc['text']), return_tensors="pt").input_ids nsamples = testenc.numel() // seqlen nlls = [] for i in tqdm(range(nsamples), desc="eval_wikitext: "): batch = testenc[:, (i * seqlen): ((i + 1) * seqlen)] inputs = {"input_ids": batch} lm_logits = llm.get_logits(inputs) if lm_logits is None: print("get logits failed!") return shift_logits = lm_logits[:, :-1, :] shift_labels = batch[:, 1:].to(lm_logits.device) loss_fct = nn.CrossEntropyLoss().to(lm_logits.device) loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) neg_log_likelihood = loss.float() * seqlen nlls.append(neg_log_likelihood) ppl = torch.exp(torch.stack(nlls).sum() / (nsamples * seqlen)) print(f'wikitext-2-raw-1-test ppl: {round(ppl.item(), 2)}')
1.6.6 仿真模型推理
用户在通过 rkllm.build()函数构建了 RKLLM 模型后,可以通过 rkllm.chat_model()函数在 PC端进行仿真推理,rkllm.chat_model()函数的具体参数定义如下:
函数名 | chat_model |
描述 | 用于 PC 端仿真模型推理。 |
参数 | messages: 文本输入,需要加上相应提示词 args: 推理配置参数,比如 topk 等采样策略参数 |
返回值 | 返回模型推理结果; |
示例代码如下:
args ={ "max_length":128, "top_k":1, "temperature":0.8, "do_sample":True, "repetition_penalty":1.1 } mesg = "Human: 今天天气怎么样?nAssistant:" print(llm.chat_model(mesg, args))
以上的这些操作涵盖了 RKLLM-Toolkit 模型转换、量化的全部步骤,根据不同的需求和应用场景,用户可以选择不同的配置选项和量化方式进行自定义设置,方便后续进行部署。
1.7 AI模型部署
本章主要说明RKLLM格式的通义千问NPU大模型如何运行在EASY-EAI-Orin-Nano硬件上。
1.7.1 快速上手
1.7.1.1 源码下载以及例程编译
本节提供转换成功的通义千问大模型文Qwen_w4a16.rkllm及对应的C/C++程序部署代码。
下载链接:https://pan.baidu.com/s/19GZ_UjsA-IjA9zf10ZZ93w?pwd=1234(提取码: 1234)。
然后把例程【复制粘贴】到nfs挂载目录中。(不清楚目录如何构建的,可以参考《入门指南/开发环境准备/nfs服务搭建与挂载》)。特别注意:源码目录和模型最好cp到板子上,如/userdata,否则在nfs目录执行大模型会导致模型初始化过慢。
进入到开发板对应的例程目录执行编译操作,具体命令如下所示:
cd /userdata/rkllm-demo ./build.sh

1.7.1.2 例程运行及效果
进入例程的rkllm-demo/rkllm-demo_release目录,执行下方命令,运行示例程序:
cd rkllm-demo_release/ ulimit -HSn 102400 ./rkllm-demo Qwen_w4a16.rkllm 256 512

至此可以进行对话测试了,试着输入“如何制作PCB板?”。回答如下所示:
1.7.2 RKLLM算法例程
例程目录为rkllm-demo/src/main.cpp,操作流程如下。
具体代码如下所示:
#include < string.h > #include < unistd.h > #include < string > #include "rkllm.h" #include < fstream > #include < iostream > #include < csignal > #include < vector > #define PROMPT_TEXT_PREFIX "<|im_start| >system You are a helpful assistant. <|im_end| > <|im_start| >user" #define PROMPT_TEXT_POSTFIX "<|im_end| ><|im_start| >assistant" using namespace std; LLMHandle llmHandle = nullptr; void exit_handler(int signal) { if (llmHandle != nullptr) { { cout < < "程序即将退出" < < endl; LLMHandle _tmp = llmHandle; llmHandle = nullptr; rkllm_destroy(_tmp); } } exit(signal); } void callback(RKLLMResult *result, void *userdata, LLMCallState state) { if (state == RKLLM_RUN_FINISH) { printf("n"); } else if (state == RKLLM_RUN_ERROR) { printf("\run errorn"); } else if (state == RKLLM_RUN_GET_LAST_HIDDEN_LAYER) { /* ================================================================================================================ 若使用GET_LAST_HIDDEN_LAYER功能,callback接口会回传内存指针:last_hidden_layer,token数量:num_tokens与隐藏层大小:embd_size 通过这三个参数可以取得last_hidden_layer中的数据 注:需要在当前callback中获取,若未及时获取,下一次callback会将该指针释放 ===============================================================================================================*/ if (result- >last_hidden_layer.embd_size != 0 && result->last_hidden_layer.num_tokens != 0) { int data_size = result->last_hidden_layer.embd_size * result->last_hidden_layer.num_tokens * sizeof(float); printf("ndata_size:%d",data_size); std::ofstream outFile("last_hidden_layer.bin", std::ios::binary); if (outFile.is_open()) { outFile.write(reinterpret_cast< const char* >(result->last_hidden_layer.hidden_states), data_size); outFile.close(); std::cout < < "Data saved to output.bin successfully!" < < std::endl; } else { std::cerr < < "Failed to open the file for writing!" < < std::endl; } } } else if (state == RKLLM_RUN_NORMAL) { printf("%s", result- >text); } } int main(int argc, char **argv) { if (argc < 4) { std::cerr < < "Usage: " < < argv[0] < < " model_path max_new_tokens max_context_lenn"; return 1; } signal(SIGINT, exit_handler); printf("rkllm init startn"); //设置参数及初始化 RKLLMParam param = rkllm_createDefaultParam(); param.model_path = argv[1]; //设置采样参数 param.top_k = 1; param.top_p = 0.95; param.temperature = 0.8; param.repeat_penalty = 1.1; param.frequency_penalty = 0.0; param.presence_penalty = 0.0; param.max_new_tokens = std::atoi(argv[2]); param.max_context_len = std::atoi(argv[3]); param.skip_special_token = true; param.extend_param.base_domain_id = 0; int ret = rkllm_init(&llmHandle, ¶m, callback); if (ret == 0){ printf("rkllm init successn"); } else { printf("rkllm init failedn"); exit_handler(-1); } vector< string > pre_input; pre_input.push_back("把下面的现代文翻译成文言文: 到了春风和煦,阳光明媚的时候,湖面平静,没有惊涛骇浪,天色湖光相连,一片碧绿,广阔无际;沙洲上的鸥鸟,时而飞翔,时而停歇,美丽的鱼游来游去,岸上与小洲上的花草,青翠欲滴。"); pre_input.push_back("以咏梅为题目,帮我写一首古诗,要求包含梅花、白雪等元素。"); pre_input.push_back("上联: 江边惯看千帆过"); pre_input.push_back("把这句话翻译成中文: Knowledge can be acquired from many sources. These include books, teachers and practical experience, and each has its own advantages. The knowledge we gain from books and formal education enables us to learn about things that we have no opportunity to experience in daily life. We can also develop our analytical skills and learn how to view and interpret the world around us in different ways. Furthermore, we can learn from the past by reading books. In this way, we won't repeat the mistakes of others and can build on their achievements."); pre_input.push_back("把这句话翻译成英文: RK3588是新一代高端处理器,具有高算力、低功耗、超强多媒体、丰富数据接口等特点"); cout < < "n**********************可输入以下问题对应序号获取回答/或自定义输入********************n" < < endl; for (int i = 0; i < (int)pre_input.size(); i++) { cout < < "[" < < i < < "] " < < pre_input[i] < < endl; } cout < < "n*************************************************************************n" < < endl; string text; RKLLMInput rkllm_input; // 初始化 infer 参数结构体 RKLLMInferParam rkllm_infer_params; memset(&rkllm_infer_params, 0, sizeof(RKLLMInferParam)); // 将所有内容初始化为 0 // 1. 初始化并设置 LoRA 参数(如果需要使用 LoRA) // RKLLMLoraAdapter lora_adapter; // memset(&lora_adapter, 0, sizeof(RKLLMLoraAdapter)); // lora_adapter.lora_adapter_path = "qwen0.5b_fp16_lora.rkllm"; // lora_adapter.lora_adapter_name = "test"; // lora_adapter.scale = 1.0; // ret = rkllm_load_lora(llmHandle, &lora_adapter); // if (ret != 0) { // printf("nload lora failedn"); // } // 加载第二个lora // lora_adapter.lora_adapter_path = "Qwen2-0.5B-Instruct-all-rank8-F16-LoRA.gguf"; // lora_adapter.lora_adapter_name = "knowledge_old"; // lora_adapter.scale = 1.0; // ret = rkllm_load_lora(llmHandle, &lora_adapter); // if (ret != 0) { // printf("nload lora failedn"); // } // RKLLMLoraParam lora_params; // lora_params.lora_adapter_name = "test"; // 指定用于推理的 lora 名称 // rkllm_infer_params.lora_params = &lora_params; // 2. 初始化并设置 Prompt Cache 参数(如果需要使用 prompt cache) // RKLLMPromptCacheParam prompt_cache_params; // prompt_cache_params.save_prompt_cache = true; // 是否保存 prompt cache // prompt_cache_params.prompt_cache_path = "./prompt_cache.bin"; // 若需要保存prompt cache, 指定 cache 文件路径 // rkllm_infer_params.prompt_cache_params = &prompt_cache_params; // rkllm_load_prompt_cache(llmHandle, "./prompt_cache.bin"); // 加载缓存的cache rkllm_infer_params.mode = RKLLM_INFER_GENERATE; while (true) { std::string input_str; printf("n"); printf("user: "); std::getline(std::cin, input_str); if (input_str == "exit") { break; } for (int i = 0; i < (int)pre_input.size(); i++) { if (input_str == to_string(i)) { input_str = pre_input[i]; cout < < input_str < < endl; } } text = PROMPT_TEXT_PREFIX + input_str + PROMPT_TEXT_POSTFIX; //text = input_str; rkllm_input.input_type = RKLLM_INPUT_PROMPT; rkllm_input.prompt_input = (char *)text.c_str(); printf("robot: "); // 若要使用普通推理功能,则配置rkllm_infer_mode为RKLLM_INFER_GENERATE或不配置参数 rkllm_run(llmHandle, &rkllm_input, &rkllm_infer_params, NULL); } rkllm_destroy(llmHandle); return 0; }
1.8 模型部署API说明
1.8.1 回调函数定义
回调函数是用于接收模型实时输出的结果。在初始化 RKLLM 时回调函数会被绑定,在模型推理过程中不断将结果输出至回调函数中,并且每次回调只返回一个 token。
示例代码如下,该回调函数将输出结果实时地打印输出到终端中:
void callback(RKLLMResult* result, void* userdata, LLMCallState state) { if(state == LLM_RUN_NORMAL){ printf("%s", result->text); for (int i=0; i< result- >num; i++) { printf("token_id: %d logprob: %f", result->tokens[i].id, result->tokens[i].logprob); } } if (state == LLM_RUN_FINISH) { printf("finishn"); } else if (state == LLM_RUN_ERROR){ printf("run errorn"); } }
(1)LLMCallState 是一个状态标志,其具体定义如下:
表 1 LLMCallState 状态标志说明
枚举定义 | LLMCallState |
描述 | 用于表示当前 RKLLM 的运行状态。 |
枚举值 | 0, LLM_RUN_NORMAL, 表示 RKLLM 模型当前正在推理中; 1, LLM_RUN_FINISH, 表示 RKLLM 模型已完成当前输入的全部推理; 2, LLM_RUN_WAITING, 表示当前 RKLLM 解码出的字符不是完整 UTF8 编码,需等待与下一次解码拼接; 3, LLM_RUN_ERROR, 表示 RKLLM 模型推理出现错误; |
用户在回调函数的设计过程中,可以根据 LLMCallState 的不同状态设置不同的后处理行为;
(2)RKLLMResult 是返回值结构体,其具体定义如下:
表 2 RKLLMResult 返回值结构体说明
结构体定义 | RKLLMResult |
描述 | 用于返回当前推理生成结果。 |
字段 | 0, text, 表示当前推理生成的文本内容; 1, token_id, 表示当前推理生成的 token id; |
1.8.2 参数结构体 RKLLMParam 定义
结构体 RKLLMParam 用于描述、定义 RKLLM 的详细信息,具体的定义如下:
表 2 RKLLMParam 结构体参数说明
结构体定义 | RKLLMParam |
描述 | 用于定义 RKLLM 模型的各项细节参数。 |
字段 | const char* model_path: 模型文件的存放路径; int32_t max_context_len: 设置推理时的最大上下文长度; int32_t max_new_tokens: 用于设置模型推理时生成 token 的数量上限; int32_t top_k: top-k 采样是一种文本生成方法,它仅从模型预测概率最高的 k个 token 中选择下一个 token。该方法有助于降低生成低概率或无意义 token 的风险。更高的值(如 100)将考虑更多的 token 选择,导致文本更加多样化;而更低的值(如 10)将聚焦于最可能的 token,生成更加保守的文本。默认值为40; float top_p: top-p 采样,也被称为核心采样,是另一种文本生成方法,从累计概率至少为 p 的一组 token 中选择下一个 token。这种方法通过考虑 token 的概率和采样的 token 数量在多样性和质量之间提供平衡。更高的值(如 0.95)使得生成的文本更加多样化;而更低的值(如 0.5)将生成更加保守的文本。默认值为 0.9; float temperature: 控制生成文本随机性的超参数,它通过调整模型输出token的概率分布来发挥作用;更高的温度(如 1.5)会使输出更加随机和创造性,当温度较高时,模型在选择下一个 token 时会考虑更多可能性较低的选项,从而产生更多样和意想不到的输出;更低的温度(例 0.5)会使输出更加集中、保守,较低的温度意味着模型在生成文本时更倾向于选择概率高的 token,从而导致更一致、更可预测的输出;温度为 0 的极端情况下,模型总是选择最有可能的下一个 token,这会导致每次运行时输出完全相同;为了确保随机性和确定性之间的平衡,使输出既不过于单一和可预测,也不过于随机和杂乱,默认值为 0.8; float repeat_penalty: 控制生成文本中 token 序列重复的情况,帮助防止模型生成重复或单调的文本。更高的值(例如 1.5)将更强烈地惩罚重复,而较低的值(例如 0.9)则更为宽容。默认值为 1.1; float frequency_penalty: 单词/短语重复度惩罚因子,减少总体上使用频率较高的单词/短语的概率,增加使用频率较低的单词/短语的可能性,这可能会使生成的文本更加多样化,但也可能导致生成的文本难以理解或不符合预期。设置范围为[-2.0, 2.0],默认为 0; int32_t mirostat: 在文本生成过程中主动维持生成文本的质量在期望的范围内的算法,它旨在在连贯性和多样性之间找到平衡,避免因过度重复(无聊陷阱)或不连贯(混乱陷阱)导致的低质量输出;取值空间为{0, 1, 2}, 0 表示不启动该算法,1 表示使用 mirostat 算法,2 则表示使用 mirostat2.0 算法; float mirostat_tau: 选项设置 mirostat 的目标熵,代表生成文本的期望困惑度。调整目标熵可以控制生成文本中连贯性与多样性的平衡。较低的值将导致文本更加集中和连贯,而较高的值将导致文本更加多样化,可能连贯性较差。默认值是 5.0; float mirostat_eta: 选项设置 mirostat 的学习率,学习率影响算法对生成文本反馈的响应速度。较低的学习率将导致调整速度较慢,而较高的学习率将使算法更加灵敏。默认值是 0.1; bool skip_special_token: 是否跳过特殊 token 不输出,例如结束符号等; bool is_async: 是否使用异步模式; const char* img_start: 选项设置多模态输入图像编码的起始标志符,在多模态输入模式下需要配置; const char* img_end: 选项设置多模态输入图像编码的终止标志符,在多模态输入模式下需要配置; const char* img_content: 选项设置多模态输入图像编码的内容标志符,在多模态输入模式下需要配置; |
在实际的代码构建中,RKLLMParam 需要调用 rkllm_createDefaultParam()函数来初始化定义,并根据需求设置相应的模型参数。示例代码如下:
RKLLMParam param = rkllm_createDefaultParam(); param.model_path = "model.rkllm"; param.top_k = 1; param.max_new_tokens = 256; param.max_context_len = 512;
1.8.3 输入结构体定义
为适应不同的输入数据,定义了 RKLLMInput 输入结构体,目前可接受文本、图片和文本、Token id 以及编码向量四种形式的输入,具体的定义如下:
表 3 RKLLMInput 结构体参数说明
结构体定义 | RKLLMInput |
描述 | 用于接收不同形式的输入数据。 |
字段 | RKLLMInputType input_type: 输入模式; union: 用于存储不同的输入数据类型,具体包含以下几种形式: - const char* prompt_input: 文本提示输入,用于传递自然语言文本; - RKLLMEmbedInput embed_input: 嵌入向量输入,表示已处理的特征向量; - RKLLMTokenInput token_input: Token 输入,用于传递 Token 序列; - RKLLMMultiModelInput multimodal_input: 多模态输入,可传递多模态数据,如图片和文本的联合输入。 |
表 4 RKLLMInputType 输入类型说明
枚举定义 | RKLLMInputType |
描述 | 用于表示输入数据类型。 |
枚举值 | 0, RKLLM_INPUT_PROMPT, 表示输入数据是纯文本; 1, RKLLM_INPUT_TOKEN, 表示输入数据是 Token id; 2, RKLLM_INPUT_EMBED, 表示输入数据是编码向量; 3, RKLLM_INPUT_MULTIMODAL, 表示输入数据是图片和文本; |
当输入数据是纯文本时,使用 input_data 直接输入;当输入数据是 Token id、编码向量以及图片 和 文 本 时 , RKLLMInput 需 要 搭 配 RKLLMTokenInput, RKLLMEmbedInput 以 及RKLLMMultiModelInput 三个输入结构体使用,具体的介绍如下:
(1)RKLLMTokenInput 是接收 Token id 的输入结构体,具体的定义如下:
表 5 RKLLMTokenInput 结构体参数说明
结构体定义 | RKLLMTokenInput |
描述 | 用于接收 Token id 数据。 |
字段 | int32_t* input_ids: 输入 token ids 的内存指针; size_t n_tokens: 输入数据的 token 数量; |
(2)RKLLMEmbedInput 是接收编码向量的输入结构体,具体的定义如下:
表 6 RKLLMEmbedInput 结构体参数说明
结构体定义 | RKLLMEmbedInput |
描述 | 用于接收 Embedding 数据。 |
字段 | float* embed: 输入 token embedding 的内存指针; size_t n_tokens: 输入数据的 token 数量; |
(3)RKLLMMultiModelInput 是接收图片和文本的输入结构体,具体的定义如下:
表 7 RKLLMMultiModelInput 结构体参数说明
结构体定义 | RKLLMMultiModelInput |
描述 | 用于接收图片和文本多模态数据。 |
字段 | char* prompt: 输入文本的内存指针; float* image_embed: 输入图片 embedding 的内存指针; size_t n_image_tokens: 输入图片 embedding 的 token 数量; |
纯文本输入示例代码如下:
// 提前定义 prompt 前后的文本预设值 #define PROMPT_TEXT_PREFIX "<|im_start| >system You are a helpful assistant. <|im_end| > <|im_start| >user" #define PROMPT_TEXT_POSTFIX "<|im_end| ><|im_start| >assistant" // 定义输入的 prompt 并完成前后 prompt 的拼接 string input_str = "把这句话翻译成英文:RK3588 是新一代高端处理器,具有高算力、 低功耗、超强多媒体、丰富数据接口等特点"; input_str = PROMPT_TEXT_PREFIX + input_str + PROMPT_TEXT_POSTFIX; RKLLMInput rkllm_input; rkllm_input.input_data = (char*)input_str.c_str(); rkllm_input.input_type = RKLLM_INPUT_PROMPT; // 初始化 infer 参数结构体 RKLLMInferParam rkllm_infer_params; memset(&rkllm_infer_params, 0, sizeof(RKLLMInferParam)); // 将所有内容初 始化为 0 rkllm_infer_params.mode = RKLLM_INFER_GENERATE;
图片和文本多模态输入示例代码如下,注意多模态输入的 prompt 中需要加入占位符用于标示图像编码插入的位置:
// 提前定义 prompt 前后的文本预设值 #define PROMPT_TEXT_PREFIX "<|im_start| >system You are a helpful assistant. <|im_end| > <|im_start| >user" #define PROMPT_TEXT_POSTFIX "<|im_end| ><|im_start| >assistant" RKLLMInput rkllm_input; // 定义输入的 prompt 并完成前后 prompt 的拼接 string input_str = "< image >Please describe the image shortly."; input_str = PROMPT_TEXT_PREFIX + input_str + PROMPT_TEXT_POSTFIX; rkllm_input.multimodal_input.prompt = (char*)input_str.c_str(); rkllm_input.multimodal_input.n_image_tokens = 256; int rkllm_input_len = multimodal_input.n_image_tokens * 3072; rkllm_input.multimodal_input.image_embed = (float *)malloc(rkllm_input_len * sizeof(float)); FILE *file; file = fopen("models/image_embed.bin", "rb"); fread(rkllm_input.multimodal_input.image_embed, sizeof(float), rkllm_input_len, file); fclose(file); rkllm_input.input_type = RKLLM_INPUT_MULTIMODAL; // 初始化 infer 参数结构体 RKLLMInferParam rkllm_infer_params; memset(&rkllm_infer_params, 0, sizeof(RKLLMInferParam)); // 将所有内容初 始化为 0 rkllm_infer_params.mode = RKLLM_INFER_GENERATE;
RKLLM 支持不同的推理模式,定义了 RKLLMInferParam 结构体,目前可支持在推理过程与预加载的 LoRA 模型联合推理,或保存 Prompt Cache 用于后续推理加速,具体的定义如下:
表 8 RKLLMInferParam 结构体参数说明
结构体定义 | RKLLMInferParam |
描述 | 用于定义不同的推理模式。 |
字段 | RKLLMInferMode mode: 推理模式,当前仅支持 RKLLM_INFER_GENERATE 模式; RKLLMLoraParam* lora_params: 推理时使用的 LoRA 的参数配置,用于在加载多个 LoRA 时选择需要推理的 LoRA,若无需加载 LoRA 则设为 NULL 即可; RKLLMPromptCacheParam* prompt_cache_params: 推理时使用Prompt Cache的参数配置,若无需生成 Prompt Cache 则设为 NULL 即可; |
表 9 RKLLMLoraParam 结构体参数说明
结构体定义 | RKLLMLoraParam |
描述 | 用于定义推理时使用 LoRA 的参数; |
字段 | const char* lora_adapter_name: 推理时使用的 LoRA 名称 |
表 10 RKLLMInferParam 结构体参数说明
结构体定义 | RKLLMPromptCacheParam |
描述 | 用于定义推理时使用 Prompt Cache 的参数; |
字段 | int save_prompt_cache: 是否在推理时保存 Prompt Cache, 1 为需要, 0 为不需要; const char* prompt_cache_path: Prompt Cache 保存路径, 若未设置则默认保存到"./prompt_cache.bin"中; |
使用 InferRaram 的示例如下:
// 1. 初始化并设置 LoRA 参数(如果需要使用 LoRA) RKLLMLoraParam lora_params; lora_params.lora_adapter_name = "test"; // 指定用于推理的 lora 名称 // 2. 初始化并设置 Prompt Cache 参数(如果需要使用 prompt cache) RKLLMPromptCacheParam prompt_cache_params; prompt_cache_params.save_prompt_cache = true; // 是否保存 prompt cache prompt_cache_params.prompt_cache_path = "./prompt_cache.bin"; // 若需要 保存 prompt cache, 指定 cache 文件路径 rkllm_infer_params.mode = RKLLM_INFER_GENERATE; rkllm_infer_params.lora_params = &lora_params; rkllm_infer_params.prompt_cache_params = &prompt_cache_params;
1.8.4 初始化模型
在进行模型的初始化之前,需要提前定义 LLMHandle 句柄,该句柄用于模型的初始化、推理和资源释放过程。注意,正确的模型推理流程需要统一这 3 个流程中的 LLMHandle 句柄对象。在模型推理前,用户需要通过 rkllm_init()函数完成模型的初始化,具体函数的定义如下:
表 11 rkllm_init 函数接口说明
函数名 | rkllm_init |
描述 | 用于初始化 RKLLM 模型的具体参数及相关推理设置。 |
参数 | LLMHandle* handle: 将模型注册到相应句柄中,用于后续推理、释放调用; RKLLMParam* param: 模型定义的参数结构体; LLMResultCallback callback: 用于接受处理模型实时输出的回调函数; |
返回值 | 0 表示初始化流程正常;-1 表示初始化失败; |
示例代码如下:
LLMHandle llmHandle = nullptr; rkllm_init(&llmHandle, ¶m, callback);
1.8.5 模型推理
用户在完成 RKLLM 模型的初始化流程后,即可通过 rkllm_run()函数进行模型推理,并可以通过初始化时预先定义的回调函数对实时推理结果进行处理;rkllm_run()的具体函数定义如下:
表 12 rkllm_run 函数接口说明
函数名 | rkllm_run |
描述 | 调用完成初始化的 RKLLM 模型进行结果推理; |
参数 | LLMHandle handle: 模型初始化注册的目标句柄; RKLLMInput* rkllm_input: 模型推理的输入数据; RKLLMInferParam* rkllm_infer_params: 模型推理过程中的参数传递; void* userdata: 用户自定义的函数指针,默认设置为 NULL 即可; |
返回值 | 0 表示模型推理正常运行;-1 表示调用模型推理失败; |
模型推理的示例代码如下:
#define PROMPT_TEXT_PREFIX "<|im_start| >system You are a helpful assistant. <|im_end| > <|im_start| >user" #define PROMPT_TEXT_POSTFIX "<|im_end| ><|im_start| >assistant" string input_str = "把这句话翻译成英文:RK3588 是新一代高端处理器,具有高算力、 低功耗、超强多媒体、丰富数据接口等特点"; input_str = PROMPT_TEXT_PREFIX + input_str + PROMPT_TEXT_POSTFIX; // 初始化 infer 参数结构体 RKLLMInferParam rkllm_infer_params; memset(&rkllm_infer_params, 0, sizeof(RKLLMInferParam)); rkllm_infer_params.mode = RKLLM_INFER_GENERATE; // 1. 初始化并设置 LoRA 参数(如果需要使用 LoRA) RKLLMLoraParam lora_params; lora_params.lora_adapter_name = "test"; // 指定用于推理的 lora 名称 // 2. 初始化并设置 Prompt Cache 参数(如果需要使用 prompt cache) RKLLMPromptCacheParam prompt_cache_params; prompt_cache_params.save_prompt_cache = true; // 是否保存 prompt cache prompt_cache_params.prompt_cache_path = "./prompt_cache.bin"; rkllm_infer_params.mode = RKLLM_INFER_GENERATE; // rkllm_infer_params.lora_params = &lora_params; // rkllm_infer_params.prompt_cache_params = &prompt_cache_params; rkllm_infer_params.lora_params = NULL; rkllm_infer_params.prompt_cache_params = NULL; RKLLMInput rkllm_input; rkllm_input.input_type = RKLLM_INPUT_PROMPT; rkllm_input.prompt_input = (char *)text.c_str(); rkllm_run(llmHandle, &rkllm_input, &rkllm_infer_params, NULL);
1.8.6 模型中断
在进行模型推理时,用户可以调用 rkllm_abort()函数中断推理进程,具体的函数定义如下:
表 13 rkllm_ abort 函数接口说明
函数名 | rkllm_ abort |
描述 | 用于中断 RKLLM 模型推理进程。 |
参数 | LLMHandle handle: 模型初始化注册的目标句柄; |
返回值 | 0 表示 RKLLM 模型中断成功;-1 表示模型中断失败; |
示例代码如下:
// 其中 llmHandle 为模型初始化时传入的句柄 rkllm_abort(llmHandle);
1.8.7 释放模型资源
在完成全部的模型推理调用后,用户需要调用 rkllm_destroy()函数进行 RKLLM 模型的销毁,并释放所申请的 CPU、NPU 计算资源,以供其他进程、模型的调用。具体的函数定义如下:
表 14 rkllm_ destroy 函数接口说明
函数名 | rkllm_ destroy |
描述 | 用于销毁 RKLLM 模型并释放所有计算资源。 |
参数 | LLMHandle handle: 模型初始化注册的目标句柄; |
返回值 | 0 表示 RKLLM 模型正常销毁、释放;-1 表示模型释放失败; |
示例代码如下:
// 其中 llmHandle 为模型初始化时传入的句柄 rkllm_destroy(llmHandle);
1.8.8 LoRA 模型加载
RKLLM 支持在推理基础模型的同时推理 LoRA 模型,可以在调用 rkllm_run 接口前通过rkllm_load_lora 接口加载 LoRA 模型。RKLLM 支持加载多个 LoRA 模型,每调用一次rkllm_load_lora 可加载一个 LoRA 模型。具体的函数定义如下:
表 15 rkllm_load_lora 函数接口说明
函数名 | rkllm_load_lora |
描述 | 用于加载 LoRA 模型。 |
参数 | LLMHandle handle: 模型初始化注册的目标句柄; RKLLMLoraAdapter* lora_adapter: 加载 LoRA 模型时的参数配置; |
返回值 | 0 表示 LoRA 模型正常加载;-1 表示模型加载失败; |
表 16 RKLLMLoraAdapter 结构体参数说明
结构体定义 | RKLLMLoraAdapter |
描述 | 用于配置加载 LoRA 时的参数。 |
字段 | const char* lora_adapter_path: 待加载 LoRA 模型的路径; const char* lora_adapter_name: 待加载 LoRA 模型的名称, 由用户自定义, 用于后续推理时选择指定 LoRA; float scale: LoRA 模型在推理过程中对基础模型参数进行调整的幅度; |
加载 LoRA 的示例代码如下:
RKLLMLoraAdapter lora_adapter; memset(&lora_adapter, 0, sizeof(RKLLMLoraAdapter)); lora_adapter.lora_adapter_path = "lora.rkllm"; lora_adapter.lora_adapter_name = "lora_name"; lora_adapter.scale = 1.0; ret = rkllm_load_lora(llmHandle, &lora_adapter); if (ret != 0) { printf("nload lora failedn"); }
1.8.9 Prompt Cache 加载
RKLLM 支持加载预生成的 Prompt Cache 文件,以此加速模型 Prefill 阶段的推理。具体的函数定义如下:
表 17 rkllm_load_prompt_cache 函数接口说明
函数名 | rkllm_load_prompt_cache |
描述 | 用于加载 Prompt Cache。 |
参数 | LLMHandle handle: 模型初始化注册的目标句柄,可见 4. 初始化模型; const char* prompt_cache_path: 待加载 Prompt Cache 文件的路径; |
返回值 | 0 表示 Prompt Cache 模型正常加载;-1 表示模型加载失败; |
表 18 rkllm_release_prompt_cache 函数接口说明
函数名 | rkllm_release_prompt_cache |
描述 | 用于释放 Prompt Cache。 |
参数 | LLMHandle handle: 模型初始化注册的目标句柄,可见 4. 初始化模型; |
返回值 | 0 表示 Prompt Cache 模型正常释放;-1 表示模型释放失败; |
加载 Prompt Cache 的示例代码如下:
rkllm_load_prompt_cache(llmHandle, "./prompt_cache.bin"); if (ret != 0) { printf("nload Prompt Cache failedn"); } 审核编辑 黄宇