Skip to content

Commit 02d5292

Browse files
committed
Add script to convert MSMARCO parquet files to raw shard files and remove outdated figures and scripts
- Introduced `convert-msmacro-parquet-to-shards.py` for converting MSMARCO v2.1 parquet parts into raw shard files with optimized memory usage and optional direct writes. - Deleted outdated PDF figures related to GloVe metrics. - Removed `ingest_compare.py` script as it is no longer needed. - Added `mock_search.py` for quick mock vector search over generated embeddings.
1 parent e8560e4 commit 02d5292

21 files changed

Lines changed: 610 additions & 126 deletions

File tree

bindings/python/examples/.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,5 @@ data/
1111
benchmark_logs/
1212
benchmark_work/
1313

14-
glove-100-angular.hdf5
15-
sift-128-euclidean.hdf5
16-
14+
datasets/
1715
benchmark-vector/sweep_results*

bindings/python/examples/benchmark-vector/benchmark_vector_params.py renamed to bindings/python/examples/benchmark-vector/benchmark_vector_params-lsm-jvector.py

File renamed without changes.
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Convert pre-downloaded MSMARCO v2.1 parquet parts into raw shard files (FIXED).
4+
5+
Key properties:
6+
- Batched Arrow → NumPy iteration (no per-row Python objects)
7+
- Vectorized cosine normalization
8+
- Flat, predictable RSS usage
9+
- Optional O_DSYNC writes (--direct)
10+
11+
To download the parquet parts first:
12+
13+
```
14+
HF_HOME=/mnt/ssd2/hf_cache
15+
mkdir -p "$HF_HOME"
16+
17+
from datasets import load_dataset
18+
load_dataset(
19+
"Cohere/msmarco-v2.1-embed-english-v3",
20+
"passages",
21+
split="train",
22+
streaming=False,
23+
cache_dir="$HF_HOME")
24+
```
25+
26+
Usage:
27+
HF_HOME=/path/to/hf_cache \
28+
python convert-msmarco-parquet-to-shards.py \
29+
--parquet-glob "$HF_HOME/datasets/Cohere___msmarco-v2.1-embed-english-v3/passages_parquet/*.parquet" \
30+
--out-dir datasets \
31+
--count 10_000_000 \
32+
--shard-size 100000 \
33+
--batch-rows 8192 \
34+
--fsync-every 50000 \
35+
--direct
36+
"""
37+
38+
from __future__ import annotations
39+
40+
import argparse
41+
import glob
42+
import heapq
43+
import json
44+
import mmap
45+
import os
46+
import time
47+
from pathlib import Path
48+
49+
import numpy as np
50+
import pyarrow.parquet as pq
51+
52+
# ---------------- constants ----------------
53+
54+
TOPK = 50
55+
Q_COUNT = 1000
56+
CHUNK = 4096
57+
PROGRESS_EVERY = 100_000
58+
59+
# ---------------- helpers ----------------
60+
61+
62+
def fmt_secs(secs: float | None) -> str:
63+
if secs is None or secs <= 0:
64+
return "?"
65+
m, s = divmod(int(secs + 0.5), 60)
66+
h, m = divmod(m, 60)
67+
return f"{h:d}:{m:02d}:{s:02d}" if h else f"{m:02d}:{s:02d}"
68+
69+
70+
def sync_and_advise(fd: int) -> None:
71+
try:
72+
os.fsync(fd)
73+
except OSError:
74+
pass
75+
try:
76+
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
77+
except (AttributeError, OSError):
78+
pass
79+
80+
81+
def close_memmap(mm: np.memmap | None) -> None:
82+
if mm is None:
83+
return
84+
mm.flush()
85+
m = getattr(mm, "_mmap", None)
86+
if m is not None:
87+
try:
88+
m.madvise(mmap.MADV_DONTNEED)
89+
except Exception:
90+
pass
91+
m.close()
92+
93+
94+
# ---------------- writer ----------------
95+
96+
97+
class DirectWriter:
98+
def __init__(self, path: Path, direct: bool):
99+
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
100+
if direct and hasattr(os, "O_DSYNC"):
101+
flags |= os.O_DSYNC
102+
self.fd = os.open(path, flags, 0o644)
103+
104+
def write(self, b: bytes) -> None:
105+
mv = memoryview(b)
106+
off = 0
107+
while off < len(mv):
108+
n = os.write(self.fd, mv[off:])
109+
if n == 0:
110+
raise OSError("write returned 0")
111+
off += n
112+
113+
def fileno(self) -> int:
114+
return self.fd
115+
116+
def close(self) -> None:
117+
os.close(self.fd)
118+
119+
120+
def open_writer(path: Path, direct: bool):
121+
if direct:
122+
return DirectWriter(path, direct=True)
123+
return open(path, "wb", buffering=1 << 20)
124+
125+
126+
# ---------------- GT computation ----------------
127+
128+
129+
def build_gt_sharded(
130+
shards: list[dict],
131+
total_count: int,
132+
dim: int,
133+
gt_path: Path,
134+
q_count: int,
135+
topk: int,
136+
):
137+
print(f"[GT] building exact GT for {q_count} queries, k={topk}")
138+
139+
q_count = min(q_count, total_count)
140+
rng = np.random.default_rng()
141+
q_indices = rng.choice(total_count, size=q_count, replace=False)
142+
143+
# Load query vectors
144+
queries = np.empty((q_count, dim), dtype=np.float32)
145+
shard_map = {}
146+
147+
for qi, gidx in enumerate(q_indices):
148+
for s in shards:
149+
if s["start"] <= gidx < s["start"] + s["count"]:
150+
shard_map.setdefault(s["path"], []).append((qi, gidx - s["start"]))
151+
break
152+
153+
for s in shards:
154+
assigns = shard_map.get(s["path"])
155+
if not assigns:
156+
continue
157+
mm = np.memmap(s["path"], dtype=np.float32, mode="r", shape=(s["count"], dim))
158+
for qi, li in assigns:
159+
queries[qi] = mm[li]
160+
close_memmap(mm)
161+
162+
# Top-k heaps
163+
heaps = [[] for _ in range(q_count)]
164+
165+
for s in shards:
166+
print(f"[GT] scanning {s['path'].name}")
167+
mm = np.memmap(s["path"], dtype=np.float32, mode="r", shape=(s["count"], dim))
168+
for off in range(0, s["count"], CHUNK):
169+
block = mm[off : off + CHUNK]
170+
sims = block @ queries.T
171+
for qi in range(q_count):
172+
heap = heaps[qi]
173+
col = sims[:, qi]
174+
for i, score in enumerate(col):
175+
doc_id = s["start"] + off + i
176+
if len(heap) < topk:
177+
heapq.heappush(heap, (score, doc_id))
178+
else:
179+
heapq.heappushpop(heap, (score, doc_id))
180+
close_memmap(mm)
181+
182+
with open(gt_path, "w") as f:
183+
for qi, heap in enumerate(heaps):
184+
heap.sort(reverse=True)
185+
json.dump(
186+
{
187+
"query_id": int(q_indices[qi]),
188+
"topk": [{"doc_id": int(d), "score": float(s)} for s, d in heap],
189+
},
190+
f,
191+
)
192+
f.write("\n")
193+
194+
print(f"[GT] wrote {gt_path}")
195+
196+
197+
# ---------------- main ----------------
198+
199+
200+
def main():
201+
ap = argparse.ArgumentParser()
202+
ap.add_argument("--parquet-glob", required=True)
203+
ap.add_argument("--out-dir", default="datasets")
204+
ap.add_argument("--count", default="1_000_000")
205+
ap.add_argument("--shard-size", type=int, default=100_000)
206+
ap.add_argument("--batch-rows", type=int, default=8192)
207+
ap.add_argument("--fsync-every", type=int, default=50_000)
208+
ap.add_argument("--direct", action="store_true")
209+
args = ap.parse_args()
210+
211+
count = int(str(args.count).replace("_", ""))
212+
shard_size = args.shard_size
213+
214+
parquet_files = sorted(glob.glob(args.parquet_glob))
215+
if not parquet_files:
216+
raise SystemExit("No parquet files found")
217+
218+
out_dir = Path(args.out_dir)
219+
out_dir.mkdir(parents=True, exist_ok=True)
220+
221+
print(f"[init] Found {len(parquet_files)} parquet files to process")
222+
223+
dim = None
224+
written = 0
225+
filled = 0
226+
shard_idx = 0
227+
shard_start = 0
228+
shards = []
229+
230+
writer = None
231+
path = None
232+
233+
t0 = time.time()
234+
last_report = 0
235+
236+
# Process files one at a time; use ParquetFile iter_batches to avoid list offset overflow in datasets
237+
for file_idx, parquet_file in enumerate(parquet_files):
238+
if written >= count:
239+
break
240+
241+
print(
242+
f"[process] File {file_idx + 1}/{len(parquet_files)}: {Path(parquet_file).name}"
243+
)
244+
245+
pf = pq.ParquetFile(parquet_file)
246+
247+
for record_batch in pf.iter_batches(
248+
columns=["emb"], batch_size=args.batch_rows
249+
):
250+
col = record_batch.column(0) # ListArray
251+
offsets = col.offsets.to_numpy()
252+
values = np.asarray(
253+
col.values.to_numpy(zero_copy_only=False), dtype=np.float32
254+
)
255+
256+
# Infer dimension from first batch if unknown
257+
if dim is None:
258+
dim = int(offsets[1] - offsets[0])
259+
path = out_dir / f"msmarco-passages-{count}.shard{shard_idx:04d}.f32"
260+
writer = open_writer(path, args.direct)
261+
262+
# Validate fixed-size lists
263+
spans = offsets[1:] - offsets[:-1]
264+
if not np.all(spans == dim):
265+
raise RuntimeError(
266+
f"Non-uniform embedding dimension detected in {parquet_file}"
267+
)
268+
269+
# Ensure a writable, contiguous float32 view before normalization
270+
embs = np.asarray(values, dtype=np.float32).reshape(-1, dim)
271+
embs = embs / (np.linalg.norm(embs, axis=1, keepdims=True) + 1e-12)
272+
embs = np.ascontiguousarray(embs)
273+
274+
off = 0
275+
while off < len(embs) and written < count:
276+
take = min(shard_size - filled, count - written, len(embs) - off)
277+
writer.write(embs[off : off + take].tobytes(order="C"))
278+
off += take
279+
filled += take
280+
written += take
281+
282+
if written % args.fsync_every == 0:
283+
sync_and_advise(writer.fileno())
284+
285+
if filled == shard_size and written < count:
286+
sync_and_advise(writer.fileno())
287+
writer.close()
288+
shards.append({"path": path, "count": filled, "start": shard_start})
289+
shard_start += filled
290+
shard_idx += 1
291+
filled = 0
292+
path = (
293+
out_dir / f"msmarco-passages-{count}.shard{shard_idx:04d}.f32"
294+
)
295+
writer = open_writer(path, args.direct)
296+
297+
if written - last_report >= PROGRESS_EVERY:
298+
elapsed = time.time() - t0
299+
rate = written / elapsed
300+
eta = (count - written) / rate if rate else None
301+
print(
302+
f"[convert] {written:,}/{count:,} | {rate:,.0f} v/s | eta {fmt_secs(eta)}"
303+
)
304+
last_report = written
305+
306+
if written >= count:
307+
break
308+
309+
if writer is not None:
310+
sync_and_advise(writer.fileno())
311+
writer.close()
312+
if filled > 0:
313+
shards.append({"path": path, "count": filled, "start": shard_start})
314+
315+
meta = {
316+
"dim": dim,
317+
"dtype": "float32",
318+
"count": written,
319+
"shard_size": shard_size,
320+
}
321+
(out_dir / f"msmarco-passages-{count}.meta.json").write_text(json.dumps(meta))
322+
323+
print(f"[done] wrote {written:,} vectors across {len(shards)} shards")
324+
325+
build_gt_sharded(
326+
shards=shards,
327+
total_count=written,
328+
dim=dim,
329+
gt_path=out_dir / f"msmarco-passages-{count}.gt.jsonl",
330+
q_count=Q_COUNT,
331+
topk=TOPK,
332+
)
333+
334+
335+
if __name__ == "__main__":
336+
main()

0 commit comments

Comments
 (0)