Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第五周周报(7.20-7.26)

本周工作集中在 k3_kernel_backend 一侧的算子补全上:以 YOLO26x 推理所需的算子集合为目标,把 YOLO 计算图实际走到的所有算子在用户态 backend 中实现,并补齐它们在 RISC-V V 扩展下的实现与量化支持。算子库本身仍以 host 端 native 代码和 QEMU 上的真实算子调用作为正确性验证手段。

注意: 周报这里提及的API不代表后续API接口一定不变(至少下次周报更新之前不会变动),请以实际代码为准.

本周工作概览

本周主要完成和推进了五件事:

  • 用 ONNX 解析 + 大模型计算图分析的方式确认 YOLO26x 推理所需的最小算子集合,并据此安排补全顺序。
  • 在 k3_kernel_backend 中实现并补全 transform(Tensor 搬运、索引与 shape 变换算子)一组算子,部分算法在 RISC-V V 扩展下走 RVV 向量实现。底层向量原语集中收敛在 rvv.rs。
  • 补齐归约、池化与模型常用 NN 算子(Softmax、RMSNorm、RoPE、GLU/SwiGLU、MaxPool、ReduceMax、TopK),其中 Softmax / RMSNorm / RoPE / MaxPool / ReduceMax 的内部 hot loop 在向量 target 下走 RVV。
  • 在 matmul 算子上补完量化支持:I8/U8 → I32 走 IME vmadot(X100/A100),ggml Q8_0 / Q3_K / IQ4_NL × F32/F16 → F32 走 dequantize + CPU matmul;F16 × F16 → F16/F32 在 A100 上通过 a100-fp16-ime feature 走 smt.vfwmadot。
  • 实现二维卷积算子(conv2d):F32、F16、I8/U8 三种 dtype 路径,覆盖 im2col + matmul 复用 matmul 引擎,int8 走 IME vmadot、F16 在 A100 上走流式 im2col + smt.vfwmadot。

本周问题与判断

  1. 量化支持补全只覆盖 ggml 量化块 + I8/U8 的 LHS:matmul 现在能处理 Q8_0 / Q3_K / IQ4_NL × F32/F16 的 LHS 量化场景,但 RHS 量化、Q4_0、K-quant 全家桶等还没接上,ggml 端如果 weight 用 Q4_0 / Q4_K 等格式需要先把 Q4 系列 dequantize 补完。
  2. A100 FP16 IME 当前不默认开:feature a100-fp16-ime 仍受 MCPM.BF16 控制约束,conv2d / matmul 在该 feature 未开时 F16 走 CPU 标量路径,性能上不去;接入到真实 LLM 推理之前需要先把 MCPM.BF16 的控制路径打通。
  3. ggml backend 接入完整 LLM 模型失败:当前对 LLM 推理整条管线理解不足,无法快速定位卡点。下一步要么先把 ONNX/ggml 算子集合在 K3 上做一次完整对照(看哪些 ONNX 节点没接到 bridge),要么用更小的子图(例如只跑 token embedding + 1 层 transformer)来分阶段定位。

YOLO26x 算子集合探测

YOLO26x 本身是一组 Ultralytics 官方 ONNX 图,常见 backbone + neck + detect head 的组合在 K3 端的运行需要 backend 提供一组相对完整的算子集合。本周先做“算子集合探测“再决定补全顺序:

  • 通过 ONNX 解析读取节点类型、输入/输出 shape、属性,把 YOLO26x 的节点表 dump 出来。
  • 把这张节点表交给大模型,让它按 ONNX 语义归纳“YOLO26x 推理实际调用到的算子集合 + 这些算子互相之间是否能拆解或合并“,并给出最小集。
  • 按最小集对照当前 k3_kernel_backend 已有算子,列出缺口,按“上游依赖 → 下游依赖“的顺序安排补全。

Transform:Tensor 搬运、索引与 shape 变换算子

transform.rs 这一组算子是后面所有算子读 / 写带 stride 输出的基础,因此本周先补齐。实现位于 k3_kernel_backend/src/transform.rs,分发入口在 k3_run_kernel 中按 KernelOp 路由:

  • Concat:按指定 axis 拼接多个输入,输出连续后写回可能带 stride 的 output。
  • Transpose:按 perm 重排维度,输出 shape 与 perm 一一对应。
  • Gather / GatherElements:ONNX 语义下的两种 gather,支持负数 index 自动归一化。
  • GetRows / SetRows:GGML 风格的 embedding lookup 与 KV cache row 写回。GetRows 额外支持 data 为 Q8_0 / IQ4_NL / Q3_K 量化块、输出升精度到 F32/F16。
  • Copy:通用 tensor 拷贝 / materialize contiguous。
  • Cast:覆盖 F32 ↔ F16、F32 ↔ I32 等常见组合;I32 ↔ F32 在向量 target 下走 RVV。
  • Resize:ONNX 2D resize,支持 nearest / linear、half_pixel / asymmetric / align_corners 三种 coordinate mode,linear 模式下双线性插值在向量 target 下走 RVV 二元算子。
  • Expand / Tile:ONNX 的 broadcast expand 和 repeats tile,统一收敛为单次 indexed gather。

RVV 下的实现

实现上,几条“逐元素 + 按 byte 偏移搬移“的路径在向量 target(PREFER_X100 / PREFER_A100)下都收敛到 rvv.rs 里的向量原语:

  • copy_bytes、gather_bytes:按 byte 偏移做固定宽度元素 gather。RVV 路径下用 vle8 / vse8 + indexed load 写,RVV 不可用时回退到 copy_from_slice。
  • cast_i32_to_f32 / cast_f32_to_i32:I32 ↔ F32 的向量 cast,RVV 路径下用 vfcvt 系列指令。

所有 RVV 内联汇编都集中在 rvv.rs 里,硬件路径使用 vsetvli + vle*.v / vse*.v 的 VLA 形式。其他 target 走同语义的标量 / slice 镜像,便于 host 端单测。

归约、池化与模型常用 NN 算子

nn.rs 一组算子集中在 k3_kernel_backend/src/nn.rs,覆盖 Transformer / 检测头里常用的几类算子:

  • Softmax:数值稳定版 exp(x - max),按任意 axis 展开,inner/outer 维度按 broadcast 处理。支持可选 mask 输入(broadcast 到 input)。在向量 target 下,shifted / exp / scale 三个步骤都走 RVV:affine_f32、exp_f32、reduce_max_f32、reduce_sum_f32。
  • RMSNorm:标准 x * rsqrt(mean(x²) + eps) * weight 公式,weight 缺省时退化为单位权重。在向量 target 下平方、求和、缩放都走 RVV。
  • RoPE:BSHD dense/strided 张量,支持 GPT-J / NeoX 两种 mode、frequency_base / frequency_scale / attn_factor 等参数;position 可由外部 positions tensor 给出。sin/cos 系数按 position 算好之后,旋转乘加统一在 RVV 下做。
  • GLU:当前 v1 仅支持 OP_SWIGLU,同时支持 split(输入 axis 0 一半左一半右)和两路独立输入两种形式。
  • MaxPool:NCHW 2D 最大池化,支持 kernel / stride / dilation / padding / ceil_mode,可选第二个 I64 indices 输出。在向量 target 下窗口最大值归约走 rvv::reduce_max_f32。
  • ReduceMax:多 axis 归约,支持 keepdims 和 noop_with_empty_axes 两个 flag。
  • TopK:当前保留 CPU 实现,largest=true 时按值降序、相等时按下标升序,保证 tie-breaking 稳定。

这几条算子的 dtype 当前都收敛在 F32 / F16 上。

矩阵乘法算子与量化支持补全

matmul.rs 是算子库的核心,量化支持补全主要落在这一块。dtype 分发在 matmul_caller 中按 (input0_dtype, input1_dtype, output_dtype) 路由到具体实现(k3_kernel_backend/src/matmul.rs:144):

LHSRHSOUT计算落点
F32F32F32CPU 软件 matmul(IME F32 matmul 尚未实现)
F16F16F16 / F32CPU 软件 matmul;A100 上 a100-fp16-ime feature 启用 smt.vfwmadot
F16F32F32CPU 标量 matmul(F16 → F32 单次解码)
I8 / U8I8 / U8I32CPU 软件 matmul;X100 / A100 走 IME vmadot,按 (I8/U8, I8/U8) 自动选 4 种符号变体
ggml quant(Q8_0 / Q3_K / IQ4_NL)F32F32quant.rs dequantize + CPU matmul
ggml quant(Q8_0 / Q3_K / IQ4_NL)F16F32quant.rs dequantize + CPU matmul

几个关键点:

  • I8/U8 输入按 byte 切片直接交给 IME vmadot 指令,不强制 LHS / RHS 同号;符号组合由 signedness_for 根据左右 dtype 推 VmadotSignedness。
  • 量化块的 dequantize 走 k3_kernel_backend/src/quant.rs:Q8_0(block 34B)、IQ4_NL(block 18B + 非线性查找表)、Q3_K(block 110B + 6bit scale + 高位 hmask)三套都按 ggml 规范实现。
  • A100 FP16 IME 当前默认不开,需要 a100-fp16-ime feature 并在 caller 中显式控住 MCPM.BF16=0;否则返回 BackendErr::UnsupportedOp。
  • X100 / A100 int8 tile 尺寸当前按实测 M=4, K=8, N=4 选(与 A60 一致),与 A100 手册的 M=8, K=16, N=8 暂时不一致的取舍写在源码注释里。

二维卷积算子

conv2d.rs 走 im2col + matmul 的标准套路:权重视作 [Cout, Cin*KH*KW] 矩阵,im2col 把输入每个输出点的感受野展开成 [Cin*KH*KW, Hout*Wout] 矩阵,二者相乘得 [Cout, Hout*Wout] 输出。dtype 分发(conv2d_caller,k3_kernel_backend/src/conv2d.rs:142):

inputweightoutput计算落点
F32F32F32im2col + CPU F32 matmul
F16F16F16im2col + matmul;A100 上 a100-fp16-ime 走流式 im2col + smt.vfwmadot
I8 / U8I8 / U8I32im2col + IME vmadot(X100 / A100)

支持的属性:NCHW 输入/输出 layout、groups(深度可分离卷积)、dilation、stride、padding、可选 bias。当前 backend 层不做 layout 转换,调用方需要把 NCHW 准备好。

ONNX 二元 elementwise 算子

binary.rs 实现 ADD / MUL / SUB / DIV / MOD 五种 ONNX 二元 elementwise 算子,语义由 KernelOp 区分。ADD / MUL / SUB / DIV 在向量 target 下统一走 rvv::binary_f32(vfadd.vv / vfmul.vv / vfsub.vv / vfdiv.vv),支持 ONNX 的右对齐 broadcast 规则;MOD 按约定保留 CPU 实现(不支持向量)。

unary.rs 的 SILU / SIGMOID / SCALE 与 binary 共用 broadcast 体系,但属于单输入 elementwise,不是本周 outline 里的“二元“,因此单独放在 unary 一侧。SILU / SIGMOID 走 RVV 下的 affine_f32 + exp_f32 + 倒数。算子库内 / k3_test 的 dispatch 中这两类算子的入口独立。

测试

单算子正确性测试

算子库内部每个模块(rvv.rs / transform.rs / nn.rs / binary.rs / unary.rs / matmul.rs / conv2d.rs / quant.rs)都附了独立 #[cfg(test)] mod tests,覆盖 happy path、边界条件、与参考实现的数值一致性。

k3_test(位于 k3x-HERA-RT 仓库根目录的 k3_test/ crate,其集成测试入口在 k3_test/src/main.rs)的 host / QEMU 集成测试在 QEMU 上把每个算子通过 k3_run_kernel 真实跑一遍,方式是:

  • 给定同样的输入。
  • 一份用 k3_kernel_backend 实际算子调用执行。
  • 一份用 Python 的 numpy / 标准参考实现执行(量化测试的参考在 k3_test/scripts/host_quant_ref.py)。
  • 对比输出,结果完全一致则通过。

k3_test 当前覆盖的算子包括 binary (ADD / SUB / MUL / DIV / MOD)、unary (SILU / SIGMOID / SCALE)、conv2d (F32 sum / F32 depthwise+bias / int8 sum)、nn (Softmax / RMSNorm / RoPE / SwiGLU / MaxPool / ReduceMax / TopK)、transform (Concat / Transpose / Gather / GetRows / SetRows / Copy / Cast / Resize / Expand / Tile)、rvv (RVV binary mul / affine)、matmul (CPU int8)、以及量化 (Q8_0 get_rows / Q8_0 matmul / IQ4_NL matmul / Q3_K matmul)。test_quantized_ops 在跑集成测试之前单独验证 ggml 量化块的 dequantize + matmul 路径。

ggml backend 接入测试

尝试把算子库接入 ggml backend,跑 tinystories.gguf 这种小模型做完整数据通路验证。具体做法是利用 AI 把 k3_ggml_bridge(基于现有 C ABI bridge)接到 ggml backend 上,让 F32 MatMul 请求转成 K3 graph 提交。

结果:失败。在相同的 seed / temperature / 输入 token 下,模型输出与预期不一致。中间某一步少东西,但具体卡在哪一步目前没有完全定位。

排查的初步判断:当前对 LLM 大模型推理的整条流水线理解还比较弱,无法立刻锁定是 prompt tokenization、position embedding、KV cache、sampling 还是某个算子语义的问题。怀疑是中间少了一个或几个 ggml 节点(例如某个隐式的 Reshape、Slice、或 group_norm 类的算子)没有接到 bridge 上。

数据通路测试改用本地测试集

在 ggml 接入未能跑通完整模型之前,把数据通路测试改回 k3_test 内部的端到端测试集:把上面列出的所有算子按“上游 → 下游“顺序串成多算子 graph(matmul → activation → reshape → …),用与单算子测试同样的 host 端参考实现做对比。这部分对应仓库里 k3_test/src/main.rs 中的 run_binary_tests / run_unary_tests / run_conv2d_tests / run_nn_tests / run_transform_tests / run_rvv_tests 这一组集成函数,以及 test.rs 里的 test_quantized_ops / test_cow_live 两组量化与共享内存保活测试。

这条路径覆盖了 YOLO26x 当前能跑到的所有算子类型(除了 GGML 接入这一段),可以保证算子库本身的算子语义和数据通路是自洽的。