想象你管理着一家拥有 32 个服务窗口的邮局。每天都有 32 人的旅行团同时进店,如果每个团友恰好走向不同的窗口,一分钟内全员办完;可如果导游刻意让一半人挤向同一个窗口,窗口前立刻排起长队,办完全团要花两倍甚至 32 倍的时间。GPU 的共享内存就是这家独特的“邮局”:32 个存储体(Bank)是 32 个窗口,每批同时到达的 32 个线程(一个 Warp)就是旅行团。当多个线程在同一时刻命中同一个存储体时,硬件只能把这次访问拆成串行处理,这便是 GPU 性能优化中赫赫有名的 Bank Conflict(存储体冲突)。本文带你从窗口排布规则出发,理解冲突如何发生、如何用“加一把椅子”的填充技巧化解排队,以及这些优化在 AI 工程中的真实收益。

岗位关联

理解 Bank Conflict 是 GPU 性能调优的必修课。无论是大模型推理、图像处理还是科学计算,这 32 个窗口的排兵布阵往往决定了内核是快 1 倍还是慢 32 倍。

目录
  1. 可视化演示
  2. 共享内存概述
  3. 存储体结构
  4. 冲突产生机制
  5. 冲突检测分析
  6. 填充优化策略
  7. 访问模式设计
  8. 性能影响评估
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示GPU共享内存32个Bank的访问模式、冲突检测与Padding优化策略,交互切换查看Bank Conflict核心机制与解决方案。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Bank访问:32线程与32 Bank映射 stride=1连续访问每线程命中不同Bank无冲突 stride=32所有线程命中同一Bank导致32路冲突 Bank = addr % 32 每个Bank每周期只能响应一个请求

共享内存概述

邮局的普通大厅(全局内存)在距离分拣台几百米外,顾客取一趟件要跑好几分钟;而共享内存是设在分拣台旁的“内部专柜”,营业员一伸手就能拿到当天要用的所有信件。在 CUDA 中,每个流多处理器(SM,相当于一座邮局分拣中心)都内置了一块这样的片上专柜——共享内存。

共享内存是一块可编程管理的片上存储,由同一个线程块内的所有线程共享。它的访问延迟通常在 1~30 个周期,而全局内存的延迟高达 200~800 个周期,带宽差距同样悬殊。由于容量很小(每个 SM 通常只有几十到一百多 KB),它一般用来存放“反复使用”的数据块,例如矩阵乘法中的分块、卷积中的输入窗口。

也正是因为它紧挨计算核心、以极快速度服务,硬件才用“窗口并行”的方式设计它的内部结构——32 个存储体同时工作。一旦访问方式设计不当,窗口就会排队,专柜的优势便荡然无存。这为后续的 Bank Conflict 埋下伏笔。

TglobalTshared≈200\displaystyle \frac{T_{\text{global}}}{T_{\text{shared}}} \approx 200

1. T_global:一次全局内存(显存)访问的延迟,通常在 200~800 个时钟周期之间,取决于是否命中缓存。

Tglobal≈200∼800 cyclesT_{\text{global}} \approx 200 \sim 800\ \text{cycles}

2. T_shared:一次共享内存访问的延迟,无冲突时约为 1 个周期,有冲突时随冲突路数增加。

Tshared≈1 cycle (无冲突)T_{\text{shared}} \approx 1\ \text{cycle (无冲突)}

3. 比值约 200:两者相差两个数量级,解释了所有“先拷到共享内存、再批量计算”优化手法的动机。

TglobalTshared≈200×\frac{T_{\text{global}}}{T_{\text{shared}}} \approx 200 \times

理解了这家“邮局”的布局后,接下来我们要走进内部,看看那 32 个窗口到底是怎么排列的——这决定了什么样的顾客流会排队,什么样的顾客流能畅通无阻。

存储体结构

走进邮局的内部专柜,你会发现柜台被分成 32 个并排的小窗口,每个窗口每次只接待一位携带 4 件行李的顾客。32 个窗口可以同时开工、互不干扰——这正是共享内存每秒能吞吐海量数据的关键。

硬件上,共享内存被均匀划分为 32 个存储体(Bank),每个存储体每周期可以提供 4 字节(一个 32 位字)的带宽。相邻的 4 字节字依次落在相邻的存储体上:第 0 个字在 Bank 0,第 1 个字在 Bank 1……第 31 个字在 Bank 31,第 32 个字又回到 Bank 0,如此循环。

一个 warp(32 个线程组成的队列)同时发出共享内存访问请求时,硬件期望 32 个请求分别落在 32 个不同窗口,这样一拍就能全部完成。而“落在哪个窗口”完全由地址决定:

bank=⌊byte_addr4⌋ mod 32\displaystyle \text{bank} = \left\lfloor \frac{\text{byte\_addr}}{4} \right\rfloor \bmod 32

1. byte_addr:共享内存访问的字节地址,由编译器根据数组下标和偏移计算得出。

byte_addr=base+offset\text{byte\_addr} = \text{base} + \text{offset}

2. 除以 4 取整:每个存储体管理 4 字节,因此先确定是第几个“字”。

word_addr=⌊byte_addr4⌋\text{word\_addr} = \left\lfloor \frac{\text{byte\_addr}}{4} \right\rfloor

3. 对 32 取模:32 个存储体循环编号,取模结果就是目标窗口的号码。

bank=word_addr mod 32\text{bank} = \text{word\_addr} \bmod 32

现在你手里有了“号码牌规则”:每个顾客(线程)进门时,邮局根据他的票号(地址)算出他该去 0~31 号中的哪个窗口。接下来最有趣的场景来了——当多个顾客被分到同一个窗口时,柜台前会发生什么?

冲突产生机制

假设导游把 32 个团友中的 16 人指向 8 号窗口,剩下 16 人指向 16 号窗口。虽然 32 个团友同时进店,但 8 号窗口前瞬间排起 16 人的长队,营业员只能一个一个办——这就是排队,也就是存储体冲突。

当一个 warp 内有两个或更多线程访问不同地址但映射到同一个存储体时,硬件无法在一个周期内同时服务它们,只能把这些访问拆成多个周期串行完成。若最多有 k 个线程挤向同一个存储体,则称发生了 k 路冲突(k-way conflict),本次访问需要 k 个周期。

唯一例外是广播:当多个线程访问完全相同的地址时,硬件会一次性把该数据“广播”给所有请求线程,只需 1 个周期,不算冲突。冲突路数的通用计算方法是对每个存储体统计被访问次数,取最大值:

cycles=max⁡b∈[0,31]count(b)\displaystyle \text{cycles} = \max_{b \in [0,31]} \mathrm{count}(b)

其中 count(b) 表示该 warp 中映射到存储体 b 的线程数。

1. count(b):遍历 warp 内 32 个线程的地址,统计每个存储体被多少个线程访问。

count(b)=∑t=0311[ bank(addrt)=b ]\mathrm{count}(b) = \sum_{t=0}^{31} \mathbb{1}\big[\, \text{bank}(addr_t) = b \,\big]

2. 对 b 取最大值:最拥挤的窗口决定整批访问的总耗时,其他窗口再空闲也要等它。

cycles=max⁡bcount(b)\text{cycles} = \max_{b} \mathrm{count}(b)

3. 冲突路数:最大值为 2 时是 2 路冲突(2 个周期),为 32 时是 32 路冲突(32 个周期)。

k-way conflict⇒k cyclesk\text{-way conflict} \Rightarrow k\ \text{cycles}

冲突的本质是“顾客扎堆”。现在我们已经能算出任何访问模式要花几个周期,下一步要学会提前发现哪个窗口会排队——就像邮局经理在高峰前查看预约单。

冲突检测分析

聪明的邮局经理不会等顾客挤爆柜台才补救,他会对着预约单提前画一张“窗口分布图”:每个时段谁会来、去几号窗,一目了然。GPU 开发者同样可以手工推算,或借助性能分析工具自动扫描。

手工检测的思路很朴素:写出每个线程的地址,代入 bank 公式,检查每个存储体上是否扎堆。更常见的是用 Nsight Compute 这类剖析工具,它们会在内核运行后直接输出共享内存冲突的统计指标,例如 l1tex__data_bank_conflicts_pipe_lsu_mem_shared_op_ld.sum,直接告诉你发生了多少次冲突。

性能分析中通常用“潜在加速比”衡量优化空间:如果当前访问模式有 m 路冲突,那么消除冲突后,该访问操作最多能快多少倍:

speedup=max⁡bcount(b)1=max⁡bcount(b)\displaystyle \text{speedup} = \frac{\max_b \mathrm{count}(b)}{1} = \max_b \mathrm{count}(b)

1. 分子(当前实际周期):由最拥挤的窗口决定,写入公式即 max_b count(b)。

current_cycles=max⁡bcount(b)\text{current\_cycles} = \max_b \mathrm{count}(b)

2. 分母(理想周期数):无冲突时一次 warp 访问只需 1 个周期。

ideal_cycles=1\text{ideal\_cycles} = 1

3. 比值即加速空间:两者相除,得到消除冲突后可获得的倍速提升。

speedup=max⁡bcount(b)1\text{speedup} = \frac{\max_b \mathrm{count}(b)}{1}

检测只是第一步。真正精彩的部分在于“整改”:既然窗口排布固定,我们能不能通过调整顾客的取号规则,让每批 32 个顾客天然错开?这就是下一节要讲的填充策略。

填充优化策略

邮局发现一个奇怪现象:如果 32 个团友拿到的号分别是 1、33、65、97……这些号虽然不同,但除以 32 取余后全是 1 号窗口!经理灵机一动:把每个团友的号码牌整体加一个“偏移”,让余数从此错开——这正是“填充(Padding)”的智慧。

最常见的冲突场景是读矩阵的同一列。32x32 的 float 矩阵按行存储,第 r 行第 c 列的字地址是 r×32 + c。当 32 个线程同时读取第 c 列时,地址对 32 取模后恒等于 c——全部挤在同一个 Bank,形成 32 路冲突。

解决办法是把每行从 32 个元素扩展到 33 个(多存一个用不上的“填充元素”)。此时地址变为 r×(32+1) + c,而 33 mod 32 = 1,因此 bank = (r + c) mod 32。同一列不同行的元素,其存储体编号各不相同——冲突烟消云散。一般地:

bank(r,c)=(r⋅(cols+1)+c) mod 32\displaystyle \text{bank}(r,c) = \big( r \cdot (\text{cols} + 1) + c \big) \bmod 32

1. cols + 1 行宽:在每行末尾多分配一个元素的空间,实际数据仍只使用前 cols 个。

padded_cols=cols+1\text{padded\_cols} = \text{cols} + 1

2. r × (cols+1) 行偏移:计算第 r 行起始位置时使用填充后的行宽,使每行起点整体右移。

row_start(r)=r⋅(cols+1)\text{row\_start}(r) = r \cdot (\text{cols} + 1)

3. 加 c 再取模:得到元素的字地址并对 32 取模。当 cols=32 时,cols+1 ≡ 1 (mod 32),同一列不同行的 bank 依次错开。

bank(r,c)=(r⋅33+c) mod 32=(r+c) mod 32\text{bank}(r,c) = \big( r \cdot 33 + c \big) \bmod 32 = (r + c) \bmod 32

填充策略只花了一个“空座位”的代价,就让每批顾客均匀散开到 32 个窗口。不过这招不是万能的——下面我们看看还有哪些访问模式天然安全,哪些暗藏风险。

访问模式设计

邮局经理总结出了顾客流的“安全清单”:团友们按顺序 0、1、2……31 号取票时,刚好一人一个窗口;但如果号牌是 0、2、4、6……,所有偶数号才会去偶数窗口,奇数窗口全部空置——这就是跨步访问的陷阱。

