narrow invalid args start + len > dim_len
主线位置:推理主线的「前向」和「生成」两步。输入截到
max_len,取最后一个位置的 logits。
版本:PyTorch 2.15.0(CPU)、candle 0.11.0(CPU,accelerate;Metal)
flowchart TB py["ids 切到 max_len"] -->|PyTorch| clip["截到实际长度"] py -->|candle| n["narrow 0 到 8"] n --> err["narrow 报错"] neg["logits 取 -1"] --> ce["编译错误"] fix1["max_len 取 min"] --> ok["正常"] fix2["seq 减 1"] --> ok
本页的对照:PyTorch 侧是实验代码里的 p07.py,candle 侧是实验 crate pita-t 的 p07_narrow(features metal accelerate,release),编译错误来自只用来看报错的 crate pita-ce。平台 M2 Max、macOS 27、rustc 1.99.0。CPU 和 Metal 的运行输出逐字相同。
PyTorch 的写法
ids = torch.arange(1, 6).reshape(1, 5)
max_len = 8
ids[:, :max_len]
logits[:, -1, :] # logits: [1, 5, 6]
ids[:, ::2]; ids[:, -3:]
ids.narrow(1, 3, 4)
ids[:, :max_len] [1, 5] [[1, 2, 3, 4, 5]]
logits[:, -1, :] [1, 6] [[24.0, 25.0, 26.0, 27.0, 28.0, 29.0]]
ids[:, ::2] [[1, 3, 5]] ids[:, -3:] [[3, 4, 5]]
ids.narrow(1, 3, 4) RuntimeError start (3) + length (4) exceeds dimension size (5).
切片和 Python 的列表一样:终点超过长度就截到长度,负数从末尾数,可以带步长。narrow 是另一回事,PyTorch 的 narrow 越界同样报错。
搬到 candle 的现象
let ids = Tensor::arange(1u32, 6, &dev)?.reshape((1, 5))?;
let max_len = 8;
ids.i((.., ..max_len));
ids.narrow(1, 0, max_len);
ids.narrow(1, 3, 4);
ids.i((.., ..max_len)) Err: narrow invalid args start + len > dim_len: [1, 5], dim: 1, start: 0, len:8
ids.narrow(1, 0, max_len) Err: narrow invalid args start + len > dim_len: [1, 5], dim: 1, start: 0, len:8
ids.narrow(1, 3, 4) Err: narrow invalid args start + len > dim_len: [1, 5], dim: 1, start: 3, len:4
i((.., ..max_len)) 是照着切片写的,报的却是 narrow 的错误。取最后一个位置:
let last = logits.i((.., -1, ..))?;
error[E0277]: the trait bound `usize: std::ops::Neg` is not satisfied
--> src/bin/ce07.rs:5:28
|
5 | let last = logits.i((.., -1, ..))?;
| ^^ the trait `std::ops::Neg` is not implemented for `usize`
|
help: consider specifying an integer type that can be negative
|
5 | let last = logits.i((.., -1isize, ..))?;
| +++++
照着 help 改成 -1isize,换一个错误:
error[E0277]: the trait bound `candle_core::Tensor: IndexOp<(RangeFull, isize, RangeFull)>` is not satisfied
--> src/bin/ce07b.rs:5:23
|
5 | let last = logits.i((.., -1isize, ..))?;
| - ^^^^^^^^^^^^^^^^^ the trait `IndexOp<(RangeFull, isize, RangeFull)>` is not implemented for `candle_core::Tensor`
...
原因
IndexOp::i 把元组里的每一项转成 TensorIndexer,Tensor::index 再把区间算成 (start, stop) 交给 narrow:
let stop = match right_bound {
Bound::Included(n) => *n + 1,
Bound::Excluded(n) => *n,
Bound::Unbounded => dims[i],
};
let out = x.narrow(current_dim, start, stop.saturating_sub(start))?;
终点原样使用,不和 dims[i] 取最小值;narrow 发现 start + len > dim_len 就返回 Err。所以 candle 的切片语义和 PyTorch 的 narrow 一样严格,和 PyTorch 的切片不一样。(saturating_sub 只处理了「起点大于终点」,那时得到空张量,两边一致。)
下标和区间的类型是 usize 及其 Range,IndexOp 没有为 isize 实现,负数从语法上就进不来,也就没有「从末尾数」。步长、布尔掩码都没有对应的 TensorIndexer。完整的对照表见归约与索引的「下标与切片:i(..)」。origin/main 上 indexer.rs 没有改。
改法
把 PyTorch 切片的隐式规则写出来:
ids.i((.., ..max_len.min(ids.dim(1)?))); // ids[:, :max_len]
let seq = logits.dim(1)?;
logits.i((.., seq - 1, ..)); // logits[:, -1, :]
ids.i((.., seq - 3..)); // ids[:, -3:]
let even: Vec<u32> = (0..ids.dim(1)? as u32).step_by(2).collect();
ids.index_select(&Tensor::new(even.as_slice(), &dev)?, 1); // ids[:, ::2]
ids.i((.., ..max_len.min(len))) U32 [1, 5] [1, 2, 3, 4, 5]
logits.i((.., seq - 1, ..)) F32 [1, 6] [24.0, 25.0, 26.0, 27.0, 28.0, 29.0]
ids.i((.., seq - 3..)) U32 [1, 3] [3, 4, 5]
ids.index_select(0,2,4) U32 [1, 3] [1, 3, 5]
四行都和 PyTorch 相同。candle-transformers 的 llama.rs 取最后一个位置也是 x.i((.., seq_len - 1, ..))?。注意 seq - 3 是 usize 减法:序列比 3 短时 debug 下 panic、release 下回绕成一个巨大的起点再报 narrow 错误;要 PyTorch 的 -3: 语义(不够长就全取),写 seq.saturating_sub(3)..。
怎么检查
- 搬代码时把每个
[:, :n]、[..., -k:]都当成一个要显式写出min或saturating_sub的地方。 narrow invalid args的报错里有shape、dim、start、len四项,start + len和那一维的长度一比就知道是哪个切片。- 用一个比
max_len短的输入跑一遍;PyTorch 侧不会暴露这个问题,因为切片会自动截断。
小结
- PyTorch 的切片越界自动截断,candle 的
i(..)区间越界报narrow invalid args start + len > dim_len。 - 负下标在 candle 里编译不过(
usize: Neg),照着编译器的提示改成isize也不行;没有步长。 - 改法:
..n.min(len)、len - 1、len.saturating_sub(k)..,步长用index_select拿一组算好的下标。