努力努力再努力wz头像
【CUDA入门系列】PTX、Compute Capability 与 sm_XX:彻底理清 GPU 代码的编译与架构兼容封面图

【CUDA入门系列】PTX、Compute Capability 与 sm_XX:彻底理清 GPU 代码的编译与架构兼容

🔥 本文专栏:CUDA
🌸作者主页:努力努力再努力wz

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

💪 今日博客励志语录:所谓前途,并不是远处有一条已经铺好的路,而是你每解决一个眼前的问题,脚下就多出一块路。


思维导图

CUDA 程序
│
├── Host Code:CPU 侧代码
│      ↓
│   nvcc 作为编译入口
│      ↓
│   调用 Host Compiler
│   例如 g++
│      ↓
│   CPU 目标代码
│
└── Device Code:GPU 侧代码
       ↓
    CUDA Device 编译链
       ↓
    compute_XX
    虚拟架构 / 虚拟计算能力目标
       ↓
       PTX
    虚拟 GPU 指令集
       ↓
      sm_XX
    真实 GPU 架构代码生成目标
       ↓
      Cubin / SASS
    GPU 真正可以执行的机器代码
       ↓
       GPU

与此同时,还需要区分两个兼容维度:

CUDA 软件栈  ↔  NVIDIA Driver
                 ↓
          nvidia-smi 中的
            CUDA Version

Device Code  ↔  GPU 硬件
                 ↓
          Compute Capability

引入

在此前学习 CUDA 的过程中,我们已经认识到一个最基本的事实:

CUDA 程序
=
CPU 侧代码
+
GPU 侧代码

例如:

#include <cuda_runtime.h>

__global__ void add(int* a, int* b, int* c)
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    c[i] = a[i] + b[i];
}

int main()
{
    // CPU / Host 代码

    add<<<1, 256>>>(...);

    return 0;
}

这里:

int main()

主要运行在 CPU 上。

而:

__global__ void add(...)

则是最终需要运行在 GPU 上的代码。

因此 CUDA 程序与普通 C++ 程序最大的不同之一就在于:

同一个源文件中,同时存在两套最终运行在不同硬件上的代码。

普通的 g++ 主要负责 CPU 代码的编译,其并不能完整识别 CUDA 所扩展出来的:

__global__
__device__
<<<grid, block>>>
threadIdx
blockIdx

等 CUDA 语法和编程模型。

所以 CUDA 提供了:

nvcc

作为 CUDA 程序的编译入口。

但是如果只停留在:

nvcc test.cu -o test

这一条命令上,我们只能知道“CUDA 程序可以被编译”,却并不知道:

GPU 侧代码究竟经历了什么?

PTX 是什么?

compute_86 是什么?

sm_86 又是什么?

Compute Capability 和 GPU 架构有什么关系?

为什么 nvidia-smi 里还有一个 CUDA Version?

-arch=sm_86 到底指定了什么?

本文就沿着这一条逻辑链逐步展开。


一、为什么 CUDA 编译不能简单交给 g++

1. CUDA 程序同时面向 CPU 和 GPU

普通 C++ 程序最终主要面向 CPU:

C/C++
  ↓
g++
  ↓
CPU 机器代码
  ↓
CPU 执行

而 CUDA 程序不同:

                    CUDA .cu
                       │
              ┌────────┴────────┐
              ↓                 ↓
          Host Code         Device Code
          CPU 侧代码         GPU 侧代码
              ↓                 ↓
             CPU               GPU

CPU 与 GPU 本身就是两种不同的处理器。

它们:

硬件结构不同
执行模型不同
机器指令不同

所以最终自然也不可能使用完全相同的一套编译流程。


2. nvcc 更适合理解成“CUDA 编译总入口”

执行:

nvcc test.cu -o test

从使用者角度来看,这和:

g++ test.cpp -o test

非常相似。

都是:

输入源文件
    ↓
一条编译命令
    ↓
得到最终可执行文件

但这只是上层看到的结果。

nvcc 本身更适合理解成一个:

CUDA Compiler Driver,也就是 CUDA 编译流程的总入口和组织者。

