Per-sample nested-Diagonal Dz/DM_Dzz (batch structure, Phases 1-3) - #4
Conversation
| This work is branched off `main`, not off PR #3 (`splu-timing`). PR #3 adds | ||
| `src/timing.py` and instruments `hessian.py` / `train_newton.py`. Since Phases 1–2 also | ||
| edit `hessian.py`, **PR #3 will need to be rebased on top of this branch once it merges** | ||
| (expect conflicts in `hessian_inverse_setup` / `hessian_inverse_solve`, where both add |
There was a problem hiding this comment.
don't need this file as part of the PR. it's ephemeral
| existing `splu` solver, before any solver change (Phase 4 is gated on these results). | ||
|
|
||
| Methodology: the "dense" baseline is `src/hessian.py` + `src/block_partitioned_matrices.py` | ||
| as of `main` (checked out via `git show main:...` into a separate module search path so it |
There was a problem hiding this comment.
as long as these benchmark results are in the pr description, don't need this file
| return parts | ||
|
|
||
|
|
||
| def _matmul_dense_rhs_blockwise(blocks: Sequence[Matrix], other: "Tensor") -> "Tensor": |
There was a problem hiding this comment.
are you doing this so that structured matrices can multiply unstructored tensors? if so, we don't want this. we want the rhs to have a structure that matches the lhs.
Exploit the verified per-sample (batch) block-diagonal structure of the activation blocks: sample i's output depends only on sample i's input, so each layer's Dz and DM_Dzz are batch independent w x w sub-blocks. Compute them by vmap-ing a per-sample jacobian/hessian over the batch and return a nested bpm.Diagonal instead of materializing the dense (batch*w)^2 tensor. The loss layer keeps dense blocks (its scalar mean-loss output has no per-sample axis). This makes hessian_inverse_setup ~2x faster and ~2x lighter at batch 32, and lets the default batch 128 -- previously OOM-killed by the ~38 GB dense DM_Dzz -- complete. The default splu solver consumes the structure directly via to_scipy_csc. The block and matrix-vector reference paths densify these per-sample blocks before solving: their factorization eliminates the shared parameters first, which couples the batch samples (the param-border Schur term is purely cross-sample), so the per-sample structure cannot survive that solve. This keeps a structured matrix from ever multiplying an unstructured tensor -- no ad-hoc Diagonal-times-dense-Tensor chunking in the block library. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6cba545 to
48b75f7
Compare
|
Addressed all three comments (force-pushed a cleaned single commit):
On "we want the rhs to have a structure that matches the lhs": in the block and matrix-vector reference paths the per-sample structure genuinely can't survive the solve, so the fix is to densify those activation blocks rather than structure the RHS. Verified numerically — the block solver eliminates the shared parameters first, and the resulting param-border Schur term The default splu path keeps the full per-sample structure via |
Summary
Exploits the verified per-sample (batch) block-diagonal structure of the activation blocks. Sample i's layer output depends only on sample i's input, so each layer's
DzandDM_Dzzarebatchindependent w×w sub-blocks. Instead of materializing the dense(batch·w)²tensor and discarding the zeros, wevmapa per-sample jacobian/hessian over the batch and return a nestedbpm.Diagonal.Implements Phases 1–3 of
docs/batch-structure-plan.md; the arrowhead solver (Phase 4) is deliberately not included — it is gated on the Phase 3 benchmark and a decision (see below).Changes
src/hessian.py:BlockWithMixedDerivatives.derivatives()computesDz/DM_Dzzper-sample viatorch.func.vmap, returned as nestedbpm.Diagonals. Gated by abatch_structured_dzclass attribute —Truefor per-sample layers,FalseforLossLayer(its scalar mean-loss output has no per-sample axis; its blocks stay dense).src/block_partitioned_matrices.py: additiveDiagonaldispatch registrations so nestedDiagonals flow through K assembly and both solvers unchanged; fixes a latent.shape[1]→.widthbug inUpperDiagonal @ Diagonal.solver="splu"andsolver="block"are untouched and remain as references.Results (independently verified)
(H+εI)⁻¹greference to 2e-6 (splu) / 1e-3 (block).SequenceOfDenseBlocks(768, 8, 10, num_layers=8)): ~2× faster setup and ~2× less peak RSS. The default batch 128 — previously OOM-killed by the ~38 GB denseDM_Dzz— now completes. Full table indocs/phase3-benchmark.md.Phase 4 go/no-go context
The Phase 3 benchmark surfaces a strategic point: the win here is entirely in setup (the derivatives pass); K assembly and the splu solve are unchanged. The arrowhead solver (Phase 4) would speed up the solve, which is currently the minority of the budget at these sizes — so its value proposition is weaker than expected. Recommend treating Phases 1–3 as a standalone win and deciding Phase 4 separately.
Coordination
Branched off
main, not off PR #3 (splu-timing). PR #3 will need rebasing on top of this once merged — both edithessian.pyaround the derivatives pass / K assembly (see the plan's coordination note).🤖 Generated with Claude Code