From d0d73169c2087c7a7dfc24d87c385a6d640bb5d3 Mon Sep 17 00:00:00 2001 From: alejandrogzi Date: Sun, 13 Sep 2026 19:14:30 +0200 Subject: [PATCH 01/10] feat: sync the tested experimental tree (rounds 83-96) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sources are alejandrogzi/hspZ `experimental` @ dd1b1d2 byte for byte, with the production packaging re-applied: file headers, the clap attribute, the hspZ spelling in usage and messages, and the help text sanitised of references to the experimental tree's planning file (restores what v0.0.3 shipped). New in this sync, on top of what the earlier release branches carried: * `hspZ index` / `run --index DIR` — on-disk reference tables, loaded instead of rebuilt, validated against the parsed reference and the resolved plan before anything reaches the GPU (src/index.rs). * `--query-list` on two or more GPUs — the batch's work units are spread over the workers by the same count partition a single query uses, and the receiver reorders each job's results, so per-job output is unchanged. Exactness on this tree: 174 unit tests; chr20 digest 70152625c1a75cb3; the 5x3 plan at --gpus 2 with the unit partition cb38f81e9dc4ee9e; a three-query batch byte-identical to the standalone runs at --gpus 1, at --gpus 2, and with --index. --- Cargo.lock | 2 +- Cargo.toml | 10 +- assets/changelog/changelog.md | 108 + assets/tests/multiplicity_gate.sh | 17 +- docs/scripts/generate-cli-reference.sh | 2 +- docs/src/generated/cli-reference.txt | 101 +- docs/src/pages/docs/cli.astro | 2 +- docs/src/pages/docs/gpus.mdx | 36 + src/benchmark.rs | 271 +- src/census.rs | 502 ++- src/cli.rs | 204 +- src/compare.rs | 25 +- src/gpu/kernels.rs | 733 +++- src/gpu/mod.rs | 3245 +++++++++++++++-- src/hits_estimate.rs | 660 ++++ src/hsp.rs | 108 +- src/index.rs | 1922 ++++++++++ src/main.rs | 22 +- src/partition.rs | 92 +- src/plan.rs | 3075 +++++++++++++++- src/run.rs | 4432 ++++++++++++++++++++++-- src/scoring.rs | 39 +- src/seed.rs | 382 +- src/sequence.rs | 109 +- src/sink.rs | 30 +- src/timing.rs | 79 +- 26 files changed, 14745 insertions(+), 1463 deletions(-) create mode 100644 src/hits_estimate.rs create mode 100644 src/index.rs diff --git a/Cargo.lock b/Cargo.lock index 2e97f0a..644d958 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,7 +385,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hspz" -version = "0.0.3" +version = "0.0.6" dependencies = [ "clap", "cuda-core", diff --git a/Cargo.toml b/Cargo.toml index 8c6b495..761a9d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hspz" authors = ["Alejandro Gonzales-Irribarren "] -version = "0.0.3" +version = "0.0.6" edition = "2024" license = "GPL-3.0" description = "GPU-accelerated high-scoring ungapped alignment pair backend" @@ -27,7 +27,7 @@ twobit = "0.2.2" tar = "0.4.46" [features] -default = ["simd-prelude", "find-hits-warp"] +default = ["simd-prelude", "find-hits-warp", "ref-loc-buckets"] # Benchmark-only find_hsps counters. Never time a build with this # on — per-hit stores and per-tile increments shift register pressure. @@ -63,6 +63,12 @@ dense-anchors = ["warp-score-gate"] # 16 hits/seed; sparse launches keep thread-per-seed. Default. find-hits-warp = ["dense-anchors"] +# Round 83-87: between find_hits and the score gate, stably reorder a chunk's anchors +# into reference-address buckets so consecutive warps of the gate gather from an +# L2-resident reference window. Output stays byte-identical. Default; at runtime +# `HSPZ_REF_BUCKETS=auto` (unset) measures both paths per engine and keeps the faster. +ref-loc-buckets = ["dense-anchors"] + # Score the first 64 left bases as 32 adjacent pairs. left-pair-tile = ["dense-anchors"] diff --git a/assets/changelog/changelog.md b/assets/changelog/changelog.md index 2a74679..f856e5b 100644 --- a/assets/changelog/changelog.md +++ b/assets/changelog/changelog.md @@ -36,6 +36,114 @@ All notable changes to `hspZ` are documented here, newest first. +## [0.0.6] — 2026-09-11 + +One reference, many queries, and a CPU-only seed-hit estimator. + +- **One reference, many queries: `--query-list`.** `hspz run --reference ref.fa --query-list queries.txt --output OUT` + runs every listed query FASTA in one process: each reference bin is built and uploaded once instead of once per + query, while every query keeps its own plan, cap, seed batches, dedup scopes and `-D` history, so its output under + `OUT/000001/`, `OUT/000002/`, … is byte-identical to a standalone run (verified file for file on 100 queries). + `-Z` writes one archive per job; `OUT/queries.tsv` maps job → path, size, sha256, blocks, units, HSPs, completion. + Measured on an L4 against hg38: 100 queries of 5 Mbp took 10,400–11,600 s as separate runs and 1,890–1,900 s + batched (−82%); three 5 Mbp queries −62%; whole-chromosome queries (61–121 Mbp) −2.6% (GPU-bound); whole-genome + queries gain under 1%. This release: W=1 (`--gpus 1`), all jobs must plan to identical reference bins, `-B 0`, + `--kegalign-bins` and `--from-manifest` are rejected in batch mode, a failing job aborts the batch (partial outputs + may remain), dumps become per-job siblings (`out.manifest.000001`). Bin-major order delays the first result. +- **`hspz hits-estimate` (CPU only).** Prints, before any GPU work, the exact number of seed hits each work unit + (reference bin × query block) will produce, from dense k-mer histograms with the transition variants folded in; + matches the `--time` ledger exactly (whole genome: 42 units, 6.12e12 hits). GPU time per unit is ~0.37 ns per hit on + a 4090, so the rows are the unit costs of a multi-GPU schedule. 56–62 s on 32 cores for hg38 × mm39 (10 GiB RSS); + `--stride N` samples the query side. A planning/diagnostic tool; `run` does not call it. + +## [0.0.5] — 2026-09-10 + +Unit-level static partitioning behind a device-class guard, with a per-unit ledger +and emitter-side receiver checks. Output bytes are unchanged. + +- **Unit-level static partition (default on for matching GPUs).** With two or more + GPUs the frozen plan's work units are now split across workers by count quotas + (whole bins first, then contiguous query slices of as few bins as possible, one + extra reference build per split bin) instead of whole-bin ownership, when every + device the run uses is the same class (equal SM count and L2, nominal clocks + within 10%) and each worker has its own device. Output bytes are unchanged. + Measured on canonical hg38 × mm39 at `MAX_HITS=16,711,680`: seed-and-filter + wall **−11.9%** on 2× RTX 4090 (1,400 → 1,233 s) and **−5.1%** on 4× RTX 4090 + (718 → 681 s), identical output. `HSPZ_UNIT_PARTITION=0` restores whole-bin + ownership, `1` forces the partition; W=1, time-sliced workers and mixed device + classes keep whole-bin ownership. The static-attribute guard cannot detect + same-model cards with different sustained clocks (on such a pair the balance + depends on device order). +- **Per-unit ledger.** `--time` prints one `unit ledger:` row per work unit + (worker, device, bins, bp, GPU ms, host start/end, pack/swap ms, seeds, hits, + raw/final HSPs) plus per-worker finish/busy lines, and every run prints + `schedule:` lines with the policy, its reason, and each worker's visits. +- **Receiver checks.** The emitter now rejects duplicate or out-of-plan work units + and verifies completeness after joining the workers, reporting a worker's own + error first. +- **Automatic layout (`-B 0`, opt-in measurement tool).** `-B 0` resolves a + count-balanced layout for the current machine instead of the fixed 500 Mbp + default; it changes the HSP set (a different frozen plan), so measure per machine + before adopting it (a 2× T4 screen was 17–22% slower than the default). At `--gpus W ≥ 2` the reference + takes `R = min(records, ceil(R_default/W)·W)` LPT-balanced bins, and the query + collapses to one bin when the whole query fits both the per-worker device budget + and the host preflight (an explicit `--query-block-size` is honoured); `W=1` + resolves to exactly the 500 Mbp plan, and a candidate that fails the final fit + falls back to the default policy rather than erroring. Resolved sizes are stored + in the manifest/`--dump-plan`, and one `layout: auto W=…` line prints the flags + that reproduce them. `-B 0` with `--kegalign-bins` is an error. + `HSPZ_LAYOUT_FORCE_WORKERS` exercises the W≥2 path on one device. +- **Diagnostics.** The ref-buckets autotune drops and restarts an OFF block that + reaches twice the block span with fewer than four settled chunks (an + inconclusive ON block decides OFF, with a ledger line). `--time` attributes + gap pairs per worker, counts the >1 s transitions the gap accounting discards, + and totals the host phases (query pack, swap, chunk prep, seed table, + standalone reference prep). + +## [0.0.4] — 2026-09-09 + +Two exact speed-ups, both byte-identical to 0.0.3 on every device tested (NVIDIA L4, +RTX 4090, AMD via ZLUDA), plus the foundation repairs behind them. + +- **Reference-locality bucketing (`ref-loc-buckets`, default feature).** Between + `find_hits` and the score gate, each hit chunk is stably reordered into + reference-address buckets so consecutive warps gather from an L2-resident + reference window; survivors are restored to their original order before + `find_hsps`, so the HSP set and file bytes are unchanged. On a 72 W L4 the + lower DRAM traffic lets the SM clock rise (1142 → 1370 MHz) and the whole-genome + wall falls **6.5%** (hg38 × mm39, one GPU, disjoint reversed pairs). Cards that + already run near their boost clock pay the bucketing pass instead (RTX 4090: + +3.7%), so the choice is made at runtime: `HSPZ_REF_BUCKETS` unset means `auto` + — each engine alternates both paths in blocks of ≥3 s of gate time, discards + each block's first second, and commits to the faster settled path (L4 → on, + 4090 → off, measured). `1`/`0` force a path; `HSPZ_REF_BUCKET_SHIFT=<16..31>` + pins the window size; cards whose L2 cannot hold the smallest window (T4) stay + off. `--time` prints the per-block measurements and the decision per engine. + The window budget is three quarters of the L2: on an L4 that selects the + 32 MiB window, measured 2.8–3.0% less GPU time than 16 MiB on two whole-genome + work units and 2.75% less wall on the full hg38 × mm39 run (9,877 vs 10,156 s), + with identical output. +- **Sparse chunk walk (default).** Every seed batch that exceeds `--max-hits` + used to copy its whole cumulative hit array to the host (~6.8 MB, pageable) to + locate two or three chunk boundaries. The walk now fetches only the ≤1 KB block + holding each boundary, producing the same chunks. On two RTX 4090s at whole + genome this is **−9.6%** wall (1,634 → 1,480 s, disjoint reversed pairs); on + an L4 the copy was hidden under the gate, so nothing changes. + `HSPZ_CHUNK_WALK=full` restores the old path. +- **Foundations.** The `--max-hits` target (semantic, decides chunk boundaries and + therefore output) is now separated from the physical hit capacity derived from + free VRAM (decides success or failure only); the historical over-cap tail that + aborted whole-genome runs is admitted without changing any chunk. `--dump-plan` + / `--dump-manifest` / `--from-manifest` freeze and replay a plan (a second node + validates fit and fails rather than replanning). The `--time` ledger gained + query-pack / swap timers, per-worker GPU-busy and stage-gap notes, and an + explicit host-memory budget line. Multi-GPU output is byte-identical to the + one-GPU output at the same plan and cap. +- **Measured but not shipped** (recorded so nobody repeats them): certified + query-context rejection (0.19% of hits), nibble-packed reference at genome + scale (+0.1%), a 64 M hit cap (+1%), per-chunk and paired-chunk autotuning of + the bucketing pass (both misread the clock-mediated L4 win). + ## [0.0.3] — 2026-08-21 Warp-per-seed `find_hits` for dense launches, on by default. diff --git a/assets/tests/multiplicity_gate.sh b/assets/tests/multiplicity_gate.sh index b69adb4..cbff48e 100755 --- a/assets/tests/multiplicity_gate.sh +++ b/assets/tests/multiplicity_gate.sh @@ -5,23 +5,26 @@ # # DUPLICATES-ONLY distinct anchors identical; the extra lines are exact # duplicates of anchors both sides keep (`max_hits` chunking) -# CONTAINED-EXTRA one side keeps a segment the other absorbed into the segment -# containing it on the same diagonal — scope-sensitive -# `contained()` removal, a real alignment, not a duplicate +# CONTAINED-EXTRA containment *equivalence*, not directional coverage: +# an extra segment is accepted if it contains, *or is +# contained by*, a kept segment on the same diagonal in the +# *other* arm. A shorter candidate inside a longer baseline +# therefore returns CONTAINED-EXTRA. Fixed-plan kernel, +# batching and sharding changes must use EXACT, not this. # # Anything else is FAIL: a lost anchor, or an extra that is neither. Comparing # distinct *counts* would pass a run that dropped one anchor and gained another, # so the sets are compared as sets and every differing line is classified. # -# Usage: assets/tests/multiplicity_gate.sh