其会针对 Host Code 与 Device Code 走不同的编译链。

可以先建立这样的心智模型:

                        test.cu
                           │
                          nvcc
                           │
               ┌───────────┴───────────┐
               ↓                       ↓
            Host Code               Device Code
            CPU 代码                 GPU 代码
               ↓                       ↓
       Host Compiler              CUDA Device
        例如 g++                   编译链
               ↓                       ↓
        CPU 目标代码                GPU 代码生成
               │                       │
               └───────────┬───────────┘
                           ↓
                          链接
                           ↓
                     最终可执行文件

因此:

nvcc 并不是简单地把整个 .cu 文件当成一种普通源代码直接编到底,而是负责组织 CUDA 程序中 Host 与 Device 两侧的编译工作。


二、为什么 GPU 代码不是一步直接生成机器指令

现在先把 CPU 一侧放下,只聚焦:

Device Code

也就是 GPU 代码这一侧。

最终目标显然是:

CUDA C++
   ↓
GPU 能真正执行的机器指令

但是和 CPU 编译类似,中间通常并不是简单一步到位。

我们熟悉的 CPU 编译过程,可以粗略理解成:

C / C++
   ↓
编译器
   ↓
汇编层
   ↓
CPU 机器指令

CUDA GPU 代码同样存在一个非常重要的中间层:

CUDA C++
   ↓
PTX
   ↓
GPU 机器代码

这里的:

PTX

就是理解 CUDA GPU 编译链的第一个关键概念。


三、PTX:CUDA C++ 与真实 GPU 机器代码之间的中间层

1. PTX 是什么

PTX 全称:

Parallel Thread Execution

对于当前阶段,可以先把它理解成:

NVIDIA 为 CUDA GPU 代码定义的一套虚拟指令集 / 中间指令表示。

例如我们写:

__global__ void add(int* a, int* b, int* c)
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    c[i] = a[i] + b[i];
}

这是人更容易编写和理解的 CUDA C++。

经过编译以后,其中类似:

blockIdx.x
blockDim.x
threadIdx.x

这样的 CUDA 编程模型概念,会继续向更加底层的表示进行转换。

PTX 中可能会看到类似:

mov.u32
mad.lo.s32
ld.global
st.global

这样的指令形式。

因此从抽象层次来看:

CUDA C++
    ↓
高级语言

PTX
    ↓
虚拟 GPU 指令

最终 GPU 机器代码
    ↓
真实硬件执行指令

2. PTX 可以类比 CPU 汇编,但不能完全等同

为了快速建立直觉,我们确实可以先做一个类比:

CPU:

C++
 ↓
汇编
 ↓
CPU 机器指令

CUDA:

CUDA C++
   ↓
PTX
   ↓
GPU 机器指令

但是这里只能写成:

PTX ≈ GPU 世界中的“中间汇编”

而不能简单写成:

PTX = 某款 GPU 的真实汇编

原因就在于:

PTX 并没有直接绑定死到某一款具体 GPU 的真实机器指令。

传统 CPU 汇编通常已经与某一套真实 ISA 强相关,例如:

x86-64 汇编
ARM64 汇编

而 PTX 更像是:

NVIDIA 定义的一套虚拟 GPU ISA

它后面仍然需要进一步针对真实 GPU 生成最终机器代码。

所以完整链路应该理解成:

CUDA C++
   ↓
PTX
   ↓
针对具体 GPU 架构继续编译
   ↓
真实 GPU 机器代码

这也是 PTX 被称为“虚拟指令集”的根本原因。


四、既然 PTX 是虚拟指令,为什么还需要计算能力

理解到这里,很容易产生一个问题:

如果 PTX 已经是一套统一的虚拟 GPU 指令集,那么是不是所有 NVIDIA GPU 都可以直接支持同一份 PTX?

答案并不是简单的“所有 GPU 都完全一样”。

因为不同 GPU 世代的硬件能力并不相同。

例如,较老的一些 GPU 可能只支持普通 CUDA Core 计算、全局内存访问、共享内存等基础能力;而更新的 GPU 还会增加新的硬件能力,例如 Tensor Core,用于加速特定类型的矩阵计算。

