v0.18.0
Fixed
-
The reads table is written in batches again, instead of one batch per file
(#297).filter,merge,subsetandsplitbuild the reads table with
build_reads_table{,_remapped}rather thanWriter, and those wrote the
whole table as a single Arrow record batch however large it was — so
read_batch_sizeonFilterOptions/MergeOptions/SubsetOptionswas
declared, defaulted, and never read.escpod filterasking for 10,000 rows
per batch wrote 40,000 reads as one.Measured, for scale: MinKNOW writes ~10,000 reads per batch (1,575,748 in
158) and the pod5 Python package exactly 1,000 (17,919,658 in 17,920).
Neither writes a whole file as one batch.It is not cosmetic for anything that reads escpod's output back.
demux
shards its reader threads by batch index and only emits a block at a batch
boundary, so a single-batch file is read by exactly one thread whatever
ESCAPEPOD_DEMUX_FILLERSsays, and nothing reaches the GPU until the entire
file has been decoded. On a 100k-read escpod-written file that was 11.2 s of
a 19 s stage with the card idle, and it made every tuning knob look flat.
Fixing it took that stage to 11.3 s with the first block arriving in 0.3 s
and GPU utilisation going from 22-30% to ~60%.Every default is now 1,000, and the five CLI sites that overrode it (merge at
100,000, the rest at 10,000) inherit it, so the geometry has one definition.
Note this never affected MinKNOW input, which was already many-batched:
measured on a real 1.3M-read run, GPU utilisation is ~94% and the first block
arrives in 0.4 s both before and after.
Build / Tooling
- The noodles crates are bumped as a set (#302):
noodles-bam0.94 -> 0.95,
noodles-sam0.89 -> 0.90,noodles-csi0.60 -> 0.61. They share types across
their own version boundary, so Dependabot's one-PR-per-crate bumps could not
compile individually (E0308inescapepod-signal, which uses bam and sam
together). No source changes were needed.
Performance
-
The CRF encoder's onnxruntime sessions no longer spin (#296). Each
encoder worker built its CUDA session with an intra-op pool--threadswide
and spinning enabled, so--threads 32with two workers put 64 spinning
intra-op threads on a 32-core allocation — for a graph that runs on the
device and gives that pool almost nothing to compute. This is the same defect
#240 fixed for the adapter-CNN session, which never reached this one.
Measured on an A30 (demux basecall, 40k reads, interleaved): process CPU
858% -> 733%,perfsamples inlibonnxruntime.so15.6% -> below the 1%
report floor, output byte-identical. -
demux --annotateinterns its barcode labels (#296), writing the sidecar
from a dictionary plus per-read codes rather than a string per read. -
The CRF encoder pool uses every visible GPU instead of reserving one for
adapter detection (#297). Detection is ~5% of device time since #187, so
holding a whole card for it left that card 12% busy on two GPUs and 27% on
four while the lone encoder device pinned at 93%. Measured on a real 1.3M-read
run, interleaved, 2 reps:GPUs reserve device 0 encode everywhere vs 1 GPU 1 162.5 s (gpu0 88%) same policy -- 2 136.6 s (12% / 93%) 80.8 s (88% / 88%) 1.19x -> 2.01x 4 56.7 s (27% / 75%) 55.3 s (72% / 58-66%) 2.86x -> 2.94xTwo GPUs go from 1.19x to 2.01x. Four are a wash — the pool was already
wide enough there. -
--ref-scoresruns its reference scan on the GPU instead of on the host
(#297). The flag that gates production demuxing was the one configuration
with no GPU decode path:try_run_and_decode_with_refsalways copied the
whole score tensor back and ran the CPU lattice decode, where the plain path
decodes on the device and never copies at all. Over the isolated CRF stage on
100k reads that was 19.7 s at 656% CPU against 12.4 s at 114% without the
flag — +57% wall for five and a half extra cores, with the card idle.The constrained scan is now a CUDA kernel (the one #241 left unwritten): one
block per read, a grid-stride loop over chain cells, both alpha buffers
double-buffered in shared memory, andlogZ_fullreduced on the device so
onlyn_refsfloats per read come back rather than a strided gather over a
157 MB alpha buffer. It runs between the two decode passes by necessity, not
by choice — it needs the raw scores that pass 1 overwrites in place.End to end on one A30 over 100k reads, arms interleaved in one allocation:
38.7 s → 31.5 s wall (1.23x), and 5.9 cores → 1.3 cores. Barcode calls are
identical for all 100,654 reads;crf_logp,crf_marginandmean_logpost
agree to 2e-4, which is the output's own print precision.Note this does not close #297. GPU utilisation is unchanged at ~22%, so
removing 4.5 cores of host work from the critical path bought only 20% wall.
That is evidence for the issue's "overlap-bound, not throughput-bound"
framing: the pipeline is waiting on something structural, not on compute.A panel that does not fit the kernel's shared memory, or whose fan-in exceeds
the fixed accumulator, falls back to the CPU scan — slower, never wrong.
Which artifact
| Artifact | Linkage | --gpu |
|---|---|---|
…-x86_64-unknown-linux-musl.tar.gz |
static (musl) | no |
…-aarch64-unknown-linux-musl.tar.gz |
static (musl) | no |
…-x86_64-unknown-linux-gnu-gpu.tar.gz |
dynamic, glibc ≥ 2.28 | yes |
…-x86_64-apple-darwin.tar.gz |
dynamic | no |
…-aarch64-apple-darwin.tar.gz |
dynamic | no |
The musl builds are the portable default and the right thing for
an unattended installer to fetch. The GPU paths cannot be static —
they dlopen their runtimes — so they ship in the single dynamically
linked …-linux-gnu-gpu artifact instead, built against glibc 2.28
(RHEL/Rocky/Alma 8+, Ubuntu 20.04+).
What …-linux-gnu-gpu needs at run time
Only when --gpu is actually requested — otherwise it behaves
exactly like the musl one. It expects a CUDA 12 runtime and
cuDNN 9, with an NVIDIA driver ≥ 535 (the DTW kernels target the
CUDA 12.2 driver API), and a CUDA-enabled libonnxruntime matching
the ort 2.0.0-rc.13 it links.
Rather than assembling that by hand, see
GPU acceleration,
which covers the pixi environment that supplies it and how to
confirm the CUDA execution provider actually loaded.