前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >DAY34:阅读算术指令

DAY34:阅读算术指令

作者头像
GPUS Lady
发布2018-06-22 18:12:57
5700
发布2018-06-22 18:12:57
举报
文章被收录于专栏:GPUS开发者GPUS开发者

5.4.1. Arithmetic Instructions

Table 2 gives the throughputs of the arithmetic instructions that are natively supported in hardware for devices of various compute capabilities.

(扫描二维码看Table 2,注意扫描后要停顿一下表格才会出现哟)

Other instructions and functions are implemented on top of the native instructions. The implementation may be different for devices of different compute capabilities, and the number of native instructions after compilation may fluctuate with every compiler version. For complicated functions, there can be multiple code paths depending on input. cuobjdump can be used to inspect a particular implementation in a cubin object.

The implementation of some functions are readily available on the CUDA header files (math_functions.h, device_functions.h, ...).

In general, code compiled with -ftz=true (denormalized numbers are flushed to zero) tends to have higher performance than code compiled with -ftz=false. Similarly, code compiled with -prec div=false (less precise division) tends to have higher performance code than code compiled with -prec div=true, and code compiled with -prec-sqrt=false (less precise square root) tends to have higher performance than code compiled with -prec-sqrt=true. The nvcc user manual describes these compilation flags in more details.

Single-Precision Floating-Point Division

__fdividef(x, y) (see Intrinsic Functions) provides faster single-precision floating-point division than the division operator.

Single-Precision Floating-Point Reciprocal Square Root

To preserve IEEE-754 semantics the compiler can optimize 1.0/sqrtf() into rsqrtf() only when both reciprocal and square root are approximate, (i.e., with -prec-div=false and -prec-sqrt=false). It is therefore recommended to invoke rsqrtf() directly where desired.

Single-Precision Floating-Point Square Root

Single-precision floating-point square root is implemented as a reciprocal square root followed by a reciprocal instead of a reciprocal square root followed by a multiplication so that it gives correct results for 0 and infinity.

Sine and Cosine

sinf(x), cosf(x), tanf(x), sincosf(x), and corresponding double-precision instructions are much more expensive and even more so if the argument x is large in magnitude.

More precisely, the argument reduction code (see Mathematical Functions for implementation) comprises two code paths referred to as the fast path and the slow path, respectively.

The fast path is used for arguments sufficiently small in magnitude and essentially consists of a few multiply-add operations. The slow path is used for arguments large in magnitude and consists of lengthy computations required to achieve correct results over the entire argument range.

At present, the argument reduction code for the trigonometric functions selects the fast path for arguments whose magnitude is less than 105615.0f for the single-precision functions, and less than 2147483648.0 for the double-precision functions.

As the slow path requires more registers than the fast path, an attempt has been made to reduce register pressure in the slow path by storing some intermediate variables in local memory, which may affect performance because of local memory high latency and bandwidth (see Device Memory Accesses). At present, 28 bytes of local memory are used by single-precision functions, and 44 bytes are used by double-precision functions. However, the exact amount is subject to change.

Due to the lengthy computations and use of local memory in the slow path, the throughput of these trigonometric functions is lower by one order of magnitude when the slow path reduction is required as opposed to the fast path reduction.

Integer Arithmetic

Integer division and modulo operation are costly as they compile to up to 20 instructions. They can be replaced with bitwise operations in some cases: If n is a power of 2, (i/n) is equivalent to(i>>log2(n)) and (i%n) is equivalent to (i&(n-1)); the compiler will perform these conversions if n is literal.

__brev and __popc map to a single instruction and __brevll and __popcll to a few instructions.

__[u]mul24 are legacy intrinsic functions that no longer have any reason to be used.

Half Precision Arithmetic

In order to achieve good half precision floating-point add, multiply or multiply-add throughput it is recommended that the half2 datatype is used. Vector intrinsics (eg. __hadd2, __hsub2,__hmul2, __hfma2) can then be used to do two operations in a single instruction. Using half2 in place of two calls using half may also help performance of other intrinsics, such as warp shuffles.

The intrinsic __halves2half2 is provided to convert two half precision values to the half2 datatype.

Type Conversion

Sometimes, the compiler must insert conversion instructions, introducing additional execution cycles. This is the case for:

· Functions operating on variables of type char or short whose operands generally need to be converted to int,

· Double-precision floating-point constants (i.e., those constants defined without any type suffix) used as input to single-precision floating-point computations (as mandated by C/C++ standards).

This last case can be avoided by using single-precision floating-point constants, defined with an f suffix such as 3.141592653589793f, 1.0f, 0.5f.

本文备注/经验分享:

今天就到了计算方面了。 其实一张GPU的主要性能或者说力量, 就在于访存和计算上.不仅仅显存的带宽远远超过内存, 计算上也性能摇摇领先, 但还是有一些需要注意的地方。 table 2 这个表是原生(native)指令的吞吐率表. 请注意, 每一代的卡, 随着时代的进步和侧重点的不同, 你会发现虽然特性上所有的运算都基本能支持, 但是它们的性能在每张卡上可能会不同.(例如double, 例如half) 这样, 挑选什么样的卡, 可以参考一下此表. 例如你可以从此表看出, 6.1的Pascal(例如GTX1080), 单精度性能是128指令每SM每周期, 但是double, 却只有4指令/SM/周期,也就是double性能对于6.1的卡只有1/32,你就知道6.1的1080这种卡, 不适合双精度运算.因为它的双精度性能只有单精度性能的3%左右(1/32) 而你会发现, 6.0的卡(GP100), 却具有32 / 64 = 50%的相对峰值单精度性能的双精度性能, 因此这卡适合双精度运算(目前最高的双精度就是50%性能--基准是用的单精度性能) 类似的, 你会看到其他性能指标. 一个很典型的历史变迁就是half,随着深度学习的越来越重要, half性能总是不断在提升,此表指出, 一些老卡, 根本就不支持half(例如Kepler, 例如5.0, 5.2(这两个是非嵌入式的Maxwell),而到了5.3(这是TX1)和Pascal以后, 所有的卡都添加了half支持.这样, 深度学习应当考虑新卡. 需要这里half指出的一点是:6.1虽然是新卡, 但half只有兼容性支持.7.0虽然这里列出的是128(200%相比它的64的单精度性能), 这是只算了SP的. TensorCore还能额外贡献大约800%的half(以及部分FP32加法)性能. 类似的, 对深度学习这表没有整理出来的是, INT8(DP4A)性能, 6.1的卡应当是400%. 7.0暂时未知.所以用来推导, 6.1的Pascal还是很好的卡, 又便宜又好。 其次这个表格需要注意的是, 32-bit整数的性能. 这里有个有趣的数字, 对于Kepler, 32-bit整数加法, 和哪怕逻辑运算, 都不是满速率的(160/192 = 83%),这说明Kepler里面的192个SP纯粹是被设计成容易打酱油的(实测的基本整数操作的性能, Kepler甚至连这个还不如, 实测往往在66%左右)。 正因为Kepler里面的SP虽然数量巨多, 但是不容易发挥性能(实际上, 它的标称100%速率的float也不容易发挥出来性能的), 所以看上去一张几千个SP的Kepler卡,从现在的角度来说, 基本不值得购买(除了K80),用NV在Maxwell优化指南里面的一句话说:Maxwell(也包括Pascal)的一个128 SP的SM,基本上等于一个Kepler的192 SP的SM,两者性能差距在10%以内等等.(居然又吐槽了一次Kepler)。

此外, 这个表格实际上不用全看, 自己从事的行业需要侧重用什么,就看什么方面的性能,例如我在做扩展精度的大整数乘法, 应当考虑这里的整数乘法之类, 和整数加法之类的性能,例如我侧重挖矿, 就应当考虑32-bit逻辑运算, 移位, 类似这种的性能。Maxwell/Pacsal+针对挖矿进行了单独优化, 这里的32-bit逻辑运算写着是100%性能, 但实际上是200%性能(需要特定的情况下, 编译器能综合出来特定的指令)。Maxwell+可以直接实现D = A ^ B ^ C这种三源操作数的按位逻辑操作(精确的说, 可以实现任何3输入的组合逻辑, 在一个指令里),类似的, 大家还能看到32-bit shift的性能提升,从maxwell+,现在普通移位, 循环移位等等, 都是50%速率的了. 类似这些. 针对Maxwell/Pascal+的额外一点提示是,很多半速率的指令(例如刚才的移位或者整数比较之类的操作), 在特定的指令配比下, 可以变身成100%速率的.这是因为从Maxwell起, SP被分配成固定的4组 * 32个 = 128个,每个周期每组SP会接受同1条指令, 一旦下一条指令不和本条指令竞争具体的执行单元(例如移位单元, 下一条指令是整数加法), 那么本条指令就能等效的变成全速率的.这点实际上往往导致maxwell和Pascal具有令人惊讶的性能(比你手工计算出来的理论性能峰值还好),而AMD的GCN卡采用了类似的结构, 但是4 * 16 = 64个SP每个CU里面, 会每组SP被连续给出4条同样指令,因此不能享受到这个福利。 因此, maxwell除了双精度不好, 和没有half/INT8支持, 其他均是一张好卡.而从Pascal起, 开始补充了double, half, INT8这些短板,同时Maxwell有的特性它都有, 因此几乎开始变成了全能的卡, 无论你是科学计算, 还是深度学习, 还是挖矿.此外, 此表的需要说明的是, maxwell的32-bit整数乘法, 其实是标准的1/4速率的(和A卡一样), 并不存在任何性能短板,这个性能比此表中的前一个(Kepler)的Single Instruction的性能实际上还要好。Pascal类似(Pascal完全在这里和Maxwell一样) 虽然这里标记的是"Multiple Instructions", 但这是有其他原因.感兴趣的可以参考一下网上的关于Maxwell/Pascal的XMAD指令(16-bit * 16-bit + 32-bit -> 32-bit整数操作, 全速率),因此maxwell在纯整数操作, 例如大整数或者超过64-bit的任意精度浮点运算,均具有良好性能(Pascal完全一样), 此外, 手册说明了部分函数具有快速版本,快速版本性能高, 但是精度低.在不需要太高精度的场合, 应当考虑低精度但更快速的版本的.这些快速版本往往也叫原生版本, 或者叫intrinsic函数, 往往具有两个__开头,用户应当注意这些。例如举个例子说, 在特定范围内的正弦(单精度版本)函数,快速版本到了最后的SFU(特殊功能单元)里面的计算一次sin后就了结,而高精度版本的后面还跟随有牛顿迭代,后续的迭代是用的SP软件算的, 因为会有较低性能---但有较高的精度。

继续谈一下这章说的较慢的高精度的数学运算和较快的低精度之间的取舍问题.其实手册后面有个表, 大致是各种运算的误差情况.里面有快速版本和高精度版本的误差比较, 单位是ULP (ULP是用最低有效位做单位的, 不过这个可以看成是某种相对于值本身的相对误差, 例如一个24-bit有效位的float值, 在正常的情况下, 一个ULP误差大约相当于,这个数的值本身的1/2^24这个级别。而double的一个ULP的误差可能大约相当于这个值的本身的1/2^53左右的误差.

例如这样的. 注意NV这里的0误差应当精确的说, 是小于0.5个ULP(下一个位, 1/2了, 因为是二进制么)的误差, 不过这已经是最精确的结果了。 从这个表可以看出, 哪怕使用了原生版本的函数或者指定了不需要高精度, 实际上的结果的精度也还是非常高的.因此一般不用担心。 此外, 需要说明的是, N卡长期提供FMA,可以对d = a * b + c的中间结果,也就是(a * b) + c这一步, 根据NV的文档(其他地方能看到, 不在这本里), 说,中间结果....取....无限精度....而AMD的说法是:对d = a * b + c(都是float)的计算,中间结果....提升精度到...double精度,虽然这个对float的融合乘加操作(FMA)的文档描述不能说明什么,但是NV既然能这样写, 证明对精度也许比AMD更有信心.类似的, 实际上用户可以分别尝试本章给出的标志, 看看结果上的变化.但因为CUDA本身的并行化, 和基本从一开始就有FMA操作(CPU是这两年才开始普及的FMA),一般情况下, 很多时候GPU和CPU的结果不同, 往往是GPU的结果更加正确(和手工计算的精确结果相比),也就是, 目前N卡算的又快又好.而并非是很多人想象中的, CPU结果更正确. 类似的, 本章节还说了一些其他方面, 例如a / N (N是2的幂)可以用移位来取代.这个如果N在编译时刻可知的常数.现在的CUDA编译器会自动发现这点, 不需要手工操作了,类似的, 还提到了一些类似__popc之类的函数,这些其实都很有用, 例如在prefix-sum或者压缩一个list去掉其中的空位之类的算法的时候. 最后说一下__umul24这种,这个是以前的1.X时代提供的24-bit * 24-bit的乘法.曾经在很多老代码中可以看到能大量的使用它,当年GPU提供它是因为它可以几乎免费的实现.(可以重用float的浮点运算中, 移位对齐后面的乘法电路),Fermi和Kepler放弃了它, 改成单独实现的32-bit乘法.而Maxwell/Pascal则提供了16-bit的版本, 依然可以重用float的运算电路.也就是之前的说的XMAD,现在的硬件上, 任何整数乘法应当直接写出,编译器会自动综合的. (可惜现在直到CUDA 9, 编译器对整数乘法的综合效果依然不好,但依然可以秒杀AMD的OpenCL编译器. AMD的OpenCL编译器在这方面直接就是弱智)

有不明白的地方,请在本文后留言

或者在我们的技术论坛bbs.gpuworld.cn上发帖

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-06-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 GPUS开发者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 5.4.1. Arithmetic Instructions
    • Single-Precision Floating-Point Division
      • Single-Precision Floating-Point Reciprocal Square Root
        • Single-Precision Floating-Point Square Root
          • Sine and Cosine
            • Integer Arithmetic
              • Half Precision Arithmetic
                • Type Conversion
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档