AUTH's THMMY "Parallel and distributed systems" course assignments.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

331 lines
12 KiB

  1. /*!
  2. * \file
  3. * \brief Distributed sort implementation header
  4. *
  5. * \author
  6. * Christos Choutouridis AEM:8997
  7. * <cchoutou@ece.auth.gr>
  8. */
  9. #ifndef DISTBITONIC_H_
  10. #define DISTBITONIC_H_
  11. #include <vector>
  12. #include <algorithm>
  13. #include <cmath>
  14. #include <cstdint>
  15. #if !defined DEBUG
  16. #define NDEBUG
  17. #endif
  18. #include <cassert>
  19. #include "utils.hpp"
  20. #include "config.h"
  21. /*!
  22. * Enumerator for the different versions of the sorting method
  23. */
  24. enum class SortMode {
  25. Bubbletonic, //!< The v0.5 of the algorithm where we use a bubble-sort like approach
  26. Bitonic //!< The v1.0 of the algorithm where we use the bitonic data-exchange approach
  27. };
  28. /*
  29. * ============================== Sort utilities ==============================
  30. */
  31. /*!
  32. * The primary function template of ascending(). It is DISABLED since , it is explicitly specialized
  33. * for each of the \c SortMode
  34. */
  35. template <SortMode Mode> inline bool ascending(mpi_id_t, [[maybe_unused]] size_t) noexcept = delete;
  36. /*!
  37. * Returns the ascending or descending configuration of the node's sequence based on
  38. * the current node (MPI process) and the depth of the sorting network
  39. *
  40. * @param node [mpi_id_t] The current node (MPI process)
  41. * @return [bool] True if we need ascending configuration, false otherwise
  42. */
  43. template <> inline
  44. bool ascending<SortMode::Bubbletonic>(mpi_id_t node, [[maybe_unused]] size_t depth) noexcept {
  45. return (node % 2) == 0;
  46. }
  47. /*!
  48. * Returns the ascending or descending configuration of the node's sequence based on
  49. * the current node (MPI process) and the depth of the sorting network
  50. *
  51. * @param node [mpi_id_t] The current node (MPI process)
  52. * @param depth [size_t] The total depth of the sorting network (same for each step for a given network)
  53. * @return [bool] True if we need ascending configuration, false otherwise
  54. */
  55. template <> inline
  56. bool ascending<SortMode::Bitonic>(mpi_id_t node, size_t depth) noexcept {
  57. return !(node & (1 << depth));
  58. }
  59. /*!
  60. * The primary function template of partner(). It is DISABLED since , it is explicitly specialized
  61. * for each of the \c SortMode
  62. */
  63. template <SortMode Mode> inline mpi_id_t partner(mpi_id_t, size_t) noexcept = delete;
  64. /*!
  65. * Returns the node's partner for data exchange during the sorting network iterations
  66. * of Bubbletonic
  67. *
  68. * @param node [mpi_id_t] The current node
  69. * @param step [size_t] The step of the sorting network
  70. * @return [mpi_id_t] The node id of the partner for data exchange
  71. */
  72. template <> inline
  73. mpi_id_t partner<SortMode::Bubbletonic>(mpi_id_t node, size_t step) noexcept {
  74. //return (node % 2 == step % 2) ? node + 1 : node - 1;
  75. return (((node+step) % 2) == 0) ? node + 1 : node - 1;
  76. }
  77. /*!
  78. * Returns the node's partner for data exchange during the sorting network iterations
  79. * of Bitonic
  80. *
  81. * @param node [mpi_id_t] The current node
  82. * @param step [size_t] The step of the sorting network
  83. * @return [mpi_id_t] The node id of the partner for data exchange
  84. */
  85. template <> inline
  86. mpi_id_t partner<SortMode::Bitonic>(mpi_id_t node, size_t step) noexcept {
  87. return (node ^ (1 << step));
  88. }
  89. /*!
  90. * The primary function template of keepSmall(). It is DISABLED since , it is explicitly specialized
  91. * for each of the \c SortMode
  92. */
  93. template<SortMode Mode> inline bool keepSmall(mpi_id_t, mpi_id_t, [[maybe_unused]] size_t) = delete;
  94. /*!
  95. * Predicate to check if a node keeps the small numbers during the bubbletonic sort network exchange.
  96. *
  97. * @param node [mpi_id_t] The node for which we check
  98. * @param partner [mpi_id_t] The partner of the data exchange
  99. * @return [bool] True if the node should keep the small values, false otherwise
  100. */
  101. template <> inline
  102. bool keepSmall<SortMode::Bubbletonic>(mpi_id_t node, mpi_id_t partner, [[maybe_unused]] size_t depth) {
  103. if (node == partner)
  104. throw std::runtime_error("(keepSmall) Node and Partner can not be the same\n");
  105. return (node < partner);
  106. }
  107. /*!
  108. * Predicate to check if a node keeps the small numbers during the bitonic sort network exchange.
  109. *
  110. * @param node [mpi_id_t] The node for which we check
  111. * @param partner [mpi_id_t] The partner of the data exchange
  112. * @param depth [size_t] The total depth of the sorting network (same for each step for a given network)
  113. * @return [bool] True if the node should keep the small values, false otherwise
  114. */
  115. template <> inline
  116. bool keepSmall<SortMode::Bitonic>(mpi_id_t node, mpi_id_t partner, size_t depth) {
  117. if (node == partner)
  118. throw std::runtime_error("(keepSmall) Node and Partner can not be the same\n");
  119. return ascending<SortMode::Bitonic>(node, depth) == (node < partner);
  120. }
  121. /*!
  122. * Predicate to check if the node is active in the current iteration of the bubbletonic
  123. * sort exchange.
  124. *
  125. * @param node [mpi_id_t] The node to check
  126. * @param nodes [size_t] The total number of nodes
  127. * @return [bool] True if the node is active, false otherwise
  128. */
  129. bool isActive(mpi_id_t node, size_t nodes);
  130. /*
  131. * ============================== Data utilities ==============================
  132. */
  133. /*!
  134. * Sort a range using the build-in O(Nlog(N)) algorithm
  135. *
  136. * @tparam RangeT A range type with random access iterator
  137. *
  138. * @param data [RangeT] The data to be sorted
  139. * @param ascending [bool] Flag to indicate the sorting order
  140. */
  141. template<typename RangeT>
  142. void fullSort(RangeT& data, bool ascending) noexcept {
  143. // Use introsort from stdlib++ here, unless ...
  144. if (ascending)
  145. std::sort(data.begin(), data.end(), std::less<>());
  146. else
  147. std::sort(data.begin(), data.end(), std::greater<>());
  148. }
  149. /*!
  150. * Core functionality of sort for shadowed buffer types using
  151. * the "elbow sort" algorithm.
  152. *
  153. * @note:
  154. * This algorithm can not work "in place".
  155. * We use the active buffer as source and the shadow as target.
  156. * At the end we switch which buffer is active and which is the shadow.
  157. * @note
  158. * This is the core functionality. Use the elbowSort() function instead
  159. *
  160. * @tparam ShadowedDataT A Shadowed buffer type with random access iterator.
  161. * @tparam CompT A Comparison type for binary operation comparisons
  162. *
  163. * @param data [ShadowedDataT] The data to sort
  164. * @param ascending [bool] Flag to indicate the sorting order
  165. * @param comp [CompT] The binary operator object
  166. */
  167. template<typename ShadowedDataT, typename CompT>
  168. void elbowSortCore(ShadowedDataT& data, bool ascending, CompT comp) noexcept {
  169. auto& active = data.getActive(); // Get the source vector (the data to sort)
  170. auto& shadow = data.getShadow(); // Get the target vector (the sorted data)
  171. size_t N = data.size(); // The total size is the same or both vectors
  172. size_t left = std::distance(
  173. active.begin(),
  174. (ascending) ?
  175. std::min_element(active.begin(), active.end()) :
  176. std::max_element(active.begin(), active.end())
  177. ); // start 'left' from elbow of the bitonic
  178. size_t right = (left == N-1) ? 0 : left + 1;
  179. // Walk in opposite directions from elbow and insert-sort to target vector
  180. for (size_t i = 0 ; i<N ; ++i) {
  181. if (comp(active[left], active[right])) {
  182. shadow[i] = active[left];
  183. left = (left == 0) ? N-1 : left -1; // cycle decrease
  184. }
  185. else {
  186. shadow[i] = active[right];
  187. right = (right + 1) % N; // cycle increase
  188. }
  189. }
  190. data.switch_active(); // Switch active-shadow buffers
  191. }
  192. /*!
  193. * Sort a shadowed buffer using the "elbow sort" algorithm.
  194. *
  195. * @tparam ShadowedDataT A Shadowed buffer type with random access iterator.
  196. *
  197. * @param data [ShadowedDataT] The data to sort
  198. * @param ascending [bool] Flag to indicate the sorting order
  199. */
  200. template<typename ShadowedDataT>
  201. void elbowSort(ShadowedDataT& data, bool ascending) noexcept {
  202. if (ascending)
  203. elbowSortCore(data, ascending, std::less<>());
  204. else
  205. elbowSortCore(data, ascending, std::greater<>());
  206. }
  207. /*!
  208. * Takes two sorted sequences where one is in increasing and the other is in decreasing order
  209. * and selects either the larger or the smaller items in one-to-one comparison between them.
  210. * The result is a bitonic sequence.
  211. *
  212. * @tparam RangeT A range type with random access iterator
  213. *
  214. * @param local [RangeT] Reference to the local sequence
  215. * @param remote [const RangeT] Reference to the remote sequence (copied locally by MPI)
  216. * @param keepSmall [bool] Flag to indicate if we keep the small items in local sequence
  217. */
  218. template<typename RangeT>
  219. void minmax(RangeT& local, const RangeT& remote, bool keepSmall) noexcept {
  220. using value_t = typename RangeT::value_type;
  221. std::transform(
  222. local.begin(), local.end(),
  223. remote.begin(),
  224. local.begin(),
  225. [&keepSmall](const value_t& a, const value_t& b){
  226. return (keepSmall) ? std::min(a, b) : std::max(a, b);
  227. });
  228. }
  229. /*
  230. * ============================== Sort algorithms ==============================
  231. */
  232. /*!
  233. * A distributed version of the Bubbletonic sort algorithm.
  234. *
  235. * @note
  236. * Each MPI process should run an instance of this function.
  237. *
  238. * @tparam ShadowedDataT A Shadowed buffer type with random access iterator.
  239. *
  240. * @param data [ShadowedDataT] The local to MPI process data to sort
  241. * @param Processes [mpi_id_t] The total number of MPI processes
  242. * @param rank [mpi_id_t] The current process id
  243. */
  244. template<typename ShadowedDataT>
  245. void distBubbletonic(ShadowedDataT& data, mpi_id_t Processes, mpi_id_t rank) {
  246. // Initially sort to create a half part of a bitonic sequence
  247. fullSort(data, ascending<SortMode::Bubbletonic>(rank, 0));
  248. // Sort network (O(N) iterations)
  249. for (size_t step = 0; step < static_cast<size_t>(Processes); ++step) {
  250. // Find out exchange configuration
  251. auto part = partner<SortMode::Bubbletonic>(rank, step);
  252. auto ks = keepSmall<SortMode::Bubbletonic>(rank, part, Processes);
  253. if ( isActive(rank, Processes) &&
  254. isActive(part, Processes) ) {
  255. // Exchange with partner, keep nim-or-max and sort - O(N)
  256. mpi.exchange(data.getActive(), data.getShadow(), part, step);
  257. minmax(data.getActive(), data.getShadow(), ks);
  258. elbowSort(data, ascending<SortMode::Bubbletonic>(rank, Processes));
  259. }
  260. }
  261. // Invert if the node was descending.
  262. if (!ascending<SortMode::Bubbletonic>(rank, 0))
  263. elbowSort(data, true);
  264. }
  265. /*!
  266. * A distributed version of the Bitonic sort algorithm.
  267. *
  268. * @note
  269. * Each MPI process should run an instance of this function.
  270. *
  271. * @tparam ShadowedDataT A Shadowed buffer type with random access iterator.
  272. *
  273. * @param data [ShadowedDataT] The local to MPI process data to sort
  274. * @param Processes [mpi_id_t] The total number of MPI processes
  275. * @param rank [mpi_id_t] The current process id
  276. */
  277. template<typename ShadowedDataT>
  278. void distBitonic(ShadowedDataT& data, mpi_id_t Processes, mpi_id_t rank) {
  279. // Initially sort to create a half part of a bitonic sequence
  280. fullSort(data, ascending<SortMode::Bitonic>(rank, 0));
  281. // Run through sort network using elbow-sort ( O(LogN * LogN) iterations )
  282. auto p = static_cast<uint32_t>(std::log2(Processes));
  283. for (size_t depth = 1; depth <= p; ++depth) {
  284. for (size_t step = depth; step > 0;) {
  285. --step;
  286. // Find out exchange configuration
  287. auto part = partner<SortMode::Bitonic>(rank, step);
  288. auto ks = keepSmall<SortMode::Bitonic>(rank, part, depth);
  289. // Exchange with partner, keep nim-or-max
  290. mpi.exchange(data.getActive(), data.getShadow(), part, (depth << 8) | step);
  291. minmax(data.getActive(), data.getShadow(), ks);
  292. }
  293. // sort - O(N)
  294. elbowSort (data, ascending<SortMode::Bitonic>(rank, depth));
  295. }
  296. }
  297. #endif //DISTBITONIC_H_