CUDA Instruction (PTX SASS) - yszheda/wiki GitHub Wiki

PTX

PTX 9.x

SASS

Control codes


Stall Counts

  • Stalling 0 means to dual issue that instruction with the following instruction. This is only legal for instructions using two different chip resources (like a cuda core and a memory unit).

Yield Hint Flag

Write Dependency Barriers

Variable latency instructions fall into two categories:

  1. memory operations
  2. shared resource operations like double precision ops, special function unit ops, or other low throughput ops.

Since the latency is variable we can't reliably use a stall count to wait for the results. Instead we set and wait on a barrier.

Barriers take one additional clock cycle to become active on top of the clock consumed by the instruction doing the setting. So you may need to also use a stall count of 2 if you intend to wait with the subsequent instruction.

Read Dependency Barriers

This type of barrier is used for instructions that don't use an operand collector and hence don't buffer their operands (mainly memory ops). These instructions also operate at variable latency and hence to avoid write after read hazards we need to set and wait on a barrier before we can overwrite the operand values.

Wait Dependency Barrier Flags

Predicated Execution and Dependency Barriers



predicate

branch在出现divergence的时候,内部也有一个mask,表明当前这个thread是否active,但是用户不能直接修改这个mask。PTX中可以通过warp vote或是load特殊寄存器%lanemask_*之类的方法获得当前warp内的mask情况。

Opcode 和 Opcode Modifier

Float指令

没有直接的除法指令。浮点除法开销很大,x/y的近似算法是用x * rcp(y)来算的。精确算法一般是需要rcp(y)得到初值后,进行多步迭代。所以浮点数除法是比较慢的操作。

Integer指令

逻辑操作指令:现在多数逻辑操作都用3输入逻辑指令LOP3来实现,它支持三输入的任意按位逻辑操作。

整数乘法的实现。通用的32bit乘法或乘加,除了Maxwell和Pascal中用的是XMAD,Kepler和Volta、Turing、Ampere都是用IMAD。但是很多地址计算中有这种模式:d=a*Stride+c,在Stride是2的幂次时,可以用移位和加法来实现。这正是LEA指令的工作模式。Turing的IMADLEA分属不同的dispatch port,两者可以独立发射。因此这是一个可能增加ILP的小优化。

Turing的IMAD是个挺神奇的指令。大量的情况下会用来做MOV操作,比如IMAD.MOV.U32 R1, RZ, RZ, R0; 的作用就相当于MOV R1, R0;。那好处在哪呢?这个应该与Turing把Float32与普通ALU的dispatch port分开有关,IMAD用的也是float32的pipe,所以可以与MOV的发射错开,这个到聊指令发射逻辑的时候再细讲。IMAD还有带shift的模式,如IMAD.SHL.U32 R0, R0, 0x10, RZ ;,还有IMAD.WIDE可以用64bit数做第三操作数,等等。

格式转换指令

数据移动指令

warp shuffle指令SHFL。warp内如果需要进行数据交换,第一要想到的就是这个指令。它支持多种交换模式,对其他warp没有依赖,因而在一些场景下有很大的用处。其中一个典型应用是做warp内的reduction,比如scan(或者叫prefix sum)之类。有兴趣的读者可以看看cuda sample里shfl_scan这个例子。

Predicate操作指令

内存操作指令

跳转和分支指令

Uniform DataPath指令

Operand 和 Operand Modifier

  • GPR
  • Predicate Register
  • Constant memory
  • Immediate
  • Uniform Register和Uniform Predicate
  • 地址操作数

Control codes

Register Reuse Cache

Wait Dependency Barrier

Read Dependency Barrier

Write Dependency Barrier

Yield Hint Flag

如果Yield,就表示下一个cycle会优先发射其他warp的指令。

Stall Count




NVidia指令集中没有提供满足IEEE标准的除法指令,而是通过低精度的特殊函数单元提供的MUFU.RCP倒数能力结合泰勒展开利用FADD、FMUL、FFMA指令进行高阶修正得到,这些高阶修正算法可以通过分析SASS指令得到,如normal数的除法的5阶Taylor修正大概如下

MUFU.RCP R8, R5 ;                                   /* 0x0000000500087308 */                                                                                      /* 0x000e220000001000 */
FADD.FTZ R10, -R5, -RZ ;                            /* 0x800000ff050a7221 */
FFMA R3, R8, R10, 1 ;                               /* 0x3f80000008037423 */
FFMA R12, R8, R3, R8 ;                              /* 0x00000003080c7223 */
FFMA R3, R7, R12, RZ ;                              /* 0x0000000c07037223 */
FFMA R8, R10, R3, R7 ;                              /* 0x000000030a087223 */
FFMA R11, R12, R8, R3 ;                             /* 0x000000080c0b7223 */
FFMA R7, R10, R11, R7 ;                             /* 0x0000000b0a077223 */
FFMA R3, R12, R7, R11 ;                             /* 0x000000070c037223 */


由于NVidia GPU的寄存器为32bit,以加法为例对于小于等于32bit的计算,都使用同样的指令实现(IADD3指令),对于大于32bit的加法,则通过两条指令实现,分别计算低32bit加法结果和高32bit的加法结果,同时在计算低32bit的结果时,使用Predication寄存器保存其进位情况,在计算高32bit时同时附带上低32bit的进位结果。

