Skip to content

Debug - #12

Merged
adamfreedman merged 7 commits into
mainfrom
debug
Aug 17, 2026
Merged

Debug#12
adamfreedman merged 7 commits into
mainfrom
debug

Conversation

@adamfreedman

Copy link
Copy Markdown
Contributor

debugging of GitHub actions, including post run quarantine script and catch of SLURM jobs that failed not captured by runner process

adamfreedman and others added 7 commits August 4, 2026 12:58
A 26-dataset run failed with ~190 errors across 13 rules. Diagnosis: 179
were OUT_OF_MEMORY (mislabeled "FAILED" by the SLURM executor), hitting the
largest datasets across nearly every rule; the remaining 3 were genuine
data-driven errors.

Data guards:
- cellbender2seurat.R: CreateSeuratObject(min.features=1) drops the rare
  denoised-to-zero barcode in CellBender *_filtered.h5 that made
  log_umi=-Inf abort SCTransform.
- find_markers.R: skip clusters with <3 cells (FindMarkers minimum), writing
  a schema-correct empty file instead of crashing.
- combine_markers.R: drop empty marker tables before bind_rows to avoid a
  logical/character type clash; handle the all-empty case.
- soupx.R: fall back to autoEstCont(forceAccept=TRUE) when contamination
  estimation aborts (e.g. estimated fraction >0.8).

Memory right-sizing (from observed peak RSS; MaxRSS undercounts killed jobs):
- doubletfinder/_cellbender: scale by input size, floor 48GB (paramSweep
  peaks ~43x the input rds; 92GB observed for the largest sample).
- downsample_clusters: scale by input size, floor 32GB (all replicates run
  in one job; ~41GB observed).
- emptydrops 24->64GB; tenx2seuratrds/soupx/soupx_emptydrops/scdblfinder/
  posthocfilter_mad/posthocfilter_threshold 24->48GB; find_markers 12->24GB.

Right-sizing attempt-1 baselines also sidesteps the unreliable retry path
(observed: ~half of OOM restart intents were never resubmitted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second debugging round on 26-dataset runs: the prior fixes cut failures from
193 to 82 and reached 99.7%, but downsample_clusters and a few heavy-dataset
jobs still failed. Root causes were the serial single-job downsampling design,
under-sized memory for the largest (emptyDrops) cell counts, a stray small
future limit, and an unguarded PCA on over-filtered samples.

downsample_clusters (all 100 replicates run serially in one job):
- Resize to a flat 64 GB / 15 h: peak is a single replicate's SCTransform
  footprint (~42 GB) and wall-time is ~100x per-replicate (worst ~10.2 h);
  input-size scaling mis-modeled this (small cteleta was among the heaviest).
- Fix future.globals.maxSize 2 GB -> 16 GB (every other script already used
  16 GB); the 2 GB cap hard-failed SCTransform on large subsamples (5 jobs).
- Strip the input to a counts-only object before the loop: the upstream SCT
  assay / PCA / UMAP / graphs were pinned across all replicates and re-copied
  into every subset() but never used (~36 -> ~30 GB peak; grows with cells).

Memory right-sizing for the largest emptyDrops cell counts:
- doubletfinder / _cellbender: input scaling 48x -> 64x (amellifera peaked
  ~105 GB from DoubletFinder's O(N^2) distance matrix).
- cellbender2seurat 24 -> 48 GB; posthocfilter (mad+threshold), soupx,
  soupx_emptydrops 48 -> 64 GB.

Robustness guard:
- require_min_cells_for_pca() in silhouette_utils.R, called before RunPCA in
  all RunPCA scripts. Over-filtered samples (e.g. cteleta: 21 of 54k cells
  survive the fixed threshold) now stop with an actionable message instead of
  a cryptic SVD error.

Memory efficiency (results-identical):
- posthocfilter (mad+threshold): rebuild a counts-only object after subset()
  so the upstream SCT/PCA/graph baggage is dropped before recomputation;
  posthocfilter_mad also frees the SingleCellExperiment QC copy early.
- soupx: extract cluster labels + UMAP from seurat_base and drop it before
  autoEstCont/adjustCounts.

Scalability docs:
- Document DoubletFinder's O(N^2) memory in doubletfinder.R and README, and
  recommend scDblFinder for >100k-cell datasets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When post-hoc filtering leaves too few cells to cluster, the require_min_cells_for_pca
guard stops the job. This adds two decoupled mechanisms to handle such samples cleanly.

Quarantine (automatic, advisory):
- The guard now embeds a stable "[LOW_QUALITY_SAMPLE]" token in its message.
- New workflow/scripts/quarantine_low_quality_samples.py scans run logs for the token,
  identifies the affected samples, and moves ALL of each flagged sample's outputs from
  results/<subdir>/ into results/low_quality_samples/<subdir>/, preserving structure
  (job logs stay in results/logs/ for debugging and stable detection). It writes an
  advisory results/low_quality_samples/flagged_samples.txt and points the user at
  excluded_samples. It never edits config and never drives the DAG.
- Wired into the runner after the snakemake call (runs on completion, preserving the
  workflow's exit status).

Exclusion (explicit, user-controlled):
- New excluded_samples list in config/config.yaml (validated in common.smk) drops the
  listed sample IDs from the DAG, with a visible "[excluded_samples] skipping ..." message
  at startup and a note for IDs not found in the sample sheet. Also filters downsample
  targets (for downsample_only mode).
- This is the ONLY thing that changes the DAG, so nothing is skipped automatically: a
  re-sequenced library that reuses a sample ID is processed normally unless the user
  deliberately lists that ID.

Verified by dry-run: excluding 2 samples removes exactly their jobs (2394 -> 2210) with
no leaked targets; quarantine moves flagged outputs, writes the advisory list, and leaves
other samples untouched; idempotent and a no-op when nothing is flagged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously the SLURM submission throttling lived on the runner command line and the
low-quality-sample quarantine ran as a step appended after the snakemake call. Both are
workflow behavior, so a run launched with a different runner script silently skipped them
(as happened with a personal runner that lacked the quarantine step). Move both into the
workflow so any launcher gets them via --workflow-profile / the Snakefile.

- profiles/slurm/config.yaml: add keep-going, jobs, max-jobs-per-timespan,
  max-status-checks-per-second, latency-wait, rerun-incomplete alongside the existing
  retries. Remove stale downsample_cluster_replicate set-threads/set-resources entries
  (that rule was consolidated into downsample_clusters).
- Snakefile: run the quarantine from BOTH onsuccess and onerror handlers, so it fires on
  completion whether or not every job succeeded (flagged samples fail the guard, so such
  runs end via onerror). It is intentionally not a rule - it moves other rules' outputs,
  which would fight Snakemake's dependency tracking.
- scrnaseq_preprocess_slurmrunner.sh: slim to the invocation-level flags only.
- quarantine_low_quality_samples.py: scan logs with grep and skip the find_markers log
  subtree (which cannot carry the guard token), cutting an 18k-file scan that timed out to
  a few seconds. Falls back to a Python scan if grep is unavailable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion

With the SLURM executor, Snakemake can exit 0 even when a job fails terminally (e.g. an OOM
that exhausts retries), so the runner batch job shows COMPLETED and a real failure escapes
detection. This adds a verification that drives the true exit state, independent of
Snakemake's own exit code, and honors the rule that low-quality-sample failures are excused.

- verify_run_complete.py (new): checks that every required target (the rule-all inputs,
  passed via a targets file) exists. A missing output is a real failure only if its sample
  was NOT flagged low quality; failures confined to flagged samples - which are quarantined
  and cannot be diagnosed without running - are excused. Exit 1 on real failure, else 0.
- Snakefile: onsuccess/onerror now call _finalize_run(), which quarantines, verifies, then
  sys.exit(verify_code). sys.exit() in a handler deterministically sets Snakemake's process
  exit code in BOTH directions (unlike shell, which can only force failure), so the runner
  job's COMPLETED/FAILED status reflects reality even when Snakemake would exit 0 on a
  terminal failure. Being in the handlers, it applies to any launcher.
- quarantine: write flagged_samples.txt before moving outputs, so the verification can excuse
  flagged samples even if a later move errors.

Detection is by missing required outputs, so it is executor-exit-code-agnostic and recovered
retries (output present) correctly do not count as failures. Verified end-to-end: a hidden
missing target for a non-flagged sample forces the process to exit 1 (runner FAILED) while a
complete run exits 0, and failures confined to flagged samples are excused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The push failed the pytest and downsample-rule CI jobs for two reasons introduced with the
run-verification handlers:

- snakemake --lint flagged "mixed rules and functions in same snakefile" because _finalize_run
  was defined in the Snakefile. Move it into rules/common.smk as finalize_run().
- The onsuccess/onerror finalization verified against ALL_TARGETS on every run, so tests that
  build a single target (e.g. the marker checkpoint and downsample-rule tests) saw the other
  targets reported missing and the process forced to exit 1. Gate finalization on
  workflow.non_local_exec so it runs only under a remote (SLURM) executor - where it is needed
  to correct that executor's unreliable exit code - and is skipped for local runs (CI, tests,
  ad-hoc builds), which have reliable exit codes and may build only a subset of targets.

Verified locally: snakemake --lint passes, and test_snakemake_lint, test_marker_checkpoint_
expansion, test_cellbender_rule, test_r_rule_execution, and the downsampling rule test all
pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ow CI)

The posthocfilter memory optimization rebuilt the filtered object with CreateSeuratObject
keeping only percent.mt, which dropped the upstream metadata columns (orig.ident extras,
scDblFinder.class, upstream silhouette/purity, etc.). The full-workflow reference comparison
checks metadata column names, so the saved posthocfilter objects no longer matched and the
pytest --run-workflow CI job failed.

Switch both posthocfilter scripts to DietSeurat(assays="RNA", dimreducs=NULL, graphs=NULL)
(with DefaultAssay set to RNA first). This still drops the heavy SCT assay, PCA/UMAP
embeddings and neighbor graphs before recomputation - the intended memory savings - but keeps
the RNA counts and the full cell metadata intact, so the outputs match the reference exactly.

Verified locally: pytest tests --run-workflow passes (37 passed, 12 skipped) reusing the
prebuilt conda envs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adamfreedman
adamfreedman merged commit 5a45bf7 into main Aug 17, 2026
13 checks passed
@adamfreedman
adamfreedman deleted the debug branch August 17, 2026 11:49
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.

1 participant