Deploying an Externally Trained Model with Datamint
This notebook shows how to take a model trained outside of Datamint — in your own script, a Jupyter notebook, or any third-party framework — and register, deploy, and run inference with it using Datamint’s serving infrastructure.
See Bringing an External Model into Datamint for a narrative walkthrough of this same flow (integrate, log, deploy).
When to use this tutorial
Use this approach when you:
Already have a trained PyTorch model (checkpoint,
state_dict, or fullnn.Module)Trained with a custom loop, Keras, Hugging Face, or any other framework
Want to deploy a third-party pre-trained model (foundation model, off-the-shelf architecture)
Comparison with the Trainer API
Step |
This tutorial |
|
|---|---|---|
Training |
Handled automatically |
manual |
MLflow logging |
Handled automatically |
|
Adapter code |
None required |
Subclass |
Deployment |
Handled automatically |
|
What You’ll Learn
Wrap a plain
nn.Modulein aDatamintModeladapterImplement
predict_defaultto produce Datamint annotationsLog and register the model in MLflow
Test inference locally
Deploy the model and run remote inference
Required Dependencies
pip install datamint segmentation-models-pytorch albumentations
0. Setup
[ ]:
from datamint import Api
PROJECT_NAME = "ExternalModel_Tutorial"
MODEL_NAME = PROJECT_NAME # MLflow registered model name
api = Api()
proj = api.projects.create(
name=PROJECT_NAME,
description="Tutorial: deploying an externally trained segmentation model",
exists_ok=True,
)
1. Train or Load Your Model
In a real scenario you already have a checkpoint. Here we train a minimal UNet++ on a toy dataset so the notebook runs end-to-end without external data.
Replace the cell below with your own model loading code, for example:
import torch
import segmentation_models_pytorch as smp
model = smp.UnetPlusPlus(encoder_name='resnet34', in_channels=3, classes=1)
state = torch.load('my_checkpoint.pth', map_location='cpu')
model.load_state_dict(state)
model.eval()
EXAMPLE TRAINING CODE:
[ ]:
import segmentation_models_pytorch as smp
import torch
CLASS_NAMES = ['lesion'] # foreground class names; one per output channel
IMAGE_SIZE = 256
# ── Build architecture ──────────────────────────────────────────────────────
net = smp.UnetPlusPlus(
encoder_name='resnet34',
encoder_weights='imagenet',
in_channels=3,
classes=len(CLASS_NAMES),
)
# ── (Optional) quick training on a toy batch ────────────────────────────────
# Replace this block with your real training loop / checkpoint loading.
optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)
loss_fn = smp.losses.DiceLoss(mode='binary')
net.train()
for _ in range(3): # 3 fake gradient steps
x = torch.randn(2, 3, IMAGE_SIZE, IMAGE_SIZE)
y = (torch.rand(2, len(CLASS_NAMES), IMAGE_SIZE, IMAGE_SIZE) > 0.5).float()
loss = loss_fn(net(x), y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
net.eval()
print(f"Model ready — {sum(p.numel() for p in net.parameters()):,} parameters")
2. Create a DatamintModel Adapter
Datamint’s deployment server calls your predict_default method with a list of Resource objects and expects a list[list[Annotation]] back — one annotation list per resource.
You wrap your model by subclassing DatamintModel and implementing predict_default:
DatamintModel
└── predict_default(model_input: list[Resource], **kwargs) → list[list[Annotation]]
Key helpers available inside predict_default
Helper |
Returns |
|---|---|
|
|
|
The |
|
PIL Image / DICOM etc. |
Supported annotation types
Import from datamint.entities.annotations:
ImageSegmentation— 2-D binary maskCategoryAnnotation— class label + optional probabilityBoxAnnotation— box from two corner points
[ ]:
import albumentations as A
import cv2
import numpy as np
from albumentations.pytorch import ToTensorV2
from datamint.entities.annotations import ImageSegmentation
from datamint.mlflow.flavors.model import DatamintModel, ModelSettings
from datamint.mlflow.flavors.task_type import TaskType
class SegmentationAdapter(DatamintModel):
"""Wraps a plain nn.Module for Datamint segmentation inference."""
task_type = TaskType.IMAGE_SEGMENTATION
def __init__(
self,
torch_model: torch.nn.Module,
class_names: list[str],
image_size: int = 256,
threshold: float = 0.5,
need_gpu: bool = False,
) -> None:
super().__init__(
torch_model=torch_model,
settings=ModelSettings(need_gpu=need_gpu),
)
self.class_names = class_names
self.image_size = image_size
self.threshold = threshold
# Preprocessing applied to every image at inference time
self._transform = A.Compose([
A.Resize(image_size, image_size),
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2(),
])
def predict_default(
self,
model_input: list, # list[Resource]
**kwargs,
) -> list[list[ImageSegmentation]]:
device = self.inference_device
model = self.get_pytorch_model().to(device).eval()
results = []
for resource in model_input:
# ── Load image ──────────────────────────────────────────────────
img = np.array(resource.fetch_file_data(auto_convert=True, use_cache=True))
if img.ndim == 2: # grayscale → 3-channel
img = np.stack([img, img, img], axis=-1)
elif img.shape[-1] == 4: # RGBA → RGB
img = img[..., :3]
orig_h, orig_w = img.shape[:2]
# ── Preprocess ──────────────────────────────────────────────────
tensor = self._transform(image=img)['image'].unsqueeze(0).to(device) # (1, 3, H, W)
# ── Forward pass ────────────────────────────────────────────────
with torch.inference_mode():
logits = model(tensor) # (1, C, H, W)
probs = logits.sigmoid().squeeze(0).cpu().numpy() # (C, H, W) [0, 1]
# ── Build annotations ───────────────────────────────────────────
annotations = [
ImageSegmentation(
name=self.class_names[i],
segmentation_data=cv2.resize(
(probs[i] > self.threshold).astype(np.uint8),
(orig_w, orig_h),
interpolation=cv2.INTER_NEAREST,
),
)
for i in range(len(self.class_names))
]
results.append(annotations)
return results
# Instantiate the adapter with the trained model
adapter = SegmentationAdapter(
torch_model=net,
class_names=CLASS_NAMES,
image_size=IMAGE_SIZE,
)
print("Adapter ready.")
2.1 Smoke-test the adapter locally
Before logging to MLflow it’s worth making sure predict_default runs without errors using a fake resource that just wraps a local file.
[ ]:
import io
from PIL import Image
from datamint.entities.resource import LocalResource
# Create a dummy in-memory PNG as a LocalResource
buf = io.BytesIO()
img = Image.fromarray(np.random.randint(0, 255, (300, 400, 3), dtype=np.uint8))
img.save(buf, format='PNG')
dummy_resource = LocalResource(raw_data=buf.getvalue(), filename='dummy.png')
predictions = adapter.predict([dummy_resource])
print(f"Received {len(predictions)} result(s)")
print(f"Annotations per resource: {[len(p) for p in predictions]}")
for ann in predictions[0]:
print(f" {ann.name!r} mask shape={ann.mask.shape} dtype={ann.mask.dtype}")
[ ]:
# upload resource to Datamint (optional, but simulates real usage more closely)
import tempfile
with tempfile.NamedTemporaryFile(suffix='.png') as tmp:
img.save(tmp.name)
api.resources.upload_resource(tmp.name, tags=['dummy'],
publish_to=proj)
3. Log and Register the Model in MLflow
datamint.mlflow.flavors.log_model is the single call that:
Serialises your adapter (including the embedded
nn.Module) as an MLflow artifactRecords the
task_typeso the Datamint server knows how to display predictionsPins the
datamintandmedimgkitpackage versions inrequirements.txt(Optionally) registers the model in the MLflow Model Registry under a name you choose
Why call datamint.mlflow.set_project first?
set_project configures the MLflow tracking URI to point to your Datamint server and associates the run with the correct project, so metrics and artifacts are visible in the Datamint dashboard.
[ ]:
import mlflow
import datamint.mlflow as datamint_mlflow
from datamint.mlflow.flavors import log_model
# Point MLflow at the Datamint server and associate runs with the project
datamint_mlflow.set_project(PROJECT_NAME)
mlflow.set_experiment(PROJECT_NAME) # Can be any name; doesn't have to match the project name
with mlflow.start_run(run_name='external_model_upload') as run:
# Log any metadata you want to track alongside the model
mlflow.log_params({
'encoder': 'resnet34',
'image_size': IMAGE_SIZE,
'class_names': str(CLASS_NAMES),
'framework': 'segmentation_models_pytorch',
})
model_info = log_model(
adapter,
task_type=TaskType.IMAGE_SEGMENTATION,
name='segmentation_model', # artifact sub-path inside the run
registered_model_name=MODEL_NAME, # registers in the Model Registry
)
print(f"Model URI : {model_info.model_uri}")
print(f"Run ID : {run.info.run_id}")
3.1 Assign the champion alias
The deployment API (api.deploy.start) resolves models by alias. Set the champion (or any of your choice) alias on the version we just registered.
[ ]:
from mlflow import MlflowClient
client = MlflowClient()
# The newly registered version is always the highest version number
versions = client.search_model_versions(f"name='{MODEL_NAME}'")
latest_version = max(versions, key=lambda v: int(v.version))
client.set_registered_model_alias(MODEL_NAME, 'champion', latest_version.version)
print(f"Alias 'champion' → version {latest_version.version}")
4. Verify: Load and Run Local Inference
Before deploying, load the registered model back from MLflow and verify that predict works correctly end-to-end.
[ ]:
from datamint.mlflow import flavors as datamint_flavor
model = api.models.get_by_name(MODEL_NAME)
loaded_model = model.get_latest_version(alias='champion').load_model()
print(f"Loaded model type : {type(loaded_model).__name__}")
print(f"Task type : {loaded_model.task_type}")
print(f"Inference device : {loaded_model.inference_device}")
[ ]:
# Run inference on a real resource from the project
resources = list(api.resources.get_list(project_name=PROJECT_NAME, limit=3))
if resources:
predictions = loaded_model.predict(resources[:1])
for ann in predictions[0]:
print(f" class={ann.name!r} mask={ann.mask.shape} positive_pixels={ann.mask.sum()}")
else:
# Fall back to the dummy resource from section 2.1
predictions = loaded_model.predict([dummy_resource])
print("No resources in project — tested on dummy image.")
for ann in predictions[0]:
print(f" class={ann.name!r} mask={ann.mask.shape}")
5. Deploy to the Datamint Server
[ ]:
job = api.deploy.start(
model_name=MODEL_NAME,
model_alias='champion',
with_gpu=False,
)
print("Deployment job started")
print(f"Job ID : {job.id}")
print(f"Status : {job.status}")
[ ]:
import time
# Poll until the deployment is complete (usually 5–10 minutes)
while True:
job = api.deploy.get_by_id(job.id)
print(f"Status: {job.status} ({job.progress_percentage:.0f}%)")
if job.status in ('completed', 'failed', 'cancelled'):
break
time.sleep(15)
if job.status == 'completed':
print("Deployment successful!")
else:
print(f"Deployment failed: {job.error_message}")
print(job.build_logs)
6. Remote Inference
Once deployed, submit resources for server-side inference.
[ ]:
# Pick a resource to run inference on
r = api.resources.get_list(project_name=PROJECT_NAME, limit=1)[0]
inf_job = api.inference.submit(
model_name=MODEL_NAME,
model_alias='champion',
resource_id=r.id,
)
inf_job.wait() # blocks until inference is complete
print(f"Inference complete — {len(inf_job.predictions)} result(s)")
[ ]:
from matplotlib import pyplot as plt
preds = inf_job.predictions[0]
fig, axes = plt.subplots(1, len(preds), figsize=(5 * max(len(preds), 1), 5))
if len(preds) == 1:
axes = [axes]
for ax, ann in zip(axes, preds):
ax.imshow(ann.mask, cmap='gray')
ax.set_title(ann.name)
ax.axis('off')
plt.suptitle(f"Remote inference on: {r.filename}")
plt.tight_layout()
plt.show()
7. Updating the Model (New Version)
When you retrain or fine-tune your model, log a new version and move the champion alias:
# Re-train / fine-tune your net ...
adapter_v2 = SegmentationAdapter(torch_model=net_v2, class_names=CLASS_NAMES)
datamint_mlflow.set_project(PROJECT_NAME)
mlflow.set_experiment(PROJECT_NAME)
with mlflow.start_run(run_name='external_model_v2'):
mlflow.log_param('version', 2)
model_info = log_model(
adapter_v2,
task_type=TaskType.IMAGE_SEGMENTATION,
name='segmentation_model',
registered_model_name=MODEL_NAME,
)
# Advance the alias so deployments pick up the new version automatically
versions = client.search_model_versions(f"name='{MODEL_NAME}'")
latest_version = max(versions, key=lambda v: int(v.version))
client.set_registered_model_alias(MODEL_NAME, 'champion', latest_version.version)
# Re-deploy
api.deploy.start(model_name=MODEL_NAME, model_alias='champion', with_gpu=False)
8. Custom Prediction Modes (Advanced)
For large images you may want slice-by-slice or frame-by-frame inference. Add extra prediction hooks using @prediction_mode:
from datamint.mlflow.flavors.prediction_router import prediction_mode
from datamint.mlflow.flavors.prediction_modes import PredictionMode
class MyAdapter(DatamintModel):
task_type = TaskType.IMAGE_SEGMENTATION
def predict_default(self, model_input, **kwargs):
... # full-image inference
@prediction_mode(PredictionMode.SLICE)
def predict_slice(self, model_input, slice_idx, **kwargs):
... # called when mode='slice' is passed as a parameter
Supported modes are defined in datamint.mlflow.flavors.prediction_modes.PredictionMode.
Summary
To deploy any externally trained model with Datamint:
Step |
Code |
|---|---|
|
Subclass |
|
|
|
|
|
|
|
|
|
|
The only part unique to your model is the adapter class — everything else is identical regardless of architecture, framework, or task type.