# 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](https://github.com/DeepRec-AI/serving/commits/deeprec),这里已经提供了接入SessionGroup的功能。 也可以使用我们提供的[Processor](https://github.com/alibaba/DeepRec/tree/main/serving)代码,Processor没有提供RPC服务框架,通常使用[PAI-EAS](https://www.aliyun.com/activity/bigdata/pai/eas)作为RPC框架。如果用户有特殊需求,亦可自行接入自有RPC框架中。 ### 1.Processor + EAS #### CPU任务 用户在processor上使用session_group,只需要在配置文件中增加如下字段: ```c++ "model_config": { "session_num": 2, "use_per_session_threads": true, ... } ``` #### GPU任务 对于GPU任务,需要做以下一些配置: ```c++ "model_config": { "session_num": 2, "use_per_session_threads": true, "gpu_ids_list": "0,2", ... } ``` 更多参数详见:[processor配置参数](https://deeprec.readthedocs.io/zh/latest/Processor.html#id5) ### 2.Tensorflow serving TFServing使用的是SavedModelBundle进行serving的,相关的代码修改参考:[SessionGroup](https://github.com/DeepRec-AI/serving/commit/8b92300da84652f00f13fd20f5df0656cfa26217),推荐直接使用我们提供的TFServing代码。 支持SessionGroup的TFServing代码见:[DeepRec-AI/serving](https://github.com/DeepRec-AI/serving/commits/deeprec) 编译文档见:[TFServing编译](https://deeprec.readthedocs.io/zh/latest/TFServing-Compile-And-Install.html) #### CPU任务 使用方式如下: ```c++ 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 ``` 主要参数: ```c++ 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进程。 ```c++ nvidia-cuda-mps-control -d ``` ##### 2.启动命令 这里以Tensorflow serving为例(后续补充其他使用方式),在启动server时需要增加下列参数, ```c++ 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/ ``` 上面环境变量详细解释见: [启动参数](https://deeprec.readthedocs.io/zh/latest/SessionGroup.html#id5) TF serving用DeepRec提供的代码: [TF serving](https://github.com/DeepRec-AI/serving/commits/deeprec) ##### 4.多卡MPS最佳实践 用户机器上可能存在多张GPU卡,对于每个serving instance一般只需要使用一个GPU Device,那么用户可能会在物理机器上启动多个不同的serving instance。在这种情况下使用MPS有一些需要注意的问题,具体如下: 1) 需要在物理机器启动mps daemon进程,这样才有机会让所有的任务docker中对于MPS后台进程可见。 ```c++ nvidia-cuda-mps-control -d ``` 2) 启动任务docker时,需要增加--ipc=host,保证在docker中对mps daemon进程可见。同时对于每个docker来说,建议mount指定的GPU Device,如下所示: ```c++ sudo docker run -it --name docker_name --ipc=host --net=host --gpus='"device=0"' docker_image bash ``` 这样在docker只会可见一张GPU卡,并且逻辑编号为0,那么可以像下面这样执行脚本: ```c++ CUDA_VISIBLE_DEVICES=0 test.py 或者 test.py ``` 如果docker mount了所有的GPU Devices,那么在执行脚本的时候,需要手动指定可见的gpu device来达到隔离的效果。 ```c++ 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。 ```c++ TF_RETURN_IF_ERROR(NewSessionGroup(*session_options_, session_group, session_num)); TF_RETURN_IF_ERROR((*session_group)->Create(meta_graph_def_.graph_def())); ``` 参考代码: [Processor](https://github.com/alibaba/DeepRec/blob/main/serving/processor/serving/model_session.cc#L143) #### SessionGroup Run 用户原有代码使用Session::Run可以直接替换为SessionGroup::Run ```c++ status = session_group_->Run(run_options, req.inputs, req.output_tensor_names, {}, &resp.outputs, &run_metadata); ``` 参考代码: [Processor](https://github.com/alibaba/DeepRec/blob/main/serving/processor/serving/model_session.cc#L308) ## 多模型服务 ### TF Serving SessionGroup支持多模型服务,在[TF_serving](https://github.com/DeepRec-AI/serving)上已经支持。对于多模型服务,用户可以对于每个模型服务配置独立的参数,包括不同的模型session group中使用几个session,指定gpu卡,指定线程池等等,从而在框架,资源上进行隔离。 对于GPU任务,目前session group可以指定一张或者多张GPU卡,所以用户在启动多模型任务时,需要注意GPU资源的划分。 启动多模型服务命令如下: ```c++ 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 ``` 上面环境变量详细解释见:[启动参数](https://deeprec.readthedocs.io/zh/latest/SessionGroup.html#id5) 上述命令中最重要的是两个配置文件, 假设机器上有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 ``` 上面环境变量详细解释见: [启动参数](https://deeprec.readthedocs.io/zh/latest/SessionGroup.html#id5) 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](https://deeprec.readthedocs.io/zh/latest/Processor.html#id5) 用户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"。