Rust in Everything
基于 candle 0.11.0

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 拿一组算好的下标。

💬 关联讨论

发起讨论
加载中...