ML Platform Frameworks, Sorted: Ray, Dagster, Flyte, MLflow, and Comet

ML Platforms Are a Stack, Not a Category

Search “ML platform framework” and the internet hands you Ray, Dagster, Flyte, MLflow, and Comet in the same breath, as if they’re five flavors of the same ice cream. They’re not. One of them scales your Python across machines, two of them decide what runs and in what order, and the last two just write down what happened so you can find it later.

Picking the wrong one, or picking all of them because a vendor’s homepage made them sound complementary, is how teams end up with three overlapping tools, no real orchestration, and an “MLOps platform” that’s actually just a folder of notebooks with anxiety.

This post draws the map. For each tool: what it actually is, the one snippet that captures its soul, and the honest pros and cons. Then how they fit together — and which ones you can skip.

The One Diagram

Before the details, get the ontology right, because it explains every argument people have about these tools.

            ┌─────────────────────────────────────────────┐
            │  experiment tracking / model registry        │
            │       MLflow            Comet (CometML)      │
            └──────────────────────┬───────────────────────┘
                                   │  "which run won, where's the model"
            ┌──────────────────────┴───────────────────────┐
            │  data & pipeline orchestration               │
            │       Dagster             Flyte              │
            └──────────────────────┬───────────────────────┘
                                   │  "what ran, in what order, from what data"
            ┌──────────────────────┴───────────────────────┐
            │  distributed compute (Python, GPUs, data)    │
            │                   Ray                        │
            └─────────────────────────────────────────────┘

Three shelves, five tools:

  • Ray is the bottom shelf: a distributed runtime for Python. It makes your functions run on many cores/machines/GPUs.
  • Dagster and Flyte are the middle shelf: workflow/orchestration. They schedule and order your jobs, track their inputs, and remember lineage.
  • MLflow and Comet are the top shelf: experiment tracking and model registries. They’re the lab notebook that doesn’t lose pages.

The shelves overlap on purpose — Flyte can track lineage MLflow-style, Ray has a serving layer, Comet has a registry — but each tool has a home problem it solves best. That home problem is what you’re actually buying.

Ray: The Compute Skeleton

Ray’s answer to “my Python is too slow and one machine isn’t enough” is: annotate your functions and it runs them anywhere.

# ray_core.py — the whole idea in six lines
import ray

ray.init()                       # start a local Ray cluster (or connect to a remote one)

@ray.remote                       # this function now runs on the cluster, not just in-process
def train_seed(seed: int) -> float:
    # ...expensive per-seed training...
    return 0.92 + (seed % 7) * 0.001

futures = [train_seed.remote(i) for i in range(32)]
scores = ray.get(futures)        # block until all 32 finish, wherever they ran

That’s the pattern: @ray.remote + .remote() + ray.get(). Everything else in Ray is this idea wearing nicer clothes:

  • Ray Data is a distributed loading/preprocessing (ray.data.read_parquet(...)).
  • Ray Train is a multi-node training loops with PyTorch/Lightning/TensorFlow.
  • Ray Tune is a hyperparameter search over a cluster instead of a single box.
  • Ray Serve is a model serving, same runtime as your training code:
# ray_serve.py
from ray import serve

@serve.deployment
class Predictor:
    def __init__(self):
        self.model = load_model()        # loaded once per replica

    def __call__(self, request):
        return self.model.predict(request)

serve.run(Predictor.bind())

Pros: one runtime from data prep to serving; genuinely scales Python; the API is small enough to learn in an afternoon; works on a laptop (ray.init() is free) and scales to a cluster later.

Cons: it’s a runtime, not an orchestrator — it won’t schedule jobs at 3 a.m., retry a failed pipeline, or tell you which dataset produced which model. You still need something on the middle shelf. And “distributed Python” is its own debugging universe once you leave a single machine.

The Orchestration Shelf: Dagster vs Flyte

These two answer “make my pipeline run on a schedule, and remember how everything connects.” They take opposite philosophical routes.

Dagster: Everything Is an Asset

Dagster’s core idea is the software-defined asset , which is a named, versioned object (a table, a model, a file) that declares which other assets it depends on. You write the graph, Dagster figures out the rest.

# dagster_assets.py
from dagster import asset, materialize
import pandas as pd

@asset
def raw_orders() -> pd.DataFrame:
    return pd.read_csv("orders.csv")

@asset
def clean_orders(raw_orders: pd.DataFrame) -> pd.DataFrame:
    return raw_orders.dropna().reset_index(drop=True)

@asset
def daily_report(clean_orders: pd.DataFrame) -> str:
    return clean_orders.to_markdown()

# run a single asset, and Dagster materializes its upstreams first
if __name__ == "__main__":
    materialize([daily_report])

The definitions get loaded into dagster dev, which gives you a UI with lineage, schedules, sensors, and partition backfills.

Pros: the asset mental model clicks fast; lineage is free and real (an asset is a function of its inputs); runs fine single-node without Kubernetes (DuckDB/Postgres as the store); first-class dbt/Spark integrations; dagster dev is a pleasant local loop.

Cons: it’s data-engineering-first — the ML training story is fine but less prescriptive than Flyte’s; heavier than Airflow, so teams already invested in Airflow feel the migration cost; deep horizontal scale still wants the Dagster+ (hybrid/k8s) deployment.

Flyte: Type-Safe Workflows on Kubernetes

Flyte came out of Lyft and is built for ML workloads that need strong typing, data lineage, and caching — on Kubernetes from day one.

# flyte_wf.py
from flytekit import task, workflow
from typing import List

@task
def fit_linear(x: List[float], y: List[float]) -> dict:
    # ...train, return a JSON-serializable artifact summary...
    return {"intercept": 1.0, "slope": 0.5}

@task
def report(fit: dict) -> str:
    return f"y = {fit['slope']}x + {fit['intercept']}"

@workflow
def training_wf(x: List[float], y: List[float]) -> str:
    return report(fit=fit_linear(x=x, y=y))

Run it locally with pyflyte run flyte_wf.py training_wf (it will prompt for inputs), or pyflyte register it to a Flyte backend.

Pros: the type system catches wiring mistakes at build time; tasks are first-class and cached by input hash (skip recompute); lineage and versioning are built in, not bolted on; scales to huge fan-out on k8s.

Cons: you’re committing to Kubernetes to get the real payoff; the type discipline is a feature but also friction (everything must be serializable); heavier operational footprint than a weekend pip install.

The short version: Dagster is “I want to understand and schedule my data assets, and I’d rather not buy a Kubernetes cluster.” Flyte is “I have ML pipelines, I already run k8s, and I want compile-time safety and caching.”

The Tracking Shelf: MLflow vs Comet

These two are the lab notebook. They record parameters, metrics, and artifacts for every run, and keep a registry of your models. Their job is memory.

MLflow: The Self-Hosted Default

MLflow is open source and splits into four pieces, and the order matters — each one builds on the last:

  • Tracking — record every run’s parameters, metrics, and artifacts so you can compare them later.
  • Models — package a trained model plus its dependencies into one loadable, servable artifact.
  • Model Registry — give that artifact a version and promote it through Staging → Production.
  • Projects — package the code so a run is reproducible from a one-line command.

Tracking: the lab notebook that never loses pages.

# tracking.py
import mlflow

mlflow.set_tracking_uri("http://localhost:5000")   # or just a local ./mlruns folder

with mlflow.start_run(run_name="lr-0.01"):
    mlflow.log_params({"lr": 0.01, "epochs": 10})
    mlflow.log_metric("accuracy", 0.93)
    mlflow.log_metric("loss", 0.18, step=10)       # step = training iteration
    mlflow.log_artifact("confusion_matrix.png")

Spin up the UI with mlflow server (older setups use mlflow ui) and compare runs side by side.

Models: turn “a run” into “a loadable thing”.

# models.py
import mlflow.sklearn

mlflow.sklearn.log_model(model, "model", registered_model_name="FraudModel")

# load it back later, from a specific run or a registered stage
loaded = mlflow.sklearn.load_model("models:/FraudModel/Production")

Serve it without writing a web server:

mlflow models serve -m "models:/FraudModel/Production" -p 1234

Model Registry: promote, don’t just save.

# registry.py
from mlflow.tracking import MlflowClient

client = MlflowClient()
client.transition_model_version_stage(
    name="FraudModel", version=1, stage="Production"
)

Projects: package the code, not just the artifact.

# MLproject
name: fraud
conda_env: conda.yaml
entry_points:
  main:
    parameters:
      lr: {type: float, default: 0.01}
    command: "python train.py --lr {lr}"
mlflow run . -P lr=0.05

That last one is why “reproducibility” in MLflow is real: the same command in a fresh environment rebuilds the same run.

Pros: free, self-hosted, no data leaves your VPC; the de-facto standard API — every training framework has an integration; mlflow.autolog() does 80% of the logging for you.

Cons: the UI is functional, not beautiful; you’re running and backing up a server; the “Projects” piece is the unloved child nobody uses; scaling a shared registry for a big org is on you.

Comet (CometML): The Pretty Hosted One

Comet is a commercial SaaS that lives on the same shelf but optimizes for the human side: gorgeous experiment views, side-by-side diffs, reports you can paste into a standup, plus a model registry and production monitoring.

# comet_run.py
import comet_ml

experiment = comet_ml.Experiment(
    api_key="...",            # or comet_ml.login()
    project_name="fraud-detection",
)
experiment.log_parameters({"lr": 0.01, "epochs": 10})
experiment.log_metric("accuracy", 0.93)
experiment.log_model("fraud-model", "model.pkl")
experiment.end()

Pros: best-in-class visualization and sharing; zero ops (it’s SaaS); production model monitoring with drift detection is a genuinely different feature from plain tracking; generous free tier for solo projects.

Cons: it’s a hosted service — data and metadata go to the cloud, which is a hard no in some regulated shops; you’re renting the notebook; pricing scales with usage.

The short version: MLflow when you want to own it and the API-standard matters. Comet when you want the best UI, zero ops, and you’re fine with SaaS.

How They Actually Fit Together

Here’s the part the marketing never says: you almost never run all five. These tools were designed to be layered, and a sane stack picks one shelf each — sometimes skipping a shelf entirely.

A realistic mid-size team looks like this:

  • Ray for the heavy compute (or nothing, if one big box is enough).
  • One orchestrator — Dagster or Flyte — to schedule jobs and hold lineage.
  • One tracker — MLflow or Comet — so every run is findable.

So a concrete pipeline reads: Flyte schedules a workflow → a step calls Ray Train for multi-GPU training → each step logs metrics to MLflow → the winning model gets registered and Flyte promotes it.

The two most common mistakes I see:

  1. Two trackers and no orchestrator. A team runs both MLflow and Comet, and still ships models by running a notebook by hand. They bought two memories and zero execution.
  2. Ray bought too early. A team with a 200 MB dataset and a 30-minute training job installs a Ray cluster, then spends a month fighting distributed debugging for a job a laptop could run overnight. Ray is for when Python genuinely doesn’t scale, not for ambition.

Which One Do I Actually Need?

Start from your problem, not the vendor’s feature grid.

Your situation Reach for Skip for now
Experiments are a folder of notebooks and you can’t reproduce anything MLflow (or Comet) everything else
You have scheduled jobs, but the schedule is cron + vibes Dagster Flyte, Ray
You’re on k8s and want type-safe, cacheable ML workflows Flyte Dagster
One machine isn’t enough for your data or training Ray —
You want gorgeous dashboards and model monitoring, zero ops Comet MLflow

The honest progression for most teams:

  1. Tracking first — MLflow (self-host) or Comet (SaaS). This is the cheapest, highest-leverage win; you get reproducibility the same week.
  2. Orchestration second — when “the pipeline” is a real thing that must run on a schedule with lineage. Dagster if you’re data-centric and k8s-averse; Flyte if you’re ML-centric and k8s-native.
  3. Ray last — only when you’ve measured and a single machine is actually the bottleneck.

TL;DR

  • Ray scales your Python; it’s a runtime, not an orchestrator or a notebook.
  • Dagster models your data as assets and schedules them — the friendly, k8s-optional orchestrator.
  • Flyte is the type-safe, Kubernetes-native ML orchestrator with lineage and caching built in.
  • MLflow is the open-source experiment-tracking standard you host yourself.
  • Comet is the hosted tracker with the best UI and production model monitoring.

They’re a stack, not a menu. Pick one per shelf, only when you need it, and spend the rest of your energy on the part that actually matters — the model, not the scaffolding around it.

Comments

Leave a comment

What is 2 + 3?
Please solve the arithmetic question above to verify you're human.