Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 126 additions & 17 deletions include/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

#pragma once

#include <charconv>
#include <errno.h>
#include <filesystem>
#include <system_error>

#include "common_includes.h"

Expand Down Expand Up @@ -182,11 +185,69 @@ inline void convert_labels_string_to_int(const std::string &inFileName, const st
const std::string &mapFileName, const std::string &unv_label,
uint32_t& unv_label_id)
{
// Optimized version of the original per-line stdio implementation.
//
// Hotspots eliminated:
// 1. `label_writer << ... << std::endl` (88M flushes → ~1700s):
// replaced with a batched writer that flushes in 32 MB chunks.
// 2. `label_writer << lbls[j]` per int (stdio format): replaced with
// locale-independent std::to_chars conversion.
//
// Byte-for-byte output preservation:
// - Label IDs are assigned in the same "first seen" order, so the
// unordered_map contents are identical to the original.
// - Row order in the output file matches the input, same as original.
// - Every row is terminated by std::endl on Windows the ofstream would
// translate '\n' to "\r\n" when we used `<<`; the optimized writer
// uses binary mode and emits CRLF
// explicitly on Windows to keep the output bytes identical.
// - The map file is written from an unordered_map, whose iteration
// order was already unspecified in the original code; we keep the
// same iteration, so semantically both are equivalent for
// downstream load_label_map (which does not rely on order).

std::unordered_map<std::string, uint32_t> string_int_map;
std::ofstream label_writer(outFileName);

// ---- Writer for the formatted labels file ----
std::ofstream label_writer(outFileName, std::ios::binary);
if (!label_writer.is_open())
{
throw diskann::ANNException(std::string("Failed to open ") + outFileName, -1);
}

// Bump the stdio buffer as a safety net in case any write bypasses our batch.
constexpr size_t kStdioBufSize = 16 * 1024 * 1024; // 16 MB
std::vector<char> stdio_buf(kStdioBufSize);
label_writer.rdbuf()->pubsetbuf(stdio_buf.data(), stdio_buf.size());

constexpr size_t kBatchSize = 32 * 1024 * 1024; // 32 MB
std::vector<char> batch;
batch.reserve(kBatchSize + 4096); // small slack for line-worst-case

auto flush_batch = [&]() {
if (!batch.empty())
{
label_writer.write(batch.data(), batch.size());
batch.clear();
}
};

auto append_uint32 = [](std::vector<char> &dst, uint32_t v) {
char buffer[10]; // uint32 max: 10 digits
const auto result = std::to_chars(buffer, buffer + sizeof(buffer), v);
if (result.ec != std::errc())
{
throw diskann::ANNException("Failed to format label ID", -1);
}
dst.insert(dst.end(), buffer, result.ptr);
};

std::ifstream label_reader(inFileName);
//if (unv_label != "")
// string_int_map[unv_label] = 0;
if (!label_reader.is_open())
{
throw diskann::ANNException(std::string("Failed to open ") + inFileName, -1);
}

std::string line, token;
while (std::getline(label_reader, line))
{
Expand All @@ -208,14 +269,28 @@ inline void convert_labels_string_to_int(const std::string &inFileName, const st
std::cout << "No label found";
exit(-1);
}

// Emit "id1,id2,...,idN\n" (with CRLF on Windows to preserve byte
// equivalence with the original text-mode ofstream + std::endl).
for (size_t j = 0; j < lbls.size(); j++)
{
if (j != lbls.size() - 1)
label_writer << lbls[j] << ",";
else
label_writer << lbls[j] << std::endl;
append_uint32(batch, lbls[j]);
if (j + 1 < lbls.size())
{
batch.push_back(',');
}
}
#ifdef _WIN32
batch.push_back('\r');
#endif
batch.push_back('\n');

if (batch.size() >= kBatchSize)
{
flush_batch();
}
}
flush_batch();
label_writer.close();

if (unv_label != "")
Expand All @@ -228,8 +303,13 @@ inline void convert_labels_string_to_int(const std::string &inFileName, const st
// else: unv_label_id remains 0, indicating label not found
}

// ---- Writer for the map file. 235 or so entries — tiny, keep simple. ----
std::ofstream map_writer(mapFileName);
for (auto mp : string_int_map)
if (!map_writer.is_open())
{
throw diskann::ANNException(std::string("Failed to open ") + mapFileName, -1);
}
for (auto &mp : string_int_map)
{
map_writer << mp.first << "\t" << mp.second << std::endl;
}
Expand Down Expand Up @@ -671,16 +751,45 @@ inline void load_bin(MemoryMappedFiles &files, const std::string &bin_file, std:

inline void copy_file(std::string in_file, std::string out_file)
{
std::ifstream source(in_file, std::ios::binary);
std::ofstream dest(out_file, std::ios::binary);

std::istreambuf_iterator<char> begin_source(source);
std::istreambuf_iterator<char> end_source;
std::ostreambuf_iterator<char> begin_dest(dest);
std::copy(begin_source, end_source, begin_dest);
// OS-level fast copy. Replaces the previous istreambuf_iterator
// per-character copy which was ~1 MB/s on 200 MB label files.
// std::filesystem::copy_file uses the OS's fast copy primitive
// (CopyFileW on Windows, copy_file_range/sendfile on Linux).
std::filesystem::copy_file(
in_file, out_file,
std::filesystem::copy_options::overwrite_existing);
}

// move_file — rename-based fast move with copy+remove fallback.
//
// Use this instead of `copy_file(src, dst); std::remove(src);` when the
// caller's intent is to *move* the file to a new location. In practice
// this is O(1) on the same filesystem (~0 ms) versus ~1 s per 200 MB
// for the copy path.
//
// Falls back to copy+remove if rename fails, which can happen for:
// - cross-filesystem/cross-device moves (rename is filesystem-local)
// - the source being held open by another process (antivirus, etc.)
// - unusual permission scenarios
//
// If the target already exists, it is removed first (best-effort) so
// that rename can succeed on platforms where rename refuses to overwrite.
inline void move_file(std::string in_file, std::string out_file)
{
std::error_code ec;
std::filesystem::remove(out_file, ec); // best-effort clear target; ignore errors
ec.clear();
std::filesystem::rename(in_file, out_file, ec);
if (!ec)
{
return; // fast path succeeded
}

source.close();
dest.close();
// Fallback: copy + remove.
std::filesystem::copy_file(
in_file, out_file,
std::filesystem::copy_options::overwrite_existing);
std::filesystem::remove(in_file, ec); // best-effort; ignore errors
}

DISKANN_DLLEXPORT double calculate_recall(unsigned num_queries, unsigned *gold_std, float *gs_dist, unsigned dim_gs,
Expand Down
37 changes: 20 additions & 17 deletions src/disk_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -698,12 +698,11 @@ int build_merged_vamana_index(std::string base_file, diskann::Metric compareMetr

if (use_filters)
{
// need to copy the labels_to_medoids file to the specified input
// need to move the labels_to_medoids file to the specified input
// file
std::remove(labels_to_medoids_file.c_str());
std::string mem_labels_to_medoid_file = mem_index_path + "_labels_to_medoids.txt";
copy_file(mem_labels_to_medoid_file, labels_to_medoids_file);
std::remove(mem_labels_to_medoid_file.c_str());
move_file(mem_labels_to_medoid_file, labels_to_medoids_file);
}

std::remove(medoids_file.c_str());
Expand Down Expand Up @@ -1396,54 +1395,58 @@ int build_disk_index(const char *dataFilePath, const char *indexFilePath, const
gen_random_slice<T>(data_file_to_use.c_str(), sample_base_prefix, sample_sampling_rate);
if (use_filters)
{
copy_file(labels_file_to_use, disk_labels_file);
// Move the formatted labels file into its final disk-index location.
// The original code did copy_file + std::remove of a *different*
// mem_labels_file (the one Index::save wrote separately). Keep the
// remove of mem_labels_file (that file is redundant with
// labels_file_to_use in the static index case) and move
// labels_file_to_use into place.
move_file(labels_file_to_use, disk_labels_file);
std::remove(mem_labels_file.c_str());
if (universal_label != "" && universal_label_id != 0)
{
copy_file(mem_univ_label_file, disk_univ_label_file);
std::remove(mem_univ_label_file.c_str());
move_file(mem_univ_label_file, disk_univ_label_file);
}
// rename bimask label file
std::string bitmask_label_file = std::string(mem_index_path) + "_bitmask_labels.bin";
if (file_exists(bitmask_label_file))
{
copy_file(bitmask_label_file, disk_bitmask_labels_file);
std::remove(bitmask_label_file.c_str());
move_file(bitmask_label_file, disk_bitmask_labels_file);
}

// rename integer label file
std::string integer_label_file = std::string(mem_index_path) + "_integer_labels.bin";
if (file_exists(integer_label_file))
{
copy_file(integer_label_file, disk_integer_labels_file);
std::remove(integer_label_file.c_str());
move_file(integer_label_file, disk_integer_labels_file);
}

std::remove(augmented_data_file.c_str());
std::remove(augmented_labels_file.c_str());
// labels_file_to_use was already moved to disk_labels_file above;
// the remove below is a no-op if that succeeded, and cleans up
// the copy-fallback case where move_file left the source behind
// because of a permission or cross-filesystem failure.
std::remove(labels_file_to_use.c_str());
}

std::string old_seller_mem_file = std::string(mem_index_path) + "_sellers.txt";
if (file_exists(old_seller_mem_file))
{
copy_file(old_seller_mem_file, old_disk_seller_file);
std::remove(old_seller_mem_file.c_str());
move_file(old_seller_mem_file, old_disk_seller_file);
}

std::string seller_mem_file = std::string(mem_index_path) + "_sellers.bin";
if (file_exists(seller_mem_file))
{
copy_file(seller_mem_file, disk_seller_file);
std::remove(seller_mem_file.c_str());
move_file(seller_mem_file, disk_seller_file);
}

if (created_temp_file_for_processed_data)
std::remove(prepped_base.c_str());
std::remove(mem_index_path.c_str());
if (use_disk_pq)
std::remove(disk_pq_compressed_vectors_path.c_str());

auto e = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = e - s;
diskann::cout << "Indexing time: " << diff.count() << std::endl;
Expand Down
61 changes: 57 additions & 4 deletions src/in_mem_graph_store.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
#include "in_mem_graph_store.h"
#include "utils.h"

#include <cstring>
#include <vector>

namespace diskann
{

Expand Down Expand Up @@ -204,32 +207,82 @@ std::tuple<uint32_t, uint32_t, size_t> InMemGraphStore::load_impl(const std::str
int InMemGraphStore::save_graph(const std::string &index_path_prefix, const size_t num_points,
const size_t num_frozen_points, const uint32_t start)
{
// Save graph to disk in batches to avoid the massive syscall overhead of
// the original per-node ofstream::write() calls.
//
// Original: 2 writes/node * ~88M nodes = ~176M syscalls,
// observed throughput ~11 MB/s on 18.7 GB output.
// New: Serialize into 32 MB batches; ~600 syscalls total.
//
// Byte-for-byte equivalent to the original layout:
// header (24B): [index_size u64][max_observed_degree u32][ep u32][num_frozen u64]
// per node: [GK u32][neighbors[GK] u32]
// header is back-patched at the end with the true index_size + max_degree.

std::ofstream out;
open_file_to_write(out, index_path_prefix);

// Bump the stdio buffer as a safety net in case any write bypasses our batch.
constexpr size_t kStdioBufSize = 16 * 1024 * 1024; // 16 MB
std::vector<char> stdio_buf(kStdioBufSize);
out.rdbuf()->pubsetbuf(stdio_buf.data(), stdio_buf.size());

size_t file_offset = 0;
out.seekp(file_offset, out.beg);
size_t index_size = 24;
uint32_t max_degree = 0;

// Placeholder header (overwritten at the end).
out.write((char *)&index_size, sizeof(uint64_t));
out.write((char *)&_max_observed_degree, sizeof(uint32_t));
uint32_t ep_u32 = start;
out.write((char *)&ep_u32, sizeof(uint32_t));
out.write((char *)&num_frozen_points, sizeof(size_t));

// Note: num_points = _nd + _num_frozen_points
// Batched write: accumulate 32 MB of node data before hitting the disk.
constexpr size_t kBatchSize = 32 * 1024 * 1024; // 32 MB
// Worst-case node bytes: (1 header uint32 + max_degree neighbors) * 4 B.
// Use a generous cap so we never overshoot the batch by too much.
const size_t kMaxNodeBytes = (static_cast<size_t>(_max_observed_degree) + 4) * sizeof(uint32_t);
std::vector<char> batch;
batch.reserve(kBatchSize + kMaxNodeBytes);

auto flush_batch = [&]() {
if (!batch.empty())
{
out.write(batch.data(), batch.size());
batch.clear();
}
};

for (uint32_t i = 0; i < num_points; i++)
{
uint32_t GK = (uint32_t)_graph[i].size();
out.write((char *)&GK, sizeof(uint32_t));
out.write((char *)_graph[i].data(), GK * sizeof(uint32_t));
size_t node_bytes = sizeof(uint32_t) * (1 + GK);

// Append GK followed by the neighbor array to the batch buffer.
size_t old_size = batch.size();
batch.resize(old_size + node_bytes);
std::memcpy(batch.data() + old_size, &GK, sizeof(uint32_t));
std::memcpy(batch.data() + old_size + sizeof(uint32_t),
_graph[i].data(), GK * sizeof(uint32_t));

max_degree = _graph[i].size() > max_degree ? (uint32_t)_graph[i].size() : max_degree;
index_size += (size_t)(sizeof(uint32_t) * (GK + 1));
index_size += node_bytes;

if (batch.size() >= kBatchSize)
{
flush_batch();
}
}
flush_batch();

// Back-patch the header with the actual index size and max degree.
out.seekp(file_offset, out.beg);
out.write((char *)&index_size, sizeof(uint64_t));
out.write((char *)&max_degree, sizeof(uint32_t));
out.close();

return (int)index_size;
}

Expand Down
Loading