LECTURE 01理解并行计算
导出本讲 PDF
01已发布

AI 时代的并行计算:基础与性能模型

从大模型与计算平台出发,经显存预算和混合精度进入GEMM;用分组活动理解依赖与通信,连续推导阿姆达尔定律、计算强度与屋顶线模型,再通过循环重排、分块与已有测量回到大模型的复用与流水线。

学完这一讲,你能

  • 从大模型的张量计算出发,解释 GPU 并行与流水线分别加速了什么
  • 估算张量的存储空间,解释浮点舍入以及输入精度与累加精度的区别
  • 用工作量、依赖链与阿姆达尔定律,分析并行加速的上限
  • 计算加法与矩阵乘法的 FLOPs、数据流量和计算强度,用屋顶线模型判断瓶颈
  • 说明分块、复用、融合与流水线改变了什么成本,区分理论上界和实测结论

大模型与计算平台

从大模型的一次回答出发

让大模型总结长文、解释代码或分析复杂问题,我们往往很快就能开始阅读回答,而且文字还在不断出现。模型把文本分成词元(token),一个词元不一定是一个完整单词。图形处理器(GPU,Graphics Processing Unit)负责加速其中的大量计算。接下来观察两种机制。

从一个问题,到一段回答
  1. 1提交问题
  2. 2处理上下文
  3. 3流式生成
  4. 4回答完成
你的问题

用一句话解释,GPU 为什么适合许多 AI 计算?

模型的回答

等待开始

我们把问题交给模型。

0 / 7 个演示步骤

教学流程示意,按短语揭示文字;不对应真实 token 边界,也不表示真实推理速度。

GPU 能把大量相似的计算分配给许多执行单元,并行推进。 生成过程:处理输入上下文,再根据已有上下文逐步生成并显示文字;用户可以边读,模型边计算后续内容,直至结束。

一方面,把推理算快:GPU 并行计算。 每一步生成内部,都有大量矩阵、向量运算可以并行执行。GPU 的并行算力与高带宽存储,配合高效的算子实现(如矩阵乘法这样的基本计算操作),是大模型能够快速响应的重要支撑。

另一方面,让用户更早读到结果:流式输出。 已生成的文字先显示,模型继续计算后文。于是,用户阅读前文与模型生成后文可以在时间上重叠,一部分计算等待被“藏”进了阅读过程。

这为理解流水线提供了一个直观类比。 后面学习通用矩阵乘法(GEMM,General Matrix Multiplication)时,我们会看到:计算当前数据块的同时,提前搬运下一块数据,用计算覆盖一部分访存等待。二者重叠的活动不同,共同思想是让不同阶段接续、重叠,减少暴露出来的等待

来源与延伸阅读

Q01 · 讨论

后一个词元(token)依赖前文,GPU 的并行发生在哪里?流式输出隐藏了什么等待,又保留了什么依赖?

参考答案

几行 PyTorch 背后:张量计算中的并行与流水线

从上一页的回答体验,走到程序中的 model(x)张量(tensor)是按多维数组组织的数据;一批图片或一段文本的特征都可以表示成张量。下面用一个分类模型对照训练与推理:训练增加了反向传播和参数更新,它们也由张量运算完成。

中央处理器(CPU,Central Processing Unit)负责主机程序与数据准备。CUDA 的全称是 Compute Unified Device Architecture(统一计算设备架构),是 NVIDIA 的 GPU 并行计算平台;PyTorch 中的 "cuda" 表示使用 NVIDIA GPU。

从熟悉的训练与推理流程,看背后的两类工作:

流程中的一步背后发生什么
DataLoader 准备下一批输入CPU 读取、变换数据,并可预取后续批次
模型前向计算矩阵乘法、逐元素运算等张量计算
反向传播与参数更新计算梯度,再用张量运算更新参数
推理使用已有参数执行前向计算

先沿这张流程表理解计算与数据准备的分工。

前向传播计算预测,反向传播计算损失对参数的梯度,优化器据此更新参数。batch_size=64 表示每批处理 64 个样本;数据加载器(DataLoader)的 4 个工作进程各预取 2 个后续批次。pin_memory 使用锁页主存,便于向 GPU 传输。

  • 并行在算子里面: 线性层(加权求和)、归约(把多个数合并,如求和)与逐元素更新,都包含大量可分工的张量计算。
  • 流水线在批次之间: GPU 处理当前批次时,CPU 的数据加载工作进程 可以读取和变换后续数据。

这里的预取发生在 CPU 数据准备端;CPU→GPU 搬运与 GPU 计算能否进一步重叠,还取决于缓冲、执行队列与依赖安排,后面再用时间线分析。

来源与延伸阅读

平台名称:NVIDIA CUDA 简介

参考:PyTorch 训练与测试DataLoader 预取拷贝与计算重叠

从 PyTorch 到 GPU:一行算子怎样落到硬件上

核函数(kernel)是提交给 GPU 执行的程序;线程是执行这段程序的一条工作路径,多个线程可以负责不同数据。

沿着 model(x) 往下看:模型表达 → 算子实现 → CUDA 提交 → GPU 执行

从模型表达,到设备执行
模型表达 · Pythontorch / torch.nn / 张量 Tensor

model(x) 表达模型;训练时自动求导组织梯度计算

算子实现 · 库与编译工具矩阵乘、逐元素变换、求和 → 设备程序

选择已有计算库,或通过编译生成与融合核函数(kernel)

CUDA · 运行时与驱动CPU 提交设备工作,管理内存与同步

把 kernel 及其参数提交给 GPU

GPU · 执行设备指令许多线程协作:读数据 → 运算 → 写结果

具体线程组织与存储层次,在后续执行模型中展开

协作的数据路径

CPU 数据准备与预取 → 主存 → CPU→GPU 拷贝 → 显存中的张量

展开:默认执行与编译执行的具体路径
默认路径 · 即时执行(Eager)分派器 Dispatcher → 张量运算库 ATen

ATen 即 A Tensor Library;按设备、布局及自动求导(Autograd)状态选择实现

原生 CUDA 算子 / 计算库

cuBLAS:CUDA 上的基础线性代数子程序库(BLAS,Basic Linear Algebra Subprograms),例如矩阵乘

cuDNN:CUDA Deep Neural Network library,CUDA 深度神经网络库,例如卷积

可选路径 · torch.compile捕获图 → 变换图 → 生成代码

Dynamo 捕获算子图 → AOTAutograd 整理求导图 → Inductor 优化并生成代码

AOT 指 Ahead-of-Time(提前),这里指提前捕获求导图

生成 kernel / 调用计算库

例如用 GPU 编程工具 Triton 生成融合核函数;也可复用库实现

CUDA 运行时与驱动接口负责提交。线程先组成线程块,分给 GPU 内部的计算单元执行;具体层次在下一讲逐步展开。

两条路径是概念对照;一次调用不必经过图中所有工具。

算子描述“做什么”,核函数安排“怎样在设备上做”。 例如矩阵乘法是数学操作,kernel 要安排线程、读取输入并写出结果。

一行 Python、一个算子与一个 kernel 并不一一对应:一个算子可能启动多个 kernel,编译优化也可能把多个算子融合执行。

图中聚焦 NVIDIA GPU;其他加速器保留类似的模型表达,下面的库、编译工具和设备程序可以不同。

来源与延伸阅读

GPU 的出发点:让大量相似工作一起推进

为什么最初为图形设计的处理器,会成为 AI 的计算平台?先把一帧游戏画面拆成工作。

《黑神话:悟空》水池场景:人物与建筑的几何形状、表面纹理、光照和水面反射共同形成画面;图中保留 NVIDIA 原图的 RTX ON 标识

图中展示现代光线追踪效果;下面用简化的光栅化流程说明 GPU 的图形起源。

从场景到画面需要完成的工作并行性在哪里
顶点与三角形将人物、建筑的几何坐标变换到屏幕空间大量顶点执行相似的坐标变换
光栅化与片元着色(计算像素候选的颜色)确定三角形覆盖的采样位置,计算纹理、材质与光照大量片元执行相似的着色程序
深度测试与合成处理遮挡,将结果写入画面按位置组织大量工作,同时保留必要的顺序与依赖

假设输出 3840 × 2160、每秒 60 帧,每秒对应约 4.98 亿个输出像素位置;每个位置背后还可能涉及多次采样、着色与访存。这是输出规模的算术示例,不是该游戏的实测性能或真实着色次数。

从“许多顶点、片元运行相似程序”,到“许多张量元素执行相似运算”:工作对象变了,大量数据上的并行计算需求相通。这推动 GPU 强调吞吐量(单位时间完成的工作量),也为后来 CUDA 将其用于通用计算提供了基础。

图形流水线仍有阶段依赖;能够并行的是其中的大批相似工作。下一步的突破,是让更多程序员也能使用这种能力。

来源与延伸阅读

图源:NVIDIA《黑神话:悟空》官方技术介绍,游戏:游戏科学。图中展示现代光线追踪效果;下面用简化的光栅化流程说明 GPU 的图形起源。

架构背景:CUDA Programming Guide

黄仁勋的远见:在 AI 热潮之前,为通用计算铺路

1993:从图形起步 → 2006:公布 CUDA 架构 → 2012:AlexNet 的突破

上一页的 GPU 已经能处理海量图形工作。黄仁勋和团队进一步追问:这种计算能力,能不能交到科学家和更多程序员手里?

一个早期信号:医院里的显卡。 黄仁勋在 2024 年斯坦福访谈中回忆,麻省总医院的医生用 Cg 图形编程工具做计算机断层成像(CT,Computed Tomography)重建,他专程去了解;计算化学研究者也开始用这类工具表达算法。这些具体应用,让他逐步相信 GPU 可以走出图形领域。

一次艰难选择:在市场尚不清晰时继续建设。 他回顾,公司曾经历约十年的持续投入,而许多未来市场还未形成,支撑业务的主要市场仍是图形。继续推进,需要带着工程师、管理团队、股东和合作伙伴共同承担不确定性。CUDA 把这种判断落实成可供开发者使用的通用编程平台。

一种坚持的方法:寻找“未来成功的早期信号”。 他在访谈中强调,研究者是否解决了新问题、是否出现新的研究成果,可以帮助团队判断方向。信念需要这些证据不断支撑,也需要在条件变化时重新检查。

这份前瞻性,是提前建设可复用的平台;这份勇气,是在回报尚不明确时持续投入,并用真实应用检验方向。到了 2012 年,AlexNet 让这些积累与深度学习的需求相遇。

来源与延伸阅读

故事依据:黄仁勋在 Stanford GSB 的访谈,属于当事人的回顾;时间节点对照 NVIDIA 公司历史。末段为课程解读。

AlexNet:从一张图片,到 1000 类识别结果

它做的是图像分类。 输入一张红、绿、蓝三通道(RGB,Red / Green / Blue)图片,模型为 ImageNet 大规模视觉识别挑战(ILSVRC,ImageNet Large Scale Visual Recognition Challenge)的 1000 个类别输出分数与概率,例如判断图片属于虎斑猫、帆船等哪一类。训练使用约 120 万张带标签图片

AlexNet · 五层卷积 + 三层全连接点击层,或按“下一层”沿数据流查看
卷积(Conv)提取局部特征;通道是一张张特征图
局部取最大值(池化)→ 排成向量(展平)→ 全连接(FC)综合特征
输入 · RGB 图片

图像首先表示成数值张量。网络接收像素值,再用训练得到的权重计算类别分数。

训练时把多张图组成一批(batch),为后续算子提供更多可并行的数据。

1 / 10 个阶段
展开:两张 3 GB GPU 怎样连接?
Conv2Conv3Conv4GPU 0128 通道192 通道192 通道GPU 1128 通道192 通道192 通道读取两卡输入读取同卡输入

交叉线表示跨卡读取。图示选取 Conv2→Conv4;两卡协作运行一个网络,通信只出现在选定的层间连接。

结构符号不按比例,也不是实际特征图或全部神经元。逐层高亮表示讲解顺序;训练时还要沿依赖反向计算梯度。

任务:RGB 图片的 1000 类分类。Conv2 两卡各 128 通道;Conv3 各 192 通道并读取两卡输入;Conv4 各 192 通道、只读取同卡输入。

2012 年,Krizhevsky、Sutskever 与 Hinton 的团队用 两张 GTX 580,每张 3 GB 显存,训练这个约 6000 万参数的网络,耗时约 5–6 天。为了容纳并训练网络,作者将部分层按通道或神经元分到两卡,只在需要的地方交换结果。两卡分工同时面对容量与通信。

还有与第二页呼应的流水线。 原论文第 4.1 节写道:CPU 用 Python 准备增强后的图像,同时 GPU 训练前一批图像。数据准备与模型计算的重叠,在这个早期案例里已经出现。

模型、算子与硬件共同决定能做多大的实验。 这个发生在多伦多的突破,也让 CUDA 平台与深度学习需求相遇。

图示按 2012 年论文的主干整理,逐层高亮用于讲解,不运行真实模型。

来源与延伸阅读

来源:AlexNet 原论文,第 2、3.2、3.5、4.1 节及图 2。图示按 2012 年论文的主干整理,逐层高亮用于讲解,不运行真实模型。

Q02 · 讨论

两张各 3 GB 的显卡,为什么不等于一张可任意访问的 6 GB 显卡?

参考答案

ChatGPT 横空出世:规模增长,为什么能带来进步

2012:识别图片 → 2022 年 11 月 30 日:直接与模型对话。 ChatGPT 的研究预览让用户能够解释概念、协助改代码并连续追问。它在 GPT-3.5 系列模型上经过对话微调与人类反馈强化学习(RLHF,Reinforcement Learning from Human Feedback);对话体验建立在预训练、后续训练与产品设计的共同积累上。GPT 指生成式预训练 Transformer(Generative Pre-trained Transformer)。

支撑规模化投入的一条线索,是 尺度定律(scaling laws)

Kaplan 2020 的三幅拟合曲线:在各自研究条件下,模型参数、训练数据或有效训练计算量增大,测试预测损失按幂律下降;不是 ChatGPT 实测曲线。

图按 Kaplan 等(2020)的尺度定律重绘拟合,无实验散点。横轴依次为参数、数据和有效训练计算预算;纵轴为预测损失,越低越好。三图使用双对数坐标,研究条件各有不同。

从曲线读出什么对计算的含义
在研究范围内,规模增加伴随损失有规律地下降更多有效计算,让更大规模的模型实验成为可能
右图中,计算预算增至10倍,拟合损失约下降11%收益不会与资源投入等比例增长
模型与数据需要配合更大的模型还需要足够的数据与训练;Chinchilla 的研究进一步讨论了这种配比

预测损失衡量模型对后续文本的预测,不能直接换成任务准确率。图中计算预算单位与幂律参数见原论文;具体能力仍需单独评测。

对本课程最直接的启发:把有限时间和资源,转化为更多有效计算。 为此,需要从编程工具继续走向完整的硬件系统。

来源与延伸阅读

从厨房发布会,看一块芯片之外的系统

更大的计算规模,最后必须由真实的机器承载。回看 2020 年,黄仁勋在家中厨房录制 GTC 主题演讲,拿出了画面中的计算硬件。

2020 年 GTC 厨房主题演讲中展示的计算硬件

顺着这块硬件往外看,GPU 需要几种配合:

配合要解决的问题
显存与互连数据怎样送到计算单元,多块设备怎样交换结果?
供电与散热持续运行时,怎样提供电力并带走热量?
软件与系统模型中的算子怎样调用这些设备?

一颗能计算的芯片,要与数据供给、软件和工程系统一起工作。 这也解释了为什么下一页的另一种 AI 芯片,要与软件栈一起登场。

图中为整块计算硬件,不是单个裸芯片。

来源与延伸阅读

来源:NVIDIA · GTC 2020 主题演讲。图中为整块计算硬件,不是单个裸芯片。

昇腾故事:为什么芯片与软件栈一起登场

2018 年 10 月,华为在上海发布 AI 战略与昇腾系列,并同时介绍 CANN、MindSpore(深度学习框架)和应用服务。2019 年,昇腾 910 成为其训练计算产品的一部分。

CANN 全称 Compute Architecture for Neural Networks(面向神经网络的计算架构),是昇腾的异构计算软件平台,包含算子库、编译与执行支持。

从 CUDA 到 CANN,平台路线不同,却都需要回答同一个问题:一个数学算子,如何被可靠、高效地落实到不同硬件上。

来源与延伸阅读

名称与定位:昇腾 CANN 官方说明

来源:华为 2018 年发布资料2019 年报

Q03 · 讨论

同一段 Python 模型代码能在两种加速器上运行,能否据此认为底层程序和性能相同?

参考答案

从昇腾人工智能计算核心(AI Core)看计算与搬运的分工

软件最后要调动硬件。先沿这张图辨认三种工作:控制、计算、搬运

昇腾 AI Core 抽象架构:Scalar、Vector、Cube、DMA、Local Memory 与 Global Memory

图中的名称职责
标量单元(Scalar)处理单个数与控制指令
向量单元(Vector)/ 矩阵单元(Cube)分别处理一组数与矩阵乘加
直接存储器访问(DMA,Direct Memory Access)专用搬运引擎,在存储区域之间传输数据
局部存储(Local Memory)/ 全局存储(Global Memory)前者靠近计算单元,后者保存较大规模的设备数据

计算单元再快,也需要及时拿到数据。 后面分析 GEMM 时,我们会把“做多少次运算”和“搬多少字节”分开计数。

这是编程架构抽象;具体缓冲区、指令与同步机制在后续课程展开。

来源与延伸阅读

图源:昇腾 CANN 7.0 官方文档,图1。这是编程架构抽象;具体缓冲区、指令与同步机制在后续课程展开。

从马斯克到 Claude:GPU 集群成为可交易的资源

2026 年 5 月 6 日,Anthropic 宣布与马斯克旗下的 SpaceX 达成算力合作,使用 Colossus 1 数据中心的全部计算容量。 公告称,这项安排将在当月为它提供超过 22 万张 NVIDIA GPU、超过 300 兆瓦(MW,1 MW = 100 万瓦) 的新增容量,用于扩展 Claude 服务。

Colossus 数据中心航拍:大面积机房建筑及周围供电和配套设施,NVIDIA 2025 年公布的资料照片

这个故事的反差在于:开发模型的公司,也可以向另一家模型公司的算力平台购买服务。 SpaceXAI 提供 Colossus,Anthropic 用它服务 Claude;NVIDIA GPU 则成为不同模型背后共同使用的计算基础。

张量处理器(TPU,Tensor Processing Unit)也在同一场算力竞争中。 这是 Google 面向 AI 张量计算设计的专用加速器。2025 年 10 月 23 日,Anthropic 宣布扩大 Google Cloud 合作,计划使用多达 100 万个 TPU,扩展规模为数百亿美元。采购对象是云计算能力与服务,TPU 是支撑这些服务的专用 AI 加速器。

从本课程看,这些案例说明:模型能力背后,是可以采购的GPU、TPU和云算力服务。 MW 在这里描述电力容量口径,不能直接换算成模型性能;以上规模按各自公告日期记录。

来源与延伸阅读

图源:NVIDIA 官方博客,2025-09-18。Colossus 数据中心资料照。

星际之门:围绕并行算力,组织 5000 亿美元投资计划

租用已有集群之外,另一条路径是建设下一批算力。 2025 年 1 月 21 日,OpenAI 与 SoftBank 宣布 星际之门(Stargate),提出未来四年在美国投资 5000 亿美元建设 AI 基础设施的计划,并宣布立即启动部署首期 1000 亿美元。这里是项目投资计划口径。

Stargate 在得州 Abilene 的建设现场航拍:成排的数据中心建筑、变电设施与正在施工的大片园区

谁在其中围绕算力承担的角色
SoftBank、OpenAI、Oracle、MGX公告列出的初始股权出资方;SoftBank 负责财务,OpenAI 负责运营
NVIDIA、Oracle 等技术伙伴提供芯片、系统与云基础设施,把投资转成可运行的 GPU 集群
OpenAI用新增计算能力训练模型、运行推理,承接 AI 产品需求

从发布会走到实际计算。 OpenAI 在 2025 年 7 月披露:得州 Abilene 站点已有部分设施运行,Oracle 已开始交付 NVIDIA GB200 机架,并已执行早期训练与推理任务。

从购买现成算力,到投资建设未来算力,资本围绕同一类计算需求组织起来。 资金需要经过建设、交付和实际使用,才能转化为服务能力;维持这些机器运行,还要持续供电。

来源与延伸阅读

图源:OpenAI,2025 年 7 月 Abilene 进展公告。照片展示该次公告中的建设现场。

视频:小Lin说 · AI 巨头资本混战

小Lin说 · Bilibili 视频在原站观看

一座 GPU 集群,一年会用多少电?

Colossus 1 的合作公告给出了超过 300 MW 的容量量级。以此为参照,先设一个可计算的情景:假设整个设施全年平均用电功率为 300 MW,连续运行 365 天。

E=300,000 kW×24×365 h=2.628×109 kWh.E=300{,}000\ \mathrm{kW}\times24\times365\ \mathrm{h}=2.628\times10^9\ \mathrm{kWh}.

一年约 26.28 亿度电,也就是 2.628 太瓦时(TWh,1 TWh = 10 亿 kWh)。 1 度电 = 1 kWh。把这个数放到两个熟悉的尺度上:

用什么比较26.28 亿度电相当于比较基准
居民生活约 230 万人一年的生活用电用中国 2025 年居民生活用电与年末人口粗算,人均约 1130 度/年
三峡电站2025 年全年发电量的约 2.75%三峡电站 2025 年发电量为 957.15 亿度

居民比较只含生活用电,不含工商业用电。

以上是按平均设施功率300 MW推算的情景,不是集群实测年度用电。 300 MW若仅指计算设备,还需另计制冷等用电;三峡比较只表示年度电量的比例。

当 AI 算力扩展到这样的规模,电力就成为计算基础设施的一部分。 下一页从这个量级出发,看 AI 的能源需求。

来源与延伸阅读

比较来源:国家能源局的2025年居民生活用电量与 国家统计局的年末人口;居民比较不含工商业用电。三峡数据来自 长江电力 2025 年发电量公告(原文,新浪存档)

视频:小Lin说 · AI 能源之战

小Lin说 · Bilibili 视频在原站观看

用 GEMM,把前面的问题落到同一项计算上

从模型能力到集群与能源,最后都要落到一项项实际计算。我们选 通用矩阵乘法(GEMM) 贯穿本讲:用同一个操作,研究并行、数据搬运、精度与资源代价。

我们先研究它最简单的形式:

C=AB,Cij=kAikBkj.C=AB,\qquad C_{ij}=\sum_k A_{ik}B_{kj}.

  • 并行与速度:把矩阵乘加分给许多线程,协作完成同一个结果。
  • 存储与搬运:矩阵需要放得下,计算时还要及时取得数据、尽量复用。
  • 精度与代价:不同数值格式影响误差、存储量与执行效率;完成同一任务的时间和能耗也随实现而变。

先看数据怎样存、精度怎样选,再建立一份朴素的 C++ 矩阵乘法,逐步看懂 GPU 如何把它算快、算准。

数据与显存预算

从装机视频认识:一台计算机由什么组成

硬件茶谈 · Bilibili 视频在原站观看

把视频里的实物与职责对应起来:

部件在计算中承担什么
CPU 与独立显卡上的 GPU执行主机程序与设备上的并行计算
内存条与显卡上的显存分别存放主机和设备正在使用的数据
固态硬盘(SSD,Solid-State Drive)与主板互连持久保存文件,连接计算与存储部件
电源与散热器提供电能,带走运行产生的热量

这里的独立显卡有自己的显存,区别于 CPU 使用的主存。高带宽内存(HBM,High Bandwidth Memory)是高端 AI 芯片常用的一种显存实现:通过堆叠存储芯片与宽接口提供高数据传输能力;并非所有 GPU 都采用 HBM。 接下来追踪:SSD 上的模型文件,怎样变成 GPU 正在使用的操作数?

一个模型文件,怎样进入计算过程

一次 GPU 计算的数据与命令流程
读取拷贝提交命令读取写入CPU 需要结果时,才回传固态硬盘 SSD模型文件主存 RAM主机工作数据CPU程序与任务提交GPU寄存器与计算单元显存设备工作数据绿色:数据橙色:命令
先认清位置

SSD 长期保存文件,主存与显存保存运行中的数据,CPU 提交工作,GPU 执行并行计算。

0 / 7

按依赖分步说明,省略缓存细节;箭头速度不表示真实带宽。CPU 可以异步提交,实际阶段也可能重叠。

SSD → 主存 → 显存;CPU 提交 kernel;GPU 从显存准备操作数、并行计算,再将结果写入显存。仅在 CPU 需要时回传主存。

跟着一次矩阵乘法,区分两条路径:数据从哪里来,计算命令由谁提交。动画可逐步推进,也可连续播放。

图示采用离散 GPU(与 CPU 分立的设备)+ 独立显存,展示显式拷贝的一种常见路径。CPU 执行主机程序并提交任务,GPU 执行 kernel(设备上的并行程序);GPU 算完后,数据可以继续留在显存,交给下一个算子。

搬到显存还不等于已经送入计算单元。 GPU 还要执行读取(load)准备操作数;写回称为 store。例如 r2 保存地址 0x1000,示意指令 load r0 ← [r2] 将该地址处的值读入寄存器 r0,再用于后续乘法。寄存器保存当前操作数与部分结果,缓存可保留近期访问数据的副本。

统一内存及直接存储访问等路径可以有不同的数据组织。

来源与延伸阅读

参考:CUDA 编程模型。统一内存及直接存储访问等路径可以有不同的数据组织。

从比特到搬运时间:计算术语的交互复习

一个字节,八个二进制位

bit(位)取 0 或 1;本课平台的 byte(字节,B)由 8 bit 组成,共有 2⁸ = 256 种位模式。二、十、十六进制是同一个整数的不同写法。

负数怎样表示?补码是一种表示有符号整数的编码。以 8 位为例,把最高位的权重从 128 改成 −128,其余位不变。切换规则时,下面的比特保持不变。

二进制0000 1101无符号 · uint813有符号 · int8(补码)13十六进制0x0D

uint8 解读:8 + 4 + 1 = 13

例如,全 1 按 uint8 是 128 + 127 = 255;按 int8 补码是 −128 + 127 = −1。按 int8 解读时,最高位单独为 1 表示 −128。

用 −5 看懂“取反加一”,以及这样编码的好处
  1. ① 先写出 +50000 0101
  2. ② 每一位取反(0↔1)1111 1010
  3. ③ 再加 1,得到 −5 的补码1111 1011

8 位全 1 的无符号值是 255,因此取反得到 255−5,再加 1 就是 256−5 = 251。按补码解读,251−256 = −5

0000 0101 (+5) + 1111 1011 (−5) ─────────── 1 0000 0000

+5 与 −5 相加,保留低 8 位就得到 0;最左边的 1 是超出 8 位的进位。这样,减去 5 可以交给同一套二进制加法电路处理。

补码是这里的整数解释规则;float32 使用符号、指数和有效数字,下一页签会比较它们。

位与字节:本课平台 1 B = 8 bit。00001101₂ = 13₁₀ = 0x0D;补码用固定的位数表示有符号整数;8 位补码把最高位权重改为 −128,其余位不变,范围是 −128~127。11111111₂ 按 uint8 是 128+127=255,按 int8 补码是 −128+127=−1。−5 的编码:00000101 → 逐位取反 11111010 → 加一 11111011。

数据类型:13 = 1101₂ = 1.101₂ × 2³。float32 分成符号 1 位、指数 8 位、小数字段 23 位:s=0,E=3+127=130(10000010),F=101 后补 0,合起来是 0x41500000。解码时 1.101₂=1+1/2+1/8=1.625,数值为 +1.625×2³=13。同样的位按 uint32 或 int32 是 1095761920。float 13.0 转成 int32 的数值仍为 13(0x0000000D);位重解释保留 0x41500000,按 int32 读为 1095761920。float32 的 1.0 则是 s=0、E=127、f=0,因此 +1×2⁰=1。

容量:1024 × 1024 个 float32 占 4,194,304 B = 4 MiB = 4.194304 MB。GB = 10⁹ B,GiB = 2³⁰ B。

带宽与延迟:启动开销 20 μs,传输 64 MiB,带宽 8 GB/s:总时间 ≈ 0.02 + 8.388608 = 8.408608 ms。仅为教学估算。8 Gb/s = 1 GB/s。

位模式 → 类型解释 → 数据量 → 搬运时间:这四步把“一个数”连接到“一个算子的存储与性能预算”。本课示例平台采用 每字节 8 位(bit),float 采用 IEEE 754 浮点标准中的 32 位二进制格式(binary32,即 FP32;FP 表示 Floating Point,浮点)。

来源与延伸阅读

参考:NIST 二进制前缀NVIDIA 浮点格式C++ 位重解释浮点转整数

显存不足(out of memory):模型放得下,训练仍可能爆显存

模型成功搬到了 GPU,前向传播也跑完了,到了 loss.backward() 却停住了——一次训练中的报错场景(节选示意):

训练日志 · 节选示意
  File "train.py", line 42, in train_step
    loss.backward()

torch.cuda.OutOfMemoryError: CUDA out of memory.
Tried to allocate 512.00 MiB.

模型各层产生的中间结果称为激活。显存不足常缩写为 OOM(Out of Memory)。这里的 512 MiB 是这一次额外申请的空间。模型参数、保存的激活和已经生成的梯度等,此时可能已占用了大部分显存。OOM 也可能出现在前向传播或优化器更新阶段,关键是运行中的显存峰值

回到一个容量预算:设备有 16 GiB 可用显存,模型有 7×1097\times10^9 个参数,每个参数占 2 B。

先只计算权重:

7×109×2=14×109  B13.04  GiB.7\times10^9\times2=14\times10^9\;\mathrm{B}\approx13.04\;\mathrm{GiB}.

权重只是显存账本中的一项。 推理需要运行中的中间数据与临时空间,训练还要保存反向传播所需的激活、梯度和优化器状态。

一个直接的想法是:让每个数少占一些字节。 接下来就从这笔存储收益出发,看低精度会怎样改变可表示的数和计算结果。

来源与延伸阅读

参考:PyTorch:理解 GPU 显存占用与 OOM

Q04 · 讨论

“权重约 13.04 GiB,小于 16 GiB,因此推理一定能运行”——这条推理缺少哪些条件?若改为训练,还会新增什么?

参考答案

浮点数与混合精度

浮点数:省下一半空间,数值会怎样变化

上一页按每个参数 2 B 估算权重。FP32 是 32 位单精度浮点;FP16 是 16 位半精度浮点;BF16 指 bfloat16(Brain Floating Point,源自 Google Brain 的 16 位浮点格式),位数与 FP16 相同但分配不同。相同的 70 亿个参数,FP32 存储约需 26.08 GiB,FP16 或 BF16 约需 13.04 GiB。权重数据量减半,整个训练过程的显存峰值却不一定减半。

少用的位数会改变数值表示。类似十进制科学计数法,规格化二进制浮点数用“符号 × 有效数字 × 数量级”表示:

(1)s×(1.f)2×2e.(-1)^s\times(1.f)_2\times2^e.

格式符号 / 指数 / 尾数字段有效二进制位数(含隐含首位)从 1 向上的相邻间隔
FP161 / 5 / 10112102^{-10}
BF161 / 8 / 78272^{-7}
FP321 / 8 / 23242232^{-23}

指数位主要影响范围,有效数字位主要影响精细程度。BF16 与 FP16 都占 2 B,但不能据此认为它们的数值行为相同。

因此选择精度时,要同时核算存储收益、数值范围和舍入误差

零、极小数、无穷大和“非数值”(NaN,Not a Number,如无效运算产生的结果)有特殊编码,后续按需要再讨论。

来源与延伸阅读

舍入:精确结果存不下,应该存哪个数?

数学上算出的数,不一定能由目标格式精确表示。舍入就是按规定的规则,选一个能够存下的近似值。 先看一个只有 3 位有效数的教学例子,再回到 FP16 和 FP32,最后对照一组数在类型转换前后的结果。

数学上的精确值 → 目标格式能够存下的近似值
1/4

先把可存的数画出来,再谈怎么舍入

用一个教学格式:只保留 3 位二进制有效数,包括开头的 1。在 8 到 16 之间,指数固定为 3,有效数只能是下面四种。

每个有效数都乘同一个 2³ = 8:例如 1.012 × 8 = (1 + 1/4) × 8 = 10。框出的位是最后保留的一位。

8.5、9、11 都没有自己的刻度。例如 9 = 1.0012 × 8,需要 4 位有效数;这个格式只有 3 位,所以必须选一个近似值。

仅为简化教学,不是 FP16:这里显示区间 [8,16),不包含右端点 16。

教学格式只保留 3 位有效二进制数,在 [8,16) 可存 8、10、12、14。8.5 就近存成 8;9 位于中点,选有效数 1.00₂ 对应的 8;11 位于中点,选 1.10₂ 对应的 12;12 可精确表示。中点取偶看有效数最后一位是否为 0。FP16 的 2049 舍入为 2048;1+2⁻¹² 舍入为 1;FP32 的 16777217 舍入为 16777216。

转换结果:

[2048.0, 2049.0, 2050.0, 2051.0]
[2048.0, 2048.0, 2050.0, 2052.0]
[2048.0, 2048.0, 2050.0, 2052.0]

转成 FP16 时,2049 → 2048,2051 → 2052;转回 FP32 也不能恢复原值。PyTorch 初始默认浮点类型为 FP32。

本页采用 就近舍入、中点取偶(round to nearest, ties to even)。存入一个输入、转换到较低精度,或把运算结果写回目标浮点格式时,都可能发生舍入。

来源与延伸阅读

浮点加法不满足结合律:一个完全可算的反例

采用 FP32;每次加法后做就近舍入,中点取偶:

a=224,b=1,c=224.a=2^{24},\quad b=1,\quad c=-2^{24}.

比较两条路径:

fl(fl(a+b)+c)fl(a+fl(b+c)).\operatorname{fl}(\operatorname{fl}(a+b)+c)\qquad\text{与}\qquad\operatorname{fl}(a+\operatorname{fl}(b+c)).

这里 fl\operatorname{fl} 表示一次 FP32 舍入,实数精确结果为1。并行求和常会改变加法的分组,先用这个小例子看看顺序为何重要。

来源与延伸阅读

Q05 · 讨论

两种加法顺序各得到什么结果?哪一步发生了不可恢复的丢失?

参考答案

混合精度:低精度输入,高精度累加

神经网络中经常要把两组数对应相乘,再把乘积加起来,即点积 s=kakbks=\sum_k a_kb_k。输入数据有很多,而一个部分和会被反复更新:它们不必使用相同精度。

图中的张量核心(Tensor Core)是 NVIDIA GPU 中专门加速矩阵乘加的计算单元;Volta 是这代 GPU 架构的名称。

Volta Tensor Core 示意:FP16 存储与输入,完整乘积,FP32 累加与结果

一种常见分工是:用 FP16 存储大量输入,用 FP32 累加部分和。这样可减少输入存储与搬运量,并在支持的硬件上利用低精度计算路径。上图是 Volta Tensor Core 的实例,明确区分 FP16 输入、完整乘积与 FP32 累加。

用于理解的逐项模型是:

