读者:调用者(库使用者 / 扩展者)——本分册定位见
README.md。 本文的语义契约章节(§7.2)与实测限制章节(§12)是写给"要选精度 / 要写层"的人的; 项目内部的路线图 / 盘点在../todo/,历史在../history.md。
状态:Phase 1(存储类型化 / cast / f16 GEMM / 类型化 Tensor)与 Phase 2(f16 边界 cast (P-1 已下沉为
ComputeEngine基类 NVI 入口,原PrecisionEngine装饰器已删除,见docs/history/history.md「15 号归档」§4.1)+ DSL/Layer/Loss/Optimizer 全链精度接线 + in-kernel f16 带类型变体(逐元素 / 归约 / matmul 段 / fold) + op-level f16 GEMM)均为当前实现;当前--f16实测峰值显存 低于 f32(§12.11)。当前状态、实测结论与已知问题见 §12;各轮实施过程与 A/B 流水见docs/history/history.md。 2026-10-18 口径更新:F5 之后四字段全 f16(profile_all_f16())已可训练;「全流程真 f16」 作为独立目标恢复为活跃计划(../todo/26-f16-speedup-plan.md§8)。 本文 §9.4 / §12.5 中「不可训练」的旧结论只适用于 F5 之前。 范围:f16 + f32(bf16 / f64 仅枚举占位)。 配套:01-compute-engine-development.md(引擎开发)、../usage/03-compute-engine-usage.md(引擎使用)、08-pitfalls-and-lessons.md(踩坑)。
- 概述
- 需求追溯
- 决策记录
- 核心概念:三层分离
- 精度类型系统
- 存储层:Tensor 与类型化存储
- 计算层:能力、语义、分派、f16 GEMM
- 精度推导规则(resolve)
- 模型级控制:PrecisionProfile
- 全链路走查
- 对现有架构的影响
- 分期
- 测试计划
- 风险与对策
- 涉及文件(模块级)
- 开放问题
把精度变成数据的一等属性(存储)+ 算子的一等参数(计算):张量创建时显式指定存储精度,算子调用时显式指定计算精度,模型级默认值在构造器一行给全;设备只决定执行路径(硬件加速 / 兼容路径),不改变精度语义。
| 现状 | 位置 | 含义 |
|---|---|---|
Scalar = float 编译期全局别名 |
core_config.hpp |
一个 build 只有一种精度 |
Matrix = std::vector<Scalar> |
algebra_matrix.hpp |
存储无精度维度 |
GpuTensor buffer 尺寸硬编码 sizeof(float) |
backend/compute_vk_backend.hpp |
GPU 存储无精度维度 |
Tensor = device + 形状 + 类型擦除存储 |
compute_tensor.hpp |
容器无精度属性 |
ComputeEngine 原语以 Tensor 为参数 |
compute_engine.hpp |
算子无精度概念 |
| 序列化 1B 全局 tag(f32/f64) | model_serialization.hpp |
文件级、非张量级,校验编译期 Scalar |
AOT 融合 shader 按 expr_spec_key 匹配 |
expr_spec.hpp |
key 无精度维度 |
目标
- G1 存储精度是 Tensor 的类型参数,设备具体适配(
Matrix/GpuTensor各自实现) - G2 精度与设备无关;设备支持硬件加速则启用,否则走兼容路径
- G3 计算精度:不指定 = 所有操作数中最高;指定 = 指定值
- G4 代码显式指定(张量创建 + 计算算子),不存在隐式精度推导(无隐藏全局状态)
- G5 默认配置(全 f32)= 今天的行为,零回归
范围外(当前不做):bf16、f64 计算路径、loss scaling、per-layer 精度覆盖、f16 权重镜像缓存。(in-kernel f16 融合不在其列——已实现,见 §11.1 与 §12.6–§12.11。)
| # | 原始需求 | 落地章节 |
|---|---|---|
| 1 | 存储精度是 Tensor 类型参数,具体设备具体适配(matrix、gputensor) | §5 + §6:Tensor 携带 precision_ 属性;CPU 侧 Matrix<P> 模板、GPU 侧 GpuTensor<P> 模板,元素字节数由 P 决定 |
| 2 | 精度与设备无关;设备支持硬件加速则启用,否则使用兼容方案 | §7:P 精度算术有唯一的、设备无关的形式化定义(§7.2);(算子, P, 设备) 能力表在设备初始化时定死分派到 {硬件路径, 兼容路径} |
| 3 | 计算不指定精度 = 操作数中最高;指定 = 指定值 | §8:P_op = 调用点显式 P,否则 max(操作数精度);显式 P 的语义 = 整个算子在该精度下做(决策 Q3-A:操作数 cast、计算、输出存储全在 P) |
| 4 | (补充)代码显式指定精度(算子 + 张量创建),不存在隐式精度推导 | §8.5 + §9:所有 P 实参在调用点可见;值统一来自 PrecisionProfile(模型构造器一行);唯一兜底 Auto = max(操作数) 是可见操作数的纯函数;引擎无任何隐藏的精度状态 |
全部选项与结论(用户逐项确认)。
| ID | 决策 | 结论 | 关键理由 |
|---|---|---|---|
| Q1 | 精度集合 | f16 + f32;bf16 移出范围(GPU 上 bf16 无硬件 ALU,永远兼容路径,价值仅在存储/带宽,留 Phase 2);f64 枚举占位不实现(消费级 GPU 无 f64 ALU) | 范围最小且覆盖 f16 训练主诉求 |
| Q2 | "Tensor 类型参数"的落地方式 | 运行时 tag + 内部类型化:Tensor 非模板(运行时 precision_ + 类型擦除存储),Matrix<P> / GpuTensor<P> 为存储/代数层模板 |
全模板方案会迫使 ComputeEngine 虚接口、所有 Layer、dsl 路径按 P 分裂,与"Layer 只写一次"铁律冲突 |
| Q3 | 指定精度的语义 | A:指定 = 整个算子的精度——操作数升/降 cast 到 P、在 P 下计算、输出存 P。配套:① matmul/归约累加 = max(P, f32);② 数值敏感算子(softmax/LayerNorm/loss)的 P 由 stable 参数显式给(默认 F32) |
语义单一可预测;"f32 算存 f16"不塞进算子参数,用显式 cast 表达 |
| Q4 | 兼容路径(无 P 硬件加速时)的语义 | f32 参考计算 + 每个算子输出舍入到 P(round-half-to-even)。P 精度算术的形式化定义 = f32 参考 + 每算子输出舍入;matmul/归约另加累加 max(P, f32) |
硬件路径与兼容路径语义等价;差异仅在归约内累加顺序 → 跨设备同精度容差内相等,不字节一致 |
| Q5 | 精度控制层级 | 显式 + Model 构造器 PrecisionProfile(4 参数:param / compute / stable / optimizer);每算子调用点显式传 P(值来自 profile) |
比"引擎级隐式 target"更干净;Layer 仍只写一次(P 是可见实参) |
| Q6 | GPU 融合 / AOT 路线 | 分期:Phase 1 = f32 融合世界 + 边界显式 cast + f16 GEMM(expr_spec_key 不变);Phase 2 = in-kernel f16 融合(key 加 1B 精度维度,glsl_gen f16 变体) |
Phase 1 改动面最小 |
| D7 | P 参数形式 | std::optional<Precision> p = std::nullopt;nullopt = Auto = max(操作数);代码库约定 Layer/工厂/优化器/loss 调用永远显式传 |
数据操作类算子的 Auto = 源精度,自然语义 |
| D8 | "张量精度"的拆分 | 用户提的"张量精度"落为 param(权重/参数存储精度);激活/梯度精度不设独立参数 |
激活/梯度精度由产生它的算子参数决定 |
| D9 | loss 的 backward 输出精度 | = stable(loss 前向 + backward 全链路同一 P,默认 F32) |
loss 链 f32 是数值安全默认 |
| D10 | Profile 默认值 | 全 F32 = 今天的行为,零回归 | G5 |
| D11 | 提升序 | F16 < F32;BF16 / F64 为保留值,Phase 1 使用 → Result 清晰报错"精度未实现" |
无 bf16 后无不可公度问题 |
精度是数据的属性(存储)+ 操作的参数(计算);设备只决定执行路径,不改变语义。
graph TB
subgraph L1["L1 语义层(设备无关)"]
P["Precision 枚举<br/>f16 / f32(bf16 / f64 占位)<br/>位布局 + 取值范围 + 舍入规则"]
DEF["P 精度算术的形式化定义(§7.2)<br/>f32 参考 + 每算子输出舍入到 P<br/>matmul/归约累加 max(P, f32)"]
end
subgraph L2["L2 存储层(设备具体适配)"]
T["Tensor(非模板)<br/>device + 形状 + precision_ + 类型擦除存储"]
M["Matrix<P>(CPU,模板)"]
G["GpuTensor<P>(GPU,模板)<br/>buffer = elems × sizeof(elem<P>)"]
T --> M
T --> G
end
subgraph L3["L3 计算层(分派)"]
E["ComputeEngine 原语(+ 显式 P 参数)"]
D["dispatch(op, P, device)<br/>→ 硬件路径 \| 兼容路径(设备初始化时定死)"]
E --> D
end
P --> DEF
P --> T
T --> E
DEF --> D
- L1 保证"精度与设备无关":任何算子在任何精度下的含义唯一(§7.2),设备只是提供实现。
- L2 落实需求 1:存储精度是
Tensor的属性,Matrix/GpuTensor各自类型化承载。 - L3 落实需求 2:
(算子, P, 设备)查能力表分派,硬件加速优先,否则兼容路径。
enum class Precision : uint8_t
{
F16 = 0, // IEEE half(binary16),2 字节
F32 = 1, // 参考精度(reference)
BF16 = 2, // 保留(Phase 2;使用 → 清晰报错)
F64 = 3, // 保留(使用 → 清晰报错)
};F16 < F32(提升序,D11);max()有定义。- 序列化 tag 沿用 v4 数值:0=f32、1=f64(占位,不改动,避免 v4 文件误读)、2=f16、3=bf16(保留)(§11.3)。
| P | 元素类型 | 字节 | 说明 |
|---|---|---|---|
| F16 | nn::f16(新增) |
2 | IEEE binary16 位布局(uint16 存储的 value type);与 GPU 内存中的 f16 / R16F 布局一致(little-endian)→ 同精度 CPU↔GPU 传输 = 原始字节拷贝 |
| F32 | nn::f32 = float |
4 | 现状 |
elem<P>= 上述映射(f16/float)。nn::f16语义:trivially copyable、无异常;构造/比较经 f32 转换;一切 f16↔f32 转换与 f16 输出舍入统一 round-half-to-even(与 IEEE / GPU 硬件一致,Q4 依赖此一致性)。- f16 数值事实(写进预期):最大 65504,最小正规数 2⁻¹⁴,相对精度 2⁻¹⁰——范围远窄于 f32,溢出是 f16 训练的首要数值风险(§12.4 已知限制)。
using Scalar = float 保留,含义收窄为:参考精度(Q4 的 f32 参考)+ 宿主标量类型(lr / wd / epsilon / alpha 等 f32 常量)。BLOCK_SIZE 的 64KB 栈预算 static_assert 维持按 f32 核算。
设计基线:device_ + rows_/cols_ + 每设备一个 shared_ptr 存储成员(互斥)。
改造:
| 成员 | 基线(改造前) | 现状 |
|---|---|---|
precision_ |
无 | Precision,已有 |
cpu_data_ |
shared_ptr<Matrix> |
variant<shared_ptr<Matrix<F16>>, shared_ptr<Matrix<F32>>>(f64 候选可增) |
gpu_data_ |
shared_ptr<GpuTensor> |
variant<shared_ptr<GpuTensor<F16>>, shared_ptr<GpuTensor<F32>>> |
| 其余 | device_ / rows_ / cols_ / 拷贝语义 |
不变(存储仍 shared_ptr 共享,廉价拷贝) |
precision_即"激活候选"的标记,与device_共同唯一确定存储的有效类型。- 无裸指针(铁律 2):类型擦除用
std::variantof 类型化shared_ptr,不做shared_ptr<void>+ 强转。 - 访问器按 P 模板化:
Matrix<P>& cpu_matrix<P>(),P 与precision_不符 →NN_ASSERT(编程错误)。 TensorRef(reference_wrapper)不变。- 图 IR 录制机制不存在(IR-C 不在库中,
Tensor无virtual_tag_),与本文正交;cuda_data_亦不存在(CUDA 后端已整体移除)。
std::vector<elem<P>> data_;现有全部运算代码模板化到 P(代数层是纯 CPU、无虚接口,模板化零成本)。- Phase 1 只实例化 F32 / F16;F32 实例化必须与现状逐字节一致(验收 A/B 测试,§13.1)。
- dsl / 表达式模板链(
expr_dsl.hpp)随 P 实例化。
buffer 字节数 = elems × sizeof(elem<P>)(消除现状sizeof(float)硬编码);分配对齐 ≥ 4 字节(向量宽度)。GpuBuffer::create_device_local / create_host_visible的elem_count语义改为字节数(或 (elem_count, P) 双参,工程细节)。
engine.create_tensor(rows, cols, P) // 张量创建唯一入口(M1 起
// Tensor::cpu 等静态工厂收归引擎私有)
engine.create_tensor(rows, cols, P, InitSpec) // 声明式初值(M2 起,17 §4.4;分布 seed 必填)
GpuTensor<P>::create_empty(rows, cols, backend)
Matrix<P>(rows, cols[, value])
engine.from_matrix(const Matrix& m, P) // 宿主上传(I/O 分组);P 显式engine.to_matrix(const Tensor& t, P) // 重载;P ≠ t.precision() → Result 硬错误
// "需要别的精度" → 先显式 cast(§7 的 cast 原语)- 同精度跨设备(CPU f16 ↔ GPU f16)= 原始字节拷贝(
vkCmdCopyBuffer/ memcpy),零转换——f16 位布局 CPU/GPU 一致。 - 跨精度跨设备 = 逐元素
cast。 reshape(零拷贝视图)继承精度(同一底层存储);CPU 侧 reshape 的复制语义不变。
b_block64KB 栈预算(BLOCK_SIZE² × sizeof(Scalar))按 P 重核:f16 同块元素数可 ×2,或保持块数不变(工程选择,§16)。- activation offload slab 的偏移单位现注释为 "float 单位" → 统一改为"元素单位"(P 决定每元素字节数)。
| P | CPU | GPU(Vulkan) |
|---|---|---|
| F32 | 硬件(现状) | 硬件(shaderFloat32 必有) |
| F16 | 看 ISA:AVX512-FP16 / AVX10 / ARMv8.2-AFP(编译期宏探测)→ 硬件;否则兼容 | 看特性:shaderFloat16(Vulkan 1.2 核心 / 对应扩展)→ 硬件;否则兼容 |
Phase 1 只有两个精度,"兼容路径"实际只有两种情形:① CPU 无 f16 ISA;② GPU 无 shaderFloat16。
前瞻(Phase 2 的 bf16):任何已知 CPU/GPU 均无 bf16 硬件 ALU → bf16 永远是兼容路径精度,其价值在存储/带宽减半而非算术加速。
对 P ≠ F32:
P 精度算术 := 以 f32 参考精度计算 + 每个算子输出舍入到 P(round-half-to-even)
matmul / 归约类算子额外:累加精度 = max(P, f32)
P = F32:参考即自身,不舍入(= 现状行为)
- 硬件路径 ≡ 兼容路径(语义等价):硬件 f16 运算的输出本来就舍入到 f16;tensor core 点积用 f32 累加,与"累加 max(P, f32)"一致。两路径差异仅在归约内部累加顺序 → 同精度跨设备结果容差内相等(最后几个 ulp),不字节一致。
- 该定义使"精度与设备无关"(需求 2)严格成立:任何 (算子, P) 的含义唯一,设备只是实现差异。
- 兼容性推论:CPU 无 f16 ISA 时的 f16 = 逐元素 decode→f32 计算→encode 舍入(慢,但语义精确;省内存不省时间,§12.4)。
- 分派粒度 =
(算子, P, 设备)三元组;设备初始化时建表(GPU:特性查询;CPU:编译期 ISA 宏),运行期只查表。 - 确定性:同一
(算子, P, 设备)永远走同一路径(无容器迭代顺序依赖,铁律 8 满足)。 - 逐算子路径(当前落地表):
| 算子 | CPU f16(无 ISA) | GPU f16(无 16bit 存储) | GPU f16(有 16bit 存储) |
|---|---|---|---|
| 逐元素(融合链内) | f32 参考 + 逐元素舍入(dsl f16 实例化) | 边界 cast → f32 融合世界 → cast 回 f16 | in-kernel f16 变体(float16_t 缓冲、读写点转换、算术默认 f32;设备有 f16 ALU 时命中 native16 变体,§11.1) |
| matmul | f16 读 / f32 累加 / f16 写(tiled,f16 版) | 同左(边界 cast) | f16 GEMM(-DNN_SHADER_F16 双份 SPIR-V,§7.4;小 N 走 f32 回退) |
| 归约 | f32 累加 + 输出舍入 | 边界 cast | in-kernel f16 变体(读写 f16、累加 f32——无 f16 归约硬件) |
| 数据操作(clone/slice/gather…) | 按 P 字节拷贝 | 按 P 字节拷贝 | 同左(supports_native_data_move() 对 f16 直接放行,无边界 cast) |
上表是当前实现;真 f16 计划要求 capable 设备取消 f32 累加与任何 f16↔f32 转换,见
../todo/26-f16-speedup-plan.md§8。
定义(Q4 特化):f16 输入、f32 累加、f16 输出(round-half-to-even)。当前实现:
口径区分(2026-10-18):本节描述当前实现;
../todo/26-f16-speedup-plan.md§8 的“全流程真 f16”是更高目标——支持 f16 计算的设备要求取消 f16↔f32 转换(包括 f32 累加), 累加误差改用 f16 分块/补偿方案。不要把本节的“f32 累加”当作真 f16 的验收口径。
- 一份 .comp 用
glslc -DNN_SHADER_F16=1编出两份 SPIR-V(matmul_tiled.comp/batched_matmul.comp/matmul_gemv.comp,CMakenn_embed_shader(... -DNN_SHADER_F16=1)):#if defined(NN_SHADER_F16)分支把缓冲声明为float16_t、别名槽改uvec2(4×half=8B)、unpackHalf2x16解 vec4、读写点float()/float16_t()转换;f32 分支留在#else逐字未动(宏展开后与裸写 f32 同形 → SPIR-V 字节零差异,零回归由构造保证)。共享 tile / vec4 外积 / 分块 / 双缓冲 / barrier 节奏两版同构,只有全局加载与写出的元素类型不同。 - 依赖与分派:
GL_EXT_shader_16bit_storage+ 设备storageBuffer16BitAccess(后端仅在该能力为真时创建 f16 pipeline,否则句柄空 → 自动回退边界 cast);对齐判据复用 f32 的「元素下标 %4==0」条件(f16uvec28B 对齐 ⇔ f32vec416B 对齐)。 - 未实现:f16 FMA / tensor core 部分积舍入到 f16(原变体③,仍属 §12.3 Phase 2 清单);CPU 侧为
matmul的 f16 实例化(f16 读 / f32b_block/ f16 写)。
engine.cast(const Tensor& src, Precision dst) → Result<Tensor>- 唯一"变精度"算子,永远显式。升 cast 精确(f16→f32 无损);降 cast round-half-to-even。
- 用途:① 混合精度算子的操作数转换由引擎在 dispatch 内完成(§8);② 用户 "f32 算存 f16" = 算子(f32)+
cast(f16);③ 跨精度跨设备传输。
对算子 op,张量操作数 t1..tn,调用点显式参数 p(std::optional,D7):
P_op = *p(调用点显式指定)
否则 max(P(t1), …, P(tn)) // Auto = 用户规则:操作数中最高,
// 纯函数、输入全部可见,非隐藏状态
每个操作数:若 P(ti) ≠ P_op 则 ti ← cast(ti, P_op) // 引擎 dispatch 内完成
计算:逐元素按 P_op;matmul/归约累加 = max(P_op, f32)
输出:存 P_op
lr / wd / epsilon / alpha 等宿主 f32 常量不进 max(否则 scale_inplace(f16 张量, 1e-4) 会把整个张量顶成 f32);标量在算子边界 cast 到 P_op。
in-place 算子(add_inplace / scale_inplace / zero…)的输出即操作数本身,存储精度不可变。显式 P 只改变计算/累加路径,结果舍回原存储精度。例:add_inplace(A_f16, B, P = F32) = f32 计算、结果舍回 f16。想改变张量精度 = 显式 cast。
clone / transpose / slice_rows / insert_rows / gather_rows / scatter_add_rows / rearrange_3d / zero / scan_* 的 Auto = 源精度(单操作数时"操作数中最高"的自然语义);也可显式指定 P(例:gather_rows(f32 嵌入表, idx, P = F16) → f16 输出)。
P 的来源只有两个:
- 调用点显式参数(值可追溯到模型构造器的
PrecisionProfile,§9); - Auto = max(操作数)——可见操作数的纯函数。
不存在第三个来源:引擎无任何默认精度状态,Layer 无任何隐藏策略。唯一"非用户选择"的精度是设备能力分派(硬件/兼容,§7.3)——那是设备物理事实而非语义选择,且 Q4 保证两路径语义等价。代码库约定:Layer / 工厂 / 优化器 / loss 的所有引擎调用显式传 P(可见实参,如 p_.compute),Auto 只留给用户直调引擎与数据操作的自然语义。
struct PrecisionProfile
{
Precision param = F32; // 权重 / 嵌入表 / 参数存储(工厂创建时指定)
Precision compute = F32; // 常规算子的显式 P(matmul/逐元素/gather/scan/数据操作…)
Precision stable = F32; // 数值敏感算子的显式 P(softmax / LayerNorm / RMSNorm / loss,
// 含 loss backward 输出,D9)
Precision optimizer = F32; // 优化器状态(m / v / momentum)创建精度
};
// 默认全 F32(D10)→ 默认 profile = 今天的行为,零回归(G5)参数映射说明(D8):用户原始三参数"张量精度 / 计算精度 / 优化器精度"中——
- "张量精度"拆为
param(工厂创建的权重);激活/梯度不设独立参数:按 Q3-A,算子输出存储精度 = 该算子的 P,激活由compute产生、梯度由反向算子(compute)与 loss backward(stable)产生; - "计算精度" =
compute; - 新增
stable:f16 范围溢出(65504)风险集中在 softmax / loss / 归一化,显式化为构造器参数; - "优化器精度" =
optimizer(与计算精度解耦;f16 训练下 Adam 的 m/v 必须 f32,否则状态被 f16 舍入污染)。
class Layer
{
PrecisionProfile p_; // Model 在 add / 构造 Layer 时注入(每层一份)
};
// Layer 代码仍只写一次,每个调用点的 P 是可见实参(值来自构造器):
Tensor Linear::forward(const Tensor& x)
{
return engine_.matmul(x, W_, false, false, p_.compute);
}
Tensor LayerNorm::forward(const Tensor& x)
{
auto mean = engine_.row_reduce_sum(x, p_.stable);
// …(该层全部原语调用用 p_.stable)
}Model model(engine, PrecisionProfile{F32, F16, F32, F32}); // 显式
Model model(engine); // 默认全 F32 = 现状
auto m = build_gpt_model(engine, vocab, d, s, h, f, L, profile); // 工厂透传
auto opt = create_optimizer("adamw", engine, params, grads, lr, wd,
/*state_p =*/ Precision::F32); // 显式
loss = ce.forward_sparse(engine, logits, labels, mask, vocab,
/*P =*/ stable); // 显式(= 模型 stable)- 权重/嵌入表由工厂按
param创建;优化器状态按optimizer创建。 - 优化器更新算子全部显式:参数更新 P =
param(in-place,存储不变),状态更新 P =optimizer。 - per-layer 覆盖 = Phase 2:
p_本就是每层成员,改成员即覆盖(如末层 head 强制 F32),纯增量。 - CLI 入口的
--f16标志 =profile_f16()= {param=F16, compute=F16, stable=F32, optimizer=F32}(§9.4"f16 存储"行);text_train/mnist_train另有--precision-param/compute/stable/optimizer逐字段覆盖,mem_probe有--f16/--f16-all。master-weights 配方 {param=F32, compute=F16, stable=F32, optimizer=F32} 对应profile_master_weights()。 - 精度预设(2026-10-10 新增):CLI
--t1_58=profile_t1_58()= {param=T1_58, compute=F16, stable=F32, optimizer=F32} —— 定义上就是profile_f16()把param换成三值(另三槽在同一函数里派生 → 不可能与--f16漂移),即"其他默认 f16,模型参数 1.58"。三值只描述权重(T1_58不是存储精度),落地点是BitLinear,见docs/todo/21-quantized-weights.md§4.10。
| 配方 | param | compute | stable | optimizer |
|---|---|---|---|---|
全 f32(现状/默认,profile_f32()) |
F32 | F32 | F32 | F32 |
f16 存储(推荐安全配方)——即 CLI --f16 = profile_f16() |
F16 | F16 | F32 | F32 |
三值权重 + f16 存储——即 CLI --t1_58 = profile_t1_58() |
T1_58 | F16 | F32 | F32 |
master-weights(经典混合精度,profile_master_weights()) |
F32 | F16 | F32 | F32 |
四字段全 f16(真 f16 目标)——profile_all_f16() |
F16 | F16 | F16 | F16 |
(stable 可放宽为 F16,用户自担溢出风险,见 §12.4。)
注意区分:--f16 只把 param/compute 设为 F16,stable/optimizer 留 F32,是默认推荐的安全配方。
四字段全 F16 在 F5(2026-10-14)后已可训练:Adam 状态缩放 S=4096 把 m/v 移出 f16 下溢区,
CE 的 (1,1) loss 标量归约当前恒 F32(见 §12.5)——这是 F5 的数值补丁,与真 f16 的零转换目标
冲突,R2 要改成 f16 安全归约;tiny GPT 20 步与 GPU 500 步记录见
../todo/24-f16-training-defects.md §4.6。但它仍是实验配方:
长跑质量、全模型覆盖与一键 CLI 预设尚未完成,产品化计划见
../todo/26-f16-speedup-plan.md §8。不要把「F5 前不可训练」的
旧结论当作当前事实。同理 --t1_58 只把 param 换成三值(T1_58 只描述权重、不是存储精度):
compute/stable/optimizer 与 --f16 逐字段相同;字面"四槽全 f16 + 三值权重"=
--t1_58 --precision-stable f16 --precision-optimizer f16。
配置:{param=F32, compute=F16, stable=F32, optimizer=F32}(master-weights 配方)。
| 环节 | Layer 内实际调用(P 可见) | P_op | 存储 |
|---|---|---|---|
| 批次上传 | 用户 from_matrix(batch_f16) |
— | f16 |
| embedding | gather_rows(W_f32, idx, p_.compute) |
F16 | f16(W 按 op 降 cast) |
| QKᵀ / AV | batched_matmul(…, p_.compute) |
F16 | f16,f16 GEMM(f32 累加,§7.4) |
| softmax / LayerNorm | …(p_.stable) |
F32 | f32(输入升 cast) |
| FFN | matmul(…, p_.compute) |
F16 | f16 |
| loss 及梯度 | forward_sparse(…, p_.stable) |
F32 | loss f32,grad f32(D9) |
| 反向 matmul | matmul(grad_f32, x_f16, p_.compute) |
F16 | grad 降 cast f16(经典 f16 训练形态) |
| 优化器 | dsl::compute_into(p + g·rparam(-lr), p) |
F32(optimizer) | 权重 f32 更新 |
两个可见的推论(写入文档防止误解):
- 激活"弹跳":
stable算子按 Q3-A 输出存 F32 → 激活在层边界 f16→f32→f16 弹跳;f32 中间量最多多一个张量的显存,可接受。 - 权重按 op cast:f32 权重每个 matmul 降 cast(功能正确,有带宽开销);f16 权重镜像缓存 = Phase 2 优化(D 类),语义不变。
- 精度不进结构 key。
expr_spec_key保持无精度维度;f16 变体按(key, ExprPrecSig)注册(签名 = 逐输入 bit + 输出 bit,sig == 0= 全 f32 → 键就是结构 key 本身)——故全 f32 路径的注册表 key、bin 内容、生成器产物逐字节不变(全 f32 = 零回归,f16_precision_test逐字节断言)。变体段在 bin v9(kExprBinVersion = 9,每变体只存{sig, 基础结构下标},见 §12.6)。 - 三条 f16 执行路径(当前):① in-kernel f16 变体——
GlslEmitter按签名把缓冲声明为float16_t、读写点转换(算术默认 f32,§7.2);同一(key, sig)另可生成 native16 变体(f16 ALU 算术,键key#sig#a),按设备shaderFloat16能力选择 pipeline、运行时优先命中 native16、回退 f32 算术变体;② op-level f16 GEMM——手写 GEMM shader 用-DNN_SHADER_F16编出第二份 SPIR-V,按操作数精度选 pipeline;③ 无变体可命中时回退 边界 cast(ComputeEngine基类 NVI 入口抬 f32 计算、按 P 落回)。 - emitter:IR-D 只有
GlslEmitter一个注册后端。
| 场景 | 契约 |
|---|---|
| f32,同设备 | 不变:并行 = 单线程逐字节(现状契约) |
| f16,同 (设备, P, 路径) | 串行确定性;并行容差 = 与现有 f32 并行同级 |
| f16,CPU vs GPU | 容差内相等(建议 rtol 1e-2 / atol 1e-2,待实测校准),不字节一致 |
| f16 路径 vs f32 路径 | 不可比(不同语义,Q4) |
- gradcheck 容差按 P 分级(f32 沿用现值;f16 用 f16 表)。
- 现有"并行非逐字节"的已知项(col_reduce 并行)不在本文范围,f16 不使其恶化。
- v4:全局 1B tag(0=f32 / 1=f64),校验编译期
Scalar。 - v5:每张量 1B tag:0=f32、1=f64(占位,不改动数值,避免 v4 f64 文件被误读为 f16)、2=f16、3=bf16(保留)。
- v4 文件读入 = 全 f32,向后兼容;v5 读 v4 自动按全 f32。
model_spec.hpp每参数增加精度字段。- 附带收益:f16 权重入
.nnpkg体积减半。
- 状态精度独立于模型计算精度(D8/D10);全部更新算子显式 P(§9.3)。
- 禁止隐含假设:f16 参数 + f32 状态时,参数更新(
dsl::compute_into/add_inplace等 in-place 算子)的 P 显式传optimizer(f32),参数存储精度不变(§8.3 in-place 规则)。
- dsl 模板按 P 实例化(L1 代数层模板化后自然跟随);F32 实例化 = 现状,F16 实例化 = §7.2 定义(无 f16 ISA 时逐元素 decode→f32→encode,慢)。
- 融合 IR(
expr_spec/expr_opt)与精度的关系 = §11.1(结构 key 不含精度,变体按(key, sig)注册)。表达式概念位于expr_dsl.hpp(旧代数 AST 已不在库中)。
- 设备初始化查询
shaderFloat16(VkPhysicalDeviceVulkan12Features或对应扩展),进入 §7.3 分派表。 - 当前 GPU f16 路径:in-kernel f16 变体(
float16_t缓冲 + 读写点转换,算术默认 f32;设备有 f16 ALU 时命中 native16 变体,§11.1)+ op-level f16 GEMM(-DNN_SHADER_F16双份 SPIR-V,§7.4)+ 无变体时的边界 cast 回退。设备初始化另查询storageBuffer16BitAccess(16 位存储,in-kernel 变体的前提;NN_VULKAN_NO_16BIT_STORAGE=1可强制关闭)。 - 无
shaderFloat16的设备:GPU f16 = 兼容路径(功能正确,无加速),平滑降级正是"设备无关"(G2)的含义——能力表自动处理,上层无感知。 GpuBuffer创建参数化(§6.3);MemoryPool块大小按字节,不受 P 影响。
BLOCK_SIZE = 64与PARALLEL_THRESHOLD(按元素数)不变;f16 GEMM 分块是否放大(f16 同块 ×2 元素)= 工程问题(§16)。- 大词表 one-hot 禁止、batch-major 布局等铁律与 P 正交,全部沿用。
过程记录已移出:本章各节只保留当前状态、当前结论与仍然适用的验收标准;分期实施流水、各轮 A/B 实测与被推翻的中间结论见
docs/history/history.md。
(当前状态:D1–D9 全部实现,即当前代码形态。)
| # | 交付物 | 涉及 |
|---|---|---|
| D1 | Precision 枚举 + nn::f16 类型 + elem<P> |
core_config.hpp / precision.hpp |
| D2 | 存储类型化:Matrix<P> 模板化(F32 实例化逐字节不变)、GpuTensor<P>、GpuBuffer 字节数参数化 |
algebra_matrix.hpp、backend/compute_vk_backend.hpp |
| D3 | Tensor 精度属性 + variant 存储 + 显式创建/读取 API |
compute_tensor.hpp |
| D4 | 能力查询:CPU ISA 宏(编译期)、GPU shaderFloat16(运行期)→ 分派表 |
compute_cpu_engine.hpp、GpuBackend |
| D5 | 引擎原语 P 参数 + cast 原语 + from/to_matrix 按 P 重载 |
compute_engine.hpp + 两引擎 |
| D6 | f16 计算实现:CPU(f32 参考 + 舍入;有 ISA 则向量化);GPU(边界 cast 路径 + f16 GEMM,§7.4) | 两引擎 + -DNN_SHADER_F16 双份 SPIR-V(§7.4) |
| D7 | PrecisionProfile + Model/Layer/Loss/Optimizer/工厂接线(全部显式 P) |
model_container.hpp、compute_layer_*.hpp、compute_loss.hpp、compute_optimizer.hpp、domain_*.hpp |
| D8 | 序列化 v5(每张量 tag)+ model_spec 精度字段 |
model_serialization.hpp、model_spec.hpp |
| D9 | gradcheck / 测试容差按 P 分级 + 新增测试集 | src/*_test(见 §13) |
(当前状态:以下 10 条是仍有效的验收标准;f16 类型系统测试已下沉 base 子仓(extern/base/tests/precision_type_test.cpp,其头注释直接引用 §12.2-2 / §12.2-10),门面仓 precision_test(张量精度/计算/配方)、f16_precision_test、gpu_f16_test、model_spec_validation_test 等目标按 §13 覆盖其中对应条目。)
- 回归 A/B:现有 ctest 全量(f32 默认路径)改造前后逐字节一致;
Scalar相关行为零变化。 - cast 往返:f32→f16→f32 误差 ≤ 1 ulp(f16)(相对 2⁻¹⁰)。
- f16 GEMM:CPU/GPU 各自 vs "f32 GEMM 后舍入 f16" 参考在 f16 容差内;CPU vs GPU 同容差。
- 逐元素 / 归约:四组合(CPU 有/无 f16 ISA × GPU 有/无特性)vs §7.2 参考实现(宿主 f32 参考)在容差内。
- 端到端:MNIST MLP 按 master-weights 配方 f16 训练收敛,逐步 loss 与 f32 基线在 f16 容差内。
- in-place 语义:P 参数不改变存储精度,只改计算路径(§8.3 用例)。
- 布局:batch > 1 的序列/注意力 f16 用例(铁律 5:batch=1 测不出布局 bug)。
- 确定性:同 (设备, P, 路径) 串行 3 次运行逐字节一致。
- 序列化:v5 f16 模型 save/load 往返逐字节;v4 文件按全 f32 读入;tag 互锁校验生效。
- 错误路径:BF16 / F64 使用 → 清晰
Result报错("精度未实现")。
- ✅ in-kernel f16 融合:
ExprPrecSig精度签名 +GlslEmitterf16 变体 + 手写 GEMM-D双份 SPIR-V(机制见 §11.1,实测见 §12.6–§12.11)。 - per-layer
PrecisionProfile覆盖(p_成员已就位,纯增量)。 - bf16(兼容路径精度,§7.1 注)。
- f16 权重镜像缓存(消除按 op cast 的带宽开销)。
- tensor core f16 FMA GEMM(当前实现用 f32 累加,§7.4;真 f16 目标要求 f16 累加/分块补偿,见 §12.13)。
- f64(若出现需求)。
- 动态 loss scaling(GradScaler 语义)——Plan A 的标准项(§12.13 A1.1);F5 的 optimizer state scaling 不能替代它。Plan B 可复用为数值缓解技术。
- 全流程真 f16(四槽全 F16)产品化——F5 已关闭「能不能训练」,本项关闭「支持 f16 的设备能不能全程零转换、全模型、可发布地训练」:一键
--f16-all、零转换普查、native16 全覆盖、logits/CE 融合、全模型/全设备长跑与质量基线。细则见../todo/26-f16-speedup-plan.md§8(R0–R6)。
- 当前无训练循环 loss scaling(Plan A A1.1 补):f16 范围 65504,f16 matmul 输出 / 大归约可能溢出 inf。
stable = F32把 softmax / LayerNorm 移出 f16 是第一道防线;训练侧应监控 inf/nan。注:CE 的 (1,1) loss 标量归约输出当前恒 F32(2026-10-13 的溢出修复)。这是 F5 的数值补丁,不是真 f16 的终态:真 f16 计划(todo/26 §8.B)要求用 per-token 平均/分块缩放把它改回 f16 安全归约。 - CPU f16(无 f16 ISA)省内存不省时间(逐元素 decode/encode 开销)。
- 跨设备 f16 容差内相等,不字节一致(§11.2)——不要写"CPU f16 与 GPU f16 逐字节对拍"的测试。
- bf16 / f64 未实现,使用 → 清晰报错。
- 全流程真 f16 仍是实验配方:F5 后
optimizer=f16可训练,但|g| ≳ 0.0625时v会溢出 f16(实践配--max-norm);stable=f16的 softmax / Norm 尾数与长跑质量仍待 R2 硬化;当前 matmul/归约/CE 仍有 f32 累加或 F32 标量,不满足真 f16 的零转换门禁;RAPT f16 另有已知 NaN(../todo/18-roadmap.md§3.1 C11)。产品化门禁见../todo/26-f16-speedup-plan.md§8。
当前实现
| 组件 | 现状 |
|---|---|
| 引擎接口 | 运算类原语(逐元素 / 归约 / 分组归约 / 扫描 / outer_col / eval_expr*)带 Precision P = F32 形参;cast_into(写入既有存储、保留张量对象身份)与 copy_into(同精度就地覆盖)。纯数据搬运原语不带 P(输出 = 源精度,§8.4) |
| 边界 cast 入口 | ComputeEngine 基类 NVI(P-1:原 PrecisionEngine 装饰器已删除下沉至此):f16 边界 cast 集中一处 —— 入参抬 f32 → 调引擎 *_impl 既有 f32 实现 → 输出按 P 落回;in-place 原语走 cast_into 写回原存储(§8.3)。全 f32 配置为纯直通(与原生引擎逐字节一致,f16_precision_test 断言) |
| DSL | dsl::compute / compute_reduce 带 Precision P(输出精度);compute_into 取 dst 存储精度;CPU 侧 f16 叶子一次性转 f32 镜像(CpuViewCache,at() 保持无分支 → 热路径零回归)、eval_cpu/eval_into_tensor_cpu 支持 f16 输出 |
| Layer 接线 | Linear(param + compute)、ReLU/GeLU/SwiGLU(compute)、LayerNorm/RMSNorm(stable + param)、AttentionBase(4 投影 + Softmax + RoPE 一并下传)、FeedForward、GPTModel(位置编码器下传;LM head 强制 stable,见下)、TransformerEncoderLayer/Encoder/PatchEmbedding/PositionalEncoding |
| Loss / Optimizer | Loss 基类带 p_(loss 链 = stable,D9);Optimizer 构造器接 PrecisionProfile,状态张量按 p.optimizer 创建,参数更新 in-place |
| 工厂 / CLI | Model::set_default_precision_profile(必须在 add 之前调用:权重在 init 时按精度创建);build_mnist_*、create_optimizer 接 profile;--f16(= profile_f16())/--t1_58(= profile_t1_58())/--precision-* 在 text_train/mnist_train/mem_probe 启用适配层 |
| 测试 | f16_precision_test:f32 零回归逐字节、f16 DSL / 搬运 / 归约 / matmul、in-place 存储精度不变、GPT 模型级 f32↔f16 逐 step 轨迹对拍 |
机制结论(当前有效)
- 边界 cast 的代价:适配层对每个未命中变体的算子把 f16 入参抬成 f32 副本(再按 P 落回)。被 k 个算子读取的张量要 k 份 f32 副本——典型是
(vocab, batch·seq)量级的 logits 被 loss 链读 4~6 次。LM head 计算精度固定为stable(F32) → logits 与 f32 基线同构、loss 链零 cast。结论:边界 cast 只能拿"存储减半",拿不到"峰值下降"(训练峰值由 transient 决定)——这是 in-kernel f16(§12.6 起)的存在理由;变体命中时不再走该路径(§11.1 三条路径)。 - 数值结论(当前):GPU 上
profile_f16(param+compute f16)与 f32 轨迹一致——同初值/同超参 3.4776→3.1822 vs 3.4776→3.1803,最大相对偏差 6e-4(§7.2 语义成立)。四字段全 f16(profile_all_f16)在 F5 前不可训练:optimizer=F16时 Adam 的 v≈g²~1e-10 下溢到 0 → 更新爆炸(loss 7.9→3.6e4);两者同时 f16 时 loss 恒定(更新被 f16 舍入吃光)。F5(2026-10-14)后已可训练:Adam 状态缩放S=4096把 m/v 移出下溢区(更新量数学不变),tiny GPT 20 步与 GPU 500 步记录见../todo/24-f16-training-defects.md§4.6。CLI--f16仍是安全默认 =profile_f16()= {param:F16, compute:F16, stable:F32, optimizer:F32}(§9.4);四字段全 f16 的产品化见../todo/26-f16-speedup-plan.md§8。 订正(2026-10-13):原记的「stable=F16时 CE 链 ~200 步 NaN」根因是 CE 的 (1,1) loss 归约按stable=f16落盘溢出(大 token 数下 Σ≈ -log(vocab)·N 超 f16 上限 65504 → ±inf,进而 NaN),当时的修复是compute_loss.hpp两条链的 (1,1) 归约输出恒 F32。该补丁让训练健康(GPU 120 步、6.9→5.76),但它是真 f16 的已知偏差:R2 要改成 per-token 平均/分块缩放的 f16 安全归约,不能把 F32 标量当作永久例外。optimizer=F16的爆炸当时不在本修范围,后由 F5(2026-10-14)的 Adam 状态缩放修复。详见../todo/24-f16-training-defects.md§2.1/§3.1/§4.6。 - 外部数值鲁棒性方案(2026-10-18):SR / GradientStabilizer / ECO / Unit Scaling / Peri-LN / 低精度 attention 的
δ修复已列入真 f16 计划;其中 SR / GradientStabilizer / ECO / attentionδ修复进入 R2.5(SR/ECO 需局部 f32 临时量,按 B-Mem 口径不产生 f32 持久态),Unit Scaling 与 Peri-LN 是模型级 R5.x 实验。证据、边界与落地门禁见../todo/26-f16-speedup-plan.md§8.B.7。 - 边界 cast 期的分阶段实测 A/B 与探针数据(f32 3069 MiB / f16 4124 MiB 等)见
docs/history/history.md;当前峰值数字见 §12.11。
已知问题(当前)
- CPU 侧 f16 全模型训练发散(已修复)(双根因与验证见 §12.12);
f16_precision_test对应的容忍分支已改为硬失败。 - 边界 cast 的 transient 膨胀(见上):未命中变体的算子仍走该路径,剩余归因与下一步见 §12.8 / §12.11。
- RAPT / CNN 的 f16 接线已完成(P2-3);剩余覆盖问题不是“未传 P”,而是 RAPT f16 的已知 NaN(
../todo/18-roadmap.md§3.1 C11)与长跑质量验证。 --activation-offload下 f16 激活写入 slab 前被抬为 f32(正确,但该份激活不再减半)。
当前机制(ExprPrecSig 变体索引地基)
ExprPrecSig(expr_spec.hpp):位 i = 第 i 个输入是 f16,bit16 = 输出是 f16;全 0 = 全 f32。expr_prec_sig_key():全 f32 → 结构 key 本身(逐字节等同旧行为),否则key#xxxx—— 故 f32 路径的注册表 key、bin 内容、生成器行为都不变。expr_prec_sig_of(inputs, P)(compute_engine.hpp):由实际张量精度 + 目标输出精度算签名。ExprRegistry的variants/add(spec, sig):sig == 0仍进旧的specs表;sig != 0进变体表。bin(kExprBinVersion = 9)在规格表之后有变体段:每个变体只存{sig, 基础结构下标}(变体与基础结构同 key,不重复序列化 spec 体)。dsl::compute/compute_reduce/compute_into的扫描分支登记(结构, 签名),占位张量按目标精度返回(f16 占位让 dry-run 下游继续看到 f16,否则变体发现不到)。ComputeEngine基类 NVI 入口的NN_PREC_TRACE=1变体发现与trace_miss_():只有入口看得到真实输入精度(引擎*_impl收到的要么是原张量、要么已是 f32 副本),故变体发现放在这里。
实测变体空间(GPT d64/h4/L4/ff256、seq64、profile_f16、真实训练一步)
| 类别 | 变体数 | 输出精度 |
|---|---|---|
| 逐元素 / 归约 | 32 | out=f16 9 / out=f32 23 |
| matmul 段(Linear 族) | 5 | out=f16 |
| fold(注意力) | 1 | out=f16 |
| 合计 | 38 |
- 结论:签名必须逐输入。实测存在
in=[f16,f32] out=f32、in=[f32,f16,f16] out=f32、in=[f16,f32,f32] out=f16等混合组合(LayerNorm/loss 链与参数/激活的精度不同源)——"全 f16 / 全 f32 两变体"的方案会漏掉相当一部分。 - 逐元素/归约占 84%(32/38),且是激活侧 transient 的主要来源(残差、激活、Norm 内部量都在这一类)→ 变体按类别逐批生成:纯逐元素(§12.7)→ matmul 段 / 含归约 / 目标传递(§12.10)→ op-level f16 GEMM(§12.11);fold 变体与运行时签名回填见 §12.10 ⑤。
本节记录的是第一期机制(构建期按签名枚举变体 +
bin v9变体段 +key#sig注册键)。 该机制已整体删除:签名从「身份」降级为「参数」,变体集合不再按签名枚举(见 §12.10 ⑤)。 保留本节仅作机制对照——不是当前实现。
| 组件 | 第一期实现(全部已删除/改写) |
|---|---|
scan_exprs 双 pass |
整段 dry-run 收进 dry_run(engine, profile)(profile_f32 + profile_f16)。已删除:实测 f16 遍对结构贡献为 0(跳过它签名 66 → 0、结构恒 84),而签名已不是构建期集合。 |
bin v9 变体段 |
规格表之后的变体段,每变体只存 {sig, 基础结构下标}。已删除:write_registry/read_registry 连同整个 .bin 中间序列化一起移除(构建期单步)。 |
GlslEmitter |
generate/generate_reduce 的 sig 形参保留至今,但语义已扩展:sig == 0 → V0 全 f32;sig == EXPR_PREC_SIG_DISPATCH → V1 运行期精度分派;真实位图 → V2 存储变体;native16=true → V2b 原生 f16 算术(键 #a)。 |
key#sig 注册 |
当时的键 = key#<sighex>(+ #a)。已消失:现在的键只有 key(V0)/ key#x(V1)/ key#a(V2b),不带签名 hex。 |
| 设备 | 仍有效:查询并启用 storageBuffer16BitAccess(NN_VULKAN_NO_16BIT_STORAGE=1 强制回退);未启用时后端跳过带 # 的键。 |
| 运行时 | 仍有效的两条经验:① FusedInputs{owners, bufs} 必须同时持有 owner——只存裸 GpuBuffer* 会在录制中途释放上传缓冲(铁律 6,实测 A+=B err=0.5);② f16 输出必须按 2B/元素分配缓冲并重贴 GpuTensorF16(漏了 = shader 只写前半 + f32 标签 → 训练 loss=NaN)。变体选择改为 §12.10 ⑤。 |
纯逐元素变体阶段的实测(f32 / f16 边界 cast / in-kernel 三方对照)与该阶段的"剩余缺口"清单见 docs/history/history.md;当前数字与下一步见 §12.11。
归因手段(当前工具):mem_probe 逐阶段探针(transient live / pending / 池分桶)+ ComputeEngine 基类入口的形状级 cast 归因(NN_PREC_TRACE=1 → note_temp_() 记录每次"物化临时量"的 (rows, cols, 方向) → 次数/字节,dump_temp_stats() 在 mem_probe 末尾按字节降序打印)。⚠ 两者都属探针:需 -DNN_ENABLE_PROBES=ON 配置构建(默认构建里 mem_probe 目标不创建、note_temp_ 为空实现,见 AGENTS §2)。
结论(当前有效):
- f16 的额外开销 = 每个未命中变体的算子物化 f16→f32 副本造成的 transient(临时块)数量膨胀,不是驻留张量变大——步末 released 与 f32 基线基本持平,多出来的全部落在 backward 峰值的 transient 桶(10–100MB 中块与 <10MB 小块显著增多)。
- "哪些 cast"必须用形状/尺寸归因表裁决,不要用总量推理(机制之争以归因表为准)。
- 池底材粒度不是峰值杠杆(同窗交错 A/B 复测,见 §12.9/§12.10)。
- 数据搬运类原语(
clone/slice_rows/insert_rows/zero)由supports_native_data_move()对 f16 直接放行,省 2 份全尺寸临时量;GPT 路径上峰值中性(数据量小),保留(对其它 workload 有净收益)。
逐阶段探针数字与归因过程见 docs/history/history.md;当前剩余 cast 与下一步见 §12.11。
当前结论:池底材粒度不是峰值杠杆。 同窗交错 A/B 复测三档池配置(默认 12MB 固定块 / NN_POOL_LADDER_MAX_MB=16 / NN_POOL_BLOCK_MB=4):峰值均值差仅个位数百分比、耗时无差异 → 池保持默认;NN_POOL_BLOCK_MB / NN_POOL_LADDER_MAX_MB 只作探针 / 实验旋钮(text_train 的提示指向本节)。方法论教训:跨会话单点 + 不同时段的对照组会被系统漂移误导;机制之争必须用形状/尺寸归因表裁决(§12.10),不要用总量推理。
本节原始 A/B 数据(含一个不可复现的"−21% 峰值 + 2× 耗时"跨会话单点)与复盘见
docs/history/history.md。 仍有效的副产物:NN_PREC_TRACE=1的[prec][miss]打印——真实 GPT 配置 38 个非零签名请求只 6 个未命中(eval_expr 路径 84% 已原生),说明 cast 大头不在融合路径上。
bench/run_ab_env.ps1(按 env/args 同窗交错 A/B + 峰值/耗时配对)复测三档池配置(默认 12MB 固定块 / NN_POOL_LADDER_MAX_MB=16 / NN_POOL_BLOCK_MB=4)→ 峰值均值差仅个位数百分比、耗时全部 ≈5.7s:池粒度不是杠杆,池保持默认;NN_POOL_BLOCK_MB / NN_POOL_LADDER_MAX_MB 是探针 / 实验旋钮(text_train 启动提示即指向本节)。池账本计数器并入 pool_stats()(calls / blk_new-free / 扫描 block/region 数 / vkalloc ms,mem_probe 每阶段打印)——1 万次 allocate 只扫 ~5 万 block、vkAllocateMemory 累计 ~150ms,池记账不是瓶颈。
ComputeEngine::note_temp_()(NN_PREC_TRACE=1 时记录每次"物化临时量"的 (rows, cols, 方向) → 次数/字节)+ ComputeEngine::dump_temp_stats()(mem_probe 末尾按字节降序打印)。这份表把 GB 级 cast 直接落到具体形状——判定"哪些 cast 还在、哪些已被变体消掉"的当前手段(如注意力反向物化的 (32768,256) W / grad_A,曾是单项最大头,补齐 matmul 段变体后该项消失,见 §12.11)。
下表描述的三条生成路径仍在(同一批生成器、同一份索引数学),但变体已不再按签名枚举: 每条路径现在都被要求能发射
EXPR_PREC_SIG_DISPATCH(V1 双视图 + PCprec)形态,见 ⑤。 表中"(key,sig)匹配"的措辞已被find_prec_variant_的key/key#x/key#a取代。
| 项 | 机制 | 效果 |
|---|---|---|
| matmul 段 | generate_glsl_matmul(name, spec, sig):A/B 槽 float16_t 声明 + uvec2 别名槽(4×half=8B,unpackHalf2x16 解 vec4)+ 全局加载处统一转 f32(共享 tile / VFMA 累加 / 尾链全 f32)+ 输出按 out 位 float16_t();分块/双缓冲/barrier 节奏与 f32 逐字一致(V1 下这两条加载路径都发射,运行时按 PC mm_trans 选) |
matmul 段变体可生成 |
| 归约 kernel | generate_glsl_reduce(name, spec, sig):输入 float16_t + rd()/wr() 在读写点统一转换(含 emit_mm_decl 点积、行/列两个 pass 的直接索引读、operand() 的视图读与广播读、输出写) |
含归约的变体可生成 |
| 目标传递 | GpuEngine::eval_expr_into / eval_expr_reduce 带变体匹配(输出精度 = dst.precision() / P),f16 输出重贴 GpuTensorF16(归约向量形状按 raxis 取 (rows,1)/(1,cols));两入口先查 supports_expr_precision_variant 再回退 cast |
compute_into / compute_reduce 不再必然走边界 cast |
生成阶段的基础结构数与变体条目数以构建输出 [scan] / [gen] 为准([gen] 打印"N 条融合表达式 + M 条变体")。
回填清单已不存在(无 tools/prec_backfill.txt;M = V1 条数 + V2b 条数,全部由构建期一次算定)。
matmul 段 / 归约 / 目标传递三类变体落地后的同窗交错实测(f32 与 f16 峰值、耗时、backward transient 分桶变化)见 docs/history/history.md;当前(含 op-level f16 GEMM)的最终数字见 §12.11。
签名已从「身份」降级为「参数」:融合 shader 的变体集合不再按签名枚举,而是对每个结构发一份运行期精度分派 shader(键 key#x)——它声明每个输入/输出各 f32 + float16_t 双视图(同一 VkBuffer 绑两次,只解引用与真实元素类型一致的那一个),加载/存储处按 push constant uint prec(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支。索引数学与旧带类型变体逐字相同 ⇒ 数值等价。
- 精度路径三档(不是"永不 miss"):
key#a(V2b native16)→key#x(V1 运行期分派)→ 基类边界 cast。V1 默认对每个结构无条件生成,但生成被跳过([skip] 分派变体…)、NN_SCAN_NO_DISPATCH=1、或设备无 16bit 存储时会 miss 并落到边界 cast ——[prec][miss]因此仍是通用诊断(实测 f16 融合路径 miss = 0)。 - native16 改由结构谓词判定生成(键
key#a,代入"全输入 f16 + 输出 f16"这一常量签名)。⚠ 该谓词含常量值判据(0 < |c| < 6e-8会 flush-to-0),而常量值不进 key ⇒ 运行时选#a前必须用真实实例的 spec 重跑同一谓词(GpuEngine::find_prec_variant_),否则"生成时那一个实例的常量"会被套用到同结构但常量不同的实例上(静默错值)。 - 回填清单已删除:
tools/prec_backfill.txt连同NN_PREC_TRACE的"补变体"工作流一并移除;f16 扫描遍也因"对结构贡献为 0"而删除(实测:跳过它签名 66 → 0、结构恒 84)。 - 追踪日志口径:变体命中只在确认命中后、且只由真正 dispatch 的入口打(
supports_expr_precision_variant只是查询)。修复前该函数也打日志 ⇒ 计数翻倍(实测 12040 行 = 6020 次真实分派 × 2;修复后同为 f16_precision_test 实测 6022 行 / 24 条 distinct = 20 条#x+ 4 条#a)。 - 实测(GPT f16 训练,
NN_PREC_TRACE=1):[prec][miss]=0、边界 cast 归因表为空、[prec][alu-hit]6670 —— ⚠ 6670 是修复"双重打点"前的口径,真实分派数为其一半。
glslc 支持 -Dmacro[=defn],matmul_tiled.comp / batched_matmul.comp / matmul_gemv.comp 顶部加编译期开关(f32 分支留在 #else 里逐字未动,宏展开后与裸写 f32 同形 → f32 SPIR-V 字节零差异,零回归由构造保证):
#if defined(NN_SHADER_F16)
#extension GL_EXT_shader_16bit_storage : require
#define NN_ETYPE float16_t
#define NN_V4 uvec2 // 4×half = 8B,std430 步长恰好 8
#define NN_LOADV4(arr, i) vec4(unpackHalf2x16(arr[i].x), unpackHalf2x16(arr[i].y))
#define NN_RD(x) float(x)
#define NN_WR(x) float16_t(x)
#else
#define NN_ETYPE float
#define NN_V4 vec4
#define NN_LOADV4(arr, i) arr[i]
#define NN_RD(x) x
#define NN_WR(x) x
#endif- 共享 tile / vec4 外积累加 / BM/BN/BK / 双缓冲 / barrier 节奏完全同构,只有全局加载与写出的元素类型变了 → 语义 = §7.2「f32 参考累加 + 输出舍入」。
- f16 下不提供
C的 vec4 别名视图(逐标量float16_t(...)写出,与 f32 的N%4!=0回退分支同构);对齐判据完全复用 f32 的(f16uvec2需 8B 对齐 ⇔ 元素下标 %4==0,与 f32vec4需 16B 对齐是同一条件)。 - CMake:
nn_embed_shader(matmul_tiled_f16 ... -DNN_SHADER_F16=1)(同法已扩展到reduce/transpose/gather/rearrange_3d/scan_*/outer_col/group_reduce/scatter_add/im2col/col2im/elementwise_v2等 op 级 shader,清单见CMakeLists.txt)。
- 后端为 f16 变体各建一套 pipeline,
device_.has_16bit_storage()为假时不创建 → 句柄空 → 引擎自动走边界 cast(正确性不变,只是拿不到收益)。 matmul_gpu/batched_matmul_gpu带f16_io:输出按 2B/元素 分配(GpuTensorF16::create_empty后包一层GpuTensor(shared_buffer, rows, cols)纯绑定视图,与run_fused_gpu的out_f16同一套做法),pipeline 选 f16 版,调用方按GpuTensorF16重贴标签;f16_view(Tensor)只借 buffer + 形状、不做精度转换。- 适配层把
P != F32直接下传内层(matmul/batched_matmul/matmul_with_bias)——本层不再预 cast,否则内层永远看不到 f16、原生 GEMM 永远命中不了。 - 小 N 走 f32 回退(
matmul的n_out > 8门槛):GEMV 场景张量本就小,f16 收益为零却要吃 64×64 块空转(推理 batch=1 热路径)。 CpuEngine::matmul_with_bias同样下传P——scan 的 f16 dry-run 靠dsl::compute的NN_EXPR_SCAN钩子按真实操作数精度 + P 登记(结构, 签名),吞掉 P 就只登记全 f32 签名,Linear::forward 的带类型 matmul 段变体永远发现不到。
| 峰值 MiB(3 轮) | 耗时 | text_train avg_loss | |
|---|---|---|---|
| f32 | 1754 / 1754 / 1754 | 5.11s | 6.6979 |
f16(--f16) |
1570 / 1570 / 1570 | 5.05s | 6.7493(差 0.77%,f16 容差内) |
- −10.5% 显存(184 MiB)且略快;f16 峰值完全确定(不再双峰)。batch64 同为 −10.5%(3052 vs 3412 MiB;batch64 不再 OOM)。
f16_precision_test覆盖 f32 零回归逐字节 + GPT 逐 step 轨迹对拍。 - 该数字是 op-level f16 GEMM 落地时的记录;其后 fold 带类型变体(Phase D2)、run-only 签名回填(Phase D3,§12.10 ⑤)与更多 op 级 f16 变体继续落地,当前峰值以
mem_probe --f16同窗复测为准。 - 演进全景(f16 峰值如何从"高于 f32"走到"低于 f32")见
docs/history/history.md。
- 仍走边界 cast 的 op-level 原语:用
NN_PREC_TRACE=1+dump_temp_stats()复测形状表,对剩余大项按 §12.11 ① 的-DNN_SHADER_F16手法补变体(同法已覆盖大部分 op 级 shader,见 ① 的 CMake 清单)。 - 签名覆盖:不需要任何操作——生成阶段对每个结构发运行期精度分派 shader(§12.10 ⑤),任何签名都命中;新的调用点只要被构建期收集到结构即自动获得 f16 路径(实测:Layer 头里新写一个
dsl::compute→ 注册表自动多出key/key#x/key#a三条,其他文件零改动)。 - native16(f16 ALU 算术)变体按设备能力命中/回退(§11.1),无需额外操作。
§12.5 已知问题 1("CPU f16 全模型训练 5~7 步 NaN,未定位;单算子测试全过")已定位并修复。 实为两个独立缺陷叠加,且两者都被测试盲区掩盖:
MatmulRef::prepare_cpu固定以P=F32物化 C,而CpuEngine::matmul的 f32 路径按 f32 存储直读A.cpu_matrix()—— f16 张量上cpu_get_ptr<F32>()返回空指针 (NN_ASSERT在 NDEBUG 下为空)→ Debug 构建 fail-fast、Release 读空/垃圾。 症状:text_train --f16(CPU)0xC0000005 访问违例;组件探针 A1 阶段断言cpu_matrix<P>() const: tensor has no P-precision CPU storage。- 同族缺陷:
ReduceViewRef::prepare直读r.t.cpu_matrix();适配层matmul/batched_matmul把P != F32无条件下传(操作数混合精度时内层按单一精度直读);CPU 解释器eval_expr_impl输入/输出未校验精度。 - 修复:
prepare_cpu先把非 f32 操作数抬到 f32 再物化(C 按 f32 绑定);ReduceViewReff16 输入一次性镜像;CpuEngine::matmul/batched_matmul精度与存储不匹配时 统一回退 f32 空间计算 + 输出按 P 舍入(两端同 f16 才走原生 f16 GEMM);eval_expr_impl加精度NN_REQUIRE(把静默 UB 变成响亮报错)。
precision.hpp的 f32→f16 转换在exp < -14分支的守卫写成exp <= -46,但其注释 自己的公式只对 exp ∈ [-25,-15](shift = -exp-1 ∈ [14,24])成立: exp ∈ [-45,-33] 时 shift ≥ 32 → uint32 移位 UB(x86 按 5 位掩码 →d = full >> (shift&31)取到大数 → 低 16 位回绕成垃圾 half)。- 症状:
|v| ∈ [2.8e-14, 1.2e-10](2⁻⁴⁵~2⁻³³)的值被转成0x4000(=2.0)、0xCCCD、 512、8192、11776、18432,随机落进 e=31 时是 NaN。梯度正是这个量级(前向激活 ~0.1 从不落入该窗口)→ 损坏只出现在 f16 梯度/参数写回 → 若干步后更新爆炸、loss NaN。 GPU 走硬件 f16 转换(packHalf2x16/half())故全程正常 —— 与"只有 CPU 错"完全吻合。 - 为什么既有测试没抓住:① 全位域往返测试(h→f32→f16)的输入全部来自 half→float,
exp ≥ -24,进不了窗口;②
ref_ulp写成2^(e-10)(应为2^(e-25),大 2¹⁵ 倍)→ RHE 容差比被测值本身还大 → 垃圾值(如 f=2.0 vs v=1e-12,容差 32)全部放行。 - 修复:守卫改
exp <= -26(≤-26 按公式 D < 0.5 恒 flush 到 0,与注释一致); 测试ref_ulp改2^(e-25);新增 exp ∈ [-60,-26] 全网格断言(必须恒 0)+ 两个历史 垃圾代表值(1.14e-12、8.44e-11)定点断言。
- 组件探针
f16_cpu_probe(阶段 A–I,可独立编译也可走 CMake 目标):修复前 param/compute/f16(CLI) 三组 profile 在 6 步内出现 11776/512/8192 级垃圾梯度 (NN_F16_DEBUG=1逐中间量扫描把故障钉到lin.grad_w(post-accum)与accumulate的0 + 8.7e-13 → 18432);修复后 bad=0,三组轨迹与 f32 逐位一致 (3.4776→3.4640→3.4508→3.4381→…)。 f16_precision_test的"已知问题"容忍分支改为硬失败(未来回归直接红)。- 诊断开关沉淀(默认零开销):
NN_F16_DEBUG=1→ 层内nn_dbg_scan逐中间量 max/非有限 扫描 +compute_into预绑定失败打印 +accumulate/eval_into/prepare异常打印。 - profile 单字段矩阵(f16_precision_test
NN_F16_DEBUG=1):param/compute/stable/CLI 四组均收敛且贴合 f32;optimizer=F16的旧发散已由 F5 的 Adam 状态缩放修复 (§12.5 / todo/24 §4.6),与本节两个缺陷无关。 - ctest 全绿(含新增断言);
text_train --f16CPU 可正常训练。
| 路线 | 配方 | 目标 | 转换策略 |
|---|---|---|---|
A 标准 --f16 |
profile_f16() = {F16,F16,F32,F32};可选 profile_master_weights() = {F32,F16,F32,F32} |
参考 PyTorch AMP:稳定训练/推理,允许 f32 累加与边界 cast | stable/optimizer 保持 f32;补动态 loss scaling(GradScaler 语义)与 master weights 对照 |
| B 全流程真 f16 | profile_all_f16() = {F16,F16,F16,F16} |
支持设备全程零 f16↔f32 转换;不支持设备只由引擎在算子内隐式转换 | capable:native16 全覆盖、零转换;incapable:转换只进 *_impl |
Plan A 当前事实:--f16 已是推荐安全配方(f16 参数/激活 + f32 stable/optimizer),
profile_master_weights() 已存在;缺 GradScaler 语义的动态 loss scaling,且
bench/train_torch.py --dtype fp16 是 model.half() 纯 fp16,不能代表 PyTorch AMP。
严格 AMP 的默认映射(保留 --f16 还是切到 master weights)由 26 §8.A A0 先裁定,不先改语义。
Plan B 当前事实
| 项 | 状态 |
|---|---|
profile_all_f16() |
✅ 可构造(四槽全 F16) |
optimizer=f16 |
✅ F5 后可训练:Adam 状态缩放 S=4096,m/v 回到 f16 正常区 |
stable=f16 的 CE 标量 |
🟡 当前 (1,1) loss 归约输出恒 F32;这是 F5 的数值补丁,与真 f16 冲突,R2 要改成 f16 安全归约 |
| 一键 CLI 预设 | ❌ text_train / mnist_train 尚无 --f16-all,目前只能拼三个旗标 |
| 长跑与质量 | 🟡 已有单模型 500 步记录;缺 3 seeds、真实规模、全模型与 f32 同窗基线 |
| capable 设备零转换 | ❌ 当前仍有 #x/边界 cast/cast_into 与 f32 累加路径;真 f16 要求 cast = 0、f32 临时量 = 0 |
| f16 累加 / 归约 | ❌ matmul / reduce / fold / CE 仍存在 f32 累加器;真 f16 要求 native16 全链或分块/补偿实现 |
| 原生 f16 ALU 覆盖 | ❌ #a 仍排除 fold/matmul/reduce;真 f16 要求覆盖全部计算结构 |
无 shaderFloat16 设备 |
🟡 允许转换,但只能由引擎在算子内部隐式完成;尚未建立 Layer/DSL 零 cast 的自动门禁 |
| RAPT f16 | 🟡 仍有已知 NaN(todo/18 §3.1 C11),R5 前必须关闭 |
“全流程真 f16”的定义(Plan B):{param, compute, stable, optimizer} 四槽全 F16 覆盖从张量进入引擎到
输出离开引擎的整条计算图。设备支持 f16 计算时,全程不允许任何 f16↔f32 转换:native16 覆盖
全部算子,load/store、算术、累加、常量与输出都保持 f16,不允许 cast/cast_into/f32 镜像/半真路径。
设备不支持 f16 计算时,转换只允许发生在 ComputeEngine 的算子实现内部,由引擎自动插入;
Layer/DSL/CLI/模型代码不得手写 cast,不得产生跨算子的 f32 驻留副本。模型文件 / 张量 I/O 保持
f16;数据集 token/id 是整数;只有日志/调试打印这类宿主展示边界允许 f32。
后续计划:Plan A = A1.1 动态 loss scaling → A1.2 master weights 对照 → A1.3 敏感链 f32
→ A2 PyTorch AMP 长跑验收;Plan B = R0 口径统一 → R1 --f16-all 预设 → R2 零转换普查与真 f16
数值硬化 → R3 logits/CE 融合(先补 CE backward 扫描)→ R4 native16 全覆盖(核心门禁)→ R5 全模型/
全设备覆盖 → R6 转正。完整验收矩阵与旧结论(S0/S1/S2/S5)的处理见
../todo/26-f16-speedup-plan.md §8。
一句话:F5 关闭了“能不能训练”;Plan A 关闭“标准混合精度能不能稳定可用”, Plan B 关闭“支持 f16 的设备能不能全程零转换、全模型可发布”。
| # | 测试 | 说明 |
|---|---|---|
| T1 | 回归 A/B(§12.2-1) | 改造前后全量 ctest 输出逐字节 diff |
| T2 | cast 往返 / 舍入 | f32→f16→f32;round-half-to-even 边界值(midpoint、denormal、±65504、inf/nan 传播) |
| T3 | f16 GEMM 对拍 | 小/中/大尺寸;CPU vs GPU;f32/f16 双份 SPIR-V 产物语义一致(同设备内逐字节,跨设备容差) |
| T4 | 逐元素 / 归约四组合 | 对照 §7.2 宿主 f32 参考实现 |
| T5 | 端到端 MNIST f16 | master-weights 配方;loss 曲线 vs f32 基线(f16 容差);batch > 1 |
| T6 | in-place 语义 | §8.3 用例:P 不改变存储精度 |
| T7 | 布局 | batch > 1 注意力/序列 f16(铁律 5) |
| T8 | 确定性 | 同 (设备, P, 路径) 串行 3 次逐字节 |
| T9 | 序列化 v5 | f16 模型 save/load 往返;v4 读入 = 全 f32;tag 互锁 |
| T10 | 能力分派 | 无 f16 ISA 的 CPU 构建 / 无 shaderFloat16 的 GPU(真实设备或 mock 能力表)走兼容路径,功能正确 |
| T11 | 错误路径 | BF16 / F64 → 清晰 Result 错误;to_matrix 精度不符 → 硬错误 |
| 风险 | 等级 | 对策 |
|---|---|---|
L1 代数层模板化改动面大(algebra_matrix.hpp ~1000 行) |
高 | Phase 1 只实例化 F32 / F16;T1 A/B 测试强制 F32 逐字节不变;改动分批 |
| GPU f16 GEMM 是新代码路径 | 中 | 小矩阵先行 vs CPU f16 对拍;单一 -DNN_SHADER_F16 双份 SPIR-V 实现(f32 分支逐字未动,§7.4) |
shaderFloat16 设备覆盖不全 |
低 | 无特性 = 兼容路径,功能正确(Q4),平滑降级 |
| AOT 世界被破坏 | 低 | 结构 key 不含精度;f16 变体按 (key, sig) 另行注册(§11.1),全 f32 路径逐字节零回归 |
| f16 溢出导致训练发散 | 中 | stable = F32 + §12.4 明示 + 训练 inf/nan 监控;loss scaling 留 Phase 2 |
| f16 容差取值不当(过松掩盖 bug / 过紧误报) | 中 | T4/T5 实测校准后定默认,gradcheck 按 P 表分级 |
模块级清单(设计用途),非实现分工。
| 文件 | 影响 |
|---|---|
core_config.hpp / precision.hpp |
Precision、nn::f16、elem<P>;Scalar 保留(参考精度,§5.3) |
algebra_matrix.hpp |
Matrix<P> 模板化(F32 实例化逐字节不变) |
compute_tensor.hpp |
Tensor 精度属性 + variant 存储 + 显式创建/读取 API |
backend/compute_vk_backend.hpp |
GpuTensor<P>、GpuBuffer 字节数参数化、shaderFloat16 查询、f16 GEMM pipeline |
compute_engine.hpp + compute_cpu_engine.hpp + compute_gpu_engine.hpp |
原语 P 参数、cast、from/to_matrix 重载、f16 实现、能力分派表 |
shaders/ |
op 级 shader 的 -DNN_SHADER_F16 f16 变体(matmul_tiled / batched_matmul / matmul_gemv / reduce / transpose …,双份 SPIR-V,见 §7.4 / §12.11) |
expr_dsl.hpp / expr_* |
按 P 实例化(F32 不变 + F16);结构 key 不含精度,变体按 (key, sig) 注册(§11.1) |
compute_layer_*.hpp |
p_ 成员 + 全部原语调用显式 P |
compute_loss.hpp / compute_optimizer.hpp |
显式 P(loss = stable;状态 = optimizer;参数 = param) |
model_container.hpp / model_spec.hpp |
PrecisionProfile 持有与透传 |
model_serialization.hpp |
v5(每张量 tag) |
src/(CLI 入口) |
预设 --f16(= profile_f16())/ --t1_58(= profile_t1_58())与 --precision-* 逐字段覆盖 |
src/*_test / src/*_probe |
§13 测试集 + gradcheck 容差按 P 分级 |
(第 3、4 项已定:边界 cast = ComputeEngine 基类 NVI 入口(原 PrecisionEngine 已下沉,§12.5);CLI 标志 = --f16 + --precision-*(§9.3)。其余仍开放:)
- f16 容差默认值(当前建议 rtol 1e-2 / atol 1e-2)——T4/T5 实测后校准。
- f16 GEMM 分块尺寸:复用
BLOCK_SIZE = 64还是放大(f16 同块 ×2 元素,64KB 预算下可 128)——bench 定。 边界 cast 的实现形态(已定,见上)。CLI(已定,见上)。--f16的确切标志名nn::f16的 denormal 处理:按 IEEE 全精度 denormal 还是 flush-to-zero(GPU 硬件行为不一致处需统一;默认全精度,性能敏感路径再议)。