Skip to content

Avoid copying logk for the normal systematic type - #152

Open
davidwalter2 wants to merge 1 commit into
WMass:mainfrom
davidwalter2:260826_logkNoCopy
Open

Avoid copying logk for the normal systematic type#152
davidwalter2 wants to merge 1 commit into
WMass:mainfrom
davidwalter2:260826_logkNoCopy

Conversation

@davidwalter2

@davidwalter2 davidwalter2 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

For systematic_type == "normal", _init_logk_scaled absorbed the param-model factor rnorm_init into logk at construction time. That allocates a second full-size copy of the [nbins, nproc, nsyst] tensor and, in sparse mode, rebuilds the CSR matrix.

rnorm_init carries no systematic index, so it factors straight out of the contraction over systematics:

sum_s (rnorm_init[b,p] * logk[b,p,s]) * theta[s]
  == rnorm_init[b,p] * sum_s logk[b,p,s] * theta[s]

This keeps logk / logk_csr aliased to the indata tensors and applies the factor to the [nbins, nproc] contraction result instead of the [nbins, nproc, nsyst] input.

Only the normal systematic type is affected; log_normal already carried the scaling multiplicatively and takes the same early return as before.

Measured effect

The scaling now runs once per loss/HVP evaluation instead of once over the full tensor at construction, so it is worth checking that this does not cost more than it saves. Measured on a 10000-bin x 2048-systematic normal-type model (logk = 819 MB), with --minimizerMaxiter pinned so both versions do matched work:

peak RSS wall time n_hvp
before 2115 +- 211 MB 178 +- 36 s 1100
after 1350 +- 18 MB 190 +- 95 s 1089
  • Memory: 766 +- 122 MB saved (a 6.3 sigma effect), matching the 819 MB logk tensor -- i.e. the copy is gone.
  • Runtime: neutral. The wall-time difference is 12 +- 59 s, consistent with zero. Call counts are deterministic and differ by 1% (1089 vs 1100), the expected small trajectory shift from the reassociation.

The per-call cost is one elementwise multiply on [nbins, nproc], a factor nsyst smaller than the matmul it follows, and it lands on data already in cache.

The saving scales with the tensor: for a 100k-bin x 8192-systematic model the avoided copy is ~33 GB. In sparse mode the win is structural rather than just an allocation -- the old path rebuilt the CSR matrix via CSRSparseMatrix(...), which is replaced by a one-time gather_nd onto the [norm_nnz] layout the CSR contraction already returns.

rnorm_init remains a constant evaluated at xparamdefault, so the loss stays strictly linear in theta and derivatives are unchanged.

Validation

Compared against unpatched main using tests/make_tensor.py, running the fit with both and comparing fitted parameter values and variances:

case max relative difference
dense + normal 5.7e-12
sparse + normal 4.7e-11
dense + log_normal (control) 0 (bit-identical)

Run with --expectSignal sig 2.5 so that rnorm_init != 1 and the scaling is genuinely exercised -- with the default expectSignal=1 the factor is exactly 1.0 and both orderings agree trivially. The residual differences are consistent with the change in summation order (summing then scaling instead of scaling then summing); the new ordering performs one multiply per (bin, proc) rather than nsyst of them.

🤖 Generated with Claude Code

For systematic_type == normal the param-model factor rnorm_init was
absorbed into logk at construction, which allocates a second full-size
copy of the [nbins, nproc, nsyst] tensor and, in sparse mode, rebuilds
the CSR matrix.

rnorm_init carries no systematic index, so it factors out of the
contraction over systematics:

    sum_s (rnorm_init[b,p] * logk[b,p,s]) * theta[s]
      == rnorm_init[b,p] * sum_s logk[b,p,s] * theta[s]

Keep logk aliased to the indata tensor and apply the factor to the
[nbins, nproc] contraction result instead. This is exactly equivalent,
costs one elementwise multiply on a tensor a factor nsyst smaller, and
leaves the hot path strictly linear in theta. In sparse mode the factor
is gathered onto the [norm_nnz] layout the CSR contraction returns.

Verified against the unpatched code with tests/make_tensor.py for
dense+normal and sparse+normal (with --expectSignal 2.5, so the scaling
is non-trivial): agreement at the 1e-11 level, consistent with the
change in summation order. The log_normal path is bit-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMhAVnF8rjPLuhftU8gTVm
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