想象你是一家超级快递分拣中心的总调度。每天清晨,上百万件包裹如同潮水般涌进仓库。如果只靠几十名员工按顺序一件件地手递手分拣,这个中心早就瘫痪了。于是你设计了这样的架构:整个中心(Grid)被划分成许多条独立的流水线(Block),每条流水线上站着若干名分拣员(Thread)。包裹到达后,每位分拣员在同一时刻取下自己负责的那件,各自扫描、各自归类,互不等待。GPU 做并行计算时,上演的正是这场“万人同时开工”的画面。CUDA 中,一次计算任务被组织成一个 Grid,Grid 里包含多个 Block,Block 里又包含成百上千个 Thread——它们是真正执行运算的“分拣员”。这篇文章将带你在分拣中心的场景中,彻底看懂 Grid/Block/Thread 的职能划分、寻址方式和协作规则。

岗位关联

对 AI 从业者而言,理解线程层次是优化 GPU 内核、读懂大模型推理框架(如 vLLM、DeepSpeed)底层改造的根基。它决定了你的显存带宽能否跑满、训练吞吐能否再翻一番。

目录
  1. 可视化演示
  2. 核心概念
  3. 层次结构
  4. 线程索引
  5. 并行配置
  6. 共享协作
  7. 性能调优
  8. 应用实践
  9. 常见问题
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

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

核心概念

分拣中心的三个层级,今天要正式对号入座。整个分拣中心(Grid)是一个大集合:它接手一次计算任务的全部数据,就像中心要消化当天涌入的所有包裹。中心里有许多条流水线(Block),每条流水线是一个独立的小组,负责一部分包裹。流水线上的每一位分拣员(Thread)才是真正弯下腰干活的人——单个包裹的扫码、归类、搬运,都靠他们完成。

搬到 CUDA 的世界里,这三个词的含义完全对应。Thread 是 GPU 上执行计算的最小单位,每个线程运行同一个函数(这个函数叫 kernel,可以想象成一份统一的“分拣动作手册”),但作用于不同的数据。Block 是一组线程的集合,通常由几十到几百个线程组成,组内线程可以协作。Grid 则是所有 Block 的集合,代表整个计算任务的整体配置。

一维情况下,总线程数很容易算出来:网格中的块数乘以每块中的线程数。

回到分拣中心的画面:Grid、Block、Thread 不是三个抽象名词,而是从“整体任务”到“局部小组”再到“单个工人”的逐级分工。理解这层嵌套,后面所有索引计算和性能分析都从这三者出发。

Ntotal=gridDim.x×blockDim.x\displaystyle N_{\text{total}} = \text{gridDim}.x \times \text{blockDim}.x
这个公式相当于“流水线条数 × 每条线上的人数 = 分拣员总数”。
  1. blockDim.x一个块内包含多少线程,即一条流水线上的分拣员人数。
    blockDim.x\text{blockDim}.x
  2. gridDim.x网格里有多少个块,即分拣中心的流水线条数。
    gridDim.x\text{gridDim}.x
  3. 两者相乘总线程数,即这次任务总共动员了多少名分拣员。
    gridDim.x×blockDim.x\text{gridDim}.x \times \text{blockDim}.x

层次结构

分拣中心不是把员工随机扔进仓库就完事。总调度会把中心划分成若干个作业区(对应 GPU 上的 SM,流式多处理器),每条流水线(Block)会被整体分配到某个作业区里运行。作业区里再细分为 32 人一组的小队(Warp),硬件真正派工的最小单位就是这个小队。

三层结构的包含关系非常严格:Grid 包含若干 Block,Block 包含若干 Thread。Thread 不会被 Grid 直接管理,就像分拣员从来只听自己那条线的组长指挥,不会直接去找总调度。这层“中间层”的意义在于:Block 是软件与硬件之间的接驳点。GPU 的 SM 以 Block 为单位接收任务,一个 SM 可以同时驻留多个 Block,但同一个 Block 不会被拆到两个 SM 上。

你可能会问:为什么硬件调度的最小单位是 Warp 这个 32 人小队,而不是单个 Thread?因为 GPU 的硬件设计是“同进同出”式:一条 warp 的 32 个线程总是被一起派发到同一个执行单元上,执行同一条指令。就像一队分拣员接到命令“全体向左转两步”,32 个人必须同时做出这个动作。理解这一点,对后面“分支发散”的坑有直接的帮助。

从分拣中心的角度看,整个布局就是一张组织架构图:中心管流水线,流水线管小队,小队管个人。Block 是沟通软件网格与硬件作业区的“集装箱”,Warp 则是硬件真正端上桌的“一盘菜”。

线程索引

100 名分拣员同时开工,总调度怎么知道“编号 30 的那位”站在哪条流水线、哪个工位上?反过来,每个分拣员又怎么知道该取传送带上的第几件包裹?答案是一套固定的编号规则。CUDA 里每个线程都有两个身份:它在哪个块(blockIdx),以及它在块内的第几个位置(threadIdx)。

一维场景下,全局 ID 的计算公式是 CUDA 编程的“第一公式”:

扩展到二维时,规则是“外层块号 × 内层维度 + 内层块号”。例如二维网格中,每个线程在垂直方向的全局行号为 blockIdx.y * blockDim.y + threadIdx.y。CUDA 按行主序展开所有维度,理解了行主序,任何维度的全局编号都能拆解出来。

分拣中心里的每位分拣员都拿着一张工牌:上面写着“流水线号 + 工位号”,两者加在一起就是全局工号。查表时顺着工号就能精确定位到人,这就是线程索引的全部意义。

global_id=blockIdx.x×blockDim.x+threadIdx.x\displaystyle \text{global\_id} = \text{blockIdx}.x \times \text{blockDim}.x + \text{threadIdx}.x
这个公式等价于:先算出前面所有流水线上的人数总和,再加上自己在当前线上的排位。
  1. blockIdx.x当前块在整个网格中的编号,从 0 开始。好比第 2 条流水线的编号是 1。
    blockIdx.x\text{blockIdx}.x
  2. blockDim.x每个块里有多少线程,即每条流水线的人数。
    blockDim.x\text{blockDim}.x
  3. threadIdx.x当前线程在块内的编号,从 0 开始。好比一条流水线上第 7 位分拣员的编号是 6。
    threadIdx.x\text{threadIdx}.x
  4. 完整表达式先算出排在当前块之前的所有块的线程总数,再加上块内偏移,得到全局唯一编号。
    blockIdx.x×blockDim.x+threadIdx.x\text{blockIdx}.x \times \text{blockDim}.x + \text{threadIdx}.x

并行配置

分拣中心的调度员在开工前要拍板两件事:开几条流水线(gridDim),每条流水线站几个人(blockDim)。这两组数字一旦定下来,整个分拣流程的“体能”就基本确定了。CUDA 也一样,配置线程层次是每次写 kernel 都要做出的核心决策。

先看网格设计。假设今天到了 1000 件包裹,每条流水线安排 256 名分拣员,那么至少需要几条流水线?答案是向上取整:

块大小也有讲究。GPU 硬件按 32 线程一个 Warp 来调度,所以 blockDim.x 最好取 32 的整数倍(如 128、256),让调度不出现“半队”的尴尬。同时每个 SM 的线程容量有限,blockDim 过大(如 2048)会让单个 Block 占满整个 SM,导致资源碎片化。

还有一个隐蔽但致命的坑:1000 件货、256 人一条线,4 条线就是 1024 个工位,多出的 24 个工位是空的。这些空工位上的“虚拟分拣员”也会执行代码,所以必须用 if (global_id < N) 保护数据访问,否则他们会抢着去读写不存在的第 1001 件包裹。

调度员拍板之后,流水线开工。配置的好坏直接决定了这班人马是满负荷运转,还是有一半人在闲置聊天。这就是下一节要聊的协作与性能问题。

gridDim.x=⌈NblockDim.x⌉\displaystyle \text{gridDim}.x = \left\lceil \frac{N}{\text{blockDim}.x} \right\rceil
就像不能开半条流水线,网格大小必须是整数,多出来的零头靠“开一条能装下的线”来兜底。
  1. N待处理的总数据量,例如 1000 件包裹。
    NN
  2. blockDim.x每条流水线配置的线程数,例如 256 人。
    blockDim.x\text{blockDim}.x
  3. 除法 $\frac{N}{\text{blockDim}.x}$理论上需要的流水线条数。
    NblockDim.x\frac{N}{\text{blockDim}.x}
  4. 上取整 $\lceil \cdot \rceil$补齐小数部分,保证流水线数量足以覆盖全部数据。
    ⌈NblockDim.x⌉\left\lceil \frac{N}{\text{blockDim}.x} \right\rceil

共享协作

同一条流水线上的分拣员不是孤岛。传送带旁边有一张公共工作台(对应共享内存 shared),几个人可以顺手把自己刚扫完的包裹放到台上,让旁边的人核对。这张台子只有本流水线的人能用,别的流水线碰都碰不到。CUDA 里 Block 内的线程同样共享一块显存缓冲区,这就是 shared 声明的共享内存,它比全局内存快得多,是块内数据交换的主通道。

有共享就有一致性问题。假设所有分拣员约定“先把包裹放上工作台,再一起去搬下一批”,如果有人动作慢,把包裹放上台的动作还没完成,别人就开始搬下一批,台面就会乱套。于是组长喊一嗓子“同步!”——所有人放下手里的活,等最后一个伙伴放完包裹,再整齐地开始下一轮。CUDA 里这一嗓子就是 __syncthreads()。它保证块内所有线程都执行到这里之后,才继续往下走。

但注意:同步边界严格划在 Block 内部。不同流水线之间没有公共工作台,也没有人对另一条线的进度负责。GPU 的 kernel 设计中,Block 之间被刻意设计为相互隔离——你想让 Block 0 和 Block 1 的线程互相等待,在同一个 kernel 里是做不到的。跨块协作只能通过“结束整个 kernel、再启动下一个 kernel”来完成。

分拣中心的运营哲学很简单:小团队内部高度协作,团队之间彻底解耦。这种设计让 GPU 可以任意扩展流水线数量,而不必担心跨团队同步带来的等待开销。理解了这条边界,你在设计算法时就会自觉地让每个 Block 只负责自己的一亩三分地。

性能调优

同样是开 8 条流水线,为什么有的分拣中心效率是别人的两倍?关键在于每个作业区(SM)实际“上座”了多少人。如果某个作业区只坐了 20 人,剩下 12 个工位空着,资源就浪费了。CUDA 里把这个上座率叫做占用率:

占用率不是越高越好,但它是首要的体检指标。影响它的因素包括每个线程用了多少寄存器、每个 Block 占了多少共享内存、Block 大小是否合适。线程太多会导致寄存器不够用,GPU 只能把数据挤到本地内存,反而变慢。

另一个常见性能杀手是分支发散。一个 Warp 里的 32 个线程执行同一条指令,如果代码里出现 if (id % 2 == 0) 这样的分支,一半线程走 A 路,另一半走 B 路,硬件只能分两轮执行。就像一队 32 人的分拣小队收到命令后,16 人往左 16 人往右,队长只能分两次带队。优化技巧是把分支条件设计成按 Warp 对齐,让同一小队的线程统一走一边。

负载均衡同样重要。如果 Block 0 的数据特别重、Block 1 的数据特别轻,先干完的 SM 只能闲着等人。设计数据结构时尽量让每个 Block 分到的任务量相当,分拣中心的每一条线才能在同一时间收工,整个中心的吞吐才能最大化。

occupancy=active_warpsmax_warps_per_SM\displaystyle \text{occupancy} = \frac{\text{active\_warps}}{\text{max\_warps\_per\_SM}}
占用率度量的是 SM 上有多少 Warp 小队真正驻扎,类似影院入座率。
  1. active_warps当前实际驻留在 SM 上的 Warp 数量,相当于已入座的小队数。
    active_warps\text{active\_warps}
  2. max_warps_per_SM该 SM 硬件最多能容纳的 Warp 数,相当于影院的座位总数。
    max_warps_per_SM\text{max\_warps\_per\_SM}
  3. 占用率两者之比,用来判断硬件资源被利用了多少。
    active_warpsmax_warps_per_SM\frac{\text{active\_warps}}{\text{max\_warps\_per\_SM}}