可以先直观理解成:

较老 GPU:
支持普通 CUDA Core 计算
支持全局内存访问
支持共享内存
……

较新 GPU:
支持普通 CUDA Core 计算
支持全局内存访问
支持共享内存
……
+
支持 Tensor Core 等新的硬件能力

如果我们的 CUDA 代码只使用这些较老 GPU 本来就支持的基础能力,那么旧 GPU 也有可能正常运行。

但是如果 CUDA 代码使用了某种只有新 GPU 才具备的硬件能力,例如依赖 Tensor Core 的功能,那么旧 GPU 本身没有对应的硬件支持,就无法完成这种计算。

为了方便后面继续讨论,我们可以把这些具体硬件能力进一步抽象一下。

假设:

较老 GPU:
支持 A
支持 B
支持 C

而更新一代 GPU 在此基础上增加了新的能力:

较新 GPU:
支持 A
支持 B
支持 C
支持 D
支持 E

这里的:

A / B / C

可以理解成较早已经存在的 GPU 硬件能力,而:

D / E

则代表后来新增加的硬件功能。

如果 CUDA 代码只使用:

A / B / C

那么较老 GPU 和较新 GPU 都可能支持。

但如果 CUDA 代码使用了:

D

那么:

较新 GPU
→ 支持 D
→ 可以实现

较老 GPU
→ 没有 D
→ 无法实现

于是问题就来了:

PTX 虽然是一套虚拟 GPU 指令集,但它依然必须告诉系统:这份 PTX 最低假设底层 GPU 具备哪一档硬件能力?

这也就是为什么 PTX 还需要和:

Compute Capability

联系起来。

也就是说,Compute Capability 本质上就是在描述:

这一档 GPU 到底支持哪些硬件能力

因此即使 PTX 是虚拟指令,它仍然需要表达:

这份 PTX 可以假设底层 GPU
至少具备什么能力?

于是就引出了:

Compute Capability

也就是:

计算能力

五、Compute Capability:GPU 的硬件能力版本

1. Compute Capability 不是“性能评分”

NVIDIA 会为 GPU 定义一个:

Compute Capability

通常简称:

CC

形式为:

X.Y

例如:

7.5
8.0
8.6
8.9
9.0

其中:

X = Major Version
    主版本号

Y = Minor Version
    次版本号

例如:

Compute Capability 8.6
                   │ │
                   │ └── Minor = 6
                   │
                   └──── Major = 8

但是这里一定不要把:

8.6

理解成:

GPU 性能 8.6 分

Compute Capability 描述的是:

这块 GPU 对 CUDA 编程模型暴露了哪一档硬件功能和指令能力。

它关注的是:

支持哪些计算特性
支持哪些指令语义
支持哪些数据类型
支持哪些原子操作
共享内存等硬件规格
SM 能力
Tensor Core 等相关能力
……

而不是直接描述:

显存多大
显存带宽多高
实际算力有多强
游戏帧数有多高

因此:

Compute Capability 越高,并不等价于实际性能一定越高。

同一个 Compute Capability 下,不同 GPU 也完全可以拥有不同的:

SM 数量
显存容量
显存带宽
工作频率
功耗设计
浮点运算吞吐

下面这张图正好体现了这一点。

在这里插入图片描述

例如图中 RTX 2070 与 RTX 2080 Ti 都属于:

Compute Capability 7.5

但它们的显存容量、显存带宽以及实际浮点计算峰值明显不同。

所以应该建立:

Compute Capability
        ≠
GPU 性能评分

而应该理解成:

Compute Capability
        =
GPU 的 CUDA 硬件能力版本

2. Major 与 Minor 怎么理解

可以先建立一个直觉:

Major Version
    ↓
较大的硬件能力代际变化

Minor Version
    ↓
同一大能力体系中的进一步变化

例如:

8.0
8.6
8.9

它们 Major 都是:

8

但是 Minor 不同,因此具体硬件功能和代码生成目标仍然可能存在差异。


六、Kepler、Turing、Ampere 到底是什么

在 CUDA 和 GPU 资料中,我们经常会看到:

