SessionGroup

简介

当前Inference场景中,无论用户直接使用TFServing还是使用TF提供的C++接口调用Session::Run,都无法实现多个Session并发处理Request,导致单个Session无法很好的实现CPU or GPU的有效利用。用户如果通过多Instance方式(多进程),无法共享底层的Variable,导致大量使用内存,并且每个Instance各自加载一遍模型,严重影响资源的使用率和模型加载效率。

SessionGroup功能提供了可以配置一组Session,并且将Request通过Round Robin(支持用户自定义策略)方式分发到某一个Session。SessionGroup中的每个Session有私有的线程池,并且支持每个线程池绑定底层的CPU Core,这样可以最大程度的避免共享资源导致的锁冲突开销。SessionGroup中唯一共享的资源是Variable,所有Session共享底层的Variable,并且模型加载只需要加载一次。

通过使用SessionGroup,可以解决内存占用大,但模型CPU使用率低的问题,大大提高资源利用率,在保证latency的前提下极大提高QPS。此外SessionGroup也可以在GPU场景下通过多Session并发执行,大大提高GPU的利用效率。

接口介绍

如果用户使用Tensorflow Serving进行服务,可以使用我们提供的代码: DeepRec-AI/serving,这里已经提供了接入SessionGroup的功能。 也可以使用我们提供的Processor代码,Processor没有提供RPC服务框架,通常使用PAI-EAS作为RPC框架。如果用户有特殊需求,亦可自行接入自有RPC框架中。

1.Processor + EAS

CPU任务

用户在processor上使用session_group,只需要在配置文件中增加如下字段:

"model_config": {
  "session_num": 2,
  "use_per_session_threads": true,
  ...
}

GPU任务

对于GPU任务,需要做以下一些配置:

"model_config": {
  "session_num": 2,
  "use_per_session_threads": true,
  "gpu_ids_list": "0,2",
  ...
}

更多参数详见:processor配置参数

2.Tensorflow serving

TFServing使用的是SavedModelBundle进行serving的,相关的代码修改参考:SessionGroup,推荐直接使用我们提供的TFServing代码。

支持SessionGroup的TFServing代码见:DeepRec-AI/serving

编译文档见:TFServing编译

CPU任务

使用方式如下:

bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --tensorflow_intra_op_parallelism=16 --tensorflow_inter_op_parallelism=16 --use_per_session_threads=true --session_num_per_group=4 --model_base_path=/xxx/pb

主要参数:

session_num_per_group:表示session group中创建几个sessions。

use_per_session_threads:为true表示每个session使用独立的线程池,减少session之间的干扰,建议配置为true。每个session的线程池都是通过tensorflow_intra_op_parallelis和tensorflow_inter_op_parallelism控制大小。

用户可以为SessionGroup中每个session指定在哪些cpu cores上执行,默认功能关闭,有两种方式开启:

1.用户手动设置,
SESSION_GROUP_CPUSET="2-4;5-7;8-10;11-13"
或者
SESSION_GROUP_CPUSET="2,3,4;5,6,7;8,9,10;11,12,13"
表示有4个session,每个session分别指定cpu上执行。
session0: 2 3 4
session1: 5 6 7
session2: 8 9 10
session3: 11 12 13

2.如果用户不设置环境变量SESSION_GROUP_CPUSET,那么需要设置SET_SESSION_THREAD_POOL_AFFINITY=1,
这样进程会检测哪些cpu可以被分配,从而分给不同的session。

上述参数在GPU任务中也可用。

GPU任务

在Inference场景中,用户常使用GPU进行线上服务,来提升计算效率,减小延迟。这里可能会遇到的一个问题是,线上GPU利用率低,造成资源浪费。那么为了利用好GPU资源,我们使用Multi-streams处理请求,在保证延迟的前提下极大提升QPS。在GPU场景下,使用session group会默认使用multi-stream,即每个session使用一个独立的stream。

目前multi-streams功能是和SessionGroup功能绑定使用的,SessionGroup的用法详见前面链接。后续我们会在DirectSession上直接支持multi-streams功能。

具体用法和SessionGroup的用法一样,在此基础上需要做如下修改。

1.docker启动配置

本优化中使用了GPU MPS(Multi-Process Service)优化(可选,建议打开),这要求在docker启动后,需要使用下面命令启动后台MPS service进程。

nvidia-cuda-mps-control -d
2.启动命令

