Skip to content

Commit 75b4742

Browse files
akaszynskiclaude
andauthored
Fix SMP serial-unstable filters: SurfaceNets3D BoundaryLabels + LengthDistribution RNG (#176)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 03d078e commit 75b4742

5 files changed

Lines changed: 65 additions & 28 deletions

File tree

Common/Math/vtkReservoirSampler.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ template <typename Integer, bool Monotonic = true>
4242
class vtkReservoirSampler : public vtkReservoirSamplerBase
4343
{
4444
public:
45+
vtkReservoirSampler() = default;
46+
47+
/// Construct a sampler with a fixed seed for reproducible sampling.
48+
///
49+
/// By default a sampler draws its seed from std::random_device, so results
50+
/// vary run-to-run (and, when the same instance is shared across threads,
51+
/// from thread to thread). Passing an explicit seed makes every draw this
52+
/// instance performs deterministic, which callers that need reproducible
53+
/// output (and byte-stable serial-vs-parallel behavior) should prefer.
54+
explicit vtkReservoirSampler(SeedType seed)
55+
: Seed(seed)
56+
, HasSeed(true)
57+
{
58+
}
59+
4560
/// Choose kk items from a sequence of (0, nn - 1).
4661
///
4762
/// This will throw an exception if kk <= 0.
@@ -98,7 +113,7 @@ class vtkReservoirSampler : public vtkReservoirSamplerBase
98113
return;
99114
}
100115

101-
std::mt19937 generator(vtkReservoirSampler::RandomSeed());
116+
std::mt19937 generator(this->HasSeed ? this->Seed : vtkReservoirSampler::RandomSeed());
102117
std::uniform_real_distribution<> unitUniform(0., 1.);
103118
std::uniform_int_distribution<Integer> randomIndex(0, kk - 1);
104119
double w = exp(log(unitUniform(generator)) / kk);
@@ -136,6 +151,9 @@ class vtkReservoirSampler : public vtkReservoirSamplerBase
136151
std::sort(data.begin(), data.end());
137152
}
138153
}
154+
155+
SeedType Seed = 0;
156+
bool HasSeed = false;
139157
};
140158

141159
VTK_ABI_NAMESPACE_END

Filters/Core/vtkSurfaceNets3D.cxx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "vtkStreamingDemandDrivenPipeline.h"
2121
#include "vtkTriangle.h"
2222

23+
#include <algorithm>
2324
#include <cstdint>
2425
#include <memory>
2526

@@ -1411,7 +1412,16 @@ void SurfaceNets<TArray>::ConfigureOutput(
14111412

14121413
// Scalars, which are of type T and 2-components
14131414
newScalars->SetNumberOfTuples(outputEMD.NumQuads);
1414-
this->NewScalars = vtk::DataArrayValueRange<2>(newScalars).begin();
1415+
// The quad count is a padded upper bound: an isolated boundary case can
1416+
// reserve a scalar tuple that GenerateQuads never writes (the connectivity
1417+
// array zero-fills on ResizeExact, but SetNumberOfTuples leaves the scalar
1418+
// storage uninitialized). Those never-written BoundaryLabels tuples read
1419+
// back as garbage -- run-to-run nondeterministic under threading, and even
1420+
// serially unstable on a dirtied heap. Zero-init so any padding slot is
1421+
// deterministic; every genuinely-emitted quad overwrites its own tuple.
1422+
auto scalarRange = vtk::DataArrayValueRange<2>(newScalars);
1423+
std::fill(scalarRange.begin(), scalarRange.end(), 0);
1424+
this->NewScalars = scalarRange.begin();
14151425

14161426
// Smoothing stencils, which are represented by a vtkCellArray
14171427
stencils->ResizeExact(outputEMD.NumPoints, outputEMD.NumStencilEdges);

Filters/Statistics/vtkLengthDistribution.cxx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ void vtkLengthDistribution::PrintSelf(ostream& os, vtkIndent indent)
2222
this->Superclass::PrintSelf(os, indent);
2323
os << indent << "SampleSize: " << this->SampleSize << "\n";
2424
os << indent << "SortSample: " << (this->SortSample ? "T" : "F") << "\n";
25+
os << indent << "Seed: " << this->Seed << "\n";
2526
}
2627

