Repository navigation
Release v1.10.0: 生命周期 v3 与训练工厂(P2b)
neuralnet.cpp v1.10.0 Release Notes
⚠️ 勘误(2026-10-23 追加,发布后标注;本版本不撤回)
- 梯度累积归一化缺陷:本版
TextTrainer在--accum-steps > 1时对每个 micro 梯度乘
1/accum_steps(“micro 均值之均值”);当 loss_mask 使各 micro 有效 token 数不同或末批不满时,
训练目标会偏离理论 global batch。默认--accum-steps 1路径不受影响。修复随 v1.11.0 发布
(按gradient_units / normalization_units精确归一化,见v1.11.0.md§3)。--batch-size/ step 计数语义在 v1.11.0 变更:batch_size从 micro-batch 改为 global batch
(micro = batch / accum,必须整除),--log-interval/--save-interval/--max-steps/ lr 调度
统一按 optimizer update 计。升级脚本请按v1.11.0.md§4.2 换算
(旧--batch-size 8 --accum-steps 4= 有效 batch 32 → 新--batch-size 32 --accum-steps 4)。- 断点兼容:v1.10.0 写出的
.nnckpt可在 v1.11.0 直接--resume(缺语义键按 legacy micro-batch
自动折算);反向(新断点回旧二进制)语义不同,回退前请保留原文件。- 本版本 tag 与产物保持不变(已发布版本不重打);新用户请直接使用 v1.11.0。
发布范围:
v1.9.0(795886f,2026-10-18)→v1.10.0(tagv1.10.0,2026-10-21;代码基线a9c6c6f+ 发布审查修复)。
主题:对象生命周期 v3(设备域模型) +TrainingSession训练生命周期容器 + 训练样本层与训练工厂TextTrainer+GpuBackend确定性回收。
兼容性:有破坏性变更(API 层)——Tensor不再保存引擎身份(engine_id()/bound()删除);ComputeEngine::id()、GpuBackend::instance()、NN_BIND_DEBUG删除;GPU 引擎默认拒绝 CPU 张量操作数;Dataset::load_text(bool)→load_text(DatasetSubset);DatasetIdentity.num_windows→num_samples;CLI 不再实现训练循环。模型文件格式 v8 与project VERSION 1.10.0不变。迁移见 §6。
版本关系:正式版汇总两个预发布候选——v1.10.0-rc0(生命周期 v3 +TrainingSessionP2a)与v1.10.0-rc0.1(P2b 训练工厂);rc0.1 之后未再引入新的功能变更,正式版收口独立发布审查发现的 P1-1 / P2-1 / P2-2(§2.5)并补齐发布验收与文档口径。
0. 亮点速览(TL;DR)
| 方向 | 变化 | 收益 |
|---|---|---|
| 生命周期 v3 | 删除 8 B 引擎身份字段(EngineId)与整套传播机制(stamp_ / adopt / bound / NN_BIND_DEBUG);设备域从存储派生(CPU → kHostDomain,GPU → backend 标识,空 → 0) |
减少一个概念:张量不需要知道"谁创建了我",只需要知道"现在在哪块设备上";机制点 101 → 42、净代码约 −120 行;sizeof(Tensor) 64 → 56 B(GPU 构建 88 → 80 B) |
| 入口检查 | 每个 public NVI 入口逐个操作数比较设备域(旧 bind_check_ 只比较操作数之间,单操作数入口可让外来 GPU 张量进入任意引擎) |
修复 D1:跨 backend 张量不再静默别名/UB,而是带 source_location + 形状 + 迁移提示的 Result 错误 |
| CPU/GPU 统一语义 | GPU 引擎拒绝 CPU 张量操作数(显式 import / to_matrix 是唯一跨域通道);删除 GPU impl 的 CPU 回退分支 |
推理/训练不再有"隐式上传"的语义分歧;CPU 与 GPU 走同一条域规则 |
GpuBackend 回收 |
shared() 改 weak 缓存 + mutex;删除 instance()(16 处调用点迁移) |
最后一个使用者释放后 Vulkan 资源确定性回收;再次 shared() 重建(不自动初始化设备) |
| 生命周期缺口 | activation offload 状态机 fail-fast + discard() 作废通道 + f16/t1_58 组合配置期拒绝;ExprSegment 改持 shared_ptr;GPU end_expr 无录制明确报错 |
推理/评估路径"未配对 backward"不再静默复用上一轮 slab(D4);录制段作用域内释放引擎句柄不再 UAF(D6) |
TrainingSession(P2a) |
新头 training_session.hpp:拥有 Model + Optimizer + TrainState,成员声明顺序保证 optimizer 先析构 |
D3 的结构性修复:Optimizer::params_/grads_ 的非拥有 TensorRef 永远有效,UAF 结构性地不可能发生 |
| 训练样本层(P2b) | 数据集存储单位 = doc;训练单位 = sample(seq_len 窗口);step = ceil(样本数 / batch_size) 是派生量 |
"样本"不再与"step"混用;数据集/断点身份记样本数,训练步数由 batch 派生 |
训练工厂 TextTrainer |
create(TrainingSession, Dataset, TextTrainConfig) → fit():切窗、epoch×step、累积/裁剪、lr 调度、评估、断点状态装配全在库层 |
训练入口从数百行手工接线降到"打开数据集 + 建 Session + create + fit";算法循环可测试、可被库外复用 |
| 配方单一真源 | nn::TextTrainConfig(库层)+ nn::make_text_train_config(session);CLI TrainConfig 继承之;lr_scheduler.hpp 上收库层 |
不再出现 CLI 与训练工厂两份默认值各自漂移;不一致在 TextTrainer::create 逐项 fail-fast |
1. 对象生命周期 v3:设备域模型
1.1 目标模型
Tensor 只拥有存储:
CPU 存储 → 域 = kHostDomain(进程常量;CPU 引擎无状态、共享宿主内存)
GPU 存储 → 域 = 其 GpuBuffer 的 backend 标识(buffer 持 shared_ptr<GpuBackend>
keepalive ⇒ 张量存活期内地址不被新 backend 复用)
空/无效 → 0(唯一豁免,交由各入口既有的无效/形状检查拒绝)
ComputeEngine 在派生类构造时写入本引擎域(非虚函数,虚表口径 52 方法不变);
每个 public 入口逐个操作数比较 t.device_domain_() == domain_。
import 是唯一跨域入口:CPU → GPU 上载;同域 = 别名/cast;GPU 异 backend 或
GPU → CPU(应走 to_matrix)= 明确错误。
1.2 入口检查与失败语义
| 场景 | v1.10.0 行为 |
|---|---|
| 张量活过引擎(含 GPU) | 安全(GPU 由存储 keepalive 保证) |
| 跨 CPU 引擎使用张量 | 允许(同一宿主域;CPU 引擎无状态) |
| GPU 张量进入不同 backend 的引擎 | Result 错误:跨 GPU backend,提示先 to_matrix 落宿主 |
| CPU 张量进入 GPU 引擎 op | Result 错误:提示先 engine.import(t) |
| GPU 张量进入 CPU 引擎 op | Result 错误:提示先 engine.to_matrix(t) |
跨 backend import |
Result 错误(提示先落 CPU) |
| 空张量参与运算 | 域检查跳过(0 豁免),由既有无效/形状检查拒绝 |
错误信息带 source_location、张量形状与两侧域值,不静默。
1.3 删除清单
include/neuralnet.cpp/core_engine_id.hpp与引擎租约头(core_engine_lease.hpp)整体删除;EngineId、ComputeEngine::id()、Tensor::engine_id()、Tensor::bound();ComputeEngine的stamp_/adopt出生绑定通道(及nn::dslfriend);NN_BIND_DEBUG行为开关与其 CI 第二轮 ctest(域检查现已是默认入口检查);- GPU impl 的 CPU 回退分支(
cast/copy_from/clone/copy_into/cast_into); GpuBackend::instance()(见 §2.1)。
新增 core_domain.hpp:using DomainId = std::uintptr_t;、inline constexpr DomainId kHostDomain = 1;、
ComputeEngine(DomainId) 保护构造与只读 domain()。
1.4 结构收益与数值不变量
| 项 | v1.9.0 | v1.10.0 |
|---|---|---|
sizeof(Tensor)(CPU / GPU 构建) |
64 B / 88 B | 56 B / 80 B |
| Tensor 拷贝 | 一次 8 B 身份复制 + 存储原子对 | 少一次 8 B 复制(原子行为不变) |
| 机制点(ID / stamp / adopt / bind / 调试开关) | 101 | 42(只留入口域检查调用点) |
| 净代码 | — | 约 −120 行 |
预期数值路径零改动:生命周期 v3 不改变任何计算表达式;CPU/GPU 字节锚必须逐位不变,
由 expr_fuse_test(融合 vs 未融合逐位一致、批量 vs 逐个 memcmp)、fused_gpu_test、
expr_fold_test 覆盖。scan_bitwise_test 仍以 Linux + 850M 为逐位权威口径。
2. GpuBackend 确定性回收与生命周期缺口收口
2.1 GpuBackend::shared() 弱缓存
static std::shared_ptr<GpuBackend> shared(); // weak 缓存 + mutex;全部释放后回收
static std::shared_ptr<GpuBackend> create(); // 独立实例(测试/多设备)
// instance() 已删除(16 处调用点迁移为持有 shared() 的 shared_ptr)行为:有活使用者时复用同一实例;最后一个使用者释放后 backend 与 Vulkan 资源按序析构;
下一次 shared() 重新创建(不自动初始化设备,调用方仍需 initialize())。
GpuBuffer 的 backend keepalive 保证"张量存活期内 backend 不析构、地址不复用"。
回归测试锁"全部释放 → 再 shared() 重建"。
2.2 activation offload 状态机(D4)
export_activations在offloaded_已为真时 fail-fast,不再静默return {};- forward 入口新增
discard()作废通道,推理/评估路径"未配对 backward"不会导入上一轮 slab; --activation-offload与--f16/--t1_58/--precision-*在配置期拒绝(slab 恒 f32,最短正确路径);- CPU 引擎的 offload 在 CLI / Session / 库三层 fail-fast(CPU offload 原语是 no-op,开启会破坏 backward)。
2.3 ExprSegment 录制段生命周期(D6)
ExprSegment 改持 std::shared_ptr<ComputeEngine>:作用域内释放最后一个外部引擎句柄不再 UAF;
GPU end_expr 在无录制时返回明确错误(跨线程 begin/end 从静默 no-op 变为可诊断错误)。
2.4 评审收口(rc0 之后)
- D1 收口:
GpuEngine::to_matrix拒绝异 backend 张量(v3 前会读外来VkBuffer= UB),
只接受本引擎域 GPU 张量与宿主域 CPU 张量; offload_restore补设备域检查;CPU offload 在库层 fail-fast;- 生命周期负例测试
lifetime_contract_test(CPU 可跑)随 P0 修复落地。
2.5 正式版前的独立发布审查修复(2026-10-21)
发布前对 v1.9.0..HEAD 做了独立代码审查(结论 P0 = 0 / P1 = 1 / P2 = 9),正式版收口了其中 3 项:
| 项 | 问题 | 修复 |
|---|---|---|
| P1-1 | TextTrainer::create 不校验"配方 seq_len ↔ 模型 seq_len":库外误配时 GPT 的 forward 用 input.rows()、位置编码/掩码却按模型 seq_len 构建(GPU 行 gather 无越界守卫 ⇒ 越界读),backward 又固定用 seq_len_(形状错配) |
create_impl 新增 fail-fast:模型带 ModelSpec 且 spec.seq_len 与配方不一致即拒绝;回归用例进 text_trainer_test [6] |
| P2-1 | --resume + --activation-offload 会绕过配置期拒绝:adopt 用断点状态段覆盖 cfg.precision 发生在组合校验之后,只能到首个 block 才由库层 fail-fast |
组合校验抽为 nn::cli::precision_offload_combo_error(cfg)(返回错误文案),argparse 结束与 adopt_resume_state 之后各校验一次;argparse 层 exit(1)、run_text_train 经 NN_FAIL 走 Result(保持三模块不终止进程) |
| P2-2 | 空 test 子集(合法文件:test 全为空 doc)不再打印 样本数: 0,与 rc0.1 承诺及 v1.9.0 输出锚不符 |
CLI 保持打印测试集概况行(含 assistant 掩码行)后跳过评估,训练继续 |
其余 P2(fit() 错误路径 begin_batch 无 RAII、hooks 错误路径未清空、offload 导出中途失败的状态残留、
基类 copy_into/cast_into 缺域检查、TrainState.global_step 注释语义等)记录为后续项,不阻塞本版发布。
3. TrainingSession(P2a)
3.1 用法
新头文件 include/neuralnet.cpp/training_session.hpp(nn.hpp / train.hpp 已转发):
nn::TrainingConfig cfg;
cfg.optimizer = "adamw"; cfg.lr = 1e-3f; cfg.weight_decay = 0.01f;
cfg.clip_grad_norm = 1.0f; // 0 = 不裁剪
auto session = NN_UNWRAP(nn::TrainingSession::create(std::move(model), cfg));
NN_UNWRAP(session.zero_grad());
auto logits = NN_UNWRAP(session.model().forward(x));
/* ... loss / backward(由调用方组合;Loss 不归 Session)... */
NN_UNWRAP(session.clip_grads());
NN_UNWRAP(session.step()); // optimizer.step() + step_count++TrainingConfig 字段 = optimizer / lr / weight_decay / precision / beta1 / beta2 /
clip_grad_norm / activation_offload / checkpoint_every;模型侧训练期开关在构造优化器前应用。
3.2 所有权不变式
model_声明在opt_之前 ⇒ optimizer 先于 Model 析构(TensorRef永不悬空);- Session 不可拷贝、可移动(Layer 对象在堆上,移动后
TensorRef仍有效); Model保持纯值对象——推理可直接用Model,不需要 Session;- Loss / 数据集 / epoch / CLI / 日志不归 Session(防 god object,见
../todo/29-session-architecture.md§4); Session反查引擎走session.engine()(=model().engine()),不持裸引擎引用。
3.3 自由工厂保留
create_optimizer(name, engine, params, grads, ...) 保留:调用方必须保证
"optimizer 在其参数所有者存活期内销毁";需要结构保证时用 TrainingSession。
其签名(unique_ptr / nullptr)本轮不收口,见 §9.3。
4. 训练样本层与训练工厂 TextTrainer(P2b)
4.1 样本概念(dataset_samples.hpp)
存储单位 = doc(.nndataset 里一行文本)
训练单位 = sample(一个 seq_len 滑动窗口,可跨 doc)
step = ceil(sample_count / batch_size) ← 派生量,不写进数据集格式
nn::DatasetSubset{Train, Test}:Dataset::load_text(subset)、doc_count(subset)、
text_doc(subset, index)(存储单位访问);nn::TextWindowConfig{seq_len, stride, pad_id}:窗口化策略(训练策略,不进文件格式);nn::TextSample:样本视图(tokens / doc_ids / loss_mask / valid_len / offset);nn::TextSampleSet::create(dataset, subset, cfg):一次完成切窗 + 元数据;
sample_count()、sample(i)、steps_per_epoch(batch_size)、make_batch(indices, out);nn::TextSampleBatch:input矩阵(seq_len, batch)+ batch-major 扁平
flat_targets / flat_mask / doc_ids+valid_tokens。
逐位约定(与旧 CLI 循环一致,dataset_samples_test 锁住):
x[t] = token_flow[offset+t],y[t] = token_flow[offset+t+1](目标 = 下一 token);- 位置参与 loss ⇔
t+1 < valid_len && loss_mask[offset+t+1](mask 按目标位置); - PAD 位置
x/y = pad_id、flat_mask = 0、doc_ids = 0;末批可不满。
4.2 训练工厂
auto dataset = NN_UNWRAP(nn::Dataset::open("train.nndataset"));
auto session = NN_UNWRAP(nn::TrainingSession::create(std::move(model), session_cfg));
nn::TextTrainConfig recipe = nn::make_text_train_config(session); // 推荐起步
recipe.seq_len = 128; recipe.batch_size = 8; recipe.epochs = 2;
auto trainer = NN_UNWRAP(nn::TextTrainer::create(std::move(session), dataset, recipe));
auto report = NN_UNWRAP(trainer.fit()); // 返回 TextTrainReportTextTrainer拥有:训练/测试TextSampleSet、CrossEntropyLoss(precision = 配方)、
shuffle、梯度累积/裁剪、lr 调度(epoch / step_cosine)、非阻塞 loss 回读、测试评估、
断点状态装配。- 配方单一真源(P0 校验):优化器侧字段的真源是
TrainingSession的TrainingConfig
(optimizer/lr/weight_decay/beta1/beta2/precision/clip_grad_norm);
TextTrainConfig中的对应副本(optimizer_name及同名lr/weight_decay/beta1/
beta2/precision/max_norm)必须与之完全一致,否则createfail-fast(防"实际用 Session
的 lr、日志/断点写配方 lr"的静默错训)。推荐起步 =nn::make_text_train_config(session)。 - 其余前置校验:
batch_size>0、accum_steps>0、log_interval>0(0 会在进度打印时除零)、
epochs>=0、start_epoch>=0且<=epochs、start_step<steps、训练样本非空、窗口网格/词表 pad 一致。 - 编排不归 Trainer:模型构建/恢复/产物保存/CLI 文案(
TextTrainerHooks:out/err、
on_batch_ready、on_probe_check、on_update、on_checkpoint、on_phase)。
hooks 的契约 = 只活到fit()返回(fit结束时清空,避免捕获的局部引用被下一次调用复用)。 - 高性能入口:调用方已切好样本集时用
create(session, train_set, std::optional<TextSampleSet> test, cfg, hooks)
(CLI 走这条,避免同一数据集重复加载/哈希)。 - 结果:
TextTrainReport{seconds, elapsed_ns, steps_executed, samples_executed, ckpt_failures, abort_code, final_lr, final_epoch, final_step_in_epoch}。
行为语义(与旧 CLI 对齐,已写进 text_trainer_test / dataset_samples_test):
- 空 test 子集(合法文件:test 全为空 doc)= 无 test ⇒ 打印
样本数: 0并跳过评估,训练继续; - 测试评估失败(GPU 瞬时错误等)⇒ 打印到
hooks.err后放弃本轮剩余 test batch,训练继续并照常保存最终.nnmodel; --max-steps/start_step提前停 ⇒ Session 的TrainState记录真实下一步位置,不写(epochs,0);
探针中止时不更新 Session 状态(TextTrainReport.final_epoch / final_step_in_epoch仍报告实际位置)。
4.3 TrainingSession 持有 TrainState
TrainingSession新增train_state() / set_train_state()(写入方 = 训练工厂;Session 只负责生命周期);nn::make_text_train_state(cfg, identity, steps_per_epoch, epoch, step_done, lr, optimizer_step)
是状态装配唯一实现(计数器 epoch 边界归一化、global_step、配方、数据集身份、优化器t);- 断点状态段:
DatasetIdentity.num_windows → num_samples,落盘键dataset_windows → dataset_samples;
读盘时旧键回退,写出一律用新键;.nnckpt的 v8 文件版本与格式不变; dataset_mismatch_reason文案改为"样本数"(文档数 / 样本数 / 每 epoch 步数三元组)。
4.4 CLI 迁移
| 入口 | 变化 |
|---|---|
text_train |
Dataset → TextSampleSet(train/test)→ 模型构建/resume → TrainingSession → TextTrainer;text_train.cpp 不再手工拼 token 流/切窗/填 batch;cli/text_train_loop.hpp 只剩 CLI hooks(--grad-log、NN_NAN_TRACE、NN_MEM_STATS、定期 .nnckpt 写盘) |
mnist_train |
自由工厂 create_optimizer + unique_ptr<Optimizer> → TrainingSession(Model + Optimizer 同生共死);训练/评估/保存逻辑与输出文案不变 |
--resume |
语义不变:状态段仍是权威(形状/配方/计数器),数据集身份门禁按 num_samples / steps_per_epoch 比较;rc0 旧断点可读 |
4.5 新增测试
dataset_samples_test:样本概念(doc vs sample vs step)、切窗/样本视图、batch 逐位、
assistant 掩码、错误语义、断点旧键兼容;text_trainer_test:数据集直连工厂、fit()端到端、TrainState装配(含
--max-steps终态)、断点、高性能样本集入口 +nullopttest、前置条件
(batch_size>0/log_interval>0/ 配方与 Session 逐项一致);training_session_test增TrainState用例;lifetime_contract_test(P0 负例,CPU 可跑)。
5. 配方/调度上收与文档同步
nn::TextTrainConfig(text_train_config.hpp)= 数据网格/调度/记录配方(库层);
优化器侧字段与 Session 同源(见 §4.2 的 P0 校验);- CLI
TrainConfig : nn::TextTrainConfig,只多入口字段(数据集/保存/恢复/模型结构/GPU/flush/checkpoint/offload); lr_scheduler.hpp上收库层(nn::LrScheduleConfig/nn::StepLrScheduleConfig/
nn::compute_epoch_lr/nn::compute_step_lr);cli/cli_lr_scheduler.hpp变转发头(nn::cli::*旧名保留);- 文档:
docs/development/09-engine-ownership.md重写为生命周期 v3 唯一真源、
08-object-lifetimes.md降为转发;cpp-api-manual/usage/02/usage/03/AGENTS.md训练范式与迁移示例同步。
6. 破坏性变更与迁移
6.1 生命周期 / 引擎层(v1.9.0 → v1.10.0)
| 变更 | 旧 → 新 |
|---|---|
Tensor 不再保存引擎身份 |
t.engine_id() / t.bound() 删除。不要从张量反查引擎;"能否在本引擎用"看操作返回的 Result |
| 引擎身份诊断 | engine->id() → engine->domain()(DomainId) |
f16 回退路径里的 adopt / stamp_ |
删除(内部机制;不再有回退路径需要补 stamp) |
NN_BIND_DEBUG=1 ctest |
删除;域检查是默认入口检查,CI 第二轮 ctest 一并删除 |
GpuBackend::instance() |
auto owner = nn::GpuBackend::shared(); nn::GpuBackend& backend = *owner;(owner 必须活到使用者结束) |
| GPU 引擎隐式接收 CPU 张量 | auto t = NN_UNWRAP(gpu_engine.import(cpu_tensor)); 再调用 |
| CPU 引擎消费 GPU 张量 | auto m = NN_UNWRAP(gpu_engine.to_matrix(gpu_tensor));(to_matrix 是 GPU→CPU 的正常出口) |
跨 backend 的 import / op |
明确 Result 错误;先 to_matrix 落宿主(跨 backend 传张量不再是 UB,而是报错) |
ExprSegment seg(engine)(裸引用) |
nn::ExprSegment seg(engine_owner)(持 shared_ptr<ComputeEngine>;录制段内引擎不会析构) |
自定义 ComputeEngine 子类构造 |
ComputeEngine() → ComputeEngine(DomainId):CPU 传 kHostDomain,GPU 传 backend->identity() |
GpuEngine::to_matrix(跨 backend 张量) |
明确 Result 错误;to_matrix 只接受本引擎域 GPU 张量与宿主域 CPU 张量 |
Model 未绑定引擎的 abort 路径 |
新增只读 Model::engine_bound();TrainingSession::create 返回 Result 错误 |
| 简化"优化器先于 Model 析构"的手工保证 | 推荐 nn::TrainingSession(Model + Optimizer + TrainState 同生共死);自由工厂仍可用,契约不变并在文档写明 |
sizeof(Tensor) 64 / 88 B |
56 / 80 B(只影响按字节布局编译的代码;无源码改动) |
6.2 数据集 / 训练 API 层(v1.9.0 → v1.10.0)
| 变更 | 旧 → 新 |
|---|---|
| 数据集子集选择 | dataset.load_text(false/true) → dataset.load_text(nn::DatasetSubset::Train / ::Test) |
| 存储单位访问 | 只有 dataset.info().num_docs → 另加 doc_count(subset) / text_doc(subset, i) |
| 手工切窗 | 手工 window_offsets + 逐 step 填 Matrix → TextSampleSet::create(dataset, subset, {seq_len, stride, pad_id}) + make_batch(indices, batch) |
| "样本数"来源 | window_offsets.size()(散落在 CLI)→ sample_count();steps_per_epoch(batch_size) 是派生量 |
| 训练循环位置 | CLI 内的 epoch×step 循环 → nn::TextTrainer::fit()(库层),CLI 只装 hooks |
| 优化器与状态装配 | 手工 create_optimizer + 手工保证析构顺序 → nn::TrainingSession(Model + Optimizer + TrainState) |
| 训练状态装配 | nn::cli::make_train_state(...)(CLI 头)→ nn::make_text_train_state(...)(库层唯一实现) |
| lr 调度 | nn::cli::LrScheduleConfig / nn::cli::compute_epoch_lr → nn::LrScheduleConfig / nn::compute_epoch_lr(nn::cli::* 旧名转发保留,旧 include 可用) |
| CLI 配方结构 | TrainConfig 自带全部配方字段 → TrainConfig : nn::TextTrainConfig(配方继承,防两份默认值漂移) |
| 断点身份字段 | DatasetIdentity::num_windows / 键 dataset_windows → num_samples / 键 dataset_samples(旧键兼容读,新键单向) |
6.3 文件与断点兼容
- 模型/断点文件格式仍是 v8:v1.10.0 可读 v5 / v6 / v7 / v8;写出一律为 v8。
- v1.10.0 可读 rc0 与 rc0.1 两个预发布候选产生的
.nnckpt(rc0 旧键dataset_windows走回退读)。 - 反向不成立:rc0 二进制读不到 rc0.1 / 正式版写出的
dataset_samples键(会得 0 ⇒ 数据集身份对比失败);
升级方向(rc0 → v1.10.0)完全可读。需要回退旧二进制时保留升级前的原文件。 .nnmodelwarm start 与.nnckpt续训语义不变;--resume的禁列旗标规则不变。
7. 验收基线
口径:
NN_ENABLE_NATIVE=OFF可移植基线;GPU = NVIDIA CMP 40HX / Vulkan 1.4.351 / 驱动 616.92;
最终源码 =a9c6c6f+ §2.5 的 3 项发布审查修复。构建目录均为全新构建(build/review/*)。
逐项命令、日志与报告索引见../todo/25-release-requirements.md§7。
| # | 验收项 | 结果 |
|---|---|---|
| 1 | Release + GPU ctest(Windows clang 23.1 + MSVC STL / Vulkan) | 59 注册 → 58 pass + 1 skip + 0 fail(229.52 s,最终源码;skip = scan_bitwise_test 平台口径);全新构建 219/219 步 0 warning(782.2 s),最终源码增量 6/6 步 |
| 2 | Debug + GPU ctest(同上,Debug) | 59 → 58 pass + 1 skip + 0 fail(547.12 s,最终源码);rapt_test_f16 Passed(复跑 3 次全通过);Debug 迭代器断言零触发 |
| 3 | CPU ctest(Release + NN_ENABLE_GPU=OFF) |
WSL g++ 15.2 + libstdc++:57 → 39 pass + 18 skip + 0 fail(13.95 s);clang 24 + libc++ 同口径一致 |
| 4 | MSVC(cl.exe 19.51 + MSVC STL,Debug CPU-only) | 57 → 37 pass + 20 skip + 0 fail(236.62 s);CI 口径(-E "^rapt_test_f16$")56 → 36 pass + 20 skip + 0 fail(196.57 s) |
| 5 | clang 24 + libc++(CPU-only,WSL) | 57 → 39 pass + 18 skip + 0 fail(13.57 s);ldd 确认链 libc++.so.1 + libc++abi.so.1 |
| 6 | g++ 15.2 + libstdc++(CPU-only,WSL) | 57 → 39 pass + 18 skip + 0 fail(19.96 s);链 libstdc++.so.6 |
| 7 | 端到端训练(真实 tinystories 子集 20k docs / 4.24M tokens,GPT d64/h4/L2/ff128/seq64/b8,各 20 step,GPU) | f32 9.0211 → 7.7263;--f16 9.0319 → 7.6534;--t1_58 9.0319 → 8.1533;均无 NaN/冻结、权重确实移动;.nnckpt + --resume 续 5 步 loss 7.5669 → 7.2324(优化器状态 t=20 恢复、数据集身份校验通过) |
| 8 | 文档审计 | bench/doc_align_audit.ps1 / bench/doc_inventory.ps1 / bench/gui_cli_audit.py / bench/audit_dangling.ps1 均 exit 0;doc_inventory 为 virtual 52/52、L2-VIOLATIONS: 0、HANDWRITTEN-RESIDUE: 0、TERMINATING-CALLS: 0;doc_align 的 [A]/[D]/[E]/[F] 可行动 0;gui_cli_audit 七节 PASS |
说明:
scan_bitwise_test在 Windows / Debug 树按既有平台口径主动 skip(逐位锚以 Linux + GTX 850M
为权威,Windows/40HX 的 FMA/舍入路径不同);MSVC 的 20 个 skip = 17 GPU 依赖 + 1 Vulkan 探针 +
2 个 app 发现类用例(VS 多配置下 app exe 位于<build>/Debug/,见 §9.12)。
layer_bench配对 A/B 未重跑(本版无 kernel 数值路径改动)。
8. 提交索引
区间
v1.9.0..v1.10.0含 16 个历史提交(代码基线a9c6c6f):下表 14 个主题提交 + 2 个构建配置提交
(98c7515/d2f0a42,均只改CMakeLists.txt;d2f0a42为 PR #21 合并提交)。
正式版另含 2 个发布提交(发布审查修复 + 发布文档/版本对齐,tagv1.10.0),不在此表。
| 主题 | 提交 |
|---|---|
| 生命周期 v3 / Session 层立项与设计(docs 28、29) | 4eadc27 |
生命周期 P0 修复(D4 offload / D6 ExprSegment)+ 文档假承诺纠正(D3/D5) |
fa8961a / 7b6feb5 / d559aee |
P0 契约负例测试 lifetime_contract_test(CPU 可跑) |
7bbe792 |
生命周期 v3 核心:设备域模型、删除 EngineId / stamp_ / adopt / bound / NN_BIND_DEBUG |
d092c0e |
GpuBackend::shared() 弱缓存 + 删除 instance()(D5 / P3) |
f7b1f42 |
D1 收口:GpuEngine::to_matrix 拒绝异 backend 张量 |
f383f08 |
TrainingSession(P2a)+ 版本 1.10.0 |
c30617d |
v1.10.0-rc0 发布文档与迁移表 |
f0b8a07 |
生命周期评审收口:CPU offload fail-fast、offload_restore 域检查、迁移文档 |
a176d8e |
P2b:训练样本层 + TextTrainer 训练工厂 + text_train 迁移 |
1a97720 |
mnist_train 迁移到 TrainingSession |
bc73a17 |
v1.10.0-rc0.1 发布文档与全量文档同步 |
a9c6c6f |
| 构建配置(区间内另计,非功能提交) | 98c7515 / d2f0a42 |
完整变更:git log v1.9.0..v1.10.0 / git diff v1.9.0..v1.10.0。
9. 已知限制与仍 Open
- P2-4 流式 DataLoader 仍未做:
TextSampleSet仍把子集整段物化到TextCorpus
(token_flow / doc_ids / loss_mask),超大语料的内存曲线问题与 rc0/rc0.1 相同;
流式分块迭代(for_each_chunk)留后续(../todo/18-roadmap.mdP2-4 /../todo/19-unified-dataset.md)。 InferenceSession/RLTrainingSession仍为占位(../todo/29-session-architecture.md§6/§7),
未实现;触发条件 = 真实调用方出现(推理服务化 / RL 训练入口)。create_optimizer自由工厂签名未收口(D7):仍返回unique_ptr/nullptr,
失败不是Result;改名/改返回类型属破坏性变更,留待下一个破坏性窗口。
Session 已覆盖训练路径,CLI 不再直接使用自由工厂。TextTrainer目前只服务文本 / GPT 任务(内置 sparse-CE);MNIST / 自定义任务的通用
Step 协议未抽象。触发条件 = 出现第二类需要同一训练循环的任务。- 断点旧键兼容是单向的:v1.10.0 写
dataset_samples,rc0 二进制读不到(得 0 ⇒ 数据集身份对比失败);
升级方向(rc0 → v1.10.0)完全可读。v8 文件版本号未变。 - C11:RAPT f16 冒烟的 NaN 历史问题(根因仍未定位)。历史上在 CPU-only Debug 复现过
(rapt_test --f16step 7 → step 8 NaN),故 CI 的msvc-debugjob 仍保留既有
-E "^rapt_test_f16$"排除(避免概率性红)。v1.10.0 发布复验(2026-10-21)四棵树均未复现:
Debug+GPU 完整 ctest 与 MSVC Debug CPU-only 完整 ctest 中rapt_test_f16均 Passed
(MSVC 累计 5/5 通过)⇒ 本轮 Debug 完整 ctest 全绿(未用豁免)。 - 矩阵口径(2026-10-21 正式版实跑):Release+GPU / Debug+GPU(Windows clang 23.1 + MSVC STL)、
MSVC cl 19.51 Debug CPU-only、WSL clang 24 + libc++、WSL g++ 15.2 + libstdc++ 均全新构建 + 完整 ctest;
GPU = NVIDIA CMP 40HX / Vulkan 1.4.351 / 驱动 616.92。真实数据集端到端训练与断点续训
使用最终源码二进制实跑。详见 §7 与../todo/25-release-requirements.md§7。
layer_bench配对 A/B 未做系统性回归对比(本版无 kernel 数值路径改动);最终源码上做了单次
功能冒烟:GPUmatmul4.365 ms / 491.9 GFLOPS(hashcbf7d4478bd536ab)、matmul_q0.796 ms /
2697.2 GFLOPS(hashc5860cfcfcb371f9),CPU 512³ 小规模两组哈希齐全(计时受并发负载影响,仅作参考)。 - activation offload 与混合精度/三值:本轮选择配置期拒绝(slab 恒 f32,最短正确路径);
slab 按存储精度记账的"精度协议化"留作后续立项。offload + checkpoint 混合模式仍无独立回归用例
(只有各自单模式测试)。 mnist_train的优化器侧训练状态未接入.nnckpt(mnist 路径本来就没有优化器状态段);
文本路径的断点语义不变。- D9 洁癖项(低风险):
Layer::BindingState三态化、nn::TensorRef与nn::dsl::TensorRef
重名未处理;不阻塞本版发布。 TextTrainer的 seq_len 校验只在模型带ModelSpec时生效(发布审查 P1-1 修复的边界):
工厂用build_*_from_spec/ 文件加载路径时Model::spec()有值,create会 fail-fast 拒绝
配方 seq_len 与模型 seq_len 不一致;手工build_gpt_model(...)不写 spec 的模型无法在该校验点
检测(后续给Layer::sequence_length()只读查询后补全覆盖)。- MSVC Debug 下两个 app 发现类用例按 77 skip:
cli_precision_warning_test/chat_prompt_test
在 VS 多配置生成器下 app exe 位于<build>/Debug/,用例的三条候选路径找不到 ⇒ skip(非失败)。
该行为对 CIwindows-latest的 MSVC Debug job 同样成立 ⇒ 这两个用例在 MSVC Debug 下实际不执行;
已记录为已知覆盖缺口(Release/CPU 树照常执行)。 - P2-1 的
run_text_train错误上抛分支无直接 ctest 覆盖:argparse 层的配置期拒绝有手工冒烟
(--activation-offload --f16 --gpu⇒ exit 1 + 原文案),但--resume之后走NN_FAIL的分支
只有间接覆盖(cli_precision_warning_test在 MSVC Debug 下 skip,本机 CPU-only 无 offload 场景)。
后续可把precision_offload_combo_error()(纯函数、不依赖 GPU)单测加进text_train_modules_test。
完整台账见 ../todo/18-roadmap.md 与 ../todo/00-code-review-remediation.md。