跳到主要内容

ZipMap:基于 Test-Time Training 的线性时间有状态 3D 重建

论文:ZipMap: Linear-Time Stateful 3D Reconstruction via Test-Time Training
会议:CVPR 2026(据参考材料)
arXiv / 项目主页 / 代码:参考材料中未提供,待补充

这篇论文解决什么问题

前面读过的 VGGT、π³ 都是同一路线:feed-forward 3D reconstruction。它们把多张图一次性塞进 Transformer,用 global attention 让所有 token 互相看,从而建立多视角几何一致性。效果很好,但有一个硬伤:

图片数量 N 增加时,global attention 的复杂度是 O((N×M)²)
其中 M×M 是每张图的 patch token 数

假设每张图有 M² 个 token,总 token 数 L = N×M²,attention 复杂度 O(L²)。10 张图还能跑,100 张很慢,700 张直接爆炸。ZipMap 要解决的就是这个可扩展性问题:

不让所有图片互相 attention,而是把整个场景压缩成一个可更新的隐藏状态(scene state),随帧线性更新。

一句话版本

ZipMap 把 VGGT / π³ 的“全局 Transformer 建图”改造成“带神经记忆的建图系统”:

images / video frames
-> image tokens(DINOv2 encoder)
-> TTT update:用当前帧的 K,V 把 fast weights 更新一步
-> fast weights W_hat = scene memory
-> 用 query 读 memory -> point / depth / camera / target-view RGB+depth

复杂度从 O(N²) 降到 O(N),并且支持 streaming:来一帧,更新一次。

和 VGGT / π³ 的定位差异

VGGT / π³ZipMap
思想全局理解(所有视角同时优化)持续记忆(state update)
关键机制global attentionTTT fast-weight update
复杂度O(N²)O(N)
适合几十张图长视频 / 几百上千帧
类比一次性 bundle adjustmentSLAM + Transformer

ZipMap 并不是“不用 attention”:它保留 local-window attention(初始化自 VGGT frame-wise attention),真正替换的是昂贵的 global all-token attention,换成 TTT memory。

网络结构

整体流程:

Images
-> DINOv2 / VGGT-style encoder
-> image tokens
-> [local window attention <-> TTT block] × 24 层
-> fast weights W_hat(scene state)
-> query heads -> point / depth / camera

论文原文是 24 layers of local window attention interleaved with large-chunk TTT blocks,不是 24 个纯 TTT。两种模块分工:

local window attention:局部 token 交互(同一图的局部结构、邻近 patch、局部纹理几何)
TTT block: 全局场景信息写入与读取(替代 global attention)

TTT 参数的一部分初始化自 VGGT global-attention 参数。所以 ZipMap 可以看作对 VGGT aggregator 的可扩展化重构:

VGGT:   frame attention <-> global attention <-> frame attention ...
ZipMap: local attention <-> TTT <-> local attention ...

TTT 模块:核心公式与 fast-weight 使用过程

⭐ ZipMap 论文里 TTT(Test-Time Training)模块的核心公式

TTT 的核心公式:q/k/v 投影、虚拟目标 Eq.(2)、fast-weight 更新 Eq.(3)、SwiGLU 与线性 attention 推导

q / k / v 投影

输入 token xix_i(d 维)先经过三个可学习的投影:

qi=WQxi,ki=WKxi,vi=WVxiq_i = W_Q x_i,\quad k_i = W_K x_i,\quad v_i = W_V x_i

虚拟目标(virtual objective,Eq. 2)

TTT 的 fast-weight 更新不是由 3D 重建 loss 直接驱动,而是用一个自监督的“虚拟目标”:

LTTT=ifW(ki)vi\mathcal{L}_{\mathrm{TTT}} = -\sum_{i} f_W(k_i)^{\top} v_i

