【问题+修复】K3 Pico-ITX:llama.cpp IME2 后端逐 token 解码结果错误(flash-attn _mrow 中 M[QLEN] 未全部初始化为 -INF),一行修复已验证

概括

在 K3 Pico-ITX(8 GB)上,llama.cpp-tools-spacemit(0.0.8 / 0.1.1 / 0.1.9 均可复现)以及上游 llama.cpp 0a1ef30(-DGGML_CPU_RISCV64_SPACEMIT=ON)在逐 token 解码(n_ubatch = 1,也就是文本生成)时,输出分布明显偏离参考结果;批处理(prefill)基本正常。生成的文本中出现重复的子词(如 “the the”、“cloudss”),单词分类和 JSON 抽取会出错。

原因已定位到 flash-attention 解码路径中一个数组的初始化问题,一行修复后逐 token 结果与批处理一致,速度不变。

环境

  • 板卡:SpacemiT K3 Pico-ITX,8 GB LPDDR5,128 GB UFS
  • 系统:Bianbu 4.0.1,内核 6.18.3-1.0.8.4(2026-10-22 apt full-upgrade 后)
  • 频率:X100 2.0 GHz,A100 1.8 GHz,cpufreq/boost = 0
  • 后端初始化日志:use_ime2: 1, num_perfer_cores: 8, perfer_core_arch_id: a064, mem_backend: HPAGE, cpu_mask: ff00
  • 模型:Qwen2.5-7B-Instruct Q4_K_M;参考:官方 Qwen/Qwen2.5-7B-Instruct-GGUF F16
  • 参考实现:同一块板上的上游 llama.cpp CPU 版本(X100,同一提交)

测量结果

llama-perplexity --kl-divergence,逐 token(-b 512 -ub 1),2 个 chunk,与 CPU 版本对比:

版本 平均 KLD top-1 一致率
当前(-fa 默认 auto) 0.2148 81.96 %
-fa off 0.0289 91.57 %
加上下面的修复 0.0290 91.77 %
对照:CPU 版本 -fa off 对比 -fa on 0.0047 97.45 %

与 F16 对比(前 4 个 chunk,逐 token):修复后 0.0658,修复前 0.2841,上游 CPU 0.0426。修复后速度:pp512 40.1 t/s,tg128 5.12 t/s。

llama-perplexity 打印的 “Same top p” 就是上表中的 top-1 一致率。

原因

ggml/src/ggml-cpu/spacemit/rvv_kernels.cpp 第 669 行(0a1ef30,2026-10-26 的 master 中仍相同),函数 flash_attn_ext_f16_one_chunk_inner_vlen1024_vf16_mrow<QLEN>:

float M[QLEN] = { -INFINITY };  // maximum KQ value

这种聚合初始化只把 M[0] 设为 -∞,M[1..QLEN-1] 为 0。对于每组中第 1…QLEN-1 个 head,online softmax 的初始最大值是 0:当它们的注意力分数都为负时,exp(s - 0) 很小,转换为 _Float16(V 累加器为 fp16)后下溢为次正规数或 0,而 S 在 float 中累加——输出被缩小并失真。批处理使用的 tiled 内核用循环把每个 M[i] 设为 -INFINITY,因此不受影响。Qwen2.5-7B 有 28 个 query head、4 个 KV head(GQA),解码时大部分 head 走 _mrow<4> / _mrow<2>。

修复

--- a/ggml/src/ggml-cpu/spacemit/rvv_kernels.cpp
+++ b/ggml/src/ggml-cpu/spacemit/rvv_kernels.cpp
@@ -666,7 +666,8 @@
     GGML_ASSERT(flash_attn_ext_supported_shape_vlen1024_vf16(DK, DV));
     float S[QLEN] = { 0.0f };       // sum
-    float M[QLEN] = { -INFINITY };  // maximum KQ value
+    float M[QLEN];                  // maximum KQ value
+    std::fill(M, M + QLEN, -INFINITY);

临时规避:不修改代码时使用 -fa off,结果同样正确,但 pp512 约慢 11 %(35.6 t/s)。

复现

# 参考:任意 CPU 版本,逐 token
llama-perplexity -m qwen2.5-7b-instruct-q4_K_M.gguf -f text.txt -c 512 --chunks 2 -b 512 -ub 1 -t 8 --kl-divergence-base ref.kld
# SpacemiT IME2 版本,同样模式
llama-perplexity -m qwen2.5-7b-instruct-q4_K_M.gguf -f text.txt -c 512 --chunks 2 -b 512 -ub 1 -t 8 --kl-divergence-base ref.kld --kl-divergence
# 期望 KLD ≈ 0.005,实际 ≈ 0.21;不加 -ub 1(批处理)约 0.03

另一个精度问题:Q4_K 重排

repack.cpp 中 make_block_q4_1x32 把 Q4_K 每个子块的最小值转换成整数零点 round(m / d)(并截断到 0…15)。这个偏移(最多半个量化步长)作用在子块的全部 32 个权重上,不会平均掉。用本模型的真实权重行测试:IME2 矩阵乘输出相对精确 Q4_K 结果的误差为 6–13.5 %(相对 L2;激活同号时 20–45 %),而上游 CPU 内核为 0.5–3.2 %。ime2_kernels.cpp 中的参考内核 *_mrow_ref 与向量内核结果一致(0.1–0.3 %),所以损失来自重排格式本身。这就是批处理模式下剩余的偏差(相对 F16 的 KLD 0.066,上游 0.046)。Q6_K(i8i8)和 Q4_0(i8i4_hp)路径与上游接近;Q4_0 7B 在 IME2 上的 KLD 与上游一致(0.090),tg128 6.36 t/s。

问题

  1. 上面的 flash-attention 修复能否加入下一版 llama.cpp-tools-spacemit 并提交到上游 llama.cpp?
  2. 是否计划让 Q4_K 重排保留子块最小值(例如以 fp16 存储)?还是建议在 IME2 上使用 Q4_0?

日志、KLD 报告和测试文本可以提供。所有测量都在板上实际运行。


English summary. On K3 Pico-ITX, token-by-token decode (n_ubatch = 1) on the IME2 path of llama.cpp-tools-spacemit 0.0.8–0.1.9 and of upstream llama.cpp 0a1ef30 (-DGGML_CPU_RISCV64_SPACEMIT=ON) is wrong: KLD 0.215 vs the CPU build (top-1 82 %), duplicated tokens in generated text. Root cause: rvv_kernels.cpp:669, float M[QLEN] = { -INFINITY }; in flash_attn_ext_f16_one_chunk_inner_vlen1024_vf16_mrow initialises only M[0]. With std::fill(M, M + QLEN, -INFINITY) decode matches the batched path (KLD 0.029, top-1 91.8 %), tg128 5.12 t/s. Separately, make_block_q4_1x32 rounds the Q4_K sub-block minimum to an integer zero point (6–13.5 % matmul error vs exact Q4_K); Q6_K and Q4_0 paths are fine.

Q6_K(i8i8)和 Q4_0(i8i4_hp)路径与上游接近。Q4_0 7B(由官方 F16 用 llama-quantize 默认方式量化,output.weight 为 Q6_K)在 IME2 + 上述修复下,相对 F16 的 KLD(前 4 个 chunk):逐 token 0.0317、批处理 0.0323;上游 CPU 为 0.0334 / 0.0340——IME2 没有额外损失。速度 pp512 57.5 t/s,tg128 5.97 t/s。若用 --pure(包括 output 在内全部为 Q4_0),KLD 为 0.090(IME2 与上游一致),tg128 6.36 t/s。注:Q4_0 由与参考相同的 F16 文件量化,Q4_K_M 是另一来源的文件,与 Q4_K_M 的比较可能偏向 Q4_0。

Q6_K and Q4_0 paths are fine: Q4_0 with the default llama-quantize mix (output Q6_K) gives KLD to F16 0.032 on IME2 with the fix vs 0.033 on the CPU build, tg128 5.97 t/s.