diff --git a/.github/workflows/gpu-zluda.yml b/.github/workflows/gpu-zluda.yml index cfb8ec1..cf221ec 100644 --- a/.github/workflows/gpu-zluda.yml +++ b/.github/workflows/gpu-zluda.yml @@ -4,6 +4,9 @@ # which no hosted runner has. It is therefore the only workflow that answers "are the HSPs still # identical to the C++ reference", and it is free and unlimited. # +# Machine paths (CUDA toolkit root, ZLUDA, shim, oracle, test data) are not in this file either: +# they live in the self-hosted runner host's per-user test.env and are read by the first step. +# # SECURITY, non-negotiable. A self-hosted runner that executes pull-request code from a fork is # arbitrary code execution on the workstation. The job-level `if` below is the control -- not the # repository's "require approval" setting, because an approved fork PR would still run here. @@ -37,22 +40,53 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository runs-on: [self-hosted, linux, x64, zluda] timeout-minutes: 120 - env: - CUDACONDA: /home/alejandro/opt/cudaconda - ZLUDA: /home/alejandro/opt/zluda - HIPFIX: /home/alejandro/opt/zluda-guide/hipfix.so - KEGALIGN: /tmp/kegalign/build/kegalign - TESTDATA: /tmp/kegalign/test-data - # Outside the workspace on purpose: actions/checkout runs `git clean -ffdx`, which would - # throw away the incremental build on every run. - CARGO_TARGET_DIR: /home/alejandro/.cache/hspZ-ci-target steps: - uses: actions/checkout@v4 - - run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + # Machine paths are not in the tree: they live in this runner host's per-user file + # (plain KEY=value lines, never committed), read before anything else needs them. + # CARGO_TARGET_DIR also stays outside the workspace on purpose: actions/checkout runs + # `git clean -ffdx`, which would throw away the incremental build on every run. + - name: machine environment (runner host file, not the tree) + run: | + f=${HSPZ_TEST_ENV:-${XDG_CONFIG_HOME:-$HOME/.config}/hspz/test.env} + if [ ! -f "$f" ]; then + echo "::error::missing machine path file $f (CUDA_HOME, ZLUDA, HIPFIX, KEGALIGN, TESTDATA)" + exit 1 + fi + set -a; . "$f"; set +a + for v in CUDA_HOME ZLUDA KEGALIGN TESTDATA; do + if ! eval "[ -n \"\${$v:-}\" ]"; then + echo "::error::$v not set in $f" + exit 1 + fi + eval "printf '%s=%s\n' '$v' \"\${$v}\"" >> "$GITHUB_ENV" + done + if [ -n "${HIPFIX:-}" ]; then + printf 'HIPFIX=%s\n' "$HIPFIX" >> "$GITHUB_ENV" + fi + echo "CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-$HOME/.cache/hspZ-ci-target}" >> "$GITHUB_ENV" + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + # The codegen backend must come from the cuda-oxide revision pinned in Cargo.lock: when + # the runner's backend cache is missing or invalidated, cargo-oxide would otherwise clone + # upstream HEAD, which tracks newer nightlies (see assets/container/Dockerfile). + - name: pin the cuda-oxide backend source + run: | + pin=$(grep -m1 -o 'cuda-oxide\.git#[0-9a-f]\{40\}' Cargo.lock | cut -d'#' -f2) + test -n "$pin" + src="${CARGO_HOME:-$HOME/.cargo}/cuda-oxide/src" + if [ "$(git -C "$src" rev-parse HEAD 2>/dev/null)" != "$pin" ]; then + git init -q "$src" + git -C "$src" fetch -q --depth 1 https://github.com/NVlabs/cuda-oxide.git "$pin" + git -C "$src" checkout -q --detach FETCH_HEAD + rm -f "$(dirname "$src")/librustc_codegen_cuda.so" + fi + test "$(git -C "$src" rev-parse HEAD)" = "$pin" - name: build run: | - export CUDA_HOME="$CUDACONDA" LD_LIBRARY_PATH="$ZLUDA:$CUDACONDA/lib" + export LD_LIBRARY_PATH="$ZLUDA:$CUDA_HOME/lib" cargo oxide build -- --release --locked strings -n 6 "$CARGO_TARGET_DIR/release/hspZ" | grep -qx '.target sm_80' \ || { echo "::error::expected sm_80 PTX -- is .cargo/cuda-oxide.toml present?"; exit 1; } diff --git a/Cargo.lock b/Cargo.lock index 2e97f0a..1b60063 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,7 +385,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hspz" -version = "0.0.3" +version = "0.0.4" dependencies = [ "clap", "cuda-core", diff --git a/Cargo.toml b/Cargo.toml index 8c6b495..7936b8a 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.4" 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", "gate-int-keep", "gate-payload-ballot"] # 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"] @@ -70,6 +76,21 @@ left-pair-tile = ["dense-anchors"] # (lanes 0-7 / 8-23), width-8 and width-16 scans. simd-prelude = ["left-pair-tile"] +# Score-gate instruction diet. Both are byte-identical and default. L4, unit 0 +# of hg38 x mm39 at W=1, reversed arms, output digest unchanged: with the +# reference-bucketed gate kernel gate-int-keep alone takes the score gate -6.5% +# and both together -30.1% (wall -25.3%); with the plain kernel both together +# take it -25.9% (wall -23.4%). +# +# Lane 0's keep compares `total >= t_min` in integers instead of rounding +# through f32 and f64; the host computes t_min = keep_threshold(hspthresh) once +# per engine. find_hsps keeps its own f32 threshold. +gate-int-keep = [] +# The SIMD prelude's first-drop recovery from one payload ballot (each lane +# offers its maximum before its own first drop) instead of four ballots, the +# first-of decodes, the min cascades and the recover branches. +gate-payload-ballot = ["simd-prelude"] + [profile.release] # `debug = 1` used to live here; `strip = true` discards it immediately, so it only cost # build time (70 s -> 43 s once removed). The two container integrity gates do not need it: diff --git a/assets/changelog/changelog.md b/assets/changelog/changelog.md index 2a74679..99dacb6 100644 --- a/assets/changelog/changelog.md +++ b/assets/changelog/changelog.md @@ -36,6 +36,138 @@ All notable changes to `hspZ` are documented here, newest first. +## [0.0.4] — 2026-09-13 + +Exact speed-ups for the GPU pass, a work-unit scheduler for multi-GPU runs, and a +new way to run one reference against many queries. Every arm of every measurement +below produced byte-identical output to 0.0.3 at the same plan and cap, on every +device tested (NVIDIA L4, RTX 4090, Tesla T4, AMD via ZLUDA); the score-gate diet and +`run --index` without `-r` ran on the L4, RTX 4090 and ZLUDA, not yet on a T4. + +- **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 for the whole list 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 its standalone run. `-Z` writes one archive per job; `OUT/queries.tsv` maps job → + path, size, sha256, blocks, units, HSPs, completion. With `--gpus W ≥ 2` the whole 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 the per-job output is unchanged. Measured against hg38 with 100 queries of 5 Mbp: + on one L4, 10,400–11,600 s as separate runs versus 1,890–1,900 s batched (**−82%**); on two RTX 4090s, + 497/482 s at `--gpus 1` versus 255/255 s at `--gpus 2` (**−48%**, workers balanced to 1.2%, all 100 jobs + identical), and −49% on two T4s. Both arms of each comparison ran the same pinned settings (device + seeder; bucketing on for the L4, off for the RTX 4090s; partition forced at `--gpus 2`), not the + `--gpus 1` defaults. Three 5 Mbp queries are −62%; whole-chromosome queries (61–121 Mbp) −2.6%; + whole-genome queries are expected to gain under 1% (not measured in batch mode). 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, so amortized time per query is not request latency. +- **On-disk reference index: `hspZ index` and `run --index DIR`.** `hspZ index --reference hg38.fa --index DIR` + builds every reference bin's seed tables and encoded bases once and writes them as raw arrays with a text + manifest, published atomically. A later `hspZ run … --index DIR` loads them instead of rebuilding, inside the + same prefetch, and the output is byte-identical. The run proves the index matches: before any upload the + resolved plan must have exactly the index's bins and every array must match its recorded length and checksum, + and a reference given with `-r` must hash to the recorded value (a same-size edit is caught) before any output + is written (see the next entry). Measured on an L4 with + hg38 and 5 Mbp queries run as separate processes: three queries 333 → 87 s (**−74%**) and 100 queries + 10,883/10,789 → 2,683/2,678 s (**−75%**), after a one-off index build of 119–123 s producing 8.93 GB. + Measured with bucketing pinned on, the device seeder and the index on local NVMe with a warm page cache. + Loading the seven hg38 bins costs ~12.6 s per run; on an L4 all but the first load (~1.7 s) hide behind the + GPU, each of those runs also re-read and re-hashed the reference before planning (~4.7 s), and on faster GPUs + a small query's loads set the pace. `-B 0` is rejected on both + commands; put the index on local storage (a load above 10 s warns). +- **`run --index` without `-r`.** With `--index`, `-r` is optional: the run plans from the record names and + lengths in the index `MANIFEST` instead of parsing the reference first. With `-r` the FASTA is verified against + the index in the background and no output is written until it matches (a mismatch exits non-zero with no output + files); without `-r` the index is trusted (its arrays are still checksum-verified as bins load). Measured on an + L4 with separate 5 Mbp runs against an hg38 index at production defaults, 20 runs per arm: wall per run + 27.8 → 23.5 s (**−15.6%**), time to first work unit 6.15 → 1.77 s (1.76 s without `-r`), peak RSS 5.1 → 2.0 GB without `-r`, outputs + identical. On an RTX 4090 (5 runs per arm, one pass): 20.8 → 16.0 s with `-r` (**−22.9%**) and 15.8 s without it, first + work unit 7.12 → 2.21 s. The saving is a fixed ~4.3 s per run on the L4, so it matters for short queries; a `-r` + that does not match the index is reported when the first work unit finishes, before any output is written. +- **Unit-level static partition across GPUs (default on for matching devices).** With two or more GPUs the frozen + plan's work units are split 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 is unchanged. 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); other worker counts and device + classes are unmeasured. `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, and on + such a pair the balance depends on device order. +- **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). + An engine needs about 20 s of gate time to decide, so short engines (e.g. 5 Mbp queries run as separate + processes) stay on the off path; pin `HSPZ_REF_BUCKETS=1` on L4-class cards for those. + `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 run (9,877 vs 10,156 s). +- **Score-gate instruction diet (`gate-int-keep`, `gate-payload-ballot`, default features).** The gate's SIMD + prelude finds each side's first X-drop with one ballot of the lanes with any drop and reads the value from the + first dropping lane (the maximum before its first drop), instead of four ballots, first-of decodes and min + cascades. Lane 0 keeps a hit with one integer comparison against a threshold the host precomputes once per + engine so that it equals KegAlign's f32-rounded comparison, instead of rounding the total through f32 before + the threshold; output is byte-identical. Measured on an L4 (hg38 × mm39 unit 0 at W=1, four reversed arms per + variant, output identical to the frozen digest): score gate **−30.1%**, GPU busy −26.1%, wall **−25.3%** with + the default reference-bucketed kernel; −25.9% gate / −23.4% wall with the plain kernel (two arms per variant, + one reversed pair). On an RTX 4090 (same unit, plain kernel, four reversed arms per variant, output identical): + score gate **−24.1%**, GPU busy −21.9%, wall **−18.4%**. Those unit-0 arms ran with the device seeder on and the + gate kernel forced per pass. The whole genome on an L4 at production defaults (W=1, `auto` bucketing on in all + seven engines), one reversed pair, output identical to the frozen digest: wall 9,949 → 7,352 s (**−26.1%**), + score gate −30.2%, GPU busy −26.3%. Not yet measured on a Tesla T4. +- **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 no regression was detected (one overlapping unit-0 pair). + `HSPZ_CHUNK_WALK=full` restores the old path. +- **`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; + it 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 and diagnostic tool; `run` does not call it. +- **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 and `--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. +- **Ledger and receiver checks.** Every run prints `schedule:` lines with the scheduling policy, its reason and each + worker's visits. `--time` prints one `unit ledger:` row per work unit (worker, device, bins, bp, GPU ms, host + start/end, pack/swap ms, seeds, hits, raw and final HSPs) plus per-worker finish and busy lines, attributes gap + pairs per worker, counts the >1 s transitions the gap accounting discards, and totals the host phases. The emitter + rejects duplicate or out-of-plan work units and verifies completeness after joining the workers, reporting a + worker's own error first. The ref-buckets autotune drops and restarts an inconclusive block rather than guessing. +- **Foundations.** The `--max-hits` target (semantic: it decides chunk boundaries and therefore output) is separated + from the physical hit capacity derived from free VRAM (which 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` and `--from-manifest` freeze and replay a plan, and a second node validates fit and fails rather + than replanning. Multi-GPU output is byte-identical to one-GPU output at the same plan and cap. +- **Fixes.** A seed batch whose hits exceed 2^32 is refused by name: its 32-bit hit total used to wrap, + either aborting with a misleading "not monotonic" error or, when the wrapped value fell under the cap, + writing past the anchor buffer (reachable with unmasked, closely related inputs and ~1 Gbp bins; KegAlign's + scan wraps too). `hspZ index` refuses record names its manifest cannot store and bins over 4,294,967,295 bp + before building. `-I 0` and `-C 0` are argument errors (`-I 0` used to loop forever). With `-Z`, the `--time` + footer reports the archive's size on disk (it read the size before the gzip trailer was written and fell back + to the formatted byte count); `--dump-raw` files are flushed explicitly, so a failed final write is an error + instead of a silently truncated dump; frozen and index manifests list the `ref-loc-buckets` feature; the + `--time` line for `HSPZ_REF_BUCKETS=0` names the override; `--help` for `-B` and `--gpus` describes the + shipped multi-GPU policy instead of recommending one reference bin per worker. +- **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), overlapping the next chunk's `find_hits` with the current + score gate (a wash on both an L4 and a 4090: the gate loses what the expansion saves), computing hit-count weights + inside a run to balance the schedule (exact, but 56 s of host work against a 7.6 s budget), and a 7 × 12 query + layout for four GPUs (−4.3%, below the bar). + ## [0.0.3] — 2026-08-21 Warp-per-seed `find_hits` for dense launches, on by default. diff --git a/assets/container/Dockerfile b/assets/container/Dockerfile index 99f6a84..6416651 100644 --- a/assets/container/Dockerfile +++ b/assets/container/Dockerfile @@ -44,11 +44,12 @@ ENV DEBIAN_FRONTEND=noninteractive \ PATH=/usr/local/cargo/bin:$PATH # clang/libclang 18 is what cuda-oxide actually resolves (doctor reports resource dir -# /usr/lib/clang/18) — the plan's LLVM 21 requirement does not apply, and `llc` comes from -# rustup's llvm-tools component, not from a system LLVM (amendment A2). +# /usr/lib/clang/18) — no LLVM 21 is needed, and `llc` comes from rustup's llvm-tools +# component, not from a system LLVM. libffi-dev: the codegen backend at the pinned +# cuda-oxide revision links -lffi (build stage only; the runtime images do not need it). RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl git build-essential pkg-config python3 \ - clang libclang-dev \ + clang libclang-dev libffi-dev \ && rm -rf /var/lib/apt/lists/* # CUDA 12.4 headers, libNVVM, libdevice, ptxas and nvJitLink. cuda_cudart gives cuda.h and @@ -88,7 +89,14 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ WORKDIR /src -# cargo-oxide is pinned to the SHA in Cargo.lock, never to upstream HEAD (plan Phase 1.3). +# cargo-oxide is pinned to the SHA in Cargo.lock, never to upstream HEAD. +# +# The codegen backend needs the same pin. On its first build cargo-oxide clones the backend +# source into $CARGO_HOME/cuda-oxide/src with an unpinned `git clone --depth 1`, i.e. +# upstream HEAD, and HEAD tracks newer nightlies than rust-toolchain.toml: in September +# 2026 it stopped compiling on the pinned nightly (rustc_public API drift) and every image +# build broke. Seeding that directory at the pin first makes cargo-oxide skip its clone, +# so the CLI, the backend and the cuda-* crates all come from one revision. COPY Cargo.lock Cargo.toml rust-toolchain.toml ./ RUN set -eux; \ sha=$(grep -m1 -o 'cuda-oxide\.git#[0-9a-f]\{40\}' Cargo.lock \ @@ -96,7 +104,12 @@ RUN set -eux; \ test -n "$sha"; \ echo "cuda-oxide pin: $sha" | tee /cuda-oxide.sha; \ cargo install cargo-oxide --git https://github.com/NVlabs/cuda-oxide.git \ - --rev "$sha" --locked --root /usr/local + --rev "$sha" --locked --root /usr/local; \ + src="$CARGO_HOME/cuda-oxide/src"; \ + git init -q "$src"; \ + git -C "$src" fetch -q --depth 1 https://github.com/NVlabs/cuda-oxide.git "$sha"; \ + git -C "$src" checkout -q --detach FETCH_HEAD; \ + test "$(git -C "$src" rev-parse HEAD)" = "$sha" COPY . . # One target tree per architecture so artifacts cannot leak between them. @@ -137,7 +150,7 @@ RUN mkdir -p /build/fixtures && python3 assets/tests/gen_fixtures.py /build/fixt FROM nvidia/cuda:12.6.3-base-ubuntu24.04 AS nvidia ARG HSPZ_NVIDIA_ARCH=sm_75 ARG GIT_SHA=unknown -ARG HSPZ_VERSION=0.0.3 +ARG HSPZ_VERSION=0.0.4 LABEL org.opencontainers.image.source="https://github.com/hillerlab/hspZ" \ org.opencontainers.image.revision="$GIT_SHA" \ org.opencontainers.image.version="$HSPZ_VERSION" \ @@ -170,7 +183,7 @@ FROM rocm/dev-ubuntu-24.04:6.2 AS zluda ARG ZLUDA_VERSION=v6 ARG ZLUDA_ASSET=zluda-linux-3fe12063.tar.gz ARG GIT_SHA=unknown -ARG HSPZ_VERSION=0.0.3 +ARG HSPZ_VERSION=0.0.4 LABEL org.opencontainers.image.source="https://github.com/hillerlab/hspZ" \ org.opencontainers.image.revision="$GIT_SHA" \ diff --git a/assets/container/nvidia.sh b/assets/container/nvidia.sh index 2ad4f21..fd07f09 100644 --- a/assets/container/nvidia.sh +++ b/assets/container/nvidia.sh @@ -40,7 +40,7 @@ fi # `bash` argument and die on "unrecognized subcommand"). case "${1:-}" in hspZ) shift; exec hspZ "$@" ;; - run|benchmark|compare|--help|--version|-h|-V) + run|index|benchmark|compare|hits-estimate|--help|--version|-h|-V) exec hspZ "$@" ;; *) exec "$@" ;; esac diff --git a/assets/container/zluda.sh b/assets/container/zluda.sh index 59fbf8a..5a35b97 100644 --- a/assets/container/zluda.sh +++ b/assets/container/zluda.sh @@ -37,7 +37,7 @@ fi # by whatever is exec'd, so the task shell gets ZLUDA's libcuda.so.1 too. case "${1:-}" in hspZ) shift; exec hspZ "$@" ;; - run|benchmark|compare|--help|--version|-h|-V) + run|index|benchmark|compare|hits-estimate|--help|--version|-h|-V) exec hspZ "$@" ;; *) exec "$@" ;; esac diff --git a/assets/kaggle/kernel/run.py b/assets/kaggle/kernel/run.py index 6c78bf7..4834b11 100644 --- a/assets/kaggle/kernel/run.py +++ b/assets/kaggle/kernel/run.py @@ -197,10 +197,13 @@ def build(): # library, which has no resource directory, so bindgen resolves `#include # ` against nothing and dies on /usr/include/stdlib.h. Internet is # enabled, so install clang and take both paths from it. - if not shutil.which("clang") or not shutil.which("time"): + # The cuda-oxide codegen backend at the Cargo.lock pin links -lffi, which needs the + # unversioned libffi.so from libffi-dev. + need_ffi = not out("ldconfig -p | grep -E 'libffi\\.so \\('") + if not shutil.which("clang") or not shutil.which("time") or need_ffi: # `time` is not in the Kaggle image either, and the arms report max RSS. sh( - "apt-get -qq update && apt-get -qq install -y clang libclang-dev time", + "apt-get -qq update && apt-get -qq install -y clang libclang-dev time libffi-dev", check=False, ) if not shutil.which("clang"): @@ -263,10 +266,30 @@ def build(): # a cargo invocation from anywhere else fails with "could not choose a version of # cargo to run" — which is exactly how the first attempt died. sh(f"cd {SRC} && rustup show active-toolchain") + # cargo-oxide and its codegen backend come from the cuda-oxide SHA pinned in Cargo.lock, + # never upstream HEAD: HEAD tracks newer nightlies than rust-toolchain.toml. The backend + # source is seeded at the pin because cargo-oxide's first build would otherwise clone it + # unpinned into $CARGO_HOME/cuda-oxide/src. + pin = out(f"grep -m1 -o 'cuda-oxide\\.git#[0-9a-f]\\{{40\\}}' {SRC}/Cargo.lock | cut -d'#' -f2") + if len(pin) != 40: + raise RuntimeError("no cuda-oxide pin found in Cargo.lock") if not shutil.which("cargo-oxide"): sh( - f"cd {SRC} && cargo install --quiet --git https://github.com/NVlabs/cuda-oxide.git cargo-oxide" + f"cd {SRC} && cargo install --quiet --git https://github.com/NVlabs/cuda-oxide.git " + f"--rev {pin} --locked cargo-oxide" ) + cargo_home = pathlib.Path(os.environ.get("CARGO_HOME", str(pathlib.Path.home() / ".cargo"))) + backend_src = cargo_home / "cuda-oxide" / "src" + if out(f"git -C {backend_src} rev-parse HEAD") != pin: + # Absent, or left at another revision (e.g. an earlier unpinned clone): check out the pin + # and drop any backend built from the old source so cargo-oxide rebuilds it. + sh( + f"git init -q {backend_src} && git -C {backend_src} fetch -q --depth 1 " + f"https://github.com/NVlabs/cuda-oxide.git {pin} && git -C {backend_src} checkout -q --detach FETCH_HEAD" + ) + (backend_src.parent / "librustc_codegen_cuda.so").unlink(missing_ok=True) + if out(f"git -C {backend_src} rev-parse HEAD") != pin: + raise RuntimeError(f"cuda-oxide backend source at {backend_src} is not at the Cargo.lock pin {pin}") # One binary. Both stages here exercise the shipped default build; the seeding path is # chosen at runtime on the worker count, so a feature-gated second build would be # byte-identical anyway. 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