一个越界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 | input_ids: 1003000 ~ 1004029 |
一个 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 堆栈走。
相关记录:
一个越界Token如何让CUDA错误出现在无关代码上
https://blog.novashen.top/2026/07/29/tech/AI Infra/一个越界Token如何让CUDA错误出现在无关代码上/