MILAB-6725: size exportClones from the .clns instead of a flat 12 GiB - #205
Merged
Conversation
exportClones was requesting a constant 12 GiB (or perProcessMemGB / 4), which has no relationship to what the command holds in memory. It reads the entire CloneSet into heap, and for single-cell exports then materialises a second, expanded list via divideClonesByTags -- one clone per (clonotype x cell), each with its own split TagCount -- which it sorts and re-ranks before writing. The --chains filter is applied only after that division, so exporting one chain group still pays the whole-file cost. With the memory-from-limits entrypoint a 12 GiB grant yields only 8788 MiB of heap (flat 3500 MiB non-heap reserve), and that was not enough for a 10.7k-cell / 26.7k-clone 10x BCR sample: exportClones died with OutOfMemoryError, taking the clonotypes, clonotypeTables and qcReportTable outputs with it. RAM is now clamp(perByte * size(clns), floor, 128 GiB) -- 16 GiB / 16 for the bulk export, 32 GiB / 32 for the single-cell export. The floor carries small inputs, where the in-heap tag-count object graph rather than the stored file dominates; the linear term carries large ones. Also stop quartering the Advanced Settings memory override (it now applies as-is, matching the analyze step), and leave the two PTabler steps unsized so workflow-tengo 6.8's built-in input-volume formula applies to them, as it already does elsewhere in the single-cell pipeline.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Lead each comment block with its point rather than its background, state what the code does instead of what it avoids, and drop filler. No behaviour change.
PaulNewling
marked this pull request as ready for review
August 4, 2026 08:28
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
AStaroverov
approved these changes
Aug 4, 2026
Contributor
Author
-Xms is half the container grant, so a memory request is a hard pre-allocation rather than just a ceiling, and a sample runs one bulk plus one single-cell export per chain group -- the floors are paid several times over concurrently. A separate incident this week had a mixcr analyze step granted 484 GiB, pre-touch 242 GiB at JVM start, and get OOM-killed before it could finish; over-estimating a request is not free. 24 GiB yields 20889 MiB of heap, 2.4x the 8788 MiB ceiling that failed, and sits at the point where the entrypoint's flat 3500 MiB non-heap reserve stops costing a disproportionate share of the grant.
PaulNewling
marked this pull request as draft
August 4, 2026 11:31
PaulNewling
marked this pull request as ready for review
August 4, 2026 12:07
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket: MILAB-6725 — https://app.notion.com/3b23a83ff4af81d78906f6ff5c9dbd11
Reported in: https://platforma-bio.slack.com/archives/C0B0KCF0VDF/p1785763510195099
What broke
A customer running 10x single-cell BCR on EKS lost three block outputs at once —
clonotypes,clonotypeTablesandqcReportTable. All three are one failure: theIGK/IGL single-cell
exportClonesdied withOutOfMemoryError(exit 2), and all threedepend on its
tsvForSingleCell.The failing command, verbatim from the report:
The chain:
mixcr-export.tpl.tengosized the export exec at a flat 12 GiB(
max(12, perProcessMemGB / 4)).memory-from-limitsentrypoint turns a container grantRinto-Xmx = max(min(0.85·R, R − 3500 MiB), 0.5·R). AtR= 12288 MiB that gives-Xmx8788m, matching the observed command line exactly. The flat 3500 MiBreserve dominates until
R≈ 23 GiB, so small grants lose a disproportionate shareto non-heap.
exportClonesloads the wholeCloneSetinto heap (CommandExportClones.kt:238).Single-cell exports then call
divideClonesByTags(:308), which materialises asecond, expanded list — one clone per (clonotype × cell), each with its own split
TagCount— then sorts and re-ranks it (mixcr-algoCloneSet.kt:146-150).--chainsfilters only after that division (:348), so exporting one chain groupstill pays the whole-file cost. This block loops chain groups and runs bulk plus
single-cell per group: four of these per two-chain-group BCR sample.
Main.kt:107catchesOutOfMemoryError→ exit 2.MiXCR's message misleads. "This run used approximately 8792m of memory, this machine
has 12288 Mb in total" reads as "used 8.6 of 12, plenty spare", but
8792misRuntime.getRuntime().maxMemory()— the heap ceiling, not usage.The dataset was small — 26,744 clones, ~10.7k cell barcodes. The 12 GiB was never
connected to anything. The same block sizes
analyzeatclamp(192 GiB + 4·size(reads), 192 GiB, 256 GiB)for this preset — a 16× gap againsta step that holds an expanded copy of the clone set.
This is the second incident on this container.
83c4eb99in the mixcr repo raised theentrypoint's non-heap reserve 2500 → 3500 MiB because "Export commands on 12 GiB
containers were getting OOM-killed". That fixed a container kill by lowering
-Xmx,which set up today's heap OOM.
The change
Both export execs now derive RAM from the
.clns:divideClonesByTagsexpansionsize()sums the stored blob size viagetBlobSize— a metadata read, no pre-execpass. The floor carries small inputs, where the in-heap tag-count graph rather than the
stored file dominates; the linear term carries large ones. A 24 GiB grant yields
20889 MiB of heap, 2.4× the ceiling that failed.
The floors are deliberately tight.
-Xmsis half the grant, so a request is a hardpre-allocation rather than just a ceiling, and a sample runs one bulk plus one
single-cell export per chain group — the floors are paid several times over
concurrently. A separate incident this week had a
mixcr analyzestep granted 484 GiB,pre-touch 242 GiB at JVM start, and get OOM-killed before finishing. Over-estimating a
request is not free.
between()supplies the floor rather than.plus(gib(floor)). The additive formdouble-counts: the floors are total-memory values, so adding them to a size-proportional
term charges for the data twice.
Two changes follow from it:
perProcessMemGBreachesthe export exec unquartered, matching
mixcr-analyze.tpl.tengo. Previously a usersetting 48 GiB got 12 GiB here, so the documented escape hatch barely moved the failing
step. Projects that set it will request 4× more for the export step than before —
the one behaviour change worth pushing back on.
(
ram = clamp(2 GiB + 4·size, 2 GiB, 64 GiB)), which tracks the exported TSV betterthan a fixed share of a sibling step's budget. They previously took ⅔ of
exportMemGB, which cannot survive that value becoming a formula. fix: size single-cell PTabler steps via workflow-tengo 6.8 formulas #204 gave the restof the single-cell pipeline the same treatment; an explicit
.mem()/.cpu()here wassuppressing it.
The template's
hash_overridestays as-is. Resource requests ride as CID-transparentmeta inputs so a re-run with more memory reuses everything else, and the failed step
re-runs regardless — a failure is not a cached success.
Verified
Live, on a local backend, running the block's own
simple sc projecttest — same10x-sc-xcr-vdjpreset and the same IGH + IGK,IGL chain groups as the customer:-Xmx12884m(bulk,16 GiB grant) and
-Xmx27852m(single-cell, at the 32 GiB floor this ran under),both matching the entrypoint arithmetic exactly. Before the fix all eight were
-Xmx8788m. This provesformula.size("clns")resolves at run time — an unmatched tagwould have hard-failed the template — and that bulk and single-cell get different
grants. The floor has since been lowered to 24 GiB; the mechanism is unchanged.
analyzewas observed requesting 192 GiB and receiving-Xmx33934m— the silentbucket clamp, live.
exportClonesargument list directly against a real single-cell
.clnsbelow ~64 MiB of heap givesNot enough memory for run commandand exit 2 — the reported signature. Thefailure is heap-bound and this is the controlling knob.
changing the template hash, adding a distinguishing env var and rebuilding the block
pack all failed to invalidate the cached exec. Worth knowing before anyone tries to
bisect memory through the UI.
pnpm run build:dev-local— 9/9 tasks green.sizeaccepts.clns:_assertMetricCompatibleinformula.lib.tengorestricts onlylineCountto line-oriented text formats.bucket.go:199-204/:301-307) andfeeds the clamped value to
{system.ram.mib}rendering, so the JVM gets a consistent-Xmx. Raising the floor cannot strand a desktop user with an unschedulable request.Not verified
passing heap across two real
.clnsfiles (86 KB and 1.24 MB, a 14× range) gave64 MiB for both bulk and single-cell on both inputs. At that scale heap is entirely
JVM plus reference-library overhead; the data contributes nothing measurable. The
largest
.clnsavailable locally holds 345 clone×cell pairs against the customer'stens of thousands, so no local dataset can calibrate the slope or discriminate bulk
from single-cell. That is a measurement, not an assumption.
mechanism is identical.
The
.clnsfrom the failed run — or just its size — would turn the coefficient into ameasurement. It is derived data, not raw sequence. A calibration plan against the
studies library is drafted separately.
How to verify locally
The bug is "grant too small for the data", which is two independent claims. The mechanism
is testable today; the sufficiency of these specific numbers needs larger data.
Pin the export to
mem("1GiB")(→-Xmx512m) and run the single-cell fixture intest/assets: expect the sameNot enough memory for run command/ exit 2 signatureand the same error chain through
tsvForSingleCell. Restore the formula and itpasses. Proves the failure is heap-bound and that this is the controlling knob.
--log-blocks-enabledandgrep the per-block log for the
java -Xmx…line. Before:-Xmx8788m. After: the24 GiB request, clamped by the medium bucket to whatever the host allows. This is also
the only check that proves
formula.size("clns")resolves at run time.-Xmxagainst a fixture.clnsfor a measured heap-per-clns-byte ratio.Treat it as a lower bound — tag-count overhead does not scale linearly from a 1.2 MB
fixture to a real sample.
Follow-up
The same flat-12-GiB export sizing exists in
mixcr-shm-trees(12 GiB on the lightqueue, the worst of the set),
miltenyi-tcr-bcr-clonotyping(also single-cell, samemultiplier) and
mixcr-amplicon-alignment. Staged so this one can be confirmed againstthe customer first.
Interim workaround
Set Advanced Settings → memory per process = 192 GB. On the current release that
gives the export 48 GiB (~41 GiB heap). Use 192 specifically: the override also replaces
analyze's formula, and 192 GiB is the flooranalyzealready computes for MiToolpresets, so nothing regresses.
Greptile Summary
The PR replaces fixed
exportClonesmemory grants with bounded formulas based on.clnsblob size, preserves direct Advanced Settings overrides, and delegates PTabler sizing to workflow-tengo 6.8.2 defaults.exportClones: MiXCR command that loads a CloneSet and emits clone tables; its bulk and single-cell executions now receive separate size-based RAM formulas..clns: Stored MiXCR CloneSet artifact; it is tagged asclnsso its blob size can drive runtime resource calculation.CloneSet: In-memory collection of clones loaded byexportClones; the new bulk formula uses a 16 GiB floor, a 16× stored-size multiplier, and a 128 GiB cap.divideClonesByTags: Single-cell expansion that creates clone-per-cell representations; the corresponding export uses a 24 GiB floor and a 32× multiplier.perProcessMemGB: Advanced Settings memory override; it now applies directly to export executions instead of being divided by four.Confidence Score: 5/5
The PR appears safe to merge, with no blocking failure remaining in the eligible follow-up-review scope.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[.clns blob] --> B[Tagged input: clns] B --> C{Export mode} C -->|Bulk| D[RAM: clamp 16x size, 16 GiB, 128 GiB] C -->|Single-cell| E[RAM: clamp 32x size, 24 GiB, 128 GiB] F[perProcessMemGB override] -->|When configured| D F -->|When configured| E D --> G[exportClones] E --> H[exportClones plus tag expansion] G --> I[Exported TSV] H --> J[Single-cell TSV] I --> K[PTabler default input-volume sizing] J --> L[PTabler default input-volume sizing]Reviews (3): Last reviewed commit: "fix: lower the single-cell export floor ..." | Re-trigger Greptile