CUDA:页锁定内存(pinned memory)和按页分配内存(pageable memory )

发布时间 2023-11-19 17:40:15作者: 牛犁heart

CUDA架构而言,主机端的内存分为两种,一种是可分页内存(pageable memroy), 一种是页锁定内存(page-lock或 pinned)
可分页内存是由操作系统API malloc()在主机上分配,页锁定内存是由CUDA函数cudaMallocHost()cudaHostAlloc()在主机内存中分配,页锁定内存的重要属性是主机的操作系统将不会对这块内存进行分页交换操作,确保该内存始终保留在物理内存中。

GPU知道页锁定内存的物理地址,可以通过直接内存访问(Direct Memory Access,DMA) 技术直接在主机和GPU之间复制数据,速率更快。由于每个页锁定内存都需要分配物理内存,并且这些内存不能交换到磁盘上,所以页锁定内存比使用标准malloc()分配的可分页内存更消耗内存空间。

可分页内存

image
数据在主机到设备的拷贝过程中,GPU驱动会先将数据拷贝到主机中的临时页锁定内存中,然后在由临时锁定内存拷贝到GPU内存中,从而完成数据从主机到设备的拷贝

页锁定内存

image
而页锁定内存,CUDA平台会直接申请一块页锁定内存,免去了从可分页内存中从CPU内存到临时页锁定内存的数据拷贝,从而达到数据拷贝效率。


host内存: 分为pageable memory和pinned memory
pageable memory: 通过操作系统API(malloc/new)分配的存储空间
pinned memory: 始终在物理内存中,不会被分配到低速的虚拟内存中,能够通过DMA加速与设备端通信;使用cudaMallocHost/cudaHostAlloc来分配pinned memory,cudaFreeHost来释放内存

使用Malloc分配的内存都是Pageable(交换页)的,而另一个模式就是Pinned(Page-locked),实质是强制让系统在物理内存中完成内存申请和释放的工作,不参与页交换,从而提高系统效率,需要使用cudaHostAlloc和cudaFreeHost(cudaMallocHost的内存也这样释放)来分配和释放。

使用pinned memory优点:主机端-设备端的数据传输带宽高;某些设备上可以通过zero-copy功能映射到设备地址空间,从GPU直接访问,省掉主存与显存间进行数据拷贝的工作;

使用pinned memory缺点:pinned memory 不可以分配过多:导致操作系统用于分页的物理内存变少, 导致系统整体性能下降;通常由哪个cpu线程分配,就只有这个线程才有访问权限;


页锁定内存的分配、操作和可分页内存的对比

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include "iostream"

using namespace std;


float cuda_host_alloc_test(int size, bool up)
{
    // 耗时统计
    cudaEvent_t start, stop;
    float elapsedTime;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);

    int *a, *dev_a;

    // 主机上分配页锁定内存
    // cudaError_t cudaStatus = cudaHostAlloc((void **)&a, size * sizeof(*a), cudaHostAllocDefault);
    cudaError_t cudaStatus = cudaMallocHost((void **)&a, size * sizeof(*a));
    if(cudaStatus != cudaSuccess)
    {
        printf("host alloc fail!\n");
        return -1;
    }

    // 在设备上分配内存空间
    cudaStatus = cudaMalloc((void **)&dev_a, size * sizeof(*dev_a));
    if(cudaStatus != cudaSuccess)
    {
        fprintf(stderr, "cudaMalloc failed\n");
        return -1;
    }

    cudaEventRecord(start, 0);

    // 计时开始
    for(int i = 0; i < 100; ++i)
    {
        // 主机拷贝到设备
        cudaStatus = cudaMemcpy(dev_a, a, size * sizeof(*a), cudaMemcpyHostToDevice);
        if(cudaStatus != cudaSuccess)
        {
            fprintf(stderr, "cudaMemcpy host to device failed!!!\n");
            return -1;
        }

        // 从设备拷贝到主机
        cudaStatus = cudaMemcpy(a, dev_a, size * sizeof(*dev_a), cudaMemcpyDeviceToHost);
        if(cudaStatus != cudaSuccess)
        {
            fprintf(stderr, "cudaMemcpy device to host failed!!!\n");
            return -1;
        }
    }

    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&elapsedTime, start, stop);

    cudaFreeHost(a);
    cudaFree(dev_a);
    cudaEventDestroy(start);
    cudaEventDestroy(stop);

    return (float)elapsedTime / 1000;
}

float cuda_host_Malloc_test(int size, bool up)
{
    // 耗时统计
    cudaEvent_t start, stop;
    float elapsedTime;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);

    int *a, *dev_a;

    // 主机上分配页锁定内存
    a = (int *)malloc(size * sizeof(*a));

    // 在设备上分配内存空间
    cudaError_t cudaStatus = cudaMalloc((void **)&dev_a, size * sizeof(*dev_a));
    if(cudaStatus != cudaSuccess)
    {
        fprintf(stderr, "cudaMalloc failed\n");
        return -1;
    }

    cudaEventRecord(start, 0);

    // 计时开始
    for(int i = 0; i < 100; ++i)
    {
        // 主机拷贝到设备
        cudaStatus = cudaMemcpy(dev_a, a, size * sizeof(*a), cudaMemcpyHostToDevice);
        if(cudaStatus != cudaSuccess)
        {
            fprintf(stderr, "cudaMemcpy host to device failed!!!\n");
            return -1;
        }

        // 从设备拷贝到主机
        cudaStatus = cudaMemcpy(a, dev_a, size * sizeof(*dev_a), cudaMemcpyDeviceToHost);
        if(cudaStatus != cudaSuccess)
        {
            fprintf(stderr, "cudaMemcpy device to host failed!!!\n");
            return -1;
        }
    }

    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&elapsedTime, start, stop);

    free(a);
    cudaFree(dev_a);
    cudaEventDestroy(start);
    cudaEventDestroy(stop);

    return (float)elapsedTime / 1000;
}

int main()
{
    float allocTime = cuda_host_alloc_test(100000, true);
    cout << "页锁定内存: " << allocTime << " s" << endl;

    float mallocTime = cuda_host_Malloc_test(100000, true);
	cout << "可分页内存: " << mallocTime << " s" << endl;
	getchar();
    return 0;
}

运行结果:
image

PS:

  • 官方文档中关于cudaMallocHost 有这样一段描述:

On systems where pageableMemoryAccessUsesHostPageTables is true, cudaMallocHost may not page-lock the allocated memory.

可通过

    cudaDeviceProp deviceProp;
    cudaGetDeviceProperties(&deviceProp, dev);

结构体cudaDeviceProppageableMemoryAccessUsesHostPageTables字段来查询是否支持

  • cudaHostAlloccudaMallocHost之间的区别
  1. cudaHostAlloc是CUDA运行时API中的一个函数,可以在主机端分配内存,并且将该内存分配为可由设备访问的内存。这意味着,如果使用cudaHostAlloc分配内存,那么可以在主机端和设备端直接传输数据,而无需使用额外的数据传输函数。 ---- 零拷贝
  2. cudaMallocHost是CUDA驱动程序API的一个函数,也可以在主机端分配内存,并且可以在主机和设备之间进行数据传输。与cudaHostAlloc不同的是,cudaMallocHost分配的内存只能由主机访问,并且必须使用额外的数据传输函数才能将数据传输到设备端。
    因此,如果需要在主机端和设备端之间频繁传输数据,可以使用cudaHostAlloc来分配内存,但如果只需要在主机端进行一些计算,然后将结果传输到设备端,可以使用cudaMallocHost来分配内存

参考:https://blog.csdn.net/lilai619/article/details/109199235
https://wenku.csdn.net/answer/2c2ff61f776e4c25948eb1fd86ad28a6