Kepler
Maxwell
Pascal
Volta
Turing
Ampere
Ada Lovelace
Hopper
Blackwell

这些并不是 CUDA API,也不是 CUDA Toolkit 版本。

它们本质上是:

NVIDIA 不同 GPU 微架构世代的名称。

NVIDIA 很喜欢使用科学家、数学家、计算机科学家等人物的名字来命名 GPU 架构,例如:

Kepler        → Johannes Kepler
Maxwell       → James Clerk Maxwell
Pascal        → Blaise Pascal
Volta         → Alessandro Volta
Turing        → Alan Turing
Ampere        → André-Marie Ampère
Ada Lovelace  → Ada Lovelace
Hopper        → Grace Hopper
Blackwell     → David Blackwell

但是要建立一个非常关键的认知:

GPU 架构名称和 Compute Capability 并不是简单的一一对应关系。

下面这张图展示了架构名称、Compute Capability 与 sm_XX 之间的关系。

在这里插入图片描述

例如:

Volta
→ CC 7.0

Turing
→ CC 7.5

它们架构世代已经不同,但是 Major 都是:

7

再例如 Ampere 架构内部还可能对应:

CC 8.0
CC 8.6
CC 8.7

而后来的 Ada Lovelace:

CC 8.9

Major 依然是:

8

因此不能理解成:

Major 8
=
Ampere

更加准确的层次是:

GPU 微架构世代
例如 Ampere
        ↓
具体 GPU 芯片 / 产品
        ↓
拥有一个确定的 Compute Capability
例如 8.6

一个 GPU 架构世代可能对应一个或多个 Compute Capability;而某一块具体 GPU 设备本身只有一个确定的 Compute Capability。

因此:

Ampere
 ├── CC 8.0
 ├── CC 8.6
 └── CC 8.7

这种关系是合理的。


七、compute_XX:给 PTX 指定“虚拟计算能力目标”

认识了 Compute Capability 以后,再来看:

compute_80
compute_86
compute_89

就不再神秘了。

例如:

compute_86

数字 86 本质上就是:

Compute Capability 8.6

去掉小数点后的写法。

因此:

CC 7.5 → compute_75
CC 8.0 → compute_80
CC 8.6 → compute_86
CC 8.9 → compute_89
CC 9.0 → compute_90

这里的 compute_XX 被称为:

Virtual Architecture
虚拟架构目标

可以先这样理解:

在生成 PTX 时,告诉编译器:这份 PTX 可以假设底层 GPU 至少具备 Compute Capability X.Y 这一档提供的能力。

例如:

CUDA C++
   ↓
compute_80
   ↓
PTX

这里的含义不是:

我要在某一块具体 GPU 上运行

而是:

生成 PTX 时,
允许依赖 CC 8.0 这一档硬件能力

所以 compute_80 更像是:

一条能力门槛

1. 为什么 PTX 也需要版本约束

假设:

compute_86

那么这份 PTX 就可以使用:

CC 8.6 所允许的能力

如果实际 GPU 只有:

CC 8.0

那么问题并不是:

GPU 已经开始执行机器代码,
执行到一半发现某条指令不会。

更加准确的理解是:

compute_86 PTX
       ↓
准备针对 CC 8.0 GPU 生成机器代码
       ↓
发现 PTX 所依赖的能力
超过这块 GPU 的硬件能力
       ↓
无法生成合法的目标机器代码

因此普通 PTX 的兼容规则可以先记成一句非常简单的话:

实际 GPU Compute Capability
              >=
PTX 的 compute_XX

也就是:

大于等于即可。

例如:

compute_80 PTX

面对:

CC 7.5  → 不满足
CC 8.0  → 满足
CC 8.6  → 满足
CC 8.9  → 满足
CC 9.0  → 满足

因此:

compute_80

可以理解成:

这份普通 PTX 的最低硬件能力门槛是 CC 8.0。


八、SM 与 sm_XX:不要把两者混成一个概念

1. SM 本身是什么

SM 全称:

Streaming Multiprocessor

它是 NVIDIA GPU 内部真实存在的硬件执行单元。

我们此前已经建立过:

