读者:调用者(库使用者 / 扩展者)——本分册定位见
README.md。 项目内部的设计 / 路线图 / 盘点在../internal/,历史在../history.md。 状态:现行。
面向想要理解或修改计算引擎实现的开发者。本文覆盖 ComputeEngine 接口全解、CPU / GPU 实现模式、以及添加新原语的完整方法。
graph TB
subgraph "Layer 层(算法)"
L1[Linear::forward]
L2[Attention::forward]
L3[ReLU::forward]
end
subgraph "ComputeEngine 接口"
E[ComputeEngine]
end
subgraph "引擎实现"
CPU[CpuEngine]
GPU[GpuEngine]
end
subgraph "底层硬件"
C[CPU 矩阵运算]
G[Vulkan GPU]
end
L1 & L2 & L3 --> E
E --> CPU & GPU
CPU --> C
GPU --> G
核心思想:
- 接口与实现分离:Layer 只依赖
ComputeEngine接口 - 算法与原语分离:引擎只提供基础操作,不含算法逻辑
- 一次编写,多后端运行:同一个 Layer 代码自动适配 CPU/GPU
include/neuralnet.cpp/
├── compute_engine.hpp # 抽象接口定义
├── compute_cpu_engine.hpp # CPU 实现
├── compute_gpu_engine.hpp # GPU 实现(Vulkan)
├── compute_tensor.hpp # Tensor 类定义
├── core_config.hpp # 常量配置
└── core_errors.hpp # 错误处理
[[nodiscard]] virtual Device device() const noexcept = 0;作用:返回引擎运行的设备类型(CPU/GPU)
实现要求:
- 必须是
noexcept - 返回值在引擎生命周期内不变
[[nodiscard]] virtual Result<void> begin_batch() = 0;
[[nodiscard]] virtual Result<void> end_batch() = 0;
[[nodiscard]] virtual Result<void> flush_batch() { return {}; }CPU 引擎:
- 全部为 no-op(操作立即同步执行)
GPU 引擎:
begin_batch():开始录制 command bufferend_batch():提交 command buffer + 等待完成flush_batch():中间提交(防 TDR)
使用模式:
engine.begin_batch();
// ... 执行多次操作 ...
engine.end_batch(); // 提交并等待接口分层(NVI,P-1/P1 之后):引擎的公共入口全部非虚——承载边界 cast (f16 抬 f32 算→按 P 落回,15 §4.1)、
bind_check_(设备域检查,docs/internal/28 §4.3) ;引擎实现侧是 protected 的虚*_impl。 下文各"原语"清单列出的就是引擎需要实现的*_impl签名;调用方(Layer/测试) 永远调用同名公共入口(不带_impl)。
// 公共非虚入口(包装在尾部统一 stamp 出生绑定,15 §3.1):
[[nodiscard]] Tensor create_tensor(std::size_t rows, std::size_t cols,
Precision P = Precision::F32);
// M2(17 §4.4):声明式创建/初始化——层算分布参数、引擎填数
//(基类 host 生成 + from_matrix 上传;seed 与引擎内创建序号混流):
[[nodiscard]] Tensor create_tensor(std::size_t rows, std::size_t cols, Precision P,
const InitSpec& spec);
[[nodiscard]] Result<Tensor> from_matrix(const Matrix& m, Precision P = Precision::F32);
// to_matrix 输出宿主 Matrix,仍为纯虚(不参与 stamp):
[[nodiscard]] virtual Result<Matrix> to_matrix(const Tensor& t, Precision P = Precision::F32) = 0;
// M3(17 §4.3 D3/D9):批量读写 I/O 分组——基类非虚,复用 to_matrix/copy_from:
template <class T> Result<void> read(const Tensor& t, std::span<T> dst);
template <class T> Result<void> write(Tensor& t, std::span<T> src); // 覆盖既有存储、不替换对象
Result<Scalar> get_index(const Tensor& t, std::size_t row, std::size_t col); // 语法糖
Result<void> set_index(Tensor& t, std::size_t row, std::size_t col, Scalar v);
// 引擎实现侧(protected 虚;默认实参只写在公共入口上):
[[nodiscard]] virtual Tensor create_tensor_impl(std::size_t rows, std::size_t cols, Precision P) = 0;
[[nodiscard]] virtual Result<Tensor> from_matrix_impl(const Matrix& m, Precision P) = 0;作用:
create_tensor(3 参):纯分配(CPU 分配零填充、GPU 分配未初始化——需要零的调用方显式zero)create_tensor(4 参,InitSpec):声明式初值(Uninitialized/Zero/Constant/Uniform/Normal), 层算分布参数、引擎填数;策略(host 生成上传 vs 将来设备端原生)由引擎自选(17 §4.4 D6)from_matrix:CPU Matrix → Tensor(按 P 分配存储;可能拷贝)to_matrix:Tensor → CPU Matrix(按 P 转换;可能下载)read/write:批量 span 进出本体——元素类型与precision()精确匹配(U2:错配运行期 错误、类型非法编译期 static_assert);GPUread隐含 flush + 同步(走to_matrix同路)、write走copy_from既有 drain 语义(17 §4.3/§7-4)get_index/set_index:语法糖——宿主直读写、GPU 走批量 read/write(每次一整轮 staging, 不承诺热循环性能,17 §7-1)- L2+ 宿主桥(M4,17 §3 D11 / AGENTS.md 铁律 #12):
nn::detail::upload_span(engine, rows, cols, P, span)(标量缓冲 → 新建张量,f16 目标按 RHE,等价from_matrix的 span 形态)、download_span(engine, t, span)与download_vector(engine, t)(张量 → 标量缓冲,f16 存储先升 f32,等价to_matrix(t, F32))。定义在compute_engine.hpp尾部,内部只用write/read、不加虚函数。Layer/Loss/Optimizer/Model 里只准用这三个 与create_tensor+InitSpec/zero,不得出现Matrix或from_matrix/to_matrix/copy_from——审计:pwsh -File bench\doc_inventory.ps1第 [4] 节,L2-VIOLATIONS必须为 0。
实现注意:
from_matrix_impl应返回拷贝,避免外部修改影响to_matrix应返回拷贝,避免内部状态泄露create_tensor_impl失败时可返回Tensor()空槽——公共入口不做身份绑定; 空槽保持无效(域检查豁免,v3)
// 矩阵乘法(实现侧;公共入口 matmul 带设备域检查,下同)
[[nodiscard]] virtual Result<Tensor> matmul_impl(
const Tensor& A, const Tensor& B,
bool transA, bool transB,
Precision P) = 0;
// 批量矩阵乘法
[[nodiscard]] virtual Result<Tensor> batched_matmul_impl(
const Tensor& A, const Tensor& B,
std::size_t batch,
bool transA, bool transB,
Scalar alpha,
Precision P) = 0;
// 矩阵乘 + bias 广播(默认实现 = matmul + 逐行加 bias)
[[nodiscard]] virtual Result<Tensor> matmul_with_bias_impl(
const Tensor& A, const Tensor& B, const Tensor& bias,
bool transA, bool transB,
Precision P);
// 就地加法
[[nodiscard]] virtual Result<void> add_inplace_impl(Tensor& A, const Tensor& B) = 0;
// 就地缩放
[[nodiscard]] virtual Result<void> scale_inplace_impl(Tensor& A, Scalar s) = 0;
// 置零
[[nodiscard]] virtual Result<void> zero_impl(Tensor& A) = 0;梯度累加
accumulate不是引擎实现接口——它是公共入口,内部经add_inplace_impl完成(f16 时基类先做边界 cast)。
矩阵乘法细节:
| 参数 | 含义 |
|---|---|
transA |
是否转置 A |
transB |
是否转置 B |
batch |
批量大小(batched_matmul) |
alpha |
输出缩放系数(batched_matmul) |
P |
计算精度(F32 默认;F16 走 f16 GEMM),由 Layer 按 PrecisionProfile.compute 显式传入 |
batched_matmul 语义:
- A:
(batch * A_rows, A_cols)按 batch 切分 - B:
(batch * B_rows, B_cols)按 batch 切分 - 输出:
(batch * M, N)垂直堆叠
// 按行求和: (rows, cols) → (rows, 1)
[[nodiscard]] virtual Result<Tensor> row_reduce_sum_impl(const Tensor& A, Precision P) = 0;
// 按列求和: (rows, cols) → (1, cols)
[[nodiscard]] virtual Result<Tensor> col_reduce_sum_impl(const Tensor& A, Precision P) = 0;
// 按列求最大值: (rows, cols) → (1, cols)
[[nodiscard]] virtual Result<Tensor> col_reduce_max_impl(const Tensor& A, Precision P) = 0;
// 分组归约(segmented reduce,沿行方向每连续 R 行为一组):
// x: (G*R, N) → out: (G, N),把"逐通道/逐头一次 dispatch"的层内循环压成单次调用
[[nodiscard]] virtual Result<Tensor> grouped_reduce_sum_impl(
const Tensor& x, std::size_t G, std::size_t R, Precision P) = 0;
[[nodiscard]] virtual Result<Tensor> grouped_reduce_max_impl(
const Tensor& x, std::size_t G, std::size_t R, Precision P) = 0;按行求最大值不走引擎算子——用 DSL 归约叶子
dsl::row_reduce_max(...)(详见expr_dsl.hpp)。 归约表达式在 Layer 里统一走dsl::compute_reduce。
引擎没有逐元素/广播/条件选择类原语(UnaryOp/BinaryOp/CompareOp 枚举也不存在)——这类运算统一用表达式 DSL 表达(单 kernel、无中间张量):
auto y = dsl::compute(engine, dsl::exp(dsl::leaf(*a))); // 一元
auto z = dsl::compute(engine, dsl::leaf(*a) + dsl::leaf(*b)); // 二元
auto w = dsl::compute_into(engine, dsl::leaf(*a) * dsl::rparam(0.1f), *a); // 原地+标量
auto rg = dsl::compute(engine, // 条件选择
dsl::select(dsl::leaf(*x) > Scalar{0}, dsl::leaf(*g), dsl::rparam(0)));
// 需要显式形状时(推导不出的网格,如裸广播向量):dsl::compute(engine, expr, rows, cols[, P])输出形状可省:
dsl::compute(engine, expr[, P])从表达式推导输出网格—— 各"网格贡献者"按广播规则(逐维相等或其一为 1)合并(张量叶/形状保持视图给张量形状、 归约视图给输入整网格、grouped_reduce(t,R)给(rows/R, cols)、matmul 段给(batch·M, N)、标量/索引叶/稀疏与批视图中性);冲突、不可推导或全无贡献者一律 报错并提示改用显式形式(不猜形状)。dsl::compute_reduce(engine, expr[, P])同理 (它要的是输入网格)。细则见02-cpp-api-manual.md§6。
理由:DSL 与逐元素算子能力完全重叠,且天然融合为单 kernel。 引擎原语盘点见
../internal/12-compute-engine-inventory.md; 逐元素算子收敛的历史演进记录见docs/history/history.md。
// 行切片
[[nodiscard]] virtual Result<Tensor> slice_rows_impl(
const Tensor& src, std::size_t start_row, std::size_t count) = 0;
// 行插入
[[nodiscard]] virtual Result<void> insert_rows_impl(
Tensor& dst, std::size_t dst_start_row, const Tensor& src) = 0;
// 行收集(embedding 查表)
[[nodiscard]] virtual Result<Tensor> gather_rows_impl(
const Tensor& table, const Tensor& indices) = 0;
// 行散列累加(embedding 梯度)
[[nodiscard]] virtual Result<void> scatter_add_rows_impl(
Tensor& dst, const Tensor& indices, const Tensor& grad) = 0;
// 3D 重排
[[nodiscard]] virtual Result<Tensor> rearrange_3d_impl(
const Tensor& x, std::size_t M, std::size_t B, std::size_t N,
bool inverse) = 0;
// 转置
[[nodiscard]] virtual Result<Tensor> transpose_impl(const Tensor& A) = 0;
// 卷积/池化窗口展开与其伴随散射(纯数据搬运)
[[nodiscard]] virtual Result<Tensor> im2col_impl(
const Tensor& x, std::size_t C, std::size_t H, std::size_t W,
std::size_t k, std::size_t stride, std::size_t pad,
std::size_t OH, std::size_t OW) = 0;
[[nodiscard]] virtual Result<Tensor> col2im_impl(
const Tensor& col, std::size_t C, std::size_t H, std::size_t W,
std::size_t k, std::size_t stride, std::size_t pad,
std::size_t OH, std::size_t OW) = 0;
// CPU Matrix → 已有张量(序列化加载、优化器参数写回)
[[nodiscard]] virtual Result<void> copy_from_impl(Tensor& dst, const Matrix& src) = 0;
// 深拷贝
[[nodiscard]] virtual Result<Tensor> clone_impl(const Tensor& src) = 0;// 逐元素表达式融合(输出 (rows, cols),P 为输出精度)
[[nodiscard]] virtual Result<Tensor> eval_expr_impl(
const ExprSpec& spec,
std::span<const Tensor> inputs,
std::size_t rows, std::size_t cols,
Precision P) = 0;
// 目标传递:结果直接写入 out(不分配新张量,表达"原地更新")
[[nodiscard]] virtual Result<void> eval_expr_into_impl(
const ExprSpec& spec, std::span<const Tensor> inputs,
std::size_t rows, std::size_t cols, Tensor& out);
// 归约向量原生形状输出(行归约 → (rows,1);列归约 → (1,cols))
[[nodiscard]] virtual Result<Tensor> eval_expr_reduce_impl(
const ExprSpec& spec, std::span<const Tensor> inputs,
std::size_t rows, std::size_t cols, Precision P);作用:将多个逐元素操作合并为一次调用,减少临时张量。Layer 侧一般经 dsl::compute / dsl::compute_into / dsl::compute_reduce 间接使用,不手写 ExprSpec。
// 前缀扫描(RLA forward / backward pass 1 / forward_step):
// causal=true : A_t = A0 + Σ_{i≤t, 同文档} k_i·k_i^T(文档边界处清零)
// causal=false: 全集常数(无边界重置)
// 输出 (B·H·5·d_k, seq):[0) B·P [1) A·P [2) B^T·R [3) s [4) r
[[nodiscard]] virtual Result<Tensor> scan_prefix_outer_impl(
const Tensor& K, const Tensor& V, const Tensor& P, const Tensor& R,
const Tensor& A0, const Tensor& B0, bool has_state,
std::size_t dk, std::size_t heads, bool causal,
const Tensor& boundary, bool has_bnd,
Precision prec) = 0;
// 后缀扫描(RLA backward pass 2):
// causal=true : S_i = Σ_{t≥i, 同文档} D_t
// causal=false: S_i = D_i(Layer 已把全集梯度沿 seq 广播)
// 输出 (B·H·3·d_k, seq):[0) S·X [1) S·Y [2) S^T·Y
[[nodiscard]] virtual Result<Tensor> scan_suffix_outer_impl(
const Tensor& D, const Tensor& X, const Tensor& Y,
std::size_t dk, std::size_t heads, bool causal,
const Tensor& boundary, bool has_bnd,
Precision prec) = 0;
// 逐列外积(RLA backward 物化 dL/dA、dL/dB):
// 输出 (B·H·d_k², seq):out = P·R^T(has_scale 时逐列乘 S[t])
[[nodiscard]] virtual Result<Tensor> outer_col_impl(
const Tensor& P, const Tensor& R, const Tensor& S,
std::size_t dk, bool has_scale,
Precision prec) = 0;形状约定:batch-major i = b*seq+t;头 (b,h) 行块起点 r0=(b*H+h)*d_k;K/V/P/R(X/Y)(B·H·d_k, seq)、D (B·H·d_k², seq)、A0/B0 (H·d_k, d_k);boundary (1, B·seq)(1=文档起点);空参数用 (1,1) dummy + bool 标志(规避 0 字节 GPU buffer);d_k ≤ 64(GPU MAX_DK);标量块 s/r 头内逐行重复(实现写全部行,调用方读任一行)。
使用方:ReLULinearAttention(RAPT 层)——shader 只含"带状态的顺序归约 + matvec 读出 / 外积",算法(L2 分母 / ReLU 门控 / 梯度公式 / 文档重置)全部由 Layer 用原语组合表达(见 08-rapt-algorithm.md §4)。
// 池/直配统计(非虚语义由 override 决定,但**返回值必须非空**):
[[nodiscard]] virtual std::string pool_stats() const; // 基类默认 = 宿主直配账本
[[nodiscard]] virtual Result<void> release_idle_pool_blocks(); // CPU = 语义成立的 no-op作用:
- GPU(
GpuEngineoverride):persist{ blocks=… allocs=… total=…MB dev=… host=… free=… frag=… | … } transient{ … } pending=…MB——双池 + 已析构未 reap 的 pending 字节。 - 无池引擎(CPU 走基类默认):
direct{ blocks=<> live_bytes=<> peak_bytes=<> total_blocks= <> total_bytes=<> }——账本nn::host_alloc_ledger()(algebra_matrix.hpp)在MatrixT的allocate_/release_storage_(析构、移动赋值、resize三个释放出口)记账,memory_order_relaxed原子、只在分配路径,元素热循环零参与。 - 两引擎同一动词、都非空 → 训练日志/GUI 一套代码读两个引擎。观测入口:
NN_MEM_STATS=1 text_train …([mem] engine-init / model-built / optimizer-created打点)。
实现注意:新增后端若自带池,必须 override pool_stats();基类默认值的语义是"没有池就是
直配",不是"没有统计"。release_idle_pool_blocks() 在直配模式无可归还整块,保持 no-op 并写明理由
(不要为了"对称"返回假错误)。
include/neuralnet.cpp/compute_cpu_engine.hpp
class CpuEngine final : public ComputeEngine {
public:
// 1. 设备查询
[[nodiscard]] Device device() const noexcept override {
return Device::CPU;
}
// 2. 批处理控制(no-op)
[[nodiscard]] Result<void> begin_batch() override { return {}; }
[[nodiscard]] Result<void> end_batch() override { return {}; }
// 3. 张量工厂(实现侧;公共入口 create_tensor 负责 stamp 出生绑定)
[[nodiscard]] Tensor create_tensor_impl(std::size_t rows, std::size_t cols, Precision P) override {
if (P == Precision::F16) return Tensor::cpu<Precision::F16>(rows, cols);
return Tensor::cpu(rows, cols);
}
// 4. 矩阵乘法(委托给 Matrix::multiply;公共入口 matmul 带设备域检查)
[[nodiscard]] Result<Tensor> matmul_impl(
const Tensor& A, const Tensor& B,
bool transA, bool transB, Precision P) override {
// 实现细节...
}
};[[nodiscard]] Result<Tensor> matmul_impl(...) override {
if (!A.is_cpu() || !B.is_cpu())
return std::unexpected(Error{"matmul: tensors are not CPU"});
// ...
}原则:
- 检查输入设备类型
- 返回
Result<T>而不是抛异常 - 错误信息清晰明确
[[nodiscard]] Result<Tensor> gather_rows_impl(...) override {
const std::size_t total = num * D;
if (total >= PARALLEL_THRESHOLD && num > 1) {
// 行块并行
auto row_indices = std::views::iota(std::size_t{0}, num);
nn::for_each(row_indices.begin(), row_indices.end(), [...](std::size_t i) {
// 每行独立查表,无数据竞争
});
} else {
// 串行路径
}
}并行策略:
- 行块并行:每行独立,无数据竞争
- 阈值控制:小任务串行,避免调度开销
[[nodiscard]] Result<Tensor> eval_expr_impl(
const ExprSpec& spec,
std::span<const Tensor> inputs,
std::size_t rows, std::size_t cols, Precision P) override {
// CPU:IR 解释器求值(含归约/matmul 视图);
// 逐元素模板路径在公共 dsl::compute 的 CPU 分支,不经过本函数
}include/neuralnet.cpp/compute_gpu_engine.hpp + backend/compute_vk_backend.hpp
class GpuEngine final : public ComputeEngine {
private:
VkBackend backend_; // Vulkan 后端
public:
// 1. 设备查询
[[nodiscard]] Device device() const noexcept override {
return Device::GPU;
}
// 2. 批处理控制(录制 command buffer)
[[nodiscard]] Result<void> begin_batch() override {
return backend_.begin_recording();
}
[[nodiscard]] Result<void> end_batch() override {
return backend_.end_recording_and_submit();
}
// 3. 矩阵乘法(dispatch shader)
[[nodiscard]] Result<Tensor> matmul_impl(...) override {
// 选择 matmul shader(naive/tiled/batched)
// dispatch compute shader
}
};[[nodiscard]] Result<void> begin_batch() override {
// 开始录制 Vulkan command buffer
VkCommandBufferBeginInfo beginInfo{};
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(commandBuffer_, &beginInfo);
return {};
}
[[nodiscard]] Result<void> end_batch() override {
// 结束录制 + 提交 + 等待完成
vkEndCommandBuffer(commandBuffer_);
vkQueueSubmit(queue_, 1, &submitInfo, fence_);
vkWaitForFences(device_, 1, &fence_, VK_TRUE, UINT64_MAX);
return {};
}[[nodiscard]] Result<Tensor> matmul_impl(...) override {
// 根据矩阵大小选择 shader
if (M < 64 || N < 64 || K < 64) {
// 小矩阵:naive shader
return dispatch_matmul_naive(A, B, transA, transB);
} else {
// 大矩阵:tiled shader
return dispatch_matmul_tiled(A, B, transA, transB);
}
}[[nodiscard]] Tensor create_tensor_impl(std::size_t rows, std::size_t cols, Precision P) override {
// 从显存池分配
auto buffer = backend_.allocate_buffer(rows * cols * sizeof(Scalar));
return Tensor::from_gpu(std::move(buffer));
}内存池策略:
- 预分配大块显存
- 按需切分给张量
- 延迟销毁(
pending_destroys_)
前置判断:新需求若是逐元素 / 广播 / 条件选择类,不要加引擎原语——用表达式 DSL(§6), 融合为单 kernel、零临时张量。值得新增原语的是数据搬运 / 结构类能力(如
im2col、grouped_reduce_*这类"一次 dispatch 消灭层内循环"的原语)。 下文以假想原语为例,仅演示"接口 → CPU → GPU → shader → 测试 → 文档"的接线模式。
在 compute_engine.hpp 中按 NVI 模式声明两件套:公共非虚入口
(bind_check_ 设备域检查)+ protected 纯虚实现
(引擎真正 override 的):
// 新原语(示意;逐元素类需求见上方前置判断,一律走 DSL)
// 1) 公共入口(非虚)
[[nodiscard]] Result<Tensor> elementwise_abs_diff(
const Tensor& A, const Tensor& B)
{
if (auto ec = bind_check_({&A, &B}); !ec)
return std::unexpected(ec.error());
return elementwise_abs_diff_impl(A, B);
}
// 2) protected 虚实现(引擎侧实现这个)
[[nodiscard]] virtual Result<Tensor> elementwise_abs_diff_impl(
const Tensor& A, const Tensor& B) = 0;在 compute_cpu_engine.hpp 中添加实现:
[[nodiscard]] Result<Tensor> elementwise_abs_diff_impl(
const Tensor& A, const Tensor& B) override {
if (!A.is_cpu() || !B.is_cpu())
return std::unexpected(Error{"elementwise_abs_diff: tensors are not CPU"});
if (A.rows() != B.rows() || A.cols() != B.cols())
return std::unexpected(Error{"elementwise_abs_diff: shape mismatch"});
Matrix result(A.rows(), A.cols());
auto a_span = A.cpu_matrix().span();
auto b_span = B.cpu_matrix().span();
auto r_span = result.span();
for (std::size_t i = 0; i < a_span.size(); ++i) {
r_span[i] = std::abs(a_span[i] - b_span[i]);
}
return Tensor::from_matrix(std::move(result));
}在 compute_gpu_engine.hpp 中添加实现:
[[nodiscard]] Result<Tensor> elementwise_abs_diff_impl(
const Tensor& A, const Tensor& B) override {
// 1. 选择或创建 shader
// 2. 准备 push constants
// 3. dispatch compute shader
// 4. 返回结果张量
}在 shaders/ 目录创建 .comp 文件:
#version 450
layout(local_size_x = 64) in;
layout(push_constant) uniform PushConstants {
uint rows;
uint cols;
};
layout(binding = 0) readonly buffer BufferA { float A[]; };
layout(binding = 1) readonly buffer BufferB { float B[]; };
layout(binding = 2) writeonly buffer BufferR { float R[]; };
void main() {
uint idx = gl_GlobalInvocationID.x;
if (idx >= rows * cols) return;
R[idx] = abs(A[idx] - B[idx]);
}在 test/ 目录创建测试文件:
TEST_CASE("elementwise_abs_diff") {
auto engine_owner = CpuEngine::create();
CpuEngine& engine = *engine_owner;
Matrix a(2, 2);
Matrix b(2, 2);
// ... 初始化 ...
auto ta = engine.from_matrix(a);
auto tb = engine.from_matrix(b);
auto result = engine.elementwise_abs_diff(*ta, *tb);
REQUIRE(result);
auto r_matrix = engine.to_matrix(*result);
// ... 验证结果 ...
}- 更新
compute_engine.hpp的注释 - 更新本文档的原语列表
- 更新架构文档的原语分类表
问题:频繁分配/释放显存会导致碎片和性能下降
解决方案:
// 使用内存池
class GpuEngine {
MemoryPool pool_; // 显存池
[[nodiscard]] Tensor create_tensor_impl(std::size_t rows, std::size_t cols, Precision P) {
auto buffer = pool_.allocate(rows * cols * sizeof(Scalar));
return Tensor::from_gpu(std::move(buffer));
}
};问题:多个小操作导致多次 kernel launch 开销
解决方案:
// 使用表达式 DSL:一条表达式 = 一个融合 kernel(无中间张量)
auto result = dsl::compute(engine,
dsl::leaf(*a) + dsl::leaf(*b) * dsl::leaf(*c),
rows, cols);问题:串行执行浪费多核 CPU
解决方案:
// 使用行块并行
if (total_work >= PARALLEL_THRESHOLD) {
nn::for_each(row_indices.begin(), row_indices.end(),
[&](std::size_t i) {
// 每行独立处理,无数据竞争
});
}问题:随机内存访问导致 cache miss
解决方案:
// 行主序存储 + 顺序访问
for (std::size_t r = 0; r < rows; ++r) {
for (std::size_t c = 0; c < cols; ++c) {
// 顺序访问 data_[r * cols + c]
}
}每个原语必须有对应的单元测试:
TEST_CASE("matmul") {
auto engine_owner = CpuEngine::create();
CpuEngine& engine = *engine_owner;
Matrix a(2, 3);
Matrix b(3, 2);
// ... 初始化 ...
auto ta = engine.from_matrix(a);
auto tb = engine.from_matrix(b);
auto result = engine.matmul(*ta, *tb);
REQUIRE(result);
auto r_matrix = engine.to_matrix(*result);
// ... 验证结果 ...
}使用 gradcheck 验证梯度计算:
TEST_CASE("linear_gradcheck") {
auto engine_owner = CpuEngine::create();
CpuEngine& engine = *engine_owner;
Linear layer(10, 5);
Matrix input(10, 1);
// ... 初始化 ...
bool ok = gradcheck(engine, layer, input);
REQUIRE(ok);
}使用 layer_bench(src/layer_bench.cpp)验证性能:
# 单算子(默认 CPU,--gpu 走 Vulkan);调参前务必 --warmup 等时钟爬坡(见 AGENTS §12)
./build/layer_bench --op matmul --m 1024 --n 1024 --k 1024 --warmup 20
# 状态扫描原语(RLA/RAPT):--dk 给 head 维,--hash 打印输出的位级哈希
# (**逐位不变**的 kernel 改写必须哈希相同;哈希刻意打在计时之后)
./build/layer_bench --op scan_prefix,scan_suffix --gpu --batch 8 --heads 4 --dk 32 \
--seq 128 --iter 10 --warmup 2 --hash./build/gpu_test注意:无 Vulkan 时返回 77(跳过)。
A: 项目禁止使用异常(-fno-exceptions 编译选项)。Result<T> 提供类似的错误处理能力,但不需要异常支持。
A:
- 启用 Vulkan 验证层
- 使用
gpu_test运行测试 - 检查
VK_ERROR_DEVICE_LOST错误 - 使用 RenderDoc 捕获 frame
- 环境诊断开关:
NN_PREC_TRACE=1(融合精度分派#a/#x命中与 miss 逐条打日志)、 设备域检查(v3 起默认生效,无独立开关:跨域操作数直接Result错误)、NN_MEM_STATS=1(内存池账本)、NN_VULKAN_DEVICE(多 GPU 选择,CLI 另有--gpu)
A:
- 在 Layer 中用表达式 DSL 组合表达(绝大多数激活 = 一条表达式,参照
ReLU::forward:dsl::max(dsl::leaf(input), Scalar{0})) - 在 Layer 中实现 forward/backward(backward 同样是 DSL 表达式)
- 添加 gradcheck 测试
- 引擎与 shader 不认识"激活函数"——绝不在引擎侧加算法名算子(铁律 3)
A:
- shader 无需手动选择:引擎按形状自动分派(f16 存储版 > 小 N GEMV(N≤8)> 粗化分块
matmul_tiled> naive),见backend/compute_vk_backend.hpp的 matmul pipeline 选择 - 调整
BLOCK_SIZE(core_config.hpp,CPU 缓存分块大小) - 批量运算使用
batched_matmul(不走逐次 matmul) - CPU 自适应并行是自动的、无开关:工作量 ≥
PARALLEL_THRESHOLD(524288)即并行, 由nn::parallel_for_blocks/nn::parallel_for_samples门控(阈值core_config.hpp);本机运行保持NN_ENABLE_NATIVE=ON(-march=native) - GPU 显存带宽吃紧时用
--f16(f16 存储 + in-kernel f16 GEMM 变体,实测峰值显存低于 f32, 见docs/development/05-mixed-precision.md§12.11)
A: CPU 操作是同步的,立即执行。GPU 操作是异步的,需要录制 command buffer 后统一提交。
A: 这是注册表 miss(铁律 #7:AOT 注册表为预热加速器;eval_expr 主路径未命中默认走运行期
JIT 现编,NN_STRICT_CLOSED_WORLD=1 或 fold / reduce / into / multi 入口下为硬报错,不静默回退),
不是崩溃。按 key 依次排查:
- 改了 Layer 里的内联表达式但没重新构建:直接重跑构建即可——
scan_exprs单步 (收集 + 生成)会自动重建fused_registry.hpp(重建后该结构回到预热路径,免 JIT 冷启动) - 该 Layer 路径没被扫描覆盖:结构来自锚点自登记 + per-layer dry-run + 模型 pass 三路;
报错带
key=,可查build/generated/fused_registry.hpp是否有该条目 - 库外自定义层:必须用
nn_enable_gpu_fusion(<target> MAIN <src>)生成该目标专属注册表, 且${NNF_OUT_DIR}的 include 顺序要在库自身注册表之前(见 AGENTS §7) - 注意区分:
[prec][miss]是精度变体的诊断日志、不是错误——精度三档key#a→key#x→ 基类边界 cast,最后一档正确但更慢;只有 f32 结构 key 缺失才触发 miss