Intro-ops/course/scripts/01_copy_script.md

4.0 KiB
Raw Permalink Blame History

Copy 算子 — 教学视频脚本

时长15-20 分钟 | 难度:入门


[5min] 概念讲解

开场30s

"今天我们从 intro-ops 训练营最简单的算子开始——copy。GPU 上做 copy 和 CPU 上不同——你写不出 memcpy 那种一行代码,因为 GPU 有成百上千个线程同时在工作。怎么让每个线程知道它该搬哪些数据?这就是 grid-stride loop 要解决的问题。"

算子在深度学习中的用途1min

"Copy 看起来 trivial但在深度学习框架中无处不在。Tensor 的 clone()contiguous()、数据加载中的 host-to-device 拷贝——底层都是类似的 copy kernel。理解 copy 就是理解 GPU 内存模型的第一步。"

算法推导3.5min

关键画面: 展示 grid-stride-loop Mermaid 图(docs/diagrams/grid-stride-loop.md

讲解要点:

  1. GPU 的线程组织grid → block → thread
  2. 每个线程如何计算自己的全局索引:idx = blockIdx.x * blockDim.x + threadIdx.x
  3. stride = grid 总线程数 = gridDim.x * blockDim.x
  4. 为什么需要循环?因为线程数可能少于元素数
  5. 循环条件 i < N 保证了越界安全

过渡语: "好,理论就这么多。接下来我们在 IDE 里把它写出来。"


[10min] 代码实操

CUDA Kernel5min

打开文件: ops/copy/nvidia/kernel.cuh

边写边讲:

模板参数 T支持 float、half 等多种数据类型
__global__告诉 nvcc 这是 GPU 函数
命名空间 oprt::copy::nvidia遵循项目约定

关键决策点:

  • "为什么用 int64_t 而不是 int——tensor 可能有超过 2^31 个元素"
  • "为什么 idxstride 的计算不放在循环条件里?——编译器优化和可读性"
  • "为什么循环变量叫 i 不是 idx?——idx 是起始位置,i 是当前处理位置"

写完后展示完整代码。

TileLang Kernel5min

打开文件: ops/copy/tilelang/kernel.py

边写边讲:

@tilelang.jitJIT 编译装饰器——和 nvcc 不同,这里编译发生在运行时
T.Parallel(N // BLOCK_N):告诉编译器这 N/BLOCK_N 个 tile 可以并行
T.copy把全局内存的一整块搬进 fragment

对比时刻: "看TileLang 不需要你写 grid、block、thread——编译器帮你做。这 5 行 Python 生成的 CUDA code 比你手写的可能还高效。"


[3min] 测试验证

跑测试

# NVIDIA
PYTHONPATH=python:. CAMP_BUILD_DIR=build-nvidia \
  pytest tests/op_tests/test_copy.py -v --backend nvidia

展示: 终端中测试全部绿色的画面。

跑 benchmark

PYTHONPATH=python:. CAMP_BUILD_DIR=build-nvidia \
  python tests/run_ops.py --op copy --backend nvidia --mode all

解读 benchmark 输出:

  • "看这个 Bandwidth——如果你跑不到峰值的 80%+,说明合并访问有问题"
  • "copy 是纯内存搬运,它的性能天花板就是 GPU 内存带宽"

[2min] 常见错误演示

错误 1忘记 grid-stride loop30s

// 错误——只处理了前 blockDim.x 个元素
int idx = threadIdx.x;
dst[idx] = src[idx];  // 超过 blockDim.x 的部分没被处理!

"这就是为什么需要 grid-stride loop——如果不写循环每个线程只处理一个元素你搬不完整个 tensor。"

错误 2循环写反条件30s

// 错误——死循环
for (int i = idx; i < n; i++) {  // 忘记 += stride

"每次循环 i 都只加 1——第一个线程就把所有活干完了其他线程白等而且还死循环。"

错误 3TileLang 里用 T.Serial1min

# 错误——用 T.Serial 跑 copy
for i in T.Serial(N // BLOCK_N):
    ...

"T.Serial 告诉编译器'这些 tile 必须顺序执行'——你的并行性全丢了。copy 的每个 tile 完全独立,应该用 T.Parallel。"


课后挑战

"把 copy kernel 的带宽从 60% 跑到 90% 以上。提示:检查你的 grid/block 尺寸是否合理。下周 vector_add 见!"