跳转到主要内容
  • Daily Report
  • GPU
  • Instrumentation
  • Profiling
  • Runtime

GPU 插桩能改写内核,却不改变内核原本的行为吗?

动态 GPU 二进制插桩能在 SASS 层观察真实执行代码,却也可能改变寄存器、占用率和控制流。本报告讨论一种可验证、可量化的安全插桩契约。

设想一个线上 GPU 内核只在某类客户负载下变慢。源码级日志无法复现问题,重新编译又会改变真正出问题的二进制。动态二进制插桩看起来很合适:直接修改已经生成的 GPU 指令,在可疑指令附近插入一个很小的探针,把缺失的状态采回来。

问题在于,这个探针并不是站在 GPU 外面观察。它会和被观测程序一起执行,需要寄存器,会增加指令,可能访问显存,也可能引入新的分支。对于一个已经接近占用率或资源分配临界点的内核,即使探针在功能语义上完全正确,也可能改变调度和执行时序,从而把原来的性能问题隐藏掉。更糟的探针甚至会直接改变程序行为。

所以,GPU 插桩需要同时回答两个问题:它收集到了什么,以及它在多大程度上保留了原来的执行。今天的工具已经能做采样、跟踪和二进制改写,但“这个观测结果到底有多透明”仍然主要依赖各个工具自己的隐含假设。

本文主张在插桩运行时和探针之间加入一个明确的 GPU 插桩安全契约。契约应描述探针可以读取和修改哪些体系结构状态,哪些动态位置确实可以安全插桩,允许多少执行资源扰动,以及探针被跳过、采样或限流时要保留哪些覆盖率信息。目标不是让插桩没有开销,而是避免把“工具成功运行”误认为“工具忠实观察了原程序”。

这个问题和本站此前的 GPU 内核启动延迟归因 以及 主机与设备因果关系 不同。那两篇讨论的是一条 trace 能否解释 GPU 工作何时运行、为什么运行;这里更靠近执行本身:当观察者被插进内核以后,我们还能不能相信被观察到的是原来的那个内核。

GPU 动态插桩已经足够强,因此需要明确边界

NVBit 证明了一个重要能力:即使 CUDA 源码和 PTX 已经被继续降低为 NVIDIA SASS,运行时仍然可以在最终执行的二进制上做动态插桩。当前的 NVBit 仓库 允许工具检查和修改 SASS,在一条 SASS 指令之前或之后调用任意设备函数,甚至可以删除指令。仓库同时明确提醒,删除指令以后并不保证应用仍然正确;调用插桩函数也有保存和恢复应用状态的成本。

Intel 侧也有类似的执行层能力。2026 年 ISPASS 的 GTPin 复现实验 提供了 Intel GPU 高层二进制插桩代码。两个不同厂商都在最终设备代码附近提供可编程插桩,说明问题并不局限于 SASS:只要运行时能改写 GPU 真正执行的代码,就必须说明它准备保留什么。

厂商提供的 profiling 接口可以作为一个很好的对照。当前 CUPTI 文档 提供 Activity、Callback、PC Sampling、SASS Metrics、PM Sampling、Checkpoint 和 profiling 等接口。PC Sampling 会周期性采集一个活跃 warp 的程序计数器和调度器状态;SASS Metrics 能在指令粒度收集特定指标。这些接口的观测面没有任意二进制改写那么自由,但也因此不需要把同样多的程序修改权限交给第三方探针。

可编程插桩的价值正来自这种自由:固定 profiler 没有预先设计的问题,动态探针仍然可能回答。但自由越大,工具和运行时承担的正确性责任也越大。

保存寄存器只是第一步,透明性还包括资源和调度

一种常见理解是,只要插桩运行时先保存应用寄存器,执行探针,再把寄存器恢复,程序就没有被改变。NVBit 已经让这种机制能够实际工作,寄存器保护当然是必要条件,但它不是完整的透明性。

GPU 每个 SM 上的执行资源是有限的。NVIDIA 当前的 CUDA Best Practices Guide 明确说明,寄存器数量会影响一个 SM 能同时驻留多少 thread block 和 warp;shared memory、block 数量和 warp 数量也共同限制调度。资源分配还是离散的,因此很小的额外需求就可能跨过一个边界,让 occupancy 突然下降。

因此,一个把所有应用寄存器值都恢复正确的探针,仍然可能通过下面这些路径改变性能:

  • 新增寄存器或 local memory spill 改变内核资源占用;
  • 新增 load、store、atomic 或 cache 流量和应用竞争;
  • 探针里的分支参与 warp 控制流,增加 divergence;
  • 探针中的同步操作遇到条件执行或部分活跃 warp 时可能产生新的约束;
  • 新增指令改变 issue 压力,也可能改变竞争和 race 出现的时序;
  • 遥测缓冲区暂时不 flush,也只是把压力推迟到后面的执行阶段。

对于安全监控和调试工具,还有另一个问题:并不是每一个 SASS 位置都同样容易分析。间接跳转、复杂控制流、barrier 附近,或者无法可靠恢复寄存器和栈语义的位置,都不应该和普通算术指令一样,被简单地标记为“已插桩”。

最近的 WarpGuard 虽然研究目标是 CUDA SASS 的控制流完整性,而不是通用 profiling,但它的结果呈现方式很值得借鉴。它在 77 个 CUDA artifact 中对数万个 SASS 控制流位置做分类,并明确区分 protected、unsupported、profile-excluded、fallback 和 no-surface 等结果。不支持的位置没有从统计口径里消失,而是成为结果的一部分。

真正缺少的是“可观测的不干扰”,不是零开销

如果要求零开销,大多数动态插桩都没有存在空间。更实际的目标是 可观测的不干扰:运行时先说明准备保留哪些程序属性,再量化自己能够观察到的资源变化;遇到未知或不支持的情况时,不能把它伪装成成功。

性能探针可以允许一定的时序扰动,但禁止写入探针私有遥测区以外的应用内存。安全监控可以接受更高执行成本,但必须在被保护的控制转移发生前 fail closed。调试探针甚至可能故意修改应用寄存器,但这种修改应当在契约中明确,而不是藏在“instrumentation”这个词后面。

这至少要把两个维度分开:

  1. 语义影响:探针可以改变哪些应用可见状态和控制行为;
  2. 测量影响:观察者改变了多少调度、occupancy、内存流量和时序。

一个方案可能在一个维度很强、另一个维度很弱。例如 PC Sampling 对程序语义的侵入很小,但观测能力不如任意设备端函数;二进制改写能看到更多东西,却需要更强的约束和记账。

本站的 bpftime GPU 工作 是把可编程运行时逻辑推进到设备执行附近的一种尝试。类似 eBPF 的 verifier 可以限制内存和控制流行为,但即使字节码本身是安全的,GPU 特有的问题仍然存在:探针资源是否让 occupancy 下降、某个 attach site 是否真的可插、采样和限流是否留下了关键盲区,这些都不能只靠“程序通过 verifier”来回答。

现有研究还缺什么

工具安全和程序透明性通常不是同一个接口

二进制插桩框架会处理 ABI 状态、代码重定位、函数调用和工具私有存储;profiler 则会提供指标和开销控制。现在缺少的是一个把这些事实连接到用户问题的机器可读接口:这个探针到底有没有保留我正在诊断的行为?

可以用一组接近寄存器、shared memory 和 occupancy 临界点的内核来测试。插桩前后不仅比较功能输出,还要比较资源状态。运行时应该能够区分“语义保持,但性能已经被扰动”和“语义与测量预算都满足”这两种情况。

不支持的位置和丢失的观测没有稳定地进入最终结果

动态工具可能无法解析某种二进制模式,主动拒绝一个位置,遥测缓冲区溢出,也可能为了控制开销只采样部分执行。如果最终报告只展示成功采到的事件,用户无法知道“什么都没发生”和“观察者没有证据”之间的区别。

WarpGuard 把不支持的位置明确分类,是一种可借鉴的做法。通用 profiling 和 debugging 同样需要报告:哪些位置支持、哪些被拒绝、哪些动态实例因为预算被跳过、哪些被采样,以及有多少记录丢失。

最直接的验证方法是故意加入不支持位置和高压力执行点。如果工具在覆盖率下降后仍给出和完整观测相同的高置信度结论,它的结果就不可信。

资源扰动往往在运行后统计,而不是 attach 前做预算

很多 GPU 工具会报告总 slowdown,这当然有用,但粒度太粗。一个只偶尔执行的关键内核可能被局部扰动很多,却只让整个程序慢 2%;反过来,一个 microbenchmark 慢 20%,如果探针只在单次线上请求中打开,也可能完全可接受。

