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));
128 if (options.streams == 0) {
133 file.state_ = std::make_unique<state>(path, options);
134 }
catch (std::exception
const& error) {
144 return state_->write(tile, stream);
148 return write(tile.tile, tile.stream);
153 return state_->write(rows);
159 return state_->finish();
163 std::lock_guard lock(state_->mutex);
164 return state_->statistics;
168 using clock = std::chrono::steady_clock;
169 using pinned_floats =
170 cuda::buffer<float, cuda::mr::host_accessible, cuda::mr::device_accessible>;
174 std::optional<host_batch_results> host;
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;
187 slot* tile =
nullptr;
188 std::vector<batch_search_result> rows;
191 static double seconds_since(clock::time_point start) {
192 return std::chrono::duration<double>(clock::now() - start).count();
196 state(std::filesystem::path
const& path, result_file_options
const& options)
197 : format(options.format),
199 options.threads != 0 ? options.threads
200 : std::max(1U, std::thread::hardware_concurrency())
203 auto const paths =
files(path, options.streams);
204 for (
auto const& file : paths) {
206 std::make_unique<detail::file_writer>(
207 file, options.durable, (
size_t{1} << 30) / paths.size()
211 thread = std::thread([
this] { run(); });
213 state(state
const&) =
delete;
214 state& operator=(state
const&) =
delete;
219 Result<void> write(batch_result_tile
const& tile, cuda::stream_ref stream) {
220 slot* free =
nullptr;
222 std::unique_lock lock(mutex);
224 auto& [pair, next] =
slots[stream.get()];
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));
232 auto const start = clock::now();
235 std::lock_guard lock(mutex);
236 statistics.download_seconds += seconds_since(start);
238 queue.push_back({free, {}});
244 Result<void> write(std::span<batch_search_result const> rows) {
246 std::lock_guard lock(mutex);
247 if (failure)
return Err(error_of(failure));
249 queue.push_back({
nullptr, {
rows.begin(),
rows.end()}});
255 Result<void> finish() {
256 auto const start = clock::now();
259 if (failure)
return Err(error_of(failure));
261 for (
auto& file : streams) file->finish();
262 }
catch (std::exception
const& error) {
265 std::lock_guard lock(mutex);
266 statistics.stall_seconds += seconds_since(start);
273 download(batch_result_tile
const& tile, cuda::stream_ref stream, slot& into)
const {
275 if (!into.converted) {
280 auto const pairs =
static_cast<size_t>(
tile.slot_count());
281 if (!into.device || into.device->size() <
pairs) {
284 cuda::make_device_buffer<float>(
285 stream, stream.device(), pairs, cuda::no_init
300 cub::DeviceTransform::Transform(
304 detail::packed_identity{tile.bucket_count},
311 cuda::std::span{into.device->data(), pairs},
312 cuda::std::span{into.floats->data(), pairs}
317 into.first_query_id =
tile.first_query_id;
318 into.query_count =
tile.query_count;
323 static Error error_of(std::exception_ptr
const& failure) {
325 std::rethrow_exception(failure);
326 }
catch (std::exception
const& error) {
335 std::lock_guard lock(mutex);
339 if (thread.joinable()) thread.join();
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());
353 if (next.tile !=
nullptr) {
354 write_tile(*next.tile);
356 write_rows(next.rows);
359 std::lock_guard lock(mutex);
360 failure = std::current_exception();
361 written.notify_all();
364 if (next.tile !=
nullptr) {
365 std::lock_guard lock(mutex);
366 next.tile->queued =
false;
368 written.notify_all();
372 detail::file_writer& next_stream() {
373 return *streams[next_file++ % streams.size()];
376 void count(uint64_t pairs, uint64_t bytes) {
377 std::lock_guard lock(mutex);
378 statistics.pairs +=
pairs;
379 statistics.bytes += bytes;
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));
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;
395 out.data() +
sizeof(header) + at, source + at, std::min(piece, bytes - at)
404 expand(results, [](batch_search_result
const& result) {
return result; });
406 expand(results, [](batch_search_result
const& result) {
407 return similarity_record{
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&>;
420 if (total == 0)
return;
421 auto& file = next_stream();
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");
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;
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);
448 count(total, total *
sizeof(record));
451 void write_rows(std::vector<batch_search_result>
const& rows) {
452 auto& file = next_stream();
455 {
reinterpret_cast<char const*
>(
rows.data()),
rows.size() *
sizeof(
rows[0])}
460 std::vector<similarity_record> records;
461 records.reserve(
rows.size());
462 for (
auto const& row :
rows) {
468 {
reinterpret_cast<char const*
>(records.data()), records.size() *
sizeof(records[0])}
470 count(records.size(), records.size() *
sizeof(records[0]));
474 std::vector<std::unique_ptr<detail::file_writer>> streams;
475 size_t next_file = 0;
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;
482 std::condition_variable queued, written;
483 std::exception_ptr failure;
484 result_file_statistics statistics;
489 std::unique_ptr<state> state_;