2728
double vtkLengthDistribution::GetLengthQuantile(double qq)
@@ -81,7 +82,10 @@ int vtkLengthDistribution::RequestData(
8182
vtkNew<vtkIdList> dummyIds;
8283
dataIn->GetCellPoints(0, dummyIds.GetPointer());
8384

84-
vtkReservoirSampler<vtkIdType> sampler;
85+
// Seed the sampler so both the cell subset and the per-cell connectivity
86+
// picks are deterministic -- otherwise vtkReservoirSampler draws from
87+
// std::random_device and each thread (and each run) samples differently.
88+
vtkReservoirSampler<vtkIdType> sampler(this->Seed);
8589
std::vector<vtkIdType> ids = sampler(numSamples, dataIn->GetNumberOfCells());
8690
vtkSMPTools::For(0, static_cast<vtkIdType>(ids.size()),
8791
[&dataIn, &lengths, &sampler, &ids](vtkIdType begin, vtkIdType end)

Filters/Statistics/vtkLengthDistribution.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ class VTKFILTERSSTATISTICS_EXPORT vtkLengthDistribution : public vtkTableAlgorit
5454
vtkSetMacro(SortSample, bool);
5555
vtkBooleanMacro(SortSample, bool);
5656

57+
/// Set/get the seed used to randomly sample cells and connectivity entries.
58+
///
59+
/// The sampling is seeded so the output is reproducible from run to run
60+
/// (and byte-identical whether the sampling loop runs serially or threaded).
61+
/// Change the seed to obtain a different random subset. The default is 0.
62+
vtkGetMacro(Seed, unsigned int);
63+
vtkSetMacro(Seed, unsigned int);
64+
5765
/// Return the length scale at a particular quantile.
5866
///
5967
/// This method must only be invoked after the filter
@@ -77,6 +85,7 @@ class VTKFILTERSSTATISTICS_EXPORT vtkLengthDistribution : public vtkTableAlgorit
7785

7886
vtkIdType SampleSize = 100000;
7987
bool SortSample = true;
88+
unsigned int Seed = 0;
8089

8190
private:
8291
vtkLengthDistribution(const vtkLengthDistribution&) = delete;

cvista-validate/smpParityCases.cxx

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ std::vector<Case> RegisterCases()
341341
};
342342
// Tag the just-registered case as a known, documented nondeterminism exception
343343
// (see Case::knownIssue): reported with its magnitude but not a gate failure.
344-
auto markKnown = [&cases](const std::string& reason) {
344+
[[maybe_unused]] auto markKnown = [&cases](const std::string& reason) {
345345
cases.back().knownIssue = true;
346346
cases.back().knownIssueReason = reason;
347347
};
@@ -523,22 +523,17 @@ std::vector<Case> RegisterCases()
523523
f->SetInputData(in.ugrid);
524524
return vtkSmartPointer<vtkAlgorithm>(f);
525525
});
526-
add(
527-
"vtkLengthDistribution", "Filters/Statistics", Risk::Reduce,
528-
[](const Inputs& in) {
529-
vtkNew<vtkLengthDistribution> f;
530-
f->SetInputData(in.ugrid);
531-
return vtkSmartPointer<vtkAlgorithm>(f);
532-
},
533-
/*orderRelaxed=*/true);
534-
// NOT a threading defect: vtkLengthDistribution draws its samples from a
535-
// vtkReservoirSampler seeded from std::random_device on every call
536-
// (Common/Math/vtkReservoirSampler.cxx), and for each sampled cell it randomly
537-
// picks which 2 vertices to measure. Its output is therefore nondeterministic
538-
// even SINGLE-THREADED -- the serial reference is itself random -- so a
539-
// parallel-vs-serial gate cannot apply. Excluded from the pass/fail count.
540-
markKnown("unseeded RNG (vtkReservoirSampler <- std::random_device); "
541-
"nondeterministic even single-threaded, not a threading defect");
526+
add("vtkLengthDistribution", "Filters/Statistics", Risk::Reduce, [](const Inputs& in) {
527+
vtkNew<vtkLengthDistribution> f;
528+
f->SetInputData(in.ugrid);
529+
return vtkSmartPointer<vtkAlgorithm>(f);
530+
});
531+
// Now GATED byte-exact: vtkLengthDistribution used to seed its
532+
// vtkReservoirSampler from std::random_device on every call, making the
533+
// output nondeterministic even single-threaded. The sampler is now seedable
534+
// and the filter seeds it (default Seed=0), so both the sampled cell subset
535+
// and the per-cell vertex picks are deterministic; each sample writes its own
536+
// table row, so serial and threaded output are byte-identical.
542537
add("vtkSelectEnclosedPoints", "Filters/Modeling", Risk::Reduce, [](const Inputs& in) {
543538
vtkNew<vtkSelectEnclosedPoints> f;
544539
f->SetInputData(in.cloud);
@@ -650,15 +645,16 @@ std::vector<Case> RegisterCases()
650645
f->SetClipFunction(plane(1, 1, 0));
651646
return vtkSmartPointer<vtkAlgorithm>(f);
652647
});
653-
// Expected byte-exact: the GEOMETRY of the default OUTPUT_STYLE_BOUNDARY path is
648+
// GATED byte-exact: the GEOMETRY of the default OUTPUT_STYLE_BOUNDARY path is
654649
// deterministic (integer voxel-center coords, serial prefix-sum offsets,
655-
// per-thread label lookup, Jacobi double-buffered smoothing) -- and indeed the
656-
// validator measures points + connectivity byte-identical. HOWEVER the
657-
// "BoundaryLabels" cell-data comes out as garbage (~9e8, far outside the 0..3
658-
// label range) that changes every run -- an uninitialized/wild read that is
659-
// nondeterministic even SINGLE-THREADED. The serial-vs-serial pre-check catches
660-
// this and reports it as SERIAL-UNSTABLE (a real defect, but orthogonal to
661-
// threading), so it is not charged against the parallel-vs-serial gate.
650+
// per-thread label lookup, Jacobi double-buffered smoothing) -- points +
651+
// connectivity are byte-identical. The "BoundaryLabels" cell-data used to come
652+
// out as garbage (~9e8, far outside the 0..3 label range) that changed every
653+
// run: the quad count is a padded upper bound, so an isolated boundary case
654+
// could reserve a scalar tuple GenerateQuads never wrote, and SetNumberOfTuples
655+
// left that storage uninitialized. The scalar buffer is now zero-initialized at
656+
// allocation, so any padding tuple is deterministic and every emitted quad
657+
// overwrites its own -- BoundaryLabels is now byte-identical serial-vs-parallel.
662658
add("vtkSurfaceNets3D", "Filters/Core", Risk::Iso, [](const Inputs& in) {
663659
vtkNew<vtkSurfaceNets3D> f;
664660
f->SetInputData(in.labelImage);

0 commit comments

Comments
 (0)