GPU Gems 2 · Chapter 32. Taking the Plunge into GPU Computing

从局部性、计算密度、传输摊销、浮点精度和 scatter 绕行策略出发,判断一个 CPU 算法是否值得迁移到 GPU。

学习目标

  • 能解释内存访问、计算量、数据传输和数值表示如何共同决定 GPU 迁移收益
  • 能修改 StrategyLab 中的 workload、locality 与 precision,并根据派生指标选择 scatter 绕行策略
  • 能回答:当地址动态变化、读回成本很高且结果需要高精度时,应如何判断 GPU 是否值得使用

先问:为什么“搬到 GPU”不等于“自然变快”

想象两间工厂:一间擅长让很多工人同时做同一种动作,另一间擅长让少数工人灵活地处理不同订单。把订单送进第一间工厂有固定的装卸成本;如果只做一小步就把半成品搬回来,工人再多也救不了总时间。

本章解决的问题是:面对一个 CPU 算法,怎样在动手改写前判断它是否值得迁移?如果忽略访问方式、装卸成本和数字表示,最常见的结果是并行程序内部很快,但端到端更慢,或者结果看似接近却在大索引处出错。

1. 访问顺序决定内存能否跟上

CPU 和 GPU 都需要 locality,但两者的缓存职责不同。GPU 的 texture cache 主要服务纹理过滤,容量和布局更偏向二维局部区域;它不是一个能像 CPU cache 那样同时缓存任意读写的通用工作区。因此,连续扫描大向量通常容易发挥 GPU 带宽,而 pointer chasing 和随机读会让读取延迟更难隐藏。

locality:连续访问更接近 GPU 的强项sequential相邻 texels 连续抵达,吞吐容易拉满01234567random / pointer chasing地址跳跃,读取之间更难被并行工作覆盖51726043纹理 cache 偏向二维局部读取,不等于 CPU 的大而通用的 read/write cache

原章用 GeForce 6800 Ultra 与 Pentium 4 的历史测量说明了这一差异:同一 GPU 对 cached、sequential、random 三种访问模式的表现可以相差很大。数字只属于当时硬件;可迁移的结论是“先画出地址序列,再判断数据布局”,而不是把旧带宽数字当作现代保证。

2. 让计算覆盖读取等待

发出 texture fetch 后,GPU 可以先切换去处理下一个 fragment;数据返回后再继续原来的计算。若 kernel 只有一次读取、一次加法和一次写回,等待很容易暴露出来;如果每个读取之后有足够多的独立算术,计算就有机会覆盖这段 latency。

可以用一个粗略比值判断方向:

AI=NarithmeticNmemory.AI=\frac{N_{arithmetic}}{N_{memory}}.

这个式子在说:每次内存操作支撑的计算越多,任务越可能受 GPU 算力而不是读写等待限制。

arithmetic intensity:用计算覆盖 memory latencytexture fetch发出读取请求next fragment先做别的工作resume数据返回后继续更多 arithmetic instructions → 更有机会隐藏读取等待AI = arithmetic operations / memory operations

原章把这一点和 CPU/GPU 的计算吞吐差异联系起来,但“高 AI 必然更快”仍是不完整的结论。元素必须足够多、访问不能完全随机,且 host 与 GPU 之间不能反复同步;这些条件缺一个,计算优势都可能被边界成本抵消。

3. 把下载和读回放进总时间

CPU 版本通常不需要先下载数据,也不需要把结果读回。GPU 版本若只运行一次简单向量加法,必须把两份输入送入显存、运行 kernel,再把结果拿回主机;这些步骤可能超过 kernel 本身节省的时间。模拟类任务更有机会摊薄成本,因为数据可以在 GPU 上迭代许多次之后再读回。

download + compute + readback:传输成本必须进入总账端到端时间线downloadGPU compute × iterationsreadback同样的传输,计算次数不同one vector add传输占比高many simulation steps传输被多次计算摊薄与 CPU 比较时,不能只比较 shader 时间

因此比较时至少记录四段:CPU 计算、download、GPU compute、readback。纹理与 frame-buffer 的格式也会影响传输速度;驱动若需要做格式转换,测到的就不只是算法成本。

4. 浮点格式会改变正确性

GPU Gems 2 所处的硬件时代,GPU 可能提供 NVIDIA fp16、ATI fp24 或 NVIDIA fp32 等格式。mantissa 位数越少,能精确表示的有效数字越少;这对着色颜色可能足够,对数值模拟、归约和地址计算却可能造成明显误差。fp16 在历史表格中只能连续表示到 2,048 左右的整数,超过这个范围时,某些整数会被跳过。

precision 不是颜色细节:它也决定地址和数值能否可信formatmantissa bitslast exact integer143.98375329 →NVIDIA fp16102,048143.875ATI fp2416131,072143.98242NVIDIA fp322316,777,216143.98375地址用浮点数计算时,跳过的整数可能表现成 off-by-one bug

地址计算尤其危险:如果把一维索引压进浮点值,再用它计算二维纹理坐标,表示误差会表现成偶发的 off-by-one。修法不是“多看几位小数”,而是明确目标格式的 mantissa、最大连续整数、边界行为和允许误差;现代设备支持的格式与旧 GPU 不同,必须在目标 API 上复核。

5. scatter 不能直接照搬

