Gentlemen, start your ~engines~ agents! :checkered...
# rnaseq_dev
a
Gentlemen, start your engines agents! šŸ We go full markdupes After a weeklong Rust vibecode and 24/7 benchmarking I ended up with three things worth showing. All of it sits comfortably inside the rewrites.bio policy @Phil Ewels has been driving — one verified (to my personal limits) rewrite, and two knob-turns that help the unmodified pipeline on the same hardware. 1. markdup-wea — a Rust rewrite of Picard MarkDuplicates 8/8 ENCODE RNA-seq samples (1.55 B records, 934 M duplicates): byte-identical duplicate flags + metrics vs Picard 3.4.0. Zero flag mismatches, zero metrics data divergence. K562_REP1 head-to-head, same 32GB box: 3.26Ɨ faster, 54Ɨ less RAM (325 MB vs 17.6 GB peak). Drop-in for the
PICARD_MARKDUPLICATES
step in nf-core/rnaseq — same
KEY=VALUE
CLI. • Repo: https://github.com/WeTheAgents/markdup8x-wea • Parity report: https://github.com/WeTheAgents/markdup8x-wea/blob/main/docs/session-report.md • Performance: https://github.com/WeTheAgents/markdup8x-wea/blob/main/docs/perf.md • Known deltas from Picard: https://github.com/WeTheAgents/markdup8x-wea/blob/main/docs/deviations.md 2. Picard memory-tuning proposal for nf-core/rnaseq — zero code change nf-core's
PICARD_MARKDUPLICATES
uses
label 'process_medium'
→ 36 GB reservation,
-Xmx28g
. Measured per-JVM peak RSS on our ENCODE corpus: under 8 GB. ~4Ɨ over-allocation on every invocation. Sweep on a 30 GB box:
-Xmx6g Ɨ 4
parallel delivers 8/8 byte-identical output to the
-Xmx28g
default and 3.05Ɨ end-to-end throughput on the 8-sample batch. Retry envelope verified with a "werewolf" BAM (2Ɨ largest ENCODE sample, 445 M records, 17 GB): clean finish at
-Xmx9g
, peak RSS 9.6 GB. Two werewolves in parallel at
-Xmx9g
each → sum RSS 20.8 GB, same wall as one, no OOM. • Full doc: https://github.com/WeTheAgents/markdup8x-wea/blob/main/docs/picard-tuning-proposal.md • Issue: https://github.com/nf-core/rnaseq/issues/1801 3. Downstream parallelism — so the markdup win doesn't get swallowed An 8Ɨ throughput bump on markdup is wasted if Qualimap and bedtools then serialize behind it. Both are single-threaded by design, and nf-core's default labels over-budget them — Qualimap asks for 36 GB and really uses ~5–6 GB; bedtools asks for 6 GB and actually scales 4–14 GB by sample complexity. On a 30 GB node that means exactly one of each at a time. Right-sized and run by hand on the same Hetzner 32BG: Qualimap 4-wide Ɨ 2 waves → 3.30Ɨ speedup (7:51 h → 2:23 h on the 8-sample set), bedtools 2-wide → 1.55Ɨ (2-wide is the safe ceiling — 4-wide OOMed on MCF7). Combined across the three heavy BAM-side steps (markdup + Qualimap + bedtools): ~12:49 h → ~3:13 h on 8 samples, same box, no hardware upgrade. Non-specialist caveat: this assumes batch re-processing of pre-staged BAMs as the workload — all inputs local, no aligner in the critical path. I don't know how common that is vs the canonical one-sample-at-a-time flow. If it's common, the savings are literally sitting on the ground. Numbers in the Phase E section: https://github.com/WeTheAgents/markdup8x-wea/blob/main/docs/perf.md Scope and caveats: my dear Claude Agent and I went through everything we could surface: Picard sources + JavaDoc, MarkDuplicates forums and GATK issues, the nf-core module, and the relevant nf-core/rnaseq config layers. What neither of us can speak to is tribal knowledge — production workloads or library types where the 36 GB reservation is genuinely load-bearing rather than conservative legacy. I'm not a practicing bioinformatician. Explicitly outside the benchmark: WGS / WGBS / long-read, very-large-library inputs (> 2Ɨ werewolf), optical-duplicate configurations, unsorted BAMs, MarkDuplicatesSpark (different code path entirely). Happy to dig into whichever of the three is most interesting to you right now.
🤩 1
m
#CGFUX04HZ next?
šŸ–– 1
p
Note that qualimap (and I think also bedtools, as that's a prereq for rseqc that we skip) are already replicated in RustQC
I wonder if markdups is only needed for QC. Could be interesting in importing your crate into RustQC and doing it all in one pass if so šŸ¤”
Like, incorporate another tool into that hot loop over all the reads
Don't worry too much about the over-provision of memory for the process. That's a well known issue and not as trivial as it sounds. It's already being worked on in other places.
šŸ–– 1
@Alexander Noskov next step if you're keen is to package your rewrite in a docker image. Maybe include a
COPY
to give a binary with the name Picard (might not make sense, not checked what the current process is). But then run the #CE8SSJV3N pipeline using your rewrite and see if the nf-test unit tests pass
You'll probably find 100 other things, like CLI flags, stdout / report formatting, very minor changes etc.
But if you can get the nf-test tests to pass as an exact drop-in replacement, it's a good sign!
The 4 main stages of the rnaseq pipeline are trimming, alignment, markdups, and QC. We now (very nearly) have rust rewrites for all 4
Excited to start stacking them up and benchmarking together!
j
Sorry to be a part pooper, but really want to inject some caution here. Taking the three separately since they're really independent proposals. āŒ On markdup-wea: I stand by my earlier point on Picard and duplicate marking, as I said in that conversation the burden of proof is really high. Matching Picard's output on 8 RNA-seq samples is a start but nowhere near what we'd need to replace a Broad-maintained tool. The tricky cases for MarkDuplicates (UMI libraries, optical duplicates, unusual read alignments, very deep libraries) are exactly where matching gets hard, and you've explicitly excluded most of them. The maintenance question is also real: modules in the pipeline need long-term owners, and a week of AI-assisted coding is a hard sell for something this important. I'd want to see it proven out as a standalone tool with real users before it's a candidate here, because it's such a central thing. Folks will take the duplicate-marked bams from the pipeline to downstream analysis, we can't just drop-in a Claude re-code without really extensive validation. āŒ On the Qualimap and bedtools parallelism: I think this assumes everyone runs the pipeline on a single machine, which isn't how most users deploy it. On a cluster or cloud setup, the scheduler runs tasks in parallel across whatever compute is available. The memory labels are per-task budgets, not bottlenecks that force one-at-a-time execution. Eight Qualimap tasks already run in parallel if the slots are there. There's a fair cost argument about over-reserving cloud memory, but that's a different pitch than "the pipeline is serial." Worth reframing if you want to make the point. Also RustQC overlaps with this stuff. āœļø On the memory tuning this is possible worth talking about, with provisos as Phil indicated. The current 36 GB / 28 GB Java heap default is conservative and there's probably headroom. A couple of pushbacks on the framing though: - "6 GB heap Ɨ 4 in parallel" is a throughput argument, but on a cluster the scheduler decides how many tasks run at once. The memory setting is a budget per task, not a cap on how many can run. - The big test sample at roughly 2Ɨ the largest ENCODE one isn't really the worst case we need to plan for. UMI libraries, deep targeted panels, and heavily amplified samples push further, and the automatic retry with more memory is there specifically to catch those. That said, I'd entertain a modest reduction with the retry behaviour preserved. Can you share memory usage across all 8 samples (not just the peak), and ideally add at least one UMI library to the test set? That would let us pick a default with confidence rather than optimising for one workload.
šŸ‘ 1
šŸ–– 1
t
I agree with Jonathan that for deduplicating the memory challenge does not come from the total number of reads (alone), but from the number of reads mapping to any given locus. So if you have a locus (e.g. rRNA in a bad-quality library) that sucks up a huge part of the reads into one spot, all of those reads have to be processed simultaneously, and that determines the maximum RAM consumption. That does not mean that the default setting has to accommodate the worst case scenario (actually, in this case there is no such thing -- you can always make it worse by adding one more read to the pile). As long as it works for most typical cases, anything more extreme can be salvaged with a custom config if needed (and nf-core pipelines by default allocate more resources in on retries after failure).
āž• 1
a
@Jonathan Manning @Thomas Danhorn @Phil Ewels — closing the loop on the three leads from this thread. What's done and proven (links go to evidence in the repo): • Per-locus pileup, 8 ENCODE + 3 UMI libraries — Thomas's per-locus axis. Deepest 5′-bucket = 212,846 reads (GM12878_REP2), ~0.4Ɨ Picard's MAX_RECORDS_IN_RAM; none of the 11 libraries push the sorting-collection threshold on per-locus depth. https://github.com/WeTheAgents/markdup8x-wea/blob/main/docs/per-locus-pileup.md • Picard -Xmx sweep on GSE75823 UMI (210M SE, BARCODE_TAG=RX) — Jonathan's UMI ask. [4g, 6g, 9g, 28g] all flag-md5-match baseline; peak RSS 2.2 / 3.2 / 5.1 / 18.0 GB; wall flat ~21.5 min. Min non-OOM heap = -Xmx4g. https://github.com/WeTheAgents/markdup8x-wea/blob/main/docs/picard-tuning-proposal.md#gse75823-umi-library-memory-envelope • nf-test drop-in — Phil's ask. picard-shim docker image, 6/6 PASS on modules/nf-core/picard/markduplicates (3 real + 3 stub, byte-for-byte snapshot match on metrics line 2). https://github.com/WeTheAgents/markdup8x-wea/blob/main/docs/nftest-dropin.md • markdup-wea UMI parity — 3 independent public UMI datasets (2 organisms Ɨ 3 chemistries, 255.6M records combined, 0 flag divergence vs Picard 3.4.0, exact metrics). https://github.com/WeTheAgents/markdup8x-wea/blob/main/docs/parity-evidence.md @Jonathan Manning — on memory tuning: do we try a PR? What's the smallest reduction from the current 36 GB / -Xmx28g you'd entertain as a first step, with the retry envelope preserved? I'll prep whatever shape you'd review. @Phil Ewels — on your earlier point about folding markdup into the RustQC hot loop: do you see a real path there? Happy to sketch what the integration shape might look like before anyone commits time. On markdup-wea as a standalone replacement — past this point feels insurmountable without outside enthusiasm: no proof = no production, no production = no additional proof. If anyone reading this has a BAM in one of the harder categories (UMI + patterned flowcell, deep panels, optical dups) and wants to send it, I'll run it and own any non-zero diff.
p
@Alexander Noskov can you give me a TLDR of the above written with your own hands please? Bit jet lagged and the LLM-writing style is making my brain hurt
āž• 2
a
VERY TLDR: tested all I could find from public BAMs. Went good, but I realized that proof never will be enough for Jonathan, and that's totally ok. I see two paths left: (1) rust markdup-wea inside rust pipeline, and (2) memory optimized Picard config
šŸ™šŸ» 1
p
Ok, so your Rust rewrite of Picard Markdups (https://github.com/WeTheAgents/markdup8x-wea) is effectively finished and passing every validation test you can think of now?
a
can think of now?
no-no, I can think of many more things. )) I cannot get my hands on them. Like Thomas said: for any bad scenario we can come with a next-worse one with more rigged locus. Infinity loop. And I just do not know if the opensource samples are rigged enough.
@Phil Ewels what now?
p
Cleaning up, writing docs, doing benchmarking reports has been my next step in other projects
The benchmark report for example has a range of different public data (you can find stuff via https://sra-explorer.info/). You'll need to align it for Picard though, which is a bit of a faff.
But this is a good way to build trust: run it on as many different and varied real life datasets as possible and demonstrate that you get entirely identical outputs each time
🤩 1
Maybe clean up the docs a bit. For example, on your perf docs via Picard it's unclear to me whether you're also running Picard in multi-threaded mode (I don't remember if it has a multi-threaded mode tbh). But having solid performance metrics will help.
You can see the docs that I put together for RustQC too for more examples, both comparing outputs side by side and performance benchmarking docs: https://seqeralabs.github.io/RustQC/rna/dupradar/ and https://seqeralabs.github.io/RustQC/usage/performance/#cpu-scaling-benchmarks