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 相乘的不止是 f32。同一套引擎、驱动与分块模型服务于 4 个元素类型族。每一个都是一个 Cargo feature,每一个也都在每个后端上有 SIMD 实现,并以可移植的标量回退路径兜底。

各族之间变化的是输入类型、累加器类型和输出类型,随之而来的还有你应当预期的精度变化。本页是一张地图:有哪些可用,以及各自有多精确。

内置的实数浮点

f32f64 不需要任何 feature 开关。它们经由通用的 gemm(以及 gemm_with 和各 unchecked 入口),以自身类型累加,是其余每个族赖以对照的基准:

#![allow(unused)]
fn main() {
use gemmkit::{gemm, MatMut, MatRef, Parallelism};

let a = [1.0_f64, 2.0, 3.0, 4.0];
let b = [5.0_f64, 6.0, 7.0, 8.0];
let mut c = [0.0_f64; 4];
gemm(2.0, MatRef::from_row_major(&a, 2, 2), MatRef::from_row_major(&b, 2, 2),
     0.0, MatMut::from_row_major(&mut c, 2, 2), Parallelism::Serial);
}

精度遵循教科书里的 GEMM 故事:相对误差大致随收缩深度 k 和该类型的机器 epsilon 增长。正确性测试套件把结果约束在相对 Frobenius 门限 8*k*eps 之内,对照一个独立的 f64 参考核验。因此 f64 对任何现实的 k 都近乎精确,而 f32 保有其惯常的每元素约 1e-7 相对精度。

窄浮点:half feature

开启 half 后,f16bf16 成为元素类型。gemmkit 把它们重导出为 gemmkit::f16gemmkit::bf16,因此你无需直接依赖 half。它们共用通用的 gemm 表面:输入 MatRef<'_, f16>,输出 MatMut<'_, f16>,因为它们实现了与实数浮点相同的标量 trait。

其决定性特征是混合精度。引擎在加载时把输入加宽到 f32。整个收缩都在 f32 中累加。引擎只在写回时把结果舍入回窄类型一次。 k 循环内部没有反复的窄舍入,正是这唯一一次舍入让精度可用。

#![allow(unused)]
fn main() {
use gemmkit::{f16, gemm, MatMut, MatRef, Parallelism};

let a: Vec<f16> = (0..6).map(|i| f16::from_f32(i as f32)).collect();
let b: Vec<f16> = (0..6).map(|i| f16::from_f32(i as f32)).collect();
let mut c = vec![f16::ZERO; 4];
gemm(f16::ONE, MatRef::from_row_major(&a, 2, 3), MatRef::from_row_major(&b, 3, 2),
     f16::ZERO, MatMut::from_row_major(&mut c, 2, 2), Parallelism::Serial);
}

由于累加在 f32 中进行,主导误差是那唯一一次最终舍入,而非求和本身。f16 保有约 9.8e-4(2^-10)的相对精度,bf167.8e-3(2^-7),两者基本都与 k 无关。因此窄精度 GEMM 接近于用 f32 计算、只舍入一次,远比在 16 位中累加要精确。

只舍入一次带来一个后果:在大 k 时,单个深度面板会流式吞吐一个大到 L2 缓存放不下的中间结果。引擎会自行处理这个问题。越过一个自动导出的字节门限后,它会切换到一个 f32 输出的内部孪生体。这个孪生体把收缩重新分块以保持驻留缓存,并在最后才把结果舍入回窄类型。对常见的 beta in {0, 1} 情形,孪生体与单面板逐字节相同,其他情形则落在容差之内。这个切换是自动的,无需任何配置。机制细节见点积内核与深 K 孪生。在 AVX-512 BF16 硬件上,bf16 还会使用 vdpbf16ps 点积内核,参见运行时 ISA 分发了解该机制。

整数:int8 feature

int8 加入 gemm_i8,一个独立的入口。它的输入与输出类型不同,i8 进、i32 出,同构的 gemm<T> 表面无法表达这一点。alphabetaC 全都是 i32

#![allow(unused)]
fn main() {
use gemmkit::{gemm_i8, MatMut, MatRef, Parallelism};

let a = [1_i8, 2, 3, 4, 5, 6];
let b = [7_i8, 8, 9, 10, 11, 12];
let mut c = [0_i32; 4];
gemm_i8(1, MatRef::from_row_major(&a, 2, 3), MatRef::from_row_major(&b, 3, 2),
        0, MatMut::from_row_major(&mut c, 2, 2), Parallelism::Serial);
}

整数 GEMM 是精确的:它是溢出时回绕i32 环算术,即整数 GEMM 的惯例语义。谈不上什么容差,因为根本没有舍入。结果在每个 ISA 上逐位相同(标量、FMA、AVX-512F,以及 AVX-512 VNNI 的 vpdpbusd 点积内核)。串行与并行运行之间也逐位相同,因为环上的整数加法与顺序无关。

若你喂入的乘积会超出 i32,回绕是有定义且可复现的,而非未定义行为。int8 feature 不引入任何额外依赖。在其之上再加 epilogue 可解锁重量化入口,一趟即可得到 i8u8 输出。参见融合 Epilogue了解这些入口。

复数:complex feature

complexnum-complex 值上加入 gemm_cplx。gemmkit 把它们重导出为 gemmkit::c32Complex<f32>)和 gemmkit::c64Complex<f64>)。它的签名为每个操作数带一个共轭标志:

#![allow(unused)]
fn main() {
use gemmkit::{c32, gemm_cplx, Complex, MatMut, MatRef, Parallelism};

let a = [Complex::new(1.0_f32, 1.0), Complex::new(2.0, 0.0)];
let b = [Complex::new(0.0_f32, 1.0), Complex::new(1.0, 0.0)];
let mut c = [c32::default(); 1];
gemm_cplx(
    Complex::new(1.0, 0.0),
    MatRef::from_row_major(&a, 1, 2), false, // conj_a
    MatRef::from_row_major(&b, 2, 1), false, // conj_b
    Complex::new(0.0, 0.0),
    MatMut::from_row_major(&mut c, 1, 1),
    Parallelism::Serial,
);
}

计算为 C <- alpha*op(A)*op(B) + beta*C:当 conj_a 置位时 op(A)conj(A)conj_b 置位时 op(B) 同理为 conj(B)。传入 false, false 就得到普通乘积 A*B。这些标志只共轭操作数本身。

复数以自身类型累加,被约束在相对 Frobenius 门限 16*k*eps 之内,其中 eps 取实部的 epsilon。因此 c32 GEMM 的精度约与 f32 相当,c64 GEMM 约与 f64 相当。complex 引入 num-complex

在内部,复数并不搭乘浮点内核,而是使用一个专用的拆分(结构体数组,SoA)内核,这也是它成为独立入口的原因。该设计见复数拆分内核

如何选择类型

类型族Feature输入 / 累加 / 输出精度确定性
f32, f64(内置)同 / 同 / 同教科书级,约 8*k*eps可复现,如今在驱动路径上串行与并行逐位相同
f16, bf16half窄 / f32 / 窄只一次最终舍入,约 1e-3(f16)、8e-3(bf16)可复现,深 K 孪生对 beta in {0,1} 逐位相同
i8int8i8 / i32 / i32精确,回绕 i32跨每个 ISA 与每个工作线程数逐位相同
c32, c64complex同 / 同 / 同16*k*eps可复现,如今串行与并行逐位相同

若你需要速度、又能容忍约 1e-3 的精度,选 bf16f16。二者每个元素占用的字节都只有 f32 的一半,同时仍在 f32 中累加。若你需要精确,int8 给你精确。若你需要动态范围与精度,就留在 f32/f64

它们的可复现契约都是同一份。“可复现”承诺什么、不承诺什么,见并行实践

接下来读什么