Skip to content

Deterministic vtkSMPTools clustering; input polydata left untouched - #55

Open
JensMunkHansen wants to merge 2 commits into
valette:masterfrom
JensMunkHansen:deterministic-smp-clustering
Open

JensMunkHansen wants to merge 2 commits into
valette:masterfrom
JensMunkHansen:deterministic-smp-clustering

Conversation

@JensMunkHansen

Copy link
Copy Markdown
Contributor

Threaded clustering (vtkThreadedClustering, used by ACVDP / ACVDQP / AnisotropicRemeshingQP) now runs on vtkSMPTools instead of a vtkMultiThreader pool. Each loop runs the regions in greedy rounds (region index order) whose regions share no cluster and no edge, then the edges between regions alone. The schedule no longer depends on measured run times or on the number of threads, and the per-cluster mutexes are gone, so the result is the same for every backend and thread count. The region count is items / max(ItemsPerRegion, 8 x cluster size); ItemsPerRegion (default 512) replaces PoolingRatio (ACVDQP: -ipr replaces -p).

The per-edge calls of the clustering loop are static instead of virtual: the most-derived class is passed down as a template parameter (Derived) through vtkSurfaceClustering / vtkDiscreteRemeshing to vtkThreadedClustering. vtkIsotropicDiscreteRemeshing, vtkQIsotropicDiscreteRemeshing and vtkAnisotropicDiscreteRemeshing pass themselves; the AnisotropicRemeshing tools use a small VerticesRemeshing class for the same purpose.

vtkUniformClustering::ReComputeStatistics and the polynomial fitting in vtkCurvatureMeasure use vtkSMPTools instead of OpenMP / vtkMultiThreader (vtkCurvatureMeasure::SetNumberOfThreads is removed). No OpenMP is left; the examples no longer look for it. The tools select the STDThread backend (VTK builds default to Sequential, and TBB is often not available); -np sets the vtkSMPTools thread count.

vtkSurfaceBase::CreateFromPolyData always copies the polygons. They were shared with the caller's polydata through the shallow copy whenever they were already in 64-bit storage, and CheckNormals() reoriented the caller's cells in place.

Checked (tooth 32k vertices and a lower jaw scan, 3000 vertices, gradation 1.5):

  • ACVDP, ACVDQP, AnisotropicRemeshingQP: byte-identical output over the Sequential, STDThread and TBB backends and 1, 4, 16 threads (upstream ACVDQP gives a different mesh on every run). Jaw ACVDQP 3.7 s at 1 thread, 1.0 s at 16.
  • ACVD, ACVDQ, AnisotropicRemeshing, AnisotropicRemeshingQ: byte-identical to the unmodified tools.
  • A polydata with flipped triangles passed to CreateFromPolyData and remeshed: connectivity changed before, points and connectivity unchanged now.

Note, not addressed here: AnisotropicRemeshing and AnisotropicRemeshingQ segfault on the tooth mesh (1000 vertices, gradation 1.5 or 0) in vtkUniformClustering::ReComputeStatistics, called from MinimizeEnergy. The unmodified upstream tools crash the same way (the jaw scan runs fine with both), so this predates this change.

Threaded clustering (vtkThreadedClustering, used by ACVDP / ACVDQP /
AnisotropicRemeshingQP) now runs on vtkSMPTools instead of a
vtkMultiThreader pool. Each loop runs the regions in greedy rounds (region
index order) whose regions share no cluster and no edge, then the edges
between regions alone. The schedule no longer depends on measured run times
or on the number of threads, and the per-cluster mutexes are gone, so the
result is the same for every backend and thread count. The region count is
items / max(ItemsPerRegion, 8 x cluster size); ItemsPerRegion (default 512)
replaces PoolingRatio (ACVDQP: -ipr replaces -p).

The per-edge calls of the clustering loop are static instead of virtual: the
most-derived class is passed down as a template parameter (Derived) through
vtkSurfaceClustering / vtkDiscreteRemeshing to vtkThreadedClustering.
vtkIsotropicDiscreteRemeshing, vtkQIsotropicDiscreteRemeshing and
vtkAnisotropicDiscreteRemeshing pass themselves; the AnisotropicRemeshing
tools use a small VerticesRemeshing<Metric> class for the same purpose.

vtkUniformClustering::ReComputeStatistics and the polynomial fitting in
vtkCurvatureMeasure use vtkSMPTools instead of OpenMP / vtkMultiThreader
(vtkCurvatureMeasure::SetNumberOfThreads is removed). No OpenMP is left; the
examples no longer look for it. The tools select the STDThread backend
(VTK builds default to Sequential, and TBB is often not available); -np
sets the vtkSMPTools thread count.

vtkSurfaceBase::CreateFromPolyData always copies the polygons. They were
shared with the caller's polydata through the shallow copy whenever they
were already in 64-bit storage, and CheckNormals() reoriented the caller's
cells in place.

