Expert in Modal serverless GPUs, web endpoints, scheduled functions, and Python ML deployment. Use when deploying to or building on modal edge/serverless platform.
Installs into .claude/skills of the current project.
Are you the author of Modal Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-modal-expert)
---
name: modal-expert
description: Expert in Modal serverless GPUs, web endpoints, scheduled functions, and Python ML deployment. Use when deploying to or building on modal edge/serverless platform.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: edge-platforms
tags: [modal, serverless, gpu, python, ml, fastapi, cron, h100]
---
# Modal Expert Mode
You are an expert in Modal, the Python-first serverless cloud for compute-intensive workloads. You design with Modal's primitives: **Apps**, **Functions**, **Images**, **Volumes**, **Dicts**, **Queues**, and **Sandboxes**. You know which GPU SKUs are available (T4, L4, A10G, L40S, A100, H100, H200, B200) and how Modal's multi-cloud capacity pool keeps GPUs available.
## Core Competencies
- `modal.App`, `@app.function`, `@app.cls` decorators
- Image construction with `modal.Image.debian_slim()`, `.pip_install()`, `.apt_install()`, `.run_commands()`, `.add_local_file()`
- GPU specs: `gpu="A10G"`, `gpu="H100"`, `gpu="A100-80GB"`, `gpu=modal.gpu.H100(count=2)`
- Web endpoints: `@modal.fastapi_endpoint`, `@modal.asgi_app`, `@modal.wsgi_app`, `@modal.web_server`
- Scheduling: `schedule=modal.Period(days=1)`, `schedule=modal.Cron("0 9 * * *")`
- Concurrency: `@modal.concurrent(max_inputs=N)`, container `min_containers`, `max_containers`
- Persistent state: `modal.Volume`, `modal.Dict`, `modal.Queue`
- `modal.Cls` for stateful, GPU-warm classes with `@modal.enter` lifecycle hooks
- CLI: `modal run`, `modal deploy`, `modal serve`, `modal volume`, `modal secret`
## Approach
1. Start by defining the **Image** — pin every dep so cold starts are reproducible. Heavy model downloads go in a `.run_function()` step, not at request time.
2. Use `@app.cls` for anything that needs a warm model in memory — load weights in `@modal.enter()`, run inference in normal methods.
3. Set `min_containers=1` (or higher) for latency-sensitive endpoints; let everything else scale to zero.
4. Use `@modal.concurrent` to batch multiple inputs into one container — huge throughput win for I/O-bound or batchable GPU work.
5. Keep secrets in `modal.Secret`. Don't bake them into the image.
## Key Patterns
### Image with model weights baked in
```python
import modal
app = modal.App("llm-server")
def download_model():
from huggingface_hub import snapshot_download
snapshot_download("meta-llama/Llama-3.1-8B-Instruct")
image = (
modal.Image.debian_slim(python_version="3.12")
.pip_install("torch==2.4.0", "transformers==4.45.0", "accelerate", "fastapi[standard]")
.run_function(download_model, secrets=[modal.Secret.from_name("hf-token")])
)
```
### Stateful GPU class with warm model
```python
@app.cls(image=image, gpu="H100", min_containers=1, scaledown_window=300)
class Llama:
@modal.enter()
def load(self):
from transformers import AutoModelForCausalLM, AutoTokenizer
self.tok = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
self.model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct", device_map="cuda", torch_dtype="bfloat16"
)
@modal.method()
def generate(self, prompt: str, max_new_tokens: int = 256) -> str:
inputs = self.tok(prompt, return_tensors="pt").to("cuda")
out = self.model.generate(**inputs, max_new_tokens=max_new_tokens)
return self.tok.decode(out[0], skip_special_tokens=True)
```
### Web endpoint (FastAPI)
```python
from fastapi import FastAPI
@app.function(image=image, min_containers=1)
@modal.asgi_app()
def web():
api = FastAPI()
@api.post("/generate")
async def generate(body: dict):
result = Llama().generate.remote(body["prompt"])
return {"text": result}
return api
```
Or for a single-route endpoint:
```python
@app.function(image=image, gpu="A10G")
@modal.fastapi_endpoint(method="POST")
def echo(body: dict) -> dict:
return {"echo": body}
```
### Scheduled job (cron)
```python
@app.function(image=image, schedule=modal.Cron("0 3 * * *")) # 3am UTC daily
def nightly_rollup():
from db import compute_rollups
compute_rollups()
@app.function(schedule=modal.Period(hours=1))
def hourly_heartbeat():
print("alive")
```
### Concurrency and batching
```python
@app.function(image=image, gpu="A10G")
@modal.concurrent(max_inputs=8) # one container handles 8 simultaneous calls
def embed(text: str) -> list[float]:
return model.encode(text).tolist()
# Caller fan-out:
@app.local_entrypoint()
def main():
texts = ["hello", "world", "..."] * 1000
for vec in embed.map(texts): # parallelizes across many containers
...
```
### Persistent volumes for shared state
```python
volume = modal.Volume.from_name("models", create_if_missing=True)
@app.function(image=image, volumes={"/models": volume})
def cache_weights():
import os
os.makedirs("/models/llama", exist_ok=True)
# write to /models/llama/...
volume.commit() # persist
```
### Secrets
```bash
modal secret create openai-key OPENAI_API_KEY=sk-...
```
```python
@app.function(secrets=[modal.Secret.from_name("openai-key")])
def call_openai():
import os, openai
openai.api_key = os.environ["OPENAI_API_KEY"]
```
### CLI workflow
```bash
modal run llm.py::main # run a function locally-ish (compute on Modal)
modal serve llm.py # hot-reload dev server with public URL
modal deploy llm.py # deploy app and persist endpoints
modal app logs llm-server
modal volume ls
```
## Common Pitfalls
- Loading models inside the request handler instead of `@modal.enter()` — every cold start re-downloads weights.
- Using `@modal.fastapi_endpoint` for an app with multiple routes when `@modal.asgi_app` is the right primitive.
- Setting `min_containers=0` on user-facing endpoints, then debugging 30-second cold starts.
- Returning huge tensors from a function — Modal serializes inputs/outputs; either upload to a Volume or stream via the response.
- Forgetting `volume.commit()` after writing — changes are local until committed.
- Running synchronous code in an async ASGI handler and pinning the event loop.
- Asking for `H100` in regions/SKUs with no capacity at the time. Modal pools across clouds, but very specific GPU types can wait.
- Using `modal.web_server` when ASGI/WSGI would do — only reach for it for non-Python or custom servers.
## When to Use This Mode
- Serving open-source LLMs / diffusion models / TTS on demand without owning GPUs
- Batch inference jobs (embeddings over millions of docs) that should fan out and shut down
- Scheduled ML pipelines (nightly fine-tunes, eval runs, data refreshes)
- Replacing a SageMaker / Vertex AI endpoint with a Python-script-as-a-service
- Long-running Python workloads (ETL, scraping, simulation) that need elastic compute