在android 系统上qnn sdk转换,运行模型示例

前面讲了如何配置qnn sdk的环境,这一篇总结下qnn 实际转换一个onnx 模型,并运行的实现步骤。

设备:

1. ubuntu22.04 的Linux 服务器。

2. 一台android手机。

一、下载模型

from optimum.onnxruntime import ORTModelForSequenceClassification
from transformers import AutoTokenizer

def download_model():
    model_path = "BAAI/bge-base-zh-v1.5"
    save_directory = "./bge-base-zh-v1.5.onnx"

    # 从 Transformers 加载模型并将其导出为 ONNX
    model = ORTModelForSequenceClassification.from_pretrained(model_path, export=True)
    tokenizer = AutoTokenizer.from_pretrained(model_path)

    # 保存 ONNX 模型和分词器
    model.save_pretrained(save_directory)
    tokenizer.save_pretrained(save_directory)

Check模型和简化模型,固定shape.方便后续的处理。

onnxsim ./bge-model.onnx bge-simple2-model.onnx --overwrite-input-shape inputs_ids:1,512 --overwrite-input-shape attention_mask:1,512  --overwrite-input-shape token_type_ ids:1,512

 二、构建量化数据

因为真实数据集对于构建量化是非常重要的,量化的质量直接影响到模型的推理结果准确性,使用的校准数据应该最大程度上接近训练和测试的数据集,这样子测试的效果更加的贴合实际业务场景的测试结果。

Embedding模型的主要测试数据集通常包括多种语言和任务类型的数据集,以全面评估模型的性能。例如,MTEB(Massive Text Embedding Benchmark)是一个大规模的文本嵌入基准测试,旨在全面评估文本嵌入方法的性能。MTEB 覆盖了 8 种嵌入任务,包含 58 个数据集和 112 种语言。此外,C-MTEB 是专门针对中文文本向量的评测基准,共收集了35个公共数据集,分为6类的评估任务。这些数据集可以帮助评估Embedding模型在不同语言和任务上的表现。所以我们选择C-MTEB(Chinese Massive Text Embedding Benchmark)测试中使用的LCQMC数据集进行构建量化需要的校准数据。

  1. 使用python 加载C-MTEB/LCQMC数据集。

  2. 使用tokener进行文本的词化。

  3. 将数据保存成三个输入,格式是raw,也就是原始的二进制文件。

def create_lcqmc_quant_data():
    dataset = load_dataset("C-MTEB/LCQMC", split="test")
    sentence = dataset["sentence1"]
    model = AutoModel.from_pretrained("BAAI/bge-base-zh-v1.5")
    tokenizer = AutoTokenizer.from_pretrained("./")
    #开启评估模式
    model.eval()
    calib_data_size = 100
    input_data = tokenizer(sentence, padding="max_length", truncation=True, max_length=512, return_tensors="pt")
    input_ids = input_data["input_ids"].numpy().astype(np.int32)[:calib_data_size, :]
    attention_mask = input_data["attention_mask"].numpy().astype(np.int32)[:calib_data_size, :]
    token_type_ids = input_data["token_type_ids"].numpy().astype(np.int32)[:calib_data_size, :]

    calib_data_dir = "calib_data"
    os.makedirs(calib_data_dir)
    path_lists = []
    path_file = "input_list.txt"


    # one for funcation to iter multi arr
    for index, (arr, name) in enumerate(zip([input_ids, attention_mask, token_type_ids], ["input_ids", "attention_mask", "token_type_ids"])):
        # iter rows.
        # iter input_id, then mask, last token_id
        for i in range(arr.shape[0]):
            # one arr save in one file.
            file_path = os.path.join(calib_data_dir, f"{name}_{i}.raw")
            #sava path.
            path_lists.append(file_path)
            ## save data.
            arr[i].tofile(file_path)

    with open(path_file, "w", encoding="utf-8") as f:
        len = input_ids.shape[0]
        print(3*input_ids.shape[0])
        for i in range(input_ids.shape[0]):
            # one raw contain three input data file path.
            f.write(f"{os.path.abspath(path_lists[i])} {os.path.abspath(path_lists[i + len])} {os.path.abspath(path_lists[i + 2*len])}\n")

    print("create quant data finish.")

三、构建模型的运行输入数据

def create_model_input_data():
    model = AutoModel.from_pretrained("BAAI/bge-base-zh-v1.5")
    tokenizers = AutoTokenizer.from_pretrained("BAAI/bge-base-zh-v1.5")
    input_data = "ZhongGuo, nihao, 日本再见, good cat!"
    model.eval()

    current_path = os.path.abspath(__file__)
    dir_name = os.path.dirname(current_path)
    print(dir_name)

    real_data = "real_data"
    os.makedirs(real_data)

    input_ids_path = os.path.join(dir_name, "real_data" + "\\"  + "input_ids.raw")
    attention_mask_path = os.path.join(dir_name,   "real_data" + "\\" + "attention_mask.raw")
    token_type_ids_path = os.path.join(dir_name,  "real_data" + "\\" + "token_type_ids.raw")

    ## get input
    input_tensor_data = tokenizers(input_data, padding="max_length", truncation=True, max_length=512, return_tensors="pt" )
    input_tensor_data["input_ids"].numpy().astype(np.int32).tofile(input_ids_path)
    input_tensor_data["attention_mask"].numpy().astype(np.int32).tofile(attention_mask_path)
    input_tensor_data["token_type_ids"].numpy().astype(np.int32).tofile(token_type_ids_path)

    print(input_ids_path)
    path_list = [input_ids_path + " ", attention_mask_path + " ", token_type_ids_path + " "]
    with open("./input_data.txt", "w", encoding="utf-8") as f:
        f.writelines(path_list)

四、运行onnx  模型得到基准数据

def run_bge_model():
    model = AutoModel.from_pretrained("BAAI/bge-base-zh-v1.5")
    tokenizers = AutoTokenizer.from_pretrained("BAAI/bge-base-zh-v1.5")
    input_data =  "ZhongGuo, nihao, 日本再见, good cat!"

    device = "cuda" if torch.cuda.is_available() else "cpu"
    model.to(device)
    model.eval()
    input_tensor_data = tokenizers(input_data, padding="max_length", truncation=True, max_length=512, return_tensors="pt" ).to(device)
    with torch.no_grad():
        output = model(**input_tensor_data)

    output_data = output.last_hidden_state.flatten().tolist()[:100]
    print(len(output.last_hidden_state.flatten().tolist()))
    print(output_data)

五、转换onnx 模型得到模型文件和权重数据。

 python /home/shengqing.liu/qaisw-v2.12.0.230626174329_59328-auto/bin/x86_64-linux-clang/qnn-onnx-converter -o ./bge_real_data_qnn.cpp -i ./bge-simple-model.onnx  --input_list ./input_list_calib.txt

转换成得到了三个文件bge_real_data_qnn.bin, bge_real_data_qnn.cpp, bge_real_data_qnn_net.json. 这三个文件就是qnn的模型文件。

.bin文件: 这个保存了qnn模型的参数。

.cpp文件:这个文件描述了qnn 模型结构的api 文件,依据这个文件可以创建qnn的计算图,Tool utility api 是用于生成 QNN API 调用的辅助模块。这些 API 是core QNN API 之上的轻量级包装器,旨在减少创建 QNN 图的重复步骤。

.json 文件:这个json 文件也是描述了qnn的模型结构文件,只是用来辅助描述模型结构文件的,这个文件后续的步骤中不会再使用到。

注意:

1. 将量化数据文件的绝对路径写入到input_list.txt中,相对路径是不行的,到时模型转换的时候会报错。

2. 加入--input_list 会使用默认的量化W8A8 进行量化模型,参看官方文档,查看支持的量化方法是min/max, 精度其实很差,基于经验,qnn的量化基本不可使用,勉强能用的W8A16 还要看特定的业务,一般也是精度达不到要求的。w16A16的量化需要芯片架构大于V73的才支持。

3. 设置其他的量化精度使用参数比如W8A16 --act_bw 16 --weight_bw 8

4. 实际中使用的都是半精度FP16 , 在转换的时候去除--input_list 参数,加上--float_bw 16 就是半精度模型了。类似这种

python ${QNN_SDK_ROOT_27}/bin/x86_64-linux-clang/qnn-onnx-converter -o ./bge_qnn_27_fp16_noquant.cpp -i ./bge-simple-model.onnx --float_bw 16

5. 半精度模型运行时候,加上--use_native_input_files ,  精度会好很多,实际中需要加上这个参数。

六、得到动态库的so模型文件

将cpp 和bin 文件作为输入文件,使用qnn-model-lib-generator转换成so 库文件。得到了Android和linux 平台的so 文件,这个文件其实是动态加载模型文件,这个so 也可以进行运行,但是初始化的时间需要较长,当进一步转换成bin文件,进行编译优化的时候,推理运行时加载准备时间就大大减少。

 ${QNN_SDK_ROOT}/bin/x86_64-linux-clang/qnn-model-lib-generator -c ./bge_real_data_qnn.cpp -b ./bge_real_data_qnn.bin -o rag_qnn_so

得到so 模型文件,这两个目录下的so 都是可以直接运行的qnn 模型文件,android 目录下的是可以直接在android 系统运行的,linux 下的是在linux 平台运行的,并且在转bin 文件的时候,在linux 平台使用这个linux 目录下的so 转出来的bin, 可以在android 设备htp等后端运行,这点注意。

 七、进一步编译得到bin 模型文件

我们使用htp 后端进行推理,可以构建一个后端backend配置文件,然后将配置文件作为输入,转换得到和硬件相关联的bin 文件,类似于c++ 代码实现中的构建context的那一步。

 ${QNN_SDK_ROOT}/bin/x86_64-linux-clang/qnn-context-binary-generator --model ./output-so/x86_64-linux-clang/libbge_real_data_qnn.so --backend ${QNN_SDK_ROOT}/lib/x86_64-linux-clang/libQnnHtp.so --output_dir ./bin --binary_file bge_8295_qnn.serialized --config_file ./htp_backend_extension.json

配置文件如下,我配置的文件如下:

htp_backend_extension.json

{ 
  "backend_extensions":{
    "shared_library_path":"/home/shengqing.liu/qaisw-v2.12.0.230626174329_59328-auto/lib/x86_64-linux-clang/libQnnHtpNetRunExtensions.so",
    "config_file_path": "/home/shengqing.liu/model/rag-bge-base/htp_device_config.json"
  }
}

htp_device_config.json

{
    "graphs":
        {
          "vtcm_mb":8,  // 片上内存设置
          "O":3.0,     // 等级有1.0, 2.0, 3.0 , 3.0优化最大
          "fp16_relaxed_precision":1,  // 支持fp16计算
          "graph_names":["bge_smiply_qnn"]  //生成的so文件名中去除lib,就是图名称。
        },
    
    
     "devices":[
         {
             "device_id":0,    //npu的device id, 可以不设置
             "soc_id":39,      // 芯片系统的id.
             "dsp_arch":"v68",   // 芯片的架构
             "cores":[{
                 "core_id":0,
                 "perf_profile":"burst"  //性能等级,这个是很快的设置,还有其他的。
                 
             }]
           
         }
     ]
}

sdk2.20 以后定义图配置是使用图数组,之前的版本是使用图对象的格式,所以定义图数组之后,会出现找不到key的错误。

 八、运行模型

1. Linux平台运行

${QNN_SDK_ROOT}/bin/x86_64-linux-clang/qnn-net-run --backend ${QNN_SDK_ROOT}/lib/x86_64-linux-clang/libQnnHtp.so --model ./rag_qnn_so/x86_64-linux-clang/libreal_data_qnn.so --input_list ./input_list.txt

2. android 系统运行

将qnn的so库, 模型,数据,可执行文件push 到android系统中。

adb shell mkdir -p /data/local/tmp/bge

//模型文件
 adb push D:\work_project\2025\bge\real_data_quant\bin\bge_8295_qnn.serialized.bin  /data/local/tmp/bge_model
//输入数据
adb push D:\work_project\2025\bge\real_data_quant\real_input_data\* /data/local/tmp/bge
//输入数据地址
adb push D:\work_project\2025\bge\real_data_quant\input_data .txt  /data/local/tmp/bge_model

//可执行文件
adb push $QNN_SDK_ROOT/bin/aarch64-android/qnn-net-run /data/local/tmp/bge 

adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtp.so /data/local/tmp/bge
adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtpNetRunExtensions.so /data/local/tmp/bge
adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtpPrepare.so /data/local/tmp/bge
adb push $QNN_SDK_ROOT/lib/hexagon-v68/unsigned/libQnnHtpV68Skel.so /data/local/tmp/bge
adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtpV68Stub.so /data/local/tmp/bge
#系统so
adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnSystem.so /data/local/tmp/bge

修改/data/local/tmp/bge_model权限,然后设置环境变量,进行使用bin文件模型的运行。

chmod -R 777 /data/local/tmp/bge_model/*

export LD_LIBRARY_PATH=/data/local/tmp/bge_model/:/vendor/lib64/:$LD_LIBRARY_PATH
export ADSP_LIBRARY_PATH=/data/local/tmp/bge_model

./qnn-net-run --backend ./libQnnHtp.so --retrieve_context  ./bge_8295_qnn.serialized.bin --input_list ./input_data.txt  --output_dir output2

 使用so 模型文件进行运行

./qnn-net-run --backend ./libQnnHtp.so --model  ./bge_8295_qnn.so --input_list ./input_data.txt --output_dir output1

如果是int 型的输入数据,强烈建议加上--use_native_input_files. 表示读输入文件的时候按照int32读而不是float32,精度会大大改善。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值