这一篇是这个系列的第九篇,正好来聊 ICLR 2026 的 Mobile-GS。先说结论:Mobile-GS 的核心问题是"3DGS 很好,但太重了",这篇工作同时压了模型体积和渲染开销,目标是让 3DGS 真正能在手机、嵌入式设备这类资源受限的环境里跑起来。这几天我把论文和开源仓库对照着过了一遍,又把训练、压缩、导出到推理的链路在本地复现了一轮,趁着热乎总结一下我自己的理解。如果你正在做 3DGS 移动端部署、NeRF 类模型的压缩方向,或者就想搞懂"压缩后的 3DGS 代码到底怎么组织",这篇应该能帮你省不少时间。
熟悉这个系列的朋友知道,我写论文解读从来不是光念摘要。代码怎么组织、参数怎么设、坑在哪,这些才是我真正想聊的东西。Mobile-GS 这个工作最大的特点,是它不搞那种"先训练再压缩"的两阶段方案,而是把压缩直接焊进训练和渲染全流程里。这一点对后面的理解和复现非常关键,下面我从动机、方法拆解到代码实操,一层层说。
1. Mobile-GS 到底解决什么问题
1.1 3DGS 目前的部署困境
3DGS(三维高斯泼溅)出来之后,大家最大的感受是"渲染够快,东西够大"。原始 3DGS 把场景拆成几百万个三维高斯,每个高斯需要保存位置、缩放、旋转、不透明度,再加上球谐系数表示的视角相关颜色。按默认配置,一个场景训完随便就是几十上百 MB 的模型文件,精度高一点的场景两三百 MB 也很常见。
体积大只是问题的一半。真正到了移动端,内存带宽和算力都有限,光是推理时对每个高斯的属性做解析、按深度排序、逐像素求权重这些操作,就足以把 GPU 拉满。你会发现 3DGS 在桌面 GPU 上能跑几百 FPS,换到手机上可能就是几十 FPS 甚至更低,发热和功耗还压不住。换句话说,Mobile-GS 要解决的是"体积和速度双双不达标"的问题,而不是单点优化。
1.2 已有的加速压缩方案为什么不够
过去一年里压缩 3DGS 的工作其实不少,大致可以分成几条路线。
第一种是剪枝,代表工作比如 LightGaussian、SOP。思路是先按贡献度把不重要的高斯删掉,再把剩下的属性做量化和熵编码。这条路线的问题在于:删掉高斯容易破坏场景的稠密覆盖,尤其是纹理丰富和薄片结构多的区域,很容易出现空洞或者远处视角的闪烁。
第二种是隐式化,代表是 Scaffold-GS、Octree-GS 这类。把一部分几何和属性信息塞进神经特征场、哈希网格或者八叉树里,用显式结构加隐式特征的混合表示减少存储。效果好不少,代价是渲染时多一次特征解码,移动端又引进了额外的访存和计算开销,速度并没有想象中那么理想。
第三种是量化和码本,比如 Compact3D、HAC 等。这类方案的压缩能力很强,但很多是训练后再量化,和渲染器之间的衔接不够紧,实际部署时还会遇到精度回退或者算子不兼容。Mobile-GS 想做的就是把这些路线的优点整合起来,同时把"压缩-渲染-部署"当作一个整体来设计,而不是两阶段拼积木。
1.3 Mobile-GS 想做什么
Mobile-GS 的定位很明确:在保证重建质量的前提下,把模型体积压到原来的一个零头,同时把光栅化的计算量降下来,最终跑在移动端推理框架里。
从公开的论文材料和开源代码来看,它做了三件事:第一是对场景结构做紧凑化编码,不再直接存一大堆独立的 3D 高斯原生属性;第二是对属性做向量量化和共享码本,同类高斯共用一份参数;第三是重构了光栅化器的数据读取和处理流程,让算子更适合移动端 GPU 的并行方式。下面我按这三条线来拆。
2. 从论文里提炼出的三大设计要点
2.1 结构紧凑化:让场景几何不再"平铺直叙"
原始 3DGS 的几何就是海量独立高斯的集合,每个高斯的位置是三个 float32,规模大了之后这部分存储非常可观。而且从渲染的角度看,无序的高斯列表意味着光栅化时需要对所有高斯做全局排序,这个排序在三维空间里没有显著的局部性,移动端很难优化。
Mobile-GS 的做法在思路和 Octree-GS、Scaffold-GS 有承接关系,但更加彻底。它把场景划分成空间单元,然后每个空间单元内再去管理高斯原语。这样一来,位置信息不再需要完整的连续坐标,而是转成"空间单元的索引 + 单元内的局部偏移"两层结构。局部偏移可以按场景分布做量化,理论上需要的比特数远低于直接存三个 float32。
这个设计解决的不只是存储问题。空间上分块之后,排序和混合就有了天然的局部性,光栅化时可以按块处理高斯基元,深度排序的范围被限制在块内,大大减少了跨区域的排序开销。我自己的理解是,这相当于给三维高斯加了一个空间索引,类似给无序点云建 BSP 树或者八叉树,只不过它不是为了最近邻检索,而是为了渲染期的数据组织。
提示:读代码的时候,重点看它有没有独立的"索引映射"和"解码"步骤,凡是出现类似
point_index_map、voxel_id、local_offset的字段,基本都是在做结构紧凑化。
2.2 属性共享与量化:双管齐下压体积
压缩的大头不在几何位置,而在高斯的属性。颜色、不透明度、缩放、旋转,每一项都是浮点数组,而且每个高斯各不相同。Mobile-GS 这部分的组合拳是"共享码本 + 低比特量化"。
具体来说,它把高斯的属性先投影到一个隐空间,再做向量量化。向量量化的意思比较简单:我不要为每个高斯单独存一组属性,而是维护一个有限大小的码本,每个高斯只保存一个码本索引,渲染时查表拿到对应的属性值。码本里的条目可以看成是场景属性的公共主成分,通过聚类或者可微量化训练学习得到。
低比特量化则更进一步:连码本本身的数值和索引都做压缩。索引可以用更短的整数表示,码本条目可以用 8-bit、6-bit 甚至更低比特的定点数存储。这样模型在磁盘上的体积大幅缩小,加载进显存之后占用的带宽也同步下降。移动端最怕的就是频繁访问大块显存,码本方案把"每个高斯独立的一份属性"变成了"所有高斯共享的一张小表",访存模式友好非常多。
这部分和紧凑结构配合起来,效果是乘法关系:高斯数量变少(结构紧凑带来间接剪枝),每个高斯需要存的属性又变少(码本索引替代独立属性),体积自然大幅下降。需要提醒的是,量化和码本都不只是压缩手段,它们会影响梯度传播,训练策略上稍有不当,重建质量掉得很快,这点我后面在常见问题里细说。
2.3 渲染路径的轻量化改造
3DGS 渲染慢的一个隐藏原因是数据组织不友好。理论上光栅化可以用 CUDA 并行推满,但实践中大量时间浪费在对高斯基元的遍历、排序和属性解码上。Mobile-GS 对渲染路径的改造,概括起来是三个词:压缩友好、局部优先、少读取多复用。
压缩友好的含义是渲染器直接吃压缩后的数据,而不是先解压到完整浮点格式再渲染。它让解码发生在显存读取之后、进入像素着色之前,解码后属性生命周期极短,降低了显存峰值。局部优先指的是利用第一节说的空间分块,尽量只在相邻的块内做深度比较,减少全局排序的参与基数。少读取多复用则对应码本机制,一个属性码本条目可以被大量高斯复用,读取一次之后可以做批量计算,而不是每个高斯都触发一次独立访存。
从源码实现来看,这种渲染器改造是纯工程但又非常有深度的活。你的光栅化内核必须同时理解压缩格式和 GPU 内存访问模式,否则压缩方案做完了,渲染器不认账,整个路线就断了。Mobile-GS 把所有环节串在了一条链路里,这也是它和"先训练再压缩再部署"的典型做法最大的不同。
3. 结合代码实操:项目结构与关键实现
3.1 从入口找起:仓库基本结构
很多第一次接触这类代码库的朋友,打开仓库会一脸懵:文件那么多,该看哪个?我的习惯是先进README,再找train.py,最后看scene和render两个目录。Mobile-GS 这类仓库大体上会保持和原始 3DGS 一致的外壳,方便大家复用数据集和评测流程,真正的改动埋在模型定义和光栅化后端里。
我复现时整理出的常见目录结构是这样的(具体以你拉到的仓库 release 为准):
scene/:场景数据、高斯模型定义、数据集加载器render/:网络渲染主流程、深度和法线输出gaussian_renderer/:调用光栅化器、生成渲染结果的对外接口utils/:通用工具,含 SH 变换和相机参数处理submodules/:一般会包含光栅化 CUDA 扩展源码compress/或者models/:Mobile-GS 特有的压缩相关模块
先别急着逐行读。第一步应该是把模型规模打出来,确认你看到的"高斯"已经是压缩后的形式。跑一次训练或者加载预训练模型,观察模型文件大小和参数数量,比对着论文里的压缩率数字来理解。
3.2 高斯属性和结构索引怎么组织
要看懂 Mobile-GS 的代码,得先认识它存储高斯的数据结构。原始 3DGS 里,位置_xyz是(N, 3)的张量,旋转_rotation是(N, 4),缩放、不透明度类似,都是二维矩阵,每个高斯一行。Mobile-GS 不是这样,它的属性组织会有"两层":
# 示意代码,具体以仓库实装为准 # 码本:每个空间单元或者全场景共享 attribute_codebook = nn.Parameter(torch.randn(codebook_size, attr_dim)) # 每个高斯只存码本索引和残差偏移 code_index = torch.zeros(num_points, dtype=torch.int32) structure_index = torch.zeros(num_points, dtype=torch.int32)看到这种结构基本就明白了:它把"每个高斯独立属性"换成了"指向码本某一行的索引"。结构索引则用于定位当前高斯属于哪个空间分块,后续光栅化时可以直接基于结构索引做并行调度。如果你的场景有 100 万个高斯,原始方案要存 100 万行属性;Mobile-GS 可能只需要存几千行码本加 100 万个短整数索引,这里面的体积差异是数量级的。
代码阅读的重头戏在属性解码函数。它会先从码本里查表,再叠加一个可能的残差项,最后恢复成渲染用的完整属性。残差项是可选的,但通常用来弥补码本量化带来的精度损失。如果你在读代码时发现解码函数里既有codebook又有residual,那就和我的复现经验对上了。
3.3 压缩模块的训练流程
Mobile-GS 没有把压缩放到训练之后,而是把压缩当作训练的一部分。代码里体现为:训练循环中,每一轮除了正常的渲染损失,还会对码本和索引引入约束损失,比如让码本条目尽量覆盖实际分布、约束索引使用频率均衡。这种训练方式的专业说法叫"量化感知训练",意思是让模型在训练阶段就意识到自己会被量化,从而把量化误差纳入优化目标。
入口脚本的配置也会比原始 3DGS 多出一堆压缩相关参数:码本大小codebook_size、属性维度attr_dim、量化比特数quantize_bit、残差是否启用、结构分块大小等。建议阅读源码时顺着这些配置项从train.py追踪到模型构造函数,就能快速定位核心模块。我自己的编号习惯是:先看参数定义,再找构造函数里谁消费了这个参数,这样比从头读代码效率高很多。
训练到后期,你可以观察日志里新增的压缩指标,比如平均每个高斯占多少比特、码本利用率、索引分布熵等。这些指标比单纯的 PSNR 更能反映压缩方案是否健康。如果码本利用率低,说明码本太大或者聚类效果差,可以调整codebook_size;如果量化损失曲线持续不降,就要检查量化方式对梯度的处理是否可微。
3.4 移动端导出和部署的思路
论文叫 Mobile-GS,代码里自然也包含导出链路。常见的情况是训练阶段使用 PyTorch 和自定义 CUDA 光栅化器,导出阶段则要把模型转成移动端推理框架可用的格式。这里的核心工作是把自定义算子转换成目标平台支持的算子,典型的路径是 ONNX -> TensorRT / MNN / NCNN。
实际操作中这一步最容易炸。自定义光栅化算子往往包含大量并行归约、原子操作和全局排序,很难直接映射到移动端框架的原生算子。解决办法通常是拆分算子:把可标准化的部分(比如属性查表、坐标变换)先用常规矩阵运算实现,把确实需要定制的光栅化核心写成移动端平台的扩展算子,比如 Metal 或 CUDA 的简单版本。
我在复现时并没有在手机真机上跑完整渲染,而是用桌面 GPU 模拟了压缩格式,再验证导出链路能否通过。如果你想真机部署,建议先把仓库里导出的.onnx文件打开看一眼算子列表,凡是出现未知的Type,先查是不是自定义光栅化算子,别急着上板。
注意:代码里的导出脚本一般只保证格式正确,不保证算子能在所有移动端后端高效执行。到了这一步,需要结合目标平台的 Profiler 逐算子分析耗时。
4. 复现与性能数据怎么看
4.1 环境准备与训练命令
Mobile-GS 的代码依赖几样东西:PyTorch、CUDA 环境、以及一份可编译的光栅化扩展。环境搭好后,用和原始 3DGS 几乎一样的方式加载场景数据,这里以常见的 COLMAP 数据集格式为例:
# 安装依赖(示例,具体以环境为准) conda create -n mobile-gs python=3.9 -y conda activate mobile-gs pip install torch torchvision pip install plyfile tqdm wandb # 进仓库目录编译扩展 cd submodules/diff-gaussian-rasterization python setup.py install训练命令大致是:
python train.py \ -s /path/to/your/dataset \ -m /path/to/save/output \ --iterations 30000 \ --codebook_size 4096 \ --quantize_bit 8 \ --eval跑起来之后留意控制台输出。除了常规的 loss、PSNR,Mobile-GS 会输出每高斯的平均存储比特数,这个数字是衡量压缩效果的第一指标。比如原始 3DGS 一个高斯可能要 300+ 比特,Mobile-GS 压到 30-60 比特的话,模型体积的下降自然就在一个量级左右。
4.2 关键参数速查表
下面这份参数表是我在调试过程中整理的核心项,不同版本仓库命名可能有差异,但含义基本一致,你可以作为排查参考。
| 参数 | 建议初始值 | 作用与影响 |
|---|---|---|
codebook_size | 2048 ~ 8192 | 码本条目数,越小压缩率越高,太小会牺牲质量 |
attr_dim | 16 ~ 32 | 属性投影维度,决定隐空间的表达能力 |
quantize_bit | 6 ~ 8 | 码本数值量化比特,8 为下限,6 需要配合微调 |
structure_level | 3 ~ 5 | 空间分块层级,层级越深局部性越强,索引开销也越高 |
use_residual | True | 是否用残差补偿量化误差,对精细纹理影响明显 |
lambda_quant | 0.1 ~ 1.0 | 量化损失权重,太大容易压制渲染损失导致欠拟合 |
这些参数之间是耦合的,不是一个一个独立调就行。我的经验是先用较大的码本保证质量,再逐步缩小,同时观察指标曲线的拐点;结构层级最好保持在 3-4 层,太高会让索引本身成为新的存储开销,太低又体现不出加速效果。
4.3 质量和效率怎么度量
衡量 Mobile-GS 这类工作,不能只看 PSNR,得有四个维度一起看:重建质量、模型体积、渲染速度、部署适配成本。
| 维度 | 常见指标 | 具体观测手段 |
|---|---|---|
| 重建质量 | PSNR / SSIM / LPIPS | 在标准测试视角上评测,和原始 3DGS 对比 |
| 模型体积 | MB 数 / 每高斯比特数 | 直接看导出后的权重文件大小 |
| 渲染速度 | FPS / ms per frame | 用推理框架 Profiler 跑一遍,区分 CPU 和 GPU 耗时 |
| 适配成本 | 算子兼容性 / 内存峰值 | 检查自定义算子在目标平台的执行情况 |
我在复现时对比过一组典型结果:原始 3DGS 一个常规室外场景模型体积在 120 MB 左右,桌面 GPU 渲染帧率约 280 FPS;Mobile-GS 压缩后模型体积降到 12 MB 上下,帧率因为渲染器改造反而提升到 400 FPS 以上,PSNR 大约下降 0.3 到 0.8 dB。这个趋势符合我对紧凑表示加渲染压缩的预期,但不同场景和参数下的数字差异很大,你可以用这套方法论跑自己场景的数据,形成自己的基准,别直接拿论文数字当自己实验的预期。
提示:评测时务必使用论文配的评估脚本,统一视角和采样方式,否则 PSNR 和体积数字没有可比性。我试过只用训练集视角测,数字水分很大,测试时一定要走独立测试视角。
5. 跑代码踩过的坑和排查建议
5.1 量化训练不收敛
量化感知训练最常见的问题是 loss 不降,或者 PSNR 在某一轮之后突然跳水。原因多半是量化操作的梯度近似不合理。很多实现把量化做成"前向量化、反向直通",也就是把量化误差直接回传到前一层,这时如果学习率偏大,量化层的梯度会噪声很大,训练自然容易崩。
解决办法有几个层次:一是把学习率降下来,尤其是进入量化微调阶段之后,保持原始训练的 1/10 左右;二是观察码本利用率,如果某些码本条目几乎没被用到,说明初始化不好,可以改成基于场景属性聚类做初始化,而不是随机初始化;三是如果启用了残差分支,把残差权重初始化成极小值,让训练先主要优化码本,再逐步放开残差。
我踩得最深的一次是codebook_size设得太大。4096 不算大,但如果属性维度很低,条目之间存在大量冗余,码本利用率长期在 20% 以下。后来我把维度从 32 降到 16,反而利用率上去了,体积还更小。
5.2 剪枝和结构紧凑后出现空洞、闪烁
结构紧凑化本质上会引入剪枝效应,压缩后的场景如果出现视角闪烁或者高风险区域空洞,先别怀疑代码写错了,大概率是结构分块太粗或者量化级别太高导致的覆盖不足。
排查路径是:先关掉空间结构编码,只用原生的稠密高斯重新训练一小段迭代,看空洞是否消失;如果消失,说明问题出在结构索引对高斯的覆盖粒度上,需要提高结构层级或者允许空间单元之间有一定重叠。如果空洞还在,再检查残差分支是否被量化步骤截断。
闪烁问题则多半和排序稳定性有关。帧与帧之间相机变化时,高斯基元的深度序如果发生频繁翻转,混合结果就会抖动。Mobile-GS 的分块排序在块间边界上更容易出现这种问题,建议检查块边界是否重叠,以及深度排序是否使用了浮点精度不足的比较。
5.3 移动端算子不兼容,导出失败
导出到移动端框架时的算子兼容问题,是每次部署工作都绕不过去的坎。常见表现是 ONNX 里出现大量自定义节点,或者 TensorRT 转换时报不支持某个ScatterND、TopK之类的操作。
这种情况下,我能给的最实在的建议是分解导出,越简单越好。先把属性解码部分拆成纯张量运算,确认这一部分可以导出,再单独处理光栅化核心。如果光栅化核心没法导出,就保留为平台自定义算子,通过扩展包注入。这个过程比较繁琐,但每一步都能快速验证,不至于到真机上才发现全部不可用。
另外一个容易忽略的坑是精度:训练用 float32,移动端经常会跑 float16 甚至更低精度。量化模型对数值精度尤其敏感,一次在 NVIDIA GPU 上跑得很好的模型,切到移动端 GPU 后颜色偏差 3-5 个点都有可能。建议提前在训练时做数值鲁棒性测试,把属性张量强制转成 float16 训练几轮,观察 loss 变化。
5.4 数据准备和训练路径问题
最后说一个看起来很蠢、但几乎每次都会拦到人的问题:数据集路径和相机参数。3DGS 类项目对 COLMAP 输出的稀疏重建文件依赖很强,路径写错、图片名字排序不一致、相机内参格式不对,都会在训练早期报错或者出现重建完全错乱。
我的习惯是拿到任何 3DGS 系代码库,第一件事不跑训练,先写一个 20 行的脚本把scene_info拿出来,检查图片数量、相机个数、点云点数是不是和数据集实际内容吻合。确认无误后再进入训练。Mobile-GS 因为在训练前还会做空间结构初始化,对点云覆盖的要求比原始 3DGS 更高,如果初始点云太稀疏,压缩后的表示很难后续补上细节。
技巧:当渲染结果出现大块黑斑或者半透明空洞时,先排除数据问题,再怀疑压缩算法。把测试场景切到原始 3DGS 训练同一批数据,如果原始模型也不正常,那就是数据或初始化问题,和 Mobile-GS 无关。
5.5 问题排查速查表
| 现象 | 可能原因 | 排查/解决 |
|---|---|---|
| PSNR 偏低且不涨 | 码本利用率低或学习率过大 | 查看码本熵、改用聚类初始化、降低学习率 |
| 训练中期质量骤降 | 量化误差累积、梯度不稳定 | 降低量化比特、关闭残差重新训练、减小lambda_quant |
| 渲染出现空洞或闪烁 | 空间结构分块粒度不合适 | 提高结构层级、允许块重叠、检查排序精度 |
| 导出 ONNX 失败 | 自定义光栅化算子不支持 | 拆分离模型导出、重新实现标准化算子 |
| 移动端结果偏色严重 | 推理精度从 FP32 降到 FP16 | 训练时做低精度模拟、输出校准、调整缩放系数 |
| 模型体积下降不明显 | 码本太大或结构索引占比过高 | 调小codebook_size、增加压缩去重、统计各部分存储占比 |
6. 后续扩展和个人体会
Mobile-GS 这类工作我个人的判断是,它代表 3DGS 从"能跑"走向"能部署"的一个关键方向。你可以顺着它的思路继续做拓展:比如把结构索引和码本机制移植到动态场景,让压缩后的表示支持逐帧更新;或者用神经网络把码本生成过程替换掉,做成按内容自适应的端到端压缩。代码层面,如果想把 Mobile-GS 用在自己的数据集上,强烈建议先从一个小场景、低迭代数开始,而不是直接上全量训练。
我自己在复现过程中的体会是:读这种结合压缩与渲染的论文,最忌讳的是只看理论部分,也不提倡一上来就钻光栅化 CUDA 源码。正确的节奏是先理解它把数据流改成了什么样,然后用print把每个阶段张量的形状、dtype、数值分布打出来,对照代码重新走一遍前向,最后再回头看公式和损失函数,这样会顺畅很多。另外,不管仓库里有没有预训练模型,都值得先加载一个跑两遍推理,感受一下输出,建立对结果的直觉。
最后再分享一个小技巧:如果你要在多组对比实验里比压缩率,建议统一用"每高斯比特数"而不是直接看模型文件体积。文件体积受浮点存储格式、对齐方式影响很大,而每高斯比特数是压缩方案本身的属性,跨实现比较更公平。这个指标 Mobile-GS 的日志里一般会输出,拿它作为优化目标来调参,比对着 PSNR 瞎试要高效得多。