Skip to content

Release v1.10.0: 生命周期 v3 与训练工厂(P2b)

Choose a tag to compare

@EthanPeng-2048 EthanPeng-2048 released this 21 Oct 13:45
· 32 commits to main since this release

neuralnet.cpp v1.10.0 Release Notes

⚠️ 勘误(2026-10-23 追加,发布后标注;本版本不撤回)

  1. 梯度累积归一化缺陷:本版 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)。
  2. --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)。
  3. 断点兼容:v1.10.0 写出的 .nnckpt 可在 v1.11.0 直接 --resume(缺语义键按 legacy micro-batch
    自动折算);反向(新断点回旧二进制)语义不同,回退前请保留原文件。
  4. 本版本 tag 与产物保持不变(已发布版本不重打);新用户请直接使用 v1.11.0。

发布范围:v1.9.0(795886f,2026-10-18)→ v1.10.0(tag v1.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 + TrainingSession P2a)与 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::dsl friend);
  • 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 所有权不变式

  1. model_ 声明在 opt_ 之前 ⇒ optimizer 先于 Model 析构(TensorRef 永不悬空);
  2. Session 不可拷贝、可移动(Layer 对象在堆上,移动后 TensorRef 仍有效);
  3. Model 保持纯值对象——推理可直接用 Model,不需要 Session;
  4. Loss / 数据集 / epoch / CLI / 日志不归 Session(防 god object,见 ../todo/29-session-architecture.md §4);
  5. 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());        // 返回 TextTrainReport
  • TextTrainer 拥有:训练/测试 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)必须与之完全一致,否则 create fail-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 终态)、断点、高性能样本集入口 + nullopt test、前置条件
    (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)完全可读。需要回退旧二进制时保留升级前的原文件。
  • .nnmodel warm 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 个发布提交(发布审查修复 + 发布文档/版本对齐,tag v1.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

  1. P2-4 流式 DataLoader 仍未做:TextSampleSet 仍把子集整段物化到 TextCorpus
    (token_flow / doc_ids / loss_mask),超大语料的内存曲线问题与 rc0/rc0.1 相同;
    流式分块迭代(for_each_chunk)留后续(../todo/18-roadmap.md P2-4 / ../todo/19-unified-dataset.md)。
  2. InferenceSession / RLTrainingSession 仍为占位(../todo/29-session-architecture.md §6/§7),
    未实现;触发条件 = 真实调用方出现(推理服务化 / RL 训练入口)。
  3. create_optimizer 自由工厂签名未收口(D7):仍返回 unique_ptr / nullptr,
    失败不是 Result;改名/改返回类型属破坏性变更,留待下一个破坏性窗口。
    Session 已覆盖训练路径,CLI 不再直接使用自由工厂。
  4. TextTrainer 目前只服务文本 / GPT 任务(内置 sparse-CE);MNIST / 自定义任务的通用
    Step 协议未抽象。触发条件 = 出现第二类需要同一训练循环的任务。
  5. 断点旧键兼容是单向的:v1.10.0 写 dataset_samples,rc0 二进制读不到(得 0 ⇒ 数据集身份对比失败);
    升级方向(rc0 → v1.10.0)完全可读。v8 文件版本号未变。
  6. C11:RAPT f16 冒烟的 NaN 历史问题(根因仍未定位)。历史上在 CPU-only Debug 复现过
    (rapt_test --f16 step 7 → step 8 NaN),故 CI 的 msvc-debug job 仍保留既有
    -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 全绿(未用豁免)。
  7. 矩阵口径(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 数值路径改动);最终源码上做了单次
    功能冒烟:GPU matmul 4.365 ms / 491.9 GFLOPS(hash cbf7d4478bd536ab)、matmul_q 0.796 ms /
    2697.2 GFLOPS(hash c5860cfcfcb371f9),CPU 512³ 小规模两组哈希齐全(计时受并发负载影响,仅作参考)。
  8. activation offload 与混合精度/三值:本轮选择配置期拒绝(slab 恒 f32,最短正确路径);
    slab 按存储精度记账的"精度协议化"留作后续立项。offload + checkpoint 混合模式仍无独立回归用例
    (只有各自单模式测试)。
  9. mnist_train 的优化器侧训练状态未接入 .nnckpt(mnist 路径本来就没有优化器状态段);
    文本路径的断点语义不变。
  10. D9 洁癖项(低风险):Layer::BindingState 三态化、nn::TensorRef 与 nn::dsl::TensorRef
    重名未处理;不阻塞本版发布。
  11. 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() 只读查询后补全覆盖)。
  12. MSVC Debug 下两个 app 发现类用例按 77 skip:cli_precision_warning_test / chat_prompt_test
    在 VS 多配置生成器下 app exe 位于 <build>/Debug/,用例的三条候选路径找不到 ⇒ skip(非失败)。
    该行为对 CI windows-latest 的 MSVC Debug job 同样成立 ⇒ 这两个用例在 MSVC Debug 下实际不执行;
    已记录为已知覆盖缺口(Release/CPU 树照常执行)。
  13. 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。