3.3_上下文

3.3 上下文

上下文类似于CPU中的进程。除了少数例外,它是管理CUDA程序中所有对象生命周期的容器,包括如下部分:

·所有内存分配(包括线性设备内存、主机内存、CUDA数组)
·模块
·CUDA流
· CUDA事件
·纹理与表面引用
·使用本地内存的内核的设备内存
·进行调试、分析、同步操作时,所使用的内部资源
·换页内存复制所使用的锁定中转缓冲区

CUDA运行时不提供对CUDA上下文的直接访问,它通过延迟初始化(deferred initialization)来创建上下文。每一个CUDART库调用和内核调用都会检查上下文是否是当前使用的,如果必要的话,创建CUDA上下文(使用之前用

CUDASetDevice(),CUDASetDeviceFlags(),CUDAGLSetGLDevice()等函数设置的状态)。

许多应用程序倾向于控制初始化延迟时间。为了让CUDART进行没有任何负面效果的初始化,调用

cudafree(0);

CUDA运行时应用程序可以通过驱动程序API访问当前上下文栈(后文描述)。

对驱动程序API中的每个指定上下文状态的函数,CUDA运行时把上下文和设备同等对待。对于驱动程序API函数cuCtxSynchronize(),CUDA运行时用CUDADeviceSynchronize()取而代之;对驱动程序API函数cuCtxSetCacheConfig(),CUDA运行时中则有CUDADeviceSetCacheConfig()对应。

当前上下文

为了代替当前上下文栈,CUDA运行时提供CUDASetDevice()函数,为调用的线程设置当前上下文。一个设备可以是多个CPU线程的当前上下文 [1]。

3.3.1 生命周期与作用域

所有与CUDA上下文相关的分配资源都在上下文被销毁的同时被销毁,除了少数例外,给定CUDA上下文创建的资源可能不能被其他的CUDA上下文使用,这一限制不仅适用于内存,也同样适用于CUDA流与CUDA事件等对象。

3.3.2 资源预分配

CUDA尽力去避免“懒惰分配”(lazy allocation)。懒惰分配只分配需要的资源以避免因缺少资源导致的失败操作。例如,换页内存复制不会因内存不足而失败,因为机器通过分配锁页中转缓冲区以执行换页内存复制,而这个操作发生在上下文创建时。如果CUDA不能分配这些缓冲区,上下文创建就失败了。

在少量情况下,CUDA不会预分配一个给定操作所需的全部资源。内核启动所需的本地内存数量可能被限制,因此CUDA不会预分配最大理论数量的内存。所以,当它需要比默认分配给CUDA上下文的内存值更多的本地内存时,内核启动可能失败。

3.3.3 地址空间

除了在上下文销毁时被自动销毁的(清理)对象外,另一个上下文中的重要抽象是地址空间:一组私有的虚拟内存地址,它可以分配

线性设备内存或用以映射锁页主机内存。这些地址每一个上下文都不相同。同一地址对不同上下文可能有效也可能无效,当然也不会解析到相同的地址空间除非有特殊规定。CUDA上下文的地址空间是与CUDA主机代码使用的CPU地址空间独立的。事实上,不同于共享内存的多CPU,多GPU中的CUDA上下文并不共享一个内存空间。当系统使用统一虚拟地址时,CPU和GPU共享相同的地址空间,其中的每个分配都有进程中唯一的地址。但在特殊情况下,CPU和GPU只能读写各自的内存区,像映射的锁页内存(参见5.1.3小节)或点对点内存(参见9.2.2小节)。

3.3.4 当前上下文栈

大多数CUDA入口点并不含有上下文参数。取而代之的,它们在CPU线程的“当前上下文”上进行操作,它存储在线程的本地存储句柄里。在驱动程序API中,每一个CPU线程有一个当前上下文栈,创建一个上下文的同时将这个新上下文压入栈。

当前上下文栈有3个主要功能:

·单线程应用程序可以驱动多个GPU上下文。

·库可以创建并管理它们自己上下文,而不需要干涉调用者的上下文。

·库不知晓调用它的CPU线程信息。

为CUDA建立当前上下文栈的最初动机是使单线程CUDA应用程序可以驱动多个CUDA上下文。在创建并初始化每个CUDA上下文后,程序可以将其中的某个上下文从栈弹出,使其变成一个飘浮上下文。由于对一个CPU线程,只有一个当前上下文,所以一个单线程CUDA应用程序可以在栈中依次压入和弹出上下文,使其在任何时间点上,只有一个飘浮上下文,实现对多个上下文的驱动。

在多数的驱动程序架构中,压入和弹出上下文的成本足够廉价,所以一个单线程应用程序可以保持多个GPU同时忙碌。对只在Windows Vista和其后续版本中的WDDM驱动程序,弹出当前上下文操作只在没有GPU命令挂起的时候才能快速运行。当有命令挂起,驱动程序会引发内核转换,以保证在弹出CUDA上下文之前提交挂起的命令 [2]。

当前上下文栈的另一个优势是驱动来自多个CPU线程的CUDA上下文的能力。使用驱动程序API的应用程序可以传递CUDA上下文到其他CPU线程中:使用cuCtxPopCurrent()函数弹出上下文,再使用cuCtxPushCurrent()从另一个线程中压入上下文。库可以使用这个功能创建CUDA上下文,而不需要了解或干涉它们的调用者。例如,一个CUDA插件库可以在初始化时创建它自己的CUDA上下文,然后弹出并保持其“漂浮”,除非被主程序调用才会中断。漂浮上下文使库完全对哪一个CPU线程调用不知情。当然这种使用方式也是有利有弊:一方

面,漂浮上下文内存不会被第三方CUDA内核的恶意写入污染;另一方面,库只能够在分配的CUDA资源上进行操作。

附加和分离上下文

在CUDA 4.0之前,每一个CUDA上下文都有一个“使用计数”,上下文被创建时设置为1。cuCtxAttach()和cuCtxDetach()函数分别为使用计数加1或减1 [3]。采用“使用记数”的目的是为了使应用程序创建的上下文与链接到的库联系起来,这样程序就可以通过自己创建的CUDA上下文与库相互操作了 [4]。

当CUDART第一次调用时,如果一个上下文已经处于当前状态,程序会把上下文附加到CUDART上,而不是新建一个上下文。CUDA运行时无法访问上下文的“使用记数”。而在CUDA 4.0之后,使用计数已被弃用,使用cuCtxAttach()/cuCtxDetach()函数没有任何作用。

3.3.5 上下文状态

类似于CPU中的malloc()和printf()函数,cuCtxSetLimit()和cuCtxGetLimit()函数可以设置和获取内核函数中对应函数的GPU空间上限。cuCtxSetCacheConfig()函数在启动内核时指定所期望的缓存配置(是否分别分配16kb和48kb给共享内存和一级缓存)。这暗示我

们,任何需要16Kb以上共享内存空间的内核需要分配48Kb的共享内存。除此之外,上下文状态可以被内核特定的状态重写

(cuFuncSetCacheConfig())。这些状态存在于整个上下文范围(换句话讲,它们并不是特定于每一次内核启动的),它们改动的代价十分昂贵。

[1] 早期版本的CUDA禁止上下文同时成为多个线程的当前状态,因为驱动程序并不是线程安全的。现在驱动程序实现了所需的同步——即使在应用程序调用同步函数,例如CUDADeviceSynchronize()的情况下。
[2] 这一花销不仅仅局限在驱动程序API或当前上下文栈上,当有命令挂起时,调用CUDASetDevice()来切换设备同样会引发一次WDDM上的内核转换。
[3] 直到CUDA 2.2中添加了cuCtxDestroy(), 在此之前上下文通过调用 cuCtxDetch()分离。
[4]回头来看,英伟达公司把引用记数放在比驱动程序API更高的软件层会是更明智的选择。

3.3_上下文 - The CUDA Handbook | OpenTech