Skip to content

Latest commit

 

History

History
327 lines (236 loc) · 11.7 KB

02-performance.md

File metadata and controls

327 lines (236 loc) · 11.7 KB

性能优化手段

读者:想了解本库架构/性能/算法的读者(调用者视角的总览)。总索引见 ../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)│
└─────────────────────────────────────────────────────────────┘

1. 自适应并行策略

原理

根据数据规模自动决定串行还是并行执行,避免小数据量时线程调度开销(约 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);
}

2. 线程池 latch 零分配设计

设计目标

并行分块不能按"每任务一次堆分配 + 一次加锁 + 一个 future 同步"的方式提交—— N 个分块会带来 N 次堆分配、N 次互斥开销与 N 次 future 同步,在中等粒度任务上 调度成本超过计算收益。

当前实现

  • 一次加锁批量入队:N 个分块仅 1 次 lock_guard
  • 原子计数器替代 future:std::atomic<int> latch 替代 N 个 future
  • 调用者参与计算:调用者不空等,而是从队列取任务执行(work-stealing)
  • 三阶段等待策略:
    1. 短自旋(16 次 pause 指令)— 应对 latch 即将归零的快路径
    2. Work-stealing:尝试从队列取任务执行
    3. condition_variable 短超时等待(100μs),避免 CPU 空转

开销构成

N 分块 = 1 次加锁 + 0 次堆分配 + 1 个原子计数器

3. 矩阵乘法缓存分块(Tiling)

原理

将矩阵乘法分解为 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) 块独立,可并行调度

4. 编译器优化选项

Release 模式编译标志

不使用 -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 语义。


5. GPU 加速(Vulkan Compute)

架构设计

GPU 加速遵循引擎化架构铁律:

  • ComputeEngine 抽象接口不变
  • GpuEngine 实现所有原语的 GPU 版本
  • Layer 的 forward/backward 零修改

关键设计

5.1 批量命令提交

// 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 统一提交

5.2 零层间 PCIe 传输

训练数据从 CPU 上传一次 → 全程 GPU 计算 → 仅 evaluate 时下载

5.3 算子融合

  • 表达式 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

6. 张量零拷贝传递

Tensor 的 shared_ptr 语义

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);

7. 融合 axpy(dst += scalar * src)

问题

三步写法 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);

8. 多头注意力批量化

朴素写法:per-head 循环(不批量化时的形态)

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});

当前做法:单 fold kernel(forward)+ 批量 dispatch(backward)

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 路径。


9. 因果掩码:不物化、无缓存

当前因果掩码不生成矩阵、也没有缓存路径:单 fold kernel 在 body 内以 select 链 表达掩码(tri_skip 把被屏蔽的整块钳成空转),掩码在每次前向于寄存器/shared 分片内 即时判定。因此不存在"按 (batch, seq_len) 缓存掩码矩阵"这类状态——训练热路径中 需要跨步复用的只有位置编码(§10)。


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;
    }
};

11. L1 Cache 友好的数据布局

行主序存储

// data_[row * cols + col] — 行方向连续
// 相邻元素在内存中连续 → SIMD 向量化友好

转置内核

// 分块转置:每个块内行优先读、列优先写
// 块间并行 → 利用 L1 缓存的空间局部性

12. 编译期零开销抽象

表达式模板(DSL)

// 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 实测)