Skip to content

[PERF] Memory Profiling Baseline for O(N^3) EDoF Calculations #575

Description

@hritikkumarpradhan

Description

While preparing my GSoC 2026 proposal focused on optimizing pyGAM's matrix operations, I profiled the GAM._pirls optimization loop and isolated a severe $O(N^3)$ memory bottleneck.

When fitting overparameterized models (where the number of splines/features $p$ exceeds the number of samples $n$), the Effective Degrees of Freedom (EDoF) calculation creates a massive intermediate dense matrix that scales quadratically with $p$, causing massive RAM spikes and fatal MemoryError crashes on standard hardware.

The Bottleneck

The leak originates during the hat matrix trace extraction. The current codebase calculates this via cross-multiplication:
edof = np.diagonal(U1.dot(U1.T))

If $p = 10,000$, U1.dot(U1.T) forces the allocation of a $10,000 \times 10,000$ float64 matrix before immediately discarding everything except the diagonal.

Memory Profiling Proof

I wrote a synthetic benchmark ($n=500, p=10000$) to isolate this exact line using memory_profiler.

As shown below, the legacy operation triggers an immediate ~760 MiB memory spike. I have also profiled my proposed GSoC fix, which replaces the dense matrix creation with a vectorized $O(N)$ tensor-sum: (U1**2).sum(axis=1). This drops the overhead to near zero while preserving mathematical equivalence.

--- Running Legacy Calculation ---
Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    22     76.4 MiB      0.0 MiB           1       start_time = time.time()
    24                                             # The bottleneck:
    25    839.5 MiB    763.1 MiB           1       edof = np.diagonal(u1_matrix.dot(u1_matrix.T))
    27    839.5 MiB      0.0 MiB           1       end_time = time.time()

--- Running Proposed Vectorized Fix ---
Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    38     76.5 MiB      0.0 MiB           1       start_time = time.time()
    40                                             # The fix:
    41    114.6 MiB     38.1 MiB           1       edof = (u1_matrix**2).sum(axis=1)
    43    114.6 MiB      0.0 MiB           1       end_time = time.time()

Next Steps

I have already contributed this specific stress-test as an EDoFBenchmark class to the ASV automated benchmarking suite currently being built in PR #503.

I am opening this issue to formally track the algorithmic bottleneck itself. I plan to submit the vectorized (U1**2).sum(axis=1) fix as a core part of my GSoC 2026 project deliverables.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions