GPU内存模型:全局内存、共享内存、寄存器、局部内存、常量内存、纹理内存的区别与选择

做量化回测优化,说白了就是在跟内存打交道。

我刚开始接触CUDA时,觉得只要把数据扔进GPU就能快。结果呢?跑出来的速度还不如CPU单核。后来才明白,GPU的内存模型如果不搞清楚,你写的内核代码就是一辆堵在高速上的法拉利——引擎再强也跑不起来。

今天咱们就把GPU的六种内存掰开揉碎讲清楚。每种内存什么脾气、适合干什么、怎么选,我都会结合自己做量化回测的经验来说。

核心观点:GPU性能优化的本质,就是让数据尽可能靠近计算单元。离计算单元越近,访问越快,但容量越小。你得学会在这之间做权衡。

一、一张图看懂GPU内存层次

先看这张图,我花了不少心思画的。它把GPU的内存层次和访问速度关系讲得很清楚。

GPU内存层次结构图 全局内存 (Global Memory) 容量: GB级 | 延迟: 400-600 cycles | 所有线程可访问 常量内存 (Constant Memory) 容量: 64KB | 延迟: ~10 cycles (缓存命中) 特点: 只读、广播、适合所有线程读相同数据 量化场景: 参数表、固定系数 纹理内存 (Texture Memory) 容量: 同全局内存 | 延迟: 依赖缓存 特点: 空间局部性优化、硬件插值 量化场景: 二维数据访问、图像类数据 共享内存 (Shared Memory) 容量: 48KB~164KB/block | 延迟: ~5 cycles | 同一block内线程共享 量化场景: 数据重用、规约操作、矩阵分块 寄存器 (Register) 延迟: 0 cycles | 每个线程私有 局部内存 (Local Memory) 寄存器溢出时使用 | 实际在全局内存 速度↑ 容量↓ 速度↑ 容量↓ ← 速度越来越快 容量越来越大 →

这张图我画了好几个版本。你看,从外到内,速度越来越快,但容量越来越小。全局内存像个大仓库,什么都能放,但取东西慢。寄存器就像你口袋里的笔,随手就能用,但只能装几支。

二、六种内存逐个拆解

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 (缓存命中) 所有线程 只读 策略参数、固定系数
纹理内存 同全局内存 依赖缓存 所有线程 只读 二维数据、不规则访问

四、量化回测中的实战选择策略

说了这么多,到底怎么选?我总结了一个简单的决策流程:

  1. 数据是否只读且所有线程访问相同地址? → 用常量内存
  2. 数据是否在Block内被反复使用? → 用共享内存
  3. 数据是否每个线程私有且频繁使用? → 用寄存器
  4. 数据是否二维且访问模式不规则? → 试试纹理内存
  5. 以上都不满足? → 用全局内存,但注意合并访问

举个例子:我在做多因子选股回测时,需要计算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