-
Notifications
You must be signed in to change notification settings - Fork 0
β‘ Bolt: Replace array squared norm sums with np.einsum for faster execution #182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
afa12e2
946bf66
f38cfc2
f4f0af2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| ## 2024-05-18 - Fast row-wise Euclidean norm in pure NumPy | ||
| **Learning:** In performance-critical paths, computing the batch norm of a 2D array via `np.linalg.norm(arr, axis=1)` is relatively slow. Using `np.sqrt(np.einsum('ij,ij->i', arr, arr))` is significantly faster (~4x speedup on a laptop CPU for typical batch sizes). If `keepdims=True` behavior is needed, appending `[:, np.newaxis]` matches the original shape seamlessly. | ||
| **Action:** Always prefer `np.sqrt(np.einsum('ij,ij->i', arr, arr))` over `np.linalg.norm(arr, axis=1)` when computing row-wise vector norms in NumPy to eliminate dispatch overhead and improve execution speed. | ||
| ## 2024-08-08 - Use np.einsum for row-wise vector norms and differences | ||
| **Learning:** In performance-critical paths, calculating row-wise squared Euclidean norms with `(X ** 2).sum(axis=1)` causes intermediate arrays to be allocated which is slower. `np.einsum('ij,ij->i', X, X)` avoids this and provides a ~3-5x execution speedup. Same logic applies for calculating the difference first before squaring. | ||
| **Action:** Always prefer `np.einsum('ij,ij->i', X, X)` over `(X ** 2).sum(1)` when computing row-wise squared vector norms in NumPy, especially when running over batches of data to avoid unnecessary allocations and boost performance. | ||
|
Comment on lines
+4
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π Performance & Scalability | π‘ Minor | β‘ Quick win π§© Analysis chainπ Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- file ---'
cat -n .jules/bolt.md
printf '%s\n' '--- related NumPy guidance and usages ---'
rg -n -i --glob '!node_modules' --glob '!dist' 'einsum|row-wise|squared Euclidean|Always prefer|\(X \*\* 2\)' .Repository: stffns/snapvec Length of output: 6171 π Script executed: #!/bin/bash
set -eu
cat -n .jules/bolt.md
rg -n -i 'einsum|row-wise|squared Euclidean|Always prefer|\(X \*\* 2\)' .Repository: stffns/snapvec Length of output: 6116 π Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- documented benchmark evidence ---'
sed -n '175,205p' CHANGELOG.md
fd -i 'bench.*einsum|einsum.*bench' experiments . --type f 2>/dev/null | head -20
printf '%s\n' '--- relevant implementation context ---'
sed -n '295,320p' snapvec/_pq.py
sed -n '45,65p' snapvec/_kmeans.py
printf '%s\n' '--- standalone NumPy timing probe ---'
python3 - <<'PY'
import platform
import time
import numpy as np
print("numpy", np.__version__, "python", platform.python_version())
rng = np.random.default_rng(0)
cases = [
("f32-C", np.array(rng.standard_normal((4096, 128)), dtype=np.float32, order="C")),
("f32-F", np.array(rng.standard_normal((4096, 128)), dtype=np.float32, order="F")),
("f64-C", np.array(rng.standard_normal((4096, 128)), dtype=np.float64, order="C")),
("f64-F", np.array(rng.standard_normal((4096, 128)), dtype=np.float64, order="F")),
("f32-strided", np.array(rng.standard_normal((4096, 256)), dtype=np.float32)[:, ::2]),
]
for name, x in cases:
einsum = lambda: np.einsum("ij,ij->i", x, x)
summed = lambda: (x ** 2).sum(axis=1)
np.testing.assert_allclose(einsum(), summed(), rtol=1e-5 if x.dtype == np.float32 else 1e-12)
for fn in (einsum, summed):
for _ in range(5):
fn()
def median_time(fn):
samples = []
for _ in range(15):
t0 = time.perf_counter()
fn()
samples.append(time.perf_counter() - t0)
return np.median(samples)
te = median_time(einsum)
ts = median_time(summed)
print(f"{name:12s} shape={x.shape!s:14s} dtype={x.dtype} "
f"C={x.flags.c_contiguous} F={x.flags.f_contiguous} "
f"einsum={te*1e6:9.1f}us sum={ts*1e6:9.1f}us ratio(sum/einsum)={ts/te:5.2f}x")
PYRepository: stffns/snapvec Length of output: 3640 π Script executed: #!/bin/bash
set -eu
sed -n '175,205p' CHANGELOG.md
fd -i 'bench.*einsum|einsum.*bench' experiments . --type f 2>/dev/null | head -20
sed -n '295,320p' snapvec/_pq.py
sed -n '45,65p' snapvec/_kmeans.py
python3 - <<'PY'
import platform, time
import numpy as np
print("numpy", np.__version__, "python", platform.python_version())
rng = np.random.default_rng(0)
cases = [
("f32-C", np.array(rng.standard_normal((4096,128)), dtype=np.float32, order="C")),
("f32-F", np.array(rng.standard_normal((4096,128)), dtype=np.float32, order="F")),
("f64-C", np.array(rng.standard_normal((4096,128)), dtype=np.float64, order="C")),
("f64-F", np.array(rng.standard_normal((4096,128)), dtype=np.float64, order="F")),
("f32-strided", np.array(rng.standard_normal((4096,256)), dtype=np.float32)[:,::2]),
]
for name, x in cases:
e = lambda: np.einsum("ij,ij->i", x, x)
s = lambda: (x ** 2).sum(axis=1)
np.testing.assert_allclose(e(), s(), rtol=1e-5 if x.dtype == np.float32 else 1e-12)
for fn in (e, s):
for _ in range(5): fn()
def med(fn):
a=[]
for _ in range(15):
t=time.perf_counter(); fn(); a.append(time.perf_counter()-t)
return np.median(a)
te, ts = med(e), med(s)
print(name, x.shape, x.dtype, x.flags.c_contiguous, x.flags.f_contiguous,
f"{te*1e6:.1f}us", f"{ts*1e6:.1f}us", f"{ts/te:.2f}x")
PYRepository: stffns/snapvec Length of output: 3524 Replace the universal performance guidance with benchmark-based wording. The documented benchmark covers a different comparison and does not support a general π€ Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Maintainability & Code Quality | π‘ Minor | β‘ Quick win
Add blank lines around the new heading.
markdownlint-cli2reports MD022 because Line 4 has no blank line before or after it. Add one blank line on each side of the heading.π§° Tools
πͺ markdownlint-cli2 (0.23.2)
[warning] 4-4: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above
(MD022, blanks-around-headings)
[warning] 4-4: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
π€ Prompt for AI Agents
Source: Linters/SAST tools