Skip to content

Races in the parallel passes, the not_a_disk map flake, and what preserve_topology keeps - #1158

Merged
udaykusupati merged 9 commits into
mainfrom
uday/races-and-preserve-topology
Nov 5, 2026
Merged

udaykusupati merged 9 commits into
mainfrom
uday/races-and-preserve-topology

Conversation

@udaykusupati

Copy link
Copy Markdown
Collaborator

Two data races in the parallel passes, the intermittent python_tracking_correspondences CI failure, and what preserve_topology does and does not keep.

Commits

  1. Engine 3D: a vertex's tagged edges are counted from its own fan. The link condition asks the order of the link vertices of a collapsing edge; with feature edges tracked, the count read the fans of their neighbours, two steps from the edge and outside the collapse's lock. ThreadSanitizer flagged it on double_sphere at 8 threads. On macOS it threw "Tuple::eid ... empty tet vector": 6 runs in 25 before the fix, 0 in 25 after. It also accounts for about 1% of tetwild runs on the open bunny. Outputs are byte-identical to main at 1 thread. Introduced with the feature-edge tags (Preserve input feature edges and points #1049).

  2. simwild 2D: the last split's vertex is per thread. A write-write race in the parallel split pass (ThreadSanitizer, 6 of 6 dense runs). Its readers are serial; the output is byte-identical at 1 thread.

  3. tracking: a boundary vertex collapsed into one inside keeps the outline. SurfaceMap::flatten split an edge that was already there, and counted the edit as not_a_disk. That is python_tracking_correspondences' intermittent CI failure: about 1 in 160 Linux runs; on the mac-mini, 9 hits in 1400 tetwild runs, every one this collapse. Regression test: a 4-vertex history, with a 4-face control.

  4. tetwild: preserve_topology reports whether the surface handed to the insertion crosses itself. The surface simplification before insertion can fold the surface through itself, and the insertion then changes the topology. The hidden test tetwild-surface-history-octocat failed about 1 run in 3–5 for this reason. This is now warned about and reported, not prevented: crossing pairs, degenerate triangles, and the Euler characteristics right after insertion. The octocat test checks by phase. The check costs 3–32 ms. The specs state the three phases.

  5. simwild 2D: under preserve_topology, the remeshing simplifies after the insertion, as 3D does. Before, 2D did not simplify at all under preserve_topology. Topology is identical on every 2D case and across eps_simplify; the simwild suite is 49/49. One case gains vertices, through a separate, older weakness: stuck-refinement splits a triangle at its energy floor, and it does the same on main at 8 threads.

  6. simwild 2D: the simplification relabels the envelope it hands on. Envelope IO: components write and read what they hold as tags #1156 labels the envelope simwild hands to the next component, and the 3D simplification refreshes those labels when it rebuilds the envelope. The 2D twin did not, so under envelope_reference "simplified" the envelope came out unlabelled. Measured: a 64-edge straight interface simplified to 23 edges with no labels; now 23 edges with 23 label rows.

Tests

ctest 234/234 on the tree merged with main, the new map test, the 4-vertex history through Python (0 warnings; main gives not_a_disk 1), and octocat 3/3 (macOS arm64, Release). ThreadSanitizer: 2D runs of triwild, simwild 2D and the 2D offset show no other race in the collapse path, with a 3D positive control.

🤖 Generated with Claude Code

udaykusupati and others added 7 commits November 5, 2026 13:05
…vertex's own fan

The link condition asks the order of the link vertices of a collapsing edge. With feature
edges tracked, the order walked the vertex's neighbours u and read fan(u) through
tuple_from_edge and eid(): two steps from the edge, outside the collapse's lock, where
another collapse or its rollback rewrites it. ThreadSanitizer flagged the read on
double_sphere at 8 threads; on macOS, where libc++'s move assignment empties the fan first,
6 runs in 25 threw "Tuple::eid ... empty tet vector", 0 in 25 after. The edge's slot is its
lowest tet's tid * 6 + local index, and the fan is sorted ascending, so the first tet of the
vertex's own fan holding u gives the same slot: outputs byte-identical to main at 1 thread
(double_sphere held and protected, cube), ctest unchanged; the collapse pass 11% faster.
Introduced with the feature-edge tags (728b493, #1049).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
split_after_vertex() stored the new vertex in one size_t of the mesh, which every thread of
the parallel split pass wrote: a write-write race, reported by ThreadSanitizer in 6 of 6
dense simwild 2D runs at 8 threads. Its readers, AnnotationsTri.cpp and TightSealGeom.cpp,
split serially and read their own split's vertex right after, so it is per thread now, as
3D's SplitTagCache::v_new: 0 reports in 3 runs, and the output byte-identical at 1 thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SurfaceMap::flatten tried to split the edge (prev, next) for the removed outline vertex
before letting the survivor stand in its place. When the survivor was inside the patch,
every edge the collapse makes holds the survivor, so an edge (prev, next) the patch has after
was there before -- the third side of the face that survives a survivor of three faces -- and
splitting it cut the outline: the edit was counted not_a_disk. That is
python_tracking_correspondences' intermittent CI failure (1 in about 160 Linux runs; on the
mac-mini 9 in 1400 tetwild runs of the open bunny, every hit this collapse). The survivor now
takes the slot first; the split stays for a collapse along the outline. Test: the 4-vertex
history (valence 3) and the valence-4 control, 0 warnings and a round trip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…insertion crosses itself

The surface simplification before insertion keeps the connectivity only, and the envelope
admits a fold of the surface through itself; the insertion then cuts the crossing triangles,
which changes the topology. Not prevented: under preserve_topology one exact check of the
simplified surface counts the crossing pairs and degenerate triangles, warns, and reports
them, with the Euler characteristics right after the insertion. The octocat test checks by
phase: always that the optimization keeps the inserted topology, and the input's only when
nothing crosses. Octocat, 10 runs at 8 threads: 2 with 3 crossing pairs, Euler [2] -> [3]
at the insertion; 8 with 0, all [2]. The check costs 3-32 ms. Specs and docs state the three
phases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he insertion, as 3D does

Without preserve_topology simwild simplifies before the insertion in both dimensions; with
it, 3D simplifies the tet mesh after the insertion (SimWildMesh::simplify), where the exact
inversion check and the substructure link condition keep the topology, and 2D did not
simplify at all -- no reason was recorded; it was never written. SimWildMeshTri::simplify()
is the 2D twin: the curve envelopes at eps_simplify, every edge collapsed with the quality
check on, then the envelopes rebuilt around the result (envelope_reference "simplified") or
restored around the input. 2D differs where its base does: no order-2 retagging (the curve
ends and junctions are held by their anchors alone) and no quality switch. Default 2D
remeshing now simplifies. Topology identical before and after on every 2D case and across
eps_simplify 2e-5..1e-2 and both references; simwild suite 49/49. One case grows
(curves_2d 2432 -> 3554 vertices): a triangle at a 1.5e-3 rad curve wedge sits at its energy
floor, which stuck-refinement keeps splitting -- the same on main at 8 threads, not this
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve-topology

# Conflicts:
#	components/simwild/wmtk/components/simwild/SimWildMeshTri.cpp
…ope it hands on

Since #1156 simwild hands on the envelope it held to, its edges labelled by their inputs
(m_E_envelope_edge_sets), and the 3D simplification refreshes those labels when it rebuilds the
envelope around the simplified surface (m_F_envelope_face_sets). The 2D twin rebuilt the
envelope without them, so under envelope_reference "simplified" the envelope handed on carried
no labels at all: a 64-edge straight interface simplified to 23 edges and came out unlabelled;
now 23 edges, 23 label rows (tag_0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@udaykusupati
udaykusupati force-pushed the uday/races-and-preserve-topology branch from 7a36cff to a3e5f99 Compare November 5, 2026 18:37
udaykusupati and others added 2 commits November 5, 2026 14:36
The hidden case tetwild-surface-folds-compare runs tetwild twice per model, with the fold
check off and on. Since #1118 the input simplification uses the exact envelope by default, and
on crown.obj (171k faces) that simplification alone took 34 min (2054 s, macOS arm64), so the
case exceeded the hidden run's 5400 s limit. data2's tetwild_crown config already sets
simplify_use_sample_envelope for this reason; this test builds its own settings and was not
covered. folds_of_run takes the flag, and only the compare case sets it, so the CI case
tetwild-surface-folds-none-made is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve-topology

# Conflicts:
#	components/simwild/wmtk/components/simwild/SimWildMeshTri.cpp
#	components/tetwild/wmtk/components/tetwild/tetwild.cpp
@udaykusupati
udaykusupati merged commit 1717f32 into main Nov 5, 2026
12 checks passed
@udaykusupati
udaykusupati deleted the uday/races-and-preserve-topology branch November 5, 2026 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant