cuda half编程的各种坑
Posted yutianzuijin
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了cuda half编程的各种坑相关的知识,希望对你有一定的参考价值。
自cuda7.5开始我们可以直接用half(fp16)编程,理论上速度会比float快一倍左右。理想虽好,现实却比较骨感,在实际中会遇到各种坑,最终的结果却是不一定有收益,下面把自己在用half编程中踩过的坑记录一下。
1. half编程和计算能力密切相关
half编程要求GPU的计算能力要大于等于5.3,这就意味着大家很多GPU不支持此功能。例如,GTX 1050之前的GPU全不支持half计算,此外Tesla K和M系列也不支持half计算。可以通过该网页查看自己GPU的计算能力。即使我们的GPU支持half计算,性能也不一定高,当计算能力为6.1的时候,GPU的half计算能力几乎为0。对比不同计算能力的吞吐量我们可以发现当计算能力为6.1时,其16bit的算术运算能力只有2,显著小于其他计算能力。注意,此处并不是写错了,而确实就是如此,英伟达官方也给出了解释:
It’s not an error.
The design of the cc6.1 SM is different from the design of the cc6.0/cc6.2/cc7.x/cc5.3 SM in this respect.
The throughput of FP16 on cc6.1 is relatively low. The reason for the existence of such a low throughput capability is for application compatibility. It is not a performance path on cc6.1
Note that for parameter storage (as opposed to compute throughput) FP16 could still possibly be a “win” on cc6.1, in some cases, where memory traffic drives application performance. The 2:1 ratio of parameter storage density over FP32 means that in some cases it may be beneficial to store data in (packed) FP16 format, but convert on the fly to FP32 (for calculations) and back to FP16 (for storage). This assumes your app/algorithm can tolerate the numerical implications of parameter storage in FP16.
简单解释就是为了程序兼容性阉割了half计算能力。比较不幸的是,估计大家目前还在使用的GPU大部分还是计算能力6.1的,比如Tesla P4/P40,GTX 1080,TITAN X/Xp等。如果你还在用这些GPU就放弃通过使用half加速代码的想法吧。
2. float转half的位置和cuda版本相关
我们在代码中应用half的场景基本如下:在host端将float模型或者特征转换为half,然后将half模型或者特征传输到device端进行计算,计算完成后将half结果再传递到host端,最后在host端将half转换成float。但是在cuda9.2之前居然不支持这么做:因为在host端没有float2half函数,该函数只能在device端执行!这个设计真是有点反人类啊。如果你想在host端完成类型转换,请自行搜寻开源代码,给大家提供一个host端的float2half:
#define F16_EXPONENT_BITS 0x1F
#define F16_EXPONENT_SHIFT 10
#define F16_EXPONENT_BIAS 15
#define F16_MANTISSA_BITS 0x3ff
#define F16_MANTISSA_SHIFT (23 - F16_EXPONENT_SHIFT)
#define F16_MAX_EXPONENT (F16_EXPONENT_BITS << F16_EXPONENT_SHIFT)
inline half F32toF16(float val)
uint32_t f32 = (*(uint32_t *) &val);
uint16_t f16 = 0;
/* Decode IEEE 754 little-endian 32-bit floating-point value */
int sign = (f32 >> 16) & 0x8000;
/* Map exponent to the range [-127,128] */
int exponent = ((f32 >> 23) & 0xff) - 127;
int mantissa = f32 & 0x007fffff;
if (exponent == 128)
/* Infinity or NaN */
f16 = sign | F16_MAX_EXPONENT;
if (mantissa) f16 |= (mantissa & F16_MANTISSA_BITS);
else if (exponent > 15)
/* Overflow - flush to Infinity */
f16 = sign | F16_MAX_EXPONENT;
else if (exponent > -15)
/* Representable value */
exponent += F16_EXPONENT_BIAS;
mantissa >>= F16_MANTISSA_SHIFT;
f16 = sign | exponent << F16_EXPONENT_SHIFT | mantissa;
else
f16 = sign;
return *(half*)&f16;
在device端有__float2half和__half2float两个函数可以完成类型转换。不过这两个函数是对单个变量完成转换,要想device端实现对矩阵的转换还需要自己写kernel。
3. 基础运算需要利用intrinsic指令完成
float类型可以直接用+、-、*、/完成基本的数学运算,但是对half类型,我们必须用half相关的intrinsic指令完成。指令类型包括:基本算术运算、比较运算、类型转换运算和math运算等。而且比较恶心的是,基本算术运算指令前面需要将__,而math运算指令却又没有__,非常不统一。用intrinsic指令的坏处就是代码变长,开发略微复杂。
4. cublas中没有gemv相关的half函数
为了加速half类型的矩阵乘法,cublas中提供了cublasHgemm和cublasGemmEx函数,但是却没有提供level2相关的矩阵向量乘法cublasHgemv函数,gemv只有float和double版。导致我们只能手写kernel实现gemv或者用gemm代替,但是这两种方案都会使half性能大大折扣。尤其是在实现LSTM层的时候,其内部循环中包含两次gemv操作,结果导致half版本比float版本慢很多。所以当你想用half来加速LSTM模型,也请放弃幻想。
5. cublasHgemm不一定快
在计算能力大于6.1的GPU上,实现half矩阵乘法首先想到用cublasHgemm,但是在实测中发现该函数不一定比cublasSgemm快。如果出现这种情况,请试一下cublasGemmEX函数。本人在T4上验证的结论是:cublasHgemm运行速度比cublasSgemm还要慢,但是换用cublasGemmEX就会比cublasSgemm快。
6. half计算容易溢出
由于half位数较短,表示的数范围很小,大约在-65535~65536之间,所以在运算过程中比较容易溢出。比如在行归一化操作中,我们要求一行数据的平方和,如果行中出现较大值就极易出现溢出。此时就要求我们不能直接照搬float代码,而需要做适当变换,避免溢出。在行归一化中,我们可以通过在求平方和的过程中除以行数来避免溢出。
以上是关于cuda half编程的各种坑的主要内容,如果未能解决你的问题,请参考以下文章
ubuntu16.04安装CUDA 8.0(很详尽,包括一些坑的解决方法)