☰
CUDA C++--统一内存和系统内存以及nvcc编译
2026/11/4 7:05:18 网站建设 项目流程

统一内存和系统内存

统一虚拟地址空间——CPU内存和每个GPU的内存都在单一虚拟地址空间内拥有独立的地址范围

统一内存——一种CUDA功能,可实现托管内存,并能自动在CPU和GPU之间迁移

受限统一内存——一种具有部分限制的统一内存范式

完全统一内存——全面支持统一内存功能

硬件一致性下的完全统一内存——利用硬件能力全面支持统一内存

统一内存提示——用于指导特定分配中统一内存行为的API

页锁定主机内存——非页可访问的系统内存,某些CUDA操作需要使用

映射内存——一种不同于统一内存的机制,允许内核直接访问主机内存

异构管理内存(HMM)——Linux内核的一项特性,可实现对完整统一内存的软件一致性

地址转换服务(ATS)——当GPU通过NVLink芯片间互连(C2C)连接到CPU时可用的硬件特性,为完整统一内存提供硬件一致性

统一虚拟地址空间(UVA)

在单个操作系统进程内,所有主机内存和系统中所有GPU的全局内存均使用同一个虚拟地址空间,无论是通过CUDA API(如cudaMalloc、cudaMallocHost)还是系统分配API(如new、malloc、mmap)进行内存分配,所有内存分配都位于该虚拟地址空间内。

简单来说统一虚拟地址空间是CUDA提供的一种内存寻址机制,其作用便是让CPU和GPU使用同一虚拟地址空间来管理内存地址。

作用

其主要有以下三点作用

自动识别指针指向哪里的内存--cudaPointerGetAttributes()
cudaError_t cudaPointerGetAttributes( cudaPointerAttributes* attributes, const void* ptr );

attributes:输出参数,用于保存查询结果

ptr:输入参数,需要查询的内存指针

cudaPointerAttributes结构体
struct cudaPointerAttributes { cudaMemoryType type; int device; void* devicePointer; void* hostPointer; };

type:内存类型

枚举值

含义

常见分配方式

cudaMemoryTypeUnregistered

未注册的普通主机内存

malloc()

cudaMemoryTypeHost

CUDA 注册或分配的主机内存

cudaMallocHost()

cudaMemoryTypeDevice

GPU 设备内存

cudaMalloc()

cudaMemoryTypeManaged

CUDA 托管内存

cudaMallocManaged()

device:关联的GPU设备编号

devicePointer:当前GPU可以用来访问该内存的指针,无对用地址时为NULL

hostPointer:CPU可以用来访问该内存的指针,无对用地址时为NULL

#include <iostream> #include <cuda_runtime.h> void checkPointer(void* ptr) { cudaPointerAttributes attr{}; cudaError_t err = cudaPointerGetAttributes(&attr, ptr); if (err != cudaSuccess) { std::cout << "查询失败:" << cudaGetErrorString(err) << std::endl; return; } std::cout << "Memory type:" << attr.type << std::endl; std::cout << "Device:" << attr.device << std::endl; std::cout << "Device pointer:" << attr.devicePointer << std::endl; std::cout << "Host pointer:" << attr.hostPointer << std::endl; std::cout << std::endl; } int main() { int* h_ptr; int* d_ptr; int* m_ptr; // 普通 CPU 内存 h_ptr = (int*)malloc(1024); // GPU 显存 cudaMalloc(&d_ptr, 1024); // 托管内存 cudaMallocManaged(&m_ptr, 1024); checkPointer(h_ptr); checkPointer(d_ptr); checkPointer(m_ptr); free(h_ptr); cudaFree(d_ptr); cudaFree(m_ptr); return 0; }

自动判断拷贝方向--cudaMemcpyDefault

在没有UVA时,使用cudaMemcpy通常需要明确指定数据传输方向

cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice); cudaMemcpy(h_A, d_A, size, cudaMemcpyDeviceToHost);

而有UVA后,通常可以写成

cudaMemcpy(d_A, h_A, size, cudaMemcpyDefault); cudaMemcpy(h_A, d_A, size, cudaMemcpyDefault);

CUDA根据指针信息判断数据传输方向

简化部分CPU/GPU指针的使用

在支持UVA的情况下,CPU和GPU在某些内存分配方式下,可以使用相同的指针值访问同一块内存,不需要分别维护两个指针。

#include <cuda_runtime.h> #include <iostream> __global__ void kernel(int* ptr) { ptr[0] = 100; } int main() { int* ptr; cudaHostAlloc(&ptr, sizeof(int), cudaHostAllocMapped); // CPU写入 ptr[0] = 10; // GPU使用同一个指针 kernel<<<1, 1>>>(ptr); cudaDeviceSynchronize(); // CPU读取GPU修改后的值 std::cout << ptr[0] << std::endl; cudaFreeHost(ptr); }