a^k=round16(ak),b^k=round16(bk),sk+1=round32(sk+a^kb^k).\begin{aligned} \hat a_k &= \operatorname{round}_{16}(a_k),\\[0.4em] \hat b_k &= \operatorname{round}_{16}(b_k),\\[0.6em] s_{k+1} &= \operatorname{round}_{32}(s_k+\hat a_k\hat b_k). \end{aligned}

重点在于:不必把乘积先舍入成 FP16,再转回 FP32。 输出最终还可能转换为低精度存储。

逐项公式用于教学;实际矩阵指令的内部归约顺序与舍入保证以对应指令规范为准。

来源与延伸阅读

图源:NVIDIA · Programming Tensor Cores in CUDA 9,图 2。逐项公式用于教学;实际矩阵指令的内部归约顺序与舍入保证要查对应指令文档

4096 个 1,为什么只得到 2048

数值实验台 · 输入精度与累加精度分开选择
Σ 1 × 1
实数参考值待预测按题设精确计算
FP16 乘积 + FP16 累加待预测每一步均舍入至 FP16
FP16 输入 + FP32 累加待预测乘积不先舍入至 FP16
固定顺序,逐项累加

软件数值模型:舍入采用 round-to-nearest, ties-to-even;不是 GPU 性能或 Tensor Core 内部归约顺序的测量。改变归约树也可能改变结果。

N=4096 时:累加 1 的 FP16 结果为 2048,FP32 累加为 4096;输入为 1+2⁻¹² 时实数结果为 4097,FP16 输入加 FP32 累加为 4096。256×256 的 FP32 乘积为 65536,若乘积先转 FP16 则溢出。

先选“累加吞掉了 1”,令 N=4096N=4096。所有输入和乘积都是 1,都可以用 FP16 精确表示;固定按从左到右的顺序累加。

Q06 · 讨论

输入没有任何表示误差,为什么逐项 FP16 累加得到 2048,而 FP32 累加得到 4096?若先两两相加,再两两合并,结果会改变吗?

参考答案

FP32 累加的边界:输入丢失与乘积溢出

FP32 累加保护的是部分和。沿着“输入 → 乘积 → 累加”再检查两处:

短对照数值经过了什么路径关键区别
4096项 (1+212)×1(1+2^{-12})\times1,实数和为4097每个输入先存成FP16,再进入FP32累加2122^{-12} 小于1附近FP16的半个格距,输入先变成1
256×256=65536256\times256=65536,两个输入都可用FP16精确表示路径A:乘积先舍入FP16;路径B:乘积直接进入FP32累加65536超过FP16最大有限值65504,但能用FP32表示

可在前页实验台切换“输入先丢失信息”和“乘积不必落回FP16”验证。

Q07 · 讨论

第一行只提高累加精度,能恢复4097吗?第二行两条路径分别得到什么?这说明“FP16乘、FP32加”必须说清楚什么?

参考答案

回到显存不足(OOM):省下空间,也守住数值要求

现在可以沿数据流解释混合精度的分工:

保存或计算什么选择背后的考虑
大量输入与权重低精度减少存储和搬运,数值范围与输入舍入要合适
反复更新的部分和较高精度减少累加中的信息丢失
最终输出按后续运算需要保存,转换仍会发生舍入

70亿个参数从FP32换成16位格式,权重约从26.08 GiB降到13.04 GiB。 运行还要容纳激活、工作空间和训练状态,完整显存峰值需要一起核算。

选择方案时,同时观察显存是否够用、运行是否更快、结果与训练质量是否满足要求。混合精度的基本思想是按数据的作用分配精度

接下来,把这些乘加组织成一个矩阵乘法,再看它们怎样存入内存、写成循环、交给不同执行者。

来源与延伸阅读

矩阵乘法与并行分工

神经元的加权求和,怎样变成矩阵乘法

前面用“成对相乘、再累加”检验了精度。现在把它放回神经网络,看看许多这样的计算怎样组织成一个 GEMM。

① 一个输入,两个神经元:各算一次加权求和。

输入有 3 个特征 (1,2,3)(1,2,3)。两个神经元分别使用权重 (7,9,11)(7,9,11)(8,10,12)(8,10,12),先忽略偏置与激活函数:

y0=1×7+2×9+3×11=58,y1=1×8+2×10+3×12=64.\begin{aligned} y_0 &= 1\times7+2\times9+3\times11=58,\\ y_1 &= 1\times8+2\times10+3\times12=64. \end{aligned}

② 每个神经元的权重排成一列,两列合起来就是 WW

输入排成一行,“这一行分别乘两列”就是刚才的两个点积:

[123]一个输入[789101112]两列权重 W=[5864]两个输出.\underbrace{\begin{bmatrix}1&2&3\end{bmatrix}}_{\text{一个输入}} \underbrace{\begin{bmatrix}7&8\\9&10\\11&12\end{bmatrix}}_{\text{两列权重 }W} =\underbrace{\begin{bmatrix}58&64\end{bmatrix}}_{\text{两个输出}}.

③ 多个输入往下叠,共用同一个 WW

再加入输入 (4,5,6)(4,5,6);每个输入在 XX 中占一行,对应的输出也在 YY 中占一行:

[123456]X:2×3[789101112]W:3×2=[5864139154]Y:2×2.\underbrace{\begin{bmatrix}1&2&3\\4&5&6\end{bmatrix}}_{X:\,2\times3} \underbrace{\begin{bmatrix}7&8\\9&10\\11&12\end{bmatrix}}_{W:\,3\times2} =\underbrace{\begin{bmatrix}58&64\\139&154\end{bmatrix}}_{Y:\,2\times2}.

所以 XM×KWK×N=YM×NX_{M\times K}W_{K\times N}=Y_{M\times N}MM 行输入,每行 KK 个特征,得到 NN 个输出。 矩阵乘法把这些共享权重的加权求和组织在了一起;后面将 X,W,YX,W,Y 简记为 A,B,CA,B,C

与 PyTorch 的线性层 nn.Linear 对照:它的 weight 按“输出 × 输入”存放,忽略偏置时写作 Y = X @ weight.T;本页的 WW 对应 weight.T

来源与延伸阅读

二维下标,怎样变成一维地址

点击一个元素,对照它在连续内存中的位置
二维矩阵 · 2 行 × 3 列
连续内存 · 行优先
元素索引 0 × 3 + 0 = 0字节偏移 0 × 4 = 0 B

本例每元素 4 字节,索引从 0 开始。高亮表示同一个元素;每格代表 4 字节。

本课采用连续的行优先布局:一行接一行存放,下标从 0 开始。步长(stride) 表示某个维度的下标增加 1 时,地址跨过多少个元素;例如一行有 N 个数,行步长就是 N。一般张量还可能有步长、转置视图和填充,不能仅凭形状假定连续。

更详细的数据布局,会在后续“现代 GPU 架构”部分展开:矩阵如何分块、如何分布到线程与寄存器,以及共享内存中的布局如何影响访问效率。这里先掌握“二维下标 → 元素索引 → 字节地址”这条基本对应。

Q08 · 讨论

对于行优先的 A[M,K]、B[K,N]、C[M,N],写出 A[i,k]、B[k,j]、C[i,j] 的元素索引;为什么 B 的行步长是 N?

参考答案

用动画检查一个矩阵乘法

2 × 3 与 3 × 2 矩阵相乘
A
123
456
×
B
78
910
1112
=
C(累加中)
00
00
从零开始,点击「下一步」执行一次乘加。
0 / 12 次乘加

完整结果 C = [[58, 64], [139, 154]]。该演示说明数学步骤,不表示硬件执行速度。

每个输出元素经历一条点积链:取一对输入 → 相乘 → 累加。动画中的小整数可精确表示,用于检查索引、依赖和输出语义

同一行的输入会参与多个输出;不同输出也会读取同一列数据。这个重复使用的现象,性能模型部分再用缓存与计算强度分析。

这组输入也会贯穿后续线性层:令 W=Bᵀ,则 D=AWᵀ 仍为 [[58,64],[139,154]]。后面改变的是分工、布局与交接,输入含义保持一致。

朴素 C++ 基线:让每一个循环都有数学含义

cpp
// A: M×K, B: K×N, C: M×N;连续行优先,输出不覆盖输入。
for (int i = 0; i < M; ++i) {
    for (int j = 0; j < N; ++j) {
        float acc = 0.0f;
        for (int k = 0; k < K; ++k) {
            acc += A[i * K + k] * B[k * N + j];
        }
        C[i * N + j] = acc;
    }
}

i,ji,j 选择一个输出;kk 遍历点积。每个输出有自己的累加器,从 0 开始;函数语义是 C = AB,而非 C += AB。

这里先用 FP32 存储输入和累加器,固定源码中的累加顺序,建立便于检查的基线。后续改变精度或并行分工时,对照同一输入检查结果,性能比较也使用一致的编译选项。

这里尚未手工做分块、单指令多数据(SIMD,Single Instruction, Multiple Data;一条指令处理多个数)或多线程优化。

分组加法:四位同学,一台并行计算机

4 位同学分别认领 A–D,老师汇总,全班观察。 每人算自己的一列,算完举手,全部完成后再依次报数。

4 位计算者 · 共 32 个数 · 每人只报告一个局部和
0.0 s
认领自己的字母。点击开始后显示数字;两组连续累加,算完举手,暂不报数。
A2 组 · 4 个数
1
··
2
··
B2 组 · 4 个数
3
··
4
··
C2 组 · 12 个数
5
······
6
······
D2 组 · 12 个数
7
······
8
······

第一轮每人处理 4、4、12、12 个数;第二轮换用新数字,每人处理 8 个数。各人算完后举手,再依次交付局部和并合并。在线页面可展开两轮数字和答案。

两轮求和:分组相同,不代表负载相同

同一批 4 位计算者第一轮:连续两组第二轮:一短一长
每人分到的组数都是 2 组都是 2 组
A / B / C / D 的数字项数4 / 4 / 12 / 128 / 8 / 8 / 8
总工作与交付方式共 32 项,交付 4 个局部和共 32 项,交付 4 个局部和

等待发生在两个位置: 先完成的人等其余人算完;全部算完后,还要依次报告、确认并合并结果。把一个人的数据分出去,也引入了这些额外步骤。

Q09 · 讨论

第二轮为什么可能减少等待,却未必让四人同时完成?“最后一人算完”和“得到总和”之间的时间,能够直接称为通信时间吗?

参考答案

用时间线分清:工作量、速度与通信

分工之后,谁还在算,谁已经在等
每组数字项数
计算者速度
分配方式
计算等待最晚完成者速度:项 / 时间单位
A · 速度 14 项 · 4 时完成
B · 速度 14 项 · 4 时完成
C · 速度 112 项 · 12 时完成
D · 速度 112 项 · 12 时完成
最晚算完 12依次交付 8最终相加 323 时间单位

同一工作量:32 项。相对速度为 1 的单人基线(32 单位),加速比为 1.39×

教学模型:每输入从 0 累加一次;各计算者保留自己的运行和。全部算完后,单通道依次交付 4 个局部和,再由汇总者做 3 次加法。暂不计领取调度与重算,未模拟通信与计算重叠;实际活动要记录这些额外时间。

默认:工作量 [4,4,12,12],速度均为 1,通信每次 2,耗时 12+8+3=23;改为一短一长,工作量为 [8,8,8,8],耗时 8+8+3=19。通信每次增至 6 时,即使后一种分工也需 35,比单人基线 32 更慢。

在“先全部算完,再依次交付并合并”的简化协议下:

T=maxiWiri+4c+3.T=\max_i\frac{W_i}{r_i}+4c+3.

WiW_i 是第 ii 人从 0 累加的项数,rir_i 是每时间单位处理的项数,cc 是交付一个局部和的时间;汇总者每单位做一次加法,合并四个和需 3 次。这里的时间单位是模型假设,不是活动实测秒数。

Q10 · 讨论

速度都为 1、通信每次为 2 时,两轮分工各需多久?若通信每次升为 6,均分工作量还能胜过耗时 32 的单人模型基线吗?