在上一章的模型里,gather 是从计算出的地址读取;本章关注相反的 a[i] = x。fragment program 没有任意 texture write 指令,输出地址在 fragment 执行前已经由 rasterization 决定,因此很多 CPU 算法的间接写入不能直接翻译。

固定连接:把 scatter 改写成 gather

spring-mass 系统中,弹簧连接的质量点如果是固定的,就可以先把每条 spring 的 force 写到连续 buffer,再让第二个 pass 遍历每个 mass,主动 gather 会影响它的力。多一个 pass 换来了固定的输出位置和可验证的读取关系。

动态地址:记录地址、排序、再 gather

粒子在每个时间步移动到不同 voxel 时,scatter 地址运行时才知道。可以输出 (value, address) 对,按 address 排序,让相同目标相邻,再对每个目标做搜索与 gather。它的 pass 多、实现重,但不需要把中间结果读回 CPU。

少量写入:用 vertex point rendering

vertex program 天然能改变点的目标位置,因此少量 scatter 可以用点渲染表达。代价是 rasterization 利用率较低,而且多个点落到同一个位置时会发生 collision;可以用 z-buffer 或 blending 设计冲突规则,但不能假设最后写入者稳定。

scatter 的三条绕行路线固定地址convert to gather动态地址address sorting少量写入render pointsspring forcebuffer forcesmass gathers neighborsvalue + addresssort address pairsbinary search + gathervertex reads addresspoint destinationwatch collisions选择依据:地址是否固定、散射量大小、碰撞是否可接受

6. 一个可重复的迁移决策

从 CPU 任务到 GPU 策略的验收顺序访问连续吗?locality计算够多吗?arithmetic intensity能摊薄传输吗?download / readback否:留在 CPU 或改布局否:收益会被带宽吞掉通过三关后,才进入 scatter 的具体实现选择

把决策压缩成四问:访问是否连续?每次读取能否支撑足够计算?download/readback 能否被多次迭代摊薄?若存在 scatter,地址是否固定、散射量多大、碰撞如何处理?这四问比“GPU 有多少 Gflops”更接近真正的工程选择。

measure(addressPattern, arithmeticOps, memoryOps, transfers)
if locality is low: reshape data or keep the CPU path
if arithmeticOps / memoryOps is low and transfers are large: amortize or stop
if scatterAddress is fixed: convert scatter to gather
else if scatterCount is small: render points and define collisions
else: sort (value, address) pairs, then gather

代码只是决策骨架,不是某个历史 API 的可运行实现。真正落地时仍需把纹理格式、同步、精度和设备 profile 填进测量表。

先猜一猜:把 histogram 切到 vector,再把 locality 拖低,哪一项会改变推荐策略?打开实验,依次只改一个控件,并观察 texture readstransfer wordssafe integer ceilingrecommended mapping

Strategy Lab · compare the bottleneck shape

What this workload exposes

动态写入地址的 histogram,需要先把 scatter 改成 address sort + gather。

Derived metrics

arithmetic operations36864
texture reads5899
transfer words9216
iterations1
safe integer ceiling16777216

recommended mapping

address sort + gather

这些是由控件直接推导的工作量与表示范围,不是合成性能分数;最终仍需在目标设备上 profile。

三步验收:先筛选,再改写

分步1 / 3

第一步:画地址流与计算流

把 CPU 版本每次读写的地址列出来,标出连续段、跳跃段和数据复用;再计算每个元素的 arithmetic operations 与 memory operations。先确认任务的访问形状,再决定是否值得迁移。

locality:连续访问更接近 GPU 的强项sequential相邻 texels 连续抵达,吞吐容易拉满01234567random / pointer chasing地址跳跃,读取之间更难被并行工作覆盖51726043纹理 cache 偏向二维局部读取,不等于 CPU 的大而通用的 read/write cache
arithmetic intensity:用计算覆盖 memory latencytexture fetch发出读取请求next fragment先做别的工作resume数据返回后继续更多 arithmetic instructions → 更有机会隐藏读取等待AI = arithmetic operations / memory operations

本章小结

  • locality 决定 GPU 内存系统能否持续供数。
  • arithmetic intensity 帮助判断计算能否覆盖读取等待。
  • download/readback 属于端到端成本,必须被迭代摊薄。
  • floating-point precision 同时影响数值误差和地址安全。
  • scatter 要按地址是否固定、规模和碰撞语义改写为 gather、排序或点渲染。

练习

问题 1|迁移判断。 一个向量加法 kernel 每个元素只做 2 次加法,却需要下载两份输入并读回结果;一个反应扩散模拟在同一份数据上运行 500 个迭代。哪一个更可能从 GPU 受益?需要补测哪些数据才能确认?

问题 2|精度与地址。 使用历史 fp16 路径处理一个需要索引到 3,079 的二维纹理。为什么即使小索引测试通过,也不能相信所有地址都正确?你会在 StrategyLab 中改变哪个控件并记录什么证据?

问题 3|修改实验代码。 StrategyLab 中把 histogram 的推荐路径固定为 address sorting + gather。请修改决策逻辑:当散射地址固定时改为 convert scatter to gather,当散射值少于输入的 5% 时改为 render points,并说明要新增哪两个验证指标。

名词解释

本章出现的专业名词,用大白话再讲一遍。

locality
arithmetic intensity
download/readback
floating-point precision
scatter

资料与写作方式声明

本章以GPU Gems 2 · Chapter 32. Taking the Plunge into GPU Computing权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…