Skip to content

Commit 1489c30

Browse files
hyj2003momowayclaude
authored
[2.0] Add new Frontier-CS 2.0 problem vector_db_ann_disk (#146)
* feat: add vector search on disk * feat: add tested vector search on disk * vector_db_ann_disk: auto-build SIFT100M judge image + anti-cheat + readme cleanup - docker/: judge image that builds the benchmark from scratch at build time (download real BIGANN base/query + official GT, compile FreshDiskANN, build the on-disk graph+PQ, measure baseline); zero manual download/hosting - config.yaml: point to custom judge_image; drop the unused harbor mount block - evaluator.py: load truth/baseline into memory then delete from disk before any candidate runs (candidate executes in the judge container as root) - readme: strictly agent-facing; remove host paths / truth/baseline names; fix stale query-count and query-variation wording * vector_db_ann_disk: fast iterative eval (full-Q final) + docs #1 Two-tier evaluation. The judge sets FRONTIER_SUBMISSION_ROLE per submission: iterative `submit.sh` feedback now times a small FRONTIER_VECTOR_DB_ITER_Q subset (default 2000) for fast turnaround, while the final verifier (role=final) times the full FRONTIER_VECTOR_DB_Q=10000 set for the authoritative score. recall@10 and QPS are stable averages so the iterative estimate tracks the final; this cuts the iterate loop from ~4-5 min to ~2 min/submission and removes cancel-thrashing. #4 Docs. readme: eval wall-time note + iterative-vs-final behavior. docker/judge/Dockerfile: bake FRONTIER_VECTOR_DB_ITER_Q. docker/README.md: document constrained/rootless local runs (mount-data variant, --cpus ignore, env-only credentials). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: momoway <3499622023@qq.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4ac2879 commit 1489c30

20 files changed

Lines changed: 2309 additions & 0 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
tag: systems
2+
runtime:
3+
language: rust
4+
timeout_seconds: 10800
5+
environment: "Rust project; hidden disk ANN benchmark; Python/NumPy judge"
6+
apt_packages:
7+
- build-essential
8+
- cargo
9+
- git
10+
- rustc
11+
judge_apt_packages:
12+
- build-essential
13+
- cargo
14+
- rustc
15+
- python3-pip
16+
- python3-numpy
17+
judge_pip_packages:
18+
- faiss-cpu
19+
docker:
20+
# Agent image is the default ubuntu:24.04 (the agent needs no hidden data).
21+
# The judge image bakes the SIFT100M benchmark data in; build it before a
22+
# local Harbor trial with 2.0/problems/vector_db_ann_disk/docker/build_images.sh.
23+
image: ubuntu:24.04
24+
judge_image: frontiercs/vector-db-ann-disk-judge:experimental-v1
25+
environment:
26+
# If these resource limits change, also update the resource budget text in
27+
# readme and harbor/app/README.md so agents can design parallel algorithms
28+
# for the actual CPU and memory budget.
29+
cpus: 8
30+
memory_mb: 8192
31+
storage_mb: 8192
32+
build_timeout_seconds: 3600
33+
evaluation:
34+
# The judge drives the search service with this many concurrent workers.
35+
# Keep this aligned with the CPU budget unless the task is intentionally
36+
# changed into a higher-concurrency service benchmark.
37+
query_concurrency: 8
38+
# Fallback only: the actual timed query count is pinned to the full official
39+
# query set via FRONTIER_VECTOR_DB_Q=10000 in docker/judge/Dockerfile, which
40+
# overrides this. (The local CI smoke in evaluate.sh sets its own small Q.)
41+
queries_per_worker: 64
42+
# The hidden benchmark data is baked into the custom judge image
43+
# (runtime.docker.judge_image); the data paths, dtype, N=100,000,000, and
44+
# Q=10,000 are pinned as ENV in docker/judge/Dockerfile. truth.bin / baseline.json
45+
# live under /data/private_100M, which is never passed to /load.
46+
submission:
47+
kind: directory
48+
path: /app
49+
exclude:
50+
- target
51+
- .git
52+
- .frontier-cs
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Experimental vector_db_ann_disk Judge Image
2+
3+
This task **constructs** the hidden SIFT100M benchmark data from scratch at
4+
judge-image build time and bakes it in (the duckdb-e2e / vllm bake pattern). No
5+
manual download, no data hosting, no host staging — the image build downloads the
6+
public BIGANN dataset, builds the DiskANN index, and measures the baseline.
7+
8+
The **agent** image stays the default `ubuntu:24.04` — the agent never sees the
9+
hidden data; it only implements the `/load` + `/search` contract.
10+
11+
## Build
12+
13+
```bash
14+
bash 2.0/problems/vector_db_ann_disk/docker/build_images.sh
15+
```
16+
17+
Default tag (kept in sync with `config.yaml` `runtime.docker.judge_image`):
18+
19+
```text
20+
JUDGE_TAG=frontiercs/vector-db-ann-disk-judge:experimental-v1
21+
```
22+
23+
Full scale (N=100M) is **network / RAM / time heavy** (see Resources). Quick
24+
smoke image at small scale:
25+
26+
```bash
27+
N=100000 Q=1000 bash docker/build_images.sh
28+
```
29+
30+
## Pipeline (docker/build_all.sh, run inside the image builder)
31+
32+
1. **build_data.py** — downloads the real BIGANN data and slices it:
33+
- `100M.u8bin` — first N rows of `base.1B.u8bin` via HTTP range, header
34+
rewritten to N (`uint8`, 128-dim).
35+
- `query.bin``query.public.10K.u8bin` (10,000 × 128 `uint8`).
36+
- `truth.bin` — top-`TOP_K` ids per query, sliced from the **official** exact
37+
ground truth `GT_100M/bigann-100M` (so no 51 GB exact-search is needed).
38+
2. **build_index.sh** — clones + compiles
39+
[FreshDiskANN-baseline](https://github.com/g4197/FreshDiskANN-baseline) and runs
40+
`build_disk_index``100M_disk.index`, `100M_pq_pivots.bin`,
41+
`100M_pq_compressed.bin`.
42+
3. **build_baseline.py** — exact Faiss `IndexFlatL2` throughput → `baseline.json`.
43+
44+
Source URLs (public, from big-ann-benchmarks):
45+
46+
```text
47+
https://dl.fbaipublicfiles.com/billion-scale-ann-benchmarks/bigann/base.1B.u8bin
48+
https://dl.fbaipublicfiles.com/billion-scale-ann-benchmarks/bigann/query.public.10K.u8bin
49+
https://dl.fbaipublicfiles.com/billion-scale-ann-benchmarks/GT_100M/bigann-100M
50+
```
51+
52+
## What the judge image contains
53+
54+
```text
55+
/data/index_100M/ # handed to the candidate via /load
56+
100M.u8bin query.bin 100M_disk.index 100M_pq_pivots.bin 100M_pq_compressed.bin
57+
/data/private_100M/ # judge-only, NEVER referenced by /load
58+
truth.bin baseline.json
59+
```
60+
61+
Data paths, dtype, `N=100,000,000`, `Q=10,000` are pinned as image `ENV`
62+
(`judge/Dockerfile`). The adapter builds the final judge image on top of this one,
63+
layering `cargo`/`rustc` + `numpy`/`faiss-cpu` from `config.yaml`.
64+
65+
## Resources (full N=100M build)
66+
67+
- **base download**: ~12.8 GB (first 100M rows of `base.1B.u8bin` via range).
68+
- **DiskANN index build**: tens of GB output, multiple cores, ~hours; tune
69+
`R/L/B/M/T` via env in `build_index.sh` (`B` = PQ budget; keep PQ within the
70+
8 GiB eval budget).
71+
- **baseline**: exact FlatL2 over 100M holds vectors as float32 (~**51 GB RAM**)
72+
on the build host. The baseline therefore reflects the *build* host, not the
73+
8 GiB eval container — a deliberate bake. Adjust if you want a different
74+
reference.
75+
76+
## Running locally on constrained Docker (rootless / vfs / limited disk)
77+
78+
The baked judge image is **large (~58 GB)** because the SIFT100M data lives
79+
inside it. On a host with `overlay2` and ample disk this is fine. But on a
80+
**rootless daemon using the `vfs` storage driver** (no copy-on-write), every
81+
derived image layer and every container re-copies the full image, so a single
82+
trial can need **~3x** the image size in scratch space, and a near-full shared
83+
disk can be exhausted.
84+
85+
For those hosts, use a **mount-data variant**: build a tiny base image (ubuntu +
86+
the pinned evaluator ENV + the small `private_100M` secrets) and bind-mount the
87+
54 GB `index_100M` read-only into the judge service at `/data/index_100M`
88+
instead of baking it. The data then lives once on the host. Other rootless
89+
gotchas: set `DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock`; pass
90+
`--cpus ignore` to `harbor trial start` if the `cpu` cgroup controller is not
91+
delegated (only `memory`/`pids` usually are); and inject agent credentials via
92+
**env vars** (e.g. `OPENAI_API_KEY` / `CLAUDE_CODE_OAUTH_TOKEN`), since
93+
`docker cp` of host-owned credential files fails under the user namespace.
94+
95+
Note also that iterative `submit.sh` evaluations time only
96+
`FRONTIER_VECTOR_DB_ITER_Q` queries (default 2000) for fast feedback; the final
97+
verifier (`FRONTIER_SUBMISSION_ROLE=final`) always times the full
98+
`FRONTIER_VECTOR_DB_Q` set.
99+
100+
## Security note (anti-cheat)
101+
102+
The candidate service is built and run by the judge **in this same container**,
103+
so plain directory isolation is not enough on its own. Three layers protect the
104+
ground truth / baseline:
105+
106+
1. **Out of the /load directory**`truth.bin` / `baseline.json` live under
107+
`/data/private_100M`, never passed to `/load`, so they cannot be reached via
108+
`dirname(vector_path)`.
109+
2. **Restricted permissions**`/data/private_100M` is `0700` root-only (the
110+
`/load` files under `/data/index_100M` stay world-readable). A non-root
111+
candidate cannot read them.
112+
3. **Removed after load**`evaluator.py` loads the truth + baseline into memory
113+
at judge startup and then **deletes the files from disk** before any candidate
114+
runs, so even a root candidate finds nothing to read. (`_ensure_benchmark`
115+
caches in memory; later submissions never need the files. Opt out for
116+
debugging with `FRONTIER_VECTOR_DB_KEEP_TRUTH=1`.)
117+
118+
They are also never named in the agent-facing `readme`.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
# Full data-build pipeline for vector_db_ann_disk. Operator tooling, NOT
3+
# agent-facing. Produces, with zero manual download, everything the judge needs:
4+
#
5+
# <data_root>/index_100M/{100M.u8bin,query.bin,100M_disk.index,
6+
# 100M_pq_pivots.bin,100M_pq_compressed.bin}
7+
# <data_root>/private_100M/{truth.bin,baseline.json}
8+
#
9+
# Steps:
10+
# 1. build_data.py download real BIGANN base/query + slice official GT
11+
# 2. build_index.sh clone+compile FreshDiskANN, build the on-disk graph + PQ
12+
# 3. build_baseline.py exact Faiss baseline throughput
13+
#
14+
# Parametrized by FRONTIER_VECTOR_DB_N / _Q for small-scale testing. At full
15+
# scale (N=100,000,000) this needs a large-RAM host, tens of GB of disk, and
16+
# hours of compute — run it offline on the build host.
17+
set -euo pipefail
18+
19+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
20+
DATA_ROOT="${1:?usage: build_all.sh <data_root>}"
21+
22+
export FRONTIER_VECTOR_DB_N="${FRONTIER_VECTOR_DB_N:-100000000}"
23+
export FRONTIER_VECTOR_DB_Q="${FRONTIER_VECTOR_DB_Q:-10000}"
24+
export FRONTIER_VECTOR_DB_TOP_K="${FRONTIER_VECTOR_DB_TOP_K:-10}"
25+
26+
echo "=== [1/3] data (vectors + queries + ground truth) ==="
27+
python3 "$SCRIPT_DIR/build_data.py" "$DATA_ROOT"
28+
29+
echo "=== [2/3] DiskANN on-disk graph + PQ ==="
30+
bash "$SCRIPT_DIR/build_index.sh" "$DATA_ROOT"
31+
32+
echo "=== [3/3] reference baseline throughput ==="
33+
python3 "$SCRIPT_DIR/build_baseline.py" "$DATA_ROOT"
34+
35+
echo "=== done. contents: ==="
36+
ls -la "$DATA_ROOT/index_100M" "$DATA_ROOT/private_100M"
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env python3
2+
"""Measure the reference (exact, brute-force) baseline throughput -> baseline.json.
3+
4+
Operator tooling — NOT agent-facing. Runs at judge-image build time. Mirrors the
5+
evaluator's reference: an exact Faiss IndexFlatL2 over the base vectors, queried
6+
with the same Q query set and CONCURRENCY, timed to produce baseline_qps. The
7+
candidate must beat this baseline_qps to score.
8+
9+
NOTE (resource + fairness): an exact FlatL2 over N=100M holds the vectors as
10+
float32 in RAM (~51 GB), so this step needs a large-RAM BUILD host. The resulting
11+
baseline reflects the build host, not the 8 GiB eval container — bake it
12+
deliberately. Parametrized by N/Q so it is testable at small scale.
13+
14+
N=2000 Q=256 python3 build_baseline.py <data_root>
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import json
20+
import os
21+
import struct
22+
import sys
23+
import time
24+
from pathlib import Path
25+
26+
import numpy as np
27+
28+
DIM = 128
29+
N = int(os.environ.get("FRONTIER_VECTOR_DB_N", "100000000"))
30+
Q = int(os.environ.get("FRONTIER_VECTOR_DB_Q", "10000"))
31+
TOP_K = int(os.environ.get("FRONTIER_VECTOR_DB_TOP_K", "10"))
32+
CONCURRENCY = int(os.environ.get("FRONTIER_VECTOR_DB_CONCURRENCY", "8"))
33+
ADD_BATCH = int(os.environ.get("FRONTIER_VECTOR_DB_REFERENCE_BATCH_SIZE", "50000"))
34+
35+
36+
def _load_u8_matrix(path: Path, rows: int) -> np.memmap:
37+
with path.open("rb") as f:
38+
npts, dim = struct.unpack("<II", f.read(8))
39+
if dim != DIM:
40+
raise RuntimeError(f"{path}: dim {dim} != {DIM}")
41+
return np.memmap(path, dtype=np.uint8, mode="r", offset=8, shape=(rows, DIM))
42+
43+
44+
def main() -> int:
45+
if len(sys.argv) != 2:
46+
print("usage: build_baseline.py <data_root>", file=sys.stderr)
47+
return 2
48+
root = Path(sys.argv[1])
49+
base = _load_u8_matrix(root / "index_100M" / "100M.u8bin", N)
50+
queries = _load_u8_matrix(root / "index_100M" / "query.bin", Q)
51+
52+
import faiss # noqa: PLC0415 — only needed here, present in the judge image
53+
54+
faiss.omp_set_num_threads(CONCURRENCY)
55+
index = faiss.IndexFlatL2(DIM)
56+
print(f"[baseline] adding {N:,} vectors to FlatL2 (float32, ~{N * DIM * 4 / 1e9:.0f} GB RAM) ...")
57+
for start in range(0, N, ADD_BATCH):
58+
end = min(start + ADD_BATCH, N)
59+
index.add(np.asarray(base[start:end], dtype=np.float32))
60+
61+
qf = np.asarray(queries[:Q], dtype=np.float32)
62+
# Warm up, then time the exact search of the full query set.
63+
index.search(qf[: min(32, Q)], TOP_K)
64+
t0 = time.perf_counter()
65+
index.search(qf, TOP_K)
66+
baseline_seconds = max(time.perf_counter() - t0, 1e-9)
67+
baseline_qps = Q / baseline_seconds
68+
69+
out = root / "private_100M" / "baseline.json"
70+
out.parent.mkdir(parents=True, exist_ok=True)
71+
out.write_text(
72+
json.dumps(
73+
{
74+
"baseline_seconds": baseline_seconds,
75+
"baseline_qps": baseline_qps,
76+
"baseline_load_seconds": 0.0,
77+
},
78+
indent=2,
79+
),
80+
encoding="utf-8",
81+
)
82+
print(f"[baseline] {Q:,} queries in {baseline_seconds:.3f}s -> baseline_qps={baseline_qps:.3f}")
83+
print(f"[baseline] wrote {out}")
84+
return 0
85+
86+
87+
if __name__ == "__main__":
88+
raise SystemExit(main())

0 commit comments

Comments
 (0)