CUDA C++ 高效入门第二章 -- CUDA 线程组织之两个数组相加
CUDA C 高效入门第二章 -- CUDA 线程组织之两个数组相加1 资料2 正文2.1 C 版本的两个数组相加2.2 CUDA 版本的两个数组相加2.3 为 CUDA 版本的两个数组相加添加运行时错误检查3 总结1 资料上一篇文章 CUDA C 高效入门第二章 – CUDA 线程组织之 Hello World我们用了五个 Hello World 样例由浅入深地为大家介绍了 CUDA 的线程组织。本文我们将基于 CPU 和 GPU 分别实现两个数组相加体会 CUDA 编程与 C 编程的区别。本文参考资料如下1樊哲勇CUDA编程 基础与实践 第二三四五章2cuda-programming-guide 1.2 编程模型3cuda-programming-guide 2.1 CUDA C 入门4cuda-programming-guide 2.2 编写 CUDA SIMT 内核5cuda-programming-guide 2.5 NVCC 编译器本系列博客汇总链接CUDA C 高效入门系列。2 正文2.1 C 版本的两个数组相加1下面是只使用 C 实现两个数组相加02_array_add/add.cpp基本逻辑是数组 a 与 数组 b 对应元素相加复制给数组 c 的对应元素并检查结果是否正确。#includecmath#includecstdlib#includecstdioconstdoubleEPSILON1e-15;constdoublea1.23;constdoubleb2.34;constdoublec3.57;voidadd(constdouble*px,constdouble*py,double*pz,constintN){for(inti0;iN;i){pz[i]px[i]py[i];}}voidcheck(constdouble*pz,constintN){boolhas_errorfalse;for(inti0;iN;i){if(fabs(pz[i]-c)EPSILON){has_errortrue;}}printf(%s\n,has_error?Has errors:No errors);}intmain(void){constintN1e7;constintMsizeof(double)*N;double*px(double*)malloc(M);double*py(double*)malloc(M);double*pz(double*)malloc(M);for(inti0;iN;i){px[i]a;py[i]b;}add(px,py,pz,N);check(pz,N);free(px);free(py);free(pz);return0;}编译并运行ycaoThinkpad-T14:~/cuda_junior$ ./run_demo.sh 02_array_add/add.cpp -------------------- g-stdc17-Wall-Wextra-O2-o/tmp/tmp.HHABvx7Hb5/run_demo_exec 02_array_add/add.cpp -------------------- No errors2.2 CUDA 版本的两个数组相加1使用 C 实现两个数组相加对于 C 程序员来说实在太简单了。但如果用 CUDA 实现对于新手而言就不一定那么容易了。因此我们需要为大家引入简单 CUDA 程序的基本框架请重点关注 main 函数中的逻辑。头文件包含 常量定义或者宏定义 C自定义函数和 CUDA 核函数的定义实现intmain(void){分配主机与设备内存 初始化主机中的数据 将某些数据从主机复制到设备 调用核函数在设备中进行计算 将某些数据从设备复制到主机 释放主机与设备内存}2下面是使用 CUDA 实现的两个数组相加02_array_add/add1.cu里面用到了若干 CUDA 运行时 API细节直接读注释即可。#includecmath#includecstdlib#includecstdioconstdoubleEPSILON1e-15;constdoublea1.23;constdoubleb2.34;constdoublec3.57;// CUDA 设备函数由 __device__ 修饰符修饰表示这是一个在 GPU 上运行的函数// CUDA 设备函数由核函数或其他设备函数调用不能由 CPU 端调用// CUDA 设备函数可以有返回值// CUDA 设备函数可以同时由 __host__ 和 __device__ 修饰符修饰表示这个函数既可以在 CPU 上运行也可以在 GPU 上运行减少冗余代码// CUDA 设备函数不能同时由 __global__ 和 __device__ 修饰符修饰// CUDA 设备函数可以使用 __noinline__ 修饰符修饰表示这个函数不进行内联展开编译器不一定接受减少编译器优化带来的问题// CUDA 设备函数可以使用 __forceinline__ 修饰符修饰表示这个函数强制进行内联展开减少函数调用开销__device__doubleadd1_device(constdoublex,constdoubley){returnxy;}__global__voidadd1(constdouble*px,constdouble*py,double*pz,constintN){constinttidblockDim.x*blockIdx.xthreadIdx.x;if(tidN){return;}pz[tid]add1_device(px[tid],py[tid]);}__device__voidadd2_device(constdoublex,constdoubley,doublerz){rzxy;}__global__voidadd2(constdouble*px,constdouble*py,double*pz,constintN){constinttidblockDim.x*blockIdx.xthreadIdx.x;if(tidN){return;}add2_device(px[tid],py[tid],pz[tid]);}// CUDA 核函数的返回值必须是 void, 不能有返回值但核函数里面可以使用 return 语句提前结束函数的执行return 后面不能跟任何值。// CUDA 核函数必须使用 __global__ 修饰符修饰表示这是一个在 GPU 上运行的函数// CUDA 核函数不能同时由 __host__ 和 __global__ 修饰符修饰// CUDA 核函数支持 C 的重载overload// CUDA 核函数的参数个数必须确定不支持可变参数// CUDA 核函数支持传递非指针变量内容对每个线程可见// CUDA 核函数不能是类的成员函数。// CUDA 核函数由 CPU 端调用由 GPU 端执行CC 3.5 之前核函数不能调用核函数// CUDA 核函数被调用时必须使用 ... 语法指定 grid 和 block 的尺寸__global__voidadd(constdouble*px,constdouble*py,double*pz,constintN){// 这个公式得到是线程在整个 Grid 中的 x 维度的坐标// 但由于本样例中只使用了一维网格和一维线程块因此 x 维度的坐标就是线程在整个 Grid 中的一维全局索引。// 这个一维全局索引值与数组长度一致因此可以直接用这个索引值访问数组元素// 从而实现每个线程处理一个数组元素的计算所有数组元素并发执行计算。constinttidblockDim.x*blockIdx.xthreadIdx.x;// printf(tid %d\n, tid);if(tidN){return;}pz[tid]px[tid]py[tid];}voidcheck(constdouble*pz,constintN){boolhas_errorfalse;for(inti0;iN;i){if(fabs(pz[i]-c)EPSILON){has_errortrue;}}printf(%s\n,has_error?Has errors:No errors);}intmain(void){// 踩坑记录对于 GeForce MX450nvidia-smi 显示显存大小为 1878MiB测试发现如果设为 1e8则超过显存大小程序失败1e7 可以。constintN1e7;constintMsizeof(double)*N;double*h_px(double*)malloc(M);double*h_py(double*)malloc(M);double*h_pz(double*)malloc(M);for(inti0;iN;i){h_px[i]a;h_py[i]b;}// CUDA 编程的传统是将 CPU 端的变量命名为 h_ 开头表示 host 端的变量// 将 GPU 端的变量命名为 d_ 开头表示 device 端的变量。double*d_px,*d_py,*d_pz;// 所有的 CUDA 运行时 API 都是以 cuda 开头// cudaMalloc() 函数在 GPU 显存上分配内存API 接口// cudaError_t cudaMalloc(void **address, size_t size);// address 是待分配内存的指针是一个二重指针size 是待分配内存的大小单位是字节。// cudaError_t 是一个枚举类型表示 CUDA 运行时 API 的返回值cudaSuccess 表示成功其他值表示失败。cudaMalloc((void**)d_px,M);cudaMalloc((void**)d_py,M);cudaMalloc((void**)d_pz,M);// cudaMemcpy() 函数的作用是拷贝数据API 接口// cudaError_t cudaMemcpy(void *dst, const void *src, size_t count, enum cudaMemcpyKind kind);// dst 是目标地址src 是源地址count 是拷贝的字节数// kind 是拷贝的方向cudaMemcpyHostToDevice 表示从 CPU 端拷贝到 GPU 端cudaMemcpyDeviceToHost 表示从 GPU 端拷贝到 CPU 端cudaMemcpyDeviceToDevice 表示在 GPU 端拷贝。// cudaMemcpyDefault 表示由 CUDA 运行时 API 自动选择拷贝的方向通常情况下CUDA 运行时 API 会根据 dst 和 src 的地址判断拷贝的方向。// cudaMemcpyHostToHost 表示从 CPU 端拷贝到 CPU 端。cudaMemcpy(d_px,h_px,M,cudaMemcpyHostToDevice);cudaMemcpy(d_py,h_py,M,cudaMemcpyHostToDevice);constintblock_size128;constintgrid_size(N%block_size0)?(N/block_size):(N/block_size1);printf(grid_size %d, block_size %d\n,grid_size,block_size);addgrid_size,block_size(d_px,d_py,d_pz,N);// add1grid_size, block_size(d_px, d_py, d_pz, N);// add2grid_size, block_size(d_px, d_py, d_pz, N);cudaMemcpy(h_pz,d_pz,M,cudaMemcpyDeviceToHost);check(h_pz,N);free(h_px);free(h_py);free(h_pz);cudaFree(d_px);cudaFree(d_py);cudaFree(d_pz);return0;}编译运行ycaoThinkpad-T14:~/cuda_junior$ ./run_demo.sh 02_array_add/add1.cu -------------------- nvcc-archsm_75-o/tmp/tmp.ePKavUQjc6/run_demo_exec 02_array_add/add1.cu -------------------- grid_size78125, block_size128No errors3C 版本 add 函数与 CUDA 版本的核心区别重中之重C 版本的 add 函数就是一个循环通过逐个遍历数组元素完成求和以及赋值操作。这个循环就是前面文章所说的 CPU 顺序执行的思维方式见 CUDA C 高效入门第一章 – CUDA 导学之为什么要学习 GPU CUDA 。到了 CUDA 编程时代默认的思维方式就是并行就是想办法分解任务然后使用足够多的线程去执行这是所有 CUDA 程序设计的出发点。非如此不足以发挥 CUDA 成千上万个处理核的威力。针对两个数组相加这个例子使用 CUDA 编程下意识的就要想到数组有多长就用多少个 CUDA 线程一个线程只处理一个元素即可。因此CUDA 版本的 add 核函数就需要先获得线程的一维索引然后使用这个索引去访问数组这样就能做到一个线程处理一个元素。至于求和以及赋值则与 C 版本是一样的。总结一下看似长得差不多的两个 add 函数其思维方式是有本质区别的而这个区别又来自于 CPU 和 GPU 设计原理的的本质区别。voidadd(constdouble*px,constdouble*py,double*pz,constintN){for(inti0;iN;i){pz[i]px[i]py[i];}}__global__voidadd(constdouble*px,constdouble*py,double*pz,constintN){constinttidblockDim.x*blockIdx.xthreadIdx.x;// printf(tid %d\n, tid);if(tidN){return;}pz[tid]px[tid]py[tid];}2.3 为 CUDA 版本的两个数组相加添加运行时错误检查1上一节使用到的几个 CUDA 运行时 API 都有 cudaError_t 类似的返回值因此我们可以写一个宏在调用这些 API 时对返回值进行检查。我们将这个宏写在一个头文件中03_check_api/cuda_error.cuh#ifndef__ERROR_CUH__#define__ERROR_CUH__#includecstdio#defineCHECK_CUDA_CALL(call)\do{\constcudaError_t error_codecall;\if(error_code!cudaSuccess){\printf(CUDA Error: \n);\printf( File: %s\n,__FILE__);\printf( Line: %d\n,__LINE__);\printf( Error code: %d\n,error_code);\printf( Error string: %s\n,cudaGetErrorString(error_code));\exit(1);\}\}while(0)#endif// __ERROR_CUH__2下面是对 CUDA 版本的两个数组相加添加运行时错误检查细节看注释就行03_check_api/check1.cu#includecmath#includecstdio#includecuda_error.cuhconstdoubleEPSILON1e-15;constdoublea1.23;constdoubleb2.34;constdoublec3.57;__global__voidadd(constdouble*px,constdouble*py,double*pz,constintN){constinttidblockDim.x*blockIdx.xthreadIdx.x;// printf(tid %d\n, tid);if(tidN){return;}pz[tid]px[tid]py[tid];}voidcheck(constdouble*pz,constintN){boolhas_errorfalse;for(inti0;iN;i){if(fabs(pz[i]-c)EPSILON){has_errortrue;}}printf(%s\n,has_error?Has errors:No errors);}intmain(void){constintN1e7;constintMsizeof(double)*N;double*h_px(double*)malloc(M);double*h_py(double*)malloc(M);double*h_pz(double*)malloc(M);for(inti0;iN;i){h_px[i]a;h_py[i]b;}double*d_px,*d_py,*d_pz;CHECK_CUDA_CALL(cudaMalloc((void**)d_px,M));CHECK_CUDA_CALL(cudaMalloc((void**)d_py,M));CHECK_CUDA_CALL(cudaMalloc((void**)d_pz,M));// 这里故意将 cudaMemcpyHostToDevice 错写成 cudaMemcpyDeviceToHost测试 CHECK_CUDA_CALL 宏的功能// CHECK_CUDA_CALL(cudaMemcpy(d_px, h_px, M, cudaMemcpyDeviceToHost));CHECK_CUDA_CALL(cudaMemcpy(d_px,h_px,M,cudaMemcpyHostToDevice));CHECK_CUDA_CALL(cudaMemcpy(d_py,h_py,M,cudaMemcpyHostToDevice));// 由于 block 最大为 1024, 这里故意写成 1280, 测试核函数调用失败的情况// const int block_size 1280;constintblock_size128;constintgrid_size(N%block_size0)?(N/block_size):(N/block_size1);printf(grid_size %d, block_size %d\n,grid_size,block_size);addgrid_size,block_size(d_px,d_py,d_pz,N);// 这两句是检查核函数调用的错误cudaGetLastError() 的作用是在主机与设备同步之前捕捉最后一个错误// 通常情况下主机异步调用核函数调用后立即执行下一条语句// 因此捕捉核函数调用的错误需要在主机与设备同步之前调用 cudaGetLastError() 函数。// 除了显示调用 cudaDeviceSynchronize() 实现主机与设备同步还可以临时设置环境变量 CUDA_LAUNCH_BLOCKING1强制主机与设备同步。// 设置之后所有核函数的调用都变为同步的主机在调用核函数之后必须等待设备执行完核函数之后才能继续执行下一条语句。// CHECK_CUDA_CALL(cudaGetLastError());// CHECK_CUDA_CALL(cudaDeviceSynchronize());CHECK_CUDA_CALL(cudaMemcpy(h_pz,d_pz,M,cudaMemcpyDeviceToHost));check(h_pz,N);free(h_px);free(h_py);free(h_pz);CHECK_CUDA_CALL(cudaFree(d_px));CHECK_CUDA_CALL(cudaFree(d_py));CHECK_CUDA_CALL(cudaFree(d_pz));return0;}编译和运行ycaoThinkpad-T14:~/cuda_junior$ ./run_demo.sh 03_check_api/check1.cu -------------------- nvcc-archsm_75-o/tmp/tmp.ZitlLOKBof/run_demo_exec 03_check_api/check1.cu -------------------- grid_size78125, block_size128No errors3 总结本文所有代码都托管在本人的 github 上cuda_junior。下一篇我们仍然基于两个数组相加这个例子对 CUDA 的加速效果进行度量

相关新闻

Rope 换脸 GUI 实操指南:3 步把 inswapper_128 跑成图形界面

Rope 换脸 GUI 实操指南:3 步把 inswapper_128 跑成图形界面

Rope 换脸 GUI 实操指南:3 步把 inswapper_128 跑成图形界面 【免费下载链接】Rope GUI-focused roop 项目地址: https://gitcode.com/GitHub_Trending/ro/Rope Rope 是一款带 GUI 的换脸工具,把 insightface 的 inswapper_128 模型封装成窗口化程…

2026/10/15 3:21:39 阅读更多 →
Rust RFC 1758 深度解读:`[repr(transparent)]` 让 newtype 在 FFI 中与底层类型 ABI 完全一致

Rust RFC 1758 深度解读:`[repr(transparent)]` 让 newtype 在 FFI 中与底层类型 ABI 完全一致

文档 【免费下载链接】rfcs RFCs for changes to Rust 项目地址: https://gitcode.com/gh_mirrors/rf/rfcs 点击查看 免费下载 导读 本文以本仓库中的 RFC 1758 为核心,系统讲解 Rust #[repr(transparent)] 属性的设计动机、语法规则、适用条件与限制。…

2026/10/15 3:21:39 阅读更多 →
系统代码每次编译,工作区必然变脏的本质原因!

系统代码每次编译,工作区必然变脏的本质原因!

系统代码每次编译,工作区必然变脏的本质原因在哪里? 系统代码每次编译,工作区编译就一定会把那些文件回写,而这些文件已被仓库跟踪。 所以每次git status 变脏,是因为编译把结果写进了 Git 已经跟踪的路径。 ProjectCo…

2026/10/15 3:20:38 阅读更多 →

最新新闻

git 源代码里,出现一大堆红色文件的一次思考

git 源代码里,出现一大堆红色文件的一次思考

git 源代码里,出现一大堆红色文件 是因为切换分支的时候,底座导致的。 1.首先要理解rebase是什么意思? rebase是录制本地代码,把最新的代码作为背景,然后把最新修改加在最新代码后面。 2.如何解决呢? git c…

2026/10/15 4:02:46 阅读更多 →
18.从 IP 到 MQTT:STM32 用 AT 指令驱动 ESP-12F 上云的完整链路(含全套实战代码 + 踩坑清单)

18.从 IP 到 MQTT:STM32 用 AT 指令驱动 ESP-12F 上云的完整链路(含全套实战代码 + 踩坑清单)

这是我做 STM32F411 ESP-12F 跑通 MQTT 上云之后的一次系统复盘。上一篇讲了「网络地基 → MQTT 模型 → AT 指令遥控」三层逻辑,有同学说想要能直接抄走跑的代码。这篇我把全套实战代码补上:mqtt.h 头文件、mqtt.c 逐函数实现(每步为什么这…

2026/10/15 4:02:46 阅读更多 →
git 里的 rebase到底是什么?

git 里的 rebase到底是什么?

一句话:rebase-把本地代码录制下来,把背景换成别人的最新代码,然后把本地代码在最新代码基础上播放出来。那么 git pull --rebase origin master 是什么意思呢?意思是什么 取服务器的最新代码作为底座,origin 代码仓库…

2026/10/15 4:02:46 阅读更多 →
从背了就忘到主动复习 江苏一个初中英语班的变化

从背了就忘到主动复习 江苏一个初中英语班的变化

苏南一位初三孩子的妈妈跟我聊起上学期的事。孩子英语卷子拿回来,阅读错一半,完形填空连蒙带猜,最让她头疼的是词汇——今天默写全对,三天后再测,能记住的不到三成。她试过打印单词表、买过卡片、也盯着孩子抄过几遍&a…

2026/10/15 4:02:46 阅读更多 →
GitHub Copilot Chat:用对话方式让 AI 帮你重构代码

GitHub Copilot Chat:用对话方式让 AI 帮你重构代码

GitHub Copilot Chat:用对话方式让 AI 帮你重构代码一、重构——AI 编程的"杀手级场景" 在所有的编程任务中,代码重构是从 AI 辅助中获益最大的场景之一。重构有个特点:它需要理解现有代码的逻辑,然后进行"安全的变…

2026/10/15 4:02:46 阅读更多 →
一种可快速恢复的Zabbix高可用架构设计

一种可快速恢复的Zabbix高可用架构设计

做运维的朋友们可能都有过这样的深夜惊魂:好容器完成了监控的Server、Proxy的部署、告警的收敛,监控的覆盖率也达标了,结果承载Zabbix Server和PostgreSQL的机器突然宕机。那一刻,不是“少看几张图”的问题,而是整个团…

2026/10/15 4:01:45 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →