返回关卡PARALLEL HORIZONSREAD // 01

关卡阅读材料 // 01

内存层次

历史锚点2010 · Fermi 内存系统转折阅读时间 · 约 60 分钟

线程已经足够多之后,下一步不是继续增加线程,而是让一个 Warp 的内存请求更集中,并把会重复使用的数据放到合适的位置。

建议边读边写下答案;时长包含代码推演与练习。

进入互动关卡
本课节奏约 60 分钟
  1. 01导读与目标05 MIN
  2. 02心智模型08 MIN
  3. 03概念深挖15 MIN
  4. 04代码推演12 MIN
  5. 05历史生态08 MIN
  6. 06练习复盘12 MIN
加速计算能力映射独立学习补充 · 非 NVIDIA 官方课程或认证
PATH // 01
FOUNDATIONALPROFILINGNSIGHT COMPUTE
官方路径强调

不仅让程序在 GPU 上运行,还要使用行业工具识别内存瓶颈,以实验数据推动优化。

本课的衔接

关卡把合并访问、数据复用和 Shared Memory 变成可见的数据路径;实验要求用 Profiler 证明事务、吞吐与 Bank Conflict 是否真的改善。

建议前置
  • 完成课程 00 或能编写基础 Kernel
  • 理解 Warp 与全局索引
  • 会比较参考输出
实践工具
  • Nsight Compute
  • CUDA C++
  • CUDA C++ Best Practices Guide
01 · 05 MIN

导读与目标

先记住这一句

GPU 性能不仅取决于算多少,还取决于数据以什么形状到达计算单元。

完成后你应该能够

  1. 01

    从一个 Warp 的地址集合判断访问是否可能产生过多全局内存事务。

  2. 02

    根据数据复用范围选择寄存器、Shared Memory 或全局内存。

  3. 03

    解释 Halo、协作加载与同步在邻域计算中的角色。

  4. 04

    识别 Bank Conflict、错误同步与过度缓存等常见问题。

02 · 08 MIN

建立心智模型

  1. 01
    地址

    一个 Warp 同时发出 32 个地址;硬件会把它们合并成尽可能少的内存事务。

  2. 02
    复用

    若同一 Block 多次使用相邻数据,可先协作加载到 Shared Memory,再从片上复用。

  3. 03
    同步

    只有确认所有参与线程完成加载后,其他线程才能安全读取完整 Tile。

03 · 15 MIN

概念深挖

01

合并访问

相邻线程访问相邻、适当对齐的数据时,通常能减少为服务一个 Warp 所需的全局内存事务。

02

作用域决定位置

寄存器属于线程,Shared Memory 属于 Block,全局内存可被整个 Grid 与 Host 访问。数据共享范围决定存放位置。

03

Shared Memory 是程序管理的缓存

它很快但容量有限,也可能出现 Bank Conflict;需要显式加载、同步与布局设计。

小节 01

从地址数量转向事务数量

一个 Warp 执行一次全局内存 load 时,会同时提出一组地址。硬件按所需的内存段组织事务,因此性能问题不是简单的“读取了 32 个值”,而是“为了得到这些值搬运了多少段数据”。连续地址通常能提高有效字节与传输字节之比。

合并规则会随 Compute Capability、数据宽度和缓存路径变化,所以不要把某个固定事务数字当成永久规律。稳定的推理方式是观察同一 Warp 的地址分布、对齐和实际使用的字节,并用 Nsight Compute 等工具验证。

停下来想一想32 个线程读取连续 float,和读取相隔 4 KB 的 float,哪一种通常浪费更多传输?

参考答案相隔 4 KB 的访问通常落到更多内存段,产生更多事务与无用传输;连续访问更容易合并。

小节 02

Tile 把重复搬运变成协作

邻域、卷积与矩阵算法常让相邻线程重复读取同一输入。Shared Memory Tile 的思想是让 Block 先以合并方式加载一块数据,再让多个线程从片上位置复用。Halo 是为了让 Tile 边缘线程仍能访问邻居而额外加载的边界数据。

Tile 大小要同时考虑复用收益、Shared Memory 容量、Block 大小与边界开销。Tile 过小会让 Halo 占比变高,过大会限制驻留 Block 数。真正的优化需要比较减少的全局请求与新增同步、索引和资源成本。

停下来想一想64 线程做三点邻域,为什么 66 个输入值就可能服务 64 个输出?

参考答案64 个主体位置加左右两个 Halo 覆盖了所有相邻访问;主体值可以被相邻线程复用,而不必每个线程各读三次全局内存。

小节 03

同步必须被整个参与组一致到达

__syncthreads() 是 Block 级屏障:只有当所有参与线程都到达后,任何线程才能继续。把它放在只有部分线程执行的分支里,可能让一部分线程等待永远不会到达的伙伴。正确模式通常是条件化加载,但无条件到达屏障。

Shared Memory 由 Bank 组织。不同线程访问映射到同一 Bank 的不同地址时,请求可能被串行化;对二维 Tile 增加 Padding 是常见处理方式。同步保证可见性,却不会自动消除 Bank Conflict 或索引错误。

停下来想一想if (threadIdx.x < 16) __syncthreads(); 为什么危险?

参考答案同一 Block 中只有部分线程进入屏障,其余线程不会到达,行为未定义并可能死锁。

04 · 12 MIN

把概念放回代码

PROGRAM MODELBlock 协作加载 Tile,再同步后复用
01__shared__ float tile[BLOCK_SIZE + 2];02int i = blockIdx.x * blockDim.x + threadIdx.x;03tile[threadIdx.x + 1] = input[i];04// 边界线程再加载左右 halo05__syncthreads();06output[i] = tile[t] + tile[t + 1] + tile[t + 2];

代码逐段推演

  1. 01
    ALLOC为主体与 Halo 分配 Tile

    BLOCK_SIZE + 2 为左右各留一个额外位置;二维问题通常会在两个维度加入边界。

  2. 02
    LOAD让相邻线程合并读取

    主体数据按 threadIdx.x 映射到连续地址;边界线程再承担少量 Halo 加载。

  3. 03
    BARRIER在消费前完成协作

    所有线程都完成自己的加载职责后统一 __syncthreads(),确保 Tile 对整个 Block 可见。

  4. 04
    REUSE从片上位置组合输出

    每个输入只需从全局内存加载一次,却可被多个相邻输出读取;最后仍要处理全局边界。

REF · 01

知识图谱

补充参考 · 不要求一次读完,也不计入核心 60 分钟

核心术语

Memory Transaction
硬件为满足一个 Warp 的地址请求而搬运的内存段;程序请求的字节数不等于总传输字节数。
Coalescing
让 Warp 的地址落在尽可能少且对齐的内存段中,提高有效字节占比。
Shared Memory
由 Block 内线程显式管理和共享的片上存储,适合协作加载与复用。
Halo
Tile 边缘为计算邻域输出而额外加载的输入区域。
Bank Conflict
同一请求中的线程访问同一 Bank 的不同地址,导致 Shared Memory 请求被拆分或串行化。
EXAMPLE

带数字推演

  1. 01
    直接读取

    64 × 3 = 192 次标量读取请求,相邻线程会重复请求许多输入。

  2. 02
    协作 Tile

    64 个主体值加左右各 1 个 Halo,共加载 66 个值到 Shared Memory。

  3. 03
    算复用

    192 ÷ 66 ≈ 2.9,代码层面的请求数量约降到原来的三分之一。

算完得到

理想计数从 192 次请求降到 66 次协作加载,但还要支付同步与边界处理。

为什么重要

这只是复用量估算,不是显存事务或速度预测;最终仍要用 Profiler 检查事务、命中和同步等待。

诊断手册

01现象

带宽利用率低且 Load 请求很多

先检查
Warp 地址是否连续、对齐,数据布局是否让相邻线程访问相邻元素
需要的证据
Nsight Compute 的内存事务、吞吐与 Warp 地址模式
02现象

加入 Shared Memory 后更慢