Checked (tooth 32k vertices and a lower jaw scan, 3000 vertices, gradation
1.5):
- ACVDP, ACVDQP, AnisotropicRemeshingQP: byte-identical output over the
  Sequential, STDThread and TBB backends and 1, 4, 16 threads (upstream
  ACVDQP gives a different mesh on every run). Jaw ACVDQP 3.7 s at 1 thread,
  1.0 s at 16.
- ACVD, ACVDQ, AnisotropicRemeshing, AnisotropicRemeshingQ: byte-identical
  to the unmodified tools.
- A polydata with flipped triangles passed to CreateFromPolyData and
  remeshed: connectivity changed before, points and connectivity unchanged
  now.

Note, not addressed here: AnisotropicRemeshing and AnisotropicRemeshingQ
segfault on the tooth mesh (1000 vertices, gradation 1.5 or 0) in
vtkUniformClustering<vtkAnisotropicMetricForClustering>::ReComputeStatistics,
called from MinimizeEnergy. The unmodified upstream tools crash the same way
(the jaw scan runs fine with both), so this predates this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JensMunkHansen

Copy link
Copy Markdown
Contributor Author

I kept your I/O stuff inside some classes (I do not have stuff like this). But without it, you tests couldn't execute. It is now deterministic, it is using SMPTools and input are not overwritten.

Found with valgrind --leak-check=full on ACVD, ACVDQ, ACVDP and ACVDQP
(tooth mesh, 3000 vertices, gradation 1.5); all four now report 0 bytes
lost and 0 errors, with byte-identical output.

- vtkQEMetricForClustering never freed Items (allocated in BuildMetric;
  3.3 MB on the tooth). It is now initialised to 0 and deleted in the
  destructor, as vtkIsotropicMetricForClustering does.
- vtkSinglePolynomialMeasure allocated SecondMember[i] with new double and
  freed it with delete [] (1600 mismatched frees per run).
- vtkThreadedClustering::ComputeEdgesLayout never freed its EdgesLayout
  array; it is now a std::vector moved into EdgesProcess.
- vtkSurface::WriteToFile never deleted its writer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@valette

valette commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you for this PR. I will probably pull the memory leak fixes, but I have some questions:

  1. What is this fix for?
  • SecondMember[i]= new double;
  • SecondMember[i]= new double[1];
  1. on my box (14 cores, 64GB RAM), processing a large mesh crashes (OOM):
    bin/ACVDQP ~/data/meshes/xyzrgb_statuette.ply 200000 1.5
    load : /home/valette/data/meshes/xyzrgb_statuette.ply

Mesh with 10000000 polygons, 4999996 points, 15000000 edges
Bounding Box: [-117.973, -190.474, -99.8223]
[117.251, 205.567, 103.339]
The mesh is made only of 10000000 triangles
0 non-manifold edges and 0 boundary edges
The mesh has 1 connected components
Vertex valence entropy: 1.93073
0 disconnected vertices, 5e+06 connected vertices
55.5547 percent of irregular vertices
Mesh geometry quality:
AngleMin=0.000926766
AverageMinAngle=39.256
Qmin=1.40081e-05
Qav=0.731144
Angles bellow 30 (%)=6.62604


Computing Curvature ........
The curvature indicator calculation took :9.11181 seconds.
Indicators Range : 0.000339764 39.5465
Input mesh: 4999996 vertices and 10000000 faces
Creating 9765 Regions for thread pooling...done
Multithreading layout computed in :2.19295 seconds
Processus arrêté bin/ACVDQP ~/data/meshes/xyzrgb_statuette.ply 200000 1.5

  1. About the CreateFromPolyData : indeed the original mesh is indeed modified, but this was for memory efficiency purpose. As the ACVD executables run on their own, it is not a problem for them. But if you use vtkSurface from an other code, maybe a simple solution is to clone the polydata via DeepCOPY?

Could you split the modifications in different PRs? The threading code does not seem mature enough to be pulled right now anyway.

@JensMunkHansen

JensMunkHansen commented Nov 5, 2026 via email

Copy link
Copy Markdown
Contributor Author

@JensMunkHansen

JensMunkHansen commented Nov 5, 2026 via email

Copy link
Copy Markdown
Contributor Author

@JensMunkHansen

JensMunkHansen commented Nov 5, 2026 via email

Copy link
Copy Markdown
Contributor Author

@JensMunkHansen

JensMunkHansen commented Nov 5, 2026 via email

Copy link
Copy Markdown
Contributor Author

@valette

valette commented Nov 5, 2026

Copy link
Copy Markdown
Owner

OK thanks, will have a look!
"Writing I/O inside classes is a nasty mesh, modifying input is a violation
of the VTK pipeline." : yes, you're right it needs fixing

@JensMunkHansen

Copy link
Copy Markdown
Contributor Author

I am using another adaptive remeshing now, but I think the smallest PR is something you should consider merging. It removes invalid memory handling and fixes the memory leaks. The non-determinism was fixed in my first larger PR. I will not look into that again.

But I think the smaller PR fixing leaks is a good small improvement. Leaks in public repos is not nice.

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.

2 participants