更缺的是 attach 前能检查的资源预算,例如额外寄存器、local/shared memory、插入指令数量、遥测字节和预计探针频率。它不需要准确预测最终 slowdown,但至少可以在测量失效之前拒绝明显危险的组合。

如果实验发现简单的全局开销阈值和按内核资源预算在热点排名、诊断正确性、应用行为上没有区别,那么复杂预算就没有必要。

跨厂商插桩缺少共同的“保留了什么”语言

NVBit 操作 NVIDIA SASS,GTPin 面向 Intel GPU 二进制,厂商 profiler 的采样和指标能力也不同。一个可移植工具不能把所有后端的“instruction probe”或“PC sample”都当成同一件事。

更合理的公共层不是把 ISA 抹平,而是统一表达保留声明和不确定性,例如 state clobber set、attach-site 类型、coverage、resource delta 和 telemetry-loss state。底层仍然可以保留厂商原生事实。

兼具学术价值与生产价值的方向

1. 可验证的探针影响清单

缺口。 工具可以描述自己想在哪里插入探针,却缺少一个可移植对象来说明探针允许改变什么,以及运行时必须保护什么。

机制。 每个设备端探针在 attach 时携带一份 effect manifest,声明可读取的体系结构状态、允许写入的状态、可访问内存区域、可能发生的控制转移、同步行为、最大遥测写入量,以及是否故意修改应用状态。后端再把它和从 SASS、其他设备 ISA 或中间表示得到的 attach-site capability 结合起来检查。

加载结果必须明确区分:验证通过、带降级条件通过、不支持的位置、影响范围违规而拒绝。运行时不能为了让工具成功加载而静默放宽 manifest。

如果后端使用类似 eBPF 的字节码,一部分 effect 可以由 verifier 证明;如果探针是原生 CUDA/C++ callback,静态验证能力较弱,就使用更保守的 capability class。公共抽象仍然成立,因为它描述的是承诺的影响,而不是绑定某一种 verifier。

和现有工作的差异。 NVBit 已经解决了 ABI 兼容插桩和设备状态访问,WarpGuard 也会从受支持的 SASS 控制流位置推导保护策略。这里进一步把“允许产生什么影响”变成任意观测和调试探针的 attach-time 契约,而不是再实现一个二进制改写器。

可实现 artifact。 一个厂商无关的 manifest schema,加上 NVBit 和 GTPin adapter,并提供可选的 eBPF-like verified probe backend。CLI 可以直接回答 why-not-attached <kernel,site>,显示具体是哪条约束不满足。

评估。 覆盖普通算术、访存、间接控制流、barrier、分支 divergence 和库内核。比较原始框架与 manifest-gated 插桩,测 unsupported-site recall、错误放行、功能差异、attach 延迟以及能被接受的有效探针比例。

学术价值。 核心问题是:在厂商二进制语义不同的前提下,动态 GPU 插桩能否拥有一个可移植的 effect system。

生产价值。 profiler、安全工具和运行时可以给第三方探针一个机器可检查的权限边界,而不是只依赖代码审查和崩溃测试。

失败条件。 如果真实探针的行为过于动态,manifest 为了安全必须拒绝大多数有用位置,那么静态 attach 契约就是错误的抽象。

2. 带覆盖率的资源预算插桩

缺口。 一个语义上安全的探针仍然可能因为寄存器、occupancy、带宽或遥测开销,破坏正在被测量的性能行为。

机制。 激活之前,后端先估算探针对每个内核变体增加的资源。运行时按内核预算决定是否允许插桩,例如:

max_register_delta
max_local_memory_delta
max_shared_memory_delta
max_injected_instructions_per_event
max_telemetry_bytes_per_second
max_sampled_event_fraction

完整探针超预算时,可以轮换插桩位置、对 invocation 采样,或者降级到 PC Sampling 这类更低成本的观测方式。但任何降级都必须同步更新 coverage 记录。

因此查询结果不仅回答“看到了什么”,还回答“有多少本来应该观察的执行真正被看到了”。例如,一个热点指令占已收集样本的 40%,但 eligible event 只覆盖了 12%,这两个数字不能被混成同一个结论。

和现有工作的差异。 CUPTI 已经提供低开销采样模式,NVBit 也支持选择性插桩。这里把资源准入和观测覆盖率变成运行时共享策略,而不是让每个工具重新实现自己的 sampling 开关。

可实现 artifact。 资源估算器、admission controller 和携带 coverage 的遥测格式。原型可以使用 CUPTI 指标或 occupancy 计算做验证,同时驱动 NVBit 风格的二进制插桩。

