• CUDA运行时 Runtime(三)


    CUDA运行时 Runtime

    一.异步并发执行             

    CUDA将以下操作公开为可以彼此并发操作的独立任务:             

    主机计算;             

    设备计算;             

    从主机到设备的内存传输;              

    从设备到主机的存储器传输;             

    在给定设备的存储器内的存储器传输;             

    设备之间的内存传输。             

    这些操作之间实现的并发级别将取决于设备的功能集和计算能力,如下所述。             

    .  主机和设备之间的并发执行             

    在设备完成请求的任务之前,通过异步库函数将控制权返回给主机线程,可以促进并发主机执行。使用异步调用,当适当的设备资源可用时,许多设备操作可以一起排队,由CUDA驱动程序执行。这减轻了主机线程管理设备的大部分责任,让它可以自由地执行其他任务。以下设备操作相对于主机是异步的:             

    内核启动;             

    单个设备内存中的内存副本;             

    64 KB或更小的内存块从主机到设备的内存拷贝;             

    由以Async为后缀的函数执行的内存拷贝;             

    内存设置函数调用。             

    程序员可以通过将CUDA启动阻塞环境变量设置为1,全局禁用系统上运行的所有CUDA应用程序的内核启动异步性。此功能仅用于调试目的,不应用作使生产软件可靠运行的方法。              如果通过探查器(Nsight,Visual profiler)收集硬件计数器,则内核启动是同步的,除非启用了并发内核评测。如果异步内存副本涉及未页锁定的主机内存,则它们也将是同步的。             

    .  并发内核执行             

    一些计算能力为2.x及更高的设备可以同时执行多个内核。应用程序可以通过检查concurrentKernels设备属性(请参阅设备枚举)来查询此功能,对于支持此功能的设备,该属性等于1。             

    设备可以并发执行的最大内核启动次数取决于其计算能力,如表15所示。             

    一个CUDA上下文中的内核不能与另一个CUDA上下文中的内核同时执行。             

    使用许多纹理或大量本地内存的内核不太可能与其他内核同时执行。

    .  数据传输与内核执行的重叠             

    有些设备可以在内核执行的同时执行到GPU或从GPU执行的异步内存复制。应用程序可以通过检查asyncEngineCount设备属性(请参阅设备枚举)来查询此功能,对于支持此功能的设备,该属性大于零。如果复制涉及主机内存,则必须将其页锁定。             

    还可以在内核执行(在支持concurrentKernels设备属性的设备上)和/或与设备之间的副本(对于支持asyncEngineCount属性的设备)同时执行设备内复制。使用标准内存复制功能启动设备内复制,目标和源地址位于同一设备上。             

    .  并发数据传输             

    一些计算能力为2.x及更高的设备可以在设备之间重叠拷贝。应用程序可以通过检查asyncEngineCount设备属性(请参阅设备枚举)来查询此功能,对于支持此功能的设备,该属性等于2。为了重叠,传输中涉及的任何主机内存都必须被页锁定。             
    .  线程流             

    应用程序通过流管理上述并发操作。流是按顺序执行的命令序列(可能由不同的主机线程发出)。另一方面,不同的流可能会执行它们的命令,彼此不按顺序执行,或者同时执行;这种行为没有保证,因此不应依赖于正确性(例如,内核间通信未定义)。流上发出的命令可以在满足命令的所有依赖项时执行。依赖项可以是以前在同一流上启动的命令,也可以是来自其他流的依赖项。同步调用的成功完成保证所有启动的命令都已完成。             

    .  创造与销毁             

    流是通过创建一个流对象并将其指定为内核启动序列和主机<->设备内存副本的流参数来定义的。下面的代码示例创建两个流并在页锁定内存中分配float的数组hostPtr。

    cudaStream_t stream[2];

    for (int i = 0; i < 2; ++i)

    cudaStreamCreate(&stream[i]);

    float* hostPtr;

    cudaMallocHost(&hostPtr, 2 * size);

    以下代码示例将这些流中的每个流定义为一个从主机到设备的内存副本、一个内核启动和一个从设备到主机的内存副本的序列:

    for (int i = 0; i < 2; ++i)

    {

    cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size, size, cudaMemcpyHostToDevice, stream[i]);

    MyKernel <<<100, 512, 0, stream[i]>>>(outputDevPtr + i * size, inputDevPtr + i * size, size);

     cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size, size, cudaMemcpyDeviceToHost, stream[i]);

    }

    每个流将其输入数组hostPtr的部分复制到设备内存中的数组inputDevPtr,通过调用MyKernel()在设备上处理inputDevPtr,并将结果outputDevPtr复制回hostPtr的相同部分。重叠行为描述了在本例中,根据设备的能力,流如何重叠。请注意,hostPtr必须指向页锁定的主机内存,否则将发生任何重叠。             

    通过调用cudaStreamDestroy()释放流。

    for (int i = 0; i < 2; ++i)

    cudaStreamDestroy(stream[i]);

    如果在调用cudaStreamDestroy()时设备仍在流中工作,则函数将立即返回,并且在设备完成流中的所有工作后,与流关联的资源将自动释放。             

    八. 默认流             

    内核启动和主机<->未指定任何流参数或等效于将流参数设置为零的设备内存副本将被发送到默认流。因此,它们是按顺序执行的。             

    对于使用--default stream per thread编译标志编译的代码(或在包含CUDA头(CUDA.h和CUDA_runtime.h)之前定义CUDA API_per_thread_default_stream宏的代码),默认流是常规流,每个主机线程都有自己的默认流。             

    注意:#define CUDA_API_PER_THREAD_DEFAULT_STREAM 1不能用于在nvcc编译代码时启用此行为,因为nvcc在转换单元的顶部隐式包含CUDA_runtime.h。在这种情况下,需要使用--default stream per thread编译标志,或者需要使用

    -DCUDA_API_per_thread_default_stream=1编译器标志定义

    CUDA_API_per_thread_default_stream宏。            

    对于使用--default stream legacy compilation标志编译的代码,默认流是一个称为空流的特殊流,每个设备都有一个用于所有主机线程的空流。空流是特殊的,因为它导致隐式同步,如隐式同步中所述。             

    对于未指定--default stream compilation标志而编译的代码,假定-default stream legacy为默认值。             

    九.显式同步             

    有多种方法可以显式地同步流。             

    cudaDeviceSynchronize()等待,直到所有主机线程的所有流中的所有前面的命令都完成。             

    cudaStreamSynchronize()接受流作为参数,并等待给定流中所有前面的命令完成。它可用于将主机与特定流同步,从而允许其他流在设备上继续执行。              cudaStreamWaitEvent()接受一个流和一个事件作为参数(有关事件的描述,请参见事件),并使调用cudaStreamWaitEvent()后添加到给定流的所有命令延迟执行,直到给定事件完成。             

    cudaStreamQuery()为应用程序提供了一种方法,可以知道流中所有前面的命令是否都已完成。             

    十.隐式同步             

    如果主机线程在不同流之间发出以下任一操作,则来自不同流的两个命令不能同时运行:

    页锁定的主机内存分配,              

    设备内存分配,             

    一个设备内存设置,             

    两个地址之间到同一设备存储器的存储器副本,             

    任何对空流的CUDA命令,             

    在计算能力3.x和计算能力7.x中描述的L1/共享内存配置之间的切换。             

    对于支持并发内核执行且具有计算能力3.0或更低版本的设备,任何需要依赖性检查以查看流式内核启动是否完成的操作:             

    只有当从CUDA上下文中的任何流启动的所有先前内核的所有线程块都已开始执行时,才能开始执行;             

    阻止以后从CUDA上下文中的任何流启动所有内核,直到选中的内核启动完成。             

    需要依赖项检查的操作包括与要检查的启动和对该流上cudaStreamQuery()的任何调用相同的流中的任何其他命令。因此,应用程序应遵循以下准则,以提高并发内核执行的潜力:              所有独立操作应在从属操作之前发布,任何类型的同步都应尽量延迟。             

    十一.重叠行为             

    两个流之间的执行重叠量取决于向每个流发出命令的顺序,以及设备是否支持数据传输和内核执行重叠(请参阅数据传输和内核执行重叠)、并发内核执行重叠(请参阅并发内核执行),和/或并发数据传输(参见并发数据传输)。             

    例如,在不支持并发数据传输的设备上,创建和销毁代码样本的两个流根本不重叠,因为从主机到设备的内存副本在从设备到主机的内存副本颁发给流[0]之后,会将从主机到设备的内存副本颁发给流[1],因此,它只能在从设备到主机的内存复制完成后启动。如果代码按以下方式重写(并且假设设备支持数据传输和内核执行的重叠)

    for (int i = 0; i < 2; ++i)

    cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size, size, cudaMemcpyHostToDevice, stream[i]);

    for (int i = 0; i < 2; ++i)

    MyKernel<<<100, 512, 0, stream[i]>>>(outputDevPtr + i * size, inputDevPtr + i * size, size);

    for (int i = 0; i < 2; ++i)

    cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size, size, cudaMemcpyDeviceToHost, stream[i]);

    然后,向流[1]发出的从主机到设备的内存复制与向流[0]发出的内核启动重叠。             

    在支持并发数据传输的设备上,创建和销毁的代码示例的两个流确实重叠:从主机到设备的内存副本发出到流[1]与从设备到主机的内存副本发出到流[0]重叠,甚至与内核启动发出到流[0]重叠(假设设备支持数据传输和内核执行重叠)。但是,对于计算能力为3.0或更低的设备,内核执行不可能重叠,因为在将设备到主机的内存副本颁发给流[0]之后,第二次内核启动将颁发给流[1],因此它将被阻止,直到根据隐式同步完成颁发给流[0]的第一次内核启动。如果如上所述重写代码,则内核执行重叠(假设设备支持并发内核执行),因为在将设备到主机的内存副本颁发给流[0]之前,第二次内核启动被颁发给流[1]。然而,在这种情况下,根据隐式同步,从设备到主机的内存拷贝(发给流[0])只与发给流[1]的内核启动的最后一个线程块重叠,后者只能表示内核总执行时间的一小部分。             

    十二.主机函数(回调)             

    运行时提供了一种通过cudaLaunchHostFunc()将CPU函数调用插入流中的方法。一旦在回调完成之前向流发出所有命令,则在主机上执行提供的函数。             

    下面的代码示例在将主机到设备的内存副本、内核启动和设备到主机的内存副本发送到每个流之后,将主机函数MyCallback添加到两个流中的每个流中。此函数将在每个设备到主机的内存复制完成后开始在主机上执行。

    void
    CUDART_CB MyCallback(cudaStream_t stream, cudaError_t status, void *data)

    {

    printf("Inside callback %d ", (size_t)data);

    }

    ...

    for (size_t i = 0; i < 2; ++i)

    {

    cudaMemcpyAsync(devPtrIn[i], hostPtr[i], size, cudaMemcpyHostToDevice, stream[i]);

    MyKernel<<<100, 512, 0, stream[i]>>>(devPtrOut[i], devPtrIn[i], size);

    cudaMemcpyAsync(hostPtr[i], devPtrOut[i], size, cudaMemcpyDeviceToHost, stream[i]);

    cudaLaunchHostFunc(stream[i], MyCallback, (void*)i);

    }

    在宿主函数之后在流中发出的命令在函数完成之前不会开始执行。             

    列队到流中的主机函数不能(直接或间接)进行CUDAAPI调用,因为如果它进行这样的调用而导致死锁,它可能会自己等待。             

    十三.流优先级             

    流的相对优先级可以在创建时使用cudaStreamCreateWithPriority()指定。可以使用cudaDeviceGetStreamPriorityRange()函数获得允许的优先级范围,顺序为[最高优先级,最低优先级]。在运行时,高优先级流中的挂起工作优先于低优先级流中的挂起工作。             

    下面的代码示例获取当前设备允许的优先级范围,并创建具有最高和最低可用优先级的流。

    // get the range of stream priorities for this device

    int priority_high, priority_low;

    cudaDeviceGetStreamPriorityRange(&priority_low, &priority_high);

    // create streams with highest and lowest available priorities

    cudaStream_t st_high, st_low;

    cudaStreamCreateWithPriority(&st_high, cudaStreamNonBlocking, priority_high);

    cudaStreamCreateWithPriority(&st_low, cudaStreamNonBlocking, priority_low);

  • 相关阅读:
    IOSUITextField类
    IOSUITableView设置背景图片,方式与众不同。
    IOS图标知识详情(转载自http://luoyl.info/blog/2012/03/iphoneipadicons/)
    IOSCreate UIActionSheet 'otherButtons' by passing in array
    Offset文件名(绝对偏移量)
    单例模式(Singleton)Holder
    在 spring 中一共定义了七种事务传播属性
    UML,各种关系合集
    Java,线程池,ThreadPoolExecutor
    EasyMock
  • 原文地址:https://www.cnblogs.com/wujianming-110117/p/13048288.html
Copyright © 2020-2023  润新知