<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Two and One</title><description>learn like a kid.</description><link>https://www.hjcheng0602.cn</link><item><title>nanoPD:一个 LLM P/D 分离推理引擎的实现笔记</title><link>https://www.hjcheng0602.cn/blog/nanopd/nanopd</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/nanopd/nanopd</guid><description>A from-scratch Prefill/Decode disaggregation inference engine for LLMs</description><pubDate>Sat, 11 Apr 2026 17:48:00 GMT</pubDate><content:encoded>&lt;p&gt;这个项目的起因是读vllm, DistServe 和 Mooncake 的时候有些地方没有完全想清楚，觉得与其在论文层面反复打转，不如自己动手实现一遍，于是花了一段时间和claude大人一起从零写了一个支持 Prefill/Decode 分离调度的推理引擎，覆盖了从 CUDA 内核到自适应路由器的完整栈，代码大约 2000 行 Python 加 400行 CUDA C++，每个模块有单独的文档，也顺手做了中英双语版。(菜菜勿喷呜呜）&lt;/p&gt;
&lt;h2&gt;背景和动机&lt;/h2&gt;
&lt;p&gt;LLM 推理的两个阶段在计算特性上的差异非常显著，prefill 是一次性处理整个 prompt 的过程，本质上是大规模的GEMM，算力密集，GPU 的计算单元在这个阶段是满载的；而 decode 每步只生成一个token，每次前向传播只有一个（或很少几个）新 token 需要计算，但需要从显存里读取所有历史 token 的 KV cache，是典型的memory bound操作，GPU 的计算单元大部分时间在等数据，实际上 decode 阶段的算术强度非常低，主要的开销是 HBM 的读取带宽。&lt;/p&gt;
&lt;p&gt;这两种操作对 GPU 资源的需求模式截然不同，把它们放在同一张卡上并发运行时会产生 SM 资源的竞争，prefill 的矩阵乘法会占用大量 SM，导致同时在跑的 decode 请求的 attention 计算被推迟，表现出来就是 decode的延迟在有 prefill 并发时会显著上升，这种干扰在高并发场景下尤其明显，也是 vLLM 等系统在负载较高时尾延迟劣化的一个重要原因。&lt;/p&gt;
&lt;p&gt;P/D 分离（Disaggregated Serving）的思路是把 prefill 和 decode 分配到专用的 GPU 上，让两个阶段互不干扰，这个方向在工业界和学术界都有比较多的工作，DistServe 比较系统地分析了分离的收益，Mooncake 则是
月之暗面在生产系统中实践分离调度的工程经验，两篇文章读起来都很有意思，但读完之后我对一些具体的设计决策仍然有疑问，比如路由策略在不同硬件上的表现差异有多大，代价模型里的参数对结论的敏感性如何，这些问题通过实现一遍会有更直接的感受。&lt;/p&gt;
&lt;h2&gt;实现栈，从底到上&lt;/h2&gt;
&lt;p&gt;CUDA 内核部分手写了 paged attention kernel 和 KV store ops，主要是想理解非连续内存块上的 attention 是怎么做的，传统的 attention 假设KV cache 存储在连续内存里，但 paged KV cache 把显存切成固定大小的物理块，序列的 KV 数据分散在这些块中，attention 计算需要根据 blocktable 做间接寻址，实现上需要在 CUDA kernel 里根据 token 的位置先找到对应的物理块再读取数据，这个 gather 操作相比连续 KV cache 有额外的开销，但换来的是显存利用率的显著提升，因为不再需要为每条序列预先分配最大长度的连续显存，这个 tradeoff 在长序列和高并发场景下非常值得。内核的实现参考了 vLLM 的设计，但为了保持简单没有做 Flash Attention 那样的 tiling 优化，所以在长序列上性能差很多，这部分如果要真正优化的话工作量还是比较大的。（next work预定了说是）&lt;/p&gt;
&lt;p&gt;块管理器以 block 为粒度管理显存的分配和释放，逻辑上类似操作系统的虚拟内存管理，每个物理块有引用计数，引用计数降为零时才真正释放，支持 Copy-on-Write fork，这个特性在 beam search 或者需要复制序列状态的场景下有用，fork 的时候共享物理块，只有在某条路径需要写入新token 时才触发实际的物理块复制，避免了不必要的显存拷贝。&lt;/p&gt;
&lt;p&gt;推理引擎实现了 chunked prefill，长 prompt 会被切成固定大小的 chunk和当前正在 decode 的请求交错执行，而不是一次性把整个 prompt 打进去阻塞所有 decode 请求，这个设计的好处是降低了 prefill 对 decode 的干扰，代价是单个请求的 prefill 总时间会因为被切分而略有增加，调度器维护 waiting、prefilling、running 三个队列，每一步决定哪些请求进入 prefill、哪些进行 decode、chunk 大小是多少，这些调度决策对系统的整体延迟和吞吐影响很大，实现上采用了比较简单的启发式策略，没有做复杂的动态调整。&lt;/p&gt;
&lt;p&gt;Worker 层分三类，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CollocatedWorker 在单卡上同时做 prefill 和 decode，内部复用了推理引擎的调度器；&lt;/li&gt;
&lt;li&gt;PrefillWorker 专门处理 prefill，完成后把生成的 KV cache 从 GPU 显存提取到 pinned memory buffer（为什么我的服务器连PCIe都没有！！！），准备传输；&lt;/li&gt;
&lt;li&gt;DecodeWorker 接收传输过来的 KV cache，加载到自己的显存后把对应的序列加入 decode 队列。
三类 Worker 可以在不同 GPU 上并发运行，由上层的 CentralScheduler 协调，CentralScheduler 把 collocated 和disaggregated 两条 pipeline 跑在独立的线程上，每个 step 并发执行，然后汇总结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KV 传输部分用了独立的 transfer stream，目标是和 compute stream 尽量overlap，减少等待时间，代码里用 torch.cuda.Event 来同步两个 stream之间的依赖，实现上比较简单，没有做精细的 pipeline 重叠，实际测下来overlap 的效果取决于传输数据量和计算量的比例，在小 batch 或短序列的时候效果不太明显。同时会自动检测当前硬件是否支持 P2P 直传，通过torch 的 _check_p2p 接口查询，不支持的话 fallback 到 pinned memory relay，也就是先把数据从 GPU 拷到 CPU 的 pinned memory，再从 pinned memory 拷到目标 GPU，这个路径的带宽会受限于 PCIe，在没有 NVLink 的多卡环境下（比如 8 张 4090 通过 PCIe 互联）这个开销是比较显著的，实测大约 12.9 GB/s，而有 NVLink 的 H20 可以达到约 392 GB/s，差了将近 30 倍，这个差距直接决定了路由判断在两种硬件上的结论会有多大的不同。&lt;/p&gt;
&lt;h2&gt;代价模型和路由&lt;/h2&gt;
&lt;p&gt;为了决定每个请求走合并路径还是分离路径，我实现了一个解析式代价模型，思路是先在真实设备上跑 micro-benchmark 测四个参数，prefill 速度α（ms/token）、decode 单步延迟 β（ms）、prefill 对 decode 的干扰系数 γ（ms/token）、以及 GPU 间传输带宽 bw（GB/s），然后用这四个参数建立两条路径的延迟估算公式，每个请求来了之后实时计算预估延迟并选更小的那条，所有参数都来自实测，没有用论文里的理论值，因为不同的硬件和软件环境下这些数字差异很大，直接用实测值比从理论推导更可靠。&lt;/p&gt;
&lt;p&gt;profiling 的过程大概是：prefill latency 用不同长度的 prompt 各跑若干次取中位数，用线性回归拟合得到 α；decode latency 在不同的 KV 长度和 batch size 下各测一遍，用来拟合 β 和 batch_thresh（decode 从内存带宽瓶颈切换到算力瓶颈的拐点）；interference 通过对比纯 decode和混合 prefill+decode 下 decode 延迟的差值来测量，用线性回归拟合得到 γ；P2P 带宽直接测一次大块数据传输的时间来估算。&lt;/p&gt;
&lt;p&gt;路由判断最后可以化简成比较两个都正比于 prompt 长度 L 的量，分离路径相比合并路径的额外代价是传输 KV cache 的时间，大约是 transfer_rate× L，合并路径相比分离路径的额外代价是 prefill 对并发 decode 的干扰，大约是 γ × L × (system_load / batch_thresh)，分离更划算的条件可以化简为 γ / transfer_rate &gt; batch_thresh / system_load，其中γ / transfer_rate 是一个只取决于硬件的比值，反映了&quot;干扰有多贵&quot;相对于&quot;传输有多贵&quot;的比例，而 batch_thresh / system_load 则反映了当前系统的负载程度。&lt;/p&gt;
&lt;p&gt;在 RTX 4090 上实测，γ / transfer_rate ≈ 7.6，代入 batch_thresh = 16可以得到 system_load ≥ 约 2.1 时分离就开始划算，也就是只要有两到三个并发请求在跑，分离路径在延迟上就已经比合并路径更好了；而在H20 上这个比值达到 346，几乎在任何非零负载下分离都是更优的选择，两种硬件上的结论差距这么大主要是因为传输带宽差了将近 30 倍，γ 的差异相对没那么大（0.087 vs 0.130 ms/token），所以比值主要是被transfer_rate 拉开的。这个结论确实感觉很不合理，直接原因就导致路由决策非常单一，理论上能充分展示路由决策的机器本人无钱无时间找到（谁来帮我考算分期中！），于是懒得改了。&lt;/p&gt;
&lt;p&gt;此外还实现了一个输出长度预测器，因为路由需要估算 decode 阶段的代价，而 decode 代价正比于输出长度，但输出长度在请求来的时候是未知的，用了一个在线的贝叶斯预测器，按 prompt 长度分桶，每桶内维护历史输出长度的统计，新请求来了用对应桶的均值作为预测，桶内样本不足时fallback 到全局均值，这部分比较粗糙，输出长度预测本身就是一个很难的问题，实际上即使是 vLLM 这样的成熟系统目前也还没有特别好的解法，这里的实现只是为了让路由可以跑起来，准确性有限。&lt;/p&gt;
&lt;h2&gt;测试和结果&lt;/h2&gt;
&lt;p&gt;写了一个 Poisson 到达过程的benchmark，模拟真实服务里请求按泊松过程到达的场景，固定到达率 λ，跑固定时长，然后统计完成的请求数、端到端延迟的分布、以及 drop 的请求数，这比简单的串行测试更接近实际的服务场景，因为串行测试本质上是测单个请求的延迟，没有并发，没有排队，和真实负载差距比较大。&lt;/p&gt;
&lt;p&gt;结果上，在 RTX 4090 × 8 的环境下，adaptive 路由在中等到达率下的吞吐和延迟比 collocated 有一定改善，在 H20 上分离路径的优势更明显，和代价模型的预测基本一致，但实际的性能数字和成熟框架比是没有可比性的，因为缺少了 CUDA Graph、Flash Attention、算子融合等优化，这里就不列具体数字了，主要是看各策略之间的相对趋势是否符合理论预期，从这个角度来看结果是比较合理的。
性能出现巨大问题的原因还有disagg路径没有像collocated做那么多小优化，such as cotinious batching等等，在项目的文档中我做了具体说明。&lt;/p&gt;
&lt;p&gt;另外一个有意思的观察是，在 H20 上 adaptive 的峰值吞吐反而低于RTX 4090，原因是 H20 的 decode 步更快（β = 33ms vs 51ms），更多请求在 collocated GPU 上就快速完成了，disaggregated 路径的利用率相对
更低，多卡的优势没有完全发挥出来，这有点反直觉，但想清楚之后也是合理的，更快的 decode 反而减少了分离的必要性，加之我较为naive的paged attention实现（肉眼可见有一处就可以改为reduce求和），使得max_request_num调大了就会影响TTFT等乱七八糟的事，本人又买不起autodl的H20 * 4呜呜呜，就这样吧。&lt;/p&gt;
&lt;h2&gt;一些感受&lt;/h2&gt;
&lt;p&gt;实现上没有做 CUDA Graph、Flash Attention 或量化这些会显著影响性能的优化（实际上懒了写不了了），主要是想保持每一层的逻辑足够清晰，方便对照论文理解设计决策，也方便文档解释，所以性能上和成熟的推理框架没有可比性，仅仅是作为理解系统设计的实践项目。&lt;/p&gt;
&lt;p&gt;写完之后最大的收获可能不是哪个具体的技术细节，而是对整个系统各层之间的依赖关系有了更清晰的感受，代价模型的准确性依赖 micro-benchmark 的质量，micro-benchmark 又依赖引擎本身的实现是否足够接近真实推理路径，路由的效果依赖输出长度预测的准确性，调度器的策略影响 profiling 时测出来的 interference 数值，这些依赖关系在读论文的时候是模糊的，实现一遍之后会更具体地感受到每一层的假设在什么条件下成立，什么条件下会失效，以及各层之间的耦合程度，这种感受很难单纯通过读代码或读论文获得。&lt;/p&gt;
&lt;p&gt;已经放到了github上，文档里对每层的设计决策有比较详细的说明。&lt;/p&gt;</content:encoded><h:img src="/_astro/image.DnGvFd5Q.png"/><enclosure url="/_astro/image.DnGvFd5Q.png"/></item><item><title>3D Reconstruction Series</title><link>https://www.hjcheng0602.cn/blog/3dreconseries/3dreconseries</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/3dreconseries/3dreconseries</guid><description>一个长期更新的3D recon(或许还有generate)论文的浅要阅读(已完结)</description><pubDate>Sun, 15 Mar 2026 08:18:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;更新，已决定停止更新（x&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;可以看到本文的publishDate是4096-16-64, 实际上的publishDate是2026-02-10。
本文的初衷是一个长期更新的3D recon系列论文阅读，之前其实已经发过了一些该领域的论文的精读了，但是显然精读必然是不可长期持续的。因此，我想以本文——一个系列的形式记录对大多数论文的浅要阅读，当然如果有特别重要的论文，我也会单开一篇文章进行精读的。&lt;/p&gt;
&lt;p&gt;本文的cover image是一个词云，记录了本文包含的工作的名称，希望它能不断地更新，成为一个3D recon领域的词云图谱。&lt;/p&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;
&lt;p&gt;import {Spoiler} from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;CUT3R&lt;/h2&gt;
&lt;p&gt;CUT3R的输入是视频序列，但是也可以unordered（据作者所言训练的时候是无序训练的，但是推理的时候推理的时候是dataloader先计算重合率来进行初步排序。），使用一个feed forward网络预测camera parameters和点云。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./CUT3R.png&quot; alt=&quot;cut3r&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后是一个recurrent模型，每一帧输入的时候添加一个pose token然后经过encoder和decoder，之后使用交叉注意力更新$s_t$ 和 $F_t$，之后再使用不同head来从$s_t$和$F_t$中提取output。&lt;/p&gt;
&lt;p&gt;显然这样缺少修正，对于长序列容易造成偏移。但是作者似乎也提到了一个revisit机制，在输入结束之后拿着全局的$s$来做之前的预测，在7scene上的acc和comp是有改善的，但是NRGBD不怎么明显。&lt;/p&gt;
&lt;p&gt;此外，作者也说因为数据集质量的原因，采用的head即使已经有一个pose head和local points head，也仍然要加入一个world ptshead（缺乏高质量的数据集）。
&lt;/p&gt;
&lt;h2&gt;$\pi ^ 3$&lt;/h2&gt;
&lt;p&gt;$\pi ^ 3$ 是一个相对来说比较有趣的东西，模型结构如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Pi3.png&quot; alt=&quot;pi3&quot;&gt;&lt;/p&gt;
&lt;p&gt;首先与之前的最大不同是它没有显式地选取参考帧和一个特定的scale factor，像VGGT就是先选取了一个ref frame然后做重建，但是重建质量受ref影响很大，因此$\pi^3$选择了一个方案，就是一次性将所有帧全部输入，所有帧之间均平等，然后inference出一组相对位姿和局部点云，这样就能规避确定某一个frame作为坐标原点造成的不确定性问题。&lt;/p&gt;
&lt;p&gt;但是仔细一想，$\pi^3$仍然不怎么好避免一个ref的问题，首先，在一个batch内部，虽然我们预测的是一组相对位姿，但是直觉上感觉仍然是把&lt;strong&gt;某一帧与其他帧不融洽&lt;/strong&gt;所导致的原先的那种&lt;strong&gt;大的，显著的，偶然性的&lt;/strong&gt;损失转化为了现在的&lt;strong&gt;看起来不明显的、高一致的、所有帧都有的&lt;/strong&gt;系统性损失。但作者通过实验证明了损失会变小，其实这也是比较好解释的，因为原先的可能是$T_2$依赖$T_1$，$T_3$依赖$T_2$……这种&lt;strong&gt;单向参考&lt;/strong&gt;，而$\pi^3$则进行了&lt;strong&gt;交叉注意力计算&lt;/strong&gt;，仔细想来确实会更好。&lt;/p&gt;
&lt;p&gt;其次，交叉注意力的复杂度大概是$O(n^2)$，显然对于长序列是不可接受的，作者训练和测试的时候均采用了有限个batch内frame的做法，但对于实际的长序列的话，感觉并不是很好做。如果切片进行拼接的话，显然也会面临ref的选择问题，但是这时候是一个scene之间的拼接，感觉确实会降低很多错误，如果分层做的话，也会降低误差，总之感觉似乎确实是一个不错的方案。
&lt;/p&gt;
&lt;h2&gt;DA3&lt;/h2&gt;
&lt;p&gt;DA3是字节seed的一个项目，可以说是力大飞砖，充分体现了工业界解决问题的规模（x。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./DA3.png&quot; alt=&quot;da3&quot;&gt;
DA3的主要创新点在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;更简单的模型&lt;/strong&gt;，作者的意思是VGGT即使结构很简单，但是由于其在DINO后接AA层的操作，因为AA layers是新训练的，因此过程中可能数据的利用率不高。而DA3选择了只利用DINO这一个方案，通过在DINO的$L_g$层中变形数据完成了AA层所做的事情。因此，DA3的几乎所有参数都是预训练过的，而vggt则有$\frac{2}{3}$ 的参数是从头开始训的，这是DA3的简洁之处。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;预测任务的简洁性&lt;/strong&gt;。相比于VGGT通过不同head得出了不同结果，DA3则使用了一个更新的表达方式：Ray-depth表达，具体来说就是使用一个Dual head来分别输出一个像素的深度信息和光心与之相连的射线的信息，从而天然地同时包含了点云和pose信息，而且在设计loss的时候是可以加入一致性信息的。相比与vggt，这似乎加强了一致性，也提高了数据利用率，感觉pose和pts3d反而是不容易加入一致性的，作者做的消融实验也证实了这一点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用teacher标定数据&lt;/strong&gt;，首先训了一个teacher模型用于给深度不好的frame重新生成depth，之后依照这个depths训练。感觉最终效果也很依赖这个teacher模型。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是，DA3的弊端也有一些，他的效果确实非常好，但是阅读之后才发现他是用128 x H100训练的，这个规模确实有点难以复现。小算力情况下上面两条结论似乎很有帮助，可以尝试。
&lt;/p&gt;
&lt;h2&gt;MapAnything&lt;/h2&gt;
&lt;p&gt;首先是Meta的项目，和VGGT难道不构成什么竞争关系嘛（）&lt;/p&gt;
&lt;p&gt;主要创新点在于他的输入很有意思，不同于VGGT还有以往的重建工作只输入图像序列，MapAnything 支持多种多样的输入，对于每一个输入都会通过一个encoder最后对齐到DINOv2输出的image token上，然后就是正常处理的流程，不过似乎它多加了一个scale token，用于预测scale信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./MapAnything.png&quot; alt=&quot;mapanything&quot;&gt;&lt;/p&gt;
&lt;p&gt;感觉其利用了nlp里面的多模态，证明了给定不同类型的输入其预测的准确性与相应的专家模型性能相似，这是很有价值的，因为他减少了很多训练量（虽然也是在64xH200上训了10天）。&lt;/p&gt;
&lt;p&gt;另外一个比较有趣的地方在于，他最后的点云数据不是直接输出的，而是由depth，ray，pose联合输出，这解耦了VGGT的冗余预测模式，而且在设计loss的时候能保持更好的一致性，感觉这个跟DA3输出Depth-ray的做法还是很像的。&lt;/p&gt;
&lt;p&gt;不过其缺点也非常明显，首先对于长序列情况下，其仍然没有摆脱$O(n^2)$的处理复杂度；其次模型是offline的，不过感觉各有各的应用场景；最后就是推理速度和显存占用，推理速度在100frame的时候就已经接近10s，而且这时的显存占用也已经来到了65G左右，即使采用了作者提出的Mem Efficent策略，即在dpt头采用串行计算策略也是20G左右，似乎有点太大了（x&lt;/p&gt;
&lt;p&gt;此外，作者表示了在输入过程中模型无法对噪声数据进行处理，也就是说潜在的噪声可能会污染整个transformer的内容，另外融合时机是在encoder之后进行，而且是简单的相加，可能有更精细的融合方式。&lt;/p&gt;
&lt;h2&gt;AnySplat&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./AnySplat.png&quot; alt=&quot;anysplat&quot;&gt;&lt;/p&gt;
&lt;p&gt;与之前讲过的大多数点云重建的工作不同，AnySplat是3dgs重建。具体来讲就是他在vggt的基础上进行改造，backbone与vggt相同，但其head则是一个gaussian head, 一个depth head，还有一个Camera head。然后通过一个可微体素化将原本稠密的高斯球聚合到一起，训练的时候则监督：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;每一帧位置的rgb loss&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;depth的深度与gaussian depth的差异损失&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;相机参数与vggt预测出的损失&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模型预测深度与vggt之间的深度差异&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先，2的loss保证了其几何一致性，也就是让不同视角的深度尽量保持一致，可以避免分层现象。此外，文章作者说他们实现了一个Differentiable Voxelization，可以有效解决生成的稠密高斯球产生的复杂度问题。&lt;/p&gt;
&lt;p&gt;总体来说，这是一个高度模仿vggt的工作，只不过换了一下head和输出形式，其余部分都差不多。此外为offline的重建，看上去速度似乎还可以，但是同样面临长序列问题。另外，固定世界坐标系为第一张图片，去监督每一个绝对位姿是否正确，似乎也是存在$\pi ^ 3$所述的归纳偏置问题的。&lt;/p&gt;
&lt;h2&gt;RayZer&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./RayZer.png&quot; alt=&quot;rayzer&quot;&gt;&lt;/p&gt;
&lt;p&gt;令人耳目一新的自监督模型，训练过程只需要图片而不需要gt的pose和内参，训练过程大概是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先输入$K$张图片，将其分为$L_a$ 和$L_b$两个集合。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后模型通过Camera Estimator模块，预测出pose和intrinsics。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之后对于$L_a$ ，模型根据其对应的预测出来的$R_a$ 和本身的图片输入，生成场景的token$z$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后对于$L_b$，模型选择通过$z$和$R_b$ $L_b$ 预测出$\hat{L}_b$ 然后监督$\hat{L}_b$ 与$L_b$ 之间的损失，然后更新所有的值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，推理时的大致步骤大概就是先把场景的已知几张图片输入得到$z$ ，之后针对一个特定的pose，计算一个光线图，之后输入到rendering decoder里得到在这个特定的pose下的rgb图片。&lt;/p&gt;
&lt;p&gt;感觉和nerf好像，都是一个隐式的表达整个场景，不过不同的是RayZer是一个更直接的模型，图里的三个模块每个都是8层naive transformer，loss仅由最后的rgbloss 和LPIPS loss决定，感觉挺聪明的。不过感觉rendering部分采用的表现形式——类raymap形式似乎真的挺好用的。&lt;/p&gt;
&lt;p&gt;另外，值得注意的是第一部分，在预测pose和intrinsics时，直接选取了中间帧作为参考帧，使得模型能跨越更长的距离。此外，如果说我们在第一部分就引入$z$ ，能否实现定位功能？不过作者似乎做了消融实验，发现在训练的时候，从图像特征中提取几何关系比从一个未成形的$z$ 中提取容易得多。但是我觉着可以在rendering部分再添加一个decoder用于定位。&lt;/p&gt;
&lt;p&gt;另外，这个模型完全打败了LVSM（一个有监督的模型），感觉是一个非常惊艳的工作，看项目主页的demo视频感觉真的很不错啊。&lt;/p&gt;
&lt;h2&gt;Spa3R&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./Spa3R.png&quot; alt=&quot;spa3r&quot;&gt;&lt;/p&gt;
&lt;p&gt;首先是一个自监督的模型，模型的backbone设计的有点复杂：&lt;/p&gt;
&lt;p&gt;我们给出一个场景的views，然后将views分为context view和target view，首先将所有views通过一个改造过的vggt（似乎是只引入了head之前的部分），改造内容是在context Views的AA层那里把 Target Views给mask掉，然后得到Context Views的feature $F_c$ 和Target Views的camera token和Feature $F_t$ ，之后，数据流向两条路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Context views ：$F_c$ 与一组可学习的$q$ 通往Encoder，然后得到$z$ 作为空间的隐式表征。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Target views ：camera tokens通过camera head生成camera embeds $r$ ，然后与$z$ 一起输入到Decoder里生成对Target views的预测过的feature $\hat{F}_t$ ，然后将得到的预测feature与$F_t$ 进行监督得到loss。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推理的阶段我们就只看Context Views得到的$z$ ，将$z$与qwen2.5 vl 得到的$F_V$ 输入到一个&lt;strong&gt;Adapter&lt;/strong&gt;里，然后将这个adapter和text prompt输入到llm里得到最终结果。&lt;/p&gt;
&lt;p&gt;首先，肉眼可见这项工作把大量的其他工作缝合到了一起，Target View阶段用了DINOv3和VGGT，$z$ 的后续处理用到了qwen2.5 vl，但是这篇文章叫Spa3R啊，Dust3R被放到哪了呢？然后可训练的内容只有Encoder和Decoder，仅6层Transformer，而且通过两个$F_t$作为loss进行训练，训练结束之后即丢弃Decoder，保留训好的Encoder和q。然后后续还有一个针对Adapter的一个微调，让其学到怎么生成一个合理的融合$F_{input}$ 。&lt;/p&gt;
&lt;p&gt;模型做了几个消融实验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Target Views阶段作者证明了同时使用VGGT和DINO会更好（包含语义和空间信息），这是一个比较显然的结论。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提取出一个场景$z$ 表征是一个更好的手段，相对于现有的几个类似于VG-LLM简单把所有特征输入到llm里效果更好（但是只提升了3个点，感觉有点低于预期，考虑到第二阶段训练只进行了1个epoch，有没有可能是训练量不够？我也是第一次读VLM相关的文章（），不过看具体的比分，Multi-Choice涨分了，而Numerical几乎没变，确实是make sense的）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pose embedding的影响，PRoPE比plucker更好。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mask Ratio，这也是一个比较显然的消融实验。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Adapter使用提高了点数，比较make sense。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;模型只在ScanNet和ScanNetpp上进行了pre train，使用了8张5090进行训练，在VSI-Bench上达到了58.6的水平，超过了之前的大部分model，查看现在的VSI-Bench Leaderboard，其性能也是处于前列的（不过论文里的表格好像有些数据有点不对？可能有更新吧）。算是为领域开了一个新坑（），自监督看上去也不错（）。&lt;/p&gt;
&lt;p&gt; 看上去这篇文章正在投CVPR，是笔者写阅读笔记的两天前才登上了arxiv，也不知道中没中，方法是很有趣 &lt;/p&gt;
&lt;h2&gt;Spann3R&lt;/h2&gt;
&lt;p&gt;结构很复杂，首先大部分模型权重继承自Dust3r，然后模型的backbone大致如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Spann3R.png&quot; alt=&quot;spann3r&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;预编码&lt;/strong&gt; ：首先将一帧输入到ViT Encoder得到一个$f_t^I$ ，此时我们手上还有一个上一帧的$f_{t-1}^Q$ 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查询记忆：&lt;/strong&gt; 根据$f_{t-1}^Q$ ，我们可以从历史记忆中查询出一个$f_{t-1}^G$ 来作为下一步的输入。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;主要推理部分：&lt;/strong&gt; 之后我们将这两个feature输入到Target Decoder和Reference Decoder，这两个Decoder会做self attention和Cross attention然后分别得到$f_t^{H’}$ 和$f_{t - 1}^{H}$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Heads：&lt;/strong&gt; 对于$f_t^{H’}$ ，在推理阶段我们会使用一个query head来提取出$f_t^{Q}$，然而在训练阶段我们也会加入一个head将其转化为点云和置信度来监督训练；对于$f_{t-1}^H$ ，我们会通过一个reference head将其重建出点云和置信度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;记忆：&lt;/strong&gt; 之后，根据$f_{t-1}^H$和$f_{t-1}^I$ ，我们将其通过一个Memory encoder + MLP head生成一个$f_{t-1}^K$ ，然后根据这个和点云通过一个Memory Encoder生成 $f_{t-1}^V$ ，之后$f_{t-1}^K$会对已有记忆去重， 如果工作记忆已满剩下的就会进长期记忆然后做进一步处理。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是一篇24年的文章了，主要创新点就在于他改良了Dust3R，使得可以对多个图片输出一个一致的全局坐标系下的点云，此外使用记忆方法，分层处理记忆。&lt;/p&gt;
&lt;p&gt;但很显然的是，虽然该方法加入了记忆，但是记忆看上去也是近期记忆的方案，客观上因此而存在长距离漂移的现象，此外，如果遇到reloop现象，记忆是否能健康提取也会是一个比较大的问题。&lt;/p&gt;
&lt;p&gt;做的消融实验大致有这几个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;关于记忆方面的消融实验，去掉长期记忆会引起很大的漂移现象，而注意力不截断的话也会引发噪声的干扰&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关于长期记忆应该取多大：作者发现1000-2000token的过程中漂移得到极大修正，但是4000+之后就不会有明显的提升，因此最后作者选择了4000.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dust3R采用了exp confidence function，本文将其改为了sigmoid，事实证明是有所改善的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Flow4R&lt;/h2&gt;
&lt;p&gt;一个局限性很大的三维重建追踪方案，不过在表现形式上很有新意。&lt;/p&gt;
&lt;p&gt;模型的backbone很优雅，首先接收两张图片作为输入，通过共享权重和cross attention的两个对称encoder-ecoder-head结构得到每张图的${ P,F,W,C}$ 其中， $P$是相机坐标系下的点云，$F$ 是一个场景流，描述每一个像素如何从本张图片移动到下一张点云，之后还有一个$W$ 指示哪个像素在求解pose的时候最可靠，最后的$C$ 是全局的置信度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Flow4R.png&quot; alt=&quot;flow4r&quot;&gt;&lt;/p&gt;
&lt;p&gt;得到这些元素之后，可以首先将pose通过最小二乘法求出：&lt;br&gt;
$\hat{T} = \arg \min_{T \in SE(3)} \sum_{i=1}^{HW} W^i ||P_{vt}^i - T P^i||_2$&lt;/p&gt;
&lt;p&gt;$P_{vt}$ 是由$P + F$ 得到的，得到pose之后就可以做位姿流和场景流的分解，然后很多下游任务就可以进行处理了。&lt;/p&gt;
&lt;p&gt;针对于长序列数据，作者提出了将第1张frame作为锚点，后续的每一张都与之输入处理，好处是可以通过L2 norm来归一化尺度，但是坏处也非常明显，一是稍微长一点的序列，就会出现遮挡现象，模型目前来看没有一种很好的应对方式；二是极其依赖第一张frame的质量，鲁棒性不算太好。观察其论文里呈现的demo，看起来也通常是对一个角落or一个相似视角区域做的重建，完整场景重建效果存疑。&lt;/p&gt;
&lt;p&gt;此外，作者竟然只做了一个消融实验（能中吗？）对比了三种不同的网络预测和监督变体 ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;预测场景流 $F$ ，并用真实的 $F$  进行损失监督 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;预测场景流 $F$ ，但用目标帧的真实 3D 点位置 $\overline{P}_{vt}$  进行监督 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直接预测目标帧的 3D 点位置 $P_{vt}$ ，并用真实的 $P_{vt}$  监督（场景流则通过简单的减法推导：$F = P_{vt} - P$ ） 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;消融结果&lt;/strong&gt;：实验证明，&lt;strong&gt;直接预测并监督&lt;/strong&gt; $P_{vt}$ &lt;strong&gt;的性能最佳&lt;/strong&gt; 。因此后来直接预测的实际上是$\mathcal{P} = {P, P_{vt}, W, C}$&lt;/p&gt;
&lt;p&gt;总体来说，这篇工作证明了一点，可以通过引入流的方式来完成Dust3R这种结构从静态到动态的拓展，但确实局限性很大。&lt;/p&gt;
&lt;p&gt;这项工作似乎还没有开源（）&lt;/p&gt;
&lt;h2&gt;AMB3R&lt;/h2&gt;
&lt;p&gt;把三维体素引入到了重建中，使得模型能够真正地从空间角度来考虑重建任务。简而言之就是之前的重建采用的ViT将图像分为一个一个patch造成隐式几何中缺乏空间紧凑性约束，于是论文作者想了一个办法把空间紧凑性加入到了backbone当中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./AMB3R.png&quot; alt=&quot;amb3r&quot;&gt;&lt;/p&gt;
&lt;p&gt;大致的backbone分为前端和后端，其中，前端继承了VGGT的网络和参数，一张图片进入之后会经过Encoder得到一个初步的feature，然后数据的主题是向decoder移动，但是这部分feature也会使用一个scale head预测一个绝对尺度。&lt;/p&gt;
&lt;p&gt;然后，进入decoder的feature会对keyframes做cross attention，这里的keyframe就可以理解为场景的隐式表达，经过该过程之后，decoder就会输出一个pointmap和一个confidence，在推理阶段，之后会有一个门控机制：如果置信度足够高，那就直接进入下一阶段，反之则会将点云和feature变为体素，然后通过一个point transformer优化该体素的feature，之后再会逆变换变为 2Dfeature，之后我们会将该feature注入到前端的decoder中，重新拿到一个高级的点云。&lt;/p&gt;
&lt;p&gt;然后我们拿到了当前帧的点云以及物理尺度，然后系统会将该结构放大/缩小，然后根据keyframes和VGGT预测出的pose将该结构拼接到大的全局点云中，最后我们会评测该点云是否可以成为keyframes，然后将其处理掉。&lt;/p&gt;
&lt;p&gt;将体素引入到点云重建里很厉害，作者做的几个消融实验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;移除了基于sparse voxel 的后端，转而使用一个2D做alternate attention的后端，发现精度不如之前。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;去除了零卷积机制，发现模型短时间内根本就未收敛。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在算loss的时候去除了scale发现效果变差，也就是说模型需要去专注思考几何结构。这是在训练阶段做的事情&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这篇文章的训练成本非常非常的低，依赖于一个已经训好的VGGT，只训练了微调点云特征的一个point Transformer和一堆head，感觉非常有启发性非常厉害，同时也中了CVPR2026，符合预期（似乎是Spann3R的续作）&lt;/p&gt;
&lt;h2&gt;VGGT-SLAM&lt;/h2&gt;
&lt;p&gt;我说这是一篇数学论文，文中没有训练任何模型，仅仅是介绍了一种局部点云拼合办法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./VGGT-SLAM.png&quot; alt=&quot;vggt-slam&quot;&gt;&lt;/p&gt;
&lt;p&gt;顾名思义，这篇工作基于VGGT输出的点云和pose，作者认为VGGT预测出pose和局部点云之后直接进行Sim(3)变化为全局点云是有问题的，主要灵感来自于传统CV里面的双目立体视觉：相机之间的单应性矩阵或者说是本征矩阵并非仅仅包含了pose中进行的旋转、平移，更有一些拉伸，透视等等等。具体来讲就是VGGT预测出的点云深度包含了相机的射影形变，直接使用Sim(3)方法来还原是不准确的。&lt;/p&gt;
&lt;p&gt;因此，作者转而使用了SL(4)进行点云的对齐，具体来讲，当VGGT得出了点云和pose之后，会进行以下几个操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于一个子地图里的帧，作者选择相信VGGT的质量，作者在代码里设置了一个submap_size参数用于控制子地图的大小。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于不同子地图之间，因为我们想得到一个在不同坐标系下共享的三维点，所以作者这里采用了一个很聪明的办法，将上一个子地图的最后一帧重复输入到下一个子地图里，这样VGGT的输出就包含了相同图片在不同坐标系下的点云，由此可以建立点与点之间的对应关系。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之后根据传统的一些算法，可以计算两个子地图之间的SL(4)矩阵，到这里第一步就算完成了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下一个步骤就是全局对齐，作者也写得太数学了吧：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;具体来讲，作者构建了一个基于最大后验估计的非线性因子图，目标是最小化所有子地图之间的相对单应性误差：&lt;/p&gt;
&lt;p&gt;$\hat{H}=argmin_{H\in SL(4)}\sum_{(i,j)\in\mathcal{L}}||Log(H_{i}^{-1}H_{j}(H_{j}^{i})^{-1})||&lt;em&gt;{\Omega&lt;/em&gt;{ij}^{H}}^{2}$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后引入各种优化器，这里我的数学太烂了（x）根本看不懂，只知道他是需要迭代优化的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;嗯嗯，所以这样我们就可以得到一个后端，对于每一个子地图，都给出了一个将其变换到潜在全局坐标系下的SL(4)矩阵，从而消除了Sim(3)变换带来的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外，文章还提出了一种reloop机制，就是说在一个子地图待输入的时候，系统会利用SALAD描述子去寻找历史子地图中是否有相似的图片，若有，系统就会选择将那张图片作为共享帧，我们这时候就会有多个相对的信息。&lt;/p&gt;
&lt;p&gt;总体来说，这篇工作就是提供了一个偏传统的对齐方法，比较优雅，但是很显然缺点也很明显，首先对于单个子地图，该工作完全信任VGGT的输出结果，缺乏鲁棒性；其次，其得出对齐是通过迭代优化得出，相对于直接拼接会慢上很多，另外有太多的查询操作（如reloop），感觉复杂度还是有点高的。&lt;/p&gt;
&lt;p&gt;不过可以从上图看到，他确实改善了点云拼接时可能产生的分层的质量。但是，查看其github里的issue，似乎稳定性存疑：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Due to potential randomness in our approach caused by RANSAC, we report the average performance over five runs, which have a low spread (small standard deviation) as shown in Sec. 5.5.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而且那个issue到最后作者都没有回答，感觉有点尴尬（x&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.Ck_Fv-M0.png"/><enclosure url="/_astro/cover.Ck_Fv-M0.png"/></item><item><title>学习笔记：Tensor Parallelism（TP）</title><link>https://www.hjcheng0602.cn/blog/tensorparallel/tp</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/tensorparallel/tp</guid><description>本文是读完Megatrion-LM及其他相关博文之后，对Tensor Parallelism（TP）的理解和总结，用于未来查阅</description><pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;引言&lt;/h1&gt;
&lt;p&gt;经历了一些对未来选择的思考之后，最近在了解mlsys相关的内容，本文即为对TP的理解和总结，目前网上已经有大量的博文详细介绍了TP的实现细节，本文主要是为了自己未来查阅方便而写的文章，欢迎大家指正。&lt;/p&gt;
&lt;h1&gt;TP简介&lt;/h1&gt;
&lt;p&gt;Tensor Parallelism是在DP, MP之后提出的一个方法，由Magatrion-LM首创。其出发点在于DP, MP仍然需要单卡在计算时凑齐一个完整的layer的参数和各种激活值、梯度、优化器状态，当一个layer过大的时候，单卡就放不下了。
而Tensor Parallelism将模型的计算拆成分布式的了，使得一层能够分布于不同卡上进行计算。&lt;/p&gt;
&lt;h2&gt;Transformer-like model&lt;/h2&gt;
&lt;p&gt;一个经典的Transformer 模型的架构大致如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Transformer_arch.png&quot; alt=&quot;transformerarch&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，一个layer主要由Attention和MLP层组成，TP的关键优化点也就是在这两层上，下面将具体说明。&lt;/p&gt;
&lt;h2&gt;MLP&lt;/h2&gt;
&lt;p&gt;我们先从MLP层开始，简而言之，一个MLP层的数学描述大致这样：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{Out} = \mathrm{Dropout}(\mathrm{GeLU}(X W_1) W_2)
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
X &amp;#x26;: (B, S, d_{\text{model}}) \
W_1 &amp;#x26;: (d_{\text{model}}, d_{\text{ff}}) \
W_2 &amp;#x26;: (d_{\text{ff}}, d_{\text{model}})
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;一般来说，$d_{\text{ff}} = 4 \times d_{\text{model}}$&lt;/p&gt;
&lt;p&gt;我们先考虑不进行TP，仅仅进行单卡计算：&lt;/p&gt;
&lt;h3&gt;单卡forward&lt;/h3&gt;
&lt;p&gt;参数量：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
W_1 &amp;#x26;: d_{\text{model}} \times d_{\text{ff}} \
W_2 &amp;#x26;: d_{\text{ff}} \times d_{\text{model}} \
\text{总参数} &amp;#x26;: 8 d_{\text{model}}^2
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;计算量：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
&amp;#x26;\text{两个矩阵乘均贡献 } 2 \times B \times S \times d_{\text{model}} \times d_{\text{ff}} \
&amp;#x26;\mathrm{FLOPS} = 16 \times B \times S \times d_{\text{model}}^2
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;激活量：在backward里考虑。&lt;/p&gt;
&lt;h3&gt;单卡backward&lt;/h3&gt;
&lt;p&gt;首先对dropout反向：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial L}{\partial Z} = \frac{\partial L}{\partial \mathrm{Out}} \odot \frac{\mathrm{mask}}{1 - p}
$$&lt;/p&gt;
&lt;p&gt;这一步的FLOPS差一个数量级，可忽略不计，另外使用 $LX$ 表示 $\partial L / \partial X$。&lt;/p&gt;
&lt;p&gt;然后对 $W_2$ 进行反向：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
LW_2 &amp;#x26;= A^T \cdot LZ \
LA &amp;#x26;= LZ \cdot W_2
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;其中 $A = \mathrm{GeLU}(\cdots)$。&lt;/p&gt;
&lt;p&gt;这一步的FLOPS为 $2 \times (2 \times B S , d_{\text{model}} , d_{\text{ff}}) = 16 , B S , d_{\text{model}}^2$&lt;/p&gt;
&lt;p&gt;GeLU的FLOPS几乎也可以忽略不计。&lt;/p&gt;
&lt;p&gt;然后对 $W_1$ 进行反向，几乎与 $W_2$ 相同。&lt;/p&gt;
&lt;p&gt;因此整个过程的FLOPS为 $32 , B S , d_{\text{model}}^2$，为前向传播的两倍。&lt;/p&gt;
&lt;p&gt;然后我们从激活值占用角度分析，在没有梯度检查点的情况下，我们有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X           (BS, d_model)       use for compute L_W1
H = XW_1    (BS, d_ff)          use for compute the gelu 
A = GeLU(H) (BS, d_ff)          use for compute L_W2 
Dropout mask(BS, d_model)       use for Dropout
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;TP forward&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./mlp.png&quot; alt=&quot;sd&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们进行这样的切分方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;W_1 -&gt; (W_11, W_12, W_13, ... W_1n)  # W_1i : (d_model, d_ff / n) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们在输入X的时候全部注入，然后得到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;H -&gt; (XW_11, XW_12, XW_13, ... XW_1n) # XW_1i : (B, S, d_ff / n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得注意的是，我们选择按列切分 $W_1$ 使得我们得到的结果是可以独立通过gelu的，省去了这一步通信的麻烦。&lt;/p&gt;
&lt;p&gt;之后考虑 $W_2$&lt;/p&gt;
&lt;p&gt;我们选择将 $W_2$ 进行这样的切分：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;W_2 -&gt; [
    W_21,
    W_22,
    W_23,
    ...
    W_2n 
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，显然我们现在可以每张卡计算&lt;code&gt;XW_11 @ W_21&lt;/code&gt;，而且他的形状就是最后矩阵的形状，
因此，我们算出来然后最后采用all reduce就可以得到最后结果啦。&lt;/p&gt;
&lt;p&gt;ok，我们现在对这整个过程进行分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数量：
显然，我们现在把所有参数分散到了多卡上，而且分散均匀，&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\begin{aligned}
W_1 &amp;#x26;: d_{\text{model}} \times d_{\text{ff}} \
W_2 &amp;#x26;: d_{\text{ff}} \times d_{\text{model}} \
\text{总参数} &amp;#x26;: 8 d_{\text{model}}^2 / n
\end{aligned}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算量：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\begin{aligned}
&amp;#x26;\text{两个矩阵乘均贡献 } 2 \times B \times S \times d_{\text{model}} \times d_{\text{ff}} / n \
&amp;#x26;\mathrm{FLOPS} = 16 \times B \times S \times d_{\text{model}}^2 / n
\end{aligned}
$$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;但是这里还要考虑一个问题，就是最后reduce-all操作还要对所有激活值进行累加，但是这部分数量级过小，可忽略。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;激活量：在backward里考虑。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;TP backward&lt;/h3&gt;
&lt;p&gt;在每张卡上的前向是：&lt;/p&gt;
&lt;p&gt;$$
Z^i = \mathrm{GeLU}(X W_1^i) W_2^i, \quad \text{AllReduce} \to Z = \sum_i Z^i
$$&lt;/p&gt;
&lt;p&gt;由于AllReduce之后每张卡上的Z完全相同，所以上游传回的梯度也完全一样，不需要额外通信。&lt;/p&gt;
&lt;p&gt;此后，每一步的计算基本上与单张卡相同，但是要除以 $N$。&lt;/p&gt;
&lt;p&gt;因此，每张卡的反向FLOPS为：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{FLOPS} = 32 \times B S , d_{\text{model}}^2 / N
$$&lt;/p&gt;
&lt;p&gt;然后，之后需要注意的是我们在反向传播的最后仍然需要一步all-reduce，因为我们此前计算的都是独立的梯度。&lt;/p&gt;
&lt;p&gt;激活值的占用：我们有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X           (BS, d_model)       use for compute L_W1
H = XW_1    (BS, d_ff / N)          use for compute the gelu 
A = GeLU(H) (BS, d_ff / N)          use for compute L_W2 
Dropout mask(BS, d_model)       use for Dropout
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Attention&lt;/h2&gt;
&lt;h3&gt;单卡forward&lt;/h3&gt;
&lt;p&gt;输入数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;X       (B, S, d_model) 
X_h     (B, S, h, d_head)      W_Q  (h, d_head, d_Q)    W_K (h, d_head, d_K)    W_V(h, d_head, d_V)
# 注意到在单卡情况下我们这一步计算Q, K, V通常不做维度划分，但可以这么理解，方便后续对TP的理解
Q       (B, S, h, d_Q)      K   (B, S, h, d_K)      V   (B, S, h, d_V)
Q @ K.transpose  -&gt; S   (B, h, S, S)
S @ V       -&gt;      (B, h, S, d_V)
reshape -&gt; (B, S, h *d_V)

# 接着引入一个W_O : (h * d_V, d_model)
O       (B, S, d_model)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ok，对整个过程清晰之后我们便可以分析其各个指标：&lt;/p&gt;
&lt;p&gt;参数量：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
W_Q &amp;#x26;: h \times d_{\text{head}} \times d_Q \
W_K &amp;#x26;: h \times d_{\text{head}} \times d_K \
W_V &amp;#x26;: h \times d_{\text{head}} \times d_V
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;通常来说这几个都相等，所以总参数为 $4 d_{\text{model}}^2$。&lt;/p&gt;
&lt;p&gt;计算量：&lt;/p&gt;
&lt;p&gt;| 操作 | 形状 | FLOPS |
|---|---|---|
| $X \to Q, K, V$ | $(BS, d_{\text{model}}) \times (d_{\text{model}}, d_{\text{model}})$ | $6 , BS , d_{\text{model}}^2$ |
| $Q K^T \to S$ | $(B, h, S, d_{\text{head}}) \times (B, h, d_{\text{head}}, S)$ | $2 , BS^2 d_{\text{model}}$ |
| $SV \to AV$ | $(B, h, S, S) \times (B, h, S, d_{\text{head}})$ | $2 , BS^2 d_{\text{model}}$ |
| $AV \cdot W_O$ | $(B, S, d_{\text{model}}) \times (d_{\text{model}}, d_{\text{model}})$ | $2 , BS , d_{\text{model}}^2$ |&lt;/p&gt;
&lt;p&gt;因此，总的FLOPS为：&lt;/p&gt;
&lt;p&gt;$$
8 , BS , d_{\text{model}}^2 + 4 , BS^2 d_{\text{model}}
$$&lt;/p&gt;
&lt;p&gt;需保存的激活值：&lt;/p&gt;
&lt;p&gt;| Tensor | shape | num |
|---|---|---|
| $X$ | $(B, S, d_{\text{model}})$ | $BS , d_{\text{model}}$ |
| $Q, K, V$ | $(B, S, d_{\text{model}})$ | $3 , BS , d_{\text{model}}$ |
| $S$ | $(B, h, S, S)$ | $BhS^2$ |
| $AV$ | $(B, h, S, d_{\text{head}})$ | $BS , d_{\text{model}}$ |&lt;/p&gt;
&lt;h3&gt;单卡backward&lt;/h3&gt;
&lt;p&gt;首先我们做 $W_O$ 的反向：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
LW_O &amp;#x26;= (AV)^T \cdot LO \quad (d_{\text{model}}, S, B) \times (B, S, d_{\text{model}}) \
LAV &amp;#x26;= LO \cdot W_O^T \quad (B, S, d_{\text{model}}) \times (d_{\text{model}}, d_{\text{model}})
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;加起来是 $4 , BS , d_{\text{model}}^2$&lt;/p&gt;
&lt;p&gt;然后我们回到 $AV$：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
LS &amp;#x26;= LAV \cdot V^T \quad (B, h, S, d_{\text{head}}) \times (B, h, d_{\text{head}}, S) \
LV &amp;#x26;= S^T \cdot LAV \quad (B, h, S, S) \times (B, h, S, d_{\text{head}})
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;这一步的FLOPS为 $4 , BS^2 d_{\text{model}}$。&lt;/p&gt;
&lt;p&gt;然后经过Softmax反向，FLOPS可以忽略，然后计算Q, K，FLOPS为 $4 , BS^2 d_{\text{model}}$。&lt;/p&gt;
&lt;p&gt;之后对 $W$ 做反向，权重梯度和输入梯度均为 $2 , BS , d_{\text{model}}^2$，共计为12。&lt;/p&gt;
&lt;p&gt;因此，总反向FLOPS为：&lt;/p&gt;
&lt;p&gt;$$
16 , BS , d_{\text{model}}^2 + 8 , BS^2 d_{\text{model}}
$$&lt;/p&gt;
&lt;p&gt;为前向的两倍，所以我们在这里也可以认为反向传播的FLOPS为前向的两倍。&lt;/p&gt;
&lt;h2&gt;TP&lt;/h2&gt;
&lt;p&gt;显然这时我们就可以完全将head分到多张卡上，所有的几乎均乘上一个 $\frac{1}{N}$ 即可。&lt;/p&gt;
&lt;p&gt;但此时仍然需要注意的是，我们得到O之后仍然需要all-reduce，这与mlp是一样的。&lt;/p&gt;
&lt;p&gt;先写到这里.&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.dWtetyyA.png"/><enclosure url="/_astro/cover.dWtetyyA.png"/></item><item><title>HPCGames 题解 D E 题</title><link>https://www.hjcheng0602.cn/blog/hpcgamesde/hpcgamesde</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/hpcgamesde/hpcgamesde</guid><description>继上篇文章介绍了 HPCGames 题解 A B C 题，本文将继续探讨 D E 题的解决方案。</description><pubDate>Thu, 05 Feb 2026 08:18:00 GMT</pubDate><content:encoded>&lt;p&gt;在上一篇文章中，我们介绍了 HPCGames 题解 A、B、C 题的解决方案。本文将继续探讨 D、E 题的解决方案，深入分析每道题目的挑战和我们的应对策略。&lt;/p&gt;
&lt;h2&gt;D. Hyperlane Hopper&lt;/h2&gt;
&lt;p&gt;import {Tabs, TabItem} from &apos;astro-pure/user&apos;;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;背景&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;公元 30777 年，人类文明实现了银河系的互通，连接这些星系的是一种被称为&quot;Hyperlane&quot;的古老网络。每条航道连接两个星系，由于引力波动、空间碎片以及道路维护费不同，每条航道的通行代价都是不一样的，且均为非负整数。&lt;/p&gt;
&lt;p&gt;你作为银河速运的首席算法架构师，面临着一个严峻的问题：随着双十一的临近，由于订单量暴涨，从人类文明早期传承下来的单线程导航核心（基于古老的 &lt;code&gt;Dijkstra&lt;/code&gt; 算法）已经无法在客户失去耐心前规划出最短路径。因为系统还运行在30000年前构建的服务器上（没有人敢动），无法使用高效的量子计算资源。好在你从地球南极挖出了github保存的16核CPU服务器，准备将导航系统升级为多线程版本，以提升路径规划的效率。至于其他部分，那就期待后人的智慧吧。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;任务描述&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;本题是一个经典的单源最短路 (Single Source Shortest Path, SSSP) 问题。给定一个包含 $n$ 个节点和 $m$ 条边的有向图 $G=(V,E)$，边的权重 $w\leq0$。你需要计算从源点 $s=0$ 到图中所有其他节点
$v\in V$ 的最短路径长度。&lt;/p&gt;
&lt;p&gt;你可以使用任何算法来解决这个问题，只要最终结果正确即可。建图部分也可以优化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输入输出&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;本题中输入输出函数&lt;code&gt;main.cpp&lt;/code&gt;已经为你实现，它会调用&lt;code&gt;sssp.cpp&lt;/code&gt;中的&lt;code&gt;calculate&lt;/code&gt;函数，函数原型如下。不过我们仍然在此处说明输入输出格式以便参考。你可以在&lt;code&gt;handout&lt;/code&gt;目录下找到&lt;code&gt;main.cpp&lt;/code&gt;的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void calculate(uint32_t n, uint32_t m, uint32_t *edges, uint64_t *dis)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;程序接受三个参数，&lt;code&gt;n&lt;/code&gt;、&lt;code&gt;m&lt;/code&gt;和&lt;code&gt;seed&lt;/code&gt;，分别表示节点数、边数和随机数种子。程序会根据这些参数生成一个有向图。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;程序输出一个二进制文件，文件名是&lt;code&gt;dis.bin&lt;/code&gt;，其中包括
$n$
个 64 位无符号整数，第
$i$
个整数表示从源点
$0$
到节点
$i$
的最短路径长度。如果节点不可达，输出 1e18。保证输出不会溢出uint64_t的范围。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;运行环境、编译与测试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;本题将在Intel Xeon Platinum 8358 CPU 的 16 核心环境下运行。&lt;/p&gt;
&lt;p&gt;评测容器镜像：cr.hpc.lcpu.dev/hpcgame/base:latest，基础发行版为Rocky Linux 10.1。编译命令如下。我们在handout里提供了一个Makefile，你也可以直接使用下面的命令编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;g++ -O3 -std=c++20 -flto -march=native -fopenmp -pthread -o sssp main.cpp sssp.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们会这样运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export OMP_NUM_THREADS=16
export OMP_PLACES=cores
export OMP_PROC_BIND=close
./sssp n m seed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;评测&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所有结果必须正确，否则不得分。&lt;/p&gt;
&lt;p&gt;数据可能存在重边和自环。数据保证所有点可达。&lt;/p&gt;
&lt;p&gt;对于所有测试点，所有图都是随机图，dis已初始化，具体详见main.cpp。&lt;/p&gt;
&lt;p&gt;每个测试点得到正确结果获得基本分数，性能分数与运行时间倒数成正比。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final_score = correct ? base_score + perf_score * min(goal / time, 1) : 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;|n	|m	|base score	|perf score	|limit	|goal|
|---|---|---|---|---|---|
|1e5	|2e5	|10	|10	|0.03 s	|0.005 s
|1e5	|1e7	|5	|15	|1 s	|0.06s
|1e6	|2e8	|5	|5	|20 s	|1.2s
|1e6	|1e9	|5	|5	|100 s	|5.5s
|1e7	|1e7	|0	|10	|2 s	|0.2s
|1e7	|2e7	|0	|15	|5 s	|0.35s
|1e7	|1e8	|0	|15	|17 s	|1.4s


可以访问&lt;a href=&quot;https://hpcgame.pku.edu.cn/org/&quot;&gt;hpcgames&lt;/a&gt;查找相关附件下载。
&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.C4RiyFVG.png"/><enclosure url="/_astro/cover.C4RiyFVG.png"/></item><item><title>HPCGames 题解 A B C 题</title><link>https://www.hjcheng0602.cn/blog/hpcgamesabc/hpcgamesabc</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/hpcgamesabc/hpcgamesabc</guid><description>最近参加了HPCGames比赛，写下这篇Blog记录一下自己对A B C题的理解与题解</description><pubDate>Wed, 04 Feb 2026 08:18:00 GMT</pubDate><content:encoded>&lt;h2&gt;A 题&lt;/h2&gt;
&lt;p&gt;import {Tabs, TabItem} from &apos;astro-pure/user&apos;;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;因此可以直接把这段代码复制粘贴作为答案提交即可。
&amp;#x3C;/TabItem&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;B 题&lt;/h2&gt;
&lt;p&gt;这里又来到了经典的小北问答环节，结合一些理论知识和具体论文的查阅，我们可以对题目进行详细的分析和解答。&lt;/p&gt;
&lt;h3&gt;1. Amdahl &amp;#x26; Gustafson&lt;/h3&gt;
&lt;p&gt;某程序的代码中 10% 必须串行执行，90% 可完美并行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据 Amdahl&apos;s Law，无论核心数如何增加，该程序的理论最大加速比极限是 ____ 倍；&lt;/li&gt;
&lt;li&gt;若在 10 核系统中通过扩大问题规模来保持每核计算负载不变，根据 Gustafson&apos;s Law，该系统的加速比将达到 ____ 倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;import { Collapse } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;首先，根据 Amdahl 定律，加速比 S 可以通过以下公式计算：&lt;/p&gt;
&lt;p&gt;$$
S = \frac{1}{(1 - P) + \frac{P}{N}}
$$&lt;/p&gt;
&lt;p&gt;其中 P 是可并行部分的比例，N 是处理器的数量。对于该问题，P = 0.9（90% 可并行），串行部分为 0.1（10% 必须串行）。当 N 趋近于无穷大时，公式简化为：&lt;/p&gt;
&lt;p&gt;$$
S_{max} = \frac{1}{(1 - P)} = \frac{1}{0.1} = 10
$$&lt;/p&gt;
&lt;p&gt;因此，该程序的理论最大加速比极限是 10 倍。&lt;/p&gt;
&lt;p&gt;接下来，根据 Gustafson 定律，加速比 S 可以通过以下公式计算：&lt;/p&gt;
&lt;p&gt;$$
S = N - (1 - P) \times (N - 1)
$$&lt;/p&gt;
&lt;p&gt;在 10 核系统中，N = 10，P = 0.9，因此：&lt;/p&gt;
&lt;p&gt;$$
S = 10 - (1 - 0.9) \times (10 - 1) = 10 - 0.1 \times 9 = 10 - 0.9 = 9.1
$$&lt;/p&gt;
&lt;p&gt;因此，在 10 核系统中通过扩大问题规模来保持每核计算负载不变，该系统的加速比将达到 9.1 倍。&lt;/p&gt;
&lt;h3&gt;2. OpenMP&lt;/h3&gt;
&lt;p&gt;以下代码使用 OpenMP 并行执行循环：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int sum = 0;
#pragma omp parallel for
for (int i = 0; i &amp;#x3C; 100; i++) {
    sum += i;
}
printf(&quot;sum = %d\n&quot;, sum);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于该代码，请问以下说法中正确的是 ____ 。&lt;/p&gt;
&lt;p&gt;|选项 | 描述 |
|---|---|
| A | 代码一定能正确计算出 0 到 99 的和（4950）|
| B | 代码存在数据竞争， 结果不确定。 |
| C | &lt;code&gt;sum&lt;/code&gt;变量默认为&lt;code&gt;private&lt;/code&gt;，每个线程有自己的副本。 |
| D | OpenMP 会自动为&lt;code&gt;sum&lt;/code&gt;变量添加原子操作，保证结果正确。 |&lt;/p&gt;
&lt;p&gt;正确答案是 B。&lt;/p&gt;
&lt;p&gt;解释如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选项 A 是错误的，因为代码中存在数据竞争，多个线程同时修改共享变量 &lt;code&gt;sum&lt;/code&gt;，导致结果不确定。&lt;/li&gt;
&lt;li&gt;选项 B 是正确的，因为在并行执行时，多个线程可能同时读取和写入 &lt;code&gt;sum&lt;/code&gt;，导致数据竞争，从而使得最终结果不确定。&lt;/li&gt;
&lt;li&gt;选项 C 是错误的，因为 &lt;code&gt;sum&lt;/code&gt; 变量在默认情况下是共享的（shared），而不是私有的（private）。因此，所有线程都访问同一个 &lt;code&gt;sum&lt;/code&gt; 变量。&lt;/li&gt;
&lt;li&gt;选项 D 是错误的，因为 OpenMP 不会自动为共享变量添加原子操作。要确保结果正确，需要显式地使用 &lt;code&gt;#pragma omp atomic&lt;/code&gt; 或其他同步机制来保护对 &lt;code&gt;sum&lt;/code&gt; 的访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 低精度&lt;/h3&gt;
&lt;p&gt;已知 IEEE 754 标准的 FP32 拥有 8 位指数位。请问：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BF16 拥有 ____ 位指数位，____ 位尾数位&lt;/li&gt;
&lt;li&gt;NVFP4 拥有 ____ 位指数位，____ 位尾数位&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：可以查阅资料，了解 NVFP4 如何在低精度下保持较高的数值范围和动态范围。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;NVFP4 比较特殊，他只有4个bit，显然如果直接使用的话，其范围会很小，精度也不理想，但经过查阅资料可知：
NVFP4 首先将一组数视为了一个块，一个块中会共享一个高精度的scale factor，确定大致的数量级，然后NVFP4只存储每个数相对于这个scale factor的偏移量，这样就能在保持较大数值范围的同时，使用更少的位数来表示每个数，从而提高了存储效率和计算速度。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. MPI 通信&lt;/h3&gt;
&lt;h4&gt;4.1 基本原语&lt;/h4&gt;
&lt;p&gt;4 个进程执行以下代码，每个进程有局部值 local_val，操作后每个进程都有所有进程的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int rank;
MPI_Comm_rank(MPI_COMM_WORLD, &amp;#x26;rank);
int local_val = rank; // rank 为进程编号，0~3
int recv_buf[4];
/* 填这一行代码 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 通信器&lt;/h3&gt;
&lt;p&gt;创建一个 2 维笛卡尔拓扑，尺寸为 2×2，行优先排列，允许环绕连接。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Comm comm_cart;
int dims[2] = {2, 2};
int periods[2] = {1, 1}; // 环绕连接
/* 填这一行代码 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4.1 基本原语&lt;/h4&gt;
&lt;p&gt;可以使用 &lt;code&gt;MPI_Allgather&lt;/code&gt; 来实现该功能。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Allgather(&amp;#x26;local_val, 1, MPI_INT, recv_buf, 1, MPI_INT, MPI_COMM_WORLD);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码会将每个进程的 &lt;code&gt;local_val&lt;/code&gt; 收集到所有进程的 &lt;code&gt;recv_buf&lt;/code&gt; 中。&lt;/p&gt;
&lt;h4&gt;4.2 通信器&lt;/h4&gt;
&lt;p&gt;可以使用 &lt;code&gt;MPI_Cart_create&lt;/code&gt; 来创建一个二维笛卡尔拓扑。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, 1, &amp;#x26;comm_cart);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码会创建一个2x2的笛卡尔拓扑通信器 &lt;code&gt;comm_cart&lt;/code&gt;，并允许环绕连接。&lt;/p&gt;
&lt;h3&gt;5.NCCL 延迟&lt;/h3&gt;
&lt;p&gt;在深度学习的并行推理与训练中，进程之间会频繁进行集合通信操作。NVIDIA 的开源集合通信库 NCCL，提供了在 GPU 之间进行集合通信的高性能解决方案。&lt;/p&gt;
&lt;p&gt;当在异构的硬件上进行大规模集合通信时，如何选择通信的算法将很影响集合通信操作的效率。为了解决这个问题，NCCL 的解决方案是：基于一套硬编码的调优常数，估算不同集合通信算法下的集合通信完成时间，由此选择最优的算法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：在 NCCL 2.28 的默认调优常量中，用 NVLink 连接的两 GPU、采用 Tree 算法和 LL 协议时，在估算时每跳（单步）的硬件延迟取值为 ______ µs。&lt;/p&gt;
&lt;p&gt;根据 NCCL 2.28 的默认调优常量，当使用 NVLink 连接的两 GPU，采用 Tree 算法和 LL 协议时，每跳（单步）的硬件延迟取值为 0.6 µs。&lt;/p&gt;
&lt;p&gt;具体参考资料可见 &lt;a href=&quot;https://github.com/NVIDIA/nccl/blob/master/src/graph/tuning.cc&quot;&gt;NCCL Github src/graph/tunning.cc&lt;/a&gt;中的151行。&lt;/p&gt;
&lt;h3&gt;6. 高性能网络&lt;/h3&gt;
&lt;p&gt;Rail-optimized networking 与 Clos 都是高性能网络设计方案。以下说法正确的有：
|选项 | 描述 |
|---|---|
| A |在 Rail-optimized 网络中，来自不同 HB 域（High-Bandwidth Domain）但具有相同 local rank 的 GPU 会被连接到同一个 rail switch 上，以减少跨域通信的延迟 |
| B |常见部署模式下，Rail-optimized 网络相比传统 Clos 网络的主要优势是完全不需要 Spine 层交换机，因此可以大大节省网络设备成本 |
| C |Clos 网络因其使用 Spanning Tree Protocol (STP) 而在大规模部署时存在扩展性问题，这是 Rail-optimized 网络要解决的核心问题之一 |
| D | Rail-optimized 网络保证了任何情况下集群内任意两个 GPU 之间都能以网络线速（如 400 Gbps InfiniBand）进行通信，无论它们是否在同一个 rail 中 |
| E | NCCL 2.12 引入的 PXN 特性可以结合 NVLink 和 PCI 通信来优化网络流量，这个优化对于 Rail-optimized 网络尤为重要 |
| F | 对于 LLM 训练工作负载，最优的通信策略会将大部分网络流量集中在相同 local rank 的 NIC 之间，并且会多用 NVLink 等高速互联进行跨 rail 交换，这使得 Rail-optimized 架构特别适合此类场景 |&lt;/p&gt;
&lt;p&gt;正确答案是 A、E、F。&lt;/p&gt;
&lt;p&gt;解释如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;选项 A 是正确的，这是 Rail-optimized 网络的核心定义。 在这种架构中，网络拓扑是根据 GPU 的 rank 进行物理隔离的。例如，所有服务器上的 0 号 GPU 都连接到同一组交换机（Rail 0），1 号 GPU 连接到另一组（Rail 1）。这使得在进行数据并行（Data Parallelism）训练时，AllReduce 等操作只需在同一个 Rail 内进行，无需跨越复杂的交换层级，大大降低了拥塞和延迟。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选项 B 是错误的，Rail-optimized 仍然基于 Clos 架构，通常需要 Spine 交换机。 Rail-optimized 描述的是 Leaf 层交换机与 GPU 的连接方式以及流量的导向方式，而不是一种去除了 Spine 的新型拓扑。对于大规模集群（超过一个 Leaf 交换机的容量），Rail 0 的 Leaf 交换机之间仍然需要通过 Spine 交换机互联，以构成一个完整的 Rail 0 网络平面。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选项 C 是错误的，Clos 网络并不使用 STP，且 STP 是传统以太网的痛点。 传统二层以太网使用 STP (Spanning Tree Protocol) 防止环路，这会导致大量链路被阻塞，带宽利用率低。而现代 Data Center Clos 网络（无论是基于 IP 路由的 ECMP 还是 InfiniBand）的设计初衷就是利用所有链路进行负载均衡，完全摒弃了 STP。因此，C 选项描述的前提本身就是错误的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选项 D 是错误的，Rail-optimized 并不保证“跨 Rail”通信的效率等同于“同 Rail”。 Rail-optimized 的设计哲学是“专路专用”。虽然物理上可以通过 Spine 进行跨 Rail 通信（例如 Node A 的 GPU 0 发给 Node B 的 GPU 1），但这通常不是最优路径，且可能面临 oversubscription（收敛比）的问题。实际上，这种架构倾向于利用 E 选项和 F 选项提到的技术来避免在网络层面上进行跨 Rail 数据传输。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选项 E 是正确的，PXN (PCIe/NVLink Cross-NIC) 是解决 Rail 架构灵活性的关键。 在 Rail-optimized 网络中，如果 GPU 0 需要向网络中的 Rail 1 发送数据，传统的路径非常低效（走 PCIe -&gt; CPU -&gt; NIC -&gt; Switch -&gt; ...）。NCCL 的 PXN 特性允许 GPU 0 通过 NVLink 直接把数据传给同机的 GPU 1，然后由 GPU 1 的 NIC（连接着 Rail 1）发送出去。这相当于在节点内部利用 NVLink 完成了“变轨”，从而充分利用 Rail 网络的优势。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选项 F 是正确的，这准确描述了 LLM 训练中的混合通信模式。 在 LLM 训练中，通常结合了数据并行（DP）和模型并行（TP/PP）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+ TP (Tensor Parallelism) 流量极大，通常限制在单机内部，完全走 NVLink。

+ DP (Data Parallelism) 需要跨机同步梯度，流量发生在相同 rank 的 GPU 之间，这完美契合 Rail-optimized 的网络路径。

+ 如果需要跨 rank 的操作（如 Pipeline Parallelism 的某些阶段或特定的 All-to-All），结合 NVLink（节点内）+ Rail（节点间）是目前最优的策略。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. GPU&lt;/h3&gt;
&lt;p&gt;NVIDIA 的 Hopper 架构引入了 TMA（Tensor Memory Accelerator） 以提升 GPU 内存访问效率。以下说法正确的有：&lt;/p&gt;
&lt;p&gt;|选项|	描述|
|---|---|
|A	|相比 cp.async，TMA 可以直接将数据从全局内存加载到共享内存，无需经过寄存器中转，从而能节省寄存器 |
|B	|在 cutlass 的异步流水线抽象中，Producer 调用 producer_acquire 获取空闲的 buffer stage，完成数据加载后调用 producer_commit 通知 Consumer；Consumer 则通过 consumer_wait 等待数据就绪，使用完毕后调用 consumer_release 释放 buffer |
|C	|在使用 TMA 进行数据传输时，所有参与的线程都需要执行相同的 TMA 指令，TMA 硬件会自动处理线程间的协调  |
|D	|Cutlass Pipeline 使用多级缓冲（multi-stage buffering），通过 PipelineState 追踪当前读写的 stage index 和 phase，实现 Producer 和 Consumer 之间的流水线重叠|
|E	|TMA 的 multicast 功能允许一次 TMA 操作将同一块数据广播到 Cluster 内的多个 Thread Block 的共享内存中，减少了重复的全局内存访问|
|F	|TMA 描述符（TMA Descriptor）需要在 kernel 启动前在 host 端创建，描述符中包含了张量的形状、步长和 swizzle 模式等信息，kernel 执行时通过预取描述符（prefetch_tma_descriptor）来减少首次 TMA 操作的延迟|&lt;/p&gt;
&lt;p&gt;正确答案是 B D E F.&lt;/p&gt;
&lt;p&gt;解释 ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 选项A是错误的。Ampere 架构引入的 cp.async 指令同样也是绕过寄存器（Register File），直接将数据从全局内存（GMEM）搬运到共享内存（SMEM）。
- 选项B是正确的。这描述了 Cutlass 异步流水线中 Producer 和 Consumer 之间的交互方式，符合 Cutlass 的设计理念。
- 选项C是错误的。TMA 操作允许线程组内的线程根据需要选择性地执行 TMA 指令，而不是所有线程都必须执行相同的指令。如果所有线程都执行，会导致重复发射多个拷贝操作（除非有特殊的掩码处理）。这一点与 Ampere 的 cp.async（通常每个线程负责一部分）不同。
- 选项D是正确的。Cutlass Pipeline 确实使用多级缓冲，通过 PipelineState 来追踪当前读写的 stage index 和 phase，从而实现 Producer 和 Consumer 之间的流水线重叠。
- 选项E是正确的。TMA 的 multicast 功能允许一次 TMA 操作将同一块数据广播到 Cluster 内的多个 Thread Block 的共享内存中，减少了重复的全局内存访问。
- 选项F是正确的。TMA 描述符需要在 kernel 启动前在 host 端创建，包含张量的形状、步长和 swizzle 模式等信息，kernel 执行时通过预取描述符来减少首次 TMA 操作的延迟。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8. LLM&lt;/h3&gt;
&lt;p&gt;对于参数如下的一个标准的 Transformer-Decoder 模型，所有的 all reduce 操作都使用 ring all reduce。假设一共有 4 张卡。&lt;/p&gt;
&lt;h4&gt;模型参数&lt;/h4&gt;
&lt;p&gt;|参数 | 值 |
| --- | --- |
| 层数   | 32 层 |
|隐藏层维度 (h) |   4096 |
|FFN 结构 | 两层线性层，中间层维度为 4h |
|序列长度 | 2048 |
|Batch Size | 32 |
|优化器 | Adam + 混合精度训练 |
|精度设置 | 参数和梯度使用 fp16，Adam 优化器状态使用 fp32（包括 momentum、variance 和 master weights） |&lt;/p&gt;
&lt;h4&gt;问题&lt;/h4&gt;
&lt;p&gt;请计算在以下三种并行方式下，进行一个 batch 的前向传播和反向传播，每张卡需要的发送量（以 GB 为单位）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据并行&lt;/strong&gt;：每张卡上存放完整的模型，把 batch 均匀拆分到每张卡上，分别计算完成后对梯度进行 All-Reduce 操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;流水并行&lt;/strong&gt;：按层拆分模型放到不同卡上，只需要前向传播的时候发送 activation，反向传播的时候发送 gradient。（计算通信量时只考虑中间的卡）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;张量并行&lt;/strong&gt;：对于 MHA 操作，按照 head 拆分到不同卡上。对于 FFN，第一个线性层按照输出维度进行拆分，第二个线性层按照输入维度进行拆分&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;计算过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据并行&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;模型参数量：&lt;/p&gt;
&lt;p&gt;$$
P = 32 \times 12 \times (4096 ^ 2)
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;梯度大小：&lt;/p&gt;
&lt;p&gt;$$
G = P \times 2 \text{ bytes (fp16)} = 12 \text{ GB}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通信量：&lt;/p&gt;
&lt;p&gt;$$
\text{通信量} = 2 \times \frac{N-1}{N} \times G = 18 \text{ GB}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;流水并行&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每层激活大小：&lt;/p&gt;
&lt;p&gt;$$
A = 32 \times 2048 \times 4096 \times 2 \text{ bytes (fp16)} = 0.5 \text{ GB}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通信量：&lt;/p&gt;
&lt;p&gt;$$
\text{通信量} = 2 \times A = 1 \text{ GB}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;张量并行&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单次 All-Reduce 大小：&lt;/p&gt;
&lt;p&gt;$$
AR = 32 \times 2048 \times 4096 \times 2 \text{ bytes (fp16)} = 0.5 \text{ GB}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单次 Ring All-Reduce 通信量：&lt;/p&gt;
&lt;p&gt;$$
\text{单次通信量} = 2 \times \frac{N-1}{N} \times AR = 0.75 \text{ GB}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;总通信量：&lt;/p&gt;
&lt;p&gt;$$
\text{通信量} = 32 \times 4 \times 0.75 \text{ GB} = 96 \text{ GB}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9. UB 互联&lt;/h3&gt;
&lt;p&gt;在高性能计算系统中，集合通信（Collective Communication）的性能主要受带宽（Bandwidth） 与延迟（Latency） 两个因素制约。&lt;/p&gt;
&lt;p&gt;NVIDIA 通过 NVLink 与 NVSwitch 构建 GPU 间的高速 Scale-up 互联网络，而华为则提出了 Unified Bus（UB）协议，作为面向 NPU 的统一互联与内存访问机制。UB 协议基于华为自研的 UB Switch 交换芯片，并通过高带宽物理链路 HCCS（High-Capacity Coherent System） 进行连接。&lt;/p&gt;
&lt;p&gt;传统 AI 集群通常以 8 卡服务器为基本单元进行 Scale-out 扩展，而华为在 CloudMatrix 384（CM384） 架构中，通过两级 UB Switch 组网，将 384 颗昇腾 910C NPU 构建为一个统一的超节点（SuperPod）。在该超节点范围内，所有 NPU 均处于同一个低延迟的轨道优化网络中，实现全对等 Scale-up 互联。&lt;/p&gt;
&lt;p&gt;CM384 进一步将 UB 网络划分为 7 个相互独立的物理平面。每颗 NPU 的 7 个 HCCS 接口分别接入不同的交换平面，从而保证大规模并行通信过程中，数据流在物理路径上完全隔离、无链路冲突。&lt;/p&gt;
&lt;h4&gt;问题&lt;/h4&gt;
&lt;p&gt;在 CloudMatrix 384 的标准满配部署方案中，为了支撑 384 颗昇腾 910C NPU 实现无收敛、全对等的 Scale-up 互联，系统采用两级交换架构。在该超节点的物理拓扑中，分别使用了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;____ 个 Level 1 UB Switch&lt;/li&gt;
&lt;li&gt;____ 个 Level 2 UB Switch&lt;/li&gt;
&lt;li&gt;最终实现了理论上 ____ GB/s 的系统级聚合带宽&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设 switch chip 提供的单个 Port 可以提供 28GB/s 的通信带宽&lt;/p&gt;
&lt;p&gt;该问题直接查阅华为 CloudMatrix 384 的&lt;a href=&quot;https://arxiv.org/pdf/2506.12708&quot;&gt;白皮书&lt;/a&gt;即可得到答案。&lt;/p&gt;
&lt;h3&gt;10. Cache 行为分析&lt;/h3&gt;
&lt;p&gt;假设我们需要进行一个矩阵乘法 $C=A×B$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试环境&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了简化分析，假设：&lt;/p&gt;
&lt;p&gt;|参数类型|	配置|
| --- | --- |
|数据类型 |	double (8 Bytes) |
|L1 Cache 大小 |	4KB (4096 Bytes) |
|相联度 |	直接映射 (Direct Mapped, E=1) |
|块大小 |	64 Bytes（1 个 Cache Line 可存 8 个 double）|
|矩阵规模 |	A,B,C 均为 64×64 的方阵 (N=64) |
|存储方式 |	数组按行优先存储 |
|内存对齐 |	A,B,C 的起始地址均对齐到 Cache 的起始 Set |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 假设变量 sum 已优化到寄存器中，忽略 C 的访存影响
// 仅考虑内层循环中 A 和 B 的读取
for (int j = 0; j &amp;#x3C; 64; ++j) {       // Loop 1
    for (int i = 0; i &amp;#x3C; 64; ++i) {   // Loop 2
        double sum = 0.0;
        for (int k = 0; k &amp;#x3C; 64; ++k) { // Loop 3
            sum += A[i][k] * B[k][j];
        }
        C[i][j] = sum;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题 10.1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们试图分析上述代码中最内层循环 Loop 3 对矩阵 $B$ 的访存行为。&lt;/p&gt;
&lt;p&gt;已知 Cache 总共有 $4096/64=64$ 个 Set。&lt;/p&gt;
&lt;p&gt;在计算
$C[i][j]$ 的过程中（即一次完整的 Loop 3），关于
$B[k][j]$ 的 Cache Miss Rate（不命中率），下列说法正确的是：&lt;/p&gt;
&lt;p&gt;|选项	|描述 |
|---|---|
|A|	12.5% - 这里有良好的空间局部性，每 8 个 double 只有 1 次 Miss |
|B|	25% - 虽然是列优先访问，但 Cache 够大，只有冷不命中 |
|C|	约 50% - A 和 B 互相打架（冲突），导致一半的数据被驱逐 |
|D|	100% - 发生了严重的 Cache Thrashing（抖动），每次读取都是 Miss |&lt;/p&gt;
&lt;p&gt;💡 提示：计算一下访问 $B[k][j]$ 和 $B[k+1][j]$ 时的内存地址差值（Stride），以及它们映射到的 Set Index 的跨度。&lt;/p&gt;
&lt;p&gt;正确答案是 D。&lt;/p&gt;
&lt;p&gt;解释如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;矩阵 B 是按行优先存储的，因此访问 B[k][j] 时，k 的变化会导致访问的内存地址以列为单位跳跃。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算地址差值（Stride）：&lt;/p&gt;
&lt;p&gt;$$
\text{Stride} = \text{sizeof(double)} \times N = 8 \text{ Bytes} \times 64 = 512 \text{ Bytes}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次访问 B[k][j] 时，地址增加 512 Bytes，而每个 Cache Line 大小为 64 Bytes，因此每次访问都会跨越多个 Cache Line。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 Set Index 的跨度：&lt;/p&gt;
&lt;p&gt;$$
\text{Set Index Span} = \frac{\text{Stride}}{\text{Cache Line Size}} = \frac{512 \text{ Bytes}}{64 \text{ Bytes}} = 8
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因为 Cache 有 64 个 Set，跨度为 8 意味着每次访问都会映射到不同的 Set，但由于 k 从 0 到 63，共有 64 次访问，这些访问会循环映射到同一组 Set 上，导致频繁的冲突和驱逐。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最终结果是每次读取 B[k][j] 都会导致 Cache Miss，即 Cache Thrashing。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题 10.2
为了进一步提升矩阵乘法的效率，我们决定使用分块技术。你将矩阵分成了 $8×8$ 的小块（Block Size = 8）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 8x8 分块优化演示
for (int jj = 0; jj &amp;#x3C; 64; jj += 8) {
    for (int ii = 0; ii &amp;#x3C; 64; ii += 8) {
        for (int kk = 0; kk &amp;#x3C; 64; kk += 8) {
            // 在这里处理 8x8 的子块乘法
            for (int j = jj; j &amp;#x3C; jj + 8; ++j) {
                for (int i = ii; i &amp;#x3C; ii + 8; ++i) {
                     double sum = C[i][j]; // 简化写法
                     for (int k = kk; k &amp;#x3C; kk + 8; ++k) {
                         sum += A[i][k] * B[k][j];
                     }
                     C[i][j] = sum;
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;针对一个 $8×8$ 的 $B$ 矩阵子块（假设该子块已预加载），在处理该子块内部的计算时，关于其在 L1 Cache 中的状态，下列分析正确的是：
|选项	|描述 |
|---|---|
|A|	一个 8×8 的子块大小为 512 Bytes，远小于 Cache 大小，因此完全没有冲突，所有数据都能驻留在 Cache 中 |
|B|	尽管子块很小，但由于 B 的原始列宽（Stride）很大，导致子块内的 8 行数据全部映射到了同一个 Set 中，依然存在严重的冲突 |
|C|	子块内的 8 行数据分别映射到了 8 个不同的 Set 中（Set 索引间隔为 8），且在子块计算期间不会发生自我冲突（Self-Conflict） |
|D|	分块主要是为了利用 L2/L3 Cache，对这么小的 L1 Cache (4KB) 来说，8×8 的分块没有任何意义 |&lt;/p&gt;
&lt;p&gt;正确答案是 C。&lt;/p&gt;
&lt;p&gt;解释如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个 8×8 的子块大小为：&lt;/p&gt;
&lt;p&gt;$$
\text{Block Size} = 8 \times 8 \times \text{sizeof(double)} = 64 \times 8 \text{ Bytes} = 512 \text{ Bytes}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;该子块的大小（512 Bytes）确实远小于 L1 Cache 大小（4KB），但关键在于访问模式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在处理该子块时，访问 B[k][j] 时，k 的变化会导致访问的内存地址以列为单位跳跃。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 Set Index 的跨度：&lt;/p&gt;
&lt;p&gt;$$
\text{Set Index Span} = \frac{\text{Stride}}{\text{Cache Line Size}} = \frac{512 \text{ Bytes}}{64 \text{ Bytes}} = 8
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因此，子块内的 8 行数据分别映射到了 8 个不同的 Set 中，且在子块计算期间不会发生自我冲突（Self-Conflict）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分块技术有效地利用了 Cache 的空间局部性，减少了冲突，提高了数据的命中率。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;C 题&lt;/h2&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;在分秒必争的高频交易领域，系统的响应速度直接决定了盈亏。我们的核心交易引擎在最新的鲲鹏服务器上部署后，发现了一个令人困惑的现象：虽然我们为每个交易对分配了独立的计算核心，理论上应当实现完美的线性加速，但实际吞吐量却远低于预期，甚至不如少用几个核心时的表现。&lt;/p&gt;
&lt;p&gt;作为团队的首席性能架构师，你需要深入底层，找出阻碍性能释放的元凶，并对关键数据结构进行重构，让这台多核怪兽展现出它在这个并行任务上应有的爆发力。&lt;/p&gt;
&lt;h2&gt;任务描述&lt;/h2&gt;
&lt;p&gt;程序包含两个文件：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;main.cpp&lt;/code&gt;：负责启动 16 个 OpenMP 线程，生成随机模拟行情，并统计最终结果。
&lt;code&gt;market.h&lt;/code&gt;：定义了核心数据结构 Candle 。
你需要修改 &lt;code&gt;market.h&lt;/code&gt; 中的结构体定义，以提升程序在多核环境下的运行效率。&lt;/p&gt;
&lt;p&gt;修改限制：允许调整结构体的定义方式、内存布局等；禁止修改成员变量的名称（如 &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;vol&lt;/code&gt; 等），禁止修改变量类型及其精度（必须保持 &lt;code&gt;double/long long&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;请提交修改后的 &lt;code&gt;market.h&lt;/code&gt; 文件。&lt;/p&gt;
&lt;h2&gt;输入输出&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;输入 format&lt;/strong&gt;
程序从标准输入读取数据：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;N (long long)&lt;/code&gt;: 每个线程模拟的 Tick 数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Seed (int)&lt;/code&gt;: 随机种子。
&lt;strong&gt;输出 format&lt;/strong&gt;
程序向标准输出打印结果：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一行：所有合约的总成交额，即&lt;/p&gt;
&lt;p&gt;$$
\text{Total Turnover} = \sum Price \times Volume
$$&lt;/p&gt;
&lt;p&gt;第二行：所有合约的 VWAP (成交量加权平均价)，计算方式为 &lt;code&gt;Total Turnover / Total Volume&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;样例输入&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;50000000 123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;样例输出&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;769984094225.13
174.9963
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;运行环境、编译与测试&lt;/h2&gt;
&lt;p&gt;本题将在 Huawei Kunpeng 920A (ARM64, 64-Core) 的 16 个核心上运行。评测容器镜像：&lt;code&gt;cr.hpc.lcpu.dev/hpcgame/3rd-kunpeng920:latest&lt;/code&gt;，基础发行版为&lt;code&gt;Fedora 43&lt;/code&gt;，安装了&lt;code&gt;clang 21&lt;/code&gt;,&lt;code&gt;flang 21&lt;/code&gt;和&lt;code&gt;gcc 15&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以使用以下命令编译程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;g++ main.cpp -o main -O3 -fopenmp -std=c++17 -march=native
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们会使用如下命令运行程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export OMP_NUM_THREADS=16
export OMP_PLACES=cores
export OMP_PROC_BIND=close
./main &amp;#x3C; input.txt &gt; output.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;评分标准&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;正确性：输出结果必须与标准答案的相对误差不超过1e-12。&lt;/li&gt;
&lt;li&gt;性能：程序运行时间为 t，满分时间为t_0则得分为 min(20, 20^(t_0/t))。
共5个测试点，迭代次数有所不同。满分标准：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;|迭代次数 |	满分时间  |	最长运行时间  |	分值 |
|---|---|---|---|
|5e7	|0.35s	|0.7s	|20|
|5e8	|3.5s	|7.0s	|20|
|1e9	|7.0s	|14.0s	|20|
|5e9	|35.0s	|70.0s	|20|
|5e10	|349.0s	|698.0s	|20|&lt;/p&gt;
&lt;h2&gt;Hint&lt;/h2&gt;
&lt;p&gt;鲲鹏 CPU 有自己的硬件特质，了解这些特质有助于优化性能。&lt;/p&gt;
&lt;p&gt;可以访问&lt;a href=&quot;https://hpcgame.pku.edu.cn/org/&quot;&gt;hpcgames&lt;/a&gt;查找相关附件下载。&lt;/p&gt;
&lt;p&gt;首先，我拿到这道题的时候，因为只需要修改结构体的定义，所以我先看了下 market.h 里面 Candle 结构体的定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct Candle {
    double high;
    double low;
    double close;
    long long vol;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个结构体一共包含了 3 个 double 类型的变量和 1 个 long long 类型的变量。根据 C++ 的内存对齐规则，double 类型通常需要 8 字节对齐，而 long long 也需要 8 字节对齐，看起来
这个结构体的内存布局应该是比较紧凑的，没有明显的内存浪费。&lt;/p&gt;
&lt;p&gt;但是显然这道题不能不改就交上去(x)，因此：我们需要考虑这个结构体在内存下的访问模式。&lt;/p&gt;
&lt;p&gt;首先，我们的评测环境是 Kunpeng 920A，基于 ARMv8 架构的CPU, 其L1 L2 Cache Line Size 为 64 Bytes, L3 Cache Line Size 为 128 Bytes, 因此如果他们的粒度均为32 Bytes，
那多个Candle 对象可能会落在同一个 Cache Line 上， 造成不同核对同一 Cache Line 的争用，从而影响性能。&lt;/p&gt;
&lt;p&gt;因此，一个很朴素的想法就是，增大结构体的大小，让每个 Candle 对象独占一个 Cache Line，从而避免不同核对同一 Cache Line 的争用，并且将结构体的起点对齐到 Cache Line 边界。&lt;/p&gt;
&lt;p&gt;因此选择使用&lt;code&gt;alignas(128)&lt;/code&gt;来对齐结构体，编译器会自动在结构体后面填充一些字节，使得每个 Candle 对象的大小为 128 Bytes。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;struct alignas(128) Candle {
    double high;
    double low;
    double close;
    long long vol;
};
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/cover.C4RiyFVG.png"/><enclosure url="/_astro/cover.C4RiyFVG.png"/></item><item><title>SF3D 论文阅读记录</title><link>https://www.hjcheng0602.cn/blog/sf3d/sf3d</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/sf3d/sf3d</guid><description>最近想了解一下mesh reconstruction, 于是阅读了SF3D这篇论文, 做笔记记录</description><pubDate>Sat, 29 Nov 2025 17:48:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Spoiler } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;mesh construction是我刚刚开始了解的一个方向, 今天读了&lt;a href=&quot;https://arxiv.org/pdf/2408.00653&quot;&gt;SF3D: Scene Fusion for 3D Reconstruction with Transformers&lt;/a&gt;这篇论文, 本文笔记记录用于后续翻阅学习。&lt;/p&gt;
&lt;p&gt;读完这篇论文之后, 感觉mesh reconstruction与point cloud reconstruction还是有很大区别的, 尤其是这篇文章中引入的几个新的 mesh 专有的 module, 感觉要比 point cloud reconstruction 更加复杂一些.OK,
废话不多说, 直接进入正题.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;作者一上来就提出了几个issue:
&lt;img src=&quot;./1.png&quot; alt=&quot;SF3D提出的问题&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Light bake-in : 现有的模型将光照信息直接bake到texture里, 使得生成的mesh难以利用, 而在SF3D中, 作者提出了使用explicit illumination和一个不同的使用 Spherical Gaussian 的 shading model来解决这个问题(如上图第一行所示).&lt;/li&gt;
&lt;li&gt;Vertex Coloring : 现有的工作中, 生成的vertex的数量过多, 使得性能开销很大. 作者认为一个关键问题就是 UV unwrapping的额外处理时间, 于是作者提出了一种highly parallelizable fast box projection-based UV
unwrapping method 来解决这个问题(如上图第二行所示), 这使得时间从10-30s 减少到了0.5s, 而且从图上来看, 细节比baseline的 TripoSR 的效果更好.&lt;/li&gt;
&lt;li&gt;Marching Cube Artifacts : feed-forward network 通常生成类似与 Triplane NeRFs 的体素网格, 然后使用 marching cube 来提取mesh, 但是这种方法会引入一些artifacts,
作者提出了使用一个对高分辨率 Triplane 更有效的 architecture, 并且使用 DMTet 来对生成的vetex diplacement 和 normal map生成最终的mesh, 这样可以有效减少marching cube引入的artifacts(如上图第三行所示).&lt;/li&gt;
&lt;li&gt;Lack of Material Properties : 现有的工作生成的mesh在不同光照下都会看起来dull, 这是因为缺乏explicit的material properties.为解决这个问题, 作者预测了non-spartially varying material properties
(如上图第4, 5行所示).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过以上的改进, SF3D 可以从单张图像生成高质量的mesh, 且生成的3D 资产体积小(1 MB)并且可以在0.5s内生成.&lt;/p&gt;
&lt;h2&gt;Method&lt;/h2&gt;
&lt;p&gt;为了解决上面提到的问题, 作者提出了 SF3D.&lt;/p&gt;
&lt;p&gt;首先, SF3D是在TripoSR的基础上进行改进的. TripoSR训练了一个能够生成Triplane 3D representation的transformer. 它使用DINO encode image, 然后把token送入transformer中, transformer输出一个$64 \times 64$分辨率的
triplane, 然后triplane feature之后被decode为color和渲染成标准NeRF. TripoSR 只学到了colors并且不能处理反射等材质属性.&lt;/p&gt;
&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;SF3D的整体架构如下图所示:
&lt;img src=&quot;./2.png&quot; alt=&quot;SF3D架构图&quot;&gt;
可以看到, SF3D由5个主要模块组成:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enhanced Transformer : 用于预测高分辨率的triplane feature.&lt;/li&gt;
&lt;li&gt;Merterial Estimation : 用于预测材质属性.&lt;/li&gt;
&lt;li&gt;Illumination Modeling : 处理光照问题.&lt;/li&gt;
&lt;li&gt;Mesh extraction and refinement : 用于从triplane中提取mesh并进行细化.&lt;/li&gt;
&lt;li&gt;UV Unwrapping and Export : 产生low-poly mesh 和 高分辨率 texture map.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Enhanced Transformer&lt;/h3&gt;
&lt;p&gt;为了生成高分辨率的triplane feature, 作者对TripoSR的transformer进行了改进, 主要有以下几点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先, 作者将DINO 替换成了DINOv2, 这样可以获得更好的image feature.&lt;/li&gt;
&lt;li&gt;其次, 作者对 triplane 导致的 aliasing 问题进行了讨论
&lt;img src=&quot;./3.png&quot; alt=&quot;aliasing问题&quot;&gt;
如上图所示, 低分辨率的triplane会导致aliasing问题, 但是简单地提高triplane的分辨率会导致模型更复杂, 作者说, 他从PointInfinity中获得启发,
(PointInfinity 提供了一个不需要计算triplane的self-attention的架构), 因此, 作者将分辨率提高到$96 \times 96$, 从而降低了走样.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Material Estimation&lt;/h3&gt;
&lt;p&gt;SF3D 输出了 metallic 和 roughness 两个材质属性. 论文中提到, 理想状况下, 人们希望材质属性是spatially varying的, 但是这样并不现实. 于是作者简化了这个问题, 为整个物体
预测这两个属性, 作者提到虽然这种非空间变化的材质属性通常适用于同质物体, 但是实际上能显著改善渲染效果.&lt;/p&gt;
&lt;p&gt;为了实现这个预测, 作者引入了一个 Material net, 首先将图像通过CLIP encoder编码, 然后通过2个MLP预测 metallic 和 roughness.&lt;/p&gt;
&lt;h3&gt;Illumination Modeling&lt;/h3&gt;
&lt;p&gt;作者提出要显式estimating光照, 如果不这样做的话, 输出的RGB 颜色会将光照信息bake进去, 使得生成的mesh难以利用. 为此, 作者提出了一个 Light net, estimate SG 光照. 因为triplane encode了场景的几何信息, 所以可以能够推断光照变化.&lt;/p&gt;
&lt;p&gt;具体实现上, 作者使用 Transformer 输出的 $96 \times 96$ 分辨率的triplane作为输入, 使其通过 2 个 CNN 层, 接着进行max pool,
最后通过一个MLP。Light Net 输出 24 个 SG 的grayscale amplitude values, 并使用 Softplus 以确保值为正数。这些 SG 的轴和锐度值保持固定, 其设置旨在覆盖整个球体。
利用这些振幅值, 作者实施了一种类似于 NeRD [4] 中使用的deferred physically based rendering方法.&lt;/p&gt;
&lt;p&gt;此外, 作者的方法在训练阶段还引入了一个lighting demodulation loss $\mathcal{L}_{\text{Demod}}$, 该损失函数旨在确保：一个具有entirely white albedo的物体上的光照,
能与输入图像的亮度紧密匹配。lighting demodulation loss强制学习到的光照与训练数据中观察到的光照条件保持一致.
这可以被视为一种bias, 用于解决appearance和shading之间的ambiguity.&lt;/p&gt;
&lt;h3&gt;Mesh Extraction and Refinement&lt;/h3&gt;
&lt;p&gt;为了从triplane中提取mesh, 作者使用了DMTet. 作者提出了两个MLP head来预测vertex offsets和vertex normals. 这里受MeshLRM启发, 作者也单独使用了分离的decoder MLP来辅助这两个head的训练.
作者发现, vertex offset能够反走样, 而vertex normal则能提升细节表现. 鉴于一开始normal map的预测不会太准确, 于是作者使用了slerp来稳定训练, 这是在一开始的5K step里发生.&lt;/p&gt;
&lt;p&gt;然后引入了各种loss来训练这个mesh extraction and refinement模块:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$$\mathcal{L}_{\text{Nrmconsistency}}$$ : 法线一致性损失&lt;/li&gt;
&lt;li&gt;$$\mathcal{L}_{\text{Laplacian}}$$ : Laplacian 平滑损失&lt;/li&gt;
&lt;li&gt;$$\mathcal{L}_{\text{Offset}} = v_o^2$$ : 顶点偏移正则化&lt;/li&gt;
&lt;li&gt;$$\mathcal{L}_{\text{Nrmrepl}} = 1 - n \cdot \hat{n}$$ : 法线复制损失&lt;/li&gt;
&lt;li&gt;$$\mathcal{L}_{\text{Nrmsmooth}} = (\hat{n}(x) - \hat{n}(x + \epsilon))^2$$ : 法线平滑损失&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;UV Unwrapping and Export&lt;/h3&gt;
&lt;p&gt;SF3D模型的最终阶段是一个高效的导出流水线, 关键挑战在于传统UV展开的计算密集性, 这不符合快速生成的要求. 为此, 作者提出了一个基于立方体投影的展开方法. 该方法利用网格面法线独立决定投影方向, 实现了可并行化的展开过程.
具体实现上, 该方法执行2D三角形-三角形相交测试来处理UV图集中的遮挡, 并根据深度和接近度对相交面进行重新分配. 同时, 通过遵循径向 $z$ 切线方向旋转UV岛以最小化阴影接缝. 接着, 通过UV展开将世界坐标和占用率烘焙到UV图集上
, 用于从triplane中查询反照率和表面法线. 为防止接缝伪影, 作者采用了一个迭代过程, 使用 $3 \times 3$ 部分卷积和最大池化来扩展UV边界, 确保纹理平滑向外混合.&lt;/p&gt;
&lt;p&gt;之后, 作者将所有文件作为glb格式导出.&lt;/p&gt;
&lt;h2&gt;Overall Training and Loss Functions&lt;/h2&gt;
&lt;p&gt;由于直接在网格渲染任务上训练方法会产生不满意的结果, 作者首先在 NeRF 任务上进行了预训练. 完成预训练后, 模型过渡到网格训练,
将 NeRF 渲染替换为differentiable mesh rendering和基于 SG 的着色.&lt;/p&gt;
&lt;p&gt;分步的损失函数如下所示:
$$
\begin{split}\mathcal{L}&lt;em&gt;{\rm render}&amp;#x26;=\underbrace{ \lambda&lt;/em&gt;{\rm MSE}}&lt;em&gt;{ 1 0}\mathcal{L}&lt;/em&gt;{\rm MSE}+\underbrace{ \lambda_{\rm LPIPS}}&lt;em&gt;{ 2}\mathcal{L}&lt;/em&gt;{\rm LPIPS}+\underbrace{\lambda_{ \rm Mask}}&lt;em&gt;{ 1 0}\mathcal{L}&lt;/em&gt;{\rm Mask}\ \mathcal{L}&lt;em&gt;{\rm mesh}&amp;#x26;=\underbrace{\lambda&lt;/em&gt;{\rm Laplacian }}&lt;em&gt;{ 0.01}\mathcal{L}&lt;/em&gt;{\rm Laplacian}+\underbrace{\lambda_{\rm Nrm Consistency}}&lt;em&gt;{ 0.001}\mathcal{L}&lt;/em&gt;{\rm Nrm consistency}+\underbrace{\lambda_{\rm Offset}}&lt;em&gt;{ 0.1}\mathcal{L}&lt;/em&gt;{\rm Offset}\ \mathcal{L}&lt;em&gt;{\rm shading}&amp;#x26;=\underbrace{\lambda&lt;/em&gt;{\rm Nrm repl}}&lt;em&gt;{ 0.2}\mathcal{L}&lt;/em&gt;{\rm Nrm repl}\underbrace{\lambda_{\rm Nrm smooth}}&lt;em&gt;{ 0.02}\mathcal{L}&lt;/em&gt;{\rm Nrm smooth}+\underbrace{\lambda_{\rm Demod}}&lt;em&gt;{ 0.01}\mathcal{L}&lt;/em&gt;{\rm Demod}\end{split}
$$
总损失为:
$$
\mathcal{L}=\mathcal{L}&lt;em&gt;{\rm render}+\mathcal{L}&lt;/em&gt;{\rm mesh}+\mathcal{L}_{\rm shading}
$$&lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;作者在GSO和OminiObject3D数据集上对SF3D进行了评估. 结果如下图所示:
&lt;img src=&quot;./4.png&quot; alt=&quot;结果图&quot;&gt;
可以看到, SF3D在视觉效果上明显优于其他方法, 并且在数值指标上也有显著提升.&lt;/p&gt;
&lt;p&gt;在速度方面, 确实如作者所说, SF3D的UV展开非常快, 只需0.5s, 远快于其他方法的10-30s.
&lt;img src=&quot;./5.png&quot; alt=&quot;速度对比&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;因此, 我似乎大致总结完了SF3D的主要结构, 从一张图像生成高质量的mesh, 能不能对视频进行这样的操作呢? 我们看到这个任务里实际上用了大量生成的先验知识, 我在想一个完全
基于image的3D reconstruction方法, 能不能做到不依赖于这些先验知识?&lt;/p&gt;</content:encoded><h:img src="/_astro/image.v3-Mtmiu.png"/><enclosure url="/_astro/image.v3-Mtmiu.png"/></item><item><title>ViT Transformer 的阅读?(应该算是阅读吧)</title><link>https://www.hjcheng0602.cn/blog/vit_transformer/vit</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/vit_transformer/vit</guid><description>一直在做三维重建相关的工作, 也一直用到 ViT encoder, 但是并不了解 ViT 的具体结构, 于是读了开山之作, 做一个简单的记录</description><pubDate>Tue, 25 Nov 2025 21:10:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Spoiler } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;在快要到2026年的今天, ViT 相比于当下的复杂的结构而言, 已经显得比较简单了, 我读论文的时候的最大感觉是, 它充满了 Transformer 在各领域蓬勃发展的野蛮生长的气息.
但是作为 Transformer 在CV领域的里程碑式的工作, 并且我作为这方面的初学者, 我觉着还是需要读一下这一篇论文&lt;a href=&quot;https://arxiv.org/abs/2010.11929&quot;&gt;An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale&lt;/a&gt;, 做一个简单的记录.&lt;/p&gt;
&lt;h2&gt;ViT 的整体结构&lt;/h2&gt;
&lt;p&gt;ViT 的整体结构如下图所示:
&lt;img src=&quot;./image.png&quot; alt=&quot;1&quot;&gt;
可以看到, 他的特殊处理是在于输入部分, 传统的 CNN 是通过 kernel 来滑动提取局部信息, 这样的一个 CNN 的输出很难直接送入 Transformer 中进行处理, 因为
Transformer 需要的是一个序列化的输入, 而CNN 的输出是一个三维的 feature map.&lt;/p&gt;
&lt;p&gt;因此, 相较于同期的其他处理, ViT 直接将输入图像划分为若干个小的 patch, 然后将每个 patch 展平并映射到一个固定维度的向量空间中, 形成一个序列化的输入, 这样就可以直接送入 Transformer 中进行处理.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;具体来说, 假设输入图像的尺寸为$H \times W \times C$ (高度, 宽度, 通道数), 我们将其划分为大小为$P \times P$的若干个不重叠的 patch, 则总共会得到$N = \frac{HW}{P^2}$个 patch.&lt;/li&gt;
&lt;li&gt;每个 patch 被展平为一个向量, 并通过一个线性投影映射到一个$d$维的向量空间中, 形成一个序列化的输入矩阵$X \in \mathbb{R}^{N \times d}$.&lt;/li&gt;
&lt;li&gt;此外, 为了让模型能够捕捉到位置信息, ViT 还引入了可学习的位置编码, 将其与输入序列相加, 形成最终的输入表示.&lt;/li&gt;
&lt;li&gt;接下来, 这个序列化的输入就可以直接送入标准的 Transformer Encoder 中进行处理, 经过多层的 Transformer Encoder Layer 的处理后, 得到最终的输出表示.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其具体的一个维数变换大概是这样:
$$
X \in \mathbb{R}^{224 \times 224 \times 3} \rightarrow 196 \times Patchs^{16 \times 16 \times 3} \rightarrow Flattened_Patchs^{196 \times 768} \rightarrow \
Transformer Input^{197 \times 768} \rightarrow Transformer Output^{197 \times 768} \rightarrow Classifier Output^{1 \times 1000}
$$
为什么新加上的class token work?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为在transformer中, 两两token之间是可以相互attention的, 因此class token可以和所有的patch token进行attention, 从而聚合全局的信息, 这样我们就可以在最终的输出中使用class token来进行分类任务.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;一些其他的细节&lt;/h2&gt;
&lt;p&gt;相对于 CNN 而言, ViT 的先验信息很少, 因此在中小数据集上的表现并不理想, 论文中提到需要在大规模数据集上进行预训练, 然后再进行微调, 才能取得较好的效果.&lt;/p&gt;
&lt;p&gt;此外, ViT 的 attention 机制也与 Transformer 类似, 主要包括 Multi-Head Self-Attention 和 Feed-Forward Neural Network (FFN) 两个部分, 具体的计算过程与 Transformer 中的 Self-Attention 类似, 这里就不再赘述.&lt;/p&gt;
&lt;p&gt;总的来说, ViT 通过将图像划分为 patch 并使用 Transformer 进行处理, 提供了一种新的思路来解决计算机视觉中的图像分类问题, 并且在大规模数据集上取得了优异的表现, 成为计算机视觉领域的重要里程碑.&lt;/p&gt;</content:encoded><h:img src="/_astro/image.BlosjlpU.png"/><enclosure url="/_astro/image.BlosjlpU.png"/></item><item><title>回顾一下Transformer</title><link>https://www.hjcheng0602.cn/blog/transformer/transformer</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/transformer/transformer</guid><description>最近在学习一些模型架构方面的内容, Transformer快被我忘记了, 做一个简单的回顾以便日后查询</description><pubDate>Mon, 24 Nov 2025 23:48:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Spoiler } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;Transformer 在&lt;a href=&quot;https://arxiv.org/abs/1706.03762&quot;&gt;Attention is All You Need&lt;/a&gt;一文中被提出, 本来想读一下原文的, 但是时间并不太够, 因此我们这里就简单捋一下就行.&lt;/p&gt;
&lt;h2&gt;整体结构&lt;/h2&gt;
&lt;p&gt;Transformer 的整体结构如下图所示:
&lt;img src=&quot;./transformer.png&quot; alt=&quot;Transformer架构图&quot;&gt;
可以看到, 其主要由 Encoder 和 Decoder 两部分组成.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Transformer 的工作流程:
&lt;ul&gt;
&lt;li&gt;首先获取输入每一个词的表示向量$X$, $X$由单词的embedding和位置的embedding相加得到.&lt;/li&gt;
&lt;li&gt;然后将$X$输入到Encoder中, 经过多层的Encoder Layer的处理, 得到编码后的表示$Z$.
&lt;ul&gt;
&lt;li&gt;$Z$用$X_{n \times d}$表示, 其中$n$是序列长度, $d$是词向量的维度.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;接着将目标序列的输入$Y$输入到Decoder中, 经过多层的Decoder Layer的处理, 并结合Encoder的输出$Z$, 最终得到预测结果$\hat{Y}$.如下图:
&lt;img src=&quot;./transformer_decoder.png&quot; alt=&quot;Transformer Decoder架构图&quot;&gt;
&lt;ul&gt;
&lt;li&gt;使用的过程中, 翻译到单词$i + 1$时, 需要通过&lt;strong&gt;Mask&lt;/strong&gt;操作掩盖住未来的信息, 以防止模型在预测时看到未来的词.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OK, 下面我们来具体看看Encoder Layer和Decoder Layer的结构.&lt;/p&gt;
&lt;h2&gt;Self-Attention 机制&lt;/h2&gt;
&lt;p&gt;Transformer 的核心是 Self-Attention 机制, 其结构如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./self_attention.png&quot; alt=&quot;Self-Attention架构图&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;左侧为&lt;strong&gt;Encoder block&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;右侧为&lt;strong&gt;Decoder block&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;红圈中的部分为&lt;strong&gt;Multi-Head Attention&lt;/strong&gt;机制, 是由多个Self-Attention组成的.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;可以看到&lt;strong&gt;Encoder block&lt;/strong&gt;包含一个&lt;strong&gt;Multi-Head Attention&lt;/strong&gt;层.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decoder block&lt;/strong&gt;包含两个&lt;strong&gt;Multi-Head Attention&lt;/strong&gt;层, 第一个用于处理目标序列的输入, 第二个用于结合Encoder的输出.&lt;/li&gt;
&lt;li&gt;每个Attention层后面都跟着一个**Feed-Forward Neural Network (FFN)**层.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为&lt;strong&gt;Self-Attention&lt;/strong&gt;机制是Transformer的核心, 因此我们重点来看一下它的计算过程.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;dsa&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图是&lt;strong&gt;Self-Attention&lt;/strong&gt;的计算流程图, 计算时需要用到三个矩阵: Query ($Q$), Key ($K$), Value ($V$), 实际过程中, 这三个矩阵都是通过输入的表示$X$经过线性变换得到的.&lt;/p&gt;
&lt;h3&gt;Q, K, V 的计算&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Self-Attention&lt;/strong&gt;机制中, 对于输入的表示$X \in \mathbb{R}^{n \times d}$, 可以使用线性变换矩阵$W_Q, W_K, W_V \in \mathbb{R}^{d \times d_k}$来计算$Q, K, V$:
$$
Q = X W_Q, \quad K = X W_K, \quad V = X W_V
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;2&quot;&gt;&lt;/p&gt;
&lt;h4&gt;实现&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np
from math import sqrt
import torch
import torch.nn as nn


class SelfAttention(nn.Module):
    def __init__(self, d_model, d_k, d_v):
        &quot;&quot;&quot;
        input: X : (batch_size, n, d_model)
        q : (batch_size, n, d_k)
        k : (batch_size, n, d_k)
        v : (batch_size, n, d_v)
        &quot;&quot;&quot;
        super(SelfAttention, self).__init__()
        self.d_k = d_k
        self.W_Q = nn.Linear(d_model, d_k)
        self.W_K = nn.Linear(d_model, d_k)
        self.W_V = nn.Linear(d_model, d_v)
        self._norm_factor = sqrt(d_k)
    
    def forward(self, X):
        Q = self.W_Q(X)  # Q : (batch_size, n, d_k)
        K = self.W_K(X)  # K : (batch_size, n, d_k)
        V = self.W_V(X)  # V : (batch_size, n, d_v)
        
        scores = torch.matmul(Q, K.transpose(-2, -1)) / sqrt(self.d_k)  # (batch_size, n, n)
        attn_weights = torch.softmax(scores, dim=-1)  #  (batch_size, n, n)
        output = torch.matmul(attn_weights, V)  # (n_batch_size, n, d_v)
        
        return output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此, 当我们得到了$Q, K, V$后, 就可以计算Attention的输出了:
$$
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{Q K^T}{\sqrt{d_k}}\right) V
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;3&quot;&gt;&lt;/p&gt;
&lt;p&gt;得到$QK^T$之后, 使用Softmax函数对每一行进行归一化, 即每一行的和都变为1.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;4&quot;&gt;&lt;/p&gt;
&lt;p&gt;最后将归一化后的权重矩阵与$V$相乘, 得到最终的Attention输出.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5.png&quot; alt=&quot;5&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图中&lt;strong&gt;softmax&lt;/strong&gt;矩阵的第一行可以理解为单词1对其他单词的关注程度, 最终单词1的输出$Z_1$等于所有单词的值$V$加权求和.&lt;/p&gt;
&lt;h3&gt;Multi-Head Attention&lt;/h3&gt;
&lt;p&gt;上一步中, 我们已经知道怎么使用Self-Attention机制来计算Attention的输出了, 但是Transformer中使用的是&lt;strong&gt;Multi-Head Attention&lt;/strong&gt;机制, 其结构如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./multi_head_attention.png&quot; alt=&quot;Multi-Head Attention架构图&quot;&gt;&lt;/p&gt;
&lt;p&gt;从上图中可以看到&lt;strong&gt;Multi-Head Attention&lt;/strong&gt;机制包含多个并行的Self-Attention头, 每个头都有自己的一组线性变换矩阵$W_Q^i, W_K^i, W_V^i$.&lt;/p&gt;
&lt;p&gt;首先将输入$X$分别传递到h个Self-Attention头中, 得到h个不同的Attention输出, 下面是h = 8的例子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from math import sqrt
import torch
import torch.nn as nn

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, d_k, d_v, h):
        &quot;&quot;&quot;
        input: X : (batch_size, n, d_model)
        q : (batch_size, d_model, d_k)
        k : (batch_size, d_model, d_k)
        v : (batch_size, d_model, d_v)
        &quot;&quot;&quot;
        super(MultiHeadAttention, self).__init__()
        self.h = h
        self.d_k = d_k
        self.d_v = d_v
        
        self.W_Q = nn.ModuleList([nn.Linear(d_model, d_k) for _ in range(h)])
        self.W_K = nn.ModuleList([nn.Linear(d_model, d_k) for _ in range(h)])
        self.W_V = nn.ModuleList([nn.Linear(d_model, d_v) for _ in range(h)])
        self.linear = nn.Linear(h * d_v, d_model)
    
    def forward(self, X):
        heads = []
        for i in range(self.h):
            Q = self.W_Q[i](X)
            K = self.W_K[i](X)
            V = self.W_V[i](X)
            
            scores = torch.matmul(Q, K.transpose(-2, -1)) / sqrt(self.d_k)
            attn_weights = torch.softmax(scores, dim=-1)
            head = torch.matmul(attn_weights, V) # (batch_size, n, d_v)
            heads.append(head)
        
        concat_heads = torch.cat(heads, dim=-1)  # (batch_size, n, h * d_v)
        output = self.linear(concat_heads)  # (batch_size, n, d_model)
        
        return output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./6.png&quot; alt=&quot;6&quot;&gt;&lt;/p&gt;
&lt;p&gt;得到8个输出后, 将它们在最后一个维度上进行拼接, 得到一个新的表示, 然后通过一个线性变换矩阵$W_O$将拼接后的表示映射回原始的维度$d_{model}$.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7.png&quot; alt=&quot;7&quot;&gt;&lt;/p&gt;
&lt;p&gt;可见&lt;strong&gt;Multi-Head Attention&lt;/strong&gt;输出的矩阵维度与输入矩阵的维度相同, 这样就可以方便地将其与后续的层进行连接.&lt;/p&gt;
&lt;h3&gt;other components&lt;/h3&gt;
&lt;p&gt;剩余的层比较简单, 因此不再赘述.&lt;/p&gt;
&lt;h2&gt;Decoder Layer&lt;/h2&gt;
&lt;p&gt;Decoder Layer的结构如下图红框内所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./8.png&quot; alt=&quot;8&quot;&gt;&lt;/p&gt;
&lt;p&gt;其与Encoder Layer的主要区别在于多了一个&lt;strong&gt;Masked Multi-Head Attention&lt;/strong&gt;层, 该层用于处理目标序列的输入, 并且在计算Attention时会掩盖住未来的信息, 以防止模型在预测时看到未来的词.&lt;/p&gt;
&lt;h3&gt;第一个Multi-Head Attention&lt;/h3&gt;
&lt;p&gt;我们重点解释一下Mask操作.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一步是Decoder的输入矩阵和Mask矩阵, Mask矩阵是一个上三角矩阵, 用于掩盖未来的信息.&lt;/li&gt;
&lt;li&gt;接下来的操作和之前的Self-Attention机制类似, 通过输入矩阵计算$Q, K, V$., 之后计算$QK^T$.&lt;/li&gt;
&lt;li&gt;然后将Mask矩阵应用到$QK^T$上, 将被掩盖的位置设置为负无穷大, 这样在Softmax计算时, 这些位置的权重会变为0.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./9.png&quot; alt=&quot;9&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;最后进行Softmax归一化, 并与$V$相乘, 得到最终的Attention输出.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第二个Multi-Head Attention&lt;/h3&gt;
&lt;p&gt;第二个Multi-Head Attention层与Encoder Layer中的Multi-Head Attention层类似, 只是这里的$K$和$V$来自于Encoder的输出$Z$, 而$Q$来自于第一个Attention层的输出.&lt;/p&gt;
&lt;p&gt;根据Encoder的输出$C$计算得到$K$和$V$, 根据上一个Attention的输出$D$计算得到$Q$, 然后计算Attention的输出.&lt;/p&gt;
&lt;h2&gt;时间复杂度分析&lt;/h2&gt;
&lt;p&gt;Transformer 的时间复杂度主要来自于 Self-Attention 机制. 对于一个长度为$n$的序列, Self-Attention 的时间复杂度为$O(n^2 \cdot d)$, 其中$d$是词向量的维度. 这是因为在计算$QK^T$时, 需要进行$n \times n$的矩阵乘法, 每个元素的计算涉及到$d$维的向量点积.
因此, 对于一个包含$L$层Encoder和Decoder的Transformer模型, 总的时间复杂度为$O(L \cdot n^2 \cdot d)$.&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Transformer 应该是这样的.&lt;/p&gt;</content:encoded><h:img src="/_astro/image.BcXSMP-P.png"/><enclosure url="/_astro/image.BcXSMP-P.png"/></item><item><title>SLAM Former 阅读</title><link>https://www.hjcheng0602.cn/blog/slam_former/slam_former</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/slam_former/slam_former</guid><description>论文SLAM Former的阅读记录</description><pubDate>Sat, 01 Nov 2025 15:48:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Spoiler } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;最近几天读了&lt;a href=&quot;https://arxiv.org/abs/2509.16909&quot;&gt;SLAM-Former: Putting SLAM into One Transformer&lt;/a&gt;这篇很近很近的工作，本文笔记记录用于后续翻阅学习&lt;/p&gt;
&lt;p&gt;首先，SLAM-Former与之前读到的所有论文相似，都是致力于从RGB图像序列中恢复三维场景结构和相机位姿等属性的工作。但是与之前的工作（包含一个冗长复杂的pipeline）不同，
SLAM-Former对已有的transformer架构进行了大胆的改进，使之更适合进行重建任务，并在实验中得到了competitive的结果。&lt;/p&gt;
&lt;h2&gt;模型结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./image.png&quot; alt=&quot;SLAM-Former架构图&quot;&gt;&lt;/p&gt;
&lt;p&gt;据作者所述， SLAM-Former的主要pipeline由frontend和backend两部分组成，至于模型的backbone，SLAM-Former建立在一个Transformer架构之上，
而这个Transformer aggregate了intraframe和interframe的信息，并使用task specific heads预测不同的三维属性。
值得注意的是， 这个Transformer的输入与$\pi^3$类似，对所有的输入的image token共享一个相同的register tokens
从而使模型不依赖于一个不稳定的reference frame。&lt;/p&gt;
&lt;p&gt;模型的backbone包含了$L$层组合了intra-frame attention和inter-frame attention
来联合捕捉图像内容和图像之间的关系。&lt;/p&gt;
&lt;p&gt;此外，Front end部分负责增量式的逐帧重建，back end负责全局的点云对齐和相机优化，他们共享一个
Transformer backbone。&lt;/p&gt;
&lt;h3&gt;Front end&lt;/h3&gt;
&lt;p&gt;图中大部分内容都是front end的处理细节，当一个新的frame输入时，frontend首先会
决定其是否为keyframe，如果是的话，则会进行进一步处理。&lt;/p&gt;
&lt;p&gt;当给定一个frame sequence时，frontend将每一个frame映射到一个map token集合中：
$$
\mathbb{F}&lt;em&gt;t = f&lt;/em&gt;{fn}(\mathbb{I}&lt;em&gt;t)&lt;/em&gt;{{C_k }&lt;em&gt;{K\in S}}
$$
这里, ${C_k}&lt;/em&gt;{K\in S}$表示之前keyframe的&lt;strong&gt;KV cache&lt;/strong&gt;，
， $S$代表着keyframe的索引集合，$F_t$是当前frame的map token, 作为该frame的
一个隐式神经表示。 同时新的KV cache也通过$C_t = Cache(f(\mathbb{F}&lt;em&gt;t))$产生，
也会视情况被扩充到${C_k}&lt;/em&gt;{K\in S}$中。&lt;/p&gt;
&lt;h4&gt;Keyframe detection&lt;/h4&gt;
&lt;p&gt;在上一步中我们已经对当前帧generated了map token，接下来我们需要决定是否为keyframe.&lt;/p&gt;
&lt;p&gt;作者采用了pose head来预测当前帧的pose：
$$
g_t = h_{pose}(\mathbb{F}_t)
$$&lt;/p&gt;
&lt;p&gt;当当前frame的relative pose与最近的keyframe的pose之间的差异大于一个阈值时，
则将当前frame标记为keyframe。&lt;/p&gt;
&lt;p&gt;但是作者在论文里又表明，在检测frame是否为keyframe时，他们并没有依赖KV cache
,而是直接应用了$f_{fn}(I_{k_{prev}}, T_t)$来检测，就相当于之前的KV cache是将该图片
与所有的keyframe进行attention计算，而这里则是只与最近的keyframe进行attention计算。
这样增加了效率并且避免了选取一个特定的reference frame。（这里似乎我没怎么懂跟特定的reference frame有什么关系）&lt;/p&gt;
&lt;h4&gt;Front end tracking and mapping&lt;/h4&gt;
&lt;p&gt;接着上一步，如果一个新的frame已经被认为是一个keyframe，我们就可以重新利用全部的KV cache来重新
计算他的map token, 并更新M, S.&lt;/p&gt;
&lt;p&gt;好了， front end 到这里差不多结束了，作者说frontend只依赖于过去的keyframe，
使得其适合于online的tracking，然而， 这种处理顺序会导致误差累积和局部不一致，
为了解决这一问题，作者引入了一个back end模块来进行global refinement.&lt;/p&gt;
&lt;h3&gt;Backend&lt;/h3&gt;
&lt;p&gt;Backend的主要任务是refine所有的frame来达到全局的一致性。传统的
SLAM系统通常会使用loop closure和bundle adjustment来实现这一点，
但是这些方法都非常的costly, 作为对比，作者使用了一个transformer-based的
back end来进行全局的优化。&lt;/p&gt;
&lt;p&gt;作者认为这个设计的有效性在于backend transformer内部的full attention机制，
他的全局感受野使得模型能够完成误差纠正和结构一致性。&lt;/p&gt;
&lt;p&gt;此外， 为了继承backend refinement的优势，frontend和backend共享了KV cache，
使得frontend能够受益于backend的全局优化。&lt;/p&gt;
&lt;h2&gt;Training Strategy&lt;/h2&gt;
&lt;p&gt;与以往的一些论文不同，SLAM-Former的创新点不止在于模型架构，也在于一些训练策略。&lt;/p&gt;
&lt;p&gt;作者的目标是使一个transformer同时胜任frontend和backend的任务，为了达到这个目标，
作者用三种模式联合训练，每一个模式都对应着不同的输入输出对。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image2.png&quot; alt=&quot;训练模式图&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Training Frontend&lt;/h3&gt;
&lt;p&gt;Frontend用了一个causal mask来确保每一个frame只能访问之前的keyframe。&lt;/p&gt;
&lt;p&gt;然而，纯净的使用causal mask会自动的将第一帧作为reference frame，
作者又注意到党对两帧或更多帧进行联合操作时，没有单一的refernce frame,
这避免了后续帧需要与reference frame pose 相似的要求。&lt;/p&gt;
&lt;p&gt;因此， 作者对前两帧使用了full attention，并同时对所有后续frame使用causal mask,
在这种情况下，inference时，keyframe detection将最后一帧关键帧和当前的输入帧进行处理，
tracking and mapping时， 前两个keyframe则会联合处理决定全局坐标。&lt;/p&gt;
&lt;p&gt;import { Aside, Tabs, TabItem, MdxRepl } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For tracking and mapping, the
first two keyframes are jointly processed to determine the
global coordinate.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;取前两帧的做法与之前的tracking and mapping部分提到的use full KV cache不符，
我感觉不怎么理解。&lt;/p&gt;
&lt;h3&gt;Training Frontend with Backend Cooperation&lt;/h3&gt;
&lt;p&gt;为了在frontend和backend之间建立联系，作者使用maxed attention来模拟backend和
cache sharing的过程。&lt;/p&gt;
&lt;p&gt;具体来说，采用混合注意力在一个统一的正向传播中同时完成地图精炼（后端/全注意力）和新数据处理，
并且前端的casual attention并非独立工作，而是以KV cache为条件，实现了高效且信息流一致的前端-后端协作，确保前端的实时处理结果能够立即对齐到后端修正后的全局结构。&lt;/p&gt;
&lt;p&gt;$$
F = f_{fn}(I)&lt;em&gt;{C&lt;/em&gt;{M}}
$$&lt;/p&gt;
&lt;p&gt;woc这什么花式操作啊&lt;/p&gt;
&lt;h3&gt;Training Backend&lt;/h3&gt;
&lt;p&gt;作者最后使用full attention来训练backend transformer，&lt;/p&gt;
&lt;h2&gt;Joint Training&lt;/h2&gt;
&lt;p&gt;在所有的三种模式中，三维属性均是由task specific heads 预测的：&lt;/p&gt;
&lt;p&gt;$$
\mathbf{P}^&lt;em&gt;,\mathbf{\Sigma}^&lt;/em&gt;,\mathbf{g}^*=h(\mathbf{F}).
$$&lt;/p&gt;
&lt;p&gt;但值得注意的是， 并不像其他的工作一样，SLAM-Former只预测每一帧的local
pointmap 来避免设定一个特定的世界坐标系的需求，这倒是与$\pi^3$非常相似。&lt;/p&gt;
&lt;p&gt;剩下的loss函数都比较常规。
这三种模式都会在一个batch中共享权重依次训练。&lt;/p&gt;
&lt;h2&gt;Pipeline&lt;/h2&gt;
&lt;p&gt;在图片和叙述过程中， pipeline已经是显而易见的，于是我便不再赘述。&lt;/p&gt;
&lt;h2&gt;Experimental Setup&lt;/h2&gt;
&lt;p&gt;本模型有36层framewise 和 global attention相结合的transformer layer, 训了10个
epoch, 在32个A100 上训练了11小时。可以可以。&lt;/p&gt;
&lt;h2&gt;results&lt;/h2&gt;
&lt;p&gt;模型在pose， tracking 和 reconstruction等任务上都达到了很好的指标。数据冗长不再多说。
值得一提的是作者对Front end 和 back end 的联系的理解。&lt;/p&gt;
&lt;p&gt;back end assist front end无疑是显而易见的，但是作者还发现back end同样也
benefit from front end, 作者解释了是因为back end 使用了来自于frontend 的
implicit的顺序信息，从而使得back end能够更好地理解frame之间的关系。（迷）&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;总之，SLAM-Former通过对transformer架构的改进和训练策略的设计，
成功地实现了一个统一的模型来处理SLAM任务。&lt;/p&gt;
&lt;p&gt;但SLAM-Former仍然存在一些局限性，比如说作者用full attention来替代传统的loop
closure和bundle adjustment，受限于full attention的计算复杂度，模型难以处理非常长的序列，
其次，frontend 不支持一个local的inference， 因为在inference之前需要将所有的KV cache输入到frontend 中。&lt;/p&gt;
&lt;p&gt;此外， 文章中没有提到的是，我去看他们的demo， 发现重建结果有很明显的分块化现象，目前不知是否与transformer的架构有关。
&lt;img src=&quot;./image3.png&quot; alt=&quot;重建结果&quot;&gt;&lt;/p&gt;
&lt;p&gt;此文撰写的时候，SLAM-Former的代码尚未开源，期待后续的代码发布。&lt;/p&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;</content:encoded><h:img src="/_astro/imagecopy.CfyJiNiE.png"/><enclosure url="/_astro/imagecopy.CfyJiNiE.png"/></item><item><title>重返vggt</title><link>https://www.hjcheng0602.cn/blog/vggt_new/vggt</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/vggt_new/vggt</guid><description>一段时间之后, 对vggt的重新的详细阅读</description><pubDate>Fri, 31 Oct 2025 09:04:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Spoiler } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;这是本人在学了一些基础知识并做了一些实验之后, 察觉到之前对于一些经典论文的阅读并不充分, 于是决定重新阅读&lt;a href=&quot;https://jytime.github.io/data/VGGT_CVPR25.pdf&quot;&gt;VGGT&lt;/a&gt;一文, 并写下这篇文章, 以供后续查阅.&lt;/p&gt;
&lt;p&gt;首先, VGGT是一个完全的前馈式神经网络用于多目重建任务, 通过look into他的代码, 可以看到基本上是没有什么pipeline的, 直接将图片输入网络, 然后输出各种三维属性, 并在作者的宣称下, 他们所预测的多个指标在存在BA的前提下
均达到
子领域的SOTA水平, 这一点非常厉害.&lt;/p&gt;
&lt;h2&gt;模型结构&lt;/h2&gt;
&lt;p&gt;VGGT的backbone是一个标准的transformer结构, 首先接受大量图片作为输入, 首先通过一个DINO提取了分块的feature, 然后将这些feature通过一个主体网络结构(包含了Alternating frame-wise layer和global attention layer)
进行处理, 最后通过多个task-specific heads输出不同的三维属性.
&lt;img src=&quot;./imagecopy.png&quot; alt=&quot;VGGT架构图&quot;&gt;
接下来, 我们详细叙述各个细节部分:&lt;/p&gt;
&lt;h3&gt;Alternating attention frame-wise layer&lt;/h3&gt;
&lt;p&gt;据文章作者所述, 该AA机制与标准的transformer attention机制有所不同, 能够使Transformer以交替的方式聚焦每一帧和全局.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;frame wise attention layer: 该层的attention仅在同一帧内进行, 也就是说, 每个patch只能与同一帧内的其他patch进行attention计算. 这样做的好处是能够更好地捕捉每一帧内部的局部特征.&lt;/li&gt;
&lt;li&gt;global attention layer: 该层的attention在所有帧之间进行, 也就是说, 每个patch可以与所有帧内的其他patch进行attention计算. 这样做的好处是能够捕捉不同帧之间的全局特征.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外值得一提的是, 作者采用了$L = 24$层的AA机制, 并通过消融实验证明了AA机制的有效性, 此外, 作者声称他们的架构并没有采用cross attention, 只采用self attention.&lt;/p&gt;
&lt;h3&gt;任务特定的heads&lt;/h3&gt;
&lt;p&gt;将输入的图片通过backbone网络处理后, 会得到一个全局的feature表示, 然后通过多个task-specific heads输出不同的三维属性. 值得注意的是, DINO编码的feature并非直接输入到AA中, 而是被添加了一个额外的相机token
$t_i^g \in \mathbb{R}^{1 \times C}$和四个register tokens$t_i^R \in \mathbb{R}^{4 \times C}$进行增强, 然后将$(t_i^L, t_i^g, t_i^R)$作为最终的输入.&lt;/p&gt;
&lt;p&gt;此处值得注意的是, 第一帧的输入token是$(t_1^g = t_{ini}^g, t_1^R = t_{ini}^R)$, 之后的帧的输入token是$(t_i^g = t_{follow}^g, t_i^R = t_{follow}^R)$, 也就是说, 第一帧和之后的帧的camera token和register token是不同的.
但是作者说他们都是learnable的. 这使得模型能够将第一帧和其他帧区分开来, 并在第一个相机的坐标系下表示全局点云以及各种数据.但是, 经过AA层之后, 本来被赋予同一初值的camera token和register
token均会变为帧特定的, 这是因为AA层的frame-wise attention layer会使得每一帧的token在不同的计算中产生不同的表示.&lt;/p&gt;
&lt;p&gt;最后遵循常规做法, register token会被丢弃, camera token和image token会被保留用于预测.&lt;/p&gt;
&lt;h4&gt;Camera parameter head&lt;/h4&gt;
&lt;p&gt;这个head从上图中的模型的backbone就可以看到, 他是将camera token通过4个self-attention layers进行处理, 然后通过一个MLP预测出每一帧的相机参数(包含内参和外参).&lt;/p&gt;
&lt;h4&gt;Dense Prediction&lt;/h4&gt;
&lt;p&gt;输出的image token 在这里被使用, 用于预测depth map $D_i$, point map $P_i$ 和 tracking features $F_i$. 更具体地来讲, $\hat{t}_i^I$首先会通过一个DPT head转化为一个dense feature map
$F_i \in \mathbb{R}^{C&apos;&apos; \times H \times W}$, 之后每一个$F_i$会通过一个$3 \times 3$的卷积层解析出corresponding depth和point map. 另外, DPT头同样也会输出 dense feature map $T_i$用于后续的tracking,
在此同时, vggt同样也会输出confidence map $\Sigma_i^D \in \mathbb{R}^{C \times H \times W}$和$\Sigma_i^P \in \mathbb{R}^{C \times H \times W}$用于表示depth和point的置信度. 这个置信度用于后续的模型的loss计算和
真实预测时的conf输出.&lt;/p&gt;
&lt;h4&gt;tracking&lt;/h4&gt;
&lt;p&gt;这一方面我并不打算去深入了解, 因此先跳过.&lt;/p&gt;
&lt;h2&gt;Training&lt;/h2&gt;
&lt;h3&gt;Loss function&lt;/h3&gt;
&lt;p&gt;VGGT的loss function包含多个部分, 主要包含以下几种:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Camera loss: 这个loss监管了相机参数$L_{camera} = \sum_{i=1}^{N} ||\hat{g}&lt;em&gt;i - g_i||&lt;/em&gt;{\epsilon}$, 使用了Huber loss.&lt;/li&gt;
&lt;li&gt;Depth loss: 这个loss沿用了dust3r的loss设计$\mathcal{L}&lt;em&gt;{\mathrm{depth}}=\sum&lt;/em&gt;{i=1}^N|\Sigma_i^D\odot(\hat{D}_i-D_i)|+|\Sigma_i^D\odot(\nabla\hat{D}_i-\nabla D_i)|-\alpha\log\Sigma_i^D$&lt;/li&gt;
&lt;li&gt;Point loss: 这个loss同样沿用了dust3r的loss设计$\mathcal{L}&lt;em&gt;{\mathrm{point}}=\sum&lt;/em&gt;{i=1}^N|\Sigma_i^P\odot(\hat{P}_i-P_i)|+|\Sigma_i^P\odot(\nabla\hat{P}_i-\nabla P_i)|-\beta\log\Sigma_i^P$&lt;/li&gt;
&lt;li&gt;Tracking loss: 这个loss监管了tracking feature的质量, 具体细节我并不打算深入了解, 因此先跳过.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此, 最终的loss function为:
$$
\mathcal{L}&lt;em&gt;{total} = \mathcal{L}&lt;/em&gt;{camera} + \mathcal{L}&lt;em&gt;{depth} + \mathcal{L}&lt;/em&gt;{point} + \lambda_{tracking} \mathcal{L}_{tracking}
$$&lt;/p&gt;
&lt;h3&gt;坐标Normalization&lt;/h3&gt;
&lt;p&gt;如果缩放的话, 重建结果应该同样也是正确的, 为了消除这种不确定性, 作者采用了归一化进行处理. 首先将所有量表示在第一个相机的坐标系中, 然后计算所有点的平均欧氏距离, 然后利用该尺度归一化相机平移, 点云坐标和深度值.&lt;/p&gt;
&lt;p&gt;值得注意的是, 作者没有对预测结果施加任何归一化, 相反强制模型去学习预测归一化后的值, 这样做的好处是能够使得模型更好地适应不同尺度的场景.&lt;/p&gt;
&lt;h3&gt;details&lt;/h3&gt;
&lt;p&gt;我难以想象训练的规模, 按照作者所述, 这一个transformer模型包含了$1.2B$的参数, 在64块A100上训练了9天, 属实是第一次见了.&lt;/p&gt;
&lt;p&gt;另外, 训练的数据集之多也是难以想象:
&lt;img src=&quot;./imagecopy1.png&quot; alt=&quot;dsfa&quot;&gt;
有点离谱了.&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;vggt 的指标基本上达到SOTA水平, 但是值得注意的是, 直接的输出并没有达到, 作者加入了BA优化之后才达到了SOTA, 因为BA是一个costly的优化过程, 因此我觉着这一方面或许还可以改进? 作者在论文中提到了
应用diffentiable BA的可行性, 但是也因为BA的计算量过大, 因此并没有进行进一步的尝试.&lt;/p&gt;
&lt;p&gt;此外, VGGT向我们展示了不需要一个复杂的pipeline也可以进行高质量的多目重建说你呢, SLAM3R, 我TM的快改吐了, 再结合最近发布的SLAM Former, 我觉着这是一个很有意义的方向.&lt;/p&gt;
&lt;p&gt;import { Aside, Tabs, TabItem, MdxRepl } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;此外, vggt另一个重要的发现是, 通过depth和pose反解出来的点云比直接预测的点云要好.&lt;/p&gt;
&lt;p&gt;ok, 让我们把仓库链接抬出来:&lt;/p&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;
&lt;p&gt;另外, 这是真的可以的嘛?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./imagecopy2.png&quot; alt=&quot;iasdf&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/image.DB_YUSHx.png"/><enclosure url="/_astro/image.DB_YUSHx.png"/></item><item><title>论文阅读记录：reloc3r</title><link>https://www.hjcheng0602.cn/blog/bloc3r/bloc3r</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/bloc3r/bloc3r</guid><description>位姿估计模型reloc3r的阅读和学习</description><pubDate>Sat, 06 Sep 2025 14:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;最近，我们在尝试将SLAM3R进行使之输出不限于点云，还有位姿估计、深度图、局部定位等结果的改造，大体上来讲，我对这个改造的感觉就是端了一个类似于VGGT的重建结构出来。于是，为了了解一下现在利用transformer做位姿估计的工作，我选择了组里的学长的论文：&lt;a href=&quot;https://github.com/ffrivera0/reloc3r&quot;&gt;Reloc3r: Large-Scale Training of Relative Camera Pose Regression for
Generalizable, Fast, and Accurate Visual Localization&lt;/a&gt;来阅读，本文用来记录对这个模型的理解以及个人的感受。&lt;/p&gt;
&lt;p&gt;首先，论文上来又是经典的针砭时弊环节🤣，论文指出了之前的工作分为&lt;strong&gt;APR&lt;/strong&gt;和&lt;strong&gt;RPR&lt;/strong&gt;两种方式，但是各有各的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;APR : 绝对位姿回归，它主要是从图片中直接回归位姿，优点是有更高的推理速度和准确度，但是它的缺点也很明显：大多数这种方法都是针对场景有效，并且在训练时需要密集点图，这限制了他们在真实世界中投入应用。&lt;/li&gt;
&lt;li&gt;RPR : 相对位姿回归：它是估计一对图片的相对位姿，相比于绝对位姿回归的好处在于它不需要密集点图的训练，但是，它的准确度表现非常差，远远不及APR。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了解决这些问题，论文提出了一种新型的对称有效的网络，并在一个&lt;strong&gt;特大&lt;/strong&gt;的数据集上进行训练，最终得到了&lt;strong&gt;state of the art&lt;/strong&gt;的水平。&lt;/p&gt;
&lt;h2&gt;模型结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./model.png&quot; alt=&quot;d&quot;&gt;
模型主要由两个模块组成：相对位姿回归网络和运动平均模块&lt;/p&gt;
&lt;h3&gt;相对位姿回归网络&lt;/h3&gt;
&lt;p&gt;这个网络如图片左边所示，是由两个完全相同的vit transformer分支构成，并且两个分支共享权重，这有效的消除了输入顺序带来的不利影响，代表着训练得到了大幅简化，并且提高了计算速度和存储效率。&lt;/p&gt;
&lt;p&gt;细节在于通过ViT encoder图片被编码成特征序列之后，他们之后通过的decoder是Cross attention的，这能够使模型同时理解两张图片之中的信息，最后，decoder输出的信息会经过Pose regression Head
这个head会将decoder的输出转化为相对旋转和相对位移，其中相对旋转一开始会以一个9维向量来表示，随后通过SVD分解完成得到旋转矩阵。&lt;/p&gt;
&lt;p&gt;因此，我们这个网络最后的输出就是图A相对于图B的位姿变换和图B相对于图A的位姿变换。&lt;/p&gt;
&lt;h3&gt;运动平均模块&lt;/h3&gt;
&lt;p&gt;理论上来说，第一步网络的输出的精度应当已经达标，并且网络同时输出的两个相对位姿变换矩阵应该互你，从经验上来看，这两个位姿变换矩阵的精度相似，因此我们直接选择了一个非学习的模块用于转换两个输出的相对位姿。&lt;/p&gt;
&lt;p&gt;其中有一些细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;旋转平均的处理：模型将多个对于一张图片的相对旋转转换为绝对旋转处理，并使用四元数表示，最终选取中位数来作为绝对旋转，增强了模型的鲁棒性。&lt;/li&gt;
&lt;li&gt;相机中心三角化的处理：因为几何点的平均/中位数化并不可解，因此我们转而通过最小二乘法寻找到所有平移方向距离之和最小的点，将这个点作为相机预测的光心。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;损失函数&lt;/h3&gt;
&lt;p&gt;模型的损失包括两方面：旋转损失和位移损失。文章将他们都表示成了角度：
$$
\mathcal{l}_R = \arccos(\frac{tr(\hat{R}^{-1}R) - 1}{2}), \mathcal(l)_T = \arccos(\frac{\hat{t} \cdot t}{||\hat{t}||||t||})
$$
然后将两者相加得到最后的总损失。显然这是一种无尺度的方法，解决了不同数据集之间度量尺度不统一的问题。&lt;/p&gt;
&lt;h2&gt;分析流程&lt;/h2&gt;
&lt;p&gt;该模型的处理流程大致如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输入： 一个查询图像$I_q$和一个带位姿数据的数据库${I_{d_n}}$.&lt;/li&gt;
&lt;li&gt;检索： 使用NetVLAD在数据库中为$I_q$检索出Top-K个最相似的图像${I_{d_K}}$.&lt;/li&gt;
&lt;li&gt;相对位姿预测：将$K$个图像对$(I_q, I_{d_i})$逐一送入相对位姿回归网络，得到$K$个相对位姿估计（旋转矩阵和无尺度的平移方向）&lt;/li&gt;
&lt;li&gt;绝对位姿聚合：
&lt;ul&gt;
&lt;li&gt;利用数据库图像已知的绝对位姿旋转和预测的相对旋转计算出$K$个图像的绝对旋转统计，然后通过取中值得到最终的旋转$\hat{R}_q$。&lt;/li&gt;
&lt;li&gt;利用所有有效的图像对和估计的$\hat{R}_q$进行相机中心的三角化，然后通过最小二乘法解出相机中心，从而得到所有的位姿估计。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;输出&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;数据分析&lt;/h2&gt;
&lt;p&gt;第一次写数据分析模块🧐，有所不完善请原谅🥺。&lt;/p&gt;
&lt;h3&gt;性能评价指标&lt;/h3&gt;
&lt;h4&gt;相对位姿&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./rra.png&quot; alt=&quot;rra&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RRA@15, RTA@15, mAA@30&lt;/strong&gt;，分别是相对旋转、相对位移在15°阈值内的准确度、以及30°阈值下的平均准确率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./auc.png&quot; alt=&quot;auc&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AUC@5°/10°/20°&lt;/strong&gt;: 位姿误差（旋转和平移角度误差的最小值）在5°/10°/20°阈值下的精度曲线下面积 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;绝对位姿&lt;/h4&gt;
&lt;p&gt;平移和旋转中位数误差（m and degree）：
&lt;img src=&quot;./abso.png&quot; alt=&quot;abso&quot;&gt;&lt;/p&gt;
&lt;h3&gt;有效性验证&lt;/h3&gt;
&lt;p&gt;查看上面的图表便可看出，模型在个主流的公开数据集 (ScanNet1500, RealEstate 10K, ACID, CO3Dv2, 7 Scenes, Cambridge Landmarks) 上与当前最先进的方法（包括非回归和回归两大类）进行全面对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;相对位姿估计&lt;/strong&gt;: 在ScanNet1500, RealEstate 10K和ACID数据集上，Reloc3r显著优于所有其他相对位姿回归(PR)方法，并且性能达到甚至超过了顶尖的非PR方法，同时速度快了几个数量级（例如，在ScanNet上比NoPoSplat快50倍以上） 。在CO3Dv2数据集上，Reloc3r在所有多视图评估指标上均达到SOTA 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视觉定位&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在7Scenes (室内) 数据集上，Reloc3r的平均误差为 0.04m / 1.02°，超越了所有之前在新场景上评估的RPR方法，并达到了与需要场景专门训练的APR方法相媲美的精度。&lt;/li&gt;
&lt;li&gt;在Cambridge Landmarks (室外) 数据集上，Reloc3r同样超越了所有RPR方法，与之前的SOTA RPR方法相比，平均位姿误差降低了约一半，其平均旋转误差甚至优于所有APR方法 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;消融实验&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./lab.png&quot; alt=&quot;lab&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对称性&lt;/strong&gt;
论文另外训练了一个使用了独立的两个ViT分支的相对位姿回归网络，显而易见性能是弱于default版本的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不含尺度信息&lt;/strong&gt;
同样训练了一个同时输出尺度信息的模型，显而易见其准确性比不对称还差。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;有趣的发现&lt;/h2&gt;
&lt;p&gt;论文在查看decoder的交叉熵注意力图时发现：模型在没有直接监督的情况下，自发地学会了在图像对之间建立有意义的块级别匹配。（如下图）
&lt;img src=&quot;./findings.png&quot; alt=&quot;finding&quot;&gt;&lt;/p&gt;
&lt;h2&gt;局限性&lt;/h2&gt;
&lt;p&gt;作者发现当检索到的数据库图像与目标图像共线的时候，运动平均模块并不能恢复尺度。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Reloc3r使用了一个相当简洁的模型结构完成了SOTA水平，但其付出的代价是非常庞大的训练数据。这似乎在向我们说明只要数据够多够大，我们便可以训练出足够高性能的模型，这似乎在
告诉我们多造一下SLAM3R V2的数据🤣。&lt;/p&gt;
&lt;p&gt;OK，这篇论文的代码仓库如下：&lt;/p&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.BlrgzKm7.png"/><enclosure url="/_astro/cover.BlrgzKm7.png"/></item><item><title>采用waline配置博客评论出现fail to fetch解决方案</title><link>https://www.hjcheng0602.cn/blog/waline_set/waline</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/waline_set/waline</guid><description>在使用waline配置博客评论功能时，出现了fail to fetch错误，尝试网上方法失败后，找到了另一个导致失败的原因</description><pubDate>Thu, 04 Sep 2025 21:32:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;在配置本博客时，按照&lt;code&gt;docs&lt;/code&gt;中给的方法配置waline评论系统完成后，尝试使用却发现总是报&lt;code&gt;Fail to fetch&lt;/code&gt;错误，尝试到网上搜索解决方案发现全是重新填写
&lt;code&gt;LEAN_ID&lt;/code&gt;..之类的方法，本人按照这些方法逐一试过之后发现均未能解决问题，经过一系列的排查后，发现问题出在&lt;strong&gt;Vercel&lt;/strong&gt;服务端。&lt;/p&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;p&gt;请查看您对应的vercel服务设置中的&lt;strong&gt;Vercel Authentication&lt;/strong&gt;选项，如果开启的话，关闭之后就可以正常使用评论系统了：
&lt;img src=&quot;./protect.png&quot; alt=&quot;picture&quot;&gt;&lt;/p&gt;
&lt;p&gt;这一个方法解决的是：控制台-网络中fetch失败的现象。&lt;/p&gt;
&lt;p&gt;另外，如果仍然不好，在对应网页页面按下F12打开控制台，如果网络一项没有相关报错而控制台中出现了水合失败之类的报错，这时候就可以编辑本地文件来规避错误了。&lt;/p&gt;
&lt;p&gt;这篇有点太水了（x，但是这个评论的bug属实是困扰了我好长时间啊，前前后后搭进去的时间快20小时了哼。&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.Q_EE2igi.png"/><enclosure url="/_astro/cover.Q_EE2igi.png"/></item><item><title>论文阅读记录：Fast3R</title><link>https://www.hjcheng0602.cn/blog/fast3r/fast3r</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/fast3r/fast3r</guid><description>Fast3R这篇论文的阅读，本人自己对Fast3R的理解</description><pubDate>Thu, 04 Sep 2025 08:18:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;OK,本人昨天又读了一篇3D reconstruction方向的论文：&lt;a href=&quot;https://arxiv.org/abs/2501.13928&quot;&gt;Fast3R: Towards 3D Reconstruction of 1000+ Images in One Forward Pass&lt;/a&gt;，因此写下此篇Blog分享自己的理解与发现。&lt;/p&gt;
&lt;p&gt;Fast3R从本质上来说感觉和SLAM3R解决的是一类问题，都是对原本DUst3R存在的局限性：一次只能对两张图片进行处理，如果对多张图片进行处理的话，DUst3R则是选择进行两两配对进行重建，最后进行全局坐标下的对齐，显然这将会是一个
$\mathcal{O}(N^{2})$的过程。而Fast3R提出了对于&lt;strong&gt;打乱序列的多张图片（1000+）&lt;strong&gt;的处理方法，SLAM3R则是解决了由&lt;/strong&gt;视频&lt;/strong&gt;进行重建的方法。感觉两者的本质上的区别就是input的图像集是否有序，后续两者的网络结构区别也正是在此。&lt;/p&gt;
&lt;p&gt;从论文的introduction上来看，他们主要做了以下三方面的贡献：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建了Fast3R，一个基于Transformer的对多目图片重建点图的端到端的模型，据论文所述，它在速度上取得显著提升，并且可以规模化计算。&lt;/li&gt;
&lt;li&gt;展示了随着训练时视角增多，模型表现也会加强。另外，当推理时视角增多时，每张视角重建结果的精确度也会提升。并且模型可以处理比训练时多得多的模型。&lt;/li&gt;
&lt;li&gt;在相机的位姿定位上达到了&lt;strong&gt;SOTA&lt;/strong&gt;水平，另外也展现出了极快的速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;好的，现在到了我们喜闻乐见的介绍模型环节啦！&lt;/p&gt;
&lt;h2&gt;模型&lt;/h2&gt;
&lt;p&gt;Fast3R给出了一个看起来在推理环境就很庞大的结构图：
&lt;img src=&quot;./model.png&quot; alt=&quot;Fast3R&quot;&gt;&lt;/p&gt;
&lt;h3&gt;问题定义&lt;/h3&gt;
&lt;p&gt;从图中右边就可以看到，Fast3R采用了两个头：Global Head 和 Local Head来处理输出的token，因此可见，Fast3R为每张图片预测了两个点图：本地坐标系下的点图$X_L$和全局坐标系下的点图$X_G$，可以用公式表示：
$$
\mathrm{Fast3R}:\mathbf{I}\to(\mathbf{X}&lt;em&gt;\mathrm{L},\Sigma&lt;/em&gt;\mathrm{L},\mathbf{X}&lt;em&gt;\mathrm{G},\Sigma&lt;/em&gt;\mathrm{G})
$$
$\Sigma_X$指代的是$X$点图的置信度。&lt;/p&gt;
&lt;p&gt;值得注意的是，全局坐标系值得是第一张图片的坐标系，本地坐标系是每个对应图片的坐标系。（虽然Fast3R并没有次序的概念，但其也需要一个切入点，所以随机选取了一张图片作为第一张图片）&lt;/p&gt;
&lt;h3&gt;训练对象&lt;/h3&gt;
&lt;p&gt;类似于Dust3R，Fast3R的损失函数分别采用了同样的处理方法处理本地点图和全局点图两部分：
$$
\mathcal{L}&lt;em&gt;\mathrm{total}=\mathcal{L}&lt;/em&gt;\mathrm{X_G}+\mathcal{L}_\mathrm{X_L}
$$
阅读其论文，发现其与Dust3R的损失函数基本一致，因此不多赘述。&lt;/p&gt;
&lt;h3&gt;模型架构&lt;/h3&gt;
&lt;h4&gt;Image Encoder&lt;/h4&gt;
&lt;p&gt;由上图所示，我们可以看到每一个输入的图片都会经过一个共享权重的Vit Encoder生成对应的token序列 $H_i = {h_{ i , j }}_{j = 1}^{HW/P^2}$，即：
$$
H_i=\mathcal{F}(I_i),i\in1,...,N
$$
论文中提到，他们使用了和Dust3R相同的Encoder：CroCo ViT，但是他们提到了DINOv2的表现与之相似。&lt;/p&gt;
&lt;p&gt;另外，在把token传入fusion transformer之前，作者为每一个token添加了一个一维的位置编码，目的是让模型知道哪些图像块来自于同一张图片，并且帮助模型认出上文标定的第一张图片。这同样也能让模型隐式地去理解这些图片里反映的相机位姿。&lt;/p&gt;
&lt;h4&gt;Fusion Transformer&lt;/h4&gt;
&lt;p&gt;模型中大多数计算都发生在Fusion Transformer里面，作者使用了一个类似于&lt;strong&gt;ViT-L&lt;/strong&gt;的24层的transformer作为这一模块的主体。它将来自所有的视角的token作为输入，并且通过全连接的自注意力机制进行处理，使的模型能够理解所有视角的信息，远超Dust3R能理解的两个视角的信息。&lt;/p&gt;
&lt;h4&gt;Pointmap Decoding Heads&lt;/h4&gt;
&lt;p&gt;最后，Fast3R使用了两个独立的DPT解码头将Fusion Transformer的输出解码为点图，即图片中右边部分。&lt;/p&gt;
&lt;h4&gt;位置编码&lt;/h4&gt;
&lt;p&gt;论文最后的目标是进行多图片处理，并且实现推理时的可以处理的图片数量远远多于训练时的图片数量，因此我们就要考虑推理时为token嵌入位置编码的手段。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一开始，文章尝试使用相同的球谐函数嵌入编码，文章中又提到：在LLM中，这种方法导致性能不佳。果不其然，在文章的初步实现中，他们同样发现当输入图像数量超过训练时使用图像的数量时，模型的效果并不好。&lt;/li&gt;
&lt;li&gt;因此，文章借鉴了大预言模型中的&lt;strong&gt;位置插值&lt;/strong&gt;方法：在训练时从一个集合${1,...,N&apos;}$中&lt;strong&gt;均匀随机&lt;/strong&gt;抽取$N$个索引，这样模型便被迫去学习处理&lt;strong&gt;更大范围&lt;/strong&gt;的索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于transformer来说，这种策略感觉和masking没什么区别，文章中也说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This strategy enables Fast3R to handle N = 1000 images during inference, even if only trained with N = 20 images.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;有效利用显存&lt;/h2&gt;
&lt;p&gt;从模型架构的图片来看，这看起来就是一个占用很大显存的模型。但是文章提出，由于模型的特点（meta-architecture），这个模型可以广泛使用各种并行化以及分片技术。
文章提出他们在训练和推理的时候利用了两种不同形式的并行化和FlashAttention技术，并认为随着未来的技术成熟他们的模型会持续受益（废话）。&lt;/p&gt;
&lt;h3&gt;具体采用的策略来实现高效训练。&lt;/h3&gt;
&lt;p&gt;首先，使用FlashAttention来提高时间和内存效率。即便如此，当N&gt;16时，一个朴素的实现即使在批量大小为1的情况下也会耗尽内存（128 x A100-80GB啊，离大谱）。
因此，后来使用了DeepSpeed ZeRO stage 2训练，将优化器状态、动量估计和梯度在不同的机器上进行分区。这样就能够以每个数据样本最多N=28个视角进行训练，同时每个GPU的批量大小为1。&lt;/p&gt;
&lt;h2&gt;模型效果：&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./result1.png&quot; alt=&quot;miaomiao&quot;&gt;
就模型所给出的表格而言，确实是达到了Sota水平。&lt;/p&gt;
&lt;p&gt;在推理速度上，由于所做的各种优化，它也得到了显著的提升。&lt;/p&gt;
&lt;p&gt;但是，其实我更好奇的是它跟同期的SLAM3R的性能比较，阅读论文，发现两者并没有过同一个精度指标的比较，通过本人的本地测试，发现对于一个很小的数据集（82张有序图片），两者速度上并没有太多差距，但是重建质量上来说
，SLAM3R的质量远超Fast3R。这很好的符合了SLAM3R对有序图像序列进行针对性重建的特性，而fast3R是对一个随机图像重建的方法。&lt;/p&gt;
&lt;p&gt;所以，当我看到Fast3R的demo里有对视频重建的选项时，我感觉并不适合。因为从直觉上来说，人们从一个没有次序的图像集中理解环境的过程也大致遵循一个先排序再重建的过程，也就是说人们对无次序的图片集中还原3D场景的难度远大于从视频中还原场景的难度。&lt;/p&gt;
&lt;p&gt;论文中也提到了局限性的存在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缺少包含大型场景的数据因而缺少在此类场景下的泛化能力。&lt;/li&gt;
&lt;li&gt;没有更好的位置嵌入，不过论文提出可以参考那些能处理极长上下文序列的大语言模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ok，关于Fast3R我就处理到这里，欸，我觉着或许我以后应该认真去看看训练细节和实验部分，总去看模型结构有种高屋建瓴的感觉，还是应该多看看代码（x&lt;/p&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.DzruMhU_.png"/><enclosure url="/_astro/cover.DzruMhU_.png"/></item><item><title>论文阅读记录：MAst3R</title><link>https://www.hjcheng0602.cn/blog/mst3r/mast3r</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/mst3r/mast3r</guid><description>MAst3R这篇论文的阅读，本人自己对mast3r的理解</description><pubDate>Tue, 02 Sep 2025 10:49:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;经过一周的对&lt;a href=&quot;https://github.com/HJCheng0602/SLAM3R&quot;&gt;SLAM3R&lt;/a&gt;进行online以及可视化demo改造的低效率劳作且工作完成，我终于有时间来补档我这篇早在近两个周之前就读完的论文&lt;a href=&quot;https://github.com/naver/mast3r&quot;&gt;Grounding Image Matching in 3D with MASt3R&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;读完这篇论文之后，我的第一感觉就是：这是一个DUst3R的修补模型，他并没有太多的像DUst3R那样的开创性地将transformer运用于双目三维重建那样的举动，而是在DUst3R模型上进行了
少许修补，并提出了少许修补中的一些独创性方法，感觉是一篇介绍small trick的论文。同时，我们似乎也可以这么说：MAst3R发现本聚焦于三维重建任务的DUst3R在像素匹配问题上同样达到了SOTA
于是，MAst3R将DUst3R稍加改造，得到了一个在像素匹配上表现更强的模型MAst3R.&lt;/p&gt;
&lt;h2&gt;模型介绍&lt;/h2&gt;
&lt;p&gt;MASt3R的模型结构与Dust3R大致相同：
&lt;img src=&quot;./12.png&quot; alt=&quot;mast3r&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Encoder&lt;/h3&gt;
&lt;p&gt;与DUst3R相同，MAst3R的encoder部分同样是由ViT组成的，且与DUst3R相同的是，MAst3R的encoder部分也是共享权重的。
就像这样：
$$
H_1 = Encoder(I^1) \
H_2 = Encoder(I^2)
$$&lt;/p&gt;
&lt;h3&gt;Decoder&lt;/h3&gt;
&lt;p&gt;MASt3R的Decoder同样采用了cross-attention的机制，这能使得MAst3R能够理解同一像素在不同视角下的信息，有助于后续进行像素匹配。
$$
H&apos;^1, H&apos;^2 = Decoder(H^1, H^2)
$$&lt;/p&gt;
&lt;h3&gt;Heads&lt;/h3&gt;
&lt;p&gt;对于Dust3R来说，他只有一个head，直接将decoder的输出转化为点图信息和置信度（上图灰色部分）&lt;/p&gt;
&lt;h4&gt;3D Heads&lt;/h4&gt;
&lt;p&gt;MASt3R对这个head基本上与DUst3R的head相同，都是将decoder的输出转化为点图信息和置信度。&lt;/p&gt;
&lt;h4&gt;Matching Heads&lt;/h4&gt;
&lt;p&gt;MASt3R在此基础上又增加了一个head，专门用于像素匹配任务(上图蓝色部分)，这个头部由一个简单的两层的MLP组成，使用了GELU作为激活函数，另外在处理完后进行归一化处理，负责输出两张密集的特征图：
$$
D^1 = Head_{desc}^1([H&apos;_1, H&apos;&lt;em&gt;2]) \
D^2 = Head&lt;/em&gt;{desc}^2([H&apos;_1, H&apos;_2])
$$&lt;/p&gt;
&lt;h3&gt;Loss&lt;/h3&gt;
&lt;p&gt;Mast3R的损失函数由两部分组成：
$$
\mathcal{L}&lt;em&gt;{total}=\mathcal{L}&lt;/em&gt;{conf}+ \beta\mathcal{L}_{match}
$$&lt;/p&gt;
&lt;h4&gt;3D Loss&lt;/h4&gt;
&lt;p&gt;MAst3R的3D Loss与DUst3R的3D Loss基本相同，都是由点图的L1损失和置信度的交叉熵损失组成。
但是，MAst3R在计算回归损失的时候，原本的DUst3R计算公式是这样的：
$$
\ell_{\mathrm{regr}}(\nu,i)=\left|\frac{1}{z}X_i^{\nu,1}-\frac{1}{\hat{z}}\hat{X}&lt;em&gt;i^{\nu,1}\right|,
$$
MAst3R 认为在它的应用场景中，并不鼓励尺度不变性，而更多的是需要绝对的尺度一致性，因此MAst3R将上式改为了：
$$
\ell&lt;/em&gt;{\mathrm{regr}}(\nu,i)=\frac{\left|X_i^{\nu,1}-\hat{X}&lt;em&gt;i^{\nu,1}\right|}{\hat{z}}
$$
因此，MAst3R的3D Loss计算公式为：
$$
\mathcal{L}&lt;/em&gt;{\mathrm{conf}}=\sum_{\nu\in{1,2}}\sum_{i\in\mathcal{V}^\nu}C_i^\nu\ell_{\mathrm{regr}}(\nu,i)-\alpha\log C_i^\nu.
$$&lt;/p&gt;
&lt;h4&gt;Matching Loss&lt;/h4&gt;
&lt;p&gt;这个损失函数是对Matching Head输出的特征图进行监督的，基本思想是：我们鼓励一个图像中的一个特征匹配符，最多与另一张图像中代表同一个3D点的特征匹配符进行匹配，
需要注意的是，这个匹配本质上是一个交叉熵分类损失，当网络猜到正确的像素（而非邻近的像素）时，才会得到奖励。&lt;/p&gt;
&lt;p&gt;具体实现上，我们利用了InfoNCE loss来实现这个想法，其作用于一组对应关系$\hat{\mathcal{M}} = { (i, j)|\hat{X_i}^{1,1} = \hat{X_j}^{2,1} }$，具体公式如下：
$$
\mathcal{L}&lt;em&gt;{\mathrm{match}}=-\sum&lt;/em&gt;{(i,j)\in\hat{\mathcal{M}}}\log\frac{s_\tau(i,j)}{\sum_{k\in\mathcal{P}^1}s_\tau(k,j)}+\log\frac{s_\tau(i,j)}{\sum_{k\in\mathcal{P}^2}s_\tau(i,k)}
$$
其中，$s_\tau(i,j)=\exp(\frac{D_i^1\cdot D_j^2}{\tau})$，$\tau$是一个温度参数，$\mathcal{P}^1$和$\mathcal{P}^2$分别是图像1和图像2中所有像素的集合。&lt;/p&gt;
&lt;p&gt;这极大地鼓励了网络进行高精度匹配。&lt;/p&gt;
&lt;p&gt;最后，两个损失函数被结合起来，形成了MAst3R的总损失函数：
$$
\mathcal{L}&lt;em&gt;{total}=\mathcal{L}&lt;/em&gt;{conf}+ \beta\mathcal{L}_{match}
$$
有了上述模型与Loss就可以训练了，但是网络的输出还需要经过一些处理，才能得到需要的匹配关系。注意，网络只输出了PointMap和每个像素的LocalFeature，而期望得到的是两个图像之间的像素点级别的匹配，匹配相关的部分就是图中新增的NN模块。&lt;/p&gt;
&lt;h2&gt;快速互惠匹配&lt;/h2&gt;
&lt;p&gt;当给定两张特定的预测图$DD^1,D^2\in\mathbb{R}^{H\times W\times d}$时，我们的目标是提取一组可靠的像素对应关系，即互惠最近邻。&lt;/p&gt;
&lt;p&gt;数学定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互惠最近邻集合由公式定义：
$$
\mathcal{M}={(i,j)|j=\mathrm{NN}_2(D_i^1)\mathrm{~and~}i=\mathrm{NN}_1(D_j^2)}
$$&lt;/li&gt;
&lt;li&gt;这里的$NN_A(D_j^B)$表示在特征图$D^A$中与特征$D_j^B$距离最近的特征的索引。其数学定义为：
$$
\mathrm{NN}_A(D_j^B)=\arg\min_i|D_i^A-D_j^B|
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;传统方法&lt;/h3&gt;
&lt;p&gt;传统上，计算互惠最近邻的方法是通过暴力搜索来实现的，这种方法的时间复杂度为$O((HW)^2)$，这在高分辨率图像中是不可行的。&lt;/p&gt;
&lt;p&gt;虽然优化最近邻搜索是可能的，例如使用 &lt;strong&gt;K-d&lt;/strong&gt; 树，但这种优化在高维特征空间中通常会变得非常低效，在某些情况下，其速度甚至比 MASt3R 输出$D_1$和$D_2$的推理时间慢几个数量级。&lt;/p&gt;
&lt;h3&gt;MASt3R的方法&lt;/h3&gt;
&lt;p&gt;MASt3R 提出了一种基于&lt;strong&gt;子采样&lt;/strong&gt;*的快速方法。&lt;/p&gt;
&lt;p&gt;这个方法是从一个稀疏的第一张图片的像素集合出发的，通过找到这个集合中每个像素在第二张图片上的最近邻得到最近邻集合，然后再从这个最近邻集合中找到每个像素在第一张图片上的最近邻，最后通过检查互惠性来得到最终的互惠最近邻集合。&lt;/p&gt;
&lt;p&gt;整个过程可以表示为：
$$
U^t\mapsto[\mathrm{NN}&lt;em&gt;2(D_u^1)]&lt;/em&gt;{u\in U^t}=V^t\mapsto[\mathrm{NN}&lt;em&gt;1(D_v^2)]&lt;/em&gt;{v\in V^t}=U^{t+1}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 $U_n^t = U_n^{t+1}$ 时，这些像素形成了一个闭环，并被收集为一组&lt;strong&gt;互惠匹配&lt;/strong&gt; $\mathcal{M}_k^t = { (U_n^t, V_n^t) | U_n^t = U_n^{t+1} }$。&lt;/li&gt;
&lt;li&gt;对于下一次迭代，那些已经收敛的像素（即 $U_n^t = U_n^{t+1}$）会被过滤掉，新的 $U^t$ 更新为 $U^{t+1} \setminus U^t$。&lt;/li&gt;
&lt;li&gt;这个过程会迭代固定的次数，直到所有的对应关系都收敛到稳定的（互惠）对为止。&lt;/li&gt;
&lt;li&gt;最终的输出对应关系集合 $\mathcal{M}$ 由所有互惠匹配集合的拼接而成：$\mathcal{M} = \bigcup_t \mathcal{M}_k^t$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种快速匹配算法的总体复杂度大概是$O(kWH)$，相比朴素方法的$O((WH)^2)$，有了显著的提升。
&lt;img src=&quot;./chart.png&quot; alt=&quot;chart&quot;&gt;&lt;/p&gt;
&lt;p&gt;具体证明过程可以参考论文的附录部分。&lt;/p&gt;
&lt;h2&gt;个人总结&lt;/h2&gt;
&lt;p&gt;MAst3R这篇论文的阅读，本人自己对mast3r的理解，以及对transformer在三维重建任务中应用的理解，基本上就到这里了，当然，mast3r的实验部分我并没有过多地去阅读，因为我觉得mast3r的实验部分并没有太多的创新性，基本上都是在验证mast3r在各个任务上都达到了SOTA的水平。
我个人觉得mast3r的创新点主要有以下几点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在DUst3R的基础上，增加了一个匹配头，用于像素匹配任务，这个头部的设计比较简单，但是效果却非常好。&lt;/li&gt;
&lt;li&gt;在3D损失函数中，改变了点图回归损失的计算方式，使其更加适合绝对尺度一致性的任务。&lt;/li&gt;
&lt;li&gt;提出了一个快速的互惠匹配算法，大大提升了匹配的效率。
总的来说，MAst3R是一篇比较实用的论文，通过一些小的改动和创新，使得模型在多个任务上都达到了SOTA的水平，值得学习和借鉴。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;另外，MAst3R的代码也已经开源：&lt;/p&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;</content:encoded><h:img src="/_astro/image.cJ_XNao2.png"/><enclosure url="/_astro/image.cJ_XNao2.png"/></item><item><title>VGGT读后有感</title><link>https://www.hjcheng0602.cn/blog/vggt/vggt</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/vggt/vggt</guid><description>本人读完VGGT:Visual Geometry Grounded Transformer之后的感想喵</description><pubDate>Thu, 14 Aug 2025 20:04:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Spoiler } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;继写完&lt;strong&gt;SLAM3R&lt;/strong&gt;的onlinee处理后，我又将目光投向了今年CVPR的最佳论文：&lt;a href=&quot;https://github.com/facebookresearch/vggt&quot;&gt;VGGT:Visual Geometry Grounded Transformer&lt;/a&gt; 不要问我研究3R为什么不先看vggt😂,问就是我太摆了一开始懒得看了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VGGT&lt;/strong&gt;主要介绍了一个离线的多视图重建，位姿估计和轨迹追踪的强大的模型，与之前类似于&lt;em&gt;SfM&lt;/em&gt;、&lt;strong&gt;DUst3R&lt;/strong&gt;的重建方法相比，它的先进之处在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;摆脱了这些方法所依赖的昂贵的后处理过程（而这通常没有计入到之前模型的性能评估中）&lt;/li&gt;
&lt;li&gt;将多个任务：深度估计、位姿估计、视图重建、轨迹追踪等全部输出，表现甚至超过了之前单一领域的&lt;strong&gt;SOTA&lt;/strong&gt;方法。&lt;/li&gt;
&lt;li&gt;在将多个任务的结果全部输出的过程中，作者发现了引入不同结果之间的内在数学联系限制后会大幅提高模型的性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;项目架构&lt;/h2&gt;
&lt;p&gt;与之前的模块化解决问题不同，&lt;strong&gt;VGGT&lt;/strong&gt;的主要结构是一个大的Transformer，它接受一个图片集作为输入，然后输出场景图片的不同三维属性。&lt;/p&gt;
&lt;p&gt;值得一提的是，它所能解决的多视角三维属性几乎涵盖了三维视觉的方方面面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;相机位姿以及内参&lt;/li&gt;
&lt;li&gt;点图重建&lt;/li&gt;
&lt;li&gt;关键区域追踪&lt;/li&gt;
&lt;li&gt;关于单张图片的深度图&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并且，VGGT通过更加创新的举动，它将输出的多任务成果的内在几何关系作为归纳偏置整合进了模型，并发现了大幅度的性能提升，这个很值得去研究。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;感觉&lt;strong&gt;VGGT&lt;/strong&gt;就是一个巨大的transformer，通过极其暴力的手段解决问题，客观上来说，这确实展示了transformer在三维重建领域的应用，但其实我是有一些疑问的：
像自然语言处理这种工作，它是无法定量化去研究的，所以我们引入了transformer，似乎是用未知对抗不确定性的手段，但是，在这个三维重建这个领域，它真的有那么多不确定性吗？
还是感觉transformer对于三维重建的成果属于是结果能看，但是要达到更高的精度会让人很迷惑。&lt;/p&gt;</content:encoded><h:img src="/_astro/image.CZ_2N9gA.png"/><enclosure url="/_astro/image.CZ_2N9gA.png"/></item><item><title>为SLAM3R补充实时处理函数方法</title><link>https://www.hjcheng0602.cn/blog/slam3r_online-edit/slam3r_online_contribute</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/slam3r_online-edit/slam3r_online_contribute</guid><description>原本的SLAM3R的recon.py的处理顺序是一个offline的逻辑，将其添加了online处理的recon_online.py</description><pubDate>Tue, 12 Aug 2025 15:57:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Spoiler } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;在上个周阅读&lt;strong&gt;SLAM3R&lt;/strong&gt;论文结束后，学长让我去看一下它的&lt;a href=&quot;https://github.com/PKU-VCL-3DV/SLAM3R&quot;&gt;源代码&lt;/a&gt;，读完代码之后，发现虽然论文里讲述的是“可以实时重建”，但是实际上在&lt;code&gt;recon.py&lt;/code&gt;文件中的&lt;code&gt;scene_recon_pipeline&lt;/code&gt;函数中，代码采取了先对所有&lt;code&gt;input_views&lt;/code&gt;进行输入到&lt;code&gt;i2p_model&lt;/code&gt;得到&lt;code&gt;res_feats&lt;/code&gt;，然后再将所有图片的token输入到l2w网络中进行重建的大致逻辑。&lt;/p&gt;
&lt;p&gt;显然，这样的处理方法不是论文里所提出的&lt;strong&gt;online&lt;/strong&gt;处理方法，因此，在过去的一个周里，本人一边练着科三显然今天上午刚挂掉，该死的直线行驶😡，同时抽出了一点点时间完成了&lt;code&gt;recon_online.py&lt;/code&gt;,一个把原本的&lt;code&gt;scene_recon_pipeline&lt;/code&gt;改成&lt;code&gt;online&lt;/code&gt;处理的改动。&lt;/p&gt;
&lt;h2&gt;原函数的处理逻辑&lt;/h2&gt;
&lt;p&gt;阅读原函数的代码，我们可以将其分为以下几段：&lt;/p&gt;
&lt;h3&gt;预处理&amp;#x26;得到所有view的token&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Pre-save the RGB images along with their corresponding masks 
# in preparation for visualization at last.
rgb_imgs = []
for i in range(len(data_views)):
    if data_views[i][&apos;img&apos;].shape[0] == 1:
        data_views[i][&apos;img&apos;] = data_views[i][&apos;img&apos;][0]        
    rgb_imgs.append(transform_img(dict(img=data_views[i][&apos;img&apos;][None]))[...,::-1])
if &apos;valid_mask&apos; not in data_views[0]:
    valid_masks = None
else:
    valid_masks = [view[&apos;valid_mask&apos;] for view in data_views]   

#preprocess data for extracting their img tokens with encoder
for view in data_views:
    view[&apos;img&apos;] = torch.tensor(view[&apos;img&apos;][None])
    view[&apos;true_shape&apos;] = torch.tensor(view[&apos;true_shape&apos;][None])
    for key in [&apos;valid_mask&apos;, &apos;pts3d_cam&apos;, &apos;pts3d&apos;]:
        if key in view:
            del view[key]
    to_device(view, device=args.device)
# pre-extract img tokens by encoder, which can be reused 
# in the following inference by both i2p and l2w models
res_shapes, res_feats, res_poses = get_img_tokens(data_views, i2p_model)    # 300+fps
print(&apos;finish pre-extracting img tokens&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里重点就是最后的&lt;code&gt;res_shapes, res_feats, res_poses = get_img_tokens(data_views, i2p_model)&lt;/code&gt;，采用&lt;code&gt;i2p_model&lt;/code&gt;的&lt;code&gt;_encode_multiview&lt;/code&gt;方法批次化地(&lt;em&gt;batchify&lt;/em&gt;)对&lt;code&gt;data_views&lt;/code&gt;进行处理，从而得到所有的view的&lt;code&gt;token&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;对所有view进行推理得到最合适的key_frame_stride&lt;/h3&gt;
&lt;p&gt;这里的核心代码就是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# decide the stride of sampling keyframes, as well as other related parameters
if args.keyframe_stride == -1:
    kf_stride = adapt_keyframe_stride(input_views, i2p_model, 
                                        win_r = 3,
                                        adapt_min=args.keyframe_adapt_min,
                                        adapt_max=args.keyframe_adapt_max,
                                        adapt_stride=args.keyframe_adapt_stride)
else:
    kf_stride = args.keyframe_stride
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;adapt_keyframe_stride&lt;/code&gt;函数是一个典型的&lt;strong&gt;offline&lt;/strong&gt;处理函数，它的功能是在所有的input_view中遍历可能的&lt;code&gt;kf_stride&lt;/code&gt;取值，然后对每一个可能的取值随机取样，然后利用&lt;code&gt;i2p_inference_batch&lt;/code&gt;函数得出置信度作为相似度？然后选取最高的所对应的&lt;code&gt;kf_stride&lt;/code&gt;作为最优的取值。&lt;/p&gt;
&lt;h3&gt;使用初始的几个滑动窗口创建初始的全局scene&amp;#x26;初始化buffer set&lt;/h3&gt;
&lt;p&gt;因为&lt;strong&gt;SLAM3R&lt;/strong&gt;初始化时的&lt;a href=&quot;http://localhost:4321/blog/slam3r/slam3r&quot;&gt;特殊性&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于第一个帧这种特殊情况，我们采用了重复运行多次I2P获取足够多数量的初始帧作为缓冲集&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在原本的offline格式的&lt;code&gt;recon.py&lt;/code&gt;中，这种做法以这种样式呈现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;initial_pcds, initial_confs, init_ref_id = initialize_scene(input_views[:initial_winsize*kf_stride:kf_stride], 
                                                i2p_model, 
                                                winsize=initial_winsize,
                                                return_ref_id=True) # 5*(1,224,224,3)

# start reconstrution of the whole scene
init_num = len(initial_pcds)
per_frame_res = dict(i2p_pcds=[], i2p_confs=[], l2w_pcds=[], l2w_confs=[])
for key in per_frame_res:
    per_frame_res[key] = [None for _ in range(num_views)]

registered_confs_mean = [_ for _ in range(num_views)]

# set up the world coordinates with the initial window
for i in range(init_num):
    per_frame_res[&apos;l2w_confs&apos;][i*kf_stride] = initial_confs[i][0].to(args.device)  # 224,224
    registered_confs_mean[i*kf_stride] = per_frame_res[&apos;l2w_confs&apos;][i*kf_stride].mean().cpu()

# initialize the buffering set with the initial window
assert args.buffer_size &amp;#x3C;= 0 or args.buffer_size &gt;= init_num 
buffering_set_ids = [i*kf_stride for i in range(init_num)]

# set up the world coordinates with frames in the initial window
for i in range(init_num):
    input_views[i*kf_stride][&apos;pts3d_world&apos;] = initial_pcds[i]
    
initial_valid_masks = [conf &gt; conf_thres_i2p for conf in initial_confs] # 1,224,224
normed_pts = normalize_views([view[&apos;pts3d_world&apos;] for view in input_views[:init_num*kf_stride:kf_stride]],
                                            initial_valid_masks)
for i in range(init_num):
    input_views[i*kf_stride][&apos;pts3d_world&apos;] = normed_pts[i]
    # filter out points with low confidence
    input_views[i*kf_stride][&apos;pts3d_world&apos;][~initial_valid_masks[i]] = 0       
    per_frame_res[&apos;l2w_pcds&apos;][i*kf_stride] = normed_pts[i]  # 224,224,3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;initial_pcds, initial_confs, init_ref_id = initialize_scene(input_views[:initial_winsize*kf_stride:kf_stride], 
                                                   i2p_model, 
                                                   winsize=initial_winsize,
                                                   return_ref_id=True) # 5*(1,224,224,3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行是对初始化的几个&lt;code&gt;view_token&lt;/code&gt;进行场景重建，并选出一开始的&lt;code&gt;init_ref_id&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后之后就是把所有初始化的帧放到&lt;code&gt;buffer_set&lt;/code&gt;里，然后进行一些归一化处理。&lt;/p&gt;
&lt;h3&gt;对原始的view再继续进行i2p重建点图&lt;/h3&gt;
&lt;p&gt;这里我们重新遍历所有图像，对应论文里面通过&lt;code&gt;I2P&lt;/code&gt;的&lt;code&gt;decoder&lt;/code&gt;重建所有&lt;code&gt;view&lt;/code&gt;的点图。此外，注意&lt;code&gt;initial window&lt;/code&gt;的关键帧图片基本上已经在上面的初始化中被创建出了点图，因此我们选择略过他们，只对没有被创建点图的帧进行&lt;code&gt;I2P&lt;/code&gt;处理
以得到点图，然后就采用论文中的输入窗口多个帧，重建每个帧的点云作为&lt;code&gt;L2W model&lt;/code&gt;的输入。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for view_id in tqdm(range(num_views), desc=&quot;I2P resonstruction&quot;):
    # skip the views in the initial window
    if view_id in buffering_set_ids:
        # trick to mark the keyframe in the initial window
        if view_id // kf_stride == init_ref_id:
            per_frame_res[&apos;i2p_pcds&apos;][view_id] = per_frame_res[&apos;l2w_pcds&apos;][view_id].cpu()
        else:
            per_frame_res[&apos;i2p_pcds&apos;][view_id] = torch.zeros_like(per_frame_res[&apos;l2w_pcds&apos;][view_id], device=&quot;cpu&quot;)
        per_frame_res[&apos;i2p_confs&apos;][view_id] = per_frame_res[&apos;l2w_confs&apos;][view_id].cpu()
        continue
    # construct the local window 
    sel_ids = [view_id]
    for i in range(1,win_r+1):
        if view_id-i*adj_distance &gt;= 0:
            sel_ids.append(view_id-i*adj_distance)
        if view_id+i*adj_distance &amp;#x3C; num_views:
            sel_ids.append(view_id+i*adj_distance)
    local_views = [input_views[id] for id in sel_ids]
    ref_id = 0 
    # recover points in the local window, and save the keyframe points and confs
    output = i2p_inference_batch([local_views], i2p_model, ref_id=ref_id, 
                                tocpu=False, unsqueeze=False)[&apos;preds&apos;]
    #save results of the i2p model
    per_frame_res[&apos;i2p_pcds&apos;][view_id] = output[ref_id][&apos;pts3d&apos;].cpu() # 1,224,224,3
    per_frame_res[&apos;i2p_confs&apos;][view_id] = output[ref_id][&apos;conf&apos;][0].cpu() # 224,224

    # construct the input for L2W model        
    input_views[view_id][&apos;pts3d_cam&apos;] = output[ref_id][&apos;pts3d&apos;] # 1,224,224,3
    valid_mask = output[ref_id][&apos;conf&apos;] &gt; conf_thres_i2p # 1,224,224
    input_views[view_id][&apos;pts3d_cam&apos;] = normalize_views([input_views[view_id][&apos;pts3d_cam&apos;]],
                                                [valid_mask])[0]
    input_views[view_id][&apos;pts3d_cam&apos;][~valid_mask] = 0 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;对初始窗口非关键帧进行注册&lt;/h3&gt;
&lt;p&gt;显然我们在之前的初始化场景中只注册了关键帧，因此我们现在开始对非关键帧进行注册：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Special treatment: register the frames within the range of initial window with L2W model
# TODO: batchify
if kf_stride &gt; 1:
    max_conf_mean = -1
    for view_id in tqdm(range((init_num-1)*kf_stride), desc=&quot;pre-registering&quot;):  
        if view_id % kf_stride == 0:
            continue
        # construct the input for L2W model
        l2w_input_views = [input_views[view_id]] + [input_views[id] for id in buffering_set_ids]
        # (for defination of ref_ids, see the doc of l2w_model)
        output = l2w_inference(l2w_input_views, l2w_model, 
                                ref_ids=list(range(1,len(l2w_input_views))), 
                                device=args.device,
                                normalize=args.norm_input)
        
        # process the output of L2W model
        input_views[view_id][&apos;pts3d_world&apos;] = output[0][&apos;pts3d_in_other_view&apos;] # 1,224,224,3
        conf_map = output[0][&apos;conf&apos;] # 1,224,224
        per_frame_res[&apos;l2w_confs&apos;][view_id] = conf_map[0] # 224,224
        registered_confs_mean[view_id] = conf_map.mean().cpu()
        per_frame_res[&apos;l2w_pcds&apos;][view_id] = input_views[view_id][&apos;pts3d_world&apos;]
        
        if registered_confs_mean[view_id] &gt; max_conf_mean:
            max_conf_mean = registered_confs_mean[view_id]
    print(f&apos;finish aligning {(init_num-1)*kf_stride} head frames, with a max mean confidence of {max_conf_mean:.2f}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里正如注释所说，是一个&lt;strong&gt;Special treatment&lt;/strong&gt;。也是一个特殊情况处理。&lt;/p&gt;
&lt;h4&gt;缩放confs&lt;/h4&gt;
&lt;p&gt;我们发现，我们只用&lt;code&gt;l2w&lt;/code&gt;网络对非关键帧进行了置信度预测，关键帧的置信度是由之前的&lt;code&gt;i2p&lt;/code&gt;网络进行预测的，作者在这里为了控制计算成本，选择直接将后者乘上一个常数因子进行缩放，大致反映出了场景的置信度分数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# A problem is that the registered_confs_mean of the initial window is generated by I2P model,
# while the registered_confs_mean of the frames within the initial window is generated by L2W model,
# so there exists a gap. Here we try to align it.
max_initial_conf_mean = -1
for i in range(init_num):
    if registered_confs_mean[i*kf_stride] &gt; max_initial_conf_mean:
        max_initial_conf_mean = registered_confs_mean[i*kf_stride]
factor = max_conf_mean/max_initial_conf_mean
# print(f&apos;align register confidence with a factor {factor}&apos;)
for i in range(init_num):
    per_frame_res[&apos;l2w_confs&apos;][i*kf_stride] *= factor
    registered_confs_mean[i*kf_stride] = per_frame_res[&apos;l2w_confs&apos;][i*kf_stride].mean().cpu()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;对剩下的views进行注册&lt;/h3&gt;
&lt;p&gt;OK，经过了以上的对于初始帧的特殊处理，我们终于踏入了正途：在过程中对每个帧进行实时处理&lt;/p&gt;
&lt;h4&gt;从buffer set里选择最相近的sel_num个帧：&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# select sccene frames in the buffering set to work as a global reference
cand_ref_ids = buffering_set_ids
ref_views, sel_pool_ids = scene_frame_retrieve(
    [input_views[i] for i in cand_ref_ids], 
    input_views[ni:ni+num_register:2], 
    i2p_model, sel_num=num_scene_frame, 
    # cand_recon_confs=[per_frame_res[&apos;l2w_confs&apos;][i] for i in cand_ref_ids],
    depth=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里正如论文中所述，采用了&lt;code&gt;i2p_model&lt;/code&gt;的前2个&lt;strong&gt;decoder&lt;/strong&gt;进行相似评分。&lt;/p&gt;
&lt;h4&gt;将选取的最相近的几个帧作为参考合并当前帧进行l2w重建&lt;/h4&gt;
&lt;p&gt;显而易见，言以概之：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# register the source frames in the local coordinates to the world coordinates with L2W model
l2w_input_views = ref_views + input_views[ni:max_id+1]
input_view_num = len(ref_views) + max_id - ni + 1
assert input_view_num == len(l2w_input_views)

output = l2w_inference(l2w_input_views, l2w_model, 
                        ref_ids=list(range(len(ref_views))), 
                        device=args.device,
                        normalize=args.norm_input)

# process the output of L2W model
src_ids_local = [id+len(ref_views) for id in range(max_id-ni+1)]  # the ids of src views in the local window
src_ids_global = [id for id in range(ni, max_id+1)]    #the ids of src views in the whole dataset
succ_num = 0
for id in range(len(src_ids_global)):
    output_id = src_ids_local[id] # the id of the output in the output list
    view_id = src_ids_global[id]    # the id of the view in all views
    conf_map = output[output_id][&apos;conf&apos;] # 1,224,224
    input_views[view_id][&apos;pts3d_world&apos;] = output[output_id][&apos;pts3d_in_other_view&apos;] # 1,224,224,3
    per_frame_res[&apos;l2w_confs&apos;][view_id] = conf_map[0]
    registered_confs_mean[view_id] = conf_map[0].mean().cpu()
    per_frame_res[&apos;l2w_pcds&apos;][view_id] = input_views[view_id][&apos;pts3d_world&apos;]
    succ_num += 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h4&gt;通过一些手段更新buffer set&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;buffer_set&lt;/code&gt;的选取方法差不多就和论文里面讲的一样，基本上就是随机选取了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# update the buffering set
if next_register_id - milestone &gt;= update_buffer_intv:  
    while(next_register_id - milestone &gt;= kf_stride):
        candi_frame_id += 1
        full_flag = max_buffer_size &gt; 0 and len(buffering_set_ids) &gt;= max_buffer_size
        insert_flag = (not full_flag) or ((strategy == &apos;fifo&apos;) or 
                                            (strategy == &apos;reservoir&apos; and np.random.rand() &amp;#x3C; max_buffer_size/candi_frame_id))
        if not insert_flag: 
            milestone += kf_stride
            continue
        # Use offest to ensure the selected view is not too close to the last selected view
        # If the last selected view is 0, 
        # the next selected view should be at least kf_stride*3//4 frames away
        start_ids_offset = max(0, buffering_set_ids[-1]+kf_stride*3//4 - milestone)
            
        # get the mean confidence of the candidate views
        mean_cand_recon_confs = torch.stack([registered_confs_mean[i]
                                    for i in range(milestone+start_ids_offset, milestone+kf_stride)])
        mean_cand_local_confs = torch.stack([local_confs_mean[i]
                                    for i in range(milestone+start_ids_offset, milestone+kf_stride)])
        # normalize the confidence to [0,1], to avoid overconfidence
        mean_cand_recon_confs = (mean_cand_recon_confs - 1)/mean_cand_recon_confs # transform to sigmoid
        mean_cand_local_confs = (mean_cand_local_confs - 1)/mean_cand_local_confs
        # the final confidence is the product of the two kinds of confidences
        mean_cand_confs = mean_cand_recon_confs*mean_cand_local_confs
        
        most_conf_id = mean_cand_confs.argmax().item()
        most_conf_id += start_ids_offset
        id_to_buffer = milestone + most_conf_id
        buffering_set_ids.append(id_to_buffer)
        # print(f&quot;add ref view {id_to_buffer}&quot;)                
        # since we have inserted a new frame, overflow must happen when full_flag is True
        if full_flag:
            if strategy == &apos;reservoir&apos;:
                buffering_set_ids.pop(np.random.randint(max_buffer_size))
            elif strategy == &apos;fifo&apos;:
                buffering_set_ids.pop(0)
        # print(next_register_id, buffering_set_ids)
        milestone += kf_stride
# transfer the data to cpu if it is not in the buffering set, to save gpu memory
for i in range(next_register_id):
    to_device(input_views[i], device=args.device if i in buffering_set_ids else &apos;cpu&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;保存环节&lt;/h3&gt;
&lt;p&gt;当我们处理完所有帧后，我们会保存我们的所有帧的点云，把这些所有帧的点云合到一起进行重建，得出最后的场景点云。&lt;/p&gt;
&lt;h3&gt;review&lt;/h3&gt;
&lt;p&gt;显而易见，原&lt;code&gt;recon.py&lt;/code&gt;中的这个&lt;code&gt;pipeline&lt;/code&gt;是一个完全的&lt;strong&gt;offline&lt;/strong&gt;处理方法，因此，我编写了一个真正的（？&lt;strong&gt;online&lt;/strong&gt;版本的方法，处理逻辑如下所示：&lt;/p&gt;
&lt;h2&gt;online 函数的处理逻辑&lt;/h2&gt;
&lt;p&gt;既然是要online，我们显然第一件要做的事情就是写下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for i in range(len(data_views)):
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后我们在进行一系列处理：&lt;/p&gt;
&lt;h3&gt;预处理 &amp;#x26; 得到当前view的token&lt;/h3&gt;
&lt;p&gt;显然，通过对原先&lt;strong&gt;offline&lt;/strong&gt;版本的函数分析，这个过程没有初始化的困扰，因此，我们可以大胆对所有遍历到的view都进行这一步：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Pre-save the RGB images along with their corresponding masks
# in preparation for visualization at last.

if data_views[i][&apos;img&apos;].shape[0] == 1:
    data_views[i][&apos;img&apos;] = data_views[i][&apos;img&apos;][0]
rgb_imgs.append(transform_img(dict(img=data_views[i][&apos;img&apos;][None]))[...,::-1])

if is_have_mask_rgb:
    valid_masks.append(data_views[i][&apos;valid_mask&apos;])

# process now image for extracting its img token with encoder
data_views[i][&apos;img&apos;] = torch.tensor(data_views[i][&apos;img&apos;][None])
data_views[i][&apos;true_shape&apos;] = torch.tensor(data_views[i][&apos;true_shape&apos;][None])
for key in [&apos;valid_mask&apos;, &apos;pts3d_cam&apos;, &apos;pts3d&apos;]:
    if key in data_views[i]:
        del data_views[key]
to_device(data_views[i], device=args.device)

# pre-extract img tokens by encoder, which can be reused 
# in the following inference by both i2p and l2w models
temp_shape, temp_feat, temp_pose = get_single_img_tokens([data_views[i]], i2p_model, True)
res_shapes.append(temp_shape[0])
res_feats.append(temp_feat[0])
res_poses.append(temp_pose[0])
print(f&quot;finish pre-extracting img token of view {i}&quot;)

input_views.append(dict(label=data_views[i][&apos;label&apos;],
                        img_tokens=temp_feat[0],
                        true_shape=data_views[i][&apos;true_shape&apos;],
                        img_pos=temp_pose[0]))
for key in per_frame_res:
    per_frame_res[key].append(None)
registered_confs_mean.append(i)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我使用了一个&lt;code&gt;get_single_img_tokens&lt;/code&gt;函数，与之前的&lt;code&gt;get_img_tokens&lt;/code&gt;函数相比，该函数除了不能batch化(online的限制)之外，效果输出别无二致。&lt;/p&gt;
&lt;h3&gt;积累帧以用于场景初始化&lt;/h3&gt;
&lt;p&gt;需要注意的是，当帧序数小于初始化所需要的帧数时，我们后续的程序均无法进行，因此在我的代码中，我选择直接跳过，先蓄势待发🤣&lt;/p&gt;
&lt;p&gt;一旦积累到初始化场景所需帧后，函数会采用一系列操作初始化场景以及初始化buffer set，对初始化后的各帧点云进行归一化处理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# accumulate the initial window frames
if i &amp;#x3C; (initial_winsize - 1)*kf_stride and i % kf_stride == 0:
    continue
elif i == (initial_winsize - 1)*kf_stride:
    initial_pcds, initial_confs, init_ref_id = initialize_scene(input_views[:initial_winsize*kf_stride:kf_stride],
                                                                i2p_model,
                                                                winsize=initial_winsize,
                                                                return_ref_id=True)
    # set up the world coordinates with the initial window
    init_num = len(initial_pcds)
    for j in range(init_num):
        per_frame_res[&apos;l2w_confs&apos;][j * kf_stride] = initial_confs[j][0].to(args.device)
        registered_confs_mean[j * kf_stride] = per_frame_res[&apos;l2w_confs&apos;][j * kf_stride].mean().cpu()
    # initialize the buffering set with the initial window
    assert args.buffer_size &amp;#x3C;= 0 or args.buffer_size &gt;= init_num 
    buffering_set_ids = [j*kf_stride for j in range(init_num)]
    # set ip the woeld coordinates with frames in the initial window
    for j in range(init_num):
        input_views[j*kf_stride][&apos;pts3d_world&apos;] = initial_pcds[j]
    initial_valid_masks = [conf &gt; conf_thres_i2p for conf in initial_confs]
    normed_pts = normalize_views([view[&apos;pts3d_world&apos;] for view in input_views[:init_num*kf_stride:kf_stride]],
                                                initial_valid_masks)
    for j in range(init_num):
        input_views[j*kf_stride][&apos;pts3d_world&apos;] = normed_pts[j]
        # filter out points with low confidence
        input_views[j*kf_stride][&apos;pts3d_world&apos;][~initial_valid_masks[j]] = 0
        per_frame_res[&apos;l2w_pcds&apos;][j*kf_stride] = normed_pts[j]

elif i &amp;#x3C; (initial_winsize - 1) * kf_stride:
    continue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，这里一旦积累到足够多的初始帧，我们就不会进行continue处理了，然后直接进行下一部分。&lt;/p&gt;
&lt;h3&gt;对之前积累的view进行i2p重建点图（包含正在处理的帧） &amp;#x26; 注册初始窗口非关键帧&lt;/h3&gt;
&lt;p&gt;这里我们采用类似于之前&lt;strong&gt;offline&lt;/strong&gt;的顺序，只不过把外在的表现形式作出了改变，实际上内在的顺序逻辑基本不变：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# first recover the accumulate views
if i == (initial_winsize - 1) * kf_stride:
    for view_id in range(i + 1):
        # skip the views in the initial window
        if view_id in buffering_set_ids:
            # trick to mark the keyframe in the initial window
            if view_id // kf_stride == init_ref_id:
                per_frame_res[&apos;i2p_pcds&apos;][view_id] = per_frame_res[&apos;l2w_pcds&apos;][view_id].cpu()
            else:
                per_frame_res[&apos;i2p_pcds&apos;][view_id] = torch.zeros_like(per_frame_res[&apos;l2w_pcds&apos;][view_id], device=&quot;cpu&quot;)
            per_frame_res[&apos;i2p_confs&apos;][view_id] = per_frame_res[&apos;l2w_confs&apos;][view_id].cpu()
            print(f&quot;finish revocer pcd of frame {view_id} in their local coordinates(in buffer set), with a mean confidence of {per_frame_res[&apos;i2p_confs&apos;][view_id].mean():.2f} up to now.&quot;)
            continue
        # construct the local window with the initial views
        sel_ids = [view_id]
        for j in range(1, win_r + 1):
            if view_id - j * adj_distance &gt;= 0:
                sel_ids.append(view_id - j * adj_distance)
            if view_id + j * adj_distance &amp;#x3C; i:
                sel_ids.append(view_id + j * adj_distance)
        local_views = [input_views[id] for id in sel_ids]
        ref_id = 0

        # recover poionts in the initial window, and save the keyframe points and confs
        output = i2p_inference_batch([local_views], i2p_model, ref_id=ref_id,
                                        tocpu=False, unsqueeze=False)[&apos;preds&apos;]
        # save results of the i2p model for the initial window
        per_frame_res[&apos;i2p_pcds&apos;][view_id] = output[ref_id][&apos;pts3d&apos;].cpu()
        per_frame_res[&apos;i2p_confs&apos;][view_id] = output[ref_id][&apos;conf&apos;][0].cpu()

        # construct the input for L2W model
        input_views[view_id][&apos;pts3d_cam&apos;] = output[ref_id][&apos;pts3d&apos;]
        valid_mask = output[ref_id][&apos;conf&apos;] &gt; conf_thres_i2p
        input_views[view_id][&apos;pts3d_cam&apos;] = normalize_views([input_views[view_id][&apos;pts3d_cam&apos;]],
                                                                [valid_mask])[0]
        input_views[view_id][&apos;pts3d_cam&apos;][~valid_mask] = 0

        local_confs_mean_up2now = [conf.mean() for conf in per_frame_res[&apos;i2p_confs&apos;] if conf is not None]
        print(f&quot;finish revocer pcd of frame {view_id} in their local coordinates, with a mean confidence of {torch.stack(local_confs_mean_up2now).mean():.2f} up to now.&quot;)

    # Special treatment: register the frames within the range of initial window with L2W model
    if kf_stride &gt; 1:
        max_conf_mean = -1
        for view_id in tqdm(range((init_num - 1) * kf_stride), desc=&quot;pre-registering&quot;):
            if view_id % kf_stride == 0:
                continue
            # construct the input for L2W model

            l2w_input_views = [input_views[view_id]] + [input_views[id] for id in buffering_set_ids]
            # (for defination of ref_ids, seee the doc of l2w_model)
            output = l2w_inference(l2w_input_views, l2w_model,
                                    ref_ids=list(range(1,len(l2w_input_views))),
                                    device=args.device,
                                    normalize=args.norm_input)
            # process the output of L2W model
            input_views[view_id][&apos;pts3d_world&apos;] = output[0][&apos;pts3d_in_other_view&apos;] # 1,224,224,3
            conf_map = output[0][&apos;conf&apos;] # 1,224,224
            per_frame_res[&apos;l2w_confs&apos;][view_id] = conf_map[0] # 224,224
            registered_confs_mean[view_id] = conf_map.mean().cpu()
            per_frame_res[&apos;l2w_pcds&apos;][view_id] = input_views[view_id][&apos;pts3d_world&apos;]
            
            if registered_confs_mean[view_id] &gt; max_conf_mean:
                max_conf_mean = registered_confs_mean[view_id]
        print(f&apos;finish aligning {(init_num)*kf_stride} head frames, with a max mean confidence of {max_conf_mean:.2f}&apos;)
        # A problem is that the registered_confs_mean of the initial window is generated by I2P model,
        # while the registered_confs_mean of the frames within the initial window is generated by L2W model,
        # so there exists a gap. Here we try to align it.
        max_initial_conf_mean = -1
        for i in range(init_num):
            if registered_confs_mean[i*kf_stride] &gt; max_initial_conf_mean:
                max_initial_conf_mean = registered_confs_mean[i*kf_stride]
        factor = max_conf_mean/max_initial_conf_mean
        # print(f&apos;align register confidence with a factor {factor}&apos;)
        for i in range(init_num):
            per_frame_res[&apos;l2w_confs&apos;][i*kf_stride] *= factor
            registered_confs_mean[i*kf_stride] = per_frame_res[&apos;l2w_confs&apos;][i*kf_stride].mean().cpu()
    # register the rest frames with L2W model
    next_register_id = (init_num - 1) * kf_stride + 1
    milestone = init_num * kf_stride + 1
    update_buffer_intv = kf_stride*args.update_buffer_intv   # update the buffering set every update_buffer_intv frames
    max_buffer_size = args.buffer_size
    strategy = args.buffer_strategy
    candi_frame_id = len(buffering_set_ids) # used for the reservoir sampling strategy
    continue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在处理完这么一堆之后我们直接&lt;code&gt;continue&lt;/code&gt;到下一个循环。&lt;/p&gt;
&lt;h3&gt;处理新图片&lt;/h3&gt;
&lt;p&gt;在下一个循环中，我们拿到了新图片，此时我们也在我们的&lt;strong&gt;online&lt;/strong&gt;函数中踏上了正途，可以对每一个帧进行实时处理了。&lt;/p&gt;
&lt;p&gt;这里，我们的处理逻辑与第一种方法类似，不同的一点是我是一帧一帧地去处理。&lt;/p&gt;
&lt;h3&gt;保存环节&lt;/h3&gt;
&lt;p&gt;与上一个方法略微不同，我提供了参数选项选择是否在线保存/逐几帧保存，因此我重写了一个增量式保存的类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class IncrementalReconstructor:
    &quot;&quot;&quot;
    A class used for reconstruting the pts incrementally
    &quot;&quot;&quot;
    def __init__(self):
        self.res_pcds = None
        self.res_rgbs = None
        self.res_confs = None
        self.res_valid_masks = None
        self.is_initialized = False

    def add_frame(self, view: dict, img: np.ndarray, conf: np.ndarray = None, valid_mask: np.ndarray = None):
        &quot;&quot;&quot;
        Incrementally add a new frame of view data.

        Args:
            view (dict): a dictionary for a new view
            img (np.ndarray): rgb_img
            conf (np.ndarray, optional): 
            valid_mask (np.ndarray, optional): 
        &quot;&quot;&quot;
        try:
            new_pcd = to_numpy(view[&apos;pts3d_world&apos;]).reshape(-1, 3)
            new_rgb = to_numpy(img).reshape(-1, 3)
        except KeyError:
            print(f&quot;Warning: &apos;pts3d_world&apos; not found in the new view. Frame skipped.&quot;)
            return
        if not self.is_initialized:
            self.res_pcds = new_pcd
            self.res_rgbs = new_rgb
            if conf is not None:
                self.res_confs = to_numpy(conf).reshape(-1)
            if valid_mask is not None:
                self.res_valid_masks = to_numpy(valid_mask).reshape(-1)
            self.is_initialized = True
        else:
            self.res_pcds = np.concatenate([self.res_pcds, new_pcd], axis=0)
            self.res_rgbs = np.concatenate([self.res_rgbs, new_rgb], axis=0)
            if conf is not None:
                new_conf = to_numpy(conf).reshape(-1)
                self.res_confs = np.concatenate([self.res_confs, new_conf], axis=0)
            if valid_mask is not None:
                new_mask = to_numpy(valid_mask).reshape(-1)
                self.res_valid_masks = np.concatenate([self.res_valid_masks, new_mask], axis=0)

    def save_snapshot(self, snapshot_id: int, save_dir: str, num_points_save: int = 200000, conf_thres_res: float = 3.0):
        &quot;&quot;&quot;
        Just save
        &quot;&quot;&quot;
        if not self.is_initialized:
            print(&quot;Warning: Reconstructor not initialized. Nothing to save.&quot;)
            return
        save_name = f&quot;recon_snapshot_{snapshot_id:05d}.ply&quot;
        pts_count = len(self.res_pcds)
        final_valid_mask = np.ones(pts_count, dtype=bool)

        if self.res_valid_masks is not None:
            final_valid_mask &amp;#x26;= self.res_valid_masks
        
        if self.res_confs is not None:
            conf_masks = self.res_confs &gt; conf_thres_res
            final_valid_mask &amp;#x26;= conf_masks

        valid_ids = np.where(final_valid_mask)[0]
        
        if len(valid_ids) == 0:
            print(f&quot;Warning for snapshot {snapshot_id}: No valid points left after filtering.&quot;)
            return
            
        print(f&apos;Snapshot {snapshot_id}: Ratio of points filtered out: {(1. - len(valid_ids) / pts_count) * 100:.2f}%&apos;)
        n_samples = min(num_points_save, len(valid_ids))
        print(f&quot;Snapshot {snapshot_id}: Resampling {n_samples} points from {len(valid_ids)} valid points.&quot;)
        sampled_idx = np.random.choice(valid_ids, n_samples, replace=False)
        sampled_pts = self.res_pcds[sampled_idx]
        sampled_rgbs = self.res_rgbs[sampled_idx]
        save_path = join(save_dir, save_name)
        print(f&quot;Saving reconstruction snapshot to {save_path}&quot;)
        save_ply(points=sampled_pts, save_path=save_path, colors=sampled_rgbs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在每一个循环最后加以调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;reconstructor.add_frame(
            view=input_views[i],
            img=rgb_imgs[i],
            conf=per_frame_res[&apos;l2w_confs&apos;][i],
            valid_mask=valid_masks
        )
        if args.save_online:
            if (i + 1) % args.save_frequency == 0:
                reconstructor.save_snapshot(
                    snapshot_id=i + 1,
                    save_dir=save_dir,
                    num_points_save=num_points_save,
                    conf_thres_res=conf_thres_l2w
                )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OK，到此为止我就写完了原本的处理逻辑的解释和新写的**onlinee*处理逻辑介绍，其实要说不说，&lt;strong&gt;online&lt;/strong&gt;处理逻辑也并非太过复杂，但是奈何我这几天因为学车耽误了太多时间也没做什么东西（x&lt;/p&gt;
&lt;p&gt;又水了一篇blog😋&lt;/p&gt;
&lt;h2&gt;新的仓库：&lt;/h2&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;</content:encoded><h:img src="/_astro/image.CHGvQ2OJ.png"/><enclosure url="/_astro/image.CHGvQ2OJ.png"/></item><item><title>SLAM3R读后有感</title><link>https://www.hjcheng0602.cn/blog/slam3r/slam3r</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/slam3r/slam3r</guid><description>本人读完SLAM3R后的理解喵</description><pubDate>Sun, 03 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近几天读完了&lt;a href=&quot;https://github.com/PKU-VCL-3DV/SLAM3R&quot;&gt;SLAM3R&lt;/a&gt;的论文，这是2025年CVPR的一 篇&lt;strong&gt;Highlight&lt;/strong&gt;论文，也是我在3R方向的读过的第3篇论文。&lt;/p&gt;
&lt;p&gt;这篇论文主要介绍了一个叫做&lt;strong&gt;SLAM3R&lt;/strong&gt;的根据视频即时重建的系统，感觉是由&lt;strong&gt;DUst3R&lt;/strong&gt;中获得的灵感，不同的是&lt;strong&gt;DUst3R&lt;/strong&gt;是根据两张图片重建出三维点图，并且是离线处理；而&lt;strong&gt;SLAM3R&lt;/strong&gt;是从一个单目视频中实时在线重建，并且相较于之前的一些方法具有极高的效率。&lt;/p&gt;
&lt;h2&gt;SLAM3R的主要模块&lt;/h2&gt;
&lt;p&gt;SLAM3R主要由&lt;strong&gt;I2P&lt;/strong&gt;和&lt;strong&gt;L2W&lt;/strong&gt;两大模块组成，分别负责从视频中的关键帧重建点图(Image to Point)和利用点图增量式地重建全局点图（Local to World）,具体结构如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./overalmodule.png&quot; alt=&quot;nothing&quot;&gt;&lt;/p&gt;
&lt;h3&gt;视频预处理&lt;/h3&gt;
&lt;p&gt;首先，SLAM3R采用了滑动窗口算法将视频拆成多个小片段，把多个小片段输入到I2P中进行处理。&lt;/p&gt;
&lt;h3&gt;I2P网络&lt;/h3&gt;
&lt;p&gt;I2P模块接受预处理产生的视频片段，该视频片段由多个帧${F_i},i = 1, ... N$组成。通常我们从中选取最中间的帧作为关键帧$F_{key}$，剩下的$N - 1$个帧作为补充帧输入到I2P中。&lt;/p&gt;
&lt;p&gt;首先，我们将所有帧通过一个由$m$个ViT encoder组成的$E_{img}$，生成相应的token，然后再进行decoder操作。具体就是将关键帧的token输入到一个特殊处理的decoder:$D_{key}$里（如下图所示），然后剩下的$N - 1$个补充帧共享同一个decoder结构（继承自&lt;strong&gt;DUst3R&lt;/strong&gt;，由$n$个ViT decoder组成），均生成对应的$G_{sup_i}$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./D_key.png&quot; alt=&quot;$D_key$&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后，我们再使用类似于&lt;strong&gt;DUSt3R&lt;/strong&gt;中的方法，将这些帧（尤其是关键帧）做出一个置信度最高的三维重建。从而得到某一个视频片段对应的点图$\hat{X}_{key}$。&lt;/p&gt;
&lt;h3&gt;L2W网络&lt;/h3&gt;
&lt;p&gt;这个模块接受I2P模块产生的$X_{key}$作为输入，因为其是一个在线处理方法，所以我们引入了缓冲集这一关键的组分。&lt;/p&gt;
&lt;p&gt;首先，我们在已经处理完的关键帧点图中采用&lt;code&gt;reservoir strategy&lt;/code&gt;选取$B$个已经注册完的帧作为缓冲集（对于第一个帧这种特殊情况，我们采用了重复运行多次I2P获取足够多数量的初始帧作为缓冲集），然后，每当一个新的帧输入时，我们使用一个检索模块（由I2P中的decoder组成）在缓冲集中将特征的相似度进行匹配，我们然后选取匹配度最高的$K$个关键帧点图，然后将这$K$个关键帧点图 $$ \hat{X}_{i}^{H \times W \times 3},i = 1 , ..., K + 1 $$作为这个模块的输入。&lt;/p&gt;
&lt;p&gt;如前图所示，我们将这$K + 1$个点图输入到我们的L2W模块的encoder $E_{pts}$ 中：
$$
\mathcal{P}&lt;em&gt;i^{(T\times d)}=E&lt;/em&gt;{pts}(\hat{X}_i^{(H\times W\times3)}),i=1,...,K+1.
$$
然后，由于我们实际上不能只通过点图信息来进行建模（如纹理相同的两个不一样的平面或不同的一块地面），因此我们选择将特征与I2P网络中的特征融合：
$$
\mathcal{F}_i^{(T\times d)}=F_i^{(T\times d)}+\mathcal{P}_i^{(T\times d)},i=1,...,K+1.
$$
在这之后，我们便生成了每张点图的位置外观特征序列。&lt;/p&gt;
&lt;p&gt;紧接着，我们会这$K + 1$个点图输入到两个解码器中：&lt;/p&gt;
&lt;h4&gt;Registration Decoder&lt;/h4&gt;
&lt;p&gt;Registration Decoder将所有token作为输入，然后目的是将L2W的关键帧重建转换到场景坐标系下，它与$D_{key}$采用相同的架构。&lt;/p&gt;
&lt;p&gt;解码过程大概是：
$$
\mathcal{G}&lt;em&gt;{sce_i}=D&lt;/em&gt;{sce}(\mathcal{F}&lt;em&gt;{sce_i},\mathcal{F}&lt;/em&gt;{key}),\quad i=1,...,K
$$&lt;/p&gt;
&lt;h4&gt;Scene Decoder&lt;/h4&gt;
&lt;p&gt;Scene Decoder同样将所有token作为输入，但是它的目的是在不改变场景坐标系的情况下，精化坐标几何。他同样采用与$D_{key}$相同的架构，但是他是对每一个在已选中的关键帧点图进行优化：
$$
\mathcal{G}&lt;em&gt;{sce_i}=D&lt;/em&gt;{sce}(\mathcal{F}&lt;em&gt;{sce_i},\mathcal{F}&lt;/em&gt;{key}),\quad i=1,...,K
$$
通过这样的方式将已生成的point map进行优化&lt;/p&gt;
&lt;p&gt;最后，我们采用类似于I2P模块中的方法对我们所有已经重建的关键帧token进行点图重建：
$$
\tilde{X}_i^{(H\times W\times3)},\tilde{C}_i^{(H\times W\times1)}=\mathrm{H}(\mathcal{G}_i^{(T\times d)}),i=1,...,K+1.
$$&lt;/p&gt;
&lt;p&gt;得到一个实时的三维表示。&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;本人目前涉猎不深，但是论文最后与其他系统做比较，其展现的效率确实令我印象深刻，感觉以上的这个系统的两大模块也令非常简洁舒适。等我再去阅读其他的3R文章来进一步理解这个SOTA的含金量吧😋&lt;/p&gt;
&lt;p&gt;github项目地址：&lt;/p&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;
&lt;p&gt;喵喵又是充实的一天🥳，本人可能理解有偏差（bushi&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.uwodCb2H.png"/><enclosure url="/_astro/cover.uwodCb2H.png"/></item><item><title>Celebrate and Introduce My First Page</title><link>https://www.hjcheng0602.cn/blog/celebrate</link><guid isPermaLink="true">https://www.hjcheng0602.cn/blog/celebrate</guid><description>Just celebrate this page as a milestone and introduce the future of the site</description><pubDate>Sat, 02 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Here, I build my first &lt;a href=&quot;https://hjcheng0602.github.io/&quot;&gt;website&lt;/a&gt;(not the first but the first one I&apos;m serious about building/running)😋.&lt;/p&gt;
&lt;p&gt;My website will include:&lt;/p&gt;
&lt;h2&gt;study course expriences&lt;/h2&gt;
&lt;p&gt;This kind of content will record my experiences learning some meaningful courses in PKU.I hope it will help me review my courses.&lt;/p&gt;
&lt;h2&gt;research experiences&lt;/h2&gt;
&lt;p&gt;As a college student, researching and finding will be the main task in the future. Currently I am interested in 3R(3D reconstruction). So maybe I will update huge contents about my reflections for each paper.&lt;/p&gt;
&lt;h2&gt;my own projects&lt;/h2&gt;
&lt;p&gt;Of course, my some great(just in my standard) project will be post on the site. It&apos;s meaningful to me as long as I think it&apos;s great, regardless of how others see it.&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;p&gt;Above might be the main topics of content in the site.&lt;/p&gt;
&lt;h3&gt;Additions&lt;/h3&gt;
&lt;p&gt;The posts will be in Chinese and English randomly(maybe most time Chinese🤣).Please forgive my poor English.&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>