news 2026/4/3 4:10:52

从简单CUDA开始

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从简单CUDA开始

从简单CUDA开始

我们将从一个简单的C++程序开始,这个程序将两个包含一百万个元素的数组相加。下面是完整的代码。

#include<iostream>#include<math.h>// 函数:将两个数组的元素相加voidadd(intn,float*x,float*y){for(inti=0;i<n;i++)y[i]=x[i]+y[i];}intmain(void){intN=1<<20;// 1M elementsfloat*x=newfloat[N];float*y=newfloat[N];// 在CPU上初始化数组for(inti=0;i<N;i++){x[i]=1.0f;y[i]=2.0f;}// 在CPU上运行add函数add(N,x,y);// 检查结果中是否有错误floatmaxError=0.0f;for(inti=0;i<N;i++)maxError=fmax(maxError,fabs(y[i]-3.0f));std::cout<<"Max error: "<<maxError<<std::endl;// 释放内存delete[]x;delete[]y;return0;}

这个程序在CPU上初始化两个一百万元素的数组xy,然后调用add函数将它们相加,并将结果存回数组y。最后,它检查结果是否正确。这是一个典型的内存带宽受限的计算,因为每个加法操作都需要两次内存读取和一次内存写入。

转换为CUDA内核

为了将这个函数转换为可以在GPU上运行的CUDA内核,我们需要做的主要改动是添加__global__修饰符。这个修饰符告诉CUDA编译器,这是一个我们将在GPU上调用的函数。

__global__voidadd(intn,float*x,float*y){for(inti=0;i<n;i++)y[i]=x[i]+y[i];}

在CUDA中,在CPU上运行的代码称为主机代码(host code),而在GPU上运行的代码称为设备代码(device code)__global__表示一个函数可以在设备上执行,并且可以从主机全局调用。我们通过一种特殊的语法<<<...>>>来从主机调用这个内核。

add<<<1,1>>>(N,x,y);

现在,add函数将在GPU上执行。但我们还需要处理内存。

CUDA中的内存分配

为了让GPU能够访问数据,我们需要在GPU内存中分配数据,或者使用一种特殊的CUDA特性,称为统一内存(Unified Memory)。统一内存创建了一个托管的内存池,CPU和GPU都可以访问。要使用统一内存,我们用cudaMallocManaged()替换new,用cudaFree()替换delete

#include<iostream>#include<math.h>// CUDA内核:在GPU上执行__global__voidadd(intn,float*x,float*y){for(inti=0;i<n;i++)y[i]=x[i]+y[i];}intmain(void){intN=1<<20;float*x,*y;// 使用统一内存分配x和ycudaMallocManaged(&x,N*sizeof(float));cudaMallocManaged(&y,N*sizeof(float));// 在CPU上初始化数组for(inti=0;i<N;i++){x[i]=1.0f;y[i]=2.0f;}// 在GPU上运行内核add<<<1,1>>>(N,x,y);// 等待GPU完成计算cudaDeviceSynchronize();// 检查结果floatmaxError=0.0f;for(inti=0;i<N;i++)maxError=fmax(maxError,fabs(y[i]-3.0f));std::cout<<"Max error: "<<maxError<<std::endl;// 释放内存cudaFree(x);cudaFree(y);return0;}

我们还需要在调用内核后添加cudaDeviceSynchronize()。这是因为CUDA内核启动是异步的。主机代码在启动内核后会立即继续执行,而不会等待内核完成。cudaDeviceSynchronize()会阻塞主机线程,直到所有先前发出的CUDA任务(包括内核)完成。这确保了我们在CPU上访问结果之前,GPU已经完成了计算。

现在,我们可以编译并运行这个CUDA程序了。

# 编译CUDA代码nvcc add.cu -o add_cuda# 运行程序./add_cuda# 输出: Max error: 0.0

程序成功运行,但速度非常慢。这是因为内核只在单个GPU线程上运行。

性能分析

为了衡量性能,我们可以使用NVIDIA Nsight Systems命令行工具nsys。我们可以创建一个简单的bash脚本nsys_easy来简化这个过程。

#!/bin/bashnsys profile -t cuda --stats=true"$@"

现在,我们可以用这个脚本来分析我们的程序。

# 分析单线程CUDA版本./nsys_easy ./add_cuda

在NVIDIA T4 GPU上,单线程内核的执行时间大约是91.8毫秒。这比CPU版本慢得多。为了进行比较,我们可以测量CPU版本的性能。在我的系统上,CPU版本大约需要2.5毫秒。GPU版本慢了36倍!这是因为我们的CUDA内核只使用了一个GPU线程。

引入线程

为了利用GPU的并行能力,我们需要使用多个线程。我们可以修改内核启动配置,使用256个线程。

// 启动256个线程add<<<1,256>>>(N,x,y);

现在,我们需要修改内核代码,让每个线程处理一部分数据。我们可以使用内置变量threadIdx.x,它提供了当前线程在块内的索引。

__global__voidadd(intn,float*x,float*y){intindex=threadIdx.x;intstride=blockDim.x;for(inti=index;i<n;i+=stride)y[i]=x[i]+y[i];}

在这个修改后的内核中,每个线程从threadIdx.x开始,以blockDim.x(块中的线程总数,这里是256)为步长,处理数组中的元素。这种循环方式被称为grid-stride loop,它有几个好处:

  1. 可扩展性:无论我们用多少线程启动内核,它都能正确工作。
  2. 高效性:它能很好地合并内存访问。

再次运行性能分析,我们看到执行时间降到了2.05毫秒,比单线程版本快了45倍!

引入块

我们已经使用了256个线程,但现代GPU可以同时运行成千上万个线程。为了进一步扩展,我们可以使用多个线程块(thread blocks)。我们可以修改内核启动配置,使用多个块。

