CPU 上训练 3DGS:分辨率、线程与内存预算

0 阅读6分钟

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

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

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

磁刻当前的视频训练核心运行在 CPU 上,分辨率、线程和内存配置都会直接影响用户等待时间。我们分享资源预算与测量方式,是为了把端侧训练的代价说清楚,也让后续优化建立在具体瓶颈和真实约束上。

CPU 训练器能够帮助建立数学正确性基线,但进入手机后,资源问题会立即变得具体:一帧要多少内存,一次迭代花在哪里,增加线程是否值得,以及用户取消后多久能结束。磁刻把训练分辨率、点数、线程和缓存作为统一预算来管理。

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

当前 App 输入从 1080×1440 RGB 降采样四倍到 270×360,默认训练 3,000 次,使用 4 线程,最多 60,000 个高斯。图像缓存最多 32 帧,并同时受 128 MiB 预算限制。这组设置描述当前入口行为,不能直接推出所有真实视频的完成时间。

当前 App 视频训练配置

图 1:这些是入口配置,不是任何设备上的性能保证

分辨率影响的不只是像素数

降低图像宽高会减少损失与像素合成工作,但也改变高斯在屏幕上的覆盖和可见细节。细线、纹理和小物体可能在降采样时消失。比较不同分辨率时,应同时调整内参并保持对应关系,不能只缩图不改相机。

计算量也不是简单的像素数乘高斯数量。Tile 分配、排序和实际覆盖决定每个像素访问多少高斯。相同点数下,尺度较大的模型可能更昂贵。因此,分析性能要记录覆盖分布,而不是只给一个“每秒处理多少点”。

128 MiB 不是整个训练器的内存上限

该预算约束训练图像缓存。总内存还包括高斯参数、梯度、Adam 一阶二阶矩、视角缓存、Tile 索引、线程局部缓冲以及导出验证时的数据。把一个局部缓存限制宣传成应用总内存占用,会形成错误预期。

以 RGB 浮点图像为例,270×360×3×4 约为 1.11 MiB,一组 32 帧约为 35.6 MiB,尚未包含容器与其他数据。这个算例只解释像素存储量,真实进程峰值还需要设备测量。缓存帧数同时受到数据集帧数和字节预算约束。

把资源预算写在同一个配置入口

TrainingConfig cfg;
cfg.downscale = 4;
cfg.iterations = 3000;
cfg.max_gaussians = 60000;
cfg.threads = 4;
cfg.image_cache_count = 32;
cfg.image_cache_bytes = 128ull * 1024 * 1024;
cfg.densify_from = 300;
cfg.densify_until = 2400;
cfg.densify_interval = 100;
cfg.opacity_reset_interval = 0;

命名在这里按文章统一,数值对应当前应用训练配置。downscale=4 将 1080×1440 的归一化帧变成 270×360 训练图像,减少的是像素级计算量;它不会按同样比例直接减少所有内存,因为高斯参数、梯度、Tile 列表与线程临时区依然存在。

缓存的 128 MiB 只是图片缓存预算,不能宣传成训练总内存上限。线程增加也会引入额外临时空间,并受内存带宽与调度影响。最后一项为零表示应用配置关闭周期性不透明度重置,不代表训练核心不存在该机制。

从输入到峰值内存的组成

图 2:缓存只是其中一部分,需要分别测量

多线程需要考虑局部梯度成本

前向可以按 Tile 分配独立像素区域,反向却会对同一高斯产生跨区域贡献。使用线程局部缓冲再归约可以减少写竞争,但会增加内存。线程数增加后,调度开销、缓存压力和归约成本也可能上升,不能假定加倍线程就加倍速度。

手机还受到温度和系统负载影响。短小合成样本的速度不代表持续训练十几分钟后的表现。基准应记录设备、系统版本、运行时间、起始温度或稳定条件,并避免将模拟器 CPU 时间当作真机性能。

把阶段拆开测量

至少需要区分数据加载、初始化、前向、损失、反向、参数更新、致密化、评估和保存。训练中某次迭代突然变慢,可能是致密化后点数增加,也可能是图像缓存未命中。只有分阶段计时才能决定优化应该放在哪一处。

缓存策略也有取舍。保留全部图像减少磁盘读取,却增加常驻内存;较小缓存节省内存,但相机遍历顺序可能带来更多重新加载。当前实现保留有限缓存与访问记录,能够在受控预算下运行,而不把全部图像永久驻留。

先分阶段测量,再决定优化哪一段

auto t0 = Clock::now();
LoadImage();
auto t1 = Clock::now();
Forward();
auto t2 = Clock::now();
Backward();
auto t3 = Clock::now();
AdamStep();
auto t4 = Clock::now();
ReportTimes(t1-t0, t2-t1, t3-t2, t4-t3);

总耗时只能说明用户等了多久,分段耗时才能指导实现。如果图片读取占比高,应检查缓存和数据布局;如果反向传播占比高,则要看局部梯度累积、线程归约和内存访问。直接增加线程数,可能对真正瓶颈几乎没有帮助。

测量也会有开销,特别是每一步同步打印大量日志时。应使用一致采样方式,并在质量不变的条件下比较。真机还需要连续多次运行观察温度与频率变化:第一次短任务的峰值速度,不能代表长视频连续处理时的稳定表现。

CPU 参考版的长期价值是提供可检查的基线。GPU 后端应以相同输入和指标比较质量误差与资源成本。现阶段更有说服力的成果,是明确预算、可重放测试和真实阶段数据,而不是脱离样本条件的“移动端高速训练”口号。