Changelog¶
Changelog¶
All notable changes to echelon3 are documented here. Format follows Keep a Changelog loosely; versions follow SemVer once 1.0.0 ships.
0.11.0 — 2026-08-11¶
Changed (BREAKING)¶
- keep_best direction is now per-name; the global
high_is_betterand the overloadedvaluekey are removed. Akeep_best_onentry that names a loss defaults tolow(minimise); a metric defaults tohigh(maximise). This fixes the reported case wherekeep_best_on: <loss_name>saved only the initial checkpoint — the loss fell but the old default (high_is_better=True) treated a bare name as "maximise", so a falling loss never counted as an improvement. - New schema:
- bare:
keep_best_on: <name>orkeep_best_on: [<name>, …](per-type default direction); - shorthand:
keep_best_on: {<name>: high|low}; - directional:
keep_best_on: {<name>: {mode: directional, direction: high|low}}; - tolerance:
keep_best_on: {<name>: {mode: tolerance, direction: high|low, tolerance_value: 0.1%}}.
- bare:
- Migration (hard errors, not silent): passing
trainer.config.high_is_betternow raises; a{<name>: {value: …}}entry now raises — replacevaluewithdirection(directional) ortolerance_value(tolerance). Update legacy configs accordingly.
Fixed¶
- A
keep_best_onkey that is neither a metric nor a loss is now a hard error at build time. When a base recipe'skeep_best_onmerges with a child config and leaks a key this run never computes, the old code silently kept only the initial checkpoint (the missing key never "improved") — this could waste whole training runs. The stray key now fails immediately with the list of available metrics/losses. - A declared keep_best metric/loss that is not actually computed at validation now warns
loudly instead of silently never saving. If
metrics_onroutes a tracked key to a loader that does not run it, the trainer prints a one-time warning naming the missing key(s) rather than quietly skipping every checkpoint. net.weights+ auto-resume now warns that the init weights are discarded. Whentarget.pathalready holds checkpoints, training resumes from the latest and thenet.weightsinitialisation is silently overridden — the trainer now says so and tells you to clear/movetarget.pathto start fromnet.weightsinstead.
0.10.9 — 2026-08-08¶
Fixed¶
- A crashing rank under DDP now surfaces the real traceback in the launcher's failure
summary. echelon3 hard-exits a crashed rank (
os._exit(1)) to avoid an NCCL-teardown hang, which bypassed torch elastic's error recording — so the summary showedChildFailedError / <NO_OTHER_FAILURES> / exitcode 1with the actual exception buried in the interleaved multi-rank output. The rank now records the exception to the elastic error file before exiting, and the entrypoint is wrapped with@recordfor uncaught setup errors, so the launcher reports the real rank/message/traceback.
Added¶
Metriccan derivereset()/to()/dist_reduce()from a declared counter list. Declare_counters = ("total", "count")and a "sum a few scalar counters, return a ratio" metric only needsupdate()/compute()— the base zeroes, moves, and SUM-all-reduces the counters. Opt-in and backward compatible (empty by default; metrics that manage their own state are unchanged).- Build-time metric validation.
create_metricsnow checks each training metric exposesupdate/compute/reset/toand thatreset()runs (and, for declared_counters, actually creates them) — so a broken metric fails at construction with a clearTypeError, not ~a minute later on the first validation after DDP spin-up +torch.compile.
Changed¶
python -m echelon3.clinow prints how to invoke the CLI (echelon3 train …) instead of Python's opaque "package cannot be directly executed".
0.10.8 — 2026-08-05¶
Added¶
- DDP performance levers for bandwidth-limited interconnects (consumer GPUs, PCIe without
NVLink). On such boxes DDP's per-step gradient all-reduce runs at a fraction of NVLink
bandwidth (measured ~7 GB/s over PCIe vs ~47 GB/s NVLink on 3090, ~210 GB/s on H200), so for
a small net the sync is largely exposed and adding GPUs scales poorly. New opt-in knobs under
trainer.config: grad_accum_steps: N(default 1) — run the optimizer every N micro-batches (effective batch = N × batch_size). Under DDP the all-reduce is skipped (no_sync) on the N-1 non-boundary micro-steps and done ONCE per window, so gradient sync happens N× less often — the biggest lever when comm-bound. Not supported with closure optimizers (SAM/LBFGS) or cleanly withddp_find_unused_parameters: true(warned).ddp_comm: bf16 | fp16 | none(default none) — register a DDP gradient-compression comm hook that halves the all-reduce payload (≈2× faster sync).ddp_gradient_as_bucket_view: true(default) — reduce gradients in place in the bucket (less memory, small speed win); overridable.numa_affinity: true(default off) — pin each rank to its GPU's NUMA-node CPUs before the dataloaders spawn, cutting cross-socket traffic on multi-socket boxes. Best-effort; correctly maps the CUDA-visible index to the physical GPU underCUDA_VISIBLE_DEVICES.- Opt-in
torch.profilerhook (trainer.config.profile). Profiles a short wait/warmup/active window and writes a Chrome/TensorBoard trace (toprofile.dir) that separates compute vs NCCL all-reduce vs dataloader wait — for quantifying DDP scaling. Rank 0 by default;stop_after: trueends the (throwaway) run once the window is captured.
0.10.7 — 2026-08-04¶
Fixed¶
- Checkpoint epoch/step metadata and resume semantics. The initial validation added in
0.8.7 runs before training with
_current_epoch=1, andsave_checkpointstored that as the epoch — so the step-0 baseline checkpoint was labelledepoch=1, colliding with the one saved after the first real epoch (alsoepoch=1), andresumefrom a checkpoint of a completed epoch N re-ran epoch N (an off-by-one that silently repeated an epoch). Now: - the stored
epochis the number of completed epochs (0for the initial baseline,Nafter epoch N) — the baseline no longer collides with after-epoch-1; global_stepis persisted in the checkpoint (and doubles as the new-format marker);- resume from a new-format checkpoint starts the next epoch (completed
N→ epochN+1), no longer re-running a finished epoch; - the scheduler is reconciled on resume: an end-of-epoch checkpoint is saved before that
epoch's
scheduler.step(), so its stored scheduler lagged by one; resume now advances it to match the completed-epoch count, so the LR schedule continues correctly instead of running one step behind; - the "save every checkpoint" path (no
keep_best_on) now announces the initial baseline save (--> Initial baseline (epoch 0). Saving checkpoint.) instead of saving silently.
Backward compatibility: pre-0.10.7 checkpoints (no global_step key) resume with the
old semantics unchanged — no epoch is skipped for an in-progress run mid-upgrade.
0.10.6 — 2026-08-04¶
Fixed¶
- DataLoader workers no longer oversubscribe the CPU. Each worker inherited a thread pool
sized to the machine's core count (
cv2.getNumThreads()andtorch.get_num_threads()both default to all cores), sonum_workersworkers spun up ~num_workers × coresthreads — e.g. 28 workers on 32 cores → ~1600 threads thrashing on context switches, the GPU starved at 0%, and an epoch several times slower, all with no error in the log. The injected worker-init now caps each worker to a single intra-op thread (torch.set_num_threads(1)+cv2.setNumThreads(0)), as mmcv / detectron2 / ultralytics do; the DataLoader itself provides the parallelism. Override per loader withdataloaders.<split>.config.threads_per_worker(default 1). Applies totrain, in-training validation, andevaluate.
Changed¶
find_unused_parametersnow defaults tofalseunder DDP (wastrue) — matching torch's own default.trueforces an extra autograd-graph traversal every iteration (torch itself warns it "can adversely affect performance") and is only needed for nets that leave some parameters out of the loss on a step (branchy / conditional heads). Migration: if a DDP run of such a net starts erroring with "Expected to have finished reduction … enablefind_unused_parameters=True", settrainer.config.ddp_find_unused_parameters: true. Nets that use all parameters every step (the common case, includingMultiHeadTrainer) just get the speedup and lose torch's warning.
0.10.5 — 2026-07-27¶
Added¶
PairEvaluator— a pair-input evaluator, theevaluatecounterpart ofPairTrainer. Two-image ("image-in-image") models take a(base, query)pair, but the genericEvaluatorcallsnet(single_input), soechelon3 evaluatecouldn't score them (the metric was never fed, yieldingNone).echelon3.evaluators.pair.PairEvaluatormirrorsPairTrainer's forward: it installspair_collate_fnto keep pairs paired, callsnet(base, query)(forwarding a positional return-features flag whenconfig.return_features: true), and feeds the metric(prediction, gt)— cast to fp32 and dim-normalised exactly as training's validation does, so an eval score matches the train-time validation score. The metric itself is supplied by config, as intrain; the evaluator stays task-agnostic.evaluator: { module: echelon3.evaluators.pair, type: PairEvaluator, config: { return_features: false }, metric: <name> }
0.10.4 — 2026-07-27¶
Fixed¶
echelon3 evaluatenow accepts a named dict of test sets, liketrain.data.testcan be a single set or a named dict ({test: {...}, test_ms: {...}});trainsupports both, butevaluateassumed a single set and readdata.test.module, so a multi-test config that trains fine crashed with an opaqueomegaconf … Missing key module.evaluatenow resolves the same two formats: for a named dict it evaluates each set (pairing it with itsdataloaders.testentry) with a fresh metric + evaluator and printsValidation [name] <metric>: …per set; a single set is unchanged. A named set without a matchingdataloaders.testloader — or an emptydata.test— now raises a clear error instead of the opaque one. The dataloader info line also tolerates a loader with noconfigblock.
0.10.3 — 2026-07-27¶
Fixed¶
- MultiHeadTrainer no longer silently disables mixed precision. It overrode
one_step_train/one_step_validatewith hand-rolled copies that dropped the base'storch.autocastcontext (and the fp16 GradScaler) — so a run configured for bf16/fp16 ran in fp32 while the banner reported AMP was on (double the memory, half the throughput, no warning). It now overrides onlycompute_losses(the seam the base runs inside autocast), inheriting the base step's precision/scaler/closure path unchanged. The tensor-only image/data logger it needs to skip moved to overridable_log_{train,test}_step_datahooks, so skipping it no longer means re-implementing the step. (This also removes an incidental double-logging of validation losses.) - Validation progress bar no longer looks truncated for non-tensor batches.
validate()sized the bar in samples but advanced it by the batch'ssource.size(0), which fell back to 1 for a non-tensorsource(a dataclass, a variable-size graph batch) — so the bar crawled (e.g. "30/968") and looked like validation stopped early, mimicking a metric- inflating truncation bug. It now counts batches (len(loader), one step per batch), uniform across source shapes and matching the training bar. Display only — the data was always read in full.
0.10.2 — 2026-07-27¶
Fixed¶
- Resume now warns when the checkpoint overrides config optimizer hyperparameters. On
resume (
reset=False) the checkpoint's optimizer state restoreslr/weight_decay/ etc., silently overriding the config values the optimizer was just built from — while other config (e.g.batch_size) still applies. A run could therefore look like it honoured a new config but actually mix new and old settings (new batch size, old LR). The trainer now snapshots the config-built hyperparameters before restoring the checkpoint and prints a rank-0 warning listing each one the checkpoint overrides, pointing attrainer.config.reset: true(keep the weights, restart optimizer/scheduler/epoch from the config). When an LR scheduler is in use the comparison uses the base LR (initial_lr), so a normal scheduled resume does not warn on the decayed learning rate — only a genuine base-LR change does. Fused/capturable optimizers (LR stored as a tensor) are handled too.
0.10.1 — 2026-07-27¶
Fixed¶
- Stateful losses are now moved onto the trainer device. The net and metrics were
moved to
device, butself._losseswas assigned as-is. A loss carrying a buffer or parameter —CrossEntropyLoss(weight=…),BCEWithLogitsLoss(pos_weight=…)(both common for class imbalance), or a learnable loss such as ArcFace — kept that state on CPU while predictions were on cuda, so the forward crashed with a device mismatch. Stateless losses never hit this (no buffers), which is why it went unnoticed. The trainer now callsloss.to(device)on eachnn.Moduleloss (a no-op for stateless ones; non-Module callables are left as-is), mirroring how the net and metrics are handled.
0.10.0 — 2026-07-23¶
Added¶
MultiDatasetMetric— a metric that spans several validation datasets with a singlecompute(). Some metrics need a cross-dataset context that a per-loader metric cannot express — e.g. retrieval, where recall/mAP is defined over a query set matched against a gallery set. AMultiDatasetMetric(inechelon3.metrics) declares the test datasets it spans viaself.datasets(typically built in its constructor from roles such asquery_dataset/gallery_dataset), and itsupdate(predicted, target, dataset)receives the name of the current batch's source dataset.Trainer.validate()orchestrates it: onereset()before all of the metric's datasets, taggedupdate()s while iterating the loaders (in the same pass as ordinary metrics — no extra forward), and a singledist_reduce()+compute()after all of them. Ordinary single-dataset metrics are unchanged.keep_bestcan track a multi-metric by name. The datasets a metric declares must exist among the test loaders and the roster must be non-empty, else validation raises a clear error. Under DDP,dist_reduce()gathers each metric's buffers across ranks (helperall_gather_cat) socompute()sees the full set; the console prints a shortFinalizing multi-dataset metrics…/Finalized multi-dataset metrics: …summary after the per-loader lines.
Changed¶
- All in-code comments and docstrings translated to English. echelon3 is a public, multi-language package; the source is now uniformly English. No behavior change — the sweep touched only comments/docstrings (and a single informational runtime line in the estimator entry point).
0.9.6 — 2026-07-18¶
Changed¶
persistent_workersnow defaults totruefor the training DataLoader whennum_workers > 0. Previously workers were torn down and respawned every epoch; a Ctrl-C landing at an epoch boundary could catch a worker mid-bootstrap (underspawn: importing torch / unpickling the payload — before echelon3's worker-init installsSIGINT → SIG_IGN), which dumpedKeyboardInterrupttracebacks and leaked semaphores from the half-started processes. Keeping workers alive across epochs removes that per-epoch window (and saves the respawn cost). Applied viasetdefault, so an explicitdataloaders.train.config.persistent_workers: falseis respected, and only whennum_workers > 0(torch rejects the option otherwise). Eval loaders are intentionally left non-persistent — validation is not a tight per-epoch loop, and transient eval workers avoid holding node RAM for the whole run. The DDP launcher now prints a short informational line noting the default is on and how to opt out.
0.9.5 — 2026-07-18¶
Fixed¶
-
DDP Ctrl-C no longer hangs ~30s and leaks semaphores when a rank is interrupted a second time mid-shutdown. After 0.9.4 stopped the abort, a rank that had already entered its interrupt handler (printed
shutting down) could still hang: the elastic agent re-sends SIGINT to every rank, and that second SIGINT — delivered while inside ourKeyboardInterrupthandler — re-raisedKeyboardInterruptpast theos._exit(130), landing in thefinallywheredestroy_process_group()deadlocks on NCCL teardown. The rank then hung until the agent force-SIGKILLed it at the grace period (~30s), which hard-killed the DataLoader workers without releasing their semaphores (leaked semaphore objects). The interrupt handlers now setSIGINT → SIG_IGNas their very first action (helper_silence_sigint, before any print/flush), so the path down toos._exit(130)can no longer be diverted; workers are reaped in the bounded window and the rank exits promptly. Only the interrupt path is silenced — the genuine crash path keeps Ctrl-C live. -
albumentations no longer stalls startup and spams
UserWarnings trying to fetch version info. albumentations runs a network version-check on import (check_version.py) that, offline, blocks on an SSL handshake timeout and warns every epoch. echelon3 now setsNO_ALBUMENTATIONS_UPDATE=1at package import (viasetdefault, before albumentations is imported), disabling the check; set the env var yourself to override.
0.9.4 — 2026-07-17¶
Fixed¶
- DDP Ctrl-C no longer aborts with a scary traceback /
Fatal Python error: Abortedwhen a DataLoader worker is killed by SIGINT first. On Ctrl-C the whole process group gets SIGINT and a worker can die before echelon3's worker_init installsSIG_IGN(startup race); torch then raises, in the rank, eitherDataLoader worker ... is killed by signal: Interruptor — more often on recent torch —DataLoader worker ... exited unexpectedly, which the crash path reported as a failure (traceback +os._exit(1)), and teardown could C++-abort. The rank now records that a SIGINT was actually seen (a tiny handler layered on the defaultKeyboardInterruptbehaviour) and treats a worker-deathRuntimeErroras a clean interrupt (exit 130) — but only after a real SIGINT, so genuine worker crashes (OOM SIGKILL, segfault) and non-worker errors still surface loudly.
0.9.3 — 2026-07-16¶
Fixed¶
- MultiPartDataset now works under DDP.
create_dataloadersno longer injects an int-indexDistributedSamplerfor aMultiPartDataset(its index is a(part, sample)tuple — the mismatch crashed the worker with'int' object is not subscriptable).MultiPartBatchSampleris now DDP-aware and rank-shards the largest part, padding to an equal per-rank count so every rank yields the same number of batches (no gradient all-reduce hang). AMultiPartDatasetpaired with a plainDataLoader(train or test) now raises a clear error namingMultiPartDataLoaderinstead of the cryptic worker crash. MultiPartBatchSampler.__len__now uses the same per-part quota as__iter__(quants[max_part], notint(share*batch_size)): the mismatch overstatedlen()when the largest part was configured last with fractional shares, which silently skipped end-of-epoch validation and checkpoint saving.MultiPartDataLoaderdefaultprefetch_factor2 -> None(the hardcoded2crashednum_workers=0on modern torch).
0.9.2 — 2026-07-16¶
Fixed¶
- DDP Ctrl-C no longer leaks DataLoader-worker semaphores. Under DDP the rank's
KeyboardInterrupt/Exception handler calls
os._exit(), which bypasses thefinally:that runstrainer.close()— so the DataLoader workers were hard-killed by PDEATHSIG without releasing their semaphores, and the launcher'sresource_trackerwarned about "leaked semaphore objects" (leaking /dev/shm).trainer.close()now runs before everyos._exitintrain/finetune(via_close_quietly, a best-effort call bounded by a watchdog timeout so it can never hang the hard exit — the_shutdown_workerspin-memory-thread join is otherwise untimed), reaping the (persistent) workers cleanly.
0.9.1 — 2026-07-15¶
Added¶
dataloaders.*.config.collate_fncan be a component (module/type/config): the engine builds it into a callable and passes it to the DataLoader, instead of leaving a dict that would break the loader. Enables variable-size / graph batching (sets, molecular complexes) on the SGD path; domain-agnostic, applied to both train and test dataloaders. (Used by the docking components inechelon3_zoo[docking].)
0.9.0 — 2026-07-15¶
Added¶
- Tabular fit/predict (estimator) trainer family — a second, independent trainer stack
next to the image/SGD one, for models that are fit once rather than trained by gradient
descent: gradient-boosted trees (CatBoost/XGBoost/LightGBM/sklearn) and tabular foundation
models (TabPFN/TabICL/TabFM/TabGPT). Same
module/type/configidiom, sameechelon3 train: a config with amodel:section (and nonet:) routes to the estimator assembly. trainers/estimator.py:EstimatorTrainerandMultiTargetEstimatorTrainer(one cloned model per target, fit only on rows where that target is measured — NaN-masked; bundle{target: model}). No optimizer/loss/dataloaders/scheduler — the objective/loss is a hyperparameter of the model itself (model.config).data/tabular.py:TabularDataset(sources: csv/parquet/feather/json/tsv, SQL, or an in-memory frame; single- or multi-target) andTabularPreprocessor(declarative sklearnColumnTransformeras afeature_transform, so swapping engines stays a change of onlymodel:even on categorical/NaN data).metrics/tabular.py: classification AUC/Gini/KS/LogLoss/Accuracy and regression MAE/RMSE/R2/SpearmanR/PearsonR.inference/tabular.py:load_bundle+predict— the saved.taris a self-contained inference artifact (model(s) + fitted feature pipeline + feature names + target).- Molecular/ADMET components (SMILES featurizer, molecular-graph dataset, a 2D GNN) live in the
public
echelon3_zoopackage under themolecularextra — the engine stays domain-agnostic and free of an rdkit dependency.
0.8.7 — 2026-07-13¶
Fixed¶
- Initial validation now runs BEFORE training, for both fresh and resumed runs.
Previously a from-scratch run went straight into
train_epoch()and the first validation (the one printed asInitial metrics baseline) only happened partway through the first epoch — so the "baseline" was measured after some training. The initialvalidate_and_check_for_saving()is now called once before the epoch loop for both paths (scratch and checkpoint), so the baseline is the step-0 / loaded-checkpoint state, and training must beat it. (Side effects: a fresh run now saves an initial checkpoint at step 0; a resumed run printsInitial metrics baselineand re-saves it.)
0.8.6 — 2026-07-12¶
Changed¶
- Reworked the training / validation console output into clean per-cycle summary
lines. The live
tqdmbars (Training epoch N …,Evaluating [name] …) are now transient (leave=False); when each closes it is replaced by one past-tense summary line instead of a leftover bar: --> Trained epoch N: 25% (256/1000), lr=3.00e-04, loss1=…, loss2=…— how far the epoch had progressed at this validation point, the current LR, and the latest losses;--> Evaluated [name]: loss1=…, metric1=…— one line per test loader. Numbers use an adaptive format (trailing zeros trimmed, scientific for very small / large values), so metric values no longer print astensor(0.0161, device='cuda:0')— which used to overflow the terminal and leave garbled, half-overwritten lines. The keep-best "Saving checkpoint" lines round the same way.
0.8.5 — 2026-07-12¶
Fixed¶
- Validation output no longer leaves garbled "Evaluating [...]" lines. Metric
values were pushed into the tqdm postfix as raw
tensor(0.0161, device='cuda:0'); the line overflowed the terminal width, so tqdm's\rcould not clear it and each evaluated test loader left two or three half-overwritten lines before the next epoch. The per-loader eval bar is nowleave=False(cleared on completion) and the result prints as one tidy line per test set with plain floats, e.g.--> [test_geoloc3] l1=0.9636 mse=1.4694. - Warning summary (
warncollect) no longer cuts a message mid-word — over-long messages are trimmed with an ellipsis.
0.8.4 — 2026-07-12¶
Docs¶
- README (PyPI front page): added a "Use it from your AI coding agent" section
pointing at the
echelon3-agent-skillsrepo, with the one-line marketplace-install commands for Codex and Claude Code.
0.8.3 — 2026-07-12¶
Docs¶
- Rewrote the README (the PyPI front page) — clear value prop, a complete copy-and-run
config + command, a "what you get" list, and the CLI/overrides — and swept the docs,
examples and CI onto the 0.8.0 interface:
echelon3 <cmd>everywhere (not theechelon3-<cmd>aliases), OmegaConf overrides anddefaults:composition, built-in DDP viagpus=[...]. Fixed stale DataParallel/torchrunwording, dropped the legacydevice_idsfrom the example configs, removed committed editing artifacts from doc pages, added thedetectionextra to the install docs, and corrected theddp.pymodule docstring. Verified by a documentation-review pass andmkdocs build --strict.
0.8.2 — 2026-07-12¶
Packaging¶
CHANGELOG.mdnow ships inside the package —echelon3/CHANGELOG.mdin the wheel (so it lands insite-packages) and in the sdist — and aChangelogproject URL is exposed in the metadata (shown bypip show echelon3and on PyPI). Previously the changelog lived only in the repo, so tools inspecting the installed package couldn't find it.
0.8.1 — 2026-07-12¶
Fixed¶
ZeroDivisionErroron small datasets whentimes_to_validate_per_epochexceeds the number of batches in an epoch (e.g. 2 batches with=5). The validation trigger computed... % (total_batches // times_to_validate_per_epoch), which is% 0whentotal_batches < times_to_validate_per_epoch. Nowmax(1, total_batches // times_to_validate_per_epoch). Hit GPU and CPU alike, on any tiny dataset.device: cpunow forces CPU even on a multi-GPU host. With nogpusset the DDP launcher used every visible GPU and ignoreddevice: cpu; it now returns early whendeviceiscpu, so a CPU run stays on CPU.
0.8.0 — 2026-07-11¶
Changed¶
- Dropped Hydra; the CLI is now a single
echelon3command with subcommandstrain | finetune | evaluate | export | run(the oldechelon3-<cmd>scripts stay as transitional aliases). Config loading and CLI overrides run on OmegaConf directly. The override syntax is compatible —key=value,+key,++key,~key(delete), typed values, lists,${oc.env:...}— with no strict/struct footgun (key=valueadds a new key;+is optional).defaults:composition is supported (base configs + config-groups +_self_), so existing configs — including composed ones — run unchanged;hydra:blocks andhydra.*overrides are ignored. This removes the recurring Hydra footguns (cwd/output-dir coupling, exception wrapping, struct-mode prefixes) and a heavy dependency (hydra-core→click).
0.7.12 — 2026-07-11¶
Fixed¶
- Single-GPU
device=cudaruns crashed (regression since 0.7.9). Non-DDP device selection returnstorch.device('cuda')(no index) when nogpus=is given, andtorch.cuda.set_device()rejected it ("Expected a torch.device with a specified index").set_deviceis now called only for an explicitcuda:{index}(fromgpus=); barecudaalready defaults to device 0. - Ctrl-C now stops cleanly when DataLoader workers are used. SIGINT reaches the
whole process group, so a worker could die first and the main process (waiting in
next(iterator)) saw "DataLoader worker exited unexpectedly" (aRuntimeError) instead ofKeyboardInterrupt, dumping a traceback — the real cause behind the original Ctrl-C complaint (a race: sometimes clean, sometimes not). Workers now ignore SIGINT; the main process handles the interrupt and reaps them via PDEATHSIG.
0.7.11 — 2026-07-10¶
Fixed¶
- Ctrl-C now stops cleanly instead of dumping a traceback.
KeyboardInterruptis handled separately from real errors — a one-line "Interrupted by user (Ctrl-C)" message and exit code 130, no traceback. Under DDP the launcher also catches torchelastic'sSignalException(SIGINT reaches the whole process group) and exits cleanly; workers hard-exit (130) so peers and DataLoader workers are still reaped. - Stray training progress bar after validation. With
times_to_validate_per_epoch=1(validate at the end of each epoch) the training bar was re-created after the "Evaluating" bar even though the epoch had already finished, printing a phantom "Training epoch N" line between the validation and the next epoch. The bar is now re-created only when batches remain in the epoch.
0.7.10 — 2026-07-10¶
Changed¶
- DDP launcher messages now print after the banner and in the same colour. The
--> DDP: launching …/ dataloader-RAM / warning lines are emitted by the parent before workers start; they used to appear before the product banner and uncoloured, because the banner andFore.CYANwere set only in the worker. The banner and colour are now set in the parent (trainer_app/finetune_app) before the DDP launch, so the banner comes first and the launcher lines inherit the cyan style; workers no longer reprint the banner.
0.7.9 — 2026-07-10¶
Fixed¶
- Single-GPU runs now honour the
gpusindex instead of always landing on GPU 0. Non-DDP device selection readcfg.device(cuda→cuda:0) and ignoredgpus, soechelon3-train … gpus=[1]silently ran on physical GPU 0 — colliding with other jobs and breaking "don't touch GPU 0" reservations on shared hosts (DDP already honoured the index). The CLI now pins the process tocuda:{gpus[0]}(plustorch.cuda.set_device) via aresolve_single_devicehelper;device: cpuoverrides still win and configs withoutgpusare unchanged. Done via an explicit device index rather thanCUDA_VISIBLE_DEVICES, which is a no-op in-process once the CUDA runtime has initialised (torch.cuda.is_available()alone locks it).
0.7.8 — 2026-07-09¶
Fixed¶
MultiHeadBinaryIoUnow aggregates across ranks under DDP, fixing noisy best-checkpoint selection for every project that uses it (validation is sharded per rank, and the metric previously computed on rank 0's shard only — inflating the tracked value). It implements thedist_reduce()hook (from 0.7.7): a SUM all-reduce of each head's rawtp/fp/fn/ncounters beforecompute(). This is exact because the counters are additive over samples;nis reduced too, so the set of seen heads — and thus the macro-mean denominator — is consistent across ranks. No-op outside DDP; the counters are non-persistent buffers, so checkpoints are unchanged.
0.7.7 — 2026-07-08¶
Added¶
Metric.dist_reduce()hook — exact custom-metric aggregation under DDP. Validation is sharded per rank; torchmetrics reduce their state insidecompute(), but customechelon3.metrics.base.Metricsubclasses used to compute on rank 0's shard only, making keep-best selection noisy. The baseMetricnow has adist_reduce()method (default no-op) that the trainer calls on every rank right beforecompute()under DDP. A counter-based metric (e.g. IoU's intersection/union accumulators) implements it as a SUM all-reduce of its buffers — helperall_reduce_sum_(*tensors)provided — which is exact, because summation commutes with sharding (unlike averaging per-shard ratios). Single-GPU runs and torchmetrics are unaffected.
0.7.6 — 2026-07-08¶
Changed¶
- The per-component
config:block is now optional everywhere, uniformly. One rule across everycreate_*(net, backbone, dataset, loss, metric, optimizer, scheduler, dataloader, trainer, evaluator, wrapper, constructor, batch sampler, exporter): a component whose constructor needs no arguments may omit itsconfig:block. Removes the previous "optional in some builders, required in others" inconsistency (present-configbehaviour is unchanged).
Fixed¶
Metric.to()no longer strands buffers on CPU. The baseechelon3.metrics.base.Metric.to()was a no-op that shadowednn.Module.tovia the MRO for metrics that are alsonn.Modules with buffers/parameters — their buffers stayed on CPU and validation on CUDA raised a device-mismatch error. It now delegates tonn.Module.to()when the metric is a module; pure non-module metrics stay a no-op as before.
Docs¶
- DDP guide: corrected the validation section — only torchmetrics reduce their state
across ranks; custom
Metricsubclasses compute per-shard, so keep-best is driven by rank 0's shard (use a torchmetrics metric, or add a distributed reduce, for exact behaviour).
0.7.5 — 2026-07-08¶
Fixed¶
- Regression from 0.7.2:
DataLoader(multiprocessing_context="spawn")crashed while picklingworker_init_fn. The default PDEATHSIGworker_init_fn(added in 0.7.2 for worker reaping) was a closure, and spawn must pickleworker_init_fnto hand it to the fresh worker process — closures are not picklable. Only the fork path (the default) had been tested, so this shipped unnoticed. It is now a module-level function wrapped infunctools.partial, which pickles cleanly;spawnworks again with worker reaping intact.
0.7.4 — 2026-07-08¶
Changed¶
- Revert the per-component
config:-block optionality added in 0.7.3:net, datasets, and schedulers require theirconfig:block again (as before 0.7.3). Makingconfigoptional only in some builders was inconsistent; making it optional everywhere would spread that leniency, and requiring it literally everywhere is impossible (no-arg leaf transforms such asTo01legitimately omitconfig). Section-level omission (transform/metrics/scheduler/keep_best_on) is unchanged.
0.7.3 — 2026-07-08¶
Changed¶
- CLI output is now uniformly English and single-colour. A handful of runtime
messages (and raised errors) were Russian and/or highlighted in yellow, which
looked out of place in the public package; they now read in English in the
ambient colour like the rest of the
-->log. - Warnings no longer corrupt progress bars. Library warnings are collected
instead of printed inline; a short summary (
--> N warning(s) since last reportwith per-message counts) is emitted before each validation and once at the end. Deprecation/Future noise is dropped outright. The c10dbarrier(): using the device under current contextwarning is silenced at the source by passingdevice_idtoinit_process_group.
Fixed¶
- Config parameters with sensible defaults are now optional — omitting them no
longer crashes.
transform(augment/preprocess),metrics, andschedulermay be omitted entirely (→ ToTensorV2-only, no metrics, constant LR);keep_best_onis optional (→ save every validation); and the per-objectconfig:block is optional fornet, datasets, losses, and schedulers (constructed with no extra args). Lossweightdefaults to1.0. A new pytest suite (tests/,pip install -e .[test]) covers these paths.
0.7.2 — 2026-07-08¶
Fixed¶
- No more orphaned DataLoader / DDP processes after a crash,
kill, or failed restart. DataLoader workers and the DDP rank processes now setPR_SET_PDEATHSIG(Linux) via a defaultworker_init_fn/ at process-group init, so they are SIGKILL'd the instant their parent dies — for any reason, including theos._exitfast-path from 0.7.1 and an externalkill -9. Previously a rank that died uncleanly left its workers running, holding/dev/shmand host RAM; a fresh run's workers then couldn't get shared memory and hung at the first batch (GPUs idle).echelon3-train/-finetunenow also catchKeyboardInterrupt(Ctrl-C goes through the teardown path) and call a newTrainer.close()on exit to shut DataLoader workers down promptly — relevant withpersistent_workers: true. The launcher additionally warns on CPU over-subscription (ranks × num_workers > cores) and onpersistent_workersunder DDP.
0.7.1 — 2026-07-08¶
Fixed¶
- DDP no longer hangs silently when a rank dies (typically an OOM). Under the
built-in launcher, a rank that OOM'd could wedge in
destroy_process_group()(NCCL teardown blocks on an in-flight collective) and never exit, soelastic_launchnever saw a failure and the peers blocked on the next collective — a silent hang up to the process-group timeout. On the error path a DDP rank now prints its traceback to stderr and hard-exits (os._exit) instead of relying on a clean shutdown that can block, so the launcher tears the group down immediately.max_restarts=0(fail-fast instead of silently retrying an OOM into a desynced group);ChildFailedErroris surfaced with an OOM hint;TORCH_NCCL_ASYNC_ERROR_HANDLING/TORCH_NCCL_DESYNC_DEBUGare on so the NCCL watchdog aborts (and reports the stuck rank) rather than waiting; the process-group timeout is configurable viaECHELON3_DDP_TIMEOUT_MIN. - The launcher prints the per-node DataLoader prefetch total
(
ranks × num_workers × prefetch_factor) and warns when it is large — these are per-rank and multiply under DDP, the common cause of the OOM above.
Changed¶
torch.compileis no longer marked experimental — validated on single-GPU and 4×H200 DDP including production image-in-image runs. Thecompile/compile_modeknobs are unchanged.
0.7.0 — 2026-07-08¶
Added¶
torch.compilesupport (experimental, opt-in).trainer.config.compile: true(with optionalcompile_mode) compiles the network — kernel fusion to cut launch overhead, the lever for small nets that under-use a big GPU where bf16 does nothing (launch-bound, not compute-bound). Compiled before the DDP wrapper;ddp.unwrap()and checkpoint save/load now also strip torch.compile's_orig_mod.prefix, so checkpoints stay interchangeable with uncompiled runs. Off by default; verified single-GPU and on 4×H200 DDP (trains + checkpoints round-trip). The actual speedup and any shape-driven recompiles are model-dependent (seeguide/ddp.md).
Changed¶
PairTrainercalls the network positionally —net(base, query, True)instead ofnet(base, query, return_features=True). Pair nets name the third argument differently (return_features,return_intermediates, …); passing it by position keeps the trainer agnostic to the name.
0.6.0 — 2026-07-08¶
Added¶
Trainer.compute_losses(source, labels, net=None)— an extension seam for the forward pass and loss routing. It runs inside the trainer's autocast and returns(predictions, {name: (loss, weight)}); both training and validation call it, so a subclass customizes what the network is fed and how losses map to its outputs without ever re-implementing the precision / scaler / closure / DDP machinery, which stays in the base. Behaviour is unchanged — the defaultcompute_lossesis the previous single-input forward.echelon3.trainers.pair.PairTrainer— a trainer for two-image ("pair" / image-in-image) inputs. Consumes((base, query), gt)batches (viapair_collate_fn), callsnet(base, query, return_features)and delegates loss routing topair_losses(heatmap, features, labels)(default: every loss on the heatmap). A domain-specific image-in-image trainer becomes a natural subclass overriding onlypair_losses— seeguide/extending.md.
0.5.2 — 2026-07-08¶
Fixed¶
GrayscaleCLAHEis now picklable, so it survives DataLoader workers started with thespawnstart method (and spawn-only platforms). It stored a barecv2.CLAHEC++ handle, which cannot be pickled — any run whose data workers pickle the dataset died withTypeError: cannot pickle 'cv2.CLAHE' object. The handle is now dropped on pickle and rebuilt from the storedclip/gridparams on unpickle. Under the defaultforkstart method the transform is inherited and never pickled, so single-GPU runs and the example smokes never triggered it — reproduced and fixed under aspawnDataLoader on 4×H200.
0.5.1 — 2026-07-08¶
Fixed¶
- README (the PyPI project description) rewritten for 0.5.0. It still
documented the removed
DataParallelfallback and presentedtorchrunas the only multi-GPU path. Now it shows the built-ingpus=[...]launcher (notorchrunneeded), the bf16-by-default mixed precision, the full CLI set, and all three example smokes. Docs-only release — no code changes.
0.5.0 — 2026-07-08¶
Multi-GPU and performance release. Breaking: DataParallel is gone and mixed precision defaults to bf16.
Changed¶
- Multi-GPU is built-in DDP, launched from the CLI — no
torchrunneeded. Passgpus=[0,1,2,3](a root config key; default = all visible GPUs on the node) and echelon3 spawns one DDP worker per GPU via PyTorch'selastic_launch, wiring upRANK/LOCAL_RANK/WORLD_SIZE/MASTER_*itself.torchrunand multi-node runs still work unchanged through the environment-variable path. Applies toechelon3-trainandechelon3-finetune. - Mixed precision (AMP) on by default. Training,
evaluateandrunautocast in bf16 on capable GPUs (fp32 on CPU / unsupported GPUs). Settrainer.config.precision: fp32to restore full fp32.precision: fp16uses aGradScaler; with closure optimizers (SAM/LBFGS, which double-backward) it falls back to bf16. TF32 matmul andcudnn.benchmarkare on by default (trainer.config.tf32,trainer.config.cudnn_benchmark). - Checkpoints save the unwrapped
state_dict(nomodule.prefix); oldermodule.-prefixed checkpoints still load (prefix stripped automatically).
Removed¶
- DataParallel. Multiple GPUs always run as DDP; a single process drives one
GPU.
device_idsno longer selects multiple GPUs — usegpus. Therun/evaluate/exportCLIs load checkpoints directly instead of wrapping the network inDataParallel.
0.4.1 — 2026-07-07¶
Fixed¶
IoU.compute(segmentation metric) returnednanwheneverignore_indexwas unset (the default):use_idx[None] = Falseadded an axis and wiped the whole keep-mask, sonanmeanran over an empty array. Now the mask is only touched when there are classes to ignore. Verified end to end — aSegmentersmoke on synthetic masks moves mIoU from ~0.85 to ~0.96.
Added¶
examples/segmentation/: a self-contained semantic-segmentation smoke (synthetic image/mask generator, a tiny dependency-free backbone, aSegmenter- cross-entropy +
IoUconfig) mirroringexamples/detector/.
0.4.0 — 2026-07-07¶
Fixed¶
- Detection pipeline now trains end to end (CenterNet-style
HeatmapDetector, YOLO-formatDetectionDataset). Several defects along the path were fixed: Trainer.set_to_devicenow stacks the per-sample image tensors thatVariableDataLoaderproduces into one(N, C, H, W)batch, and leaves the variable-length box lists as Python lists instead of calling.to()on them (image-in-image(base, query)pairs are unaffected).- The train/validate steps no longer assume
labelsis a tensor (they readlabels.shapeonly when it is), so list-valued detection targets work. metrics.base.Metricgained a no-op.to()so custom metrics (mAP, EER, AUC, IoU, …) interoperate with the trainer's uniformmetric.to(device).HeatmapBasedDetectionLossuses the penalty-reduced CornerNet/CenterNet focal loss on the sigmoid heatmaps the head emits, instead ofsigmoid_focal_loss(which double-applied a sigmoid and crushed the gradient, so heatmap peaks never formed).DecodeHeatmaps.decodekeeps YOLO/Albumentations boxes as normalized floats (they were cast toLongTensor, truncating every coordinate to 0).- Class labels are coerced to integer tensors in both the encoder and the mAP metric (Albumentations round-trips them through float arrays).
Added¶
detectionextra (faster-coco-eval) — themAPmetric needs a COCO backend;mAPtakes an optionalbackendargument (defaultfaster_coco_eval).examples/detector/: a self-contained CenterNet-like detector smoke — synthetic YOLO dataset generator, a tiny dependency-free heatmap backbone, and a config that trains, validates (mAP rises above zero) and checkpoints on CPU or GPU.
0.3.1 — 2026-07-07¶
Added¶
- Full MkDocs (Material) documentation site published to GitHub Pages at
https://veryviolet.github.io/echelon3/: getting-started, concepts
(how-it-works, run anatomy), guides (extending, DDP, ONNX export) and reference
(config schema, built-in components, CLI).
docsextra + adocsworkflow thatmkdocs gh-deploys on everyv*tag. [project.urls]with aDocumentationlink, so PyPI shows it.
0.3.0 — 2026-07-05¶
Added¶
echelon3-finetuneCLI: warm-start from a checkpoint (init_from), freezing by regex patterns (finetune.freeze_patterns), head-only training and per-layer parameter groups (finetune.param_groups). With none of those blocks present it behaves exactly likeechelon3-train.
0.2.0 — 2026-07-05¶
Added¶
- ONNX export:
ModelExporterbase +OnnxExporter(preprocess → net → postprocess wrapped into a single graph),echelon3-exportCLI, export section in the smoke example, CI step exporting the smoke model and verifying it with onnxruntime. - All CLIs insert the current working directory into
sys.path, so zoo repositories can reference their local packages from configs (module: my_zoo.nets.foo) when running from the repo root.
Fixed¶
create_exportersinstantiatedtorch.nn.Identityincorrectly when no preprocess/postprocess is configured.
0.1.0 — 2026-07-05¶
First public release. Core of the framework extracted from the internal echelon2 codebase and cleaned up:
Added¶
- Config-driven component factory (
echelon3.creator): every component is amodule/type/configYAML triple resolved by dynamic import, with a file-path fallback for project-local extensions. - Trainers: baseline
Trainer(DataParallel + DDP via torchrun, global batch size semantics, multi-metric keep-best checkpointing, multiple named test loaders),MultiHeadTrainer. - Generic datasets (folder-hive / CSV classification, segmentation pairs, detection, multi-head binary masks), balanced / classwise / multipart dataloaders, albumentations augment + torch preprocess pipeline.
- Generic losses and metrics for classification, segmentation (OHEM, boundary, clDice, Lovasz, multi-head BCE/IoU) and heatmap detection.
- Nets:
ClassifierNet/Segmentercomposition wrappers, timm backbone adapter, DDRNet, SegFormer-style heads, DASPP/PSP/FPN necks, CDC layers. - Checkpoint manager, tensorboard mlops logger, weight loaders (full/partial),
CLI entry points
echelon3-train/echelon3-evaluate/echelon3-run(Hydra,--config-dir). - Smoke example: synthetic dataset generator + minimal classifier config.
Changed vs internal predecessor¶
- All hardcoded
'cuda'calls removed — CPU training works. - Vendored copies of timm and mmsegmentation dropped; external
timmis used, the only needed mmseg op (resize) lives inechelon3.nets.ops. - Heavy/optional dependencies (mosaicml SAM, segmentation-models-pytorch)
import lazily and install via extras (
echelon3[sam],echelon3[smp]).