Skip to content

Fix correctness and performance defects found by codebase review - #32

Merged
subhk merged 2 commits into
mainfrom
fix/normalization-convention-and-correctness-audit
Aug 11, 2026
Merged

Fix correctness and performance defects found by codebase review#32
subhk merged 2 commits into
mainfrom
fix/normalization-convention-and-correctness-audit

Conversation

@subhk

@subhk subhk commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Two rounds of review: first over this branch's diff, then over the ~9k lines of src//ext/ the branch never touched. Every finding was reproduced before fixing and re-verified after.

Breaking changes

Both are documented with porting instructions in the new CHANGELOG.md.

analysis_axisym / analysis_axisym_l return different values. They omitted the φ quadrature factor cphi*nlon = 2π, so every coefficient was 1/2π too small — they inverted neither synthesis_axisym nor the m=0 column of analysis. Anything compensating for the old scale downstream must drop that compensation.

dist_synthesis! / dist_synthesis_sphtor! still reject real_output=false with real-backed output arrays. I built the softer option (accept when the imaginary part is negligible) and measured that it cannot work: real_output=false no longer means "the real field, typed complex" — it sums only the m≥0 half without the Hermitian mirror. On a typical config the result has |imag| up to 1.34 and a real part differing from the real field by 1.31, against field magnitude 2.96, so no tolerance bridges the two. Callers who wanted the real field should pass real_output=true; the error messages now say exactly that.

Correctness

  • Batch QST and sphtor transforms derived their output eltype from one input instead of promoting across all of them, silently truncating double-precision components (2.05e-8 error → 1.39e-17).
  • ForwardDiff could not flow through any plan-based batch transform. SHTPlan is FFTW-backed and cannot hold Dual, so batch entry points now route non-FFTW eltypes through the plan-free cfg-form transforms. Batch gradients match the non-batch reference to 0.0.
  • load_config silently rebuilt :driscoll_healy grids as :regular, changing θ nodes and weights (8.7e-16 → 3e-3). save_config now records use_dh_weights.
  • shtns_set_grid returned success for unrecognized grid codes while producing a mirrored grid — the fallback branch was missing the reverse!.
  • Pole-inclusive grids with nlat == 1 built an all-NaN config from π/0 and returned NaN from every later transform without raising.
  • dist_SH_mul_mx! crashed on every mres > 1 config (missing m % mres guard).
  • dist_SH_Yrotate crashed on mres > 1. Worth noting the obvious fix is wrong: a Y-rotation mixes orders and is not representable in an mres-strided layout at all, so adding the stride just moves the failure deeper. It now rejects up front with the reason.
  • device_transfer_arrays rejected :cuda/:amdgpu — the config's own vocabulary.
  • Eleven comments left by the orthonormal refactor prescribed conversions the code no longer performs, including the AD extension's only statement of the rrule/primal normalization contract, which stated its inverse. Following it would have made every non-default-norm gradient wrong by M[l,m].

Performance

  • Distributed analysis: 3 collectives per call → 1 on first use, 0 after. φ_is_local_all and θ_is_distributed are reduced together in a single Allreduce and cached per (pencil, communicator). The cache is identity-keyed with wholesale eviction so the hit/miss pattern stays identical on every rank — a rank-asymmetric cache would deadlock the collective.
  • dist_synthesis_packed_cplx is single-pass, down from two full distributed syntheses. The negative-m φ bins are filled in the same θ/m traversal as the positive ones, reusing one Legendre row per (m, θ). It now matches the serial reference exactly.
  • Legendre table memory halved. prepare_plm_tables! was building NP_tables/NdP_tables bit-for-bit identical to plm_tables/dplm_tables; they now alias. estimate_table_memory reported half the true figure, so jobs sized by it allocated twice their budget and were OOM-killed.
  • Cached FFT plans are built UNALIGNED, removing a silent fallback to an O(n²) DFT; batch FFT helpers reuse the cache instead of re-planning per call.

Internal and tests

  • pack_lm!/pack_lm/unpack_lm!/unpack_lm in src/layout.jl replace six open-coded copies of the packed↔dense (l,m) mapping. The m % mres guard had needed fixing three separate times across those copies.
  • The axisym round-trip test fitted and divided out its own scale factor, so it passed while every coefficient was 1/2π off — and passed again after the fix. It now asserts the absolute identity and pins axisym analysis to the m=0 column of analysis.

Verification

  • Serial suite: 67279/67279.
  • MPI comprehensive + audit-fix suites: pass at 2 and 4 ranks.
  • Distributed analysis matches serial to 2.2e-16 on 1/2/4 ranks, both θ- and φ-decompositions.
  • Single-pass complex synthesis matches the two-pass form to 1e-15 and serial to 0.0.

🤖 Generated with Claude Code

subhk and others added 2 commits August 11, 2026 11:50
Two rounds of review over the branch diff and then over the files the
branch never touched. Every finding below was reproduced before fixing
and re-verified after.

Breaking (see CHANGELOG.md for porting instructions):

- analysis_axisym / analysis_axisym_l omitted the phi quadrature factor
  cphi*nlon = 2pi, so every coefficient was 1/2pi too small and they
  inverted neither synthesis_axisym nor the m=0 column of analysis.
- dist_synthesis! / dist_synthesis_sphtor! keep rejecting real_output=false
  with real-backed output arrays. real_output=false now computes a
  different function (m>=0 half, no Hermitian mirror), so no tolerance can
  accept the old usage; the error messages now name the porting path.

Correctness:

- Batch QST and sphtor transforms derived their output eltype from a
  single input instead of promoting across all of them, silently
  truncating double-precision components (2.05e-8 error, now 1.39e-17).
- ForwardDiff could not flow through any plan-based batch transform.
  SHTPlan is FFTW-backed and cannot hold Dual, so the batch entry points
  now route non-FFTW eltypes through the plan-free cfg-form transforms.
- load_config silently rebuilt :driscoll_healy grids as :regular, changing
  theta nodes and weights (8.7e-16 -> 3e-3 error). save_config now records
  use_dh_weights.
- shtns_set_grid returned success for unrecognized grid codes while
  producing a mirrored grid; the fallback branch was missing reverse!.
- Pole-inclusive grids with nlat == 1 built an all-NaN config from pi/0
  and returned NaN from every later transform without raising.
- dist_SH_mul_mx! crashed on every mres > 1 config (missing m % mres guard).
- dist_SH_Yrotate crashed on mres > 1; a Y-rotation mixes orders and is
  not representable in an mres-strided layout, so it now says so up front.
- device_transfer_arrays rejected :cuda/:amdgpu, the config vocabulary.
- Eleven comments left by the orthonormal refactor prescribed conversions
  the code no longer performs, including the AD extension's only statement
  of the rrule/primal normalization contract, which stated its inverse.

Performance:

- Distributed analysis went from three collectives per call to one on
  first use and none after: the phi_is_local_all / theta_is_distributed
  predicates are reduced together in a single Allreduce and cached per
  (pencil, communicator), with a rank-symmetric eviction policy.
- dist_synthesis_packed_cplx is single-pass, down from two full
  distributed syntheses; it now matches the serial reference exactly.
- prepare_plm_tables! built NP/NdP tables bit-for-bit identical to
  plm/dplm. They now alias, halving table memory and build time, and
  estimate_table_memory finally matches reality (it reported half).
- Cached FFT plans are built UNALIGNED, removing a silent fallback to an
  O(n^2) DFT; batch FFT helpers reuse the cache instead of re-planning.

Internal and tests:

- pack_lm!/pack_lm/unpack_lm!/unpack_lm in src/layout.jl replace six
  open-coded copies of the packed<->dense (l,m) mapping. The m % mres
  guard had needed fixing three separate times across those copies.
- The axisym round-trip test fitted and divided out its own scale factor,
  so it passed while every coefficient was 1/2pi off. It now asserts the
  absolute identity and pins axisym analysis to the m=0 column of analysis.

Verified: serial suite 67279/67279; MPI comprehensive and audit-fix suites
pass at 2 and 4 ranks; distributed analysis matches serial to 2.2e-16 on
1/2/4 ranks for both decompositions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…actly

CI failed on x86_64 at test_plan.jl:158 with a 2e-16 relative difference,
while the same assertion passed on arm64.

The cause is this branch's own UNALIGNED change: `synthesis` reaches its
phi transform through the shared plan cache, which is now built UNALIGNED
because it reuses plans across arbitrary caller arrays, while `SHTPlan`
plans its own stably-aligned buffers with default flags. FFTW can pick
different codelets for the two, so results may differ in the last ulp.
That is legitimate floating point, not a convention bug, and it varies by
CPU and FFTW build — so bit-exactness was never a property to assert.

Keeping the plan's own flags as they are: SHTPlan owns buffers of stable
alignment, and forcing UNALIGNED there would permanently give up the
aligned SIMD codelets that are the entire point of planning, purely to
satisfy an over-specified test.

The assertions become isapprox at rtol=1e-12, which is ~4 orders above
roundoff and ~10 orders below what this test exists to catch. Verified
that a simulated normalization revert still fails it: the relative error
would be 0.845 for :schmidt and 4.29 for :fourpi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.92405% with 57 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/batch_transforms.jl 44.23% 29 Missing ⚠️
ext/ParallelTransforms.jl 52.27% 21 Missing ⚠️
src/api_compat.jl 42.85% 4 Missing ⚠️
ext/ParallelLocal.jl 0.00% 3 Missing ⚠️
Files with missing lines Coverage Δ
ext/ParallelTransposeTransforms.jl 0.00% <ø> (ø)
ext/SHTnsKitAdvancedADExt.jl 21.75% <ø> (+1.04%) ⬆️
ext/SHTnsKitGPUExt.jl 0.00% <ø> (ø)
src/config.jl 85.52% <100.00%> (-0.80%) ⬇️
src/device_utils.jl 69.23% <100.00%> (ø)
src/fftutils.jl 57.06% <100.00%> (+0.23%) ⬆️
src/layout.jl 100.00% <100.00%> (ø)
src/parallel_dense.jl 85.71% <100.00%> (-1.08%) ⬇️
src/plan.jl 86.83% <ø> (ø)
src/qst_transforms.jl 100.00% <ø> (ø)
... and 5 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@subhk
subhk merged commit e39d1b5 into main Aug 11, 2026
12 checks passed
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