参考答案

工作、依赖与加速上限

把分组活动画成依赖图

刚才由四人交出局部和,再由一人合并。如果最后的合并也能两两同时做,会发生什么?

先缩小成一个可观察的例子:16个已知数求和,每次加法耗时1单位;加法作为节点,依赖作为边。

16 项两两归约:15 次加法,4 层依赖
12345678910111213141516+++++++++++++++输入已就绪8 次 / 24 次 / 12 次 / 11 次 / 1逐层执行:5 个时间单位总工作:15 次加法;最长依赖链:4 次加法。

每次加法成本为 1;逐层同步,忽略读取、通信与调度。4 个处理器执行 2+1+1+1=5 轮;8 个已能达到 4 层,继续增加不再缩短这条依赖链。

  • 工作量(work)WW 总共做15次加法。
  • 关键路径长度(span)DD 平衡树从输入走到总和,要经过4层依赖。

图中采用逐层完成再进入下一层的排程,暂不计通信。与刚才顺序合并的活动相比,我们改变了合并方式。

Q11 · 讨论

为什么4个处理器需要5轮?增加到8个可以缩短到几轮,增加到16个还会更快吗?

参考答案

两个不可绕过的下界

上一页的15次加法,要由 pp 个等速处理器完成;那条4层依赖链也必须依次推进。一般地:

Tpmax(Wp,D).T_p\ge\max\left(\frac{W}{p},D\right).

限制为什么绕不过去
工作量下界 W/pW/p总工作必须有人完成
关键路径下界 DD后一步要等前一步的结果

对上一页 W=15,D=4,p=4W=15,D=4,p=4 的树,下界为4,所画排程却需5轮:下界排除了更快的执行,能否达到还要看就绪任务与排程。

平均可用并行度 W/DW/D 给出一个尺度:处理器远多于它时,继续增加资源的收益会受到依赖限制。模型假设无额外调度和通信成本。

接下来,把这张依赖图放回 GEMM。

来源与延伸阅读

定义参考:CMU · Parallel Computing: Theory and Practice。接下来,把这张依赖图放回 GEMM。

寻找并行:独立输出与归约依赖

Cij=k=0K1AikBkj.C_{ij}=\sum_{k=0}^{K-1}A_{ik}B_{kj}.

当输入只读、输出不与输入重叠时,不同 (i,j)(i,j) 输出之间没有写依赖。每个输出内部的 KK 个乘积,也可以分工,但最后必须合并。

分组活动的局部和与刚才的归约树,正好解释了沿 k 分工所需的合并。按 i/j 分工则可以让不同执行者各自负责输出。

工作能否分开,还要配合正确的结果写入。

Q12 · 讨论

并行执行 i/j 与并行执行 k,分别需要怎样安排输出写入?把多个线程的 acc 直接写进同一个 C[i,j] 会发生什么?

参考答案

通信不是只有带宽:还有一次启动的成本

刚才交付一个局部和需要时间。真实系统的一次消息,既有固定启动成本,也有按字节付费的搬运:

Tmsg=L+Qβ.T_{\mathrm{msg}}=L+\frac{Q}{\beta}.

LL 是固定启动延迟,QQ 是字节数,β\beta 是有效传输速率。小消息可能主要花在启动与排队,大消息才有机会接近持续带宽。

课堂假设:L=10  μsL=10\;\mu sβ=20  GB/s\beta=20\;\mathrm{GB/s};所有消息串行、不重叠,采用十进制 KB/MB/GB。

Q13 · 讨论

1 MB 数据分成 1000 个 1 KB 消息发送,与合成一个消息发送,各需多久?

参考答案

粒度选择:省调度,也可能制造拖尾

任务切法可能的好处需要付出的代价
大块调度与消息次数少,局部复用好可并行块数减少,长尾与不均衡更难处理
小块便于分摊,空闲者能继续领工作调度、同步、消息和边界处理更频繁

刚才“先空闲先领取数字组”的理想模型没有收取调度费用。真实实现要把领取、排队、数据移动也计入时间。

分得更细,可能减少等待,也可能增加协调。 具体调度与工作窃取(work stealing,空闲执行者从其他执行者处领取未完成任务) 在并行程序设计部分展开。

衡量加速:延迟、吞吐与效率

活动中我们比较了“同一包数字多久算完”。真实程序也要先固定任务、正确性要求和计时范围,再选择指标:

指标衡量什么
延迟一次请求多久完成
吞吐量单位时间完成多少请求或工作
加速比 Sp=Tref/TpS_p=T_{\mathrm{ref}}/T_p相对一个明确基线快了多少
并行效率 Ep=Sp/pE_p=S_p/p在可比的等速处理器模型中,获得了多少理想加速

例如同一任务从100 ms降到50 ms,用了10个等速处理器,则加速比为2、并行效率为20%。请求更快完成与资源利用充分,是两个不同目标。

机器、输入、数据类型和计时边界应随结果一起记录;不能把一张GPU直接当成若干个等速CPU核。

接下来固定一份推理流程,看看局部加速怎样影响整体延迟。

来源与延伸阅读
  • Stanford CS149《Why Parallelism? Why Efficiency?》,PDF 第 13 页。

算子快了 8 倍,应用为什么只快了 3 倍多

某段推理流程的假设基线:准备 10 ms,矩阵乘法 80 ms,后处理 10 ms,共 100 ms。三个阶段按顺序执行。

团队将矩阵乘法缩短到原来的 1/81/8,其他阶段未变。把每一段还需要的时间加回来,就能得到用户实际感受到的收益。

Q14 · 讨论

新流程总耗时和整体加速比是多少?矩阵乘法若瞬间完成,整体最多快多少?

参考答案

阿姆达尔定律(Amdahl’s law):从时间分解推出来

上一页留下的20 ms,就是本次优化没有加速的部分。固定问题规模,将基线时间归一化为1,记这部分比例为 ss,其余部分获得 pp 倍加速:

Tp/T1=s+1sp,Sp=1s+(1s)/p.T_p/T_1=s+\frac{1-s}{p},\qquad S_p=\frac{1}{s+(1-s)/p}.

pp\to\inftys>0s>0Sp1/sS_p\to1/s。若有新增开销 HpH_p,应使用:

Tp=sT1+(1s)T1p+Hp.T_p=sT_1+\frac{(1-s)T_1}{p}+H_p.

ss基线中的时间占比,不是代码行数比例;并行扩展时还假设被并行化部分能理想分摊。

来源与延伸阅读

把阿姆达尔定律当作优化预算表

固定 100 ms 基线:局部加速,怎样改变整体时间
基线
100 ms
改进后30.00 ms

深色:不加速绿色:被加速橙色:新增开销

3.33× 整体加速20 + 10.00 + 0 = 30.00 ms

假设工作量固定、阶段不重叠,加速倍率只作用于指定部分。若倍率来自 p 个理想等速处理器,才可进一步解释为并行扩展;这里未自动假定倍率等于硬件数。

20% 时间不加速,80% 部分加速 8 倍:20+80/8=30 ms,整体 3.33 倍;增加 5 ms 开销后为 35 ms、2.86 倍。局部倍率无限大且无新增开销时,上限为 5 倍。

继续用同一份100 ms流程。先算被优化部分节省多少时间,再扣除新增的调度、通信、转换或同步开销。

Q15 · 讨论

100 ms 基线中,80% 的部分已加速 8 倍。若继续加速到 16 倍,却新增 5 ms 开销,这次进一步优化划算吗?

参考答案

从“有多少并行工作”,走向“每秒能完成多少”

前面分析了关键路径、未加速部分、通信与调度。即使独立工作已经足够多,GPU 仍受两种能力约束:

  • 计算能力:每秒最多完成多少次浮点运算?
  • 数据带宽:每秒最多从显存搬入、搬出多少字节?

接下来从 C[i] = A[i] + B[i] 开始,把一次加法的计算与搬运逐项计数,再回到矩阵乘法。

数据移动与屋顶线模型

先分清:FLOP、FLOPs 与 FLOP/s

FLOP(Floating Point Operation)是一次浮点运算;FLOPs 用来表示运算总量;FLOP/s 表示每秒完成的运算量。 本课用 FF 记总量,用 F/tF/t 记实际速率。

代码中的工作本课的计数约定
c = a + b1 次浮点加法 = 1 FLOP
c = a * b1 次浮点乘法 = 1 FLOP
acc = fma(a, b, acc)(融合乘加)一乘一加 = 2 FLOPs,即使融合成一条指令
读取输入、写回输出、计算整数下标不计入这里的浮点运算量;它们仍然花时间

完成 10910^9 次运算,是 1 GFLOP 的工作量;若用时 1 ms,速率才是 101210^{12} FLOP/s,即 1 TFLOP/s

芯片规格里的 “TFLOPS” 通常表示 TFLOP/s。峰值算力是速率上限,不能当成程序的实际速率。

读一张 GPU 规格表:容量、带宽、算力

NVIDIA HGX H200 八 GPU 平台官方产品图,八个 GPU 散热模组呈两行排列

图:NVIDIA HGX H200 八 GPU 平台;HGX 是多 GPU 平台,SXM 是 GPU 模组形态。下表按单颗 H200 SXM计算,并非整个平台的总规格。

规格数值它回答的问题
高带宽显存 HBM3e 的容量141 GB数据能不能放下?
HBM 显存带宽 BB4.8 TB/s = 4800 GB/s每秒最多搬多少字节?
FP32 峰值算力 PP67 TFLOP/s当前精度路径每秒最多算多少?

带宽同时承担读与写,不能给读取和写入各分配一份 4.8 TB/s。这里研究 HBM 显存 ↔ GPU 芯片内部的流量。PCIe(Peripheral Component Interconnect Express,高速外设互连)是连接主机与设备等部件的通用互连,NVLink 是 NVIDIA 的高速互连;这些链路的带宽与显存带宽是不同指标。HBM3e 是本卡采用的 HBM 代际。

规格必须与运算路径配对。 后面的FP32加法与矩阵例子统一使用上表;其他精度或运算路径有不同的峰值与精度要求。改精度时,存储量、误差与计算上限都要一起重算。以下由规格估算下界,实测成绩另列。

本段 TB、GB、TFLOP 均采用十进制前缀;FP32 峰值按乘加计数,对纯加法只作宽松计算上界。

来源与延伸阅读

来源:NVIDIA H200 官方规格。本段 TB、GB、TFLOP 均采用十进制前缀;FP32 峰值按乘加计数,对纯加法只作宽松计算上界。

一次加法:算了 1 次,却搬了 12 字节

三个互不重叠、连续的 FP32 数组,完成逐元素相加:

cpp
for (int i = 0; i < N; ++i)
    C[i] = A[i] + B[i];
逐项记账 · 一项 C[i] = A[i] + B[i]
HBM · 输入
A[i] = 2FP32 · 4 B
B[i] = 3FP32 · 4 B
片上 · 寄存器 + =
HBM · 输出
C[i] = 待写入FP32 · 4 B
浮点运算:0 FLOP · HBM 搬运:0 B从 HBM 中的两个 FP32 输入开始;计算与搬运分别记账。
读 → 读 → 加 → 写

按输入各读一次、结果写一次计数。步骤用于解释数据流,不表示 GPU 指令时序或实际耗时。

读取 A[i]:4 B;读取 B[i]:4 B;相加:1 FLOP;写入 C[i]:4 B。共 12 B、1 FLOP,计算强度为 1/12 FLOP/B。

假设输入从 HBM 各读一次,结果写回 HBM 一次。整个数组共有 F=NF=N FLOPs、Q=12NQ=12N B。

计算强度(arithmetic intensity)就是每搬 1 字节,对应多少次浮点运算:

I=FQ=N12N=1120.0833  FLOP/B.I=\frac{F}{Q}=\frac{N}{12N}=\frac{1}{12}\approx0.0833\;\mathrm{FLOP/B}.

数组变长,计算量与搬运量一起增长,计算强度不会因此提高。这里按必要读写建模;物理事务的额外流量另计。

把加法放上 H200:时间主要花在搬运

N=108N=10^8。三个 FP32 数组共 1.2 GB,假设输入尚未进入片上缓存、连续访问,每项按上一页只读写一次;仅计 GPU 内核,不含 CPU↔GPU 拷贝与分配。

要完成的工作总量由规格得到的时间下界
浮点加法10810^8 FLOPs108/(67×1012)1.49  μs10^8/(67\times10^{12})\approx1.49\;\mu s
HBM 读写1.2×1091.2\times10^9 B1.2×109/(4.8×1012)=250  μs1.2\times10^9/(4.8\times10^{12})=250\;\mu s

即使算术与访存完美重叠,也必须等较慢的一项:至少 0.25 ms。因此按本模型,性能最多为:

1080.25×103=0.4  TFLOP/s.\frac{10^8}{0.25\times10^{-3}}=0.4\;\mathrm{TFLOP/s}.

这叫 带宽受限(bandwidth-bound);它是访存受限(memory-bound)的一种情况,访存受限也可能来自等待延迟。本例的约束来自“喂数据”的速度。

Q16 · 讨论

如果只把计算峰值翻倍,0.25 ms 的下界会改变吗?如果只把 HBM 带宽翻倍呢?

参考答案

回到矩阵乘法:从三个循环算 FLOPs

前面的 C++ 基线计算 AM×KBK×N=CM×NA_{M\times K}B_{K\times N}=C_{M\times N}。沿循环逐层数:

循环含义数量
i,ji,j 选择输出MNMN 个输出元素
每个输出沿 kk 累加KK 次乘加,每次 2 FLOPs
总浮点运算量F2MNKF\approx2MNK FLOPs

三个数组的存储空间则是 S=(MK+KN+MN)sS=(MK+KN+MN)s B,ss 为每元素字节数。工作量 FF、占用空间 SS、执行流量 QQ 是不同的量。

1024×10241024\times1024 的 FP32 方阵:F2.147F\approx2.147 GFLOPs,S=12S=12 MiB。边长翻倍,FF 约增为 8 倍,SS 增为 4 倍;用时还要看怎样计算、怎样搬运。

按“每次乘加计 2 次”的工程惯例使用 2MNK2MNK;若只计数学上必要的乘法与加法,则为 MN(2K1)MN(2K-1)

规模比较: 固定总问题、增加处理器,称为强扩展;保持每个处理器的工作规模、同时扩大总问题,称为弱扩展。按这里的矩阵乘法计数,资源增至8倍时,边长增至2倍才对应每处理器计算工作不变。

来源与延伸阅读

占了 12 MiB,不代表只搬了 12 MiB

继续使用 1024×10241024\times1024 的 FP32 矩阵乘法 C=ABC=AB。三个数组占用 12 MiB,但执行时可能重复搬运它们。这里 N=1024N=1024

假设字节计数
A、B 各读取一次,C 写一次Qunique=4(3N2)Q_{\mathrm{unique}}=4(3N^2)
每次标量乘法的两个输入都重新跨边界读取Qno reuse=4(2N3+N2)Q_{\mathrm{no\ reuse}}=4(2N^3+N^2)

第一行是输入尚未进入缓存、输出也需跨边界返回时的乐观必要数据量;第二行是刻意无输入复用的执行模型。数据是否已在缓存(命中)、是否提前搬入(预取),以及写入时是否需要额外读取缓存行,都会影响真实流量。

Q17 · 讨论

两种模型各有多少字节?能否只阅读 C++ 源码中的读取次数,就认定第二行是显存的实测流量?

参考答案

同一矩阵乘法:复用决定落在哪类瓶颈

仍取 N=1024N=1024、FP32、F2.147F\approx2.147 GFLOPs,用 H200 的 P=67P=67 TFLOP/s、B=4.8B=4.8 TB/s 估算:

访问模型每字节运算数 I=F/QI=F/Q搬运下界 Q/BQ/B计算下界 F/PF/P
输入每次都重读,Q=8Q=8 GiB +4+4 MiB0.250 FLOP/B1.790 ms0.0321 ms
输入各读一次,Q=12Q=12 MiB170.7 FLOP/B0.00262 ms0.0321 ms

第一行由带宽决定上界;第二行由算术决定上界,称为 计算受限(compute-bound)。两行做的乘加一样多,区别是相同输入被反复搬运,还是留在片上重复使用

第二行是理想复用的估算,不保证具体实现可达;这些时间没有计入调度、指令依赖等额外开销。后面的分块会讨论怎样接近这种复用。

Q18 · 讨论

矩阵乘法的 FLOPs 没有减少,优化为什么仍可能大幅缩短时间?当上界转由计算限制,继续减少 HBM 流量还会按比例提速吗?

参考答案

屋顶线模型(Roofline):由两个时间下界推性能上界

PP 为指定数据类型与运算路径的计算速率上限,BB 为匹配存储层次的带宽上限:

tmax(FP,QB).t\ge\max\left(\frac{F}{P},\frac{Q}{B}\right).

即使计算与搬运能够完全重叠,也不能突破任一资源的能力;无法充分重叠或还有其他开销,只会更慢。于是:

Pachieved=F/tmin(P,BI),I=F/Q.P_{\mathrm{achieved}}=F/t\le\min(P,BI),\qquad I=F/Q.

对 H200 的这组 FP32 规格,转折点为:

I=P/B=67/4.813.96  FLOP/B.I^*=P/B=67/4.8\approx13.96\;\mathrm{FLOP/B}.

I<II<I^* 时,斜线 BIBI 更低,属于带宽限制的一侧;I>II>I^* 时,水平线 PP 更低,属于计算限制的一侧。一次加法的 1/121/12、理想复用矩阵乘法的 170.7170.7,现在能放到同一张图上。

来源与延伸阅读

来源:Williams、Waterman、Patterson,2009;LBL 文献页NVIDIA 性能模型说明

读图:提高强度与提高利用率是两个方向

屋顶线模型(Roofline)· 改工作负载,还是改硬件上限
0.010.111010010000.010.11101001000性能 · TFLOP/s计算强度 · FLOP/B(两轴均为对数刻度)橙点:自选示意性能I = 0.083,上界 0.400 TFLOP/s转折点 13.96 FLOP/B · 上界由带宽决定

默认参考 H200 SXM 的 FP32 / HBM 规格,半倍与双倍选项仅作假设对比。矩阵规模 N=1024,流量按所选复用模型计;橙点由比例控件生成,不是测量值。

P=67 TFLOP/s、B=4800 GB/s 时转折点约 13.96 FLOP/B。逐元素加法 I=1/12,上界为 0.4 TFLOP/s;1024 方阵乘法在输入各读一次的理想模型中 I≈170.7,上界为 67 TFLOP/s。实际性能还可能远低于这个上界。

默认使用前面的 H200 SXM FP32 / HBM 规格,可切换逐元素加法、无复用矩阵乘与理想复用矩阵乘。

减少相同计算所需的 Q,可以让工作向右移动;在相同 I 下更充分地利用硬件,可以让点向上移动。计算上限与带宽上限的变化,则改变屋顶本身。

Q19 · 讨论

在低计算强度区域,只升级计算峰值为什么可能无效?一个点远低于斜线,是否说明必须继续提高计算强度?

参考答案

为什么读了两次,却未必搬了两次

前面用两种流量模型比较同一个GEMM。实际的复用由什么决定?先看一个小缓存:重复使用的数据只有仍在其中,才能省下一次从下层搬运。

缓存行是缓存搬入与替换的一块连续数据;本例每行 4 B。数据仍在缓存叫“命中”,否则需从下一层搬入;容量满时,淘汰最久没访问的一行。

同样重复读取,缓存容量决定能否命中
最久未访问 → 最近访问[03]
读取 0首次访问该行累计从下层读取 4 B
1 / 6

初始为空,任意数据行都能占用任意槽位(全相联);满时淘汰最久未使用的一行(LRU)。只读、无预取;请求 1 B,未命中时搬入完整 4 B 行。这是手算模型,真实缓存还有其他约束。

同样的六次读取0、4、8、0、4、8:两行缓存从下层搬24B,三行只搬12B。数据重复出现,并不保证再次使用时还在缓存里

本例每个地址请求1B、每行4B;点击逐步查看替换。

因此计算强度必须配对说明:跨哪一层的字节数Q,与哪一层的带宽B。 接下来的CPU代码依靠缓存;此前H200模型统计的是HBM与片上系统之间的流量。

来源与延伸阅读
  • 课程原站

  • Stanford CS149《A Modern Multi-Core Processor (Part I)》,PDF 第 18–23 页。

屋顶下方:怎样区分搬得多与供给不足

回到 N=108N=10^8 的向量加法:必要读写1.2 GB,按H200规格得到0.25 ms下界。假设某次测得0.625 ms

从同一份工作计算结果
实际运算速率 108/(0.625×103)10^8/(0.625\times10^{-3})0.16 TFLOP/s
有效带宽 1.2 GB/0.625 ms1.2\ \mathrm{GB}/0.625\ \mathrm{ms}1920 GB/s,约为规格的40%

向量加法的计算强度很低,但这次执行还没有接近相应带宽屋顶。可以区分两种方向:

  • 搬得太多: 消除重复跨边界搬运,提高数据复用。
  • 供给不足或有等待: 检查并行工作是否充足、访问是否连续,以及启动、依赖和同步开销。

尤其当任务很小时,启动成本可能比计算和搬运都长。高计算强度也不保证有足够的独立工作,例如一条很长的单线程递推。

本页时间为假设示例。有效带宽用必要读写量除以时间;HBM物理事务量需要对应计数器。

来源与延伸阅读

诊断练习:先让数字彼此对得上

某报告写道:H200 上做 10810^8 项 FP32 向量加法,HBM 流量 1.2 GB,内核耗时 0.125 ms;HBM 带宽上限为 4800 GB/s。随后建议“继续提升算术吞吐”。

先核对计算量、搬运量与时间,再决定优化方向。

Q20 · 讨论

这份报告与其假设是否自洽?如果出现了这样的“超带宽”结果,你会优先检查什么,检查后怎样决定优化方向?

参考答案

分块、复用与性能实验

先做一个小改动:交换 k 与 j 的位置

还是同一个 CM×N=AM×KBK×NC_{M\times N}=A_{M\times K}B_{K\times N}。左边回顾前面的基线,右边只改变循环组织与累加位置。

蓝色:j 循环(输出列)橙色:k 循环(点积累加)
i-j-k原来:算完一个 C[i,j],再算下一个
for (int i = 0; i < M; ++i) {
  for (int j = 0; j < N; ++j) {
    float acc = 0.0f;
    for (int k = 0; k < K; ++k) {
      acc += A[i*K+k] * B[k*N+j];
    }
    C[i*N+j] = acc;
  }
}
内层 k 变化,j 固定:沿 B 的一列跳读。
B 的步长(stride):N 个元素FP32:4N 字节
B[j]+NB[N+j]+NB[2N+j]

acc 累加一个输出;k 循环结束后,写入 C 一次。

i-k-j改后:用一个 A[i,k],更新一行 C
std::fill(C, C + M*N, 0.0f);
for (int i = 0; i < M; ++i) {
  for (int k = 0; k < K; ++k) {
    float a = A[i*K+k];
    for (int j = 0; j < N; ++j) {
      C[i*N+j] += a * B[k*N+j];
    }
  }
}
内层 j 变化,k 固定:沿 B 的一行连续读。
B 的步长(stride):1 个元素FP32:4 字节
B[k*N]+1B[k*N+1]+1B[k*N+2]

先把 C 清零,再反复更新;a 在内层 j 中保持不变。

B 下标中的色块标出随内层循环变化的部分,箭头标出相邻两次访问的元素偏移增量。右侧先清零 C,再用 += 累加,仍计算 C = AB。