先检查
复用次数、额外同步、Halo 比例、Shared Memory 容量与 Occupancy
需要的证据
优化前后全局字节、屏障等待和活跃 Warp 对比
03现象

二维 Tile 出现异常 Shared Memory 延迟

先检查
行跨度是否让多个线程映射到同一 Bank
需要的证据
Bank Conflict 指标,以及增加一列 Padding 前后的对照实验

从硬件到生态

  1. 01全局内存主导

    早期 GPGPU 需要开发者严密组织地址来获得带宽。

    合并访问成为第一代通用 GPU 优化核心。
  2. 02缓存 + 可编程 Shared Memory

    缓存层次、ECC 与片上数据复用能力增强。

    优化从地址连续性扩展到复用、可靠性和资源交换。
  3. 03异步数据搬运

    更现代的架构提供更强的异步复制与 Tile 搬运机制。

    软件开始把“加载下一块”和“计算当前块”组成显式流水。
05 · 08 MIN

硬件与生态坐标

2010 · Fermi

Fermi 强化了面向通用计算的缓存、ECC 与更完整的内存系统。它让“线程如何取数、缓存如何工作、可靠性如何保证”成为数据中心 GPU 编程的重要组成,而不是只看算术单元数量。

06 · 12 MIN

练习与复盘

一个 Warp 的线程读取 input[i * 16],最先应该检查什么?

  1. A. 地址是否分散到许多内存事务
  2. B. Block 数是否一定等于 16
  3. C. 是否改成更多 CPU 线程
查看答案
A

跨步访问会把同一 Warp 的地址拉散。先观察请求形状,再决定是否重排数据或用 Shared Memory。

02判断题

一个 Warp 读取 32 个连续 float,但首地址偏移一个 float。这一定与对齐访问同样高效吗?

提示

考虑访问跨越的内存段。

参考答案

不一定。错位可能多覆盖一个内存段,增加事务;缓存复用有时会减轻影响,但应检查对齐并测量。

03计算题

64 个线程各自从全局内存读取左、中、右三个值,共提出多少次标量读取?协作 Tile 至少加载多少个值?

提示

忽略缓存,把每次表达式读取都计数。

参考答案

直接方式为 192 次标量读取;一维 Tile 为 64 个主体加 2 个 Halo,共 66 个值。事务数量仍取决于访问与硬件。

04改错题

只有负责 Halo 的线程执行 __syncthreads()。如何修复?

提示

把加载职责和屏障参与分开。

参考答案

条件分支只包住 Halo 加载;把 __syncthreads() 移到分支之后,让 Block 中所有线程一致到达。

05设计题

一个数据只被当前线程使用一次,应该优先放进 Shared Memory 吗?

提示

看共享范围与复用。

参考答案

通常不应。线程私有且短生命周期的值更适合寄存器。Shared Memory 适合 Block 内跨线程共享或显著复用的数据。

LAB · 35–45 分钟

可选实践实验

不计入核心 60 分钟 · 需要相应 CUDA / GPU 环境

实验目标

用证据比较连续、跨步与 Shared Memory Tile

构造三种访存版本,在保证结果一致的前提下,用 Nsight Compute 区分地址模式、全局内存效率与 Shared Memory 行为。

建议步骤

  1. 01

    实现 input[i] 与 input[(i * stride) % n] 两个读取版本,固定数据规模、Block 配置与预热次数。

  2. 02

    用 Nsight Compute 收集全局 Load/Store、Memory Throughput 与 Warp 相关指标,并保存命令和报告。

  3. 03

    为三点邻域增加 Shared Memory Tile 与 Halo,检查所有线程是否一致到达屏障。

  4. 04

    比较直接读取与 Tile 版本;若优化无效,先解释复用、规模或资源占用,而不是修改结论。

完成证据

提供三个版本的正确性、Profiler 指标表和一句因果解释:地址/复用发生了什么,指标如何支持这个判断。

进阶挑战

构造一个二维转置 Tile,比较无 Padding 与 +1 Padding 的 Bank Conflict。

REF

继续研究