一个越界Token如何让CUDA错误出现在无关代码上

前言

训练服务曾经在一段处理 position_ids 的代码附近报:

1
CUDA error: device-side assert triggered

第一反应当然是位置编码或者 mask 写错了,但最后发现 position_ids 完全无辜。真正的问题发生得更早:用户提交的 token ID 超出了模型词表范围,embedding gather 已经越界,只是 CUDA 异步执行让错误晚了一段时间才暴露。

异常数据

Qwen3.5-4B 的词表大小是:

1
vocab_size = 248320

失败请求却提交了:

1
2
input_ids:     1003000 ~ 1004029
target_tokens: 1003001 ~ 1004030

一个 8 × 1023 的 Batch 中,8184 个 token 全部越界。

对照请求使用 3000 ~ 4029,相同 Batch 形状能够正常运行,因此问题和 batch size、序列长度、position mask 都没有关系。

调用端很可能把带一百万偏移的测试编号直接当成 token ID,而没有经过模型 tokenizer。

真正出错的是embedding查表

Embedding 本质上是按 token ID 查表:

1
hidden = embedding_weight[input_ids]

合法范围只能是:

1
0 <= token_id < vocab_size

越界后,底层 gather kernel 触发:

1
vectorized_gather_kernel index out of bounds

但是 CUDA kernel 默认异步提交。CPU 发起 gather 后不会立刻等它完成,而是继续排队执行后面的算子。直到某个需要同步、检查状态或访问同一 stream 的位置,运行时才把之前的设备错误返回给 Python。

于是用户看到的最后一层堆栈落在 position_ids mask,甚至可能落在另一段完全无关的 tensor 操作上。

如何判断错误位置

调试时可以临时设置:

1
CUDA_LAUNCH_BLOCKING=1

这样每个 kernel 更接近同步执行,堆栈通常会靠近真正出错的位置。它会明显降低性能,只适合调试,不应该作为生产修复。

最后

这次故障表面上是 CUDA 在 position mask 附近崩溃,根因却是最普通的整数范围错误。

GPU 异步执行会把 报错的位置堆栈错误的位置 分开,因此排障不能只沿最后一层 Python 堆栈走。

相关记录:

作者

Noah Shen

发布于

2026-07-29

更新于

2026-07-29

许可协议

评论