Rust in Everything
基于 candle 0.11.0

形状与广播

主线位置:张量主线的中间一步「计算」:两个形状不同的张量怎么逐元素运算、怎么做矩阵乘,以及改形状、换维度这些只动布局的操作。

版本:PyTorch 2.15.0(CPU)、candle 0.11.0(CPU,accelerate;expand 之后的 matmul 另测了不开 accelerate 的 CPU 和 Metal)

flowchart TB
  op["a 加 row"] --> same{"形状完全相同"}
  same -->|否| err["shape mismatch"]
  same -->|是| run["逐元素计算"]
  bc["broadcast_add"] --> bs["算广播形状"]
  bs --> view["broadcast_as 视图"]
  view --> run
  mm["matmul"] --> rank{"秩相同且至少 2"}
  rank -->|否| err
  rank -->|是| mmrun["按批相乘"]
  bmm["broadcast_matmul"] --> copy["广播一边并复制"]
  copy --> mmrun

本页的对照:PyTorch 侧是实验代码里的 shape.py 和 linear_ref.py(torch.set_num_threads(1)),candle 侧是实验 crate tensora-t 的 shape、linear、expand_mm 和几个只用来看编译错误的 ce_*(features metal accelerate,release),以及去掉 accelerate 的同样代码 tensora-noacc。平台 M2 Max、macOS 27、rustc 1.99.0。编译错误里 cargo 缓存的路径前缀用 ... 略去。布局、步长、视图在 candle 内部怎么实现,见源码卷的布局与视图。下面的 a 是 arange(6) 排成的 [2, 3] 浮点矩阵,row 是 [10, 20, 30],col 是 [[100], [200]]。

+ 不广播

a + row; a + col; col + row; a + torch.ones(2, 2)
&a + &row;
a.broadcast_add(&row); a.broadcast_add(&col); col.broadcast_add(&row);
a.broadcast_add(&Tensor::ones((2, 2), DType::F32, &dev)?);
a + row                            [2, 3] [10.0, 21.0, 32.0, 13.0, 24.0, 35.0]
a + col                            [2, 3] [100.0, 101.0, 102.0, 203.0, 204.0, 205.0]
col + row                          [2, 3] [110.0, 120.0, 130.0, 210.0, 220.0, 230.0]
a + ones(2, 2)                     RuntimeError: The size of tensor a (3) must match the size of tensor b (2) at non-singleton dimension 1
a + row                            Err: shape mismatch in add, lhs: [2, 3], rhs: [3]
a.broadcast_add(&row)              [2, 3] [10.0, 21.0, 32.0, 13.0, 24.0, 35.0]
a.broadcast_add(&col)              [2, 3] [100.0, 101.0, 102.0, 203.0, 204.0, 205.0]
col.broadcast_add(&row)            [2, 3] [110.0, 120.0, 130.0, 210.0, 220.0, 230.0]
a.broadcast_add(ones(2, 2))        Err: shape mismatch in broadcast_add, lhs: [2, 3], rhs: [2, 2]

广播规则两边相同(从最后一维对齐,每一维相等或有一边是 1,秩不够的在前面补 1),candle 只是不在运算符里做。+ - * / 和 add、mul 这些方法要求两边形状完全相同,[2, 3] 和 [2, 1] 也不行;要广播就换成 broadcast_add、broadcast_sub、broadcast_mul、broadcast_div,以及 broadcast_maximum、broadcast_eq 这类。它们由 broadcast_binary_op! 生成:先用 Shape::broadcast_shape_binary_op 算出目标形状,再把需要扩展的一边 broadcast_as 成视图,最后调普通的 add。错误信息里的 op 名是一个线索:add 说明漏了 broadcast_,broadcast_add 才是形状真的不兼容。完整的报错分析见源码卷的坑 shape mismatch in add。

burn 的 + 会广播,但要求两边秩相同(见 burn 卷的归约与广播);candle 正好反过来,秩不同可以(broadcast_add 自己补 1),只是要换方法名。

和标量运算不受限制:&a + 1.0 走 affine,见创建与 dtype。0 维张量不是标量,a + Tensor::new(5f32, ..) 同样要 broadcast_add。

expand 与 broadcast_as

row.expand(2, 3); row.expand(-1, 3); col.expand(-1, 3); row.expand(3, 2)
row.expand(2, 3).stride()
row.broadcast_as((2, 3)); row.expand((2, 3)); col.broadcast_as((2, 3));
row.broadcast_as((3, 2)); row.broadcast_left(2);
row.broadcast_as((2, 3))?.stride();
row.expand(2, 3)                   [2, 3] [10.0, 20.0, 30.0, 10.0, 20.0, 30.0]
row.expand(-1, 3)                  RuntimeError: The expanded size of the tensor (-1) isn't allowed in a leading, non-existing dimension 0
col.expand(-1, 3)                  [2, 3] [100.0, 100.0, 100.0, 200.0, 200.0, 200.0]
row.expand(3, 2)                   RuntimeError: The expanded size of the tensor (2) must match the existing size (3) at non-singleton dimension 1.  Target sizes: [3, 2].  Tensor sizes: [3]
row.expand(2, 3) layout            dims=[2, 3] stride=[0, 1] contiguous=False
row.broadcast_as((2, 3))           [2, 3] [10.0, 20.0, 30.0, 10.0, 20.0, 30.0]
row.expand((2, 3))                 [2, 3] [10.0, 20.0, 30.0, 10.0, 20.0, 30.0]
col.broadcast_as((2, 3))           [2, 3] [100.0, 100.0, 100.0, 200.0, 200.0, 200.0]
row.broadcast_as((3, 2))           Err: cannot broadcast [3] to [3, 2]
row.broadcast_left(2)              [2, 3] [10.0, 20.0, 30.0, 10.0, 20.0, 30.0]
row.broadcast_as((2, 3)) layout    dims=[2, 3] stride=[0, 1] contiguous=false

expand 就是 broadcast_as 的别名,结果同样是步长为 0 的视图,不复制。区别在形状参数:candle 的形状是 usize,没有 PyTorch 的「-1 表示这一维不变」,目标形状要写全,col.expand(-1, 3) 写成 col.broadcast_as((col.dim(0)?, 3))。只在左边加维度时用 broadcast_left,参数是要加在前面的那几维。

matmul:不接一维,不广播批维

v = torch.ones(3)
w = torch.arange(12, dtype=torch.float32).reshape(3, 4)
a @ w; a @ v; v @ w
h = torch.ones(4, 2, 3)
h @ w; h @ torch.ones(1, 3, 4)
let v = Tensor::new(&[1f32, 1., 1.], &dev)?;
let w = Tensor::arange(0f32, 12., &dev)?.reshape((3, 4))?;
a.matmul(&w); a.matmul(&v); a.matmul(&v.unsqueeze(1)?)?.squeeze(1); v.matmul(&w);
let h = Tensor::ones((4, 2, 3), DType::F32, &dev)?;
h.matmul(&w); h.broadcast_matmul(&w);
let w3 = Tensor::ones((1, 3, 4), DType::F32, &dev)?;
h.matmul(&w3); h.broadcast_matmul(&w3);
a @ w                              [2, 4] [20.0, 23.0, 26.0, 29.0, 56.0, 68.0, 80.0, 92.0]
a @ v                              [2] [3.0, 12.0]
v @ w                              [4] [12.0, 15.0, 18.0, 21.0]
h(4,2,3) @ w(3,4)                  [4, 2, 4]
h @ w(1,3,4)                       [4, 2, 4]
a.matmul(&w)                       [2, 4] [20.0, 23.0, 26.0, 29.0, 56.0, 68.0, 80.0, 92.0]
a.matmul(&v)                       Err: shape mismatch in matmul, lhs: [2, 3], rhs: [3]
a.matmul(&v.unsqueeze(1))          [2, 1] [3.0, 12.0]
..squeeze(1)                       [2] [3.0, 12.0]
v.matmul(&w)                       Err: shape mismatch in matmul, lhs: [3], rhs: [3, 4]
h(4,2,3).matmul(&w(3,4))           Err: shape mismatch in matmul, lhs: [4, 2, 3], rhs: [3, 4]
h.broadcast_matmul(&w)             [4, 2, 4]
h.matmul(&w(1,3,4))                Err: shape mismatch in matmul, lhs: [4, 2, 3], rhs: [1, 3, 4]
h.broadcast_matmul(&w(1,3,4))      [4, 2, 4]

PyTorch 的 @ 有三条额外规则:一维的一边先补成矩阵、乘完再去掉;秩不同时在前面补 1;批维按广播规则对齐。Tensor::matmul 一条都没有:两边秩必须相同且至少为 2,批维只比乘积((2, 3) 对 (3, 2) 不报错,见源码卷的坑 shape mismatch in add)。对应关系:

PyTorchcandle
a @ v(v 一维)a.matmul(&v.unsqueeze(1)?)?.squeeze(1)
v @ wv.unsqueeze(0)?.matmul(&w)?.squeeze(0)
h @ w([b, s, i] 乘 [i, o])h.broadcast_matmul(&w),或 h.reshape(((), i))?.matmul(&w)?.reshape((b, s, ()))
批维广播 [4, 2, 3] @ [1, 3, 4]broadcast_matmul

broadcast_matmul 把被广播的一边 broadcast_as 之后再 contiguous(),真的复制出 b 份(源码里留着「TODO: Avoid concretising the broadcasted matrixes via contiguous.」)。第二种写法把批维并进行,一次二维 matmul,不复制,candle_nn::Linear 对连续输入就是这样做的。

expand 之后直接 matmul

从 PyTorch 搬过来,一个很自然的省事写法是先 expand 出批维再乘,也就是 lhs.expand(b, m, k) @ rhs。candle 里写成 broadcast_as 加 matmul,形状检查能过(秩相同、批维乘积相同):

let (b, m, k, n) = (4, 2, 3, 5);
let lhs = Tensor::arange(0f32, (m * k) as f32, &dev)?.reshape((1, m, k))?;
let rhs = Tensor::arange(0f32, (b * k * n) as f32, &dev)?.reshape((b, k, n))?.affine(0.01, 0.)?;
let view = lhs.broadcast_as((b, m, k))?;
let fast = view.matmul(&rhs)?;
let copied = view.contiguous()?.matmul(&rhs)?;
let bm = lhs.broadcast_matmul(&rhs)?;

开 accelerate 的 CPU(tensora-t)和 Metal 上,三种写法结果相同;不开 accelerate 的 CPU(tensora-noacc,走 gemm crate,Linux 上的默认情况):

Cpu lhs view stride [0, 3, 1]
expand+matmul vs contiguous: max|diff| = 6.5799994
broadcast_matmul vs contiguous: max|diff| = 0
expand+matmul batch 3 = [[0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0]]
contiguous    batch 3 = [[1.6, 1.63, 1.66, 1.6899999, 1.7199999], [6.1, 6.22, 6.3399997, 6.46, 6.5799994]]

PyTorch 对同一个 expand 视图做 @,和先 contiguous() 的结果差 0.0。candle 这边没有报错,最后一批全是 0。原因在 gemm 那一支的 MatMul 里合并批维的几行:

        let (b, m, n, k) = if b_skip == 0 && a_skip == m * k {
            // a_skip and c_skip should be updated but step is always 0 so
            // it wouldn't matter.
            (1, b * m, n, k)
        } else if a_skip == 0 && b_skip == n * k {
            (1, m, b * n, k)
        } else {
            (b, m, n, k)
        };

第一支(右边的批步长为 0)把批并进行,这是对的:左边各批的行在内存里本来就首尾相接。第二支(左边的批步长为 0,正是 expand 出来的情况)把批并进列,按一个 m × (b·n) 的矩阵算一次;但右边是 [b, k, n] 按批排的,各批的列在内存里并不相邻,输出的行步长也还是 n,算出来的数互相覆盖,没写到的位置保持初始的 0。这是 0.11.0 的上游 bug。accelerate 和 Metal 的实现没有这条合并,所以不受影响。在 gemm 后端上要广播左边的批维,用 broadcast_matmul(它先复制)或者自己 contiguous()。

reshape:() 就是 -1

x = torch.arange(24, dtype=torch.float32)
x.reshape(2, -1, 4); x.reshape(-1, 6); x.reshape(2, -1, -1)
x.reshape(5, -1); x.reshape(5, 5); x.reshape(2, 2, 2, 3, 1, -1)
e = torch.zeros(0)
e.reshape(0, -1); e.reshape(-1, 3)
let x = Tensor::arange(0f32, 24., &dev)?;
x.reshape((2, 3, 4)); x.reshape((2, (), 4)); x.reshape(((), 6)); x.reshape(((),));
x.reshape((5, ())); x.reshape((5, 5)); x.reshape(vec![2usize, 12]);
let empty = Tensor::zeros(0, DType::F32, &dev)?;
empty.reshape((0, ())); empty.reshape(((), 3));
x.reshape(2, -1, 4)                [2, 3, 4]
x.reshape(-1, 6)                   [4, 6]
x.reshape(2, -1, -1)               RuntimeError: only one dimension can be inferred
x.reshape(5, -1)                   RuntimeError: shape '[5, -1]' is invalid for input of size 24
x.reshape(5, 5)                    RuntimeError: shape '[5, 5]' is invalid for input of size 24
x.reshape(2,2,2,3,1,-1)            [2, 2, 2, 3, 1, 1]
empty.reshape(0, -1)               RuntimeError: cannot reshape tensor of 0 elements into shape [0, -1] because the unspecified dimension size -1 can be any value and is ambiguous
empty.reshape(-1, 3)               [0, 3]
x.reshape((2, 3, 4))               [2, 3, 4]
x.reshape((2, (), 4))              [2, 3, 4]
x.reshape(((), 6))                 [4, 6]
x.reshape(((),))                   [24]
x.reshape((5, ()))                 Err: cannot reshape tensor with 24 elements to (5, ())
x.reshape((5, 5))                  Err: shape mismatch in reshape, lhs: [24], rhs: [5, 5]
x.reshape(vec![2, 12])             [2, 12]
empty.reshape((0, ()))             Err: cannot reshape tensor of 0 elements to (0, ())
empty.reshape(((), 3))             [0, 3]

candle 的「自动推断这一维」写成元组里的一个 ()(单元类型)。reshape 的参数类型是 ShapeWithOneHole:普通形状((2, 3, 4)、vec![2, 12],凡是 Into<Shape> 的)原样通过;对 ((),)、(usize, ())、((), usize, usize) 这些元组,shape.rs 一个个手写了实现,「洞」的大小由 hole_size 算出:总元素数除以其余各维的乘积,除不尽报 cannot reshape tensor with ...,其余维乘积为 0 报 cannot reshape tensor of 0 elements ...(和 PyTorch 拒绝 reshape(0, -1) 是同一个理由)。

和 PyTorch 的 -1 有三点不同,都在编译期:

  • 只能有一个洞,因为两个洞的元组没有实现。PyTorch 是运行期报 only one dimension can be inferred,candle 是编译错误:

    error[E0277]: the trait bound `Shape: From<({integer}, (), ())>` is not satisfied
        --> src/bin/ce_twohole.rs:5:21
         |
       5 |   let b = a.reshape((2, (), ()))?;
         |             ------- ^^^^^^^^^^^ the trait `From<({integer}, (), ())>` is not implemented for `Shape`
         |             |
         |             required by a bound introduced by this call
    ...
         = note: required for `({integer}, (), ())` to implement `Into<Shape>`
         = note: required for `({integer}, (), ())` to implement `ShapeWithOneHole`
    note: required by a bound in `candle_core::Tensor::reshape`
        --> .../candle-core-0.11.0/src/tensor.rs:2523:23
    

    编译器没找到带洞的实现,退回去试「普通形状」那条 Into<Shape>,报的是 Shape: From<..> 不满足,不提「洞」。

  • 带洞的元组最多五维。(2, 2, 2, 2, 2, ()) 报的是同一种错误(Shape: From<({integer}, {integer}, {integer}, {integer}, {integer}, ())>);六维以上要把形状算好,写成 vec![..]。

  • 写 -1 本身不行。形状的元素是 usize,编译器报 usize: std::ops::Neg 不满足,并建议写成 -1isize,照做也不会通过,因为形状不接受 isize:

    error[E0277]: the trait bound `usize: std::ops::Neg` is not satisfied
     --> src/bin/ce_minus1.rs:5:25
      |
    5 |   let b = a.reshape((3, -1))?;
      |                         ^^ the trait `std::ops::Neg` is not implemented for `usize`
      |
    help: consider specifying an integer type that can be negative
    

D::Minus1 也不能用在这里,它只用于「第几维」这种参数(sum(D::Minus1)、transpose(D::Minus2, D::Minus1))。

📝NOTE
冷知识:reshape 原先只接受写全的形状

#770「Shape with holes」之前,reshape 的签名是 pub fn reshape<S: Into<Shape>>(&self, shape: S),文档注释上面挂着一行 // TODO: Do we want to allow target shape using -1 on some dimensions?。这个 PR 删掉了这行 TODO,把参数换成 ShapeWithOneHole,在 shape.rs 里加了 168 行:这个 trait 和一到五维各种洞的位置的实现,每个实现自己检查整除(报 tensor number of elements {el_count} is not divisible by {d1};共用的 hole_size 和现在的报错文字是之后 #1096「Improve the reshape error messages.」改的);文档注释加了一句「the behavior is the same as when using -1 in PyTorch」和例子 a.reshape((2, (), 1))。提交日期是 2023-09-08,早于本地仓库最早的 tag,git tag --contains 只能查到 0.5.1。0.9.0 的 #2905 把同一个 trait 用到了 from_vec 上,顺带丢了元素个数检查,见源码卷错误的冷知识。

reshape 会悄悄复制,没有 view

m = x.reshape(4, 6)
m.t(); m.t().reshape(24); m.t().reshape(24)[:6]; m.t().view(24)
let m = x.reshape((4, 6))?;
let mt = m.t()?;
let r = mt.reshape(24)?;
m.reshape((6, 4))?;
m.t()                              dims=[6, 4] stride=[1, 6] contiguous=False
m.t().reshape(24)                  dims=[24] stride=[1] contiguous=True
m.t().reshape(24) first 6          [0.0, 6.0, 12.0, 18.0, 1.0, 7.0]
m.t().view(24)                     RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
m.t()                              dims=[6, 4] stride=[1, 6] contiguous=false
m.t().reshape(24)                  dims=[24] stride=[1] contiguous=true
m.t().reshape(24) first 6          [0.0, 6.0, 12.0, 18.0, 1.0, 7.0]
m.reshape((6, 4)) (view)           dims=[6, 4] stride=[4, 1] contiguous=true

两边的 reshape 语义相同:按行主序的逻辑顺序重排,输入连续时是视图,不连续时复制一份。candle 没有 view,也就没有「不能视图就报错」这一档;想确认没有复制,看输入的 is_contiguous()。flatten_all() 对应 flatten(),flatten(1, 2) 对应 flatten(1, 2)。

transpose、permute、unsqueeze

x3 = x.reshape(2, 3, 4)
row.t(); x3.transpose(-1, -2); x3.permute(2, 0)
x3.squeeze(0); x3[None]; x3.transpose(0, 3)
let x3 = x.reshape((2, 3, 4))?;
row.t(); x3.transpose(D::Minus1, D::Minus2); x3.permute((2, 0, 1)); x3.permute((2, 0));
x3.unsqueeze(0); x3.unsqueeze(D::Minus1); x3.squeeze(0); x3.transpose(0, 3);
row.t()                            [3] [10.0, 20.0, 30.0]
x3.transpose(-1, -2)               [2, 4, 3]
x3.permute(2, 0)                   RuntimeError: permute(sparse_coo): number of dimensions in the tensor input does not match the length of the desired ordering of dimensions i.e. input.dim() = 3 is not equal to len(dims) = 2
x3.squeeze(0)                      [2, 3, 4]
x3[None]                           [1, 2, 3, 4]
x3.transpose(0, 3)                 IndexError: Dimension out of range (expected to be in range of [-3, 2], but got 3)
row.t()                            Err: unexpected rank, expected: 2, got: 1 ([3])
x3.transpose(D::Minus1, D::Minus2) [2, 4, 3]
x3.permute((2, 0, 1))              [4, 2, 3]
x3.permute((2, 0))                 Err: dimension mismatch in permute, tensor [2, 3, 4], dims: [2, 0]
x3.unsqueeze(0)                    [1, 2, 3, 4]
x3.unsqueeze(D::Minus1)            [2, 3, 4, 1]
x3.squeeze(0)                      [2, 3, 4]
x3.transpose(0, 3)                 Err: transpose: dimension index 3 out of range for shape [2, 3, 4]

transpose、permute、squeeze、unsqueeze 两边是同样的视图操作,越界、不是排列时两边都报错。写法上有三点要改:

  • 负的维度下标写成 D::Minus1、D::Minus2、D::Minus(n)(D),因为维度参数也是 usize。
  • x[None]、x[:, None] 没有对应的索引写法,用 unsqueeze。squeeze(dim) 和 PyTorch 一样在那一维长度不为 1 时原样返回;没有不带参数、去掉所有长度为 1 的维的版本。
  • 一维向量的 t():PyTorch 原样返回,candle 报 unexpected rank。t() 在两边的定义并不相同,先看下面的动脑筋。
❗IMPORTANT
动脑筋:三维张量的 t()
x3 = torch.arange(24.0).reshape(2, 3, 4)
x3.t()
let x3 = Tensor::arange(0f32, 24., &dev)?.reshape((2, 3, 4))?;
x3.t()?
两边各得到什么?如果都成功,形状分别是多少?

转置之后的张量不连续,这一点两边相同:

x3.transpose(1, 2)                 dims=[2, 4, 3] stride=[12, 1, 4] contiguous=false
x3.transpose(1, 2).contiguous()    dims=[2, 4, 3] stride=[12, 3, 1] contiguous=true

不同的是后果。PyTorch 的算子几乎都接受任意步长,contiguous() 主要是为了 view 和性能;candle 有几处会直接报错:开 accelerate 的 CPU 和 Metal 上的 matmul 只接受行主序和转置两种步长,softmax_last_dim、KvCache::append 要求连续输入。哪些步长会出错、出什么错,见源码卷的坑 Invalid matmul arguments;单纯的 w.t() 交给 matmul 不用先 contiguous()。

对得上的数字

最后用一个带广播的计算把两边对一遍:y = x @ w.T + b,x 是 [4, 7, k],w 是 [32, k],b 是 [32],元素是 sin(n)。输入只在 PyTorch 侧生成,写进 JSON,candle 侧读同一份:

y = x @ w.T + b
// form "bcast"
let y = x.broadcast_matmul(&w.t()?)?.broadcast_add(&b)?;
// form "fold": fold batch and seq into rows
let y = x.reshape(((), i_))?.matmul(&w.t()?)?.reshape((b_, s_, ()))?.broadcast_add(&b)?;

k = 1024,最大 |y| 是 512.6。两种 candle 写法的结果相同;开不开 accelerate 不同:

candle CPU对 PyTorch 的最大绝对误差
accelerate0
gemm(不开 accelerate)7.02e-4

gemm 那一行,compare.py 容差 2e-3:

y                        n=896  max|diff|=7.02e-04
worst=7.02e-04 tol=2e-03 OK

开 accelerate 时差是 0,因为两边调的是同一个 BLAS:这份 PyTorch 的 torch.__config__.show() 写着 BLAS_INFO=accelerate,candle 的 accelerate feature 调 Accelerate 的 sgemm,同一个库、同样的输入,求和顺序相同。gemm crate 用自己的分块顺序,结果在 f32 的舍入范围内:两边离 f64 参考分别是 5.92e-4(PyTorch)和 3.17e-4(candle gemm)。每个输出是 1024 个乘积之和,最大的 Σ|x·w| 是 511.8,它乘上 f32 的 ε 再乘 √k 约 1.95e-3,容差 2e-3 由此而来。反向验证:把 PyTorch 结果的一个元素加 0.01,compare.py 报 max|diff|=9.94e-03、MISMATCH,退出码 1。k = 64 时两种 CPU 后端都和 PyTorch 差 0。

动脑筋答案

x3.t()                             RuntimeError: t() expects a tensor with <= 2 dimensions, but self is 3D
x3.t()                             [2, 4, 3]

PyTorch 报错,candle 成功,得到 [2, 4, 3],也就是 PyTorch 的 x3.mT 或 x3.transpose(-2, -1)。candle 的 t() 是 self.transpose(rank - 2, rank - 1),只在秩小于 2 时报错。反过来,PyTorch 里对一维向量 t() 原样返回,candle 报 unexpected rank, expected: 2, got: 1。搬代码时:二维权重的 w.t() 两边相同;注意力里的 k.transpose(-2, -1) 在 candle 里可以直接写成 k.t(),PyTorch 的 .mT 也对应它。

小结

  • + - * / 要求形状完全相同;广播用 broadcast_add 等,规则和 PyTorch 一样,秩不同也行。
  • expand 是 broadcast_as 的别名,形状要写全,没有 -1;broadcast_left 只在前面加维。
  • matmul 两边秩相同且至少二维,不接一维、不广播批维;用 unsqueeze / squeeze、broadcast_matmul(复制)或折叠批维(不复制)。
  • 0.11.0 的 gemm 后端上,左边是 expand 出来的批维时 matmul 静默算错,最后几批是 0;accelerate 和 Metal 不受影响。
  • reshape 的 -1 写成 ():只能一个、最多五维,违反时是编译错误;不连续输入会被复制,没有 view。
  • t() 转置最后两维(PyTorch 的 mT);负的维度下标写 D::Minus1。
  • x @ w.T + b(k = 1024):开 accelerate 时和 PyTorch 差 0(同一个 BLAS),gemm 差 7.02e-4,最大 |y| 512.6。

💬 关联讨论

发起讨论
加载中...