要点:

  • 它只依赖输入图片自己产生的 kik_iviv_i,不需要任何 GT,所以训练和测试时都能算。
  • 它不是最终的 3D reconstruction loss(那是 Eq. 9 那组 Lpoint+Ldepth+Lcam\mathcal{L}_{\mathrm{point}} + \mathcal{L}_{\mathrm{depth}} + \mathcal{L}_{\mathrm{cam}} + query loss),它只是 TTT 层内部做 fast-weight 更新用的目标。

Fast-weight 更新(Eq. 3)

先求梯度:

g=WLTTTg = \nabla_W \mathcal{L}_{\mathrm{TTT}}

然后做一步(或少量几步)梯度更新:

W^=TTTUpdate(W,g)\widehat{W} = \mathrm{TTTUpdate}(W, g)

每个 token 还有一个网络预测出来的写入强度(per-token learning rate)ηi\eta_i,控制“这个 token 写进 memory 的力度”:

W^=WiηiWLTTT(fW(ki),vi)\widehat{W} = W - \sum_i \eta_i\, \nabla_W \mathcal{L}_{\mathrm{TTT}}\bigl(f_W(k_i), v_i\bigr)

注意这里有两个不同的 learning rate,别混:

η_i(Eq. 3 附近):每个 token 写进 fast weights 的强度,由网络预测
1e-4 / 1e-5:外层 Adam 优化器学习模型参数的步长

读取(query)

更新后的 fast weights 用于读取:

fW^(q)scene featureheadsf_{\widehat{W}}(q) \to \text{scene feature} \to \text{heads}

对输入视角,qq 是 image token 的 query;对 target view,qq 是 ray map 编码出的 ray query。

Fast-weight MLP:SwiGLU

fast-weight 函数不是简单的线性层,而是 SwiGLU:

fW(x)=W2[SiLU(W1x)W3x]f_W(x) = W_2\bigl[\mathrm{SiLU}(W_1 x) \odot W_3 x\bigr]

为什么它像 attention(线性情形的直观推导)

先假设 fW(k)=Wkf_W(k) = Wk,loss 为 L=(Wk)v\mathcal{L} = -(Wk)^{\top}v,则 WL=vk\nabla_W \mathcal{L} = -v k^{\top},一步梯度下降得到:

W^=W+iηiviki\widehat{W} = W + \sum_i \eta_i\, v_i k_i^{\top}

用 query qq 读取:

fW^(q)=Wq+iηivi(kiq)f_{\widehat{W}}(q) = Wq + \sum_i \eta_i\, v_i\,(k_i^{\top} q)

后半部分 ivi(kiq)\sum_i v_i (k_i^{\top} q) 就是没有 softmax 的 linear attention:匹配度 (qki)(q^{\top}k_i) 加权 viv_i。所以“K,V 通过梯度写进 W,Q 通过 W 读出”不是模糊的比喻,线性情形下几乎可以直接推导成 attention。SwiGLU 版本只是把容量变大。

⭐ ZipMap 的 TTT fast weight 更新和使用过程

TTT fast weight 的 write/read 流程、batch 与 streaming 更新、测试时执行

一个 TTT block 的完整 write / read 流程:

x
├── W_Q ────► q ──────────────────────────────┐
│ │
├── W_K ──► k │
│ │ │
│ ├── L_TTT(f_W(k), v) ──► ∂L/∂W │
├── W_V ──► v │ │
│ ▼ │
│ W ──► W_hat │
│ ▼
└────────────────────────────────────► f_W_hat(q) ──► output

用文字说:

  1. 当前 fast-weight 状态是 WW(可学习的初始化)。
  2. 当前输入产生 qqkkvv
  3. 用虚拟目标 LTTT=fW(ki)vi\mathcal{L}_{\mathrm{TTT}} = -\sum f_W(k_i)^{\top} v_i 求梯度。
  4. 做一步更新 WW^W \to \widehat{W},等于把当前视角的信息“写”进 memory。
  5. 后续网络使用 fW^(q)f_{\widehat{W}}(q) 读取,再进 point / depth / camera heads。

“写”和“读”合起来构成一个完整的 neural scene memory:

Write: (K, V) ──► W_hat
Read: q_ray ──► f_W_hat(q_ray) ──► RGB / depth

流式更新(streaming)

普通 batch 模式是一次性用全部图片更新一次:

WTTTUpdate(W, {ki,vi}i=1N)W \leftarrow \mathrm{TTTUpdate}\bigl(W,\ \{k_i, v_i\}_{i=1}^{N}\bigr)

流式模式下,第 tt 帧只用自己的 pp 个 token 更新:

W(t)TTTUpdate(W(t1); {kt,i,vt,i}i=1p)W^{(t)} \leftarrow \mathrm{TTTUpdate}\bigl(W^{(t-1)};\ \{k_{t,i}, v_{t,i}\}_{i=1}^{p}\bigr)

pp 是当前帧的 token 数(例如 224×224、patch 14 时约 16×16 = 256),不是图片数量。每帧只处理当前帧 + memory,所以每帧 O(1)、总计 O(N)。

测试时照样执行

测试时没有 GT,但 LTTT\mathcal{L}_{\mathrm{TTT}} 仍然能算(kkvv 都来自输入图片自己),所以:

image ─► x ─► K,V ─► 虚拟 loss ─► 一步梯度更新 ─► W_hat ─► Q query ─► 3D 输出

这就是 TTT 的精髓:outer loss(Lpoint+Ldepth+Lcam\mathcal{L}_{\mathrm{point}} + \mathcal{L}_{\mathrm{depth}} + \mathcal{L}_{\mathrm{cam}} + query loss)只在训练时有;inner virtual loss(Eq. 2)训练、测试都有,因为它是 self-supervised 的,作用是构建 in-context associative memory。

训练目标(3.5 节)

总损失:

L=Lpoint+Ldepth+wcLcam+Lcolort+Ldeptht\mathcal{L} = \mathcal{L}_{\mathrm{point}} + \mathcal{L}_{\mathrm{depth}} + w_c\,\mathcal{L}_{\mathrm{cam}} + \mathcal{L}_{\mathrm{color}}^{t} + \mathcal{L}_{\mathrm{depth}}^{t}

其中 wc=5w_c = 5。前三个是普通重建监督(点、深度、相机),后两个是 query loss(target ray 查 memory 恢复 RGB 和 depth)。

Point loss(尺度不变)

point map 在 local camera coordinates:

Lpoint=meani,j(s^pi,jpi,j13zi,j)\mathcal{L}_{\mathrm{point}} = \mathrm{mean}_{i,j}\left(\frac{\|\,\hat{s}\,p_{i,j} - p_{i,j}^{*}\|_1}{3\,z_{i,j}^{*}}\right)

其中 s^\hat{s} 是全局共用的尺度:

s^=argminsi,jzi,jspi,jpi,j1\hat{s} = \arg\min_{s}\sum_{i,j} z_{i,j}^{*}\,\| s\,p_{i,j} - p_{i,j}^{*}\|_1

为什么除以 3z3z^{*}:L1 含 X / Y / Z 三个维度(所以有 3),除以 zz^{*} 让误差变成相对误差——1 m 处错 10 cm 和 100 m 处错 10 cm 意义完全不同。

为什么 s^\hat{s} 全局共用:所有 view 只能有一个整体尺度模糊,不能 view 1 scale = 1.2、view 2 scale = 0.7,这是多视图一致性的体现。

Depth loss(带不确定性)

Ldepth=meani(Σi(s^DiDi)1αlogΣi),α=0.2\mathcal{L}_{\mathrm{depth}} = \mathrm{mean}_i\left(\bigl\|\Sigma_i \odot (\hat{s} D_i - D_i^{*})\bigr\|_1 - \alpha \log \Sigma_i\right),\quad \alpha = 0.2
  • 第一项:置信度高时误差惩罚大。
  • 第二项 αlogΣ-\alpha \log \Sigma:防止模型作弊把 Σ\Sigma 全设成 0(logΣ\log\Sigma \to -\infty 会让该项发散)。
  • 等价于 Laplace negative log-likelihood 形式。

