最近读到这么一篇文章 Automatic Multi-Device Inference with Intel® Distribution of OpenVINO™ toolkit ,号称使用CPU/GPU协同运算做推理,可以大幅度提高推理能力。

以mobilenet-ssd为例,文中附上了一个性能数据对比

opencv cpu占用率高 opencv intel gpu_句柄

CPU/GPU一起推理后的性能相对只用CPU推理,性能提高了到了0.79/0.64=1.234 也就是提高了大约23.4%

下面我们来实测一下

 

所谓的混合模式,实际就是在step 7的时候,调用ie.LoadNetwork时参数device_name传进去的参数为"MULTI:CPU, GPU", 这样LoadNetwork会把模型数据同时加载进CPU, GPU或者其他指定的硬件里。

// ----------------- 7. Loading the model to the device --------------------------------------------------------
        next_step();

        std::map<std::string, std::string> config = {{ CONFIG_KEY(PERF_COUNT), perf_counts ? CONFIG_VALUE(YES) :
                                                                                             CONFIG_VALUE(NO) }};
        startTime = Time::now();
        ExecutableNetwork exeNetwork = ie.LoadNetwork(cnnNetwork, device_name, config);
        duration_ms = float_to_string(get_total_ms_time());
        slog::info << "Load network took " << duration_ms << " ms" << slog::endl;
        if (statistics)
            statistics->addParameters(StatisticsReport::Category::EXECUTION_RESULTS,
                                      {
                                          {"load network time (ms)", duration_ms}
                                      });

首先是FP32模型, 当Batch size =1时

  • benchmark_app设置CPU_THROUGHPUT_STREAMS = CPU_THROUGHPUT_AUTO,GPU_THROUGHPUT_STREAMS = GPU_THROUGHPUT_AUTO时,得到openvino建议的CPU的nstream数为4, GPU的nstream数为2, 下面对应的number of ireq并发数为8 , 即同时并发8个推理请求

opencv cpu占用率高 opencv intel gpu_数据_02

此时FPS为102, 相对与CPU的97FPS, 性能提升并不大,同时可以看到统计8路infer request的latency时间明显不一样,有4路比较慢,有4路比较快,速度基本相差1倍左右,应该是CPU和GPU推理的速度不同;同时也可以看出,8个infer request推理的时间也是动态变化的,并不是某个infer request句柄就是固定对应的CPU推理,某个句柄就是固定对应的GPU推理。

 

接下来是FP16模型, 当Batch size =1时

  • benchmark_app设置CPU_THROUGHPUT_STREAMS = CPU_THROUGHPUT_AUTO,GPU_THROUGHPUT_STREAMS = GPU_THROUGHPUT_AUTO时,得到openvino建议的CPU的nstream数为4, GPU的nstream数为2, 下面对应的number of ireq并发数为8 , 即同时并发8个推理请求

opencv cpu占用率高 opencv intel gpu_句柄_03

114FPS, 好于FP32模型推理,应该是GPU部分节省了一部分内存带宽的开销导致性能提升。但是和纯GPU FP16模型推理的133FPS还差很多。

为了找原因,祭出一个免费大杀器 Intel® Graphics Performance Analyzers, 这个工具可以实时看到CPU,GPU里各种资源占用的情况。先看看默认的CPU_THROUGHPUT_STREAMS = CPU_THROUGHPUT_AUTO,GPU_THROUGHPUT_STREAMS = GPU_THROUGHPUT_AUTO模式, 这里的cpu:4 gpu:4指的是cpu有4路并发,gpu有4路并发,一共8路

opencv cpu占用率高 opencv intel gpu_opencv cpu占用率高_04

可以看到推理时 CPU已经100%耗干了,但是GPU的频率还在500Hz左右晃悠,同时占用率也就在50%左右。说明推理计算的主力还在CPU这边,GPU只是抽空来干点活,不知道是内存带宽不够还是没有CPU资源给GPU喂数据了,导致基本半闲置状态。所谓的性能提升从CPU FP32 97FPS到Multi FP32 102FPS, 也就是GPU帮忙干了点活;同时Multi FP16的114FPS 高于Multi FP32的102FPS, 也就是因为GPU在FP16计算的效率更高而已。

 

再试试把并发数降下来,从1路推理,到2路推理并发再到4路推理并发,GPA输出如下

opencv cpu占用率高 opencv intel gpu_句柄_05

非常有趣,可以看到随着并发数的增加,CPU的占用率逐渐升高,GPU一直在闲置

 

再增加并发数

opencv cpu占用率高 opencv intel gpu_句柄_06

当并发数从5开始,GPU开始参与到推理计算里。

所以可以基本认为,使用MULTI:CPU, GPU参数,并且全部使用XXX_THROUGHPUT_AUTO参数时,推理引擎的调度逻辑是先把CPU的资源跑满(并发<=4路)再开始使用GPU资源(并发>4路)

 

这在实际使用中是一个很麻烦的事情,我们没法知道当前创建的多个infer request的句柄哪个是对应CPU,哪个是对应GPU,完全是IE引擎自己控制

for (int i = 0; i < nireq; i++)
	{
		inferRequest[i] = exeNetwork.CreateInferRequest();
	}

所以只能想办法减少CPU infer request的数量,比如通过手工设置cpu_nstream的数量,先试试cpu nireq = 1, gpu nireq =4, FP16模型

// ----------------- 6. Setting device configuration -----------------------------------------------------------

		ie.SetConfig({ { CONFIG_KEY(CPU_BIND_THREAD), "NO" } }, "CPU");
		std::cout << " CPU_BIND_THREAD :" << ie.GetConfig("CPU", CONFIG_KEY(CPU_BIND_THREAD)).as<std::string>() << std::endl;
		
		// for CPU execution, more throughput-oriented execution via streams
		//ie.SetConfig({ { CONFIG_KEY(CPU_THROUGHPUT_STREAMS),"CPU_THROUGHPUT_AUTO" } }, "CPU");
		ie.SetConfig({ { CONFIG_KEY(CPU_THROUGHPUT_STREAMS),std::to_string(1) } }, "CPU");
		cpu_nstreams = std::stoi(ie.GetConfig("CPU", CONFIG_KEY(CPU_THROUGHPUT_STREAMS)).as<std::string>());
		std::cout << "CPU_THROUGHPUT_AUTO: Number of CPU streams = " << cpu_nstreams << std::endl;

		//for GPU inference
		//ie.SetConfig({ { CONFIG_KEY(GPU_THROUGHPUT_STREAMS),"GPU_THROUGHPUT_AUTO" } }, "GPU");
		ie.SetConfig({ { CONFIG_KEY(GPU_THROUGHPUT_STREAMS),"4" } }, "GPU");
		gpu_nstreams = std::stoi(ie.GetConfig("GPU", CONFIG_KEY(GPU_THROUGHPUT_STREAMS)).as<std::string>());
		std::cout << "GPU_THROUGHPUT_AUTO: Number of GPU streams = " << gpu_nstreams << std::endl;

		ie.SetConfig({ { CLDNN_CONFIG_KEY(PLUGIN_THROTTLE), "1" } }, "GPU");
		std::cout << "CLDNN_CONFIG_KEY(PLUGIN_THROTTLE), 1" << std::endl;

 

GPA输出

opencv cpu占用率高 opencv intel gpu_性能提升_07

效果不是一般的好,GPU和CPU都跑满,FPS到了139

 

再增大cpu_nireq到2, gpu_nireq还是4路,总共6路并发

opencv cpu占用率高 opencv intel gpu_opencv cpu占用率高_08

效果还不如上面的5路并发,可能是CPU只能做效率低下的FP32的推理,过多的CPU推理反而会拉低FPS, 同时可以看到红框部分有一个明显的帧率下降,这时候可以听到CPU的风扇开始狂响,应该是开始过热了。

 

经过反复测试并发数,在我这个4核CPU的笔记本上,测到了一个最高的值

opencv cpu占用率高 opencv intel gpu_数据_09

FP16, CPU 1路并发, GPU 2路并发,最高可以到150FPS。看来堆数量不是重点,简单高效才是好

 

另外一个提高效率的方法是让IE引擎优先调度GPU资源,然后再CPU,具体的方法是调用ie.LoadNetwork时参数device_name传进去的参数为"MULTI:GPU, CPU"  并发数量上还是多GPU办事,少CPU填缝的原则

opencv cpu占用率高 opencv intel gpu_opencv cpu占用率高_10

 

简单总结一下,OpenVINO的CPU/GPU混合推理

  1. 资源分配上优先使用GPU,CPU只用来做一些锦上添花的补充
  2. 尽量少用CPU,以免造成CPU过热引起的性能下降
  3. 尽量使用FP16模型,因为GPU喜欢
  4. 合理使用MULTI:CPU,GPU 和 MULTI:GPU,CPU参数来改变调度优先级
  5. 混合推理时,每路推理的完成时间会很不一样,有可能出现严重的后发先至或者先发后至。所以处理推理结果时候会更麻烦
  6. 在目前我看到的使用集成显卡的处理器上,尽量不要尝试CPU/GPU混合推理,首先是CPU/GPU是共享内存带宽,对于那些内存带宽密集型的模型性能提升不大,有2辆拉货的车,但是是单车道马路,只能大家排队一个一个走;其次是CPU和集成显卡共享TDP,处理器本身的TDP要被平均分开给CPU和集成显卡,一个硬件用的功耗大了,另一个硬件势必分配的功耗就会低,实际上有劲使不上。