## Q1 基础环境搭建与hello world kernel
### 项目文件架构
```
project/
├── csrc/
│ ├── kernels.cpp # C++ 声明 + pybind11 绑定
│ └── kernels.cu # CUDA 内核实现
├── setup.py # 使用 setuptools 编译扩展
└── main.py # import 编译好的模块
```
> [!tip]
> - 快速实验用 `load_inline()`,代码嵌入 Python 字符串中
> - 正式项目用 `setup.py` + 分离文件,便于版本控制和调试
> - `.cu` 文件由 nvcc 编译,`.cpp` 文件由系统 C++ 编译器编译
### 内联编译方式
使用 PyTorch JIT 编译 CUDA 内核时,需要将代码分为两部分:
**C++ 声明(cpp_sources)** - 只包含函数声明,由普通 C++ 编译器编译:
```cpp
#include <torch/extension.h>
// 函数声明
torch::Tensor vector_add(torch::Tensor a, torch::Tensor b);
```
**CUDA 源码(cuda_sources)** - 包含内核定义和调用内核的包装函数:
```cuda
// 内核定义
__global__ void vector_add_kernel(
const float* a, const float* b, float* c, int n
) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}
// 包装函数(必须在 .cu 文件中,因为使用了 <<<>>> 语法)
torch::Tensor vector_add(torch::Tensor a, torch::Tensor b) {
auto c = torch::empty_like(a);
int n = a.numel();
int threads = 256;
int blocks = (n + threads - 1) / threads;
vector_add_kernel<<<blocks, threads>>>(
a.data_ptr<float>(), b.data_ptr<float>(),
c.data_ptr<float>(), n
);
return c;
}
```
> [!important]
> `<<<>>>` 内核启动语法只能被 nvcc 编译,必须放在 `cuda_sources` 中,不能放在 `cpp_sources` 中。
### 编译与加载
```python
from torch.utils.cpp_extension import load_inline
module = load_inline(
name='cuda_kernels',
cpp_sources=cpp_source, # 只有声明
cuda_sources=full_cuda_source, # 内核 + 包装函数
functions=['vector_add'],
verbose=False,
extra_cuda_cflags=['-O3', '--use_fast_math'],
)
# 使用编译好的内核
a = torch.randn(1024, device='cuda')
b = torch.randn(1024, device='cuda')
c = module.vector_add(a, b)
```
## Q2 理解hello world kernel
### GPU 硬件架构 Intro
NVIDIA GPU 采用层次化的并行架构:
```
GPU
├── SM (Streaming Multiprocessor) × N # 多个流式多处理器
│ ├── CUDA Cores × M # 每个 SM 有多个 CUDA 核心
│ ├── Shared Memory # 片上共享内存(快)
│ ├── L1 Cache # 一级缓存
│ └── Warp Scheduler # 线程束调度器
└── Global Memory (HBM/GDDR) # 全局显存(慢)
```
**关键概念**:
- **SM**:独立的计算单元,可以同时运行多个线程块
- **Warp**:32 个线程组成一个 warp,是实际执行的最小单位,warp 内的线程同步执行相同指令
- **Shared Memory**:同一 block 内的线程共享,速度接近寄存器
- **Global Memory**:所有线程可访问,但延迟高(~400 cycles)
### CUDA 编程模型
CUDA 将线程组织为三层结构,与硬件对应:
```
Grid (网格)
├── Block 0 # 线程块,映射到 SM
│ ├── Thread 0..31 (Warp 0) # 线程,映射到 CUDA Core
│ ├── Thread 32..63 (Warp 1)
│ └── ...
├── Block 1
└── ...
```
### 解析
```cuda
__global__ void vector_add_kernel(
const float* a, const float* b, float* c, int n
) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}
```
**`__global__`**:声明这是一个 kernel 函数,从 CPU 调用,在 GPU 上执行
**内置变量**:
| 变量 | 含义 | 示例值 |
|------|------|--------|
| `blockIdx.x` | 当前 block 在 grid 中的索引 | 0, 1, 2, ... |
| `blockDim.x` | 每个 block 中的线程数 | 256 |
| `threadIdx.x` | 当前线程在 block 中的索引 | 0, 1, ..., 255 |
**全局索引计算**:
```
idx = blockIdx.x * blockDim.x + threadIdx.x
例如:blocks=4, threads=256, 总共 1024 个线程
Block 0: idx = 0*256 + 0..255 = 0..255
Block 1: idx = 1*256 + 0..255 = 256..511
Block 2: idx = 2*256 + 0..255 = 512..767
Block 3: idx = 3*256 + 0..255 = 768..1023
```
**边界检查** `if (idx < n)`:因为线程总数可能大于数据量,需要防止越界访问
### kernel 启动
```cuda
int threads = 256;
int blocks = (n + threads - 1) / threads; // 向上取整
vector_add_kernel<<<blocks, threads>>>(a, b, c, n);
```
**`<<<blocks, threads>>>`**:CUDA 特有的 kernel 启动语法
- 第一个参数:grid 中的 block 数量
- 第二个参数:每个 block 中的线程数
**向上取整公式**:`(n + threads - 1) / threads` 确保有足够的线程覆盖所有数据
```
n=1000, threads=256
blocks = (1000 + 255) / 256 = 4
总线程数 = 4 * 256 = 1024 >= 1000 ✓
```
### 执行流程
```
CPU GPU
│ │
├─ 分配 GPU 内存 ────────────►│
├─ 拷贝数据到 GPU ───────────►│
├─ 启动 kernel ──────────────►├─ 调度 blocks 到 SMs
│ ├─ 每个 SM 执行 warps
│ ├─ 线程并行计算
├─ 等待完成 ◄────────────────┤
├─ 拷贝结果回 CPU ◄──────────┤
│ │
```
> [!tip]
> 选择 `threads=256` 是因为:
> 1. 是 32(warp size)的倍数,避免资源浪费
> 2. 足够大以隐藏内存延迟
> 3. 不超过硬件限制(通常 1024)
Ref:
Austin et al., "How to Scale Your Model", Google DeepMind, online, 2025.