Skip to content

Integrate with Modal

Run Stardag tasks on Modal's serverless infrastructure.

Overview

Modal provides serverless cloud computing for engineers who want to build compute-intensive applications without managing infrastructure. The Stardag Modal integration enables:

  • Serverless execution of tasks
  • Automatic scaling
  • Flexible routing of individual tasks to appropriate compute resources, including GPU access

Prerequisites

Stardag Registry Environment (Optional)

We recommend setting up the Stardag Registry.

You can also run Stardag on Modal, completely without the Registry.

Sign up at app.stardag.com or follow the setup guide for running it self-hosted.

You're all set. Just skip using a Stardag API-key in the examples.

Minimal Example from Scratch

We are going to create a new minimal Python project with the following structure:

stardag-modal/
├── stardag_modal/
│   ├── __init__.py
│   └── main.py
└── pyproject.toml

Create and install the project

Create the new project (with uv as build system):

mkdir stardag-modal
cd stardag-modal
cat > pyproject.toml << 'EOF'
[project]
name = "stardag_modal"
version = "0.0.1"
requires-python = ">=3.12"
dependencies = ["stardag[modal]>=0.1.2", "modal"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
EOF
mkdir stardag_modal
touch stardag_modal/__init__.py
touch stardag_modal/main.py

And install it:

uv sync

Now in stardag_modal/main.py let's define some minimal tasks that we can compose into a DAG:

# stardag_modal/main.py
import sys

import modal
import stardag as sd
import stardag.integration.modal as sd_modal


@sd.task(name="Range")
def get_range(limit: int) -> list[int]:
    return list(range(limit))


@sd.task(name="Sum")
def get_sum(integers: sd.Depends[list[int]]) -> int:
    return sum(integers)

Then let's define the modal image we will be using:

# stardag_modal/main.py continued...

# Must match local Python version for Modal serialization compatibility
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"

# Define the Modal image
image = (
    modal.Image.debian_slim(python_version=python_version)
    .uv_sync()
    .add_local_python_source("stardag_modal")
)

# Define the StardagApp
app = sd_modal.StardagApp(
    "stardag-poc",
    builder_settings=sd_modal.FunctionSettings(
        image=image,
        secrets=[
            # required for communication with registry
            modal.Secret.from_name("stardag-api-key"),
        ],
    ),
    worker_settings={
        "default": sd_modal.FunctionSettings(image=image),
    },
)
# stardag_modal/main.py continued...

# Must match local Python version for Modal serialization compatibility
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"

# Define the Modal image
image = (
    modal.Image.debian_slim(python_version=python_version)
    .uv_sync()
    .add_local_python_source("stardag_modal")
)

# Define the StardagApp
app = sd_modal.StardagApp(
    "stardag-poc",
    builder_settings=sd_modal.FunctionSettings(image=image),
    worker_settings={
        "default": sd_modal.FunctionSettings(image=image),
    },
)

And finally, compose the tasks and add a main section for building them on modal:

# stardag_modal/main.py continued...

root_task = get_sum(integers=get_range(limit=21))

if __name__ == "__main__":
    res = app.build_spawn(root_task)
    print(res)

Now that we have the code in place and the stardag and modal Python packages installed, we need to set up the environment before we can run the example.

Set up your Modal environment

Authenticate with modal (if you haven't already):

modal token new
uv run modal token new

If you've created and want to use a dedicated Modal environment, make sure to also set:

export MODAL_ENVIRONMENT=<my-env>

Set up your Stardag environment

When running Stardag on Modal, we must use a remote filesystem for our target roots. A natural choice when running on Modal is to use Modal volumes:

Create a new isolated Stardag environment:

stardag environment create "Modal PoC" --target-root "default=modalvol://stardag-poc/target-roots/default"
uv run stardag environment create "Modal PoC" --target-root "default=modalvol://stardag-poc/target-roots/default"

Add and activate a new profile for the environment:

stardag config profile add modal-poc -e modal-poc --default
uv run stardag config profile add modal-poc -e modal-poc --default

We also need to give modal functions access to the Stardag Registry:

stardag modal stardag-api-key create
uv run stardag modal stardag-api-key create

Point the default target root to a Modal Volume via the environment variable:

export STARDAG_TARGET_ROOTS__DEFAULT="modalvol://stardag-poc/target-roots/default"

Deploy the app

Now let's deploy the app to Modal.

stardag modal deploy stardag_modal/main.py
uv run stardag modal deploy stardag_modal/main.py

You should see output like:

Using active stardag profile
  Registry URL: https://api.stardag.com
  Workspace ID: <ws-id>
  Environment ID: <env-id>
  Target roots:
    default: modalvol://stardag-poc/target-roots/default
Modal volumes:
  default: stardag-poc
Functions:
  build
  worker_default
✓ Created objects.
├── 🔨 Created mount PythonPackage:stardag_modal
├── 🔨 Created mount PythonPackage:stardag
├── 🔨 Created function build.
└── 🔨 Created function worker_default.
✓ App deployed in 2.592s! 🎉

View Deployment: https://modal.com/apps/<modal-user>/<modal-env>/deployed/stardag-poc

You can also navigate to your modal apps in the relevant environment and should see:

Deployed Stardag app in modal

Run the app

Now let's execute the main.py module:

python stardag_modal/main.py
uv run python stardag_modal/main.py

Then navigate to the app in the Modal UI to follow the execution progress.

Inspect the results

The easiest way to get the results is to use an instance of the desired task and load its output.

python -c "from stardag_modal.main import root_task; \
    print(root_task.target().uri); \
    print(root_task.load())"
uv run python -c "from stardag_modal.main import root_task; \
    print(root_task.target().uri); \
    print(root_task.load())"

Output:

modalvol://stardag-poc/target-roots/default/Sum/e0/e6/e0e66321-c097-534f-b2ae-a95e51ff9373.json
210

You can also "tab" your way through the DAG dependencies to access root_task.integers:

python -c "from stardag_modal.main import root_task; \
    print(root_task.integers.load())"
uv run python -c "from stardag_modal.main import root_task; \
    print(root_task.integers.load())"

If you connected to the Stardag Registry, you can also click the latest build to inspect the DAG execution.

modal-poc dag in the Registry UI

See Also