评估。 选择刚好位于寄存器/occupancy 临界点上下的内核,并加入 memory-bound、compute-bound、divergence 和同步密集型负载。对比全量插桩、固定比例 sampling、PC Sampling 和资源预算方案,测诊断正确率、occupancy 变化、register spill、内核时长扰动、遥测丢失和总开销。

学术价值。 这把“观察者扰动”变成一种可调度资源,并显式研究精度与开销之间的关系。

生产价值。 常驻 GPU observability 服务可以守住硬开销预算,同时在特定内核真正需要诊断时升级到更丰富的探针。

失败条件。 如果固定比例 sampling 在不同负载下都能达到相同诊断质量和扰动边界,那么按内核资源准入不值得增加复杂度。

3. 专门让“透明插桩”失败的反例 benchmark

缺口。 工具评估通常报告平均 slowdown 和采集到的信息量,却很少构造一种情况:插桩本身改变了原本应该回答的调试或性能问题。

机制。 构造外表相似、对插桩敏感性不同的 paired kernels:

表面上相同的目标隐藏边界
相同 kernel time、相同热点指令一个内核恰好只差一步就跨过寄存器分配导致的 occupancy 下降
相同分支形状、相同 attach site一个位置在 partial-warp divergence 下执行
相同 memory trace、相同访存探针一个负载接近 cache 或 atomic contention 临界点
相同 barrier 数量、相同同步区域到达探针时的 active-lane 结构不同
相同事件速率、相同遥测格式一个执行会把 probe buffer 打满并静默丢数据

测试框架分别运行原生、采样和全插桩版本,并为一个具体问题保留 ground truth,例如热点排序、stall 原因、race 是否出现或安全违规是否发生。只要工具自己的插桩改变了答案,却仍给出高置信度,就算失败。

和现有工作的差异。 NVBit、GTPin、CUPTI 以及新的 SASS 安全插桩都证明了不同机制能够工作。这个 benchmark 不再只看工具收集了多少数据,而是把观察者本身当成可能制造反例的组件。

可实现 artifact。 一个 CUDA-first 的开源 benchmark,包括二进制变体、预期输出、occupancy/资源临界点、trace-loss 注入,以及供 Intel 或未来 GPU 后端接入的统一测试接口。

评估。 核心指标是 semantic divergence、诊断排名稳定性、资源增量、coverage、false-confidence rate 和 overhead。最重要的 ablation 是保留完全相同的 probe code,只去掉 safety contract,从而单独测量准入和 coverage 报告本身的价值。

学术价值。 它把“插桩透明性”从一种非正式期待变成可以被反例推翻的系统性质。

生产价值。 工具和运行时团队可以在新 GPU 架构或新 driver 上线前,用同一套测试检查观察者诱发的问题。

失败条件。 如果真实框架只要总开销低于一个简单阈值,就总能保持这些 benchmark 的答案不变,那么实验会反过来证明一个更简单的 slowdown-only 契约已经足够。

哪些结果会改变这个判断?

本文有一个前提:设备端可编程插桩会继续存在,因为固定厂商 telemetry 不可能预先覆盖所有调试、安全和研究问题。如果未来硬件能够用很低开销、完全不修改程序的方式提供几乎所有生产环境真正需要的观测,那么通用二进制插桩可能只剩下离线研究用途,这时设计一套很广的运行时契约意义会小很多。

第二种反证是,资源扰动其实可以被一个非常简单的数字预测。例如跨多代 GPU 的实验发现,只要整个内核 slowdown 低于某个阈值,动态探针就始终不会改变热点排序、同步行为、race 出现概率和安全结果,即使内核就在寄存器或 occupancy 临界点附近。那么本文提出的多维资源预算就过度设计了。

最后,跨厂商可移植性本身也可能不成立。如果 NVIDIA SASS、Intel GPU binary 和未来加速器对 attach site、寄存器状态、同步和可恢复控制流的定义差异太大,一个统一 schema 反而会掩盖重要事实。那时更合适的方案应当是厂商专用的详细契约,只在 coverage 和 uncertainty 上保留很薄的一层公共接口。

在这些反证出现之前,更稳妥的做法是把可编程 GPU 插桩视为真正插入线上程序的一段代码:它之所以有价值,是因为能看到固定接口看不到的东西;它之所以可信,则必须因为自己的影响和盲区也进入最终结果。