内存分配方式

CPU/GPU 能否直接使用同一指针

malloc()

一般不能

cudaMalloc()

CPU 一般不能直接解引用

cudaHostAlloc()

在 UVA 支持下通常可以

cudaMallocManaged()

可以

统一内存

统一内存是CUDA的一项内存特性,它允许在CPU或GPU通过同一个指针访问由CUDA管理的内存,从而减少开发者手动管理数据传输的工作。

因此我们可以简单的认为,统一内存就是管理内存

在某些系统上,统一内存必须显式分配。可以通过以下几种方式显式分配管理内存:

cudaMallocManaged

cudaMallocFromPoolAsync,并使用“allocType”设置为cudaMemAllocationTypeManaged创建的内存池
带有“__managed__”指示符的全局变量

统一内存范式

统一内存由于各个操作系统、硬件等有所不同,因此可以通过cudaDeviceGetAttribute查询一下属性来确定可用的统一形式(具体用法可查看CUDA C++入门和重要函数讲解--GPU属性)

查询相关属性,可以使用cudaDeciveProp的成员函数--cudaDevAttrConcurrentManagedAccess:1表示完全支持统一内存,0表示仅有限支持

cudaDevAttrPageableMemoryAccess:1表示所有系统内存均支持统一内存,0表示仅显式分配为管理内存的部分才支持统一内存

cudaDevAttrPageableMemoryAccessUsesHostPageTables:指示CPU/GPU一致性机制:1 表示硬件实现,0 表示软件实现

#include <cuda_runtime.h> #include <iostream> int main() { int DevAttrConcurrentManagedAccess; cudaDeviceGetAttribute( &DevAttrConcurrentManagedAccess, cudaDevAttrConcurrentManagedAccess, 0 ); int DevAttrPageableMemoryAccess; cudaDeviceGetAttribute( &DevAttrPageableMemoryAccess, cudaDevAttrPageableMemoryAccess, 0 ); int DevAttrPageableMemoryAccessUsesHostPageTables; cudaDeviceGetAttribute( &DevAttrPageableMemoryAccessUsesHostPageTables, cudaDevAttrPageableMemoryAccessUsesHostPageTables, 0 ); std::cout << "是否完全支持统一内存:" << DevAttrConcurrentManagedAccess << std::endl; std::cout << "是否所有系统内存均支持统一内存:" << DevAttrPageableMemoryAccess << std::endl; std::cout << "CPU/GPU一致性机制实现方式:" << DevAttrPageableMemoryAccessUsesHostPageTables << std::endl; }

统一内存操作共有四种模式:
- 完全支持显式管理内存分配
- 完全支持所有分配并采用软件一致性机制
- 完全支持所有分配并采用硬件一致性机制
- 有限支持统一内存

当完全支持可用时,可以要求显式分配内存,或者所有系统内存都可能隐式地统一为共享内存。当所有内存都隐式统一时,一致性机制可以是软件或硬件实现。Windows 和部分 Tegra 设备对统一内存的支持有限。

下面将cudaDevAttrConcurrentManagedAccess、cudaDevAttrPageableMemoryAccess和cudaDevAttrPageableMemoryAccessUsesHostPageTables简化为[1]、[2]、[3]

模式[1][2][3]判定结果
10不关心不关心Limited unified memory support(受限统一内存支持)
210不关心Full support for explicit managed memory allocations(显式托管内存完全支持)
3110Full support for all allocations with software coherence(所有分配都支持,软件一致性)
4111Full support for all allocations with hardware coherence(所有分配都支持,硬件一致性)
模式能否使用cudaMallocManaged()普通malloc/new内存能否被GPU直接纳入统一内存支持一致性方式使用特点典型理解
1可以不可以无完整一致性支持只能用托管内存,且 CPU/GPU 同时访问受限,通常要严格同步能用统一内存,但限制较多
2可以不可以对托管内存提供完整支持cudaMallocManaged()分配的内存支持更完整的迁移/访问;普通主机内存仍不行托管内存很好用,但仅限托管内存
3可以可以软件一致性不仅托管内存,普通系统内存也能纳入统一内存支持;一致性主要靠软件机制维护范围更大,但一致性更多靠软件
4可以可以硬件一致性托管内存和普通系统内存都支持,且由硬件提供更强的一致性支持最完整、最理想的统一内存支持
全面支持统一内存功能

通常来说,工业上CUDA使用的环境都是linux,而大多数linux都全面支持统一内存,即模式2、3和4

对于全面支持统一内存功能的系统通常有如下特点:

- 统一内存通常在处理器首次触达其所在内存空间时进行分配
- 当统一内存被当前处理器以外的其他处理器使用时,通常会被迁移
- 统一内存的迁移或访问以内存页(软件一致性)或缓存行(硬件一致性)为粒度进行
- 允许超分配:应用程序可分配超过GPU物理可用内存的统一内存

- 分配和迁移行为可能与上述描述有所不同

有限支持统一内存功能

在 Windows(包括Windows子系统--WSL)上,仅提供统一内存功能的有限子集。在这些系统中,可使用受管理的内存,但 CPU 与 GPU 之间的迁移行为有所不同。

对于有限支持统一内存功能的系统通常有如下特点:

- 统一内存首先分配在 CPU 的物理内存中
- 统一内存以比虚拟内存页更大的粒度进行迁移
- 当GPU开始执行时,统一内存才会被迁移至GPU
- 在GPU活动期间,CPU不能访问统一内存
- 当GPU完成同步后,统一内存将被迁移回CPU
- 不允许对GPU内存进行超分配
- 仅CUDA显式分配为统一内存的内存才被统一处理

Page-Locked Host Memory(页锁定主机内存)

页锁定主机内存,也叫Pinned Memory(固定内存、锁页内存)。它是一种特殊的CPU内存,主要用于提高CPU和GPU之间的数据传输效率,并支持真正的异步数据传输。

其常使用的相关API如下(部分API可参考CUDA C++入门和重要函数讲解--内存管理):

API

作用

cudaMallocHost()

分配页锁定主机内存

cudaHostAlloc()

分配页锁定主机内存,并可指定额外属性

cudaFreeHost()

释放 CUDA 分配的页锁定内存

cudaHostRegister()

将已经分配的普通内存注册为页锁定内存

cudaHostUnregister()

解除已有内存的页锁定注册

cudaHostGetDevicePointer()

获取映射主机内存对应的 GPU 可访问指针

需要注意的是,页锁定主机内存并不是越多越好,虽然该内存可以快速的传输数据,但是分配过多会减少操作系统可以灵活调度的内存数量,从而导致整理性能的降低。
其实这个页锁定主机内存在实际中使用相当多,比如我们之前的项目,需要通过摄像头采集图像,采集好的图像,通常储存在CPU中,因此就可以通过页锁定主机内存来传输预处理后的图像给GPU,然后进行推理。

nvcc:CUDA编译器

CUDA编译器nvcc是NVIDIA提供的一套工具链,用于编译CUDA C/C++代码,这里主要讲解在构建应用程序时常用的nvcc使用方式和相关细节。

更加详细的文档请看:

1. Introduction — NVIDIA CUDA Compiler Driver 13.4 documentation

CUDA源文件和头文件

使用 nvcc 编译的源文件通常是主机代码(在 CPU 上执行)和设备代码(在 GPU 上执行)的组合。nvcc 接受常见的 C/C++ 源文件扩展名:.c、.cpp、.cc、.cxx 用于仅包含主机代码的文件,以及 .cu 用于包含设备代码或混合主机与设备代码的文件。包含设备代码的头文件通常采用 .cuh 扩展名,以区别于仅包含主机代码的头文件(如 .h、.hpp、.hh、.hxx 等)。

简单来说,.cu和.cuh分别是内核执行代码的源文件和头文

nvcc中代码文件的后缀名和作用
源代码文件

后缀

含义

主要用途

.cu

CUDA 源文件

编写 CPU 代码和 GPU 内核

.cpp

C++ 源文件

编写普通 C++ 代码

.cc、.cxx

C++ 源文件

与.cpp类似,只是文件命名习惯不同

.c

C 源文件

编写 C 语言程序

.h、.hpp

头文件

声明函数、类、模板等,供其他源文件包含

.cuh

CUDA 头文件

放置 CUDA 相关的函数声明、模板、设备端代码等

GPU编译过程中的文件

后缀

全称或含义

特点

.ptx

Parallel Thread Execution

GPU 的中间表示,类似面向 GPU 的汇编语言

.cubin

CUDA Binary

针对特定 GPU 架构生成的设备端二进制代码

.fatbin

Fat Binary

可以封装多个架构的 GPU 二进制代码和 PTX 等内容

NVCC编译工作流

命令说明产物
nvcc main.cu -o app直接生成可执行文件app
nvcc -c main.cu -o app只编译为目标文件main.o(包含CPU目标代码和GPU的fabin注册信息)
nvcc -ptx main.cu -o app生成PTXmain.ptx(GPU虚拟指令集)
nvcc -cubin main.cu -o app生成cubinmain.cubin(面向特定GPU架构的设备二进制)
nvcc - keep -v main.cu -o app保留中间文件并显示详细流程生成.ii/.cpp等一系列文件

