Skip to content

Fix bit_length=64 undefined behavior, make it configurable, and lower --samples/--max_nb defaults - #12

Merged
hfwen0502 merged 3 commits into
mainfrom
fix-bit-length-undefined-behavior
Sep 1, 2026
Merged

Fix bit_length=64 undefined behavior, make it configurable, and lower --samples/--max_nb defaults#12
hfwen0502 merged 3 commits into
mainfrom
fix-bit-length-undefined-behavior

Conversation

@hfwen0502

@hfwen0502 hfwen0502 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

1. bit_length = 64 is undefined behavior

bit_length sets how many bits SBD packs into each size_t of its
std::vector<size_t> bitstring representation. The Python wrapper hardcoded 64
in four places, but bitadvance() in framework/bit_manipulation.h computes

size_t d = (((size_t) 1) << bit_length) - 1;

Shifting a 64-bit size_t by 64 is undefined behavior. In practice the shift
count is masked to 0, so the mask collapses to d == 0 (checked on aarch64 and
x86-64). bitadvance() is reached from mpi_redistribution() and
mpi_sort_bitarray() — the multi-rank determinant distribution paths.

RIKEN documents the default as 20 (see --bit_length in
apps/chemistry_tpb_selected_basis_diagonalization/README.md), which is also
what run_sbd_diag.py already defaults to. Only run_sqd_sbd.py passed 64.

This adds SBD_DEFAULT_BIT_LENGTH = 20 and a --bit_length option. The value
is not range-checked: callers can still pass 64 explicitly if they want the old
packing.

2. Latent correctness bug: the value only reached half the pipeline

bit_length decides both how Python packs determinants and how the C++ engine
interprets them, so the two must agree. Previously a caller-supplied
bit_length reached the engine via TPB_SBD.bit_length but not
from_string()/makestring(), which stayed at 64 — so --bit_length 48 would
have silently corrupted every determinant.

_ci_strings_to_sbd_dets() and _sbd_dets_to_ci_strings() now take
bit_length, and _solve_sci_core() builds the config before packing so both
sides read one value (sbd_data.bit_length, the same object handed to
tpb_diag).

3. Defaults and example

  • --max_nb 50 → 10, matching upstream chemistry/tpb/sbdiag.h.
    davidson_thrust.h allocates 2 * num_block device vectors of length dim,
    so GPU workspace scales as dim * (2 * max_nb + ~4) * 8 bytes — roughly a 4x
    reduction.
  • --samples 10000 → 3000.
  • run_sqd_sbd.ipynb sbd_config updated from 64 to 20 (its max_nb was
    already 10). The notebook is executed by the nbmake tox env in CI.

Validation

Threading actually verified (GB200, _create_sbd_config + converters called
directly). Note that comparing energies across bit_length values cannot prove
this on its own: a silently-ignored flag also yields identical energies. These
two checks distinguish the cases.

Config reaches the object passed to tpb_diag:

no override                    -> sbd_data.bit_length = 20
sbd_config={'bit_length': 48}  -> sbd_data.bit_length = 48
sbd_config={'bit_length': 63}  -> sbd_data.bit_length = 63

Packing honours it — norb=24, so word count must be ceil(24/bit_length):

bit_length=20 -> 2 size_t words   round-trip OK
bit_length=48 -> 1 size_t word    round-trip OK
bit_length=63 -> 1 size_t word    round-trip OK

The changing word count is the observable that rules out the flag being dropped,
and each det round-trips back to its original CI string.

End-to-end energy (NVHPC 25.5, cc100, 4 GPUs, CUDA-aware MPICH 5.0.1):
H2O 1e-5 subspace, dim 303,601 → -76.2437350421 at both np=1 and 2x2,
matching the pre-change value and run_sbd_diag.py's independent file-based
path (-76.2437349413). bit_length 20/48/63 all agree to the last digit.
norb=24 at the new default spans two size_t words, so this exercises the
multi-word packing path (previously always single-word at 64).

Not verified

  • The new --max_nb 10 default is upstream's value and reduces memory as
    described, but has not been checked for convergence on a large production
    system; a smaller Krylov space may need more restarts.
  • The H2O energies above were produced at max_nb = 50; the new default was not
    separately re-validated.

…able

SBD packs determinant bitstrings into std::vector<size_t> with
`bit_length` bits per word. The Python wrapper hardcoded 64 in four
places, but bitadvance() in framework/bit_manipulation.h computes

    size_t d = (((size_t) 1) << bit_length) - 1;

Shifting a 64-bit size_t by 64 is undefined behavior. In practice the
shift count is masked to 0, so the mask collapses to d == 0 (checked on
aarch64 and x86-64). bitadvance() is reached from mpi_redistribution()
and mpi_sort_bitarray(), the multi-rank determinant distribution paths.

RIKEN documents the default as 20 (see --bit_length in
apps/chemistry_tpb_selected_basis_diagonalization/README.md), which is
also what run_sbd_diag.py already defaults to; only run_sqd_sbd.py was
passing 64.

Changes:
- Add SBD_DEFAULT_BIT_LENGTH = 20 in sbd_solver.py and use it for the
  _create_sbd_config default.
- Thread the effective value through. _ci_strings_to_sbd_dets() and
  _sbd_dets_to_ci_strings() now take bit_length, and _solve_sci_core()
  builds the config before packing so determinants are packed with the
  same value the C++ engine uses to interpret them. Previously a
  caller-supplied bit_length reached the engine but not the packing,
  which would silently corrupt the determinants.
- Add --bit_length to run_sqd_sbd.py (default 20) instead of hardcoding.

Validated on GB200 (NVHPC 25.5, cc100, 4 GPUs, CUDA-aware MPICH 5.0.1):
- H2O 1e-5 subspace, dim 303,601: -76.2437350421 at bit_length 20, both
  at np=1 and 2x2, matching the pre-change value and run_sbd_diag.py's
  independent file-based path (-76.2437349413).
- bit_length 20 / 48 / 63 give bit-identical energies, confirming that
  packing, unpacking and the engine now agree. norb=24 at bit_length=20
  spans two size_t words, so this also exercises the multi-word path.
--max_nb sets the Davidson block size, and davidson_thrust.h allocates
2 * num_block vectors of length dim on the device, so GPU memory scales
as dim * (2 * max_nb + ~4) * 8 bytes. 10 matches the upstream default in
chemistry/tpb/sbdiag.h and cuts that workspace by ~4x versus 50.
The notebook's sbd_config hardcoded 64, the value this PR removes as
undefined behavior. Its 'max_nb': 10 already matches the new CLI default.
@hfwen0502
hfwen0502 merged commit de20370 into main Sep 1, 2026
6 checks passed
@hfwen0502
hfwen0502 deleted the fix-bit-length-undefined-behavior branch September 1, 2026 18:05
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