Troubleshooting & FAQ¶
Common messages, operational states, and how to resolve them.
In short
- Each section is headed by the message or symptom you see; find yours, read the first paragraph, and open a Details block only when you need the mechanism.
- A failing baseline verdict (
no_signal,worse_than_constant) is a signal, not a crash: the run still finishes. - Config refusals (task vs label, training policy vs split,
execution.device="cuda") stop the run before any work, and each message names the field to change. - A stale artefact is refused at load: re-fit and re-persist with the ml4 you run.
- When a run misbehaves, read its
events.ndjsonandmanifest.jsonfirst.
The baseline gate verdict¶
Every supervised run ends with a baseline honesty gate that asks: did the model
actually beat the best predictor that uses none of the features — a constant
(any observed class label / mean / median / class prior) or a chance-level draw
from the target's own distribution — by the configured margin? Every candidate is
scored and the strongest one becomes the floor; the winner is reported as
strategy, and the verdict lands on model.baseline and in measures.json under
baseline_gate — which is its single published home, carrying the whole
BaselineReport verbatim:
verdict |
Meaning | Passed? |
|---|---|---|
ok |
Beats the baseline by more than the margin, and a paired bootstrap of the graded rows confirms the win. | yes |
suspected_leakage |
Removes almost all of the baseline's error — implausibly good. | yes, but investigate |
no_signal |
Beats the baseline by less than the margin, or by a margin a paired bootstrap of the graded rows cannot confirm (then low_confidence). |
no |
worse_than_constant |
Scores below the baseline. | no |
skipped |
The gate could not run (no scorable baseline), or the baseline is already perfect so there is no error left to remove. | — |
A failing verdict is a signal, not a crash — the run still finishes. Usual causes and fixes:
-
any verdict with
low_confidence: true—baseline_gate.low_confidence_reasonsays why."few_rows": fewer than 30 rows were graded (baseline_gate.n_rows), so the gate is measuring the holdout as much as the model."unconfirmed_win": the point estimate cleared the margin but the paired bootstrap's lower bound (baseline_gate.improvement_lower) did not clear zero — a win too small to tell from noise on these rows."few_rows"wins when both apply. Either way the verdict is published but never acted on:baseline.fail_on_no_signalwill not abort alow_confidencerun. A 200-row dataset on the defaults leaves the gate 16 rows; see Training workflow. -
no_signal— no learnable signal in the features for this target, a noisy target, or a task / metric mismatch. Add more informative features, or pick a metric that matches the problem (see Imbalanced data). worse_than_constant— often a target / metric mismatch. Re-checkml_taskandeval_metric.suspected_leakage— a feature encodes the target (a post-outcome column, an id that maps 1:1 to the label). Audit the feature list.skipped— there was nothing to compare against, so read it as "no verdict", not "cleared". A constant target always lands here: a correlation metric can score none of the candidates, and an error metric's constant floor is already exact. So does a metric the fitting set cannot support (AUC with one class).
Details: which rows the verdict rests on — and whether the strongest model's score is a measurement
The gate is fitted alongside calibration and the decision threshold, so it grades the
fitting set: either the bagged out-of-fold pass over every training row
(baseline_gate beside holdout.basis == "oof") or the reserved holdout
("holdout"). It is not one of the train / valid / test splits, and any surface
that calls it "validation" is describing rows no such split contains.
strongest_basis qualifies the strongest model's own number in the same block, and it is the
field to read before quoting it:
strongest_basis |
What it means |
|---|---|
untuned |
No decision parameter was chosen from these rows. |
out_of_fold |
The operating point was re-selected fold-wise before scoring — an honest estimate. |
in_sample |
Too few rows to re-select, so the score is an upper bound, not a measurement. |
The gate is weak on pearson / spearman
A constant has zero variance, so a correlation metric can only be floored by the
shuffled draw — and one shuffle is as noisy an estimate of zero correlation as a
zero-skill model is. Measured over 10 seeds, the gate catches a pure-noise
strongest model about half the time (a strong model was graded ok 10/10). An ok on
these two metrics is weak evidence, not a clearance.
Is there any signal at all?
Before a full fit, run the Instant Signal Check — a zero-shot probe that answers "is there learnable signal here?" in seconds.
A single model shipped, but the leaderboard shows the ensemble on top¶
Expected, and deliberate. The leaderboard is the training record — it keeps every model
AutoGluon built, including the weighted ensemble, with the score that won it the top row.
Which model ships is a separate decision, taken after the fit by the
ensemble advantage margin: the blend has to
beat the best standalone model by more than engine.ensemble_advantage_margin (default
0.005, i.e. 0.5% of that model's score) or the simpler model is deployed instead.
To confirm that is what happened, and see the arithmetic:
- the run log carries
ENSEMBLE_GATE_DOWNGRADEwith both scores and the relative gain; ml4_ensemble_gate.json, inside the predictor directory, carries the same verdict with areason;- the Ensemble tab says so directly, and the composition sidecar's
ensemble_gateblock holds the numbers.
The most common trigger is not a close race at all: AutoGluon frequently puts all the
blend weight on one base model and still ships a WeightedEnsemble_L2 wrapper around it.
Its score is identical to that member's, so the relative gain is exactly 0.0 and the
wrapper is removed for free.
Set engine.ensemble_advantage_margin = 0.0 to opt out and always deploy whatever
AutoGluon scored highest. Raise it if your run-to-run score spread is wider than 0.5% —
measure it rather than guessing, by refitting at several execution.seed values.
"engine.training_policy=… cannot run on engine.split_policy.strategy='temporal_holdout'"¶
The training policy and the split must fit together, and ml4 refuses the three pairings that cannot — when the config is built, again when the fit resolves its shape, and at launch in the product, which answers 400 with the same message:
| The message says | Change |
|---|---|
engine.training_policy='…' cannot run on engine.split_policy.strategy='temporal_holdout' |
Set engine.training_policy="chronological_train_validation": a temporal run trains one model and validates it on its latest rows. |
engine.training_policy='chronological_train_validation' needs engine.split_policy.strategy='temporal_holdout' |
Attach a temporal split (a time_column), or use train_validation / cross_validation. |
roster.regulated=True trains one transparent model, so it cannot use engine.training_policy='cross_validation' |
Set train_validation (or chronological_train_validation on a temporal split). |
A template that uses the chronological policy may carry the default random split until a launch attaches the dataset's temporal split; only the attached split is checked against it. See Data splitting & validation.
"MaxMind GeoLite2 databases not found"¶
IP (ip_domain) features need the MaxMind GeoLite2 databases, which aren't
bundled. This is non-fatal: geo / ASN features degrade to null with a
warning while the stdlib classification flags still emit. To enable geo lookups,
download the databases and set MAXMIND_DB_DIR (the geoip2 reader ships with
ml4); describe_domains() then reports maxmind as available. To skip IP
features entirely, leave ip_domain=[]. Full steps:
Geography & reference data.
"N engineered column(s) shipped degraded"¶
A run-level warning, not an error: the pipeline produced those columns, but not as specified. Read the cause, because the two kinds need different responses.
A step that could not run at all leaves an inert column — a missing GeoIP
database, an unreachable embedding backend, usaddress absent or a non-US
country, a degenerate Gamma-Poisson vocabulary. AutoGluon drops constant
columns, so the model was fitted without them rather than on dead width. Fix
the cause and re-fit, or drop those columns from the config so the recipe stops
promising features this environment cannot produce.
A step that ran in a reduced mode leaves the column in place, worth less than
its name implies. The one to know is TargetEncode: target encoding is not
out-of-fold — the slice had too few rows for a four-way inner split, or a class
too rare to stratify, so that encoding is self-encoded and the model will
over-trust it. Give the run more rows per fold.
The wording "group column '…' is not in the fold frame" belongs to this family, but no config can cause it: the isolation key is carried to the encoders out of band (see the split columns), so it reaches them even though the column is correctly absent from the model matrix. If you do see it, it is an ml4 defect worth reporting, not a setting to change.
Read the notice before you conclude the data is weak: a run fitted without its embeddings scores worse for a reason that has nothing to do with the dataset. See When a transformer degrades.
"Target 'y' has N classes but the task is binary_classification"¶
The declared task and the label disagree, and preflight_check refuses the run
before any work is done — at launch it comes back as a 400, so the wizard says
it rather than a run failing minutes later.
Two mismatches are possible and both are refused: more than two classes under
binary_classification, and exactly two classes under multiclass_classification.
Details: why ml4 checks the task against the label
ml_task is declared, never inferred. It becomes AutoGluon's problem_type
verbatim, and an explicit problem_type makes AutoGluon skip both its own
validate_label and infer_problem_type — so nothing downstream compares
the declaration back to the data. Without the check:
| Message | What would happen without the check |
|---|---|
has N classes … but the task is binary_classification |
The run would carve its holdout, plan the recipe and pay for the whole feature-engineering and feature-selection pass, then die inside AutoGluon's label cleaner on y does not contain exactly 2 unique values — a bare tuple-args AssertionError with no hint of what was wrong. |
has exactly 2 classes … but the task is multiclass_classification |
It would train, as the identical binary model (AutoGluon's LabelCleanerMulticlassToBinary), and quietly change everything around it: the served rule would be the per-class one-vs-rest sweep, whose two thresholds combine into an operating point neither sweep tuned, positive_class would be None, and measures.json would lack its threshold and brier blocks. |
Neither is a data defect — the fix is the task dropdown, and the message names
the value to switch to. The usual way in is a clone or a rerun carrying a stored
ml_task onto a different target: the wizard prefers the saved declaration over
the fresh guess from the target's EDA stats.
If the target genuinely has more than two levels and you want a two-class model, collapse it in the dataset (or with a derived column) first — "binary on a 3-class column" is a config error, not a different model.
"The reserved holdout is the only place N class(es) appear"¶
A hard error, and deliberately so. On the reserved-holdout path (single-model or
refit_full runs) every post-fit layer — probability calibration, the decision
thresholds, the baseline gate — is fitted on that holdout. A class that lands
entirely there is one the model never trained on, so those layers would be tuned
against rows the model cannot classify, and would ship that way.
Raised only where the split structurally cannot place the class on both sides: a temporal strategy with a class that emerges late (a new product tier, a new fraud pattern, a re-coded status), or a grouped one with a class confined to a few whole groups. An IID carve repairs itself instead, so it never raises here.
Merge or drop the class, or switch to a strategy that can place it on both sides. The message names the classes.
The outer test is not gated this way — it feeds no layer, so its unseen-class
rows are disclosed instead: test.unscored_rows in measures.json, and a banner
on the Summary tab's per-class table explaining why the support does not sum to
the held-out row count.
"N class(es) have fewer than engine.label_count_threshold=K rows and were excluded"¶
A run warning at the preflight stage, and the result of a setting you chose.
engine.label_count_threshold (multiclass only) is the fewest rows a class needs,
counted over the whole training frame, to be modelled at all. A class below it is
excluded — together with its rows — before the holdout is carved and the recipe is
planned, so the model never predicts it. Rows of an excluded class that the model is
later asked to score are counted under test.unscored_rows in measures.json, the
same treatment a never-seen class gets. The default of 1 keeps every class.
If the exclusion leaves fewer than three classes the run is refused instead: lower
the threshold, or declare binary_classification if two classes is what you mean to
model. Raising the threshold never rescues a rare class — it removes more of them —
and a class with a handful of rows has no honest generalisation estimate to report
either way; the remedy for one you need is more rows, or a coarser level.
Why ml4 settles the class space, not AutoGluon¶
ml4 applies one rare-class rule for every metric and tells AutoGluon to delete nothing
(label_count_threshold=1 in its learner kwargs).
Details: AutoGluon's own rare-class rule
AutoGluon's own rule depends on the metric and on the fold geometry: on a bagged fit it
keeps a class only with two training rows, and it copies a rare class up to that floor
only for a probability metric (logloss). Left to it, a hard-label metric (accuracy,
f1_macro, …) would delete a class with a single training row after the recipe had
already pinned one encoder slot per class — every model would then refuse to fit and the
run would train nothing.
A class left with a single training row:
| Run | What happens |
|---|---|
| Bagged, no groups (the default) | The row is copied once, so every fold can see the class. The row and its copy land in different folds, so each one's out-of-fold prediction comes from a model trained on its twin. The run warns (Single-row classes were copied for bagging) and leaves those rows out of the rows calibration, the decision threshold and the baseline gate are fitted on; holdout.copied_classes_excluded in measures.json names the classes. The stacker and the weighted ensemble still learn from them, so those classes' validation scores are optimistic. |
| Bagged over a grouped split | Refused before anything is fitted: a copy would put one group on both sides of a fold. The message names the class — collect more rows, merge it into a coarser class, drop its rows, or use a train/validation policy. |
| Not bagged (train/validation, regulated, temporal) | Kept as it is: one row is AutoGluon's floor there. |
"AutoGluon dropped N class(es) with too few examples to train on"¶
A tripwire. With the rule above AutoGluon deletes no class, so this warning at the
ag_fit_done stage — like the model error "AutoGluon is training on N classes but
the feature recipe pinned M", which says outright that it should be unreachable —
means a class slipped through: please report it with the run's class counts.
If it does fire, the run still says so honestly. AutoGluon pads a deleted class back into every probability frame as an identically-zero column, so on its own it would look like a class the model scored and always got wrong; instead:
| Where | What you see |
|---|---|
| Run events / log | A warning event naming the classes, at the ag_fit_done stage. |
measures.json |
untrained_classes at the top level (present even when the run has no outer test, and carried in the deploy bundle). |
measures.json → test |
untrained_classes again, and those rows counted under unscored_rows — the same treatment a never-seen class gets. Dropping the column is arithmetically free: it is zero, and the remaining columns still sum to 1. |
A model you asked for is missing from the leaderboard¶
measures.json lists three different things about the roster, and reading only
the first one is what makes a failed model look like one that was never
requested:
| Key | Meaning |
|---|---|
requested_models |
What the run asked for — the resolved roster, in ml4's spec vocabulary (GBM:gbdt, CAT:symmetric, …), before anything was fitted. Absent on a bundle reloaded without it, never an empty list. |
models |
The survivors: predictor.model_names(), what actually trained and can be served. |
model_failures |
What AutoGluon tried and could not fit, one entry per failure with model / exc_type / model_type / total_time. Absent when nothing failed. |
Details: the two lists are in different vocabularies — match them by eye, not by set difference
requested_models holds ml4 specs; models and model_failures[].model hold AutoGluon's
names for the fitted objects, which add the pipeline shape. A lone GBM:gbdt is
LightGBM_BAG_L1. The variant is spelled out only when the roster names more than one
variant of that family, in which case every one of them takes a suffix — a tuned_variants
run reads LightGBM_GBDT_BAG_L1 / LightGBM_DART_BAG_L1 / LightGBM_RF_BAG_L1
(ag_args.name_suffix, so the board says which variant rather than AG's positional
LightGBM_2). One requested spec can also produce several rows — one per stack level — and
the WeightedEnsemble_L* combiner is a row no spec asked for.
So a spec with no corresponding row in models either failed (look for it in
model_failures, whose exc_type usually names the cause — a memory ceiling, a
missing optional dependency, a degenerate fold) or ran out of the run's
engine.time_limit before its turn. Neither case is silent, but neither
is fatal either: AutoGluon ships the models that did fit.
"execution.device='cuda' was requested but … cannot train on a GPU"¶
execution.device = "cuda" is an assertion, not a preference: when a booster library
cannot actually take the device, the run aborts naming that library, why it cannot, and
what to install.
execution.device='cuda' was requested but lightgbm cannot train on a GPU in this
environment: this LightGBM build has neither the OpenCL nor the CUDA GPU learner.
The default LightGBM wheel is CPU-only. Build the OpenCL backend, e.g. …
Set execution.device='auto' to fall back to CPU per library instead, or 'cpu' to
disable the GPU path entirely.
This is deliberate — a four-hour run that silently drops to CPU is a worse failure mode than a clean abort at second zero. Two ways out, depending on what you meant:
- you want the GPU if there is one → set
execution.device = "auto", which falls back to CPU and logs the decision once; - you meant it → install the GPU-capable build the message names.
"Incoherent config: execution.device='cuda' with hpo.provider='ml4_optuna' and a CatBoost variant"¶
A separate refusal, and not about your hardware. ml4's hyperparameter search scores its
CatBoost trials on CPU-pinned fits, so a winning configuration can contain values that
cannot be trained on a GPU at all (MVS bootstrap, which GPU CatBoost rejects) or that
GPU CatBoost silently ignores (rsm, which would make the reported winner a lie about
the fitted model). Because AutoGluon's fit-level num_gpus is a total for the run and
ml4 emits no per-family override, there is no way to send just the tuned CatBoost to the
CPU. Drop the CAT:* variants from roster.models, choose another hpo.provider, or use
execution.device = "auto" and accept a CPU fit for this roster.
See GPU acceleration for how availability is established per library and why the decision is all-or-nothing across the roster.
"The active balancing methods require numeric transformed attributes"¶
SMOTE, ADASYN, Borderline/K-means/SVM SMOTE, NearMiss and Cluster Centroids compute distances in numeric space. Choose SMOTENC/random sampling for mixed data, or enable Use numeric attributes only to remove remaining non-numeric columns after fold-local encoding. Validation and inference then receive the same projection. See Imbalanced data & outliers.
Text embeddings are skipped¶
If embedding features don't appear, the local model's dependency is missing. Probe it before a run:
from parleml_ml4.embeddings import TextEmbeddingConfig, source_available
ok, reason = source_available(TextEmbeddingConfig())
print(ok, reason)
With text_embedding_mode="augment" the TF-IDF path still runs; the supervised
embedding encoder emits inert (zero) columns rather than failing, and the run
reports the degradation. A provider source fails the run instead: check that the
connection exists and that a credentials resolver is installed
(parleml_ml4.embeddings.set_provider_resolver). See Text & embeddings.
"Scoring stopped: the provider changed the model behind the same name"¶
The provider returns different vectors for the fingerprint sentences than when
the model was trained (EmbeddingModelChanged). Every prediction on those
vectors would be silently wrong, so ml4 refuses. Retrain the model on the
provider's current model, or point the connection at a deployment that still
serves the original one.
"Scoring stopped: … at revision …; this machine has revision(s) …"¶
The local text-embedding model's pinned revision is not on this machine and
cannot be downloaded (LocalModelMismatch) — typically an offline server whose
cache holds another snapshot. Download the pinned revision into the model cache
(HF_HOME), or retrain.
"KNN imputation downgraded to median"¶
numeric_imputation="knn" is capped at a fixed 50 000 training rows. Above
that the run fits median instead and says so in the Notifications tab — the
substitution is real, so a config left on knn does not describe the model.
The cap is not a setting, and nothing raises it. Your options are to set
numeric_imputation="median" so the configuration states what actually runs, or
to train on fewer rows if the multivariate estimate is worth the cost.
Details: why the cap exists, and what it is not
KNNImputer keeps the whole training matrix inside the fitted object and rescans it
for every scored row, so fit, serve and artefact cost all grow superlinearly
(measured on a 10-column frame: 0.5 s at 500 rows, 21 s at 8,000, 97 s at 20,000).
Two things this cap is not:
- A privacy control. A
knnartefact embeds raw training rows at every size (~19 MB at 50,000×50), so "the scorer must not carry training data" means "don't chooseknn", not "stay under the cap". - Relevant to tree models.
numeric_imputationis read only by the glass-box linear path (regulated_kindofglm/scorecard/rulefit); AutoGluon's boosters fill NaN natively and never consult it, so no downgrade warning is raised there — the message below is raised instead.
"The imputation section is set … but this run's roster does not read it"¶
The whole imputation section — numeric_imputation, cat_imputation, the ±inf
pair, and the per-column custom_num_imputations / custom_cat_imputations
overrides you set on the Columns tab — rides the glass-box variants alone:
regulated_kind of glm, scorecard or rulefit. Everywhere else it is inert,
and that includes a regulated ebm run, which bins features natively.
The settings are not substituted, mangled or half-applied; they simply do not run.
The gradient boosters route NaN natively (a learned missing direction per split),
and AutoGluon's own preprocessing imputes at median for the linear and neural
families. So nothing about the model is wrong — but the run's stored config records
an instruction that never executed, which is why the run says so.
Two ways to resolve it: choose one of the three glass-box kinds if you want those imputers, or clear the section so the configuration describes what actually ran.
"engine.sample_weight names a weight column that is ALSO declared as a model feature"¶
A weight column has to be in the training frame but not a feature, and that is a role
no other column has. This error means the column landed in data.cat_features or
data.num_features as well — usually because whatever built the config classified every
column that was not the target, a key or explicitly ignored as a predictor.
Remove it from the feature lists and leave engine.sample_weight pointing at it.
Details: why the double declaration is refused
There is no configuration in which the double declaration is what you meant: AutoGluon lifts the weight column out of the frame before any feature engineering runs, so as a "feature" the column burns a feature-selection slot, is mined into interaction pairs that can only materialise as all-NaN, and is planned into the GLOBAL recipe — which then indexes a column that is no longer there.
The mirror-image mistake produces a different error. Marking the column ignored so that it will not be modelled generally drops it from the frame, and AutoGluon then raises a missing-column error for the weight column it was told to extract. Neither "feature" nor "ignored" is right; see Passing a weight column from a host application for the four conditions a caller has to satisfy.
Two related refusals come from the same translator, both up front rather than deep inside a
fit: engine.sample_weight="auto_weight" (AutoGluon accepts the value, nulls it, and trains
completely unweighted while logging that below ml4's default verbosity), and
engine.weight_evaluation=True with no weighting set (a flag AutoGluon reads only when a
weight vector exists).
Anomaly detection¶
ImportErroron a deep detector —autoencoder/vae/difneedpyod[torch];suodneedspyod[suod]. Install the matching extra or choose a classical detector (ecod,iforest,hbos, …).- Low-quality verdict (
result.quality["verdict"] == "low"inautomode) — the engine isn't confident. Provide more data or constrain the detectors. - Anomaly ratio slightly off the configured
contamination— both modes cut atcontamination, but tied scores can move the realised share a little; the run reports the share it achieved. See Unsupervised. - A column vanished from the fitted model — a categorical in which ≥ 95 % of
rows hold a distinct value is treated as a row identifier and dropped at fit
(logged as a warning, listed in
preprocessor.dropped_constant_columns_). Keeping one makes neighbour detectors flag every served row. If it is really a timestamp, parse it to a datetime dtype so it is encoded as a quantity instead. See Unsupervised.
Recommendation & Learning-to-Rank¶
- A class was dropped — its One-vs-Rest sub-model failed the minimum
validation ROC gate (
min_submodel_roc). The run falls back to aunifiedsub-model, or excludes the class (recorded inresult.excluded_classeswith a reason). Lower the gate or improve features for that class. - Degenerate NDCG of 1.0 in tuning — ml4's Optuna maximises full-catalogue
NDCG@k from its own ranking metrics, not the library's sampled-group NDCG, so
this shouldn't surface; if a run looks too good, inspect
result.best_valid_ndcg. See Ranking.
Forecasting¶
- A series doesn't beat SeasonalNaive — its CV winner does not clear a naive
seasonal baseline (CV MASE > 1.0, or SeasonalNaive itself won the race). It is
surfaced on
result.weak_seriesas an honest warning — never silently swapped or failed. Try a differentbackend(e.g.treeto exploit exogenous drivers), revisit the horizon / aggregation grain, or accept the baseline. - Foundation / AG-TS backend unavailable — install both
forecastingandforecasting-autogluonextras. The pinned AutoGluon-TimeSeries 1.6.3 supports Python 3.11–3.13 within ml4. A run that never selects those backends doesn't import them; an explicit request fails fast with apip installhint. See Forecasting.
"Why does the model serve raw probabilities?"¶
Five different things produce that state, and measures.json's
calibration.outcome says which. Answer from the outcome, not from the absence
of a calibration map — the whole reason the outcome is recorded is that
"no calibrator shipped" was four different statements about a model.
outcome |
What happened | What to do |
|---|---|---|
fitted |
A calibrator won its bake-off; calibrated probabilities serve. | — |
declined |
It was measured and lost, or won by less than calibration.calibration_advantage_margin. |
Nothing is wrong. Lower the margin only if you have a reason to prefer a marginal calibrator. |
not_measured |
The fitting set was too thin to bake off honestly (see n_rows, rarest_class_rows). |
More data, or a larger engine.validation_holdout_frac on a single-model run. |
disabled |
calibration.apply_at_predict_time is off. A choice, not a failure. |
Switch it on and refit — or, if the run kept a decision basis, re-decide the layer without retraining. |
refit_degenerate |
The bake-off won, and the full-set refit then produced no usable map. | Try platt or temperature, which are far steadier than isotonic on thin data. |
"Why does the Scores panel show no baseline?"¶
The constant-baseline check returned skipped, so there is no verdict to show —
and a skipped gate must not render as a real number. It skips when:
baseline.enable_constant_baselineis off (a choice, and the panel is quiet rather than empty);- the fitting set has too few rows or a single class for a skill-free predictor to be scored against it at all.
The verdict lives in measures.json under baseline_gate; verdict: "skipped"
is the state to look for. Do not read passed on a skipped gate — it is True,
which is the one field that looks like a usable filter and inverts the meaning.
"This stage says it was skipped"¶
A skipped stage is not a failure and not a stall. ml4 emits a skipped event
carrying a machine-readable reason, so an optional stage that deliberately did
not run says so rather than sitting at pending forever:
| Reason | Meaning |
|---|---|
disabled_by_config |
You switched it off. Hyperparameter search is off by default, so this is the common one. |
not_applicable_to_task |
The stage does not exist for this task — calibration on a regression run, intervals on a classification one. |
insufficient_data |
It tried and there was not enough data. Distinct from the two above, and the only one that suggests an action. |
budget_exhausted |
A size or time ceiling stopped it — e.g. a decision basis above deployment.decision_basis_max_cells. |
"The decision layer cannot be re-decided"¶
Calibration, the decision threshold and the baseline check can normally be refitted without retraining. When they cannot, the reason is one of:
| Reason | Meaning | Fix |
|---|---|---|
no_basis |
The run kept no decision_basis/ — deployment.persist_decision_basis was off, or the frame was above deployment.decision_basis_max_cells. |
Enable it (or raise the ceiling) and refit. |
no_provenance |
The pipeline was not assembled by an ml4 run, so there is no fitted revision to advance from. | — |
task_not_supported |
Regression. Its post-fit layer is the prediction interval, a different object with its own basis. | — |
strongest_mismatch |
The basis was captured from a differently trained model, so a layer refitted from it would serve an operating point derived from predictions this model never made. | Refit the model. |
custom_metric_unavailable |
The run was optimised for a user_defined_metric whose callable cannot be imported here. Refitting would silently grade the new operating point against the task default. |
Make user_defined_metric_ref importable in this environment. |
The last one is a refusal rather than a fallback on purpose: a refused refit is recoverable, and a refit against the wrong metric is not detectable.
"on-disk format v… but this ml4 build reads format v…"¶
A stored artefact predates the ml4 you are running. ml4 version-stamps every
artefact and rejects a stale one at load rather than shimming it, because the
alternative is a bare AttributeError in the middle of scoring — or, worse, a
model that loads and silently scores against different geometry. There is no
migration tool: re-fit and re-persist.
| Error text | Artefact | Bumped because |
|---|---|---|
… holds an unsupervised pipeline in format v7.0, but this ml4 reads v8.0 |
UnsupervisedPipeline joblib |
Format 8.0 scales the numeric track before it imputes and normalises the text sub-blocks per block, so every axis of a 7.0 artefact's geometry differs; 8.0 has no deepsvdd detector (see Unsupervised). |
pipeline at '…/forecaster' is on-disk format v6.0 … reads format v7.0 |
FittedForecastPipeline directory |
A 6.0 bundle carries no history_tail, which the serve path needs to run the fit's own future-frame checks on a caller-supplied X_future (see Forecasting). |
FeatureRecipe is stamped 'ml4-recipe/1', but this ml4 reads 'ml4-recipe/2' |
a serialised FeatureRecipe (FeatureRecipe.from_json) |
Version 2 transformers carry new fitted fields, and a calendar step names its calendar columns in params["columns"]. Re-plan the recipe and refit the run. |
Supervised pipeline bundle at '…/bundle' is on-disk format v3 … reads format v4 |
supervised FittedPipeline bundle directory |
The fold-local chain moved to AutoGluon's per-model feature-generator channel and the fitted transformers and baseline report carry new state (see Deployment). |
This run's metrics/measures.json was written by an older ml4 (measures_version=None; this build reads 1) |
a run's measures.json, read by the platform |
The file has no measures_version stamp, or an older one: its numbers (the Brier scale, the baseline block) changed meaning. The platform answers 409 for that run's Scores, leaderboard-reference and Decision-layer views — re-run the experiment to regenerate the file. |
An artefact loads only at the exact format version this build reads. A different stamp — minor versions included — or a missing one is refused before anything is unpickled: re-fit, or re-export with this ml4 version.
A re-fit is a new model, not a format conversion: re-check anything you tuned against the old artefact, such as an anomaly operating threshold.
"…no key is set: set ML4_PSEUDONYMISATION_KEY to the key this model was fitted with (fingerprint …)"¶
The model replaces identifier columns with keyed pseudonyms (HMAC-SHA256) and — by design — does
not store the key. Set ML4_PSEUDONYMISATION_KEY in the environment that fits, loads or serves
the model to the key it was fitted with; the message names that key's fingerprint, which you can
compare with parleml_ml4.contracts.pseudonymisation.key_fingerprint(<key>). "The pseudonymisation key in
reach (fingerprint …) is not the key this model was fitted with" means a different key is set.
ParleML Studio manages the key for you (in its vault, config/vault.json under the data root). There is
no way to recover a lost key: refit the model under a new one. See
The pseudonymisation key.
Cancelling a run¶
Runs that were given an outputs_dir are cooperatively cancellable: create a
file named CANCEL in that directory and the pipeline stops at the next
checkpoint. A run never clears the sentinel when it starts — a CANCEL written
before the run reaches its first checkpoint (a worker still importing, say) is a
request for that run, and its first check honours it. The run that honours it
removes it, so the next run in the same directory starts clean; a run that ends
any other way leaves a sentinel it never read in place.
Every such run also writes structured events.ndjson and a
manifest.json — read these first when a run behaves unexpectedly.
Details: the supervised stage stream
The supervised stages are preflight → holdout_reserved → fe_structure_decided → hyperparameter_search →
ag_fit_started → ag_fit_done → post_fit_layers → registered, each a stage_start /
stage_end pair, and an error raised from inside the fit ends the stage it happened in
failed, with the error. It does not cover recipe planning: a library caller runs
RecipePlanner().plan before the first stage, so a plan-time raise is in no stage (a run
wrapped in RunTelemetry.run records it on its run_end; see
the stage stream). A run whose process was
killed before it could close its log — out of memory, a hard kill, a crash in native
code — reads as ended, not as running: the platform, knowing its worker has exited,
reports the run and every stage still open as failed or interrupted. Read the
raised error, or the log, for the cause.
Resuming after a crash¶
Once AutoGluon has trained (ag_fit_done), the models are durable at path. If
the post-fit layers crash afterwards, the run is recoverable — resume_pipeline
rebuilds the deterministic post-fit layers from that boundary without
re-training the engine.
Installation issues¶
- A workload import fails — you likely need its extra,
e.g.
pip install "parleml-ml4[unsupervised]". Importingml4itself never fails for a missing optional backend. - GPU vs CPU PyTorch — from PyPI,
torchis the CPU build on Windows and the CUDA build on Linux. Install it from PyTorch's CPU or CUDA index first to choose. See GPU acceleration.
FAQ¶
Do I need MaxMind to use ml4? No — only for IP (ip_domain) features, and
even then the run degrades gracefully without it. Everything else works out of
the box.
Does my data leave the machine? No, unless you choose a provider as the text-embedding source: the embedded text columns are then sent to that provider at fit and at every scoring call. Local models and everything else run on-device.
How do I make models small and fast to score? Use presets.real_time() —
see Deployment & export and Inference.
Where is the trained model saved? Wherever you pass to save(...) / the
path argument; reload it with parleml_ml4.load_pipeline(path).