创建与形状
主线位置:张量主线的第一步和最后一步:把主机上的数据变成张量,算完再读回主机。
版本:PyTorch 2.15.0(CPU)、burn 0.22.1(Flex 设备)
flowchart TB lit["Rust 数组字面量"] --> td["TensorData"] vec["Vec 加形状"] --> td td -->|from_data| chk["秩检查 运行期"] chk --> t["Tensor D K"] t -->|reshape| t2["Tensor D2 K"] t -->|into_data| host["主机 Vec"] rank["D 写错"] -.->|编译期| err["E0308"]
本页的对照是一对程序:PyTorch 侧 tensora_create.py,burn 侧实验 crate tensora-t 的 create(依赖 burn =0.22.1,default-features = false,开 std、flex、ndarray、autodiff、optim;debug 构建)。平台 M2 Max、macOS 27、rustc 1.99.0。panic 信息里 cargo 缓存的路径前缀用 ... 略去。
Tensor<D, K> 的两个类型参数和两个运行期的值各管什么,burn 卷的 Tensor 与 Kind 讲过:秩 D 和种类 K(Float / Int / Bool)在类型上,后端和 dtype 在运行期。本页从 PyTorch 的写法出发,看这个分工在创建张量时带来哪些不同。
从字面量创建
a = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
b = torch.tensor([[1, 2], [3, 4]])
r = torch.arange(6)
torch.arange(0, 10, 3)
rf = torch.arange(0, 1, 0.25)
z = torch.zeros(2, 3)
o = torch.ones(2, 3, dtype=torch.int64)
f = torch.full((2, 3), 7)
let a = Tensor::<2>::from_floats([[1.0, 2.0], [3.0, 4.0]], &device);
let b = Tensor::<2>::from_data([[1, 2], [3, 4]], &device);
let c = Tensor::<2, Int>::from_data([[1, 2], [3, 4]], &device);
let r = Tensor::<1, Int>::arange(0..6, &device);
let rs = Tensor::<1, Int>::arange_step(0..10, 3, &device);
let rf = Tensor::<1, Int>::arange(0..4, &device).float() * 0.25;
let z = Tensor::<2>::zeros([2, 3], &device);
let o = Tensor::<2, Int>::ones([2, 3], &device);
let f = Tensor::<2>::full([2, 3], 7, &device);
tensor floats [2, 2] torch.float32
tensor ints [2, 2] torch.int64
arange(6) [6] torch.int64 [0, 1, 2, 3, 4, 5]
arange(0,10,3) [0, 3, 6, 9]
arange(0,1,.25) torch.float32 [0.0, 0.25, 0.5, 0.75]
zeros [2, 3] torch.float32 | ones int torch.int64 | full 7 torch.int64 [7, 7, 7, 7, 7, 7]
from_floats dims [2, 2] F32
from_data ints dims [2, 2] F32 [1.0, 2.0, 3.0, 4.0]
Int from_data dims [2, 2] I32
arange 0..6 dims [6] I32 [0, 1, 2, 3, 4, 5]
arange_step 3 [0, 3, 6, 9]
float arange F32 [0.0, 0.25, 0.5, 0.75]
zeros [2, 3] F32 | ones Int I32 | full 7 F32 [7.0, 7.0, 7.0, 7.0, 7.0, 7.0]
概念上是同一件事:给值、给形状,得到一个张量。不同在于「整数还是浮点」由谁决定。
- PyTorch 看值:
[[1, 2], [3, 4]]是 int64,torch.full((2, 3), 7)因为填充值是整数也成了 int64。 - burn 看类型:
Tensor::<2>的种类默认是Float,同样的整数字面量经from_data进来变成F32;要整数就写Tensor::<2, Int>。full的填充值7只是被转换成这个种类的元素。 - 具体是
F32还是F64、I32还是I64,由设备决定(Flex 上整数是I32,PyTorch 是 int64),见下一页 dtype 与设备。
arange 只定义在 Tensor<1, Int> 上,参数是 Range<i64>;arange_step 的步长是 usize。浮点的等差序列没有直接对应,先建整数序列再 .float() 乘步长。
打印张量也不一样。print(x) 只给值;burn 的 Display 把形状、设备、种类、dtype 一起打出来:
tensor([[0., 1., 2.],
[3., 4., 5.]])
Tensor {
data:
[[0.0, 1.0, 2.0],
[3.0, 4.0, 5.0]],
shape: [2, 3],
device: Device<Flex(Cpu)>,
kind: "Float",
dtype: "f32",
}
长度到运行期才知道的数据
PyTorch 的 torch.tensor 接受任意嵌套的列表,形状在运行期从列表里数出来,参差不齐时报错:
torch.tensor([[1.0, 2.0], [3.0]])
ValueError: expected sequence of length 2 at dim 1 (got 1)
burn 的字面量是 Rust 数组,长度在类型里,参差不齐的数组根本写不出来:
error[E0308]: mismatched types
--> src/bin/ce_ragged.rs:5:49
|
5 | let a = Tensor::<2>::from_floats([[1.0, 2.0], [3.0]], &device);
| ^^^^^ expected an array with a size of 2, found one with a size of 1
从文件或网络读进来的数据是 Vec。Vec<f32> 不能直接交给 from_floats,报错把能转换成 TensorData 的类型列了出来:
error[E0277]: the trait bound `TensorData: From<Vec<f32>>` is not satisfied
--> src/bin/ce_vec.rs:6:36
|
6 | let a = Tensor::<2>::from_floats(v, &device);
| ------------------------ ^ the trait `From<Vec<f32>>` is not implemented for `TensorData`
| |
| required by a bound introduced by this call
|
= help: `TensorData` implements trait `From<T>`:
From<&[E]>
From<&[usize]>
From<[E; A]>
From<[[E; B]; A]>
From<[[[E; C]; B]; A]>
From<[[[[E; D]; C]; B]; A]>
From<[[[[[Elem; E]; D]; C]; B]; A]>
From<[usize; A]>
...
切片 &[E] 只能变成一维。多维要把形状单独给出,用 TensorData::new:
let v: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let m = Tensor::<2>::from_data(TensorData::new(v.clone(), [2, 3]), &device);
let s1 = Tensor::<1>::from_floats(v.as_slice(), &device);
TensorData::new dims [2, 3]
from &[f32] dims [6]
秩在类型上
PyTorch 的形状完全是运行期的值:t.shape 是 torch.Size,t.dim() 是一个整数。burn 里秩是常量泛型 D,形状的长度因此也在类型上:
t = torch.zeros(2, 3, 4)
print("shape", t.shape, "dim", t.dim(), "numel", t.numel())
let t = Tensor::<3>::zeros([2, 3, 4], &device);
println!("dims {:?} shape {:?} rank {} D {} numel {}", t.dims(), t.shape(), t.rank(), 3, t.shape().num_elements());
shape torch.Size([2, 3, 4]) dim 3 numel 24
dims [2, 3, 4] shape Shape { dims: [2, 3, 4] } rank 3 D 3 numel 24
dims() 返回 [usize; D],可以直接解构成 let [b, n, d] = t.dims();;shape() 返回运行期的 Shape。
编译期:秩对不上
reshape 的目标秩从数组长度推出来。写错了,错误出在编译期:
let a = Tensor::<2>::zeros([2, 3], &device);
let b: Tensor<3> = a.reshape([3, 2]);
error[E0308]: mismatched types
--> src/bin/ce_reshape.rs:6:22
|
6 | let b: Tensor<3> = a.reshape([3, 2]);
| --------- ^^^^^^^^^^^^^^^^^ expected `3`, found `2`
| |
| expected due to this
|
= note: expected struct `Tensor<3>`
found struct `Tensor<2>`
函数签名同理。一个只收 Tensor<2> 的函数,传进一个带 batch 维的 Tensor<3>:
fn rows(x: Tensor<2>) -> usize {
x.dims()[0]
}
// ...
let batch = Tensor::<3>::zeros([8, 2, 3], &device);
println!("{}", rows(batch));
error[E0308]: mismatched types
--> src/bin/ce_fnrank.rs:10:23
|
10 | println!("{}", rows(batch));
| ---- ^^^^^ expected `2`, found `3`
| |
| arguments to this function are incorrect
|
= note: expected struct `Tensor<2>`
found struct `Tensor<3>`
...
报错里没有「rank」这个词,只有 expected `3`, found `2`:那个数字就是秩。PyTorch 的张量没有带秩的类型,同样的错误要等程序运行到用这个张量的算子才暴露。
运行期:数据的形状
秩在类型上,数据的形状却在 TensorData 里,是运行期的值。两者对不上时,from_data 在运行期 panic:
let a = Tensor::<2>::from_floats([1.0, 2.0, 3.0], &device);
thread 'main' (8547747) panicked at .../burn-tensor-0.22.1/src/tensor/api/base.rs:2347:9:
=== Tensor Operation Error ===
Operation: 'From Data'
Reason:
1. Given dimensions differ from the tensor rank. Tensor rank: '2', given dimensions: '[3]'.
这条检查是 from_data 的第一行 TensorCheck::creation_ops,zeros、ones、full、empty 也走它。检查怎么攒错误、为什么 release 下也开着,见 burn 卷的检查与报错。
问: 秩不是在类型上吗?Tensor::<2>::from_floats([1.0, 2.0, 3.0], &device) 为什么能编译过?
答: 因为数组先变成了 TensorData。from_floats 的参数是 A: Into<TensorData>,[f32; 3] 满足它;TensorData 的形状是运行期的 Shape,数组的维数在这一步就从类型里消失了。之后能比较的只有 D 和 Shape 的长度,只能在运行期比。reshape 不一样:它的参数类型 [I; D2] 直接把数组长度和目标秩绑在一起,所以写错是编译错误。
同一个检查还拒绝 0 维张量。PyTorch 的 torch.tensor(3.0) 是一个形状为 [] 的张量;burn 里 Tensor<0> 这个类型写得出来,建不出来:
0-dim tensor: [] 0 tensor(3.)
thread 'main' (8547763) panicked at .../burn-tensor-0.22.1/src/tensor/api/base.rs:2347:9:
=== Tensor Operation Error ===
Operation: 'From Data'
Reason:
1. Tried to create a 0-dim tensor, which is invalid. Tensor rank: '0', given dimensions: '[]'.
所以 burn 的全局归约返回 [1] 而不是标量,见归约与广播。TensorData::new 自己还检查元素个数和形状是否相符,TensorData::new(vec![1.0f32, 2.0, 3.0], [2, 2]):
thread 'main' (8547759) panicked at .../burn-std-0.22.1/src/data/tensor/base.rs:340:9:
assertion `left == right` failed: Shape Shape { dims: [2, 2] } is invalid for input of size 3
left: 4
right: 3
#2137「Add 0-dim tensor checks for creation ops and validate TensorData shape w/ num values」(首次发布于 v0.14.0)之前,只有 from_data 查秩,检查函数叫 TensorCheck::from_data;zeros、ones、full、empty 直接把形状交给后端;TensorData::new 也不比较元素个数和形状。这个 PR 把检查改名为 creation_ops、加上 ops 参数和 if D == 0 那一条,让四个创建函数都先过它;TensorData::new 里加了 assert_eq!(shape_numel, numel, "Shape {:?} is invalid for input of size {:?}", ...)。提交说明只有标题,没写原因。0.22.1 里这条断言搬进了 check_data_len,措辞没变。
reshape 与 -1
x = torch.arange(6, dtype=torch.float32).reshape(2, 3)
x.reshape(-1)
x.reshape(-1, 2)
x.T.reshape(-1)
torch.zeros(2, 3, 4).flatten()
let x = Tensor::<1, Int>::arange(0..6, &device).float().reshape([2, 3]);
let flat: Tensor<1> = x.clone().reshape([-1]);
let y = x.clone().reshape([-1, 2]);
let tr = x.clone().transpose().reshape([-1]);
let fl: Tensor<1> = Tensor::<3>::zeros([2, 3, 4], &device).flatten(0, 2);
reshape(-1) [6]
reshape(-1, 2) [3, 2]
transpose then reshape(-1) [0.0, 3.0, 1.0, 4.0, 2.0, 5.0]
flatten() [24]
reshape [-1] dims [6]
reshape [-1, 2] dims [3, 2]
transpose then reshape [-1] [0.0, 3.0, 1.0, 4.0, 2.0, 5.0]
flatten(0, 2) dims [24]
-1 的意思两边一样:由元素个数推出这一维。转置之后再 reshape,两边都按逻辑顺序读出 [0, 3, 1, 4, 2, 5]。PyTorch 的 view 在不连续的张量上会报错(view size is not compatible with input tensor's size and stride ... Use .reshape(...) instead.);burn 没有 view,只有 reshape,转置过的张量照样能 reshape。flatten 在 burn 里要写起止维和目标秩:flatten::<1>(0, 2)。
错误的写法,两边的报错意思相同:
reshape(4, 2): RuntimeError: shape '[4, 2]' is invalid for input of size 6
reshape(-1, -1):RuntimeError: only one dimension can be inferred
thread 'main' (8547751) panicked at .../burn-tensor-0.22.1/src/tensor/api/check.rs:1394:13:
=== Tensor Operation Error ===
Operation: 'Reshape'
Reason:
1. The given shape doesn't have the same number of elements as the current shape. Current shape: [6], target shape: [4, 2].
thread 'main' (8547755) panicked at .../burn-tensor-0.22.1/src/tensor/api/check.rs:1394:13:
=== Tensor Operation Error ===
Operation: 'Reshape'
Reason:
1. Repeated -1 in reshape
这两条原因来自 cubecl 的 Shape::reshape,burn 的 ReshapeArgs 把它的 Err 交给 unwrap_shape_reshape,再包成 TensorCheck 的格式 panic。所以 panic 的位置是 check.rs:1394,不是 reshape 本身。
Tensor::<3>::zeros([1, 3, 4], &device).reshape([0, -1]) 的 dims() 是多少?PyTorch 里 torch.zeros(1, 3, 4).reshape(0, -1) 呢?平台同上,burn 0.22.1,Flex 设备。读回主机
x.tolist()
x.sum().item()
torch.tensor([1.0, 2.0]).item()
let v: Vec<f32> = x.clone().into_data().try_to_vec::<f32>().unwrap();
let s: f32 = x.clone().sum().into_scalar();
let ints = Tensor::<1, Int>::arange(0..3, &device);
ints.to_data().try_to_vec::<i64>();
ints.to_data().try_to_vec::<i32>();
ints.to_data().try_to_vec_as::<i64>();
ints.to_data().convert::<i64>().try_to_vec::<i64>();
ints.sum().into_scalar::<i64>();
tolist [[0.0, 1.0, 2.0], [3.0, 4.0, 5.0]]
sum().item() 15.0 float
item() of 2 elems:RuntimeError: a Tensor with 2 elements cannot be converted to Scalar
to_vec [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]
sum into_scalar 15
Int try_to_vec::<i64> Err(DTypeMismatch { expected: I64, actual: I32 })
Int convert::<i64> Ok([0, 1, 2])
Int try_to_vec::<i32> Ok([0, 1, 2])
Int try_to_vec::<f32> Err(DTypeMismatch { expected: F32, actual: I32 })
Int try_to_vec_as::<i64> Ok([0, 1, 2])
Int into_scalar::<i64> of sum 3
tolist() 保留嵌套结构,try_to_vec 给的是按行展开的一维 Vec,形状要另外从 dims() 拿。PyTorch 的 tolist() 和 item() 把元素换成 Python 的 int / float,不用关心 dtype;burn 里有两类方法:
try_to_vec::<E>()不做转换,E必须和存储的 dtype 完全一致,否则返回DTypeMismatch。Flex 上的Int是I32,所以try_to_vec::<i64>()失败。try_to_vec_as::<E>()、convert::<E>()先转换再取;into_scalar也会转换,I32的和可以按i64取出。
0.22 起旧的 to_vec 标成了弃用,编译时会提示:
warning: use of deprecated method `burn_std::data::tensor::conversion::<impl burn::tensor::TensorData>::to_vec`: use try_to_vec::<E>()
into_scalar 要求恰好一个元素,否则 panic。它带 #[track_caller],panic 位置落在调用它的那一行:
thread 'main' (8547767) panicked at src/bin/create.rs:86:22:
Error while reading data: use `try_into_scalar` instead to catch the error at runtime: InvalidShape { expected: 1, actual: 2 }
动脑筋答案
reshape [0, -1] of [1,3,4] dims [1, 12]
reshape(0, -1) of [1,3,4]:RuntimeError: shape '[0, -1]' is invalid for input of size 12
burn 给出 [1, 12],PyTorch 报错。burn 0.22.1 的数组形式里,0 表示「沿用原来这一维的大小」:cubecl Shape::reshape 遇到 s == 0 就取 self.dims[idx],这里是 1,剩下的 -1 推出 12。PyTorch 的 0 是字面上的 0,[0, x] 的元素个数是 0,和 12 对不上。从 PyTorch 搬过来的形状数组里如果有 0,两边的意思不同。
这一点在 0.22.1 发布之后的 main 上已经改了:#6257「make reshape zeros literal and add ZeroDimPlaceholders」让数组里的 0 变成字面上的 0(和 PyTorch 一致),原来的行为改用 reshape(ZeroDimPlaceholders([0, -1])) 显式要。截至本页写作时它还没有进任何发布版本。
小结
- PyTorch 从值推断 dtype;burn 的种类(
Float/Int)写在类型上,具体位宽由设备决定。 - 字面量是 Rust 数组,参差不齐写不出来;
Vec要配上形状走TensorData::new。 - 秩写错(
reshape目标、函数签名)是编译错误,报expected `3`, found `2`;数据形状和秩对不上、0 维张量是from_data里的运行期 panic。 reshape的-1两边一样;0.22.1 里数组中的0表示沿用原维度,PyTorch 是字面 0。- 读回:
try_to_vec::<E>不转换、dtype 必须一致;try_to_vec_as、convert、into_scalar会转换。