Skill Tracer
Skills › Scientific Agent Skills › modal › functions.md

functions.md

298 lines · skills/modal/references/functions.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Modal Functions and Classes
2
3Reviewed against Modal 1.6.0. Snippets with application-specific helpers are illustrative.
4Sources: [invocation methods](https://modal.com/docs/guide/function-invocation-methods),
5[lifecycle](https://modal.com/docs/guide/lifecycle-functions),
6[timeouts](https://modal.com/docs/guide/timeouts), and [SDK releases](https://modal.com/docs/sdk/py/releases).
7

Modal Functions and Classes

Reviewed against Modal 1.6.0. Snippets with application-specific helpers are illustrative. Sources: invocation methods, lifecycle, timeouts, and SDK releases.

Raw lines 1 to 7
8## Table of Contents
9
10- [Functions](#functions)
11- [Remote Execution](#remote-execution)
12- [Classes with Lifecycle Hooks](#classes-with-lifecycle-hooks)
13- [Parallel Execution](#parallel-execution)
14- [Async Functions](#async-functions)
15- [Local Entrypoints](#local-entrypoints)
16- [Generators](#generators)

Table of Contents

  • Functions
  • Remote Execution
  • Classes with Lifecycle Hooks
  • Parallel Execution
  • Async Functions
  • Local Entrypoints
  • Generators
Raw lines 8 to 17
18## Functions
20### Basic Function
22```python
23import modal
25app = modal.App("my-app")
27@app.function()
28def compute(x: int, y: int) -> int:
29 return x + y
30```

Basic Function

import modal

app = modal.App("my-app")

@app.function()
def compute(x: int, y: int) -> int:
    return x + y
Raw lines 20 to 31
32### Function Parameters
34The `@app.function()` decorator accepts:
36| Parameter | Type | Description |
37|-----------|------|-------------|
38| `image` | `Image` | Container image |
39| `gpu` | `str` or `list[str]` | GPU type/count or ordered fallbacks |
40| `cpu` | `float` or tuple | Physical-core request or `(request, limit)` |
41| `memory` | `int` or tuple | MiB request or `(request, limit)` |
42| `timeout` | `int` | Max execution time in seconds |
43| `startup_timeout` | `int` | Separate container initialization timeout |
44| `secrets` | `list[Secret]` | Secrets to inject |
45| `volumes` | `dict[str, Volume]` | Volumes to mount |
46| `schedule` | `Schedule` | Cron or periodic schedule |
47| `max_containers` | `int` | Max container count |
48| `min_containers` | `int` | Minimum warm containers |
49| `retries` | `int` or `Retries` | Retry policy per input |
50| `ephemeral_disk` | `int` | Disk in MiB |
52Use `max_containers` to cap containers and `@modal.concurrent(max_inputs=N)` to cap
53simultaneous inputs per container. The old `concurrency_limit` keyword is removed.

Function Parameters

The @app.function() decorator accepts:

ParameterTypeDescription
imageImageContainer image
gpustr or list[str]GPU type/count or ordered fallbacks
cpufloat or tuplePhysical-core request or (request, limit)
memoryint or tupleMiB request or (request, limit)
timeoutintMax execution time in seconds
startup_timeoutintSeparate container initialization timeout
secretslist[Secret]Secrets to inject
volumesdict[str, Volume]Volumes to mount
scheduleScheduleCron or periodic schedule
max_containersintMax container count
min_containersintMinimum warm containers
retriesint or RetriesRetry policy per input
ephemeral_diskintDisk in MiB

Use max_containers to cap containers and @modal.concurrent(max_inputs=N) to cap simultaneous inputs per container. The old concurrency_limit keyword is removed.

Raw lines 32 to 54
55## Remote Execution

Remote Execution

Raw lines 55 to 56
57### `.remote()` — Synchronous Call
59```python
60result = compute.remote(3, 4) # Runs in the cloud, blocks until done
61```

.remote() — Synchronous Call

result = compute.remote(3, 4)  # Runs in the cloud, blocks until done
Raw lines 57 to 62
63### `.local()` — Local Execution
65```python
66result = compute.local(3, 4) # Runs locally (for testing)
67```
69`.local()` applies no Image, GPU, Secret, Volume or concurrency configuration.

.local() — Local Execution

result = compute.local(3, 4)  # Runs locally (for testing)

.local() applies no Image, GPU, Secret, Volume or concurrency configuration.

Raw lines 63 to 70
71### `.spawn()` — Async Fire-and-Forget
73```python
74call = compute.spawn(3, 4) # Returns immediately
75# ... do other work ...
76result = call.get() # Retrieve result later
77```
79`.spawn()` supports up to 1 million queued asynchronous inputs. Use a deployed Function
80for jobs that must survive the invoking process; an ephemeral App still ends when its
81run ends. Persist `call.object_id` and later use `modal.FunctionCall.from_id(id)`.
82`call.get(timeout=0)` polls and raises `TimeoutError` while pending; outputs expire
837 days after completion (`modal.exception.OutputExpiredError`). This is separate from
84the Function's execution timeout. Retryable jobs must tolerate repeated execution.

.spawn() — Async Fire-and-Forget

call = compute.spawn(3, 4)  # Returns immediately
# ... do other work ...
result = call.get()  # Retrieve result later

.spawn() supports up to 1 million queued asynchronous inputs. Use a deployed Function for jobs that must survive the invoking process; an ephemeral App still ends when its run ends. Persist call.object_id and later use modal.FunctionCall.from_id(id). call.get(timeout=0) polls and raises TimeoutError while pending; outputs expire 7 days after completion (modal.exception.OutputExpiredError). This is separate from the Function's execution timeout. Retryable jobs must tolerate repeated execution.

Raw lines 71 to 85
Starts an agent spawn :71
86## Classes with Lifecycle Hooks
88Use `@app.cls()` for stateful workloads where you want to load resources once:
90```python
91@app.cls(gpu="L40S", image=image)
92class Model:
93 @modal.enter()
94 def setup(self):
95 """Runs once when the container starts."""
96 import torch
97 self.model = build_model() # Project-defined architecture matching training
98 state_dict = torch.load(
99 "/weights/model_state.pt", map_location="cpu", weights_only=True
100 )
101 self.model.load_state_dict(state_dict)
102 self.model.eval() # PyTorch inference mode — not Python's built-in eval()
104 @modal.method()
105 def predict(self, text: str) -> dict:
106 """Callable remotely."""
107 return self.model(text)
109 @modal.exit()
110 def teardown(self):
111 """Runs when the container shuts down."""
112 cleanup_resources()
113```
115This lifecycle template requires the project's architecture, preprocessing, and
116checkpoint to be included in the Image or mounted storage. Save a `state_dict`
117and instantiate the matching model before loading it; a loaded weight dictionary
118is not a callable model. For GPU inference, move both model and tensor inputs to
119the same CUDA device. See the [PyTorch checkpoint guide](https://docs.pytorch.org/tutorials/beginner/saving_loading_models.html).

Classes with Lifecycle Hooks

Use @app.cls() for stateful workloads where you want to load resources once:

@app.cls(gpu="L40S", image=image)
class Model:
    @modal.enter()
    def setup(self):
        """Runs once when the container starts."""
        import torch
        self.model = build_model()  # Project-defined architecture matching training
        state_dict = torch.load(
            "/weights/model_state.pt", map_location="cpu", weights_only=True
        )
        self.model.load_state_dict(state_dict)
        self.model.eval()  # PyTorch inference mode — not Python's built-in eval()

    @modal.method()
    def predict(self, text: str) -> dict:
        """Callable remotely."""
        return self.model(text)

    @modal.exit()
    def teardown(self):
        """Runs when the container shuts down."""
        cleanup_resources()

This lifecycle template requires the project's architecture, preprocessing, and checkpoint to be included in the Image or mounted storage. Save a state_dict and instantiate the matching model before loading it; a loaded weight dictionary is not a callable model. For GPU inference, move both model and tensor inputs to the same CUDA device. See the PyTorch checkpoint guide.

Raw lines 86 to 120
121### Lifecycle Decorators
123| Decorator | When It Runs |
124|-----------|-------------|
125| `@modal.enter()` | Once on container startup, before any inputs |
126| `@modal.method()` | For each remote call |
127| `@modal.exit()` | On shutdown/preemption, with a 30-second grace period |
129Checkpoint during work; abrupt failures need not complete cleanup. For a web method,
130use its web decorator instead of stacking it with `@modal.method()`.

Lifecycle Decorators

DecoratorWhen It Runs
@modal.enter()Once on container startup, before any inputs
@modal.method()For each remote call
@modal.exit()On shutdown/preemption, with a 30-second grace period

Checkpoint during work; abrupt failures need not complete cleanup. For a web method, use its web decorator instead of stacking it with @modal.method().

Raw lines 121 to 131
132### Calling Class Methods
134```python
135# Create instance and call method
136model = Model()
137result = model.predict.remote("Hello world")
139# Parallel calls
140results = list(model.predict.map(["text1", "text2", "text3"]))
141```

Calling Class Methods

# Create instance and call method
model = Model()
result = model.predict.remote("Hello world")

# Parallel calls
results = list(model.predict.map(["text1", "text2", "text3"]))
Raw lines 132 to 142
143### Parameterized Classes
145SDK 1.6.0 rejects custom `__init__` methods on Modal classes. Declare parameters as
146below, and initialize resources in `@modal.enter()`.
148```python
149@app.cls()
150class Worker:
151 model_name: str = modal.parameter()
153 @modal.enter()
154 def load(self):
155 self.model = load_model(self.model_name)
157 @modal.method()
158 def run(self, data):
159 return self.model(data)
161# Different model instances autoscale independently
162small = Worker(model_name="small-model")
163large = Worker(model_name="large-model")
164```

Parameterized Classes

SDK 1.6.0 rejects custom __init__ methods on Modal classes. Declare parameters as below, and initialize resources in @modal.enter().

@app.cls()
class Worker:
    model_name: str = modal.parameter()

    @modal.enter()
    def load(self):
        self.model = load_model(self.model_name)

    @modal.method()
    def run(self, data):
        return self.model(data)

# Different model instances autoscale independently
small = Worker(model_name="small-model")
large = Worker(model_name="large-model")
Raw lines 143 to 165
166## Parallel Execution

Parallel Execution

Raw lines 166 to 167
168### `.map()` — Parallel Processing
170Process multiple inputs across containers:
172```python
173@app.function()
174def process(item):
175 return heavy_computation(item)
177@app.local_entrypoint()
178def main():
179 items = list(range(1000))
180 results = list(process.map(items))
181 print(f"Processed {len(results)} items")
182```
184- Results are returned in the same order as inputs
185- Modal autoscales containers to handle the workload
186- Use `return_exceptions=True` to collect errors instead of raising

.map() — Parallel Processing

Process multiple inputs across containers:

@app.function()
def process(item):
    return heavy_computation(item)

@app.local_entrypoint()
def main():
    items = list(range(1000))
    results = list(process.map(items))
    print(f"Processed {len(results)} items")
  • Results are returned in the same order as inputs
  • Modal autoscales containers to handle the workload
  • Use return_exceptions=True to collect errors instead of raising
Raw lines 168 to 187
188### `.starmap()` — Multi-Argument Parallel
190```python
191@app.function()
192def add(x, y):
193 return x + y
195results = list(add.starmap([(1, 2), (3, 4), (5, 6)]))
196# [3, 7, 11]
197```

.starmap() — Multi-Argument Parallel

@app.function()
def add(x, y):
    return x + y

results = list(add.starmap([(1, 2), (3, 4), (5, 6)]))
# [3, 7, 11]
Raw lines 188 to 198
199### `.map()` with `order_outputs=False`
201For faster throughput when order doesn't matter:
203```python
204for result in process.map(items, order_outputs=False):
205 handle(result) # Results arrive as they complete
206```

.map() with order_outputs=False

For faster throughput when order doesn't matter:

for result in process.map(items, order_outputs=False):
    handle(result)  # Results arrive as they complete
Raw lines 199 to 207
208## Async Functions
210Modal supports async/await natively:
212```python
213@app.function(image=modal.Image.debian_slim().uv_pip_install("httpx"))
214async def fetch_data(url: str) -> str:
215 import httpx
216 async with httpx.AsyncClient() as client:
217 response = await client.get(url, timeout=30)
218 response.raise_for_status()
219 return response.text
220```
222Async functions are especially useful with `@modal.concurrent()` for handling multiple requests per container.
223From async caller code use `await fetch_data.remote.aio(url)` and
224`async for result in fetch_data.map.aio(urls)`; a blocking `.remote()` call is not awaitable.

Async Functions

Modal supports async/await natively:

@app.function(image=modal.Image.debian_slim().uv_pip_install("httpx"))
async def fetch_data(url: str) -> str:
    import httpx
    async with httpx.AsyncClient() as client:
        response = await client.get(url, timeout=30)
        response.raise_for_status()
        return response.text

Async functions are especially useful with @modal.concurrent() for handling multiple requests per container. From async caller code use await fetch_data.remote.aio(url) and async for result in fetch_data.map.aio(urls); a blocking .remote() call is not awaitable.

Raw lines 208 to 225
226## Local Entrypoints
228The `@app.local_entrypoint()` runs on your machine and orchestrates remote calls:
230```python
231@app.local_entrypoint()
232def main():
233 # This code runs locally
234 data = load_local_data()
236 # These calls run in the cloud
237 results = list(process.map(data))
239 # Back to local
240 save_results(results)
241```
243You can also define multiple entrypoints and select by function name:
245```bash
246modal run script.py::train
247modal run script.py::evaluate
248```

Local Entrypoints

The @app.local_entrypoint() runs on your machine and orchestrates remote calls:

@app.local_entrypoint()
def main():
    # This code runs locally
    data = load_local_data()

    # These calls run in the cloud
    results = list(process.map(data))

    # Back to local
    save_results(results)

You can also define multiple entrypoints and select by function name:

modal run script.py::train
modal run script.py::evaluate
Raw lines 226 to 249
250## Generators
252Functions can yield results as they're produced:
254```python
255@app.function()
256def generate_data():
257 for i in range(100):
258 yield process(i)
260@app.local_entrypoint()
261def main():
262 for result in generate_data.remote_gen():
263 print(result)
264```

Generators

Functions can yield results as they're produced:

@app.function()
def generate_data():
    for i in range(100):
        yield process(i)

@app.local_entrypoint()
def main():
    for result in generate_data.remote_gen():
        print(result)
Raw lines 250 to 265
266## Retries
268Configure automatic retries on failure:
270```python
271@app.function(retries=3)
272def flaky_operation():
273 ...
274```
276For more control, use `modal.Retries`:
278```python
279@app.function(retries=modal.Retries(max_retries=3, backoff_coefficient=2.0))
280def api_call():
281 ...
282```

Retries

Configure automatic retries on failure:

@app.function(retries=3)
def flaky_operation():
    ...

For more control, use modal.Retries:

@app.function(retries=modal.Retries(max_retries=3, backoff_coefficient=2.0))
def api_call():
    ...
Raw lines 266 to 283
284## Timeouts
286Set maximum execution time:
288```python
289@app.function(timeout=3600) # 1 hour
290def long_training():
291 ...
292```
294Default timeout is 300 seconds (5 minutes). Maximum is 86400 seconds (24 hours).
295This applies to each execution attempt, excluding queue time. Each retry gets a new
296timeout. `startup_timeout` independently bounds initialization; if omitted, `timeout`
297also supplies the startup bound. After retries are exhausted, timeout surfaces as
298`modal.exception.FunctionTimeoutError`.

Timeouts

Set maximum execution time:

@app.function(timeout=3600)  # 1 hour
def long_training():
    ...

Default timeout is 300 seconds (5 minutes). Maximum is 86400 seconds (24 hours). This applies to each execution attempt, excluding queue time. Each retry gets a new timeout. startup_timeout independently bounds initialization; if omitted, timeout also supplies the startup bound. After retries are exhausted, timeout surfaces as modal.exception.FunctionTimeoutError.

Raw lines 284 to 298