MLFlow Switch Orchestrator is a lightweight ML deployment control plane for spec-driven training, MLflow-backed run-to-model lineage, controlled model promotion, and stable inference serving.
The main workflow is:
- launch training from a spec
- pre-create and track the run in MLflow
- register the resulting model version
- start a candidate serving container and wait for health checks
- switch traffic without changing the public inference endpoint
Key behaviors:
- Spec-driven training: trainer definitions live in
router/specs/spec.yaml. - MLflow lineage: the router injects
MLFLOW_RUN_IDinto the trainer container and resolves the model version created by that run. - Controlled promotion: rollout happens only after a candidate serving container is healthy.
- Stable serving endpoint: the active model can change without changing the public inference URL.
- Serving-only promotion: existing model versions or aliases can be promoted through
/admin/roll. - Explicit rollback: the previous active deployment is recorded on each successful promotion and can be restored through
/admin/rollback.
- Router service (
router/): FastAPI control plane that loads trainer specs, launches training containers, tracks MLflow runs, resolves model versions, and coordinates rollout. - Trainer containers (
model-images/): model-specific training jobs that log parameters, metrics, datasets, and registered models to MLflow. - Serve containers: runtime images that load a promoted MLflow model URI and expose inference APIs.
- MLflow tracking server: central source of truth for experiment runs, model versions, aliases, and artifacts.
- Caddy reverse proxy: stable public inference entrypoint that is flipped only after a candidate server is healthy.
- Docker socket proxy: narrows the router's Docker access surface while still allowing container lifecycle automation.
For control-plane API and trainer spec details, see router/README.txt.
- it pre-creates the MLflow run and injects
MLFLOW_RUN_IDinto the trainer container - it resolves the exact registered model version created by that run before promotion
- it supports serving-only promotion through
/admin/roll - it records the previous deployment to support explicit rollback through
/admin/rollback
flowchart TD
A[Trainer spec] --> R[Router control plane]
B[Train / train_then_roll request] --> R
R -->|pre-create run| M[MLflow tracking]
R -->|launch trainer container| T[Sklearn or PyTorch trainer]
T -->|log params, metrics, datasets, model| M
M --> G[MLflow registry]
R -->|resolve model version or alias| G
R -->|start candidate serve container| S[Serve container]
S -->|health check passes| P[Caddy stable endpoint]
P --> C[Client /invocations]
R --> X[Persist active + previous deployment]
X --> RB[Rollback available]
sklearn-model-1: a scikit-learn random forest regressor on the diabetes dataset.pytorch-model-1: a small PyTorch MLP regressor on the same dataset, logged throughmlflow.pytorchand served through the same rollout path.
The two backends are intentionally similar so the main difference is the training framework rather than the deployment flow.
The bundled backends default to the checked-in demo_data/diabetes.csv dataset,
which is copied into the trainer images at build time. This keeps the demo path
deterministic while still allowing custom data.
- Default demo mode: the trainer spec sets
DATASET_PATH=/app/demo_data/diabetes.csvandTARGET_COLUMN=target. - Custom dataset mode: override
DATASET_PATHandTARGET_COLUMNin the train request parameters or spec. - Fast demo mode: optionally set
DATASET_SAMPLE_ROWSto train on a deterministic sample of the same dataset. - Dataset metadata:
DATASET_NAMEandDATASET_VERSIONare logged to MLflow alongside the run.
If you want to regenerate the checked-in demo CSV:
cd model-images/sklearn-model-1
uv run python ../../demo_data/prepare_demo_data.py- A request hits
/admin/train/{trainer}or/admin/train_then_roll/{trainer}. - The router resolves the trainer spec and pre-creates an MLflow run.
- The trainer container receives
MLFLOW_RUN_IDplus any spec-defined or request-level parameters. - The training job logs dataset lineage, hyperparameters, evaluation metrics, and a registered model into MLflow.
- The router resolves the model version associated with that exact run.
- For rollout, the router starts a candidate serving container pointed at
models:/<name>/<version>. - If the candidate passes health checks, the proxy flips traffic to it and the previous serving container is retired.
- Clients continue using the same inference endpoint while the production model version changes underneath it.
The reference trainer in model-images/sklearn-model-1/ demonstrates the main ML workflow:
- dataset lineage is logged for training, validation, and evaluation splits
- hyperparameters and dataset metadata are recorded on the run
- evaluation uses
mlflow.evaluatefor standardized regression metrics - the trained model is registered into the MLflow Model Registry
- the rollout path updates a per-model registry alias after a successful deployment
- Create a
.envwith any overrides for the variables referenced indocker-compose.prod.yaml. The defaults are enough for local testing. - Build the reference trainer and serving images used by the bundled
sklearn-model-1spec:To build the PyTorch reference backend instead:docker compose -f model-images/sklearn-model-1/docker-compose.prod.yml build
docker compose -f model-images/pytorch-model-1/docker-compose.prod.yml build
- Launch the stack:
docker compose -f docker-compose.prod.yaml up --build
- Trigger a train-and-roll flow:
Or run the PyTorch backend:
curl -X POST \ 'http://localhost:8000/admin/train_then_roll/sklearn-model-1' \ -H 'Content-Type: application/json' \ -d '{"wait_seconds": 600, "parameters": {"DATASET_SAMPLE_ROWS": 96, "N_ESTIMATORS": 32}}'
curl -X POST \ 'http://localhost:8000/admin/train_then_roll/pytorch-model-1' \ -H 'Content-Type: application/json' \ -d '{"wait_seconds": 600, "parameters": {"DATASET_SAMPLE_ROWS": 96, "EPOCHS": 40, "HIDDEN_DIM": 16}}'
- Query the stable inference endpoint after rollout completes:
curl -X POST \ 'http://localhost:9000/invocations' \ -H 'Content-Type: application/json' \ -d '{ "dataframe_split": { "columns": ["age", "sex", "bmi", "bp", "s1", "s2", "s3", "s4", "s5", "s6"], "data": [[0.03, 1, 0.06, 0.03, 0.04, 0.03, 0.02, 0.03, 0.04, 0.01]] } }'
- Inspect the active deployment, including the active model version:
curl http://localhost:8000/status
- Inspect MLflow runs and registered model versions at
http://localhost:${MLFLOW_SERVICE_PORT}. The default external MLflow port is9010. - Run the smoke test:
The smoke tests use lighter training overrides and sampled rows so the live path completes faster than the default demo request. To run the explicit rollback smoke scenario:
./scripts/smoke.sh
./scripts/smoke-rollback.sh
- Run fast unit tests from the router project:
cd router uv run pytest tests
- Copy
model-images/sklearn-model-1/into a new model-specific folder. - Update the training code to prepare data, train, evaluate, and log into MLflow for your model.
- Update the Dockerfiles so
docker compose buildproduces trainer and serving images with the tags referenced by the router spec. - Add a new trainer entry to
router/specs/spec.yaml:my-new-model: trainer_image: trainer-my-model:latest serve_image: server-my-model:latest timeout: 3600 env: REGISTERED_MODEL_NAME: MyCoolModel MLFLOW_EXPERIMENT: my_experiment
- Trigger training through
/admin/train/{spec-name}or/admin/train_then_roll/{spec-name}.
Serving-only promotions can reuse existing registry entries by invoking /admin/roll with a model name and version or alias.
The control-plane API lives in router/ and is documented in
router/README.txt.
The main endpoints are:
GET /statusfor active deployment state and healthPOST /admin/train/{trainer}for synchronous trainingPOST /admin/train_then_roll/{trainer}for train-and-promotePOST /admin/rollfor serving-only promotion of an existing model version or aliasPOST /admin/rollbackfor restoring the previously active deployment
- Trainer specs map logical workloads to Docker images, environment defaults, and rollout behavior.
- The router creates the MLflow run before training starts so container execution and model registry lineage stay tied together.
- The reference trainer logs dataset-level lineage and standardized evaluation artifacts to MLflow, not just a final model file.
- Rollout uses a blue/green-style candidate container, health check gate, and proxy cutover.
- Active deployment state is persisted to disk so router restarts can validate and recover the last-known active target.
- Successful promotions also persist the previous deployment so rollback can be triggered without manually supplying a model version.
/statusexposes both container-level and model-level deployment state for operational checks and demos.
router/
├── main.py # FastAPI app wiring status, trainer, and rollout routers
├── specs/spec.yaml # Trainer specifications
├── trainer/ # Training orchestration and MLflow run management
├── roll/ # Candidate deployment and proxy switching
└── status/ # Active deployment status endpoint
model-images/
└── sklearn-model-1/ # Reference scikit-learn trainer and serving images
- The default setup is a local reference deployment focused on control-plane behavior: local Docker, local MLflow, SQLite backend store, and file-based artifacts.
- Rollback currently tracks only the immediately previous deployment rather than a fuller deployment history.
- The current test suite focuses on smoke coverage for the happy path and rollback flow; broader unit and integration coverage would be the next step.
- Future extensions include richer promotion states, a more complex second-stage backend, and support for remote datasets and artifact stores.