From a03d0e1a120e6d6373c3a74839e6a1acd26be9c6 Mon Sep 17 00:00:00 2001 From: Benjamin Lozes Date: Wed, 15 Nov 2023 13:55:47 +0100 Subject: [PATCH 01/11] Adapt switch of the initialisation strategy --- include/graphblas/nonblocking/coordinates.hpp | 74 +++++++++++++++---- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/include/graphblas/nonblocking/coordinates.hpp b/include/graphblas/nonblocking/coordinates.hpp index bcb4cf42a..3ae46580e 100644 --- a/include/graphblas/nonblocking/coordinates.hpp +++ b/include/graphblas/nonblocking/coordinates.hpp @@ -49,6 +49,8 @@ #include #include +// #define _LOCAL_DEBUG + namespace grb { @@ -425,6 +427,58 @@ namespace grb { pref_sum = _buffer + num_tiles * ( tile_size + 2 ); } + bool should_use_bitmask_asyncSubsetInit( + const size_t lower_bound, + const size_t upper_bound + ) const noexcept { + assert( _cap > 0 ); + assert( _n <= _cap ); + assert( lower_bound <= upper_bound ); + return upper_bound - lower_bound < _n; + } + + void _asyncSubsetInit_bitmask( + const size_t lower_bound, + const size_t upper_bound, + config::VectorIndexType *local_stack, + config::VectorIndexType *local_nnzs + ) noexcept { +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + fprintf( stderr, "[trace] - _asyncSubsetInit_bitmask( bounds: [%zu, %zu] ) -> local_nnzs=", + lower_bound, upper_bound ); +#endif + for( size_t i = lower_bound; i < upper_bound; ++i ) { + if( _assigned[ i ] ) { + local_stack[ (*local_nnzs)++ ] = i - lower_bound; + } + } +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + fprintf( stderr, "%u\n", *local_nnzs ); +#endif + } + + void _asyncSubsetInit_search( + const size_t lower_bound, + const size_t upper_bound, + config::VectorIndexType *local_stack, + config::VectorIndexType *local_nnzs + ) noexcept { +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + fprintf( stderr, "[trace] - _asyncSubsetInit_search( bounds: [%zu, %zu] ) -> local_nnzs=", + lower_bound, upper_bound ); +#endif + for( size_t i = 0; i < _n; ++i ) { + const size_t k = _stack[ i ]; + if( lower_bound <= k && k < upper_bound ) { + assert( _assigned[ k ] ); + local_stack[ (*local_nnzs)++ ] = k - lower_bound; + } + } +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + fprintf( stderr, "%u\n", *local_nnzs ); +#endif + } + /** * Initialises a Coordinate instance that refers to a subset of this * coordinates instance. Multiple disjoint subsets may be retrieved @@ -443,9 +497,7 @@ namespace grb { const size_t lower_bound, const size_t upper_bound ) noexcept { - if( _cap == 0 ) { - return; - } + if( _cap == 0 ) { return; } const size_t tile_id = lower_bound / analytic_model.getTileSize(); @@ -453,20 +505,10 @@ namespace grb { config::VectorIndexType *local_stack = local_buffer[ tile_id ] + 1; *local_nnzs = 0; - if( upper_bound - lower_bound < _n ) { - for( size_t i = lower_bound; i < upper_bound; ++i ) { - if( _assigned[ i ] ) { - local_stack[ (*local_nnzs)++ ] = i - lower_bound; - } - } + if( should_use_bitmask_asyncSubsetInit( lower_bound, upper_bound ) ) { + _asyncSubsetInit_bitmask( lower_bound, upper_bound, local_stack, local_nnzs ); } else { - for( size_t i = 0; i < _n; ++i ) { - const size_t k = _stack[ i ]; - if( lower_bound <= k && k < upper_bound ) { - assert( _assigned[ k ] ); - local_stack[ (*local_nnzs)++ ] = k - lower_bound; - } - } + _asyncSubsetInit_search( lower_bound, upper_bound, local_stack, local_nnzs ); } // the number of new nonzeroes is initialized here From 185452512febb8079513cc85714aaac4208964e1 Mon Sep 17 00:00:00 2001 From: Benjamin Lozes Date: Wed, 15 Nov 2023 19:56:46 +0100 Subject: [PATCH 02/11] At this point it (should really) work, but not well. --- include/graphblas/nonblocking/coordinates.hpp | 195 ++++++++++++++++-- src/graphblas/nonblocking/pipeline.cpp | 106 ++++++++-- 2 files changed, 267 insertions(+), 34 deletions(-) diff --git a/include/graphblas/nonblocking/coordinates.hpp b/include/graphblas/nonblocking/coordinates.hpp index 3ae46580e..868f804a8 100644 --- a/include/graphblas/nonblocking/coordinates.hpp +++ b/include/graphblas/nonblocking/coordinates.hpp @@ -35,6 +35,7 @@ #include //size_t #include +#include #include #include @@ -49,7 +50,7 @@ #include #include -// #define _LOCAL_DEBUG +#define _LOCAL_DEBUG namespace grb { @@ -73,6 +74,10 @@ namespace grb { typedef bool ArrayType; + // TODO: Remove me + bool _debug_is_counting_sort_done = false; + + private: bool * __restrict__ _assigned; @@ -92,6 +97,9 @@ namespace grb { config::VectorIndexType * __restrict__ local_new_nnzs; config::VectorIndexType * __restrict__ pref_sum; + std::vector counting_sum; + + // the analytic model used during the execution of a pipeline AnalyticModel analytic_model; @@ -418,7 +426,10 @@ namespace grb { local_buffer.resize( analytic_model.getNumTiles() ); - #pragma omp parallel for schedule(dynamic) num_threads(nthreads) + #pragma omp parallel for default(none) \ + firstprivate(tile_size, num_tiles) \ + shared(local_buffer, _buffer) \ + schedule(dynamic) num_threads(nthreads) for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { local_buffer[ tile_id ] = _buffer + tile_id * ( tile_size + 1 ); } @@ -428,6 +439,8 @@ namespace grb { } bool should_use_bitmask_asyncSubsetInit( + const size_t /* num_tiles */, + const size_t /* tile_id */, const size_t lower_bound, const size_t upper_bound ) const noexcept { @@ -438,45 +451,59 @@ namespace grb { } void _asyncSubsetInit_bitmask( + const size_t /* num_tiles */, + const size_t /* tile_id */, const size_t lower_bound, const size_t upper_bound, config::VectorIndexType *local_stack, config::VectorIndexType *local_nnzs ) noexcept { -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - fprintf( stderr, "[trace] - _asyncSubsetInit_bitmask( bounds: [%zu, %zu] ) -> local_nnzs=", - lower_bound, upper_bound ); -#endif for( size_t i = lower_bound; i < upper_bound; ++i ) { if( _assigned[ i ] ) { local_stack[ (*local_nnzs)++ ] = i - lower_bound; } } -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - fprintf( stderr, "%u\n", *local_nnzs ); -#endif +//#if defined(_DEBUG) || defined(_LOCAL_DEBUG) +// #pragma omp critical +// fprintf( stderr, "[T%02d] - _asyncSubsetInit_bitmask( bounds: [%zu, %zu] ) -> local_nnzs=%u\n", +// omp_get_thread_num(), lower_bound, upper_bound, *local_nnzs ); +//#endif } void _asyncSubsetInit_search( + const size_t /* num_tiles */, + const size_t tile_id, const size_t lower_bound, const size_t upper_bound, config::VectorIndexType *local_stack, config::VectorIndexType *local_nnzs ) noexcept { + const auto lower_bound_idx = counting_sum[ tile_id ]; + const auto upper_bound_idx = counting_sum[ tile_id+1 ]; + if( lower_bound_idx == upper_bound_idx ) { return; } #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - fprintf( stderr, "[trace] - _asyncSubsetInit_search( bounds: [%zu, %zu] ) -> local_nnzs=", - lower_bound, upper_bound ); + #pragma omp critical + fprintf(stderr, "[T%02d] - _asyncSubsetInit_search(): tile_id=%zu, lower_bound_idx=%u, upper_bound_idx=%u\n", + omp_get_thread_num(), tile_id, lower_bound_idx, upper_bound_idx ); #endif - for( size_t i = 0; i < _n; ++i ) { + for( size_t i = lower_bound_idx; i < upper_bound_idx; ++i ) { const size_t k = _stack[ i ]; - if( lower_bound <= k && k < upper_bound ) { - assert( _assigned[ k ] ); - local_stack[ (*local_nnzs)++ ] = k - lower_bound; - } - } #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - fprintf( stderr, "%u\n", *local_nnzs ); + if( not ( lower_bound <= k && k < upper_bound ) ) { + #pragma omp critical + fprintf(stderr, "ERROR [T%02d] - _asyncSubsetInit_search(): i=%zu, k=%zu, lower_bound=%zu, upper_bound=%zu\n", + omp_get_thread_num(), i, k, lower_bound, upper_bound ); + } #endif + assert ( lower_bound <= k && k < upper_bound ); + assert( _assigned[ k ] ); + local_stack[ (*local_nnzs)++ ] = k - lower_bound; + } +//#if defined(_DEBUG) || defined(_LOCAL_DEBUG) +// #pragma omp critical +// fprintf( stderr, "[T%02d] - _asyncSubsetInit_search( bounds: [%zu, %zu] ) -> local_nnzs=%u\n", +// omp_get_thread_num(), lower_bound, upper_bound, *local_nnzs ); +//#endif } /** @@ -494,6 +521,7 @@ namespace grb { * (exclusive). */ void asyncSubsetInit( + const size_t num_tiles, const size_t lower_bound, const size_t upper_bound ) noexcept { @@ -505,16 +533,141 @@ namespace grb { config::VectorIndexType *local_stack = local_buffer[ tile_id ] + 1; *local_nnzs = 0; - if( should_use_bitmask_asyncSubsetInit( lower_bound, upper_bound ) ) { - _asyncSubsetInit_bitmask( lower_bound, upper_bound, local_stack, local_nnzs ); + if( should_use_bitmask_asyncSubsetInit( num_tiles, tile_id, lower_bound, upper_bound ) ) { + _asyncSubsetInit_bitmask( num_tiles, tile_id, lower_bound, upper_bound, local_stack, local_nnzs ); } else { - _asyncSubsetInit_search( lower_bound, upper_bound, local_stack, local_nnzs ); + assert( _debug_is_counting_sort_done ); + _asyncSubsetInit_search( num_tiles, tile_id, lower_bound, upper_bound, local_stack, local_nnzs ); } // the number of new nonzeroes is initialized here local_new_nnzs[ tile_id ] = 0; } + void countingSumComputation_sequential( + const size_t num_tiles, + const std::vector< size_t > &lower_bounds, + const std::vector< size_t > &upper_bounds + ) noexcept { + // TODO: Move me to the initialisation phase, and use _buffer instead of a vector + counting_sum.resize( num_tiles+1 ); + + for( size_t i = 0; i <= num_tiles; ++i ) { + counting_sum[ i ] = 0; + } + + // Counting sum computation + for(size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { + const auto lower_bound = lower_bounds[tile_id]; + const auto upper_bound = upper_bounds[tile_id]; + + for (size_t i = 0; i < _n; ++i) { + const auto k = _stack[i]; + if (not (lower_bound <= k && k < upper_bound)) { + continue; + } + assert(_assigned[k]); + counting_sum[tile_id + 1]++; + } + } + +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + #pragma omp critical + { + fprintf( stderr, "[T%02d] - counting_sum (not-prefixed): {num_tiles=%02zu}[ ", + omp_get_thread_num(), num_tiles ); + for(size_t i = 0; i <= num_tiles; ++i ) + fprintf( stderr, "%04u ", counting_sum[ i ] ); + fprintf( stderr, "]\n" ); + } +#endif + + // Prefix sum computation of the counting sum + for( size_t i = 1; i <= num_tiles; ++i ) { + counting_sum[i] += counting_sum[i - 1]; + } + +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + #pragma omp critical + { + fprintf( stderr, "[T%02d] - counting_sum (prefixed): {num_tiles=%02zu}[ ", + omp_get_thread_num(), num_tiles ); + for(size_t i = 0; i <= num_tiles; ++i ) + fprintf( stderr, "%04u ", counting_sum[ i ] ); + fprintf( stderr, "]\n" ); + } +#endif + if( counting_sum[ num_tiles ] != _n ) { + #pragma omp critical + fprintf( stderr, "[T%02d] - counting_sum[ num_tiles ] = %u, _n = %zu\n", + omp_get_thread_num(), counting_sum[ num_tiles ], _n ); + } + assert( counting_sum[ num_tiles ] == _n ); + } + + void countingSortComputation( + const size_t num_tiles, + const std::vector< size_t > &lower_bounds, + const std::vector< size_t > &upper_bounds + ) noexcept { + countingSumComputation_sequential( num_tiles, lower_bounds, upper_bounds ); + + // TODO: Move me to the initialisation phase, and use _buffer instead of a vector + auto counting_sum_copy = counting_sum; + + for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { + const auto lower_bound = lower_bounds[ tile_id ]; + const auto upper_bound = upper_bounds[ tile_id ]; + + size_t assigned_in_bucket = 0; + #pragma omp critical + fprintf(stderr, + "[T%02d] - i = [%u, %zu[ (tile_id=%zu)\n", + omp_get_thread_num(), counting_sum[tile_id], upper_bound, tile_id); + for (size_t i = counting_sum[tile_id]; i < _n; ++i) { + const auto k = _stack[i]; + if( not (lower_bound <= k && k < upper_bound) ) { + continue; + } + + const auto stack_new_idx = counting_sum_copy[tile_id]++; + assert( stack_new_idx < _n ); + assert( _assigned[ k ] ); + if( stack_new_idx == i ) { + assigned_in_bucket++; + continue; + } + #pragma omp critical + fprintf(stderr, + "[T%02d] - Found element %zu (%u) is in tile %zu\n", + omp_get_thread_num(), i, k, tile_id); +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + #pragma omp critical + { + fprintf( stderr, "[T%02d] - swap( %zu, %u ) {_n=%04zu}\n", + omp_get_thread_num(), i, stack_new_idx, _n ); + fprintf( stderr, "[T%02d] - counting_sum (____________): {num_tiles=%02zu}[ ", + omp_get_thread_num(), num_tiles ); + for(size_t _i = 0; _i <= num_tiles; ++_i ) + fprintf( stderr, "%04u ", counting_sum_copy[ _i ] ); + fprintf( stderr, "]\n" ); + } +#endif + assert(stack_new_idx < _n); + std::swap(_stack[i], _stack[stack_new_idx]); + assigned_in_bucket++; + } + // Print assigned_in_bucket + #pragma omp critical + fprintf(stderr, + "[T%02d] - assigned_in_bucket = %zu, should be %u\n", + omp_get_thread_num(), assigned_in_bucket, counting_sum[tile_id+1] - counting_sum[tile_id]); + assert( assigned_in_bucket == (counting_sum[tile_id+1] - counting_sum[tile_id]) ); + } + + _debug_is_counting_sort_done = true; + } + /** * Retrieves a subset coordinate instance that was previously initialised * using a call to #asyncSubsetInit. diff --git a/src/graphblas/nonblocking/pipeline.cpp b/src/graphblas/nonblocking/pipeline.cpp index 73e5d7643..6128602f1 100644 --- a/src/graphblas/nonblocking/pipeline.cpp +++ b/src/graphblas/nonblocking/pipeline.cpp @@ -30,6 +30,8 @@ #include #include +// #define _LOCAL_DEBUG + using namespace grb::internal; @@ -829,6 +831,10 @@ grb::RC Pipeline::execution() { lower_bound.resize( num_tiles ); upper_bound.resize( num_tiles ); + for( std::set< internal::Coordinates< nonblocking > * >::iterator vt = vbegin(); vt != vend(); ++vt ) { + auto* coords = *vt; + coords->_debug_is_counting_sort_done = false; + } // if all vectors are already dense and there is no out-of-place operation to // make them sparse we avoid paying the overhead for updating the coordinates @@ -849,15 +855,52 @@ grb::RC Pipeline::execution() { } #endif + { // Initialise the lower and upper bounds + #pragma omp parallel for schedule(dynamic) num_threads(nthreads) + for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { + + config::OMP::localRange( + lower_bound[tile_id], upper_bound[tile_id], + 0, containers_size, tile_size, tile_id, num_tiles + ); + assert(lower_bound[tile_id] <= upper_bound[tile_id]); + } + } + + { +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + fprintf( stderr, "Pipeline::execution(2): check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); +#endif + size_t current_coord_idx = 0; + for( auto vt = vbegin(); vt != vend(); ++vt ) { + auto *coords = *vt; + for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { + bool will_require_counting_sum = not coords->should_use_bitmask_asyncSubsetInit( + num_tiles, tile_id, lower_bound[tile_id], upper_bound[tile_id] + ); + if (will_require_counting_sum) { +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + fprintf(stderr, " -- coord %zu: will require counting sum+sort for %zu values\n", + current_coord_idx, upper_bound[tile_id] - lower_bound[tile_id]); +#endif + coords->countingSortComputation(num_tiles, lower_bound, upper_bound); + break; + } + } + ++current_coord_idx; + //coords->countingSortComputation(num_tiles, lower_bound, upper_bound); + } + } + #pragma omp parallel for schedule(dynamic) num_threads(nthreads) for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - // compute the lower and upper bounds - config::OMP::localRange( - lower_bound[ tile_id ], upper_bound[ tile_id ], - 0, containers_size, tile_size, tile_id, num_tiles - ); - assert( lower_bound[ tile_id ] <= upper_bound[ tile_id ] ); +// // compute the lower and upper bounds +// config::OMP::localRange( +// lower_bound[ tile_id ], upper_bound[ tile_id ], +// 0, containers_size, tile_size, tile_id, num_tiles +// ); +// assert( lower_bound[ tile_id ] <= upper_bound[ tile_id ] ); #ifndef GRB_ALREADY_DENSE_OPTIMIZATION for( @@ -868,7 +911,7 @@ grb::RC Pipeline::execution() { continue; } - (**vt).asyncSubsetInit( lower_bound[ tile_id ], upper_bound[ tile_id ] ); + (**vt).asyncSubsetInit( num_tiles, lower_bound[ tile_id ], upper_bound[ tile_id ] ); } #endif @@ -907,14 +950,51 @@ grb::RC Pipeline::execution() { (**vt).localCoordinatesInit( am ); } + { // Initialise the lower and upper bounds + #pragma omp parallel for schedule(dynamic) num_threads(nthreads) + for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { + + config::OMP::localRange( + lower_bound[tile_id], upper_bound[tile_id], + 0, containers_size, tile_size, tile_id, num_tiles + ); + assert(lower_bound[tile_id] <= upper_bound[tile_id]); + } + } + + { +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + fprintf( stderr, "Pipeline::execution: check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); +#endif + size_t current_coord_idx = 0; + for( auto vt = vbegin(); vt != vend(); ++vt ) { + auto *coords = *vt; + bool will_require_counting_sum = false; + for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { + will_require_counting_sum |= not coords->should_use_bitmask_asyncSubsetInit( + num_tiles, tile_id, lower_bound[ tile_id ], upper_bound[ tile_id ] + ); + if( will_require_counting_sum ) { +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + fprintf( stderr, " -- coord %zu: will require counting sum+sort for %zu values\n", current_coord_idx, upper_bound[ tile_id ]- lower_bound[ tile_id ] ); +#endif + coords->countingSortComputation( num_tiles, lower_bound, upper_bound ); + break; + } + } + ++current_coord_idx; + //coords->countingSortComputation( num_tiles, lower_bound, upper_bound ); + } + } + #pragma omp parallel for schedule(dynamic) num_threads(nthreads) for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - config::OMP::localRange( - lower_bound[ tile_id ], upper_bound[ tile_id ], - 0, containers_size, tile_size, tile_id, num_tiles - ); - assert( lower_bound[ tile_id ] <= upper_bound[ tile_id ] ); +// config::OMP::localRange( +// lower_bound[ tile_id ], upper_bound[ tile_id ], +// 0, containers_size, tile_size, tile_id, num_tiles +// ); +// assert( lower_bound[ tile_id ] <= upper_bound[ tile_id ] ); for( std::set< internal::Coordinates< nonblocking > * >::iterator vt = vbegin(); @@ -937,7 +1017,7 @@ grb::RC Pipeline::execution() { } #endif - (**vt).asyncSubsetInit( lower_bound[ tile_id ], upper_bound[ tile_id ] ); + (**vt).asyncSubsetInit( num_tiles, lower_bound[ tile_id ], upper_bound[ tile_id ] ); initialized_coordinates = true; } } From 6292639c695ea20e1e4a0dab4f1233a4fa1e238f Mon Sep 17 00:00:00 2001 From: Benjamin Lozes Date: Thu, 16 Nov 2023 13:49:09 +0100 Subject: [PATCH 03/11] At this point it works, slightly well. - Using a better counting-sum, should be upgraded to a binary-search for the tile selection - More and more assertions --- include/graphblas/nonblocking/coordinates.hpp | 79 +++++++++++++++---- src/graphblas/nonblocking/pipeline.cpp | 57 ++++++------- 2 files changed, 88 insertions(+), 48 deletions(-) diff --git a/include/graphblas/nonblocking/coordinates.hpp b/include/graphblas/nonblocking/coordinates.hpp index 868f804a8..058a06f86 100644 --- a/include/graphblas/nonblocking/coordinates.hpp +++ b/include/graphblas/nonblocking/coordinates.hpp @@ -552,10 +552,22 @@ namespace grb { // TODO: Move me to the initialisation phase, and use _buffer instead of a vector counting_sum.resize( num_tiles+1 ); + // Initialise counting sum to zero + #pragma omp for simd for( size_t i = 0; i <= num_tiles; ++i ) { counting_sum[ i ] = 0; } + if( num_tiles == 0 ) { +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + #pragma omp critical + fprintf( stderr, "[T%02d](l%04d) - guard clause: num_tiles = 0\n", + omp_get_thread_num(), __LINE__ ); +#endif + return; + } + auto counting_sum_copy = counting_sum; + // Counting sum computation for(size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { const auto lower_bound = lower_bounds[tile_id]; @@ -571,6 +583,46 @@ namespace grb { } } + // Counting sum computation +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + #pragma omp critical + fprintf( stderr, "[T%02d](l%04d) - going to compute the counting sum: _n=%zu, num_tiles=%zu \n", + omp_get_thread_num(), __LINE__, _n, num_tiles ); +#endif + for (size_t i = 0; i < _n; ++i) { + const auto k = _stack[i]; + size_t tile_id = 0; + // TODO: Binary search instead ? + while ( (k < lower_bounds[tile_id] || k >= upper_bounds[tile_id]) && tile_id < num_tiles) { + tile_id++; + } + const auto lower_bound = lower_bounds[tile_id]; + const auto upper_bound = upper_bounds[tile_id]; + +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + #pragma omp critical + fprintf(stderr, " [T%02d](l%04d) - k=%u, tile_id=%zu, bounds=[%zu, %zu[ \n", + omp_get_thread_num(), __LINE__, k, tile_id, lower_bound, upper_bound); +#endif + assert(k >= lower_bounds[tile_id]); + assert(tile_id < num_tiles); + assert(lower_bound <= k && k < upper_bound); + assert(_assigned[k]); + counting_sum_copy[tile_id + 1]++; + } + + // Compare counting_sum_copy and counting_sum + for( size_t i = 0; i <= num_tiles; ++i ) { +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) + if( counting_sum_copy[i] != counting_sum[i] ) { + #pragma omp critical + fprintf( stderr, "[T%02d] - counting_sum_copy[%zu] = %u != counting_sum[%zu] = %u\n", + omp_get_thread_num(), i, counting_sum_copy[i], i, counting_sum[i] ); + } +#endif + assert( counting_sum_copy[i] == counting_sum[i] ); + } + #if defined(_DEBUG) || defined(_LOCAL_DEBUG) #pragma omp critical { @@ -588,7 +640,7 @@ namespace grb { } #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical + #pragma omp critical { fprintf( stderr, "[T%02d] - counting_sum (prefixed): {num_tiles=%02zu}[ ", omp_get_thread_num(), num_tiles ); @@ -596,12 +648,12 @@ namespace grb { fprintf( stderr, "%04u ", counting_sum[ i ] ); fprintf( stderr, "]\n" ); } -#endif if( counting_sum[ num_tiles ] != _n ) { #pragma omp critical fprintf( stderr, "[T%02d] - counting_sum[ num_tiles ] = %u, _n = %zu\n", omp_get_thread_num(), counting_sum[ num_tiles ], _n ); } +#endif assert( counting_sum[ num_tiles ] == _n ); } @@ -620,10 +672,12 @@ namespace grb { const auto upper_bound = upper_bounds[ tile_id ]; size_t assigned_in_bucket = 0; +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) #pragma omp critical fprintf(stderr, "[T%02d] - i = [%u, %zu[ (tile_id=%zu)\n", - omp_get_thread_num(), counting_sum[tile_id], upper_bound, tile_id); + omp_get_thread_num(), counting_sum[tile_id], upper_bound, tile_id ); +#endif for (size_t i = counting_sum[tile_id]; i < _n; ++i) { const auto k = _stack[i]; if( not (lower_bound <= k && k < upper_bound) ) { @@ -633,16 +687,12 @@ namespace grb { const auto stack_new_idx = counting_sum_copy[tile_id]++; assert( stack_new_idx < _n ); assert( _assigned[ k ] ); - if( stack_new_idx == i ) { - assigned_in_bucket++; - continue; - } - #pragma omp critical - fprintf(stderr, - "[T%02d] - Found element %zu (%u) is in tile %zu\n", - omp_get_thread_num(), i, k, tile_id); #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical + #pragma omp critical + fprintf(stderr, + "[T%02d] - Found element %zu (%u) is in tile %zu\n", + omp_get_thread_num(), i, k, tile_id); + #pragma omp critical { fprintf( stderr, "[T%02d] - swap( %zu, %u ) {_n=%04zu}\n", omp_get_thread_num(), i, stack_new_idx, _n ); @@ -657,12 +707,13 @@ namespace grb { std::swap(_stack[i], _stack[stack_new_idx]); assigned_in_bucket++; } +#if defined(_DEBUG) || defined(_LOCAL_DEBUG) // Print assigned_in_bucket #pragma omp critical fprintf(stderr, - "[T%02d] - assigned_in_bucket = %zu, should be %u\n", + "[T%02d] - assigned_in_bucket = %zu, expected to be %u\n", omp_get_thread_num(), assigned_in_bucket, counting_sum[tile_id+1] - counting_sum[tile_id]); - assert( assigned_in_bucket == (counting_sum[tile_id+1] - counting_sum[tile_id]) ); +#endif } _debug_is_counting_sort_done = true; diff --git a/src/graphblas/nonblocking/pipeline.cpp b/src/graphblas/nonblocking/pipeline.cpp index 6128602f1..e7d1fc8bf 100644 --- a/src/graphblas/nonblocking/pipeline.cpp +++ b/src/graphblas/nonblocking/pipeline.cpp @@ -856,7 +856,7 @@ grb::RC Pipeline::execution() { #endif { // Initialise the lower and upper bounds - #pragma omp parallel for schedule(dynamic) num_threads(nthreads) + #pragma omp parallel for schedule(dynamic, config::CACHE_LINE_SIZE::value()) num_threads(nthreads) for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { config::OMP::localRange( @@ -871,24 +871,19 @@ grb::RC Pipeline::execution() { #if defined(_DEBUG) || defined(_LOCAL_DEBUG) fprintf( stderr, "Pipeline::execution(2): check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); #endif - size_t current_coord_idx = 0; - for( auto vt = vbegin(); vt != vend(); ++vt ) { - auto *coords = *vt; + for(auto coords : accessed_coordinates) { + // Reduction ( operator OR ) over all tiles to check if any of the tiles will need a counting sum+sort + bool will_require_counting = false; for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - bool will_require_counting_sum = not coords->should_use_bitmask_asyncSubsetInit( - num_tiles, tile_id, lower_bound[tile_id], upper_bound[tile_id] + will_require_counting |= not coords->should_use_bitmask_asyncSubsetInit( + num_tiles, tile_id, lower_bound[ tile_id ], upper_bound[ tile_id ] ); - if (will_require_counting_sum) { -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - fprintf(stderr, " -- coord %zu: will require counting sum+sort for %zu values\n", - current_coord_idx, upper_bound[tile_id] - lower_bound[tile_id]); -#endif - coords->countingSortComputation(num_tiles, lower_bound, upper_bound); - break; - } } - ++current_coord_idx; - //coords->countingSortComputation(num_tiles, lower_bound, upper_bound); + // If any of the tiles will need a counting sum+sort + if( will_require_counting ) { + // Contains an omp parallel region + coords->countingSortComputation( num_tiles, lower_bound, upper_bound ); + } } } @@ -951,9 +946,8 @@ grb::RC Pipeline::execution() { } { // Initialise the lower and upper bounds - #pragma omp parallel for schedule(dynamic) num_threads(nthreads) + #pragma omp parallel for schedule(dynamic, config::CACHE_LINE_SIZE::value()) num_threads(nthreads) for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - config::OMP::localRange( lower_bound[tile_id], upper_bound[tile_id], 0, containers_size, tile_size, tile_id, num_tiles @@ -966,24 +960,19 @@ grb::RC Pipeline::execution() { #if defined(_DEBUG) || defined(_LOCAL_DEBUG) fprintf( stderr, "Pipeline::execution: check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); #endif - size_t current_coord_idx = 0; - for( auto vt = vbegin(); vt != vend(); ++vt ) { - auto *coords = *vt; - bool will_require_counting_sum = false; + for(auto coords : accessed_coordinates) { + // Reduction ( operator OR ) over all tiles to check if any of the tiles will need a counting sum+sort + bool will_require_counting = false; for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - will_require_counting_sum |= not coords->should_use_bitmask_asyncSubsetInit( - num_tiles, tile_id, lower_bound[ tile_id ], upper_bound[ tile_id ] - ); - if( will_require_counting_sum ) { -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - fprintf( stderr, " -- coord %zu: will require counting sum+sort for %zu values\n", current_coord_idx, upper_bound[ tile_id ]- lower_bound[ tile_id ] ); -#endif - coords->countingSortComputation( num_tiles, lower_bound, upper_bound ); - break; - } + will_require_counting |= not coords->should_use_bitmask_asyncSubsetInit( + num_tiles, tile_id, lower_bound[ tile_id ], upper_bound[ tile_id ] + ); + } + // If any of the tiles will need a counting sum+sort + if( will_require_counting ) { + // Contains an omp parallel region + coords->countingSortComputation( num_tiles, lower_bound, upper_bound ); } - ++current_coord_idx; - //coords->countingSortComputation( num_tiles, lower_bound, upper_bound ); } } From c0d64e680e4c63bdb7e8ef1de38bc82f128ac000 Mon Sep 17 00:00:00 2001 From: Benjamin Lozes Date: Thu, 23 Nov 2023 10:18:31 +0100 Subject: [PATCH 04/11] minor optimisations --- include/graphblas/nonblocking/coordinates.hpp | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/include/graphblas/nonblocking/coordinates.hpp b/include/graphblas/nonblocking/coordinates.hpp index 058a06f86..da40380fb 100644 --- a/include/graphblas/nonblocking/coordinates.hpp +++ b/include/graphblas/nonblocking/coordinates.hpp @@ -568,22 +568,24 @@ namespace grb { } auto counting_sum_copy = counting_sum; - // Counting sum computation - for(size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - const auto lower_bound = lower_bounds[tile_id]; - const auto upper_bound = upper_bounds[tile_id]; - - for (size_t i = 0; i < _n; ++i) { - const auto k = _stack[i]; - if (not (lower_bound <= k && k < upper_bound)) { - continue; - } - assert(_assigned[k]); - counting_sum[tile_id + 1]++; - } - } - - // Counting sum computation +// // Counting sum computation +// for(size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { +// const auto lower_bound = lower_bounds[tile_id]; +// const auto upper_bound = upper_bounds[tile_id]; +// +// for (size_t i = 0; i < _n; ++i) { +// const auto k = _stack[i]; +// if (not (lower_bound <= k && k < upper_bound)) { +// continue; +// } +// assert(_assigned[k]); +// counting_sum[tile_id + 1]++; +// } +// } + + const auto tile_size = upper_bounds[0] - lower_bounds[0]; + + // Counting sum computation ( supposed to be faster ) #if defined(_DEBUG) || defined(_LOCAL_DEBUG) #pragma omp critical fprintf( stderr, "[T%02d](l%04d) - going to compute the counting sum: _n=%zu, num_tiles=%zu \n", @@ -591,11 +593,8 @@ namespace grb { #endif for (size_t i = 0; i < _n; ++i) { const auto k = _stack[i]; - size_t tile_id = 0; - // TODO: Binary search instead ? - while ( (k < lower_bounds[tile_id] || k >= upper_bounds[tile_id]) && tile_id < num_tiles) { - tile_id++; - } + size_t tile_id = std::floor( k / tile_size ); + assert(tile_id < num_tiles); const auto lower_bound = lower_bounds[tile_id]; const auto upper_bound = upper_bounds[tile_id]; @@ -604,25 +603,28 @@ namespace grb { fprintf(stderr, " [T%02d](l%04d) - k=%u, tile_id=%zu, bounds=[%zu, %zu[ \n", omp_get_thread_num(), __LINE__, k, tile_id, lower_bound, upper_bound); #endif + assert(k < upper_bound); assert(k >= lower_bounds[tile_id]); - assert(tile_id < num_tiles); - assert(lower_bound <= k && k < upper_bound); assert(_assigned[k]); counting_sum_copy[tile_id + 1]++; } - // Compare counting_sum_copy and counting_sum - for( size_t i = 0; i <= num_tiles; ++i ) { -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - if( counting_sum_copy[i] != counting_sum[i] ) { - #pragma omp critical - fprintf( stderr, "[T%02d] - counting_sum_copy[%zu] = %u != counting_sum[%zu] = %u\n", - omp_get_thread_num(), i, counting_sum_copy[i], i, counting_sum[i] ); - } -#endif - assert( counting_sum_copy[i] == counting_sum[i] ); - } +// // Compare counting_sum_copy and counting_sum +// for( size_t i = 0; i <= num_tiles; ++i ) { +//#if defined(_DEBUG) || defined(_LOCAL_DEBUG) +// if( counting_sum_copy[i] != counting_sum[i] ) { +// #pragma omp critical +// fprintf( stderr, "[T%02d] - counting_sum_copy[%zu] = %u != counting_sum[%zu] = %u\n", +// omp_get_thread_num(), i, counting_sum_copy[i], i, counting_sum[i] ); +// } +//#endif +// assert( counting_sum_copy[i] == counting_sum[i] ); +// } + // TODO: Remove me + counting_sum = counting_sum_copy; + + // Prefix sum computation of the counting sum #if defined(_DEBUG) || defined(_LOCAL_DEBUG) #pragma omp critical { @@ -635,8 +637,9 @@ namespace grb { #endif // Prefix sum computation of the counting sum - for( size_t i = 1; i <= num_tiles; ++i ) { - counting_sum[i] += counting_sum[i - 1]; + // TODO: Make this parallel + for( size_t i = 0; i < num_tiles; ++i ) { + counting_sum[i+1] += counting_sum[i]; } #if defined(_DEBUG) || defined(_LOCAL_DEBUG) From 203e86cfd6ade5f5e19e73284d804940516b62b3 Mon Sep 17 00:00:00 2001 From: Benjamin Lozes Date: Thu, 23 Nov 2023 14:28:10 +0100 Subject: [PATCH 05/11] eliminate counting-sum copy --- include/graphblas/nonblocking/coordinates.hpp | 143 ++++++++++-------- 1 file changed, 76 insertions(+), 67 deletions(-) diff --git a/include/graphblas/nonblocking/coordinates.hpp b/include/graphblas/nonblocking/coordinates.hpp index da40380fb..06e661c28 100644 --- a/include/graphblas/nonblocking/coordinates.hpp +++ b/include/graphblas/nonblocking/coordinates.hpp @@ -27,6 +27,20 @@ #ifndef _H_GRB_NONBLOCKING_COORDINATES #define _H_GRB_NONBLOCKING_COORDINATES +#ifndef NDEBUG +# define ASSERT(condition, message) \ + do { \ + if (! (condition)) { \ + std::cerr << "Assertion `" #condition "` failed in " << __FILE__ \ + << " line " << __LINE__ << ": " << message << std::endl; \ + std::terminate(); \ + } \ + } while (false) +#else +# define ASSERT(condition, message) do { } while (false) +#endif + + #include //std::runtime_error #include #if defined _DEBUG && ! defined NDEBUG @@ -544,6 +558,23 @@ namespace grb { local_new_nnzs[ tile_id ] = 0; } + static size_t getTileId( + size_t k, + const size_t num_tiles, + const std::vector< size_t > &lower_bounds, + const std::vector< size_t > &upper_bounds + ) { + //const size_t tile_id = std::floor( (k+1) / (upper_bounds[0] - lower_bounds[0]) ); + size_t tile_id = 0; + while( k >= upper_bounds[tile_id] ) { + ++tile_id; + } + ASSERT(tile_id < num_tiles, "tile_id = " << tile_id << ", num_tiles = " << num_tiles); + ASSERT(k < upper_bounds[tile_id], "k = " << k << ", tile_id = " << tile_id << ", upper_bounds[tile_id] = " << upper_bounds[tile_id]); + ASSERT(k >= lower_bounds[tile_id], "k = " << k << ", tile_id = " << tile_id << ", lower_bounds[tile_id] = " << lower_bounds[tile_id]); + return tile_id; + } + void countingSumComputation_sequential( const size_t num_tiles, const std::vector< size_t > &lower_bounds, @@ -566,24 +597,6 @@ namespace grb { #endif return; } - auto counting_sum_copy = counting_sum; - -// // Counting sum computation -// for(size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { -// const auto lower_bound = lower_bounds[tile_id]; -// const auto upper_bound = upper_bounds[tile_id]; -// -// for (size_t i = 0; i < _n; ++i) { -// const auto k = _stack[i]; -// if (not (lower_bound <= k && k < upper_bound)) { -// continue; -// } -// assert(_assigned[k]); -// counting_sum[tile_id + 1]++; -// } -// } - - const auto tile_size = upper_bounds[0] - lower_bounds[0]; // Counting sum computation ( supposed to be faster ) #if defined(_DEBUG) || defined(_LOCAL_DEBUG) @@ -593,8 +606,7 @@ namespace grb { #endif for (size_t i = 0; i < _n; ++i) { const auto k = _stack[i]; - size_t tile_id = std::floor( k / tile_size ); - assert(tile_id < num_tiles); + size_t tile_id = getTileId( k, num_tiles, lower_bounds, upper_bounds ); const auto lower_bound = lower_bounds[tile_id]; const auto upper_bound = upper_bounds[tile_id]; @@ -603,27 +615,10 @@ namespace grb { fprintf(stderr, " [T%02d](l%04d) - k=%u, tile_id=%zu, bounds=[%zu, %zu[ \n", omp_get_thread_num(), __LINE__, k, tile_id, lower_bound, upper_bound); #endif - assert(k < upper_bound); - assert(k >= lower_bounds[tile_id]); assert(_assigned[k]); - counting_sum_copy[tile_id + 1]++; + counting_sum[tile_id + 1]++; } -// // Compare counting_sum_copy and counting_sum -// for( size_t i = 0; i <= num_tiles; ++i ) { -//#if defined(_DEBUG) || defined(_LOCAL_DEBUG) -// if( counting_sum_copy[i] != counting_sum[i] ) { -// #pragma omp critical -// fprintf( stderr, "[T%02d] - counting_sum_copy[%zu] = %u != counting_sum[%zu] = %u\n", -// omp_get_thread_num(), i, counting_sum_copy[i], i, counting_sum[i] ); -// } -//#endif -// assert( counting_sum_copy[i] == counting_sum[i] ); -// } - - // TODO: Remove me - counting_sum = counting_sum_copy; - // Prefix sum computation of the counting sum #if defined(_DEBUG) || defined(_LOCAL_DEBUG) #pragma omp critical @@ -651,56 +646,53 @@ namespace grb { fprintf( stderr, "%04u ", counting_sum[ i ] ); fprintf( stderr, "]\n" ); } - if( counting_sum[ num_tiles ] != _n ) { - #pragma omp critical - fprintf( stderr, "[T%02d] - counting_sum[ num_tiles ] = %u, _n = %zu\n", - omp_get_thread_num(), counting_sum[ num_tiles ], _n ); - } #endif - assert( counting_sum[ num_tiles ] == _n ); + ASSERT( counting_sum[ num_tiles ] == _n, "counting_sum[ num_tiles ] = " << counting_sum[ num_tiles ] << ", _n = " << _n ); } void countingSortComputation( - const size_t num_tiles, - const std::vector< size_t > &lower_bounds, - const std::vector< size_t > &upper_bounds - ) noexcept { + const size_t num_tiles, + const std::vector< size_t > &lower_bounds, + const std::vector< size_t > &upper_bounds + ) noexcept { countingSumComputation_sequential( num_tiles, lower_bounds, upper_bounds ); // TODO: Move me to the initialisation phase, and use _buffer instead of a vector auto counting_sum_copy = counting_sum; + const auto tile_size = upper_bounds[0] - lower_bounds[0]; for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { const auto lower_bound = lower_bounds[ tile_id ]; const auto upper_bound = upper_bounds[ tile_id ]; - size_t assigned_in_bucket = 0; #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical - fprintf(stderr, - "[T%02d] - i = [%u, %zu[ (tile_id=%zu)\n", - omp_get_thread_num(), counting_sum[tile_id], upper_bound, tile_id ); + #pragma omp critical + fprintf(stderr, + "[T%02d] - i = [%u, %zu[ (tile_id=%zu)\n", + omp_get_thread_num(), counting_sum[tile_id], upper_bound, tile_id ); #endif - for (size_t i = counting_sum[tile_id]; i < _n; ++i) { + size_t assigned_in_bucket = 0; + const size_t max_assigned_in_bucket = upper_bound - lower_bound; + for (size_t i = counting_sum[tile_id]; i < _n && assigned_in_bucket < max_assigned_in_bucket; ++i) { const auto k = _stack[i]; if( not (lower_bound <= k && k < upper_bound) ) { continue; } - const auto stack_new_idx = counting_sum_copy[tile_id]++; + const auto stack_new_idx = counting_sum[tile_id] + assigned_in_bucket; assert( stack_new_idx < _n ); assert( _assigned[ k ] ); #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical - fprintf(stderr, - "[T%02d] - Found element %zu (%u) is in tile %zu\n", - omp_get_thread_num(), i, k, tile_id); - #pragma omp critical + #pragma omp critical + fprintf(stderr, + "[T%02d] - Found element %zu (%u) is in tile %zu\n", + omp_get_thread_num(), i, k, tile_id); + #pragma omp critical { - fprintf( stderr, "[T%02d] - swap( %zu, %u ) {_n=%04zu}\n", - omp_get_thread_num(), i, stack_new_idx, _n ); + fprintf( stderr, "[T%02d] - swap( %zu, %zu ) {_n=%04zu}\n", + omp_get_thread_num(), i, stack_new_idx, _n ); fprintf( stderr, "[T%02d] - counting_sum (____________): {num_tiles=%02zu}[ ", - omp_get_thread_num(), num_tiles ); + omp_get_thread_num(), num_tiles ); for(size_t _i = 0; _i <= num_tiles; ++_i ) fprintf( stderr, "%04u ", counting_sum_copy[ _i ] ); fprintf( stderr, "]\n" ); @@ -712,11 +704,28 @@ namespace grb { } #if defined(_DEBUG) || defined(_LOCAL_DEBUG) // Print assigned_in_bucket - #pragma omp critical - fprintf(stderr, - "[T%02d] - assigned_in_bucket = %zu, expected to be %u\n", - omp_get_thread_num(), assigned_in_bucket, counting_sum[tile_id+1] - counting_sum[tile_id]); + fprintf(stderr, + "[T%02d] - assigned_in_bucket = %zu, expected to be %u\n", + omp_get_thread_num(), assigned_in_bucket, counting_sum[tile_id+1] - counting_sum[tile_id]); + std::cout << "_stack (after counting sort)(tile_size=" << tile_size << "): ["; + for( size_t i = 0; i < _n; ++i ) { + const auto k = _stack[i]; + if( (k % tile_size) == 0 ) std::cout << "\n\t| "; + std::cout << k << " "; + } + std::cout << "\n]\n"; + } #endif + + { // Pass over the _stack and check that the coordinates are sorted + for (size_t i = 0; i < _n; i++) { + const auto k = _stack[i]; + const auto tile_id = getTileId( k, num_tiles, lower_bounds, upper_bounds ); + ASSERT(_assigned[k], "k = " << k << ", i = " << i << ", tile_id = " << tile_id << "\n"); + ASSERT(k < upper_bounds[tile_id], "k = " << k << ", i = " << i << ", tile_id = " << tile_id << ", lower_bound = " << lower_bounds[tile_id] << ", upper_bound = " << upper_bounds[tile_id]); + ASSERT(k >= lower_bounds[tile_id], "k = " << k << ", i = " << i << ", tile_id = " << tile_id << ", lower_bound = " << lower_bounds[tile_id] << ", upper_bound = " << upper_bounds[tile_id]); + ASSERT(tile_id < num_tiles, "k = " << k << ", i = " << i << ", tile_id = " << tile_id << ", lower_bound = " << lower_bounds[tile_id] << ", upper_bound = " << upper_bounds[tile_id] << ", num_tiles = " << num_tiles); + } } _debug_is_counting_sort_done = true; From a188ee1cb03d476cafe114a87e92bc41db8e0c00 Mon Sep 17 00:00:00 2001 From: Benjamin Lozes Date: Thu, 23 Nov 2023 15:27:58 +0100 Subject: [PATCH 06/11] Cleaning --- commit__Cleaning.png | Bin 0 -> 112431 bytes include/graphblas/nonblocking/coordinates.hpp | 171 ++++++++---------- 2 files changed, 73 insertions(+), 98 deletions(-) create mode 100644 commit__Cleaning.png diff --git a/commit__Cleaning.png b/commit__Cleaning.png new file mode 100644 index 0000000000000000000000000000000000000000..7a0456334e108a0f8562287a5f1c7841b448604d GIT binary patch literal 112431 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