GPU
 ├── SM
 ├── SM
 ├── SM
 └── SM

线程块会被调度到 SM 上,Warp 最终在 SM 内进行调度和执行。

所以:

SM

本身不是“指令集”。

它是:

真实硬件执行单元

2. 那 sm_86 又是什么

当我们在 CUDA 编译命令中看到:

sm_86

此时已经不是在说:

某一个具体 SM 单元

而是在说:

以 Compute Capability 8.6 所对应的真实 GPU / SM 架构作为机器代码生成目标。

例如:

sm_80
sm_86
sm_89
sm_90

分别表示不同的真实 GPU 代码生成目标。

这里最关键的是:

sm_XX 后面的 XX

并不是 NVIDIA 另外设计的一套独立编号。

它直接来自:

Compute Capability X.Y

例如:

CC 7.5 → sm_75
CC 8.0 → sm_80
CC 8.6 → sm_86
CC 8.9 → sm_89
CC 9.0 → sm_90

所以:

sm_86

本质上可以理解成:

针对 CC 8.6 这一档真实硬件能力
生成 GPU 机器代码

九、CC、compute_XX 与 sm_XX 到底是什么关系

到这里可以把三个概念放到一张图里:

                 Compute Capability 8.6
                    硬件能力规范
                         │
             ┌───────────┴───────────┐
             ↓                       ↓
        compute_86                  sm_86
      虚拟架构目标               真实代码生成目标
             ↓                       ↓
            PTX                 GPU 二进制机器代码

因此可以把三者压缩成:

CC X.Y
=
硬件能力版本

compute_XY
=
这套能力规范在 PTX 虚拟架构层的表示

sm_XY
=
这套能力规范在真实 GPU 机器代码生成层的表示

例如:

CC 8.6
  │
  ├── compute_86
  │      ↓
  │   生成以 8.6 能力为基础的 PTX
  │
  └── sm_86
         ↓
      针对 8.6 真实硬件生成机器代码

因此以前很容易产生一种感觉:

sm_86 中的 86
是不是某种独立的 SM 架构版本?

这个直觉并不是完全错误,因为:

sm_80
sm_86
sm_89

确实是在区分不同的真实代码生成目标。

但是现在需要补充:

这套 sm_XX 编号本身就是沿用 Compute Capability 的编号体系,并不是另一套独立版本系统。


十、PTX 与 Cubin 的兼容范围为什么不同

前面我们已经认识到:

普通 PTX

具有比较强的前向兼容能力。

例如:

compute_80 PTX

可以在运行时针对:

CC 8.0
CC 8.6
CC 8.9
CC 9.0
……

等满足能力要求的 GPU 继续 编译。

所以普通 PTX 可以先记:

实际 GPU CC
   >=
PTX compute_XX

即可。

但是如果已经生成:

sm_80

对应的 Cubin,那么它已经是:

面向某一档真实 GPU 架构生成的机器代码

因此兼容范围会明显更窄。

普通情况下可以先记成:

sm_XY Cubin

通常要求:

GPU Major 必须相同
并且 GPU Minor >= 目标 Minor

例如:

sm_80 Cubin

CC 8.0 → 可以
CC 8.6 → 可以
CC 8.9 → 可以
CC 9.0 → 不属于普通跨 Major 二进制兼容范围

而:

sm_86 Cubin

CC 8.0 → 不可以
CC 8.6 → 可以
CC 8.9 → 可以

因此:

PTX

承担了一个非常重要的作用:

为未来更高 Compute Capability 的 GPU 保留继续 JIT 编译的空间。


十一、CUDA Version 与 Compute Capability 是两个不同维度

学习到 Compute Capability 后,很容易联想到:

nvidia-smi

默认输出中经常看到:

Driver Version: ...
CUDA Version: ...

于是很容易产生一个误区:

nvidia-smi 中的 CUDA Version
是不是就等于 GPU 的计算能力?

或者:

是不是 CUDA 12.x 就对应 CC 8.x?

答案都是否定的。

这里必须拆成两个完全不同的分支。


1. 第一条分支:CUDA 软件栈与 Driver 的兼容

CUDA 程序并不是自己直接操作 GPU 硬件。

