(Performance engineering)
How I cut sparse TSDF fusion from 210 seconds to 2
The path from a readable scalar reference to a sparse implementation that stayed byte-identical on the documented RGB-D workload.
August 2026 · 5 min read
Keep the slow version
SpatialForge reconstructs geometry from calibrated RGB-D frames and supplied camera poses. I kept its dense TSDF implementation deliberately simple. It gives me a readable reference for projection, signed-distance updates, weighting, and truncation before faster data structures enter the picture.
That changed the optimization target. A mesh that looked roughly the same was not enough. The fast sparse-fusion path had to reproduce the scalar path's accumulator bytes in the parity tests, not just its total number of updated voxels.
Profile before changing the data structure
Sparse allocation had already reduced the active region, but fusion still walked 512 voxels per block and issued guarded scalar writes. The Python loop remained expensive even after contribution evaluation was vectorized.
The recorded room scan used 20 frames, 351 blocks, and 40 mm voxels. Ledgered scalar fusion took roughly 210 seconds. The next change applied one whole-block observation field at a time, preserving the order of additions while removing repeated Python calls.
- Keep the dense implementation as the correctness oracle after the first speed-up.
- Plan only the sparse blocks that the current observation can affect.
- Batch numerical work only where the defined update behaviour stays intact.
- Compare accumulator bytes after each fusion optimization, not screenshots or aggregate mesh counts.
Sparse blocks reduce the work, but not for free
The sparse path divides the volume into blocks and materializes only those selected for the current frame. Work follows observed space instead of the full bounding volume, which improves runtime and memory use without changing the TSDF update for a voxel that is visited.
Sparsity does not guarantee speed. Block selection can become the next bottleneck, and a tiny array operation may spend more time on allocation and dispatch than arithmetic. The planner still needs measured boundaries, stable indexing, and batches large enough to make vectorization worthwhile.
My first vectorized version changed the bytes
My first vectorized version was fast and numerically close. It also failed the byte comparison. Changing grouping, traversal order, intermediate precision, or rounding can alter low-order bits because floating-point addition is not associative.
For this project, ‘close enough’ was a different contract. I adjusted the optimized path until it followed the reference update behaviour on the measured replay, then turned byte equality into a regression test. Future performance work must preserve that contract or introduce a new one explicitly.
# Run the scalar/vector fusion parity tests from the repository root.
python -m unittest tests.test_tsdf_block_vector_fusion
# The newer planner has its own serialized-plan parity tests.
python -m unittest tests.test_tsdf_block_plan_vectorWhat improved
On that room workload, field-driven fusion took about 2 seconds instead of the recorded 210 seconds, with the same 1,127,112 contributions and 81,292 observed voxels. Separate fixture tests compare accumulator buffers byte for byte. The number belongs to this workload and configuration; it is not the total runtime of every reconstruction command.
These are the timings from the original 20-frame benchmark, not the current end-to-end reconstruction. Later work added streaming fusion, saved volumes, meshing, and larger evaluations. Keeping the original workload here makes the before-and-after comparison meaningful.
The planner now has a vectorized path too
The October update applies the same reference-first approach to selecting blocks. Tests compare the written .sftplan bytes, block membership, and sample counts across scalar and vectorized implementations, including noisy room fixtures and awkward voxel sizes.
Irregular inputs still use the scalar fallback so the first error and its message remain consistent. I am not attaching the older 100× fusion result to this change: planning and fusion are different stages, and the planner needs its own reported timing.
What changed after this benchmark
The October 0.5 update streams one depth frame at a time, persists sparse volumes, and resumes fusion from a frame-boundary checkpoint. Sparse meshing now splits disconnected triangle fans at pinched vertices before running the topology check. The earlier limitation that real-data meshes could not be written no longer applies.
The newer TUM run at 15 mm voxels fuses 395 frames and holds out another 395, with a 7.5 mm median TSDF residual. A separate 10 mm run reports 6.6 mm. These measure agreement with held-out depth under supplied poses, not surveyed accuracy. The ICL-NUIM work adds a known synthetic surface and makes the distinction especially clear: views can agree with each other while sharing the same depth bias.
The lesson from the original optimization still holds: keep a reference, measure a named workload, and check the output after making it faster. The current CI also checks fixed fixture volume and mesh digests across three operating systems and two Python versions.
- Byte identity is verified for the committed fixtures and CI configurations, not every possible platform and library combination.
- Held-out TSDF residuals measure field consistency under supplied poses, not ground-truth geometric accuracy.
- There is still no pose estimation, SLAM, relocalization, or semantic mapping.
- Streaming removes the all-frames depth buffer, but voxel accumulators still have a memory ceiling and glancing-angle surfaces can remain incomplete.
- Performance measurements should be rerun when data, hardware, numerical libraries, or configuration changes.