9.0 KiB
Model flavors
The flavor is how this runtime is told to deserialize a model. It is configuration, never
discovery: the value comes from the model document in the models collection and the code never
reads the artifact's MLmodel manifest to guess it.
{
"model_config": {
"transform_flavor": "sklearn",
"predict_flavor": "joblib",
"retention_minutes": 60,
"target": "SE"
}
}
predict_flavor drives load_predict_model (artifact prediction_model); transform_flavor drives
load_transform_model (artifact data_model). Both default to sklearn when the key is absent.
Accepted values
| Flavor | Read | Write (retrain) | How it loads |
|---|---|---|---|
sklearn |
yes | yes | mlflow.sklearn.load_model(<uri>) — the loader fetches what it needs |
pyfunc |
yes | yes | mlflow.pyfunc.load_model(<uri>); the wrapper path downloads the artifact instead and unwraps ._model_impl.python_model |
joblib |
yes | yes | downloads the same artifact and joblib.loads its <artifact>/model.pkl — no MLflow flavor loader involved |
Anything else raises ValueError with INVALID_FLAVOR_MESSAGE, before any artifact is resolved
or downloaded. The same message and the same three values govern reading and writing.
pytorch was accepted until this change and is now rejected like any other unknown value: torch is
not installed in the runtime, so mlflow.pytorch.load_model could never have loaded anything there.
There is no fallback between flavors. A failing loader propagates: one attempt, one cause. A model configured with the wrong flavor fails visibly instead of being rescued silently.
Same artifact, three readers
The flavor changes the reader, never the path. All three resolve the same two artifact directories of the Production run:
<run>/artifacts/
├── data_model/ ← transform_flavor loads this one
│ ├── MLmodel ← parsed by sklearn/pyfunc; ignored by joblib
│ ├── model.pkl ← what joblib.load reads
│ └── code/ ← put on sys.path by every flavor, when present
├── prediction_model/ ← predict_flavor loads this one
└── model_card/, *.csv
The run's artifacts/ root holds no .pkl — every model pickle lives one level down, inside its
artifact directory. Predict resolves that directory through the registry URI
(models:/<name>/production, which MLflow points at the same prediction_model); transform builds
the run artifact URI directly with get_model_uri, since the run is already resolved. Observability
is the same for the three: MODEL_READ_* on load, MODEL_WRITE_* on log_model, under the base
operation_type — no per-flavor metric, operation type or label.
code/ on sys.path, for every flavor
An artifact may ship the modules its own classes live in, under <artifact>/code/. Without that
directory on sys.path, unpickling raises ModuleNotFoundError — the artifact is intact, the class
is simply not importable.
MLflow's loaders do add it, but from the manifest: _add_code_from_conf_to_system_path acts only
when the MLmodel flavor config declares a code entry. An artifact carrying code/ on disk
without that entry — code logged with log_artifacts, a re-log that dropped the config, anything a
joblib retrain wrote — loads with no module path at all. So this runtime reads the directory,
which no manifest can misdescribe:
prepend_artifact_code_dirinserts<artifact>/codeat the front ofsys.path, only when that directory exists, and only once (an entry already present is not duplicated).download_artifact_code_diris what feeds it on the download paths, and it is deliberately cheap: the remote listing (list_artifacts(run_id, '<artifact>')) decides first, so a run withoutcodedownloads nothing, and a run with it downloads only<artifact>/code— a handful of.pyfiles, never the model's weights.download_modelruns that step first, for every flavor and both of its branches, before any model is loaded — the insert has to be in place before the loader unpickles.- The models themselves are not pre-downloaded:
mlflow.sklearn.load_modelandmlflow.pyfunc.load_modelare handed the model URI and fetch what they need.joblibdownloads its artifact because it must read the pickle, and adds the entry again inside_load_model_picklethrough the same helper, so the flavor works when called directly too. - When the manifest does declare
code, MLflow inserts the same directory a second time. Harmless.
The entry is never removed: a loaded model may import lazily, long after the load returned.
The directory download_artifact_code_dir downloaded into is also what download_model returns
next to the model — tuple[Any, str | None], the second element being the artifact directory the
retrain path re-logs code/ from, or None when the artifact has no code/. It falls back to
whatever the loader downloaded on its own, which is a real artifact directory for joblib and for
the wrapper branch. load_predict_model and load_transform_model stay the single place where
the flavor is decided: download_model has no joblib branch, it delegates by model_type. Its
only remaining branch is load_wrapper, which is derived from flavor == 'pyfunc' but is otherwise
orthogonal to the flavor.
sys.pathis process-global: two models embedding a package with the same name (utils/) on the same worker resolve to whichever entered first. Mitigation, if it bites: one worker per model.
sklearn
Default. A standard MLflow sklearn artifact: mlflow.sklearn.log_model writes MLmodel +
model.pkl, mlflow.sklearn.load_model reads it, and every outside consumer (MLflow UI, MLflow
serving) recognizes it as a model.
pyfunc
For models logged as mlflow.pyfunc, including the wrapper models this pipeline produces. Retrain
writes through the wrapper's own store_model(artifact_path=<artifact>, code_path=…). Two read
paths, and the second is orthogonal to the flavor:
- Plain:
mlflow.pyfunc.load_model(<uri>), called aspredict(context, model_input). - Wrapper (
download_model(load_wrapper=True)): downloads to./tmp/artifacts/<model>/<artifact>viadowload_artifacts, loads as pyfunc, then unwraps._model_impl.python_model. Inside such an artifact the real pickle is nested —<artifact>/artifacts/stacking_model.pklfor predict,<artifact>/artifacts/training_transformer.pklfor transform (PREDICTION_COMPRESSED_PATH/TRANSFORMED_COMPRESSED_PATH). That nesting is why thejobliblisting does not recurse.
joblib
For models whose .pkl is in the compressed form joblib.dump writes, which MLflow's pickle.load
does not read — the artifact is intact, but no flavor loader opens it.
- Read:
client.download_artifacts(run_id, <artifact>), thenjoblib.load(<artifact>/model.pkl).model.pklisMODEL_PICKLE_NAME, the same constantlog_modelwrites; another.pkldirectly inside the artifact is a fallback, in name order. - Write:
joblib.dump(model, '<artifact>/model.pkl', compress=3), plus a re-log of the source artifact'scode/to<artifact>/code. code/goes onsys.pathbefore the load, when present — see the section above; the joblib flavor is where that started, and it is now shared by all three.- No
MLmodelis written, so only this runtime reads the artifact back —mlflow.sklearn.load_modelandmodels:/<name>/productionboth fail on it. A model retrained in joblib stays joblib. - The pickled object must expose
predict(data), plusfit(data)if retrained. A pyfuncPythonModelwrapper expectingpredict(context, model_input)raisesTypeError.
Switching a model to joblib
The runtime does not migrate documents; editing models is an operator step.
- Inventory before the deploy — run predict/transform and note which models raise. There is no discovery afterwards: the failure names the loader, not the fix.
- Edit the document —
predict_flavorand/ortransform_flavorto'joblib'. Imported models arrive ready:import_modelwrites it itself, seemodel-import.md§ 12. - Check the first retrain — the new run must carry
prediction_model/model.pkland, when the source had one,prediction_model/code/. - Rollback is not just a revert — artifacts a joblib retrain already wrote stop being loadable, so it also means promoting the previous version back and reverting the documents' flavors.
Known gap: how the run id is resolved
get_model_run_id takes source.split('/')[2] of the registered version's source. Not confirmed
against the real tracking server: no scheme reproduced locally puts the run id there (a file store
yields '', S3 the bucket, mlflow-artifacts:/ the experiment id), yet predict works in production.
Every flavor that resolves a run goes through this line — confirm it with a read-only query
before relying on it.