77 template <u
int32_t K,
size_t BucketCount,
typename Layout>
81 std::filesystem::path
const& path,
82 cuda::stream_ref stream
87 auto database =
CUDDL_TRY(snapshot(source, stream));
90 download(index.index_offsets_.data(), index.index_offsets_.size(), stream)
95 CUDDL_TRY(download(index.index_postings_.data(), posting_count, stream));
97 CUDDL_TRY(download(index.index_keys_.data(), index.index_keys_.size(), stream));
100 writer.
temporary = path.string() +
".tmp.XXXXXX";
101 auto fd = ::mkstemp(writer.
temporary.data());
105 writer.
output = ::fdopen(fd,
"wb");
118 if constexpr (std::endian::native == std::endian::little) {
119 CUDDL_TRY(writer.
bytes(keys.data(), keys.size() *
sizeof(uint16_t)));
121 for (
auto key : keys) {
126 if (std::fclose(std::exchange(writer.
output,
nullptr)) != 0) {
129 std::filesystem::rename(writer.
temporary, path);
133 }
catch (cuda::cuda_error
const& error) {
134 return Err(
Error::cuda(
static_cast<cudaError_t
>(error.status())));
135 }
catch (std::system_error
const& error) {
137 }
catch (std::bad_alloc
const& error) {
143 template <u
int32_t K,
size_t BucketCount,
typename Layout = default_register_layout>
145 std::filesystem::path
const& path,
147 cuda::stream_ref stream
150 auto source =
CUDDL_TRY(snapshot(database, stream));
151 auto decoded =
CUDDL_TRY(read(path, std::move(source)));
154 cuda::make_device_buffer<uint32_t>(stream, stream.device(), decoded.offsets_)
157 cuda::make_device_buffer<uint16_t>(stream, stream.device(), decoded.keys_)
163 cuda::make_device_buffer<uint32_t>(
164 stream, stream.device(), index.index_posting_capacity_, cuda::no_init
167 if (!decoded.postings_.empty()) {
171 cuda::std::span{decoded.postings_.data(), decoded.postings_.size()},
172 cuda::std::span{index.index_postings_.data(), decoded.postings_.size()}
183 index.indexed_ =
true;
184 index.pair_work_ = decoded.pair_work_;
188 index.pair_fraction_ = reference_count == 0U || indexed_buckets == 0U
190 :
static_cast<double>(index.pair_work_) /
191 (
static_cast<double>(reference_count) *
192 reference_count * indexed_buckets);
194 CUDDL_TRY(index.build_posting_bitmaps(database, stream));
199 template <u
int32_t K,
size_t BucketCount,
typename Layout>
202 CUDDL_TRY((detail::validate_non_indexed_score_compatibility<K, BucketCount, Layout>(
206 database.metadata_ = source.
metadata();
207 database.names_.assign(source.
names().begin(), source.
names().end());
212 cuda::make_device_buffer<uint16_t>(
220 database.rows_ =
CUDDL_TRY(download(scores.data(), scores.size(), stream));
224 reference_index_file() =
delete;
226 struct decoded_index {
227 reference_database_file database_;
229 unsigned long long pair_work_{};
230 std::vector<uint32_t> offsets_{};
231 std::vector<uint32_t> postings_{};
232 std::vector<uint16_t> keys_{};
235 [[nodiscard]]
static Result<decoded_index>
236 read(std::filesystem::path
const& path, reference_database_file database)
try {
237 return [&]() -> Result<decoded_index> {
238 detail::database_file_reader reader{
239 std::ifstream(path, std::ios::binary | std::ios::ate)
241 auto size = reader.input.tellg();
245 reader.remaining =
static_cast<uint64_t
>(size);
246 reader.input.seekg(0);
248 CUDDL_TRY(reader.bytes(magic,
sizeof(magic)));
249 if (std::string_view(magic, 8) != std::string_view(
"CUDDLIX\0", 8)) {
252 uint32_t version{}, kind{};
255 if (version != 1 || kind > 1) {
260 auto const source = database.
metadata();
261 if (database.rows().size() !=
262 static_cast<uint64_t
>(source.
reference_count) * source.compatibility.bucket_count) {
274 unsigned long long pair_work{};
276 auto const metadata = database.metadata();
277 auto const capacity =
279 auto const offset_count =
281 auto const key_count = kind == 1 ? capacity : 0;
282 if (capacity > std::numeric_limits<uint32_t>::max() || count > capacity ||
283 (kind == 1 && count != capacity) ||
284 offset_count > std::numeric_limits<size_t>::max() /
sizeof(uint32_t) ||
285 capacity > std::numeric_limits<size_t>::max() /
sizeof(uint32_t) ||
286 reader.remaining != (offset_count + count) *
sizeof(uint32_t) +
287 key_count *
sizeof(uint16_t) +
sizeof(uint32_t)) {
290 decoded_index result{
293 result.pair_work_ = pair_work;
294 result.offsets_.resize(
static_cast<size_t>(offset_count));
295 result.postings_.resize(
static_cast<size_t>(count));
296 result.keys_.resize(
static_cast<size_t>(key_count));
297 CUDDL_TRY(reader.words(result.offsets_));
298 CUDDL_TRY(reader.words(result.postings_));
299 CUDDL_TRY(reader.bytes(result.keys_.data(), result.keys_.size() *
sizeof(uint16_t)));
300 if constexpr (std::endian::native != std::endian::little) {
301 for (
auto& key : result.keys_) {
305 auto expected =
static_cast<uint32_t
>(reader.checksum);
308 if (checksum != expected) {
314 }
catch (std::bad_alloc
const& error) {
318 template <
typename T>
319 static Result<std::vector<T>> download(T
const* data,
size_t size, cuda::stream_ref stream) {
320 std::vector<T>
host(size);
324 stream, cuda::std::span{data, size}, cuda::std::span{
host.data(),
host.size()}
334 static Result<void> validate_payload(decoded_index
const& index) {
335 auto const& offsets_ = index.offsets_;
336 auto const& postings_ = index.postings_;
337 auto const& keys_ = index.keys_;
338 auto const m = index.database_.metadata();
339 auto const& c = m.compatibility;
340 auto const rows = index.database_.rows();
342 auto const key_count =
static_cast<size_t>(c.key_mask) + 1;
344 if (offsets_.front() != 0 || offsets_.back() != postings_.size()) {
350 auto const check_bucket =
351 [&](uint32_t bucket, std::vector<uint32_t>& seen,
size_t& indexed) ->
char const* {
352 auto const score_of = [&](uint32_t id) {
353 return rows[
static_cast<size_t>(id) * c.bucket_count + bucket];
356 auto start =
static_cast<size_t>(bucket) * m.reference_count;
357 for (uint32_t i = 0; i < m.reference_count; ++i) {
358 auto pos = start + i;
359 auto id = postings_[pos];
360 if (
id >= m.reference_count || seen[
id] == bucket ||
361 (i != 0 && keys_[pos] < keys_[pos - 1]) ||
362 (i != 0 && keys_[pos] == keys_[pos - 1] &&
id <= postings_[pos - 1])) {
363 return "invalid sparse index postings";
366 auto score = score_of(
id);
368 score == 0 || c.key_mask == 0xffffU ?
score : (
score & c.key_mask) + 1U;
369 if (keys_[pos] != key)
return "sparse index key does not match reference";
373 for (uint32_t
id = 0;
id < m.reference_count; ++id) indexed += score_of(
id) != 0;
374 for (
size_t key = 0; key <
key_count; ++key) {
375 auto cell =
static_cast<size_t>(bucket) * key_count + key;
376 auto begin = offsets_[cell], end = offsets_[cell + 1];
377 if (begin > end || end > postings_.size())
return "invalid dense index offsets";
378 for (
auto pos = begin; pos < end; ++pos) {
379 auto id = postings_[pos];
380 if (
id >= m.reference_count || seen[
id] == bucket ||
381 (pos != begin &&
id <= postings_[pos - 1])) {
382 return "invalid dense index postings";
385 auto score = score_of(
id);
386 if (score == 0 || (score & c.key_mask) != key) {
387 return "dense index key does not match reference";
393 auto const workers = std::max(
394 1U, std::min<unsigned>(std::thread::hardware_concurrency(), c.indexed_bucket_count)
396 std::atomic<uint32_t> next{0};
397 std::atomic<size_t> indexed{0};
398 std::atomic<char const*> failure{
nullptr};
400 std::vector<std::jthread> pool;
401 for (
unsigned worker = 0; worker < workers; ++worker) {
402 pool.emplace_back([&] {
403 std::vector<uint32_t> seen(
404 m.reference_count, std::numeric_limits<uint32_t>::max()
407 for (uint32_t bucket; failure.load(std::memory_order_relaxed) ==
nullptr &&
408 (bucket = next++) < c.indexed_bucket_count;) {
409 if (
auto const error = check_bucket(bucket, seen, local)) {
410 char const* none =
nullptr;
411 failure.compare_exchange_strong(none, error);
419 if (!
sparse && indexed != postings_.size()) {