Skip to content

Commit 7c80992

Browse files
authored
Add Nx CPU benchmarks and refresh docs (#1)
* Add nx benchmarks for cpu * Bump package version to 0.2.0 and elixir version to 1.19 * Fix clippy bug
1 parent 7f4105a commit 7c80992

15 files changed

Lines changed: 336 additions & 113 deletions

README.md

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,18 @@ end
3535
### System Requirements
3636

3737
- **macOS (Apple Silicon)**: Uses Accelerate framework (no additional setup needed)
38-
- **Linux x86_64**: Requires OpenBLAS and AVX2 support
38+
- **Linux x86_64**: Requires OpenBLAS; AVX2 is recommended for SIMD speedups
3939
```bash
4040
# Ubuntu/Debian
4141
sudo apt-get install libopenblas-dev
4242

4343
# Fedora/RHEL
4444
sudo dnf install openblas-devel
4545
```
46+
For source builds that should use AVX2, set:
47+
```bash
48+
export RUSTFLAGS="-C target-feature=+avx2"
49+
```
4650

4751
### Rust Toolchain
4852

@@ -73,6 +77,7 @@ docs = Nx.tensor([
7377
scores = ExMaxsimCpu.maxsim_scores(query, docs)
7478
# => #Nx.Tensor<f32[2]>
7579
```
80+
Note: MaxSim expects L2-normalized embeddings. Normalize query and docs along the token axis before scoring.
7681

7782
### Variable-Length Documents
7883

@@ -138,34 +143,50 @@ For additional performance on Intel CPUs, you can enable libxsmm:
138143

139144
## Benchmarks
140145

141-
ExMaxsimCpu delivers exceptional performance, **even outperforming GPU-accelerated alternatives**:
146+
ExMaxsimCpu delivers strong CPU performance, especially on small-to-moderate batch sizes:
147+
148+
### Benchmark Environment
149+
150+
- OS: macOS 26.2 (25C56)
151+
- CPU: Apple M4 Pro (12 physical / 12 logical cores)
152+
- Architecture: arm64
153+
- Elixir: 1.19.5 (OTP 28)
154+
- BLAS: Accelerate
155+
- Env: OPENBLAS_NUM_THREADS=1, RAYON_NUM_THREADS=8
156+
- Nx CPU backend: Torchx (CPU)
142157

143158
### Performance Summary
144159

145160
| Implementation | Typical Latency | vs ExMaxsimCpu |
146161
|---------------|-----------------|----------------|
147-
| **ExMaxsimCpu** (BLAS+SIMD) | 0.09 - 0.28 ms ||
148-
| Nx + Torchx MPS (Apple GPU) | 0.86 - 4.43 ms | ~14x slower |
149-
| Nx BinaryBackend (pure Elixir) | 426 - 5,842 ms | ~13,000x slower |
162+
| **ExMaxsimCpu** (BLAS+SIMD) | 0.08 - 0.28 ms ||
163+
| Nx CPU backend (Torchx) | 0.20 - 0.89 ms | ~4x slower |
164+
| Nx + Torchx MPS (Apple GPU) | 1.09 - 3.24 ms | ~15x slower |
165+
| Nx BinaryBackend (unaccelerated) | 386 - 5,038 ms | ~13,800x slower |
150166

151-
> **Key insight:** ExMaxsimCpu is faster than GPU acceleration! The combination of optimized BLAS (Accelerate/OpenBLAS) and hand-tuned SIMD (AVX2/NEON) outperforms even Apple's Metal Performance Shaders.
167+
Notes:
168+
- Nx BinaryBackend is an unaccelerated baseline; EXLA/Torchx CPU will be faster.
169+
- Nx CPU backend results (EXLA or Torchx) are included when available.
170+
- MPS timings are for small shapes where transfer overhead can dominate.
171+
- Results are machine- and shape-dependent; run your own benchmarks for production sizing.
152172

153-
### Three-Way Performance Comparison
173+
### Performance Comparison
154174

155175
![Three-Way Comparison](assets/benchmark_three_way.png)
156176

157-
*Benchmark on Apple M4 Pro, showing ExMaxsimCpu (blue) vs Nx+MPS GPU (green) vs Nx BinaryBackend (orange) on a log scale.*
177+
*Benchmark on Apple Silicon (this run), showing ExMaxsimCpu (blue), Nx BinaryBackend (orange), and optional Nx CPU/MPS backends on a log scale.*
158178

159179
### Speedup Analysis
160180

161181
| Comparison | Speedup Factor |
162182
|------------|----------------|
163-
| vs Nx BinaryBackend | **4,000x - 19,000x** |
164-
| vs Nx + Torchx MPS (GPU) | **6x - 29x** |
183+
| vs Nx BinaryBackend | **4,100x - 24,200x** |
184+
| vs Nx CPU backend (Torchx) | **2.1x - 6.2x** |
185+
| vs Nx + Torchx MPS (GPU) | **7.5x - 24.6x** |
165186

166187
![Speedup Comparison](assets/benchmark_speedup.png)
167188

168-
### Why is CPU faster than GPU?
189+
### Why can CPU win on these shapes?
169190

170191
1. **Optimized BLAS**: Uses Apple Accelerate (or OpenBLAS on Linux) which is highly tuned for the CPU architecture
171192
2. **SIMD Instructions**: Hand-optimized AVX2/NEON code for max-reduction operations
@@ -190,7 +211,7 @@ ExMaxsimCpu delivers exceptional performance, **even outperforming GPU-accelerat
190211

191212
### Running Benchmarks
192213

193-
Generate benchmark data (includes MPS GPU comparison on Apple Silicon):
214+
Generate benchmark data (includes optional MPS and Nx CPU backends when available):
194215
```bash
195216
OPENBLAS_NUM_THREADS=1 mix run bench/generate_plots.exs
196217
```
@@ -200,7 +221,7 @@ Generate plots:
200221
uv run bench/plot_benchmarks.py
201222
```
202223

203-
> **Note:** For MPS benchmarks, ensure `torchx` is installed. The benchmark automatically detects GPU availability.
224+
> **Note:** For MPS benchmarks, ensure `torchx` is installed. For Nx CPU backends, install `torchx` or add `exla` to your dev deps. The benchmark auto-detects availability.
204225
205226
## Platform Support
206227

assets/benchmark_d_len.png

2.06 KB
Loading

assets/benchmark_data.csv

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
config,param_value,ex_time_ms,nx_time_ms,mps_time_ms,mps_transfer_time_ms,speedup_vs_nx,speedup_vs_mps
2-
n_docs,10,0.1234,426.714,0.862,1.257,3457.97,6.99
3-
n_docs,25,0.1808,1076.1877,4.4306,2.1413,5952.37,24.51
4-
n_docs,50,0.1732,2176.558,2.5852,2.7917,12566.73,14.93
5-
n_docs,100,0.2842,5397.9557,2.6614,3.1407,18993.51,9.36
6-
d_len,32,0.0929,1037.171,2.593,2.7057,11164.38,27.91
7-
d_len,64,0.1801,2149.108,1.7498,2.1703,11932.86,9.72
8-
d_len,128,0.246,5842.7707,1.6214,1.865,23751.1,6.59
9-
dim,64,0.178,1197.7157,2.3052,2.2873,6728.74,12.95
10-
dim,128,0.2202,2253.2173,2.6466,1.6743,10232.59,12.02
11-
dim,256,0.1771,4542.2847,2.8658,3.8633,25648.13,16.18
1+
config,param_value,ex_time_ms,nx_time_ms,nx_cpu_time_ms,nx_cpu_backend,mps_time_ms,mps_transfer_time_ms,speedup_vs_nx,speedup_vs_nx_cpu,speedup_vs_mps
2+
n_docs,10,0.0936,386.2173,0.198,Torchx (CPU),1.087,1.9057,4126.25,2.12,11.61
3+
n_docs,25,0.1231,970.5887,0.427,Torchx (CPU),1.376,2.182,7884.55,3.47,11.18
4+
n_docs,50,0.1502,1990.481,0.5047,Torchx (CPU),2.1504,2.3657,13252.2,3.36,14.32
5+
n_docs,100,0.2561,4990.392,0.8927,Torchx (CPU),3.2372,3.5997,19486.11,3.49,12.64
6+
d_len,32,0.0836,992.1037,0.489,Torchx (CPU),2.0586,2.6113,11867.27,5.85,24.62
7+
d_len,64,0.1499,2025.9087,0.64,Torchx (CPU),2.2548,2.2373,13515.07,4.27,15.04
8+
d_len,128,0.2753,5037.7807,0.7853,Torchx (CPU),2.0598,2.2373,18299.24,2.85,7.48
9+
dim,64,0.084,1069.5737,0.5213,Torchx (CPU),1.7228,2.34,12733.02,6.21,20.51
10+
dim,128,0.1691,2114.0027,0.6673,Torchx (CPU),2.227,2.3307,12501.49,3.95,13.17
11+
dim,256,0.1661,4027.811,0.758,Torchx (CPU),2.882,3.2097,24249.31,4.56,17.35

assets/benchmark_dim.png

3.83 KB
Loading

assets/benchmark_n_docs.png

2.98 KB
Loading

assets/benchmark_speedup.png

4.79 KB
Loading

assets/benchmark_summary.png

7.52 KB
Loading

assets/benchmark_three_way.png

-915 Bytes
Loading

bench/benchmark.exs

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,47 @@ IO.puts("======================\n")
1010

1111
# Generate random normalized vectors
1212
defmodule BenchHelper do
13+
# Suppress compile warnings for optional backends
14+
@compile {:no_warn_undefined, Torchx}
15+
@compile {:no_warn_undefined, EXLA}
16+
1317
def random_normalized(shape) do
1418
key = Nx.Random.key(System.unique_integer([:positive]))
1519
{tensor, _key} = Nx.Random.normal(key, shape: shape, type: :f32)
1620
norms = Nx.sqrt(Nx.sum(Nx.pow(tensor, 2), axes: [-1], keep_axes: true))
1721
Nx.divide(tensor, norms)
1822
end
23+
24+
def torchx_cpu_backend do
25+
with true <- Code.ensure_loaded?(Torchx),
26+
{:ok, _apps} <- Application.ensure_all_started(:torchx) do
27+
{Torchx.Backend, device: :cpu}
28+
else
29+
_ -> nil
30+
end
31+
end
32+
33+
def exla_cpu_backend do
34+
with true <- Code.ensure_loaded?(EXLA),
35+
{:ok, _apps} <- Application.ensure_all_started(:exla) do
36+
{EXLA.Backend, client: :host}
37+
else
38+
_ -> nil
39+
end
40+
end
41+
42+
def cpu_backend do
43+
case exla_cpu_backend() do
44+
nil ->
45+
case torchx_cpu_backend() do
46+
nil -> nil
47+
backend -> {"Torchx (CPU)", backend}
48+
end
49+
50+
backend ->
51+
{"EXLA (CPU)", backend}
52+
end
53+
end
1954
end
2055

2156
defmodule NxReference do
@@ -44,6 +79,14 @@ end
4479
IO.puts("Generating test data...")
4580
query = BenchHelper.random_normalized({32, 128})
4681
docs = BenchHelper.random_normalized({100, 64, 128})
82+
cpu_backend = BenchHelper.cpu_backend()
83+
84+
if cpu_backend do
85+
{label, _backend} = cpu_backend
86+
IO.puts("✓ Nx CPU backend available: #{label}")
87+
else
88+
IO.puts("⚠ Nx CPU backend not available (install torchx or exla)")
89+
end
4790

4891
# Correctness check
4992
IO.puts("\nCorrectness check:")
@@ -59,18 +102,29 @@ IO.puts(" Status: #{if max_diff < 1.0e-4, do: "✓ PASS", else: "✗ FAIL"}")
59102
# Benchmark comparison
60103
IO.puts("\nPerformance comparison (32 query tokens, 128 dim, 64 doc tokens, 100 docs):")
61104

62-
Benchee.run(
63-
%{
64-
"ExMaxsimCpu (BLAS+SIMD)" => fn ->
65-
ExMaxsimCpu.maxsim_scores(query, docs)
66-
end,
67-
"Pure Nx Reference" => fn ->
68-
NxReference.maxsim_scores(query, docs)
69-
end
70-
},
71-
warmup: 1,
72-
time: 3
73-
)
105+
benchmarks = %{
106+
"ExMaxsimCpu (BLAS+SIMD)" => fn ->
107+
ExMaxsimCpu.maxsim_scores(query, docs)
108+
end,
109+
"Nx BinaryBackend" => fn ->
110+
NxReference.maxsim_scores(query, docs) |> Nx.to_binary()
111+
end
112+
}
113+
114+
benchmarks =
115+
if cpu_backend do
116+
{label, backend} = cpu_backend
117+
query_cpu = Nx.backend_transfer(query, backend)
118+
docs_cpu = Nx.backend_transfer(docs, backend)
119+
120+
Map.put(benchmarks, "Nx #{label}", fn ->
121+
NxReference.maxsim_scores(query_cpu, docs_cpu) |> Nx.to_binary()
122+
end)
123+
else
124+
benchmarks
125+
end
126+
127+
Benchee.run(benchmarks, warmup: 1, time: 3)
74128

75129
# Larger scale benchmark (ExMaxsimCpu only - Nx reference is too slow)
76130
IO.puts("\n\nLarger scale benchmark (ExMaxsimCpu only):")

0 commit comments

Comments
 (0)