应用实践

理论讲完,看分拣中心到底接哪些活。最常见的活是“三连操作”:把两个数组的对应元素相加。CUDA 里只需要让每个线程处理一个元素——第 i 个线程负责读 A[i] 和 B[i],相加后写回 C[i]。这正是向量加法的精髓:

进阶一点的是矩阵乘法。一个线程负责计算结果矩阵中的一个元素,就需要把 A 的一行和 B 的一列做点积——这是大模型全连接层和注意力机制的底层算子。更常见的优化是把输出矩阵切成许多 16×16 的小块,每个 Block 负责一个小块,块内线程先把需要的 A、B 子块搬进共享内存,再反复复用,把全局内存访问量降到最低。

归约操作则是块内协作的经典例子:要把一整块数据求和,先让每个线程把自己那份累加进共享内存,再用类似“两两配对”的方式逐级合并,最后得出总和的局部结果。这就像分拣中心统计一天包裹量:每个分拣员先报自己那件的数量,组长把全组的数加起来,再把各组的数汇总给总调度。

这些模式覆盖了深度学习里绝大多数计算需求——向量加对应逐元素算子,矩阵乘对应全连接与注意力,归约对应 Softmax 分母、LayerNorm 统计量。理解了线程层次怎么编排,这些算子在 GPU 上的实现思路就一通百通。

C[i]=A[i]+B[i],i=blockIdx.x×blockDim.x+threadIdx.x\displaystyle C[i] = A[i] + B[i],\quad i = \text{blockIdx}.x \times \text{blockDim}.x + \text{threadIdx}.x
每个分拣员从两条传送带上各取一件包裹,合成一个新包裹放到输出带上。包裹编号 i 由全局线程 ID 决定。
  1. 全局下标 i由全局线程 ID 公式算出,保证每个线程独占一个数据位置。
    i=blockIdx.x×blockDim.x+threadIdx.xi = \text{blockIdx}.x \times \text{blockDim}.x + \text{threadIdx}.x
  2. 读取 A[i] 和 B[i]第 i 个线程从内存中取两个输入值。
    A[i], B[i]A[i],\ B[i]
  3. 求和并写回 C[i]将结果存到输出数组的相同位置。
    C[i]=A[i]+B[i]C[i] = A[i] + B[i]

常见问题

分拣中心运营久了,总会遇到几类教科书式的事故。第一种是越界访问。N 件包裹不是 blockDim 的整数倍时,向上取整会多出一些“空转线程”。如果忘了写 if (global_id < N) 这道保险,这些空转线程就会去读写数组末尾之外的内存,轻则读到垃圾值,重则程序崩溃。这是初学者最容易踩的坑,测试时数据量恰好整除,一上线就炸。

第二种是同步缺失。Block 内的多个线程需要协作时,如果只写共享内存而不调用 syncthreads(),就会出现“我还没写完、你已经来读”的竞态条件。更隐蔽的是在条件分支里调用 syncthreads()——比如只有编号为偶数的线程执行到同步点,奇数的绕道走了,两边永远等不到彼此,程序直接死锁。分拣中心的规则是:组长喊“同步”时必须所有人都在场,缺席一个人,全组就得永远等下去。

第三种是配置不当。blockDim 设得过大,单个 Block 的线程把 SM 资源占满,别的 Block 挤不进来,调度空间变小;设得过小,每个 Block 只有几十个线程,同步开销占比太高。推荐做法是让 blockDim 保持 128 到 512 之间的 32 的倍数,网格大小按照数据量上取整。这就像分拣中心每条线安排 128 人左右,既能高效协作,又不会让作业区拥挤到转不开身。

AI 应用场景

