← 文章 / 编程开发
Meta 工程博客 12小时前 · 2026-11-10 01:41:57 · 6 阅读

扩展 LLDB 以调试跨 CPU 和 GPU 的应用

  • 该工具用于调试跨 CPU 和 GPU 运行的应用程序。它将共享的调试器基础设施与特定硬件的设备控制逻辑解耦。
  • 设计上,CPU 和 GPU 被视为独立的调试器进程,但在同一个调试会话中统一管理。

现代 AI 应用极少只在一个地方执行。工作负载往往从高层框架开始,随后在原生 CPU 代码与 GPU 内核之间来回穿梭。一旦错误跨越这些边界,开发者就需要同时理解宿主端应用和设备端状态。

这比将传统调试器附加到另一个进程要复杂得多。CPU 和 GPU 可能使用不同的指令集、寄存器、内存空间和执行模型。此外,GPU 通过宿主进程内的运行时和驱动程序进行控制,因此调试设备往往需要与 CPU 进行精细协调。

过去,GPU 厂商构建了各自专用的调试器,例如 AMD 的 ROCgdb 和 NVIDIA 的 CUDA-GDB。这些工具为各自的平台提供了深度支持,但通用的调试器功能往往需要分别实现和维护。

在 Meta,我们为 LLDB 开发了一种不同的方案:在 lldb-server 中引入通用的加速器插件架构。在本文中,“GPU”指代具体的设备和示例,而“加速器”则指 LLDB 更广泛、厂商中立的插件抽象层。该架构在 Meta 工程师及整个加速器生态系统的贡献中不断演进。它使共享调试器独立于任何单一 GPU,同时为硬件厂商提供了实现特定设备行为的集中入口。

GPU 调试为何挑战传统调试器模型

传统调试器通常假设一个进程仅包含一种架构,且对线程、寄存器和内存拥有一致的视图。GPU 程序则逐一打破了这些假设。

宿主进程可能运行在 x86-64 上,而其内核使用的却是一套完全不同的指令集。CPU 线程具有熟悉的寄存器上下文,而 GPU 可能同步执行一组 lane(通道)。宿主端和设备端的指针可能指向不同的地址空间。GPU 可暴露数千个执行通道,这些通道无法映射到大多数命令行或图形调试器所呈现的线程模型上。

把这些状态全部合并到一个虚构的进程模型中,会让调试器变得更加复杂。每个操作都得先判断针对的是 CPU 还是 GPU。寄存器处理、内存读取、断点、单步执行和停止事件都要根据所选硬件做条件判断。单一进程模型还强制把 CPU 和 GPU 塞进同一个统一的内存映射,但两者的地址空间其实可能是相互独立的:同一个地址(比如 0x1000)在 CPU 和 GPU 进程中可能指向完全不同的位置。

LLDB 的 accelerator 架构没有去掩盖这些差异,而是为它们划出了清晰的边界。

lldb-server 的插件机制支持 GPU 调试

LLDB 依靠一个名为 lldb-server 的配套进程来启动或挂接程序,并执行底层调试操作。LLDB 客户端通过 GDB remote protocol 与服务端通信,这是一套命令协议,支持读取内存、查看寄存器、控制执行、设置断点等操作。

由于 lldb-server 本来就掌控着原生进程,它也是与 accelerator runtime 协同的合适位置。新的插件接口让硬件专属后端可以与常规 CPU 调试器并行工作。后端负责把厂商的设备调试 API 转换成 LLDB 已经理解的概念,比如进程、线程、寄存器、内存、模块和停止原因。

这种分层设计让 LLDB 客户端保持厂商中立。命令行、远程协议、符号处理和 IDE 集成方面的通用改进可以惠及所有 accelerator。与此同时,各厂商继续自行处理其硬件和驱动栈中独有的细节。

只有当被调试的应用使用了受支持的 GPU runtime 时,才会激活 GPU 调试支持。对于纯 CPU 应用,LLDB 和 lldb-server 依然走现有的原生调试路径,不会引入额外的运行时计算开销。

两个 Target,一套调试体验

该架构将 CPU 和 accelerator 表示为两个独立的 LLDB target。
每个 target 有自己的架构,并关联一个调试器进程,由它维护各自的线程、寄存器、地址空间和执行状态。CPU 继续沿用 LLDB 成熟的原生调试路径。当 accelerator 可用时,插件会提供第二条连接,LLDB 用它来创建设备 target。

对于调试器而言,这两个目标在技术上仍是独立的;但对开发者来说,它们可以汇聚在同一个 LLDB 或 IDE 会话中。这样既避免了将不兼容的 CPU 和 GPU 概念强行塞进单一进程,又支持了统一的工作流。

CPU 和 GPU 保持为独立的 LLDB 目标,由 lldb-server 中的插件协调主机端和设备端的调试过程。

协调主机端与设备端

典型的调试会话遵循一套简短的步骤:

  1. LLDB 连接 lldb-server 并发现可用的加速器插件。
  2. 插件可以请求 LLDB 在宿主进程中设置内部断点。这些断点用于标记关键的运行时事件,例如可以开始 GPU 调试会话的时机。
  3. 当宿主进程到达这些断点之一时,lldb-server 通知插件。插件随后初始化厂商的调试 API,并准备一个加速器调试端点。
  4. LLDB 为加速器创建第二个目标并连接上去。此时,设备线程、寄存器、内存、模块和断点就可以复用 LLDB 现有的抽象层。

结构化的操作机制负责协调两侧。插件可以请求 CPU 断点、提供连接信息、上报设备模块,并同步主机端与设备端的执行状态。通用协议描述了调试器需要执行的操作,而无需向 LLDB 客户端暴露厂商特定的 API 细节。

我们还开发了一个模拟加速器插件,无需专用硬件即可测试上述流程。它让 LLVM 测试套件能够验证插件发现、断点协调、连接创建以及基本设备状态等功能。这在开发可独立审查和测试的共享基础设施方面至关重要,使其不依赖于任何特定 GPU。

典型的 GPU 工作流

下文展示了 向量点乘 HIP 示例 产生的简化会话记录:

(lldb) target list
Current targets:
  target #0: gpu_demo (x86_64, stopped)
* target #1: amd_memory_kernel[...] (amdgcn, stopped)

(lldb) b compute_element
Breakpoint 1: no locations (pending).
(lldb) c
(lldb) target select 0
(lldb) c
Launching kernel: 2 blocks x 32 threads
1 location added to breakpoint 1
Target 1 stopped.

(lldb) target select 1
(lldb) bt
* frame #0: compute_element(..., idx=32) at gpu_demo.hip:26
  frame #1: vector_dot_kernel(..., n=64) at gpu_demo.hip:35
待定断点在 GPU 模块加载后自动解析,回溯信息来自 GPU target。

跨硬件厂商的通用插件框架

插件边界有意基于调试器操作来设计,而非绑定某个特定 GPU 平台的概念。因此它可以适用于指令集、运行时 API 和执行模型差异很大的各种设备。

该框架同时支持 GPU 事后调试(postmortem)和实时调试会话。GPU core-dump 插件共用一个 LLDB 集成的基础基类,由各厂商实现提供设备专属的线程、寄存器、内存和模块状态。

Meta 的 GPU 调试工作已经在使用这一架构。由于我们运营着由不同软硬件栈组成的异构 GPU 集群,这些工作都采用同一模型这一事实,本身就是对该设计的重要验证:通用的 LLDB 层保持稳定,而每个后端都能准确表达各自的设备。

截至撰写本文时,我们已将大部分通用框架上游到 LLVM,包括 lldb-server 插件接口;用于插件发现、初始化、运行时事件协调和 GPU 连接创建的 GDB-remote 数据包;支持关联 CPU 与 GPU 调试器进程并保留各自独立架构;支持多地址空间;以及与硬件无关的 mock 测试。其余部分预计很快也会上游。

未来展望

接下来,我们计划完成剩余通用基础设施的上游工作,并扩展基于它构建的厂商集成。我们的目标是:将新硬件接入 LLDB 只需要一个厂商专属插件,而不必另起炉灶开发和维护一个单独的调试器。

致谢

感谢 Meta 开发者基础设施领导团队,特别是 Shane Nay、Todd Fiala 和 Ale Contenti,他们的直接支持让这项工作成为可能。

我们也感谢调试与工具团队(Debugging & Tooling)的同事 Jacob Lalonde、David Young、Alexandre Perez、Zhiyuan Li、Piyush Jaiswal 和 Philip DePetro 在合作与支持方面所做出的贡献。

最后,我们要感谢开源社区,尤其是 Jonas Devlieghere、Jim Ingham、David Spickett 和 Jason Molenda,他们审阅了 RFC 和上游拉取请求(pull requests)。

分享:

原始来源: Meta 工程博客

评论 (0)