训练结束不等于保存成功:PLY 的原子提交

0 阅读7分钟

磁刻技术实录 · 第 28 篇 · 工程实践 · 技术基线 2026-10-31

我们正在做的「磁刻(Cike)」,是一款鸿蒙原生的生活记录与个性化创作应用。用户可以把照片做成数字冰箱贴,在洞洞板上自由摆放、组合自己的生活片段,也可以把喜欢的内容做成桌面卡片,用来回看、互动和分享。

在空间记录上,我们引入了 3D Gaussian Splatting(3DGS,三维高斯泼溅),把拍摄的视频重建为可以转动、靠近观察的三维场景。为此,我们重构了视频重建链路,实现自己的 C++ 训练核心,并结合鸿蒙原生的媒体处理、模型查看和分享能力,希望让用户保存一段经历时,也能留下当时的空间关系。

用户愿意等待重建,最终需要拿到一个可用的模型文件。我们分享临时产物、回读校验与提交竞争,是想解释磁刻如何区分训练结束和保存成功,避免半成品进入查看器,也避免取消与完成同时发生时留下矛盾状态。

一个非空 PLY 文件并不能证明训练任务已经成功。文件可能只有头部、顶点记录未写完,或者数量声明与实际数据不一致。磁刻的自研任务把保存分成临时写入、回读验证、重命名和终态提交,让产物有明确可用边界。

以下代码按当前实现整理,保留关键运算与控制顺序;省略导入、日志和完整类型,部分辅助函数采用说明性命名,片段需结合上下文阅读。

这种设计直接影响用户体验。页面不应因为目录里出现文件就提前进入查看器,业务服务也不应上传一个仍在写入的模型。只有当原生任务返回已提交结果,宿主才继续使用最终路径。

模型产物的提交过程

图 1:正式路径只在验证通过后出现

先保护已有内容

任务开始时检查最终输出是否存在,也检查同名临时文件是否存在。当前策略遇到已有目标直接失败,避免一次新任务覆盖此前模型。输出路径与工作目录采用独立任务范围,有助于把清理限制在本次所有的内容上。

这类保护与跨进程文件系统锁不是同一概念。当前应用依靠任务串行与私有工作目录组织正常访问;若未来允许多个进程同时写同一目标,还需要额外的互斥和文件创建协议,不能只依赖先检查后重命名。

参数有效性先于磁盘写入

训练完成后检查高斯集合非空,并确认位置、尺度、颜色、透明度和旋转为有效有限值。错误模型不应因为能够序列化成浮点数就成为成功产物。尤其 NaN 可以进入二进制文件,却会在显示端造成更难解释的问题。

随后写入带 .partial 后缀的临时文件,并记录任务确实拥有该文件。失败清理只处理自己创建的内容,避免把前一次遗留或外部文件当作本次垃圾直接移除。

回读验证检查真实落盘结果

写函数返回成功后,任务再次使用读取器加载临时模型,核对顶点数量,并检查回读参数。这样可以发现记录不完整、格式不匹配和部分写入问题。检查文件大小可以作为快速条件,但不能替代格式回读。

这个阶段会额外占用内存和时间,因此性能报告应把保存验证单独列出。它换取的是最终产物的可靠性,而不是训练循环速度。若未来优化验证方式,需要保留同等的完整性约束。

先写临时产物,回读后才公布最终名字

std::string partial = output + ".partial";
if (Exists(output) || Exists(partial)) return Conflict();
WriteModel(partial, gaussians);
auto verified = ReadModel(partial);
if (verified.size() != gaussians.size() || !AllFinite(verified))
    return RemoveOwnedPartialAndFail();
CheckCancellation();
if (Exists(output) || std::rename(partial.c_str(), output.c_str()) != 0)
    return RemoveOwnedPartialAndFail();
owns_final = true;

临时文件与最终文件处于同一目录,让最终路径的出现尽量成为一次明确提交。回读不仅检查文件存在,还验证解析后的数量与数值;磁盘写入返回成功并不能替代读取端契约检查。失败清理必须只删除本任务拥有的文件,不能把开始前已经存在的同名文件一并删掉。

这里的存在性判断不是跨进程互斥锁,也没有展示断电持久化保证。它解决当前任务流程里半成品被当作完成品的问题。要进一步支持多进程并发发布或严格持久化,还需要更完整的文件系统协议,不能仅凭一次 rename 承诺这些能力。

文件状态与任务状态

图 2:区分临时产物、正式产物和完成提交

重命名与完成之间仍有竞争

通过验证后,临时文件被重命名到最终路径。取消可能恰好在这一刻到来。当前任务用原子生命周期状态决定成功与取消谁先成立:若取消在完成提交前被接受,即使重命名已经发生,也会清理本次最终输出;提交完成后到来的取消则不撤销成功结果。

这样,文件状态和任务状态能够形成一致解释。仅靠一个普通 bool 在不同线程读写,不足以安全处理这种竞争;而仅靠文件存在也无法判断任务最终结果。

原子可见与断电持久是不同保证

同目录重命名提供清晰的文件可见性切换,但不能因此宣称已经具备完整断电持久性。若产品未来需要在突然断电后保证目录与文件数据都持久,还要研究同步写入、文件系统行为和恢复流程。当前文章讨论的是应用任务内的完整产物提交。

测试可以注入写失败、回读失败、已有目标、取消与提交竞争,检查最终结果、临时残留和旧文件是否保留。还需要确认失败任务释放活动锁,允许后续独立任务正常运行。

文件改名后,还要裁决与取消的竞争

Lifecycle expected = Lifecycle::Active;
if (!lifecycle.compare_exchange_strong(expected, Lifecycle::Finished)) {
    if (owns_final) std::remove(output.c_str());
    return CancelledResult();
}
StoreSuccessfulResult(output);
SetProgressDone();

若取消先把状态改成 CancelRequested,即使文件刚刚改名,提交也不能再宣布成功,必须撤回本任务刚发布的文件。反过来,如果完成状态先成功提交,之后的取消就不应删除用户已经获得的结果。一个原子状态给这两种情况定义明确胜者。

代码展示的是生命周期裁决,完整实现还会在统一异常路径里清理所有权标志和错误信息。测试可以把取消请求安排在回读前、改名前、改名后等边界,观察最终状态与磁盘结果是否匹配。仅检查界面显示“已取消”,无法证明没有留下错误产物。

可靠保存让算法结果成为真正的用户资产。把提交点定义清楚,查看、上传、分享和计费才能建立在同一个成功事实之上。