大规模矩阵乘法:大模型训练的引擎

阶段:模型训练与推理加速

解决的问题:Transformer 中的注意力得分计算、MLP 全连接层,本质上都是矩阵乘法 GEMM。传统 CPU 串行循环面对百亿级参数量会慢到无法接受;利用 GPU 线程层次,把输出矩阵切成子块,每个 Block 负责一块、每线程算一个元素,训练速度提升数百倍。

具体案例:GPT-3 一次前向传播需要执行数十万亿次浮点运算。在 NVIDIA A100 上,CUDA 将矩阵分成 128×128 的子块,每个 Block 加载子块到共享内存后反复复用,线程按 8×8 划分,让每个线程连续计算多个输出元素,把显存带宽利用率拉到 90% 以上。

向量检索中的相似度计算

阶段:RAG 检索与向量召回

解决的问题:RAG 系统里用户 query 需要与上百万条文档向量比对相似度。每条比对相互独立,是天然的并行任务。把向量库按 Block 切分,每个 Block 内的线程各自计算一个点积,再用块内归约求最大相似度,能实现毫秒级检索。

具体案例:FAISS 等向量检索库在 GPU 上执行时,将向量矩阵按行组织成 2D 网格:blockIdx.y 对应 query 编号,blockIdx.x 对应向量分片,threadIdx.x 负责累加部分点积。每个 Block 最终汇总出一个 Top-K 候选,完成后多 Kernel 接力排序。

图像卷积与池化加速

阶段:视觉模型前处理与推理

解决的问题:卷积核在每张图像的每个位置滑动计算,所有位置的计算互不依赖,天然适合 GPU 并行。将输出特征图的每个像素绑定一个线程,按 2D 网格组织块与线程,Block 内共享像素数据以降低重复读取全局内存的开销。

具体案例:ResNet-50 在推理解码 224×224 图像时,每个 Block 负责输出特征图的一个 16×16 小块,块内线程把对应输入区域连同边界像素一起加载到共享内存,之后每次卷积都从共享内存读数据。相比朴素实现,全局内存访问量减少约 90%。

Python 演示

python57 行
import math class CUDASimulator:    """用 Python 模拟 CUDA 的 Grid-Block-Thread 三层线程层次"""     def __init__(self, grid_dim, block_dim):        self.grid_dim = grid_dim          # (gridDim.x, gridDim.y)        self.block_dim = block_dim        # (blockDim.x, blockDim.y)        self.blocks = grid_dim[0] * grid_dim[1]        self.threads_per_block = block_dim[0] * block_dim[1]        self.total_threads = self.blocks * self.threads_per_block     def global_id(self, block_idx, thread_idx):        return block_idx * self.threads_per_block + thread_idx     def vector_add(self, A, B):        n = len(A)        C = [0] * n        for b in range(self.blocks):            for t in range(self.threads_per_block):                gid = self.global_id(b, t)                if gid < n:                    C[gid] = A[gid] + B[gid]        return C     def block_reduce_sum(self, data):        block_sums = [0] * self.blocks        for b in range(self.blocks):            for t in range(self.threads_per_block):                gid = self.global_id(b, t)                if gid < len(data):                    block_sums[b] += data[gid]        return sum(block_sums)     def locate(self, gid):        for b in range(self.blocks):            for t in range(self.threads_per_block):                if self.global_id(b, t) == gid:                    bx = b % self.grid_dim[0]                    by = b // self.grid_dim[0]                    tx = t % self.block_dim[0]                    ty = t // self.block_dim[0]                    return bx, by, tx, ty        return None  sim = CUDASimulator((3, 2), (4, 4))print("总线程数 =", sim.total_threads)print("定位全局 ID 30:", sim.locate(30)) A = list(range(1, 97))B = list(range(97, 193))C = sim.vector_add(A, B)print("vector_add: C[0] =", C[0], "C[95] =", C[95]) data = list(range(1, 97))print("归约求和 =", sim.block_reduce_sum(data), "(期望 4656)")
目的:用纯 Python 模拟 CUDA kernel 的线程层次与寻址逻辑,验证全局 ID 公式、向量加法和块内归约的过程。
  1. sim = CUDASimulator((3, 2), (4, 4))创建 3×2 的 Grid、每个 Block 4×4=16 线程,共 96 个线程。
  2. gid = self.global_id(b, t)用 blockIdx * blockDim + threadIdx 公式计算全局线程编号。
  3. if gid < n:边界保护,模拟 CUDA 中 if (global_id < N) 防止越界。
  4. block_sums[b] += data[gid]模拟块内归约:每个 Block 先累加自己的局部视图。
  5. sim.locate(30)反查全局 ID 30 对应的 blockIdx 与 threadIdx,验证索引映射关系。

常见误区

误区刚学 CUDA 时,我以为 blockIdx.x 就是线程的全局编号,直接用 blockIdx.x 访问数组下标,结果数据全错了。

正解blockIdx.x 只是“块”的编号,不是“线程”的编号。就像分拣中心里“3 号流水线”不代表某个具体分拣员;要精确定位一个人,必须同时知道他在哪条线(blockIdx)以及在这条线里的工位号(threadIdx)。全局地址必须用 blockIdx.x * blockDim.x + threadIdx.x 计算。

为何常见:一维场景下 blockIdx 看起来像一个连续编号,很容易让人忽略还有 thread 这一层维度。

误区我觉得 __syncthreads() 能同步整个 Grid 的所有线程,于是让不同 Block 之间互相等待,结果程序死锁。

正解__syncthreads() 只同步块内线程。块与块之间是设计上相互独立的,就像两条流水线各自换班,谁也不等谁。跨块协作必须通过结束当前 kernel、启动下一个 kernel 来完成。

为何常见:CPU 多线程编程中全局屏障是常见工具,把单机多线程的经验平移到了 GPU 上。

误区我把 blockDim 设成 1024 甚至 2048,觉得并行度越高就越快,结果性能反而不如 256。

正解GPU 的 SM 有寄存器和共享内存的上限。Block 太大时,单个 Block 的资源占用会挤掉同 SM 上其他 Block 的驻留空间,降低调度灵活性和占用率。通常推荐 blockDim 取 128–512 之间的 32 的倍数,给 SM 留出多 Block 交错的余地。

为何常见:直觉上“人多力量大”,忽略了硬件资源这一隐性天花板。

误区测试时 N 恰好是 blockDim 的整数倍,运行正常,我就把 if 判断省掉了,上线后数据量一变就崩。

正解边界检查是每次 kernel 都必须写的保险丝。N 不一定总能整除,网格向上取整后总会多出一些空转线程,它们会访问数组末尾之外的内存。分拣中心来 100 件货而流水线有 128 个工位,空的 28 个工位必须原地待命。

为何常见:测试集太完美掩盖了边界条件,生产环境的数据形状一变就暴露。

面试题

Q1. 一维网格中,一个线程的全局唯一索引如何计算?

单选题 · easy

  • A.blockIdx.x + threadIdx.x
  • B.blockIdx.x * blockDim.x + threadIdx.x← 正确答案
  • C.threadIdx.x * blockDim.x + blockIdx.x
  • D.blockIdx.x * threadIdx.x + blockDim.x

答案:B。全局索引 = 当前块之前所有块的线程总数 + 块内偏移。A 漏乘了 blockDim,C、D 把公式颠倒了。就好比“3 号流水线第 5 人”,应先算前 2 条线的总人数再加 5,而不是简单相加。

Q2. GPU 硬件调度线程时的最小执行单位是?

单选题 · easy

  • A.Thread
  • B.Block
  • C.Warp← 正确答案
  • D.Grid

答案:C。Warp 是 32 个线程组成的调度组团,硬件以 Warp 为单位派发指令。Thread 是软件概念,Block 是调度到 SM 的容器,Grid 是任务整体。只有 Warp 才是硬件真正端上桌的“一盘菜”。

Q3. 关于 Block 与 Thread 的关系,下列说法正确的有?

