invalid index 18446744073709551516
主线位置:训练主线的「loss」一步。标签里有要忽略的位置(填充、提示词部分),PyTorch 用
-100标出来。
版本:PyTorch 2.15.0(CPU)、candle 0.11.0(CPU,accelerate;Metal)
flowchart TB lab["标签含 -100"] --> ce["cross_entropy"] ce -->|"CPU I64"| e1["越界报错"] ce -->|Metal| s1["静默 0.752"] lab -->|"转 U32"| u["4294967196"] u -->|CPU| e2["越界报错"] m1["标签用 -1"] -->|"转 U32"| mx["u32::MAX"] mx --> s2["取到 0,静默 0.752"] lab -->|"自己做掩码"| ok["1.128 对上"]
本页的对照:PyTorch 侧是实验代码里的 p06.py,candle 侧是实验 crate pita-t 的 p06_negidx(features metal accelerate,release)。平台 M2 Max、macOS 27、rustc 1.99.0。
PyTorch 的写法
三个位置的 logits,中间那个不算 loss:
logits = torch.arange(12, dtype=torch.float32).reshape(3, 4).sin()
labels = torch.tensor([2, -100, 1])
F.cross_entropy(logits, labels) # ignore_index defaults to -100
F.cross_entropy(logits[[0, 2]], labels[[0, 2]])
F.cross_entropy(logits, labels, reduction="sum") / 3
emb[-1]; emb[torch.tensor([-1])]
F.cross_entropy(ignore_index=-100) 1.1282306909561157
mean over the 2 valid rows 1.1282306909561157
sum over valid / 3 0.7521538138389587
emb[-1] [4.0, 5.0] emb[torch.tensor([-1])] [[4.0, 5.0]]
F.cross_entropy 的 ignore_index 默认是 -100:这一行既不进分子也不进分母,结果等于只算两行。第三行是「忽略的那行算 0、但仍除以 3」的错误版本,下面会用到。负下标从末尾数。
搬到 candle 的现象
candle_nn::loss::cross_entropy 只有 (inp, target) 两个参数:
let labels = Tensor::new(&[2i64, -100, 1], &dev)?;
loss::cross_entropy(&logits, &labels);
loss::cross_entropy(&logits, &labels.to_dtype(DType::U32)?);
let labels_m1 = Tensor::new(&[2i64, -1, 1], &dev)?; // "ignore" written as -1
loss::cross_entropy(&logits, &labels_m1.to_dtype(DType::U32)?);
emb.index_select(&Tensor::new(&[-1i64], &dev)?, 0);
emb.index_select(&Tensor::new(&[-1i64], &dev)?.to_dtype(DType::U32)?, 0);
== cpu
cross_entropy(i64 labels with -100) Err: gather invalid index 18446744073709551516 with dim size 4
cross_entropy(labels.to_dtype(U32)) Err: gather invalid index 4294967196 with dim size 4
labels -1 as U32 U32 [3] [2, 4294967295, 1]
cross_entropy(-1 as U32) F32 [] [0.7521538]
emb.index_select([-1] i64) Err: index-select invalid index 18446744073709551615 with dim size 3
emb.index_select([-1] as u32) F32 [1, 2] [0.0, 0.0]
== metal
cross_entropy(i64 labels with -100) F32 [] [0.75215364]
cross_entropy(labels.to_dtype(U32)) F32 [] [0.75215364]
labels -1 as U32 U32 [3] [2, 4294967295, 1]
cross_entropy(-1 as U32) F32 [] [0.75215364]
emb.index_select([-1] i64) F32 [1, 2] [0.0, 0.0]
emb.index_select([-1] as u32) F32 [1, 2] [0.0, 0.0]
- CPU 上 -100 报越界,数字是 2⁶⁴−100 或 2³²−100,一眼看不出是 -100。
- -1 转成 U32 之后不报错,那一行取到 0,loss 是 0.752,正好是 PyTorch 第三行的「错误版本」:被忽略的行算成 0,却仍然除以 3。
- Metal 上全部不报错,-100 也给出 0.752,下标 -1 的
index_select取到一行 0。
原因
candle 的
nll(cross_entropy是log_softmax再nll)就是三步:gather出目标位置的 log 概率,sum_all,再乘-1 / b_sz。没有忽略某个标签值的逻辑,分母是全部行数。inp.gather(&target.unsqueeze(1)?, 1)? .sum_all()? .affine(-1f64 / b_sz as f64, 0.)下标在 CPU 后端里按
usize解释,I64 的 -100 变成 2⁶⁴−100,越界返回Err。to_dtype(U32)在整数之间按位截断,-100 变成 4294967196,仍然越界;-1 变成u32::MAX,而 candle 从 0.10.0 起规定「下标等于该类型的最大值时输出 0」,于是不报错、取到 0。这条规则的来历(#2940)见归约与索引的「-1 不是最后一个」。Metal 的
gather和index_select内核不检查越界(indexing.metal)。I64 的 -1 不是 I64 的最大值,不触发「输出 0」;index_select的min(input_ids[id_i], src_dim_size - 1)对有符号的 -1 不起作用,算出的偏移在缓冲区之前。本页这几次都读到 0,没有任何保证。Metal 越界的一般情况见 Metal 上越界下标不报错。
改法
自己实现 ignore_index:先把要忽略的行换成一个合法的下标,算完逐行的 loss 再乘掩码,分母用有效行数:
let valid = labels.ge(0i64)?; // U8
let safe = valid.where_cond(&labels, &labels.zeros_like()?)?; // -100 -> 0, still I64
let logp = ops::log_softmax(&logits, D::Minus1)?;
let picked = logp.gather(&safe.unsqueeze(1)?, 1)?.squeeze(1)?;
let validf = valid.to_dtype(DType::F32)?;
let n = validf.sum_all()?;
let ce = (picked * &validf)?.sum_all()?.neg()?.div(&n)?;
== cpu
masked mean F32 [] [1.1282307]
== metal
masked mean F32 [] [1.1282305]
PyTorch 是 1.1282306909561157,它作为 f32 的最短写法就是 1.1282307,所以 CPU 差 0;Metal 差 2.38e-7,即 1.13 附近 f32 的两个 ulp(一个 ulp 是 1.19e-7),在 f32 舍入的范围内。valid 的个数可能是 0(整批都被忽略),这时分母是 0,结果是 NaN,和 PyTorch 的 ignore_index 全被忽略时一样;要别的行为就在 n 上自己处理。
最后一个位置不要用 -1,用长度算:emb.narrow(0, emb.dim(0)? - 1, 1) 得到 [[4.0, 5.0]]。切片里的负下标见 narrow invalid args。
怎么检查
- 从 PyTorch 搬训练代码时,搜
ignore_index、-100、-1:candle 的 loss 和索引函数都没有对应的参数或语义。 - 看到
invalid index 1844674407370955...或invalid index 42949672..,就是一个负数被当成了无符号下标,减一下就能还原:2⁶⁴ − 18446744073709551516 = 100。 - 训练前在 CPU 上跑一步:CPU 至少会对 -100 报错,Metal 不会。
小结
- candle 的
cross_entropy没有ignore_index,分母是全部行数;PyTorch 默认忽略 -100。 - CPU 上 -100 报
gather invalid index 18446744073709551516;转成 U32 的 -1 是u32::MAX,静默取 0,loss 被稀释成 0.752(正确是 1.128);Metal 上 -100 也静默。 - 改法:
ge(0)得到掩码,where_cond把忽略行换成 0,逐行 loss 乘掩码再除以有效行数,和 PyTorch 差 0(CPU)和 2.38e-7(Metal)。