AgentCanvas / Pages / Developer Guide / Nodesets / Model / VGGT-SLAM 2
2026-07-15

The VGGT-SLAM 2 NodeSet (ModelVggtSlam2NodeSet, workspace/nodesets/model/model_vggt_slam2/) wraps VGGT-SLAM 2.0 (MIT-SPARK, BSD-2-Clause; paper 26.01, RSS 2026) as a dense RGB-only SLAM session. Where pySLAM tracks per-frame with classic features, VGGT-SLAM batches keyframes into submaps (16 new frames + 1 overlap), runs a single VGGT feed-forward pass per submap, checks DINOv2-SALAD retrieval for loop closures, and aligns everything on the SL(4) projective manifold with GTSAM — absorbing the reconstruction ambiguity of unknown intrinsics instead of pretending it is a similarity transform. No depth input, no intrinsics input: VGGT self-calibrates.

Seven nodes: the streaming session (reset / track / finalize), the read-out surface (get_trajectory / get_map), the upstream eval ruler (eval_trajectory), and the optional open-set 3D object query (query_object — Perception Encoder CLIP retrieval + SAM 3 segmentation + PCA world-frame OBBs, upstream's --run_os path).

env: ac-vggt-slam conda (Python 3.11, torch 2.3.1+cu121, pip gtsam-develop with the SL(4) types) — server-mode, not a container · upstream pinned at MIT-SPARK/VGGT-SLAM @ 35327ac, pip-installed verbatim · parallelism="replicated" (the map is per-episode mutable state) · exclusive-GPU profiles (VGGT-1B bf16 ~5 GB weights + submap activations + SALAD).

VGGT-SLAM on TUM fr1/desk, RGB only — no depth, no intrinsics. The vggt_slam2_tum graph streams frames into the live session: the trajectory arrives in submap bursts (nothing, then ~17 keyframes at once, retro-corrected as optimization refines), and the dense map materializes chunk-by-chunk as each submap's VGGT forward pass lands (the in-canvas panels draw a 500k-point subsample). The closing orbit is a full-resolution render of the exported map artifact itself — all ~9.3M points of get_map's .npz handle.
PrimitivePurpose
model_vggt_slam2__reset(Re)start the session: fresh Solver / map / SL(4) graph / SALAD database. VGGT-1B loads once per worker and is reused across episodes. run_os=true here if query_object will be used — CLIP embeddings are computed during tracking and cannot be backfilled.
model_vggt_slam2__trackFeed one RGB frame. Optical-flow keyframe gate → keyframe buffer → at submap_size+1 frames: VGGT forward + loop-closure check + optimize. pose updates in submap bursts (see §2).
model_vggt_slam2__finalizeFlush the trailing partial submap — the streaming equivalent of upstream's last-image trigger. Fire once on iterOut.final_stop; its ok gates the read-out. Idempotent.
model_vggt_slam2__get_trajectoryOptimized keyframe trajectory: TUM-format text (upstream's exact pose writer) + parsed 4×4 poses + keyframe-aligned ground-truth rows captured by track.
model_vggt_slam2__get_mapMerged confidence-filtered world-frame cloud: upstream's .pcd writer verbatim (pcd_path) + the same cloud as an .npz handle (map_handle — what pointCloudViewer loads). Heavy geometry rides disk handles. stream_on_submap=on turns it into a live in-loop streamer: it exports only when a new submap has landed and returns a cheap empty handle otherwise — the demo's live map panel.
model_vggt_slam2__eval_trajectoryATE with the upstream ruler: evo_ape tum <gt> <est> -as (Sim(3) alignment is mandatory — SL(4) poses are up-to-scale). Ground truth from the sequence name or the captured gt rows.
model_vggt_slam2__query_objectOpen-set query over the built map: PE-CLIP text embedding → best keyframe (cosine over stored per-frame embeddings) → SAM 3 masks → world-frame points + PCA OBB per instance. Post-hoc; SAM 3 lazy-loads on first query.

1. Upstream anatomy & the driver mirror

Upstream ships the algorithm as a library (vggt_slam package: Solver, GraphMap, PoseGraph, ImageRetrieval, FrameTracker) but keeps the driver loop outside the packagemain.py owns keyframe gating, submap triggering, and the optimize cadence, and setup.py does not ship it. The port therefore pip-installs the package verbatim (pinned commit) and mirrors the driver inside the nodeset backend (_backend.py), with a main.py:NN citation on every mirrored line:

UPSTREAM · main.py driver loop (per frame) read frame cv2.imread keyframe gate LK flow > min_disp buffer paths == 16+1 ? VGGT → SALAD → optimize run_predictions·add_points·SL(4) last-image flush poses.txt · .pcd next frame (for-loop) PORT · vggt_slam2_tum graph (per iterIn step) env_tum observe.rgb + ts track — gate · PNG buffer · boundary run same 3-call seq, ts-named keyframes finalize tail flush get_trajectory · get_map · eval TUM text · .pcd/.npz · evo -as iterOut → iterIn (loop-back) fires once on final_stop
Upstream driver loop (top) and the port (bottom), column-aligned. The gate, buffer, and boundary processing collapse into track; the last-image flush becomes finalize on the loop's final side.

2. Session semantics: burst poses, timestamp-named keyframes

Pose lag is intrinsic, not a bug. track.pose is null until the first submap completes (~17 keyframes), then updates once per submap boundary — and earlier poses are retro-corrected by later optimization and loop closures. The authoritative trajectory is always get_trajectory fired after finalize. Graph authors wiring track.pose into a live viewer get the burst behaviour by design.

Keyframes hit disk, named by timestamp. Upstream's run_predictions consumes image file paths (its own streaming mode saves keyframes to disk too), so track persists each accepted keyframe as a lossless PNG named <timestamp>.png. The numeric stem is load-bearing: Submap.set_frame_ids parses it into the TUM pose-file timestamp column that evo associates against ground truth. Wire observe.timestamp into track.timestamp for eval-grade output.

One server process per eval worker. The accumulating map is per-episode state on the nodeset instance, so parallelism="replicated"; inside each process a 1-worker executor serializes all session calls (CUDA-context and GTSAM thread affinity). VGGT-1B, SALAD, and (under run_os) PE-CLIP/SAM 3 load once per process and survive episode resets.

3. Fidelity: deviations from upstream

Upstream is pip-installed verbatim at the pinned commit; the deviation set is enumerable and lives in the _backend.py docstring. Verdict: faithful with justified deviations (audited 2026-07-14/15; evidence in §4 — fr1_xyz reproduces the locally-run upstream ATE bit-exactly).

IDBucketDeviationWhy · effect
D1B — substrate-forcedvggt_slam.solver.Viewer swapped for a no-op stub at import timeUpstream's Solver.__init__ unconditionally opens a viser server on port 8080; a headless multi-worker server cannot own a fixed port. No mainline math touches the viewer. Audit surface: grep "self.viewer" solver.py.
D2C — cost-forcedSAM 3 lazy-loads on first query_object (upstream builds it eagerly under --run_os)VRAM prudence only; the query math is identical.
D3B — substrate-forcedKeyframes re-encoded to timestamp-named PNGsA streaming source has no files; mirrors upstream's own save_keyframe path. Pixel-identical; filename drives evo association.
D4B — substrate-forcedSAM 3 inference wrapped in torch.autocast("cuda", bf16)The pinned sam3 on torch 2.3.1 dies with a BFloat16/Float matmul clash when called bare (as upstream's query loop does); autocast is SAM 3's documented usage. Verified standalone; no semantic effect.
C — env-forced (graph level)env_tum replays the rgb/depth/gt associated frame stream; upstream globs the raw rgb/ folderfr1_desk: 613 raw frames → 596 associated (head-heavy drop) → keyframe sets diverge in the first second, then lock step. Measured effect: +0.6 mm ATE on desk, zero on xyz.

4. Verification & TUM parity

Upstream's README publishes no TUM numbers, so the reference was banked by running upstream main.py locally (eval_tum.sh's exact flags, same GPU) — twice, confirming it is run-to-run deterministic (0.024690 both runs on fr1_desk). All ATE numbers are evo_ape tum -as (Sim(3), scale-corrected — mandatory for SL(4) up-to-scale poses).

CheckRunResult
Backend smoke (standalone, 200 fr1_desk frames)in-env script21 keyframes → 2 submaps (incl. tail flush), 22 poses, 3.3 M-point .pcd, ATE 0.0229
Graph smoke (scheduler, 200 frames)20260714_172702episode completed; metrics harvested (ate_rmse 0.023098, poses/submaps/loops)
Parity — fr1_xyz (full 798 frames)20260714_174221ATE 0.019903 vs upstream 0.019903 — bit-exact
Parity — fr1_desk (full sequence)20260714_174221ATE 0.02531 vs upstream 0.02469 (+0.6 mm) — fully attributed to the env-association row in §3
Open-set query ("keyboard", fr1_desk map)20260714_1753512 instances; keyboard-shaped OBBs (~19–35 cm × ~2 cm); overlay + points artifacts written

Two engine-level findings from this port, both fixed on the way in: (a) AutoServerApp used to initialize a second nodeset instance in on_startup while the /call handlers closed over the manifest-time throwaway — any session-type server nodeset served every call from a never-initialized instance (fixed in auto_server_app.py; belt-and-braces, nodes resolve the module singleton first). (b) The eval verdict harvest only reads graphOut sinks named metrics (dict, numeric fields flattened) or numeric scalars — any other name is skipped and the episode is convicted "no verdict".

5. Usage

Build the env once: bash scripts/install/install_ac_vggt_slam.sh (creates ac-vggt-slam; pins five upstream repos; fetches dino_salad.ckpt — upstream's own setup.sh does not, yet the Solver constructor loads it unconditionally). Fetch sequences: bash workspace/nodesets/env/env_tum/download.sh rgbd_dataset_freiburg1_desk.

/experiment:run smoke_vggt_slam2_tum vggt_slam2_tum split=freiburg1 episode_count=1 worker_count=1
/experiment:run perf_vggt_slam2_tum  vggt_slam2_tum split=freiburg1 episode_count=2 worker_count=1
/experiment:run vggt-slam2-tum-os    vggt_slam2_tum_os split=freiburg1 episode_count=1 worker_count=1

Profiles are exclusive-GPU with vram_mb deliberately at 8000 (≤ the admission ceiling — a declared marginal above usable_vram_mb can never admit and parks forever); exclusivity is the real guard. Graphs: workspace/graphs/vln/unverified/vggt_slam2_tum.json (+ _os variant); the same graph doubles as the GUI demo — open it on the canvas and press Play (the env panel defaults to the first sequence on disk), which is exactly what the clip above shows. Override the interpreter with $VGGT_SLAM_PYTHON.

6. What this nodeset is NOT

7. Sources

AgentCanvas docs