Skip to content

MILAB-6871: size analyze scratch space from the sample's reads - #213

Merged
DenKoren merged 8 commits into
mainfrom
MILAB-6871_mixcr-scratch-space
Sep 8, 2026
Merged

MILAB-6871: size analyze scratch space from the sample's reads#213
DenKoren merged 8 commits into
mainfrom
MILAB-6871_mixcr-scratch-space

Conversation

@DenKoren

@DenKoren DenKoren commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

What

mixcr analyze now asks the backend for disposable disk space sized from the sample's own reads, instead of writing its temporary files into the working directory on shared storage. --use-local-temp is dropped, since TMPDIR points at that storage.

Size

S   = 2.0 x millionsOfReads x (lengthOfR1 + lengthOfR2) / 1000
C   = 11  x millionsOfReads x (lengthOfTheLongestRead / 1000) ^ 2
GiB = max(S, C)                                                    (rounded up)

S is two bytes per base of the pair, so it tracks the volume of the input. C is quadratic in read length and overtakes S only on long reads.

sample S C request
72.6M pairs x 151 bp 43.85 18.21 44GiB
1M pairs x 2000 bp 8.00 44.00 44GiB
72.6M single-end x 151 bp 21.93 18.21 22GiB

None of those numbers is in the column metadata, so both read ends are measured in one pass before the run:

seqkit stats --threads {system.cpu} --tabular reads_R1_0.<ext> reads_R2_1.<ext>
  ->  csvtk --tabs csv2json

--threads reads the granted core count back from {system.cpu}, so it tracks the allocation rather than repeating a literal. Bases are summed rather than averaged — exact, and needs no weighting when a sample spans lanes. The read count comes from R1 alone, since it counts pairs.

Shape

file role
workflow/src/read-stats.lib.tengo new — the measurement and the size formula, in one place
workflow/src/run-mixcr.tpl.tengo new — the previous mixcr-analyze body, plus a readStats input
workflow/src/mixcr-analyze.tpl.tengo now measures, then renders run-mixcr
workflow/src/process.tpl.tengo unchanged

The split is forced, not cosmetic: a template cannot read the result of a command it started itself, and exec.formula only knows size/lineCount on files.

Deduplication is preserved

pframes.processColumn still renders mixcr-analyze with the same inputs, and mixcr-analyze carries the hash_override every released block carries. The per-group dedup key is therefore what it has always been, so an analysis that already finished resolves to its existing result instead of running again. That is safe because scratch space changes only where MiXCR writes its temporary files, never what it produces.

run-mixcr carries a freshly generated override — never the one mixcr-analyze uses, since a shared override declares two templates interchangeable to the backend.

The inner render passes the CPU and memory overrides as meta inputs, matching how process.tpl.tengo passes them in via metaExtra, so a run can still be recovered across a change of either.

Also reverts an unreleased override rotation

86d727d (MILAB-6721) rotated mixcr-analyze's override from D70EDB25 to AD2092AB because the memory request formula changed. That was a mistake — a resource formula changes what a run asks for, never what it produces — and it never shipped: every release up to v2.22.0 carries D70EDB25, and git tag --contains 86d727d is empty. Left in place it would have re-analysed every sample in every project on the next block update.

Verified on lab.research.platforma.bio: a dev build off main re-ran samples the released block had already processed, and the DB showed AD2092AB appearing for the first time on that instance the moment the dev block loaded. This PR reverts the value and documents above it, in both templates that carry one, that changing it means "re-run every sample on the next block update".

Failure behaviour

The request is an optimisation, never a precondition. A deployment that cannot serve the size shrinks or grows it, one with no scratch storage ignores it, and a sample with no read end to measure asks for nothing. Analyze runs either way.

Tests

workflow/src/read-stats.test.tengo runs on the tengo VM via pl-tengo test and covers which term wins, single-end input, multi-lane summing, the longest read crossing ends, the round up to a whole GiB, and every case that must ask for nothing.

Verified on a live cluster

Ten samples through denkoren-test (EKS, EBS gp3 scratch class), against the earlier single-term formula. Requests were read from the backend's compute request was registered log line and came out per-sample distinct — 20, 22, 23, 23, 24, 24, 24, 25, 26 GiB — each then raised to the deployment's --k8s-min-scratch-request=32Gi floor, which is the EBS gp3 IOPS-ratio minimum for that storage class. The formula in this PR sizes those same samples at roughly twice that, so most will now clear the floor on their own.

Dependencies

  • @platforma-sdk/workflow-tengo 6.8.2 -> 6.9.0 — first release carrying scratchFreeSpace
  • @platforma-open/milaboratories.software-binary-collection 1.0.12 — new, supplies seqkit and csvtk

Analyze now asks the backend for disposable disk space instead of writing its
temporary files into the working directory on shared storage. --use-local-temp
is dropped, since TMPDIR points at that storage.

The size is 11 x millionsOfReads x (averageReadLength / 1000) ^ 2 GiB, rounded
up. One read end is measured with 'seqkit stats' and read back through 'csvtk';
read ends are symmetrical, so one describes the whole sample.

The measurement and the formula live in the new read-stats library. The run
moved to the mixcr-run template because a template cannot read the result of a
command it started itself; mixcr-analyze keeps its name, inputs and outputs, so
process.tpl.tengo is unchanged. Both templates carry fresh hash overrides: the
old one pinned the template hash and would have served cached results from the
previous code.

Requires workflow-tengo 6.9.0.
Comment thread workflow/src/read-stats.lib.tengo Outdated
…ng it

mixcr-analyze keeps its original hash_override, its shape and its place as the
template holding the run, so commands that already finished still deduplicate
and recover. It gains three things: the read-stats import, the scratch size read
back from its new readStats input, and that size in the resources it already
built.

The measuring wrapper moves out to measure-and-analyze, which process.tpl.tengo
now renders. It carries its own fresh hash_override, being a new template.
…enders

The previous commit put the wrapper under a new name, which changed the template
pframes.processColumn renders and so changed the dedup key for every group. An
analysis that had already finished would have run again.

Swap the two: mixcr-analyze is now the wrapper that measures and renders the run,
keeping the hash override it has always carried and the inputs processColumn has
always passed, so a finished analysis still resolves to its result. That is safe
because scratch space changes only where MiXCR writes its temporary files, never
what it produces.

The run moves to run-mixcr with a freshly generated override - never the one
mixcr-analyze uses, since a shared override declares two templates
interchangeable. process.tpl.tengo goes back to its original text.

The inner render passes the CPU and memory overrides as meta inputs, matching how
process.tpl.tengo passes them in, so a run can still be recovered across a change
of either.
… terms

    S   = 2.0 x millionsOfReads x (lengthOfR1 + lengthOfR2) / 1000
    C   = 11  x millionsOfReads x (lengthOfTheLongestRead / 1000) ^ 2
    GiB = max(S, C)

S is two bytes per base of the pair, so it tracks the volume of the input. C is
quadratic in read length and overtakes S only on long reads. The previous
formula was C alone with the average read length, which under-sized every short
read sample: the reference 72.6M x 151bp pair asked for 18GiB where the volume
term wants 44GiB.

Both read ends are measured now, in one seqkit pass: the volume term needs their
lengths separately and the longest read may sit in either. Bases are summed
rather than averaged, which is exact and needs no weighting when a sample spans
lanes, and the read count comes from R1 alone, since it counts pairs.

read-stats.test.tengo covers which term wins, single-end input, multi-lane
summing, the longest read crossing ends, the round up to a whole GiB, and the
cases that must ask for nothing.
seqkit parallelises across files, not within one, so the request follows their
number and no core idles. Restores the rationale the STATS_CPU comment carried.
600M single-end reads of 1000bp ask for 6600GiB, well under the 16Ti that AWS
deployments configure as --k8s-max-scratch-request. The quadratic term decides
at that shape, so this is where a runaway would show first; the test fails if a
change to the formula pushes a plausible sample into the clamp, where it would
silently get less space than it was sized for.
@DenKoren
DenKoren force-pushed the MILAB-6871_mixcr-scratch-space branch 2 times, most recently from 76aff3c to b1f69ad Compare September 8, 2026 07:32
MILAB-6721 rotated the hash override because the memory request formula
changed. That was a mistake - a resource formula changes what a run asks for,
never what it produces - and it never shipped: every released block up to
v2.22.0 carries D70EDB25, and AD2092AB exists only on main. Left alone it would
have re-analysed every sample in every project on the next block update.

Reverted to D70EDB25 and documented what the value means above it, in both
templates that carry one.
@DenKoren
DenKoren force-pushed the MILAB-6871_mixcr-scratch-space branch from b1f69ad to 78f93ee Compare September 8, 2026 07:36
Dropping the flag assumed TMPDIR points at scratch storage. A backend that does
not understand the request leaves it pointing at the small temporary directory
every job shares, which is not enough for MiXCR to sort in, so the flag is
restored there and the block behaves exactly as it did before this change.

Gated on feats.hasScratchSpace, which is false both when the backend does not
report the flag and when it is too old to answer at all. The same flag stops the
seqkit pass: a measurement no backend can use is not worth a full decompression
of every sample.
@DenKoren
DenKoren enabled auto-merge September 8, 2026 07:54
@DenKoren
DenKoren added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 5a5c32e Sep 8, 2026
10 checks passed
@DenKoren
DenKoren deleted the MILAB-6871_mixcr-scratch-space branch September 8, 2026 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants