模型推理加速系列 | 01:如何用ONNX加速BERT特征抽取(附代码)

本文涉及的产品
实时数仓Hologres,5000CU*H 100GB 3个月
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时计算 Flink 版,5000CU*H 3个月
简介: 本次实验目的在于介绍如何使用ONNXRuntime加速BERT模型推理。实验中的任务是利用BERT抽取输入文本特征,至于BERT在下游任务(如文本分类、问答等)上如何加速推理,后续再介绍。

简介

近期从事模型推理加速相关项目,所以抽空整理最近的学习经验。本次实验目的在于介绍如何使用ONNXRuntime加速BERT模型推理。实验中的任务是利用BERT抽取输入文本特征,至于BERT在下游任务(如文本分类、问答等)上如何加速推理,后续再介绍。

PS:本次的实验模型是BERT-base中文版


更多、更新文章欢迎关注 微信公众号:小窗幽记机器学习。后续会持续整理模型加速、模型部署、模型压缩、LLM、AI艺术等系列专题,敬请关注。


环境准备

由于ONNX是一种序列化格式,在使用过程中可以加载保存的graph并运行所需要的计算。在加载ONNX模型之后可以使用官方的onnxruntime进行推理。出于性能考虑,onnxruntime是用c++实现的,并为c++、C、c#、Java和Python提供API/Bindings。

在本文的示例中,将使用Python API来说明如何加载序列化的ONNX graph,并通过onnxruntime在后端执行inference。Python下的onnxruntime有2种:

    • onnxruntime: ONNX + MLAS (Microsoft Linear Algebra Subprograms)
    • onnxruntime-gpu: ONNX + MLAS + CUDA

    可以通过命令安装:

    pip install transformers onnxruntime onnx psutil matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple/
    pip install transformers onnxruntime-gpu onnx psutil matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple/

    image.gif

    本文这里先以 CPU 版进行对比。

    PS:本次实验环境的CPU型号信息如下:

    32 Intel(R) Xeon(R) Gold 6134 CPU @ 3.20GHz

    Pytorch Vs ONNX

    将 transformers 模型导出为 ONNX

    huggingface 的 transformers 已经提供将 PyTorch或TensorFlow 格式模型转换为ONNX的工具(from transformers.convert_graph_to_onnx import convert)。Pytorch 模型转为 ONNX 大致有以下4个步骤:

      1. 基于transformers载入PyTorch模型
      2. 创建伪输入(dummy inputs),并利用伪输入在模型中前向inference,换一句话说用伪输入走一遍推理网络并在这个过程中追踪记录操作集合。因为convert_graph_to_onnx这个脚本转为ONNX模型的时候,其背后是调用torch.onnx.export,而这个export方法要求Tracing网络。
      3. 在输入和输出tensors上定义动态轴,比如batch size这个维度。该步骤是可选项。
      4. 保存graph和网络参数

      上述4个步骤在convert_graph_to_onnx.convert已经封装好,所以可以直接调用该函数将Pytorch模型转为ONNX格式:

      from pathlib import Path
      from transformers.convert_graph_to_onnx import convert
      # Handles all the above steps for you
      convert(framework="pt", model="/home/data/pretrain_models/bert-base-chinese-pytorch", output=Path("onnx/bert-base-chinese.onnx"), opset=11)
      # 注意:因为convert_graph_to_onnx.convert中默认的pipeline_name是"feature-extraction"。如果是其他任务,则需要对应修改。具体支持哪些pipeline_name可以在官方接口中查阅。

      image.gif

      PS:当自定义的任务不在pipeline_name中的时候,需要自己用 torch.onnx.export 导出 ONNX 模型。

      ONNX模型优化

      通过使用特定的后端来进行inference,后端将启动特定硬件的graph优化。有3种基本的优化:

        • Constant Folding: 将graph中的静态变量转换为常量
        • Deadcode Elimination: 去除graph中未使用的nodes
        • Operator Fusing: 将多条指令合并为一条(比如,Linear -> ReLU 可以合并为 LinearReLU)

        在ONNX Runtime中通过设置特定的SessionOptions会自动使用大多数优化。注意:一些尚未集成到ONNX Runtime 中的最新优化可在优化脚本中找到,利用这些脚本可以对模型进行优化以获得最佳性能。

        安装优化工具包onnxruntime-tools

        pip install onnxruntime-tools -i https://pypi.tuna.tsinghua.edu.cn/simple/

        image.gif

        onnxruntime-tools 进行优化并保存优化后的ONNX模型:

        # optimize transformer-based models with onnxruntime-tools
        from onnxruntime_tools import optimizer
        from onnxruntime_tools.transformers.onnx_model_bert import BertOptimizationOptions
        # disable embedding layer norm optimization for better model size reduction
        opt_options = BertOptimizationOptions('bert')
        opt_options.enable_embed_layer_norm = False
        opt_model = optimizer.optimize_model(
            'onnx/bert-base-chinese.onnx',
            'bert', 
            num_heads=12,
            hidden_size=768,
            optimization_options=opt_options)
        opt_model.save_model_to_file('onnx/bert-base-chinese.opt.onnx')

        image.gif

        CPU上运行优化过的ONNX模型

        ONNX Runtime 为支持不同的硬件加速ONNX models,引入了一个可扩展的框架,称为Execution Providers(EP),集成硬件中特定的库。在使用过程中只需要根据自己的真实环境和需求指定InferenceSession中的providers即可,比如如果想要用CPU那么可以如此创建会话:session =InferenceSession(model_path,options,providers=['CPUExecutionProvider'])

        优化后的graph可能包括各种优化,如果想要查看优化后graph中一些更高层次的操作(例如EmbedLayerNormalization、Attention、FastGeLU)可以通过比如Netron等可视化工具查看。

        上述已经将优化过ONNX模型保存到磁盘。下面介绍如何加载ONNX模型进行inference。

        from os import environ
        from psutil import cpu_count
        # Constants from the performance optimization available in onnxruntime
        # It needs to be done before importing onnxruntime
        environ["OMP_NUM_THREADS"] = str(cpu_count(logical=True)) # OMP 的线程数
        environ["OMP_WAIT_POLICY"] = 'ACTIVE'
        from onnxruntime import GraphOptimizationLevel, InferenceSession, SessionOptions, get_all_providers
        from contextlib import contextmanager
        from dataclasses import dataclass
        from time import time
        from tqdm import trange
        def create_model_for_provider(model_path: str, provider: str) -> InferenceSession: 
          assert provider in get_all_providers(), f"provider {provider} not found, {get_all_providers()}"
          # Few properties that might have an impact on performances (provided by MS)
          options = SessionOptions()
          options.intra_op_num_threads = 1
          options.graph_optimization_level = GraphOptimizationLevel.ORT_ENABLE_ALL
          # Load the model as a graph and prepare the CPU backend 
          session = InferenceSession(model_path, options, providers=[provider])
          session.disable_fallback()
          return session
        @contextmanager
        def track_infer_time(buffer: [int]):
            start = time()
            yield
            end = time()
            buffer.append(end - start)
        @dataclass
        class OnnxInferenceResult:
          model_inference_time: [int]  
          optimized_model_path: str

        image.gif

        在CPU上加载ONNX模型,并进行推理:

        from transformers import BertTokenizerFast
        tokenizer = BertTokenizerFast.from_pretrained("/home/data/pretrain_models/bert-base-chinese-pytorch") # 使用 Pytorch 模型的字典
        cpu_model = create_model_for_provider("onnx/bert-base-chinese.opt.onnx", "CPUExecutionProvider") # 使用 优化过的 onnx
        # Inputs are provided through numpy array
        model_inputs = tokenizer("大家好, 我是卖切糕的小男孩, 毕业于华中科技大学", return_tensors="pt")
        inputs_onnx = {k: v.cpu().detach().numpy() for k, v in model_inputs.items()}
        # Run the model (None = get all the outputs)
        sequence, pooled = cpu_model.run(None, inputs_onnx)
        # Print information about outputs
        print(f"Sequence output: {sequence.shape}, Pooled output: {pooled.shape}")

        image.gif

        CPU上运行Pytorch 模型(作为对比基准)

        from transformers import BertModel
        PROVIDERS = {
            ("cpu", "PyTorch CPU"),
        #  Uncomment this line to enable GPU benchmarking
        #    ("cuda:0", "PyTorch GPU")
        }
        results = {}
        for device, label in PROVIDERS:
            # Move inputs to the correct device
            model_inputs_on_device = {
                arg_name: tensor.to(device)
                for arg_name, tensor in model_inputs.items()
            }
            # Add PyTorch to the providers
            model_pt = BertModel.from_pretrained("/home/data/pretrain_models/bert-base-chinese-pytorch").to(device)
            for _ in trange(10, desc="Warming up"):
              model_pt(**model_inputs_on_device)
            # Compute 
            time_buffer = []
            for _ in trange(100, desc=f"Tracking inference time on PyTorch"):
              with track_infer_time(time_buffer):
                model_pt(**model_inputs_on_device)
            # Store the result
            results[label] = OnnxInferenceResult(
                time_buffer, 
                None
            )

        image.gif

        CPU上运行ONNX模型

        PROVIDERS = {
            ("CPUExecutionProvider", "ONNX CPU"),
        #  Uncomment this line to enable GPU benchmarking
        #     ("CUDAExecutionProvider", "ONNX GPU")
        }
        # ONNX
        for provider, label in PROVIDERS:
            # Create the model with the specified provider
            model = create_model_for_provider(model_onnx_path, provider)
            # Keep track of the inference time
            time_buffer = []
            # Warm up the model
            model.run(None, inputs_onnx)
            # Compute
            for _ in trange(100, desc=f"Tracking inference time on {provider}"):
              with track_infer_time(time_buffer):
                  model.run(None, inputs_onnx)
            # Store the result
            results[label] = OnnxInferenceResult(
              time_buffer,
              model.get_session_options().optimized_model_filepath
            )
        # ONNX opt
        PROVIDERS_OPT = {
            ("CPUExecutionProvider", "ONNX opt CPU")
        }
        for provider, label in PROVIDERS_OPT:
            # Create the model with the specified provider
            model = create_model_for_provider(model_opt_path, provider)
            # Keep track of the inference time
            time_buffer = []
            # Warm up the model
            model.run(None, inputs_onnx)
            # Compute
            for _ in trange(100, desc=f"Tracking inference time on {provider}"):
              with track_infer_time(time_buffer):
                  model.run(None, inputs_onnx)
            # Store the result
            results[label] = OnnxInferenceResult(
              time_buffer,
              model.get_session_options().optimized_model_filepath
            )
        # 将 result save 处理, 绘制结果对比图
        import pickle
        with open('results.pkl', 'wb') as f:
            pickle.dump(results, f, pickle.HIGHEST_PROTOCOL)

        image.gif

        三者实验结果对比

        CPU上运行Pytorch 、 ONNX 和 优化过的ONNX:

        image.gif编辑

        结论:

        基于BERT进行特征抽取,每次处理单个文本时,CPU上运行ONNX模型比Pytorch模型大约提速3倍。但是,这里有一点疑惑的是优化过的 ONNX 和未优化过的 ONNX 性能基本相同。这着实与预期不符,有待后续进一步实验,也欢迎大家一起讨论!

        相关实践学习
        部署Stable Diffusion玩转AI绘画(GPU云服务器)
        本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
        相关文章
        |
        30天前
        |
        自然语言处理 PyTorch 算法框架/工具
        掌握从零到一的进阶攻略:让你轻松成为BERT微调高手——详解模型微调全流程,含实战代码与最佳实践秘籍,助你应对各类NLP挑战!
        【10月更文挑战第1天】随着深度学习技术的进步,预训练模型已成为自然语言处理(NLP)领域的常见实践。这些模型通过大规模数据集训练获得通用语言表示,但需进一步微调以适应特定任务。本文通过简化流程和示例代码,介绍了如何选择预训练模型(如BERT),并利用Python库(如Transformers和PyTorch)进行微调。文章详细说明了数据准备、模型初始化、损失函数定义及训练循环等关键步骤,并提供了评估模型性能的方法。希望本文能帮助读者更好地理解和实现模型微调。
        55 2
        掌握从零到一的进阶攻略:让你轻松成为BERT微调高手——详解模型微调全流程,含实战代码与最佳实践秘籍,助你应对各类NLP挑战!
        |
        25天前
        |
        机器学习/深度学习 自然语言处理 知识图谱
        |
        18天前
        |
        机器学习/深度学习 自然语言处理 算法
        [大语言模型-工程实践] 手把手教你-基于BERT模型提取商品标题关键词及优化改进
        [大语言模型-工程实践] 手把手教你-基于BERT模型提取商品标题关键词及优化改进
        65 0
        |
        2月前
        |
        搜索推荐 算法
        模型小,还高效!港大最新推荐系统EasyRec:零样本文本推荐能力超越OpenAI、Bert
        【9月更文挑战第21天】香港大学研究者开发了一种名为EasyRec的新推荐系统,利用语言模型的强大文本理解和生成能力,解决了传统推荐算法在零样本学习场景中的局限。EasyRec通过文本-行为对齐框架,结合对比学习和协同语言模型调优,提升了推荐准确性。实验表明,EasyRec在多个真实世界数据集上的表现优于现有模型,但其性能依赖高质量文本数据且计算复杂度较高。论文详见:http://arxiv.org/abs/2408.08821
        55 7
        |
        30天前
        |
        机器学习/深度学习 人工智能 自然语言处理
        【AI大模型】BERT模型:揭秘LLM主要类别架构(上)
        【AI大模型】BERT模型:揭秘LLM主要类别架构(上)
        |
        3月前
        |
        算法 异构计算
        自研分布式训练框架EPL问题之帮助加速Bert Large模型的训练如何解决
        自研分布式训练框架EPL问题之帮助加速Bert Large模型的训练如何解决
        |
        3月前
        |
        数据采集 人工智能 数据挖掘
        2021 第五届“达观杯” 基于大规模预训练模型的风险事件标签识别】3 Bert和Nezha方案
        2021第五届“达观杯”基于大规模预训练模型的风险事件标签识别比赛中使用的NEZHA和Bert方案,包括预训练、微调、模型融合、TTA测试集数据增强以及总结和反思。
        40 0
        |
        6月前
        |
        机器学习/深度学习 人工智能 开发工具
        如何快速部署本地训练的 Bert-VITS2 语音模型到 Hugging Face
        Hugging Face是一个机器学习(ML)和数据科学平台和社区,帮助用户构建、部署和训练机器学习模型。它提供基础设施,用于在实时应用中演示、运行和部署人工智能(AI)。用户还可以浏览其他用户上传的模型和数据集。Hugging Face通常被称为机器学习界的GitHub,因为它让开发人员公开分享和测试他们所训练的模型。 本次分享如何快速部署本地训练的 Bert-VITS2 语音模型到 Hugging Face。
        如何快速部署本地训练的 Bert-VITS2 语音模型到 Hugging Face
        |
        6月前
        |
        PyTorch 算法框架/工具
        Bert Pytorch 源码分析:五、模型架构简图 REV1
        Bert Pytorch 源码分析:五、模型架构简图 REV1
        86 0
        |
        6月前
        |
        PyTorch 算法框架/工具
        Bert Pytorch 源码分析:五、模型架构简图
        Bert Pytorch 源码分析:五、模型架构简图
        65 0