Repository navigation
Fix a sort that can run off its vector, and a race in MonomialMap - #98
Draft
d-torrance wants to merge 2 commits into
Draft
d-torrance wants to merge 2 commits into
d-torrance wants to merge 2 commits into
Conversation
initializeRowsToReduce sorts its row tasks so that the plain rows come first and the S-pairs follow in order of lead monomial. When a is an S-pair and b is a plain row, the comparator fell through to comparing their leads, so cmp(a, b) and cmp(b, a) could both be true. That is not a strict weak ordering, and std::sort, which parallel_sort uses for short ranges, relies on one: its unguarded insertion step walks off the front of the vector when the comparator never says stop. The new test does exactly that with four tasks, and segfaults in both Debug and Release without this fix. No caller has reached it. F4Reducer fills a builder with S-pairs only or with plain rows only, and the comparator is only wrong between the two kinds; the rows that initializeRowsToReduce adds itself come after the sort. But both add methods are public, and nothing says they cannot be used on the same builder. An S-pair now compares as never less than a plain row. The test is the first to drive F4MatrixBuilder2 directly; everything else reaches it only through a whole Groebner basis computation. It sits in F4MatrixBuilder.cpp to share BuilderMaker, which gains a basis() accessor for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MonomialMap grows by building a bigger FixedSizeMonomialMap from the old one, and the new table takes over the old table's nodes rather than copying them, relinking each node's mNext into its new bucket. Those stores were relaxed, on the grounds that the constructor does not run concurrently. But a Reader made before the growth keeps its reference to the old table, and can be partway along a chain when the chain is relinked. Its consume load of mNext can then read the relaxed store and reach a node inserted after the Reader was made, with nothing ordering that node's construction before the read. By the C++ memory model that is a data race on the node's mNext and monomial. TSan found it, once oneTBB itself was built with -fsanitize=thread, in GB.liu_0_1, GB.weispfennig97_0_5 and GB.gerdt93_0_1: a worker in findProduct or findTwoProducts reading a node that the thread in F4MatrixBuilder::createColumn had just inserted. F4MatrixBuilder2 uses the same map. Repeating the GB tests ten times gave 29 reports; with this change they gave none, and neither did 15 runs of the whole suite. The relinking stores are now release stores, which the reader's consume load pairs with. The bucket stores stay relaxed: the new table is published by the seq_cst store of MonomialMap::mMap, after the constructor is done. A release store costs nothing extra on x86 and only a little elsewhere, and this loop runs only when the table grows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Written by Claude (Claude Opus 5.5, via Claude Code), not by @d-torrance, whose account posted it -- please weigh it accordingly.
Fixes in the code behind the default F4 reducer:
F4MatrixBuilder2, and theMonomialMapthat the F4 matrix builders use for their columns.A task comparator that runs
std::sortoff its vector (5734656)F4MatrixBuilder2::initializeRowsToReducesorts its row tasks so that plain rows come first and S-pairs follow in order of lead monomial. Whenawas an S-pair andba plain row, the comparator compared their leads instead of returningfalse, socmp(a, b)andcmp(b, a)could both be true. That is not a strict weak ordering, andstd::sort(which oneTBB'sparallel_sortuses below 500 elements, and which the non-TBB build always uses) relies on one: its unguarded insertion step walks off the front of the vector.No caller has reached it, because
F4Reducerfills a builder with only S-pairs or only plain rows, and the comparator was wrong only between the two kinds. But bothaddmethods are public.The new test,
F4MatrixBuilder2.SPairsAndPlainRowsTogether, adds four tasks in an order that crashes the old comparator.std::__insertion_sortIt is the first test to drive
F4MatrixBuilder2directly; until now it was only reached through whole Gröbner basis computations. It runs on one thread, because which half of an S-pair becomes the pivot row depends on which row reaches that column first, so with more threads there are several equally correct matrices.A data race when
MonomialMapgrows (4333d4c)When the map grows, the bigger
FixedSizeMonomialMaptakes over the old table's nodes and relinks each node'smNextinto its new bucket. Those stores were relaxed, on the grounds that the constructor does not run concurrently. But aReadermade before the growth keeps its reference to the old table and can be partway along a chain when it is relinked. Its consume load can then follow the relaxed store to a node inserted after theReaderwas made, with nothing ordering that node's construction before the read. That is a data race by the C++ memory model, on the node'smNextand its monomial. It is benign on x86 and very probably in practice elsewhere too, because inserts happen under a mutex.The relinking stores are now release stores, which the reader's consume load pairs with. The bucket stores stay relaxed, since the new table is published afterwards by a seq_cst store. The loop runs only when the table grows.
ThreadSanitizer found it once oneTBB itself was built with
-fsanitize=thread(the packaged oneTBB produces thousands of false reports), inGB.liu_0_1,GB.weispfennig97_0_5andGB.gerdt93_0_1. A worker infindProductorfindTwoProductsread a node thatF4MatrixBuilder::createColumnhad just inserted.GB.*repeated 10 times, beforeGB.*repeated 10 times, afterThere is no test that fails without this fix: only TSan sees the race, and CI does not run it.
Checked
-reducer 26(default F4) output on cyclic5 and hyclic8 is byte-identical to-reducer 25.Why this is a draft
Two fixes are in; three more for the same code are still to come, each as its own commit:
mgb gb cyclic7 -reducer 26 -threadCount 1aborts in a Debug build atF4MatrixBuilder2.cpp:133, and Macaulay2's Debug builds take this path. The assert, and the one at:207, check a guarantee that the comment above them credits to S-pair elimination: that all S-pairs sharing an lcm share a generator. It does not hold. On cyclic7 the lcmbcef4g5gets(276,145) (335,180) (335,216), where 276 and 335 are distinct generators whose own lcmbcef4g4strictly dividesbcef4g5. Release handles the leftover pair correctly as an ordinary two-sided row, so the only cost is a skipped optimisation, and a small one: 3 of 2224 S-pairs on cyclic7, and none on cyclic5, hyclic8 or yang1. The fix will remove the asserts and correct the comment.SparseMatrix::reserveFreeEntries. When a row being built outgrows a block that holds no finished rows, the old block is deleted without freeing the chain of earlier blocks it points to, or its own index and scalar arrays.SparseMatrix.RowsSpanningBlocks(Test rows that outgrow their SparseMatrix block while being built #97) reaches it, so the suite is not ASan-clean.fwritefor an empty row inSparseMatrix::write, which UBSan reports under-storeMatrices.🤖 Generated with Claude Code