当前位置: 首页 > news >正文

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代码,而需要做适当变换,避免溢出。在行归一化中,我们可以通过在求平方和的过程中除以行数来避免溢出。
 

相关文章:

  • VLC减少延迟的方法
  • Oracle 技术高峰论坛 2007华章现场亲情赠书!
  • ESXI 6.7 环境 centos7.6 虚拟机安装tesla k80 显卡驱动失败问题解决
  • OWC绘图控件研究(1)
  • 升级到 Kubernetes v1.16 须知API问题总结
  • OWC绘图控件研究(2)
  • K8S pod异常状态处理
  • UPS FedEx DHL TNT
  • k8s 安装helm2 和 helm3
  • Happy Feet
  • Cython的基本用法
  • 计算字段 VS 视图
  • k8s secret 详细理解和使用
  • k8s中的kubeflow1.02安装过程记录
  • 在struts中html:select 标签的disabled属性中使用java代码
  • 【node学习】协程
  • Android单元测试 - 几个重要问题
  • java 多线程基础, 我觉得还是有必要看看的
  • JDK 6和JDK 7中的substring()方法
  • Mysql数据库的条件查询语句
  • NLPIR语义挖掘平台推动行业大数据应用服务
  • nodejs:开发并发布一个nodejs包
  • node入门
  • spring cloud gateway 源码解析(4)跨域问题处理
  • XForms - 更强大的Form
  • zookeeper系列(七)实战分布式命名服务
  • 分布式熔断降级平台aegis
  • 服务器从安装到部署全过程(二)
  • 聊一聊前端的监控
  • 马上搞懂 GeoJSON
  • 配置 PM2 实现代码自动发布
  • 因为阿里,他们成了“杭漂”
  • 用Canvas画一棵二叉树
  • 用jQuery怎么做到前后端分离
  • 【运维趟坑回忆录】vpc迁移 - 吃螃蟹之路
  • ​520就是要宠粉,你的心头书我买单
  • (LNMP) How To Install Linux, nginx, MySQL, PHP
  • (附源码)python房屋租赁管理系统 毕业设计 745613
  • (七)Java对象在Hibernate持久化层的状态
  • (三)uboot源码分析
  • (十八)devops持续集成开发——使用docker安装部署jenkins流水线服务
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • (转)chrome浏览器收藏夹(书签)的导出与导入
  • (转)LINQ之路
  • (转)一些感悟
  • .gitignore文件---让git自动忽略指定文件
  • .gitignore文件设置了忽略但不生效
  • .net core IResultFilter 的 OnResultExecuted和OnResultExecuting的区别
  • .NET LINQ 通常分 Syntax Query 和Syntax Method
  • .NET 回调、接口回调、 委托
  • .NET 线程 Thread 进程 Process、线程池 pool、Invoke、begininvoke、异步回调
  • .NET6实现破解Modbus poll点表配置文件
  • .NET简谈设计模式之(单件模式)
  • .net企业级架构实战之7——Spring.net整合Asp.net mvc
  • .NET下ASPX编程的几个小问题