这里以Tensorflow serving为例(后续补充其他使用方式),在启动server时需要增加下列参数,

CUDA_VISIBLE_DEVICES=0  ENABLE_MPS=1 CONTEXTS_COUNT_PER_GPU=4 MERGE_COMPUTE_COPY_STREAM=1 PER_SESSION_HOSTALLOC=1 bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --tensorflow_intra_op_parallelism=8 --tensorflow_inter_op_parallelism=8 --use_per_session_threads=true  --session_num_per_group=4 --allow_gpu_mem_growth=true --model_base_path=/xx/xx/pb/

ENABLE_MPS=1: 开启MPS(一般都建议开启)。
CONTEXTS_COUNT_PER_GPU=4: 每个物理GPU配置几组cuda context,默认是4。
MERGE_COMPUTE_COPY_STREAM=1: 表示计算和拷贝使用相同的stream,减少不同stream之间的等待。
PER_SESSION_HOSTALLOC=1: 表示每个session使用独立的gpu host allocator。

use_per_session_threads=true: 每个session单独配置线程池。
session_num_per_group=4: session group中配置几个session。
3.多张GPU使用

如果用户不指定CUDA_VISIBLE_DEVICES=0,同时机器上存在多张GPU,那么session group会默认使用所有GPU。假设有2张GPU,并且设置session_num_per_group=4,那么session group会在每个GPU上创建4个streams,因为目前一个stream对应一个session,所以当前session group中总共有2*4=8个sessions。这些session需要的在CPU上的模型参数都是共享的,对于place到GPU上的模型参数,session关联的stream在相同的GPU上,那么这些session之间是共享的,不同GPU上的session不共享。

用户可以指定给一个session group分配几张物理GPU,

--gpu_ids_list=0,2

上面参数表示给当前session group分配0和2号GPU。需要注意的是,这里的GPU编号并非和nvidia-smi看到的编号是对应的,而是deeprec看到的编号。

举个例子,假设物理机器上有4张GPU,编号是0,1,2,3,如果用户什么都不设置,那么在deeprec中看到的编号0,1,2,3和物理GPU编号是对应的。如果用户设置CUDA_VISIBLE_DEVICES=3,2,1,0,那么此时deeprec中看到的编号0,1,2,3对应的物理GPU编号分别是3,2,1,0。

上述对应关系不影响实际的使用,用户只需要关心,deeprec实际能看到几张物理GPU即可,假设能看到3张GPU,那么deeprec可见编号就是0,1,2。

上述--gpu_ids_list=0,2表示用户可以使用0和2号GPU,如果deeprec看到的GPU少于3张(0,1,2),那么会报错。

整体命令如下:

ENABLE_MPS=1 CONTEXTS_COUNT_PER_GPU=4 bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --tensorflow_intra_op_parallelism=8 --tensorflow_inter_op_parallelism=8 --use_per_session_threads=true  --session_num_per_group=4 --allow_gpu_mem_growth=true --gpu_ids_list=0,2  --model_base_path=/xx/xx/pb/

上面环境变量详细解释见: 启动参数 TF serving用DeepRec提供的代码: TF serving

4.多卡MPS最佳实践

用户机器上可能存在多张GPU卡,对于每个serving instance一般只需要使用一个GPU Device,那么用户可能会在物理机器上启动多个不同的serving instance。在这种情况下使用MPS有一些需要注意的问题,具体如下:

  1. 需要在物理机器启动mps daemon进程,这样才有机会让所有的任务docker中对于MPS后台进程可见。

nvidia-cuda-mps-control -d
  1. 启动任务docker时,需要增加--ipc=host,保证在docker中对mps daemon进程可见。同时对于每个docker来说,建议mount指定的GPU Device,如下所示:

sudo docker run -it --name docker_name --ipc=host --net=host --gpus='"device=0"' docker_image bash

这样在docker只会可见一张GPU卡,并且逻辑编号为0,那么可以像下面这样执行脚本:

CUDA_VISIBLE_DEVICES=0 test.py
或者
test.py

如果docker mount了所有的GPU Devices,那么在执行脚本的时候,需要手动指定可见的gpu device来达到隔离的效果。

sudo docker run -it --name docker_name --ipc=host --net=host --gpus=all docker_image bash

docker0中:
CUDA_VISIBLE_DEVICES=0 test.py

