Repository navigation
Basalt 3.9.0 — 张量生命周期交给 Rust
Basalt 3.9.0
这一版只做一件事:把张量的生命周期从 Java 的 GC 手里拿回来,交给 Rust。
- 坐标 —
io.github.9178nekomc:basalt:3.9.0 - 在线文档 — https://9178nekomc.github.io/basalt/
- 运行要求 — Java 21,启动加
--add-modules jdk.incubator.vector - 规模 — 243 个公开类型 / 1568 个公开成员,全部带逐参数说明
问题
一个字符级训练任务(每轮 2000 个窗口、batch 32、2.68 M 参数)在第 15 轮左右崩溃:
memory allocation of 4194304 bytes failed
不是常见的泄漏。每个原生张量的回收挂在一个约 40 字节的 Java 句柄上,通过 Cleaner 触发——
Java 堆几乎没有压力,GC 于是根本不来,而原生侧每一批输入、每个前向中间量、损失张量都还活着。
先试过两个补丁,都只把崩溃点往后推:
| 补丁 | 结果 |
|---|---|
在 Trainer 里显式 close() 四个可见张量 |
从第 15 轮推到第 30 轮 |
每 128 步 System.gc() |
同样到第 30 轮 |
两个都是在给「GC 什么时候来」打补丁,没有触及设计本身。
改动
张量在作用域内由 Rust 持有:
Context增加帧栈,每次分配把地址记进栈顶帧;begin_scope压栈,end_scope弹栈并把帧内张量一次性释放,返回实际释放个数。- 三个 JNI 入口,对应
NativeContext.beginScope()/endScope()/scopeDepth()。 Trainer把每个训练步包进作用域:批次张量、前向中间量、损失都在这一步结束时回收。参数、输入、优化器状态在作用域外创建,不受影响。- 顺序有约束并在文档里写明:
end_scope必须在backward与clear_graph之后,否则计算图仍引用着已释放的张量。
context.beginScope();
try {
Tensor logits = model.forward(inputs);
Tensor loss = engine.crossEntropy().loss(logits, labels);
loss.backward();
optimizer.step();
} finally {
context.endScope(); // Rust 一次回收整帧,不等 GC
}验证
同一套配置改造后跑过 30 轮以上,没有再出现分配失败——这是本次修复的直接判据。
训练在 30 轮处主动停止,没有跑到收敛:这一版的目的是证明内存不再无界增长,不是产出一个模型。
./verify.sh全绿(7 组 legacy 套件 + JUnit 5 + 6 个编译期拒绝用例,-Xlint:all零告警)TextEngineTest8/8、UnifiedModelTest8/8、AdvancedApiTest9/9- 文档:19 页 / 243 类型 / 1568 个成员中 1568 个有说明 / 0 占位符 / 0 断链
产物
| 文件 | 说明 |
|---|---|
basalt-3.9.0.jar |
标准 jar,含四平台原生库 |
basalt-3.9.0-all.jar |
Fat jar,无外部依赖 |
basalt-3.9.0-sources.jar |
源码 |
basalt-docs-3.9.0.zip |
离线文档,19 页,解压双击 index.html |
前几版留下的
- 3.8.0 —
TextEngine.stream/streamSampled:逐 token 回调,解码结果与批量接口逐位一致 - 3.7.0 — 文档与基准报告 HTML 移出版本库,由 CI 构建并发布,单次推送从 17.1 MiB 降到 5.8 MiB
- 3.6.0 —
basalt.model统一模型层,Auto.sequence可筛序列模型