point 和 depth 共用同一个 s^\hat{s},隐式约束两个 head 的尺度一致。

Camera loss 与 reference view

第一阶段用第一帧作 reference view:

Lcam=1Ni=1Ncici\mathcal{L}_{\mathrm{cam}} = \frac{1}{N}\sum_{i=1}^{N}\|c_i' - c_i^{*}\|

cic_i' 的 translation 也乘 s^\hat{s},因为相机基线与场景点必须同尺度。

之后论文去掉 reference view,改成 affine-invariant camera loss(受 π³ 启发):固定第一帧对 5–10 张图没问题,但 100、500、1000 帧时越往后越依赖第一帧,形成 reference-frame bias;去掉锚点对 long sequence 更有利。

Query loss(ZipMap 特有的部分)

训练时把一部分 view 当 input views 写 memory,另一部分当 target views:只给 target 的相机 ray,让模型从 scene state 预测 target RGB 和 depth。

Lcolort=10×(MSE+LPIPS)\mathcal{L}_{\mathrm{color}}^{t} = 10 \times (\mathrm{MSE} + \mathrm{LPIPS}) Ldeptht=Eq. 12 (Dt,Σt 与 GT target depth)\mathcal{L}_{\mathrm{depth}}^{t} = \mathrm{Eq.~12}\ (D_t, \Sigma_t \text{ 与 GT target depth})

query loss 只在 finetuning 阶段开启:先练好 reconstruct,再练“任意 target ray 读 memory”——类似 curriculum。

没有 query loss 时,W^\widehat{W} 可能只是 aggregator 的中间状态;加上 target-ray RGB/depth 监督后,W^\widehat{W} 才被真正训练成“可以被查询的隐式 3D 场景表示”。

平滑损失

还加了 point-map normal loss 和 depth gradient loss,保证局部平滑(避免深度 2.00 / 5.31 / 1.20 / 7.80 这种跳变)。

Fast-weight state 大小与训练规模

  • d=1024d = 1024,SwiGLU hidden =2d=2048= 2d = 2048
  • W1W_1W3W_31024×20481024 \times 2048W2W_22048×10242048 \times 1024,参数共 6d26.296d^{2} \approx 6.29M / layer。
  • state size 不随输入帧数增长:10 帧和 1000 帧都是 6d26d^2,这才是可扩展性的本质。
  • 24 层 TTT 的 fast-weight state 约 24×6.2924 \times 6.29M \approx 1.5 亿参数(若每层都是 6d26d^2)。

三阶段训练(共 64 张 H100):

阶段数据迭代学习率说明
Stage 1static datasets80KTTT 1e-4,其他 1e-5约 5 天,第一帧作 reference view
Stage 2dynamic datasets40K统一 1e-5约 2.5 天,处理人 / 车 / 运动物体
Stage 3移除 reference view60K1e-5增强 long-sequence 与 affine-invariant 重建

另外,scene-state query 额外 fine-tune 100K iterations:每个场景随机采样 views,一半 input、一半 target。

两个关键问答(侧边栏加星号)

⭐ L(fW(ki), vi) = −fW(ki)ᵀvi 是在哪里训练的?需要先把 W 训练好再冻结吗?

Eq.(2) 的 TTT 虚拟目标到底在哪里训练?是不是“先训练 k→v 的 W,冻结后再训练这个 loss”?

不是“先训练 W → 冻结 → 再训练这个 loss”。准确的说法是:

  • 外层训练(outer training):ZipMap 正常训练整个网络时,就把 TTT 的一步临时梯度更新嵌在 forward pass 内部。
  • 内层更新(inner TTT update):每次 forward 内,用当前 WW 和当前输入的 kik_iviv_i 计算 LTTT\mathcal{L}_{\mathrm{TTT}},求梯度,做一步更新 WW^W \to \widehat{W},再用 fW^(q)f_{\widehat{W}}(q) 继续前向。

也就是说有两层优化:

外层:训练几天,Adam,loss = L_point + L_depth + L_cam + query loss
学习 W_Q / W_K / W_V、W 的初始化、η_i 预测器、heads……
内层:每次 forward / inference,loss = Eq.(2)
只临时更新 fast weights W ──► W_hat

为什么不需要“先单独训练好 k→v 的 W”?因为不存在一个预先定义好的正确 value:

vi=WVxiv_i = W_V x_i

viv_i 自己也是网络学出来的 latent,没有 GT 告诉模型“这个 patch 的 value 应该是 [0.31, 0.72, …]”。WKW_KWVW_VWQW_QWW 初始化、ηi\eta_i 是一起被外层 reconstruction objective 塑造出来的:训练最终学会的是一组 K/V/Q 表示,使得“做一次 K→V 拟合”这个动作本身成为一个有效的 scene-memory write operation。这也正是它叫 Test-Time Training 的原因。

⭐ ray query token 与 image query 形式不同,为什么能输入同一个 Fast-Weight MLP f_W_hat?

ray token 和 image token 完全不同,凭什么共用同一个 f_W_hat?

分两层看。

第一层:进入 TTT block 之前,两者已经被编码到同一个 d=1024 维 token 空间。

image:   RGB ─► DINOv2 ─► x_img ∈ R^1024
ray map: [r_o, r_d, r_o × r_d](每像素 9D)─► patchify ─► Linear ─► x_ray ∈ R^1024

论文明确写:ray map 每像素 9D,patchify 后经线性层投影到和 image token 相同的 embedding dimension,之后 image 和 ray-map tokens 进入同一套 backbone。

第二层(更关键):进 fW^f_{\widehat{W}} 的不是裸 token,而是经过 query projection + 前面 local processing 后的 query representation。

qimg=query projection(ximg),qray=query projection(xray)q_{\mathrm{img}} = \text{query projection}(x_{\mathrm{img}}), \quad q_{\mathrm{ray}} = \text{query projection}(x_{\mathrm{ray}})

训练让它们落在同一个 query space。用 cross-attention 类比:

fW^(qray)  softmax(QrayKimg)Vimgf_{\widehat{W}}(q_{\mathrm{ray}}) \ \sim\ \mathrm{softmax}(Q_{\mathrm{ray}} K_{\mathrm{img}}^{\top}) V_{\mathrm{img}}

QQ 来自 ray、K/VK/V 来自 RGB,来源不同也能点乘,因为网络学会了把它们映射到 compatible latent space。ZipMap 只是把 QKVQK^{\top}V 换成 fW^(Qray)f_{\widehat{W}}(Q_{\mathrm{ray}}),而 W^\widehat{W} 已经通过 (Kimg,Vimg)(K_{\mathrm{img}}, V_{\mathrm{img}}) 把 image context 写进去了。

这个“ray 如何读取 visual memory”的接口是学出来的,不是天然成立的:额外的 state-query finetuning(100K iterations,一半 input 一半 target views)用 target RGB/depth loss 专门训练 qrayq_{\mathrm{ray}},让它落在能正确读取 W^\widehat{W} 的位置。

你读这篇论文时最该盯住的点

  1. TTT 的 inner / outer 两层优化怎么分工。
  2. fast weights 的 write(Eq. 2 / 3)与 read(query)机制。
  3. query loss 为什么把 W^\widehat{W} 从“中间状态”变成“可查询的 scene state”。
  4. local attention + TTT memory 与 VGGT global attention 的对应关系。
  5. streaming 更新为什么保持线性复杂度。

与前面笔记的衔接

  • DUSt3R / MASt3R:pairwise pointmap。
  • VGGT / π³:全局前馈几何推理。
  • DA3:depth + ray 表示。
  • AMB3R:backend 组织全局场景。
  • ZipMap:把全局 attention 换成可更新的 fast-weight memory,支持流式、线性复杂度。

官方资料

参考材料中没有给出 ZipMap 的 arXiv 编号、项目主页或代码链接,待补充。