Conversation
The spec's correctness_ref named a module-level node id (::test_composed_chain_apply_matches_generic) that no longer exists; the real test is TestComposedChainApply::test_matches_generic. ADR-016 requires the ref pin an existing test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce spacecore/kernels/specs/_batched.py: the shared 'stack uniform flat-dense matrices -> one batched matmul' fast path (uniformity guard + matvec/right-matmul primitives + shape-only cost), so the block-diagonal and stacked folds are thin wrappers over one body rather than near-duplicate specs. Refactor the shipped apply spec to use the helper (behaviour-preserving; covered by TestBlockDiagonalUniformBatched) and add the dispatch-eligible block-diagonal rapply spec: stack _A2H -> one batched matmul, guarded to EUCLIDEAN_FLAT (a non-Euclidean adjoint uses a metric/Riesz path) and the NumPy backend (bit-exactness verified there only), with a shape-only memory cost. Wire BlockDiagonalLinOp._rapply_unchecked through dispatch() mirroring _apply_unchecked; the generic fallback (parts[i]._rapply_core) is byte-identical to the prior _rapply_parts loop, so dispatch-off is result-identical. Verified: optimized == generic == NumPy ground truth (array_equal) across a block-count/size grid; off==on bit-exact and verify mode green end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Batched twins of the apply/rapply folds for uniform flat-dense blocks: stack the cached matrices and issue one batched matmul in the dense core's transpose-on- right orientation (X @ A2T for vapply, Y @ A2H.T for rvapply, matching dense_vapply_core / dense_euclidean_rvapply_core). EUCLIDEAN_FLAT + NumPy guards and shape-only costs as for rapply. Wire BlockDiagonalLinOp vapply/rvapply through dispatch() via new _vapply_unchecked/_rvapply_unchecked; the generic now pins the check-free _vapply_core/_rvapply_core (mirroring the apply path) instead of the public checked op.vapply/op.rvapply, which is result-identical for valid inputs (the boundary @checked_method already validated the batch) and removes the redundant per-block revalidation. Verified: optimized == generic == NumPy ground truth (array_equal) across a block/size/batch grid; off==on bit-exact and verify mode green end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same defect as the composed-chain-apply ref: the explicit-entry block-diagonal-dense-apply spec named a non-existent module-level node id (::test_block_diagonal_dense_apply_matches_generic) instead of the real TestBlockDiagonalDenseApply::test_matches_generic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The broadcast-no-sum directions of the single-domain/single-codomain tree operators apply one shared operand through K components and return a tuple (no reduction), so a stacked batched matmul is exactly bit-equal to the per-component loop. Add a shared-broadcast helper (batched_matvec_shared + matvec_shared_cost) to _batched.py and two thin specs in stacked_batched.py: - linop.stacked.apply (stack _A2, broadcast x) - linop.sum_to_single.rapply (stack _A2H, broadcast y, EUCLIDEAN_FLAT only) Uniform-matrix-shape guard means heterogeneous-codomain (stacked) or heterogeneous-domain (sum_to_single) operators fall through to generic. Wire the two previously-unoptimized call sites through dispatch(); the reduction directions (which carry their own inline flat-dense fast paths) are deliberately left untouched. Verified: optimized == generic == NumPy ground truth (array_equal); off==on bit-exact and verify mode green end-to-end; the reduction paths still work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Evaluated the reserved linop.matvec.sparse key: every SparseLinOp direction is already a single optimal backend call (apply/rapply one SpMV; vapply/rvapply one batched SpMV over the stacked RHS, not an O(batch) loop). There is no faster bit-exact path, so wiring a spec would add only dispatch overhead with no win. Record the decision so the reserved key is not redundantly activated later. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the five new dispatch-eligible specs (block-diagonal rapply/vapply/ rvapply, stacked.apply, sum_to_single.rapply) and their shared helper, the SparseLinOp no-spec decision, and the two stale correctness_ref fixes under Unreleased. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add LinOp.fuse() (Tier-2 lazy-algebra simplification): collapse maximal subtrees of densely-fusible operators into one materialized DenseLinOp. The base method returns self (leaves are atomic); ComposedLinOp.fuse() fuses its operands and, when both are dense, replaces A @ B with a single DenseLinOp holding the matrix product M_A @ M_B. Correctness is up to floating-point rounding (fusion reassociates the arithmetic), not bit-exact, per ADR-021 — tests use allclose. The fused matrix is adjoint-consistent on any geometry: the shared middle-space Riesz maps cancel, so the fused operator's metric adjoint equals B* @ A* (verified on a weighted, non-Euclidean space). A matrix-free operand is never densified: a non-dense fused operand keeps the composition lazy (verified). Lives in spacecore.linop, not spacecore.kernels, and is not gated on the ADR-016 dispatch rail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ScaledLinOp.fuse(): fold the scalar into the dense matrix (c·A -> DenseLinOp with c·M_A); the adjoint stays conjugated (verified with a complex scalar). - SumLinOp.fuse(): combine the densely-fusible terms into one DenseLinOp via matrix addition, keeping matrix-free/structured terms lazy. - _AdjointViewLinOp.fuse(): fuse the wrapped operand and take its adjoint, so (A @ B).H collapses its inner composition. All keep matrix-free operands un-densified and are adjoint-consistent; correctness via allclose (Tier-2 reassociates). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BlockDiagonalLinOp, BlockMatrixLinOp, StackedLinOp, and SumToSingleLinOp fuse each of their component operators and rebuild, preserving the tree/block layout, domain/codomain, and context. A composed-dense block collapses to a single DenseLinOp -- which also makes it newly eligible for the block-diagonal dispatch fold. Correctness via allclose; matrix-free blocks stay un-densified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the ADR-021 matrix-free rail: fuse() gains a keyword-only materialize=False parameter, threaded through every override. The safe default never densifies a matrix-free operand (it stays a lazy leaf and breaks a fusible run); materialize=True is the explicit caller opt-in that densifies a MatrixFreeLinOp via its to_dense() basis probe, letting the enclosing expression collapse to a single dense operator. Verified: default keeps matrix-free lazy; materialize=True densifies and lets a dense @ matrix-free chain collapse to one DenseLinOp (allclose). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A runnable notebook covering the two performance/algebra layers: - ADR-016 dispatch: the two kernel layers, dispatch_mode off/on/verify, the exactness + memory-gate + matrix-free rails, and the live dispatch-key catalog, demonstrated on a uniform block-diagonal operator (off==on bit-exact). - ADR-021 fuse(): Tier-1 auto rewrites at construction, Tier-2 fuse() multiplying dense operators into one matrix (allclose, adjoint-consistent), the matrix-free rail, and fuse(materialize=True). Closes by showing the two layers compose (a fused composed-block becomes dispatch-foldable). Executed on NumPy; converted to RST and wired into the tutorials index and README under a new 'Performance and internals' section. Sphinx renders it clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Part 4 demonstrating the actual benefits: - Fusion: timing a depth-16 dense composition shows ~10x faster per apply once fused (16 matvecs -> 1) -- the 'fuse once, apply many' win, on any backend. - Dispatch: timing the block-diagonal apply off vs on is honest about CPU being break-even/slightly slower (NumPy's per-block loop is already cheap), with the measure-first framing -- the batched fold is a GPU win and is gated on a per-backend benchmark, which is why dispatch stays off by default. Executed on NumPy (0 errors), reconverted to RST; Sphinx renders clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Context inference joined inferred-context dtypes through numpy.result_type,
which cannot interpret a torch (or jax) dtype:
TypeError: Cannot interpret 'torch.float64' as a data type
It surfaced when constructing an operator that infers a TreeSpace from its
operands on the torch backend — e.g. BlockDiagonalLinOp/StackedLinOp/
SumToSingleLinOp.from_operators, and ADR-021 fuse() on such an operator. Join the
dtypes through the operands' own array-API namespace (ops.xp.result_type)
instead, which promotes each backend's dtypes correctly. NumPy behaviour is
unchanged.
Adds a torch regression test exercising the from_operators inference path and
fuse() on torch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The uniform block-diagonal / stacked / sum-to-single dispatch folds rebuilt ops.stack([matrix(p) for p in parts]) on every apply, even though that stacked block-matrix array is fixed by the (immutable) blocks. ADR-022 makes it a build-time-amortized value instead of per-call work. - Add CachedStackParts(tuple) + stacked_block_matrices(parts, matrix) in kernels.specs._batched: the helper memoizes the stack per matrix accessor (_A2 apply, _A2H adjoint, _A2T / _A2H.T batched) when parts carries the cache, and falls back to a fresh stack (byte-identical) for plain tuples. - Route batched_matvec / batched_matvec_shared / batched_right_matmul through it. - Wrap self.parts in CachedStackParts once in BlockDiagonalLinOp / StackedLinOp / SumToSingleLinOp __init__ so the memo persists across applies. The cache is a derived value: built lazily on first optimized use (NumPy-only, dispatch on/verify only — the default off path is untouched), excluded from operator identity (__eq__/__hash__) and from the pytree (tree_flatten re-normalizes parts to a plain tuple, so a round-trip rebuilds an empty cache), and a matrix-free operand is never cache-materialized (the fold is inapplicable). The memory gate is honored via the dispatcher's existing affordability check and the dispatcher stays stateless. Dispatch-decision caching stays out of scope. Adds tests/kernels/test_materialized_cache.py pinning the invariants; full suite green (3289 passed). Updates CHANGELOG and the ADR index. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two findings from the adversarial review of the materialized-form cache: - Drop CachedStackParts from the public __all__ of spacecore.kernels and spacecore.kernels.specs. It is an internal ADR-022 detail the fold operators wrap their parts in, not a user-facing type; it stays importable for that cross-module use but is no longer part of the blessed public API surface. - Add __getstate__/__setstate__ so copy/deepcopy/pickle rebuild an independent, empty cache instead of aliasing one shared dict (shallow copy) or restoring a populated one (deepcopy/pickle). This matches the pytree round-trip contract: the memo is derived and reconstructable, never carried across reconstruction. Adds a copy/deepcopy/pickle regression test. Full suite green (3290 passed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both are implemented and shipped (fuse() in spacecore.linop; the materialized- form fold cache across the block/stacked/sum-to-single operators), so flip their status from Proposed to Accepted in the ADR headers and the index table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Caching the materialized fold" section that shows the cache in action: the per-accessor stack cache appearing on the operator's parts (empty -> one (K, m, m) slot after a fold routes), and a measurement that isolates the stacking cost from the dispatcher's selection overhead. Re-stacking every apply the fold loses to the per-block loop (0.66x); with the stack cached it wins (1.31x) — the re-stack is ~half the uncached fold's time. Make the timing comparisons robust: per_apply_us now reports the median across repeated batches (timeit.repeat) instead of a single timed run, so the fusion, dispatch, and caching numbers are averaged and outlier-resistant. Intro and takeaways updated to cover the caching layer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
per_apply_us now returns the arithmetic mean over all repeat x number timed calls (np.mean) rather than the median, so every figure in the notebook is an average over many runs. Re-executed; the story is unchanged (fusion ~11x, dispatch ~0.92x break-even, caching flips the fold from 0.65x to 1.25x). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-render docs/source/tutorials/09_kernels_and_fusion.rst from the committed notebook (nbconvert --to rst) so the published page matches the notebook: adds §5 "Caching the materialized fold (ADR-022)" and the averaged timings. Update the tutorials index entry to mention fold-stack caching alongside dispatch and fusion. Figure asset unchanged (deterministic seeded plot). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The materializing folds only route when the memory gate has a budget, which on
NumPy comes from free_memory_bytes() (psutil-backed). The "Minimal install
tests" CI job has no psutil -> None -> "no budget, no fuse", so the fold never
routes and the cache stays empty, failing the six operator-level cache tests
that assert it fills.
Add a `routable` fixture that pins a large free-memory budget (monkeypatching
NumpyOps.free_memory_bytes) for the tests that exercise a routed fold, making
them deterministic and independent of whether psutil is installed — which is
what they actually mean to test ("once a fold routes, the cache fills"). The
direct-spec and dispatch-off tests are untouched (they don't depend on routing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dispatcher's memory gate sizes a materializing fast path against NumpyOps.free_memory_bytes(), which is psutil-backed. psutil was undeclared (effectively optional): a minimal install returned None -> "no budget, no fuse", so materializing folds never routed there. That silently disables a core dispatch rail and broke the ADR-022 cache tests on the minimal-install CI job. Declare psutil as a core dependency and import it unconditionally in free_memory_bytes(), so the CPU memory gate always has a real budget. This reverts the prior test-only `routable` workaround (no longer needed now that the budget is always available). Updates the CHANGELOG wording. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CachedStackParts is re-exported for internal cross-module use (the fold operators import it) but deliberately kept out of __all__, which ruff flagged as an unused import (F401). Use the redundant-alias form (`import X as X`) — the standard explicit-re-export marker that ruff and pyright both honor — so it stays importable and out of the public surface without a noqa. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
45 implement dispatch logic
Brings the existing micro + macro benchmark suite onto the feature branch as the baseline for the ADR-023 work: - micro layer: factory probes (bench/_probes.py), fixed-seed runner (bench/_run.py, seeds 0-3), timing/peak-memory harness, verdict and overhead diagnosis, JSON persistence, and a self-contained Plotly dashboard. - macro layer (bench/macro/): algorithm-level workloads (CG, Lanczos, PDHG, QOT, density pipeline, operator stress, JAX full loop). - tests/bench/ smoke + macro coverage (115 passing). This is the starting point the 0.4.1 benchmark-surface spec (docs/dev/0.4.1-bench-surface.md) and ADR-023 build on; the configuration-regime axis, ragged-block probes, and HTML aggregation land in follow-up commits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Agreed micro-overhead surface for ADR-023: which operations get probed and what each is compared against (hand-optimal pure-array-library bare). Records scope decisions (linalg/kernel/check_member/tree out; block operators tested uniform AND ragged), the configuration-regime model (baseline / dispatch / dispatch+cache / verify), and the implementation phases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ADR-023 phase 1. A *regime* is a named bundle of SpaceCore optimization toggles the runner activates around an otherwise-identical probe, so the same operation on the same (seed, size) inputs is timed under each: - bench/_regimes.py: the regime vocabulary (baseline / dispatch / dispatch_cache / verify), benchmark_regime() activating the real spacecore.kernels.dispatch_mode, dispatch_eligible() (linop only), and regimes_for() resolving the per-family sweep (baseline always included as the regime_speedup reference). Default sweep for linop probes is baseline + dispatch_cache; dispatch (cold cache) is deferred until probes expose their routed operator, and verify is opt-in. - ProbeResult gains `regime` + `regime_speedup` (within-run ratio vs the baseline cell at the same coordinate); _io round-trips both. - run_probes() sweeps regimes alongside check_levels, wraps the timed section in benchmark_regime, and pairs regime_speedup in the final pass; `python -m bench run --regime ...` exposes it. Non-dispatch (space/functional) probes run baseline only, so the default path and existing artifacts are unchanged. tests/bench/test_regimes.py covers the vocabulary, dispatch-mode activation, and an end-to-end run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ADR-023 / 0.4.1 surface: block operators are measured uniform AND ragged. The existing block_diagonal / stacked / sum_to_single probes stack same-shape blocks (uniform) — the ADR-016 block-diagonal-uniform-dense-batched fold's target. This adds the ragged counterparts (mixed block shapes) where no uniform fold applies, so both bare and SpaceCore stay a per-block loop and the probe isolates the abstraction's own cost when there is no fast path: - linop.block_diagonal.apply.ragged / .rapply.ragged - linop.stacked.apply.ragged (same domain, differently-shaped codomains) - linop.sum_to_single.apply.ragged (differently-shaped domains, one codomain) Each ragged bare is the idiomatic per-block tuple; all four reproduce the reference exactly. Uniform probes are now labelled "dispatch fold target", ragged "no uniform fold", so the dashboard's regime view reads the contrast directly (dispatch routes the uniform fold; ragged dispatch is a near no-op). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ADR-023 baseline contract: a probe's `bare` must compute the same value as its `reference`, so a fast-but-wrong baseline cannot inflate a speedup. Parametrized over every numpy probe, asserting bare-vs-reference error within the family tolerance. 84 probes pass — no strawman bares in the current suite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow the 0.4.1 benchmark-surface table (docs/dev/0.4.1-bench-surface.md)
by removing the out-of-scope families/probes from the suite, leaving
space / linop / functional only (56 probes):
- linalg probes removed completely (cg, power_iteration, lsqr,
lanczos_smallest).
- kernel-comparison probes removed (the optimized folds are still
measured on the real linop operators under the dispatch regimes, so
no coverage is lost); dropped the _KERNEL_PROBE_TO_* mappings and the
kernel_benchmark_ids / kernel_probe_cases helpers.
- space.check_member, the space.tree.* probes, and
linop.generated_{dense,diagonal} removed. Kept
functional.generated_linear.value and the elementwise-Jordan probes.
Threaded the removal through every consumer so nothing dangles:
- OperationFamily Literal narrowed to space/linop/functional; _verdict
_FAMILY_TOL and _dashboard family colors trimmed to match.
- _diagnose: dropped the now-dead KERNEL_WIN / KERNEL_NEUTRAL /
SOLVER_FIXED_ITERATIONS reasons, their heuristics, summaries, and the
kernel_wins rollup; _dashboard top-wins generalized from the
kernel-only `optimized` gate to the backend-agnostic optimized_speedup.
- CLI --family choices (run, list, run_micro) reduced; run_micro gains
--regime and _cmd_run reads device/regime defensively.
- README coverage table + module layout updated; stale _policy.py
docstring pointer to the deleted helper removed.
ruff + pyright clean on the touched files; tests/bench 196 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-ups from the removal audit: - ADR-023: drop linalg/kernel from the dashboard `family` grouping vocabulary (those values can no longer appear as a group key); the conceptual ADR-016 kernel/dispatch discussion is untouched. - _dashboard.py: fix the stale family-checkbox docstring to list only space/linop/functional. - _run.py: remove the dead solver-result-shape branches (.x / .eigenvalue / .result / .value) in _error_vs_reference — they existed only for the removed linalg solver probes. ruff clean; tests/bench 196 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HermitianSpace.spectrum is `ops.eigh(x)[0]` and spectral_decompose is `ops.eigh(x)` — identical SpaceCore work, spectrum just discards the eigenvectors (and its eigvalsh bare understated the eigh the impl actually runs). ElementwiseJordanSpace.spectral_decompose is likewise `spectrum` wrapped as `(x, None)`. Keep spectral_decompose (its eigh bare matches the work), drop the redundant spectrum probe in both. 54 probes now: space(18)/linop(31)/functional(5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…plots Both dashboard charts were diagnostics, not insights: the per-seed jitter scatter just re-renders measurement noise already captured by speedup_std, and the overhead-persistence chart overlaid every (operation, backend, check_level) series into unreadable spaghetti. Removed both plots (scalingCurves / perSeedJitter JS, their render calls, divs, and docstring entries) and renumbered the remaining charts. Kept the concise "Overhead persistence" diagnosis card (a short ranked list of size-persistent-overhead cases) — that is the actionable form of the concept. The clean regime-aware speedup-by-size view will come with the Phase 5 interactive aggregation (size + regime are group-by dimensions there). ruff clean; tests/bench 192 passed; dashboard renders end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HermitianSpace.spectral_decompose is correct on Torch — eigenvalues and the reconstruction U diag(λ) Uᴴ match numpy to float32 precision — but torch and numpy eigh return eigenvectors with flipped signs (and arbitrary bases on degenerate eigenspaces). The probe compared eigenvectors element-wise, so every Torch case tripped the correctness gate with error ~1.5 (6 of 8 cells flagged CORRECTNESS_FAILURE). Compare only the (ascending) eigenvalues — the basis-invariant part; eigenvector validity is already covered by the from_spectrum reconstruction probe. The bare still runs the full eigh for fair timing. `python -m bench run --backend torch --max-size 128` now reports 0 correctness failures (was 6). The other near-tolerance torch cases (from_spectrum ~1e-7, dense.apply.device on MPS ~4e-6) were already within the existing float32 widening (1e-4); heavy torch-CPU matmuls are bit-identical to numpy, so no tolerance change is needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dashboard always rendered a summary card and filter chip for every Status, including CORRECTNESS_FAILURE and REGRESSION at count 0 — so a clean run (e.g. torch after the spectral_decompose fix, 0 failures) still showed a "CORRECTNESS_FAILURE" card/chip and read as if failures existed. Only render a status card and filter chip when that status has at least one case, so the label appears only when there is a real failure to look at. Performance statuses (WIN/NEUTRAL/LOSS/HEAVY_LOSS) are unaffected when present. The embedded status_colors/enum (non-visible JSON) are untouched, so the JS filter vocabulary still works. Test now covers all four performance statuses and asserts a zero-count status gets no chip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ayer The verdict status widened the correctness tolerance for float32 backends (Torch) to 1e-4, but bench._diagnose still used the raw 1e-9 _FAMILY_TOL. So a Torch case could be classified WIN/NEUTRAL by the verdict yet carry a CORRECTNESS_FAILURE *diagnosis reason* — rendered as a brown badge next to the green status badge. spectral_decompose (eigenvalue error ~1e-6, well within float32) tripped this on every Torch row. Extract correctness_tol(result) in _verdict as the single source of truth (family tol + float32 widening) and consult it from both categorize() and the diagnosis heuristics. Verdict and diagnosis can no longer disagree; the Torch run now shows no CORRECTNESS_FAILURE in either column (reasons fall through to BARE_SATURATES_OP / NEUTRAL). ruff clean; tests/bench 192 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The table marks each case with a status badge and a diagnosis-reason badge, but nothing explained what they mean. Add a "Tag legend" section between the table and the footer that lists every status and reason tag present in the run with a one-line explanation, colored to match the badges. Only tags that actually occur are shown, so the legend tracks the page (and never explains a CORRECTNESS_FAILURE that did not happen). ruff clean; tests/bench green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Alongside the backend, family, check-mode, and status filters, the dashboard now offers a "Problem size" chip group — one checkbox per distinct size present in the run. Unchecking a size hides those rows from both the table and the charts (the filter runs through the same filtered() path). Wired into state.sizes, the change/reset handlers, and the reset button; chips render only for sizes actually present. Test exercises multiple sizes (64/256/1024) and asserts the chips render and the filter is wired; _mock_result gains a size kwarg. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the per-size checkbox chips with a two-thumb range slider (min and max) over the indices of the sorted distinct sizes, so the handles snap to actual sizes present and stay evenly spaced even though sizes span orders of magnitude (8 … 65536). A readout shows the selected "n ∈ [low, high]". Filtering keeps rows with low ≤ size ≤ high through the same filtered() path (table + charts). Handles cannot cross (each clamps to the other), single-size and empty-size runs degrade gracefully, and Reset restores the full range. Standard overlapping-range CSS (pointer-events on the thumbs) keeps the page self-contained — no slider library. Test updated to assert the slider + readout render and the index range matches the distinct sizes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review found a real UX trap in the dual-range slider: the two range inputs overlap with no z-index, so the later-painted max thumb sits on top of the min thumb. When the range collapses at the top edge (min dragged fully right, or a single-size run), the max thumb has nowhere up to go and the buried min thumb cannot be grabbed back — the range gets stuck closed. Add setSizeZ(): max stays on top by default (grabbable to re-open a collapse anywhere below the top), but when min reaches the top index the min thumb is lifted above max so it can be dragged back down. Called from both input handlers, on init, and on reset; CSS sets the default stacking. Every collapse case now re-opens. (Integration lens of the review passed: composes with all filters, table + charts honor it, 192 tests pass.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The headline count cards and the whole "What's causing overhead and wins" section were computed once in Python and baked in, so they ignored the filters. Move the rollup client-side: computeOverall(rows) mirrors bench._diagnose (dominant reasons, worst overhead, persistence, top wins, JAX summary, narrative) and renderSummaryCards(rows) rebuilds the count cards — both now run from filtered() on every refresh(), so the summary tracks whatever the backend/family/status/size/check/search/speedup filters select. To count reasons exactly as the server does, each row now carries its full diagnosis_reasons list (a case can have several); the dominant- reasons tally sums those. renderDiagnosisSection takes the computed rollup as an argument; the one-time init render is dropped (refresh() covers it). Verified the client rollup reproduces the server's on the full set (matching to a pre-existing ±1 boundary case, and consistent with the per-row badges). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Torch defaulted to float32 while NumPy and (x64-enabled) JAX ran float64, so the bare-vs-SpaceCore comparison was unfair on Torch and the float32 rounding tripped the correctness gate. Mirror the JAX treatment: add enable_torch_x64() (torch.set_default_dtype(float64)) and call it from the runner alongside enable_jax_x64(). Apple MPS is float32-only hardware and cannot take float64 tensors, so the device-aware probe builds its MPS case at float32 explicitly, and the correctness tolerance keeps a float32 width for MPS only — every other backend/device now gets the strict 1e-9 gate. Verified: a Torch run shows CPU max error 1.95e-14 (was ~1e-6 at float32) and 0 correctness failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uild) The CI docs build (`sphinx-build -W --keep-going`) failed with 13 "Pygments lexer name 'ipython3' is not known" warnings — turned into errors by -W — all in docs/source/tutorials/09_kernels_and_fusion.rst. That file was regenerated from its notebook with nbconvert's default `.. code:: ipython3` directives, whereas the other eight tutorials use `.. code:: python` (a universally-known lexer). The `ipython3` lexer is only registered when IPython is installed, which the `[docs]` extra does not pull in, so it builds locally (IPython present) but fails in CI. Switch all 13 blocks to `.. code:: python`, matching the convention. Strict build now passes with zero warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nbconvert-generated tutorial RST can use `.. code:: ipython3`, whose Pygments lexer is only registered when IPython is installed. The strict docs CI (`sphinx-build -W`) installs `.[docs]`, which lacked it, so such a tutorial failed the build. Pull IPython into the docs extra so the `ipython3` lexer is always available — a backstop against recurrence on top of normalizing tutorial 09 to `python`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JAX was benchmarked eagerly (and only the SpaceCore call was optionally jitted), so the comparison didn't reflect how JAX is actually run and the abstraction "overhead" was really trace overhead. Now, on JAX, the runner jits BOTH the SpaceCore call and the bare reference and times their post-compile steady state — the time_op warmup absorbs compilation, so the reported bare/sc medians (and hence the speedup) are jitted-vs-jitted with compilation excluded. Each side's first-call compile latency is measured separately via time_op_first_call. - bench/_run.py: `_timed_callable(backend, fn)` jits on JAX and returns (jitted, compile_ns), falling back to eager (compile None) for a non-jittable probe; used for both sc and bare. Drops the jit_compatible gate — every JAX probe is jitted where possible. - Data model: SeedTiming/ProbeResult gain `bare_compile_ns(_median)`; `compile_ns(_median)` is now explicitly the sc compile; the redundant jit_* fields are removed. _io round-trips the new shape. - Reporting: the dashboard table and the CLI per-case table get two columns — `sc compile` and `bare compile` — and the JAX compile summary panel shows both medians. The `jit_compatible` Probe flag is removed. Verified on a real JAX run: space.add steady state sc≈bare (jit erases the eager abstraction overhead), with sc/bare compile reported separately; 248/256 sc and 254/256 bare jitted, the rest gracefully eager. Full suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mmetric The review of the jit-both-sides change flagged a real hazard: when one side jits and the other falls back to eager, the speedup is an eager-vs-jitted comparison and gets mislabeled (e.g. HEAVY_LOSS) with no signal. Two fixes: - Restrict JAX to check_level="none". Under "cheap" the membership validation is value-dependent and not jittable, so the SpaceCore call would be eager while the jnp bare jits. The runner now drops "cheap" for the JAX backend (other backends keep none + cheap). - Make the jit resolution symmetric. _resolve_timed_pair jits both sc and bare, but if EITHER is not jittable both are timed eagerly — so a row is either both-jitted or both-eager, never mixed. (3 functional `.value` probes have a non-jittable sc even under "none"; they now compare eager-vs-eager fairly instead of eager-vs-jitted.) Verified: a jax+numpy run shows JAX only at none, 0 mixed rows (124 both-jitted, 4 both-eager). Tests assert none-only and the both-or-neither invariant. Full bench suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
functional.inner_product.value/grad, quadratic.value, and matrix_free.value used a 2-point (256, 4096) grid while the analogous space vector ops (add/scale/inner/norm/zeros) use the 3-point (256, 4096, 65536). Match them so the functional family scales over the same grid as the rest. (generated_linear.value stays at (3,) — it is a fixed small generator-built instance, not a scaling probe.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per review, the functional scaling probes (inner_product.value/grad, quadratic.value, matrix_free.value) move to an even x4 geometric grid capped at 1024 — every point is runnable without a huge-size run, unlike the x16 (256, 4096, 65536) grid that yielded only one usable point under --max-size 1024. (generated_linear.value stays at (3,).) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every scaling probe now uses the same x4 geometric grid (64, 256, 1024) — previously the suite mixed x16 (256,4096,65536 for the vector/diagonal/ sparse/matrix-free ops), x4 (64,256,1024 and 32,128,512), and x2 (8,16,32,64 hermitian). The x16 grids gave only one runnable point under --max-size 1024; everything is now consistently paced and fully runnable on a <=1024 machine. The one exception is the Hermitian family (spectral_decompose, from_spectrum, symmetrize, inner): eigh is O(n^3) and a 1024x1024 eigendecomposition would take many minutes per probe, so it keeps the same x4 pacing but cost-capped at (16, 64, 256). generated_linear.value stays (3,). Largest size anywhere is now 1024. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…odule-python--m-bench 47 add the unified benchmark submodule python m bench
- Bump spacecore/_version.py to 0.4.1. - CHANGELOG: promote [Unreleased] to [0.4.1] — 2026-06-28, open a fresh empty [Unreleased]. - release_notes.rst: add the Version 0.4.1 section (dispatch / fuse / cache / bench). - test_public_api: pin the version assertion to 0.4.1. Dispatch/fuse/cache (ADR-016/021/022) and the unified bench submodule ship in 0.4.1, all off the default path. Built wheel+sdist clean (twine PASSED); bench is excluded from the wheel (tooling only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.