A GPU based f-statistics and qpAdm toolset.
π§ Work in progress β research preview. Steppe is under active development. The compute paths that are done are validated to bit/tolerance parity with ADMIXTOOLS 2 on real AADR data, but install/onboarding, docs, and some APIs are still rough and may change. Not a stable release yet β expect sharp edges, and please don't rely on it for production work.
What it does Β· Install Β· Quick start Β· Commands Β· Python Β· Performance Β· Docs
One of my favorite hobbies is finding ways to make software faster, and Steppe grew out of that curiosity β I wanted a faster tool that would work on my not-so-modern desktop, and it was a great excuse to learn CUDA along the way. Somewhere in there it turned into something I was excited to share, and I hope it's useful to other people too.
It's still very much a personal project, so you'll occasionally find opinions, experiments, and design decisions that reflect how I like to work. At the same time, I've done my best to make Steppe reliable, approachable, and useful well beyond my own tinkering. ^^
If you use it, I genuinely hope it makes your work a little easier. And if you have ideas, questions, or spot something that could be better, please let me know.
Steppe computes f-statistics (the pairwise-f2 block tensor) and fits admixture models β qpAdm / qpWave / qpGraph / DATES, plus the standalone f3 / f4 / f4-ratio / D-statistic family β entirely on the GPU. The idea it rests on is ADMIXTOOLS's own: precompute once, fit many. One streaming pass over the genotypes builds an f2 cache; after that the tensor stays resident in VRAM and every fit runs device-side, so a model fit does zero host round-trips. That's most of why it's fast.
Beyond that f-statistics core, Steppe also ingests bring-your-own genotypes (VCF, GRCh37/38 auto-detected), computes standalone population statistics (per-SNP and all-pairs FST, the joint SFS, genotype PCA), detects relatedness (READv2), paints chromosomes (Li-Stephens), and runs a low-coverage / ancient-DNA family β PCAngsd, ancIBD, hapROH β each gated against its field-standard reference tool (plink2, scikit-allel, PCAngsd, ancIBD, hapROH), on the GPU.
Steppe is a GPU product. A CUDA-13 GPU is required at runtime; there is no CPU runtime path (the bundled
CpuBackendis a test/parity oracle only).
| GPU | NVIDIA GPU, compute capability sm_75βsm_120 (Turing β Blackwell: RTX 20/30/40/50-series, T4, A100, A6000/A40, L4/L40, H100/GH200, RTX PRO 6000, β¦). The prebuilt binary and wheel are a fatbinary covering all of these arches. |
| CUDA | CUDA 13.x runtime + a matching driver (verified on 13.0 and 13.1). This is a hard requirement β the binaries link libcudart.so.13 / libcublas.so.13 / libcusolver.so.12, so a CUDA 12 box will not run them. The runtime is resolved at load from your CUDA install (/usr/local/cuda/lib64 or LD_LIBRARY_PATH); it is not bundled. |
| OS | Linux x86_64. |
| GPUs used | single-GPU (--device 0). |
| Python (wheel only) | 3.12+ for the prebuilt wheel β a cp312-abi3 stable-ABI wheel, so one file runs on 3.12, 3.13, 3.14+; 3.9+ if you build from source. numpy is the only hard dep; pandas is optional/lazy (the DataFrame accessors). |
| Input formats | EIGENSTRAT, PACKEDANCESTRYMAP/GENO, TGENO, ANCESTRYMAP, PLINK (.bed/.bim/.fam) β auto-detected from the --prefix triple; all decode bit-identically. f2 dirs (STPF2BK1) are cacheable and reusable (not AT2 .rds-compatible). |
Fastest β one command. Downloads the CLI, checks your CUDA 13 runtime (with a clear message
if it's missing, not the opaque loader error), and installs a launcher that sets
LD_LIBRARY_PATH for you so you never have to:
curl -fsSL https://raw.githubusercontent.com/suzuenhasa/steppecu/main/install.sh | bash
# -> ~/.local/bin/steppe (override the dir: ... | bash -s -- --dir /usr/local/bin)
source ~/.bashrc # the installer added ~/.local/bin to PATH β load it here (or open a new terminal)
steppe --helpOr do it by hand β three manual paths below; the prebuilt CLI + wheel are attached to the
v0.2.0 release and download over
plain curl (no GitHub auth). (1) is simplest: one binary, no Python, no build.
A single standalone executable (a fatbinary for every supported GPU):
curl -L -o steppe https://github.com/suzuenhasa/steppecu/releases/download/v0.2.0/steppe
chmod +x steppe
./steppe --versionIt links the system CUDA 13 runtime at load. If you see libcudart.so.13: cannot open shared object file, your box has CUDA 12 (or CUDA isn't on the loader path) β Steppe needs
CUDA 13. Point the loader at it if needed: export LD_LIBRARY_PATH=/usr/local/cuda/lib64.
The wheel bundles the steppe command-line tool (every subcommand, incl. scan and the
f4/f3 sweeps) and the importable Python package β so one pip install gives you both:
curl -L -O https://github.com/suzuenhasa/steppecu/releases/download/v0.2.0/steppe-0.2.0-cp312-abi3-linux_x86_64.whl
pip install ./steppe-0.2.0-cp312-abi3-linux_x86_64.whl
steppe --version # the CLI β now on your PATH
python -c "import steppe; print(steppe.__version__)" # and the Python APINeeds Python 3.12+ and the CUDA 13 runtime; numpy is the only hard dependency. (It's a
cp312-abi3 stable-ABI wheel β the same file installs on 3.12, 3.13, 3.14+.) The bundled
steppe links CUDA 13 at load, same as the Python module β if it can't find libcudart.so.13,
export LD_LIBRARY_PATH=/usr/local/cuda/lib64.
Needs the full CUDA 13 toolkit (nvcc) + CMake β₯ 3.28 + Ninja.
git clone https://github.com/suzuenhasa/steppecu && cd steppecu
cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=Release -DSTEPPE_BUILD_CLI=ON
cmake --build build # β build/bin/steppeDefaults to sm_120. Build for the GPU in the box: add -DCMAKE_CUDA_ARCHITECTURES=native.
Build a portable fatbinary: -DCMAKE_CUDA_ARCHITECTURES="75-real;80-real;86-real;89-real;90-real;100-real;120-real;120-virtual".
Build the Python wheel: pip wheel . --no-deps (add --config-settings=cmake.define.CMAKE_CUDA_ARCHITECTURES=native for your GPU).
First result in seconds. The installer stages a tiny real 10-population AADR f2 cache, so you can fit a qpAdm model immediately β no data download, no build:
steppe qpadm --f2-dir ~/.local/share/steppe/example \
--target Czechia_EBA_CordedWare --left Russia_Samara_EBA_Yamnaya,Turkey_N \
--right Mbuti,Han,Papuan,Karitiana,Iran_GanjDareh_N,Israel_Natufian
# -> weights (Russia_Samara_EBA_Yamnaya ~0.73, Turkey_N ~0.27) + tail p ~0.11 β a real fit.
# Corded Ware β 73% steppe (Yamnaya) + 27% Anatolian farmer β the migration this tool is named
# for (Haak et al. 2015). That steppe ancestry then flows on into Bell Beaker β see docs/userguide/.Built from source, or want it by hand? Grab the same example directly:
mkdir example && curl -fsSL \
https://github.com/suzuenhasa/steppecu/releases/download/v0.2.0/example_f2.tar.gz \
| tar xz -C exampleYour own data? Steppe works over an f2-blocks directory β a cache you build once from a
genotype prefix, then run any number of fits against. The full workflow (data β extract-f2 β
qpadm β sweeps), every command's flags with runnable examples, and a marimo notebook walkthrough
all live in the user guide β start with getting started.
steppe --help lists the 25 subcommands (all compute runs on the GPU); the
user guide has a page per command explaining every flag with runnable examples.
| Command | What it does |
|---|---|
extract-f2 |
Precompute the f2_blocks dir from a genotype prefix (the precompute-once / fit-many artifact). |
qpadm |
Single-model qpAdm fit over an f2_blocks dir. |
qpadm-rotate |
qpAdm rotation sweep over a source pool (batched on the GPU). |
scan |
Guided proxy/model scanner: a gated, best-first-ranked qpAdm search over a pool, with a relatedness pre-rank, drop/replace swap suggestions, and a sources-only outgroup-admissibility check. |
qpwave |
qpWave rank-sufficiency sweep (no target; left[0] is the reference). |
qpgraph |
Fit a single fixed admixture graph (--graph edge-list). |
qpgraph-search |
Exhaustive bounded admixture-graph topology search (returns the global-best). |
f4 |
Standalone f4(p1,p2;p3,p4) β est/se/z/p per quartet (--all-quartets for a sweep). |
f3 |
Standalone f3(C;A,B) / outgroup-f3 β est/se/z/p per triple (--all-triples for a sweep). |
f4-ratio |
f4-ratio admixture proportion alpha = f4(p1,p2;p3,p4)/f4(p1,p2;p5,p4). |
f4-sweep |
GPU all-quartets f4 scan over C(P,4) with an on-device |z|/top-k filter (survivors only). |
f3-sweep |
GPU all-triples f3 scan over C(P,3) with an on-device filter. |
qpdstat |
D-statistic: --f2-dir reports f4 (f2-path), or --prefix reports the genotype-path normalized-D. |
qpfstats |
Genotype-path joint f2 smoother (the AADR-missingness remedy); emits an f2 dir. |
dates |
Admixture dating (DATES): generations since admixture from the ancestry-covariance decay. |
ingest |
Genotype a (b)gzipped VCF sample onto a target panel (GRCh37/38 auto-detected); --likelihoods emits a GPU GL/PL/GP tensor. |
cache |
Inspect/verify a local f2 cache and fetch AADR panels (the dataset manager). |
readv2 |
GPU pseudo-haploid relatedness (READv2) β all-pairs kinship degree over your own data. |
paint |
Li-Stephens chromosome painting β coancestry and per-SNP local ancestry (--face). |
fst |
Per-SNP Weir & Cockerham FST between two populations, or the whole populationΓpopulation matrix (--all-pairs). |
sfs |
2D joint site-frequency spectrum between two populations (folded or unfolded). |
pca |
Patterson-2006 genotype PCA (scales to the full AADR cohort) with an optional interactive HTML scatter. |
pcangsd |
Low-coverage PCA / individual allele frequencies from genotype likelihoods (a beagle GL file). |
ibd |
ancIBD IBD-segment detection between imputed ancient individuals. |
roh |
hapROH runs-of-homozygosity for an ancient genome against a phased reference panel. |
Precision & output. --precision emu40 (default: emulated-FP64 / Ozaki for the
matmul-heavy paths, native FP64 for the cancellation-sensitive stats) Β· emu32 Β· fp64
(native) Β· tf32 (screening only). f4/f3/qpDstat and the jackknife SEs run in native FP64 and
are bit-identical across GPU arches. Output: --format csv|tsv|json, --out FILE
(stdout if omitted), --device 0.
The Python facade mirrors the CLI: build or load an f2 handle, then run any fit against it β
results come back as pandas-friendly objects (.weights, β¦).
import steppe
f2 = steppe.read_f2("f2_dir/")
res = steppe.qpadm(f2, target="England_BellBeaker",
left=["Czechia_EBA_CordedWare", "Turkey_N"],
right=["Mbuti", "Han", "Papuan", "Karitiana", "Iran_GanjDareh_N", "Israel_Natufian"])
print(res.weights) # DataFrame: [target, left, weight, se, z]The full API β every function, the result objects, the DataFrame accessors β is on the
Python page of the user guide. pandas is optional (imported lazily).
Making things fast is the whole point, so here's what one RTX 5090 does on real AADR data (single GPU, the release binary; measured on a shared box, so these are conservative).
Precompute β stream genotypes β f2 cache (extract-f2, the one expensive step):
| Panel | SNPs | genotypes | 100 pops | 500 pops |
|---|---|---|---|---|
| Human Origins | 584 K | 3.8 GB | 5.5 s | 18 s |
| 1240k | 1.23 M | 6.7 GB | 9 s | 37 s |
| 2M | 2.14 M | 12 GB | 12 s | 58 s |
(Table figures are [UNVERIFIED β needs measurement]: they don't trace to a gated commit. The one
gated extract-f2 number is --auto-top-k 2000 on the 2 M panel completing in 88 s, peak host
RSS 54 GiB under a 90 GiB cap [ca7ed59].)
The genotype matrix streams out-of-core, and large caches auto-tier (the f2 blocks spill from VRAM β host RAM β disk as needed), so panels far bigger than the card's memory still build.
Fits & stats β single call on a 500-population cache: qpAdm, qpWave, f4, f3, f4-ratio, and D all
land in ~3 seconds; DATES in ~2.5 s [c7af01b]. qpfstats (the genotype-path f2 smoother) runs
in ~9 s for a 40-population tensor [9908d0b] β it scales with the number of pairs, so it isn't a
sub-second call at 500 populations.
Model search β a qpAdm rotation of 10,660 candidate models in 4.55 s (~2,340 models/s, f2-load-bound; ~2,400/s in pure GPU compute) [9264c53], and an admixture-graph topology search of 1,590 candidate graphs in 1.1 s (~1,450 topologies/s) [cf06b1b].
Sweeps β every combination, filtered on-device to the top survivors:
| Sweep | statistics computed | time |
|---|---|---|
| f3, all triples (500 pops) | 20.7 million | [UNVERIFIED β needs measurement] |
| f4, all quartets (500 pops) | 2.57 billion | 2.9 min [49ec38e] |
| f4, all quartets (700 pops) | 9.92 billion | 12 min [49ec38e] |
β roughly 14 million f4-statistics per second β 9.9 billion of them in about twelve minutes, on one GPU.
Steppe is checked against ADMIXTOOLS 2 (the reference implementation) on real AADR data: f4 / f3 / qpAdm weights, chi-square, p, and the block-jackknife standard errors match to bit/tolerance parity.
If it helped your work, please cite the software:
Steppe (v0.2.0). https://github.com/suzuenhasa/steppecu
And please also cite ADMIXTOOLS 2 and the underlying methods β they're the science Steppe reruns; see Acknowledgments for the references.
- docs/userguide/ β start here. Getting started, a page per feature (what every flag does + commands to run), the command cheatsheet, and the Python API.
- docs/examples/ β a worked end-to-end example + a marimo notebook walkthrough.
- docs/reference/ β per-module internals, one file per source file (for contributors).
Steppe stands on the shoulders of giants, and would not have been possible without the work of great people in this field. Here are the projects and papers Steppe builds on or was inspired by:
ADMIXTOOLS 2 β I used this to check Steppe against constantly. The f-statistics and qpAdm fits are the parity oracle that I validated Steppe against, bit for bit. If you use Steppe, please also cite their work, which can be found here: https://doi.org/10.7554/eLife.85492 Β· article Β· GitHub.
ADMIXTOOLS β the f2/f3/f4/D and the qp* methods (qpAdm, qpWave, qpGraph, qpDstat) all come from the original ADMIXTOOLS methods worked out by Nick Patterson, David Reich, and colleagues. Steppe is a new engine, but the hard part was based on their work: https://doi.org/10.1534/genetics.112.145037.
Dating / DATES / ALDER β Steppe reimplements the DATES admixture-dating method, which is based on the earlier approach in ALDER that Steppe's dating lineage builds on. You can find the relevant papers here: https://doi.org/10.7554/eLife.77625, https://doi.org/10.1126/science.aat7487, https://doi.org/10.1534/genetics.112.147330.
EIGENSOFT, convertf, PLINK β Steppe supports various genotype formats and can regenerate parity goldens, including PLINK. You can find the relevant information here: https://doi.org/10.1371/journal.pgen.0020190, https://doi.org/10.1038/ng1847, https://doi.org/10.1186/s13742-015-0047-8, https://www.cog-genomics.org/plink/2.0/, https://doi.org/10.1086/519795.
igraph β Steppe mirrors ADMIXTOOLS 2's graph data-structure conventions on-device with igraph. I can't find a DOI to cite, but here's the website: https://igraph.org.
AADR datasets β Steppe's validations and benchmarks were run on real ancient-DNA genotypes from the Allen Ancient DNA Resource. I'd also like to point out that the AADR datasets are curated, managed, and made freely available. If you found their efforts contributed to anything you've done, please show the team your support! Here are the relevant citations: https://doi.org/10.1038/s41597-024-03031-7, https://doi.org/10.7910/DVN/FFIDCW.
The name "Steppe" β the name was coined for the steppe-ancestry migration story, and the one paper that specifically inspired it was the Haak 2015 study of Massive migration from the steppe as a source for Indo-European languages in Europe. It was the first study I was able to reproduce and used as an example, and is the study I use in the quickstart along with the Olalde study: https://doi.org/10.1038/nature14317, https://doi.org/10.1038/nature25738.
Claude Code β a different kind of thank-you. My pair-programming partner for this whole build: GPU kernels, chasing parity bugs bit-for-bit, the proxy/model scanner, the docs you're reading, and a hundred late-night refactors β all of it side by side in the terminal. A real G, and a genuinely good bro to build with. π€
Steppe is released under the GPL-3 license (see LICENSE). I read through ADMIXTOOLS 2's source to understand how it works so I could build my own GPU version, and releasing Steppe under the same license is the right thing to do. If I've credited anything here imperfectly, please tell me β I want to get it right. ^^
