【问题标题】:How to implement handles for a CUDA driver API library?如何为 CUDA 驱动程序 API 库实现句柄?
【发布时间】:2018-07-18 11:06:53
【问题描述】:

注意:该问题已更新以解决 cmets 中提出的问题,并强调该问题的核心是关于 Runtime- 和 Driver API 之间的相互依赖关系

CUDA 运行时库(如 CUBLAS 或 CUFFT)通常使用“句柄”的概念来总结此类库的状态和上下文。使用模式很简单:

// Create a handle
cublasHandle_t handle;
cublasCreate(&handle);

// Call some functions, always passing in the handle as the first argument
cublasSscal(handle, ...);

// When done, destroy the handle
cublasDestroy(handle);

但是,关于这些句柄如何与驱动程序和运行时上下文以及多个线程和设备进行互操作,有许多微妙的细节。该文档列出了一些关于上下文处理的零散细节:

但是,有些信息似乎不是完全最新的(例如,我认为应该使用cuCtxSetCurrent 而不是cuCtxPushCurrentcuCtxPopCurrent?),其中一些似乎是以前的“主上下文”处理通过驱动程序 API 公开,有些部分过于简单化,因为它们只显示最简单的使用模式,只对多线程做出模糊或不完整的陈述,或者不能应用于“句柄”的概念在运行时库中使用。


我的目标是实现一个运行时库,它提供自己的“句柄”类型,并允许在上下文处理和线程安全方面与其他运行时库等效的使用模式。

如果库可以在内部单独使用 Runtime API 实现,事情可能很清楚:上下文管理完全由用户负责。如果他创建自己的驱动程序上下文,则将适用documentation about the Runtime- and Driver context management 中所述的规则。否则,运行时 API 函数将负责处理主要上下文。

但是,在某些情况下,库必须在内部使用驱动程序 API。例如,为了将 PTX 文件加载为CUmodule 对象,并从中获取CUfunction 对象。当库应该 - 对于用户 - 表现Runtime 库,但在内部必须使用 Driver API,就会出现一些关于如何必须“在后台”实现上下文处理。

到目前为止我所想出的都在这里勾勒出来。

(它是“伪代码”,因为它省略了错误检查和其他细节,并且......所有这些都应该在 Java 中实现,但这里不应该相关)

1.“句柄”基本上是一个包含以下信息的类/结构:

class Handle 
{
    CUcontext context;
    boolean usingPrimaryContext;
    CUdevice device;
}

2. 创建它时,必须涵盖两种情况: 它可以在调用线程的驱动程序上下文为当前时创建。在这种情况下,它应该使用这个上下文。否则,它应该使用当前(运行时)设备的主要上下文:

Handle createHandle()
{
    cuInit(0);

    // Obtain the current context
    CUcontext context;
    cuCtxGetCurrent(&context);

    CUdevice device;

    // If there is no context, use the primary context
    boolean usingPrimaryContext = false;
    if (context == nullptr)
    {
        usingPrimaryContext = true;

        // Obtain the device that is currently selected via the runtime API
        int deviceIndex;
        cudaGetDevice(&deviceIndex);

        // Obtain the device and its primary context
        cuDeviceGet(&device, deviceIndex);
        cuDevicePrimaryCtxRetain(&context, device));
        cuCtxSetCurrent(context);
    }
    else
    {
        cuCtxGetDevice(device);
    }

    // Create the actual handle. This might internally allocate
    // memory or do other things that are specific for the context
    // for which the handle is created
    Handle handle = new Handle(device, context, usingPrimaryContext);
    return handle;
}

3.当调用库的内核时,相关句柄的上下文对于调用线程来说是当前的:

void someLibraryFunction(Handle handle)
{
    cuCtxSetCurrent(handle.context);
    callMyKernel(...);
}

在这里,有人可能会争辩说,调用者负责确保所需的上下文是最新的。但是如果句柄是为 primary 上下文创建的,那么这个上下文将自动变为当前上下文。

