返回> 网站首页 

CUDA 多个独立任务并行执行 - 常用API函数介绍

yoours2024-04-19 16:35:31 阅读 674

简介一边听听音乐,一边写写文章。

一、介绍

    在CUDA编程中,使用多线程来管理多个CUDA流是一种常见的做法,特别是在需要并行执行多个独立任务或进行多重数据转移和内核执行时。每个线程可以创建自己的CUDA流,并在其中提交命令,从而实现这些命令的异步执行。


二、部分API函数

    1. cudaDeviceReset

     用于重置当前设备上的所有CUDA资源

        这些资源包括显存、设备状态和所有CUDA上下文,以及释放设备上的所有已分配资源,并清除所有已注册的回调函数。在CUDA程序中,通常需要使用cudaDeviceReset来确保在程序结束前,释放所有CUDA资源。

        但是,过多地使用cudaDeviceReset可能会导致程序的性能下降,因此需要谨慎使用。只有在程序遇到某种同步操作时,缓冲区才会刷新。

    2. cudaStreamCreate

     用于创建执行流。

        CUDA流表示一个GPU操作队列,该队列中的操作将以添加到流中的先后顺序而依次执行。可以将一个流看做是GPU上的一个任务,不同任务可以并行执行。使用CUDA流,首先要选择一个支持设备重叠(Device Overlap)功能的设备,支持设备重叠功能的GPU能够在执行一个CUDA核函数的同时,还能在主机和设备之间执行复制数据操作。支持重叠功能的设备的这一特性很重要,可以在一定程度上提升GPU程序的执行效率。

        使用CUDA流的基本步骤如下:

            创建流:通过调用cudaStreamCreate创建一个或多个流对象。

            指定流:在启动核函数、执行内存传输或其他CUDA操作时,通过传递流对象来指定应该使用哪个流。

            同步流:可以使用cudaStreamSynchronize函数等待特定流中的所有操作完成。这会阻塞主机执行,直到GPU完成该流中的所有工作。

            销毁流:当不再需要流时,应该使用cudaStreamDestroy函数来释放与流相关联的资源。

    3. cudaMallocHost

     用于在主机(通常是CPU)上分配内存,但分配的内存可以被CUDA设备(GPU)直接访问,这种内存称为“锁定”或“钉住”内存。

        与常规的主机内存分配(如使用malloc或new)不同,cudaMallocHost分配的内存是固定(或锁定)在物理内存中的,这意味着它不会在操作系统进行内存管理时移动。这种特性使得CUDA设备能够直接访问这块内存,而无需进行额外的内存复制操作。

        cudaMallocHost的原型如下:

           cudaError_t cudaMallocHost(void **ptr, size_t size);

        其中:

            ptr 是一个指向指针的指针,用于接收分配的内存地址。

            size 是要分配的内存大小(以字节为单位)。

            函数返回一个cudaError_t类型的值,用于指示操作是否成功。

        使用cudaMallocHost的一个典型场景是当需要在GPU内核和CPU代码之间频繁地共享数据时。通过直接访问同一块内存,可以避免在设备和主机之间进行显式的内存传输,从而提高程序的性能。

        需要注意的是,cudaMallocHost分配的内存必须使用cudaFreeHost来释放,而不能使用常规的free函数。此外,由于这种内存是锁定的,因此可能无法像常规主机内存那样被操作系统高效地管理,这可能会对系统性能产生一些影响,特别是在内存压力较大的情况下。因此,在使用cudaMallocHost时需要权衡这些因素。

    4. cudaMalloc

     用于在 GPU 设备上分配内存。这些内存是专门用于 CUDA 核函数(kernel functions)执行的,并且只能通过 CUDA 运行时 API 进行访问和释放。

        cudaMalloc 的原型如下:

            cudaError_t cudaMalloc(void **devPtr, size_t size);

        其中:

            devPtr 是一个指向指针的指针,该指针在调用成功后将指向在设备上分配的内存地址。

            size 是要分配的内存大小(以字节为单位)。

            函数返回一个 cudaError_t 类型的值,用于指示操作是否成功。如果成功,*devPtr 将被设置为指向新分配内存的指针。如果内存分配失败,*devPtr 将被设置为 NULL。

        与在主机(CPU)上分配内存(如使用 malloc 或 new)不同,cudaMalloc 分配的内存仅可由 CUDA 设备(GPU)直接访问。这意味着,如果主机代码需要访问这些数据,必须使用 cudaMemcpy 或类似函数将数据从设备内存复制到主机内存,反之亦然。

        使用 cudaMalloc 分配的内存必须使用 cudaFree 来释放,而不是使用常规的 free 函数。这是因为 CUDA 设备内存和主机内存是分开管理的,并且 cudaFree 负责正确地清理设备内存。

    5. cudaMemcpyAsync

     用于异步地从源内存位置复制数据到目标内存位置,其中源和目标可以是主机(CPU)内存或设备(GPU)内存。与 cudaMemcpy 不同,cudaMemcpyAsync 不会阻塞主机代码的执行,而是立即返回一个,并且数据复制操作会在后台进行。

        cudaMemcpyAsync 的原型如下:

           cudaError_t cudaMemcpyAsync(void *dst, const void *src, size_t count, cudaMemcpyKind kind, cudaStream_t stream);

        参数说明:

            dst:指向目标内存位置的指针。

            src:指向源内存位置的指针。

            count:要复制的字节数。

            kind:复制操作的种类,如 cudaMemcpyHostToHost、cudaMemcpyHostToDevice、cudaMemcpyDeviceToHost 或 cudaMemcpyDeviceToDevice。

            stream:CUDA流,指定数据复制操作应该在哪个流中执行。

        这个函数返回一个 cudaError_t 类型的值,用于指示操作是否成功。

        使用 cudaMemcpyAsync 可以提高程序的并行性,因为主机代码可以在数据复制的同时继续执行其他任务。这通常用于实现更高效的数据传输与GPU计算的重叠。

6. <<<调用>>>

        当你使用kernel<<<blocks, threads, sharedMemory, stream>>>这种语法来调用一个内核函数时,需要指定内核的配置和执行的流。

       这里的0指的是sharedMemory参数。sharedMemory参数用于指定内核中每个线程块可以使用的共享内存的大小(以字节为单位)。

        例如,kernel<<<1, 64, 0, streams[i]>>>这行代码的意思是:

            1:你要启动的线程块的数量。

            64:每个线程块中的线程数量。

            0:每个线程块使用的共享内存大小是0字节。

            streams[i]:内核将在这个特定的CUDA流上执行。

        在这个例子中,0表示不使用额外的共享内存。如果你知道你的内核函数需要一些共享内存来存储数据,你可以将这个值设置为适当的值(以字节为单位)。但请注意,不是所有的CUDA设备都支持任意大小的共享内存,而且共享内存的使用也会受到硬件的限制。因此,在分配共享内存时,需要确保所选的值与你的硬件和内核函数的需求相匹配。

    7.  cudaMemsetAsync

        用于异步地将一个设备(GPU)内存区域设置为一个特定的值。与 cudaMemset 不同,cudaMemsetAsync 不会阻塞主机代码的执行,而是立即返回一个,并且内存设置操作会在后台进行。

        这个函数的原型如下:

            cudaError_t cudaMemsetAsync(void *devPtr, int value, size_t count, cudaStream_t stream);

        参数说明:

            devPtr:指向设备内存区域的指针。

            value:要设置的值,将被解释为 unsigned char。

            count:要设置的字节数。

            stream:CUDA流,指定内存设置操作应该在哪个流中执行。

        cudaMemsetAsync 通常用于初始化设备内存区域,将内存区域中的每个字节设置为 value。由于这是一个异步操作,主机代码可以继续执行其他任务,而内存设置操作在后台进行。这对于隐藏数据传输和内存初始化操作的延迟非常有用,从而提高程序的总体性能。

    8. cudaStreamSynchronize

        用于同步 CUDA 流。这意味着它会阻塞当前主机线程,直到指定的 CUDA 流中的所有先前排队的命令完成执行。这可以确保在继续执行后续操作之前,流中的所有操作都已经完成。

        函数的原型如下:

            cudaError_t cudaStreamSynchronize(cudaStream_t stream);

        其中 stream 是要同步的 CUDA 流的句柄。

        使用 cudaStreamSynchronize 可以确保在多个流之间或流与主机代码之间的正确同步。例如,如果你有一个流用于数据从主机到设备的传输,另一个流用于在设备上执行核函数,你可能需要在启动核函数之前同步数据传输流,以确保数据已经到达设备。

    9. cudaStreamDestroy

        用于销毁之前创建的 CUDA 流。CUDA 流是一系列命令的队列,它们按照添加到流的顺序执行。通过销毁流,你可以释放与其关联的所有资源。

        函数的原型如下:

            cudaError_t cudaStreamDestroy(cudaStream_t stream);

        其中 stream 是要销毁的 CUDA 流的句柄。

        当你不再需要一个流时,应当调用 cudaStreamDestroy 来销毁它。这通常是在你确信流中的所有命令都已经完成执行,并且不再需要该流进行任何进一步的同步或命令提交时进行的。

        请注意,在销毁流之前,必须确保流中的所有命令都已经完成。你可以使用 cudaStreamQuery 或 cudaStreamSynchronize 来检查或等待流的完成。如果试图销毁一个仍在执行命令的流,可能会导致未定义的行为。

        另外,销毁流并不会自动释放与该流关联的设备内存。你需要显式地使用 cudaFree 来释放设备内存。

    10. cudaFree

        用于释放之前通过cudaMalloc,cudaMallocManaged,cudaMallocPitch或cudaMalloc3D分配的设备内存。

       在使用CUDA进行GPU编程时,通常需要在设备(GPU)上分配内存以存储数据。这些内存分配是通过CUDA运行时API中的函数(如cudaMalloc)完成的。当不再需要这些内存分配时,应该使用cudaFree来释放它们,以防止内存泄漏和其他问题。调用cudaFree后,之前由相应的cudaMalloc调用分配的内存将变得无效,不应再被访问或引用1。

    11. cudaFreeHost

        用于释放通过 cudaMallocHost 或 cudaMallocManaged 在主机(CPU)上分配的内存。这些函数允许开发者在主机上分配内存,这些内存可以直接由 GPU 访问,这对于某些类型的内存操作(如某些内存密集型算法或数据预处理任务)可能很有用。

        cudaFreeHost 的函数原型如下:

            cudaError_t cudaFreeHost(void *ptr);

        其中 ptr 是指向要释放的内存块的指针。

        当你使用 cudaMallocHost 或 cudaMallocManaged 分配内存后,一旦你完成了对这些内存的使用,就应该调用 cudaFreeHost 来释放这些内存。这样做可以防止内存泄漏,确保你的程序能够正确地管理其内存资源。


微信小程序扫码登陆

文章评论

674人参与,0条评论