乘加次数不变,访问顺序变了。 右侧内层循环连续访问 B、C,并复用同一个 A 元素;C 的反复更新能否由寄存器或缓存承接,仍取决于编译器与硬件。

Q21 · 讨论

这个改写改变了 C=AB 的数学语义吗?对每个 C[i,j],k 的累加顺序是否改变?

参考答案

分块的目标:一次搬入,为多个输出服务

循环重排已经让一次读入的A元素服务多个输出。再进一步:把一小块A和一小块B留在快速存储中,让它们共同更新一块C。

把这样的矩形数据块称为分块(tile)。选择一个 T×TT\times T 输出块:让 A 的 T×TT\times T 子块与 B 的 T×TT\times T 子块一起更新它,再沿 K 方向推进。

数学工作量仍是 2N32N^3;设计目标是减少跨某条边界重复搬入的次数。

下一页跟踪四个输出,看同一批输入究竟被用了几次。

动画:一对输入块,更新四个输出

一个 2×2 输出块,沿 K 方向重复使用输入
A · 4×8
12345678234567893456789104567891011
×
B · 8×4
10-1-2210-1321043215432654376548765
C 的左上 2×2
5283

已执行 K 块 1/4(本块 k=01)。每块装入 A 的 4 个数、B 的 4 个数,更新 4 个输出,完成 16 FLOPs。累计装入 8 个输入,完成 16 FLOPs。

C 的部分和留在快速存储中

只画一个输出块的计算。每个 A 元素服务两个输出列,每个 B 元素服务两个输出行;完整输入块的装入与计算分阶段示意,不对应某种 GPU 指令时序。

四块完成后,左上输出为 [[204,168],[240,196]]。共装入 32 个输入元素,最终写出 4 个输出;若四个输出各自重复读取输入,需要装入 64 个输入元素。

本例 A4×8B8×4A_{4\times8}B_{8\times4},只跟踪 C 左上角的 2×22\times2 块,K 每次推进 2。每个输入块装入后,用于更新多个输出,而 C 的部分和留在快速存储中。

Q22 · 讨论

算完这四个输出,共搬入多少个输入元素?若每个输出各自重复读取全部输入,又是多少?最终输出是什么?

参考答案

从一个数据块(tile)推到整个矩阵的流量

先沿用上一页的小矩阵,数清楚搬了多少个数,再换算字节,最后推广成公式。

① 先数一次:两块输入,一块部分和

A · 4×84个B · 8×44个C · 4×4Σ×部分和留在这里一个大格 = 一个 2×2 块 = 4 个数;Σ 表示正在累加的输出块。

沿用上一页:AA4×84\times8BB8×48\times4CC4×44\times4,块边长 T=2T=2

绿色 A 块有 4 个数,橙色 B 块也有 4 个数。搬入后更新 C 左上角的 4 个输出。

本次输入=4+4=8 个数=32 B(FP32)\text{本次输入}=4+4=8\ \text{个数}=32\ \mathrm{B}\quad(\mathrm{FP32})

C 还没算完。 部分和留在快速存储中,此时不写回慢速存储。

1 / 5

① 先数一次:两块输入,一块部分和

A · 4×84个B · 8×44个C · 4×4Σ×部分和留在这里一个大格 = 一个 2×2 块 = 4 个数;Σ 表示正在累加的输出块。

沿用上一页:AA4×84\times8BB8×48\times4CC4×44\times4,块边长 T=2T=2

绿色 A 块有 4 个数,橙色 B 块也有 4 个数。搬入后更新 C 左上角的 4 个输出。

本次输入=4+4=8 个数=32 B(FP32)\text{本次输入}=4+4=8\ \text{个数}=32\ \mathrm{B}\quad(\mathrm{FP32})

C 还没算完。 部分和留在快速存储中,此时不写回慢速存储。

② 走完 K 方向,才完成一个输出块

A · 4×81234B · 8×41234C · 4×4Σ×4 次累加 → 写出一次一个大格 = 一个 2×2 块 = 4 个数;Σ 表示正在累加的输出块。

点积长度是 8,每次处理 2 项,所以同一个 C 块需要 8/2=48/2=4 次推进。图中编号相同的 A、B 块配对。

输入读取=8 个/次×4 次=32 个\text{输入读取}=8\ \text{个/次}\times4\ \text{次}=32\ \text{个} 这个输出块的流量=(32 读+4 写)×4 B=144 B\text{这个输出块的流量}=(32\ \text{读}+4\ \text{写})\times4\ \mathrm{B}=144\ \mathrm{B}

不是每一步都把 C 搬进搬出。 4 次都在快速存储里累加,完成后才写出 4 个结果。

③ 输出有四块,把同一笔账算四遍

A · 4×84个4个4个4个4个4个4个4个B · 8×44个4个4个4个4个4个4个4个C · 4×41234×4 个输出块分别计算一个大格 = 一个 2×2 块 = 4 个数;Σ 表示正在累加的输出块。

C 的每个方向可分成 4/2=24/2=2 块,共 2×2=42\times2=4 个输出块。每一块都要走完上面的 4 次推进。

总读取=32×4=128 个,总写入=4×4=16 个\text{总读取}=32\times4=128\ \text{个},\qquad\text{总写入}=4\times4=16\ \text{个} Q=(128+16)×4 B=576 BQ=(128+16)\times4\ \mathrm{B}=576\ \mathrm{B}

数的是搬运次数,不是不同元素的个数。 算右侧 C 块时,左侧 C 块用过的 A 数据还要再搬一次;这里不计输出块之间的缓存复用。

④ 把具体数字换成 N 与 T

现在改为 N×NN\times N 方阵,块边长为 TT,且 TT 整除 NN。上面的三个计数依次变成:

总输入读取=2T2每次读几个×NT每块推进几次×(NT)2有几个输出块=2N3T\text{总输入读取}=\underbrace{2T^2}_{\text{每次读几个}}\times\underbrace{\frac{N}{T}}_{\text{每块推进几次}}\times\underbrace{\left(\frac{N}{T}\right)^2}_{\text{有几个输出块}}=\frac{2N^3}{T}

C 一共有 N2N^2 个结果,每个只写一次。设每个元素占 ss 字节(FP32 时 s=4s=4):

QT=(2N3T+N2)读入个数 + 写出个数×s字节/个Q_T=\underbrace{\left(\frac{2N^3}{T}+N^2\right)}_{\text{读入个数 + 写出个数}}\times\underbrace{s}_{\text{字节/个}}

⑤ 运算没少,搬运少了,计算强度就提高了

方阵乘法仍做 2N32N^3 次浮点运算。计算强度就是 运算次数 ÷ 搬运字节数。代入 FP32 的 s=4s=4

IT=2N3(2N3/T+N2)×4=T4+2T/N FLOP/BI_T=\frac{2N^3}{\left(2N^3/T+N^2\right)\times4}=\frac{T}{4+2T/N}\ \mathrm{FLOP/B}

NTN\gg T,分母中的 2T/N2T/N 很小,才可以近似为:

ITT4 FLOP/BI_T\approx\frac{T}{4}\ \mathrm{FLOP/B}

例如 N=1024,T=32N=1024,T=32,精确值约 7.877,近似值为 8。块越大,同一输入服务的输出越多;下一页看它要付出多少存储空间。

本页统计慢速存储与快速存储之间的搬运:输入块搬入,C 从零累加并保留到完成后写回一次,不读取旧 C,也不计不同输出块之间的缓存复用。这是流量模型,不是硬件实测值。

更大的数据块,需要更大的工作空间

继续取 N=1024、FP32,使用上一页的流量模型。若 A/B 输入块与 C 累加块都计入一份抽象快速存储预算:

Stile=3T2×4  B.S_{\mathrm{tile}}=3T^2\times4\;\mathrm{B}.

TI_T(FLOP/B)工作空间
163.9693 KiB
327.87712 KiB
6415.51548 KiB
12830.118192 KiB

真实 GPU 的寄存器、共享内存(shared memory,供同一组线程显式协作使用的片上存储)与缓存各有独立约束;这个合并预算只用于理解容量与复用的竞争。

Q23 · 讨论

若这份抽象预算仅有 64 KiB,T=32、64、128 中哪些放得下?哪个能跨过前面 H200 FP32 的 13.96 FLOP/B 转折点?这就能保证达到峰值吗?

参考答案

从分块思路到实现:还要照顾哪些细节

分块不改变每个输出的点积,只改变数据与计算的组织顺序:

要安排的事数学与数据含义
选一个输出块只更新 C 的这一小片区域
沿 K 方向逐块累加把完整点积分成几段,累加到同一批输出
处理不足一块的边缘只读取和更新有效元素,不能越过矩阵边界

先记住这三项职责,再区分 CPU 的缓存复用与 GPU 的显式数据搬运。

在 CPU 上,数据能否留在快速存储里,由缓存与编译器共同决定。 把循环划成小块,并不自动实现前面模型中“输入搬一次、块内反复用”的假设。GPU 上还需要把块分给线程,并安排共享内存、寄存器与同步;后续 CUDA 编程会具体实现。

读已有测量前:五种实现是否在做同一件事

以下五个版本全部在 CPU 单线程上运行,不使用 GPU。 比较朴素 i-j-k、循环重排 i-k-jT=16/32/64 分块;循环重排本身也在复用数据。先预测顺序,再看实测:改善复用的设计,最终要通过具体代码实现。

这里直接阅读已经保存的结果,先确认比较口径;不需要现场编译或计时。

比较时固定什么本实验的做法
同一计算目标FP32 的 C=AB;用相同输入构造 FP64(64 位双精度)参考,检查误差
同一计时边界只计计算实现,i-k-j和分块所需的C清零计入;不计分配、初始化与校验
同一运行条件相同编译器与选项,无显式多线程;先运行一次完成预热,再轮换版本顺序,各测 5 次

程序已核对小矩阵、非方阵和尾块;结果通过检查后,才比较耗时。下一页展示相同环境下的中位数,不把某一次最快结果当作稳定表现。

下一页直接比较记录中的结果。

CPU 实测:分块为什么不一定更快

Ni-j-ki-k-j分块 T=16分块 T=32分块 T=64
1280.773 ms0.118 ms0.233 ms0.209 ms0.145 ms
2569.366 ms0.985 ms1.874 ms1.670 ms1.203 ms
51277.984 ms8.566 ms15.450 ms13.474 ms11.747 ms

参考实验环境:Apple M5、arm64、macOS 26.6.2、Apple LLVM 21.0.0,统一 -O3 -ffp-contract=off;单线程,5 次测量的中位数,输入反复复用。

循环重排本身已经在复用: i-k-j 连续访问 B 与 C,一次取出的 A 元素服务多个输出。分块是在这个基础上争取更多复用,不是从“没有复用”开始。

可能被新增开销抵消: 这份分块代码依赖 CPU 缓存,并增加循环、边界与地址计算。哪些因素主导还需要测量;三个版本的计算循环都被编译器向量化,不能把差别简单归因于“只有一个用了 SIMD”。

GPU 上也要算收益与代价。 高效矩阵乘法通常用分块,让线程协作把输入放入共享内存与寄存器;但小矩阵、不合适的块尺寸或额外同步,也可能让分块版本变慢。CPU 分块同样可以加速;这张表只说明这些实现、规模与环境下的结果。

来源与延伸阅读

Q24 · 讨论

这张 CPU 表能否证明“CPU 不适合分块,GPU 分块一定更快”?你会怎样区分“缓存复用没有明显改善”与“额外循环开销抵消收益”这两种解释?设计一个后续对照。

参考答案

另一种复用:算子融合减少中间结果搬运

前面用分块减少输入的重复搬运。还可以消除算子之间的中间结果:先计算 u=x+yu=x+y,再计算 z=u×wz=u\times w;FP32,每项各做一次加法和一次乘法。

若两段之间的 u 必须写入再读出目标存储层,两个算子共读写 6Ns=24N6Ns=24N B;融合后只读 x/y/w 并写 z,共 4Ns=16N4Ns=16N B。

数学工作仍约 2N FLOPs。在这一搬运主导模型中,减少流量带来的理想上限是 24/16=1.524/16=1.5 倍,还可能减少一次启动。

融合也会改变寄存器需求、并行安排和中间量生命周期;若其他使用者需要 u,就不能直接省掉它。

从算子回到大模型

批量(batch)与矩阵行数 M

批量(batch) 是一组一起处理的样本;在大模型在线服务中,通常是一批请求。它不直接等于一个句子的单词数。词元(token) 是分词后的文本单位,一个单词可能对应多个词元。

对线性层 XM×KWK×N=YM×NX_{M\times K}W_{K\times N}=Y_{M\times N}M 是本次一起处理的输入向量数,每个词元在这一层对应一个 K 维向量。先区分两种来源:

本次线性层处理什么请求数 b每请求词元数 L输入行数 M理想计算强度
1 条请求推进一步1110.500 FLOP/B
1 条已知输入,含 128 个词元112812860.235 FLOP/B
128 条请求,各推进一步128112860.235 FLOP/B

等长输入可整理为 [b,L,K][bL,K][b,L,K]\to[bL,K],所以 M=bLM=bLM=128 可以来自一条输入的多个位置,也可以来自多条请求;同一请求尚未生成的未来词元通常仍需依次生成。

表中固定 K=N=4096K=N=4096,按 FP32 输入输出各必要访问一次估算:

Iunique=2MKN4(MK+KN+MN).I_{\mathrm{unique}}=\frac{2MKN}{4(MK+KN+MN)}.

H200 FP32 模型的转折点为 13.96 FLOP/B。增加可一起计算的行数,能提高权重复用机会;M=128 本身不能告诉我们有几个用户。 这是线性层的理想访问模型;实际精度、实现与其他算子仍影响整模型性能。合批也可能增加等待,吞吐提高不保证单请求更快。

来源与延伸阅读

参考:NVIDIA 矩阵乘性能模型Hugging Face 大模型推理

激活重算:用额外计算,换取更低的显存峰值

前面的分块把输入留在快速存储中,供多个输出复用。训练还可以采取另一种取舍:用额外计算,缩短中间结果在显存中保存的时间。

反向传播需要用到前向计算中的激活(中间结果)

做法前向时反向需要中间结果时
常规保存留下自动求导所需的中间激活直接读取
激活重算(activation checkpointing,也称激活检查点)保存选定边界的输入等必要状态重新执行相应计算,恢复所需激活

节省的是部分激活的存储,代价是增加重算。收益取决于数据要保存多久、重算多贵,以及当前是否被显存容量卡住。

PyTorch提供 torch.utils.checkpoint 支持这种做法;重算需保持相关程序状态与随机状态的一致性。

来源与延伸阅读

Q25 · 讨论

矩阵分块让输入留下来重复用,激活重算却选择少保存、用时再算。这两种优化为什么不矛盾?

参考答案

FlashAttention:把分块与融合用到注意力

再看一个把分块与融合用于真实AI算子的例子。标准注意力先比较查询与键,再将得分归一化成权重,最后对值加权求和:

S=QattKd,A=softmax(S),O=AV.\begin{aligned} S &= \frac{Q_{\mathrm{att}}K^\top}{\sqrt d},\\[0.6em] A &= \operatorname{softmax}(S),\\[0.4em] O &= AV. \end{aligned}

QattQ_{\mathrm{att}}KKVV 分别是查询(Query)、键(Key)、值(Value):用查询与键比较相关性,再加权汇总值。softmax 把每行得分转换为和为 1 的权重。TT 是词元数,dd 是每个查询/键向量的维度;一个注意力头是一组这样的计算。自回归模型还会遮住未来位置,式中略去。每个头的 S,AS,A 都有 T×TT\times T 个位置。

单个请求、32 个头、T=8192T=8192仅一张按 FP16 完整保存的得分或概率张量就需 32×81922×232\times8192^2\times2 B = 4 GiB

执行方式中间数据怎样处理
分阶段物化完整矩阵得分、概率等中间矩阵反复写入与读出 HBM
FlashAttention 的核心思路分块搬入输入,在片上更新归一化统计量和输出,避免在 HBM 存完整的 T×TT\times T 中间矩阵

这将前面的分块、融合与数据移动连在一起:主导计算仍是二次规模,FLOPs 不一定更少,HBM 流量与中间存储却可显著减少。

分块时仍要合并跨块的归一化统计量,以保持同一个注意力数学目标;改变运算顺序可能改变浮点末位。具体算法在FlashAttention一讲展开。

来源与延伸阅读

回到开场的流水线:让搬运与计算重叠

前面减少了重复搬运和中间存储。还有一种提速方式:让不同阶段重叠,减少暴露出来的等待。 这也回到开场“阅读前文,同时生成后文”的类比。

假设四批独立数据,每批搬运 2 ms、计算 5 ms;两阶段可独立工作、使用两份输入缓冲,没有资源争用,结果留在 GPU:

方式搬运安排GPU 计算安排全部完成
完全串行每次算完才搬下一批搬完一批,再计算一批4(2+5)=284(2+5)=28 ms
两阶段流水线第 1–4 批分别在 0–2、2–4、7–9、12–14 ms 搬运第 1–4 批分别在 2–7、7–12、12–17、17–22 ms 计算22 ms

第一批仍需 7 ms;稳态中每 5 ms 完成一批。对 nn 批,相应理想时间为:

t=tcopy+tcompute+(n1)max ⁣(tcopy,tcompute).\begin{aligned} t &= t_{\mathrm{copy}}+t_{\mathrm{compute}}\\[0.5em] &\quad +(n-1)\max\!\left(t_{\mathrm{copy}},t_{\mathrm{compute}}\right). \end{aligned}

GEMM 也可计算当前数据块、预取下一数据块;需要额外缓冲及正确同步。

来源与延伸阅读

Q26 · 讨论

流水线既没减少字节数,也没减少 FLOPs,为什么仍会更快?如果下一批输入依赖上一批输出,还能照此重叠吗?

参考答案

本讲总结与概念辨析

本讲总结:从一次 AI 回答,走到一份性能分析

从大模型、芯片与能源的故事出发,最后都要落到具体程序:数据怎么表示,工作怎么分,字节怎么搬,时间怎么省。

这一段学了什么留下的核心认识对应的例子
AI 与计算平台大量张量计算需要硬件、软件栈与数据供给共同支撑PyTorch 到 GPU;CUDA 与 CANN
数据与数值表示存得下与算得准,是两项都要满足的要求OOM;FP16 舍入;FP32 累加
矩阵与并行分工找到独立工作,还要处理依赖、负载差异与通信C=AB;四位同学分组加法
加速上限与性能模型先算工作、依赖和流量,再判断什么限制时间关键路径;阿姆达尔定律;屋顶线模型
优化与证据复用、融合、重算和流水线各有收益与代价CPU 分块实测;FlashAttention;搬运与计算重叠

分析一个算子:先说清计算目标与精度,再数工作量和数据量,提出优化,最后用同一口径核对结果与时间。

概念辨析:数据、精度与性能,先分清在量什么

容易混淆的概念区别本讲的提醒
bit / byte;GB / GiB1 B=8 bit;GB 用十进制,GiB 用二进制参数个数乘每项字节数,再按统一单位换算
数值范围 / 表示精度能表示多大,与相邻可表示数隔多远不同FP16 与 BF16 都占 2 B,范围与精细程度却不同
输入精度 / 累加精度输入先被怎样表示,与部分和怎样更新不同FP32 累加可减轻累加误差,补不回输入舍入丢掉的信息
数学等价 / 浮点结果逐位相同实数表达式相等,不保证不同运算顺序舍入相同并行求和改变分组,可能改变末位;这不等于运算随机发生
容量 / 流量 / 带宽占多少空间 / 穿过某层多少字节 / 每秒能搬多少模型大小不是训练显存峰值;数组大小也不是总搬运量
FLOPs / FLOP/s运算总量 / 每秒完成的运算量一次乘加计 2 FLOPs;峰值算力不等于实测速率

再记住一个具体数:FP16 在 [2048, 4096) 的相邻间隔为 2。 将 2049 存成 FP16 会舍入;之后转回 FP32,丢失的那个 1 也不会回来。

三个模型,回答三个不同的“为什么不能更快”

工作与关键路径:独立工作是否足够? 总工作量为 W、关键路径为 D,p 个等速处理器,即使忽略通信与调度成本,也有:

Tpmax(Wp,D).T_p\ge\max\left(\frac{W}{p},D\right).

阿姆达尔定律:只优化一部分,整体能收益多少? 固定任务,s 为未加速部分的基线时间占比,其余部分加速 p 倍,忽略新增开销:

S=1s+(1s)/p.S=\frac{1}{s+(1-s)/p}.

屋顶线模型:计算与搬运,哪项上限更低? F 为 FLOPs,Q 为指定存储层次的流量,P 与 B 分别为匹配精度路径的算力和该层带宽上限:

I=F/Q,Pachievedmin(P,BI).I=F/Q,\qquad P_{\mathrm{achieved}}\le\min(P,BI).

它们给出的是限制,不是完成时间的承诺。 排程、通信、额外指令和等待,仍可能使实现达不到这些上界。

优化方法辨析:究竟改了哪一笔成本

方法希望改善什么必须付出或检查什么
增加并行、调整分工让独立工作同时推进,减少长尾依赖、负载差异、调度与通信成本
重排循环、分块改善访问顺序,在快速存储中复用输入数据是否真的留下来;容量、循环与同步开销
算子融合省去中间结果在目标存储层的写出与读回中间量是否还有其他使用者;资源需求是否增加
激活重算降低训练时部分激活的显存占用额外计算与状态恢复是否值得
流水线、预取让独立阶段重叠,减少暴露的等待依赖、额外缓冲、同步与资源争用

分块和融合主要争取少搬;重算允许多算来省空间;流水线即使不减少 FLOPs 和字节数,也可能缩短总时间。 所以“更快”并不只有“少做运算”这一条路。

回到大模型还要区分两点:batch 是一起处理的样本或请求,M 是线性层一起处理的输入行数;吞吐提高,也不保证单请求延迟下降。

最后自检:同样的计算,为什么少搬一点就值得

沿用算子融合的例子:先算 u=x+y,再算 z=u*w。数组 x、y、w、z 互不重叠,各数组与中间结果均为 FP32,取前面用过的 N=10⁸;u 没有其他使用者。

  • 分开执行:每个算子各读两个输入、写一个输出;u 写入 HBM 后,再由第二个算子从 HBM 读回。
  • 融合执行:读取 x、y、w 后直接产生 z;中间值 u 保留在片上,不写入 HBM。

仍按每项必要读写一次建模,仅计内核中的 HBM 流量;沿用前面的 4.8 TB/s 带宽上限,并假设搬运主导。

Q27 · 讨论

  1. 两种实现的 FLOPs、HBM 流量与计算强度各是多少?按带宽得到的时间下界呢?
  2. 能否因此保证融合后实测快 1.5 倍?若输入改成 FP16、再用 FP32 计算,能否保证结果与原来相同?
参考答案

下一步:GPU 怎样把这些工作执行起来

现在我们能给同一个任务列出三份清单:正确结果与误差要求、工作与依赖、数据量与搬运量。 也能提出增加并行、减少搬运或重叠阶段的改进方向。

接下来,把模型中的“处理器”和“搬运”落到真实执行:

第一讲已经会问第二讲继续追问
是否有足够多的独立工作?工作怎样分给核心、线程和成组的执行通道?
数据搬运会花多久?一份工作等待数据时,硬件能否推进另一份工作?
复用需要多少空间?寄存器与缓存等片上资源,怎样限制可同时推进的工作?

下一讲的问题:GPU怎样把工作分给线程,又怎样在等待数据时继续执行其他工作?

我们将从具体执行方式理解:为什么程序已经包含很多并行工作,实际性能仍会低于模型上界。