4. 当句柄被销毁时,这意味着必须调用cuDevicePrimaryCtxRelease,但仅在上下文是主要上下文时:

void destroyHandle(Handle handle)
{
    if (handle.usingPrimaryContext)
    {
        cuDevicePrimaryCtxRelease(handle.device);
    }
}

从我目前的实验来看,这似乎展示了与 CUBLAS 句柄相同的行为,例如。但是我彻底测试的可能性是有限的,因为我只有一个设备,因此无法测试关键情况,例如具有两个上下文,一个用于两个设备中的每一个。

所以我的问题是:

  • 是否有任何既定的模式来实现这样的“句柄”?
  • 是否有任何使用模式(例如,使用多个设备和每个设备一个上下文)无法涵盖在上面概述的方法中,但会涵盖在“句柄”实现中CUBLAS?
  • 更笼统地说:对于如何改进当前的“Handle”实施有什么建议吗?
  • 修辞:CUBLAS 句柄处理的源代码是否在某处可用?

(我还查看了context handling in tensorflow,但我不确定是否可以从中得出有关如何为运行时库实现句柄的建议...)

(此处删除了“更新”,因为它是为响应 cmets 而添加的,应该不再相关)

【问题讨论】:

  • 我相信这个问题背后的整个前提是不正确的。 AFAIK,cublas(有问题的示例库)是一个完全简单的运行时 API 库,它完全依赖于标准运行时 API 惰性上下文管理行为。它本身从不进行任何显式的上下文管理,不尝试与驱动程序 API 的互操作性相关的任何事情,并且句柄不包含任何上下文信息。见pastebin.com/8zHi1WgV
  • 我不相信你的解释是正确的。不要将默认运行时 API 的功能与 cublas 中内置的功能相混淆。每当具有活动 CUDA 上下文的线程初始化运行时 API 时,运行时 API 都会绑定到该上下文。我链接到的内容表明,所有 cublas 都会在 init 调用的上下文中分配内存。如果两个线程启动驱动程序 API 上下文,然后初始化 cublas,则生成的句柄将不同,因为它们存在于不同的上下文中
  • 所以你的第二条评论更接近目标。所有这些只是默认的运行时 API 行为,库中没有内置一些复杂的逻辑
  • 这是正确的设计模式吗?绝对不。任何运行时 API 库的正确设计是本身不执行显式上下文管理。曾经。这就是所有“参考”NVIDIA 库的工作方式,也是您的工作方式。默认的运行时 API 行为(及其内置的线程安全)涵盖了 95% 的典型用例,另外 5% 是程序员选择通过驱动程序 API 自己管理上下文的地方。在这种情况下,必须正确管理事物的是程序员,而不是库
  • 不,图书馆根本不需要了解上下文。这很简单——如果有上下文,运行时 API 会自动绑定到它。如果没有,它将创建一个主上下文,其他线程可以通过相同的 API 绑定到该上下文(这在运行时 API 中自动发生)。默认情况下,在上下文中创建的任何内容都不能移植到其他上下文中。如果您通过驱动程序 API 显式创建两个上下文,则这两个上下文中的 anything 不能在它们之间移植。这包括在这些上下文中通过运行时 API 发生的任何事情

标签: cuda api-design jcuda cuda-context


【解决方案1】:

很抱歉我没有早点注意到这个问题——因为我们可能在这方面进行了一些合作。另外,我不太清楚这个问题是属于这里、codereview.SX 还是programmers.SX,但让我们忽略所有这些。

我现在已经完成了您的目标,而且可能更普遍。因此,我可以提供如何处理“句柄”的示例,此外,还可以提出完全不必实现此功能的前景。

该库是cuda-api-wrappers 的扩展,还涵盖了驱动程序 API 和 NVRTC;它还不是发布级的,但它处于测试阶段,在this branch

现在,回答您的具体问题:

围绕原始“句柄”编写类的模式

是否有任何既定的模式来实现这样的“句柄”?

是的。如果您阅读:

What is the difference between: Handle, Pointer and Reference

您会注意到句柄被定义为“对对象的不透明引用”。它与指针有一些相似之处。因此,一个相关的模式是PIMPL idiom 的一种变体:在常规 PIMPL 中,您编写一个实现类,而面向外的类只保存一个指向实现类的指针并将方法调用转发给它。当您在某些第三方库或驱动程序中拥有不透明对象的不透明句柄时,您可以使用该句柄将方法调用转发到该实现。

这意味着,您的面向外的类不是句柄,它表示您拥有句柄的对象。

通用性和灵活性

是否有任何使用模式(例如,使用多个设备和每个设备一个上下文)无法被上面概述的方法覆盖,但会被 CUBLAS 的“句柄”实现覆盖?

我不确定 CUBLAS 到底做了什么(老实说,我几乎从未使用过 CUBLAS),但如果它设计和实施得当,它会 创建自己的上下文,并尽量不影响您的其余代码,即它总是会这样做:

  1. 将我们的 CUBLAS 上下文推送到栈顶
  2. 做实际工作
  3. 弹出上下文堆栈的顶部。

你的班级没有这样做。

更笼统地说:对于如何改进当前的“Handle”实现有什么建议吗?

是的:

  • 在可能和相关的情况下使用 RAII。如果您的创建代码分配了资源(例如,通过 CUDA 驱动程序) - 您返回的对象的析构函数应该安全地释放这些资源。
  • 允许引用类型和值类型的句柄使用,即它可能是我创建的句柄,但也可能是我从其他地方获得的句柄,不是我的责任。如果你让用户来释放资源,这很简单,但如果你承担这个责任,那就有点棘手了
  • 您假设如果有任何当前上下文,那就是您的句柄需要使用的上下文。谁说的?至少,如果用户愿意,让他们传入上下文。
  • 除非确实必须,否则请避免自行编写此代码的低级部分。您很可能会遗漏一些东西(推送并不是您可能遗漏的唯一东西),并且您正在重复许多实际上是通用的而不是特定于您的应用程序或库的工作。我在这里可能有偏见,但是您现在可以为 CUDA 上下文、流、模块、设备等使用漂亮的 RAII-ish 包装器,甚至不知道任何东西的原始句柄。

修辞:CUBLAS 句柄处理的源代码是否在某处可用?

据我所知,NVIDIA 尚未发布它。

【讨论】:

  • 感谢您的回答。我可能需要一些时间来仔细看看(也在实际的 repo 中),当我问它时,我会重新回忆我为此所做的所有实验。我不太可能有一些关键问题,主要与句柄+线程+上下文的组合有关(大致如下:您的句柄可以“透明地”从不同线程中使用吗?他们将使用哪些上下文?(注意:这CUBLAS 句柄似乎也不可能,但例如cuCtxSetCurrent 根本没有在您的回购中使用的事实让我有点困惑......))
  • "你的句柄可以“透明地”从不同的线程中使用吗?"是的。 “他们将使用哪些上下文?” - 无论您让他们使用哪种上下文。特定于上下文的包装器在创建时获取上下文参数,通常使用 push-if-necessary 和 pop-if-we-pushed。但是,如果您在没有指定或设置某些特殊上下文的情况下使用严格运行时 API 实体,则会模仿运行时 API 行为,即通常使用某些设备的主要上下文。
  • 我给你写了一封邮件(希望没问题)——cmets 可能不是整理细节的最佳场所,正如我所提到的,我可能需要一些时间来赶上这里。
猜你喜欢
  • 1970-01-01
  • 2010-09-19
  • 2019-02-01
  • 2020-05-24
  • 2015-03-05
  • 2022-01-12
  • 2011-10-29
  • 2013-04-07
  • 1970-01-01
相关资源
最近更新 更多