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,也没有「越界」两个字。
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或先断言。