This guide explains how to benchmark the indexing pipeline performance and measure the impact of the optimizations applied.
The accelerator harness is optional development tooling and validates every
result with its packaged Draft 2020-12 schema before writing or comparing it.
Prepare its runtime with uv sync --extra benchmarks (or install the equivalent
.[benchmarks] extra). Contributors using uv sync --extra dev also receive the
validator through the development extra.
Important
Historical speedups, "Expected Output," recommendations, and utilization figures
in this guide are illustrative examples, not checked-in measurements. They must
not be used to claim that a backend or batch size is qualified. The versioned
accelerator harness below is the comparable evidence format; its
example-accelerator-v1.json file is explicitly synthetic.
The accelerator benchmark contract separates environment, fixture identity, cold start, warm throughput, p50/p95 latency, peak RSS, reliability counters, and numerical agreement. Its synthetic fixtures include short, medium, long, and oversized code/prose inputs and are identified by a content digest. Results with a different schema version or fixture digest cannot be compared.
Run the deterministic CPU contract baseline (no model download):
PYTHONPATH="$PWD/src" python scripts/benchmark_accelerators.py \
--output /tmp/arcaneum-accelerator-cpu.json \
--summary /tmp/arcaneum-accelerator-cpu.txtCompare two compatible results:
PYTHONPATH="$PWD/src" python scripts/benchmark_accelerators.py --compare \
baseline.json candidate.jsonThe canonical schema is packaged at
arcaneum/benchmarks/schemas/accelerator-result-v1.schema.json; the input manifest is
benchmarks/fixtures/accelerator-v1/manifest.json. Ordinary CI runs only the CPU
contract baseline. CUDA, MPS, and CoreML measurements are deliberately opt-in.
MLX currently runs an offline feasibility inventory rather than inference. A
backend result must name its hardware, OS, model, precision, and dependency
versions; throughput alone is insufficient without correctness and reliability.
MPS remains experimental. Run its opt-in qualification through the spawned worker:
PYTHONPATH="$PWD/src" python scripts/benchmark_accelerators.py \
--backend mps --model jina-code-st --iterations 5 --soak-batches 10000 \
--output benchmarks/results/mps-$(uname -m).jsonThe runner compares the same cached model in CPU and MPS worker processes, records
cold/warm throughput, numerical agreement, RSS and MPS driver memory, and retains
an experimental decision unless speedup is at least 1.25x and all 10,000 soak
batches complete. It refuses an unsafe
PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0. Model, dependency, or MPS unavailability
produces an inconclusive artifact rather than a false pass. Install the
sentence-transformers extra and rerun on an MPS-capable host.
CUDA also remains experimental until a concrete hardware/model/precision combination passes the qualification gates. Run it through the persistent spawned worker with a cached model:
PYTHONPATH="$PWD/src" python scripts/benchmark_accelerators.py \
--backend cuda --model jina-code-st --iterations 5 \
--soak-texts 100000 --token-budget 8192 --batch-size 8 \
--output benchmarks/results/cuda-$(uname -m).jsonThe result records the NVIDIA device, UUID, driver, compute capability, CUDA runtime, model and dependency versions, cold/warm throughput, CPU numerical agreement, parent RSS, CUDA allocated/reserved peaks, and the configured token/shape budget. The OOM policy makes at most two reduced-batch retries inside the worker; any timeout or unhandled OOM reaps the worker and leaves the run inconclusive. Qualification does not restart a failed worker or hide a failure with CPU fallback.
A combination is qualified only with at least 1.25x same-model CPU throughput,
cosine agreement of at least 0.999, zero failures/OOM retries, and either 100,000
completed soak texts or three elapsed soak hours. Use --soak-seconds 10800 for the
time gate; when both soak flags are supplied, both requested amounts are run even
though either promotion gate suffices. On a host without usable CUDA or a complete
cached model, the command writes an explicit inconclusive/experimental artifact
with zero throughput and the exact setup failure. It never invents measurements.
CoreML is opt-in and remains experimental. The runner uses a locally cached FastEmbed model, requests MLProgram with static inputs, FastPrediction specialization, compute-plan profiling, and a compiled cache outside the checkout. It measures cold construction/compilation separately from warm inference, pads fixed batch-count buckets, restores output order, compares with ONNX CPU, and reports CoreML-only, hybrid CoreML/CPU, or unknown provider placement. Tokenizer sequence shapes remain dynamic; the result records that limitation explicitly.
ARC_RUN_COREML_QUALIFICATION=1 PYTHONPATH="$PWD/src" \
python scripts/benchmark_accelerators.py --backend coreml --model bge-small \
--coreml-cache-dir "$HOME/.cache/arcaneum/coreml" \
--output benchmarks/results/coreml-local.jsonThe checked-in Apple Silicon probe was hybrid and 0.985x as fast as CPU. ONNX Runtime rejected the model's 30,522-row embedding table for CoreML, created no compiled cache entry, and fell back to CPU for unsupported nodes. Numerical output matched CPU exactly, but the speed, placement, and soak gates did not pass.
MLX is unavailable in runtime policy. Its offline probe inventories installed packages, pinned source snapshots, and converted assets without downloads:
PYTHONPATH="$PWD/src" python scripts/benchmark_accelerators.py \
--backend mlx --output benchmarks/results/mlx-local.jsonThe checked-in artifact records defer because no MLX runtime or converted
strategic-model assets were available. It contains no inferred throughput or
correctness result. See the model assessment.
Two benchmarking scripts are available:
benchmark_indexing.py- Embedding generation performancebenchmark_pdf_indexing.py- Full PDF indexing pipeline
Tests embedding throughput with different batch sizes and GPU/CPU comparison.
# Benchmark with batch sizes 256, 512, 1024
python scripts/benchmark_indexing.py --benchmark embeddings
# With output file
python scripts/benchmark_indexing.py --benchmark embeddings --output embedding_results.json --report embedding_report.txt
# CPU only (no GPU)
python scripts/benchmark_indexing.py --no-gpu--benchmark: Type of benchmark (embeddings, gpu-vs-cpu, full)--model: Embedding model (default: qwen3-embed)--batch-sizes: Comma-separated batch sizes (default: 256,512,1024)--num-texts: Number of texts to embed (default: 10000)--samples: Number of samples per batch size (default: 3)--no-gpu: Disable GPU acceleration--output: JSON output file--report: Text report file--verbose: Verbose output
The legacy script is useful for exploratory local profiling, but its output is not capability evidence. Do not copy its recommendations or speed ratios into user guidance; record comparable results with the versioned harness instead.
Tests full PDF indexing pipeline performance.
# Benchmark with existing PDFs
python scripts/benchmark_pdf_indexing.py --pdf-dir ./test_pdfs
# Generate synthetic PDFs and benchmark
python scripts/benchmark_pdf_indexing.py --generate-test-pdfs 10
# Compare batch sizes
python scripts/benchmark_pdf_indexing.py --generate-test-pdfs 10 --batch-sizes 300,500,1000
# With detailed reporting
python scripts/benchmark_pdf_indexing.py \
--pdf-dir ./test_pdfs \
--output pdf_results.json \
--report pdf_report.txt \
--verbose--pdf-dir: Directory with PDF files--generate-test-pdfs: Generate N synthetic PDFs--pages-per-pdf: Pages per synthetic PDF (default: 5)--model: Embedding model (default: qwen3-embed)--batch-size: Qdrant upload batch size (default: 300)--embedding-batch-size: Embedding batch size (default: 256)--batch-sizes: Compare multiple batch sizes--no-gpu: Disable GPU acceleration--output: JSON output file--report: Text report file--verbose: Verbose output
PDF INDEXING BENCHMARK REPORT
================================================================================
INDEXING PERFORMANCE
--------------------------------------------------------------------------------
Files indexed: 10/10
Chunks created: 5432
Total time: 12.34s
Throughput: 440.5 chunks/sec
2,658 MB/min
PER-FILE AVERAGES
--------------------------------------------------------------------------------
Time per PDF: 1.23s
Chunks per PDF: 543
================================================================================
To measure the impact of the performance optimizations, compare before and after:
# Generate test PDFs (10 PDFs × 10 pages = ~500 chunks expected)
python scripts/benchmark_pdf_indexing.py \
--generate-test-pdfs 10 \
--pages-per-pdf 10 \
--output baseline.json \
--report baseline_report.txt
# Illustrative metrics to track:
# - Total indexing time (seconds)
# - Chunks per second
# - GPU utilization (check nvidia-smi during run)
# - Memory usageThe following optimizations have been applied:
-
✅ GPU Thread Lock Removal (arcaneum-m7hg)
- Expected: 20-30% speedup when GPU + multi-file workers
- Measure: Run with
--embedding-batch-size 500and compare times
-
✅ Connection Pooling (arcaneum-ezd8)
- Expected: 10-20% speedup on uploads
- Already implemented, no measurable change expected (already optimal)
-
✅ Garbage Collection Optimization (arcaneum-d432)
- Expected: 2-5% speedup on large runs (100k+ chunks)
- Measure: Use
--generate-test-pdfs 100for noticeable impact
-
✅ Batch Size Tuning (arcaneum-9kgg)
- Expected: 5-15% speedup with batch_size=256 vs 200
- Measure: Use embedding benchmark
For detailed CPU/memory profiling:
# Profile with cProfile
python -m cProfile -s cumtime scripts/benchmark_pdf_indexing.py \
--generate-test-pdfs 10 > profile.txt
# Profile with py-spy (real-time flamegraph)
py-spy record -o profile.svg -- python scripts/benchmark_pdf_indexing.py \
--generate-test-pdfs 10
# Memory profiling
pip install memory-profiler
python -m memory_profiler scripts/benchmark_pdf_indexing.py \
--generate-test-pdfs 10Based on analysis of FastEmbed and open-source implementations:
| Component | Old Default | Optimized | Rationale |
|---|---|---|---|
| Embedding batch size | 200 | 256 | FastEmbed ONNX default (proven optimal) |
| Upload batch size | 100 | 300 | 3x improvement without memory issues |
To test other batch sizes:
# Test batch sizes 128, 256, 512, 1024
python scripts/benchmark_indexing.py \
--batch-sizes 128,256,512,1024 \
--num-texts 20000Check GPU usage during benchmarking:
# In separate terminal, monitor GPU
watch -n 0.1 nvidia-smi
# Or on Apple Silicon
sudo powermetrics --samplers gpu_power,gpu_frequency --show-empty-samplesIllustrative GPU metrics (not qualification evidence):
- GPU utilization: 80-95%
- Memory: 2-8 GB (depending on model and batch size)
- Power: 15-30W (GPU portion)
After running benchmarks, analyze:
- Throughput: chunks/sec should increase with optimizations
- GPU Utilization: Should remain high (>80%)
- Batch Size Impact: Larger batches (256-512) should be faster
- Multi-file Parallelism: Should scale with file workers (arcaneum-m7hg fix)
- Memory Stability: Memory usage should remain steady during long runs
The following historical estimates are unverified and retained only as context; use versioned benchmark results for decisions:
| Optimization | Speedup | Notes |
|---|---|---|
| GPU thread lock removal | 20-30% | With multi-file workers + GPU |
| Connection pooling | 10-20% | Already implemented |
| GC optimization | 2-5% | Scales with chunk count |
| Batch size tuning | 5-15% | Embedding + upload batches |
| Total Combined | 30-50% | Real-world impact depends on hardware |
# Install dependencies
pip install reportlab # For PDF generation
pip install psutil # For memory tracking
pip install py-spy # For profiling- "GPU not available" - Check CUDA/MPS installation
- "No PDF files found" - Use
--generate-test-pdfsto create test data - "Qdrant connection failed" - Qdrant uses in-memory database for benchmarking
- "Memory error" - Reduce
--num-textsor--generate-test-pdfscount
For real-world testing with arc CLI:
# Benchmark arc pdf index with monitoring
time arc index pdf \
--path ./test_pdfs \
--collection benchmark \
--model qwen3-embed \
--embedding-batch-size 256 \
--verboseMonitor during run:
- CPU:
top -o %CPU | head -20 - GPU:
watch nvidia-smiorpowermetrics - Disk I/O:
iostat -x 1