@N<+dYOw@5~hC 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 #include +#include -#define _LOCAL_DEBUG +// #define _LOCAL_DEBUG namespace grb { @@ -509,8 +510,8 @@ namespace grb { omp_get_thread_num(), i, k, lower_bound, upper_bound ); } #endif - assert ( lower_bound <= k && k < upper_bound ); - assert( _assigned[ k ] ); + ASSERT( lower_bound <= k && k < upper_bound, "i=" << i << ", k=" << k << ", lower_bound=" << lower_bound << ", upper_bound=" << upper_bound ); + ASSERT( _assigned[ k ], "i=" << i << ", k=" << k << ", lower_bound=" << lower_bound << ", upper_bound=" << upper_bound ); local_stack[ (*local_nnzs)++ ] = k - lower_bound; } //#if defined(_DEBUG) || defined(_LOCAL_DEBUG) @@ -569,6 +570,8 @@ namespace grb { while( k >= upper_bounds[tile_id] ) { ++tile_id; } + (void) num_tiles; + (void) lower_bounds; ASSERT(tile_id < num_tiles, "tile_id = " << tile_id << ", num_tiles = " << num_tiles); ASSERT(k < upper_bounds[tile_id], "k = " << k << ", tile_id = " << tile_id << ", upper_bounds[tile_id] = " << upper_bounds[tile_id]); ASSERT(k >= lower_bounds[tile_id], "k = " << k << ", tile_id = " << tile_id << ", lower_bounds[tile_id] = " << lower_bounds[tile_id]); @@ -583,70 +586,53 @@ namespace grb { // TODO: Move me to the initialisation phase, and use _buffer instead of a vector counting_sum.resize( num_tiles+1 ); - // Initialise counting sum to zero + // Initialise counting to zero #pragma omp for simd for( size_t i = 0; i <= num_tiles; ++i ) { counting_sum[ i ] = 0; } - if( num_tiles == 0 ) { -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical - fprintf( stderr, "[T%02d](l%04d) - guard clause: num_tiles = 0\n", - omp_get_thread_num(), __LINE__ ); -#endif - return; - } + if( num_tiles == 0 ) { return; } - // Counting sum computation ( supposed to be faster ) -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical - fprintf( stderr, "[T%02d](l%04d) - going to compute the counting sum: _n=%zu, num_tiles=%zu \n", - omp_get_thread_num(), __LINE__, _n, num_tiles ); -#endif + // For-each element in the stack for (size_t i = 0; i < _n; ++i) { const auto k = _stack[i]; + + // Find the tile id of the element size_t tile_id = getTileId( k, num_tiles, lower_bounds, upper_bounds ); - const auto lower_bound = lower_bounds[tile_id]; - const auto upper_bound = upper_bounds[tile_id]; -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical - fprintf(stderr, " [T%02d](l%04d) - k=%u, tile_id=%zu, bounds=[%zu, %zu[ \n", - omp_get_thread_num(), __LINE__, k, tile_id, lower_bound, upper_bound); -#endif - assert(_assigned[k]); + // Assertions + ASSERT( _assigned[k], "i=" << i << ", k=" << k << ", tile_id=" << tile_id ); + ASSERT( k >= lower_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", lower_bound=" << lower_bounds[tile_id] ); + ASSERT( k < upper_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", upper_bound=" << upper_bounds[tile_id] ); + ASSERT( tile_id < num_tiles, "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", num_tiles=" << num_tiles ); + + // Increment the counting for the element's tile counting_sum[tile_id + 1]++; - } - // Prefix sum computation of the counting sum + } // end for-each element in the stack + + #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical - { - fprintf( stderr, "[T%02d] - counting_sum (not-prefixed): {num_tiles=%02zu}[ ", - omp_get_thread_num(), num_tiles ); - for(size_t i = 0; i <= num_tiles; ++i ) - fprintf( stderr, "%04u ", counting_sum[ i ] ); - fprintf( stderr, "]\n" ); - } + std::cout << "counting_sum (not-prefixed): {num_tiles=" << num_tiles << "}[ "; + for(size_t i = 0; i <= num_tiles; ++i ) + std::cout << std::setw(3) << counting_sum[ i ] << " "; + std::cout << "]\n"; #endif - // Prefix sum computation of the counting sum // TODO: Make this parallel + // Prefix-sum computation of the counting for( size_t i = 0; i < num_tiles; ++i ) { counting_sum[i+1] += counting_sum[i]; } #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical - { - fprintf( stderr, "[T%02d] - counting_sum (prefixed): {num_tiles=%02zu}[ ", - omp_get_thread_num(), num_tiles ); - for(size_t i = 0; i <= num_tiles; ++i ) - fprintf( stderr, "%04u ", counting_sum[ i ] ); - fprintf( stderr, "]\n" ); - } + std::cout << "counting_sum (prefixed): {num_tiles=" << num_tiles << "}[ "; + for(size_t i = 0; i <= num_tiles; ++i ) + std::cout << std::setw(3) << counting_sum[ i ] << " "; + std::cout << "]\n"; #endif + ASSERT( counting_sum[ num_tiles ] == _n, "counting_sum[ num_tiles ] = " << counting_sum[ num_tiles ] << ", _n = " << _n ); } @@ -657,74 +643,63 @@ namespace grb { ) noexcept { countingSumComputation_sequential( num_tiles, lower_bounds, upper_bounds ); - // TODO: Move me to the initialisation phase, and use _buffer instead of a vector - auto counting_sum_copy = counting_sum; - const auto tile_size = upper_bounds[0] - lower_bounds[0]; - + // For-each tile for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - const auto lower_bound = lower_bounds[ tile_id ]; - const auto upper_bound = upper_bounds[ tile_id ]; -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical - fprintf(stderr, - "[T%02d] - i = [%u, %zu[ (tile_id=%zu)\n", - omp_get_thread_num(), counting_sum[tile_id], upper_bound, tile_id ); -#endif - size_t assigned_in_bucket = 0; - const size_t max_assigned_in_bucket = upper_bound - lower_bound; - for (size_t i = counting_sum[tile_id]; i < _n && assigned_in_bucket < max_assigned_in_bucket; ++i) { + // Bounds of the current tile + const auto lower_bound = lower_bounds[tile_id]; + const auto upper_bound = upper_bounds[tile_id]; + + // Allows to keep counting_sort intact and singular + size_t assigned_in_tile = 0; + + // Allows quick exit from the loop if the tile has already been filled + const size_t max_assigned_in_tile = upper_bound - lower_bound; + + // For-each element in the stack, from the end of the last processed tile + for (size_t i = counting_sum[tile_id + 1]; i < _n && assigned_in_tile < max_assigned_in_tile; ++i) { const auto k = _stack[i]; - if( not (lower_bound <= k && k < upper_bound) ) { - continue; - } - const auto stack_new_idx = counting_sum[tile_id] + assigned_in_bucket; - assert( stack_new_idx < _n ); - assert( _assigned[ k ] ); -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical - fprintf(stderr, - "[T%02d] - Found element %zu (%u) is in tile %zu\n", - omp_get_thread_num(), i, k, tile_id); - #pragma omp critical - { - fprintf( stderr, "[T%02d] - swap( %zu, %zu ) {_n=%04zu}\n", - omp_get_thread_num(), i, stack_new_idx, _n ); - fprintf( stderr, "[T%02d] - counting_sum (____________): {num_tiles=%02zu}[ ", - omp_get_thread_num(), num_tiles ); - for(size_t _i = 0; _i <= num_tiles; ++_i ) - fprintf( stderr, "%04u ", counting_sum_copy[ _i ] ); - fprintf( stderr, "]\n" ); - } -#endif + // If the element is not in the current tile, skip it + if(not(lower_bound <= k && k < upper_bound)) { continue; } + + // Find the new index of the element: beginning of the current tile + assigned_in_tile + const auto stack_new_idx = counting_sum[tile_id] + assigned_in_tile; + + // Increment the number of assigned elements in the current tile + assigned_in_tile++; + + // Assertions assert(stack_new_idx < _n); + assert(_assigned[k]); + assert(k >= lower_bound); + assert(k < upper_bound); + + // Swap the element with the one at the new index std::swap(_stack[i], _stack[stack_new_idx]); - assigned_in_bucket++; - } + + } // end for-each element in the stack + } // end for-each tile + #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - // Print assigned_in_bucket - fprintf(stderr, - "[T%02d] - assigned_in_bucket = %zu, expected to be %u\n", - omp_get_thread_num(), assigned_in_bucket, counting_sum[tile_id+1] - counting_sum[tile_id]); - std::cout << "_stack (after counting sort)(tile_size=" << tile_size << "): ["; - for( size_t i = 0; i < _n; ++i ) { - const auto k = _stack[i]; - if( (k % tile_size) == 0 ) std::cout << "\n\t| "; - std::cout << k << " "; + std::cout << "_stack (after sort): ["; + for( size_t _tile_id = 0; _tile_id < num_tiles; ++_tile_id ) { + std::cout << "\n\t| "; + for( size_t i = counting_sum[_tile_id]; i < counting_sum[_tile_id+1]; ++i ) + std::cout << _stack[i] << " "; } std::cout << "\n]\n"; - } #endif { // Pass over the _stack and check that the coordinates are sorted for (size_t i = 0; i < _n; i++) { const auto k = _stack[i]; const auto tile_id = getTileId( k, num_tiles, lower_bounds, upper_bounds ); - ASSERT(_assigned[k], "k = " << k << ", i = " << i << ", tile_id = " << tile_id << "\n"); - ASSERT(k < upper_bounds[tile_id], "k = " << k << ", i = " << i << ", tile_id = " << tile_id << ", lower_bound = " << lower_bounds[tile_id] << ", upper_bound = " << upper_bounds[tile_id]); - ASSERT(k >= lower_bounds[tile_id], "k = " << k << ", i = " << i << ", tile_id = " << tile_id << ", lower_bound = " << lower_bounds[tile_id] << ", upper_bound = " << upper_bounds[tile_id]); - ASSERT(tile_id < num_tiles, "k = " << k << ", i = " << i << ", tile_id = " << tile_id << ", lower_bound = " << lower_bounds[tile_id] << ", upper_bound = " << upper_bounds[tile_id] << ", num_tiles = " << num_tiles); + (void) tile_id; + ASSERT(_assigned[k], "i=" << i << ", k=" << k << ", tile_id=" << tile_id); + ASSERT(k < upper_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", upper_bounds[tile_id]=" << upper_bounds[tile_id]); + ASSERT(k >= lower_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", lower_bounds[tile_id]=" << lower_bounds[tile_id]); + ASSERT(tile_id < num_tiles, "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", num_tiles=" << num_tiles); } } From 5fdaec8e2e7a58cb3190403bef3b0626741c4a94 Mon Sep 17 00:00:00 2001 From: Benjamin Lozes Date: Tue, 28 Nov 2023 11:37:49 +0100 Subject: [PATCH 07/11] Restrict counting to tiles that needs it --- include/graphblas/nonblocking/coordinates.hpp | 69 +++++++++++++------ src/graphblas/nonblocking/pipeline.cpp | 47 ++++++++++--- 2 files changed, 84 insertions(+), 32 deletions(-) diff --git a/include/graphblas/nonblocking/coordinates.hpp b/include/graphblas/nonblocking/coordinates.hpp index 5ce7612d2..d7b0b57f1 100644 --- a/include/graphblas/nonblocking/coordinates.hpp +++ b/include/graphblas/nonblocking/coordinates.hpp @@ -47,8 +47,8 @@ #include #endif -#include //size_t -#include +#include //size_t +#include #include #include @@ -493,6 +493,10 @@ namespace grb { config::VectorIndexType *local_stack, config::VectorIndexType *local_nnzs ) noexcept { + (void) tile_id; + (void) lower_bound; + (void) upper_bound; + const auto lower_bound_idx = counting_sum[ tile_id ]; const auto upper_bound_idx = counting_sum[ tile_id+1 ]; if( lower_bound_idx == upper_bound_idx ) { return; } @@ -549,9 +553,17 @@ namespace grb { *local_nnzs = 0; if( should_use_bitmask_asyncSubsetInit( num_tiles, tile_id, lower_bound, upper_bound ) ) { + #ifdef _LOCAL_DEBUG + #pragma omp critical + std::cerr << "> Using bitmask\n"; + #endif _asyncSubsetInit_bitmask( num_tiles, tile_id, lower_bound, upper_bound, local_stack, local_nnzs ); } else { assert( _debug_is_counting_sort_done ); + #ifdef _LOCAL_DEBUG + #pragma omp critical + std::cerr << "> Using search\n"; + #endif _asyncSubsetInit_search( num_tiles, tile_id, lower_bound, upper_bound, local_stack, local_nnzs ); } @@ -565,13 +577,15 @@ namespace grb { const std::vector< size_t > &lower_bounds, const std::vector< size_t > &upper_bounds ) { - //const size_t tile_id = std::floor( (k+1) / (upper_bounds[0] - lower_bounds[0]) ); - size_t tile_id = 0; - while( k >= upper_bounds[tile_id] ) { - ++tile_id; - } + ASSERT( num_tiles > 0, "num_tiles = " << num_tiles ); (void) num_tiles; (void) lower_bounds; + (void) upper_bounds; + + const auto tile_size = upper_bounds[0] - lower_bounds[0]; + ASSERT( tile_size > 0, "tile_size = " << tile_size ); + const size_t tile_id = k / tile_size; + ASSERT(tile_id < num_tiles, "tile_id = " << tile_id << ", num_tiles = " << num_tiles); ASSERT(k < upper_bounds[tile_id], "k = " << k << ", tile_id = " << tile_id << ", upper_bounds[tile_id] = " << upper_bounds[tile_id]); ASSERT(k >= lower_bounds[tile_id], "k = " << k << ", tile_id = " << tile_id << ", lower_bounds[tile_id] = " << lower_bounds[tile_id]); @@ -581,8 +595,11 @@ namespace grb { void countingSumComputation_sequential( const size_t num_tiles, const std::vector< size_t > &lower_bounds, - const std::vector< size_t > &upper_bounds + const std::vector< size_t > &upper_bounds, + const std::vector< size_t > &tiles_to_process ) noexcept { + (void) tiles_to_process; + // TODO: Move me to the initialisation phase, and use _buffer instead of a vector counting_sum.resize( num_tiles+1 ); @@ -639,12 +656,20 @@ namespace grb { void countingSortComputation( const size_t num_tiles, const std::vector< size_t > &lower_bounds, - const std::vector< size_t > &upper_bounds + const std::vector< size_t > &upper_bounds, + const std::vector< size_t > &tiles_to_process ) noexcept { - countingSumComputation_sequential( num_tiles, lower_bounds, upper_bounds ); + if(num_tiles == 1) { + #pragma omp critical + std::cerr << "countingSortComputation(): num_tiles == 1\n"; + _debug_is_counting_sort_done = true; + return; + } + + countingSumComputation_sequential( num_tiles, lower_bounds, upper_bounds, tiles_to_process ); // For-each tile - for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { + for( size_t tile_id : tiles_to_process ) { // Bounds of the current tile const auto lower_bound = lower_bounds[tile_id]; @@ -691,17 +716,17 @@ namespace grb { std::cout << "\n]\n"; #endif - { // Pass over the _stack and check that the coordinates are sorted - for (size_t i = 0; i < _n; i++) { - const auto k = _stack[i]; - const auto tile_id = getTileId( k, num_tiles, lower_bounds, upper_bounds ); - (void) tile_id; - ASSERT(_assigned[k], "i=" << i << ", k=" << k << ", tile_id=" << tile_id); - ASSERT(k < upper_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", upper_bounds[tile_id]=" << upper_bounds[tile_id]); - ASSERT(k >= lower_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", lower_bounds[tile_id]=" << lower_bounds[tile_id]); - ASSERT(tile_id < num_tiles, "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", num_tiles=" << num_tiles); - } - } +// { // Pass over the _stack and check that the coordinates are sorted +// for (size_t i = 0; i < _n; i++) { +// const auto k = _stack[i]; +// const auto tile_id = getTileId( k, num_tiles, lower_bounds, upper_bounds ); +// (void) tile_id; +// ASSERT(_assigned[k], "i=" << i << ", k=" << k << ", tile_id=" << tile_id); +// ASSERT(k < upper_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", upper_bounds[tile_id]=" << upper_bounds[tile_id]); +// ASSERT(k >= lower_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", lower_bounds[tile_id]=" << lower_bounds[tile_id]); +// ASSERT(tile_id < num_tiles, "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", num_tiles=" << num_tiles); +// } +// } _debug_is_counting_sort_done = true; } diff --git a/src/graphblas/nonblocking/pipeline.cpp b/src/graphblas/nonblocking/pipeline.cpp index e7d1fc8bf..d1d92406b 100644 --- a/src/graphblas/nonblocking/pipeline.cpp +++ b/src/graphblas/nonblocking/pipeline.cpp @@ -872,17 +872,18 @@ grb::RC Pipeline::execution() { fprintf( stderr, "Pipeline::execution(2): check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); #endif for(auto coords : accessed_coordinates) { + std::vector< size_t > tiles_requiring_counting; + tiles_requiring_counting.reserve( num_tiles ); // Reduction ( operator OR ) over all tiles to check if any of the tiles will need a counting sum+sort - bool will_require_counting = false; for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - will_require_counting |= not coords->should_use_bitmask_asyncSubsetInit( + if( not coords->should_use_bitmask_asyncSubsetInit( num_tiles, tile_id, lower_bound[ tile_id ], upper_bound[ tile_id ] - ); + ) ) tiles_requiring_counting.push_back( tile_id ); } // If any of the tiles will need a counting sum+sort - if( will_require_counting ) { + if( tiles_requiring_counting.size() > 0 ) { // Contains an omp parallel region - coords->countingSortComputation( num_tiles, lower_bound, upper_bound ); + coords->countingSortComputation( num_tiles, lower_bound, upper_bound, tiles_requiring_counting ); } } } @@ -960,22 +961,36 @@ grb::RC Pipeline::execution() { #if defined(_DEBUG) || defined(_LOCAL_DEBUG) fprintf( stderr, "Pipeline::execution: check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); #endif + size_t coord_idx = 0; for(auto coords : accessed_coordinates) { + std::vector< size_t > tiles_requiring_counting; + tiles_requiring_counting.reserve( num_tiles ); // Reduction ( operator OR ) over all tiles to check if any of the tiles will need a counting sum+sort - bool will_require_counting = false; for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - will_require_counting |= not coords->should_use_bitmask_asyncSubsetInit( + if( not coords->should_use_bitmask_asyncSubsetInit( num_tiles, tile_id, lower_bound[ tile_id ], upper_bound[ tile_id ] - ); + ) ) tiles_requiring_counting.push_back( tile_id ); } // If any of the tiles will need a counting sum+sort - if( will_require_counting ) { + if( tiles_requiring_counting.size() > 0 ) { +#ifdef _LOCAL_DEBUG + #pragma omp critical + { + std::cerr << "countingSortComputation for coords " << coord_idx << " / " + << accessed_coordinates.size() << "; tiles=["; + for( auto tile_id : tiles_requiring_counting ) std::cerr << tile_id << ","; + std::cerr << "]" << std::endl; + } +#endif // Contains an omp parallel region - coords->countingSortComputation( num_tiles, lower_bound, upper_bound ); + coords->countingSortComputation( num_tiles, lower_bound, upper_bound, tiles_requiring_counting ); } + + coord_idx++; } } + // TODO: Uncomment me #pragma omp parallel for schedule(dynamic) num_threads(nthreads) for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { @@ -985,10 +1000,19 @@ grb::RC Pipeline::execution() { // ); // assert( lower_bound[ tile_id ] <= upper_bound[ tile_id ] ); + size_t coord_idx = 0; for( std::set< internal::Coordinates< nonblocking > * >::iterator vt = vbegin(); vt != vend(); ++vt ) { +#ifdef _LOCAL_DEBUG + #pragma omp critical + std::cerr << "-- tile_id: " << tile_id + << "; coords " << coord_idx << " / " << accessed_coordinates.size() + << "; _n/N: " << (**vt).nonzeroes() << "/" << (**vt).size() + << std::endl; +#endif + coord_idx++; // skip the initialization of coordinates of different size, which may // happen only for the input of vxm_generic as it's read-only for the @@ -1010,6 +1034,9 @@ grb::RC Pipeline::execution() { initialized_coordinates = true; } } + #ifdef _LOCAL_DEBUG + std::cerr << std::endl << std::endl; + #endif // even if only one vector is sparse, we cannot reuse memory because the first // two arguments that we pass to the lambda functions determine whether we From 0d4e3cf92cae5081be6b15e8a54f92a811801857 Mon Sep 17 00:00:00 2001 From: Benjamin Lozes Date: Thu, 30 Nov 2023 16:07:18 +0100 Subject: [PATCH 08/11] Adapt condition on asyncSubsetInit --- include/graphblas/nonblocking/coordinates.hpp | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/include/graphblas/nonblocking/coordinates.hpp b/include/graphblas/nonblocking/coordinates.hpp index d7b0b57f1..c74d4f0e8 100644 --- a/include/graphblas/nonblocking/coordinates.hpp +++ b/include/graphblas/nonblocking/coordinates.hpp @@ -462,36 +462,31 @@ namespace grb { assert( _cap > 0 ); assert( _n <= _cap ); assert( lower_bound <= upper_bound ); - return upper_bound - lower_bound < _n; + return nonzeroes() * (upper_bound - lower_bound) > size(); } void _asyncSubsetInit_bitmask( - const size_t /* num_tiles */, - const size_t /* tile_id */, - const size_t lower_bound, - const size_t upper_bound, - config::VectorIndexType *local_stack, - config::VectorIndexType *local_nnzs + const size_t lower_bound, + const size_t upper_bound, + const size_t /*tile_id*/, + config::VectorIndexType *local_nnzs, + config::VectorIndexType *local_stack ) noexcept { + assert( _cap > 0 ); + for( size_t i = lower_bound; i < upper_bound; ++i ) { if( _assigned[ i ] ) { local_stack[ (*local_nnzs)++ ] = i - lower_bound; } } -//#if defined(_DEBUG) || defined(_LOCAL_DEBUG) -// #pragma omp critical -// fprintf( stderr, "[T%02d] - _asyncSubsetInit_bitmask( bounds: [%zu, %zu] ) -> local_nnzs=%u\n", -// omp_get_thread_num(), lower_bound, upper_bound, *local_nnzs ); -//#endif } void _asyncSubsetInit_search( - const size_t /* num_tiles */, - const size_t tile_id, - const size_t lower_bound, - const size_t upper_bound, - config::VectorIndexType *local_stack, - config::VectorIndexType *local_nnzs + const size_t lower_bound, + const size_t upper_bound, + const size_t tile_id, + config::VectorIndexType *local_nnzs, + config::VectorIndexType *local_stack ) noexcept { (void) tile_id; (void) lower_bound; @@ -518,11 +513,6 @@ namespace grb { ASSERT( _assigned[ k ], "i=" << i << ", k=" << k << ", lower_bound=" << lower_bound << ", upper_bound=" << upper_bound ); local_stack[ (*local_nnzs)++ ] = k - lower_bound; } -//#if defined(_DEBUG) || defined(_LOCAL_DEBUG) -// #pragma omp critical -// fprintf( stderr, "[T%02d] - _asyncSubsetInit_search( bounds: [%zu, %zu] ) -> local_nnzs=%u\n", -// omp_get_thread_num(), lower_bound, upper_bound, *local_nnzs ); -//#endif } /** @@ -544,6 +534,7 @@ namespace grb { const size_t lower_bound, const size_t upper_bound ) noexcept { + (void) num_tiles; if( _cap == 0 ) { return; } const size_t tile_id = lower_bound / analytic_model.getTileSize(); @@ -552,20 +543,24 @@ namespace grb { config::VectorIndexType *local_stack = local_buffer[ tile_id ] + 1; *local_nnzs = 0; +#ifdef GRB_ALREADY_DENSE_OPTIMIZATION + _asyncSubsetInit_bitmask( lower_bound, upper_bound, tile_id, local_nnzs, local_stack ); +#else if( should_use_bitmask_asyncSubsetInit( num_tiles, tile_id, lower_bound, upper_bound ) ) { #ifdef _LOCAL_DEBUG #pragma omp critical std::cerr << "> Using bitmask\n"; #endif - _asyncSubsetInit_bitmask( num_tiles, tile_id, lower_bound, upper_bound, local_stack, local_nnzs ); + _asyncSubsetInit_bitmask( lower_bound, upper_bound, tile_id, local_nnzs, local_stack ); } else { assert( _debug_is_counting_sort_done ); #ifdef _LOCAL_DEBUG #pragma omp critical std::cerr << "> Using search\n"; #endif - _asyncSubsetInit_search( num_tiles, tile_id, lower_bound, upper_bound, local_stack, local_nnzs ); + _asyncSubsetInit_search( lower_bound, upper_bound, tile_id, local_nnzs, local_stack ); } +#endif // the number of new nonzeroes is initialized here local_new_nnzs[ tile_id ] = 0; From 4715d7327e48d4113f6a02dabd1db9cfc9123de8 Mon Sep 17 00:00:00 2001 From: Benjamin Lozes Date: Mon, 4 Dec 2023 17:33:56 +0100 Subject: [PATCH 09/11] Secondary logic for asyncSubsetInit --- include/graphblas/nonblocking/coordinates.hpp | 224 +++--------------- src/graphblas/nonblocking/pipeline.cpp | 103 ++------ 2 files changed, 47 insertions(+), 280 deletions(-) diff --git a/include/graphblas/nonblocking/coordinates.hpp b/include/graphblas/nonblocking/coordinates.hpp index c74d4f0e8..28de2fe37 100644 --- a/include/graphblas/nonblocking/coordinates.hpp +++ b/include/graphblas/nonblocking/coordinates.hpp @@ -482,35 +482,27 @@ namespace grb { } void _asyncSubsetInit_search( - const size_t lower_bound, - const size_t upper_bound, - const size_t tile_id, - config::VectorIndexType *local_nnzs, - config::VectorIndexType *local_stack + const size_t num_tiles, + const std::vector &lower_bounds, + const std::vector &upper_bounds ) noexcept { - (void) tile_id; - (void) lower_bound; - (void) upper_bound; - - const auto lower_bound_idx = counting_sum[ tile_id ]; - const auto upper_bound_idx = counting_sum[ tile_id+1 ]; - if( lower_bound_idx == upper_bound_idx ) { return; } -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - #pragma omp critical - fprintf(stderr, "[T%02d] - _asyncSubsetInit_search(): tile_id=%zu, lower_bound_idx=%u, upper_bound_idx=%u\n", - omp_get_thread_num(), tile_id, lower_bound_idx, upper_bound_idx ); -#endif - for( size_t i = lower_bound_idx; i < upper_bound_idx; ++i ) { + for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { + *(local_buffer[ tile_id ]) = 0; + } + for( size_t i = 0; i < nonzeroes(); ++i ) { const size_t k = _stack[ i ]; -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - if( not ( lower_bound <= k && k < upper_bound ) ) { - #pragma omp critical - fprintf(stderr, "ERROR [T%02d] - _asyncSubsetInit_search(): i=%zu, k=%zu, lower_bound=%zu, upper_bound=%zu\n", - omp_get_thread_num(), i, k, lower_bound, upper_bound ); - } -#endif - ASSERT( lower_bound <= k && k < upper_bound, "i=" << i << ", k=" << k << ", lower_bound=" << lower_bound << ", upper_bound=" << upper_bound ); - ASSERT( _assigned[ k ], "i=" << i << ", k=" << k << ", lower_bound=" << lower_bound << ", upper_bound=" << upper_bound ); + assert( _assigned[ k ] ); + + // Find the tile id of the element + const size_t tile_id = getTileId( k, num_tiles, lower_bounds, upper_bounds ); + assert( tile_id < num_tiles ); + assert( k < upper_bounds[tile_id] ); + assert( k >= lower_bounds[tile_id] ); + + const size_t lower_bound = lower_bounds[tile_id]; + + config::VectorIndexType *local_nnzs = local_buffer[ tile_id ]; + config::VectorIndexType *local_stack = local_buffer[ tile_id ] + 1; local_stack[ (*local_nnzs)++ ] = k - lower_bound; } } @@ -531,39 +523,26 @@ namespace grb { */ void asyncSubsetInit( const size_t num_tiles, - const size_t lower_bound, - const size_t upper_bound + const std::vector &lower_bound, + const std::vector &upper_bound ) noexcept { (void) num_tiles; if( _cap == 0 ) { return; } - const size_t tile_id = lower_bound / analytic_model.getTileSize(); - - config::VectorIndexType *local_nnzs = local_buffer[ tile_id ]; - config::VectorIndexType *local_stack = local_buffer[ tile_id ] + 1; - - *local_nnzs = 0; #ifdef GRB_ALREADY_DENSE_OPTIMIZATION - _asyncSubsetInit_bitmask( lower_bound, upper_bound, tile_id, local_nnzs, local_stack ); -#else - if( should_use_bitmask_asyncSubsetInit( num_tiles, tile_id, lower_bound, upper_bound ) ) { - #ifdef _LOCAL_DEBUG - #pragma omp critical - std::cerr << "> Using bitmask\n"; - #endif - _asyncSubsetInit_bitmask( lower_bound, upper_bound, tile_id, local_nnzs, local_stack ); - } else { - assert( _debug_is_counting_sort_done ); - #ifdef _LOCAL_DEBUG - #pragma omp critical - std::cerr << "> Using search\n"; - #endif - _asyncSubsetInit_search( lower_bound, upper_bound, tile_id, local_nnzs, local_stack ); + for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { + config::VectorIndexType *local_nnzs = local_buffer[ tile_id ]; + config::VectorIndexType *local_stack = local_buffer[ tile_id ] + 1; + + *local_nnzs = 0; + _asyncSubsetInit_bitmask( lower_bound[tile_id], upper_bound[tile_id], tile_id, local_nnzs, local_stack ); } +#else + _asyncSubsetInit_search( num_tiles, lower_bound, upper_bound ); #endif - - // the number of new nonzeroes is initialized here - local_new_nnzs[ tile_id ] = 0; + for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { + local_new_nnzs[ tile_id ] = 0; + } } static size_t getTileId( @@ -587,145 +566,6 @@ namespace grb { return tile_id; } - void countingSumComputation_sequential( - const size_t num_tiles, - const std::vector< size_t > &lower_bounds, - const std::vector< size_t > &upper_bounds, - const std::vector< size_t > &tiles_to_process - ) noexcept { - (void) tiles_to_process; - - // TODO: Move me to the initialisation phase, and use _buffer instead of a vector - counting_sum.resize( num_tiles+1 ); - - // Initialise counting to zero - #pragma omp for simd - for( size_t i = 0; i <= num_tiles; ++i ) { - counting_sum[ i ] = 0; - } - - if( num_tiles == 0 ) { return; } - - // For-each element in the stack - for (size_t i = 0; i < _n; ++i) { - const auto k = _stack[i]; - - // Find the tile id of the element - size_t tile_id = getTileId( k, num_tiles, lower_bounds, upper_bounds ); - - // Assertions - ASSERT( _assigned[k], "i=" << i << ", k=" << k << ", tile_id=" << tile_id ); - ASSERT( k >= lower_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", lower_bound=" << lower_bounds[tile_id] ); - ASSERT( k < upper_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", upper_bound=" << upper_bounds[tile_id] ); - ASSERT( tile_id < num_tiles, "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", num_tiles=" << num_tiles ); - - // Increment the counting for the element's tile - counting_sum[tile_id + 1]++; - - } // end for-each element in the stack - - -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - std::cout << "counting_sum (not-prefixed): {num_tiles=" << num_tiles << "}[ "; - for(size_t i = 0; i <= num_tiles; ++i ) - std::cout << std::setw(3) << counting_sum[ i ] << " "; - std::cout << "]\n"; -#endif - - // TODO: Make this parallel - // Prefix-sum computation of the counting - for( size_t i = 0; i < num_tiles; ++i ) { - counting_sum[i+1] += counting_sum[i]; - } - -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - std::cout << "counting_sum (prefixed): {num_tiles=" << num_tiles << "}[ "; - for(size_t i = 0; i <= num_tiles; ++i ) - std::cout << std::setw(3) << counting_sum[ i ] << " "; - std::cout << "]\n"; -#endif - - ASSERT( counting_sum[ num_tiles ] == _n, "counting_sum[ num_tiles ] = " << counting_sum[ num_tiles ] << ", _n = " << _n ); - } - - void countingSortComputation( - const size_t num_tiles, - const std::vector< size_t > &lower_bounds, - const std::vector< size_t > &upper_bounds, - const std::vector< size_t > &tiles_to_process - ) noexcept { - if(num_tiles == 1) { - #pragma omp critical - std::cerr << "countingSortComputation(): num_tiles == 1\n"; - _debug_is_counting_sort_done = true; - return; - } - - countingSumComputation_sequential( num_tiles, lower_bounds, upper_bounds, tiles_to_process ); - - // For-each tile - for( size_t tile_id : tiles_to_process ) { - - // Bounds of the current tile - const auto lower_bound = lower_bounds[tile_id]; - const auto upper_bound = upper_bounds[tile_id]; - - // Allows to keep counting_sort intact and singular - size_t assigned_in_tile = 0; - - // Allows quick exit from the loop if the tile has already been filled - const size_t max_assigned_in_tile = upper_bound - lower_bound; - - // For-each element in the stack, from the end of the last processed tile - for (size_t i = counting_sum[tile_id + 1]; i < _n && assigned_in_tile < max_assigned_in_tile; ++i) { - const auto k = _stack[i]; - - // If the element is not in the current tile, skip it - if(not(lower_bound <= k && k < upper_bound)) { continue; } - - // Find the new index of the element: beginning of the current tile + assigned_in_tile - const auto stack_new_idx = counting_sum[tile_id] + assigned_in_tile; - - // Increment the number of assigned elements in the current tile - assigned_in_tile++; - - // Assertions - assert(stack_new_idx < _n); - assert(_assigned[k]); - assert(k >= lower_bound); - assert(k < upper_bound); - - // Swap the element with the one at the new index - std::swap(_stack[i], _stack[stack_new_idx]); - - } // end for-each element in the stack - } // end for-each tile - -#if defined(_DEBUG) || defined(_LOCAL_DEBUG) - std::cout << "_stack (after sort): ["; - for( size_t _tile_id = 0; _tile_id < num_tiles; ++_tile_id ) { - std::cout << "\n\t| "; - for( size_t i = counting_sum[_tile_id]; i < counting_sum[_tile_id+1]; ++i ) - std::cout << _stack[i] << " "; - } - std::cout << "\n]\n"; -#endif - -// { // Pass over the _stack and check that the coordinates are sorted -// for (size_t i = 0; i < _n; i++) { -// const auto k = _stack[i]; -// const auto tile_id = getTileId( k, num_tiles, lower_bounds, upper_bounds ); -// (void) tile_id; -// ASSERT(_assigned[k], "i=" << i << ", k=" << k << ", tile_id=" << tile_id); -// ASSERT(k < upper_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", upper_bounds[tile_id]=" << upper_bounds[tile_id]); -// ASSERT(k >= lower_bounds[tile_id], "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", lower_bounds[tile_id]=" << lower_bounds[tile_id]); -// ASSERT(tile_id < num_tiles, "i=" << i << ", k=" << k << ", tile_id=" << tile_id << ", num_tiles=" << num_tiles); -// } -// } - - _debug_is_counting_sort_done = true; - } - /** * Retrieves a subset coordinate instance that was previously initialised * using a call to #asyncSubsetInit. diff --git a/src/graphblas/nonblocking/pipeline.cpp b/src/graphblas/nonblocking/pipeline.cpp index d1d92406b..0b2a1a265 100644 --- a/src/graphblas/nonblocking/pipeline.cpp +++ b/src/graphblas/nonblocking/pipeline.cpp @@ -867,27 +867,21 @@ grb::RC Pipeline::execution() { } } - { + #if defined(_DEBUG) || defined(_LOCAL_DEBUG) fprintf( stderr, "Pipeline::execution(2): check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); #endif - for(auto coords : accessed_coordinates) { - std::vector< size_t > tiles_requiring_counting; - tiles_requiring_counting.reserve( num_tiles ); - // Reduction ( operator OR ) over all tiles to check if any of the tiles will need a counting sum+sort - for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - if( not coords->should_use_bitmask_asyncSubsetInit( - num_tiles, tile_id, lower_bound[ tile_id ], upper_bound[ tile_id ] - ) ) tiles_requiring_counting.push_back( tile_id ); - } - // If any of the tiles will need a counting sum+sort - if( tiles_requiring_counting.size() > 0 ) { - // Contains an omp parallel region - coords->countingSortComputation( num_tiles, lower_bound, upper_bound, tiles_requiring_counting ); - } - } - } +#ifndef GRB_ALREADY_DENSE_OPTIMIZATION + + for( + std::set< internal::Coordinates< nonblocking > * >::iterator vt = vbegin(); + vt != vend(); ++vt + ) { + if( (**vt).size() != getContainersSize() ) { continue; } + (**vt).asyncSubsetInit( num_tiles, lower_bound, upper_bound ); + } +#endif #pragma omp parallel for schedule(dynamic) num_threads(nthreads) for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { @@ -898,18 +892,6 @@ grb::RC Pipeline::execution() { // ); // assert( lower_bound[ tile_id ] <= upper_bound[ tile_id ] ); -#ifndef GRB_ALREADY_DENSE_OPTIMIZATION - for( - std::set< internal::Coordinates< nonblocking > * >::iterator vt = vbegin(); - vt != vend(); ++vt - ) { - if ( (**vt).size() != getContainersSize() ) { - continue; - } - - (**vt).asyncSubsetInit( num_tiles, lower_bound[ tile_id ], upper_bound[ tile_id ] ); - } -#endif RC local_ret = SUCCESS; for( std::vector< stage_type >::iterator pt = pbegin(); @@ -959,65 +941,12 @@ grb::RC Pipeline::execution() { { #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - fprintf( stderr, "Pipeline::execution: check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); -#endif - size_t coord_idx = 0; - for(auto coords : accessed_coordinates) { - std::vector< size_t > tiles_requiring_counting; - tiles_requiring_counting.reserve( num_tiles ); - // Reduction ( operator OR ) over all tiles to check if any of the tiles will need a counting sum+sort - for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - if( not coords->should_use_bitmask_asyncSubsetInit( - num_tiles, tile_id, lower_bound[ tile_id ], upper_bound[ tile_id ] - ) ) tiles_requiring_counting.push_back( tile_id ); - } - // If any of the tiles will need a counting sum+sort - if( tiles_requiring_counting.size() > 0 ) { -#ifdef _LOCAL_DEBUG - #pragma omp critical - { - std::cerr << "countingSortComputation for coords " << coord_idx << " / " - << accessed_coordinates.size() << "; tiles=["; - for( auto tile_id : tiles_requiring_counting ) std::cerr << tile_id << ","; - std::cerr << "]" << std::endl; - } + fprintf( stderr, "Pipeline::execution(2): check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); #endif - // Contains an omp parallel region - coords->countingSortComputation( num_tiles, lower_bound, upper_bound, tiles_requiring_counting ); - } - - coord_idx++; - } - } - - // TODO: Uncomment me - #pragma omp parallel for schedule(dynamic) num_threads(nthreads) - for( size_t tile_id = 0; tile_id < num_tiles; ++tile_id ) { - -// config::OMP::localRange( -// lower_bound[ tile_id ], upper_bound[ tile_id ], -// 0, containers_size, tile_size, tile_id, num_tiles -// ); -// assert( lower_bound[ tile_id ] <= upper_bound[ tile_id ] ); - - size_t coord_idx = 0; for( std::set< internal::Coordinates< nonblocking > * >::iterator vt = vbegin(); vt != vend(); ++vt - ) { -#ifdef _LOCAL_DEBUG - #pragma omp critical - std::cerr << "-- tile_id: " << tile_id - << "; coords " << coord_idx << " / " << accessed_coordinates.size() - << "; _n/N: " << (**vt).nonzeroes() << "/" << (**vt).size() - << std::endl; -#endif - coord_idx++; - - // skip the initialization of coordinates of different size, which may - // happen only for the input of vxm_generic as it's read-only for the - // current design - // namely, no stage of the same pipeline can overwrite it + ) { if ( (**vt).size() != getContainersSize() ) { continue; } @@ -1030,13 +959,11 @@ grb::RC Pipeline::execution() { } #endif - (**vt).asyncSubsetInit( num_tiles, lower_bound[ tile_id ], upper_bound[ tile_id ] ); + (**vt).asyncSubsetInit( num_tiles, lower_bound, upper_bound ); initialized_coordinates = true; } } - #ifdef _LOCAL_DEBUG - std::cerr << std::endl << std::endl; - #endif + // even if only one vector is sparse, we cannot reuse memory because the first // two arguments that we pass to the lambda functions determine whether we From cd5bba4ba33c374d82ff5812dc9fbf5477a5bdf6 Mon Sep 17 00:00:00 2001 From: Benjamin Lozes Date: Mon, 4 Dec 2023 17:48:18 +0100 Subject: [PATCH 10/11] Parallel version of the job below --- src/graphblas/nonblocking/pipeline.cpp | 29 ++++++++++++-------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/graphblas/nonblocking/pipeline.cpp b/src/graphblas/nonblocking/pipeline.cpp index 0b2a1a265..7449f5c72 100644 --- a/src/graphblas/nonblocking/pipeline.cpp +++ b/src/graphblas/nonblocking/pipeline.cpp @@ -872,14 +872,12 @@ grb::RC Pipeline::execution() { fprintf( stderr, "Pipeline::execution(2): check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); #endif #ifndef GRB_ALREADY_DENSE_OPTIMIZATION + std::vector< Coordinates< nonblocking > * > accessed_coordinates_vec( accessed_coordinates.begin(), accessed_coordinates.end() ); + #pragma omp parallel for schedule(dynamic) num_threads(nthreads) + for( Coordinates< nonblocking > * vt : accessed_coordinates_vec ) { + if( (*vt).size() != getContainersSize() ) { continue; } - for( - std::set< internal::Coordinates< nonblocking > * >::iterator vt = vbegin(); - vt != vend(); ++vt - ) { - if( (**vt).size() != getContainersSize() ) { continue; } - - (**vt).asyncSubsetInit( num_tiles, lower_bound, upper_bound ); + (*vt).asyncSubsetInit( num_tiles, lower_bound, upper_bound ); } #endif #pragma omp parallel for schedule(dynamic) num_threads(nthreads) @@ -943,25 +941,24 @@ grb::RC Pipeline::execution() { #if defined(_DEBUG) || defined(_LOCAL_DEBUG) fprintf( stderr, "Pipeline::execution(2): check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); #endif - for( - std::set< internal::Coordinates< nonblocking > * >::iterator vt = vbegin(); - vt != vend(); ++vt - ) { - if ( (**vt).size() != getContainersSize() ) { + std::vector< Coordinates< nonblocking > * > accessed_coordinates_vec( accessed_coordinates.begin(), accessed_coordinates.end() ); + #pragma omp parallel for schedule(dynamic) num_threads(nthreads) + for( Coordinates< nonblocking > * vt : accessed_coordinates_vec ) { + if ( (*vt).size() != getContainersSize() ) { continue; } #ifdef GRB_ALREADY_DENSE_OPTIMIZATION - if( (**vt).isDense() && ( - !contains_out_of_place_primitive || !outOfPlaceOutput( *vt ) + if( (*vt).isDense() && ( + !contains_out_of_place_primitive || !outOfPlaceOutput( vt ) ) ) { continue; } #endif - (**vt).asyncSubsetInit( num_tiles, lower_bound, upper_bound ); - initialized_coordinates = true; + (*vt).asyncSubsetInit( num_tiles, lower_bound, upper_bound ); } + initialized_coordinates = true; } From e94329fe427c0dc962e8b1203e01490758601f31 Mon Sep 17 00:00:00 2001 From: "Albert-Jan N. Yzelman" Date: Wed, 13 Mar 2024 17:24:24 +0100 Subject: [PATCH 11/11] Resolve erroneous merge conflict resolution -- this works. TODO: first, group everything in a single parallel section. Currently, we are paying the overhead of repeated thread creations. Second, change accessed_vector type to a vector instead of a set, thus preventing dynamic allocations and copies. (I have a working stash that did #2 first, but that is the wrong order to go about) --- src/graphblas/nonblocking/pipeline.cpp | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/graphblas/nonblocking/pipeline.cpp b/src/graphblas/nonblocking/pipeline.cpp index bcde7735b..0a459d20b 100644 --- a/src/graphblas/nonblocking/pipeline.cpp +++ b/src/graphblas/nonblocking/pipeline.cpp @@ -867,10 +867,13 @@ grb::RC Pipeline::execution() { 0, containers_size, tile_size, tile_id, num_tiles ); assert( lower_bound[ tile_id ] <= upper_bound[ tile_id ] ); + } + } #if defined(_DEBUG) || defined(_LOCAL_DEBUG) - fprintf( stderr, "Pipeline::execution(2): check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); + fprintf( stderr, "Pipeline::execution(2): check if any of the coordinates will use the search-variant of asyncSubsetInit:\n" ); #endif + #ifndef GRB_ALREADY_DENSE_OPTIMIZATION std::vector< Coordinates< nonblocking > * > accessed_coordinates_vec( accessed_coordinates.begin(), accessed_coordinates.end() ); #pragma omp parallel for schedule(dynamic) num_threads(nthreads) @@ -891,17 +894,16 @@ grb::RC Pipeline::execution() { // assert( lower_bound[ tile_id ] <= upper_bound[ tile_id ] ); - RC local_ret = SUCCESS; - for( std::vector< stage_type >::iterator pt = pbegin(); - pt != pend(); ++pt - ) { - local_ret = local_ret - ? local_ret - : (*pt)( *this, lower_bound[ tile_id ], upper_bound[ tile_id ] ); - } - if( local_ret != SUCCESS ) { - ret = local_ret; - } + RC local_ret = SUCCESS; + for( std::vector< stage_type >::iterator pt = pbegin(); + pt != pend(); ++pt + ) { + local_ret = local_ret + ? local_ret + : (*pt)( *this, lower_bound[ tile_id ], upper_bound[ tile_id ] ); + } + if( local_ret != SUCCESS ) { + ret = local_ret; } } } else {