用 stride 表示线程 i 访问的字地址偏移:addr_i = base + i × stride。stride=1 时 32 个线程访问连续 32 个字,恰好覆盖 32 个窗口,无冲突;stride=2 时每个窗口有 2 人排队,出现 2 路冲突;stride 是 4、8、16 时冲突成倍加剧。

更一般地,当 stride 与 32 互质时,i×stride 对 32 取模会遍历 0~31 全部余数,必然无冲突;当 stride 是 2 的幂时,所有线程只落在部分窗口,冲突最严重。此外,所有线程访问同一地址的“广播模式”也安全。设计内核时,应尽量让线程的地址映射是“一人一窗”:

gcd⁡(stride, 32)=1  ⟹  无存储体冲突\displaystyle \gcd(\text{stride},\,32) = 1 \;\Longrightarrow\; \text{无存储体冲突}

1. stride 定义:第 i 个线程访问的地址相对于基准地址的偏移。

addri=base+i×stride\text{addr}_i = \text{base} + i \times \text{stride}

2. 取模后的余数序列:bank_i = (i × stride) mod 32,问题转化为“stride 的倍数能否覆盖全部余数”。

banki=(i×stride) mod 32\text{bank}_i = (i \times \text{stride}) \bmod 32

3. 互质判定:数论中,当且仅当 gcd(stride, 32) = 1 时,序列 {i×stride mod 32} 恰好是 0~31 的一个排列。

gcd⁡(stride,32)=1  ⟺  { i⋅stride mod 32∣i=0..31 }={0,1,…,31}\gcd(\text{stride}, 32) = 1 \iff \{\,i \cdot \text{stride} \bmod 32 \mid i=0..31\,\} = \{0,1,\dots,31\}

有了这张“安全清单”,开发者就能像导演安排入场顺序一样,把线程与数据的映射关系调整到最佳。最后,让我们用真实数据感受一下:消除冲突到底能带来多大的性能收益。

性能影响评估

邮局经理做了个实验:让 A 组按“一人一窗”的取号方式进来,1 分钟办完全团;让 B 组全部挤向 1 号窗口,32 分钟才办完。同样的顾客、同样的业务,只因取号方式不同,效率差了 32 倍——这就是 Bank Conflict 对 GPU 内核的真实杀伤力。

共享内存的带宽峰值很高,但那是无冲突时的理想值。一旦发生 k 路冲突,实际带宽直接除以 k。在矩阵乘、卷积、归约等频繁访问共享内存的算法中,一次 32 路冲突就可能让内核整体慢 3~10 倍,因为共享内存访问往往是流水线的瓶颈环节。

实际工程中,一个典型的矩阵转置内核,用填充把共享内存从 32x32 扩到 32x33 后,实测吞吐量可提升 2~8 倍;在需要反复读取共享内存的 FlashAttention 类内核中,消除冲突同样能带来可观的端到端加速。量化公式为:

effective_BW=peak_BWmax⁡bcount(b)\displaystyle \text{effective\_BW} = \frac{\text{peak\_BW}}{\max_b \mathrm{count}(b)}

1. peak_BW:无冲突时共享内存的理论峰值带宽,由 32 个存储体并行提供。

peak_BW=32×bank_width per cycle\text{peak\_BW} = 32 \times \text{bank\_width per cycle}

2. 除以 max count:冲突导致同一周期的有效请求数减少,带宽按冲突路数线性下降。

effective_BW=peak_BWk(k=冲突路数)\text{effective\_BW} = \frac{\text{peak\_BW}}{k}\quad(k=\text{冲突路数})

3. 线性关系:k=1 时达到峰值,k=32 时仅剩 1/32 的带宽。

speedup=k前k后\text{speedup} = \frac{k_{\text{前}}}{k_{\text{后}}}

至此我们完成了从“认识 32 个窗口”到“用数据量化排队损失”的完整旅程。下一个维度,我们把理论放回真实的 AI 工程现场,看看这些窗口艺术每天都在哪里上演。

AI 应用场景

场景一:大模型推理中的 FlashAttention 内核

阶段:模型推理优化

