Rust in Everything
基于 candle 0.11.0

no method named topk found

主线位置:张量主线的「计算」一步,照着 transformers 的模型代码逐行翻译,碰到 PyTorch 常用、candle 没有的算子。
版本:PyTorch 2.15.0(CPU)、candle 0.11.0(CPU,accelerate)
flowchart TB
  py["PyTorch 算子"] --> tk["topk"]
  py --> mf["masked_fill"]
  py --> nz["nonzero"]
  py --> es["einsum"]
  tk --> a1["argsort gather"]
  mf --> a2["mask where_cond"]
  nz --> a3["读回主机自己数"]
  es --> a4["broadcast 或 matmul"]

实验代码:PyTorch 侧 ops.py,candle 侧 crate pitb-fail(会编译失败的那份)、pitb-core(只依赖 candle-core 的同一份),以及 crate pitb-exp 的 bin p25_ops(release)。平台 M2 Max、macOS 27、rustc 1.99.0。transformers 里这几个算子用得有多普遍,什么时候选 candle 数过。

PyTorch 的写法

x = torch.tensor([[0.1, 0.9, 0.3, 0.7], [0.5, 0.2, 0.8, 0.4]])
v, i = torch.topk(x, 2, dim=-1)
mask = torch.tensor([[False, True, False, True], [True, False, False, False]])
print("masked_fill =", x.masked_fill(mask, float("-inf")).tolist())
print("nonzero =", mask.nonzero().tolist())
print("einsum =", torch.einsum("i,j->ij", torch.tensor([1.0, 2.0]), torch.tensor([3.0, 4.0, 5.0])).tolist())
q = torch.arange(24, dtype=torch.float32).reshape(1, 2, 3, 4) / 24
print("einsum bhqd,bhkd->bhqk [0,1,2] =", torch.einsum("bhqd,bhkd->bhqk", q, q)[0, 1, 2].tolist())
topk values = [[0.8999999761581421, 0.699999988079071], [0.800000011920929, 0.5]] indices = [[1, 3], [2, 0]]
masked_fill = [[0.10000000149011612, -inf, 0.30000001192092896, -inf], [-inf, 0.20000000298023224, 0.800000011920929, 0.4000000059604645]]
nonzero = [[0, 1], [0, 3], [1, 0]]
einsum = [[3.0, 4.0, 5.0], [6.0, 8.0, 10.0]]
einsum bhqd,bhkd->bhqk [0,1,2] = [2.024305582046509, 2.621527671813965, 3.21875]

搬到 candle 的现象

照着写:

let _top = x.topk(2)?;
let _filled = x.masked_fill(&mask, f32::NEG_INFINITY)?;
let _nz = mask.nonzero()?;
let _outer = candle_core::einsum("i,j->ij", &[&x, &x])?;

在一个同时依赖 candle-transformers 的 crate 里(pitb-fail,cargo build --color never;只截取 einsum 和 topk 两条,指向 cargo 缓存里 deepseek2.rs 的那一行换成了 ...):

error[E0425]: cannot find function `einsum` in crate `candle_core`
  --> src/bin/missing.rs:10:29
   |
10 |   let _outer = candle_core::einsum("i,j->ij", &[&x, &x])?;
   |                             ^^^^^^ not found in `candle_core`

error[E0599]: no method named `topk` found for struct `candle_core::Tensor` in the current scope
   --> src/bin/missing.rs:7:16
    |
  7 |   let _top = x.topk(2)?;
    |                ^^^^ method not found in `candle_core::Tensor`
    |
...
109 |     fn topk(&self, topk: usize) -> Result<TopKOutput>;
    |        ---- the method is available for `candle_core::Tensor` here
    |
    = help: items from traits can only be used if the trait is in scope
help: trait `TopKLastDimOp` which provides `topk` is implemented but not in scope; perhaps you want to import it
    |
  1 + use candle_transformers::models::deepseek2::TopKLastDimOp;
    |

masked_fill 是一条不带 help 的 E0599,nonzero 和 topk 一样提示 candle_transformers::models::deepseek2::NonZeroOp。同一份代码放进只依赖 candle-core 的 crate(pitb-core),四条错误还在,两条 help 都没了:

error[E0599]: no method named `topk` found for struct `candle_core::Tensor` in the current scope
  |                ^^^^ method not found in `candle_core::Tensor`

原因

candle-core 0.11.0 没有 topk、masked_fill、nonzero、einsum,origin/main 上也没有。rustc 的提示来自 DeepSeek V2 的模型文件:它为自己用到的两个算子定义了 TopKLastDimOp 和 NonZeroOp,并为 Tensor 实现,rustc 在依赖里找到了就建议导入。这不是 candle 推荐的 API,只是某个模型的内部工具碰巧是 pub。masked_fill 则是每个模型文件各写一份私有函数:candle-transformers 0.11.0 里有 23 个文件定义了自己的 fn masked_fill(llama.rs 里的见对齐 logits)。

deepseek2 的 topk 就是三步:

    fn topk(&self, topk: usize) -> Result<TopKOutput> {
        // Sorted descending
        let sorted_indices = self.arg_sort_last_dim(false)?;
        let topk_indices = sorted_indices.narrow(D::Minus1, 0, topk)?.contiguous()?;
        Ok(TopKOutput {
            values: self.gather(&topk_indices, D::Minus1)?,
            indices: topk_indices,
        })
    }

nonzero 的输出形状取决于数据,没法用已有的算子拼,NonZeroOp 是一个只有 CPU 实现的自定义算子,别的设备上先拷回 CPU。

改法

/// torch: x.masked_fill(mask, value). candle: mask.where_cond(on_true, on_false).
fn masked_fill(x: &Tensor, mask: &Tensor, value: f32) -> candle_core::Result<Tensor> {
  let v = Tensor::new(value, x.device())?.to_dtype(x.dtype())?.broadcast_as(x.shape())?;
  mask.where_cond(&v, x)
}

let idx = x.arg_sort_last_dim(false)?.narrow(D::Minus1, 0, 2)?.contiguous()?;
println!("topk values = {:?} indices = {:?}", x.gather(&idx, D::Minus1)?.to_vec2::<f32>()?, idx.to_vec2::<u32>()?);
println!("masked_fill = {:?}", masked_fill(&x, &mask, f32::NEG_INFINITY)?.to_vec2::<f32>()?);
println!("nonzero = {:?}", nonzero_2d(&mask)?); // to_vec2 读回主机,逐个数
// einsum("i,j->ij"):外积
println!("einsum i,j->ij = {:?}", a.unsqueeze(1)?.broadcast_mul(&b.unsqueeze(0)?)?.to_vec2::<f32>()?);
// einsum("bhqd,bhkd->bhqk"):注意力分数
println!("einsum bhqd,bhkd->bhqk [0,1,2] = {:?}", q.matmul(&q.t()?)?.get(0)?.get(1)?.get(2)?.to_vec1::<f32>()?);
topk values = [[0.9, 0.7], [0.8, 0.5]] indices = [[1, 3], [2, 0]]
deepseek2 topk values = [[0.9, 0.7], [0.8, 0.5]] indices = [[1, 3], [2, 0]]
masked_fill = [[0.1, -inf, 0.3, -inf], [-inf, 0.2, 0.8, 0.4]]
where_cond swapped = [[-inf, 0.9, -inf, 0.7], [0.5, -inf, -inf, -inf]]
nonzero = [[0, 1], [0, 3], [1, 0]]
deepseek2 nonzero = [[0, 1], [0, 3], [1, 0]] U32
einsum i,j->ij = [[3.0, 4.0, 5.0], [6.0, 8.0, 10.0]]
einsum bhqd,bhkd->bhqk [0,1,2] = [2.0243056, 2.621528, 3.2187502]
  • 结果和 PyTorch 一致(打印精度不同;einsum 那一行差在最后一位,是 f32 求和顺序)。导入 deepseek2 的两个 trait 也能得到相同的结果,但把一个模型文件的内部工具当成公共 API 依赖,换版本时可能消失,自己写这几行更稳。
  • where_cond 的参数顺序和 masked_fill 相反。 x.masked_fill(mask, v) 是「mask 为真的地方填 v」;candle 是 mask.where_cond(on_true, on_false),填充值放第一个。where_cond swapped 一行是把 x 和 -inf 写反的结果:保留的和屏蔽的正好颠倒,不报错。
  • 索引的 dtype:topk 的下标、nonzero 都是 U32,PyTorch 是 int64(归约与索引)。
  • arg_sort_last_dim 只排最后一维,topk(dim=1) 之类要先 transpose。
  • einsum 没有通用的替代,按下标式拆成 broadcast_mul + sum、matmul、transpose 的组合,每一种都要对一次数。

怎么检查

  • 翻译模型时先列出 PyTorch 代码里用到的算子(grep -o "\.\w*(" modeling_x.py | sort -u),对着 candle 的 Tensor 文档逐个找对应,没有的提前写好替代函数和测试,不要等编译器一个个报。
  • where_cond 的替代函数配一个两元素的单元测试(一个 mask 为真、一个为假),参数写反立刻能看出来。
  • rustc 提示 use candle_transformers::models::<某个模型>::... 时,这是在建议依赖一个模型的私有工具,先看一眼它的实现,通常几行就能自己写。

小结

  • topk、masked_fill、nonzero、einsum 在 candle-core 0.11.0 里都没有(main 上也没有),编译报 E0599 / E0425。
  • rustc 建议导入 candle_transformers::models::deepseek2::TopKLastDimOp / NonZeroOp,只因为 crate 依赖了 candle-transformers;只依赖 candle-core 时没有这条提示。
  • 替代:topk = 降序 arg_sort_last_dim + narrow + gather;masked_fill = mask.where_cond(填充值, x);nonzero 读回主机自己数;einsum 按式子拆成广播乘和 matmul。和 PyTorch 结果一致。
  • where_cond 的填充值在第一个参数,写反不报错、结果颠倒。

💬 关联讨论

发起讨论
加载中...