多选题 · mid

  • A.同一 Block 内线程可以通过共享内存通信← 正确答案
  • B.不同 Block 的线程可以在 kernel 内任意同步
  • C.Block 会被整体映射到某个 SM 上执行← 正确答案
  • D.Thread 由 Grid 直接管理和调度

答案:AC。Block 是共享内存和同步的边界,A 对;Block 间在 kernel 内无法同步,B 错;Block 是 SM 的调度单元,C 对;Thread 归属于 Block,不会越过 Block 被 Grid 直接管理,D 错。

Q4. 若 blockDim = (256, 1, 1),blockIdx.x = 3,threadIdx.x = 10,该线程的全局索引是?

单选题 · mid

  • A.266
  • B.778← 正确答案
  • C.768
  • D.1024

答案:B。代入公式 3 × 256 + 10 = 778。A 是误把 blockIdx 当线程数,C 是 blockIdx × blockDim 的结果但漏加 threadIdx,D 是 4 × 256 误算。就像 3 条满编流水线各 256 人,再加线上的第 10 人。

Q5. CUDA 中表示网格里块数量的内置变量是?

单选题 · mid

  • A.blockDim
  • B.gridDim← 正确答案
  • C.threadIdx
  • D.warpSize

答案:B。gridDim 保存网格各维度上的块数,blockDim 保存每块的线程数。threadIdx 是线程在块内的编号,warpSize 是固定值 32。它们分别对应流水线条数、每条线人数、工位号和小队规格。

Q6. 以下哪些因素会影响 kernel 的 SM 占用率?

多选题 · mid

  • A.每个线程使用的寄存器数量← 正确答案
  • B.每个 Block 占用的共享内存大小← 正确答案
  • C.Block 中的线程数量← 正确答案
  • D.编译器的产品名称

答案:ABC。寄存器、共享内存、线程数都决定了 SM 上能容纳多少 Warp,从而影响占用率;编译器名称与硬件资源无关。就像影院座位数不变,入座率只取决于占用多少人、多少空间。

Q7. 数据量 N=1000,blockDim.x=256,网格 blockDim 的 x 维至少应为?

单选题 · mid

  • A.3
  • B.4← 正确答案
  • C.5
  • D.2

答案:B。1000 / 256 = 3.906,向上取整为 4。3 条线只够 768 个线程,剩余 232 个数据无人处理;必须开 4 条线,多余 24 个线程用边界检查兜底。

Q8. 在 Warp 大小为 32 的 GPU 上,下列哪个 Block 大小最合适?

单选题 · hard

  • A.100
  • B.150
  • C.128← 正确答案
  • D.300

答案:C。128 = 4 × 32,是整数个 Warp,调度无浪费。100、150、300 都不能整除 32,会产生最后一个不满编的 Warp 小队,负载不均且浪费硬件槽位。

Q9. 关于 __syncthreads() 的说法,正确的有?

多选题 · hard

  • A.同步 Grid 内所有线程
  • B.同步 Block 内所有线程← 正确答案
  • C.要求块内所有线程都执行到该语句← 正确答案
  • D.在 if-else 的不同分支中调用可能死锁← 正确答案

答案:BCD。__syncthreads() 只同步块内线程,A 错 B 对。它要求全体到场,若只让部分线程在分支里执行它,另一部分线程永远等不到——这就是死锁,C、D 对。好比组长喊“集合”时有人没听见,全组就会一直等下去。

Q10. 设 gridDim=(4,4),blockDim=(16,16),某线程的 blockIdx=(2,1)、threadIdx=(3,7)。按行主序线性化后,该线程的全局索引是?

单选题 · extreme

  • A.1651← 正确答案
  • B.1715
  • C.1536
  • D.115

答案:A。blockIdx 线性化 = 2 + 1×4 = 6(按 x 优先),threadIdx 线性化 = 3 + 7×16 = 115,全局索引 = 6×256 + 115 = 1651。B 错在把 blockIdx 按 y 优先线性化,C 是漏加 threadIdx,D 只算了线程块内偏移。套用“块号 × 块内总数 + 块内偏移”的行主序规则即可。