Menyimpan seluruh vector embedding ke dalam RAM menjadi tidak realistis saat dataset tumbuh melampaui ratusan gigabyte. Disk-backed vector store menjadi solusi wajib, namun pola traversal indeks seperti HNSW (Hierarchical Navigable Small World) dan IVFFlat menghasilkan pola akses disk acak (random reads) berlatensi tinggi. Tanpa strategi I/O yang tepat, throughput query anjlok akibat I/O wait pada NVMe SSD.

Pola memory-mapped storage (mmap) memanfaatkan kernel virtual memory manager (VMM) untuk memetakan file indeks langsung ke address space proses tanpa alokasi buffer duplikat di user space. Artikel ini mengupas mekanisme mmap, perbandingannya dengan Direct I/O, analisis page cache eviction, dan tuning kernel Linux untuk query vector berlatensi rendah.

Karakteristik I/O Traversal Indeks HNSW vs IVFFlat

Pola akses I/O sangat bergantung pada topologi indeks vector:

  • HNSW: Menggunakan struktur multi-layer graph. Query traversal melompat antar node tetangga berdasarkan jarak kosinus atau L2. Karakteristik aksesnya adalah pointer chasing acak (small random reads 64B-4KB). Traversal ini menyebabkan read amplification tinggi jika page size OS (default 4KB) harus dimuat hanya untuk membaca satu node vector kecil.
  • IVFFlat: Membagi vector space ke dalam centroid cluster (voronoi cells). Traversal hanya membaca daftar centroid, kemudian memindai cluster terpilih secara berurutan. Karakteristik aksesnya adalah sequential-block reads yang jauh lebih ramah terhadap buffer cache dan read-ahead OS.

Implementasi: mmap vs Direct I/O (O_DIRECT)

Terdapat dua pola utama untuk membaca indeks dari persistent storage: mmap dan Direct I/O via O_DIRECT flag.

1. Pola Memory-Mapped Storage (mmap)

System call mmap memetakan file descriptor ke memory address space. OS mengeksekusi fault-driven paging: halaman disk hanya di-load via page fault saat memori tersebut diakses.

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>

struct VectorNode {
    float embedding[128];
    uint32_t neighbors[32];
};

int main() {
    int fd = open("hnsw_index.bin", O_RDONLY);
    struct stat sb;
    fstat(fd, &sb);

    // Petakan indeks file langsung ke pointer memory
    void* addr = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        close(fd);
        return 1;
    }

    // Beritahu kernel pola aksesnya: random jumping
    madvise(addr, sb.st_size, MADV_RANDOM);

    VectorNode* nodes = static_cast<VectorNode*>(addr);
    
    // Akses node secara instan via memory pointer
    float first_val = nodes[42].embedding[0];
    (void)first_val;

    munmap(addr, sb.st_size);
    close(fd);
    return 0;
}

Keunggulan: Zero-copy antara kernel page cache dan aplikasi. Data dapat di-share antar proses tanpa overhead serialisasi.

Kelemahan: Hitting missing pages memicu synchronous soft/hard page faults yang memblokir thread eksekusi.

2. Pola Direct I/O (O_DIRECT)

Direct I/O melewati Linux page cache sepenuhnya. Buffer harus dialokasikan terikat batas memori (memory-aligned, biasanya 4KB atau 512B) dan pembacaan disk dilakukan via asynchronous interfaces seperti io_uring.

Gunakan Direct I/O jika vector store Anda mengimplementasikan software-level cache sendiri (seperti RocksDB/Block Cache kustom) dan Anda ingin menghindari double-buffering atau overhead lock pada page cache Linux.

Analisis Page Cache Eviction dan Thrashing

Ketika ukuran file indeks melampaui kapasitas RAM, Linux kernel menjalankan page replacement algorithm (Two-list active/inactive LRU). Masalah yang sering terjadi:

  • Cache Thrashing: Query acak pada graf HNSW menarik page baru terus-menerus, memicu disk read simultan dan membuang page yang sebenarnya masih aktif digunakan oleh layer graf atas (upper entry points).
  • Mitigasi Tingkat Aplikasi: Pin entry points dan layer atas HNSW langsung di RAM permanen. Hanya biarkan layer dasar (Layer 0 yang memakan 80%+ ukuran indeks) berada di bawah manajemen mmap.

Tuning Linux Read-Ahead Buffer

Secara default, Linux membaca beberapa sektor disk tambahan di depan sektor yang diminta (read-ahead) untuk memaksimalkan throughput sekuensial. Pada random graph traversal HNSW, ini kontraproduktif karena data tambahan tidak pernah dibaca, membuang bandwidth NVMe.

1. Nonaktifkan Read-Ahead per Block Device

Periksa ukuran read-ahead pada block device penyimpanan vector (misal /dev/nvme0n1):

# Cek ukuran read-ahead saat ini (dalam sektor 512-byte)
blockdev --getra /dev/nvme0n1

# Set read-ahead ke 0 atau 16 sektor (8KB) untuk mengurangi disk I/O waste
sudo blockdev --setra 16 /dev/nvme0n1

2. Gunakan madvise() pada Buffer Indeks

Gunakan madvise(addr, len, MADV_RANDOM) untuk menonaktifkan read-ahead khusus untuk region memori indeks tersebut tanpa memengaruhi konfigurasi global sistem operasi.

Perbandingan Arsitektur: pgvector vs File-Backed Mmap Store

Pilihan arsitektur penyimpanan vector bergantung pada throughput dan konkurensi:

pgvector (Relational Database-Integrated)

pgvector berjalan di atas PostgreSQL shared buffers. Ketika node indeks tidak ada di shared buffer, engine membaca block via page cache OS (double buffering latency).

-- Setup pgvector HNSW dengan parameter memori optimal
SET work_mem = '2GB';
SET maintenance_work_mem = '8GB';

CREATE INDEX ON documents 
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- Eksekusi query k-NN
EXPLAIN ANALYZE
SELECT id, content
FROM documents
ORDER BY embedding <=> '[0.021, -0.043, ...]' 
LIMIT 10;

File-Backed Mmap Store (e.g., Memory-Mapped HNSW / Faiss)

Langsung membaca representasi biner indeks dari flash memory ke virtual address space tanpa perantara buffer engine SQL.

  • Throughput: File-backed mmap lebih unggul 2x hingga 5x pada throughput query murni karena overhead per-row SQL, lock shared buffers, dan overhead MVCC PostgreSQL dieliminasi.
  • Kapan Memilih pgvector: Jika vector similarity wajib digabungkan dengan relational filter (metadata filtering dinamis, ACID compliance, transaksi atomik).
  • Kapan Memilih File-Backed Mmap: Jika vector search terisolasi sebagai standalone microservice pencarian, latency latency target di bawah 10ms, dan dataset berukuran ratusan juta vector.

Rekomendasi Arsitektur: Simpan embedding dan layer 0 HNSW di flash storage via mmap dengan read-ahead minimal, lalu simulasikan pinning layer 1 hingga layer teratas di heap memory aplikasi untuk meminimalkan I/O wait pada langkah awal traversal.