Skip to content

Memory-efficient GLM fitting with convolutions and multiple epochs #454

@BalzaniEdoardo

Description

@BalzaniEdoardo

Background

Currently, NeMoS handles convolutions by pre-computing a full design matrix X from basis convolutions, then fitting via loglike(inv_link(X @ w)). This has two efficiency issues:

  1. Memory: For long time series, the design matrix X can be very large
  2. Multiple epoch handling: When fitting discontinuous trials of varying lengths, we currently loop over epochs and convolve one at a time, which is inefficient when convolution happens inside the optimization loop

Proposed solution: Generalized linear operators

Instead of hard-coding the dot product, generalize the GLM objective to:

loglike(inv_link(linear_operator(x, w)))

Where linear_operator can flexibly combine different operations (convolution, dot product, etc.) while maintaining linearity.

Example: For a mixed design with one convolution feature and other regular features:

# Conceptually:
L(x, w) = convolve(x[:, slice_x_1], w[slice_w_1]) + x[:, slice_x_2] @ w[slice_w_2]

# Where:
# - len(slice_w_1) determines convolution kernel size (unrelated to len(slice_x_1))
# - len(slice_x_2) must equal len(slice_w_2) for dot product compatibility

Example 2: Convolution with basis

# B is a basis function kernel of shape (window_size, num_bases)
# Instead of convolving each basis separately and forming design matrix:
#   X = convolve(x, B[:, 0]) | convolve(x, B[:, 1]) | ... (large matrix)
# We compute: convolve(x, B @ w) directly

lin_operator = lambda x, w: convolve(x, B @ w)
L(x, w) = lin_operator(x[:, slice_x_1], w[slice_w_1]) + x[:, slice_x_2] @ w[slice_w_2]

Advantages:

  • Memory: O(n log n) convolution without forming large design matrix
  • Speed: FFT-based convolution is faster than matrix multiplication
  • Flexibility: Can mix convolution with other linear operations

Challenge: Multiple epochs with varying lengths

People fit discontinuous time series where each epoch represents a trial. Trials may vary in duration. Currently using pynapple to chunk time series and convolving one trial at a time in a loop - fine for one-time convolution, but inefficient inside optimization.

Proposed strategy (discussed with Sarah):

  • Group epochs by similar length
  • Pad within groups to enable vectorized convolution
  • Loop over small number of groups instead of all epochs

Open questions:

  • How many groups is optimal?
  • What's a good criterion for "similar enough" length? (e.g., within 10%? fixed bin sizes?)
  • Trade-off between padding overhead vs. number of groups

Design considerations

Interface challenge: How do users specify custom linear operators intuitively?

Compatibility: Need to maintain current flexibility where:

  • GLM is agnostic about design matrix
  • Basis objects handle their own transformations
  • Users can provide arbitrary designs

Software engineering needs:

  1. Refactor GLM internals to support pluggable linear operators
  2. Implement efficient multi-epoch batching/grouping
  3. Design user-friendly API for specifying operators

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions