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

快速上手

gemmkit 在普通 Rust 切片的带步长视图上计算 C <- alpha*A*B + beta*C,并在运行时挑选 CPU 实际支持的最快指令集。你不需要在编译期选择 ISA,也不需要链接任何 BLAS 库。你只需添加 1 个依赖,把 3 个矩阵交给它,然后调用 gemm

添加依赖

核心 crate 是 gemmkit。在普通(std)目标上做 f32/f64 计算时,下面这一行就够了:

[dependencies]
gemmkit = "0.1"

这会引入 2 个默认 feature:stdparallelstd 提供运行时的缓存与 CPU 特性检测,还提供 GEMMKIT_REQUIRE_ISAGEMMKIT_* 调优旋钮,以及一个线程本地的工作区池。这个池让相同尺寸的重复调用不再分配内存。parallel 加上基于 rayon 的多线程,并隐含开启 std

可选的元素类型族(halfcomplexint8)和 epilogue 能力默认关闭。纯浮点构建不会为它们的代码生成或依赖付出任何代价。若要以 no_std(仅 core + alloc)构建 crate,用 default-features = false 关掉默认 feature。参见no_std 与 WebAssembly了解具体做法。

第一个完整示例

这个示例计算一个 2x3 矩阵乘以一个 3x2 矩阵,全部行主序,单线程运行:

use gemmkit::{gemm, MatMut, MatRef, Parallelism};

fn main() {
    // 2x3 times 3x2 = 2x2, all row-major
    let a = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0];
    let b = [7.0_f32, 8.0, 9.0, 10.0, 11.0, 12.0];
    let mut c = [0.0_f32; 4];
    gemm(
        1.0,
        MatRef::from_row_major(&a, 2, 3),
        MatRef::from_row_major(&b, 3, 2),
        0.0,
        MatMut::from_row_major(&mut c, 2, 2),
        Parallelism::Serial,
    );
    assert_eq!(c, [58.0, 64.0, 139.0, 154.0]);
}

参数恰好就是 C <- alpha*A*B + beta*C 的各项:标量 alpha、2 个输入视图、标量 beta、输出视图,以及一个 Parallelism 选择器。

MatRef::from_row_major(&a, 2, 3)a 当作 2 行 3 列的行主序矩阵读取。各维必须对得上:A.cols 必须等于 B.rows,且 C 必须是 A.rowsB.cols 列。如果对不上,调用会在触碰内存之前 panic。

转置从不需要拷贝,因为它只是步长的改变,而非数据的搬移。MatRef::from_col_major(&b, 3, 2) 把同一段缓冲当作列主序矩阵读取。MatRef::new 让你直接设置行步长和列步长。

底层发生了什么

gemm 入口在做任何算术之前,会先做一小段准备工作。

首先它做校验:检查内部维度是否相符、每个视图是否都落在其切片之内、C 是否把每个 (i, j) 映射到互不相同的偏移,以及 C 的存储是否与 AB 的存储重叠。任何一项检查失败都会带着具体的信息 panic,发生在任何一次 unsafe 操作之前。只有到这一步之后,它才把这 3 个视图降低成裸指针与步长,交给分发层。

分发层决定运行哪个内核。对某一元素类型的第一次 GEMM 调用会执行一次 CPU 特性检测,把胜出的入口点记录进一个 OnceLock,然后返回。此后每次调用都只是经由那个缓存指针的一次普通间接调用,不再重复检测。因此运行时 ISA 选择是一次性成本,摊薄到整个进程。

你可以用 GEMMKIT_REQUIRE_ISA 环境变量覆盖自动选择,也可以用它为测试锁定某个后端。gemmkit 只读取该变量一次,并以同样的方式记忆结果。参见运行时 ISA 分发了解细节。一次 GEMM 调用的生命周期走了一遍从调用到微内核的完整路径。

精确理解 alpha 与 beta

alpha 缩放乘积 A*Bbeta 缩放 C 的原有内容。唯一值得记牢的细节是边界情形下会发生什么。

beta == 0 时,引擎完全不读取 C,而是直接用 alpha*A*B 覆写它。正是这条规则,让上面的 let mut c = [0.0_f32; 4] 即便缓冲原本未初始化也依然正确。具体来说,beta == 0 的输出切片可以持有垃圾值。经由 unchecked 层,它甚至可以是真正未初始化的内存,结果仍然是良定义的。

beta == 1 时,引擎让 C 的原有内容保持不动,并把乘积累加到它上面。其他任何 beta 值都会先把 C 乘一遍。

此外还有一条退化的快速路径。若 k == 0(空收缩)或 alpha == 0(乘积消失),调用就退化成 C <- beta*C。它完全不触碰 AB,只在原地缩放输出。结合 beta == 0 的规则,alpha == 0, beta == 0 会把 C 清零,而 k == 0, beta == 1 是一次空操作。窄类型在 f32 中缩放,并在写回时舍入,因此退化路径的舍入与完整内核完全一致。

Cargo feature

Feature默认解锁引入依赖
std运行时缓存/CPU 检测、环境旋钮、线程本地工作区池。关闭即 no_stdcore + allocraw-cpuid(仅 x86)
parallel基于 rayon 的多线程(Parallelism::Rayon)。隐含 stdrayon
wasm_threadswasm32-wasip1-threads 提供定尺寸的 rayon 池。隐含 parallel(经由 parallel
halff16/bf16 混合精度 GEMM,f32 累加half
complex带共轭的 c32/c64 GEMM(gemm_cplxnum-complex
int8i8 -> i32 整数 GEMM(gemm_i8(无)
epilogue融合偏置/激活、i8/u8 重量化、逐元素映射(无)

元素类型 feature 与能力 feature 可以组合:half + epilogue 给出融合的 f16 GEMM,int8 + epilogue 给出重量化入口,以此类推。每种组合的详情见元素类型融合 Epilogue

版本要求

gemmkit 面向 Rust 1.89edition 2024。采用 MIT OR Apache-2.0 双许可。API 参考见 docs.rs/gemmkit,本书是那份参考的长文版伴读。

接下来读什么