Skip to content

Latest commit

 

History

History
1128 lines (887 loc) · 54.1 KB

cpp-api-manual.md

File metadata and controls

1128 lines (887 loc) · 54.1 KB

C++ API 手册(库使用者)

读者:调用者(库使用者)——要把 neuralnet.cpp 当成一个 C++ 库来用、写自己的程序。 本分册定位见 README.md;只想跑现成 CLI / GUI 见 ../usage/02-quickstart-train-infer.md。

目标:让你不读源码就能把库的全部能力接进自己的程序。本文所有签名都取自 include/neuralnet.cpp/ 的实际声明(稳定入口),每节给出可粘贴的代码片段。 引擎原语、张量 I/O、表达式 DSL 的逐条细节在 ../usage/03-compute-engine-usage.md(下称「引擎手册」), 本文只做速查与交叉引用,不重复。


0. 阅读路径:我要做什么 → 看哪节

我要做的事 看
把库嵌进我的 CMake 工程(add_subdirectory / FetchContent) §2 + cmake-integration.md + ../usage/05-consume-as-library.md
创建引擎、开 GPU §3
创建 / 读写张量、上传下载数据 §4(细节见引擎手册 §3–§6)
打开 --f16 / 三值权重那样的精度配置 §5
用表达式 DSL 写逐元素 / 归约数学 §6(细节见引擎手册 §8)
搭 MLP / Transformer / CNN / GPT / RAPT 模型 §7、§8
用现成模型工厂一条命令建模型 §8
算 loss、建优化器、写训练循环 §9、§10、§11(文本训练推荐 nn::TextTrainer)
写 RL / PPO / GRPO / 多模型角色 / rollout pipeline ../usage/06-reinforcement-learning.md
保存 / 加载 .nnmodel / .nnckpt、读架构 spec §12
读 .nndataset 数据集(含训练样本/窗口) §13
训练 / 使用分词器、编码解码 §14
自回归生成文本 §15
剖析性能、看环境变量开关 §16
查错误处理宏、断言宏 §17
写自己的 CLI / 用底层工具(文件、哈希、线程池) §19
写一个自己的层 §18 + write-a-custom-layer.md
往引擎里加一个新原语 01-compute-engine-development.md
多精度的语义契约(写层必读) 05-mixed-precision.md

两条最容易踩的铁律:① 一切张量的创建 / 读写 / reshape 必须经 ComputeEngine (绕过 = 编译错误);② 全程 禁止 throw/try/catch(-fno-exceptions), 错误一律走 Result<T>。


1. 起步:头文件、命名空间、错误处理

1.1 include 方式

#include <neuralnet.cpp/nn.hpp>   // 唯一聚合入口:一次拿齐引擎/层/损失/优化器/序列化/分词器/数据集

也可按用途 include 薄转发头(名字即用途,不省编译时间,只为可读):

#include <neuralnet.cpp/engine.hpp>     // ComputeEngine / CpuEngine / GpuEngine
#include <neuralnet.cpp/layer.hpp>      // Layer 基类 + 全部内置层 + 位置编码
#include <neuralnet.cpp/loss.hpp>       // Loss / MSELoss / CrossEntropyLoss
#include <neuralnet.cpp/optimizer.hpp>  // Optimizer + create_optimizer
#include <neuralnet.cpp/model.hpp>      // Model / ModelSpec / save_model / load_model
#include <neuralnet.cpp/train.hpp>      // 上面五者的合集(写训练循环够用)
#include <neuralnet.cpp/rl.hpp>         // RL:schema/named output/experience/objectives/pipeline

模型工厂(build_gpt_model 等)与分词器在 domain_*.hpp(已随 nn.hpp 一起引入)。 一切公开符号都在 namespace nn 下;所有形状 / 维度都是 std::size_t。

1.2 nn::Scalar 与 nn::f16

nn::Scalar = float;   // core_config.hpp 里的类型别名;全库标量与宿主数值都用它
nn::f16;              // IEEE binary16 值类型(2 字节,与 GPU R16F 内存布局一致)

f32↔f16 的转换一律 round-half-to-even;static_cast<float>(nn::f16) 升位精确无损。 你不需要手动 cast 操作数——引擎公共入口的边界 cast 层会自动处理(见 §5)。

1.3 错误处理宏(唯一一族,无异常)

nn::Result<T>  = std::expected<T, nn::Error>;   // Error 含 .message
宏 语义 用在哪
NN_TRY(decl, expr) 声明 + 检查 + 失败即传播 return std::unexpected(...) 函数返回 Result<T>
NN_TRY_MSG(decl, "语境", expr) 同上,失败时带上调用点语境 同上
NN_TRY_CHECK(x) 检查已声明的变量(失败传播) Result 已在手
NN_UNWRAP(expr[, "语境"]) 解包:失败打印 message + file:line 后 abort;成功返回解包后的值 main / void / 测试
NN_UNWRAP_1(x) NN_UNWRAP 的别名 同上
NN_EXIT(expr, code[, "语境"]) 解包,失败打印后 exit(code) CLI,保住退出码
NN_FAIL("msg") 源头:返回 std::unexpected(Error{"msg"}) 函数返回 Result
NN_ASSERT_ALWAYS(cond, msg) 前置条件断言,与构建类型无关(NDEBUG 下同样终止) 断言后还要用该对象
NN_FATAL(msg) 无条件 fail-fast(打印后终止) 不可恢复

⚠ NN_UNWRAP 名字叫 CHECK,其实是"解包",不是断言: auto x = NN_UNWRAP(engine.from_matrix(m)); 里 x 已经是 Tensor,再写 *x 会编译错误。 返回 Result<T> 且 T 带 [[nodiscard]] 的入口(如 Model::backward)丢弃返回值要写 (void)NN_UNWRAP(...)。NN_TRY_CHECK(x) 对带副作用的表达式会重复求值,只能对已声明变量用。

// 三种典型写法
[[nodiscard]] nn::Result<nn::Tensor> step(nn::Model& model, const nn::Tensor& x)
{
    NN_TRY(out, model.forward(x));          // 传播
    return out;
}

int main()
{
    auto engine_owner = nn::CpuEngine::create();
    nn::CpuEngine& engine = *engine_owner;
    nn::Tensor t = engine.create_tensor(4, 4);          // 纯分配:查 .valid()
    NN_UNWRAP(t.valid(), "oom");                         // bool 条件也可直接喂给 NN_UNWRAP
    nn::Matrix m = NN_UNWRAP(engine.to_matrix(t));       // 解包 + 失败终止
    (void)m;
    return 0;
}

注意 NN_FAIL(...) 展开是 return std::unexpected(...)——只能出现在返回 Result 的函数里。在 main / void / 测试里做检查用 NN_UNWRAP(cond[, "msg"]) 或 NN_FATAL("msg"), 否则 -Wreturn-mismatch 直接编译失败。


2. 作为库嵌入(速查)

两行 CMake + 一个 include:

add_subdirectory(3rd_party/neuralnet.cpp)
target_link_libraries(my_app PRIVATE neuralnet::nn)
#include <neuralnet.cpp/nn.hpp>
int main()
{
    auto engine_owner = nn::CpuEngine::create();
    nn::CpuEngine& engine = *engine_owner;
    /* ... */
}

门面目标 neuralnet::nn 传递:C++26、include 目录、线程库、GPU 宏 NN_HAS_VULKAN、 SPIR-V 嵌入头目录、Vulkan 链接与 shader 生成顺序。嵌入时 NN_BUILD_APPS 默认 OFF、 NN_ENABLE_GPU 默认 AUTO。自研 nn::dsl::compute 表达式要一行 nn_enable_gpu_fusion(...) 才进 GPU 闭合世界。