解决的问题:注意力计算需要频繁读写共享内存中的 Q/K/V 分块;若线程访问模式的 stride 选择不当,Bank Conflict 会让共享内存带宽骤降,推理延迟明显上升。通过调整数据布局与填充,可以让每个 warp 的访问都“一人一窗”。

具体案例:在某大模型的 KV-Cache 分块读取中,原本按行读取 64x64 的 K 矩阵时发生 2 路冲突;把共享内存行宽从 64 填充到 65 后,冲突归零,Attention 内核端到端延迟降低约 22%。

场景二:深度学习数据预处理中的图像转置

阶段:数据加载与预处理

解决的问题:图像 HWC 转 CHW、数据增强时大量使用矩阵转置。朴素实现把原图块读入共享内存再转置写出,若行列宽度是 32 的倍数,读列时必然发生 32 路冲突;填充一行后立即消除。

具体案例:一个批量图像转置内核,将 tile 从 32x32 填充为 32x33 后,整体吞吐从 14 GB/s 提升到 52 GB/s,数据管线的预处理时长缩短近 3 倍。

场景三:K-Means 聚类算法的距离计算

阶段:传统机器学习算法 GPU 化

解决的问题:K-Means 每轮迭代要计算每个样本到 K 个中心的距离,中心点矩阵通常放在共享内存中。若中心点按 32 的倍数对齐存储,所有线程访问同一编号中心时会产生冲突;对中心点数组做 padding 即可让访问均匀化。

具体案例:在 100 万样本、K=128 的聚类任务中,为共享内存中的中心点数组每行多加一个 float 的填充后,每轮迭代时间从 3.2 ms 降到 1.1 ms,整体加速约 2.9 倍。

Python 演示

python49 行
#!/usr/bin/env python3"""Bank Conflict simulator: demo shared memory conflicts with 32 post-office windows"""  class BankSimulator:    """Simulate 32 shared-memory banks (windows)."""     def __init__(self, num_banks=32):        self.num_banks = num_banks     def warp_access(self, word_addrs):        """A warp of 32 threads accesses 32 word addresses in parallel.        Return (cycles, loads per bank)."""        assert len(word_addrs) == 32        loads = [0] * self.num_banks        for addr in word_addrs:            bank = addr % self.num_banks            loads[bank] += 1        return max(loads), loads     def matrix_column_read(self, col, rows=32, cols=32, pad=0):        """32 threads read the same column of a row-major matrix.        pad means extra elements appended to each row."""        padded = cols + pad        addrs = [r * padded + col for r in range(rows)]        return self.warp_access(addrs)  def main():    sim = BankSimulator()     print("Experiment 1: 32x32 matrix, 32 threads read column 5")    cycles_no, _ = sim.matrix_column_read(col=5, pad=0)    cycles_yes, _ = sim.matrix_column_read(col=5, pad=1)    print("  without padding (32 cols): {} cycles".format(cycles_no))    print("  with padding    (33 cols): {} cycles".format(cycles_yes))    print("  speedup: {:.1f}x".format(cycles_no / cycles_yes))     print("\nExperiment 2: stride access patterns")    for stride in [1, 2, 3, 8, 16, 32]:        addrs = [i * stride for i in range(32)]        cycles, loads = sim.warp_access(addrs)        flag = "" if cycles == 1 else " -> BANK CONFLICT!"        print("  stride={:>2}: {:>2} cycles{}".format(stride, cycles, flag))        print("           max bank load = {}".format(max(loads)))  if __name__ == "__main__":    main()
目的:用 Python 模拟 GPU 共享内存的 32 个存储体,量化不同访问模式(矩阵列读取、跨步访问)下的冲突路数与周期数,直观验证填充策略的效果。
  1. bank = addr % self.num_banks核心的“取号规则”:地址对 32 取模,决定顾客去几号窗口。
  2. loads[bank] += 1统计每个窗口前排队的人数,为最终周期数做铺垫。
  3. return max(loads), loads最忙窗口的排队人数就是本次访问的周期数。
  4. padded = cols + pad填充策略:每行多留一个空位,改变后续所有行的起点。
  5. addrs = [r * padded + col for r in range(rows)]模拟 32 个线程同时读矩阵第 col 列。
  6. flag = "" if cycles == 1 else " -> BANK CONFLICT!"自动识别有冲突的模式。
  7. print(" speedup: {:.1f}x".format(cycles_no / cycles_yes))直接输出填充带来的加速比,一眼看清收益。

