TL;DR 我以为我的CUDA内核运行在160微秒。我错了。这是我如何使用纯Go中的CUDA事件来找到真实的硬件时间,以及为什么CPU端的计时器是GPU取证的错误工具
在我上一篇文章中,我谈到了我通过为Go构建直接CUDA绑定而不使用cgo来删除了一个8.4GB的Python侧边栏。
我让"不可能构建"工作正常后,就开始吹嘘性能了。我把内核启动包装在一个标准的Go time.Since(start) 块中,并看到了 162微秒。
我以为我构建了一个速度飞快的恶魔。然后我实现了真正的GPU事件,发现了真相。
谎言般的指标
当你启动一个CUDA内核时,它是完全异步的。CPU不会等待GPU完成;它只是将任务放入队列(一个流)中,并立即将控制权返回给你的Go程序。
我的162微秒测量并不是在测量数学。它只是测量Go运行时与NVIDIA驱动程序通信并将作业入队所需的时间。
GPU还没完成矩阵的第一行,我的计时器就停止了。
硬件真相(RTX 4070 Ti)
要找到真实数据,我不得不实现CUDA事件。这些是你直接放置在硬件流中的标记。GPU在到达标记时会记录一个时间戳,完全绕过CPU时钟。
我在RTX 4070 Ti上运行了一个10M元素向量加法。以下是硬件实际报告的情况:
| 测量方法 | 报告时间 | 实际测量值 |
|---|---|---|
CPU time.Since (异步) |
~160微秒 | 提交任务的时间 |
GPU cuda.Event (实际) |
~434微秒 | 硅上实际计算时间 |
CPU time.Since (带同步) |
~404 微秒 | 入队 + 执行 + 运行开销 |
硬件计算时间比我的CPU计时器让我相信的要慢2.7倍。
纯Go实现
准确测量这一点需要添加NewEvent、Record 和 ElapsedTime 分配到 gocudrv 包。由于我们不使用 cgo,我不得不手动绑定 cuEventElapsedTime 符号并处理 C 到 Go 的 float32 转换。
这是现在“说真话”代码的样子:
// 1. Create the hardware stopwatches
start, _ := ctx.NewEvent()
stop, _ := ctx.NewEvent()
// 2. Place markers in the stream
start.Record(stream)
fn.LaunchOn(ctx, stream, cfg, args...)
stop.Record(stream)
// 3. Wait for the STOP marker to be reached
stop.Synchronize(ctx)
// 4. Get the hardware duration
duration, _ := start.Elapsed(stop)
fmt.Printf("Actual GPU time: %v\n", duration)
人工智能基础设施的教训
随着我们转向基于Go的AI基础设施,我们必须小心处理"测量漂移"
。如果你用Go构建推理网关或实时图像处理器,使用CPU计时器会在纸面上让你的P99s看起来非常出色,而你的用户却会体验到神秘的延迟。
你无法优化你无法衡量的东西。 如果你没有使用硬件事件,你只是测量了你的请求队列的速度,而不是你的产品的速度.
下一步是什么?
现在我有了毫秒级精确的秒表,我终于可以开始优化数据路径了。我目前正在处理 CUDA Graphs 通过将复杂的任务拓扑结构捆绑成一个硬件命令来减少 160µs 的入队开销。
如果你对低级 Go 的取证感兴趣,或者想帮助构建无 cgo 桥,请查看 GitHub 上的进展。