docker1中:
CUDA_VISIBLE_DEVICES=1 test.py

3.用户框架对接

用户如果需要将session group对接到自己的框架中,可以参考下面processor中的实现。

创建SessionGroup

如果是手动创建Session::Run方式实现的Serving,那么将serving框架中NewSession改为NewSessionGroup。 session_num指定SessionGroup中创建多少个Session,用户可以通过评估当前单个Session的CPU利用率,判断需要创建多少个Session。比如如果当前单个Session CPU的最高利用率为20%,建议用户配置5个Session。

TF_RETURN_IF_ERROR(NewSessionGroup(*session_options_,
    session_group, session_num));
TF_RETURN_IF_ERROR((*session_group)->Create(meta_graph_def_.graph_def()));

参考代码: Processor

SessionGroup Run

用户原有代码使用Session::Run可以直接替换为SessionGroup::Run

status = session_group_->Run(run_options, req.inputs,
    req.output_tensor_names, {}, &resp.outputs, &run_metadata);

参考代码: Processor

多模型服务

TF Serving

SessionGroup支持多模型服务,在TF_serving上已经支持。对于多模型服务,用户可以对于每个模型服务配置独立的参数,包括不同的模型session group中使用几个session,指定gpu卡,指定线程池等等,从而在框架,资源上进行隔离。

对于GPU任务,目前session group可以指定一张或者多张GPU卡,所以用户在启动多模型任务时,需要注意GPU资源的划分。

启动多模型服务命令如下:

ENABLE_MPS=1 CONTEXTS_COUNT_PER_GPU=4 MERGE_COMPUTE_COPY_STREAM=1 PER_SESSION_HOSTALLOC=1 bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --rest_api_port=8888 --use_session_group=true --model_config_file=/data/workspace/serving-model/multi_wdl_model/models.config --platform_config_file=/data/workspace/serving-model/multi_wdl_model/platform_config_file

上面环境变量详细解释见:启动参数

上述命令中最重要的是两个配置文件,

假设机器上有4张GPU设备,那么配置如下:

model_config_file:

model_config_list:{
    config:{
      name:"pb1",
      base_path:"/data/workspace/serving-model/multi_wdl_model/pb1",
      model_platform:"tensorflow",
      model_id: 0
    },
    config:{
      name:"pb2",
      base_path:"/data/workspace/serving-model/multi_wdl_model/pb2",
      model_platform:"tensorflow",
      model_id: 1
    },
}

对于每个模型需要配一个对应的config模块,

  • name表示模型服务名称,client端request访问时,需要填充对应的服务名称request.model_spec.name = 'pb1'

  • base_path表示模型所在路径。

  • model_platform: 默认tensorflow。

  • model_id:给每个模型一个编号,从0开始。

platform_config_file:

platform_configs {
  key: "tensorflow"
  value {
    source_adapter_config {
      [type.googleapis.com/tensorflow.serving.SavedModelBundleV2SourceAdapterConfig] {
        legacy_config {
          model_session_config {
            session_config {
              gpu_options {
                allow_growth: true
              }
              intra_op_parallelism_threads: 8
              inter_op_parallelism_threads: 8
              use_per_session_threads: true
              use_per_session_stream: true
            }
            session_num: 2
            cpusets: "1,2;5,6"
            gpu_ids: [0,1]
          }
          model_session_config {
            session_config {
              gpu_options {
                allow_growth: true
              }
              intra_op_parallelism_threads: 16
              inter_op_parallelism_threads: 16
              use_per_session_threads: true
              use_per_session_stream: true
            }
            session_num: 2
            cpusets: "20,21;23,24;26,27;29,30"
            gpu_ids: [2,3]
          }
        }
      }
    }
  }
}

key和上面model_platform字段一样,默认tensorflow。对于每个模型需要配置一个model_session_config,包含session的一些配置。model_session_config最终是一个数组,那么model_session_config[0]即代表model_0的配置,以此类推。

注意上面gpu_ids参数,表示每个session group使用哪些GPU。这里假设机器上有4张GPU,那么两个session group上分别使用gpu-0,gpu1和gpu-2,gpu-3。需要特别注意,此时session group中总的session数量是session_num*gpu_ids.size,即4个sessions。

Server端示例:

