HW2: First locally working version of the 2 variations

This commit is contained in:
2024-12-30 19:12:55 +02:00
parent 4d1d7502aa
commit c5ffec9cca
9 changed files with 403 additions and 105 deletions
+1 -1
View File
@@ -25,7 +25,7 @@
#endif
// Value type selection
using distValue_t = uint8_t;
using distValue_t = uint32_t;
/*!
* Session option for each invocation of the executable
+110 -70
View File
@@ -45,8 +45,8 @@ template <SortMode Mode> inline bool ascending(mpi_id_t, [[maybe_unused]] size_t
* Returns the ascending or descending configuration of the node's sequence based on
* the current node (MPI process) and the depth of the sorting network
*
* @param node The current node (MPI process)
* @return True if we need ascending configuration, false otherwise
* @param node [mpi_id_t] The current node (MPI process)
* @return [bool] True if we need ascending configuration, false otherwise
*/
template <> inline
bool ascending<SortMode::Bubbletonic>(mpi_id_t node, [[maybe_unused]] size_t depth) noexcept {
@@ -57,10 +57,9 @@ bool ascending<SortMode::Bubbletonic>(mpi_id_t node, [[maybe_unused]] size_t dep
* Returns the ascending or descending configuration of the node's sequence based on
* the current node (MPI process) and the depth of the sorting network
*
* @param node The current node (MPI process)
* @param depth The total depth of the sorting network (same for each step for a given network)
*
* @return True if we need ascending configuration, false otherwise
* @param node [mpi_id_t] The current node (MPI process)
* @param depth [size_t] The total depth of the sorting network (same for each step for a given network)
* @return [bool] True if we need ascending configuration, false otherwise
*/
template <> inline
bool ascending<SortMode::Bitonic>(mpi_id_t node, size_t depth) noexcept {
@@ -77,9 +76,9 @@ template <SortMode Mode> inline mpi_id_t partner(mpi_id_t, size_t) noexcept = de
* Returns the node's partner for data exchange during the sorting network iterations
* of Bubbletonic
*
* @param node The current node
* @param step The step of the sorting network
* @return The node id of the partner for data exchange
* @param node [mpi_id_t] The current node
* @param step [size_t] The step of the sorting network
* @return [mpi_id_t] The node id of the partner for data exchange
*/
template <> inline
mpi_id_t partner<SortMode::Bubbletonic>(mpi_id_t node, size_t step) noexcept {
@@ -91,9 +90,9 @@ mpi_id_t partner<SortMode::Bubbletonic>(mpi_id_t node, size_t step) noexcept {
* Returns the node's partner for data exchange during the sorting network iterations
* of Bitonic
*
* @param node The current node
* @param step The step of the sorting network
* @return The node id of the partner for data exchange
* @param node [mpi_id_t] The current node
* @param step [size_t] The step of the sorting network
* @return [mpi_id_t] The node id of the partner for data exchange
*/
template <> inline
mpi_id_t partner<SortMode::Bitonic>(mpi_id_t node, size_t step) noexcept {
@@ -105,32 +104,34 @@ mpi_id_t partner<SortMode::Bitonic>(mpi_id_t node, size_t step) noexcept {
* The primary function template of keepSmall(). It is DISABLED since , it is explicitly specialized
* for each of the \c SortMode
*/
template<SortMode Mode> inline bool keepSmall(mpi_id_t, mpi_id_t, [[maybe_unused]] size_t) noexcept = delete;
template<SortMode Mode> inline bool keepSmall(mpi_id_t, mpi_id_t, [[maybe_unused]] size_t) = delete;
/*!
* Predicate to check if a node keeps the small numbers during the bubbletonic sort network exchange.
*
* @param node The node for which we check
* @param partner The partner of the data exchange
* @return True if the node should keep the small values, false otherwise
* @param node [mpi_id_t] The node for which we check
* @param partner [mpi_id_t] The partner of the data exchange
* @return [bool] True if the node should keep the small values, false otherwise
*/
template <> inline
bool keepSmall<SortMode::Bubbletonic>(mpi_id_t node, mpi_id_t partner, [[maybe_unused]] size_t depth) noexcept {
assert(node != partner);
bool keepSmall<SortMode::Bubbletonic>(mpi_id_t node, mpi_id_t partner, [[maybe_unused]] size_t depth) {
if (node == partner)
throw std::runtime_error("(keepSmall) Node and Partner can not be the same\n");
return (node < partner);
}
/*!
* Predicate to check if a node keeps the small numbers during the bitonic sort network exchange.
*
* @param node The node for which we check
* @param partner The partner of the data exchange
* @param depth The total depth of the sorting network (same for each step for a given network)
* @return True if the node should keep the small values, false otherwise
* @param node [mpi_id_t] The node for which we check
* @param partner [mpi_id_t] The partner of the data exchange
* @param depth [size_t] The total depth of the sorting network (same for each step for a given network)
* @return [bool] True if the node should keep the small values, false otherwise
*/
template <> inline
bool keepSmall<SortMode::Bitonic>(mpi_id_t node, mpi_id_t partner, size_t depth) noexcept {
assert(node != partner);
bool keepSmall<SortMode::Bitonic>(mpi_id_t node, mpi_id_t partner, size_t depth) {
if (node == partner)
throw std::runtime_error("(keepSmall) Node and Partner can not be the same\n");
return ascending<SortMode::Bitonic>(node, depth) == (node < partner);
}
@@ -138,24 +139,26 @@ bool keepSmall<SortMode::Bitonic>(mpi_id_t node, mpi_id_t partner, size_t depth)
* Predicate to check if the node is active in the current iteration of the bubbletonic
* sort exchange.
*
* @param node The node to check
* @param nodes The total number of nodes
* @return True if the node is active, false otherwise
* @param node [mpi_id_t] The node to check
* @param nodes [size_t] The total number of nodes
* @return [bool] True if the node is active, false otherwise
*/
bool isActive(mpi_id_t node, size_t nodes) noexcept;
bool isActive(mpi_id_t node, size_t nodes);
/*
* ============================== Data utilities ==============================
*/
/*!
* Sort a range using the build-in O(Nlog(N)) algorithm
*
* @tparam RangeT
* @param data
* @param ascending
* @tparam RangeT A range type with random access iterator
*
* @param data [RangeT] The data to be sorted
* @param ascending [bool] Flag to indicate the sorting order
*/
template<typename RangeT>
void fullSort(RangeT& data, bool ascending) {
void fullSort(RangeT& data, bool ascending) noexcept {
// Use introsort from stdlib++ here, unless ...
if (ascending)
std::sort(data.begin(), data.end(), std::less<>());
@@ -164,66 +167,87 @@ void fullSort(RangeT& data, bool ascending) {
}
/*!
* Core functionality of sort for shadowed buffer types using
* the "elbow sort" algorithm.
*
* @tparam ShadowedT
* @tparam CompT
* @param data
* @param comp
* @note:
* This algorithm can not work "in place".
* We use the active buffer as source and the shadow as target.
* At the end we switch which buffer is active and which is the shadow.
* @note
* This is the core functionality. Use the elbowSort() function instead
*
* @tparam ShadowedT A Shadowed buffer type with random access iterator.
* @tparam CompT A Comparison type for binary operation comparisons
*
* @param data [ShadowedT] The data to sort
* @param ascending [bool] Flag to indicate the sorting order
* @param comp [CompT] The binary operator object
*/
template<typename ShadowedT, typename CompT>
void elbowSortCore(ShadowedT& data, CompT comp) {
size_t N = data.size();
auto active = data.getActive();
auto shadow = data.getShadow();
void elbowSortCore(ShadowedT& data, bool ascending, CompT comp) noexcept {
auto& active = data.getActive(); // Get the source vector (the data to sort)
auto& shadow = data.getShadow(); // Get the target vector (the sorted data)
size_t N = data.size(); // The total size is the same or both vectors
size_t left = std::distance(
active.begin(),
std::min_element(active.begin(), active.end())
);
(ascending) ?
std::min_element(active.begin(), active.end()) :
std::max_element(active.begin(), active.end())
); // start 'left' from elbow of the bitonic
size_t right = (left == N-1) ? 0 : left + 1;
// Walk in opposite directions from elbow and insert-sort to target vector
for (size_t i = 0 ; i<N ; ++i) {
if (comp(active[left], active[right])) {
shadow[i] = active[left];
left = (left == 0) ? N-1 : left -1;
left = (left == 0) ? N-1 : left -1; // cycle decrease
}
else {
shadow[i] = active[right];
right = (right + 1) % N;
right = (right + 1) % N; // cycle increase
}
}
data.switch_active();
data.switch_active(); // Switch active-shadow buffers
}
/*!
* Sort a shadowed buffer using the "elbow sort" algorithm.
*
* @tparam ShadowedT
* @param data
* @param ascending
* @tparam ShadowedT A Shadowed buffer type with random access iterator.
*
* @param data [ShadowedT] The data to sort
* @param ascending [bool] Flag to indicate the sorting order
*/
template<typename ShadowedT>
void elbowSort(ShadowedT& data, bool ascending) {
void elbowSort(ShadowedT& data, bool ascending) noexcept {
if (ascending)
elbowSortCore(data, std::less<>());
elbowSortCore(data, ascending, std::less<>());
else
elbowSortCore(data, std::greater<>());
elbowSortCore(data, ascending, std::greater<>());
}
/*!
* Takes two sorted sequences where one is in increasing and the other is in decreasing order
* and selects either the larger or the smaller items in one-to-one comparison between them.
* The result is a bitonic sequence.
*
* @tparam RangeT
* @param local
* @param remote
* @param keepsmall
* @tparam RangeT A range type with random access iterator
*
* @param local [RangeT] Reference to the local sequence
* @param remote [const RangeT] Reference to the remote sequence (copied locally by MPI)
* @param keepSmall [bool] Flag to indicate if we keep the small items in local sequence
*/
template<typename RangeT>
void minmax(RangeT& local, RangeT& remote, bool keepsmall) {
void minmax(RangeT& local, const RangeT& remote, bool keepSmall) noexcept {
using value_t = typename RangeT::value_type;
std::transform(
local.begin(), local.end(),
remote.begin(),
local.begin(),
[keepsmall](const value_t& a, const value_t& b){
return (keepsmall) ? std::min(a, b) : std::max(a, b);
[&keepSmall](const value_t& a, const value_t& b){
return (keepSmall) ? std::min(a, b) : std::max(a, b);
});
}
@@ -232,27 +256,36 @@ void minmax(RangeT& local, RangeT& remote, bool keepsmall) {
*/
/*!
* A distributed version of the Bubbletonic sort algorithm.
*
* @tparam ShadowedT
* @param data
* @param Processes
* @note
* Each MPI process should run an instance of this function.
*
* @tparam ShadowedT A Shadowed buffer type with random access iterator.
*
* @param data [ShadowedT] The local to MPI process data to sort
* @param Processes [mpi_id_t] The total number of MPI processes
*/
template<typename ShadowedT>
void distBubbletonic(ShadowedT& data, mpi_id_t Processes) {
// Initially sort to create a half part of a bitonic sequence
fullSort(data, ascending<SortMode::Bubbletonic>(mpi.rank(), 0));
// Sort network
for (size_t step = 0; step < Processes-1; ++step) {
// Sort network (O(N) iterations)
for (size_t step = 0; step < static_cast<size_t>(Processes-1); ++step) {
// Find out exchange configuration
auto part = partner<SortMode::Bubbletonic>(mpi.rank(), step);
auto ks = keepSmall<SortMode::Bubbletonic>(mpi.rank(), part, Processes);
if (isActive(mpi.rank(), Processes)) {
if ( isActive(mpi.rank(), Processes) &&
isActive(part, Processes) ) {
// Exchange with partner, keep nim-or-max and sort - O(N)
mpi.exchange(part, data.getActive(), data.getShadow(), step);
minmax(data.getActive(), data.getShadow(), ks);
elbowSort(data, ascending<SortMode::Bubbletonic>(mpi.rank(), Processes));
}
}
// Invert if the node was descending.
if (!ascending<SortMode::Bubbletonic>(mpi.rank(), 0))
elbowSort(data, true);
@@ -260,27 +293,34 @@ void distBubbletonic(ShadowedT& data, mpi_id_t Processes) {
/*!
* A distributed version of the Bitonic sort algorithm.
*
* @tparam ShadowedT
* @param data
* @param Processes
* @note
* Each MPI process should run an instance of this function.
*
* @tparam ShadowedT A Shadowed buffer type with random access iterator.
*
* @param data [ShadowedT] The local to MPI process data to sort
* @param Processes [mpi_id_t] The total number of MPI processes
*/
template<typename ShadowedT>
void distBitonic(ShadowedT& data, mpi_id_t Processes) {
auto p = static_cast<uint32_t>(std::log2(Processes));
// Initially sort to create a half part of a bitonic sequence
fullSort(data, ascending<SortMode::Bitonic>(mpi.rank(), 0));
// Run through sort network using elbow-sort
// Run through sort network using elbow-sort ( O(LogN * LogN) iterations )
auto p = static_cast<uint32_t>(std::log2(Processes));
for (size_t depth = 1; depth <= p; ++depth) {
for (size_t step = depth; step > 0;) {
--step;
// Find out exchange configuration
auto part = partner<SortMode::Bitonic>(mpi.rank(), step);
auto ks = keepSmall<SortMode::Bitonic>(mpi.rank(), part, depth);
// Exchange with partner, keep nim-or-max
mpi.exchange(part, data.getActive(), data.getShadow(), (depth << 8) | step);
minmax(data.getActive(), data.getShadow(), ks);
}
// sort - O(N)
elbowSort (data, ascending<SortMode::Bitonic>(mpi.rank(), depth));
}
}
+69 -6
View File
@@ -23,14 +23,19 @@ template <typename T> struct MPI_TypeMapper;
// Specializations for supported types
template <> struct MPI_TypeMapper<char> { static MPI_Datatype getType() { return MPI_CHAR; } };
template <> struct MPI_TypeMapper<unsigned char> { static MPI_Datatype getType() { return MPI_UNSIGNED_CHAR; } };
template <> struct MPI_TypeMapper<short> { static MPI_Datatype getType() { return MPI_SHORT; } };
template <> struct MPI_TypeMapper<int> { static MPI_Datatype getType() { return MPI_INT; } };
template <> struct MPI_TypeMapper<long> { static MPI_Datatype getType() { return MPI_LONG; } };
template <> struct MPI_TypeMapper<long long> { static MPI_Datatype getType() { return MPI_LONG_LONG; } };
template <> struct MPI_TypeMapper<unsigned short>{ static MPI_Datatype getType() { return MPI_UNSIGNED_SHORT; } };
template <> struct MPI_TypeMapper<unsigned int> { static MPI_Datatype getType() { return MPI_UNSIGNED; } };
template <> struct MPI_TypeMapper<unsigned long> { static MPI_Datatype getType() { return MPI_UNSIGNED_LONG; } };
template <> struct MPI_TypeMapper<unsigned long long> { static MPI_Datatype getType() { return MPI_UNSIGNED_LONG_LONG; } };
template <> struct MPI_TypeMapper<float> { static MPI_Datatype getType() { return MPI_FLOAT; } };
template <> struct MPI_TypeMapper<double> { static MPI_Datatype getType() { return MPI_DOUBLE; } };
template<typename TID = int>
struct MPI_t {
using ID_t = TID; // Export TID type (currently int defined by the standard)
@@ -38,6 +43,7 @@ struct MPI_t {
void init(int *argc, char ***argv) {
// Initialize the MPI environment
MPI_Init(argc, argv);
initialized_ = true;
// Get the number of processes
int size_value, rank_value;
@@ -53,11 +59,6 @@ struct MPI_t {
name_ = std::string (processor_name, name_len);
}
void finalize() {
// Finalize the MPI environment.
MPI_Finalize();
}
template<typename T>
void exchange(ID_t partner, const std::vector<T>& send_data, std::vector<T>& recv_data, int tag) {
using namespace std::string_literals;
@@ -83,10 +84,21 @@ struct MPI_t {
[[nodiscard]] ID_t size() const noexcept { return size_; }
[[nodiscard]] const std::string& name() const noexcept { return name_; }
void finalize() {
// Finalize the MPI environment
initialized_ = false;
MPI_Finalize();
}
~MPI_t() {
// Finalize the MPI environment even on unexpected errors
if (initialized_)
MPI_Finalize();
}
private:
ID_t rank_{};
ID_t size_{};
std::string name_{};
bool initialized_{};
};
extern MPI_t<> mpi;
@@ -100,6 +112,42 @@ struct ShadowedVec_t {
using const_iterator = typename std::vector<Value_t>::const_iterator;
using size_type = typename std::vector<Value_t>::size_type;
// Default constructor
ShadowedVec_t() = default;
// Constructor from an std::vector
explicit ShadowedVec_t(const std::vector<Value_t>& vec)
: North(vec), South(), active(north) {
South.resize(North.size());
}
explicit ShadowedVec_t(std::vector<Value_t>&& vec)
: North(std::move(vec)), South(), active(north) {
South.resize(North.size());
}
// Copy assignment operator
ShadowedVec_t& operator=(const ShadowedVec_t& other) {
if (this != &other) { // Avoid self-assignment
North = other.North;
South = other.South;
active = other.active;
}
return *this;
}
// Move assignment operator
ShadowedVec_t& operator=(ShadowedVec_t&& other) noexcept {
if (this != &other) { // Avoid self-assignment
North = std::move(other.North);
South = std::move(other.South);
active = other.active;
// There is no need to zero out other since it is valid but in a non-defined state
}
return *this;
}
// Dispatch to active vector
Value_t& operator[](size_type index) { return getActive()[index]; }
const Value_t& operator[](size_type index) const { return getActive()[index]; }
@@ -155,10 +203,25 @@ struct ShadowedVec_t {
const std::vector<Value_t>& getShadow() const {
return (active == north) ? South : North;
}
// Comparisons
bool operator== (const ShadowedVec_t& other) {
return getActive() == other.getActive();
}
bool operator!= (const ShadowedVec_t& other) {
return getActive() != other.getActive();
}
bool operator== (const std::vector<value_type>& other) {
return getActive() == other;
}
bool operator!= (const std::vector<value_type>& other) {
return getActive() != other;
}
private:
enum { north, south } active{north};
std::vector<Value_t> North{};
std::vector<Value_t> South{};
enum { north, south } active{north};
};
using distBuffer_t = ShadowedVec_t<distValue_t>;