Summary
GridEdgeNetwork (src/fvdb/detail/ops/GridEdgeNetwork.cu:100-107) allocates its output as a fixed 8×/12× expansion of the input voxel count, with zero deduplication — a corner shared by 8 voxels is written 8 times, and an interior edge shared by 4 voxels is written 4 times:
| buffer |
size |
B/voxel |
outV |
8V × 3 float32 |
96 |
outE |
12V × 2 int64 |
192 |
outVBidx |
8V int32 |
32 |
outEBidx |
12V int32 |
48 |
|
|
368 |
(The two *Bidx arrays are also written and then discarded for batchSize == 1 — the existing FIXME at :151.)
This was catalogued alongside the coordinate-list topology builders addressed in #711 / #712, but it is a different problem: there is no _createNanoGridFromIJK and no radix sort here. It is purely an output-size issue, so #712 does not (and should not) cover it.
This is not a speed problem. Warmed, the op runs in 0.17 ms for 212k voxels. It is memory only — but 368 B/voxel puts a hard ceiling on the grid size that can be visualized (a 100M-voxel grid would need ~36.8 GB just for the wireframe).
Proposed fix: deduplicate via the dual grid
Both halves of this were verified experimentally, not just derived.
1. The deduplicated vertex set is exactly the dual grid.
dual(c) active ⇔ ∃d∈{0,1}³ primal(c−d) ⇔ c is a corner of some active voxel. Measured on a sparse grid (V = 212,803): unique vertices = 657,710, dual_grid().num_voxels = 657,710, sets element-wise equal.
Vertex positions become dualIjk − 0.5 (index space) or tx.applyInv(dualIjk − 0.5) (world) — numerically identical to today's values, so return_voxel_coordinates semantics are preserved exactly. Only the duplication goes away.
2. Edges need a 4-voxel incidence test.
Note that "both endpoints are dual-active" is necessary but not sufficient — two diagonally-adjacent voxels satisfy it without the segment being a voxel edge. The correct rule:
Edge (c, c + e_a) exists ⇔ any of the 4 primal voxels c − off is active, where off ranges over the offsets in {0,1}³ whose component along axis a is 0.
(Voxel v has corners v + {0,1}³; the segment lies in that corner set iff c − v has a 0 in axis a.)
Validated against ground truth (the geometrically-unique edge set produced by the current implementation) on a sparse irregular grid that exercises diagonal and corner cases: 165,527 edges from both, element-wise identical.
3. Endpoint indices come from ijk_to_index on the dual grid — no sort, no separate dedup pass.
Implementation sketch
One kernel over dual-grid voxels: for each c, apply the 4-voxel test on 3 axes to build a 3-bit mask; prefix-sum the popcounts to size outE exactly; fill. O(D) work, no expansion, no sort. Removes the discarded jidx arrays at the same time.
Expected savings
| grid |
V |
current |
deduplicated |
|
| dense 128³ |
2,097,152 |
771.8 MB (368 B/vox) |
128.0 MB (61 B/vox) |
6.0× |
| dense 64³ |
262,144 |
96.5 MB |
16.3 MB |
5.9× |
| sparse (random points) |
212,803 |
78.3 MB |
31.2 MB |
2.5× |
On dense grids dual D ≈ 1.02×V and unique edges ≈ 3.05×V, so the win is largest exactly where V is big enough to matter. Extrapolated to 100M voxels: ~36.8 GB → ~6.1 GB.
Dependency
This should be stacked on #710, not built on main. It needs the dual grid, which is only cheap once the leaf-mask PadGrid lands — on main, buildPaddedGrid still takes the coordinate-list path, so you would trade one blow-up for another.
Compatibility notes
- This changes the size and ordering of both returned tensors. The documented contract (
fvdb/viz/_utils.py:11) promises only "an (N, 3) tensor of vertices and an (M, 2) tensor of indices" and does not pin N or M, so deduplication is within contract.
- There are currently no tests for this op —
edge_network appears in tests/ only as a commented-out line (tests/unit/test_ray_marching.py:433). Nothing will break, but that is because coverage is absent; tests should be added as part of any change here.
- The only consumer is
fvdb.viz (grid_edge_network / gridbatch_edge_network). Rendering should look the same or better — interior edges are currently overdrawn 1.75–4×.
- If changing the default is undesirable, a
deduplicate: bool = True parameter would preserve the existing behaviour.
Related
Summary
GridEdgeNetwork(src/fvdb/detail/ops/GridEdgeNetwork.cu:100-107) allocates its output as a fixed 8×/12× expansion of the input voxel count, with zero deduplication — a corner shared by 8 voxels is written 8 times, and an interior edge shared by 4 voxels is written 4 times:outV8V × 3float32outE12V × 2int64outVBidx8Vint32outEBidx12Vint32(The two
*Bidxarrays are also written and then discarded forbatchSize == 1— the existing FIXME at:151.)This was catalogued alongside the coordinate-list topology builders addressed in #711 / #712, but it is a different problem: there is no
_createNanoGridFromIJKand no radix sort here. It is purely an output-size issue, so #712 does not (and should not) cover it.This is not a speed problem. Warmed, the op runs in 0.17 ms for 212k voxels. It is memory only — but 368 B/voxel puts a hard ceiling on the grid size that can be visualized (a 100M-voxel grid would need ~36.8 GB just for the wireframe).
Proposed fix: deduplicate via the dual grid
Both halves of this were verified experimentally, not just derived.
1. The deduplicated vertex set is exactly the dual grid.
dual(c)active ⇔ ∃d∈{0,1}³ primal(c−d) ⇔cis a corner of some active voxel. Measured on a sparse grid (V = 212,803): unique vertices = 657,710,dual_grid().num_voxels= 657,710, sets element-wise equal.Vertex positions become
dualIjk − 0.5(index space) ortx.applyInv(dualIjk − 0.5)(world) — numerically identical to today's values, soreturn_voxel_coordinatessemantics are preserved exactly. Only the duplication goes away.2. Edges need a 4-voxel incidence test.
Note that "both endpoints are dual-active" is necessary but not sufficient — two diagonally-adjacent voxels satisfy it without the segment being a voxel edge. The correct rule:
(Voxel
vhas cornersv + {0,1}³; the segment lies in that corner set iffc − vhas a 0 in axisa.)Validated against ground truth (the geometrically-unique edge set produced by the current implementation) on a sparse irregular grid that exercises diagonal and corner cases: 165,527 edges from both, element-wise identical.
3. Endpoint indices come from
ijk_to_indexon the dual grid — no sort, no separate dedup pass.Implementation sketch
One kernel over dual-grid voxels: for each
c, apply the 4-voxel test on 3 axes to build a 3-bit mask; prefix-sum the popcounts to sizeoutEexactly; fill.O(D)work, no expansion, no sort. Removes the discarded jidx arrays at the same time.Expected savings
On dense grids dual
D ≈ 1.02×Vand unique edges≈ 3.05×V, so the win is largest exactly whereVis big enough to matter. Extrapolated to 100M voxels: ~36.8 GB → ~6.1 GB.Dependency
This should be stacked on #710, not built on
main. It needs the dual grid, which is only cheap once the leaf-maskPadGridlands — onmain,buildPaddedGridstill takes the coordinate-list path, so you would trade one blow-up for another.Compatibility notes
fvdb/viz/_utils.py:11) promises only "an(N, 3)tensor of vertices and an(M, 2)tensor of indices" and does not pinNorM, so deduplication is within contract.edge_networkappears intests/only as a commented-out line (tests/unit/test_ray_marching.py:433). Nothing will break, but that is because coverage is absent; tests should be added as part of any change here.fvdb.viz(grid_edge_network/gridbatch_edge_network). Rendering should look the same or better — interior edges are currently overdrawn 1.75–4×.deduplicate: bool = Trueparameter would preserve the existing behaviour.Related
PadGrid/ dual-grid leaf-mask morphology (dependency)MarchingCubes.cu:357— a separate instance of the same theme (torch::unique_dimover a(3·nTri, 3)int64 tensor), also not covered by Replace coordinate-list grid-topology construction with leaf-mask morphology #712