============================================

 

 

前言废话:

自己从接触CUDA开始算起至今已有近十年的历史,最初那还是在intel赛扬的那个年代。虽然接触的早但一直没有深入的了解CUDA的一些底层原理,对CUDA的基本应用也只是了解过一些简单的DEMO,后来CUDA运算变得比较冷门,如果不是搞异构超算的估计也不太会接触CUDA了。不过随着深度学习hot起来,cuda编程又有了新的浪潮,想想十多年前最早看的CUDA的demo代码就是全连接神经网络的训练代码,想想这也是神经网络再次带火了CUDA。

 

 

最近在看到了一个关于GPU的使用率的一个说明:​​https://zhuanlan.zhihu.com/p/346389176​​,突然想对这个GPU的使用率有个小的总结。

 

 

 

------------------------------------------------------------------------

 

 

 

引用 ​​https://zhuanlan.zhihu.com/p/346389176​​中给出的GPU利用率的解释:

 

NVIDIA显卡的利用率“Volatile GPU Util_流处理

 

 

------------------------------------------------------------------------

 

 

根据上面对GPU使用率的解释,我们知道GPU的使用率就是计算一定周期时间内GPU是否被kernel函数占用。如果在这段时间内GPU一直被kernel函数占用,那么GPU的使用率为100%;如果在这段时间内有50%的时间有kernel在运行那么GPU的使用率为50% 。可以看到GPU的利用率是根据kernel在采样周期内运行的时长来计算的,并不是根据计算核心被使用的多少来计算的。了解GPU的CUDA原理的都知道一个kernel往往并不能利用整块GPU的所有流处理器,所以使用kernel占用的时间并不能完全体现出GPU的使用率。但是这不是说明现在所使用的GPU利用率的计算方法就有很大问题呢,其实也不尽然,这种GPU利用率也是有其无奈的地方的。GPU的kernel往往是独占一整块显卡的,也就是说如果没有开mps服务、没有写多kernel多队列并加参数编译的话一个时刻上只能运行一个kernel(​​CPU端多进程/多线程调用CUDA是否可以加速??? ​​),而如果计算采样周期内不同时刻下流处理器的使用个数也是在技术上难以实现的,也就是说GPU使用率的计算方法为啥采样如此不完善的计算方法主要是因为该种方法在技术可行。

 

知道了GPU利用率的计算方法我们也就知道了这样一个事情,那就是GPU利用率并不能完全体现出GPU的载荷情况,GPU利用率高的时候其实GPU的载荷未必高,但是GPU的载荷高的时候GPU的利用率必然高。

 

为了更好的体现出GPU的利用率和载荷的不匹配现象,给出一个轻载荷的代码:

const int N = 1 << 30;

__global__ void kernel(float *x, int n)
{
int tid = threadIdx.x + blockIdx.x * blockDim.x;
for (int i = tid; i < n; i += blockDim.x * gridDim.x) {
x[i] = sqrt(pow(3.14159,i));
}
}

int main()
{
const int num_streams = 8;

cudaStream_t streams[num_streams];
float *data[num_streams];

for (int i = 0; i < num_streams; i++) {
cudaStreamCreate(&streams[i]);

cudaMalloc(&data[i], N * sizeof(float));

// launch one worker kernel per stream
kernel<<<1, 32, 0, streams[i]>>>(data[i], N);

// launch a dummy kernel on the default stream
kernel<<<1, 1>>>(0, 0);
}

cudaDeviceReset();

return 0;
}

 

 

编译并执行:

export CUDA_VISIBLE_DEVICES=1

nvcc ./x.cu -o x

time ./x

NVIDIA显卡的利用率“Volatile GPU Util_多线程_02

 

 

 

 

 

可以看到这个cuda代码的kernel就是在执行一个32线程的计算任务,对于有几千流处理器的nvidia显卡来说这个计算任务是十分小的任务了,这里运行的环境为2070Super显卡,硬件配置:

NVIDIA显卡的利用率“Volatile GPU Util_多线程_03

 

 

 

 

可以看到2070super显卡共有2560个cuda cores,也就是有2560个流处理器,那么上面的代码运行起来的话其实整个GPU的运算载荷为32/2560=1/80,可以看到上面的代码运行起来载荷是如此的低,但是使用nvidia-smi 命令查看GPU的利用率就可以看到其数值为100% (如下图)。

NVIDIA显卡的利用率“Volatile GPU Util_流处理_04

 

NVIDIA显卡的利用率“Volatile GPU Util_多线程_05

 

---------------------------------------

 

 

 

 

上面的代码Demo说明的就是一个占GPU载荷1/80的cuda进程在nvidia-smi命令中显示出此时对GPU的利用率可以高达100% ,很显然GPU的利用率难以真实显示出GPU的载荷情况。

 

 

PS: 既然分析了GPU使用率难以完整体现出GPU的负载情况,那么有没有其他的方法来辅助呢,那就是分享一下个人的使用经验,在观察GPU使用率的同时也要注意观察其他的三个指标:风扇转速、显卡温度、供电水平(工作的功率也可以考虑)。 显卡的供电(功率)和温度是对显卡负载比较直观的体现,而且这种体现往往比看GPU使用率更加的靠谱,就比如上面的例子,从这几个辅助的指标上很容易看出显卡的负载并不像使用率100%显示的那么高。

 

 

---------------------------------------------

 

 

我们可以适当修改代码使其保持同样的负载但是缩小它的计算时间以减少显存占用,然后在同块显卡上同时运行两次这个代码:

代码:

const int N = 1 << 26;

__global__ void kernel(float *x, int n)
{
int tid = threadIdx.x + blockIdx.x * blockDim.x;
for (int i = tid; i < n; i += blockDim.x * gridDim.x) {
x[i] = sqrt(pow(3.14159,i));
}
}

int main()
{
const int num_streams = 8;

cudaStream_t streams[num_streams];
float *data[num_streams];

for (int i = 0; i < num_streams; i++) {
cudaStreamCreate(&streams[i]);

cudaMalloc(&data[i], N * sizeof(float));

// launch one worker kernel per stream
kernel<<<1, 32, 0, streams[i]>>>(data[i], N);

// launch a dummy kernel on the default stream
kernel<<<1, 1>>>(0, 0);
}

cudaDeviceReset();

return 0;
}

View Code

 

单个运行:

NVIDIA显卡的利用率“Volatile GPU Util_采样周期_06

 

 

 

两个同时运行:

NVIDIA显卡的利用率“Volatile GPU Util_CUDA_07

NVIDIA显卡的利用率“Volatile GPU Util_采样周期_08

 

 

 

可以看到虽然上面的代码运算时kernel只使用了1/80的CUDA CORES,但是由于kernel一直处于运行状态,因此GPU的使用率为100%,同时由于每个进程的kernel在没有mps服务情况下都是排队执行的,因此两个进程运行CUDA的时间为单个进程运行的两倍。

 


 

 

============================================

 

 

 

参考:

​https://zhuanlan.zhihu.com/p/346389176​