2. 简介

本文档描述了 CUDA 库中可用于任何调试器的例程和数据结构的 API。

从 3.0 开始,CUDA 调试器 API 包含几个重大更改,其中只有少数对最终用户是直接可见的
  • 性能得到了极大的提升,无论是在与调试器交互方面,还是在被调试应用程序的性能方面。

  • cubin 的格式已更改为 ELF,因此,大多数对调试编译的限制已被解除。下面包含了关于新对象格式的更多信息。

debugger API 已经发生了显著的变化,这反映在 CUDA-GDB 源代码中。

2.1. Debugger API

CUDA Debugger API 的开发目标是遵循以下原则

  • 策略自由

  • 显式

  • 公理化

  • 可扩展

  • 面向机器

显式是另一种说法,即我们尽量减少我们所做的假设。API 尽可能地反映机器状态,而不是内部状态。

设备有两种主要的“模式”:停止或运行。我们使用 suspendDevice 和 resumeDevice 显式地在这些模式之间切换,尽管机器可能会自行暂停,例如在命中断点时。

只有在停止时,我们才能查询机器的状态。 Warp 状态包括它正在运行哪个函数,哪个块,哪些通道是有效的等等。

从 CUDA 6.0 开始,如果未先调用 suspendDevice 入口点以确保设备已停止,则调试 API 中的状态收集函数将返回 CUDBG_ERROR_RUNNING_DEVICE。

调试 API 的客户端应在处理 CUDBGEvent 之前暂停所有设备。只有在使用 CUDBGAPI_st::setNotifyNewEventCallback() 设置通知回调后,才能保证返回有效的 CUDBGEvent。当从使用 CUDBGAPI_st::setNotifyNewEventCallback() 设置的通知回调内部调用任何调试 API 入口点时,它将返回 CUDBG_ERROR_RECURSIVE_API_CALL。

2.2. ELF 和 DWARF

CUDA 应用程序以 ELF 二进制格式编译。

从 CUDA 6.0 开始,DWARF 设备信息通过 CUDBGAPI_st::getElfImageByHandle 的 API 调用获得,该调用使用从 CUDBG_EVENT_ELF_IMAGE_LOADED 类型的 CUDBGEvent 公开的句柄。这意味着信息在运行时才可用,在 CUDA 驱动程序加载之后。DWARF 设备信息的生命周期在卸载之前都是有效的,这会呈现 CUDBG_EVENT_ELF_IMAGE_UNLOADED 类型的 CUDBGEvent

在 CUDA 5.5 及更早版本中,DWARF 设备信息作为 CUDBG_EVENT_ELF_IMAGE_LOADED 类型的 CUDBGEvent 的一部分返回。CUDBGEvent55 中提供的指针是指向由调试 API 管理的内存的只读指针。指向的内存隐式地限定于加载 CUDA 上下文的生命周期。在上下文被销毁后访问返回的指针会导致未定义的行为。

DWARF 设备信息包含除代码内存之外的所有设备内存区域的物理地址。地址类别字段 (DW_AT_address_class) 为所有设备变量设置,并用于指示内存段类型 (ptxStorageKind)。物理地址必须使用几个特定于段的 API 调用来访问。

对于内存读取,请参阅对于内存写入,请参阅访问代码内存需要虚拟地址。此虚拟地址嵌入在设备 ELF 映像中所有设备代码段中。请参阅 API 调用这是一个典型的 DWARF 条目,用于位于内存中的设备变量
<2><321>: Abbrev Number: 18 (DW_TAG_formal_parameter)
     DW_AT_decl_file   : 27
     DW_AT_decl_line   : 5
     DW_AT_name        : res
     DW_AT_type        : <2c6>
     DW_AT_location    : 9 byte block: 3 18 0 0 0 0 0 0 0       (DW_OP_addr: 18)
     DW_AT_address_class: 7

上面显示变量 'res' 的地址类别为 7 (ptxParamStorage)。其位置信息显示它位于参数内存段内的地址 18。

默认情况下,局部变量不再溢出到本地内存。DWARF 现在包含所有变量的变量到寄存器映射和活跃度信息。变量可能会溢出到本地内存,而这一切都包含在 DWARF 信息中,该信息是 ULEB128 编码的(作为 DW_AT_location 属性中的 DW_OP_regx 堆栈操作)。

这是一个典型的 DWARF 条目,用于位于本地寄存器中的变量

<3><359>: Abbrev Number: 20 (DW_TAG_variable)
     DW_AT_decl_file   : 27
     DW_AT_decl_line   : 7
     DW_AT_name        : c
     DW_AT_type        : <1aa>
     DW_AT_location    : 7 byte block: 90 b9 e2 90 b3 d6 4      (DW_OP_regx: 160631632185)
     DW_AT_address_class: 2

这显示变量 'c' 的地址类别为 2 (ptxRegStorage),其位置可以通过解码 ULEB128 值 DW_OP_regx: 160631632185 找到。有关解码此值以及如何在特定设备 PC 范围内获得哪个物理寄存器保存此变量的信息,请参阅 cuda-gdb 源代码 drop 中的 cuda-tdep.c。

访问物理寄存器活跃度信息需要基于 0 的物理 PC。请参阅 API 调用

2.3. ABI 支持

ABI 支持通过以下线程 API 调用处理返回地址在本地堆栈上不可访问,必须使用 API 调用来访问其值。

有关更多信息,请参阅标题为“Fermi ABI: Application Binary Interface”的 ABI 文档。

2.4. 异常报告

一些内核异常作为设备事件报告,可以通过 API 调用访问报告的异常在 CUDBGException_t 枚举类型中列出。每个前缀(Device、Warp、Lane)都指的是异常的精度。也就是说,负责异常起源的最低已知执行单元。所有通道错误都是精确的;导致错误的精确指令和通道是已知的。 Warp 错误通常发生在实际错误发生位置的几个指令范围内,但 Warp 内的精确通道是未知的。在设备错误上,我们 可能 知道导致它的 内核。有关每种异常类型的说明,请参见结构文档。

异常报告仅在 Fermi (sm_20 或更高版本) 上受支持。

2.5. 附加和分离

调试客户端必须采取以下步骤来附加到正在运行的 CUDA 应用程序

  1. 附加到与 CUDA 应用程序对应的 CPU 进程。应用程序的 CPU 部分此时将被冻结。

  2. 检查 CUDBG_IPC_FLAG_NAME 变量是否可以从应用程序的内存空间访问。如果不能,则意味着应用程序尚未加载 CUDA 驱动程序,并且附加到应用程序已完成。

  3. 对函数 cudbgApiInit() 进行动态(下级)函数调用,参数为 "2",即 "cudbgApiInit(2)",例如使用 Linux 上的 ptrace(2)。这会导致从应用程序中 fork 出一个辅助进程,该进程有助于附加到 CUDA 进程。

  4. 确保 CUDA 调试 API 的初始化已完成,或等到 API 初始化成功(即,调用 "initialize()" API 方法直到成功)。

  5. 调用 "initializeAttachStub()" API 方法以初始化先前从应用程序 fork 出的辅助进程。

  6. 从应用程序的内存空间读取 CUDBG_RESUME_FOR_ATTACH_DETACH 变量的值

    • 如果该值为非零值,则恢复 CUDA 应用程序,以便可以收集有关应用程序的更多数据并将其发送到调试器。当应用程序恢复时,调试客户端可以期望从 CUDA 应用程序接收各种 CUDA 事件。收集完所有状态后,调试客户端将收到事件 CUDBG_EVENT_ATTACH_COMPLETE。

    • 如果该值为零,则没有更多要收集的附加数据。将应用程序进程空间中的 CUDBG_IPC_FLAG_NAME 变量设置为 1,这将启用来自 CUDA 应用程序的更多事件。

  7. 此时,附加到 CUDA 应用程序已完成,并且属于 CUDA 应用程序的所有 GPU 都将被暂停。

调试客户端必须采取以下步骤才能从正在运行的 CUDA 应用程序中分离

  1. 检查 CUDBG_IPC_FLAG_NAME 变量是否可以从应用程序的内存空间访问,以及 CUDA 调试 API 是否已初始化。如果这些条件中的任何一个不满足,则将应用程序视为仅 CPU 应用程序并从应用程序中分离。

  2. 接下来,调用 "clearAttachState" API 方法以准备 CUDA 调试 API 进行分离。

  3. 在应用程序的内存空间中对函数 cudbgApiDetach() 进行动态(下级)函数调用,例如使用 Linux 上的 ptrace(2)。这会导致 CUDA 驱动程序设置分离状态。

  4. 从应用程序的内存空间读取 CUDBG_RESUME_FOR_ATTACH_DETACH 变量的值。如果该值为非零值,则调用 "requestCleanupOnDetach" API 方法。

  5. 将应用程序内存空间中的 CUDBG_DEBUGGER_INITIALIZED 变量设置为 0。这确保了如果调试客户端在将来重新附加到应用程序,调试器将从头开始重新初始化。

  6. 如果在步骤 4 中发现 CUDBG_RESUME_FOR_ATTACH_DETACH 变量的值为非零值,则删除所有断点并恢复 CUDA 应用程序。这允许 CUDA 驱动程序在调试客户端从中分离之前执行清理。清理完成后,调试客户端将收到事件 CUDBG_EVENT_DETACH_COMPLETE。

  7. 将应用程序内存空间中的 CUDBG_IPC_FLAG_NAME 变量设置为零。这可以防止 CUDA 应用程序向调试器发送更多回调。

  8. 然后,客户端必须完成 CUDA 调试 API。

  9. 最后,从 CUDA 应用程序的 CPU 部分分离。此时,属于 CUDA 应用程序的所有 GPU 都将恢复。