CUDA内存管理全解析
立即解锁
发布时间: 2025-08-20 01:55:15 阅读量: 5 订阅数: 4 

### CUDA 内存管理全解析
#### 1. CUDA 内存类型概述
CUDA 为了实现性能最大化,会根据不同的使用场景采用多种类型的内存。主要分为主机内存和设备内存。
- **主机内存**:与系统中的 CPU 相连,默认情况下是可分页的,操作系统可能会移动或交换其到磁盘。为了让 GPU 能直接访问,可通过“页面锁定”将其变为固定内存,这种内存有更快的传输性能、支持异步内存复制,并且映射后的固定内存可被 CUDA 内核直接访问。
- **设备内存**:与 GPU 相连,通过集成在 GPU 中的内存控制器进行读写操作,峰值带宽极高。CUDA 内核可通过设备指针访问设备内存。
设备内存又可细分为以下几种类型:
| 内存类型 | 描述 |
| ---- | ---- |
| 全局内存 | 可静态或动态分配,CUDA 内核通过指针进行读写操作,对应全局加载/存储指令。 |
| 常量内存 | 只读内存,通过特定指令访问,由优化后的缓存层次结构为多个线程广播读取请求。 |
| 局部内存 | 包含栈,用于存储无法存于寄存器的局部变量、参数和子例程的返回地址。 |
| 纹理内存 | 以 CUDA 数组形式存在,通过纹理和表面加载/存储指令访问,有单独的缓存用于优化只读访问。 |
| 共享内存 | 不是由设备内存支持,而是片上的“暂存”内存抽象,用于块内线程间的快速数据交换。在 SM 1.x 硬件中,由 16K RAM 实现;在 SM 2.x 及更新硬件中,使用 64K 缓存,可分为 48K L1/16K 共享或 48K 共享/16K L1。 |
#### 2. 主机内存管理
##### 2.1 固定内存的分配与释放
CUDA 提供了特殊函数来分配和释放固定内存:
- **CUDA 运行时**:使用 `cudaHostAlloc()` 和 `cudaFreeHost()`。
- **驱动 API**:使用 `cuMemHostAlloc()` 和 `cuMemFreeHost()`。
这些函数与主机操作系统协作,分配页面锁定的内存并为 GPU 的直接内存访问(DMA)进行映射。CUDA 会跟踪已分配的内存,并加速涉及使用这些函数分配的主机指针的内存复制操作。部分函数(如异步内存复制函数)需要固定内存。
```mermaid
graph LR
A[开始] --> B[cudaHostAlloc()/cuMemHostAlloc()]
B --> C[分配固定内存]
C --> D[内存操作]
D --> E[cudaFreeHost()/cuMemFreeHost()]
E --> F[释放固定内存]
F --> G[结束]
```
##### 2.2 可移植固定内存
默认情况下,固定内存分配只能被调用 `cudaHostAlloc()` 或 `cuMemHostAlloc()` 时的当前 GPU 访问。通过指定 `cudaHostAllocPortable` 标志(CUDA 运行时)或 `CU_MEMHOSTALLOC_PORTABLE` 标志(驱动 API),应用程序可以请求将固定分配映射到所有 GPU。当统一虚拟寻址(UVA)生效时,所有固定内存分配都是可移植的。
##### 2.3 映射固定内存
默认情况下,固定内存分配在 CUDA 地址空间外为 GPU 映射,CUDA 内核不能直接读写主机内存。在 SM 1.2 及更高能力的 GPU 上,可通过以下步骤启用映射固定内存:
1. **CUDA 运行时**:在任何初始化之前调用 `cudaSetDeviceFlags()` 并指定 `cudaDeviceMapHost` 标志。
2. **驱动 API**:在 `cuCtxCreate()` 中指定 `CU_CTX_MAP_HOST` 标志。
启用后,可通过调用 `cudaHostAlloc()` 并指定 `cudaHostAllocMapped` 标志,或 `cuMemHostAlloc()` 并指定 `CU_MEMALLOCHOST_DEVICEMAP` 标志来分配映射固定内存。除非 UVA 生效,否则应用程序必须使用 `cudaHostGetDevicePointer()` 或 `cuMemHostGetDevicePointer()` 查询对应的设备指针。
##### 2.4 写组合固定内存
写组合内存旨在让 CPU 快速写入 GPU 帧缓冲区而不污染 CPU 缓存。CUDA 可通过调用 `cudaHostAlloc()` 并指定 `cudaHostWriteCombined` 标志,或 `cuMemHostAlloc()` 并指定 `CU_MEMHOSTALLOC_WRITECOMBINED` 标志来请求写组合内存。在某些系统上,它可以提高 PCI Express 传输性能,但在 NUMA 系统上优势不大,且读取速度较慢,因此 CUDA 开发者应谨慎使用。当 UVA 生效时,写组合固定分配不会映射到统一地址空间。
##### 2.5 注册固定内存
CUDA 开发者有时无法直接分配希望 GPU 访问的主机内存。CUDA 4.0 增加了注册固定内存的功能,它将分配与页面锁定和映射分离,可对已分配的虚拟地址范围进行页面锁定并为 GPU 映射。使用 `cuMemHostRegister()`/`cudaHostRegister()` 进行注册,`cuMemHostUnregister()`/`cudaHostUnregister()` 进行注销。注册的内存范围必须页面对齐,应用程序可通过以下两种方式分配页面对齐的地址范围:
- 使用操作系统的整页分配工具,如 Windows 上的 `VirtualAlloc()` 或其他平台上的 `valloc()` 或 `mmap()`。
- 对于任意地址范围,将其调整到下一个较低的页面边界并填充到下一个页面大小。
即使 UVA 生效,映射到 CUDA 地址空间的注册固定内存的设备指针与主机指针也可能不同,应用程序必须调用 `cudaHostGetDevicePointer()`/`cuMemHostGetDevicePointer()` 来获取设备指针。
##### 2.6 固定内存与 UVA
当 UVA 生效时,所有固定内存分配都是映射且可移植的,但写组合内存和注册内存除外。UVA 在除 Windows Vista 和 Windows 7 外的所有 64 位平台上受支持,在 Windows Vista 和 Windows 7 上,只有 TCC 驱动支持 UVA。应用程序可通过调用 `cudaGetDeviceProperties()` 并检查 `cudaDeviceProp::unifiedAddressing` 结构成员,或调用 `cuDeviceGetAttribute()` 并指定 `CU_DEVICE_ATTRIBUTE_UNIFIED_ADDRESSING` 来查询 UVA 是否生效。
##### 2.7 映射固定内存的使用
对于依赖 PCI Express 传输性能的应用程序,映射固定内存有很大优势,可减少内存复制,降低开销。常见的使用方式有:
- **向主机内存写入数据**:多 GPU 应用程序可通过映射固定内存将结果写回系统内存,避免额外的设备 - 主机内存复制。
- **流式处理**:使用 CUDA 流协调与设备内存的并发内存复制,同时内核在设备内存上进行处理。
- **高效复制**:在数据通过 PCI Express 传输时进行计算,如 GPU 在传输数据时计算子数组的缩减。
但使用映射固定内存也有一些注意事项:
- 从映射固定内存进行纹理处理非常慢。
- 必须以合并内存事务的方式访问,否则性能会有 2 倍到 6 倍的损失。
- 不建议使用内核轮询主机内存。
- 不要在映射固定主机内存上使用原子操作。
##### 2.8 NUMA、线程亲和性与固定内存
从 AMD Opteron 和 Intel Nehalem 开始,CPU 集成了内存控制器,多 CPU 系统引入了非统一内存访问(NUMA)架构。在 NUMA 系统中,CPU 访问本地内存和非本地内存的性能不同。对于 CUDA 应用程序,PCI Express 传输性能可能取决于内存引用是否本地。如果系统启用了 NUMA,建议在与给定 GPU 相同的节点上分配主机内存。虽然没有官方,开发者可根据系统设计知识,使用特定平台的 NUMA 感知 API 进行内存分配,并通过主机内存注册将其固定并映射给 GPU。
以下是在 Linux 和 Windows 上进行 NUMA 感知分配的代码示例:
```c
// Linux 上的 NUMA 感知分配
bool
numNodes( int *p )
{
if ( numa_available() >= 0 ) {
*p = numa_max_node() + 1;
return true;
}
return false;
}
void *
pageAlignedNumaAlloc( size_t bytes, int node )
{
void *ret;
printf( "Allocating on node %d\n", node ); fflush(stdout);
ret = numa_alloc_onnode( bytes, node );
return ret;
}
void
pageAlignedNumaFree( void *p, size_t bytes )
{
numa_free( p, bytes );
}
```
```c
// Windows 上的 NUMA 感知分配
bool
numNodes( int *p )
{
ULONG maxNode;
if ( GetNumaHighestNodeNumber( &maxNode ) ) {
*p = (int) maxNode+1;
return true;
}
return false;
}
void *
pageAlignedNumaAlloc( size_t bytes, int node )
{
void *ret;
printf( "Allocating on node %d\n", node ); fflush(stdout);
ret = VirtualAllocExNuma( GetCurrentProcess(),
NULL,
bytes,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
node );
return ret;
}
void
pageAlignedNumaFree( void *p )
{
VirtualFreeEx( GetCurrentProcess(), p, 0, MEM_RELEASE );
}
```
#### 3. 全局内存管理
##### 3.1 指针
在 CUDA 运行时,设备指针和主机指针都被类型化为 `void *`;驱动 API 使用 `CUdeviceptr` 类型。可使用 `uintptr_t` 类型在主机指针和设备指针之间进行可移植的转换。主机代码可以对设
0
0
复制全文
相关推荐









