第 1 章 · 导论与环境

⏱️ 预计 30 分钟 🎯 跑通 deviceQuery 📂 code/ch01_intro/

学习目标

前置知识

会用 gcc/clang 编译 C++17 程序;能在终端里运行 make;理解什么是堆栈和指针。不需要任何 GPU / 并行编程经验。

核心概念

1.1 为什么是 CUDA?

大模型推理本质上是一连串巨大的矩阵乘 + 元素级运算。 CPU 一次处理几路(4~64 线程),GPU 一次能并行处理几万路。 以 NVIDIA A100 为例:6912 个 CUDA core + 432 个 Tensor Core, 理论 fp16 算力 312 TFLOPS,是顶级桌面 CPU 的 100 倍以上。

graph LR
    subgraph CPU
      C1["Core × 4-64"] --> Cmem["DRAM
~50 GB/s"] end subgraph GPU G1["CUDA core × 数千"] --> Gmem["HBM 显存
~1-3 TB/s"] G1 --> TC["Tensor Core
专攻 MMA"] end style C1 fill:#f3f1e8,stroke:#8b1538 style G1 fill:#f3f1e8,stroke:#2f5d3a style TC fill:#f3f1e8,stroke:#a86420

关键差距不只在"核数",更在内存带宽:A100 的 HBM2e 带宽 ~1.5 TB/s,是 DDR5 内存的 30 倍。 LLM 推理是访存瓶颈主导的工作负载(小 batch 下尤其明显),这是为什么 GPU 是必需品。

1.2 环境准备(三选一)

方式 A · Google Colab(推荐初学者,免费)

  1. 打开 colab.research.google.com
  2. 菜单:Runtime → Change runtime type → Hardware accelerator: T4 GPU → Save
  3. 新建 cell,运行 !nvidia-smi,看到 T4 16GB 即成功
  4. 克隆本仓库(替换为你的 fork):
!git clone https://github.com/YOUR/ops.git
%cd /content/ops
!make -C code/ch01_intro ARCH=sm_75 run

方式 B · 本地 Linux + NVIDIA GPU

# Ubuntu 22.04 为例
sudo apt update
sudo apt install -y nvidia-driver-535     # 重启后生效
sudo apt install -y cuda-toolkit-12-4     # 包含 nvcc
echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

# 验证
nvcc --version
nvidia-smi

方式 C · Docker(GPU 已可见)

docker run --gpus all -it -v $PWD:/work \
  nvcr.io/nvidia/cuda:12.4.1-devel-ubuntu22.04 bash
# 容器内:cd /work && make -C code/ch01_intro run
⚠️ macOS / Apple Silicon: Apple 在 2018 年后已停止支持 NVIDIA 显卡,本机无法跑 CUDA。 请用方式 A(Colab)或方式 C(远程 Linux + Docker)。 不过本仓库的 HTML 教程在任何系统上都能读,code/common/cpu_ref.h 里的 CPU 实现也能本机运行验证算子正确性。

关键代码

1.3 第一个程序:deviceQuery

每个学 CUDA 的人写的第一个程序,是问 GPU 自我介绍。源码: code/ch01_intro/device_query.cu。 关键 API 是 cudaGetDeviceProperties(),它把 GPU 的 100+ 项硬件参数填到一个 cudaDeviceProp 结构体里。

核心代码

cudaDeviceProp p{};
CUDA_CHECK(cudaGetDeviceProperties(&p, /*device=*/0));

std::printf("GPU 0: %s\n", p.name);
std::printf("  SM count           : %d\n", p.multiProcessorCount);
std::printf("  Max threads / SM   : %d\n", p.maxThreadsPerMultiProcessor);
std::printf("  Shared mem / block : %zu B\n", p.sharedMemPerBlock);
std::printf("  Global memory      : %.2f GiB\n",
            p.totalGlobalMem / double(1ull << 30));

// 实测带宽(DDR -> ×2)
double bw = 2.0 * p.memoryClockRate * 1e3 * (p.memoryBusWidth / 8.0) / 1e9;
std::printf("  Peak bandwidth     : %.1f GB/s\n", bw);

编译与运行

cd code/ch01_intro
make ARCH=sm_75    # T4
./device_query

典型输出(T4)

# 请实测并记录本机结果
CUDA driver:  12.2
CUDA runtime: 12.4
Devices:      1

============================================================
GPU 0: Tesla T4
  Compute capability : 7.5  (sm_75, Turing)
  SM count           : 40
  Max threads / block: 1024
  Max threads / SM   : 1024  (= 32 warps)
  Warp size          : 32
  Registers / SM     : 65536 (32-bit)
  Shared mem / block : 49152 B
  Shared mem / SM    : 65536 B
  Global memory      : 14.56 GiB
  L2 cache           : 4096 KiB
  Peak bandwidth     : 320.0 GB/s

逐行解读这些数字:

运行结果

1.4 第二个程序:内存带宽实测

源码:bandwidth_estimate.cu。 它做三组对照: pageable H2D(普通 malloc 出来的主机内存 → 显存)、 pinned H2D(cudaMallocHost 的页锁定内存 → 显存)、 D2D(显存内拷贝)。

// pinned (page-locked) host buffer:DMA 不需先复制到内核缓冲
char* h_pinned = nullptr;
CUDA_CHECK(cudaMallocHost(&h_pinned, bytes));

t.start();
CUDA_CHECK(cudaMemcpy(d_buf, h_pinned, bytes, cudaMemcpyHostToDevice));
t.stop();

结果记录表(在目标 GPU 上运行后填写):

transfer带宽说明
H2D pageableTODO(on GPU)记录 payload、PCIe 代际与计时方法
H2D pinnedTODO(on GPU)与同 payload 的 pageable 路径比较
D2DTODO(on GPU)记录 GPU、时钟与 warmup/repeats
💡 启示: 数据一旦上了显存就别再下来。 LLM 推理时,权重一次性 H2D 之后常驻显存;只有 prompt token id 和 logits 走 H2D/D2H,因为它们小到可以忽略带宽。

自检清单

Q1: 为什么不能用 CPU 跑 GPT-2?

能跑,但慢得无法接受。GPT-2 small 一次前向需要 ~250M FLOPs × 模型层数,在 CPU 上单 token 生成需要几百毫秒;GPU 上 < 10 毫秒。差距随模型规模扩大而加剧(GPT-4 在 CPU 上不可用)。

Q2: T4 有 2560 个 CUDA core,是不是同时跑 2560 个线程?

更微妙。T4 的 SM 数与每 SM 资源要按 NVIDIA T4 规格 和当前 CUDA Programming Guide 的 compute-capability 表核验;驻留 thread 数与同周期执行的 ALU 数不是同一概念,硬件通过 warp 调度隐藏延迟。

Q3: 为什么 pinned memory 比 pageable 快一倍?

pageable 内存可能被 OS 换出。CUDA 需要先把它复制到一块内核拥有的 "staging buffer" 才能 DMA 给 GPU。pinned 内存被锁在物理内存中,DMA 直接读取,少一次拷贝。代价:pinned 占用宝贵的物理内存。

Q4: cudaGetDeviceProperties 报告的 memoryClockRate 单位是什么?

千赫兹 (kHz)。所以 5001000 表示 5.001 GHz。带宽公式 2 × clock × bus_width / 8 里要把 kHz × 1e3 转成 Hz。

Q5: 我看到 sharedMemPerBlock = 48 KB,但 sharedMemPerMultiprocessor = 64 KB,差在哪?

SM 上的 SRAM 总量是 64 KB(甚至 100 KB+ 在 A100 上),默认每个 block 最多用 48 KB;如果你显式开 dynamic shared memory 并调用 cudaFuncSetAttribute(..., cudaFuncAttributeMaxDynamicSharedMemorySize, N),可以用到更多——但会限制 SM 上同时驻留的 block 数。

练习题

  1. cores per SM 表:参考 code/ch01_intro/exercises/01_cores_per_sm_starter.cu,补全函数 cores_per_sm(),让程序能打印 "总 CUDA core 数 = SM 数 × cores/SM"。
    答案见 _solution.cu
  2. 带宽测量:用不同 payload (--MB=1, 4, 16, 64, 256) 跑 bandwidth_estimate,画出 "payload vs 带宽" 曲线。你会发现小 payload 下带宽急剧下降——原因?(提示:固定延迟摊销到的字节数变少。)
  3. 查阅:在 CUDA 维基 找出你 GPU 对应的 SM 架构发布年份和 Tensor Core 代际,记在自己的笔记里。

1.7 工业实战:选 GPU、上多卡、看监控

学到这里你能用一张 GPU 跑 hello world。生产环境还要回答四个问题:选哪张卡、怎么上多卡、跑起来怎么监控、用云还是自建

1.7.1 GPU 选型矩阵(规格核验:2026-07-20)

NVIDIA 有数据中心、专业可视化与 GeForce 等产品线;ECC、NVLink、MIG、散热形态和部署许可应按具体 SKU 与官方条款核验,不能只从架构代号推断。

规格来源:NVIDIA Data Center 产品页;精确口径按各 SKU datasheet 核验。
SM 架构显存显存带宽峰值口径典型场景
T4sm_75 Turing16 GB320 GB/sFP16 Tensor Core教学 / 轻量推理
L4sm_89 Ada24 GB300 GB/sFP16/FP8 Tensor Core低功耗推理 / 视频
L40Ssm_89 Ada48 GB864 GB/sFP16/FP8 Tensor Core中等模型推理
A100 80Gsm_80 Ampere80 GB2.0 TB/sFP16/BF16 Tensor Core训练 / 推理基线
H100 80Gsm_90 Hopper80 GB3.35 TB/sFP16/BF16/FP8;官方表需区分 sparse/denseTMA/WGMMA/FP8
H200 141Gsm_90 Hopper141 GB4.8 TB/sFP16/BF16/FP8;官方表需区分 sparse/dense长上下文、显存敏感
B200sm_100 Blackwell180 GB HBM3e以 HGX/B200 规格页为准NVFP4/FP8;必须标 dense/sparseBlackwell 训练与推理
GB200sm_100 Blackwell每 GPU 186 GB;Superchip 共 372 GB每 Superchip 16 TB/sNVFP4/FP8;官方表按 sparse | denseGrace Blackwell 一致内存系统
B300Blackwell Ultra288 GB HBM3e8 TB/sNVFP4/FP8;必须标 dense/sparse大显存 reasoning inference

决策口诀

1.7.2 多 GPU 互联:NVLink vs PCIe vs IB

训练大模型时多 GPU 间数据交换量巨大(每步几 GB),互联带宽直接决定吞吐。

互联带宽(双向)延迟典型场景
PCIe Gen4 x16~64 GB/s~1 μs消费机、L4 服务器
PCIe Gen5 x16~128 GB/s~1 μsH100 PCIe 版
NVLink 4 (H100 SXM)900 GB/s~200 nsH100 8-GPU 服务器
NVLink 5 (B200/B300)按 SKU/系统规格核验TODO(on GPU)Blackwell scale-up
InfiniBand HDR/NDR200/400 Gb/s~2 μs多节点训练

判断你的卡有没有 NVLink

nvidia-smi topo -m       # 看 GPU 之间的连接拓扑
# 输出 NV4 / NV8 = NVLink (好); SYS / PHB = 走 PCIe (慢)

多卡通信库永远走 NCCL(all-reduce / all-gather / reduce-scatter / broadcast 等集合通信)。torch.distributed 和 vLLM 的 tensor/pipeline parallel 都基于它。第 8 章的 stream 与第 14 章的扩展任务会用到。

1.7.3 生产监控:nvidia-smi + DCGM

跑生产任务必须监控 GPU 状态。常用命令:

# 实时刷新基本状态 (温度、功耗、显存、利用率)
nvidia-smi -l 1

# 进程级显存占用 + GPU 利用率
nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv

# 持续监控的工程化方案 (NVIDIA DCGM)
dcgmi dmon -e 1001,1002,1003,203,150
#  1001 = GR engine active   1002 = SM active   1003 = SM occupancy
#   203 = mem util           150  = mem copy util

# 拉到 Prometheus / Grafana
docker run -d --gpus all -p 9400:9400 nvcr.io/nvidia/k8s/dcgm-exporter
curl localhost:9400/metrics  # OpenMetrics 格式

看 nvidia-smi 时关注这 4 个指标

1.7.4 云 vs 自建:建立自己的成本模型

云价随区域、承诺周期、整机 SKU、网络和供给变化,本教程不保留无日期的“每卡每小时”数字。比较时记录供应商 SKU、地区、日期、最短租期、CPU/RAM/网络配额,并把自建的采购、折旧、电力、制冷、运维和闲置率放进同一张表。

1.7.5 环境陷阱 5 连发

  1. 驱动版本 vs CUDA Toolkit 版本不匹配:每个 CUDA Toolkit 要求最低驱动版本。nvcc --version 是 Toolkit,nvidia-smi 顶部 "Driver Version: 535.xx / CUDA Version: 12.2" 是驱动支持的最高 CUDA,不是实际 Toolkit。混淆是新手第一大坑。
  2. CUDA_VISIBLE_DEVICES 改变物理 ID 顺序:默认按 PCI bus ID 排序,跟 nvidia-smi 顺序不一定相同。多卡训练时容易把同一张卡当作两张。设 CUDA_DEVICE_ORDER=PCI_BUS_ID 强制对齐。
  3. MIG (Multi-Instance GPU):A100/H100 可以切成 7 个独立小 GPU 跑多租户,每片有独立 SM 和显存。但同一时刻只能选"全卡"或"分片"模式。误开 MIG 后程序看到的算力是 1/7,很多人调试半天没头绪。nvidia-smi -mig 0 关闭。
  4. 容器里看到的 GPU 数 ≠ 物理数:docker 用 --gpus '"device=0,1"' 限制可见。K8s 用 nvidia.com/gpu: 2 resource。两者都通过 NVIDIA Container Toolkit 实现 isolation。
  5. ECC 开启时显存少 ~6%:A100-80G 实际可用 ~76 GB(ECC 校验位占空间)。生产推理通常关 ECC 换更多显存:nvidia-smi -e 0 然后重启。代价:极小概率单 bit 翻转不被检测——不重要的推理可以接受,训练绝对不行。

1.8 研究前沿(2025-2026)

1.8.1 Blackwell 与 Blackwell Ultra:B200、GB200、B300/GB300

2025 年开始 NVIDIA Blackwell 大规模出货,把"训练 / 推理大模型"的硬件单位从单卡升到整柜

形态GPU 数HBM互联/系统官方来源
B2001180 GB HBM3eHGX/DGX B200Blackwell Tuning Guide
GB200 Grace Blackwell Superchip2 Blackwell GPU + 1 Grace CPU每 GPU 186 GB;共 372 GBNVLink-C2CGB200 NVL72
GB200 NVL727213.4 TB HBM3e72-GPU NVLink domain系统规格
B300 / GB300每 GPU / 72-GPU rack每 GPU 288 GB HBM3eBlackwell UltraHGX AI Factory components

关键概念变化:NVL72 把 72 张 GPU 通过 NVSwitch 全连成"一台逻辑 GPU",跨卡带宽达 1.8 TB/s(接近单卡 HBM 带宽)。这让 TP=72 成为可能,1.8T 参数模型可以塞进一柜推理。

1.8.2 FP4 / NVFP4 / MXFP4 — 推理新单位

Blackwell 推理硬件原生支持 4-bit 浮点。NVIDIA 力推三种格式:

低精度可减少数据字节并启用不同 Tensor Core 路径,但格式转换、scale、shape、质量约束和 memory/compute bound 会共同决定端到端收益;跨 GPU 的峰值不能直接预测 LLM 吞吐。

1.8.3 模型规模与新范式(2024-2026)

模型规模关键创新发布
Llama 3.1 405B405B dense开源 SOTA dense,长 context 128K2024.07
DeepSeek-V3671B MoE (37B 激活)原生 FP8 训练 + MLA + DeepSeekMoE + MTP2024.12
DeepSeek-R1671B (同 V3)纯 RL reasoning, o1-级性能开源2025.01
Llama 4 Behemoth / Maverick2T / 400B MoE多模态原生, 10M context2025
Qwen 3 系列~32B / 235B MoE中文 SOTA, 思考链开源2025
OpenAI o1 / o3 / GPT-5未知test-time compute, 大量 token 推理2024-2025
Anthropic Claude 3.5/4未知computer use, agentic2024-2025
Gemini 2.0/2.5 Pro未知原生 multimodal + 2M context2024-2025

三个大趋势对硬件需求的影响

  1. MoE 主导(DeepSeek-V3、Llama 4、Qwen 3):参数量 10×,激活 1/3-1/10 → 需要显存大 + Expert Parallelism。NVL72 完美匹配
  2. 长 context (1M-10M):KV cache 显存爆炸 → 必须 PagedAttention + KV 量化 + 长 context attention 优化 (Ring/Stripe)
  3. Reasoning models(o1 / R1 风格):单个回答可能生成 10-100K 个 thinking token → 推理服务的 decode 吞吐变得比 prefill 还关键

1.8.4 推理硬件选型方法(2026)

不要跨厂商比较没有统一 dtype、dense/sparse、功耗和 workload 的峰值。先用模型权重、KV cache、batch/context 算容量,再用 prefill/decode 的 roofline 判断计算或带宽瓶颈,最后在相同质量门槛与 SLO 下测 goodput。采购价格与云价必须绑定供应商、SKU、地区和日期。

1.8.5 学习建议(2026 视角)

常见坑

1.10 CUDA 官方手册精讲(CUDA Programming Guide 13.2(核验:2026-07-20))

从 deviceQuery 到 fatbin 的完整版图

本节定位:把 NVIDIA 官方 CUDA Programming Guide 13.2(核验:2026-07-20) 当前版中和本章直接相关的硬核细节抽出来——概念、API、踩坑点、版本兼容性—— 让你不必通读官方手册也能掌握本章主题的"标准答案"。引用按命名章节回查,避免把旧版编号当作稳定接口。

Compute Capability:每张卡的"身份证"

NVIDIA 给每一代 GPU 分配一个 Compute Capability (CC) 号码,形如 X.Y:X 是大版本(架构代),Y 是小版本(同代内的小改)。它直接对应 SM 版本号——CC 8.0 的 GPU 上 SM 就标记为 sm_80。这个数字决定了:

CC架构代号典型卡新增的关键特性
7.0 / 7.5Volta / TuringV100, T4Tensor Core (fp16), 独立线程调度
8.0 / 8.6 / 8.9Ampere / AdaA100, RTX 30/40, L4, L40Scp.async, BF16/TF32, fp8 (8.9)
9.0HopperH100, H200thread block cluster, TMA, wgmma, distributed shared mem
10.0 / 12.0BlackwellB200/GB200 与对应 consumer SKUfp4, TMEM, 2-CTA MMA;具体 CC 按 SKU 核验

关键规律——"小版本前向兼容、大版本不兼容"

实用速查:在第 1.3 节的 deviceQuery 输出里,Compute capability : 7.5 (sm_75, Turing) 表示这张 GPU 的 CC = 7.5 → 编译时写 -arch=sm_75。完整 CC 列表见 developer.nvidia.com/cuda-gpus

CUDA 平台栈:你装的到底是什么?

装完 CUDA 之后系统里多了一堆东西。把它们的角色厘清,调试"驱动太旧"或"sm 版本不对"才知道从哪改。

    graph TD
        App["你的 .cu 源码"]
        NVCC["nvcc 编译器"]
        PTX["PTX (虚拟 ISA, 文本)
compute_80"] Cubin["cubin (真二进制)
sm_80, sm_86, ..."] Fatbin["fatbin 容器
= 多个 PTX + 多个 cubin"] Exe["可执行文件 / .so"] Driver["NVIDIA Driver
(内核态 + 用户态)"] GPU["物理 GPU
(运行 cubin)"] App --> NVCC NVCC --> PTX NVCC --> Cubin PTX --> Fatbin Cubin --> Fatbin Fatbin --> Exe Exe -- "运行时加载" --> Driver Driver -- "选最匹配的 cubin
或 JIT 编译 PTX" --> GPU style App fill:#f3f1e8,stroke:#8b1538 style GPU fill:#f3f1e8,stroke:#2f5d3a

五件套各自的角色

组件是什么谁装怎么查版本
NVIDIA DriverGPU 的"操作系统",所有 GPU 用法(图形、CUDA、Vulkan)都走它。版本如 r580系统管理员nvidia-smi 顶行 "Driver Version: 580.xx"
CUDA Toolkit编译器(nvcc)+ 头文件 + 库(cuBLAS, cuDNN, cuFFT...)。和 Driver 是两个独立产品开发者nvcc --version
CUDA Runtime APIToolkit 提供的一个"上层 API 库"(libcudart)。你写的 cudaMalloc / cudaMemcpy 都属于它。建在 Driver API 之上。跟 Toolkit 一起看链接的 libcudart.so 版本
CUDA Driver API更底层的 API(libcuda),Driver 暴露的。能控制 context、module。本教程不直接用。跟 Driver 一起跟 Driver 版本一致
PTXNVIDIA GPU 的"虚拟汇编",文本格式。一份 PTX 可被 JIT 编译到任意更高 CC 的真二进制。nvcc 产出cuobjdump --dump-ptx
cubin真正的 SM 二进制,只对特定 sm_XY 有效(如 sm_80)。nvcc + ptxas 产出cuobjdump --dump-elf-symbols
fatbin容器格式,同时塞进多份 PTX + 多份 cubin,运行时 Driver 自动挑最匹配的。nvcc 默认产出cuobjdump app.out

nvidia-smi 显示的 "CUDA Version: 12.6" 到底是什么?

新手最大的混淆nvidia-smi 顶部那行 "Driver Version: 580.xx CUDA Version: 12.6" 的 "CUDA Version" 指的是这个驱动最高支持到 CUDA 12.6 Toolkit不是你实际装的 Toolkit 版本。

真正用 nvcc --version 看到的可能是 CUDA 12.4——这说明 nvcc 编出来的程序需要 12.4+ 兼容的 Driver(580.x 满足,没问题)。规则:Driver 版本必须 ≥ 编译时用的 Toolkit 要求的最小 Driver 版本。

fatbin 的好处:一份二进制走天下

编译时给 nvcc 列一串目标架构,它会把每个架构的 cubin + 一份"最高级 PTX"塞进 fatbin:

# 同时支持 T4 (sm_75) / A100 (sm_80) / H100 (sm_90)
    nvcc kernel.cu \
         -gencode arch=compute_75,code=sm_75 \
         -gencode arch=compute_80,code=sm_80 \
         -gencode arch=compute_90,code=sm_90 \
         -gencode arch=compute_90,code=compute_90 \   # 多塞一份 PTX, 给未来卡 JIT
         -o app

    # 偷懒写法 (CUDA 11.5+): 自动列出所有"主流" CC
    nvcc kernel.cu -arch=all-major -o app

    # 只编当前这张卡 (开发期最快, 但二进制不能移植)
    nvcc kernel.cu -arch=native -o app

运行时驱动按顺序找:① 当前 GPU 有完全匹配的 cubin?用它。② 没有但有同大版本更高小版本的 cubin?也行。③ 都没有但有 PTX?JIT 编译(第一次跑会慢一点,结果缓存在 ~/.nv/ComputeCache)。④ 都没有 → 报 no kernel image is available for execution on the device

本教程的选择code/chNN_xxx/Makefile 默认用 ARCH=sm_80 编单架构 cubin,让学员快速看到结果。生产部署的 LLM 推理框架(vLLM、SGLang)发布二进制时必走 fatbin,否则用户换张卡就跑不起来。

JIT 编译:fatbin 里那份 PTX 在干嘛

fatbin 里塞的 PTX 不是给你看的(虽然能 dump 出来),是为了让程序能跑在 nvcc 编译时还不存在的 GPU 上。流程:

  1. 程序启动,Driver 加载第一个 kernel
  2. Driver 检查 fatbin:当前 GPU 是 sm_120,fatbin 里没有 sm_120 cubin,但有一份 compute_90 PTX
  3. Driver 调用内置的 PTX → cubin 编译器,把 PTX 编成 sm_120 cubin(这就是 JIT compilation)
  4. JIT 编译完的 cubin 缓存到 ~/.nv/ComputeCache(Linux)或 %APPDATA%\NVIDIA\ComputeCache(Windows)
  5. 后续运行直接读 cache,不再 JIT
JIT 的代价:第一次运行可能多 1-5 秒(大 kernel 更慢)。生产推理服务启动延迟敏感,必须用 -arch=sm_XX 编当前 GPU 的 cubin,避免 JIT。或者预热——启动后立刻跑一遍所有 kernel,让 JIT 完成。

控制 JIT 行为的环境变量

环境变量作用
CUDA_CACHE_DISABLE=1关闭 JIT 缓存。每次启动都 JIT,调试用。
CUDA_CACHE_PATH=/path改 cache 目录(多用户共享机器有用)
CUDA_CACHE_MAXSIZE=4294967296cache 上限(字节,默认 1 GB)
CUDA_MODULE_LOADING=LAZY开启 lazy loading(CUDA 11.7+):kernel 第一次用才加载,启动更快
CUDA_FORCE_PTX_JIT=1强制走 PTX → JIT,忽略 cubin。测试 PTX 兼容性用。

NVRTC:运行时编译 CUDA C++ → PTX

PyTorch、Triton、TVM 等框架不会预编译每种张量 shape 的 kernel——它们在运行时用 NVRTC(NVIDIA Runtime Compilation Library)把生成的 CUDA C++ 字符串临时编译成 PTX,再交给 Driver JIT 成 cubin。

// NVRTC 用法骨架 (本教程不深入)
    #include <nvrtc.h>
    const char* src = "__global__ void k() { /* ... */ }";
    nvrtcProgram prog;
    nvrtcCreateProgram(&prog, src, "k.cu", 0, nullptr, nullptr);
    nvrtcCompileProgram(prog, 0, nullptr);          // 编出 PTX 字符串
    size_t ptx_size; nvrtcGetPTXSize(prog, &ptx_size);
    std::string ptx(ptx_size, '\0');
    nvrtcGetPTX(prog, ptx.data());                  // → 传给 cuModuleLoadData() 加载执行

第 14 章的 Mini-LLM 不用 NVRTC(全部预编译),但生产框架不可避免。知道它存在就行。

异构系统的两套内存:host vs device

CUDA 的世界里有两个"主语",全文都会用到,先固定下来:

= 什么有自己的内存吗
hostCPU + 它直连的 DRAM有,叫 host memory / system memory
deviceGPU + 它直连的 HBM/GDDR有,叫 device memory / global memory
kernel跑在 device 上的函数(要被千万个 thread 并行执行那种)
kernel launch从 host 启动一次 kernel 在 device 上跑
    graph LR
        subgraph host["host (CPU 侧)"]
          CPU["CPU cores"] --- HDRAM["host memory
DDR4/5, ~50 GB/s"] end subgraph device["device (GPU 侧)"] SM["SM × N"] --- GMEM["global memory
HBM, ~1-3 TB/s"] SM --- SMEM["shared memory
SRAM, on-chip"] end HDRAM -. "PCIe / NVLink
~16-128 GB/s" .- GMEM style host fill:#f3f1e8,stroke:#8b1538 style device fill:#f3f1e8,stroke:#2f5d3a

三种数据搬运

方向简称带宽用什么 API
host → deviceH2D受 PCIe 限:6-30 GB/scudaMemcpy(dst, src, n, cudaMemcpyHostToDevice)
device → hostD2H同上cudaMemcpy(..., cudaMemcpyDeviceToHost)
device → deviceD2DHBM 全速:250 GB/s - 3 TB/scudaMemcpy(..., cudaMemcpyDeviceToDevice)
记住数据移动边界:D2D 与 H2D/D2H 走不同链路,差距取决于 GPU 显存和主机互联。所以 LLM 推理通常让权重常驻 device,只把必要输入输出跨边界传输;1.4 节会在本机验证差距。

设备代码、主机代码的物理隔离

这是新手最难绕过的一关:

host 代码不能直接 deref device 指针float* d_x; cudaMalloc(&d_x, ...); d_x[0] = 1.0f; 在 host 上是段错误——d_x 指向的物理内存在 GPU 上,CPU 看不见。

反过来 device 代码也不能 deref host 指针(除非那块内存被 page-lock + 显式 map,第 8 章讲)。

唯一例外是 Unified MemorycudaMallocManaged),CUDA 替你在背后搬运。本教程不依赖它(让你看清数据流),但工业代码越来越爱用。

"统一虚拟地址空间" (UVA) ≠ 统一内存

容易混淆的两个名词:

Warp 与 SIMT:32 个 thread 是一辆"小巴"

"GPU 同时跑数千 thread" 这句话是真的,但 thread 不是一个个独立调度的——它们每 32 个一组打包成 warp,整组一起执行同一条指令。理解 warp 是后面所有性能讨论的前提。

SIMT = "Single Instruction, Multiple Threads"

同一个 warp 的 32 个 thread 共享一个 PC(指令指针)。每个 cycle 整个 warp 一起执行同一条指令,但每个 thread 有自己的寄存器、自己的数据:

cycle t:    所有 32 thread 都在执行 `LDG  r1, [rA + r0*4]`  ← 同一条指令
                但 thread 0 加载 a[0]
                    thread 1 加载 a[1]
                    ...
                    thread 31 加载 a[31]

    cycle t+1:  所有 32 thread 都在执行 `FADD r2, r1, r3`
                但每个 thread 操作自己的 r1, r3, r2

这就是 SIMT——instruction 是 single 的,threads 是 multiple 的,每个 thread 看到的数据不同。和 CPU 上的 SIMD(AVX、SVE)有共通也有区别:

对比SIMD (CPU)SIMT (GPU)
编程模型程序员写"一条指令处理 8 个 float"程序员写"一个 thread 处理 1 个 float",warp 帮你打包
分支整个 vector 共享 PC,分支困难每个 thread 概念上独立 PC,支持分支但有代价
数据宽度固定(AVX 256 / 512)固定 32 thread/warp,但每 thread 数据类型自由

Warp Divergence —— 分支的代价

warp 内 32 个 thread 必须执行同一条指令。如果代码里有 if (cond) { A; } else { B; },部分 thread 走 A、部分走 B 时怎么办?答案:

硬件用 mask 把走错路的 lane 关掉,所有 thread 先一起执行 A 分支(cond=false 的 thread 不写结果),再一起执行 B 分支(cond=true 的 thread 不写结果)。结果:warp 内分支 → 两段都跑,没省时间。这叫 warp 分歧

所以 GPU kernel 的第一性原理:

  1. 同一 warp 的 32 个 thread 尽量走同一条路(写代码时按 warp 边界对齐 if 条件)
  2. block 大小总是 32 的倍数(否则最后一个 warp 部分 lane 浪费)
  3. 访存按 warp 整体规划(这就是后面要讲的"coalesced memory access")

第 4 章会用动画演示 divergence,第 5 章会讲 coalescing。这里先记住"warp = 32 thread 同步执行"。

为什么是 32?历史选择——NVIDIA 从 G80 (2006) 就定下来了。AMD 的 wavefront 是 64(RDNA 后改 32)。Intel Xe 是 16 或 32。32 的好处:和很多 cache line / memory transaction 大小匹配(32 × 4B = 128B = 一次 L1 access)。

下一章导览

环境就绪后,第 2 章我们写真正的第一个 kernel,理解 <<<grid, block>>> 这个看起来很怪的语法到底在干什么。