Skip to content
Merged
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
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@

All notable changes to the KEYSTONE search engine are documented in this file.

## [1.2.0] - 2026-08-28

### Security Fixes (P1/P2)
- **P1: tar.zst parser OOB read** — Replaced unbounded `strtoll()` with a bounded streaming integer parser that respects buffer length. Eliminates out-of-bounds read vulnerability in `.tar.zst` integer parsing.
- **P1: CUDA cache race** — Replaced spinlock-released-before-use pattern with a reader-lease protocol (refcount + generation). Eviction now waits for `readers == 0` before `cudaFree`. Added `keystone_search_batch_cuda_versioned()` with `dataset_version` for in-place host array mutation detection. Added `keystone_cuda_cache_invalidate()`.
- **P2: Archive index min/max** — `first_key`/`last_key` now recomputed from the sorted array after parsing, not trusted from stream order (wrong for unsorted source data).
- **P2: Auto-backend data races** — `g_backend_cache` and `g_last_backend_decision` protected by mutexes; `valid=1` published last after all fields written. Eliminates torn reads on concurrent access.
- **P2: Signed overflow in query-shape classifier** — All key deltas and `max-min` range computed in `__int128`, eliminating UB near `INT64_MIN`/`INT64_MAX`.
- **P2: FNV hash collision verification** — Hash indexer now retains original string bytes and verifies them on every positive hit, eliminating false matches from 64-bit hash collisions.
- **QIHSE ingestion principal** — Bridge carries an authenticated `ingestion_principal` via `keystone_qihse_bridge_set_principal()`. New `keystone_qihse_bridge_dispatch_credential_authenticated()` uses `qihse_kv_set_user()` and refuses writes without a principal, per QIHSE AGENTS.md invariant #1.

### Performance
- **Archive index keys retention** — Sorted keys retained in `tar_zst_index_entry`, eliminating repeat decompression/parsing for positive lookups. Fallback re-streams if keys not retained.
- **LSD radix sort** — Replaced `qsort` with 8-pass LSD radix sort (O(n), sequential memory access) for 64-bit hash keys, carrying offsets/strings/lens.
- **Zero-copy NumPy batch API** — New `keystone_search_keys_batch_auto()` takes raw `int64_t*` keys and `size_t*` results directly from NumPy buffers. Python `search_batch_keys()` skips per-key `_CBatchItem` marshalling. **15.6x faster** on 1M queries (0.151s vs 2.351s).

### Correctness
- **Anchor LRU tracking** — Endpoint anchors now initialize `use_count`/`last_used`; usage-update block no longer guards on `active_table != table`, so caller table anchors get LRU timestamps refreshed.

### Sandy Bridge / AVX1-only CPU Support
- **SSE4.2 SIMD path** — Added branchless 128-bit SIMD path (`_mm_cmpeq_epi64` / PCMPEQQ) to `keystone_chunked_search`, 2x unrolled for Sandy Bridge's dual 128-bit execution ports. Previously AVX1-only CPUs fell through to a scalar loop that couldn't auto-vectorize.
- **Double-precision interpolation** — Replaced `__int128` division (80-100+ cycle libgcc `__divti3` call) with double-precision fast path (~20-40 cycles). `__int128` fallback only for overflow edge cases. **2x faster single-key search** on Sandy Bridge.
- **Software prefetch enabled for SSE4.2** — Prefetch was `#ifdef`'d out on AVX1-only CPUs. Added SSE4.2 branch with Sandy Bridge-tuned distances (32/64 elements vs 64/128).
- **Branchless scalar fallback** — Removed early returns that blocked GCC auto-vectorization.
- **Wider SIMD scan window** — `keystone_local_search` uses 64-element window on SSE4.2+ (was fixed at 32).
- **OpenMP auto-enabled** — Makefile auto-detects compiler OpenMP support and enables `-fopenmp` by default. **2x faster batch search** on 8-core machines.
- **Lowered parallel threshold** — Auto-backend uses OpenMP for batches >= 4096 items (was 16384). Configurable via `KEYSTONE_AUTO_PARALLEL_MIN_ITEMS`.

### Benchmark Results (Sandy Bridge Xeon E5-2407, 2.2GHz, 8-core)
| Metric | Before | After | Speedup |
|--------|--------|-------|---------|
| Single-key search | 317 ns | 157 ns | 2.0x |
| Batch (serial) | 400 ns | 330 ns | 1.2x |
| Batch (auto+OpenMP) | N/A | 165 ns | 2.4x |
| Small-window scan | 125 ns | 82 ns | 1.5x |

## [1.1.0] - Upcoming

### API Changes
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ CC := gcc
CFLAGS := -O3 -march=native -fPIC -Wall -Wextra -Werror=implicit-function-declaration -I./include -DKEYSTONE_ENABLE_PLATFORM_TUNING
LDFLAGS := -lm

# Optional OpenMP
# Optional OpenMP (default: auto-enabled if the compiler supports it,
# since multi-core CPUs benefit from parallel batch search. Set
# KEYSTONE_ENABLE_OPENMP=0 to disable.)
ifeq ($(KEYSTONE_ENABLE_OPENMP),1)
CFLAGS += -fopenmp
LDFLAGS += -fopenmp
else ifneq ($(KEYSTONE_ENABLE_OPENMP),0)
ifeq ($(shell echo | $(CC) -fopenmp -dM -E - 2>/dev/null | grep -q '_OPENMP' && echo yes),yes)
CFLAGS += -fopenmp
LDFLAGS += -fopenmp
endif
endif

# Optional tar.zst streaming support (default: enabled if libarchive + libzstd are available)
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![C](https://img.shields.io/badge/C-11-blue.svg)](https://en.wikipedia.org/wiki/C11_(C_standard_revision))
[![Fortran](https://img.shields.io/badge/Fortran-90%2B-purple.svg)](https://en.wikipedia.org/wiki/Fortran)
[![Python](https://img.shields.io/badge/Python-3-yellow.svg)](https://www.python.org/)
[![SIMD](https://img.shields.io/badge/SIMD-AVX2%20%7C%20AVX--512-black.svg)](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions)
[![SIMD](https://img.shields.io/badge/SIMD-SSE4.2%20%7C%20AVX2%20%7C%20AVX--512-black.svg)](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions)
[![Parallel](https://img.shields.io/badge/Parallel-OpenMP-green.svg)](https://www.openmp.org/)
[![Archives](https://img.shields.io/badge/Ingestion-tar.zst-orange.svg)](https://facebook.github.io/zstd/)
[![Platform](https://img.shields.io/badge/Platform-Linux-success.svg)](https://www.kernel.org/)
Expand All @@ -34,9 +34,10 @@ KEYSTONE is a working native C library and benchmark suite, not just a design no
| Unstructured / Dirty Log Tokenizer | Implemented (zero-allocation email:pass extraction) |
| Heterogeneous Hash Indexer | Implemented (FNV-1a column projection) |
| Native Context Micro-Model | Implemented (6-class DNN with confidence gating) |
| OpenMP batch path | Available when built with OpenMP |
| OpenMP batch path | Auto-enabled by default when compiler supports it |
| Fortran batch backend | Optional; enabled when requested |
| `.tar.zst` archive search | Optional; enabled when `libarchive` and `libzstd` are available |
| SSE4.2 small-window scan | Implemented for native x86 builds with SSE4.2+ (AVX1-only CPUs) |
| AVX2 small-window scan | Implemented for native x86 builds with AVX2 |
| AVX-512 path | Build-gated and hardware-dependent |

Expand Down Expand Up @@ -373,8 +374,8 @@ KEYSTONE is intended for technical users who care about lookup correctness, runt
| **Optional archive support** | `libarchive` and `libzstd` |
| **Optional build detection** | `pkg-config` |
| **Benchmark visualization** | Python 3 with numerical and plotting support |
| **Parallel acceleration** | OpenMP-capable compiler/runtime |
| **Vector acceleration** | AVX2 or AVX-512 capable CPU where available |
| **Parallel acceleration** | OpenMP-capable compiler/runtime (auto-enabled by default) |
| **Vector acceleration** | SSE4.2, AVX2, or AVX-512 capable CPU where available |
| **Future accelerator backends** | GPU or NPU runtime/toolchain only after explicit backend implementation and measurement |

---
Expand All @@ -383,10 +384,10 @@ KEYSTONE is intended for technical users who care about lookup correctness, runt

KEYSTONE is intentionally built as a native, silicon-tuned component. The
default Makefile uses `-O3 -march=native` and enables resident CPU paths such as
AVX2, optional AVX-512, OpenMP, Fortran, and `.tar.zst` support when the local
toolchain and libraries allow it. CPU execution is the current implemented
surface; GPU and NPU execution are future backend families that must earn their
place through explicit data-movement-aware benchmarks.
SSE4.2, AVX2, optional AVX-512, OpenMP (auto-enabled), Fortran, and `.tar.zst`
support when the local toolchain and libraries allow it. CPU execution is the
current implemented surface; GPU and NPU execution are future backend families
that must earn their place through explicit data-movement-aware benchmarks.

That means the preferred deployment model is to build KEYSTONE on the machine,
container image, or target silicon family where it will run. It is not trying to
Expand Down
134 changes: 134 additions & 0 deletions benchmarks/bench_sse42.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* SSE4.2 vs scalar benchmark for AVX1-only CPUs.
* Measures the impact of the new SSE4.2 SIMD path on chunked_search
* and the overall search_batch pipeline.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include "keystone.h"

#define N_ARRAY 100000
#define N_QUERIES 100000
#define N_ROUNDS 20

static double now_sec(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec + ts.tv_nsec * 1e-9;
}

int main(void) {
/* Build a sorted array of random int64s */
int64_t* arr = malloc(N_ARRAY * sizeof(int64_t));
srand(42);
for (size_t i = 0; i < N_ARRAY; i++) arr[i] = ((int64_t)rand() << 32) | rand();
/* Sort */
for (size_t i = 1; i < N_ARRAY; i++) {
int64_t v = arr[i]; size_t j = i;
while (j > 0 && arr[j-1] > v) { arr[j] = arr[j-1]; j--; }
arr[j] = v;
}

/* Build query set: 50% hits, 50% misses */
int64_t* queries = malloc(N_QUERIES * sizeof(int64_t));
for (size_t i = 0; i < N_QUERIES; i++) {
if (i % 2 == 0) queries[i] = arr[rand() % N_ARRAY];
else queries[i] = ((int64_t)rand() << 32) | rand();
}

/* Detect CPU features */
uint32_t feat = keystone_detect_cpu_features();
printf("CPU features: 0x%08X\n", feat);
printf(" AVX: %s\n", (feat & KEYSTONE_CPU_AVX) ? "yes" : "no");
printf(" AVX2: %s\n", (feat & KEYSTONE_CPU_AVX2) ? "yes" : "no");
printf(" AVX512: %s\n", (feat & KEYSTONE_CPU_AVX512) ? "yes" : "no");
printf(" SSE42: %s\n", (feat & KEYSTONE_CPU_SSE42) ? "yes" : "no");
printf("\n");

/* --- Benchmark single-key search (keystone_search) --- */
/* Warmup */
for (size_t i = 0; i < 1000; i++) keystone_search(arr, N_ARRAY, queries[i % N_QUERIES], NULL, 4);

double t0 = now_sec();
size_t found_total = 0;
for (int r = 0; r < N_ROUNDS; r++) {
for (size_t i = 0; i < N_QUERIES; i++) {
if (keystone_search(arr, N_ARRAY, queries[i], NULL, 4) != KEYSTONE_NOT_FOUND)
found_total++;
}
}
double t1 = now_sec();
double single_ns = (t1 - t0) / (N_ROUNDS * N_QUERIES) * 1e9;
printf("Single-key search: %.1f ns/query (%zu hits in %d rounds of %d queries)\n",
single_ns, found_total, N_ROUNDS, N_QUERIES);

/* --- Benchmark batch search (keystone_search_batch) --- */
keystone_batch_item_t* items = malloc(N_QUERIES * sizeof(keystone_batch_item_t));
for (size_t i = 0; i < N_QUERIES; i++) {
items[i].key = queries[i];
items[i].ordinal = i;
items[i].result = KEYSTONE_NOT_FOUND;
}

/* Warmup */
keystone_search_batch(arr, N_ARRAY, items, 100, NULL, 4);

t0 = now_sec();
size_t batch_found = 0;
for (int r = 0; r < N_ROUNDS; r++) {
batch_found += keystone_search_batch(arr, N_ARRAY, items, N_QUERIES, NULL, 4);
}
t1 = now_sec();
double batch_ns = (t1 - t0) / (N_ROUNDS * N_QUERIES) * 1e9;
printf("Batch search: %.1f ns/query (%zu hits/round)\n",
batch_ns, batch_found / N_ROUNDS);

/* --- Benchmark zero-copy batch (keystone_search_keys_batch_auto) --- */
size_t* results = malloc(N_QUERIES * sizeof(size_t));
t0 = now_sec();
size_t zc_found = 0;
for (int r = 0; r < N_ROUNDS; r++) {
zc_found += keystone_search_keys_batch_auto(arr, N_ARRAY, queries, N_QUERIES, results, NULL, 4, NULL);
}
t1 = now_sec();
double zc_ns = (t1 - t0) / (N_ROUNDS * N_QUERIES) * 1e9;
printf("Zero-copy batch: %.1f ns/query (%zu hits/round)\n",
zc_ns, zc_found / N_ROUNDS);

/* --- Benchmark auto batch with OpenMP (keystone_search_batch_auto) --- */
keystone_parallel_config_t omp_cfg = {0};
omp_cfg.num_threads = 0; /* auto-detect */
omp_cfg.use_thread_pool = 1;
omp_cfg.batch_chunk = 64;
t0 = now_sec();
size_t omp_found = 0;
for (int r = 0; r < N_ROUNDS; r++) {
omp_found += keystone_search_batch_auto(arr, N_ARRAY, items, N_QUERIES, NULL, 4, &omp_cfg);
}
t1 = now_sec();
double omp_ns = (t1 - t0) / (N_ROUNDS * N_QUERIES) * 1e9;
printf("Auto+OpenMP batch: %.1f ns/query (%zu hits/round)\n",
omp_ns, omp_found / N_ROUNDS);

/* --- Benchmark small-window linear scan (local_search path) --- */
/* This exercises the SSE4.2 chunked_search path directly for small windows */
int64_t small_arr[64];
for (size_t i = 0; i < 64; i++) small_arr[i] = (int64_t)i * 2;
size_t small_found = 0;
t0 = now_sec();
for (int r = 0; r < 100000; r++) {
for (int64_t k = 0; k < 128; k++) {
if (keystone_search(small_arr, 64, k, NULL, 4) != KEYSTONE_NOT_FOUND)
small_found++;
}
}
t1 = now_sec();
double small_ns = (t1 - t0) / (100000 * 128) * 1e9;
printf("Small-window scan: %.1f ns/query (64-element array, 128 keys)\n", small_ns);

free(arr); free(queries); free(items); free(results);
return 0;
}
Binary file modified benchmarks/dsmil_benchmark
Binary file not shown.
Binary file modified benchmarks/performance_proof
Binary file not shown.
Loading
Loading