读者:想了解本库架构/性能/算法的读者(调用者视角的总览)。总索引见
../README.md;历史归档在../history/history.md。 状态:现行。 本文档详细梳理 neuralnet.cpp 项目中的所有性能优化策略,涵盖 CPU 多线程、缓存优化、GPU 加速、内存管理、算法级优化等层面。
┌─────────────────────────────────────────────────────────────┐
│ 性能优化层次 │
├─────────────────────────────────────────────────────────────┤
│ L5 算法级 │ 批量化注意力、表达式级融合(DSL,单 kernel) │
│ L4 GPU 加速 │ Vulkan Compute、批量提交、Command Buffer │
│ L3 内存级 │ 缓存分块、零分配热路径、Tensor 零拷贝 │
│ L2 并行级 │ 自适应并行、线程池 latch 零分配 │
│ L1 指令级 │ AVX2 SIMD、-march=native(不使用 -ffast-math)│
└─────────────────────────────────────────────────────────────┘
根据数据规模自动决定串行还是并行执行,避免小数据量时线程调度开销(约 50~200μs)大于计算收益。
// core_config.hpp
inline constexpr std::size_t PARALLEL_THRESHOLD = 524288; // 512K 元素| 元素数 | 串行 μs | 并行 μs | 加速比 | 建议 |
|---|---|---|---|---|
| 16384 | 9.6 | 9.8 | 0.98x | 串行 |
| 65536 | 38.7 | 121.7 | 0.32x | 串行 |
| 524288 | 308.7 | 201.5 | 1.53x | 首次稳定 > 1.5x |
| 1048576 | 620.3 | 186.7 | 3.32x | 并行 |
| 4194304 | 2657.3 | 340.6 | 7.80x | 并行 |
所有顶层并行函数自动判断:
// core_config.hpp — 顶层并行算法函数
template<typename Iterator, typename Func>
inline void for_each(Iterator first, Iterator last, Func&& func)
{
const auto n = std::distance(first, last);
if (n >= PARALLEL_THRESHOLD)
global_thread_pool().parallel_for_each(first, last, func);
else
for (auto it = first; it != last; ++it) func(*it);
}并行分块不能按"每任务一次堆分配 + 一次加锁 + 一个 future 同步"的方式提交—— N 个分块会带来 N 次堆分配、N 次互斥开销与 N 次 future 同步,在中等粒度任务上 调度成本超过计算收益。
- 一次加锁批量入队:N 个分块仅 1 次
lock_guard - 原子计数器替代 future:
std::atomic<int> latch替代 N 个 future - 调用者参与计算:调用者不空等,而是从队列取任务执行(work-stealing)
- 三阶段等待策略:
- 短自旋(16 次
pause指令)— 应对 latch 即将归零的快路径 - Work-stealing:尝试从队列取任务执行
condition_variable短超时等待(100μs),避免 CPU 空转
- 短自旋(16 次
N 分块 = 1 次加锁 + 0 次堆分配 + 1 个原子计数器
将矩阵乘法分解为 BLOCK_SIZE × BLOCK_SIZE 的小块,确保每块的工作集装入 CPU L1 缓存(32KB)。
// core_config.hpp
inline constexpr std::size_t BLOCK_SIZE = 64;
// 64 × 64 × 8 bytes = 32KB — 安全装入大多数 CPU 的 L1 缓存A (M×K) × B (K×N) = C (M×N)
for i_block in range(0, M, BLOCK_SIZE):
for j_block in range(0, N, BLOCK_SIZE):
for k_block in range(0, K, BLOCK_SIZE):
// 从 B 中预取一块到本地数组 b_block(转置布局)
// 累加 C 的一块
关键优化:
- B 块预取转置:
b_block[jj * k_len + kk]— 消除内层循环的跨行访问 - 栈分配 b_block:
std::array<Scalar, 64*64>— 零堆分配 - 分块级并行:每个
(i_block, j_block)块独立,可并行调度
不使用
-ffast-math:为保证 NaN/Inf 传播与训练数值稳定性,项目明确禁用-ffast-math。实际 Release 标志(Clang/GCC,权威来源CMakeLists.txt:77/79)为-O3 -fno-math-errno -fno-trapping-math -funroll-loops -march=native,另加工程告警集-fno-exceptions -Wall -Wextra -Wpedantic -Werror(注意-funroll-loops是启用的;MSVC 分支为/O2 /fp:precise——/fp:fast会禁用 NaN 语义,实测 base 子仓precision_type_test的 NaN 断言在/fp:fast下被编译器折叠为假失败,与本铁律冲突,故 MSVC 固定用 precise)。
# CMakeLists.txt(Clang/GCC;NN_ENABLE_NATIVE=ON 时含 -march=native)
set(RELEASE_FLAGS -O3 -fno-math-errno -fno-trapping-math -funroll-loops -march=native)
# 工程告警集另行附加:-fno-exceptions -Wall -Wextra -Wpedantic -Werror| 标志 | 作用 |
|---|---|
-O3 |
最高级别优化(内联、循环变换、向量化) |
-march=native |
启用本机 CPU 指令集(AVX2/AVX-512 等 SIMD) |
-fno-exceptions |
禁用异常(配合 Result<T> 错误处理铁律) |
-Wall -Wextra -Wpedantic -Werror |
严格告警,告警即错误 |
说明:项目刻意不用
-ffast-math(允许浮点重排序、忽略 NaN/Infinity 会破坏训练稳定性)。-funroll-loops显式启用(与-O3叠加);-fno-math-errno/-fno-trapping-math关闭数学错误域/陷阱语义以利向量化,不涉及 NaN 语义。
GPU 加速遵循引擎化架构铁律:
ComputeEngine抽象接口不变GpuEngine实现所有原语的 GPU 版本- Layer 的 forward/backward 零修改
// GPU 引擎:begin_batch 开始录制,end_batch 统一提交
engine.begin_batch(); // 开始录制
auto mm = engine.matmul(A, B); // 不立即执行
auto tr = engine.transpose(*mm); // 不立即执行
engine.end_batch(); // 一次性提交 + fence wait- CPU 引擎:
begin_batch/end_batch为 no-op(同步执行) - GPU 引擎:录制到 Command Buffer,
end_batch统一提交
训练数据从 CPU 上传一次 → 全程 GPU 计算 → 仅 evaluate 时下载
- 表达式 DSL(当前主力):
dsl::compute把整条逐元素/归约/matmul 链折叠为单个 GPU 融合 kernel(见04-innovative-designs.md§3) - 广播 / 条件选择 / axpy 类原地运算:一律写进表达式(
dsl::row_broadcast/dsl::col_broadcast/dsl::select/dsl::compute_into),随所在表达式融合成同一条 kernel,不单独 dispatch
class Tensor {
std::shared_ptr<Matrix> cpu_data_; // 引用计数
std::shared_ptr<GpuTensor> gpu_data_; // 引用计数
};- 拷贝 Tensor = 增加引用计数,不复制底层数据
- 修改需要显式 clone:
engine.clone(src)创建深拷贝 - Layer 间传递 Tensor 全部零拷贝
// 仅在需要修改但不影响原 Tensor 时使用
auto diff = clone_tensor(engine, input); // 深拷贝
// 原地修改 diff,input 不受影响(广播减法用 DSL)
(void)dsl::compute_into(engine, dsl::leaf(*diff) - dsl::col_broadcast(*mean), *diff);三步写法 clone → scale_inplace → add_inplace = 3 次 dispatch + 3 个临时 buffer。
// 一条 DSL 表达式 = 单个融合 kernel(optimizer 就是这么写的)
dsl::compute_into(engine, dsl::leaf(dst) + dsl::leaf(src) * dsl::rparam(scalar), dst);for (size_t h = 0; h < num_heads; ++h) { // N 次循环
Q_h = slice(Q, h*d_k, (h+1)*d_k);
scores = Q_h × K_h^T;
attn = softmax(scores);
O_h = attn × V_h;
}
output = concat(O_0, O_1, ..., O_{H-1});rearrange_3d → (batch*H*d_k, seq) // 头维度在行方向
batched_matmul → 单次 dispatch 处理所有样本和所有头
rearrange_3d_back → (H*d_k, batch*seq)
性能提升:将 H 次 matmul 融合为 1 次 batched_matmul;forward 进一步收敛为
eval_expr(fold) 单 dispatch——QKᵀ/掩码/softmax/ΣwV 全在 kernel 内。上述批量结构
现用于 backward 的 batched_matmul 与 recompute 路径。
当前因果掩码不生成矩阵、也没有缓存路径:单 fold kernel 在 body 内以 select 链
表达掩码(tri_skip 把被屏蔽的整块钳成空转),掩码在每次前向于寄存器/shared 分片内
即时判定。因此不存在"按 (batch, seq_len) 缓存掩码矩阵"这类状态——训练热路径中
需要跨步复用的只有位置编码(§10)。
class PositionalEncoding {
Tensor encoding_cache_;
std::size_t cached_total_ = 0;
void rebuild_encoding(engine, total_len) {
if (cached_total_ == total_len) return; // 已缓存
// ... 构造编码 ...
cached_total_ = total_len;
}
};// data_[row * cols + col] — 行方向连续
// 相邻元素在内存中连续 → SIMD 向量化友好// 分块转置:每个块内行优先读、列优先写
// 块间并行 → 利用 L1 缓存的空间局部性// expr_dsl.hpp — 表达式写在 Layer 内;CPU 编译期模板求值 / GPU AOT 融合
// 单次遍历、无临时矩阵(GPU 侧折叠成一条融合 kernel)
auto y = dsl::compute(engine, dsl::max(dsl::leaf(*x), Scalar{0}), rows, cols);
auto z = dsl::compute(engine, dsl::leaf(*a) + dsl::leaf(*b) * dsl::leaf(*c), rows, cols);收益:
- 消除临时矩阵分配(
a + b * c不创建b*c的中间矩阵) - 编译器可内联整个表达式树
| 优化手段 | 场景 | 预期收益 |
|---|---|---|
| 自适应并行 | 小数据串行、大数据并行 | 避免小数据 0.3x 退化 |
| 线程池 latch | 所有并行操作 | 消除 N 次堆分配/加锁 |
| 缓存分块 | 矩阵乘法 | L1 命中率 → 3-8x 加速 |
| -march=native | 所有计算 | AVX2/AVX-512 SIMD(不使用 -ffast-math,保数值稳定) |
| GPU 批量提交 | GPU 推理/训练 | 减少 kernel launch 开销 |
| Tensor 零拷贝 | Layer 间传递 | 消除数据拷贝 |
| 融合 axpy | 优化器更新 | 减少 600 次 buffer 分配/step |
| 注意力批量化 / fold 单遍 | MHA | H 次 → 1 次;forward 现为 fold 单 kernel 单 dispatch(S 绝不物化) |
| 位置编码缓存 | GPT 训练 | 相同 (batch,seq) 只构造一次(掩码不物化、无需缓存,见 §9) |
| 表达式模板 | 逐元素运算 | 消除临时矩阵 |
| 分块转置 | 转置操作 | L1 友好 + 并行 |
| GPU 注意力 fold 流式 | GPU mha/causal | fwd 5.41ms(mha)/ 4.75ms(causal)、train 17.5ms(40HX 交错 bench 实测) |