cuDDL
Loading...
Searching...
No Matches
result_file.cuh
Go to the documentation of this file.
1#pragma once
2
5#include <cuddl/error.hpp>
9
10#include <cub/device/device_transform.cuh>
11
12#include <algorithm>
13#include <array>
14#include <chrono>
15#include <condition_variable>
16#include <cstdint>
17#include <cstring>
18#include <deque>
19#include <exception>
20#include <filesystem>
21#include <map>
22#include <memory>
23#include <mutex>
24#include <optional>
25#include <ranges>
26#include <span>
27#include <stdexcept>
28#include <string>
29#include <thread>
30#include <vector>
31
32namespace cuddl {
33
35enum class result_format : uint8_t {
38 counts,
45};
46
49 uint32_t query_id{};
50 uint32_t reference_id{};
51 float similarity{};
52};
53
60 unsigned streams = 1;
62 bool durable = false;
64 unsigned threads = 0;
65};
66
69 uint64_t pairs = 0;
70 uint64_t bytes = 0;
73 double download_seconds = 0;
76 double stall_seconds = 0;
77};
78
79namespace detail {
80
83 uint32_t bucket_count;
84
85 __host__ __device__ float operator()(packed_pairwise_counts packed) const noexcept {
87 }
88};
89
90} // namespace detail
91
113 public:
115 [[nodiscard]] static std::vector<std::filesystem::path>
116 files(std::filesystem::path const& path, unsigned streams) {
117 if (streams <= 1) return {path};
118 std::vector<std::filesystem::path> paths;
119 paths.reserve(streams);
120 for (unsigned s = 0; s < streams; ++s)
121 paths.emplace_back(path.string() + "." + std::to_string(s));
122 return paths;
123 }
124
126 [[nodiscard]] static Result<result_file>
127 open(std::filesystem::path const& path, result_file_options options = {}) {
128 if (options.streams == 0) {
129 return Err(Error::invalid_argument("a result file needs at least one stream"));
130 }
131 result_file file;
132 try {
133 file.state_ = std::make_unique<state>(path, options);
134 } catch (std::exception const& error) {
135 return Err(Error::resource(error.what()));
136 }
137 return file;
138 }
139
143 [[nodiscard]] Result<void> write(batch_result_tile const& tile, cuda::stream_ref stream) {
144 return state_->write(tile, stream);
145 }
146
147 [[nodiscard]] Result<void> write(replica_tile const& tile) {
148 return write(tile.tile, tile.stream);
149 }
150
152 [[nodiscard]] Result<void> write(std::span<batch_search_result const> rows) {
153 return state_->write(rows);
154 }
155
158 [[nodiscard]] Result<void> finish() {
159 return state_->finish();
160 }
161
162 [[nodiscard]] result_file_statistics statistics() const {
163 std::lock_guard lock(state_->mutex);
164 return state_->statistics;
165 }
166
167 private:
168 using clock = std::chrono::steady_clock;
169 using pinned_floats =
170 cuda::buffer<float, cuda::mr::host_accessible, cuda::mr::device_accessible>;
171
173 struct slot {
174 std::optional<host_batch_results> host;
175 // A tile holding every pair, in the similarity format, converted on the GPU.
176 std::optional<cuda::device_buffer<float>> device;
177 std::optional<pinned_floats> floats;
178 bool converted = false;
179 uint32_t first_query_id = 0;
180 uint32_t query_count = 0;
181 uint64_t pairs = 0;
182 bool queued = false;
183 };
184
186 struct job {
187 slot* tile = nullptr;
188 std::vector<batch_search_result> rows;
189 };
190
191 static double seconds_since(clock::time_point start) {
192 return std::chrono::duration<double>(clock::now() - start).count();
193 }
194
195 struct state {
196 state(std::filesystem::path const& path, result_file_options const& options)
197 : format(options.format),
198 threads(
199 options.threads != 0 ? options.threads
200 : std::max(1U, std::thread::hardware_concurrency())
201 ),
202 pool(threads) {
203 auto const paths = files(path, options.streams);
204 for (auto const& file : paths) {
205 streams.push_back(
206 std::make_unique<detail::file_writer>(
207 file, options.durable, (size_t{1} << 30) / paths.size()
208 )
209 );
210 }
211 thread = std::thread([this] { run(); });
212 }
213 state(state const&) = delete;
214 state& operator=(state const&) = delete;
215 ~state() {
216 stop();
217 }
218
219 Result<void> write(batch_result_tile const& tile, cuda::stream_ref stream) {
220 slot* free = nullptr;
221 {
222 std::unique_lock lock(mutex);
223 if (done) return Err(Error::invalid_argument("the result file is finished"));
224 auto& [pair, next] = slots[stream.get()];
225 free = &pair[next];
226 next ^= 1U;
227 auto const waiting = clock::now();
228 written.wait(lock, [&] { return !free->queued || failure; });
229 statistics.stall_seconds += seconds_since(waiting);
230 if (failure) return Err(error_of(failure));
231 }
232 auto const start = clock::now();
233 CUDDL_TRY(download(tile, stream, *free));
234 {
235 std::lock_guard lock(mutex);
236 statistics.download_seconds += seconds_since(start);
237 free->queued = true;
238 queue.push_back({free, {}});
239 }
240 queued.notify_one();
241 return Ok();
242 }
243
244 Result<void> write(std::span<batch_search_result const> rows) {
245 {
246 std::lock_guard lock(mutex);
247 if (failure) return Err(error_of(failure));
248 if (done) return Err(Error::invalid_argument("the result file is finished"));
249 queue.push_back({nullptr, {rows.begin(), rows.end()}});
250 }
251 queued.notify_one();
252 return Ok();
253 }
254
255 Result<void> finish() {
256 auto const start = clock::now();
257 stop();
258 slots.clear();
259 if (failure) return Err(error_of(failure));
260 try {
261 for (auto& file : streams) file->finish();
262 } catch (std::exception const& error) {
263 return Err(Error::resource(error.what()));
264 }
265 std::lock_guard lock(mutex);
266 statistics.stall_seconds += seconds_since(start);
267 return Ok();
268 }
269
272 Result<void>
273 download(batch_result_tile const& tile, cuda::stream_ref stream, slot& into) const {
274 into.converted = format == result_format::similarity && tile.pass_bits == nullptr;
275 if (!into.converted) {
276 if (!into.host) CUDDL_TRY(cuda_try([&] { into.host.emplace(stream); }));
277 return cuddl::download(tile, stream, *into.host);
278 }
279 detail::device_scope const scope{stream};
280 auto const pairs = static_cast<size_t>(tile.slot_count());
281 if (!into.device || into.device->size() < pairs) {
282 CUDDL_TRY(cuda_try([&] {
283 into.device.emplace(
284 cuda::make_device_buffer<float>(
285 stream, stream.device(), pairs, cuda::no_init
286 )
287 );
288 into.floats.emplace(
289 stream,
292 ),
293 pairs,
294 cuda::no_init
295 );
296 }));
297 }
298 if (pairs != 0U) {
300 cub::DeviceTransform::Transform(
301 tile.results,
302 into.device->data(),
303 pairs,
304 detail::packed_identity{tile.bucket_count},
305 stream.get()
306 )
307 );
309 cuda::copy_bytes(
310 stream,
311 cuda::std::span{into.device->data(), pairs},
312 cuda::std::span{into.floats->data(), pairs}
313 )
314 );
315 }
316 CUDDL_CUDA_TRY(stream.sync());
317 into.first_query_id = tile.first_query_id;
318 into.query_count = tile.query_count;
319 into.pairs = pairs;
320 return Ok();
321 }
322
323 static Error error_of(std::exception_ptr const& failure) {
324 try {
325 std::rethrow_exception(failure);
326 } catch (std::exception const& error) {
327 return Error::resource(error.what());
328 } catch (...) {
329 return Error::resource("result file write failed");
330 }
331 }
332
333 void stop() {
334 {
335 std::lock_guard lock(mutex);
336 done = true;
337 }
338 queued.notify_one();
339 if (thread.joinable()) thread.join();
340 }
341
342 void run() {
343 while (true) {
344 job next;
345 {
346 std::unique_lock lock(mutex);
347 queued.wait(lock, [&] { return done || !queue.empty(); });
348 if (queue.empty()) return;
349 next = std::move(queue.front());
350 queue.pop_front();
351 }
352 try {
353 if (next.tile != nullptr) {
354 write_tile(*next.tile);
355 } else {
356 write_rows(next.rows);
357 }
358 } catch (...) {
359 std::lock_guard lock(mutex);
360 failure = std::current_exception();
361 written.notify_all();
362 return;
363 }
364 if (next.tile != nullptr) {
365 std::lock_guard lock(mutex);
366 next.tile->queued = false;
367 }
368 written.notify_all();
369 }
370 }
371
372 detail::file_writer& next_stream() {
373 return *streams[next_file++ % streams.size()];
374 }
375
376 void count(uint64_t pairs, uint64_t bytes) {
377 std::lock_guard lock(mutex);
378 statistics.pairs += pairs;
379 statistics.bytes += bytes;
380 }
381
382 void write_tile(slot const& tile) {
383 if (tile.converted) {
384 auto& file = next_stream();
385 auto const bytes = tile.pairs * sizeof(float);
386 auto const out = file.reserve(2 * sizeof(uint32_t) + bytes);
387 std::array const header{tile.first_query_id, tile.query_count};
388 std::memcpy(out.data(), header.data(), sizeof(header));
389 // A tile can hold gigabytes, more than one core copies in time.
390 constexpr size_t piece = size_t{8} << 20;
391 auto const* source = reinterpret_cast<char const*>(tile.floats->data());
392 pool.for_each((bytes + piece - 1) / piece, [&](size_t p) {
393 auto const at = p * piece;
394 std::memcpy(
395 out.data() + sizeof(header) + at, source + at, std::min(piece, bytes - at)
396 );
397 });
398 file.commit();
399 count(tile.pairs, out.size());
400 return;
401 }
402 auto const results = tile.host->tile();
403 if (format == result_format::counts) {
404 expand(results, [](batch_search_result const& result) { return result; });
405 } else {
406 expand(results, [](batch_search_result const& result) {
407 return similarity_record{
408 result.query_id, result.reference_id, weighted_kmer_identity(result.counts)
409 };
410 });
411 }
412 }
413
416 template <typename Convert>
417 void expand(batch_result_tile const& tile, Convert const& convert) {
418 using record = std::invoke_result_t<Convert, batch_search_result const&>;
419 auto const total = tile.count();
420 if (total == 0) return;
421 auto& file = next_stream();
422 // Every record and header is a whole number of 4-byte words, and extents are
423 // page-aligned, so records start on 4-byte boundaries. Stores through a char pointer
424 // instead made the compiler reload the tile on every record.
425 auto* const records =
426 reinterpret_cast<record*>(file.reserve(total * sizeof(record)).data());
427 if (reinterpret_cast<uintptr_t>(records) % alignof(record) != 0) {
428 throw std::logic_error("result records are misaligned");
429 }
430 // Row runs of about equal result counts, several per thread for upper-triangle tiles.
431 auto const runs = std::min<uint64_t>(uint64_t{threads} * 8U, tile.query_count);
432 bounds.assign(runs + 1U, tile.query_count);
433 auto const rows = std::views::iota(uint32_t{0}, tile.query_count);
434 for (uint64_t r = 0; r < runs; ++r) {
435 bounds[r] = *std::ranges::partition_point(rows, [&](uint32_t query) {
436 return tile.passing_before(query) * runs < total * r;
437 });
438 }
439 pool.for_each(runs, [&](size_t r) {
440 auto* out = records + tile.passing_before(bounds[r]);
441 tile.for_each_passing(
442 bounds[r], bounds[r + 1U], [&](batch_search_result const& result) {
443 *out++ = convert(result);
444 }
445 );
446 });
447 file.commit();
448 count(total, total * sizeof(record));
449 }
450
451 void write_rows(std::vector<batch_search_result> const& rows) {
452 auto& file = next_stream();
453 if (format == result_format::counts) {
454 file.append(
455 {reinterpret_cast<char const*>(rows.data()), rows.size() * sizeof(rows[0])}
456 );
457 count(rows.size(), rows.size() * sizeof(rows[0]));
458 return;
459 }
460 std::vector<similarity_record> records;
461 records.reserve(rows.size());
462 for (auto const& row : rows) {
463 records.push_back(
464 {row.query_id, row.reference_id, weighted_kmer_identity(row.counts)}
465 );
466 }
467 file.append(
468 {reinterpret_cast<char const*>(records.data()), records.size() * sizeof(records[0])}
469 );
470 count(records.size(), records.size() * sizeof(records[0]));
471 }
472
473 result_format format;
474 std::vector<std::unique_ptr<detail::file_writer>> streams;
475 size_t next_file = 0;
476 size_t threads;
477 detail::worker_pool pool;
478 std::vector<uint32_t> bounds;
479 std::map<cudaStream_t, std::pair<std::array<slot, 2>, unsigned>> slots;
480 std::deque<job> queue;
481 std::mutex mutex;
482 std::condition_variable queued, written;
483 std::exception_ptr failure;
484 result_file_statistics statistics;
485 bool done = false;
486 std::thread thread;
487 };
488
489 std::unique_ptr<state> state_;
490};
491
492static_assert(sizeof(batch_search_result) == 24);
493static_assert(sizeof(similarity_record) == 12);
494
495} // namespace cuddl
Writes search results to files while the GPUs search on.
Result< void > write(std::span< batch_search_result const > rows)
Queues results already on the host, such as each genome's top-k neighbours.
Result< void > write(replica_tile const &tile)
static Result< result_file > open(std::filesystem::path const &path, result_file_options options={})
Creates or truncates the files for path.
static std::vector< std::filesystem::path > files(std::filesystem::path const &path, unsigned streams)
The files a result file with streams streams writes for path.
Result< void > write(batch_result_tile const &tile, cuda::stream_ref stream)
Downloads tile, whose results stream orders, and queues it for writing.
result_file_statistics statistics() const
Result< void > finish()
Writes everything queued and closes the files, flushing them to storage first if the file is durable.
#define CUDDL_CUDA_TRY(...)
Propagates a CUDA error wrapped in cuddl::Result<void>.
Definition error.hpp:248
#define CUDDL_TRY(expr)
Propagates a cuddl::Result failure from the enclosing function (GNU statement expression).
Definition error.hpp:225
auto const pairs
Definition kernels.cuh:1592
auto const count
Definition kernels.cuh:1031
@ device_node
The device's host NUMA node, where the system supports NUMA pools: for buffers the device writes,...
__global__ size_t uint32_t const uint32_t const uint32_t uint32_t uint16_t uint32_t tile
Definition kernels.cuh:922
__global__ top_k_slots slots
Definition top_k.cuh:195
auto const total
Definition kernels.cuh:443
__global__ uint32_t const uint32_t const uint32_t const uint32_t uint32_t size_t size_t uint32_t SearchResult * packed
Definition kernels.cuh:1791
__global__ uint32_t const uint32_t uint32_t const uint32_t const SearchResult * results
Definition kernels.cuh:1544
::cuda::__ensure_current_context device_scope
Makes stream's device current for the enclosing scope and restores the previous context on exit.
__global__ size_t uint16_t * rows
Definition kernels.cuh:1499
cuda::pinned_memory_pool_ref device_pinned_pool(cuda::device_ref device, pinned_placement placement)
A page-locked memory pool accessible from device only.
auto const row
Definition top_k.cuh:202
Batch operations over contiguous device rows.
Definition a48.hpp:14
result_format
How a result_file stores each pair.
@ counts
One 24-byte batch_search_result per pair: query ID, reference ID and the lower, equal,...
@ similarity
The pair's weighted_kmer_identity as a 4-byte float, computed on the GPU.
Result< void > Ok() noexcept
Success return for Result<void>; same as return {}.
Definition error.hpp:181
Result< void > download(batch_result_tile const &tile, cuda::stream_ref stream, host_batch_results &host)
Copies a device tile's passing results into host's pinned buffers, growing them only when needed,...
cuda::std::unexpected< Error > Err(Error error)
Failure return; converts to any Result<T> via cuda::std::unexpected.
Definition error.hpp:176
__host__ __device__ constexpr float weighted_kmer_identity(pairwise_counts const &counts) noexcept
Weighted k-mer identity of a pair, equal / max(equal + min(lower, higher), 6), as sketch::wkid comput...
Result< void > cuda_try(cudaError_t error, std::source_location location=std::source_location::current())
Checks a CUDA runtime call and returns an error on failure.
Definition error.hpp:187
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
Non-owning view of one batch-search tile's results.
Weighted k-mer identity of one packed result, for the GPU conversion.
__host__ __device__ float operator()(packed_pairwise_counts packed) const noexcept
One pair's exact bucket comparison counts in 64 bits, the storage and transfer form of batch-search r...
One all-to-all tile as a replicated_database search delivers it.
unsigned streams
Files written in parallel: one writes the path given, more write <path>.0 to <path>.
bool durable
Whether result_file::finish flushes the files to storage before returning.
unsigned threads
Threads that expand results into records; 0 uses every hardware thread.
What a result_file wrote and how long its callers waited for it.
double download_seconds
Seconds callers spent in result_file::write downloading tiles and converting them on the GPU,...
double stall_seconds
Seconds callers waited in result_file::write and result_file::finish for earlier tiles to be written.
One pair of a thresholded search in result_format::similarity.