Mint 项目技术解读报告
面向零基础读者的完整技术文档,模型部分(第五章)含逐组件数学公式推导。报告基于 2026-09 会话完成分析完善后的代码状态撰写(Git 提交 8f4713f → f930f70);v1.0.0 又做了一轮静默失败加固、界面重构与赞赏入口,并修订了本文中与实测不符的数字。
目录
一、项目概览
1.1 一句话总结
Mint 是一个用 Rust 语言从零手写的中文聊天 AI 项目,包含模型训练、微调、聊天三大功能,不依赖 PyTorch/TensorFlow 等任何深度学习框架,连 GPU 加速代码(CUDA 核函数)都是手写的。
1.2 类比理解
| 常规做法 | Mint 的做法 |
|---|---|
| 用乐高成品套件拼模型 | 从烧塑料做积木开始,自己捏每一块零件 |
调用 PyTorch 的 nn.Transformer | 自己写注意力、归一化、激活函数的数学实现 |
| 用 cuBLAS 官方矩阵乘法库 | 自己写 16×16 分块矩阵乘法 GPU 程序 |
| 用 AutoGrad 自动求导 | 逐层手推反向传播公式并手写实现 |
这样做的好处是完全可控、可解释、无黑盒;代价是工作量大、精度上限不如大厂框架。
1.3 项目能力清单
- 训练:把中文语料(Belle 指令对话、维基百科、LCCC 闲聊等)训练成会续写/聊天的模型
- 微调:加载已有模型继续训练(学习率自动降低、warmup 自动缩短)
- 聊天:加载检查点,像 ChatGPT 一样逐字生成回复,支持多轮对话记忆
- 可视化:终端进度条 + 实时损失曲线 + 自动生成 HTML 训练报告
- 模型管理:列出检查点、查看模型详情、GPU 状态查询
1.4 技术栈
| 层 | 技术 | 说明 |
|---|---|---|
| 语言 | Rust(edition 2021) | 内存安全、高性能 |
| GPU | CUDA 12.0 + 手写核函数 | kernels.cu 共 26 个核函数 |
| 数据 | serde / serde_json | JSON/JSONL 解析 |
| 并行 | rayon | CPU 端矩阵乘法多线程加速 |
| 随机数 | rand 0.8 | 权重初始化、数据打乱、采样 |
| 时间 | chrono | 检查点时间戳 |
| 构建 | nvcc + MSVC + rc.exe | CUDA 编译、图标嵌入 |
二、目录结构详解
E:\mimo\Mint
├── Cargo.toml # Rust 项目清单:依赖、编译配置(cuda 特性默认开启)
├── Cargo.lock # 依赖版本锁定
├── build.rs # 编译前脚本:编译 CUDA、嵌入图标
├── mint.rc # Windows 资源脚本(引用图标)
├── icon/mint.ico # 程序图标
├── dll/ # 历史遗留 cuBLAS DLL(98MB + 466MB,已废弃,git 忽略)
├── data/ # 训练数据集目录(Belle、zhwiki、LCCC、CLUE 系列)
├── V1 ~ V21/ # 历代版本编译产物归档
├── docs/ # 本文档
└── src/ # 全部源代码
├── main.rs # 程序入口、交互菜单、训练/聊天驱动循环(约 1300 行)
├── model/
│ ├── mod.rs # 模块导出
│ └── transformer.rs # Transformer 模型本体(约 500 行)
├── tensor.rs # CPU 张量运算库(约 300 行)
├── training.rs # GPU 训练器、前向/反向传播(约 2000 行,最大文件)
├── data.rs # 数据发现/解析/词表/分批(约 1100 行)
├── checkpoint.rs # 模型保存/加载(约 250 行)
├── chart.rs # 进度条、图表、HTML 报告(约 500 行)
├── dll_embed.rs # CUDA 运行时 DLL 目录设置
└── gpu/
├── mod.rs # GPU 初始化、状态、测试锁
├── kernels.cu # 手写 CUDA 核函数(约 650 行)
├── cuda_ffi.rs # Rust ↔ CUDA 的 FFI 声明层
└── ops.rs # GPU 操作安全封装(GpuBuffer 等)
各模块职责与调用关系
main.rs(界面层)
├─ 调用 data.rs ── 加载数据、建词表、分批
├─ 调用 model/transformer.rs ── 建模型 / CPU 聊天推理
├─ 调用 training.rs ── GPU 训练(前向/反向/优化器)
│ └─ 调用 gpu/ops.rs ── GPU 算子封装
│ └─ 调用 gpu/cuda_ffi.rs ── FFI 声明
│ └─ 调用 gpu/kernels.cu ── CUDA 核函数
├─ 调用 checkpoint.rs ── 保存/加载模型
└─ 调用 chart.rs ── 进度与报告
三、技术栈与构建环境
3.1 编译产物与特性开关
cargo build --release默认启用cuda特性(Cargo.toml中default = ["cuda"])- 编译配置:
opt-level = 3+lto = true(链接时优化,充分压榨性能) - 运行时不依赖任何外部 DLL:CUDA 运行时(cudart)静态链接进 exe,单文件分发
3.2 build.rs 做了什么(编译前的三步准备)
- 编译 CUDA 核函数:
- 定位 CUDA Toolkit(读
CUDA_PATH环境变量,或按常见安装路径搜索 v12.0/v11.8/v11.0) - 定位 MSVC 编译器(调用
vswhere.exe官方工具查找 Visual Studio) - 用
nvcc把kernels.cu编译成目标文件,生成三种设备代码: sm_86:RTX 30 系显卡(如本项目用的 RTX 3050 Laptop)sm_80:A100 数据中心显卡compute_86PTX:中间代码,供未来新架构显卡即时编译兜底- 用
ar/lib.exe打包成静态库libmint_cuda.a,链接进最终 exe
- 定位 CUDA Toolkit(读
- 嵌入图标:找到 Windows SDK 的
rc.exe,把mint.rc+icon/mint.ico编译成资源文件并链接(找不到 rc.exe 只警告不报错) - 失败即报错:找不到 CUDA 或 MSVC 直接给出明确错误信息,不会生成缺胳膊少腿的二进制
3.3 开发机硬件(实测)
| 项 | 值 |
|---|---|
| GPU | NVIDIA GeForce RTX 3050 Laptop GPU,4096 MB 显存 |
| CUDA Toolkit | 12.0(D:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.0) |
| 显存限制 | 4GB 是硬约束:medium 模型 + batch 8 恰好装下,超限会明确报错 |
四、背景知识铺垫
给完全没接触过机器学习读者的最短路径解释。
4.1 模型在做什么:预测下一个字
模型的唯一任务:输入一串字,输出"下一个字最可能是谁"的概率表。
输入:今天天气真
输出:好(32%) 不(18%) 热(9%) 冷(5%) ……(对词表里每个字给一个概率)
把预测出的字接到末尾,再预测下一个,循环往复就能写出整段话。ChatGPT 等大模型的底层逻辑与此相同,只是规模大几万倍。
4.2 训练在做什么:让预测越来越准
- 给模型看大量真实文本
- 每次让模型预测,和真实答案对比,算出"错得有多离谱"(损失函数,这里是交叉熵)
- 沿"能减小错误"的方向微调每一个参数(反向传播 + 梯度下降)
- 重复几亿次,模型就"学会"了语言的统计规律
4.3 几个高频术语的直觉版解释
| 术语 | 直觉解释 |
|---|---|
| 张量(Tensor) | 多维数组。0 维=一个数,1 维=一行数,2 维=表格,3 维=一摞表格 |
| 矩阵乘法 | 表格间的"加权汇总"运算,模型 90% 的计算量都是它 |
| 向量 | 一串数字,代表一个字的"特征指纹" |
| 权重(参数) | 模型里所有可调整的数字,训练就是调它们 |
| 梯度 | 每个参数"该往哪边改、改多少"的指示 |
| 损失(Loss) | 模型当前有多离谱的分数,越低越好 |
| 困惑度(PPL) | 损失取指数(e^loss),可读性更好的指标,越低越好 |
| epoch / batch | 把全部数据学一遍 = 1 epoch;每次同时处理的一小撮样本 = 1 batch |
| 前向传播 | 输入→输出的计算过程 |
| 反向传播 | 从输出倒推每个参数梯度的计算过程 |
五、模型核心组件详解(含完整数学公式)
Mint 采用 Decoder-only Transformer(与 GPT 同族)。数据依次流过:词表 → 词嵌入 → L 个堆叠的"解码层" → 最终归一化 → 输出投影。
5.0 记号约定(本节通用)
| 记号 | 含义 |
|---|---|
d | 模型维度 d_model(medium 为 128) |
H | 注意力头数 num_heads(medium 为 8) |
dk | 每头维度 = d / H(medium 为 16) |
d_ff | 前馈网络中间维度(= 3d,medium 为 384) |
T | 序列长度(训练时 ≤ max_seq_len = 128) |
V | 词表大小 vocab_size(Belle 约 4585) |
X | 输入矩阵,形状 [T, d],第 t 行是第 t 个字的向量 |
x_t | 第 t 个位置的 d 维行向量 |
W, b | 权重矩阵与偏置(可学习参数) |
⊙ | 逐元素相乘(哈达玛积) |
ε | 防除零小量(1e-6) |
5.1 字符级词表(Vocabulary)
与传统分词的区别:主流模型把文本切成"子词"(如"语言/模型"),Mint 直接按单个字符切分("语"、"言"、"模"、"型")。
- 每个字符分配一个整数编号,词表映射存于
char_to_id/id_to_char两张哈希表 - 特殊符号占用编号 0-5:
<pad>(0 填充)、<unk>(1 未知字)、<bos>(2 开头)、<eos>(3 结尾)、<human>(4 用户)、<assistant>(5 助手) - 普通字符从编号 6 开始;Belle 数据集约 4585 个字符
- 词表按字符出现频率过滤(出现次数 ≥2 才收录),生僻字归入
<unk> - 超出词表范围的编号(编程错误防御):查表时返回零向量,反向时跳过
优缺点:字符级实现简单、不会遇到"没见过的新词";但同样一句话需要更多步预测,效率与长程理解弱于子词分词。
5.2 词嵌入(Embedding)
公式:
e_t = E[id_t] · √d (E 是 [V, d] 的嵌入表,每行一个字的 d 维向量)
- 训练开始时随机初始化,训练中自动学会"语义相近的字向量也相近"
- 缩放系数 √d(GPT-2 同款技巧):让嵌入的数值范围与后面各层输出匹配,防止初始梯度失衡
- 查表在 GPU 上由
k_gather_dev核函数完成 - 反向传播:某位置的梯度按编号用
atomicAdd累加回嵌入表对应行(k_scatter_add_dev),同一批次中重复出现的字会被正确累加
5.3 注意力机制(Attention)—— 模型的心脏
直觉:写下一句话时,你会回头参考前文里最相关的词。注意力让模型对每个位置计算"前面每个字对我有多重要",按分数加权取信息。
5.3.1 单头注意力的完整公式链
第 1 步:QKV 投影(把输入分别变换成三个角色):
Q = X·Wq + bq (查询 Query:"我在找什么")
K = X·Wk + bk (键 Key:"我能提供什么")
V = X·Wv + bv (值 Value:"我的内容是什么")
其中 Wq/Wk/Wv 形状均为 [d, d],偏置为 d 维。对某个位置 t 的行向量:
q_t = x_t·Wq + bq
第 2 步:缩放点积打分(位置 t 与位置 j 的相关性分数):
s_tj = (q_t · k_j) / √dk
- 点积
q_t · k_j = Σ_i q_t[i]·k_j[i]:两个向量方向越一致、长度越大,分数越高 - 除以 √dk 是必须的:dk 维随机向量的点积方差为 dk,不缩放则分数动辄 ±10,softmax 后概率集中在个别位置(梯度消失);除以 √dk 把方差压回 1
第 3 步:因果遮罩(不许偷看未来):
m_tj = s_tj 当 j ≤ t(过去与当前)
-1e38 当 j > t(未来,近似负无穷)
用 -1e38 而不是真正的 -inf:f32 计算下二者行为等价(softmax 中 exp(-1e38) 下溢为 0),但避免了 -inf 参与减法产生 NaN 的陷阱。
第 4 步:数值稳定的 softmax(分数变概率):
p_tj = exp(m_tj - max_k(m_tk)) / Σ_k exp(m_tk - max_k(m_tk))
- 为什么先减最大值:exp(100) 约 2.7e43,直接算会溢出 f32(上限 3.4e38)变成 inf/inf=NaN。减去最大值后最大的指数项恰为 1,永远安全
- 全遮蔽行的处理:某行全为 -1e38(如 padding 行)时 max ≤ -1e38,直接输出全 0,不做除法
- 特殊细节:出现多个 +inf 平分秋色时,用"等于最大值则记 1"代替
exp(inf-inf)=NaN
第 5 步:加权汇总:
a_t = Σ_j p_tj · v_j (注意力输出 = 各位置的值按概率加权求和)
5.3.2 多头注意力(Multi-Head)
不只用 1 个"头",而是 H=8 个头并行:把 Q/K/V 按列切成 H 份(每份 dk=16 维),每个头独立做上述完整流程,最后拼接再投影:
head_h = Attention(X·Wq_h + bq_h, X·Wk_h + bk_h, X·Wv_h + bv_h)
输出 = Concat(head_1, …, head_H) · Wo + bo (Wo 为 [d, d])
直觉:每个头相当于一个"专家",各关注不同侧面(语法关系、指代、标点节奏……),拼接后由 Wo 融合专家意见。
5.3.3 注意力矩阵的显存代价
注意力分数矩阵形状为 [T, T]。批次训练时展平为 [batch×T, batch×T],Mint 为每个头预分配一块显存缓冲(score_bufs)——这是显存占用的大头之一,也是路线图上的优化点(见第十四章)。
5.4 RoPE 位置编码(旋转位置编码)——完整数学
问题:注意力公式对字的顺序不敏感——把"我打你"的三个字顺序任意打乱,分数计算的结构完全一样。必须注入位置信息。
5.4.1 旋转操作
把 dk 维向量两两配对(第 i 维与第 i+dk/2 维为一对),对每一对做二维平面旋转:
[ x' ] [ cosθ_t,i -sinθ_t,i ] [ x ]
[ ] = [ ] [ ]
[ y' ] [ sinθ_t,i cosθ_t,i ] [ y ]
即:
x' = x·cosθ_t,i − y·sinθ_t,i
y' = x·sinθ_t,i + y·cosθ_t,i
(这正是代码 apply_rope 的实现,θ 随位置 t 变化。)
5.4.2 旋转角度(频率设计)
位置 t、维度对 i 的旋转角:
θ_t,i = t · 10000^(−2i/dk) (i = 0, 1, …, dk/2−1)
设计意图——模拟傅里叶分析的频率阶梯:
- i=0 的维度对:频率高(10000⁰=1,每个位置转角变化大)→ 擅长分辨相邻位置
- i=dk/2−1 的维度对:频率低(约 1/10000,转角几乎不变)→ 擅长表达远距离位置关系
- 不同频率混合在一起,模型能同时感知"近距离细节"和"远距离结构"
5.4.3 核心性质:只依赖相对位置(含推导)
两个位置 t、j 的旋转后向量做点积:
q_t^R · k_j^R = Σ_i [ q_t[i]·k_j[i] + q_t[i+dk/2]·k_j[i+dk/2] ] · cos(θ_t,i − θ_j,i)
+ Σ_i [ q_t[i]·k_j[i+dk/2] − q_t[i+dk/2]·k_j[i] ] · sin(θ_t,i − θ_j,i)
关键观察:结果只通过 (θ_t,i − θ_j,i) 依赖位置,而 θ_t,i − θ_j,i = (t−j)·10000^(−2i/dk) 只依赖相对距离 t−j。这带来两个好处:
- 外推友好:训练时只见过 128 个位置,推理时位置 200 的向量照样有定义(不存在"位置表越界")
- 数学稳定:旋转矩阵是正交矩阵,不改变向量长度,不引入数值漂移
5.4.4 工程实现细节
- Q 和 K 各做旋转,V 不旋转(值的内容与顺序无关)
- 反向传播:旋转的梯度 = 用负角度再做一次旋转(旋转矩阵的逆 = 转置)
- 批次训练中每个样本独立编号(位置从 0 重新计),用逐序列旋转核函数
- 频率值
10000^(−2i/dk)在 GPU 端预计算缓存(dk 不变则复用,不重复算幂) - 聊天推理中位置计数器随对话持续增长;窗口裁剪后计数器不重置——数学上等价于重新编码截断窗口(见 10.4)
5.5 RMSNorm(均方根归一化)——前向与反向完整公式
问题:数据流过很多层后数值范围会漂移(越来越小或爆炸),训练不稳定。
5.5.1 前向
对每行向量 x(d 维)独立做:
rms² = (1/d) · Σ_j x_j² + ε (均方能量 + 防除零)
y_j = x_j · g_j / rms (统一能量后逐维缩放)
- ε = 1e-6
g(gain)是可学习参数(初始全 1),让模型自己决定每个维度的尺度- 相比经典 LayerNorm 省去"减均值"一步:更快、效果相当(Llama 同款选择)
5.5.2 反向(代码核实过的精确公式)
已知上游梯度 dy,求输入梯度 dx 与 gain 梯度 dg(以下每行独立计算):
dot = Σ_j g_j · dy_j · x_j
dx_j = g_j·dy_j / rms − x_j·dot / (d·rms³)
dg_j += dy_j · (x_j / rms) (跨行累加)
解读:第一项是"缩放直接传回";第二项是"所有维度共享的 rms 分母带来的间接影响"——某个维度变化会改变整行的 rms,进而影响整行输出,所以每个维度的梯度都含一项全局修正。这也是归一化层"牵一发动全身"的数学本质。
5.6 SwiGLU 前馈网络(第二大脑)——完整公式
每层在注意力之后还有一个前馈网络,做非线性加工(引入非线性是神经网络有表达力的关键)。Mint 用 SwiGLU(Llama 同款):
gate = X·W_gate + b_gate (门控信号,形状 [T, d_ff])
up = X·W_up + b_up (内容信号,形状 [T, d_ff])
h = swish(gate) ⊙ up (门控过滤:逐元素相乘)
out = h·W_down + b_down (投影回 [T, d])
其中 swish(SiLU)激活函数:
swish(x) = x / (1 + e^(−x)) = x · σ(x)
数值稳定的实现(代码中 ±20 截断,与数学定义在 f32 精度下等价):
swish(x) = x 当 x > 20 (e^(−20)≈2e-9,σ(x)≈1,x·1≈x)
= 0 当 x < −20 (e^(20)≈4.8e8,x·σ(x)→0⁻)
= x/(1+e^(−x)) 其余
不截断会怎样:x=−100 时 e^(100)=2.7e43 溢出 f32 → inf,−100/inf 得 −0 尚可;但 x=+100 时分子分母同时爆炸,可能产生 inf/inf=NaN。
反向传播的导数:
swish'(x) = σ(x) · (1 + x·(1 − σ(x)))
5.7 残差连接与 Pre-Norm——含梯度流公式
前向(每层内部,两个子层各带一次残差):
x₁ = x + Attention(RMSNorm₁(x)) (注意力子层)
x₂ = x₁ + FFN(RMSNorm₂(x₁)) (前馈子层)
梯度流的黄金法则(本项目最深刻的数学教训之一):
∂L/∂x₁ = ∂L/∂x₂(跳过 FFN 的直通梯度) + ∂L/∂x₂ 经 FFN 回传的梯度
这条"完整的 x₁ 梯度同时流入两条路径"——既流入注意力子层的权重(因为 Attention 是 x 的函数),也流入上一层(经 x 的直通路径)。不能把直通梯度与 FFN 梯度拆开分别对待;历史上曾因在注意力反向中漏掉直通梯度导致模型质量差(PPL ~150),修复后正常(见第十六章案例 2)。
为什么需要残差:深度网络的反向传播是连乘链,梯度会指数级衰减(梯度消失)。残差的直通路径让梯度加性回传,训练几十上百层也不衰减。Pre-Norm(先归一化再进子层)进一步把归一化放进残差支路,比 Post-Norm 更稳定。
5.8 输出层与损失(logits → 概率 → 交叉熵)
第 1 步:输出投影(最后一层的输出经最终 RMSNorm 后):
logits = X_final · W_out + b_out ([T, V]:每个位置对词表每个字一个"原始分数")
其中 W_out 形状 [d, V],b_out 为 V 维。
第 2 步:softmax 转概率(同 5.3.1 第 4 步的数值稳定版本):
P_t(w) = exp(logits[t,w] − max) / Σ_w' exp(logits[t,w'] − max)
第 3 步:交叉熵损失(只统计要预测的位置,由 loss_mask 决定):
L = − (1/N) · Σ_t mask_t · log( P_t(正确答案_t) )
N = Σ_t mask_t(有效位置数)
- 预测概率 1.0 → 贡献 0(完美);概率 0.1 → 2.3;概率 0.01 → 4.6
- 对话训练中 mask 只覆盖"助手回复"部分——模型学"如何回答",不学"如何复述问题"
- GPU 端由融合核函数一次完成 softmax+损失+梯度
交叉熵的梯度(对 logits 求导,反向传播的起点):
∂L/∂logits[t,w] = (P_t(w) − 1[正确答案是 w]) · mask_t / N
含义:把概率分布整体向"正确答案=1、其余=0"的方向推。
5.9 完整前向传播公式链(汇总,共 15 式)
输入:字符编号序列 id_1 … id_T
(1) e_t = E[id_t] · √d 嵌入(5.2)
(2) X⁰ = [e_1; …; e_T] 形状 [T, d]
对每一层 l = 1…L:
(3) X̂ = RMSNorm_l,1(X^(l−1)) Pre-Norm
(4) Q,K,V = X̂Wq+bq, X̂Wk+bk, X̂Wv+bv QKV 投影(5.3.1)
(5) Q,K = RoPE(Q), RoPE(K) 位置旋转(5.4)
(6) S = QKᵀ / √dk + 因果遮罩(−1e38) 打分(5.3.1)
(7) P = softmax(S) 概率(5.3.1)
(8) A = P·V 汇总(5.3.1)
(9) SAm = A·Wo + bo 多头合并(5.3.2)
(10) X̃ = X^(l−1) + SAm 残差(5.7)
(11) X̃̂ = RMSNorm_l,2(X̃) Pre-Norm
(12) F = swish(X̃̂W_gate+b_gate) ⊙ (X̃̂W_up+b_up) SwiGLU 门控(5.6)
(13) FFO = F·W_down + b_down FFN 输出
(14) X^(l) = X̃ + FFO 残差(5.7)
输出:
(15) logits = RMSNorm_final(X^(L)) · W_out + b_out 形状 [T, V](5.8)
5.10 完整数据流图(medium 模型,输入长度 128)
输入:128 个字符编号
↓ 查嵌入表 ×√128(式 1-2)
[128, 128] 向量矩阵
↓ 第 1 层:RMSNorm → QKV → RoPE → 打分+遮罩 → softmax → 加权汇总 → 残差
↓ → RMSNorm → SwiGLU → 残差(式 3-14)
↓ 第 2、3、4 层:同上
↓ 最终 RMSNorm → 输出投影(式 15)
[128, 4585] logits 分数表
↓ softmax + 交叉熵(5.8)
损失值 + 每个字的概率分布
六、模型规格与参数量
6.1 四档规格
| 型号 | d_model | 层数 | 头数 | d_ff | 最大序列 | 菜单显示参数量 |
|---|---|---|---|---|---|---|
| small | 96 | 3 | 6 | 288 | 128 | ~1.2M |
| medium | 128 | 4 | 8 | 384 | 128 | ~1.9M |
| large | 192 | 6 | 12 | 576 | 192 | ~4.5M |
| xlarge | 256 | 8 | 16 | 768 | 256 | ~9.0M |
d_ff 固定为 d_model 的 3 倍;每头维度 dk = d_model / 头数(medium 为 16);注意力缩放系数 = 1/√dk。 上列为界面按约 4000 字词表给出的实算值(旧版本曾粗略显示 ~1M / ~3M / ~8M / ~18M,已修正);精确推导见 6.2,真实数值由数据集词表决定。
6.2 参数量手算示例(medium,词表 4585)
| 组件 | 算式 | 数量 |
|---|---|---|
| 词嵌入 | 4585 × 128 | 586,880 |
| 每层注意力权重 | 4 × 128×128 | 65,536 |
| 每层注意力偏置 | 4 × 128 | 512 |
| 每层 FFN 权重 | 3 × 128×384 | 147,456 |
| 每层 FFN 偏置 | 384+384+128 | 896 |
| 每层归一化 | 2 × 128 | 256 |
| 每层合计 | 214,656 | |
| 4 层合计 | 214,656 × 4 | 858,624 |
| 输出投影 | 128 × 4585 | 586,880 |
| 输出偏置 + 最终归一化 | 4585 + 128 | 4,713 |
| 总计 | 约 2,037,000(约 200 万) |
可见词表大小直接决定参数量大头(词嵌入 + 输出投影各占 vocab×128)。界面显示的 ~1.9M 与上式一致(旧版本显示 ~3M,属粗略估算,已修正)。
参数量通式(任意规格代入即得):
params = 2·V·d + d·V + V (词嵌入 + 输出投影 + 输出偏置 + 最终归一化 ≈ 2·V·d)
+ L · [ 4d² + 4d (注意力:4 个 [d,d] 权重 + 4 个 d 维偏置)
+ 3·d·d_ff + d_ff + d_ff + d (FFN:3 个权重 + 3 个偏置,d_ff=3d)
+ 2d ] (两个 RMSNorm gain)
代入 medium(V=4585, d=128, d_ff=384, L=4):2·4585·128 + 4·[4·128² + 4·128 + 3·128·384 + 384 + 384 + 128 + 256] ≈ 2,037,000 ✓
6.3 权重初始化
- 所有权重矩阵:
xavier初始化——标准差sqrt(2/(输入维度+输出维度))的正态分布(名字叫 xavier,公式实为 He 初始化,对 ReLU 族激活更友好) - 偏置:全零
- 归一化 gain:全 1
七、数据流水线
7.1 支持的数据集与格式
| 数据集 | 路径 | 格式 | 用途 | 规模 |
|---|---|---|---|---|
| Belle 1M | data/BelleGroup--train_1M_CN/ | JSON 数组 | 指令对话微调 | 457 MB |
| Belle 0.5M | data/BelleGroup--train_0.5M_CN/ | JSON 数组 | 同上(备选) | 286 MB |
| zhwiki | data/zhwiki/ | XML(15 GB)+ JSONL 缓存(8.8 GB) | 语言模型预训练 | 全量维基 |
| LCCC-base | data/LCCC-base-split/ | JSON 对话数组 | 闲聊 | 932 MB |
| LCCD | data/LCCC-large/ | JSON 对话数组 | 闲聊(大) | 1.5 GB |
| AFQMC/OCNLI/TNEWS/CMRC2018 | data/<task>/ | CLUE 系列 JSON | 分类/阅读理解 | 数 MB~几十 MB |
7.2 数据发现机制(find_data)
按任务名智能定位数据,路径回退链(以 belle 为例):
BelleGroup--train_1M_CN/Belle_open_source_1M.json ← 优先
↓ 不存在
BelleGroup--train_0.5M_CN/Belle_open_source_0.5M.json
↓ 不存在
belle/train.json
zhwiki 任务有专属流程;cmrc2018 有专属流程;LCCC 按文件名模式匹配对话语料;最后兜底扫描同名目录/兄弟目录/任意 JSON 文件。找不到数据时给出明确的放置路径提示。
7.3 大文件流式处理(避免内存爆炸)
维基百科 XML(15 GB)不可能整体读入内存,处理分两步:
- 首次:流式解析 XML,逐段提取正文,清洗(去链接残留、去 HTML 实体、按段落切分),边解析边写成 JSONL 缓存(每行一条样本)
- 之后:直接读缓存。本次优化后使用
StreamingDataset::take(n)单文件句柄顺序读取前 10 万条(旧实现每条样本开一次文件 + 定位,10 万次开销降到 1 次)
LCCC 对话语料同样流式解析:逐个解析对话数组元素,相邻两轮构成一条"上句→下句"训练样本,达到上限即停。
7.4 问答数据解析(CMRC2018 / SQuAD)
本次会话修复了标准嵌套格式的解析——真实文件结构是:
{ "data": [ { "paragraphs": [
{ "context": "文章片段…", "qas": [
{ "question": "问题?", "answers": ["答案"] } ] } ] } ] }
解析器现在支持三层嵌套(data → paragraphs → qas)以及旧的扁平格式(context_text + qas[].query_text),两种都归一到"上下文+问题"作为输入、"答案"作为输出的样本结构。
7.5 词表构建(build_vocab)
- 统计训练集中每个字符出现次数
- 过滤低频字符(<2 次)
- 按字符排序(保证可复现),从编号 6 开始依次分配
- 特殊符号固定占 0-5
7.6 批次打包(batch 构建)
Chat 批次(对话任务):
原始样本:{ 指令: "请翻译:你好", 输出: "Hello" }
打包为: [<bos> <human>请翻译:你好<assistant> Hello <eos> <pad>…]
loss_mask: [0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 …]
- loss_mask 是关键设计:只对"模型该学会生成的部分"(输出+
<eos>)计算损失,输入部分不计——这样模型学的是"如何回答",而不是"如何复述问题" - 不足 128 的补
<pad>且 mask 为 0;超出 128 的截断 - LM 批次(维基等生成任务):全文都计入损失,无 mask 区分
批次结构 ChatBatch { input_ids: Vec<Vec<usize>>, loss_mask: Vec<Vec<f32>> }——一个批次含 batch_size 条定长序列。
7.7 训练/验证切分
- Belle:95% 训练 / 5% 验证,训练集上限 5 万条
- 其他:90%/10% 或 95%/5% 不等
- 采用
split_off(切走尾段)而非交叉引用,从机制上杜绝训练/验证数据泄漏(曾有 3 处回退路径的泄漏 bug,已修复)
八、训练系统详解
8.1 训练循环全景(main.rs 的 train_chat)
加载数据 → 建词表 → 建/载模型 → 打包批次
→ 建 GPU 训练器(权重、梯度、Adam 状态全部上传显存)
→ 每 epoch:
每 batch:
1. 清零梯度
2. GPU 前向:嵌入查表 → 逐层注意力+FFN → 输出投影
3. 交叉熵损失(只统计 loss_mask 内的 token,取平均)
4. GPU 反向:从损失倒推每层梯度(与 CPU 手推公式逐项对拍过)
5. 损失非有限值?→ 立即中止(防止坏权重入库)
6. 梯度缩放 1/token数 → 全局梯度裁剪(上限 50)→ Adam 更新
7. 权重同步:GPU 训练器 → GPU 权重缓存(显存间拷贝,零 CPU 往返)
8. 支持训练中按 S 键手动存档
每 epoch 结束:验证集评估 → 更好则保存检查点 → 刷新图表
→ 生成 HTML 训练报告
8.2 前向传播(forward_backward_chat_batch_gpu)
批次数据展平成一个大矩阵,整批一次算完(batch 维度并行):
[total, d] = batch_size × seq_len 行
1. 嵌入查表(GPU gather 核函数,带缩放)
2. 每层:
a. RMSNorm → Q/K/V 投影(矩阵乘法)
b. 逐头切分 → RoPE(按序列内位置,各样本独立编号)
c. 分块因果掩码(批内样本互不可见)
d. Q·Kᵀ → softmax → ·V → 合并多头 → 输出投影 → 残差
e. RMSNorm → SwiGLU FFN → 残差
3. 最终 RMSNorm → 输出投影 → 交叉熵
分块因果掩码:批次按 batch_size × seq_len 展平后,普通因果掩码会让第 2 个样本"偷看"第 1 个样本。GPU 核函数 k_block_causal_mask_dev 按"样本块"切分:块间全部屏蔽、块内因果屏蔽。
8.3 损失函数:交叉熵(Cross-Entropy)
对每个要预测的位置:
loss = -log(模型给正确答案的概率)
- 预测概率 1.0 → loss 0(完美);概率 0.1 → loss 2.3;概率 0.01 → loss 4.6
- 总损失 = 所有 mask 位置的平均值
- GPU 端融合核函数一次完成 softmax + 损失 + 梯度输出(省两次显存读写)
- 数值稳定:先减每行最大值再算指数,防溢出
8.4 反向传播(Backpropagation)
原理:链式法则。从损失出发,逐层倒推"每个参数微调一点点,损失会变多少"。
Mint 的训练路径反向传播全部在 GPU 上完成,手写实现了每步的梯度公式:
| 层 | 反向步骤(简化) |
|---|---|
| 输出投影 | d_logits × final_normedᵀ → 权重梯度;d_logits × Wᵀ → 回传 |
| 最终 RMSNorm | 按链式法则拆出 gain 梯度与回传梯度 |
| SwiGLU FFN | 门控导数(swish 及其导数)、两个权重梯度、偏置=列和 |
| 注意力 | softmax 反向 → d_scores → d_Q/d_K(再逆 RoPE)→ d_V → 各权重梯度 |
| RMSNorm1 | gain 梯度 + 回传 |
| 词嵌入 | scatter-add:把每个 token 位置的梯度按编号累加回嵌入行 |
正确性保障:测试套件里用两种方法验证 GPU 反向传播——
- CPU 对拍:同一批数据用 CPU 手推公式算梯度,与 GPU 结果逐元素比较
- 有限差分:给某个权重加 ±ε,看损失变化量是否与梯度一致(误差 < 0.5%)
8.5 Adam 优化器(GPU 常驻版)——完整数学
直觉:下山找谷底。梯度是"坡度"。Adam 对每个参数 w 维护两个历史状态,公式如下(t 为步数计数器,g 为当前梯度):
(1) m_t = β1·m_{t−1} + (1−β1)·g 动量:梯度的一阶矩(指数滑动平均)
(2) v_t = β2·v_{t−1} + (1−β2)·g² 二阶矩:梯度平方的滑动平均
(3) m̂ = m_t / (1 − β1^t) 偏置校正:m₀、v₀ 初始为 0,
(4) v̂ = v_t / (1 − β2^t) 前几步的平均值被系统性低估,须除校正系数
(5) w ← w − lr · m̂ / (√v̂ + ε) 参数更新
本项目参数:β1 = 0.9,β2 = 0.999,ε = 1e-8。
为什么需要两个历史量:
- m(动量/惯性):连续同方向的梯度相互叠加,像滚下山的球越滚越快;来回震荡的梯度相互抵消
- v(自适应步长):坡度变化剧烈(g² 大)的参数自动走小步;平坦方向的参数走大步——相当于给每个参数配了专属步长调节器
- 偏置校正的数学:m₁ = 0.1·g₁(比真实梯度小 10 倍)、v₁ = 0.001·g₁²(小 1000 倍)。若直接用,前几步的更新量会异常微小甚至除零方向错误。除以 (1−β^t) 后,m̂₁ = g₁、v̂₁ = g₁²,第一即无偏
GPU 实现的三个数学细节(k_adam_update_dev 核函数,均有据可查):
- f32 存储 + f64 累积:g² 在 f32 下 |g|≈1e20 时溢出为 inf → v=inf → √v̂=inf → 更新量恒 0,该参数被永久冻结。中间计算改用 double(最大 1.8e308)从根本上避免
- 偏置校正提到核函数外:
1−β^t每个参数每步都相同,由主机端每步算一次传入(bc1/bc2),核函数内不再逐线程算幂 - 提交前校验:更新量是 NaN/Inf/绝对值>1e10 时,丢弃本次更新并清零该梯度,m/v 状态保持原值不被污染——坏梯度只影响一步,不毒化后续所有步
8.6 学习率调度(Warmup + Cosine)——精确公式
lr(step) = base_lr · step / warmup_steps 当 step < warmup_steps
= min_lr + (base_lr − min_lr) · (1 + cos(π·progress)) / 2 否则
其中 progress = (step − warmup_steps) / (total_steps − warmup_steps)
min_lr = base_lr × 0.1
lr
│ ╭╮ (warmup 直线上升)
│ ╱ ╲╮(余弦曲线平滑下降)
│╱ ╲____________
└──────────────────→ 步数
- Warmup 长度:从零训练取总步数的 1/10;微调取 1/50(至少 10 步)——随机初始化的权重前期梯度又大又乱,直接大学习率会把它们"踢飞"
- 余弦衰减的数学:progress 从 0 走到 1,cos(π·progress) 从 1 平滑走到 −1,学习率从 base 平滑降到 min——两头变化慢(gentle 起步与着陆)、中间变化快,比线性下降更利于收敛到低点
- 微调默认学习率 0.0002(训练为 0.001),且 warmup 更短——已训练好的权重只做小步调整
8.7 GPU 常驻权重与三级缓存架构
训练速度的关键设计——权重在整个训练过程中不下显卡:
CPU 模型(Transformer) ← 只有存档/评估时才同步下来
↑ 上传一次(训练开始)
GPU 权重缓存(GpuWeightCache) ← 每层权重的显存副本,供前向/反向使用
↑ 显存间拷贝(每步一次,无 CPU 往返)
GPU 训练器(GpuTrainer) ← Adam 状态(m/v)+ 梯度 + 主权重副本
旧设计每步都要把全部权重下载到 CPU 再上传(medium 约 24 MB 双向),现在只需一次设备间拷贝。
参数布局协议(两套系统并存,勿混):
GPU 训练器内部(20 槽/层,含填充对齐):
每层:0:wq 1:wk 2:wv 3:wo 4:bq 5:bk 6:bv 7:bo 8:w_gate 9:w_up 10:w_down
11:b_gate 12:b_up 13:b_down 14:rms1 15:rms2 [16-19:填充]
全局:+0:embed +1:output_proj +2:output_bias +3:final_rms
检查点 JSON(紧凑顺序,无填充):
embed → 每层[wq..norm2 共16个] → final_norm → output_proj → output_bias
两套顺序各自内部一致,但不能互相假设。
8.8 工作区预分配(GpuWorkspace)
训练前一次性申请所有中间缓冲(激活、梯度、注意力分数等几十个缓冲区),训练中零显存申请。两个防御性设计:
- i32 溢出断言:所有尺寸经核函数 i32 参数传递,构造时验证总元素数不超 i32::MAX
- 批次超限检查:
total > ws.max_seq时报错并提示调小 batch_size/序列长度,而不是越界写坏显存(历史上有过因缓冲区尺寸算错导致的"非法内存访问"血案,已修复并加了测试)
8.9 梯度裁剪(clip_grads)
- 在 GPU 上算全局梯度范数(平方和归约核函数,逐缓冲累加)
- 范数 > 50 时,全体梯度等比缩放到 50
- 防"梯度爆炸":某一步数据异常导致权重被巨大更新直接打废
8.10 训练中的交互与可视化
- 按 S 手动存档:训练循环在 Windows 上开启控制台 raw 模式,后台线程监听按键,按 S 暂停训练、输入文件名存档、再恢复训练(本次会话修复了此路径的 panic 与控制台恢复问题)
- 终端实时输出:进度条 + 当前损失 + 学习率 + 每 epoch 损失迷你曲线
- HTML 训练报告:训练结束自动生成(损失曲线、困惑度、学习率曲线、模型信息表),并尝试用默认浏览器打开
- NaN 熔断:损失一旦非有限值立即停止本轮并保留此前最佳检查点
九、GPU 加速详解
9.1 为什么 GPU 快
- CPU:几个"大力士"核心,擅长复杂逻辑,但一次只能做几件事
- GPU:几千个"小学生"核心,单个很弱,但矩阵乘法这类"海量独立简单计算"能同时做几千份
模型 90% 的计算是矩阵乘法,天然适合 GPU。
9.2 三层封装架构
┌─ training.rs(Rust 训练逻辑)──────────────────────┐
│ GpuTrainer / GpuWorkspace / GpuWeightCache │
└────────────────────┬───────────────────────────────┘
│ 调用安全封装
┌─ gpu/ops.rs(安全层)──────────────────────────────┐
│ GpuBuffer:显存智能指针(自动释放、带长度) │
│ gpu_matmul / gpu_softmax / gpu_adam_update … │
│ 职责:尺寸断言、unsafe 隔离、Send/Sync 策略 │
└────────────────────┬───────────────────────────────┘
│ extern "C" 声明
┌─ gpu/cuda_ffi.rs(FFI 层)─────────────────────────┐
│ Rust 与 C/CUDA 的"翻译官" │
│ 无 CUDA 编译时提供 no-op 桩函数(链接不失败) │
└────────────────────┬───────────────────────────────┘
│ 实际调用
┌─ gpu/kernels.cu(CUDA 层,nvcc 编译)──────────────┐
│ 26 个核函数 + 全局流 + 显存分配/拷贝 │
└────────────────────────────────────────────────────┘
9.3 手写核函数清单
| 类别 | 核函数 | 作用 |
|---|---|---|
| 矩阵乘法 | k_matmul / _t1 / _t2 | 16×16 分块矩阵乘法(三种转置组合) |
| 基础运算 | k_add / k_add_bias / k_scale / k_zero / k_d2d_copy / k_transpose | 加减、缩放、清零、拷贝、转置 |
| 注意力 | k_rope / k_rope_inv / k_softmax / k_softmax_backward / k_extract_heads / k_write_heads / k_block_causal_mask | RoPE 旋转/逆旋转、softmax 及其梯度、逐头切分/写回、分块因果掩码 |
| 归一化 | k_rmsnorm / k_rmsnorm_backward | RMSNorm 前向/反向(反向含 gain 梯度) |
| 激活 | k_swiglu_forward / k_swiglu_backward_full | SwiGLU 门控前向/反向 |
| 损失 | k_cross_entropy_forward | 融合 softmax+损失+梯度 |
| 优化器 | k_adam_update | Adam 更新(含 NaN 防护、双精度累积) |
| 嵌入 | k_gather / k_scatter_add | 查表 / 梯度按编号原子累加回嵌入 |
| 归约 | k_sum_reduce / k_sum_sq_reduce | 求和 / 平方和(梯度范数用) |
| 偏置梯度 | k_accum_bias_grad | 列和(偏置梯度) |
9.4 手写矩阵乘法:16×16 分块算法
矩阵乘法算力密度极高,写不好会慢几十倍。Mint 的实现(以 C = A×B 为例):
- 分块:把输出矩阵切成 16×16 小块,每块由一个 256 线程的线程组负责
- 共享内存:把 A、B 的对应小块先装进 GPU 片上共享内存(速度是全局显存的 ~100 倍)
- 同步:组内线程等待装填完成
- 累加:每个线程算小块内一个元素 = 一行×一列的点积
- 沿公共维度滑动,重复装填→累加,直到整行×整列算完
弃用 cuBLAS 的完整故事(三段式,历史上真实发生过):早期版本调用 NVIDIA 官方 cuBLAS 库时出现"静默算错结果",一度归咎于"RTX 3050 硬件问题"而改用早期手写核函数;后来排查发现根因是调用方参数顺序写错(mm_t2_inplace 把矩阵维度 n、p 交换传入,非方阵形状必然算错),修复后 cuBLAS 本身是可用的;但最终仍决定以手写核函数为持久方案——exe 体积从内嵌 cuBLAS DLL 的 565 MB 降到 1.65 MB(v1.0.0),且完全可控可调试。测试套件用 CPU 矩阵乘法逐元素对拍验证正确性。性能上不如深度调优的 cuBLAS,但本项目规模(d≤256、seq≤256)足够快。
9.5 核函数启动配置与数值防护细节
每个核函数的"并行形状"配置:
- 线程块:统一 256 线程/块;网格数按元素数计算
blocks = (n + 255) / 256 - 归约类核函数(softmax、RMSNorm、反向):每"行"一个线程块,共享内存做块内归约(softmax 用 512 floats、RMSNorm 用 256 floats)
- 矩阵乘法:16×16 的二维线程块 + 两块 16×16 共享内存瓦片(见 9.4)
数值防护清单(每项都有对应的历史 bug 或防患设计):
| 位置 | 防护 | 原理 | ||
|---|---|---|---|---|
| softmax / CE | 先减每行最大值 | exp 溢出 f32 上限(3.4e38)→ inf/inf=NaN | ||
| softmax | 全遮蔽阈值 −1e38;局部 max 初始化 ≤ −1e38 | 阈值判定不能是死代码 | ||
| softmax | 多个 +inf 并列时取 1 而非 exp(inf−inf) | inf−inf=NaN | ||
| swish / swiglu | 输入 ±20 截断 | x→±∞ 时 x/(1+e^(−x)) 产生 inf/inf=NaN | ||
| Adam | 梯度 | g | >1e10 视为坏值清零 | 防权重被打废 |
| Adam | 更新量提交前校验 | 坏更新不污染动量状态 | ||
| 嵌入查表 | id 越界返回 0 / 跳过 | 防御词表不一致 | ||
| 因果掩码 | 用 −1e38 而非 −inf | 避免 −inf 参与减法产生 NaN | ||
| RoPE | 频率表设备端缓存 | 免每线程每步算幂 | ||
| 交叉熵 | 目标越界检查 t<0 或 t≥cols | PAD(0) 是合法目标,靠 mask 排除 |
9.6 显存管理(GpuBuffer)
pub struct GpuBuffer {
pub ptr: *mut c_float, // 显存指针
pub len: usize, // 元素个数
}
- 自动释放:实现 Drop,对象销毁时调用
cuda_free - 带断言上传/下载:尺寸不符立即 panic,而非静默越界
- 线程安全策略:
Send(可移动所有权,CUDA 拷贝/释放线程安全);不实现Sync(训练循环单线程,共享引用跨线程无意义且危险——本次会话移除了原有的不实声明) - 配套
GpuBufferI32存整数(token 编号)
9.7 错误处理机制(本次会话加固)
CUDA 的错误是粘性的:一个核函数出错后,后续所有操作都返回同一错误。因此:
- check_kernel(每个核函数启动后调用):用
cudaPeekAtLastError只查看不清除错误并打印。旧代码用cudaGetLastError会顺手把错误清掉,导致故障被吞、损坏的权重继续流入检查点——本次会话修复 - gpu_sync(同步点):真正检查错误;发现错误立即 panic(旧代码直接
process::exit,会跳过 Rust 析构,训练中的控制台 raw 模式无法恢复——本次会话修复为 panic,保证所有 Drop 运行) - gpu::init 幂等:OnceLock 守卫,重复初始化不再泄漏 CUDA stream
9.8 性能要点
- 融合核函数:交叉熵三步合一、softmax 反向融合——少读写显存就是最快的优化
- 设备间拷贝(gpu_d2d_copy):权重同步走显存→显存,不打 CPU 往返
- 预分配:训练中零 cudaMalloc 调用
- 同步策略:全局单流顺序执行;99 处 gpu_sync 多数守护 CPU 暂存缓冲的复用安全,不做激进删减(收益小、风险大,本次会话评估后决定保持现状)
十、推理与聊天系统
10.1 生成循环:逐字"接龙"
你:你好吗?
模型:[你] [好] [吗] [?] → 预测 → [我] → 预测 → [很] → … → 预测出 <eos> → 停止
每次预测只生成一个字,把它接在末尾再预测下一个,直到产出 <eos>(结束符)或达到长度上限。
10.2 采样策略(控制"创造性")——精确公式
拿到概率表后不是永远选概率最高的字,而是按策略抽样。Mint 依次应用三个变换:
第 1 步:温度(temperature)
p'_w = exp( log(p_w) / τ ) 归一化 (等价于把 logits 除以 τ 再 softmax)
- τ=1:原分布;τ→0:分布越来越尖,极端情况退化为"永远选最高概率"(贪心解码,死板但稳定)
- τ>1:分布被抹平,低概率项获得机会(天马行空)
- 数学上 τ 控制的是分布的熵:τ 越小熵越低、越确定
第 2 步:top-k 截断
只保留概率前 k 名的字,其余概率清零后重新归一化
防止"万里挑一"的怪字被选中。
第 3 步:top-p(核采样)
按概率从高到低累加,保留"累计概率首次 ≥ p"的最小候选集合,其余清零归一化
相比 top-k 的固定名额,top-p 的候选数动态适应分布:分布集中时留少数、分布分散时留多数。
Mint 聊天实测默认值:temperature=0.7, top_k=50, top_p=0.9——中等创造性、稳健可控的组合。
健壮性(本次会话加固):概率全为 NaN 或输入为空时不再崩溃——NaN 视为负无穷参与排序,空表返回 0;随机数分母为 0 时退回最高概率项;全 NaN 时 argmax 也能给出确定下标。
10.3 KV 缓存:聊得越长越快的秘密
问题:生成第 100 个字时,注意力要回头看前 99 个字。如果每个字都从头算一遍历史,总计算量是 O(n²)。
KV 缓存的做法:把每层算过的 K(键)、V(值)存起来,新字只需要:
- 计算自己的 Q/K/V
- 把新 K/V 追加进缓存
- 用新 Q 与全部历史 K 算注意力
本次会话的优化:追加操作从"整段复制"改为原地追加(push_row,容量按需增长),每步的内存拷贝从 O(历史长度) 降到 O(1),总拷贝量从 O(n²) 降到 O(n)。
KV 缓存的注意力公式(与训练时全序列注意力完全等价):
s_tj = (q_t^R · k_j^R) / √dk j = 0…t(缓存里的键都 ≤ 当前位,天然因果)
p_t = softmax(s_t)
a_t = Σ_j p_tj · v_j
等价性由测试保证:逐 token 缓存推理与全序列一次推理的 logits 误差 < 1e-4(test_cached_forward_matches_full)。
10.4 上下文窗口管理
- 缓存超限时
trim_front裁掉最旧的记录(滑动窗口) - 绝对位置计数器不重置:RoPE 只依赖相对距离,被裁后剩余键的"指纹"仍与位置计数器一致,数学上等价于"重新编码截断后的窗口"(由
test_trim_front_preserves_data逐字节验证) - 多轮对话上下文(代码核实):保留最近 6 轮历史;输入超长时按
keep = max_seq − min(max_seq/2, 50)截断、为回复预留空间,截断保持<human>轮次完整不切半
10.5 聊天路径的技术特点
- 聊天推理运行在 CPU(模型小、单 token 计算量低,CPU 足够;训练才必须 GPU)
- 输入格式与训练严格一致:
<bos><human>用户输入<assistant>,模型从<assistant>之后开始生成 - 支持
clear清空对话历史、quit退出
十一、检查点与序列化
11.1 文件格式
检查点是单个 JSON 文件(如 chat_belle_medium_model.json):
{
"config": {
"vocab_size": 4585, "d_model": 128, "num_heads": 8,
"num_layers": 4, "d_ff": 384, "max_seq_len": 128,
"char_to_id": {"你": 123, …}, "id_to_char": {…}, "special_id_to_token": {…}
},
"meta": { "epoch": 3, "loss": 2.35, "saved_at": "…", "extra": "…" },
"params": [ [0.123, 0.456, …], … ] // 按固定顺序排列的全部权重
}
- 词表随存档:
char_to_id映射一起保存。加载时必须用它恢复词表,否则重建的词表编号不同、token 越界(本次会话提取了统一入口checkpoint::vocab_from_config) - 参数顺序即协议:权重按固定顺序(每层 16 个真实参数 + 4 个填充槽 = 20 槽/层,之后 4 个全局参数)排列,训练器与存档器必须同序——本次会话将魔数统一为命名常量(
PARAM_SLOTS_PER_LAYER等)
11.2 安全设计
- 原子写入:先写
.tmp临时文件再改名——断电/崩溃不会留下半个文件 - 自愈加载:历史版本的 NaN 会被 serde 序列化成
null,加载时自动修复为 0.0 - 参数用 Option<f32> 存储:兼容旧文件的 null 值
- 元数据消毒:损失为 NaN 时存档强制写 0
11.3 已知不足
JSON 明文存储权重体积偏大(比二进制约大 2-4 倍)、to_string_pretty 序列化时内存翻倍——路线图上的改进项是二进制格式。
十二、测试体系
12.1 测试全景(18 个,全部通过)
| 模块 | 测试 | 验证内容 |
|---|---|---|
| data | test_encode_pair_respects_max_len | 配对编码不超长、BOS 开头、EOS 结尾 |
| data | test_encode_pair_short_input | 短输入正确填充 PAD |
| data | test_data_split_no_leakage | 训练/验证切分无泄漏 |
| data | test_vocab_size_reconstruction | 词表大小重建正确(特殊符号占位细节) |
| transformer | test_cached_forward_matches_full | 逐 token 缓存推理 = 全序列一次推理(误差 < 1e-4) |
| transformer | test_cached_window_matches_full | 新窗口冷启动一致性 |
| transformer | test_trim_front_preserves_data | 窗口裁剪保留数据逐字节一致、位置计数不重置 |
| ops | test_gpu_matmul_vs_cpu | 手写矩阵乘法 × 13 组尺寸对拍 CPU(误差 < 1e-2) |
| training | test_gpu_forward_matches_cpu | GPU 前向 = CPU 前向 |
| training | test_gpu_backward_kernels_numeric | GPU 反向核函数数值正确 |
| training | test_gpu_backward_finite_diff | 有限差分验证梯度(小规模) |
| training | test_gpu_backward_finite_diff_medium_scale | 有限差分验证(生产规模 4585 词表) |
| training | test_gpu_attention_backward_vs_cpu | 注意力反向 GPU vs CPU |
| training | test_rmsnorm2_backward_gain_path | RMSNorm gain 梯度路径 |
| training | test_two_layer_dx_handoff | 两层间残差梯度交接 |
| training | test_gpu_cross_entropy_masked_rows | 被 mask 的行损失必须为 0 |
| training | test_gpu_batched_gradients_accumulate | 批处理梯度累加正确 |
| training | test_param_index_uses_20_per_layer | 参数布局常量自检 |
12.2 GPU 测试的串行化
所有 GPU 测试共享同一条全局 CUDA 流和错误上下文,cargo test 默认并行执行会让核函数互相穿插、结果错乱。因此每个 GPU 测试开头调用 gpu::begin_test() 获取一把全局互斥锁,测试全程持有——并行模式下也能稳定通过。
12.3 验证方法论总结
- 数值对拍:GPU 结果 vs CPU 手推公式,逐元素比较
- 有限差分:数学定义级验证(梯度 = 损失变化率),不依赖手推公式的正确性
- 精度门槛:小规模 1e-4 级、生产规模 5e-3 级(f32 累积误差)
十三、本次会话改进记录
2026-09 会话完成的分析与完善工作,共 6 次提交:
| 提交 | 内容 | 价值 |
|---|---|---|
| 8f4713f | 保存工作区基线;dll/ 加入 gitignore | 版本保护 |
| d417d62 | 修复明确 bug + 删除全部死代码 | 编译警告 65 → 0 |
| bc38a38 | 参数布局常量统一、词表恢复去重 | 可维护性 |
| e7692b1 | CUDA FFI 错误处理加固 | 消除静默损坏隐患 |
| 206e69d | KV 缓存原地追加、流式数据单次读取 | 性能 |
| f930f70 | 帮助文本与真实行为对齐 | 一致性 |
修复的具体 bug 清单(T2)
- CMRC2018/SQuAD 标准嵌套格式
data→paragraphs→qas解析(旧代码只认扁平格式,实际提取 0 条样本) split_dataset的 train_ratio 未钳制(>1 时切片越界 panic)split2d头数不整除时静默截断(加断言)- 交叉熵 target 越界无检查(加断言)
- 采样函数 NaN/空输入崩溃(NaN 安全排序、空表保护、分母零保护)
- 训练循环内
process::exit绕过析构 → 控制台 raw 模式永久损坏(改为错误返回 + 错误标志) - 手动存档路径
g.0.unwrap()在 raw 模式二次启用失败时 panic(改 if-let) - raw 模式重启用失败静默(加警告 + 优雅降级)
- 空训练批次导致 total_steps=0 → 学习率除零(前置检查报错)
- GPU 内存不足提示"回退 CPU 训练"但实际直接退出(改为如实报错)
- NaN 中止后仍打印"训练完成"(改为如实报告)
- CLI 未知参数静默忽略(加警告)
清理的死代码规模(T3)
- GpuTrainer 9 个废弃方法(CPU 梯度累加路径整体废弃)
- GpuWorkspace 11 个从未读取的缓冲区字段(节省显存)
- 7 个废弃 CUDA 核函数及其三层包装(Rust 封装 / FFI 声明 / 核函数本体)
- tensor.rs 的 CPU autograd 残留(grad 字段、gelu、layer_norm 等)
- data.rs 的 8 个废弃工具函数
- 13 处 GBK 编码损坏的乱码注释
安全加固细节(T5)
| 修复 | 危害(修复前) |
|---|---|
| check_kernel 改用 cudaPeekAtLastError | cudaGetLastError 清掉粘性错误 → 核函数故障被吞 → 损坏权重静默入库 |
| gpu_sync 改 panic | process::exit 跳过析构 → 训练中控制台永久损坏 |
| init 幂等 | 重复初始化泄漏 CUDA stream |
| 移除 GpuBuffer 的 unsafe Sync | 声明了不存在的线程安全保证 |
| FFI 边界尺寸断言 | 越界调用静默写坏显存 |
性能优化(T6)
- KV 缓存原地追加:聊天生成每步的内存拷贝 O(n) → O(1),总量 O(n²) → O(n)
- 流式数据单次读取:zhwiki 加载从 10 万次"开文件+定位"降为 1 次顺序读,偏移索引表不再存储
- 有意不做:gpu_sync 微调——99 处同步多数守护暂存缓冲复用,删了有数据竞争风险、收益小
十四、已知局限与路线图
14.1 当前局限
| 局限 | 说明 |
|---|---|
| 字符级建模 | 一句话需要的预测步数多,长程语义理解弱于子词(BPE)分词 |
| 模型规模 | ~2M 参数 vs GPT-3 的 1750 亿,只能短对话、无深度推理 |
| 显存硬约束 | 4GB 显存限制 batch 与模型大小;聊天已有显存推理路径,但当前规格下受核函数启动次数限制(见 15.3),默认仍走 CPU |
| 检查点体积 | JSON 格式偏大;序列化时内存翻倍 |
| 训练报告 | HTML 依赖 CDN 图表库,离线打开无图表 |
| 手写矩阵乘法 | 未做向量化/寄存器级调优,理论峰值利用率低于 cuBLAS |
14.2 建议路线图
- 检查点二进制化:体积缩小数倍、加载更快(需要版本号与向前兼容设计)
- 注意力显存优化:score_bufs 每头一个 [total×total] 缓冲,可改为分块复用
- 子词分词(BPE):词表更紧凑、相同长度信息量更大——中期最大的质量杠杆
- 报告离线化:Chart.js 本地化或改为内嵌 SVG
- 聊天推理 GPU 化:已实现(
src/infer.rs:权重常驻显存 + K/V 常驻显存 + 单 token 增量前向,logits 回传 CPU 后与 CPU 路径共用同一套采样,逐值对拍误差 < 1e-3)。但实测当前规格下显存路径反而更慢:每 token 约 1050 次核函数启动(12 头 × 13 次/层),启动开销压过计算。要真正用上 GPU 需要合并核函数(把每头的 extract/RoPE/追加/注意力合成一次启动,例:缓存改用交错头布局、加一个融合单 token 注意力核),属后续优化项。 - 量化(f16/int8):显存减半、速度翻倍的可能
- 学习率调度可配置、早停(early stopping):训练工程完善
十五、项目历史与演进
15.1 版本编年(V1 → V21)
仓库根目录保留着 V1 至 V21 共 21 个版本快照(各含当时的 mint.exe),记录了项目从 2026-09-03 到 09-09 的一周高强度迭代。演进主线:
| 阶段 | 里程碑 |
|---|---|
| V1~V10 前后 | 基础功能成型:CPU/GPU 训练、聊天、CLUE 任务 |
| V16~V17 | cuBLAS 时代(exe 内嵌两个 cuBLAS DLL,体积 565 MB) |
| V18 之后 | 弃用 cuBLAS、cudart 静态链接(exe 缩至 1.45 MB)、GPU 训练管线重写 |
| V21 | 最新快照(2026-09-09) |
15.2 GPU 优化五步曲(P0 → P5,2026-09)
一次系统性性能攻坚,按用户确认的顺序执行:
| 阶段 | 内容 | 效果 |
|---|---|---|
| P0 | 全链路 f64 → f32 迁移 | RTX 3050 的 FP64 吞吐只有 FP32 的 1/64,这是最大的单项收益 |
| P1 | 先接回 cuBLAS f32 路径并 A/B 验证 | 后被 P3 的发现取代(见下) |
| P2 | 消除反向传播中的 CPU 往返 | 每样本 30+ 次"显存↔内存"拷贝归零 |
| P3 | 修复批处理反向的 NaN + 恢复真批处理 | 发现了 9 个深层 bug(详见第十六章) |
| P4 | 权重 GPU 常驻 | 每步 24 MB 双向拷贝 → 一次设备间拷贝 |
| P5 | 验证集评估 GPU 化 | 训练全程不再离开 GPU |
15.3 实测基准数据
| 项目 | 数据 |
|---|---|
| medium 32 批次基准 | 旧版 exe 41s → f32+反向修复后 8s → 真批处理后 5s(约 8 倍提速) |
| 损失表现 | 32 批次内 8.43 → 7.5,无 NaN |
| small 小规模验证训练 | 500 样本 2 epoch 共 28s,损失 8.22→7.78,检查点 33.2 MB |
| 初始损失理论值 | 8.43 ≈ ln(4585)(随机模型每个字概率 1/4585 的交叉熵) |
| exe 体积 | 565 MB(内嵌 cuBLAS DLL)→ 1.65 MB(cudart 静态链接,v1.0.0) |
| 测试套件 | 从 87 个编译警告、14 个测试 → 15 条警告(全部为仅测试使用的函数)、38 个测试 |
| 聊天推理速度(4.5M 参数) | CPU 605 字/秒 vs 显存增量推理 49.8 字/秒 —— 显存路径每 token 约 1050 次核函数启动,属启动受限而非算力受限 |
| 训练一步拆解(large,bsz=8,seq=192,优化后) | 前向+反向 360 ms · scale+clip 11 ms · Adam+d2d 5 ms —— 合计约 376 ms(优化前为 443/923 ms,视 GPU 频率状态) |
| 逐算子同步的代价(同进程交替 A/B) | 单层注意力 12 头:逐算子同步 43.2/46.5 ms vs 批式提交 20.4/20.1 ms —— 2.01~2.21 倍。据此删除训练热路径上 109 处逐算子 gpu_sync()(同一条 stream 上本就按序执行,只有 CPU 要读回结果时才需要同步)。注意:GPU 频率在进程之间可能相差 2~3 倍(同一份代码实测 344 ms 与 924 ms),跨进程的前后对比会得出假结论,必须用同进程 A/B 或取多次最小值 |
| GEMM:朴素分块 → 寄存器分块 | 4×4 线程级分块(分块 64×64、16×16 线程、共享内存补一列防 bank 冲突),算术强度从 0.5 提到 2 FMA/共享读。同进程 A/B(5 组形状交替跑):3.63× / 3.94× / 4.10× / 4.26×,GEMM 吞吐 225 → 917 GFLOP/s;唯 K 维极小(1536×1536×16)的注意力分数形状持平(0.96×),因为只有一次 k 步可摊 |
| 注意力:补齐的"按序列分块" | batch 内的序列互不可见,注意力本应是块对角;旧实现却按 [total, total] 算完整分数矩阵再掩码 —— 87.5% 的算力与 8 倍带宽花在块外,且每个头要对 9.4MB 的分数矩阵做约 6 遍全局读写。改为按 (序列, 头) 分块(4 个新核函数:分数、融合掩码+缩放+softmax、两次上下文乘),同进程 A/B:前向 20.2 → 5.1 ms(3.99×)、反向 39.1 → 8.5 ms(4.59×);顺带把每头一份的分数缓存从 113 MB 降到 9.4 MB(反向改为按头重算 sm,两次小启动换 113 MB 显存)。分数缓存不再需要复制,也不再需要块对角掩码核函数(已删) |
| 梯度裁剪:逐缓冲回传 → 显存内累加 | 100 个梯度缓冲各自"归约 + 回传",每个参数一次 cudaStreamSynchronize —— 实测 59.9 ms/步,几乎全是同步代价。改为在显存里累加平方和、只回传一次,并把单块归约从 256 线程提到 1024 线程(8 个 warp 藏不住访存延迟):59.9 → 5.6 ms(10.7×)。顺带删掉不再使用的非累加版归约 |
| Adam:double → float | m/v 的累加原本用 double,理由是"怕 g² 溢出";但同一核函数上方那道 |g| > 1e10 的守卫已经保证 g² ≤ 1e20,远在 f32 范围内。双精度在这张卡上只有单精度 1/32 的吞吐,而 Adam 是纯带宽受限核:25.9 → 5.4 ms(4.8×),与 CPU 参考实现逐值对拍偏差 < 1e-4 |
| 训练加速总账(真实训练,2 轮 400 样本 large) | 训练循环 97 s → 39 s(2.5×),墙钟 98.34 s → 41.41 s(2.37×);损失轨迹不变(1.23 → 0.33)。逐步归因:去除逐算子同步、GEMM 寄存器分块(1.27×)、注意力按序列分块(1.8×)、梯度裁剪(10.7×)、Adam f32(4.8×)。逐算子同步的收益在完整一步里被队列重叠稀释,小于单独测量的 2.01× |
15.4 数据丢失事故(重要教训)
2026-09 会话中期,根目录 6 个训练好的 chat_*.json 检查点与备份目录无原因消失(未执行过删除命令、检查点被 gitignore 无法从 git 恢复)。教训:重要检查点必须手动异地备份,不要只依赖工作目录。目前仅存 chat_belle_medium_model.json(被基准覆盖)与 V1 目录内一份旧模型。
十六、经典 Bug 案例集(工程故事)
这些真实案例展示了"数值计算 + 高性能并行"领域的典型陷阱,每个都值得初学者细读。
案例 1:矩阵维度参数交换(最昂贵的单行 bug)
现象:GPU 前向结果与 CPU 完全对不上;但所有正方形矩阵的测试却全部通过。
根因:mm_t2_inplace(计算 A×Bᵀ)调用底层函数时把 n、p 两个维度参数交换传入。正方形时 n==p 交换无影响,测试全绿;非正方形时静默算错。
为什么难查:错误只在非正方形形状显现,且表象怪异(一度被归咎于"cuBLAS 硬件缺陷")。最终被逐层隔离测试锁定。
教训:测试必须覆盖非正方形矩阵;"硬件有问题"往往是最不可能的结论。
案例 2:残差梯度的"完整流"规则
现象:模型能训练但质量极差(PPL 约 150)。
根因:反向传播中,残差连接 x₁ = x + Attention(·) 的梯度 ∂L/∂x₁ 必须完整地流入注意力子层(因为注意力是 x 的函数),同时经直通路径流入上一层。早期实现错误地只把"FFN 支路梯度"传给注意力反向,导致每层的注意力权重都在学错误的梯度。
教训:残差是"加法",梯度不拆分、只复制——两条路径各拿全量。
案例 3:梯度检查点(重计算)的两个错位
现象:启用"激活重计算"(省显存技巧,反向时重算前向激活)后梯度错误。
根因:(1) 重计算用的输入取错了缓冲——取的是"残差之后"的值,而重计算需要"残差之前"的值;(2) FFN 回传梯度从已被下一层覆盖的旧缓冲读取。修复:为每个层单独保存 pre-norm2 检查点、用正确的 dx 缓冲。
教训:缓冲复用省显存,但必须画清每个缓冲的"生命周期",谁在读、谁在写、何时被覆盖。
案例 4:softmax 核函数的共享内存竞态
现象:GPU softmax 结果随机出错(并行竞态的典型症状——时好时坏)。
根因:核函数先用共享内存归约求最大值,随后复用同一块共享内存做求和归约,中间漏了一次 __syncthreads() 线程同步。快的线程开始写求和数据时,慢的线程还在读最大值——读到一半新一半旧的数据。
教训:GPU 共享内存的两阶段使用之间必须显式同步;竞态 bug 不报错、结果时对时错,比崩溃更可怕。
案例 5:NaN 检查点的"自愈"设计
现象:某次训练权重出现 NaN,保存的 JSON 里 NaN 被 serde 写成 null,加载时报"invalid type: null"。
根因:JSON 标准没有 NaN,serde_json 的兼容写法是 null;而 null 无法反序列化回 f32。
修复:检查点参数类型改为 Option<f32>(null 合法)——加载时 null 自动修复为 0.0 并打印修复计数;保存前把非有限值消毒为 0。某次事故中 large 模型恰好有一个 192×192 的权重矩阵整体 NaN(36864 个 null),靠此机制抢救成功。
教训:序列化格式设计要覆盖"非法状态";诊断信息(修复计数)要可见。
案例 6:梯度裁剪悄悄失效
现象:梯度爆炸时裁剪似乎没生效。
根因:全局范数应为 √Σg²,早期实现却算了 Σg(平方和归约核函数被普通求和替代)。更隐蔽的是:只要有一个梯度是 NaN,范数就是 NaN,NaN > 阈值永远为 false,裁剪直接跳过。
修复:专用平方和归约核函数;训练循环加"损失非有限即中止"的熔断(在裁剪之前拦截)。
教训:防御代码本身需要防御;NaN 会让所有比较判断失效。
案例 7:粘性错误被"检查函数"吞掉
现象:CUDA 核函数报错打印在日志里,训练却继续"成功"完成——坏权重入库。
根因:检查函数用 cudaGetLastError() 读取错误,该函数会顺手清除错误状态(粘性错误被擦掉);后续同步点看到的是一切正常。修复:改用 cudaPeekAtLastError()(只看不清除),让真正的同步点(gpu_sync)拦截并终止。
教训:错误处理代码要理解 API 的副作用;"检查"不应该是"清除"。
十七、快速上手与完整命令参考
17.1 环境要求
- Rust 工具链(cargo 1.98+)
- NVIDIA GPU + CUDA Toolkit 12.0(或设置
CUDA_PATH指向其他版本) - Visual Studio Build Tools(MSVC,build.rs 通过 vswhere 自动查找)
17.2 完整 CLI 参数参考
| 参数 | 说明 | 默认值 |
|---|---|---|
-train | 进入训练模式 | — |
-model=<file> | 加载模型;单独使用=聊天,与 -train 同用=微调 | — |
-task=<name> | 训练任务(belle/zhwiki/LCCC-base-split/afqmc/ocnli/tnews/cmrc2018…) | belle |
-size=<size> | 模型规格(small/medium/large/xlarge) | medium |
-epochs=<n> | 训练轮数 | 3 |
-lr=<rate> | 学习率 | 0.001 |
-batch=<n> | 批次大小 | 8 |
-max=<n> | 最大训练样本数(0=不限) | 20000 |
-gpu | 确认使用 GPU(默认即 GPU,无 GPU 时报错退出) | — |
-cpu | 已禁用(CPU 训练不支持,直接报错) | — |
-h / --help | 显示帮助 | — |
参数解析细节:-model= 只在未指定其他模式时默认进入聊天,因此 -train -model=X 正确地表示微调;未知参数会打印警告而非静默忽略;数字参数解析失败时回落默认值。
17.3 交互式主菜单(无参数启动)
| 选项 | 功能 |
|---|---|
| 1 | 训练新模型(选任务 → 选规格 → 调参 → 确认) |
| 2 | 加载模型聊天(列出检查点供选择) |
| 3 | 微调已有模型(学习率默认 0.0002、5 轮、上限 1 万样本) |
| 4 | 浏览可用数据集(扫描 data/ 目录) |
| 5 | 模型管理(列出检查点、查看详情) |
| 6 | GPU 状态(设备名、显存) |
| 7 | 帮助 |
| 0/q | 退出 |
17.4 常用命令
# 编译(release + CUDA 默认开启)
cargo build --release
# 交互式菜单(训练 / 聊天 / 微调 / 模型管理)
.\target\release\mint.exe
# 直接训练(Belle 数据集、medium 规格、3 轮)
.\target\release\mint.exe -train -task=belle -size=medium -epochs=3
# 加载模型聊天
.\target\release\mint.exe -model=chat_belle_medium_model.json
# 微调已有模型
.\target\release\mint.exe -train -task=belle -model=chat_belle_medium_model.json
# 运行测试(18 个,含 GPU 数值对拍)
cargo test
# 查看帮助
.\target\release\mint.exe -h
17.5 训练中的注意事项
- 训练会自动覆盖
chat_<task>_<size>_model.json与对应 HTML 报告(仅在验证损失改善时)——重要模型请先备份(第十五章的数据丢失事故是前车之鉴) - 训练中按 S 可手动存档(自定义文件名或自动命名)
- 显存不足时:换小规格(
-size=small)、减小批次(-batch=4)或减少样本(-max=5000) - 数据放置:
data/<任务名>/train.json(Belle 例外,见第七章路径表) - Belle 数据加载有 5 万样本的硬上限(设计如此)
- 日志文件(train_output.log / train_error.log)为 UTF-16 LE 编码,Windows 下需
Get-Content -Encoding Unicode读取
十八、附录:术语表
| 术语 | 解释 |
|---|---|
| Transformer | 2017 年提出的神经网络结构,注意力机制为核心,现代大语言模型的基础 |
| Decoder-only | 只用 Transformer 的解码器部分,适合"逐字生成"任务(GPT 同款) |
| 注意力 | 让每个位置按相关性加权参考其他位置信息的机制 |
| 多头注意力 | 多组注意力并行,捕捉不同层面的关系 |
| RoPE | 旋转位置编码:给向量按位置"旋转",让模型感知顺序 |
| RMSNorm | 均方根归一化:把向量能量缩放到统一水平,稳定训练 |
| SwiGLU | 带门控的前馈激活结构,Llama 等模型的主流选择 |
| 交叉熵 | 衡量"预测概率分布与真实答案差距"的损失函数 |
| 反向传播 | 链式法则逐层倒推参数梯度 |
| Adam | 带动量与自适应步长的优化器 |
| Warmup | 训练初期学习率从零线性上升,防初期震荡 |
| Cosine 衰减 | 学习率按余弦曲线平滑下降 |
| 梯度裁剪 | 梯度超限时等比缩放,防爆炸 |
| KV 缓存 | 保存历史键/值,加速逐字生成 |
| 因果掩码 | 屏蔽未来信息,只允许"看前文" |
| 困惑度 PPL | e^损失,越低模型越好 |
| 核函数 | GPU 上并行执行的小程序 |
| 共享内存 | GPU 片上高速缓存(比显存快约百倍) |
| 粘性错误 | CUDA 出错后所有后续操作返回同一错误的现象 |
| 原子操作 | 多线程同时写一个地址时保证不丢失更新的硬件指令 |
| 融合核函数 | 把多个步骤合并进一个 GPU 程序,省显存读写 |
| 激活重计算 | 反向时不保存中间激活、用时重算,以计算换显存(梯度检查点) |
| 有限差分 | 给参数加 ±ε 观察损失变化,用导数定义直接验证梯度正确性 |
| 滑动窗口 | 上下文超限时裁掉最旧记录,保持长度有界 |
| 数据泄漏 | 训练集与验证集存在重叠,导致评估分数虚高 |
| 残差连接 | 输出 = 输入 + 子层结果,梯度可直通回传不衰减 |
| 点积 | 两向量逐元素相乘求和,衡量方向一致性 |
| 哈达玛积 | 两矩阵逐元素相乘(⊙) |
| 共享内存竞态 | 并行线程读写同一内存缺同步导致的数据错乱(时对时错) |
报告完。基于 2026-09 会话完善后的代码状态;相关数字已于 v1.0.0 按实测修订(15 条仅测试用函数的警告、36/36 测试通过、release 构建验证)。