CUDA_VISIBLE_DEVICES=1,3 ENABLE_MPS=1 MERGE_COMPUTE_COPY_STREAM=1 PER_SESSION_HOSTALLOC=1 bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --rest_api_port=8888 --use_session_group=true --model_config_file=/xxx/model_config_file --platform_config_file=/xxx/platform_config_file

上面环境变量详细解释见: 启动参数

Client端示例:

...
request = predict_pb2.PredictRequest()
request.model_spec.name = 'pb2' # set model name here, like 'pb1', 'pb2' ...
request.model_spec.signature_name = 'serving_default'
...

EAS+Processor

多模型请求的分发需要EAS框架支持,Processor目前仅支持CPU任务(GPU待支持),我们可以为不同的模型服务配置不同的cpu资源,线程池等,配置文件如下:

{
  "platform": "local",
  "engine": "python",
  "language_type": "PYTHON",
  "name": "pttest",
  "models": [
    {
      "model_path": "https://tf115test.oss-cn-hangzhou.aliyuncs.com/test/libserving_processor_1226_debug3.tar.gz",
      "model_entry": "",
      "name": "model1",
      "processor": "tensorflow",
      "uncompress": true,
      "model_config": {
        "session_num": 2,
        "use_per_session_threads": true,
        "cpusets": "1,2,3;4,5,6",
        "omp_num_threads": 24,
        "kmp_blocktime": 0,
        "feature_store_type": "memory",
        "serialize_protocol": "protobuf",
        "inter_op_parallelism_threads": 24,
        "intra_op_parallelism_threads": 24,
        "init_timeout_minutes": 1,
        "signature_name": "serving_default",
        "model_store_type": "local",
        "checkpoint_dir": "/data/workspace/ckpt/",
        "savedmodel_dir": "/data/workspace/pb/",
        "oss_access_id": "",
        "oss_access_key": "",
        "oss_endpoint": "oss-cn-shanghai.aliyuncs.com"
      }
    },
    {
      "model_path": "https://tf115test.oss-cn-hangzhou.aliyuncs.com/test/libserving_processor_1226_debug3.tar.gz",
      "model_entry": "",
      "name": "model2",
      "processor": "tensorflow",
      "uncompress": true,
      "model_config": {
        "session_num": 4,
        "use_per_session_threads": true,
        "cpusets": "7-9;10-12;13-15;16-18",
        "omp_num_threads": 24,
        "kmp_blocktime": 0,
        "feature_store_type": "memory",
        "serialize_protocol": "protobuf",
        "inter_op_parallelism_threads": 24,
        "intra_op_parallelism_threads": 24,
        "init_timeout_minutes": 1,
        "signature_name": "serving_default",
        "model_store_type": "local",
        "checkpoint_dir": "/data/workspace/ckpt2/",
        "savedmodel_dir": "/data/workspace/pb2/",
        "oss_access_id": "",
        "oss_access_key": "",
        "oss_endpoint": "oss-cn-shanghai.aliyuncs.com"
      }
    }
  ],
  "processors": [
    {
      "name": "tensorflow",
      "processor_path": "https://tf115test.oss-cn-hangzhou.aliyuncs.com/test/libserving_processor_1226_debug3.tar.gz",
      "processor_entry": "libserving_processor.so",
      "processor_type": "cpp"
    }
  ],
  "metadata":{
    "cpu":32,
    "eas":{
      "enabled_model_verification":false,
      "scheduler":{
        "enable_cpuset": false
      }
    },
    "gpu":0,
    "instance":1,
    "memory":40960,
    "rpc":{
      "io_threads":10,
      "worker_threads":20,
      "enable_jemalloc":true
    }
  },
}

上面配置文件和单模型下的区别在于多了“models”和“processors”字段。

“processors”字段是一个list,用户可以配置多个processor,在models中对于不同的模型可以配置不同的processor。

“models”字段是一个list,用户可以配置多个模型服务,每个模型服务都有单独的配置,主要是“model_config”字段,更详细的字段介绍见:model config

用户Client请求示例:

client = PredictClient('127.0.0.1:8080', 'pttest/model1')
#client = PredictClient('127.0.0.1:8080', 'pttest/model2')
client.init()

pb_string = open("./warm_up.bin", "rb").read()
request = TFRequest()
request.request_data.ParseFromString(pb_string)
request.add_fetch("dinfm/din_out/Sigmoid:0")

resp = client.predict(request)

在构建PredictClient时需要在url增加模型名称,如"pttest/model1"。