Rust in Everything
基于 candle 0.11.0

cannot broadcast [5, 5] to [1, 9, 5, 29]

主线位置:推理主线第四步「带 KV 缓存生成」,照着 transformers 的手写循环搬:每步只送一个 token、不写位置;一个缓存对象跑完一段对话接着用。
版本:PyTorch 2.15.0 / transformers 5.22.0(CPU)、candle 0.11.0(CPU,accelerate)
flowchart TB
  f["forward"] --> rope["RoPE 按位置取行"]
  f --> cat["k v 接在缓存后面"]
  rope --> p0{"index_pos 对吗"}
  p0 -->|"一直 0"| g1["静默乱位置"]
  cat --> old{"缓存是旧对话的吗"}
  old -->|"多 token"| e1["cannot broadcast"]
  old -->|"单 token"| g2["静默串题"]

实验代码:PyTorch 侧 kv.py,candle 侧 crate pitb-exp 的 bin p21_cache(release)。模型是本地的 SmolLM2-135M-Instruct 权重,f32;PyTorch 固定 torch.set_num_threads(8)、注意力用 eager。prompt 是 The capital of France is(5 个 token),第二段是 The capital of Germany is。两边的缓存对象逐项对照见 KV 缓存;quantized_llama 上同一类错误见源码卷的坑 cannot broadcast [13, 45] to [1, 9, 13, 13]。

PyTorch 的写法

transformers 的手写贪心循环里没有位置,一个 DynamicCache 从头用到尾:

def greedy(prompt, cache, n=20):
  out = model(prompt, past_key_values=cache, use_cache=True)
  nxt = out.logits[0, -1].argmax().view(1, 1)
  toks = [nxt.item()]
  for _ in range(n - 1):
    out = model(nxt, past_key_values=out.past_key_values, use_cache=True)
    nxt = out.logits[0, -1].argmax().view(1, 1)
    toks.append(nxt.item())
  return toks

cache = DynamicCache(config=model.config)
first = greedy(ids, cache)
again = greedy(second, cache)  # 不清缓存,接着用
fresh = greedy(second, DynamicCache(config=model.config))
cache type DynamicCache layers 30 seq_length 24
layer 0 keys (1, 3, 24, 64) torch.float32
first: ' Paris. Paris is the largest city in France and the capital of the French department of the Espace'
reuse: seq_length 48 ' Berlin. Berlin is the largest city in Germany and the capital of the German state of the EThe'
fresh: ' Berlin. Berlin is the largest city in Germany and the capital of the Federal Republic of Germany. Berlin'

位置是 LlamaModel.forward 用 past_key_values.get_seq_length() 推出来的,所以循环里不用管。复用缓存不报错:第二段接在第一段的 24 个位置后面,从第 15 个 token 起和新缓存的结果不同( German state 对 Federal Republic)。这在 transformers 里也是错的,只是同样安静。

搬到 candle 的现象

candle 的 Llama::forward(x, index_pos, cache) 多一个参数。照 Python 的样子不管位置,decode 时一直传 0(p21_cache pos0),和每步传正确位置(ok)比:

pos0: " Paris, the capital of France, the United States, the United States, and the capital of France"
ok: " Paris. Paris is the largest city in France and the capital of the French department of the Espace"

不报错,第二个 token 起就变了。ok 和 transformers 逐个相同。

复用同一个 Cache 跑第二个 prompt,index_pos 从 0 开始(reuse):

Error: cannot broadcast [5, 5] to [1, 9, 5, 29]

第二个 prompt 只有一个 token The 时(reuse1):

reuse1: " capital of France. It is the largest city in Europe by population and the second most populous department of"

这次不报错,生成的是接着法国那一段往下说的话。

原因

CausalSelfAttention::forward 对 index_pos 只做两件事,都不和缓存的长度比对:

  • apply_rotary_emb 用它从 cos / sin 表里取行:cache.cos.narrow(0, index_pos, seq_len)。一直传 0,每个新 token 都按位置 0 旋转,和缓存里键之间的相对位置全错。
  • Cache::mask 用它定 mask 的列数 index_pos + seq_len。

而键和值总是接到缓存后面:

        if cache.use_kv_cache {
            if let Some((cache_k, cache_v)) = &cache.kvs[block_idx] {
                k = Tensor::cat(&[cache_k, &k], 2)?.contiguous()?;

复用缓存、index_pos 为 0、5 个 token:mask 按 5 × 5 建,键却有 24 + 5 = 29 个,(1, 9, 5, 29) 的分数广播不上。单 token 时 seq_len == 1 不建 mask:

            let att = if seq_len == 1 {
                att
            } else {
                let mask = cache.mask(seq_len, index_pos)?.broadcast_as(att.shape())?;
                masked_fill(&att, &mask, f32::NEG_INFINITY)?
            };

没有形状要对,The 看得见缓存里关于法国的 24 个位置。llama.rs 的 Cache 没有清空的方法,index_pos == 0 时也不丢旧缓存(quantized_llama 会丢,见 KV 缓存)。所以 candle 这边,同一个错误在多 token 时报错、单 token 时静默;transformers 两种都静默。origin/main 上 llama.rs 和 0.11.0 相同。

改法

位置由调用方记着:prefill 传 0,之后每步加上刚送进去的 token 数。每段新对话新建一个 Cache:

let mut pos = start + ids.len();
for _ in 1..n {
  let x = Tensor::new(&[next], dev)?.unsqueeze(0)?;
  next = model.forward(&x, pos, cache)?.squeeze(0)?.argmax(0)?.to_scalar::<u32>()?;
  out.push(next);
  pos += 1;
}
// 新对话
let mut cache = Cache::new(true, DType::F32, &cfg, &dev)?;
fresh: " Berlin. Berlin is the largest city in Germany and the capital of the Federal Republic of Germany. Berlin"
fresh1: "\n\nThe key to a successful online course is to create a clear and engaging learning experience that resonates"

fresh 和 transformers 新缓存的那一行逐字相同。fresh1 是单个 The 在新缓存上的续写,和上面串题的 reuse1 完全不同。分段 prefill(seq_len > 1 且 index_pos > 0)是允许的,index_pos 写对就行(KV 缓存)。

怎么检查

  • 把 Cache 和位置包在一个自己的结构体里,forward 只接 token,位置在结构体里加;Cache 字段私有,没法从它本身读出长度,只能自己记。
  • 对照测试:同一段 token,带缓存逐个送和不带缓存(Cache::new(false, ..))一次送,最后一个位置的 logits 应在 1e-4 以内;位置写错时这一条立刻不过。
  • 新对话的入口处新建 Cache,不要靠报错提醒:单 token 的输入不会报错。

小结

  • transformers 从缓存长度推位置;candle llama.rs 的 index_pos 由调用方传,不核对。一直传 0 不报错,生成从第二个 token 起变样。
  • 复用 Cache 跑新 prompt:多 token 报 cannot broadcast [5, 5] to [1, 9, 5, 29],单 token 静默接着上一段说;transformers 两种都静默接上。
  • 改法:自己累加 pos,每段新对话 Cache::new;改后和 transformers 逐个相同。

💬 关联讨论

发起讨论
加载中...