Fix index range size and partitioning when the range is wider than the index type - #815
Open
Arthur031221 wants to merge 1 commit into
Open
Arthur031221 wants to merge 1 commit into
Arthur031221 wants to merge 1 commit into
Conversation
…e index type tf::distance computed (end - beg + step - 1) / step in the index type. When the span of the range does not fit in that type, the signed sum overflowed (undefined behaviour) and the unsigned sum wrapped, so for_each_index, for_each_by_index and reduce_by_index silently ran zero iterations, for example for_each_index(0u, UINT_MAX, 1u << 20) or for_each_index(0, INT_MAX, 1 << 20). The signed path also divided by zero for an empty range with a zero step, such as for_each_index(5, 5, 0), and did not compile for int8_t and int16_t. Compute the span in the unsigned type of the same width, which always holds it, and return 0 for an empty or zero-step range. IndexRange::unravel and the N-dimensional box builders had the same problem one step later: the exclusive end of the last partition is one step past the last index and wrapped, which dropped the last partition (4095 of 4096 iterations with the default partitioner on 4 workers). Add tf::index_at, which returns that end as the range's own end when it is not representable. for_each_index also no longer advances its index past the last iteration, which overflowed a signed index near INT_MAX.
This branch has not been deployed
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.
Anyone who calls
for_each_index,for_each_by_indexorreduce_by_indexover a range whose span does not fit in the index type (for examplefor_each_index(0u, UINT_MAX, 1u << 20)to walk a 32-bit space in 1 MiB blocks) got zero iterations and no error, andfor_each_index(5, 5, 0)crashed with SIGFPE.tf::distancecomputed(end - beg + step - 1) / stepin the index type, which went wrong in three ways:Tcan hold. For signed types that is undefined behaviour and in practice a negative count that is clamped to 0; for unsigned types it wraps. Before the fix, on x86-64 with g++ 13:beg == end,step == 0) passesis_index_range_invalidand then divides by zero.std::max(T{0}, ...)does not compile forint8_tandint16_tbecause the division promotes toint.Two more places had the same overflow one step later:
IndexRange::unraveland the N-dimensional box builders inpartitioner.hppcompute the exclusive end of the last partition asbeg + size * step. That is one step past the last index and wraps for the same ranges, so even with a correctdistancethe last partition was dropped. For examplefor_each_by_indexoverIndexRange<unsigned>(0, UINT_MAX, 1u << 20)counted 4095 elements instead of 4096 with the default partitioner on 4 workers, and other chunk sizes lost more.for_each_indexadvanced the index once more after the last iteration (beg += inc,idx += inc), which is signed overflow when the last index is within one step ofINT_MAX. UBSan reported it forfor_each_index(0, INT_MAX, 1 << 20).The fix computes the span in
std::make_unsigned_t<T>, which always holds it, and returns 0 for an empty or zero-step range. A new helpertf::index_atreturns the index at a position, or the range's ownendwhen that position is not representable inT;unraveland the box builders use it. Thefor_each_indexloops no longer step past the last index. Results for ranges that already worked are unchanged.Tests added to
unittests/test_for_each.cpp:Distance.*checks the values above plus small types and empty ranges directly.IndexRange1D.unravel.WideRangeandByIndex.WideRange.*check the last partition of 1D ranges and a 2D range with the guided, dynamic and static partitioners, andreduce_by_index, with 1 and 4 workers.ForEachIndex.WideRangerunsfor_each_indexover theintandunsignedblock ranges and the(5, 5, 0)range with 1 and 4 workers and checks the iteration counts.Run locally (g++ 13, C++20,
-O1, Linux):test_for_each: 302 of 302 test cases pass, also when built with-fsanitize=undefined -fno-sanitize-recover=undefined;test_reduce: 342 of 342 pass;test_iterators: 41 of 41 pass. A comparison against sequential loops of 1D, 2D and 3Dfor_each_by_indexandreduce_by_indexwith all three partitioners also showed no differences.test_for_eachdoes not compile (theint8_tandint16_tcases). With those two lines removed, theDistance.SpanExceeds*,IndexRange1D.unravel.WideRangeandByIndex.WideRange.*cases fail their assertions, andDistance.SmallTypesAndEmptyRangesandForEachIndex.WideRangedie with SIGFPE.No scheduling code changed, so there is no measurable performance impact: a few integer operations per range, and one fewer index increment per chunk.