Performance: Power Diagrams
The cost of the diagram alone, for fixed weights ( the inner loop of every solver ), and of its exact dual.
One power diagram
The cost of computing every cell's measure for fixed weights, the inner loop of every solver, per dirac. On the CPU, the same clouds go through CGAL ( Regular_triangulation_2/3, bulk insertion ), which then rebuilds and clips every cell so that both sides give the same result, and through geogram ( Lévy's exploragram: Delaunay triangulation of the lifted seeds, then the cells restricted to a mesh of the box ), whose cell measures match sdot's to 1e-10.
sdot is timed twice. From scratch, like the others: the tree, the diagram and its first measures. Then PREPARED: a Newton solve, a Lloyd step or a gradient flow at fixed positions computes many diagrams on the same diracs, and each new one reuses the tree and the neighbours found by the previous one. CGAL and geogram have nothing to reuse: each new weight vector costs their whole chain again. On the cards ( kernel time, double ), a diagram is a prepared one.
Cycles per dirac: the time times the clock, the median of the clocks measured on the machine at that thread count ( the cores' on the CPU, the SMs' on a card ): one run's sample is too noisy. Log-log; hover a point for its time and clock.
On the 128 cores and a uniform cloud of 1e6 diracs, a 2D diagram costs 167 ns per dirac from scratch and 29 ns prepared; a 3D one 332 ns and 110 ns. From scratch, the tree is two thirds of the 2D time on 128 cores: it gains 5× up to 32 threads, then slows down once the threads span both sockets. An H100 brings a prepared diagram down to 6 ns in 2D and 90 ns in 3D, and one from scratch to 8 ns and 92 ns: the card builds its tree in one call, for almost nothing. A 2080 Ti takes 57 ns and 590 ns prepared, 61 ns and 600 ns from scratch: its double units are few ( 13 ns and 270 ns in float ). In float, the cell measures match the double kernel to a median relative error of about 1e-14 in 2D: the mixed mode then finishes the solve in double.
Scaling with the threads
A diagram is cut cell by cell, with no shared state. In 3D a prepared diagram gains 86× on 128 cores ( 114× in cycles ). In 2D the cells are cheap and the memory bandwidth stops the gain past 96 cores, at 52× ( 67× in cycles ). On the clustered 2D clouds the cells are uneven and the gain is lower ( 14× to 25× ). CGAL's 2D triangulation is sequential. Its 3D one runs in parallel ( TBB ) and gains 9× up to 64 threads, then slows down once the threads span both sockets: at 128 threads it is twice as slow as on one thread. On the clustered 3D clouds it stops gaining past 16 threads and then does the same. geogram builds the cells in parallel, but its triangulation is sequential in 2D and gains about 3× in 3D, and it is most of the cost.
Scaling with the number of diracs
The CPU on all its cores, and each card, from 1e3 to 1e7 uniform diracs, in time per dirac: a flat curve is a cost linear in n.
Time per dirac, the measure of every cell: flat is linear in the number of diracs. Log-log; hover a point for its time.
On a card, a diagram of less than about 1e5 diracs does not fill the SMs, and below 1e4 the launches set its cost. Up there, an H100 computes a prepared 2D diagram in 5 ns per dirac at 1e7 ( float ), and a 3D one in 72 ns; a 2080 Ti in 16 ns and 310 ns. From scratch, the tree adds 1 to 4 ns per dirac.
Exact connectivity
sdot is not built for this: its cells are cut in floating point, and two cells may disagree about a vertex where several seeds are almost on a sphere. dual() gives the regular triangulation restricted to the box, checked against qhull's on random clouds. Where seeds are tied ( a grid, rings, seeds on a circle ) it gives one of the valid triangulations of the tie.
In 2D, the connectivity is REPAIRED. Each cell is cut in floating point and only gives its list of neighbours. An edge is kept when both of its seeds list it. When two kept edges cross ( the two diagonals of a square of a grid ), a single power test removes one. The faces left are triangulated by one seed each. A triangle that its three cells propose is kept as it is; a larger face is cut by the triangulation of its polygon that lies lowest on the lifted paraboloid. Each choice is made once, so no two cells can disagree, and the floats only rank the choices. On random clouds almost every seed takes the direct path, where its neighbours agree all around it. A tie of hidden seeds that leaves a face touching itself sends the call to the certified path below.
In 3D each cell is cut in floating point, then CERTIFIED. A cell passes when no plane came within 1e-11 ( relative ) of one of its vertices during the cut, and no vertex lies that close to a plane that is not its own. Its vertices are then those of the exact cell. Only the doubtful cells are cut again with canonical predicates ( in double-double, with a symbolic perturbation of the ties ). On random clouds, about one cell in ten thousand is cut again. On a grid, every cell is. Double-double is not always enough: on a 2D grid of 10⁶ seeds nudged by 1e-14, a square is cocircular to 4e-35 and two canonical cells take different diagonals ( the repair does not depend on such a precision ).
Cycles per dirac: the time times the clock, the median of the clocks measured on the machine at that thread count ( the cores' on the CPU, the SMs' on a card ): one run's sample is too noisy. Log-log; hover a point for its time and clock.
A triangulation is what CGAL and geogram are made for, and on one thread they stay ahead, in 3D especially: the certification costs about a cell's worth on top of the cell itself. On the 128 cores sdot takes 160 ns per seed in 2D and 440 ns in 3D. On a grid, where every cell is cut canonically, it is ten times slower.
Scaling with the threads
3D gains 45× on 128 cores ( 60× in cycles ), the clustered 3D clouds 28× and 44×. A uniform 2D cloud gains 11× and stops there from 32 threads on: a 2D cell is too little work for its memory traffic. The clustered 2D clouds gain 5× and 9×. CGAL's 2D triangulation is sequential: its time on one thread is drawn flat ( dashed ) over the threads.
The tables
One power diagram
| sdot | CGAL | geogram | |||
|---|---|---|---|---|---|
| n | 1 / 32 / 128 threads | RTX 2080 Ti / H100 | 1 / 32 / 128 threads | 1 / 32 / 128 threads | |
| 2D uniform, Voronoi | 1e6 | 1 414 / 60 / 29 | 57 / 6.0 | 1 495 / 1 155 / 1 188 | 2 340 / 1 421 / 1 276 |
| 2D uniform, Laguerre | 1e6 | 1 615 / 66 / 46 | 61 / 6.6 | 1 087 / 818 / 847 | 2 238 / 1 361 / 1 214 |
| 2D lines, Voronoi | 1e5 | 1 344 / 136 / 94 | 119 / 26 | 1 281 / 1 131 / 1 141 | 1 829 / 1 192 / 1 242 |
| 2D lines, equal areas | 1e5 | 3 897 / 261 / 158 | 187 / 33 | 900 / 730 / 830 | 1 837 / 1 158 / 1 196 |
| 3D uniform, Voronoi | 1e6 | 9 444 / 340 / 110 | 591 / 90 | 11 558 / 1 617 / 26 158 | 15 159 / 4 705 / 5 897 |
| 3D planes, Voronoi | 1e5 | 9 154 / 412 / 283 | 578 / 106 | 11 270 / 7 350 / 127 850 | 12 456 / 5 581 / 6 613 |
| 3D planes, equal volumes | 1e5 | 19 316 / 803 / 447 | 1 492 / 188 | 11 570 / 4 070 / 140 330 | 13 079 / 5 658 / 6 737 |
ns per dirac, the measure of every cell, double ( GPU: kernel time ). CGAL: triangulation ( in parallel in 3D only ), then each cell rebuilt and clipped ( over the threads ). geogram: triangulation of the lifted seeds, then the cells restricted to a mesh of the box. Shaded: the fastest on the CPU, and on the GPU.
On the GPU, each card in its own columns:
| float | double | |
|---|---|---|
| RTX 2080 Ti / H100 | RTX 2080 Ti / H100 | |
| 2D uniform, Voronoi | 13 / 4.4 | 57 / 6.0 |
| 2D uniform, Laguerre | 11 / 4.7 | 61 / 6.6 |
| 2D lines, Voronoi | 37 / 21 | 119 / 26 |
| 2D lines, equal areas | 49 / 28 | 187 / 33 |
| 3D uniform, Voronoi | 270 / 67 | 591 / 90 |
| 3D planes, Voronoi | 278 / 82 | 578 / 106 |
| 3D planes, equal volumes | 437 / 139 | 1 492 / 188 |
ns per dirac, kernel time only.
A sequence of diagrams on the same diracs: the preparation, then each next diagram.
| preparation | next diagram | |
|---|---|---|
| 1 / 32 / 128 threads | 1 / 32 / 128 threads | |
| 2D uniform, 1e6 | 398 / 73 / 126 | 1 712 / 88 / 69 |
| 3D uniform, 1e6 | 455 / 86 / 159 | 10 546 / 407 / 159 |
ns per dirac. 10 diagrams on the same positions, the weights going from 0 to a Laguerre diagram: the preparation ( tree, structures ) once, then each diagram reuses it.
Exact connectivity
| sdot | CGAL | geogram | ||
|---|---|---|---|---|
| 1 / 32 / 128 threads | RTX 2080 Ti / H100 | 1 / 32 / 128 threads | 1 / 32 / 128 threads | |
| 2D uniform, Voronoi | 1 681 / 158 / 160 | 201 / 15 | 1 103 / – / – | 1 593 / 1 360 / 1 235 |
| 2D uniform, Laguerre | 1 871 / 153 / 149 | 228 / 17 | 728 / – / – | 1 520 / 1 300 / 1 171 |
| 2D lines, Voronoi | 2 167 / 500 / 462 | 410 / 58 | 1 011 / – / – | 1 278 / 1 078 / 1 120 |
| 2D lines, equal areas | 4 257 / 604 / 475 | 908 / 67 | 690 / – / – | 1 273 / 1 072 / 1 091 |
| 3D uniform, Voronoi | 19 900 / 917 / 444 | 4 466 / 620 | 4 032 / 817 / 14 513 | 9 129 / 4 366 / 4 500 |
| 3D planes, Voronoi | 18 193 / 943 / 659 | 4 512 / 769 | 3 970 / 3 920 / 100 750 | 6 570 / 5 018 / 4 757 |
| 3D planes, equal volumes | 36 176 / 1 623 / 825 | 12 812 / 1 499 | 4 010 / 2 420 / 170 290 | 6 804 / 5 227 / 4 910 |
ns per dirac, the exact dual: the regular triangulation, with exact predicates and a symbolic perturbation of the ties. sdot: `dual()`, the cells cut with canonical predicates, each simplex written by the cell of its smallest seed ( restricted to the box ); GPU: kernel time, double. CGAL and geogram: their regular triangulation of the whole space ( CGAL in parallel in 3D only; geogram's 2D one is sequential ). Shaded: the fastest on the CPU, and on the GPU.
The machines and the protocol ( cold starts, repetitions, clocks ) are on the overview.