Files
Mini-Nav/tests/test_sw_retrieval_benchmark.py
SikongJueluo b5a40819cc feat: vectorize CAM retrieval with NumPy and add multi-worker support
- Replace scalar hamming distance with NumPy bitwise_count for batch retrieval
- Add ThreadPoolExecutor-based multi-worker query parallelism
- Improve missing dataset error message with generation command hint
- Increase DEFAULT_MAX_QUERIES from 128 to 8192 for meaningful throughput tests
2026-06-07 20:45:20 +08:00

273 lines
8.2 KiB
Python

from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
import numpy as np
import pytest
def load_sw_benchmark():
path = Path(__file__).resolve().parent.parent / "scripts" / "sw_retrieval_benchmark.py"
spec = importlib.util.spec_from_file_location("sw_retrieval_benchmark", path)
assert spec is not None, f"Could not find spec for {path}"
assert spec.loader is not None, f"No loader available for {path}"
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _write_dataset(path: Path) -> None:
np.savez_compressed(
path,
rows_words=np.array(
[
[0b11110000],
[0b11100000],
[0b00001111],
],
dtype=np.uint64,
),
row_labels=np.array([0, 0, 1], dtype=np.int64),
queries_words=np.array(
[
[0b11110000],
[0b00001111],
],
dtype=np.uint64,
),
query_labels=np.array([0, 1], dtype=np.int64),
)
def test_load_dataset_preserves_little_endian_words(tmp_path):
bench = load_sw_benchmark()
dataset_path = tmp_path / "dataset.npz"
np.savez_compressed(
dataset_path,
rows_words=np.array([[0x2, 0x1]], dtype=np.uint64),
row_labels=np.array([7], dtype=np.int64),
queries_words=np.array([[0x4, 0x3]], dtype=np.uint64),
query_labels=np.array([8], dtype=np.int64),
)
dataset = bench.load_retrieval_dataset_npz(dataset_path)
assert dataset.rows == [0x1_0000_0000_0000_0002]
assert dataset.queries == [0x3_0000_0000_0000_0004]
assert dataset.row_labels == [7]
assert dataset.query_labels == [8]
def test_run_benchmark_reports_quality_and_query_speed(tmp_path):
bench = load_sw_benchmark()
dataset_path = tmp_path / "dataset.npz"
_write_dataset(dataset_path)
timer_values = iter([1_000, 3_000])
result = bench.run_benchmark(
dataset_path,
hash_bits=64,
topk_values=(1, 2),
run_id="unit-test",
timer_ns=lambda: next(timer_values),
)
assert result["mode"] == "software-numpy"
assert result["status"] == "pass"
assert result["params"]["engine"] == "numpy"
assert result["params"]["workers"] == 1
assert result["dataset"]["num_queries"] == 2
assert result["params"]["num_rows"] == 3
assert result["params"]["topk_k"] == 2
assert result["metrics"]["1"]["exact_match_rate"] == 1.0
assert result["metrics"]["1"]["recall@k"] == 1.0
assert result["performance"]["total_elapsed_ns"] == 2_000
assert result["performance"]["ns_per_query"] == 1_000.0
assert result["performance"]["queries_per_second"] == 1_000_000.0
def test_numpy_topk_matches_reference_with_tiebreak(tmp_path):
bench = load_sw_benchmark()
dataset_path = tmp_path / "dataset.npz"
np.savez_compressed(
dataset_path,
rows_words=np.array(
[
[0b11110000],
[0b11110000],
[0b11100000],
[0b00001111],
],
dtype=np.uint64,
),
row_labels=np.array([0, 0, 0, 1], dtype=np.int64),
queries_words=np.array([[0b11110000]], dtype=np.uint64),
query_labels=np.array([0], dtype=np.int64),
)
dataset = bench.load_retrieval_dataset_npz(dataset_path)
assert bench.match_topk_numpy(dataset.queries_words[0], dataset.rows_words, width=64, k=3) == [0, 1, 2]
def test_numpy_batch_topk_vectorizes_across_queries(monkeypatch):
bench = load_sw_benchmark()
rows_words = np.array(
[
[0b11110000],
[0b11100000],
[0b00001111],
],
dtype=np.uint64,
)
queries_words = np.array(
[
[0b11110000],
[0b00001111],
],
dtype=np.uint64,
)
monkeypatch.setattr(
bench,
"match_topk_numpy",
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("scalar path called")),
)
assert bench._match_topk_numpy_batch(queries_words, rows_words, width=64, k=2) == [
[0, 1],
[2, 1],
]
def test_run_benchmark_exact_match_compares_against_reference(tmp_path, monkeypatch):
bench = load_sw_benchmark()
dataset_path = tmp_path / "dataset.npz"
_write_dataset(dataset_path)
monkeypatch.setattr(bench, "ref_match_topk", lambda query, rows, width, k: ([1, 0], []))
result = bench.run_benchmark(
dataset_path,
hash_bits=64,
topk_values=(1,),
timer_ns=lambda: 0,
)
assert result["metrics"]["1"]["exact_match_rate"] == 0.0
def test_run_benchmark_threaded_numpy_matches_single_worker(tmp_path):
bench = load_sw_benchmark()
dataset_path = tmp_path / "dataset.npz"
_write_dataset(dataset_path)
single = bench.run_benchmark(
dataset_path,
hash_bits=64,
topk_values=(1, 2),
run_id="single",
workers=1,
timer_ns=lambda: 0,
)
threaded = bench.run_benchmark(
dataset_path,
hash_bits=64,
topk_values=(1, 2),
run_id="threaded",
workers=2,
timer_ns=lambda: 0,
)
assert single["mode"] == "software-numpy"
assert threaded["mode"] == "software-numpy"
assert single["params"]["engine"] == "numpy"
assert threaded["params"]["workers"] == 2
assert threaded["metrics"] == single["metrics"]
def test_run_benchmark_rejects_hash_bits_that_do_not_match_npz_width(tmp_path):
bench = load_sw_benchmark()
dataset_path = tmp_path / "dataset.npz"
_write_dataset(dataset_path)
with pytest.raises(ValueError, match="hash_bits"):
bench.run_benchmark(dataset_path, hash_bits=8)
def test_cli_missing_dataset_prints_generation_hint_without_traceback(monkeypatch, capsys):
bench = load_sw_benchmark()
missing_path = Path("outputs/cam_retrieval_benchmark/datasets/cifar100_hash512_rows512_queries8192.npz")
monkeypatch.setattr(
sys,
"argv",
[
"sw_retrieval_benchmark.py",
"--dataset",
str(missing_path),
"--hash-bits",
"512",
"--topk-k",
"5",
"--workers",
"1",
],
)
with pytest.raises(SystemExit) as exc_info:
bench.main()
assert exc_info.value.code == 2
captured = capsys.readouterr()
assert "retrieval dataset not found" in captured.err
assert "python scripts/prepare_cam_retrieval_dataset.py" in captured.err
assert "--dataset cifar100" in captured.err
assert "--num-rows 512" in captured.err
assert "--max-queries 8192" in captured.err
assert "--hash-bits 512" in captured.err
assert "Traceback" not in captured.err
def test_write_outputs_includes_quality_and_performance_csv(tmp_path):
bench = load_sw_benchmark()
result = {
"run_id": "unit-test",
"mode": "software-numpy",
"status": "pass",
"params": {"num_rows": 3, "hash_bits": 64, "topk_k": 2, "workers": 2, "engine": "numpy"},
"dataset": {"num_classes": 2, "num_queries": 2},
"metrics": {
"1": {
"macro_precision": 1.0,
"retrieval_recall": 0.75,
"macro_f1": 0.85,
"exact_match_rate": 1.0,
"recall@k": 1.0,
}
},
"performance": {
"total_elapsed_ns": 2_000,
"total_elapsed_sec": 0.000002,
"ns_per_query": 1_000.0,
"queries_per_second": 1_000_000.0,
},
}
bench.write_outputs(tmp_path, result)
metrics_json = json.loads((tmp_path / "metrics.json").read_text(encoding="utf-8"))
metrics_csv = (tmp_path / "metrics.csv").read_text(encoding="utf-8")
summary_md = (tmp_path / "summary.md").read_text(encoding="utf-8")
assert metrics_json["performance"]["queries_per_second"] == 1_000_000.0
assert "queries_per_second" in metrics_csv
assert "1000000.0" in metrics_csv
assert "Software CAM Retrieval Benchmark Summary" in summary_md
assert "software-numpy" in metrics_csv
assert "workers" in metrics_csv
assert "workers: `2`" in summary_md
assert "queries_per_second" in summary_md