完整版(全部构建开关、GPU 语义、融合注册、故障排查)见 cmake-integration.md; 消费方视角的精简版见 ../usage/05-consume-as-library.md。


3. 引擎:选择与创建

引擎由 shared_ptr 管理。Layer / Model / Optimizer 会持有强引用, Tensor 不保存引擎身份(设备域从存储派生);不再需要“引擎最后析构”的调用约定。 ⚠ 但这不覆盖优化器与模型的顺序:Optimizer 的参数/梯度是 Model 内 Tensor 的非拥有引用,优化器必须先于 Model 销毁(否则 step() UAF); 用 nn::TrainingSession 统一拥有两者可结构性避免(todo/29)。

// ① CPU(默认,无额外依赖)
auto engine_owner = nn::CpuEngine::create();
nn::CpuEngine& engine = *engine_owner;

// ② GPU(需 Vulkan,编译期宏 NN_HAS_VULKAN)
#ifdef NN_HAS_VULKAN
auto backend = nn::GpuBackend::shared();                   // 进程共享,RAII 析构
auto r = backend->initialize("40HX");                      // 空串 = 自动选卡;也可传枚举索引 "2"
if (r)
{
    auto gpu_engine_owner = nn::GpuEngine::create(backend);
    nn::GpuEngine& gpu_engine = *gpu_engine_owner;
}
else
{
    std::fprintf(stderr, "%s\n", r.error().message.c_str());
}
#endif

CLI 里更常用的是工厂函数(与各可执行文件同一套选择逻辑;显式请求失败不回退 CPU):

#include <neuralnet.cpp/cli/cli_engine_factory.hpp>

nn::cli::EngineConfig cfg;
cfg.use_gpu    = true;      // 启用 Vulkan
cfg.gpu_device = "NVIDIA";  // 索引 "2" 或名称子串 "40HX"/"NVIDIA";空 = 自动
std::unique_ptr<nn::ComputeEngine> engine =
    NN_UNWRAP(nn::cli::create_engine(cfg, std::cerr));
// 引擎能力是**只读信息**,经 describe() 披露(EngineInfo)——禁止用它选路(铁律 #13)
std::cout << engine->describe().text() << "\n";

引擎原语(matmul / 归约 / 数据搬运 / 扫描 / 表达式求值 / 批处理录制)的完整清单与签名 见引擎手册 §19 的「方法速查总表」。本文不再重复。


4. 张量:创建、初始化、读写

Tensor 是跨设备统一句柄(CPU 持 Matrix<P>,GPU 持 GpuTensor<P>,内部 std::shared_ptr, 拷贝廉价)。公共面只有:device / precision / rows / cols / size / is_cpu / is_gpu / valid / shape_str / bound / engine——存储访问器与静态工厂已私有(铁律 #11)。

4.1 创建

// 纯分配(3 参):CPU 零填充,GPU 未初始化;失败返回**无效张量**(查 .valid())
nn::Tensor t = engine.create_tensor(784, 32);
NN_UNWRAP(t.valid(), "create_tensor failed");

// 指定精度
nn::Tensor h = engine.create_tensor(784, 32, nn::Precision::F16);

// 声明式初始化(4 参,推荐):层算分布参数,引擎填数;分布类 seed 必填
nn::Tensor w = engine.create_tensor(32, 10, nn::Precision::F32,
                                    nn::InitSpec::uniform(-0.1f, 0.1f, nn::kInitSeed));
nn::Tensor b = engine.create_tensor(10, 1, nn::Precision::F32, nn::InitSpec::zero());

// 需要结构化错误(而非"无效张量")时用 try_create_tensor:失败会说明原因
nn::Tensor s = NN_UNWRAP(engine.try_create_tensor(10, 1, nn::Precision::F32,
                                                 nn::InitSpec::normal(0.f, 0.02f, nn::kInitSeed)));

nn::InitSpec 工厂:uninitialized() / zero() / constant(v) / uniform(lo, hi, seed) / normal(mean, stddev, seed)。同 seed 的不同张量不撞流(引擎把 seed 与创建序号混流) → 跨进程初值逐字节确定(铁律 #8)。

4.2 宿主 ↔ 张量(只准经这 7 个动词)

from_matrix / to_matrix / import / read / write / get_index / set_index
nn::Matrix m(784, 32);                                   // 宿主 I/O 载体(行主序)
// ... 填 m ...
nn::Tensor t   = NN_UNWRAP(engine.from_matrix(m));        // 上传
nn::Matrix back = NN_UNWRAP(engine.to_matrix(t));         // 下载(GPU 会先等在飞命令)

// 大批量、免 Matrix 开销(性能敏感路径用它):span 直读直写
std::vector<nn::Scalar> host(t.size());
NN_UNWRAP(engine.write(t, std::span(host)));              // 宿主 → 张量
NN_UNWRAP(engine.read(t, std::span(host)));               // 张量 → 宿主

reshape / clone / cast / copy_from 等语义见引擎手册 §4–§6。要点: engine.reshape(t, rows, cols)(不是 t.reshape(),那个已私有);元素数不匹配报错。

4.3 布局(必须背下来)

Matrix(宿主):行主序 (rows, cols),data[row*cols + col]
神经网络张量:**列主序 batch-major**
  输入 (feature_dim, batch_size)   每列一个样本
  权重 (out_features, in_features)
GPT 序列展平:列序 i = b*seq + t(batch 在列方向,铁律 #5)

最典型的错误是写成 create_tensor(batch, features)——正确是 (features, batch)。


5. 精度:PrecisionProfile 与预设

enum class nn::Precision : std::uint8_t { F16, F32, BF16, F64, T1_58 };
// 存储 / 舍入精度只有 F16 / F32;BF16 / F64 是保留值(使用即报错);
// T1_58 = 三值权重**声明**(不是存储精度:precision_bytes(T1_58) == 0)。

struct nn::PrecisionProfile {
    Precision param     = Precision::F32;   // 权重 / 嵌入表 / 参数存储
    Precision compute   = Precision::F32;   // 常规算子(matmul / 逐元素 / 数据操作)
    Precision stable    = Precision::F32;   // 数值敏感算子(softmax / Norm / loss)
    Precision optimizer = Precision::F32;   // 优化器状态(m / v / momentum)
};

现成配方(与 CLI 预设一一对应):

nn::profile_f32();               // 全 F32(默认)
nn::profile_f16();               // Plan A 标准 --f16:param=F16, compute=F16, stable=F32, optimizer=F32
nn::profile_t1_58();             // --t1_58:param=T1_58(其余三槽 = profile_f16)
nn::profile_master_weights();    // Plan A 的 AMP master-weights 形态:f32 主权重 + f16 计算 + f32 稳定/优化器
nn::profile_all_f16();           // Plan B 真 f16:四槽全 F16;F5 后可训练,但 native16/零转换尚未完成,见 todo/26 §8

两种用法:

// ① 模型级:在 add() 之前设默认 profile(权重按精度创建)
nn::Model model(engine);
model.set_default_precision_profile(nn::profile_f16());
NN_UNWRAP(model.add<nn::Linear>(784, 256));

// ② 工厂直接传:build_* 的 precision 形参
nn::GptConfig cfg;
cfg.precision = nn::profile_t1_58();
nn::Model gpt = NN_UNWRAP(nn::build_gpt_model(engine, cfg));

唯一要记的口诀:f16 只改变"数据存哪儿",不改变"算子怎么算"——每个算子以 f32 参考计算、输出舍入到目标 P;matmul / 归约累加恒 f32。边界 cast 全在引擎公共入口完成, 不要自己 cast 操作数。

层的能力校验集中在 Layer::init() 一处:不兼容组合(例如给普通 Linear 传 param=T1_58) = 初始化期报错,信息含层名 + 槽位 + 取值 + 允许集合。三值权重只被 BitLinear 接受 (GPT / MLP 工厂会把 param=T1_58 自动分流到 BitLinear;RAPT / ViT 明确拒绝)。

完整语义契约(P 精度算术的形式化定义、分派规则、实测数值限制)见 05-mixed-precision.md。


6. 表达式 DSL:写逐元素 / 归约数学

DSL 是库内所有逐元素与归约运算的写法(层不写手工循环)。三个入口:

// 输出一个**新张量**(默认 P = F32)。输出形状可省——由表达式推导:
nn::Tensor y = NN_UNWRAP(nn::dsl::compute(engine, expr, nn::Precision::F32));

// 显式给形状:推导不出的网格(如裸广播向量)用这一形
nn::Tensor y = NN_UNWRAP(nn::dsl::compute(engine, expr, rows, cols, nn::Precision::F32));

// 就地写进已有张量(只支持逐元素表达式,无归约)
NN_UNWRAP(nn::dsl::compute_into(engine, expr, dst));

// 归约:输出归约向量((rows,1) 或 (1,cols) 原生形状);rows/cols 是**输入网格**,可省
nn::Tensor r = NN_UNWRAP(nn::dsl::compute_reduce(engine, expr, P));

形状推导口径(expr_dsl.hpp 的 GridInfer):输出网格 = 各"网格贡献者"按广播规则(逐维相等,或其一为 1)的合并。 张量叶 / 形状保持视图(RotateHalf/RowMod/RowAccess/Row|ColBroadcast)贡献 张量形状;归约视图与归约指令(row_reduce_max(x) 等)贡献输入整网格 (在 compute 下它们是"按行/列读"的广播读,输出仍是整网格);grouped_reduce(t, R) 贡献 (rows/R, cols);dsl::matmul 段贡献 (batch·M, N)(GQA 的 batch/ratio 行块与 to_spec 的 k 同口径;A/B 本身不参与合并);标量、网格索引叶 (row()/col()/batch())与稀疏/批视图(RowGather/BatchMod/BatchCol)中性。 形状冲突、无法推导(如 GQA 行块不整除)、或全无贡献者(如裸 row_broadcast(v)) 一律返回错误并提示改用显式形式——不猜形状(静默错值远比报错昂贵)。 回归用例:src/dsl_shape_infer_test.cpp。

叶子与算子(全部在 namespace nn::dsl):

nn::dsl::leaf(t)                          // 张量输入
nn::dsl::rparam(v)                        // 标量常量(值不进 expr_spec_key)
nn::dsl::matmul(a, b, transA, transB)     // 一个表达式**至多一个** matmul
nn::dsl::row_broadcast(t)                 // (F,1) → 沿行广播
nn::dsl::col_broadcast(t)                 // (1,B) → 沿列广播
nn::dsl::col_reduce_sum(x), nn::dsl::row_reduce_sum(x)
nn::dsl::col_reduce_max(x), nn::dsl::row_reduce_max(x)
nn::dsl::select(cond, a, b)               // 唯一的条件选择(cond 是 BoolExpression)
t1 + t2, t1 - t2, t1 * t2, t1 / t2, -t    // 运算符重载
// 例:逐列减去列最大值(数值稳定 softmax 的一步)
nn::Tensor z = NN_UNWRAP(nn::dsl::compute(engine,
    nn::dsl::leaf(x) - nn::dsl::col_broadcast(mx), x.rows(), x.cols(), P));

四条必须知道的约束:① 一个表达式只允许一个 matmul(ExprSpec::matmul 是单个 optional)——两层 matmul 要拆成两次 dsl::compute;② 归约是独立叶子,配合 compute_reduce 用;③ 条件是 nn::dsl::BoolExpression(比较 / 逻辑运算的结果), 只能喂给 select;④ 传 T1_58/BF16/F64 作输出 P 会被拒绝。

一元函数(exp/log/sqrt/tanh 等 7 个 + 一元负号)、视图 / 广播 / 归约的完整清单 与 5 个陷阱见引擎手册 §8。GPU 侧:库内层的融合 shader 构建期已生成;自研表达式 需一行 CMake 注册(见 §2 / cmake-integration.md),否则运行期硬报错。


7. 层与 Model 容器

7.1 Model

nn::Model model(engine);                          // 必须绑定引擎(默认构造后使用时断言失败)

// 添加层:add<T>(args...) 构造 + 自动 init(engine),返回 Result<void>(不能链式调用)
NN_UNWRAP(model.add<nn::Linear>(784, 256));
NN_UNWRAP(model.add<nn::ReLU>());
NN_UNWRAP(model.add<nn::Linear>(256, 10));

// 添加工厂构造的层(如按 NormType 建归一化层),可显式注入 profile
NN_UNWRAP(model.add_layer(nn::make_norm_layer(256, nn::NormType::RMSNorm)));
NN_UNWRAP(model.add_layer(nn::make_linear_layer(256, 10, nn::WeightQuant::T1_58), profile));

nn::Tensor out = NN_UNWRAP(model.forward(input));       // (10, batch)
nn::Tensor gin = NN_UNWRAP(model.backward(grad_out));   // 逐层反向

std::vector<nn::TensorRef> ps = model.parameters();    // 供优化器
std::vector<nn::TensorRef> gs = model.param_gradients();
NN_UNWRAP(model.zero_grad());                           // 每步开头

其它 Model 方法:

方法 作用
num_layers() / layer_at(i) 层数与按序号取 Layer&(越界即 fail-fast)
engine() 取绑定引擎
set_spec(spec) / spec() 记录 / 读取架构 spec(load_model 用它校验)
set_default_precision_profile(p) add 之前调用,给后续层设默认 profile
set_precision_profile(p) 给已有顶层层注入 profile(复合层会下传子层)
set_flush_interval(n) GPU:每 n 个块 flush 一次(防 TDR;GPTModel 生效)
set_checkpoint_every(n) 梯度检查点(激活重计算)间隔
set_activation_offload(bool) 激活卸载到 host 可见内存
offload_ram_bytes() 理论 offload 字节数(诊断)
set_training(bool) 训练 / 推理模式(BatchNorm 等用)
set_doc_ids(ids) 文档边界(掩码注意力用)
extra_state() 非可学习状态(BatchNorm running 统计量,供序列化)

7.2 内置层目录(构造签名)

全部层都派生自 nn::Layer,经 Model::add<T>(...) 或 Model::add_layer(std::make_unique<T>(...)) 接入。= 后为默认值。

层 构造签名 用途
nn::Linear Linear(in_features, out_features) 全连接
nn::BitLinear BitLinear(in_features, out_features) 三值权重线性(param 必须 T1_58)
nn::ReLU ReLU() ReLU
nn::GeLU GeLU() QuickGeLU
nn::SwiGLU SwiGLU() 门控激活
nn::ReLU2GLU ReLU2GLU() 门控平方 ReLU
nn::LayerNorm LayerNorm(normalized_shape, epsilon = EPSILON) 层归一化
nn::RMSNorm RMSNorm(normalized_shape, epsilon = EPSILON) RMSNorm
nn::BatchNorm BatchNorm(features, epsilon = EPSILON, momentum = MOMENTUM) 批归一化(含 running 统计量)
nn::Softmax Softmax() 行级 softmax
nn::FeedForward FeedForward(d_model, d_ff, activation = GeLU, subln = false, subln_norm_type = LayerNorm, weight_quant = None) FFN
nn::PositionalEncoding PositionalEncoding(d_model, max_len = 5000, tile_size = 0) 正弦位置编码
nn::TransformerEncoderLayer TransformerEncoderLayer(d_model, num_heads, d_ff, seq_len = 0, norm_type = LayerNorm) Pre-Norm 编码器层
nn::TransformerEncoder TransformerEncoder(d_model, num_heads, d_ff, num_layers, num_patches, norm_type = LayerNorm, final_norm = false) ViT 编码器
nn::PatchEmbedding PatchEmbedding(img_size, patch_size, d_model) 图像 patch 嵌入
nn::CausalSelfAttention CausalSelfAttention(d_model, num_heads, max_len = 1024, seq_len = 0, pos_enc = Learned, subln = false, subln_norm_type = LayerNorm, n_head_kv = 0, weight_quant = None) 因果自注意力
nn::GPTBlock GPTBlock(d_model, num_heads, d_ff, max_len = 1024, seq_len = 0, pos_enc = Learned, activation = GeLU, norm_type = LayerNorm, precision = {}, subln = false, n_head_kv = 0, weight_quant = None) Pre-Norm 解码块
nn::GPTModel GPTModel(vocab_size, d_model, seq_len, num_heads, d_ff, num_layers, pos_enc_type = Learned, activation = GeLU, norm_type = LayerNorm, precision = {}, subln = false, n_head_kv = 0, tie_embeddings = false, weight_quant = None) 完整 GPT(单层封装)
nn::Conv2D Conv2D(in_channels, out_channels, kernel, stride = 1, padding = 0, in_h = 0, in_w = 0) 卷积
nn::MaxPool2D MaxPool2D(channels, in_h, in_w, pool = 2, stride = 0) 最大池化
nn::ReLULinearAttention ReLULinearAttention(d_model, num_heads, seq_len = 0, causal = true, pos_enc = RoPE) RLA 注意力
nn::RAPTBlock RAPTBlock(d_model, num_heads, d_ff, seq_len, pos_enc, activation = GeLU, norm_type = LayerNorm, causal = true, precision = {}) RAPT 块
nn::RAPTModel RAPTModel(vocab_size, d_model, seq_len, num_heads, d_ff, num_layers, pos_enc = RoPE, activation = GeLU, norm_type = LayerNorm, causal = true, precision = {}) 完整 RAPT

nn::MultiHeadAttention 的构造函数是 protected(设计为基类),不通过 add 直接添加—— 用 CausalSelfAttention / TransformerEncoderLayer 组合。

每个层的数学公式与原语分解见 ../introduction/03-algorithm-reference.md。

7.3 归一化 / 线性工厂(按枚举建层)

std::unique_ptr<nn::Layer> nn::make_norm_layer(std::size_t features, nn::NormType t);
std::unique_ptr<nn::Layer> nn::make_linear_layer(std::size_t in, std::size_t out,
                                                 nn::WeightQuant q = nn::WeightQuant::None);

nn::NormType = LayerNorm / RMSNorm / BatchNorm;nn::WeightQuant = None / T1_58。

7.4 位置编码

模型工厂内部已按 spec 选好;手动组装时用工厂(compute_position_encoding.hpp):

std::unique_ptr<nn::PositionEncoder> pe =
    nn::make_attention_position_encoder(nn::PosEncodingType::RoPE,
                                        d_model / num_heads, num_heads);   // 注意力注入点
// 嵌入注入点:nn::make_embedding_position_encoder(type, d_model, seq_len)
//   (Learned / Sinusoidal 生效;ALiBi / RoPE 返回 NoPositionEncoder)

nn::PosEncodingType = Learned / Sinusoidal / ALiBi / RoPE。


8. 模型工厂(domain_*.hpp)

工厂 = 一条调用建出带正确权重初始化与正确 spec 的模型,是最省心的入口。

8.1 MNIST(MLP / ViT)

// MLP:784 → 512 → 256 → 128 → 64 → 10(默认层维度)
nn::Model mlp = NN_UNWRAP(nn::build_mnist_mlp_model(engine));

// ViT:28×28 图像,patch=7(默认含末端 final norm)
nn::Model vit = NN_UNWRAP(nn::build_mnist_transformer_model(engine));

// 从 spec 还原(加载 .nnmodel 时用)
nn::ModelSpec spec = NN_UNWRAP(nn::peek_any_model_spec("mnist_model.bin"));
nn::Model m2 = NN_UNWRAP(nn::build_mnist_model_from_spec(engine, spec, nn::profile_f32()));

签名(默认值节选): build_mnist_mlp_model(engine, layer_dims = MNIST_LAYER_DIMS, norm_type = LayerNorm, precision = {}); build_mnist_transformer_model(engine, img_size = 28, patch_size = 7, d_model = 64, num_heads = 4, d_ff = 128, num_layers = 2, precision = {}, norm_type = LayerNorm, norm_place = NormPlace::Final)。

8.2 GPT

nn::GptConfig cfg;                        // 每个字段都有默认值
cfg.vocab_size = 10000; cfg.d_model = 128; cfg.seq_len = 256;
cfg.num_heads  = 4;     cfg.d_ff = 512;    cfg.num_layers = 4;
cfg.pos_enc    = nn::PosEncodingType::RoPE;
cfg.activation = nn::ActivationType::SwiGLU;
cfg.norm_type  = nn::NormType::RMSNorm;
cfg.n_head_kv  = 0;                       // 0 = MHA;>0 = GQA 的 KV 头数
cfg.tie_embeddings = false;               // true = head 复用 token embedding
cfg.precision  = nn::profile_f16();
nn::Model gpt = NN_UNWRAP(nn::build_gpt_model(engine, cfg));

// 位置参数版(等价的简写)
nn::Model gpt2 = NN_UNWRAP(nn::build_gpt_model(engine, 10000, 128, 256, 4, 512, 4));

8.3 CNN 与 RAPT

// CNN(LeNet 风格可自定义)
nn::CnnConfig c;                          // in_channels / in_size / pool / convs / fc_dims ...
nn::Model cnn = NN_UNWRAP(nn::build_cnn_model(engine, c));
nn::ModelSpec cspec = nn::make_cnn_spec(1, 28, 2, nn::MNIST_CNN_CONVS, nn::MNIST_CNN_FC);

// RAPT(线性注意力 causal LM;默认 pos_enc = RoPE,RLA 约束)
nn::RAPTConfig r;
nn::Model rapt = NN_UNWRAP(nn::build_rapt_model(engine, r));

8.4 spec 构造与还原(统一表)

模型族 从 spec 还原 构造 spec
MNIST MLP / ViT nn::build_mnist_model_from_spec nn::make_mnist_transformer_spec
GPT nn::build_gpt_model_from_spec nn::make_gpt_spec
CNN nn::build_cnn_model_from_spec nn::make_cnn_spec
RAPT nn::build_rapt_model_from_spec nn::make_rapt_spec

nn::ModelSpec(model_spec.hpp)是架构的唯一描述:type(nn::ModelType)、 vocab_size / d_model / seq_len / num_heads / d_ff / num_layers / patch_size、 layer_dims、pos_encoding / activation / norm_type / norm_place、 subln / n_head_kv / tie_embeddings / weight_quant、CNN 的 cnn_in_channels / cnn_in_size / cnn_pool / cnn_channels 等。nn::spec_matches(a, b) 判定两份 spec 是否兼容(load_model 用它拦截架构不匹配)。


9. 损失函数(compute_loss.hpp)

class nn::Loss {
public:
    void set_precision_profile(const nn::PrecisionProfile& p);
    [[nodiscard]] nn::Result<nn::Scalar> forward(nn::ComputeEngine&, const nn::Tensor& pred,
                                                 const nn::Tensor& target);
    [[nodiscard]] nn::Result<nn::Tensor> backward();   // 对 pred 的梯度
};

9.1 nn::MSELoss

nn::MSELoss loss;
nn::Scalar l    = NN_UNWRAP(loss.forward(engine, pred, target));   // pred/target 同形状
nn::Tensor grad = NN_UNWRAP(loss.backward());                      // = (2/N)·(pred−target)

9.2 nn::CrossEntropyLoss(含 softmax)

// 稠密版:pred/target 都是 (classes, batch)
nn::CrossEntropyLoss loss;
nn::Scalar l    = NN_UNWRAP(loss.forward(engine, logits, one_hot_target));
nn::Tensor grad = NN_UNWRAP(loss.backward());                      // (softmax − target)/batch

大词表禁止物化 one-hot(铁律 #9)——用稀疏标签版:

std::vector<std::size_t> labels;   // 平坦标签,长度 = logits.cols()
std::vector<nn::Scalar>  mask;     // 可选;空 = 全参与;>0.5 参与 loss

// ① 同步版(测试 / 评估):内部下载标量
nn::Scalar l = NN_UNWRAP(loss.forward_sparse(engine, logits, labels, mask, vocab_size));

// ② 热路径版:设备端求和 + 异步标量回读(避免每 step 全流水线 drain)
std::size_t num_valid = 0;
nn::Tensor loss_sum = NN_UNWRAP(loss.forward_sparse_sum(engine, logits, labels, mask,
                                                       vocab_size, num_valid));
// 训练热循环配合 engine.submit_scalar_readback(...) 异步取回

稀疏 CE 走融合路径(online-softmax 单趟 fold),不物化全 softmax; 前置条件是 vocab_size <= 2^24。稠密 / 稀疏两条路径的数值都稳定(避免 0·log(0)=NaN)。


10. 优化器(compute_optimizer.hpp)

10.1 工厂(名单唯一真源 = kOptimizerTable)

std::unique_ptr<nn::Optimizer> nn::create_optimizer(
    std::string_view name, nn::ComputeEngine& engine,
    std::vector<nn::TensorRef> params, std::vector<nn::TensorRef> grads,
    nn::Scalar lr, nn::Scalar weight_decay = 0,
    nn::PrecisionProfile precision = {}, nn::Scalar beta1 = 0.9, nn::Scalar beta2 = 0.999);
auto opt = nn::create_optimizer("adamw", engine, model.parameters(),
                                model.param_gradients(), 1e-3f, 0.01f, nn::profile_f16());
NN_UNWRAP(opt != nullptr, "unknown optimizer");

可用名字(nn::optimizer_names(", ") 可打印):sgd / sgd_momentum / adam / adamw / adamwquant / muon。未知名字返回 nullptr(由调用方处理)。 beta1 / beta2 仅 adam / adamw 系列使用(LLM 预训练常取 beta2=0.95)。 nn::find_optimizer(name) 查元数据(状态缓冲数、是否半精度存储)。

10.2 训练循环里的调用

opt->set_lr(lr);                      // 动态改学习率(调度器用)
NN_UNWRAP(opt->zero_grad());           // 每步开头清零(或 model.zero_grad();均 [[nodiscard]])
// ... forward / loss / backward ...
NN_UNWRAP(opt->clip_grad_norm(1.0f));  // 全局 L2 范数裁剪(在 step 之前;<=0 或空则 no-op)
NN_UNWRAP(opt->step());

其它:opt->precision_profile()、opt->set_precision_profile(p)。


11. 完整训练循环模板

11.1 推荐:数据集直连训练工厂 nn::TextTrainer(P2b)

文本/GPT 训练的最小闭环 = Dataset →(内部切样本)→ TextTrainer → global/micro batch → TrainingSession。 nn::TextTrainConfig 是训练配方唯一真源(seq_len/batch_size/epochs/lr 调度…); epoch×step 循环、shuffle、global→micro 切分、lr 调度、测试评估、断点状态装配都在 Trainer; Session 只接收 micro-batch,内部累积并按 global batch 有效单位归一化后执行 update:

#include <neuralnet.cpp/nn.hpp>
#include <neuralnet.cpp/text_trainer.hpp>   // 训练工厂(L5;不在 nn.hpp 聚合头内)

auto engine_owner = nn::CpuEngine::create();
nn::CpuEngine& engine = *engine_owner;

// 1. 模型(也可 build_gpt_model_from_spec)
nn::Model model = NN_UNWRAP(nn::build_gpt_model(engine, /*vocab=*/10000,
                                                /*d_model=*/128, /*seq_len=*/256,
                                                /*heads=*/4, /*d_ff=*/512,
                                                /*layers=*/4));

// 2. Session(Model + Optimizer + update state):优化器侧配置的唯一真源
nn::TrainingConfig scfg;
scfg.optimizer      = "adamw";
scfg.lr             = 3e-4f;
scfg.clip_grad_norm = 1.0f;               // 梯度裁剪(0 = 不裁剪)
scfg.grad_accum_steps = 4;
auto session = NN_UNWRAP(nn::TrainingSession::create(std::move(model), scfg));

// 3. 训练配方:从 Session 派生(优化器/精度/lr 同源),再改数据网格与调度
nn::TextTrainConfig recipe = nn::make_text_train_config(session);
recipe.seq_len     = 256;
recipe.batch_size  = 8;                   // 理论 global batch;每 micro = 2
recipe.accum_steps = 4;
recipe.epochs      = 2;
recipe.lr_schedule = "cosine";

// 4. 数据集直连训练工厂:内部建 TextSampleSet 并跑 epoch × step
auto dataset = NN_UNWRAP(nn::Dataset::open("train.nndataset"));
auto trainer = NN_UNWRAP(nn::TextTrainer::create(std::move(session), dataset, recipe));
nn::TextTrainReport report = NN_UNWRAP(trainer.fit());
std::printf("steps=%zu samples=%zu final_lr=%.3e\n",
            report.steps_executed, report.samples_executed,
            static_cast<double>(report.final_lr));

要点:

  • 配方与 Session 的重叠字段必须一致(optimizer/lr/weight_decay/beta1/beta2/ precision/max_norm/accum_steps),TextTrainer::create 会逐项校验并明确报错;用 nn::make_text_train_config(session) 起步最省事。

  • 已切好样本集(如 CLI 先打印数据集统计)时用高性能入口 TextTrainer::create(session, std::move(train_set), std::move(test_set), recipe),避免重复加载/哈希; 样本集由 nn::TextSampleSet::create(dataset, nn::DatasetSubset::Train, {seq_len, stride, pad_id}) 建。

  • 训练进度/断点/探针走 nn::TextTrainerHooks(out/err/on_batch_ready/on_probe_check/ on_update/on_checkpoint/on_phase);断点请求 = nn::TextCheckpointRequest{session, state}, 落盘由调用方决定(CLI 用 save_model 的 CheckpointState)。

  • 单位约定:存储单位 = doc(.nndataset);训练单位 = 样本(seq_len 窗口); batch_size = 一次 update 的理论 global batch,micro_batch_size = batch_size / accum_steps; step = ceil(样本数 / batch_size) 按 optimizer update 计。

  • TextTrainer 目前内置文本 sparse-CE,只服务文本/GPT 任务;MNIST/自定义任务用下面的低层模板。

11.2 低层手写循环(自定义任务 / 非文本)

#include <neuralnet.cpp/nn.hpp>

int main()
{
    auto engine_owner = nn::CpuEngine::create();
    nn::CpuEngine& engine = *engine_owner;

    // 1. Model + Optimizer + Loss(Task Step 回调持有 Loss)
    nn::Model model = NN_UNWRAP(nn::build_mnist_mlp_model(engine));
    nn::TrainingConfig cfg;
    cfg.optimizer = "adam";
    cfg.lr = 1e-3f;
    cfg.grad_accum_steps = 1;
    nn::TrainingSession session =
        NN_UNWRAP(nn::TrainingSession::create(std::move(model), cfg));
    nn::CrossEntropyLoss loss;

    const std::size_t batch = 64;
    for (int step = 0; step < 400; ++step)
    {
        // 2. 取一个 batch:输入 (784, batch),标签 (10, batch)
        nn::Tensor x = engine.create_tensor(784, batch, nn::Precision::F32);
        nn::Tensor y = engine.create_tensor(10,  batch, nn::Precision::F32);
        // ... 用 engine.write(...) / engine.from_matrix(...) 填 x, y ...

        // 3. Session 完成一次 micro-step;累积到边界时自动 update
        nn::TrainingStepOptions opts;
        opts.lr = 1e-3f;
        opts.clip_grad_norm = 1.0f;
        opts.normalization_units = batch;
        auto report = NN_UNWRAP(session.train_step(
            x, opts,
            [&](nn::Model& m, nn::ComputeEngine& e, const nn::Tensor& input)
                -> nn::Result<nn::TrainingStepOutput> {
                auto out = m.forward(input);
                if (!out) return std::unexpected(out.error());
                auto l = loss.forward(e, *out, y);
                if (!l) return std::unexpected(l.error());
                auto grad = loss.backward();
                if (!grad) return std::unexpected(grad.error());
                return nn::TrainingStepOutput{std::move(*grad), batch};
            }));
        if (report.parameters_updated && step % 50 == 0)
            std::printf("[update %d]\n", step);
    }
    return 0;
}

含真实数据加载的完整版见 src/mnist_train.cpp(Session 手写循环)与 src/text_train.cpp (CLI 接线:Dataset → TextSampleSet → Session → TextTrainer),以及 ../usage/02-quickstart-train-infer.md 的「C++ API 编程训练」节。 GPU 录制作用域由 TrainingSession::train_step 统一管理;调用方无需再手工包 begin_batch/end_batch。


12. 模型文件:角色、保存、加载

一份格式、两种角色(model_serialization.hpp,v7):*.nnmodel = 推理角色 (三值层写打包形态),*.nnckpt = 训练断点(写 f32 latent)。文件头 = magic | version | 精度 | role | spec,之后是参数级 kind 条目。

// 保存:role 默认 Model(.nnmodel);断点传 ModelRole::Checkpoint
NN_UNWRAP(nn::save_model("gpt.nnmodel", model, spec));
NN_UNWRAP(nn::save_model("gpt_ckpt.nnckpt", model, spec, /*tokenizer_data=*/{},
                        nn::ModelRole::Checkpoint));

// 只读头:拿架构 spec / 文件角色(不读参数,用于先建对模型)
nn::ModelSpec s1    = NN_UNWRAP(nn::peek_any_model_spec("gpt.nnmodel"));
nn::ModelSpec s2    = NN_UNWRAP(nn::peek_model_spec("gpt.nnmodel"));
nn::ModelRole role  = NN_UNWRAP(nn::peek_model_role("gpt_ckpt.nnckpt"));
std::cout << nn::model_role_name(role) << "\n";

// 加载:先用 spec 建同构模型,再灌参数(tokenizer_out 取内嵌词表)
nn::Model model = NN_UNWRAP(nn::build_gpt_model_from_spec(engine, s1));
std::string embedded_tok;
NN_UNWRAP(nn::load_any_model("gpt.nnmodel", model, &embedded_tok));

// 载入意图(P1-7):推理用默认 Inference(三值层进部署只读态,0.25 B/权重);
// 训练侧(三值模型的 SFT / QAT 续训)传 Trainable —— 打包权重按文件 τ 反量化成
// f32 latent,首次 forward 与文件里的模型逐位一致、可反向、可更新。
NN_UNWRAP(nn::load_model("ternary.nnmodel", model2, nullptr, nullptr,
                         nn::WeightLoadIntent::Trainable));

相关类型:

  • nn::ModelRole:Model(.nnmodel)/ Checkpoint(.nnckpt);nn::model_role_name(r) 取名字。
  • nn::WeightLoadIntent:Inference(默认,部署只读态)/ Trainable(反量化成可训练 latent); 与 ModelRole 正交——角色回答"这份文件是什么",意图回答"载入方要拿它做什么"。.nnckpt 的三值层 写 TernaryLatent,两种意图同结果。
  • nn::ParamKind:Dense / TernaryLatent / TernaryPacked2 / TernaryPacked3 /(预留)I8Packed4; nn::param_kind_name(k) 取名字。形态由拥有该参数的层经 Layer::export_param / import_param 决定,不被接受即 fail-fast(铁律 #14)。TernaryLatent 带 aux(τ)时表示 "反量化 warm start":层按该 τ 播种量化缓存(首次 forward 逐位复现文件里的模型)。

版本兼容:load_model 接受 v4 及以上自描述格式;v5/v6 老 .bin 仍可读 (v6 起内嵌 .nnvocab 词表)。v1–v3 在头校验阶段拒绝并提示重新训练保存。


13. 数据集:nn::Dataset 与生成 CLI

.nndataset 是统一数据集:token 流、词表、loss 掩码、test 子集都在生成期定好并嵌入。

13.1 生成与转换(CLI)

# 先训词表,再打包数据集
./build/tokenizer_train corpus.txt --vocab-size 10000 --output my_bpe.nnvocab
./build/dataset_gen corpus.txt --vocab my_bpe.nnvocab -o llm.nndataset

# 对话 SFT:生成期写 assistant 掩码
./build/dataset_gen dialogs.txt --vocab my_bpe.nnvocab -o sft.nndataset --loss-scope assistant

# 旧 JSON 词表迁移
./build/dataset_convert vocab old.json -o old.nnvocab

13.2 C++ 读取:数据集(存储单位 = doc)

#include <neuralnet.cpp/dataset.hpp>

nn::Dataset ds = NN_UNWRAP(nn::Dataset::open("llm.nndataset"));   // 也接受 .nnvocab(词表独立场景)
const nn::DatasetInfo& info = ds.info();    // kind / num_docs / test_docs / loss_scope / source_*

nn::TextCorpus train = NN_UNWRAP(ds.load_text(nn::DatasetSubset::Train));  // train 子集
nn::TextCorpus test  = NN_UNWRAP(ds.load_text(nn::DatasetSubset::Test));   // test 子集(无则报错)

std::uint64_t n_docs = ds.doc_count(nn::DatasetSubset::Train);   // 存储单位 = doc
nn::TextDoc doc0 = NN_UNWRAP(ds.text_doc(nn::DatasetSubset::Train, 0));  // 单 doc(检查/调试)

const nn::KeyValueRecord& vocab = ds.vocab_kvrec();      // 词表(模型 v6 内嵌词表来源)

nn::TextCorpus { token_flow, doc_ids, loss_mask }——三者都是扁平数组, doc_ids 是每 token 的文档 id(1 起),loss_mask 在 loss_scope=all 时全 1。 Dataset 只读、不做窗口化(存储单位是 doc)。

13.3 训练样本:nn::TextSampleSet(训练单位 = 样本/窗口)

窗口化是训练策略,在 dataset_samples.hpp:TextSampleSet 把子集 corpus 切成 seq_len 窗口(样本),并负责 batch 装配;step 是派生量,不属于数据集格式。

#include <neuralnet.cpp/dataset_samples.hpp>

nn::TextWindowConfig win;
win.seq_len = 256;
win.stride  = 128;          // 0 = 不重叠(stride = seq_len)
win.pad_id  = 0;            // 末窗不足 seq_len 时的填充 id(通常 = tokenizer.pad_id())

nn::TextSampleSet samples =
    NN_UNWRAP(nn::TextSampleSet::create(ds, nn::DatasetSubset::Train, win));

std::size_t n_samples = samples.sample_count();                    // 训练样本数
std::size_t n_steps   = samples.steps_per_epoch(/*global_batch_size=*/8); // update step 数
nn::TextSample s0     = samples.sample(0);   // tokens/doc_ids/loss_mask/valid_len/offset 视图

// 按样本下标装配一个 batch(末批可不满):
nn::TextSampleBatch batch;
std::vector<std::size_t> indices = {0, 1, 2, 3, 4, 5, 6, 7};
std::size_t valid_tokens = NN_UNWRAP(samples.make_batch(indices, batch));
// batch.input = (seq_len, batch);batch.flat_targets / flat_mask / doc_ids 是 batch-major
// (i = b·seq_len + t);valid_tokens = Σ 参与 loss 的位置数

大多数调用方不需要直接操作 TextSampleSet:nn::TextTrainer 在内部建好 train/test 样本集并完成 batch 装配(§11.1)。需要自定义循环时才按上面的接口取样本/装配 batch。


14. 分词器(domain_tokenizer*.hpp)

class nn::Tokenizer {
public:
    [[nodiscard]] std::vector<std::size_t> encode(const std::string& text) const;
    [[nodiscard]] std::string decode(std::span<const std::size_t> ids) const;
    [[nodiscard]] std::size_t vocab_size() const noexcept;
    [[nodiscard]] const std::vector<std::string>& vocab() const noexcept;
    [[nodiscard]] std::size_t bos_id() const noexcept;
    [[nodiscard]] std::size_t eos_id() const noexcept;
    [[nodiscard]] std::size_t pad_id() const noexcept;
    void set_encode_threads(std::size_t n) noexcept;   // 0=自动(全部核心),1=顺序
    [[nodiscard]] nn::Result<void> save(const std::string& path) const;
    [[nodiscard]] nn::Result<void> load(const std::string& path);
    static constexpr std::size_t npos = static_cast<std::size_t>(-1);
};

两种实现:nn::BPETokenizer(字节级,适合英文)与 nn::CharBPETokenizer(字符级,适合中文)。

// 训练一个 BPE
nn::BPETokenizer tok;
nn::BPETokenizer::Config cfg;
cfg.vocab_size    = 10000;
cfg.min_freq      = 2;
cfg.show_progress = true;
cfg.threads       = 0;               // 预分词并行度:0=自动 / 1=顺序 / >1=指定
NN_UNWRAP(tok.train(text, cfg));
NN_UNWRAP(tok.save("my_bpe.nnvocab"));        // .nnvocab(kvrec v2 自描述格式)

// 编码 / 解码
std::vector<std::size_t> ids = tok.encode("Hello world");
std::string s = tok.decode(ids);

// CharBPE 同款 API:nn::CharBPETokenizer(PAD=0 / UNK=1 / BOS=2 / EOS=3)

从文件 / 模型内嵌词表加载:

std::unique_ptr<nn::Tokenizer> t1 = NN_UNWRAP(nn::load_tokenizer_from_file("my_bpe.nnvocab"));
std::unique_ptr<nn::Tokenizer> t2 = NN_UNWRAP(nn::load_tokenizer_from_model_blob(embedded_tok, model_version));
std::unique_ptr<nn::Tokenizer> t3 = NN_UNWRAP(nn::load_tokenizer_from_bytes(bytes));  // 按内容嗅探
std::unique_ptr<nn::Tokenizer> t4 = nn::make_tokenizer("bpe");   // 只造对象、未加载("bpe"/"char_bpe")

旧 JSON 词表不直读,用 dataset_convert vocab 迁移。任意并行度下 encode 输出 与顺序执行逐字节一致(铁律 #8)。


15. 自回归生成(GPTModel / RAPTModel)

nn::GPTModel 与 nn::RAPTModel 各自暴露 generate(带 KV cache,按 seq_len 滑动窗口):

nn::Model model = NN_UNWRAP(nn::build_gpt_model_from_spec(engine, spec));
NN_UNWRAP(nn::load_any_model("gpt.nnmodel", model));

// Model 只有一个顶层层;取出来 downcast
auto* gpt = dynamic_cast<nn::GPTModel*>(&model.layer_at(0));
if (gpt != nullptr)
{
    std::vector<std::size_t> prompt = tok->encode("Once upon a time");
    std::vector<std::size_t> out = NN_UNWRAP(gpt->generate(
        engine, prompt, /*max_new_tokens=*/200, /*temperature=*/0.8,
        /*eos_token_id=*/tok->eos_id(), /*min_new_tokens=*/0));
    std::cout << tok->decode(out) << "\n";
}

签名(旧,纯 temperature):generate(engine, prompt, max_new_tokens, temperature = 1.0, eos_token_id = npos, min_new_tokens = 0)。temperature = 0 为贪心。

采样配置重载(P0-5):generate(engine, prompt, max_new_tokens, const SamplingConfig& cfg, eos_token_id = npos, min_new_tokens = 0):

nn::SamplingConfig cfg;          // 默认 = 旧行为(仅 temperature)
cfg.temperature    = 0.8;
cfg.top_k          = 40;         // 0 = 关闭
cfg.top_p          = 0.9;        // 1.0 = 关闭(核采样)
cfg.repeat_penalty = 1.2;        // 1.0 = 关闭
cfg.repeat_window  = 64;         // 0 = 全部上下文
cfg.seed           = 7;          // has_seed = true ⇒ 同 prompt 输出可复现
cfg.has_seed       = true;
auto out = NN_UNWRAP(gpt->generate(engine, prompt, 200, cfg));

顺序固定:重复惩罚 → 温度 → top-k → top-p(保留集内重新归一化)→ softmax → 采样/贪心。SamplingConfig{}(全默认)与旧的 temperature 重载逐位一致 (sampling_test 锁该等价性:返回下标、概率向量、RNG 消耗序三者相同)。 完整流程(加载模型含内嵌词表 → 编码 → 生成 → 解码)见 src/text_infer.cpp。

多轮对话 prompt 构建(P0-5 尾项):Tokenizer::chat_prompt 把 (system + 交替的 user/assistant 轮次)编成模型可吃的 token 序列 —— 对话词表套 BOS<|system|>…<|end_of_system|><|user|>…<|end_of_user|><|assistant|>… 模板, 末轮是 user 时补 <|assistant|>(待生成);无对话标记的词表退化为轮次文本按换行相接。 超 max_tokens(调用方通常传模型 spec.seq_len)时从最旧的一问一答整轮丢弃 (system 块与末轮恒保留,丢弃后仍以 user 起头)。

// ChatTurn{user, text, tokens}:text 走本词表 encode;tokens 非空则**原样灌入**
// (把上一轮生成的 token 直接回灌,避免 decode → encode 往返漂移)
std::vector<nn::Tokenizer::ChatTurn> history{
    {true,  "你好", {}},
    {false, "",     last_generated_tokens},   // 上一轮的生成结果
    {true,  "再讲一遍", {}},
};
std::size_t dropped = 0;
std::vector<std::size_t> prompt =
    tok->chat_prompt(history, "你是一个有用的AI助手。", spec.seq_len, &dropped);
auto out = NN_UNWRAP(gpt->generate(engine, prompt, 200, cfg, tok->eos_id()));

"上下文不丢"的可验证形式:第 N 轮的 prompt 以第 N-1 轮的 prompt 为 逐 token 前缀(chat_prompt_test 锁这条不变式)。text_infer 的两条多轮路径 都走它:--interactive(进程内累积历史,助手轮用生成的原 token 回灌)与 --turn <role>:<text> + --reply-out <path>(一次一问、历史由调用方自带,GUI 用)。


16. 剖析与诊断

训练 step 剖析器(core_profile.hpp)默认零开销,设 NN_PROFILE=1 打开:

NN_PROFILE=1 ./build/text_train llm.nndataset --epochs 1
NN_PROFILE=1 NN_PROFILE_TRACE=trace.json ./build/mnist_train --epochs 1
// 自己加计时点:name 必须是字符串字面量或静态存储(聚合表按地址做 key,不拷贝)
for (std::size_t li = 0; li < model.num_layers(); ++li)
{
    NN_PROF_SCOPE(nn::prof::interned("fwd.layer", li));   // 动态名经 interned 驻留
    // ...
}
NN_PROF_OP("my_primitive");   // 原语级计时,归到最内层 stage
nn::prof::dump(stdout);       // 进程结束前打表(stage / op / GPU / per-key)

常用环境变量(诊断类只在 -DNN_ENABLE_PROBES=ON 构建下生效;初始化期读一次并缓存的 行为开关是例外):

环境变量 作用
NN_PROFILE=1 / NN_PROFILE_TRACE=<path> / NN_PROFILE_EVERY=<n> host + 设备侧聚合计时 / Chrome trace / 每 n 次 dump 打表
NN_F16_DEBUG / NN_PREC_TRACE / NN_NAN_TRACE / NN_MEM_STATS 精度 / NaN / 内存归因探针(探针门 NN_ENABLE_PROBES)
NN_VULKAN_DEVICE / NN_VULKAN_NO_16BIT_ALU / NN_VULKAN_NO_TIMELINE GPU 设备与特性逃生阀
NN_POOL_BLOCK_MB / NN_POOL_LADDER_MAX_MB / NN_NO_EARLY_REAP / NN_CPU_PAUSE / NN_GPU_PROFILE 内存池 / 调度 A/B

探针门语义(哪些算探针、默认为什么不编译)见 cmake-integration.md。


17. 错误 / 断言宏速查

见 §1.3 的表;两条易混点再强调:

  • NN_UNWRAP = 解包 + 失败终止(不是断言);NN_TRY = 声明 + 传播。
  • 断言后仍会使用被校验对象(解引用 / 索引 / 除零)的点位,一律用 NN_ASSERT_ALWAYS / NN_FATAL(core_assert.hpp,与构建类型无关);NN_ASSERT 是"可选诊断"(NDEBUG 展开为空), 只留给失败也不影响后续正确性的检查。

失败协议要记牢(与 create_tensor 的"无效张量"不同): from_matrix / clone / cast / reshape / 各类运算 / model.forward / backward 返回 Result<T>;*_inplace / write 返回 Result<void>。把字符串解析成数值用 nn::parse_number<T>(s)(返回 Result<T>)。


18. 写自己的层 / 加新原语(去哪读)


19. 工具与周边(CLI 辅助 / 底层工具)

19.1 CLI 辅助(nn::cli,include/neuralnet.cpp/cli/)

写自己的可执行文件时复用这套,与仓库自带 CLI 行为逐字一致:

#include <neuralnet.cpp/cli/cli_engine_factory.hpp>   // EngineConfig / create_engine
#include <neuralnet.cpp/cli/cli_gpu_option.hpp>       // parse_gpu_option
#include <neuralnet.cpp/cli/cli_train_common.hpp>     // TrainCommonArgs / parse_train_common_args
#include <neuralnet.cpp/cli/cli_lr_scheduler.hpp>     // compute_epoch_lr / compute_step_lr
#include <neuralnet.cpp/cli/cli_mnist_io.hpp>         // load_mnist_csv / evaluate_mnist
#include <neuralnet.cpp/cli/cli_help.hpp>             // Help:统一的 --help 排版器
名字 作用
nn::cli::EngineConfig / nn::cli::create_engine(cfg, log) 引擎选择(--gpu 语义;显式请求失败不回退 CPU)
nn::cli::parse_gpu_option(argc, argv, i, allow_name_value) 解析 --gpu / --gpu=<设备>(返回设备选择子或空)
nn::cli::TrainCommonArgs / nn::cli::parse_train_common_args(argc, argv, i, cfg) 解析 --epochs / --lr / --batch-size / --optimizer / --weight-decay / --lr-schedule / --warmup-epochs / --min-lr / --lr-per-epoch / --gpu
nn::cli::LrScheduleConfig / nn::cli::compute_epoch_lr(cfg, epoch) epoch 级调度(手动 lr 表 > cosine > 常数)
nn::cli::StepLrScheduleConfig / nn::cli::compute_step_lr(cfg, step) step 级 warmup + 余弦
nn::cli::load_mnist_csv(path, max_samples) 读 MNIST CSV → (feat (784,N), one-hot 标签 (10,N))
nn::cli::evaluate_mnist(model, engine, x, y_onehot, eval_samples, eval_batch) 全量前向 → argmax 准确率
nn::cli::Help 各 CLI 共用的帮助版式(宽度感知对齐;usage/section/note/text/item/opt 链式填表)

19.2 底层工具

nn::load_text_file(path);          // 读整个文本文件(Result<std::string>)
nn::sha256_hex(data);              // SHA-256 十六进制摘要(数据集 source.sha256 用它)
nn::parse_number<int>("123");      // 字符串 → 数值(Result<T>)
nn::KeyValueRecord rec;            // 自描述键值记录(模型 spec 头 / .nnvocab 的容器)
rec.set("key", uint64_t{1});
rec.set("name", std::string("x"));
uint64_t v = 0;
if (rec.get("key", v)) { /* ... */ }

并行:nn::global_thread_pool() 返回全局 nn::ThreadPool,parallel_for_each(first, last, f) / parallel_for_each_indexed(...) 按安全切分点并行(结果与顺序执行逐字节一致,铁律 #8)。 多数场景不需要直接用它——引擎原语与分词器的并行都在内部完成。


20. 已废弃 / 不存在的 API(避免照着旧代码写)

  • Tensor::reshape() / Tensor::cpu_matrix() / Tensor::from_matrix() —— 已私有, 走 engine.reshape / engine.to_matrix / engine.from_matrix(铁律 #11)。
  • nn::Layer::sub_layers() —— 已删除(层树枚举没有消费者)。
  • 带 engine 形参的旧 Layer::forward(engine, input) —— 现为 forward(input), engine 在 init 时绑定。
  • ensure_gpu —— 不存在(改名即防线),跨设备统一走 engine.import。
  • CLI --export-deploy 与 ZiPT(AttnZip) 相关选项 / nn::*ZiPT* —— 已随库级清理移除。
  • 不支持 CUDA:后端只有 CPU / Vulkan,无 --cuda。