NVCC基础用法

NVCC的通用结构如下:nvcc [编译选项] 源文件... [-o 输出文件]

nvcc:调用CUDA编译器驱动

编译选项:指定编译标准、优化、架构等

-Ipath--指定包含目录(大写的i)、-Lpath--指定包含库路径、-lpath--链接其他库

源文件:要编译的一个或多个文件

-o:指定输出文件名称和路径

观察编译过程

命令

作用

nvcc -keep main.cu -o app

保留编译中间文件

nvcc -v main.cu -o app

显示详细的编译工具调用信息

nvcc --version

查看 NVCC 版本

nvcc --help

查看帮助信息

nvcc --list-gpu-code

查看支持的 GPU 机器代码目标

nvcc --list-gpu-arch

查看支持的 GPU 虚拟架构

指定C++标准(-std)和GPU架构目标(-arch)

通过使用-std=C++17来指定使用C++17标准

通过使用-arch=sm_120来指定使用CC为12的机器代码架构目标。

优化级别(-o)

选项

含义

典型用途

-O0

关闭常规优化

调试

-O1

较低级别优化

开发测试

-O2

较高级别优化

常规性能测试

-O3

更积极的优化

性能实验

调试与错误检查相关选项

选项

作用

说明

-g

生成主机端调试信息

方便调试 CPU 代码

-G

启用设备端调试

通常会关闭设备端常规优化,影响性能

-lineinfo

生成设备端源代码行信息

适合使用性能分析工具

-Werror

将警告视为错误

编译警告会导致编译失败

-Xcompiler

向主机编译器传递参数

例如传递 GCC 警告选项

-ccbin

指定主机编译器

例如选择特定 GCC 或 MSVC 工具链

性能优化

-maxrregcount=64:限制寄存器数量为64

-Xptxas=-v:查看GPU资源使用情况

-res-usage:编译后打印资源使用报告,包括每个内核函数分配的寄存器、共享内存、常量内存和局部内存的数量

-opt-info=inline:打印内联函数的相关信息

浮点运算精度相关参数

选项

作用

注意事项

-use_fast_math

启用一组更偏向速度的数学运算设置

可能降低数值精度

-ftz=true

将部分非规格化浮点数按零处理

可能改变计算结果

-prec-div=false

允许使用精度较低的除法实现

应验证误差

-prec-sqrt=false

允许使用精度较低的平方根实现

应验证误差

-fmad=true

允许融合乘加运算

可能改变浮点舍入结果

-fmad=false

禁止融合乘加运算

可能改变性能和数值结果

Xptxas

Xptxas会讲参数传递给PTX汇编共ptaxs,其之所以重要,是因为其主要用法可以查看GPU资源使用、检查寄存器溢出、控制设备代码优化,以及调整缓存策略。

其常用格式为:-Xptxas=参数,部分用法在上面已经提及

写法

作用

-Xptxas=-v

输出 GPU 代码资源使用信息

-Xptxas=-warn-spills

对寄存器溢出发出警告

-Xptxas=-warn-lmem-usage

对本地内存使用发出警告

-Xptxas=-v,-warn-spills

同时输出资源信息并检查寄存器溢出

-Xptxas=-v,-warn-lmem-usage

同时输出资源信息并检查本地内存使用

-Xptxas=-Werror

将ptxas的警告视为错误

-Xptxas=-dlcm=ca

设置全局/通用加载的默认缓存策略

-Xptxas=-dscm=wb

设置全局/通用存储的默认缓存策略

从上表可以看出,Xptxas支持多个ptxas参数同时使用,不同参数之间使用都好隔开即可

默认缓存策略

正如前面文章所说的,L1缓存更加靠近SM,因此其访问延迟通常更低,因此更加适合在同一个线程块内的线程反复读取相同或附近的数据。而L2缓存容量更大,同时由多个SM共享,因此更加适合多个线程块或多个SM访问同一批数据。通过默认缓存策略便可以控制在加载或者储存是对于L1和L2的缓存策略。

控制加载时的缓存策略(-dlcm)
-Xptxas=-dlcm=ca -Xptxas=-dlcm=cg

参数值

含义

用法

ca

Cache at All Levels

加载时允许使用 L1、L2 等缓存层级

cg

Cache at Global Level

加载时绕过 L1,使用 L2 及以下缓存层级

控制储存时的缓存策略(-dscm)
-Xptxas=-dscm=wb -Xptxas=-dscm=cg

参数值

含义

直观理解

wb

Write-back

使用常规写回缓存策略

cg

Cache at Global Level

存储时绕过 L1,缓存于 L2 及以下层级

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询