惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
L
Lohrmann on Cybersecurity
aimingoo的专栏
aimingoo的专栏
V
V2EX
S
Security Affairs
T
Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
IT之家
IT之家
J
Java Code Geeks
The Register - Security
The Register - Security
U
Unit 42
C
CERT Recently Published Vulnerability Notes
月光博客
月光博客
A
About on SuperTechFans
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
The Blog of Author Tim Ferriss
Cisco Talos Blog
Cisco Talos Blog
Project Zero
Project Zero
S
Schneier on Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
D
DataBreaches.Net
博客园 - 司徒正美
V
Vulnerabilities – Threatpost
T
Tor Project blog
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
T
Threat Research - Cisco Blogs
Scott Helme
Scott Helme
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
M
MIT News - Artificial intelligence
云风的 BLOG
云风的 BLOG
小众软件
小众软件
L
LangChain Blog
Attack and Defense Labs
Attack and Defense Labs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
P
Palo Alto Networks Blog
A
Arctic Wolf
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 叶小钗
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 最新话题
MongoDB | Blog
MongoDB | Blog
Webroot Blog
Webroot Blog
H
Hacker News: Front Page
Know Your Adversary
Know Your Adversary
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
Engineering at Meta
Engineering at Meta

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
CUDA进阶学习与深入
梁文锋之深圳分锋 · 2026-05-07 · via 博客园_首页

CUDA 补充教程 - 进阶与深入

目录

  1. 第九课:CUDA 错误处理
  2. 第十课:原子操作
  3. 第十一课:CUDA 流与异步执行
  4. 第十二课:CUDA 事件与性能计时
  5. 第十三课:统一内存
  6. 第十四课:常量内存
  7. 第十五课:纹理内存
  8. 第十六课:并行归约算法
  9. 第十七课:前缀和(扫描)算法
  10. 第十八课:Warp 级编程
  11. 第十九课:动态并行
  12. 第二十课:多 GPU 编程
  13. 第二十一课:CUDA 调试技术
  14. 第二十二课:性能分析工具
  15. 第二十三课:内存访问模式深入
  16. 第二十四课:寄存器与缓存优化

第九课:CUDA 错误处理

知识点

为什么需要错误处理?

CUDA API 调用可能失败,常见原因:

  • 内存不足
  • 设备不存在
  • 内核启动失败
  • 驱动程序错误

不检查错误会导致:

  • 程序崩溃
  • 结果错误
  • 难以调试

CUDA 错误类型

typedef enum cudaError {
    cudaSuccess = 0,                    // 成功
    cudaErrorInvalidValue = 1,          // 无效参数
    cudaErrorMemoryAllocation = 2,      // 内存分配失败
    cudaErrorInvalidDevice = 10,        // 无效设备
    cudaErrorInvalidMemcpyDirection = 21, // 无效拷贝方向
    // ... 更多错误码
} cudaError;

错误检查函数

// 基本错误检查
cudaError_t err = cudaMalloc(&d_data, size);
if (err != cudaSuccess) {
    printf("CUDA 错误: %s\n", cudaGetErrorString(err));
    exit(1);
}

封装错误检查宏

// 定义错误检查宏
#define CUDA_CHECK(call) \
    do { \
        cudaError_t err = call; \
        if (err != cudaSuccess) { \
            fprintf(stderr, "CUDA 错误 at %s:%d: %s\n", \
                    __FILE__, __LINE__, cudaGetErrorString(err)); \
            exit(1); \
        } \
    } while(0)

// 使用宏
CUDA_CHECK(cudaMalloc(&d_data, size));
CUDA_CHECK(cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice));

内核启动错误检查

__global__ void myKernel(int *data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        data[idx] = idx * 2;
    }
}

int main() {
    // 启动内核
    myKernel<<<grid, block>>>(d_data, n);
    
    // 检查内核启动错误
    cudaError_t err = cudaGetLastError();
    if (err != cudaSuccess) {
        printf("内核启动失败: %s\n", cudaGetErrorString(err));
        return -1;
    }
    
    // 等待内核完成并检查执行错误
    err = cudaDeviceSynchronize();
    if (err != cudaSuccess) {
        printf("内核执行失败: %s\n", cudaGetErrorString(err));
        return -1;
    }
    
    return 0;
}

完整的错误处理模板

#include <stdio.h>
#include <stdlib.h>

#define CUDA_CHECK(call) \
    do { \
        cudaError_t err = call; \
        if (err != cudaSuccess) { \
            fprintf(stderr, "CUDA 错误 at %s:%d: %s\n", \
                    __FILE__, __LINE__, cudaGetErrorString(err)); \
            exit(1); \
        } \
    } while(0)

#define CUDA_KERNEL_CHECK() \
    do { \
        cudaError_t err = cudaGetLastError(); \
        if (err != cudaSuccess) { \
            fprintf(stderr, "内核启动错误 at %s:%d: %s\n", \
                    __FILE__, __LINE__, cudaGetErrorString(err)); \
            exit(1); \
        } \
        err = cudaDeviceSynchronize(); \
        if (err != cudaSuccess) { \
            fprintf(stderr, "内核执行错误 at %s:%d: %s\n", \
                    __FILE__, __LINE__, cudaGetErrorString(err)); \
            exit(1); \
        } \
    } while(0)

int main() {
    int n = 1000;
    size_t size = n * sizeof(float);
    
    float *d_data;
    CUDA_CHECK(cudaMalloc(&d_data, size));
    
    myKernel<<<grid, block>>>(d_data, n);
    CUDA_KERNEL_CHECK();
    
    CUDA_CHECK(cudaFree(d_data));
    
    return 0;
}

练习题 9

  1. CUDA 错误码 cudaSuccess 的值是什么?
  2. cudaGetLastError()cudaDeviceSynchronize() 分别检查什么错误?
  3. 为什么内核启动后需要调用 cudaDeviceSynchronize() 才能检测到执行错误?

第十课:原子操作

知识点

什么是原子操作?

原子操作是不可分割的操作,在多线程环境下保证数据一致性。

问题场景

// 非原子操作(危险!)
int count = 0;
__global__ void increment(int *count) {
    (*count)++;  // 多个线程同时执行,结果不确定
}

解决方案:使用原子操作

CUDA 原子函数

函数 操作 说明
atomicAdd() 加法 *addr += val
atomicSub() 减法 *addr -= val
atomicExch() 交换 *addr = val
atomicMin() 最小值 *addr = min(*addr, val)
atomicMax() 最大值 *addr = max(*addr, val)
atomicInc() 递增 *addr = (*addr >= val) ? 0 : *addr + 1
atomicDec() 递减 `addr = (addr == 0)
atomicCAS() 比较并交换 条件交换
atomicAnd() 与运算 *addr &= val
atomicOr() 或运算 *addr |= val
atomicXor() 异或运算 *addr ^= val

atomicAdd 示例

#include <stdio.h>

__global__ void atomicAddKernel(int *count, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        atomicAdd(count, 1);  // 原子递增
    }
}

int main() {
    int n = 10000;
    int h_count = 0;
    int *d_count;
    
    cudaMalloc(&d_count, sizeof(int));
    cudaMemcpy(d_count, &h_count, sizeof(int), cudaMemcpyHostToDevice);
    
    int blockSize = 256;
    int gridSize = (n + blockSize - 1) / blockSize;
    
    atomicAddKernel<<<gridSize, blockSize>>>(d_count, n);
    
    cudaMemcpy(&h_count, d_count, sizeof(int), cudaMemcpyDeviceToHost);
    
    printf("计数结果: %d (预期: %d)\n", h_count, n);
    
    cudaFree(d_count);
    return 0;
}

atomicCAS(比较并交换)

// atomicCAS(int *addr, int compare, int val)
// 如果 *addr == compare,则 *addr = val
// 返回 *addr 的旧值

__global__ void casExample(int *data, int old_val, int new_val) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx == 0) {
        int old = atomicCAS(data, old_val, new_val);
        printf("旧值: %d, 新值: %d\n", old, new_val);
    }
}

原子操作实现锁

struct Lock {
    int *mutex;
    
    Lock() {
        cudaMalloc(&mutex, sizeof(int));
        cudaMemset(mutex, 0, sizeof(int));
    }
    
    ~Lock() {
        cudaFree(mutex);
    }
    
    __device__ void lock() {
        while (atomicCAS(mutex, 0, 1) != 0) {
            // 等待锁释放
        }
    }
    
    __device__ void unlock() {
        atomicExch(mutex, 0);
    }
};

__global__ void kernelWithLock(int *data, Lock lock) {
    lock.lock();
    // 临界区代码
    (*data)++;
    lock.unlock();
}

原子操作性能考虑

  • 原子操作比普通操作慢
  • 多个线程对同一地址原子操作会串行化
  • 尽量减少原子操作的使用
  • 考虑使用共享内存减少全局内存原子操作

练习题 10

  1. 为什么多线程环境下普通递增操作 (*count)++ 会产生错误结果?
  2. atomicAdd(addr, val) 的作用是什么?返回值是什么?
  3. 如何使用原子操作实现一个简单的互斥锁?

第十一课:CUDA 流与异步执行

知识点

什么是 CUDA 流?

CUDA 流是一系列按顺序执行的命令队列。不同流中的命令可以并发执行。

默认流(Stream 0):
┌─────────────────────────────────────┐
│ 内核A → 拷贝1 → 内核B → 拷贝2       │  串行执行
└─────────────────────────────────────┘

多流并发:
Stream 1: ┌─────────────────────────────┐
          │ 内核A → 拷贝1               │
          └─────────────────────────────┘
Stream 2: ┌─────────────────────────────┐
          │ 内核B → 拷贝2               │  并发执行
          └─────────────────────────────┘

创建和使用流

cudaStream_t stream1, stream2;

// 创建流
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);

// 在指定流中执行操作
cudaMemcpyAsync(d_a1, h_a1, size, cudaMemcpyHostToDevice, stream1);
kernel1<<<grid, block, 0, stream1>>>(d_a1, d_c1);
cudaMemcpyAsync(h_c1, d_c1, size, cudaMemcpyDeviceToHost, stream1);

cudaMemcpyAsync(d_a2, h_a2, size, cudaMemcpyHostToDevice, stream2);
kernel2<<<grid, block, 0, stream2>>>(d_a2, d_c2);
cudaMemcpyAsync(h_c2, d_c2, size, cudaMemcpyDeviceToHost, stream2);

// 同步流
cudaStreamSynchronize(stream1);
cudaStreamSynchronize(stream2);

// 销毁流
cudaStreamDestroy(stream1);
cudaStreamDestroy(stream2);

异步内存拷贝

// 同步拷贝(阻塞)
cudaMemcpy(dst, src, size, cudaMemcpyHostToDevice);

// 异步拷贝(非阻塞)
cudaMemcpyAsync(dst, src, size, cudaMemcpyHostToDevice, stream);

流同步

// 同步单个流
cudaStreamSynchronize(stream);

// 同步所有流
cudaDeviceSynchronize();

// 等待多个流
cudaStreamWaitEvent(stream, event);

流优先级

// 创建高优先级流
int priority_high, priority_low;
cudaDeviceGetStreamPriorityRange(&priority_low, &priority_high);

cudaStream_t stream_high;
cudaStreamCreateWithPriority(&stream_high, cudaStreamNonBlocking, priority_high);

完整示例:多流并发

#include <stdio.h>

#define N_STREAMS 4
#define N 1000000

__global__ void vectorAdd(float *a, float *b, float *c, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

int main() {
    int n = N;
    size_t size = n * sizeof(float);
    
    // 分配主机内存(页锁定内存,用于异步传输)
    float *h_a[N_STREAMS], *h_b[N_STREAMS], *h_c[N_STREAMS];
    for (int i = 0; i < N_STREAMS; i++) {
        cudaMallocHost(&h_a[i], size);
        cudaMallocHost(&h_b[i], size);
        cudaMallocHost(&h_c[i], size);
    }
    
    // 分配设备内存
    float *d_a[N_STREAMS], *d_b[N_STREAMS], *d_c[N_STREAMS];
    for (int i = 0; i < N_STREAMS; i++) {
        cudaMalloc(&d_a[i], size);
        cudaMalloc(&d_b[i], size);
        cudaMalloc(&d_c[i], size);
    }
    
    // 创建流
    cudaStream_t streams[N_STREAMS];
    for (int i = 0; i < N_STREAMS; i++) {
        cudaStreamCreate(&streams[i]);
    }
    
    // 并发执行
    int blockSize = 256;
    int gridSize = (n + blockSize - 1) / blockSize;
    
    for (int i = 0; i < N_STREAMS; i++) {
        cudaMemcpyAsync(d_a[i], h_a[i], size, cudaMemcpyHostToDevice, streams[i]);
        cudaMemcpyAsync(d_b[i], h_b[i], size, cudaMemcpyHostToDevice, streams[i]);
        vectorAdd<<<gridSize, blockSize, 0, streams[i]>>>(d_a[i], d_b[i], d_c[i], n);
        cudaMemcpyAsync(h_c[i], d_c[i], size, cudaMemcpyDeviceToHost, streams[i]);
    }
    
    // 同步所有流
    cudaDeviceSynchronize();
    
    // 清理
    for (int i = 0; i < N_STREAMS; i++) {
        cudaStreamDestroy(streams[i]);
        cudaFree(d_a[i]);
        cudaFree(d_b[i]);
        cudaFree(d_c[i]);
        cudaFreeHost(h_a[i]);
        cudaFreeHost(h_b[i]);
        cudaFreeHost(h_c[i]);
    }
    
    return 0;
}

页锁定内存(Pinned Memory)

// 普通主机内存(可分页)
float *h_data = (float*)malloc(size);

// 页锁定主机内存(不可分页,用于异步传输)
float *h_data_pinned;
cudaMallocHost(&h_data_pinned, size);

// 释放
cudaFreeHost(h_data_pinned);

优点

  • 支持异步传输
  • 传输速度更快
  • DMA 直接访问

缺点

  • 占用物理内存
  • 分配速度较慢

练习题 11

  1. CUDA 流的作用是什么?
  2. cudaMemcpycudaMemcpyAsync 的区别是什么?
  3. 为什么异步传输需要使用页锁定内存?

第十二课:CUDA 事件与性能计时

知识点

什么是 CUDA 事件?

CUDA 事件是 GPU 上的时间标记,用于:

  • 测量内核执行时间
  • 流同步
  • 性能分析

创建和使用事件

cudaEvent_t start, stop;

// 创建事件
cudaEventCreate(&start);
cudaEventCreate(&stop);

// 记录事件
cudaEventRecord(start);
myKernel<<<grid, block>>>(...);
cudaEventRecord(stop);

// 等待事件完成
cudaEventSynchronize(stop);

// 计算时间
float milliseconds = 0;
cudaEventElapsedTime(&milliseconds, start, stop);
printf("执行时间: %.3f ms\n", milliseconds);

// 销毁事件
cudaEventDestroy(start);
cudaEventDestroy(stop);

完整的性能测试示例

#include <stdio.h>

__global__ void vectorAdd(float *a, float *b, float *c, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

int main() {
    int n = 10000000;
    size_t size = n * sizeof(float);
    
    // 分配内存
    float *h_a = (float*)malloc(size);
    float *h_b = (float*)malloc(size);
    float *h_c = (float*)malloc(size);
    
    float *d_a, *d_b, *d_c;
    cudaMalloc(&d_a, size);
    cudaMalloc(&d_b, size);
    cudaMalloc(&d_c, size);
    
    // 初始化数据
    for (int i = 0; i < n; i++) {
        h_a[i] = (float)i;
        h_b[i] = (float)(i * 2);
    }
    
    // 拷贝数据
    cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);
    
    // 创建事件
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    
    // 测试不同 Block 大小
    int blockSizes[] = {32, 64, 128, 256, 512, 1024};
    int numTests = sizeof(blockSizes) / sizeof(int);
    
    for (int i = 0; i < numTests; i++) {
        int blockSize = blockSizes[i];
        int gridSize = (n + blockSize - 1) / blockSize;
        
        // 预热
        vectorAdd<<<gridSize, blockSize>>>(d_a, d_b, d_c, n);
        cudaDeviceSynchronize();
        
        // 计时
        cudaEventRecord(start);
        vectorAdd<<<gridSize, blockSize>>>(d_a, d_b, d_c, n);
        cudaEventRecord(stop);
        cudaEventSynchronize(stop);
        
        float ms;
        cudaEventElapsedTime(&ms, start, stop);
        
        printf("Block=%4d, Grid=%6d, 时间=%.3f ms\n", 
               blockSize, gridSize, ms);
    }
    
    // 清理
    cudaEventDestroy(start);
    cudaEventDestroy(stop);
    cudaFree(d_a);
    cudaFree(d_b);
    cudaFree(d_c);
    free(h_a);
    free(h_b);
    free(h_c);
    
    return 0;
}

事件同步

// 等待单个事件
cudaEventSynchronize(event);

// 流等待事件
cudaStreamWaitEvent(stream, event, 0);

// 事件完成检查
cudaError_t err = cudaEventQuery(event);
if (err == cudaSuccess) {
    printf("事件已完成\n");
}

流间同步

cudaStream_t stream1, stream2;
cudaEvent_t event;

cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
cudaEventCreate(&event);

// Stream1 记录事件
kernel1<<<grid, block, 0, stream1>>>(...);
cudaEventRecord(event, stream1);

// Stream2 等待事件
cudaStreamWaitEvent(stream2, event, 0);
kernel2<<<grid, block, 0, stream2>>>(...);  // 等待 kernel1 完成

练习题 12

  1. CUDA 事件的主要用途是什么?
  2. cudaEventRecord()cudaEventSynchronize() 的区别?
  3. 如何使用事件实现两个流之间的同步?

第十三课:统一内存

知识点

什么是统一内存?

统一内存(Unified Memory)创建一个在 CPU 和 GPU 之间共享的内存池,自动管理数据传输。

传统内存模型:
┌─────────┐         ┌─────────┐
│ CPU 内存 │ ←─────→ │ GPU 内存 │
└─────────┘  手动传输  └─────────┘

统一内存模型:
┌─────────────────────────────┐
│       统一内存池             │
│   CPU 和 GPU 共享访问        │
│   自动管理数据迁移           │
└─────────────────────────────┘

创建统一内存

// 分配统一内存
float *data;
cudaMallocManaged(&data, size);

// CPU 和 GPU 都可以直接访问
data[0] = 1.0f;  // CPU 写入

myKernel<<<grid, block>>>(data);  // GPU 读取和修改

cudaDeviceSynchronize();

printf("%f\n", data[0]);  // CPU 读取 GPU 修改后的值

// 释放
cudaFree(data);

完整示例

#include <stdio.h>

__global__ void add(float *a, float *b, float *c, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

int main() {
    int n = 1000000;
    size_t size = n * sizeof(float);
    
    // 分配统一内存
    float *a, *b, *c;
    cudaMallocManaged(&a, size);
    cudaMallocManaged(&b, size);
    cudaMallocManaged(&c, size);
    
    // CPU 初始化
    for (int i = 0; i < n; i++) {
        a[i] = (float)i;
        b[i] = (float)(i * 2);
    }
    
    // GPU 计算
    int blockSize = 256;
    int gridSize = (n + blockSize - 1) / blockSize;
    add<<<gridSize, blockSize>>>(a, b, c, n);
    
    // 等待 GPU 完成
    cudaDeviceSynchronize();
    
    // CPU 验证结果
    bool success = true;
    for (int i = 0; i < n; i++) {
        if (c[i] != a[i] + b[i]) {
            success = false;
            break;
        }
    }
    printf("验证: %s\n", success ? "成功" : "失败");
    
    // 释放
    cudaFree(a);
    cudaFree(b);
    cudaFree(c);
    
    return 0;
}

内存迁移提示

// 提示内存将在设备上访问
cudaMemPrefetchAsync(data, size, deviceId, stream);

// 提示内存访问模式
cudaMemAdvise(data, size, cudaMemAdviseSetPreferredLocation, deviceId);
// 预取示例
int deviceId;
cudaGetDevice(&deviceId);

// 初始化数据(CPU)
for (int i = 0; i < n; i++) {
    data[i] = i;
}

// 预取到 GPU
cudaMemPrefetchAsync(data, size, deviceId);

// GPU 计算
kernel<<<grid, block>>>(data, n);

统一内存的优势

优势 说明
简化编程 无需手动管理内存拷贝
减少代码 不需要 cudaMalloc/cudaMemcpy
自动迁移 数据按需在 CPU/GPU 间迁移
超额订阅 可使用超过 GPU 内存的数据

统一内存的限制

  • 性能可能不如手动优化
  • 需要调用 cudaDeviceSynchronize() 同步
  • 频繁迁移会有开销
  • 旧 GPU 不支持某些特性

练习题 13

  1. 统一内存与传统内存模型的主要区别是什么?
  2. cudaMallocManaged() 分配的内存可以被谁访问?
  3. 为什么使用统一内存后还需要调用 cudaDeviceSynchronize()

第十四课:常量内存

知识点

什么是常量内存?

常量内存是只读内存,具有缓存优化,适合存储不会改变的数据。

常量内存特点:
- 大小限制:64KB
- 只读
- 有缓存
- 适合广播读取(所有线程读取相同地址)

声明和使用常量内存

// 声明常量内存(全局作用域)
__constant__ float constData[256];

// 从主机拷贝到常量内存
cudaMemcpyToSymbol(constData, h_data, size);

// 在内核中使用
__global__ void kernel(float *output) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    output[idx] = constData[idx % 256];  // 读取常量内存
}

完整示例:使用常量内存存储滤波器

#include <stdio.h>

#define FILTER_SIZE 5

// 声明常量内存
__constant__ float filter[FILTER_SIZE];

__global__ void convolution(float *input, float *output, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    
    if (idx >= FILTER_SIZE / 2 && idx < n - FILTER_SIZE / 2) {
        float sum = 0.0f;
        for (int i = 0; i < FILTER_SIZE; i++) {
            sum += input[idx - FILTER_SIZE / 2 + i] * filter[i];
        }
        output[idx] = sum;
    }
}

int main() {
    int n = 1000;
    size_t size = n * sizeof(float);
    
    // 准备滤波器数据
    float h_filter[FILTER_SIZE] = {0.1f, 0.2f, 0.4f, 0.2f, 0.1f};
    
    // 拷贝到常量内存
    cudaMemcpyToSymbol(filter, h_filter, FILTER_SIZE * sizeof(float));
    
    // 分配内存
    float *h_input = (float*)malloc(size);
    float *h_output = (float*)malloc(size);
    float *d_input, *d_output;
    cudaMalloc(&d_input, size);
    cudaMalloc(&d_output, size);
    
    // 初始化输入
    for (int i = 0; i < n; i++) {
        h_input[i] = (float)i;
    }
    
    // 拷贝数据
    cudaMemcpy(d_input, h_input, size, cudaMemcpyHostToDevice);
    
    // 执行卷积
    int blockSize = 256;
    int gridSize = (n + blockSize - 1) / blockSize;
    convolution<<<gridSize, blockSize>>>(d_input, d_output, n);
    
    // 拷贝结果
    cudaMemcpy(h_output, d_output, size, cudaMemcpyDeviceToHost);
    
    // 清理
    cudaFree(d_input);
    cudaFree(d_output);
    free(h_input);
    free(h_output);
    
    return 0;
}

常量内存 vs 全局内存

特性 常量内存 全局内存
访问权限 只读 读写
大小限制 64KB
缓存 有(常量缓存) 有(L1/L2)
广播优化
适用场景 常量数据、滤波器、查找表 通用数据

何时使用常量内存?

  • 数据不会改变
  • 所有线程读取相同数据(广播)
  • 数据量小于 64KB
  • 需要缓存优化

练习题 14

  1. 常量内存的大小限制是多少?
  2. 使用什么函数将数据拷贝到常量内存?
  3. 常量内存适合什么场景?

第十五课:纹理内存

知识点

什么是纹理内存?

纹理内存是专门为图像处理优化的只读内存,支持:

  • 硬件插值
  • 边界处理
  • 缓存优化

纹理内存特点

纹理内存特性:
- 只读
- 支持插值(线性、最近邻)
- 支持边界模式(截断、环绕、镜像)
- 2D 空间局部性缓存优化
- 适合图像处理

使用纹理内存

// 声明纹理引用(旧方式,CUDA 10 之前)
texture<float, 2, cudaReadModeElementType> texRef;

// 绑定纹理
cudaBindTextureToArray(texRef, texArray);

// 在内核中读取
__global__ void kernel(float *output, int width, int height) {
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    
    if (x < width && y < height) {
        // 使用纹理读取(支持插值)
        float val = tex2D(texRef, x, y);
        output[y * width + x] = val;
    }
}

// 解绑纹理
cudaUnbindTexture(texRef);

现代方式:纹理对象(CUDA 11+)

// 创建纹理对象
cudaResourceDesc resDesc;
memset(&resDesc, 0, sizeof(resDesc));
resDesc.resType = cudaResourceTypeLinear;
resDesc.res.linear.devPtr = devPtr;
resDesc.res.linear.desc = cudaCreateChannelDesc<float>();
resDesc.res.linear.sizeInBytes = size;

cudaTextureDesc texDesc;
memset(&texDesc, 0, sizeof(texDesc));
texDesc.addressMode[0] = cudaAddressModeClamp;
texDesc.addressMode[1] = cudaAddressModeClamp;
texDesc.filterMode = cudaFilterModeLinear;
texDesc.readMode = cudaReadModeElementType;

cudaTextureObject_t texObj;
cudaCreateTextureObject(&texObj, &resDesc, &texDesc, NULL);

// 在内核中使用
__global__ void kernel(cudaTextureObject_t texObj, float *output, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        output[idx] = tex1Dfetch<float>(texObj, idx);
    }
}

// 销毁纹理对象
cudaDestroyTextureObject(texObj);

纹理寻址模式

// 边界模式
texDesc.addressMode[0] = cudaAddressModeClamp;   // 截断(默认)
texDesc.addressMode[0] = cudaAddressModeWrap;    // 环绕
texDesc.addressMode[0] = cudaAddressModeMirror;  // 镜像
texDesc.addressMode[0] = cudaAddressModeBorder;  // 边界值

// 过滤模式
texDesc.filterMode = cudaFilterModePoint;   // 最近邻
texDesc.filterMode = cudaFilterModeLinear;  // 线性插值

纹理内存应用场景

  • 图像处理(缩放、旋转)
  • 纹理映射
  • 数据插值
  • 查找表

练习题 15

  1. 纹理内存的主要优势是什么?
  2. 纹理内存支持哪两种过滤模式?
  3. 纹理内存适合什么类型的应用?

第十六课:并行归约算法

知识点

什么是归约?

归约是将数组中所有元素通过某种操作合并为单个结果的过程。

求和归约:
[1, 2, 3, 4, 5, 6, 7, 8] → 36

求最大值归约:
[1, 2, 3, 4, 5, 6, 7, 8] → 8

串行归约

// CPU 串行归约
int sum = 0;
for (int i = 0; i < n; i++) {
    sum += data[i];
}

并行归约策略

步骤1: [1, 2, 3, 4, 5, 6, 7, 8]
       ↓
步骤2: [3,   7,   11,  15  ]  (相邻配对)
       ↓
步骤3: [10,       26        ]
       ↓
步骤4: [36                 ]

基本并行归约实现

__global__ void reduceSum(float *input, float *output, int n) {
    __shared__ float sdata[256];
    
    int tid = threadIdx.x;
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    
    // 加载数据到共享内存
    sdata[tid] = (idx < n) ? input[idx] : 0;
    __syncthreads();
    
    // 归约
    for (int s = blockDim.x / 2; s > 0; s >>= 1) {
        if (tid < s) {
            sdata[tid] += sdata[tid + s];
        }
        __syncthreads();
    }
    
    // 写回结果
    if (tid == 0) {
        output[blockIdx.x] = sdata[0];
    }
}

完整示例

#include <stdio.h>

#define BLOCK_SIZE 256

__global__ void reduceSum(float *input, float *output, int n) {
    __shared__ float sdata[BLOCK_SIZE];
    
    int tid = threadIdx.x;
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    
    sdata[tid] = (idx < n) ? input[idx] : 0;
    __syncthreads();
    
    for (int s = blockDim.x / 2; s > 0; s >>= 1) {
        if (tid < s) {
            sdata[tid] += sdata[tid + s];
        }
        __syncthreads();
    }
    
    if (tid == 0) {
        output[blockIdx.x] = sdata[0];
    }
}

int main() {
    int n = 1000000;
    size_t size = n * sizeof(float);
    
    // 分配内存
    float *h_input = (float*)malloc(size);
    float *d_input, *d_output;
    cudaMalloc(&d_input, size);
    
    // 初始化
    float h_sum = 0;
    for (int i = 0; i < n; i++) {
        h_input[i] = 1.0f;  // 每个元素为1,总和应为n
        h_sum += h_input[i];
    }
    
    cudaMemcpy(d_input, h_input, size, cudaMemcpyHostToDevice);
    
    // 计算需要的 Block 数量
    int blockSize = BLOCK_SIZE;
    int gridSize = (n + blockSize - 1) / blockSize;
    
    // 分配输出内存
    cudaMalloc(&d_output, gridSize * sizeof(float));
    
    // 执行归约
    reduceSum<<<gridSize, blockSize>>>(d_input, d_output, n);
    
    // 如果结果数量大于1,需要继续归约
    while (gridSize > 1) {
        int newGridSize = (gridSize + blockSize - 1) / blockSize;
        reduceSum<<<newGridSize, blockSize>>>(d_output, d_output, gridSize);
        gridSize = newGridSize;
    }
    
    // 获取结果
    float result;
    cudaMemcpy(&result, d_output, sizeof(float), cudaMemcpyDeviceToHost);
    
    printf("CPU 结果: %.0f\n", h_sum);
    printf("GPU 结果: %.0f\n", result);
    printf("误差: %.6f\n", fabs(h_sum - result));
    
    // 清理
    cudaFree(d_input);
    cudaFree(d_output);
    free(h_input);
    
    return 0;
}

优化技术

  1. 展开循环
  2. 避免 Bank 冲突
  3. 减少同步
// 优化版本:展开最后一个 warp
__global__ void reduceOptimized(float *input, float *output, int n) {
    __shared__ float sdata[BLOCK_SIZE];
    
    int tid = threadIdx.x;
    int idx = blockIdx.x * (BLOCK_SIZE * 2) + threadIdx.x;
    
    // 每个线程加载两个元素
    sdata[tid] = (idx < n) ? input[idx] : 0;
    sdata[tid] += (idx + BLOCK_SIZE < n) ? input[idx + BLOCK_SIZE] : 0;
    __syncthreads();
    
    // 归约
    for (int s = blockDim.x / 2; s > 32; s >>= 1) {
        if (tid < s) {
            sdata[tid] += sdata[tid + s];
        }
        __syncthreads();
    }
    
    // 展开最后一个 warp(不需要同步)
    if (tid < 32) {
        sdata[tid] += sdata[tid + 32];
        sdata[tid] += sdata[tid + 16];
        sdata[tid] += sdata[tid + 8];
        sdata[tid] += sdata[tid + 4];
        sdata[tid] += sdata[tid + 2];
        sdata[tid] += sdata[tid + 1];
    }
    
    if (tid == 0) {
        output[blockIdx.x] = sdata[0];
    }
}

练习题 16

  1. 什么是归约操作?举三个例子。
  2. 为什么并行归约需要使用共享内存?
  3. 为什么最后一个 warp 的归约不需要 __syncthreads()

第十七课:前缀和(扫描)算法

知识点

什么是前缀和?

前缀和是将数组转换为每个位置包含该位置之前所有元素和的数组。

输入: [1, 2, 3, 4, 5, 6, 7, 8]
前缀和: [1, 3, 6, 10, 15, 21, 28, 36]

解释:
位置0: 1
位置1: 1 + 2 = 3
位置2: 1 + 2 + 3 = 6
位置3: 1 + 2 + 3 + 4 = 10
...

串行前缀和

// CPU 串行前缀和
void prefixSum(float *input, float *output, int n) {
    output[0] = input[0];
    for (int i = 1; i < n; i++) {
        output[i] = output[i - 1] + input[i];
    }
}

并行前缀和:Blelloch 算法

分为两个阶段:

  1. 上扫描(Up-sweep):构建二叉树
  2. 下扫描(Down-sweep):计算前缀和
上扫描阶段:
[1, 2, 3, 4, 5, 6, 7, 8]
       ↓
[1, 3, 3, 7, 5, 11, 7, 15]
       ↓
[1, 3, 3, 10, 5, 11, 7, 26]
       ↓
[1, 3, 3, 10, 5, 11, 7, 36]

下扫描阶段:
[1, 3, 3, 10, 5, 11, 7, 0]
       ↓
[1, 3, 3, 0, 5, 11, 7, 10]
       ↓
[1, 3, 0, 3, 5, 6, 7, 10]
       ↓
[0, 1, 3, 6, 10, 15, 21, 28]

CUDA 实现

__global__ void prefixSum(float *input, float *output, int n) {
    __shared__ float temp[256];
    
    int tid = threadIdx.x;
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    
    // 加载数据
    temp[tid] = (idx < n) ? input[idx] : 0;
    __syncthreads();
    
    // 上扫描
    int offset = 1;
    for (int d = blockDim.x >> 1; d > 0; d >>= 1) {
        __syncthreads();
        if (tid < d) {
            int ai = offset * (2 * tid + 1) - 1;
            int bi = offset * (2 * tid + 2) - 1;
            temp[bi] += temp[ai];
        }
        offset *= 2;
    }
    
    // 清除最后一个元素
    if (tid == 0) {
        temp[blockDim.x - 1] = 0;
    }
    
    // 下扫描
    for (int d = 1; d < blockDim.x; d *= 2) {
        offset >>= 1;
        __syncthreads();
        if (tid < d) {
            int ai = offset * (2 * tid + 1) - 1;
            int bi = offset * (2 * tid + 2) - 1;
            float t = temp[ai];
            temp[ai] = temp[bi];
            temp[bi] += t;
        }
    }
    
    __syncthreads();
    
    // 写回结果
    if (idx < n) {
        output[idx] = temp[tid];
    }
}

使用 Thrust 库

#include <thrust/scan.h>
#include <thrust/device_vector.h>

int main() {
    int n = 8;
    thrust::device_vector<int> d_input(8);
    thrust::device_vector<int> d_output(8);
    
    d_input[0] = 1; d_input[1] = 2; d_input[2] = 3; d_input[3] = 4;
    d_input[4] = 5; d_input[5] = 6; d_input[6] = 7; d_input[7] = 8;
    
    // 计算前缀和
    thrust::exclusive_scan(d_input.begin(), d_input.end(), d_output.begin());
    
    // 输出: [0, 1, 3, 6, 10, 15, 21, 28]
    
    return 0;
}

练习题 17

  1. 什么是前缀和?与归约的区别是什么?
  2. 前缀和算法的两个阶段分别是什么?
  3. 前缀和有什么应用场景?

第十八课:Warp 级编程

知识点

什么是 Warp?

Warp 是 GPU 执行的基本单位,包含 32 个线程。同一 Warp 内的线程:

  • 同时执行相同指令(SIMT)
  • 可以高效交换数据
  • 可以使用 Warp 级原语

Warp 级原语

// Warp 归约
unsigned __ballot_sync(unsigned mask, int predicate);  // 统计满足条件的线程
int __all_sync(unsigned mask, int predicate);          // 所有线程都满足条件
int __any_sync(unsigned mask, int predicate);          // 任一线程满足条件
unsigned __activemask();                                // 获取活跃线程掩码

// Warp shuffle(线程间交换数据)
int __shfl_sync(unsigned mask, int var, int srcLane);           // 从指定 lane 获取值
int __shfl_up_sync(unsigned mask, int var, unsigned delta);     // 向上获取值
int __shfl_down_sync(unsigned mask, int var, unsigned delta);   // 向下获取值
int __shfl_xor_sync(unsigned mask, int var, int laneMask);      // XOR 获取值

Warp 归约示例

__device__ float warpReduceSum(float val) {
    for (int offset = 16; offset > 0; offset >>= 1) {
        val += __shfl_down_sync(0xffffffff, val, offset);
    }
    return val;
}

__global__ void reduce(float *input, float *output, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    
    float val = (idx < n) ? input[idx] : 0;
    
    // Warp 归约
    val = warpReduceSum(val);
    
    // 只有每个 Warp 的第一个线程写入结果
    if (threadIdx.x % 32 == 0) {
        atomicAdd(output, val);
    }
}

Warp Shuffle 示例

__global__ void shuffleExample() {
    int laneId = threadIdx.x & 31;
    int value = laneId;
    
    // 从 lane 0 获取值
    int value_from_0 = __shfl_sync(0xffffffff, value, 0);
    
    // 向上获取值(delta=1)
    int value_from_up = __shfl_up_sync(0xffffffff, value, 1);
    
    // 向下获取值(delta=1)
    int value_from_down = __shfl_down_sync(0xffffffff, value, 1);
    
    // XOR 获取值
    int value_from_xor = __shfl_xor_sync(0xffffffff, value, 1);
    
    if (laneId < 4) {
        printf("Lane %d: original=%d, from_0=%d, up=%d, down=%d, xor=%d\n",
               laneId, value, value_from_0, value_from_up, value_from_down, value_from_xor);
    }
}

mask 参数说明

// mask 指定参与操作的线程
0xffffffff  // 所有 32 个线程参与
0x0000000f  // 只有前 4 个线程参与
__activemask()  // 当前活跃的线程

练习题 18

  1. Warp 的大小是多少个线程?
  2. __shfl_sync() 的作用是什么?
  3. 为什么 Warp 级操作比共享内存更快?

第十九课:动态并行

知识点

什么是动态并行?

动态并行允许 GPU 内核在运行时启动新的内核,无需 CPU 介入。

传统模式:
CPU 启动内核 → GPU 执行 → CPU 启动新内核 → GPU 执行

动态并行:
CPU 启动内核 → GPU 执行 → GPU 启动新内核 → GPU 执行

启用动态并行

编译时需要链接 CUDA device runtime:

nvcc -rdc=true myprogram.cu

基本示例

__global__ void childKernel(int *data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        data[idx] *= 2;
    }
}

__global__ void parentKernel(int *data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    
    if (idx == 0) {
        // 在 GPU 中启动子内核
        childKernel<<<1, n>>>(data, n);
        
        // 等待子内核完成
        cudaDeviceSynchronize();
    }
}

int main() {
    int n = 10;
    int *d_data;
    cudaMalloc(&d_data, n * sizeof(int));
    
    // 启动父内核
    parentKernel<<<1, 1>>>(d_data, n);
    cudaDeviceSynchronize();
    
    cudaFree(d_data);
    return 0;
}

动态并行的应用场景

  • 递归算法
  • 不规则并行
  • 自适应细分
  • 图遍历

注意事项

  • 需要额外的内存资源
  • 启动开销
  • 递归深度限制
  • 需要同步

练习题 19

  1. 什么是动态并行?
  2. 动态并行与传统内核启动的区别是什么?
  3. 编译时需要什么选项启用动态并行?

第二十课:多 GPU 编程

知识点

多 GPU 架构

┌─────────┐     ┌─────────┐     ┌─────────┐
│  GPU 0  │     │  GPU 1  │     │  GPU 2  │
│ 内存 0  │     │ 内存 1  │     │ 内存 2  │
└────┬────┘     └────┬────┘     └────┬────┘
     │               │               │
     └───────────────┴───────────────┘
                     │
              ┌──────┴──────┐
              │     CPU     │
              └─────────────┘

选择 GPU

int deviceCount;
cudaGetDeviceCount(&deviceCount);

for (int i = 0; i < deviceCount; i++) {
    cudaDeviceProp prop;
    cudaGetDeviceProperties(&prop, i);
    printf("GPU %d: %s\n", i, prop.name);
}

// 设置当前 GPU
cudaSetDevice(deviceId);

多 GPU 执行

int main() {
    int n = 1000000;
    int numGPUs = 2;
    int chunkSize = n / numGPUs;
    
    // 在每个 GPU 上分配内存
    float *d_a[2], *d_b[2], *d_c[2];
    for (int i = 0; i < numGPUs; i++) {
        cudaSetDevice(i);
        cudaMalloc(&d_a[i], chunkSize * sizeof(float));
        cudaMalloc(&d_b[i], chunkSize * sizeof(float));
        cudaMalloc(&d_c[i], chunkSize * sizeof(float));
    }
    
    // 在每个 GPU 上执行计算
    for (int i = 0; i < numGPUs; i++) {
        cudaSetDevice(i);
        cudaMemcpy(d_a[i], h_a + i * chunkSize, chunkSize * sizeof(float), cudaMemcpyHostToDevice);
        cudaMemcpy(d_b[i], h_b + i * chunkSize, chunkSize * sizeof(float), cudaMemcpyHostToDevice);
        
        int blockSize = 256;
        int gridSize = (chunkSize + blockSize - 1) / blockSize;
        vectorAdd<<<gridSize, blockSize>>>(d_a[i], d_b[i], d_c[i], chunkSize);
        
        cudaMemcpy(h_c + i * chunkSize, d_c[i], chunkSize * sizeof(float), cudaMemcpyDeviceToHost);
    }
    
    // 清理
    for (int i = 0; i < numGPUs; i++) {
        cudaSetDevice(i);
        cudaFree(d_a[i]);
        cudaFree(d_b[i]);
        cudaFree(d_c[i]);
    }
    
    return 0;
}

GPU 间通信

// 点对点访问
int canAccess;
cudaDeviceCanAccessPeer(&canAccess, gpu0, gpu1);

if (canAccess) {
    cudaSetDevice(gpu0);
    cudaDeviceEnablePeerAccess(gpu1, 0);
    
    // GPU 0 可以直接访问 GPU 1 的内存
    kernel<<<grid, block>>>(d_data_on_gpu1);
}

练习题 20

  1. 如何获取系统中 GPU 的数量?
  2. 如何切换当前活动的 GPU?
  3. 什么是点对点访问?

第二十一课:CUDA 调试技术

知识点

调试方法

  1. printf 调试
  2. cuda-gdb
  3. Nsight Compute
  4. cuda-memcheck

printf 在内核中使用

__global__ void debugKernel(int *data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    
    if (idx < n) {
        if (idx < 5) {
            printf("Thread %d: data[%d] = %d\n", idx, idx, data[idx]);
        }
        data[idx] *= 2;
    }
}

int main() {
    // ...
    debugKernel<<<grid, block>>>(d_data, n);
    cudaDeviceSynchronize();  // 必须同步才能看到 printf 输出
    // ...
}

cuda-memcheck

检查内存错误:

cuda-memcheck ./my_program

检测:

  • 越界访问
  • 未初始化内存
  • 内存泄漏
  • 竞争条件

cuda-gdb

cuda-gdb ./my_program

# 命令
(gdb) break myKernel    # 在内核设置断点
(gdb) run               # 运行
(gdb) cuda thread       # 查看当前线程
(gdb) cuda block        # 查看当前 block
(gdb) print data[0]     # 打印变量

assert 在内核中使用

__global__ void kernel(int *data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    
    if (idx < n) {
        assert(data[idx] >= 0);  // 断言检查
        data[idx] = sqrt(data[idx]);
    }
}

练习题 21

  1. 在内核中使用 printf 后需要调用什么函数?
  2. cuda-memcheck 可以检测哪些错误?
  3. 如何在 cuda-gdb 中设置断点?

第二十二课:性能分析工具

知识点

Nsight Systems

系统级性能分析:

nsys profile ./my_program

分析内容:

  • CPU/GPU 时间线
  • 内核执行时间
  • 内存传输时间
  • API 调用开销

Nsight Compute

内核级性能分析:

ncu ./my_program

分析内容:

  • 内存吞吐量
  • 计算吞吐量
  • Warp 执行效率
  • 内存访问模式

nvprof(旧工具)

nvprof ./my_program

# 输出示例
==12345== Profiling result:
            Type  Time(%)      Time     Calls       Avg       Min       Max  Name
 GPU activities:   65.43%  123.45ms         1  123.45ms  123.45ms  123.45ms  myKernel(int*, int)
                   34.57%   65.43ms         2   32.72ms   32.71ms   32.72ms  [CUDA memcpy HtoD]

性能指标

指标 说明
GPU Time 内核执行时间
Grid Size Block 数量
Block Size 每个 Block 的线程数
Registers Per Thread 每个线程使用的寄存器数
Shared Memory Per Block 每个 Block 使用的共享内存
Theoretical Occupancy 理论占用率
Achieved Occupancy 实际占用率
Memory Throughput 内存吞吐量

练习题 22

  1. Nsight Systems 和 Nsight Compute 的区别是什么?
  2. 如何使用 nvprof 分析程序?
  3. 什么是 Occupancy?

第二十三课:内存访问模式深入

知识点

全局内存访问

GPU 内存层次:
┌─────────────────────────────────┐
│         全局内存 (DRAM)          │  大,慢
├─────────────────────────────────┤
│            L2 缓存               │
├─────────────────────────────────┤
│            L1 缓存               │
├─────────────────────────────────┤
│          共享内存                │  小,快
├─────────────────────────────────┤
│           寄存器                 │  最快
└─────────────────────────────────┘

内存合并

理想情况:相邻线程访问相邻地址

线程:  0   1   2   3   4   5   6   7
地址: [0] [4] [8] [12][16][20][24][28]
       └─────────────────────────────┘
              一次内存事务

不理想情况:跳跃访问

线程:  0   1   2   3   4   5   6   7
地址: [0] [64][128][192][256][320][384][448]
       └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘
       8 次内存事务

Bank 冲突

共享内存分为 32 个 Bank:

Bank:  0   1   2   3  ...  31
      [0] [1] [2] [3] ... [31]
      [32][33][34][35]... [63]
      ...

Bank 冲突:多个线程访问同一 Bank 的不同地址

// 无冲突
sdata[threadIdx.x] = value;  // 每个线程访问不同 Bank

// 冲突
sdata[threadIdx.x * 2] = value;  // 线程 0 和 16 访问同一 Bank

解决 Bank 冲突

// 方法1:填充
__shared__ float sdata[256 + 16];  // 添加填充避免冲突

// 方法2:改变访问模式
__shared__ float sdata[32][33];  // 33 列避免冲突
float val = sdata[threadIdx.x][threadIdx.y];

练习题 23

  1. 什么是内存合并?
  2. 什么是 Bank 冲突?
  3. 如何避免 Bank 冲突?

第二十四课:寄存器与缓存优化

知识点

寄存器使用

每个线程有私有寄存器:

  • 最快的存储
  • 数量有限(通常 255 个)
  • 影响占用率
// 查看寄存器使用
// 编译时添加 -Xptxas=-v 选项
nvcc -Xptxas=-v myprogram.cu

// 输出示例
ptxas info    : Used 32 registers, 256 bytes smem, ...

占用率计算

占用率 = 活跃 Warp 数 / 最大 Warp 数

影响因素:
- 每个 Block 的线程数
- 每个线程使用的寄存器数
- 每个 Block 使用的共享内存

优化寄存器使用

// 减少寄存器使用
// 1. 重用变量
float temp = a[i];
temp = temp * 2;  // 重用 temp

// 2. 避免过多局部变量

// 3. 使用编译提示
__launch_bounds__(256, 2)  // 每个 Block 256 线程,最少 2 个 Block
__global__ void myKernel(...) {
    // ...
}

缓存配置

// 配置 L1 缓存和共享内存比例
cudaFuncSetCacheConfig(myKernel, cudaFuncCachePreferShared);
cudaFuncSetCacheConfig(myKernel, cudaFuncCachePreferL1);
cudaFuncSetCacheConfig(myKernel, cudaFuncCachePreferEqual);

练习题 24

  1. 寄存器的特点是什么?
  2. 什么是占用率?
  3. 如何查看内核使用的寄存器数量?

附录:CUDA 最佳实践总结

内存优化

  1. 使用共享内存减少全局内存访问
  2. 确保内存合并访问
  3. 避免 Bank 冲突
  4. 使用常量内存存储只读数据
  5. 使用统一内存简化编程

执行优化

  1. 选择合适的 Block 大小(128、256、512)
  2. 保持高占用率
  3. 避免分支分歧
  4. 使用 Warp 级原语
  5. 使用流实现并发

算法优化

  1. 使用并行归约
  2. 使用前缀和算法
  3. 使用原子操作保证正确性
  4. 减少线程间同步

开发建议

  1. 始终检查 CUDA 错误
  2. 使用性能分析工具
  3. 先保证正确性,再优化性能
  4. 参考 NVIDIA 最佳实践指南

参考资料

  1. CUDA C Programming Guide: https://docs.nvidia.com/cuda/cuda-c-programming-guide/
  2. CUDA Best Practices Guide: https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/
  3. CUDA Toolkit Documentation: https://docs.nvidia.com/cuda/
  4. NVIDIA Developer Blog: https://developer.nvidia.com/blog/

学习日期:2026-05-07