DGX Spark 上如何定位 CUDA 越界访问并验证修复

DGX Spark 上如何定位 CUDA 越界访问并验证修复

CUDA 内核已经启动,错误却在结果回传时才出现;同一段程序缩小输入后又能运行。这类现象首先需要检查设备访存,而不是立即调整驱动。在 DGX Spark 上,可以用一个独立的小程序建立诊断链:主动触发越界、定位源码行、修复索引,再确认错误检测没有被关闭。整个过程不需要模型权重,也不涉及线上服务。

一、固定环境并划清测试边界

TcpNode 的 DGX Spark 使用 NVIDIA GB10 Grace Blackwell、ARM64 架构与 NVIDIA DGX OS,配备 128GB LPDDR5x 统一内存。统一内存由 CPU 与 GPU 共用,不意味着任意指针都能跨越分配边界。下面使用 cudaMalloc 分配设备内存,使合法地址范围明确。

付款后在控制台查看连接凭据,经 SSH 登录。测试应放在独立工作目录,以普通用户运行,不在正在承担请求的推理进程中注入错误。

mkdir -p "$HOME/cuda-access-check"
cd "$HOME/cuda-access-check"
uname -m
nvidia-smi
command -v nvcc
command -v compute-sanitizer
nvcc --version
compute-sanitizer --version

架构应显示 aarch64。记录工具版本与驱动信息,后续复验使用同一环境。CUDA 栈预装并不等于每个工具都已进入当前 PATH;命令不存在时,先核对已安装工具链与安装路径,不要直接替换整套驱动。

越界样例只能在可丢弃的独立进程中执行。出现非法访存后,应结束该进程重新运行,不能把同一 CUDA 上下文里的后续结果当作有效证据。

二、构造只有一个变量的越界样例

保存以下内容为 repro.cu。它只分配一个整数,合法索引为零;编译时通过 TEST_INDEX 切换写入位置。这样错误版与修复版共享源码,避免同时改变输入、分配大小和执行路径。

#include <cuda_runtime.h>
#include <cstdio>
#include <cstdlib>

static void checked(cudaError_t status) {
    if (status != cudaSuccess) {
        std::fprintf(stderr, "%s\n", cudaGetErrorString(status));
        std::exit(EXIT_FAILURE);
    }
}

__global__ void write_value(int* data) {
    data[TEST_INDEX] = 7;
}

int main() {
    int* data = nullptr;
    int result = 0;
    checked(cudaMalloc(reinterpret_cast<void**>(&data), sizeof(int)));
    checked(cudaMemset(data, 0, sizeof(int)));
    write_value<<<1, 1>>>(data);
    checked(cudaGetLastError());
    checked(cudaDeviceSynchronize());
    checked(cudaMemcpy(&result, data, sizeof(int), cudaMemcpyDeviceToHost));
    checked(cudaFree(data));
    std::printf("result=%d\n", result);
    return result == 7 ? EXIT_SUCCESS : EXIT_FAILURE;
}

分别编译两个程序:

nvcc -arch=native -lineinfo -DTEST_INDEX=1 repro.cu -o repro-bad
nvcc -arch=native -lineinfo -DTEST_INDEX=0 repro.cu -o repro-good
sha256sum repro.cu repro-bad repro-good > build.sha256

-arch=native 面向当前可见 GPU 编译,适用于本机验证,不用于生成跨设备通用产物。若编译器不支持当前设备,先核对工具链兼容性,不要随意套用其他架构参数。-lineinfo 保留设备源码行信息;首次定位不必启用会明显改变执行特征的完整设备调试模式。

为什么同时检查启动与同步

cudaGetLastError 检查启动相关错误,但内核通常异步执行。cudaDeviceSynchronize 等待前面的设备工作完成,执行期间的错误才有机会在这里返回。两者必须分别检查,不能只看程序是否打印了结果。

三、让检测结果成为可判断的退出码

先运行错误版,并分别保存标准输出与检测日志:

if compute-sanitizer --tool memcheck --error-exitcode 86 \
    ./repro-bad > bad.stdout 2> bad.stderr; then
    status=0
else
    status=$?
fi
printf 'exit=%s\n' "$status"
cat bad.stdout bad.stderr

预期能看到非法全局内存写入报告,并定位到 write_value 中的写操作。报告通常还包含访问大小、线程位置和地址关系,具体排版随工具版本变化,不应把某段固定文字作为唯一判断条件。

--error-exitcode 86 用于让检测到的错误影响自动化结果;若被测程序自身已经失败,也可能得到程序返回的其他非零值。因此,验收要求是“退出非零且报告指向预期越界”,而不是强制所有故障都返回 86。

不要把命令直接接到普通日志管道后,只读取最后一个日志命令的状态。上面的重定向与条件分支可以保留真正的运行结果,也适用于启用了失败即退出策略的脚本。

四、根据地址范围修复,不掩盖症状

样例的分配长度为一个整数,写索引却为一,已经越过合法范围。真实程序需要把报告中的地址与对应分配关联,再检查索引计算、元素类型、步长及批次偏移。

常见问题包括:分配参数使用字节数,索引却按元素数解释;最后一个线程块不足整块;二维数组的行跨度与逻辑宽度不同;释放后的指针又被另一个任务使用。扩大分配可能暂时隐藏错误,却不能证明算法正确。

对一维任务,通常应在访问前检查 index < count,同时确认索引计算使用的整数类型能够容纳乘积。对共享内存协作内核,不能随意让部分线程提前返回,再让剩余线程进入块级屏障,否则可能引入新的同步问题。

若错误只出现在大型工作流,可暂时在可疑阶段后增加同步检查,逐段缩小范围。一次只增加一个检查点,保留首个错误;不要在错误发生后继续提交任务,产生大量次生报错。

五、用正反向测试完成修复验收

运行修复版:

if compute-sanitizer --tool memcheck --error-exitcode 86 \
    ./repro-good > good.stdout 2> good.stderr; then
    status=0
else
    status=$?
fi
printf 'exit=%s\n' "$status"
cat good.stdout good.stderr

本例应同时满足三个条件:退出码为零、检测报告没有访存错误、输出为 result=7。只看“没有报错”不够,内核没有执行或结果没有真正写入,也可能让测试表面通过。

随后重新运行错误版,确认它依然失败。这项反向测试能够发现检测命令被删掉、测试误用了旧二进制,或流水线吞掉失败状态等问题。真实内核还应覆盖最小有效输入、整块边界与不足整块的尾部输入;若接口允许空输入,也要明确其处理方式。

最终归档源码、编译命令、版本信息、校验和及两次运行日志。检测器会改变执行开销,检测期间的耗时不能直接作为性能结论;memcheck 通过也不代表数据竞争和同步问题已经排除。可靠的收尾是让访问合法性、计算结果与失败传播分别拥有可复查的证据,而不是仅留下一个成功截图。

常见问题

CUDA 内核启动后没有立即报错,是否说明访存正确?

不能。内核通常异步执行,启动错误检查不能代替执行结果检查。应检查 cudaGetLastError 与 cudaDeviceSynchronize,并用 Compute Sanitizer 验证设备访存。

Compute Sanitizer 的 memcheck 没有报错,是否可以直接上线?

不能仅凭一次扫描决定。还应验证输出内容、边界输入及失败退出码;memcheck 通过也不等于已经排除数据竞争或同步错误。

EPYC 7543P 独享物理主机,$472.5/月起

配置 TcpNode 物理主机 查阅 TcpNode 月价

物理主机,按目录配置

AMD EPYC 7543P 整机独享。配置前可核对月价、选配项与周期应付。

配置 TcpNode 物理主机