例如:

cudaMalloc(...)
cudaMemcpy(...)
kernel<<<...>>>(...)

最终都需要经过 CUDA 软件栈以及 NVIDIA Driver 与硬件完成交互。

可以先建立:

CUDA 应用
   ↓
CUDA Runtime / Driver API
   ↓
NVIDIA Driver
   ↓
GPU 硬件

因此上层 CUDA 软件需要调用 Driver 所提供的能力。

如果:

CUDA 软件栈太新
       ↓
需要较新的 Driver 能力
       ↓
当前 Driver 太老

就可能出现软件兼容问题。

所以:

nvidia-smi

默认输出中的:

CUDA Version

更加准确地说表示:

当前 NVIDIA Driver 所支持的最高 CUDA 软件兼容版本。

2. 第二条分支:Device Code 与 GPU 硬件能力的兼容

另一条链则是:

CUDA Device Code
       ↓
PTX / Cubin
       ↓
GPU Compute Capability
       ↓
真实 GPU 是否能够支持

这里关注的是:

GPU 硬件本身有没有这个能力

例如:

程序要求 CC 8.6 的硬件能力

实际 GPU
=
CC 8.0

即使 Driver 非常新,也无法凭空让硬件增加原本不存在的能力。

所以两个分支分别是:

第一维度:

CUDA 软件栈
    ↕
NVIDIA Driver

关注:
软件兼容

以及:

第二维度:

Device Code
    ↕
GPU

关注:
硬件 Compute Capability

3. 用一个服务器类比理解第一条分支

可以做一个不完全严格、但比较形象的类比。

假设 HTTP Server 只能正确处理某些协议版本和功能。

客户端发来的请求如果使用服务器完全不理解的新能力:

Client
  ↓
新版本请求能力
  ↓
Server 不支持

服务器就无法正确处理。

CUDA 软件栈与 Driver 之间也存在类似的“软件能力匹配”问题。

但是这里要注意:

CUDA Driver 不是在解析一种类似 HTTP 文本报文的东西。

CUDA 更准确的本质是:

API / ABI / Driver Capability

的兼容问题。

这个类比只用于理解:

上层软件提出的能力
必须是底层软件能够支持的

而不能继续把两者机械等同。


十二、三个经常混淆的“版本”必须彻底分开

到这里可以把:

nvcc --version

nvidia-smi 中的 CUDA Version

Compute Capability

彻底区分开。

nvcc --version

回答:

我实际安装的 CUDA Toolkit / nvcc
是什么版本?

例如:

nvcc --version

可能看到:

Cuda compilation tools, release 12.0

这属于:

CUDA 开发工具链版本

nvidia-smi 中的 CUDA Version

回答:

当前 Driver
最高支持到哪一档 CUDA 软件兼容能力?

属于:

CUDA 软件栈
     ↕
Driver

这一条兼容链。


Compute Capability

回答:

这块 GPU 硬件
属于哪一档 CUDA 硬件能力?

例如:

8.6
8.9
9.0

属于:

Device Code
    ↕
GPU 硬件

这一条兼容链。

最终可以画成:

             CUDA Toolkit
             nvcc --version
                   │
                   │ 软件兼容
                   ↓
            NVIDIA Driver
                   │
         nvidia-smi CUDA Version
                   │
                   ↓
                  GPU
                   ↑
                   │ 硬件能力
                   │
          Compute Capability
                   ↑
                   │
            PTX / Cubin

十三、回到 nvcc:-arch 到底指定了什么

现在有了:

PTX
Compute Capability
compute_XX
sm_XX

这些基础,再来看:

-arch

就非常自然了。

最简单的 CUDA 编译命令是:

nvcc test.cu -o test

它与:

g++ test.cpp -o test

在使用体验上类似:

一条命令
   ↓
完成编译与链接
   ↓
生成最终程序

这里并不是没有中间阶段,而是:

nvcc

帮助我们把多个编译阶段组织起来了。

加入:

-arch

以后,就是开始显式告诉 nvcc:

Device Code 希望按照什么 GPU 架构目标进行代码生成。


1. -arch=compute_86

例如:

nvcc test.cu -arch=compute_86 -o test

这里:

compute_86

就是:

虚拟架构目标

其核心含义是:

生成 PTX 时,
假设目标 GPU 至少具备 CC 8.6 的能力

可以先理解成:

CUDA C++
   ↓
compute_86
   ↓
PTX

当前 nvcc 中,这种写法属于 PTX-only 的目标形式。


2. -arch=sm_86

更常见的是:

nvcc test.cu -arch=sm_86 -o test

这里:

sm_86

表示:

以 CC 8.6 所对应的真实 GPU 架构作为代码生成目标。

所以从逻辑上可以理解为:

CUDA C++
   ↓
compute_86
   ↓
PTX
   ↓
sm_86
   ↓
真实 GPU 二进制

而当前 nvcc 对:

-arch=sm_86

提供了一个方便的简写语义。

可以近似展开为:

虚拟目标:
compute_86

真实代码目标:
sm_86

同时保留:
compute_86 PTX

因此:

nvcc test.cu -arch=sm_86 -o test

可以用一句人话理解成:

编译这个 CUDA 程序,GPU 侧以 CC 8.6 为能力基础生成 PTX,并为 sm_86 真实架构生成对应的 GPU 二进制代码,同时保留相应 PTX 以提供更好的前向兼容能力。


3. 更完整的参数形式

以后继续深入 nvcc 时,会看到:

--gpu-architecture=compute_86
--gpu-code=sm_86,compute_86

其中:

--gpu-architecture

解决:

前端生成 PTX 时
假设什么虚拟计算能力

而:

--gpu-code

解决:

后端最终保留哪些真实 Cubin
以及是否保留 PTX

不过现阶段并不需要立刻深入复杂的:

-gencode

先把:

compute_XX
=
PTX 虚拟能力目标

sm_XX
=
真实 GPU 代码生成目标

固定下来即可。


十四、如何查询当前 GPU 的 Compute Capability

理解了这么多以后,一个最实际的问题就是:

我手上的 GPU 到底是什么 Compute Capability?

最直接的方法之一就是:

nvidia-smi --query-gpu=name,compute_cap

例如可能得到:

name, compute_cap
NVIDIA GeForce RTX XXXX, 8.9

这里:

8.9

就是这块 GPU 的:

Compute Capability

于是可以立刻得到:

CC = 8.9

对应虚拟目标:
compute_89

对应真实代码目标:
sm_89

如果想让输出更简洁,也可以使用:

nvidia-smi --query-gpu=name,compute_cap --format=csv,noheader

1. 也可以通过 CUDA Runtime API 查询

程序运行时,也可以通过 CUDA Runtime API 获取 Compute Capability。

例如:

#include <cuda_runtime_api.h>
#include <iostream>

int main()
{
    int device = 0;

    int major = 0;
    int minor = 0;

    cudaDeviceGetAttribute(
        &major,
        cudaDevAttrComputeCapabilityMajor,
        device
    );

    cudaDeviceGetAttribute(
        &minor,
        cudaDevAttrComputeCapabilityMinor,
        device
    );

    std::cout << "Compute Capability: "
              << major << "."
              << minor << '\n';

    return 0;
}

如果输出:

Compute Capability: 8.6

那么就可以对应:

compute_86
sm_86

十五、现在重新看整条 CUDA GPU 编译链

到这里,可以把此前零散的概念重新放回同一条链路。

CUDA .cu
   │
   │
   ├────────────────────────────────────┐
   │                                    │
   ↓                                    ↓
Host Code                           Device Code
CPU 代码                             GPU 代码
   │                                    │
   ↓                                    ↓
Host Compiler                        nvcc Device
例如 g++                              编译链
   │                                    │
   ↓                                    ↓
CPU 目标代码                     指定虚拟能力目标
                                   compute_XX
                                        │
                                        ↓
                                       PTX
                                虚拟 GPU 指令集
                                        │
                                        ↓
                                指定真实架构目标
                                     sm_XX
                                        │
                                        ↓
                                Cubin / GPU Binary
                                        │
                                        ↓
                                真实 GPU 机器指令
                                        │
   └──────────────────────┬─────────────┘
                          ↓
                       最终程序
                          ↓
                       运行阶段
                          ↓
                    NVIDIA Driver
                          ↓
                         GPU

其中:

Compute Capability

贯穿了虚拟与真实两个代码生成阶段。

例如:

CC 8.6
   │
   ├── compute_86
   │      ↓
   │   PTX 虚拟能力目标
   │
   └── sm_86
          ↓
       真实 GPU 代码目标

所以:

Compute Capability 是能力规范,compute_XX 是虚拟架构视角,sm_XX 是真实代码生成视角。


十六、最后再把几个最容易混淆的问题一次说清

1. PTX 是不是 GPU 真正执行的机器指令?

不是。

PTX
=
虚拟 GPU ISA

其后还需要:

PTX
 ↓
针对 sm_XX 编译
 ↓
GPU Binary

GPU 最终执行的是面向真实硬件生成的机器代码。


2. compute_86 是不是某款具体 GPU?

不是。

它表示:

CC 8.6 的虚拟架构目标

用于约束:

生成 PTX 时可以依赖哪些 GPU 能力

3. sm_86 是不是第 86 个 SM?

不是。

这里:

86

来自:

Compute Capability 8.6

sm_86 表示:

面向 CC 8.6 对应真实 GPU/SM 架构
生成 GPU 二进制代码

4. SM 本身是不是指令集?

不是。

SM
=
Streaming Multiprocessor
=
GPU 内部真实硬件执行单元

而:

sm_86

是在 CUDA 编译语境下描述:

真实 GPU 代码生成架构目标

5. Compute Capability 越高是不是性能越强?

不一定。

它主要描述:

硬件功能版本

而实际性能还与:

SM 数量
频率
显存
带宽
功耗
芯片规模
具体工作负载

等大量因素有关。


6. Kepler、Turing、Ampere 是不是 Compute Capability 的主版本号?

不是。

它们是:

GPU 微架构世代名称

一个架构可能对应一个或多个 Compute Capability。

同时,不同架构也可能处于同一个 Compute Capability Major 之下。


7. nvidia-smi 中的 CUDA Version 是不是 GPU 的 Compute Capability?

不是。

nvidia-smi CUDA Version

主要反映:

Driver 的 CUDA 软件兼容能力

而:

Compute Capability

描述:

GPU 硬件能力

这是两个不同维度。


8. 普通 PTX 对 GPU 的版本要求是不是必须完全一致?

不是。

基础阶段可以直接记:

实际 GPU CC
   >=
PTX compute_XX

即可。


十七、最终心智模型

如果只保留本文最核心的一条链,可以记成:

我们写 CUDA C++
        ↓
程序员不用直接面对具体 GPU 机器指令
        ↓
nvcc 组织 CUDA 编译流程
        ↓
Host Code
交给 Host Compiler
例如 g++
        ↓
CPU 目标代码


Device Code
        ↓
指定 compute_XX
        ↓
告诉编译器:
PTX 可以假设什么 GPU 硬件能力
        ↓
生成 PTX
        ↓
PTX 是虚拟 GPU 指令集
        ↓
指定 sm_XX
        ↓
告诉编译器:
最终为哪一档真实 GPU 架构生成代码
        ↓
生成 Cubin / GPU 机器代码
        ↓
GPU 执行

而:

CC X.Y

是贯穿其中的核心硬件能力版本:

CC 8.6
   │
   ├── compute_86
   │     虚拟架构目标
   │
   └── sm_86
         真实架构目标

同时 CUDA 程序真正运行起来,还要再看两个维度:

维度一:
CUDA 软件栈
    ↕
NVIDIA Driver
    ↓
软件兼容性


维度二:
Device Code
    ↕
GPU Compute Capability
    ↓
硬件兼容性

到这里:

nvcc
PTX
Compute Capability
compute_XX
sm_XX
Cubin
Driver
nvidia-smi CUDA Version

这些原本看起来分散的概念,就能够真正被放进同一套 CUDA 编译与运行心智模型中。


在这里插入图片描述

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2301_80260194/article/details/167083001

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--