常见误区

误区只要多个线程访问同一个 Bank,就一定会发生存储体冲突,哪怕是访问完全相同的地址。

正解硬件对“同一地址”有专门的广播机制:当多个线程请求同一个字时,存储体会用一拍把数据同时分发给所有请求线程,既不排队也不串行,因此不算冲突。只有“不同地址落在同一 Bank”才构成冲突。用邮局比喻:多位团友到同一个窗口取同一封信,营业员可以复印分发;只有取不同的信才需要排队。

为何常见:初学者容易把“同一 Bank”与“同一地址”混为一谈,忽略了地址映射的粒度差异。

误区既然填充能消除矩阵列访问的冲突,那不管什么访问模式,把数组每行加长一列就万事大吉。

正解填充只对“行宽是 32 的倍数、导致同列元素同 Bank”这类模式有效。若访问本身是跨步 2 的幂,或者填充后的宽度仍然是 32 的倍数(如 63→64),冲突依然存在甚至更隐蔽。正确的做法是结合公式 bank=(r×width+c)%32 逐模式核算,而非无脑加一。

为何常见:大家记住了"32→33"这个经典案例,却没有理解其背后的取模错位原理,于是把特例当成了通则。

误区全局内存也有多个通道,访问不连续时也慢,这和 Bank Conflict 是一回事。

正解Bank Conflict 严格特指共享内存中的存储体并行访问冲突;全局内存的性能问题对应另一个概念——内存合并(Memory Coalescing),两者发生的位置和优化手段完全不同。邮局比喻里,共享内存是“店内 32 窗口”,全局内存是“马路对面的仓库”,排队规则各有各的逻辑。

为何常见:两者都是“访存模式影响性能”,很容易被初学者张冠李戴。

误区只要访问模式算出是 32 路冲突,这个内核就没救了,只能靠换硬件。

正解32 路冲突只代表“当前这一条访问指令”慢 32 倍,并不代表整个内核慢 32 倍——共享内存访问可能只占内核总时间的一部分。而且通过填充、改变线程与数据的映射、用广播模式、拆分为多个 warp 分批访问等手段,冲突往往能被显著缓解。实际优化中,先 profiling 找到热点,再对症下药。

为何常见:初学者把“局部指令级损失”直接等同于“全局性能损失”,忽略了瓶颈占比的工程常识。

面试题

Q1. 在主流 NVIDIA GPU 中,共享内存被划分为多少个存储体(Bank)?

单选题 · easy

  • A.8 个存储体
  • B.16 个存储体
  • C.32 个存储体← 正确答案
  • D.64 个存储体

答案:C。共享内存通常被划分为 32 个存储体,对应一个 warp 的 32 个线程,每个存储体每周期提供 4 字节带宽。用邮局比喻就是 32 个并排服务窗口,32 人旅行团一人一窗时一拍办完。

Q2. 一个 warp 内 32 个线程同时访问共享内存中完全相同的地址,会发生什么?

单选题 · easy

  • A.32 路冲突需 32 周期
  • B.广播机制一拍全分发← 正确答案
  • C.未定义行为随机结果
  • D.编译器直接拒绝编译

答案:B。硬件支持广播机制,同一地址的数据可以一拍分发给所有请求线程,不算冲突。就像 32 个团友到同一窗口领取同一封信,营业员复印分发即可,无需排队。

Q3. 一个 32x32 的 float 矩阵按行存储于共享内存,32 个线程同时读取第 5 列所有元素,会发生几路冲突?

单选题 · mid

  • A.无冲突一个周期完成
  • B.2 路冲突两个周期完成
  • C.8 路冲突八个周期完成
  • D.32 路冲突三十二个周期← 正确答案

答案:D。第 r 行的第 5 列字地址为 r×32+5,对 32 取模恒等于 5,32 个线程全部挤向 Bank 5,形成 32 路冲突,需要 32 个周期。这正是“同列元素同窗口”的经典场景。

Q4. 将上题矩阵填充为 32x33 后,再次让 32 个线程读取第 5 列,冲突情况如何?

单选题 · mid

  • A.仍是 32 路冲突不减少
  • B.冲突消失变无冲突状态← 正确答案
  • C.冲突减轻为 2 路冲突
  • D.冲突减轻为 8 路冲突

答案:B。填充后地址为 r×33+5,由于 33≡1 (mod 32),bank=(r+5) mod 32,32 行对应 32 个不同 Bank,恰好一人一窗,无冲突。这是“列数加一”填充策略的典型效果。

Q5. 某 warp 内线程 i 访问的字地址为 i×8(stride=8),该访问会发生几路冲突?

单选题 · mid

  • A.无冲突一个周期完成
  • B.2 路冲突两个周期完成
  • C.4 路冲突四个周期完成
  • D.8 路冲突八个周期完成← 正确答案

答案:D。i×8 mod 32 的值只有 0、8、16、24 四种,每个 Bank 被 8 个线程访问,因此是 8 路冲突。stride 是 2 的幂时,线程只会落在部分 Bank 上,跨步越大,覆盖窗口越稀疏。

Q6. 以下哪些方法可以有效缓解或避免存储体冲突?

多选题 · mid

  • A.每行末尾额外填充一个元素← 正确答案
  • B.跨步改为与 32 互质奇数← 正确答案
  • C.调整线程与数据块映射← 正确答案
  • D.提升 GPU 核心频率

答案:ABC。填充让行起点错位(A),互质跨步保证覆盖全部 Bank(B),调整映射可让线程按“一人一窗”分配(C),三者都是经典手段。提高主频并不能改变窗口分配规则,无法消除冲突。

Q7. 线程 i 访问字地址 i×3(stride=3)时,32 个线程的冲突情况是?

单选题 · hard

  • A.无冲突一人一窗分配← 正确答案
  • B.2 路冲突两人排队等待
  • C.4 路冲突四人排队等待
  • D.8 路冲突八人排队等待

答案:A。3 与 32 互质,i×3 mod 32(i=0..31)恰好是 0~31 的一个排列,每个 Bank 恰好被访问一次,故无冲突。互质跨步是设计访问模式时避免冲突的核心判据。

Q8. 某共享内存访问发生 k 路冲突,则该访问消耗的周期数约为?

单选题 · hard

  • A.1 个周期一拍全部完成
  • B.k 个周期依次排队服务← 正确答案
  • C.32/k 个周期轮转服务
  • D.固定消耗 32 个周期

答案:B。冲突路数 k 表示最忙 Bank 前排队人数,硬件一次只能服务一个人,故需要 k 个周期完成整批访问。这与“最忙窗口决定整团耗时”的邮局逻辑一致。

Q9. 关于 Bank Conflict,以下说法正确的是?

多选题 · hard

  • A.只发生在共享内存访问← 正确答案
  • B.全局内存也会发生冲突
  • C.同 warp 同 Bank 不同地址← 正确答案
  • D.不同 warp 间互相冲突

答案:AC。Bank Conflict 严格发生在共享内存的同一 warp 内(A、C 正确);全局内存对应的概念是内存合并,不叫 Bank Conflict(B 错误);不同 warp 的访问由硬件分开调度,互不产生冲突(D 错误)。

Q10. 共享内存中每个存储体每个周期最多提供多少字节的带宽?

单选题 · mid

  • A.每周期提供 1 字节
  • B.每周期提供 4 字节← 正确答案
  • C.每周期提供 16 字节
  • D.每周期提供 32 字节

答案:B。每个存储体每周期服务一个 32 位字,即 4 字节。32 个存储体并行就是每周期最多 128 字节,这正是共享内存高带宽的来源:窗口虽小,但 32 个同时开工。