由于指令集方面只有32bit的计算指令,所以在GPU程序中采用低bit的整数计算并没有计算性能优势,反而低bit会引入截断指令,继而降低程序效率。对于大语言模型而言低bit量化可以减少内存使用量和数据IO开销,更多的是利用其存储空间优势,而非计算优势。

NVidia GPU整数指令集

整数加法

// 8bit, 16bit, 32bit
IADD3 R7, R0, R7, RZ ; // R7 = R0 + R7 + RZ

// substraction
IADD3 R7, R0, -R7, RZ ;

// 64bit R6-7 = R4-5 + R6-7
IADD3 R6, P0, R4, R6, RZ ;  // (R6, P0) = R4 + R6 + R0; P0 indicate carry
IADD3.X R7, R5, R7, RZ, P0, !PT ; // R7 = R5 + R7 + RZ + P0;

整数乘法

// d = a * b + c
IMAD
IMAD.HI
IMAD.HI.U32
// b == 1
IMAD.IADD
IMAD.IADD.U32
// a == 0 && b == 0
IMAD.MOV 
IMAD.MOV.U32
// b == 2 ^ x
IMAD.SHL
IMAD.SHL.U32
IMAD.U32
IMAD.U32.X
IMAD.WIDE
IMAD.WIDE.U32
IMAD.WIDE.U32.X
IMAD.X

整数位移

整数除法和取余

NVidia GPU指令集架构中并没有提供独立的整数除法指令,而是先将整数转换为浮点数,通过SFU(Special Function Unit提供的倒数指令,参看浮点计算中的除法)完成除法计算,然后再转换为整数进行结果修正。对于除数为编译时常量的,编译器会使用数学变换将该除法变换成更简单的位移和乘法指令cutlass中也有类似实现

整数转浮点数

整数转Packed整数

I2IP.S8.S32.SAT

整数绝对值

IABS

整数点积

针对低精度整数的点积运算(Integer Dot Product),对于深度学习场景,针对特别小的channel,利用该指令可以实现较好的效果

IDP.2A.LO.U16.U8
IDP.4A.S8.S8

整数最大最小

对于32bit及以下数据都可以使用该指令实现,对于64bit数据使用ISETP实现

IMNMX R7, R0, R7, !PT ; // when PT return R7 = min(R0, R7), when !PT R7 = max(R0, R7)
IMNMX.U32

整数比较产生条件

Tensor Core整数指令



无类型的寄存器表示

POPC指令(POPulation Count)

FLO指令(First Leading One)

BREV指令

SGXT指令(SiGn eXTend)

BMSK指令(Bit MaSK)

PRMT指令(PeRMuTe)

LOP3指令( Logical OPeration on 3 inputs)

LOP3.LUT R7, R0, R7, R6, 0xf, !PT ;
方法一 方法二 方法三
A B C
D = ~A D = ~B D = ~C
~Ta ~Tb ~Tc
0x0F 0x33 0x55
lop3(x, 0, 0, 0x0F) lop3(0, x, 0, 0x33) lop3(0, 0, x, 0x55)
LOP3.LUT R7, R0, RZ, RZ, 0x0f, !PT ; LOP3.LUT R7, RZ, R0, RZ, 0x33, !PT ; LOP3.LUT R7, RZ, RZ, R0, 0x55, !PT ;


Warp Level指令

DMMA指令

HMMA指令

IMMA指令

SHFL等

从Volta开始lane可以分裂执行,其可以解决竞争情况下锁步造成的死锁问题,但是如果都以独立的形式运行,效率会受很打影响,所以NVidia GPU的指令集架构也提供了WARPSYNC来实现warp内线程的同步,即该指令可以等待前面分裂执行的lane,使lane的执行对齐。提高后续指令的效率。

Uniform指令

在AMD体系下该类型的寄存器称为Scalar寄存器,也明确表明了该类型的计算单元为Scalar计算单元,但在NVidia体系下对该部分没有暴露编程能力,而是保留给编译器。当代码执行流分析时,如果路径上有warp一致的公共计算和控制逻辑,编译器会自动决断该部分是否使用Uniform寄存器和Uniform指令控制。 使用Uniform寄存器和Uniform指令可以一定程度上减少通用寄存器的使用,继而提高kernel的Occupancy并发度,最终提高kernel的执行效率。



指令层的控制流

Predicate实现控制流

//  if (c > 0) {
//    v = __sinf(f);
//  } else {
//    v = __cosf(f);
//  }
@!P0 MUFU.COS R0, R8 ;
@P0 MUFU.SIN R0, R8 ;

选择指令(SEL)

//  if (c > 0) {
//    v = 1;
//  } else {
//    v = 2;
//  }
MOV R7, 0x1 ;
...
SEL R7, R7, 0x2, P0 ;

分支指令(BRA,BRX,BRXU)

函数调用和返回(CALL,RET)

线程退出(EXIT)

执行逻辑分裂与合并

这部分能力由编译器裁决,更详细的信息可以参考NVidia的专利

// 设置同步点指令
BSSY B0, 0x150
// 同步等待指令
BSYNC B0 ;
// 打破同步点指令
@!P0 BREAK B2 ;

原子操作

ATOM指令需要获取读取的值,RED不需要。针对最终的内存而言,RED和ATOM的副作用一致。由于RED不必返回原子操作之前的值,效率也自然的会更高,具体的差异可以参看NVidia的专利


Compute Cache

Instructions

FMAD

PRMT

LDMATRIX