Summary
The ql-balance / KiLCA golden-record comparison is unstable because the KiLCA linear solve is not reproducible run to run for identical inputs — even single-threaded. Two independent memory bugs in code shared by all backends are the cause; both are pre-existing on main and unrelated to the fortnum migration. This issue records the root-cause analysis and links the fixes.
Case used throughout: golden f_6_2 (m=6, n=2). Primary observable: flre/linear-data/m_6_n_2_flab_[1,0]/zone_0_poy_test_err.dat and EB.dat.
Root causes
1. Uninitialized interpolation index (dominant) — fixed in #170
interp_profile (in QL-Balance/src/base/wave_code_data_64bit.f90 and KiLCA/interface/wave_code_data_64bit.f90) passes a local integer ind to eval_neville_polynom without initializing it. ind is the in/out warm-start window index; on the first call it is read uninitialized (guard in KiLCA/interp/interp.h only catches out-of-range garbage; in-range garbage flows through, out-of-range garbage triggers an OOB read of the grid array). Because this interpolates the initial background profiles onto the balance grid, it seeds a stack-garbage-dependent perturbation that propagates through the whole solve.
- valgrind memcheck: 22 "uninitialised value" reads in
eval_neville_polynom, origin interp_profile's ind.
- After fixing: those reads -> 0; run-to-run
EB.dat goes from ~7e-4 relative spread to byte-identical across runs.
2. Buffer over-read on the run path — fixed in #171
calc_wave_code_data_, calc_wave_code_data_for_mode_, calc_conductivity_matrices_for_mode_ in KiLCA/interface/wave_code_interface.cpp strcpy a non-NUL-terminated Fortran character buffer of length *pathlength. strcpy over-reads past the buffer (and can overflow the fixed 1024-byte destination). Fixed by memcpy of the known length.
- valgrind: "Invalid read of size 1" in
calc_wave_code_data_for_mode_ -> 0.
3. Residual nondeterminism (open)
After #170 and #171, a much smaller run-to-run effect remains: the solve settles into one of two discrete states differing by ~1e-9 in EB.dat (~1 ULP in the resonant radius r_res). It is valgrind-clean (not uninitialized memory), single-threaded, and appears in both GSL and fortnum builds. Cause not yet identified (candidate: address/ordering-dependent branch). This is the last blocker to a byte-exact golden.
poy_test_err is a diagnostic, not a physics output
zone_0_poy_test_err.dat is the Poynting energy-balance consistency residual |div(PoyntingFlux) - (absorbed_power + j.E)| — the solution's own numerical error (max ~1e-3), a difference of nearly-equal terms. It amplifies last-ULP input differences by ~1e8 (a ~1e-9 field perturbation becomes ~50% per-node). It should not be compared byte/rtol-exact across runs or backends. Recommend excluding it from the golden or flooring it at its own scale (atol ~1e-3), documented as a diagnostic.
The fortnum migration is transparent
Relevant to #144 and #164: with the solve made deterministic (threads pinned), GSL (main) and fortnum builds produce byte-identical output across the whole linear-data tree, including r_res. The apparent GSL-vs-fortnum differences reported earlier (sort tie order; r_res 1-ULP) are the run-to-run bimodality of item 3, present in the unmodified GSL code too — not a fortnum defect. #164's own control experiment (perturbing GSL's own root reproduces the failure) is consistent with this. Once items 1-3 land, the ql-balance step exclusions added for #164 should be revisited and the fortnum-regression framing corrected.
Reproduction
Build ql-balance.x, run it twice on the same f_6_2 runfolder with OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1, and diff flre/linear-data/m_6_n_2_flab_[1,0]/EB.dat. On main the two runs differ; with #170 applied they are byte-identical (modulo item 3). valgrind --tool=memcheck --track-origins=yes on the same run surfaces both memory errors.
Status
@marjohma for visibility — this reframes the #144 golden failures as pre-existing KiLCA nondeterminism, not a fortnum regression.
Summary
The ql-balance / KiLCA golden-record comparison is unstable because the KiLCA linear solve is not reproducible run to run for identical inputs — even single-threaded. Two independent memory bugs in code shared by all backends are the cause; both are pre-existing on
mainand unrelated to the fortnum migration. This issue records the root-cause analysis and links the fixes.Case used throughout: golden
f_6_2(m=6, n=2). Primary observable:flre/linear-data/m_6_n_2_flab_[1,0]/zone_0_poy_test_err.datandEB.dat.Root causes
1. Uninitialized interpolation index (dominant) — fixed in #170
interp_profile(inQL-Balance/src/base/wave_code_data_64bit.f90andKiLCA/interface/wave_code_data_64bit.f90) passes a local integerindtoeval_neville_polynomwithout initializing it.indis the in/out warm-start window index; on the first call it is read uninitialized (guard inKiLCA/interp/interp.honly catches out-of-range garbage; in-range garbage flows through, out-of-range garbage triggers an OOB read of the grid array). Because this interpolates the initial background profiles onto the balance grid, it seeds a stack-garbage-dependent perturbation that propagates through the whole solve.eval_neville_polynom, origininterp_profile'sind.EB.datgoes from ~7e-4 relative spread to byte-identical across runs.2. Buffer over-read on the run path — fixed in #171
calc_wave_code_data_,calc_wave_code_data_for_mode_,calc_conductivity_matrices_for_mode_inKiLCA/interface/wave_code_interface.cppstrcpya non-NUL-terminated Fortrancharacterbuffer of length*pathlength.strcpyover-reads past the buffer (and can overflow the fixed 1024-byte destination). Fixed bymemcpyof the known length.calc_wave_code_data_for_mode_-> 0.3. Residual nondeterminism (open)
After #170 and #171, a much smaller run-to-run effect remains: the solve settles into one of two discrete states differing by ~1e-9 in
EB.dat(~1 ULP in the resonant radiusr_res). It is valgrind-clean (not uninitialized memory), single-threaded, and appears in both GSL and fortnum builds. Cause not yet identified (candidate: address/ordering-dependent branch). This is the last blocker to a byte-exact golden.poy_test_erris a diagnostic, not a physics outputzone_0_poy_test_err.datis the Poynting energy-balance consistency residual|div(PoyntingFlux) - (absorbed_power + j.E)|— the solution's own numerical error (max ~1e-3), a difference of nearly-equal terms. It amplifies last-ULP input differences by ~1e8 (a ~1e-9 field perturbation becomes ~50% per-node). It should not be compared byte/rtol-exact across runs or backends. Recommend excluding it from the golden or flooring it at its own scale (atol ~1e-3), documented as a diagnostic.The fortnum migration is transparent
Relevant to #144 and #164: with the solve made deterministic (threads pinned), GSL (
main) and fortnum builds produce byte-identical output across the whole linear-data tree, includingr_res. The apparent GSL-vs-fortnum differences reported earlier (sort tie order;r_res1-ULP) are the run-to-run bimodality of item 3, present in the unmodified GSL code too — not a fortnum defect. #164's own control experiment (perturbing GSL's own root reproduces the failure) is consistent with this. Once items 1-3 land, the ql-balance step exclusions added for #164 should be revisited and the fortnum-regression framing corrected.Reproduction
Build
ql-balance.x, run it twice on the samef_6_2runfolder withOMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1, and diffflre/linear-data/m_6_n_2_flab_[1,0]/EB.dat. Onmainthe two runs differ; with #170 applied they are byte-identical (modulo item 3).valgrind --tool=memcheck --track-origins=yeson the same run surfaces both memory errors.Status
poy_test_errgolden handling (exclude/floor as diagnostic)@marjohma for visibility — this reframes the #144 golden failures as pre-existing KiLCA nondeterminism, not a fortnum regression.