zoukankan      html  css  js  c++  java
  • 我不会用 Triton 系列:Triton 搭建 ensemble 过程记录

    Triton 搭建 ensemble 过程记录

    本文记录 Triton ensemble 搭建的过程,在 Triton 这个特性叫做 ensemble,但是这个特性叫做 pipeline 更为常见,后面就叫 pipeline 吧。首先要说明的是,本文中的例子只是为了试试看 Triton pipeline 这个特性,我认为搭建出的 pipeline 不一定就是高效的。

    先来说说本文将要搭建什么样的 pipeline。本文将使用 resnet50 来进行图片分类,分类的类别保持不变。在对图片进行分类之前,一般都需要有一个预处理的过程。因此,这篇文章将搭建的 pipeline 很简单,就先进行预处理,然后分类。预处理采用 DALI 来处理,resnet50 使用 Pytorch 导出。

    本文使用的模型配置文件,已经放在了 Github 上面:https://github.com/zzk0/triton

    Pytorch 搭建 resnet50

    Pytorch 导出 resnet50 模型

    非常简单的一个代码片段,于是我们得到了一个 torchscript 模型。

    import torch
    import torchvision.models as models
    
    resnet50 = models.resnet50(pretrained=True)
    resnet50.eval()
    image = torch.randn(1, 3, 244, 244)
    resnet50_traced = torch.jit.trace(resnet50, image)
    resnet50(image)
    resnet50_traced.save('model.pt')
    

    Pytorch 模型配置

    我们将文件按照如下方式进行组织,其中 config.pbtxt 是模型配置文件,labels.txt 是 resnet50 训练时候的分类类别,里面有一千个类。另外还需要注意的是,labels.txt 里面的写法,就是一个字符串一个类别就好了。

    .
    ├── 1
    │   └── model.pt
    ├── config.pbtxt
    ├── labels.txt
    

    config.pbtxt 的写法,通过指定 label_filename 来设定标签文件,输出有 1000 维。

    name: "resnet50_pytorch"
    platform: "pytorch_libtorch"
    max_batch_size: 128
    input [
      {
        name: "INPUT__0"
        data_type: TYPE_FP32
        dims: [ 3, -1, -1 ]
      }
    ]
    output [
      {
        name: "OUTPUT__0"
        data_type: TYPE_FP32
        dims: [ 1000 ]
        label_filename: "labels.txt"
      }
    ]
    instance_group [
      {
        count: 1
        kind: KIND_GPU
      }
    ]
    

    客户端

    将模型放到 Triton 的模型仓库之后,启动服务器。之后我们使用下面的脚本进行请求。在这个客户端里,我们先自己做预处理,后续我们将会把预处理的操作放置到服务端。

    如果我们想要获取分类的结果,我们可以设置 class_count=k,表示获取 TopK 分类预测结果。如果没有设置这个选项,那么将会得到一个 1000 维的向量。

    import numpy as np
    import tritonclient.http as httpclient
    import torch
    from PIL import Image
    
    
    if __name__ == '__main__':
        triton_client = httpclient.InferenceServerClient(url='172.17.0.2:8000')
    
        image = Image.open('../resources/images/cat.jpg')
        
        image = image.resize((224, 224), Image.ANTIALIAS)
        image = np.asarray(image)
        image = image / 255
        image = np.expand_dims(image, axis=0)
        image = np.transpose(image, axes=[0, 3, 1, 2])
        image = image.astype(np.float32)
    
        inputs = []
        inputs.append(httpclient.InferInput('INPUT__0', image.shape, "FP32"))
        inputs[0].set_data_from_numpy(image, binary_data=False)
        outputs = []
        outputs.append(httpclient.InferRequestedOutput('OUTPUT__0', binary_data=False, class_count=1))
    
        results = triton_client.infer('resnet50_pytorch', inputs=inputs, outputs=outputs)
        output_data0 = results.as_numpy('OUTPUT__0')
        print(output_data0.shape)
        print(output_data0)
    

    DALI

    接下来,我们将客户端预处理的操作放到了服务端上。这里必须要指出的是,这么做只是为了搭建 pipeline,并不是为了性能。你想,图片没有预处理之前,是不是很大,通过网络传输到服务端的开销可能盖过了服务端预处理的收益。

    导出 DALI 预处理 pipeline

    通过下面的脚序列化 pipeline。

    import nvidia.dali as dali
    import nvidia.dali.fn as fn
    
    @dali.pipeline_def(batch_size=128, num_threads=4, device_id=0)
    def pipeline():
        images = fn.external_source(device='cpu', name='DALI_INPUT_0')
        images = fn.resize(images, resize_x=224, resize_y=224)
        images = fn.transpose(images, perm=[2, 0, 1])
        images = images / 255
        return images
    
    
    pipeline().serialize(filename='./1/model.dali')
    

    DALI 模型配置

    我们将文件按照如下方式组织。

    .
    ├── 1
    │   └── model.dali
    ├── config.pbtxt
    

    模型配置如下。需要注意一个问题:模型实例化的时候,如果没有设置设备,Triton 会在每个设备上初始化一个,接着会发生 core dump。目前猜想的原因是,序列化保存的 pipeline 保存了 device_id=0 这个信息,然后在我的服务器上的第二张卡上初始化模型实例的时候,会出错。后续仔细分析看看,提个 issue 或 pr。

    配置好之后,放到模型仓库,然后使用 Github 中对应的脚本做请求试试看,这里就不啰嗦了。

    name: "resnet50_dali"
    backend: "dali"
    max_batch_size: 128
    input [
      {
        name: "DALI_INPUT_0"
        data_type: TYPE_FP32
        dims: [ -1, -1, 3 ]
      }
    ]
    
    output [
      {
        name: "DALI_OUTPUT_0"
        data_type: TYPE_FP32
        dims: [ 3, 224, 224 ]
      }
    ]
    instance_group [
      {
        count: 1
        kind: KIND_GPU
        gpus: [ 0 ]
      }
    ]
    

    搭建 pipeline

    模型配置

    pipeline 的配置方法也挺简单的,只不过个人觉得手写 protobuf 不太顺手,用户体验不太好。

    下面说几个要注意的点:一,ensemble 的 key 是 platform,不是 backend。二,model_version 设为数字,而不是字符串。三,ensemble_scheduling 的输入输出 key 都是对应模型的输入输出名字。

    name: "resnet50_ensemble"
    platform: "ensemble"
    max_batch_size: 128
    input [
      {
        name: "ENSEMBLE_INPUT_0"
        data_type: TYPE_FP32
        dims: [ -1, -1, 3 ]
      }
    ]
    output [
      {
        name: "ENSEMBLE_OUTPUT_0"
        data_type: TYPE_FP32
        dims: [ 1000 ]
      }
    ]
    ensemble_scheduling {
      step [
        {
          model_name: "resnet50_dali"
          model_version: 1
          input_map: {
            key: "DALI_INPUT_0"
            value: "ENSEMBLE_INPUT_0"
          }
          output_map: {
            key: "DALI_OUTPUT_0"
            value: "preprocessed_image"
          }
        },
        {
          model_name: "resnet50_pytorch"
          model_version: 1
          input_map: {
            key: "INPUT__0"
            value: "preprocessed_image"
          }
          output_map: {
            key: "OUTPUT__0"
            value: "ENSEMBLE_OUTPUT_0"
          }
        }
      ]
    }
    

    客户端请求

    虽然在客户端避开预处理,但是不能完全避开。比如我们一定需要设置好输入的 shape,否则 Triton 就是不认你这个请求,所以还是自己手动加一个维度。此外,输入要设置成 float32 类型。于是我们避开了 resize 等预处理操作。

    请求的时候,你会发现,即使 pipeline 没有设置 label_filename,我们仍然可以获取分类的结果。这里我猜测 Triton 的内部实现可能是,输入的 Shape 会进行检查,输出的 Shape 就不理了(这个不是看 Backend 是否检查嘛。

    import numpy as np
    import tritonclient.http as httpclient
    import torch
    from PIL import Image
    
    
    if __name__ == '__main__':
        triton_client = httpclient.InferenceServerClient(url='172.17.0.2:8000')
    
        image = Image.open('../resources/images/cat.jpg')
        image = np.asarray(image)
        image = np.expand_dims(image, axis=0)
        image = image.astype(np.float32)
    
        inputs = []
        inputs.append(httpclient.InferInput('ENSEMBLE_INPUT_0', image.shape, "FP32"))
        inputs[0].set_data_from_numpy(image, binary_data=False)
        outputs = []
        outputs.append(httpclient.InferRequestedOutput('ENSEMBLE_OUTPUT_0', binary_data=False, class_count=1))
    
        results = triton_client.infer('resnet50_ensemble', inputs=inputs, outputs=outputs)
        output_data0 = results.as_numpy('ENSEMBLE_OUTPUT_0')
        print(output_data0.shape)
        print(output_data0)
    

    至此,我们就可以使用一张没有预处理过的照片,然后直接发送给 Triton,Triton 帮你做预处理,然后d对处理的结果做分类。不过,我现在对 pipeline 的原理还不是很清楚,比如有个问题:pipeline 的模型和模型之间,是否会发生额外的内存复制开销呢?这个要深入源码看一看了。

    附上自己的请求结果。

  • 相关阅读:
    Apache Spark 2.2.0 中文文档
    Apache Spark 2.2.0 中文文档
    Apache Spark 2.2.0 中文文档
    Apache Spark 2.2.0 中文文档
    Apache Spark 2.2.0 中文文档
    Apache Spark RDD(Resilient Distributed Datasets)论文
    Apache Spark 2.2.0 中文文档
    Apache Spark 2.2.0 中文文档
    【机器学习实战】第10章 K-Means(K-均值)聚类算法
    [译]flexbox全揭秘
  • 原文地址:https://www.cnblogs.com/zzk0/p/15517120.html
Copyright © 2011-2022 走看看