GPU内存模型:全局内存、共享内存、寄存器、局部内存、常量内存、纹理内存的区别与选择
做量化回测优化,说白了就是在跟内存打交道。
我刚开始接触CUDA时,觉得只要把数据扔进GPU就能快。结果呢?跑出来的速度还不如CPU单核。后来才明白,GPU的内存模型如果不搞清楚,你写的内核代码就是一辆堵在高速上的法拉利——引擎再强也跑不起来。
今天咱们就把GPU的六种内存掰开揉碎讲清楚。每种内存什么脾气、适合干什么、怎么选,我都会结合自己做量化回测的经验来说。
核心观点:GPU性能优化的本质,就是让数据尽可能靠近计算单元。离计算单元越近,访问越快,但容量越小。你得学会在这之间做权衡。
一、一张图看懂GPU内存层次
先看这张图,我花了不少心思画的。它把GPU的内存层次和访问速度关系讲得很清楚。
这张图我画了好几个版本。你看,从外到内,速度越来越快,但容量越来越小。全局内存像个大仓库,什么都能放,但取东西慢。寄存器就像你口袋里的笔,随手就能用,但只能装几支。
二、六种内存逐个拆解
1. 全局内存:GPU的"硬盘"
全局内存是所有线程都能访问的。容量最大,但延迟也最高——400到600个时钟周期。我做过测试,一次全局内存访问的时间,足够寄存器做几百次加法了。
量化回测中的应用:
- 存储历史行情数据(OHLCV、因子值)
- 存储回测结果(交易记录、绩效指标)
- 存储参数组合(网格搜索的参数空间)
我的经验:全局内存访问要尽量合并。什么意思?就是让相邻的线程访问相邻的地址。我在做因子回测时,把数据按时间序列排列,让每个线程处理一个股票,这样访问模式就是连续的,带宽利用率能到80%以上。
2. 共享内存:Block内的"共享白板"
共享内存是同一个Block内所有线程都能读写的。延迟只有5个时钟周期左右,比全局内存快两个数量级。但容量有限,每个Block通常只有48KB到164KB。
量化回测中的应用:
- 因子计算中的中间结果缓存
- 滑动窗口类指标(移动平均、布林带)
- Block内的数据规约(求和、求最大值)
避坑指南:我曾经在计算50只股票的滚动协方差矩阵时,直接把数据放在全局内存里反复读取。后来改成用共享内存缓存窗口数据,速度提升了3倍。但要注意——共享内存用多了会降低Occupancy,你得在两者之间找平衡。
3. 寄存器:最快的"口袋"
寄存器是每个线程私有的,访问延迟为0。每个线程最多能用255个32位寄存器。但如果你用了太多,编译器会把多余的数据"溢出"到局部内存——那就慢了。
量化回测中的应用:
- 循环计数器、临时变量
- 线程私有的累加器
- 频繁使用的中间计算结果
注意:我见过有人写内核时,一个线程里定义了50多个double变量。结果呢?寄存器不够用,编译器默默地把一半变量放到了局部内存里。性能直接腰斩。所以,控制寄存器用量很重要。
4. 局部内存:寄存器的"替补"
局部内存听起来像"局部"的,其实它物理上就在全局内存里。只是每个线程有自己的地址空间。当寄存器不够用时,编译器会自动把变量放到局部内存。
量化回测中的应用:
- 大型局部数组(比如一个线程要处理100个元素的数组)
- 寄存器溢出时的自动降级
说白了,局部内存就是个备胎。能用寄存器就别用局部内存。我一般会通过--maxrregcount编译选项来控制寄存器数量,避免不必要的溢出。
5. 常量内存:只读的"广播站"
常量内存只有64KB,但有个绝活——如果同一个Warp内的所有线程访问同一个地址,它只需要一个周期就能把数据广播给所有线程。这比全局内存快太多了。
量化回测中的应用:
- 策略参数(阈值、权重、系数)
- 固定的查找表
- 所有线程都需要的常量数据
我的习惯:做参数优化时,我会把参数表放在常量内存里。因为所有线程都在读同一组参数,常量内存的广播机制正好派上用场。但要注意,如果每个线程读的地址不同,常量内存反而比全局内存还慢。
6. 纹理内存:空间局部性的"利器"
纹理内存最初是为图形处理设计的。它利用空间局部性做缓存优化,还支持硬件插值。在量化回测中,如果你要访问二维数据(比如股票×时间的矩阵),纹理内存能带来不错的加速。
量化回测中的应用:
- 二维因子矩阵的随机访问
- 需要插值的场景(比如不同频率数据对齐)
- 不规则的内存访问模式
说实话,纹理内存在量化回测中用得不多。我只有在处理高频数据对齐时才用过一次。但如果你遇到非连续访问的场景,可以试试它。
三、怎么选?一张表说清楚
| 内存类型 | 容量 | 延迟 | 作用域 | 读写 | 量化回测最佳场景 |
|---|---|---|---|---|---|
| 全局内存 | GB级 | 400-600 cycles | 所有线程 | 读/写 | 存储原始行情、回测结果 |
| 共享内存 | 48-164 KB/block | ~5 cycles | 同一Block | 读/写 | 滑动窗口、数据重用、规约 |
| 寄存器 | 255个/线程 | 0 cycles | 单个线程 | 读/写 | 临时变量、累加器 |
| 局部内存 | 同全局内存 | 同全局内存 | 单个线程 | 读/写 | 寄存器溢出时使用 |
| 常量内存 | 64 KB | ~10 cycles (缓存命中) | 所有线程 | 只读 | 策略参数、固定系数 |
| 纹理内存 | 同全局内存 | 依赖缓存 | 所有线程 | 只读 | 二维数据、不规则访问 |
四、量化回测中的实战选择策略
说了这么多,到底怎么选?我总结了一个简单的决策流程:
- 数据是否只读且所有线程访问相同地址? → 用常量内存
- 数据是否在Block内被反复使用? → 用共享内存
- 数据是否每个线程私有且频繁使用? → 用寄存器
- 数据是否二维且访问模式不规则? → 试试纹理内存
- 以上都不满足? → 用全局内存,但注意合并访问
举个例子:我在做多因子选股回测时,需要计算1000只股票、500个交易日的20日移动平均。每个线程处理一只股票,需要访问连续20天的数据。
我的做法是:先把行情数据加载到全局内存。每个Block处理32只股票,把32×20的数据块加载到共享内存。然后每个线程从共享内存里读自己的20天数据,用寄存器做累加。参数(比如窗口大小20)放在常量内存里。
这样一搞,全局内存只访问一次,后面全在共享内存和寄存器里操作。速度比纯全局内存版本快了5倍。
五、几个容易踩的坑
坑1:共享内存bank conflict
共享内存被分成32个bank。如果同一个Warp里的多个线程访问同一个bank的不同地址,就会发生冲突,导致串行化。我遇到过因为数据结构没对齐,性能直接掉了30%。解决办法是加padding或者调整访问模式。
坑2:寄存器溢出
你以为用了寄存器就快了?如果每个线程用了太多寄存器,编译器会把一部分放到局部内存。局部内存可是在全局内存里的!我曾经有个内核,寄存器用了200多个,结果性能还不如少用点寄存器、多用共享内存。
坑3:常量内存的误用
常量内存只有在所有线程访问同一地址时才快。如果每个线程访问不同的地址,它比全局内存还慢。我见过有人把因子值放在常量内存里,结果每个线程读不同的因子,性能惨不忍睹。
六、总结一下
GPU内存模型其实不复杂。记住一句话:数据离计算越近越快,但容量越小。你要做的就是根据数据的使用模式,把它放在最合适的内存里。
我个人习惯是:先默认用全局内存,然后分析数据访问模式。如果有数据重用,就搬到共享内存。如果是只读且所有线程共享,就放常量内存。临时变量用寄存器。这样一步步优化,性能自然就上来了。
嗯,这一章的内容就到这里。内存模型是GPU优化的基础,搞懂了它,后面的优化技巧才能派上用场。
公众号:蓝海资料掘金营,微信deep3321