cuDDL
Loading...
Searching...
No Matches
distributed.cuh
Go to the documentation of this file.
1#pragma once
2
3// Sketching and searching one genome collection across the ranks of an MPI communicator.
4// Needs MPI, so cuddl.cuh leaves it out; link `cuddl_distributed_dep`.
5//
6// Every rank calls each function collectively, with the same arguments where noted:
7//
8// auto slice = CUDDL_TRY(cuddl::slice_genomes(input, comm));
9// auto mine = CUDDL_TRY((cuddl::reference_database_file::build<K, B>(slice.mine(), stream)));
10// auto file = CUDDL_TRY(cuddl::gather_sketches(mine, slice, comm));
11//
12// `file` then holds every genome on every rank, in reference-ID order, the same rows a
13// single-process build of `slice.paths` gives. To search it all-to-all across the ranks,
14// upload it with replicated_database::upload on each rank and search collectively:
15//
16// auto group = CUDDL_TRY((cuddl::replicated_database<K, B>::upload(file, devices)));
17// CUDDL_TRY(cuddl::search_all_to_all(group, on_tile, comm));
18//
19// Ranks claim tiles from a counter they share until none are left, so a faster GPU searches
20// more of the pairs.
21//
22// A rank that fails inside one of these functions returns its error, and every other rank
23// returns an error naming it, so no rank is left waiting in a collective. A rank that fails
24// between them (for example in the build) must still reach the next call or abort the
25// communicator.
26
27#include <mpi.h>
28
29#include <algorithm>
30#include <cstdint>
31#include <filesystem>
32#include <fstream>
33#include <limits>
34#include <memory>
35#include <mutex>
36#include <numeric>
37#include <span>
38#include <string>
39#include <system_error>
40#include <thread>
41#include <vector>
42
45
46namespace cuddl {
47
51 std::vector<std::filesystem::path> paths;
53 std::vector<uint64_t> bytes;
55 size_t first = 0;
56 size_t last = 0;
57
58 [[nodiscard]] std::span<std::filesystem::path const> mine() const noexcept {
59 return std::span{paths}.subspan(first, last - first);
60 }
61};
62
63namespace detail {
64
65[[nodiscard]] inline Result<void> mpi_try(int status, char const* call) {
66 if (status == MPI_SUCCESS) return Ok();
67 char text[MPI_MAX_ERROR_STRING];
68 int length = 0;
69 MPI_Error_string(status, text, &length);
70 return Err(Error::resource(std::string(call) + ": " + std::string(text, length)));
71}
72
75[[nodiscard]] inline Result<void> agree(Result<void> local, MPI_Comm comm) {
76 int rank = 0, size = 1;
77 CUDDL_TRY(mpi_try(MPI_Comm_rank(comm, &rank), "MPI_Comm_rank"));
78 CUDDL_TRY(mpi_try(MPI_Comm_size(comm, &size), "MPI_Comm_size"));
79 int const mine = local ? size : rank;
80 int failed = size;
81 CUDDL_TRY(mpi_try(MPI_Allreduce(&mine, &failed, 1, MPI_INT, MPI_MIN, comm), "MPI_Allreduce"));
82 if (!local) return local;
83 if (failed == size) return Ok();
84 return Err(Error::resource("rank " + std::to_string(failed) + " failed"));
85}
86
89share_listing(std::filesystem::path const& input, int rank, MPI_Comm comm) {
90 std::string joined;
91 auto listed = [&]() -> Result<void> {
92 if (rank != 0) return Ok();
93 auto const paths = CUDDL_TRY(list_genomes(input));
94 for (auto const& path : paths) joined += path.string() + '\n';
95 return Ok();
96 }();
97 CUDDL_TRY(agree(std::move(listed), comm));
98 uint64_t length = joined.size();
99 CUDDL_TRY(mpi_try(MPI_Bcast(&length, 1, MPI_UINT64_T, 0, comm), "MPI_Bcast"));
100 joined.resize(length);
101 // MPI counts are int: send the listing in chunks below 2 GiB.
102 constexpr uint64_t chunk = uint64_t{1} << 30U;
103 for (uint64_t offset = 0; offset < length; offset += chunk) {
104 auto const count = static_cast<int>(std::min(chunk, length - offset));
105 CUDDL_TRY(
106 mpi_try(MPI_Bcast(joined.data() + offset, count, MPI_CHAR, 0, comm), "MPI_Bcast")
107 );
108 }
109 std::vector<std::filesystem::path> paths;
110 for (size_t begin = 0; begin < joined.size();) {
111 auto const end = joined.find('\n', begin);
112 paths.emplace_back(joined.substr(begin, end - begin));
113 begin = end + 1;
114 }
115 return paths;
116}
117
119[[nodiscard]] inline size_t
120slice_begin(std::span<uint64_t const> bytes, uint64_t total, int rank, int size) {
121 auto const target = static_cast<uint64_t>(static_cast<double>(total) * rank / size);
122 uint64_t sum = 0;
123 size_t i = 0;
124 while (i < bytes.size() && sum + bytes[i] / 2 < target) sum += bytes[i++];
125 return i;
126}
127
131 reference_database_file const& like,
132 std::vector<std::filesystem::path> const& paths,
133 std::vector<uint16_t> rows
134 ) {
136 file.blacklist_ = like.blacklist_;
137 file.metadata_ = like.metadata_;
138 file.metadata_.reference_count = static_cast<uint32_t>(paths.size());
139 file.names_.reserve(paths.size());
140 for (auto const& path : paths) file.names_.push_back(path.string());
141 file.rows_ = std::move(rows);
142 return file;
143 }
144};
145
146} // namespace detail
147
160 std::filesystem::path const& input,
161 MPI_Comm comm,
162 unsigned workers = default_parser_workers()
163) {
164 if (workers == 0U)
165 return Err(Error::invalid_argument("slice_genomes needs at least one worker"));
166 int rank = 0, size = 1;
167 CUDDL_TRY(detail::mpi_try(MPI_Comm_rank(comm, &rank), "MPI_Comm_rank"));
168 CUDDL_TRY(detail::mpi_try(MPI_Comm_size(comm, &size), "MPI_Comm_size"));
169 genome_slice slice;
170 slice.paths = CUDDL_TRY(detail::share_listing(input, rank, comm));
171 auto const& paths = slice.paths;
172 if (paths.size() > static_cast<size_t>(std::numeric_limits<int>::max())) {
173 return Err(Error::resource("genome collection exceeds MPI counts"));
174 }
175
176 auto const share = [&](int r) {
177 return paths.size() * static_cast<size_t>(r) / static_cast<size_t>(size);
178 };
179 auto const own = share(rank);
180 std::vector<uint64_t> mine(share(rank + 1) - own);
181 std::vector<std::error_code> errors(mine.size());
182 {
183 std::vector<std::jthread> threads;
184 for (unsigned t = 0; t < workers && t < mine.size(); ++t) {
185 threads.emplace_back([&, t] {
186 for (size_t i = t; i < mine.size(); i += workers) {
187 mine[i] = std::filesystem::file_size(paths[own + i], errors[i]);
188 }
189 });
190 }
191 }
192 auto sized = [&]() -> Result<void> {
193 for (size_t i = 0; i < mine.size(); ++i) {
194 if (errors[i]) {
195 return Err(Error::resource(paths[own + i].string() + ": " + errors[i].message()));
196 }
197 }
198 return Ok();
199 }();
200 CUDDL_TRY(detail::agree(std::move(sized), comm));
201
202 std::vector<int> counts(size), offsets(size);
203 for (int r = 0; r < size; ++r) {
204 offsets[r] = static_cast<int>(share(r));
205 counts[r] = static_cast<int>(share(r + 1) - share(r));
206 }
207 slice.bytes.resize(paths.size());
208 CUDDL_TRY(
210 MPI_Allgatherv(
211 mine.data(),
212 static_cast<int>(mine.size()),
213 MPI_UINT64_T,
214 slice.bytes.data(),
215 counts.data(),
216 offsets.data(),
217 MPI_UINT64_T,
218 comm
219 ),
220 "MPI_Allgatherv"
221 )
222 );
223 auto const total = std::accumulate(slice.bytes.begin(), slice.bytes.end(), uint64_t{0});
224 slice.first = detail::slice_begin(slice.bytes, total, rank, size);
225 slice.last =
226 rank + 1 == size ? paths.size() : detail::slice_begin(slice.bytes, total, rank + 1, size);
227 return slice;
228}
229
230namespace detail {
231
234[[nodiscard]] inline Result<std::vector<uint32_t>>
235slice_counts(reference_database_file const& mine, genome_slice const& slice, MPI_Comm comm) {
236 auto const metadata = mine.metadata();
237 auto reference = metadata.compatibility;
238 CUDDL_TRY(mpi_try(MPI_Bcast(&reference, sizeof(reference), MPI_BYTE, 0, comm), "MPI_Bcast"));
239 auto local = [&]() -> Result<void> {
240 if (metadata.reference_count != slice.last - slice.first) {
241 return Err(Error::invalid_argument("sketches do not match this rank's genome slice"));
242 }
243 if (!(reference == metadata.compatibility)) {
244 return Err(Error::invalid_argument("ranks sketched with different settings"));
245 }
246 return Ok();
247 }();
248 CUDDL_TRY(agree(std::move(local), comm));
249 int size = 1;
250 CUDDL_TRY(mpi_try(MPI_Comm_size(comm, &size), "MPI_Comm_size"));
251 uint32_t const count = metadata.reference_count;
252 std::vector<uint32_t> counts(size);
254 MPI_Allgather(&count, 1, MPI_UINT32_T, counts.data(), 1, MPI_UINT32_T, comm),
255 "MPI_Allgather"
256 ));
257 if (std::accumulate(counts.begin(), counts.end(), uint64_t{0}) != slice.paths.size()) {
258 return Err(Error::invalid_argument("ranks hold slices of different genome collections"));
259 }
260 return counts;
261}
262
264[[nodiscard]] inline Result<MPI_Datatype> row_type(uint32_t buckets) {
265 MPI_Datatype row = MPI_DATATYPE_NULL;
267 MPI_Type_contiguous(static_cast<int>(buckets), MPI_UINT16_T, &row), "MPI_Type_contiguous"
268 ));
269 CUDDL_TRY(mpi_try(MPI_Type_commit(&row), "MPI_Type_commit"));
270 return row;
271}
272
273} // namespace detail
274
284[[nodiscard]] inline Result<reference_database_file>
285gather_sketches(reference_database_file const& mine, genome_slice const& slice, MPI_Comm comm) {
286 auto const counts = CUDDL_TRY(detail::slice_counts(mine, slice, comm));
287 std::vector<int> sizes(counts.begin(), counts.end()), offsets(counts.size());
288 std::exclusive_scan(sizes.begin(), sizes.end(), offsets.begin(), 0);
289 auto const buckets = mine.metadata().compatibility.bucket_count;
290 auto row = CUDDL_TRY(detail::row_type(buckets));
291 std::vector<uint16_t> rows(slice.paths.size() * buckets);
292 auto const gathered = detail::mpi_try(
293 MPI_Allgatherv(
294 mine.rows().data(),
295 static_cast<int>(mine.metadata().reference_count),
296 row,
297 rows.data(),
298 sizes.data(),
299 offsets.data(),
300 row,
301 comm
302 ),
303 "MPI_Allgatherv"
304 );
305 MPI_Type_free(&row);
306 CUDDL_TRY(gathered);
307 return detail::reference_file_assembly::make(mine, slice.paths, std::move(rows));
308}
309
326 public:
327 __host__ shared_tile_counter(shared_tile_counter&& other) noexcept
328 : state_(std::move(other.state_)) {}
330 state_ = std::move(other.state_);
331 return *this;
332 }
333 __host__ ~shared_tile_counter() {} // NOLINT(modernize-use-equals-default)
334
335 [[nodiscard]] static Result<shared_tile_counter> make(MPI_Comm comm) {
336 int provided = MPI_THREAD_SINGLE;
337 CUDDL_TRY(detail::mpi_try(MPI_Query_thread(&provided), "MPI_Query_thread"));
338 if (provided < MPI_THREAD_MULTIPLE) {
339 return Err(
341 "shared_tile_counter needs MPI initialised with MPI_THREAD_MULTIPLE"
342 )
343 );
344 }
345 auto state = std::make_unique<shared_state>();
346 state->comm = comm;
347 CUDDL_TRY(detail::mpi_try(MPI_Comm_rank(comm, &state->rank), "MPI_Comm_rank"));
348 auto const bytes = state->rank == 0 ? static_cast<MPI_Aint>(sizeof(uint64_t)) : 0;
349 CUDDL_TRY(
351 MPI_Win_allocate(
352 bytes, sizeof(uint64_t), MPI_INFO_NULL, comm, &state->count, &state->window
353 ),
354 "MPI_Win_allocate"
355 )
356 );
357 CUDDL_TRY(detail::mpi_try(MPI_Win_lock_all(0, state->window), "MPI_Win_lock_all"));
358 state->locked = true;
359 if (state->rank == 0) {
360 *state->count = 0;
361 CUDDL_TRY(detail::mpi_try(MPI_Win_sync(state->window), "MPI_Win_sync"));
362 CUDDL_TRY(
363 detail::mpi_try(MPI_Comm_dup(MPI_COMM_SELF, &state->progress_comm), "MPI_Comm_dup")
364 );
365 // Probing a communicator nobody sends on runs MPI's progress engine, which
366 // completes the claims other ranks have in flight.
367 if (!detail::ablated("progress-thread")) {
368 state->progress =
369 std::jthread([comm = state->progress_comm](const std::stop_token& stop) {
370 while (!stop.stop_requested()) {
371 int flag = 0;
372 MPI_Iprobe(MPI_ANY_SOURCE, MPI_ANY_TAG, comm, &flag, MPI_STATUS_IGNORE);
373 std::this_thread::yield();
374 }
375 });
376 }
377 }
378 CUDDL_TRY(detail::mpi_try(MPI_Barrier(comm), "MPI_Barrier"));
379 shared_tile_counter counter;
380 counter.state_ = std::move(state);
381 return Result<shared_tile_counter>::ok(std::move(counter));
382 }
383
385 [[nodiscard]] uint64_t operator()() {
386 std::lock_guard const lock(state_->update);
387 uint64_t const one = 1;
388 uint64_t previous = 0;
389 if (MPI_Fetch_and_op(&one, &previous, MPI_UINT64_T, 0, 0, MPI_SUM, state_->window) !=
390 MPI_SUCCESS ||
391 MPI_Win_flush(0, state_->window) != MPI_SUCCESS) {
392 MPI_Abort(state_->comm, 1);
393 }
394 return previous;
395 }
396
398 [[nodiscard]] Result<void> reset() {
399 CUDDL_TRY(detail::mpi_try(MPI_Barrier(state_->comm), "MPI_Barrier"));
400 if (state_->rank == 0) {
401 uint64_t const zero = 0;
402 CUDDL_TRY(
404 MPI_Accumulate(
405 &zero, 1, MPI_UINT64_T, 0, 0, 1, MPI_UINT64_T, MPI_REPLACE, state_->window
406 ),
407 "MPI_Accumulate"
408 )
409 );
410 CUDDL_TRY(detail::mpi_try(MPI_Win_flush(0, state_->window), "MPI_Win_flush"));
411 }
412 return detail::mpi_try(MPI_Barrier(state_->comm), "MPI_Barrier");
413 }
414
415 private:
416 struct shared_state {
417 MPI_Comm comm = MPI_COMM_NULL;
418 MPI_Comm progress_comm = MPI_COMM_NULL;
419 MPI_Win window = MPI_WIN_NULL;
420 uint64_t* count = nullptr;
421 int rank = 0;
422 bool locked = false;
423 std::mutex update;
424 std::jthread progress;
425
426 ~shared_state() {
427 if (progress.joinable()) {
428 progress.request_stop();
429 progress.join();
430 }
431 int finalized = 0;
432 MPI_Finalized(&finalized);
433 if (finalized) return;
434 if (progress_comm != MPI_COMM_NULL) MPI_Comm_free(&progress_comm);
435 if (window == MPI_WIN_NULL) return;
436 if (locked) MPI_Win_unlock_all(window);
437 MPI_Win_free(&window);
438 }
439 };
440
441 shared_tile_counter() = default;
442 std::unique_ptr<shared_state> state_;
443};
444
445namespace detail {
446
448template <typename Search>
449[[nodiscard]] Result<void> shared_search(MPI_Comm comm, Search&& search) {
450 int rank = 0;
451 int size = 1;
452 CUDDL_TRY(mpi_try(MPI_Comm_rank(comm, &rank), "MPI_Comm_rank"));
453 CUDDL_TRY(mpi_try(MPI_Comm_size(comm, &size), "MPI_Comm_size"));
454 auto counter = CUDDL_TRY(shared_tile_counter::make(comm));
455 return search(
457 .shard = {.index = static_cast<uint32_t>(rank), .count = static_cast<uint32_t>(size)},
458 .next_tile = detail::ablated("tile-queue") ? std::function<uint64_t()>{}
459 : [&counter] { return counter(); },
460 }
461 );
462}
463
464} // namespace detail
465
474template <uint32_t K, size_t BucketCount, typename Layout, typename OnTile>
477 OnTile&& on_tile,
478 MPI_Comm comm
479) {
480 return detail::shared_search(comm, [&](replica_search split) {
481 return group.search_all_to_all(on_tile, std::move(split));
482 });
483}
484
487template <uint32_t K, size_t BucketCount, typename Layout, typename OnTile>
490 OnTile&& on_tile,
491 search_options options,
492 MPI_Comm comm
493) {
494 return detail::shared_search(comm, [&](replica_search split) {
495 return group.search_all_to_all(on_tile, options, std::move(split));
496 });
497}
498
499} // namespace cuddl
Host-owned reference sketches and labels, ready for binary storage or GPU upload.
std::span< uint16_t const > rows() const noexcept
Winner scores in reference-ID order, one bucket row per genome.
reference_database_metadata metadata() const noexcept
Compatibility metadata and reference count.
One reference database replicated on explicitly chosen devices, searched all-to-all by all of them at...
A tile counter shared by every rank of a communicator, for replica_search::next_tile.
static Result< shared_tile_counter > make(MPI_Comm comm)
__host__ shared_tile_counter(shared_tile_counter &&other) noexcept
uint64_t operator()()
The next tile number of the search, counting from 0 across every rank.
__host__ shared_tile_counter & operator=(shared_tile_counter &&other) noexcept
Result< void > reset()
Starts the count again at 0 once every rank has finished the previous search.
#define CUDDL_TRY(expr)
Propagates a cuddl::Result failure from the enclosing function (GNU statement expression).
Definition error.hpp:225
size_t slice_begin(std::span< uint64_t const > bytes, uint64_t total, int rank, int size)
First genome of rank rank's slice: contiguous slices of about equal bytes.
Result< std::vector< uint32_t > > slice_counts(reference_database_file const &mine, genome_slice const &slice, MPI_Comm comm)
Every rank's genome count, after checking collectively that mine holds the sketches of this rank's sl...
auto const count
Definition kernels.cuh:1031
Result< void > agree(Result< void > local, MPI_Comm comm)
Returns local on the rank that failed, and on every other rank an error naming the lowest failing ran...
unsigned long long local
Definition kernels.cuh:478
auto const total
Definition kernels.cuh:443
Result< std::vector< std::filesystem::path > > share_listing(std::filesystem::path const &input, int rank, MPI_Comm comm)
Rank 0's listing of input on every rank.
Result< void > shared_search(MPI_Comm comm, Search &&search)
Runs search with a replica_search that shares tiles across the ranks of comm.
Result< MPI_Datatype > row_type(uint32_t buckets)
One row of buckets scores as a single MPI element, so counts of rows stay within int.
constexpr bool ablated(std::string_view) noexcept
Always false: this build compiles the ablation switches out.
Definition ablation.hpp:82
Result< void > mpi_try(int status, char const *call)
__global__ size_t uint16_t * rows
Definition kernels.cuh:1499
auto const row
Definition top_k.cuh:202
Batch operations over contiguous device rows.
Definition a48.hpp:14
@ counts
One 24-byte batch_search_result per pair: query ID, reference ID and the lower, equal,...
Result< void > Ok() noexcept
Success return for Result<void>; same as return {}.
Definition error.hpp:181
Result< reference_database_file > gather_sketches(reference_database_file const &mine, genome_slice const &slice, MPI_Comm comm)
Gathers every rank's sketches of its genome_slice into one file on every rank.
Result< std::vector< std::filesystem::path > > list_genomes(std::filesystem::path const &input)
Paths from a directory (see genome_paths) or from a text file with one path per line,...
cuda::std::unexpected< Error > Err(Error error)
Failure return; converts to any Result<T> via cuda::std::unexpected.
Definition error.hpp:176
unsigned default_parser_workers()
Loaders a path build runs unless the caller asks for another count.
Result< void > search_all_to_all(replicated_database< K, BucketCount, Layout > &group, OnTile &&on_tile, MPI_Comm comm)
Compares every pair of group's references, sharing the tiles among the ranks of comm; collective.
Result< genome_slice > slice_genomes(std::filesystem::path const &input, MPI_Comm comm, unsigned workers=default_parser_workers())
Lists a genome collection once and splits it into one contiguous slice per rank of comm,...
static Error invalid_argument(std::string message)
Definition error.hpp:99
static Error resource(std::string message)
Definition error.hpp:103
Fallible API result: cuda::std::expected<T, Error> with cuDDL factories.
Definition error.hpp:118
static Result ok(T value)
Definition error.hpp:121
Builds a reference_database_file from gathered parts; the file's members are private.
static reference_database_file make(reference_database_file const &like, std::vector< std::filesystem::path > const &paths, std::vector< uint16_t > rows)
One rank's share of a genome collection split across an MPI communicator.
std::vector< std::filesystem::path > paths
Every genome of the collection in reference-ID order; the same on every rank.
size_t first
This rank's genomes are paths[first, last).
std::vector< uint64_t > bytes
Size of each genome's file in bytes.
std::span< std::filesystem::path const > mine() const noexcept
How a replicated_database search splits and delivers its tiles.
all_to_all_shard shard
This process's part of the pairs, when several processes share the search.
Matching-bucket threshold, independent of whether an index accelerates the search.