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

推荐订阅源

GbyAI
GbyAI
博客园 - 三生石上(FineUI控件)
S
Securelist
U
Unit 42
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Simon Willison's Weblog
Simon Willison's Weblog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
B
Blog
T
Tenable Blog
The Hacker News
The Hacker News
The Register - Security
The Register - Security
IT之家
IT之家
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
P
Privacy & Cybersecurity Law Blog
博客园_首页
T
Tailwind CSS Blog
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tor Project blog
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
A
Arctic Wolf
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
V2EX
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
V
Visual Studio Blog
月光博客
月光博客
爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .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 PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
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