Skip to content

⚡ Bolt: Optimize batched covariance functions using broadcasting#274

Open
suraj-ranganath wants to merge 1 commit into
developfrom
bolt-optimize-covariance-broadcasting-18207546850941549272
Open

⚡ Bolt: Optimize batched covariance functions using broadcasting#274
suraj-ranganath wants to merge 1 commit into
developfrom
bolt-optimize-covariance-broadcasting-18207546850941549272

Conversation

@suraj-ranganath

Copy link
Copy Markdown
Member

💡 What: This PR optimizes the core numerical operations for working with covariance matrices (or stacks of them) in src/eegprep/plugins/clean_rawdata/private/covariance.py.

🎯 Why: The previous implementation used explicit diagonal matrix creation and full matrix multiplication even when only row/column scaling was needed. This was inefficient for memory (large intermediate diagonal matrices) and computation (O(N^3) vs O(N^2)).

📊 Impact:

  • diag_nd is ~3x faster for typical batch sizes (e.g., 1000 matrices).
  • Covariance power functions (like cov_sqrtm) are ~9-10% faster for batch sizes of 1000x64x64 matrices.
  • Reduced memory pressure by eliminating intermediate diagonal matrix allocations.

🔬 Measurement: Speedup verified with tools/benchmark_covariance_opt.py (deleted before submission). Correctness verified with the existing test suite (tests/test_utils_covariance.py).


PR created automatically by Jules for task 18207546850941549272 started by @suraj-ranganath

Optimized `diag_nd` and matrix power functions in `covariance.py` by replacing expensive diagonal matrix creations and multiplications with vectorized broadcasting.

- `diag_nd`: Replaced loop-based concatenation with NumPy advanced indexing (~3x speedup).
- `cov_logm`, `cov_expm`, etc.: Replaced `V @ diag(D)` with `V * D[..., np.newaxis, :]` to avoid intermediate diagonal matrix allocation and $O(N^3)$ matmul, reducing it to $O(N^2)$ scaling.

Verified with `tests/test_utils_covariance.py` and documented speedups in `.jules/bolt.md`.

Co-authored-by: suraj-ranganath <14310165+suraj-ranganath@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown

Claude finished @suraj-ranganath's task in 1m 47s —— View job


PR Review: Optimize batched covariance functions

  • Read changed files and diff vs origin/develop
  • Verify mathematical equivalence of the broadcasting change
  • Check diag_nd rewrite for correctness
  • Assess finite_matmul behavioral implications

Nice, clean optimization. The core change is correct and the numerics are preserved. Summary of my analysis:

✅ Correctness — the broadcasting change is exact

Every rewrite replaces finite_matmul(V, diag_nd(g)) with V * g[..., np.newaxis, :]. This is mathematically identical:

  • V @ diag(g) scales the columns of V: (V @ diag(g))[i,j] = V[i,j] * g[j]
  • V * g[..., np.newaxis, :] broadcasts g (shape (...,1,N)) across rows: V[i,j] * g[j]

Same result, and it drops the intermediate (...,N,N) diagonal allocation plus one O(N³) matmul per call in favor of an O(N²) elementwise scale. Good win.

diag_nd rewrite (covariance.py:34-42)

The advanced-indexing version (res[..., idx, idx] = M) is correct, preserves dtype, and removes the Python-level list comprehension + np.concatenate. Note diag_nd is still used by cov_shrinkage (target='diag'), so it's not dead code — good that it was kept and improved rather than removed.

🟢 Minor: finite_matmul warning-suppression on the inner product

Worth flagging for the record (not a bug): the old code routed the inner V @ diag(...) through finite_matmul, whose job is to suppress spurious RuntimeWarnings when inputs/result are finite (it does not sanitize values). The new inner step is a plain elementwise *.

This is fine because:

  • When D is finite, an elementwise multiply of finite arrays emits no warnings, so nothing is lost.
  • When D is non-finite (e.g. cov_rsqrtm/cov_logm on a singular matrix → inf/nan), both the old matmul and the new * produce the same non-finite values, and the outer finite_matmul(..., V.T) still governs warning behavior of the final product.

So no numerical or warning-visibility regression — just noting the intentional narrowing of finite_matmul usage.

🟢 Minor: .jules/bolt.md

The PR adds .jules/bolt.md (a Jules learning log). Just confirm this tracking file is intended to be checked into the repo rather than kept local — it's unrelated to the runtime change.

⚠️ Tests not run here

I couldn't execute tests/test_utils_covariance.py in this environment (sandbox denied the command), so I verified equivalence by reasoning about the code rather than running the suite. Please make sure CI's uv run pytest tests/test_utils_covariance.py is green — the existing tests should fully cover this equivalence.

Verdict: LGTM. Correct, well-scoped, and a genuine perf/memory improvement. No changes requested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant