[关闭]
@Velaciela 2017-08-17T14:54:36.000000Z 字数 3468 阅读 213

一维卷积的SASS实现

CUDA



对应代码和工程,见GitHub


1、一维卷积计算过程

线性卷积:

设x(n)长度为N1,h(n)长度为N2 , 则y(n)长为 N=N1+N2-1
为方便表示,在序列x(n)后添N2-1个0 ,使x(n)的长度变为 N

卷积公式 :

可用矩阵表示为:

image_1avo8jtg41p6e5egvii1vt9uec1t.png-77.1kB


一个简单的计算过程示意:

x(n)=[4,3,2,1]
h(x)=[3,2,1]
y(n)=x(n)*h(n)

  1. x | 4 3 2 1 |
  2. | |
  3. y0 | 1 2 3 | = 4*3 = 12
  4. y1 | 1 2 3 | = 4*2 + 3*3 = 17
  5. y2 | 1 2 3 | = 4*1 + 3*2 + 2*3 = 16
  6. y3 | 1 2 3 | = 3*1 + 2*2 + 1*3 = 10
  7. y4 | 1 2 3 | = 2*1 + 1*2 = 4
  8. y5 | 1 2 3 | = 1*1 = 1

为保证局部性:

在实现时,卷积核H的数据先放到片上
(H点数较小时直接放到寄存器,点数稍大可放在共享内存,过大时分批从片外内存读取)

实际的原始数据X,点数较大,分块(blocking)处理。

Y的计算不按照上例中的一次性计算
而是 多次乘加 得到结果 :

  1. //Y初始化为0
  2. regX = shareadX[0]; //共享内存读取x
  3. Y0 += regX*H1;
  4. Y1 += regX*H2;
  5. Y2 += regX*H3;
  6. regX = shareadX[1];
  7. Y1 += regX*H1;
  8. Y2 += regX*H2;
  9. Y3 += regX*H3;
  10. ...
  11. //计算完成后把Y 在共享内存reshape 然后放回片外

上例每次读一个X。实现时,X批量读入,计算掩盖访存延迟。


2、两个实现:16点卷积与1024点卷积

算法分析

算法 卷积
数据量 N*2 (输入X / 输出Y / H较小不计入)
计算量 N*N2*2 (乘加按两个指令计算)
硬件 GTX970
峰值计算能力 1.25GHz * 1664cores * 2 = 4160 GFLOPS(实测达到)
访存带宽: 224GB/S (实测连续访存140GB/s ,即35G/s的FP32数据)


(即一维卷积计算算法强度与卷积核长度直接相关: 16点卷积计算强度为16, 1024点卷积计算强度为1024)

所以16点卷积计算 :受限于带宽 (16 < 116.8 访存密集)
1024点卷积计算 : 受限于核心计算能力(1024 > 116.8 计算密集)

所有算法都可以从这一角度,划分为上述两个类型之一。
计算密集的算法,可以针对硬件架构进行优化。


16点卷积

一维卷积核H:16 float (直接拷贝到寄存器)
输入数据量X:1024000 float
输出数据量Y:1024000 + 16 float (最后16个Y为了方便,没有进行计算)
线程结构 : 32个thread一个blcok,每个thread计算16点Y,一共需要2000个block。
片上存储 : 分配544 float * 4 bytes = 2176 bytes 大小的共享内存空间用于存储。

存储结构

Shared存储
image_1avomqh2tgdqqvvct61ubn3u583.png-45kB

每个block负责32*16=512点Y计算
32个threads 对显存合并访问 读取X
每个thread进行4次LDG.128访问 + 一次LDG访问(32bit,末尾的32个X,只用到1/2的数据)
image_1be05en8318a1vtp1fqd1jetng5m.png-32kB

Shared访存
整体来看,一个线程需要访问8次共享内存(128bit形式)
eg:
第一次,所有线程访问第一行,第二次第二行,第三次第三行,第四次第四行
第五次时,需要向左移一列访问,接下来再访问三行

(注:图看不清的话,可以点击放大)
image_1avolnfikeh316dp1crrcciqgt7m.png-34kB

以thread 0为例 ,蓝色框是其访存范围,红色线条是访存顺序。
黑框是 thread 1 的访存范围,前后两个线程共享16个 float 数据
image_1avon1gnd1c38v0j1clh15kt78d8g.png-17.9kB

这样的LD/ST方法,利用了完整的共享内存带宽。


算法结构

计算示意(结合 1、一维卷积计算过程 理解)

image_1avonaiq71mp1i8p11od4qnfka8t.png-203.3kB

完全避免寄存器冲突的办法

image_1avonpv7ieokp3dis41r66f289a.png-228.4kB

SASS代码:
conv16_final_32blockOcup.sass


1024点卷积

计算的核心循环与16点卷积一致,32点X与16点H参与计算。
区别在于下一次循环还要使用32个当前计算X中的的后16个;下一次计算使用的H是新的16个。

理论分析:由于一个小循环算完16点Y,数据都在片上,所以再套上大循环,对X、Y做双缓冲不会有速度提升
实际处理:H加双缓冲无效,寄存器双缓冲预取共享内存中的X,计算掩盖延迟。

一维卷积核H:1024 float
输入数据量X:2097152 float
输出数据量Y:2097152+1024 float (最后1024个Y为了方便,没有进行计算)
线程结构:128个thread一个blcok,每个thread计算16点Y,一共需要1024个block。

存储结构

X数据存储与16点类似,一个block存128线程*(4float*6次加载)=3072个单精度浮点。
还需要1024*4byes存放H。
共分配4096*4bytes的共享内存空间
image_1avoqm9741tr17n6b7jd4f1eufbb.png-66.4kB

计算结构

block内计算示意
image_1avop9ch21fqrovk1nrsk9k1821a4.png-72.9kB

block间计算示意
image_1avopgujc51q1fhf1nhf1j7iu8sau.png-84.9kB

注:Y存回片外时通过共享内存reshape,有bank conflict,只用到了共享内存1/4的完整带宽。
但实测对kernel执行时间基本没有影响(占比太小)。

SASS代码:
conv1024_nobuffer.sass


3、实验结果

16点卷积

每个线程使用60个寄存器(优化后),恰好达到1个SM上32个Block的占用上限。

image_1avoeeebb182op4fusfj8s1r053u.png-51kB

实测,只要一个SM上达到6个Block的占用,就可占满带宽。
6占用kernel执行时间和32个占用的时间一致。
占用多了,带宽跟不上,还是需要等待。

数据量(32*16*2000)*2 = 2*1024000FP32 = 2*1024000*4bytes/1024/1024 = 7.8125MB

下图可知,kernel平均计算时间为56us
image_1avof1ebn1grl7jo7am9qmso54o.png-53.5kB

带宽占用: 7.8125MB / 0.056ms = 139.51GB/s
与理论分析一致


1024点卷积

Visual Profiler (nvvp)分析:

带宽未占满
image_1avofvn3l1ngb1l8a10r2ulobpl5v.png-12.1kB

计算指令占比接近90%
image_1avofsmlrdqc1bq7415114d11o65i.png-12.8kB

SM占用6
image_1avog5jnc1g2tapc1iej1f7negm6p.png-51.1kB
实测占用 6、4、2个block的情况,kernel执行时间一致。
由于计算能力限制,占用多了也算不过来。

kernel执行时间1.4ms
image_1avog45dp1nhm14qc15ho14m9e86c.png-44.6kB

结果 达到硬件峰值计算能力的74% (未达到90%)
数据量 输入X+输出Y = 2*2097152 float = 2*2097152*4bytes/1024/1024 = 16MB
kernel时间 1.4ms
带宽占用 16MB / 1.4ms = 11.42GB/s
kernel算力 2097152_X输入*1024_H卷核*2_乘加 / 1.4ms = 4.295 GigaFloatOp / 0.0014s = 3067.86 GFLOPS
硬件算力 1.25GHz * 1664cores * 2 = 4160 GFLOPS

数据生成及结果验证

原始数据X、卷积核H和结果Y的长度分别为N、M、P。
卷积核与原始数据用伪随机数填充。
CPU计算的卷积结果放在数据块T中。
GPU计算的卷积结果拷贝到数据块Y中。

打印前1024个T与Y的误差
Y与T差的绝对值大于1则打印出来(计算精度所在位置随数据大小变化)

image_1avoclkfcqsai1f1uu71sua123u3h.png-134.7kB


4、小结

常用的优化方法:
调整算法结构(匹配硬件特性),kernel间整合(减少访存),kernel优化(数值计算过程与汇编)

16点卷积和1024点卷积恰好可划分在访存密集和计算密集这两个类型中。
是比较好的练手项目。

访存密集型的算法不需要用汇编优化,保证基本的合并访存即可。
但一些算法结构复杂,需要仔细分析,利用好片上和片外内存的带宽。

1024点卷积kernel应该达到90%+的峰值算力,未找到受限原因。
理论上1024点卷积不需要嵌套循环,不用对X做双缓冲预取,因为当前计算的Y与下一大组X没有数据依赖关系。

按现在的理解,在SM上block占用低于一个阈值的情况下,双缓冲可以带来一定的计算能力提升。
在占用足够的情况下,线程级并行、线程间的切换,其实和双缓冲是等效的。
(2017补充)

实际工程中常用FFT在频域进行快速卷积。


5、后续

二维卷积
FFT

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注