Python bindings for SBD (Selected Basis Diagonalization): MPI-parallel eigensolvers for a Hamiltonian projected onto a selected subspace, on CPUs and on NVIDIA or AMD GPUs. The main use is as the eigensolver of a sample-based quantum diagonalization (SQD) loop from qiskit-addon-sqd; the solvers can also be called directly.
Documentation: https://qiskit.github.io/sbd-eigensolver-python/, with the API reference and release notes.
Note
This package is newly open-sourced. The Python API follows semantic versioning, but the build configuration and GPU backends have been exercised on a limited set of platforms — please report issues.
-
Molecules, from an FCIDUMP, restricted or unrestricted (UHF):
- TPB,
sbd.tpb_diag: the subspace is the product of an alpha and a beta string list, so its dimension is|adet| × |bdet|. The mature path, and the one the SQD integration uses. - GDB,
sbd.gdb_diag: the subspace is an explicit list of determinants, which can be sharded across ranks and grown by heat-bath expansion. Experimental: newer, a smaller tested surface, and still evolving as upstream SBD does.
If your subspace is a product of alpha and beta lists, use TPB; otherwise GDB.
- TPB,
-
Lattice and spin models, written as creation and annihilation operators (CAOP): the ground state,
sbd.caop_diag_from_files, and its Green's function,sbd.spectrum. -
Energy variance and Epstein–Nesbet PT2 of a saved state,
sbd.analysis: how far it is from the exact one. Runs on CPU with MPI + OpenMP across nodes. -
qiskit-addon-sqd integration,
sbd.sbd_solver: SBD as thesci_solverof the SQD loop.
Upstream SBD ships each feature as its own C++ app, configured and compiled by hand, with the backend, unrestricted integrals and complex amplitudes fixed when it is compiled. The bindings bring them under one install and one API:
- One install.
pip installbuilds TPB, GDB, CAOP, the Green's function and the variance evaluators together; there is no separate executable to build, configure or keep in step for each. - Choices per call, not per build. The device is a
device=argument, a UHF FCIDUMP selects the unrestricted build by itself, and CAOP takescomplex=True, so changing any of them means changing an argument, not recompiling. - Python in, Python out. TPB and GDB take determinants as arrays and return the energy, RDMs and next subspace as Python objects, so a loop such as SQD or heat-bath rounds runs in one MPI job without files passed between programs.
- The right code path, or a clear error. A feature a device does not support, such
as GDB on
gpu-omp, raises an error naming the devices that do, instead of running on the host; a backend that was not built says how to build it. Inputs upstream would get silently wrong or hang on, such as a rank count that does not fit the MPI grid, raise aValueErroron every rank before any work starts.
The examples/ drivers keep the command-line workflow, for
running from mpirun with options as upstream's apps do.
Every install builds the CPU backend, and every GPU backend the toolchain supports;
each is loaded only when first used. Choose one per call with device=:
cpu: the host, with OpenMP.gpu: NVIDIA GPUs, with Thrust.gpu-omp: NVIDIA or AMD GPUs, with OpenMP offload.
What each runs:
cpu |
gpu |
gpu-omp |
|
|---|---|---|---|
| TPB | ✅ | ✅ | ✅ |
| GDB | ✅ | ✅ | not supported |
| CAOP ground state | ✅ | ✅ | not supported |
| CAOP Green's function | ✅ | ✅ (1) | not supported |
| Unrestricted (UHF) FCIDUMPs | ✅ | ✅ | not supported |
| Energy variance and PT2 | on the CPU (2) | on the CPU (2) | on the CPU (2) |
- Only applying the Hamiltonian on the fly (
--hamiltonian-mode on-the-fly) uses the GPU; the default stored Hamiltonian and the rest of the work run on the CPU. Seeexamples/caop/README.md. - The analysis always runs on the CPU, with MPI and OpenMP, whichever device the
solve used, so it takes no
device=.
gpu-omp does not support GDB or CAOP because upstream has no offload code for them,
nor UHF files, since its kernels read only the restricted integral layout. It is one
device for both vendors: an install compiles for one, and
sbd.get_backend("gpu-omp").__sbd_offload_target__ says which. The GPU backends expect
a GPU-aware MPI; see INSTALL.md for the fallbacks.
The extension modules are compiled on your machine — no wheels are published — so the backends you get depend on the toolchain the build finds. For the common CPU case:
conda create -y -n sbd -c conda-forge \
python=3.13.12 pybind11 numpy setuptools wheel openblas pyscf pip mpi4py
# ...plus llvm-openmp on macOS
conda activate sbd
pip install sbd-eigensolverThen check what was built:
python -c "import sbd; print(sbd.available_backends())"
# CPU only: ['cpu']
# NVIDIA, default build: ['cpu', 'gpu', 'gpu-omp']
# AMD, default build: ['cpu', 'gpu-omp']cpu and gpu are also built with upstream's -D_UHF, for unrestricted FCIDUMPs; a
solve picks that build from the file, so it is not a separate device.
sbd.print_info() lists it.
See INSTALL.md for the rest: prerequisites per platform, installing from a git checkout with the submodule, every environment variable the build reads (GPU toolchains, architectures, MPI and BLAS selection, narrowing which backends get built), building against an existing host MPI, GPU-aware MPI, and fuller verification.
from functools import partial
from qiskit_addon_sqd.fermion import diagonalize_fermionic_hamiltonian
from sbd.device_config import DeviceConfig
from sbd.sbd_solver import solve_sci_batch
sbd_solver = partial(
solve_sci_batch,
sbd_config={"method": 0, "eps": 1e-5, "max_it": 10, "max_nb": 10},
device_config=DeviceConfig.gpu(), # or .cpu(), .gpu_omp()
)
result = diagonalize_fermionic_hamiltonian(
hcore, eri, bit_array,
sci_solver=sbd_solver, # SBD plugs in here
norb=norb, nelec=nelec,
samples_per_batch=3000, num_batches=3, max_iterations=5,
symmetrize_spin=True,
)Run it under mpirun, for example mpirun -np 8 python my_sqd.py with one rank per
GPU: every rank calls the solver, and SBD spreads each diagonalization over them. No
sbd.init() is needed. It requires qiskit-addon-sqd
0.13.1 or later, for this distributed (SPMD) mode; SBD-selected carryover and subspace
policies need 0.14.0 or later.
- TPB only. qiskit-addon-sqd's fermion solver assumes the subspace is a product of
alpha and beta string lists, a restriction inherited from PySCF, so a GDB determinant
list cannot be passed through it
(Qiskit/qiskit-sqd#389). For
GDB, call
gdb_diagdirectly or use the drivers inexamples/gdb/. - Who chooses the next subspace. By default qiskit-addon-sqd chooses the carryover
from the amplitudes SBD returns. With
"carryover_type"insbd_config, SBD chooses it instead; see SBD-selected carryover and Trim SQD. - Growing the subspace.
run_sqd_enlarge_subspace_sbd.pyadds single excitations of the dominant determinants between rounds, and so keeps improving past a fixed sample pool where the plain loop stops.
examples/tpb/README.md has every SQD and
SBD parameter, and run_sqd_sbd.py is a complete
driver.
Located in examples/, organized by solver. examples/README.md
shows how they fit together, from a solve through its variance check to, for lattice
models, the Green's function. Each folder's README covers its drivers, options, and the
backend and threading settings that matter for it:
examples/tpb/— TPB diagonalization, the SQD loops, the subspace-enlargement driver, and unrestricted (UHF) FCIDUMPs.examples/gdb/— GDB over an explicit determinant list, and iterative heat-bath expansion.examples/caop/— lattice and spin models: the ground state, its variance, and its Green's function.examples/analysis/— energy variance and PT2 of a state saved by any solver, as a separate MPI job or at the end of the same run.
Build and runtime problems — GPU backends building as host code, MPI mismatches, a non-GPU-aware MPI, macOS OpenMP, GPU assignment across ranks — are in TROUBLESHOOTING.md. Please report anything else as a GitHub issue.