intblockSize=256;intnumBlocks=(N+blockSize-1)/blockSize;add<<<numBlocks,blockSize>>>(N,x,y);

这里,我们计算了需要的块数,以确保每个元素都至少被一个线程访问。现在,我们需要修改内核,使用blockIdx.x(当前块在grid中的索引)和gridDim.x(grid中的块总数)来计算全局索引。

__global__voidadd(intn,float*x,float*y){intindex=blockIdx.x*blockDim.x+threadIdx.x;intstride=blockDim.x*gridDim.x;for(inti=index;i<n;i+=stride)y[i]=x[i]+y[i];}

再次运行性能分析,执行时间降到了47.5微秒,比单块版本快了43倍,比单线程版本快了1932倍!这个版本的内核实现了265 GB/s的内存带宽,达到了NVIDIA T4 GPU峰值带宽(320 GB/s)的80%以上。

统一内存预取

虽然我们的内核现在非常快,但Nsight Systems的分析显示,仍然存在一些性能瓶颈。这是因为统一内存的按需页面迁移(on-demand page migration)。当GPU内核首次访问尚未迁移到GPU内存的数据时,会发生页面错误(page fault),导致数据从CPU内存迁移到GPU内存。这些迁移会增加内核的执行时间。

为了解决这个问题,我们可以使用cudaMemPrefetchAsync()在内核启动前将数据**预取(prefetch)**到GPU。

// 在内核启动前预取数据到GPUcudaMemPrefetchAsync(x,N*sizeof(float),0,0);cudaMemPrefetchAsync(y,N*sizeof(float),0,0);add<<<numBlocks,blockSize>>>(N,x,y);cudaDeviceSynchronize();

预取操作是异步的,并且可以与数据传输和内核执行重叠。通过预取,我们告诉CUDA运行时,我们即将在GPU上使用这些数据,从而避免了运行时的页面错误。

总结

通过这个简单的向量加法示例,我们学习了如何将一个CPU程序转换为一个高度并行的CUDA程序。我们使用了统一内存来简化内存管理,并使用grid-stride loop来实现可扩展的并行内核。最后,我们使用预取来优化数据传输。

以下是不同版本的性能对比:

版本时间 (ns)相对单线程加速带宽
单线程91,811,2061x137 MB/s
单块(256线程)2,049,03445x6 GB/s
多块47,5201932x265 GB/s

下一步

这只是CUDA编程的开始。要继续学习,我们推荐以下资源:

  • CUDA Toolkit文档: Quick Start Guide, Programming Guide, Best Practices Guide
  • 后续教程系列: 探索更高级的CUDA主题,如共享内存、原子操作和多维索引。
  • NVIDIA DLI课程: Getting Started with Accelerated Computing in Modern CUDA C++
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/3 3:29:02

58、Linux 网络服务配置与安全防护全解析

Linux 网络服务配置与安全防护全解析 1. 文件权限与所有权设置 在 Linux 系统中,合理设置文件的权限和所有权对于系统安全至关重要。例如对于 index.html 文件,我们可以通过以下命令将其所有权赋予特定用户和用户组: [root@server1 root]# chown webma:webma /var/www…

作者头像 李华
网站建设 2026/3/28 23:13:53

纯净版刷机固件合集 + 避坑指南

本次整理的固件均为精简纯净版&#xff0c;剔除冗余预装应用&#xff0c;保留核心系统功能&#xff0c;刷机后运行流畅度直接翻倍&#xff0c;还能自由安装第三方应用。固件涵盖华为悦盒、中兴 B860、创维 E900S、魔百盒 CM201 等热门机型&#xff0c;每款都标注了适配芯片型号…

作者头像 李华
网站建设 2026/4/3 2:38:02

哔哩下载姬DownKyi:零基础也能轻松下载B站8K视频的完整指南

哔哩下载姬DownKyi&#xff1a;零基础也能轻松下载B站8K视频的完整指南 【免费下载链接】downkyi 哔哩下载姬downkyi&#xff0c;哔哩哔哩网站视频下载工具&#xff0c;支持批量下载&#xff0c;支持8K、HDR、杜比视界&#xff0c;提供工具箱&#xff08;音视频提取、去水印等&…

作者头像 李华
网站建设 2026/4/3 3:12:24

终极服务器监控实战:哪吒监控完整部署与应用指南

在当今数字化时代&#xff0c;服务器监控已成为每个运维团队不可或缺的运维工具。面对复杂的服务器环境和频繁的系统故障&#xff0c;如何快速搭建一个稳定可靠的自托管监控系统&#xff1f;哪吒监控为您提供了完美的解决方案&#xff01;&#x1f3af; 【免费下载链接】nezha …

作者头像 李华
网站建设 2026/4/1 9:06:10

露,无创血压测量系统 小动物无创血压系统 大鼠血压测量系统 大鼠无创血压测量系统 小动物无创血压分析系统

无创血压测量系统的测量原理&#xff0c;与常规人体血压计依托克氏音检测动脉血压的原理基本一致。安徽正华&#xff0c;生物&#xff0c;露核心构成包含测量组件与信号处理模块&#xff1a;关键核心部件为高敏脉搏换能器&#xff0c;其能够精准捕捉动脉血流量变化所引发的不同…

作者头像 李华
网站建设 2026/4/3 3:07:11

WritingTools终极配置手册:零基础快速掌握跨平台AI写作助手

WritingTools终极配置手册&#xff1a;零基础快速掌握跨平台AI写作助手 【免费下载链接】WritingTools The worlds smartest system-wide grammar assistant; a better version of the Apple Intelligence Writing Tools. Works on Windows, Linux, & macOS, with the free…

作者头像 李华