Rust in Everything
基于 burn 0.22.1

s![.., -1] 不降秩,s![5] 越界不报错

主线位置:张量主线的「计算」一步:用一个整数下标从张量里取一片,比如 RNN 输出的最后一个时间步。
版本:PyTorch 2.15.0(CPU)、burn 0.22.1(Flex 设备)
flowchart TB
  h["h 形状 2x3x4"] -->|"PyTorch h[:, -1]"| p["形状 2x4"]
  h -->|"slice s![.., -1]"| k["形状 2x1x4"]
  k --> e["matmul 报 E0308"]
  h -->|"select_dim 1, -1"| ok["形状 2x4"]
  x["x 形状 3x2"] -->|"s![5]"| emp["形状 0x2 不报错"]
  x -->|"narrow 0, 5, 1"| nar["Narrow 报错"]

实验 crate pita-t 的 p07、编不过的例子 pita-ce 的 ce07,PyTorch 侧 p07_index.py。debug 构建。平台 M2 Max、macOS 27、rustc 1.99.0。panic 行和 note 里的本地路径前缀换成了 ...。

PyTorch 的写法

h = torch.arange(24, dtype=torch.float32).reshape(2, 3, 4)  # [B, T, H] RNN outputs
w_out = torch.ones(4, 2)
last = h[:, -1]
print("h[:, -1]", list(last.shape), "| @ w_out", list((last @ w_out).shape))
x = torch.arange(6).reshape(3, 2)
i = 5  # an index that came from the data
x[i]
h[:, -1] [2, 4] | @ w_out [2, 2]
x[5]: index 5 is out of bounds for dimension 0 with size 3

两个习惯:整数下标会去掉这一维;下标越界当场 IndexError。

搬到 burn 的现象

let logits = h.slice(s![.., -1]).matmul(w_out);
error[E0308]: mismatched types
   --> src/bin/ce07.rs:7:43
    |
  7 |   let logits = h.slice(s![.., -1]).matmul(w_out);
    |                                    ------ ^^^^^ expected `3`, found `2`
    |                                    |
    |                                    arguments to this method are incorrect
    |
    = note: expected struct `Tensor<3>`
               found struct `Tensor<2>`
note: method defined here
   --> .../burn-tensor-0.22.1/src/tensor/api/numeric.rs:841:12
    |
841 |     pub fn matmul(self, other: Self) -> Self {
    |            ^^^^^^

报错说 matmul 要一个三维的 w_out。原因在左边:h.slice(s![.., -1]) 还是三维的,形状是 [2, 1, 4]。越界的下标则什么也不报:

let row = x.clone().slice(s![i]); // i = 5, x is [3, 2]
println!("x.slice(s![5]) {:?} sum {:?}", row.dims(), pita_t::iv(row.sum()));
h.slice(s![.., -1]) [2, 1, 4]
...
x.slice(s![5]) [0, 2] sum [0]

一个空张量,求和是 0,后面的代码照常往下走。

原因

s! 里的整数经 Slice::index 变成长度为 1 的范围(-1 是「从倒数第一个到末尾」,5 是 5..6),而 slice 返回 Self,秩写在类型里不会变。范围越界时 slice 把它截到形状以内,5..6 截成 3..3,于是得到长度为 0 的一维。这两点的细节见切片与索引,截断的机制见 burn 卷切片越界不报错。

降秩的版本是 select_dim,它的实现只有一行:

        let index = index.as_index();
        self.slice_dim(dim, index).squeeze_dim(dim)

所以它越界时也先被截成空的一维,再由 squeeze_dim 拒绝:

let row: Tensor<1, Int> = x.select_dim(0, i); // i = 5
thread 'main' (8791246) panicked at .../burn-tensor-0.22.1/src/tensor/api/base.rs:815:9:
=== Tensor Operation Error ===
  Operation: 'Squeeze'
  Reason:
    1. Can't squeeze dimension 0 because its size is not 1 

会 panic,但报错里没有下标 5,也没有「越界」两个字。

📝NOTE
冷知识:select_dim 是 0.22 才有的

#5246「feat(tensor): add tensor.select_dim(dim, index)」(2026-07-29,首次发布于 v0.22.0-pre.2)之前,burn 里「按一个下标取一片并去掉这一维」要自己写 slice 再 squeeze_dim。提交说明的第一句是「Add a torch.select(t, dim, idx) equivalent which selects and removes a dimension」,后续一条把 dim 和 index 都改成 impl AsIndex,于是两者都支持负数。它就是把那两步包成一个函数,所以越界时报的是第二步 Squeeze 的错误。旧教程里 slice(..).squeeze_dim(..) 的写法在 0.22.1 上仍然能用,结果相同。

改法

要 PyTorch 的 h[:, -1],用 select_dim,目标秩写在类型上:

let last: Tensor<2> = h.select_dim(1, -1);
let logits = last.clone().matmul(Tensor::ones([4, 2], &device));
h.select_dim::<2>(1, -1) [2, 4] -> matmul [2, 2] [8.0, 9.0, 10.0, 11.0, 20.0, 21.0, 22.0, 23.0]

形状和 PyTorch 的 [2, 4]、[2, 2] 一致,取到的是每个序列最后一个时间步(8..11、20..23)。

下标来自数据(标签、词表 id、样本编号)时,要的是 IndexError 那样的检查。narrow 检查的是原始的起点和长度:

let row = x.narrow(0, i, 1); // i = 5
thread 'main' (8791277) panicked at .../burn-tensor-0.22.1/src/tensor/api/base.rs:2701:9:
=== Tensor Operation Error ===
  Operation: 'Narrow'
  Reason:
    1. Can't narrow at dimension 0, start exceeds the size of the tensor along this dimension (Size=3) 
    2. Can't narrow at dimension 0, start + length exceeds the size of the tensor along this dimension (Size=3) 

narrow 保留这一维(下标在范围内时,x.narrow(0, 1, 1) 的形状是 [1, 2]),需要降秩时再接 squeeze_dim::<1>(0)。narrow 只收非负的 usize 起点,负下标要自己换算。

怎么检查

  • 搬代码时,把每个 a[i]、a[:, i]、a[..., -1] 都问一遍:PyTorch 里这一维被去掉了,burn 里要不要 select_dim?编译器只在秩真的对不上时才提醒,下一步刚好也接受三维的话(比如逐元素运算),多出来的那一维会一路传下去。
  • 下标来自运行期数据时,先断言 i < x.dims()[d],或者用 narrow。
  • 看到 Operation: 'Squeeze' 而代码里没有写 squeeze,先查 select_dim 的下标有没有越界。

小结

  • burn 的 s![i] 是长度为 1 的范围,不降秩;PyTorch 的 a[i] 降秩。
  • 越界的 s![5] 截成空的一维,不报错;select_dim 越界报的是 Squeeze。
  • 取一片并降秩用 select_dim::<D-1>(dim, i),支持负下标;来自数据的下标用 narrow 或先断言。

💬 关联讨论

发起讨论
加载中...