290 lines
7.6 KiB
Markdown
290 lines
7.6 KiB
Markdown
---
|
|
title: "Modal Common Examples"
|
|
task: ""
|
|
lineage_type: import
|
|
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/9c9bd2e9/skills/modal/references/examples.md
|
|
upstream_sha: 9c9bd2e9
|
|
imported_at: 2026-06-27
|
|
prompt_class: prompt
|
|
upstream_changes: accepted
|
|
author: upstream
|
|
validated: false
|
|
---
|
|
|
|
# Modal Common Examples
|
|
|
|
> **Pin dependencies in production.** The version pins below were current at the time of
|
|
> writing; bump them to the versions you have validated. For reproducible builds, pin
|
|
> every package (and ideally use a lockfile) — unpinned installs can pull in breaking or
|
|
> compromised releases.
|
|
|
|
## LLM Inference Service (vLLM)
|
|
|
|
```python
|
|
import modal
|
|
|
|
app = modal.App("vllm-service")
|
|
|
|
image = (
|
|
modal.Image.debian_slim(python_version="3.11")
|
|
.uv_pip_install("vllm==0.21.0")
|
|
)
|
|
|
|
@app.cls(gpu="H100", image=image, min_containers=1)
|
|
class LLMService:
|
|
@modal.enter()
|
|
def load(self):
|
|
from vllm import LLM
|
|
self.llm = LLM(model="meta-llama/Llama-3-70B-Instruct")
|
|
|
|
@modal.method()
|
|
def generate(self, prompt: str, max_tokens: int = 512) -> str:
|
|
from vllm import SamplingParams
|
|
params = SamplingParams(max_tokens=max_tokens, temperature=0.7)
|
|
outputs = self.llm.generate([prompt], params)
|
|
return outputs[0].outputs[0].text
|
|
|
|
@modal.fastapi_endpoint(method="POST")
|
|
def api(self, request: dict):
|
|
text = self.generate(request["prompt"], request.get("max_tokens", 512))
|
|
return {"text": text}
|
|
```
|
|
|
|
## Image Generation (Flux)
|
|
|
|
```python
|
|
import modal
|
|
|
|
app = modal.App("image-gen")
|
|
|
|
image = (
|
|
modal.Image.debian_slim(python_version="3.11")
|
|
.uv_pip_install(
|
|
"diffusers==0.38.0",
|
|
"torch==2.12.0",
|
|
"transformers==5.9.0",
|
|
"accelerate==1.13.0",
|
|
)
|
|
)
|
|
|
|
vol = modal.Volume.from_name("flux-weights", create_if_missing=True)
|
|
|
|
@app.cls(gpu="L40S", image=image, volumes={"/models": vol})
|
|
class ImageGenerator:
|
|
@modal.enter()
|
|
def load(self):
|
|
import torch
|
|
from diffusers import FluxPipeline
|
|
self.pipe = FluxPipeline.from_pretrained(
|
|
"black-forest-labs/FLUX.1-schnell",
|
|
torch_dtype=torch.bfloat16,
|
|
cache_dir="/models",
|
|
).to("cuda")
|
|
|
|
@modal.method()
|
|
def generate(self, prompt: str) -> bytes:
|
|
image = self.pipe(prompt, num_inference_steps=4, guidance_scale=0.0).images[0]
|
|
import io
|
|
buf = io.BytesIO()
|
|
image.save(buf, format="PNG")
|
|
return buf.getvalue()
|
|
```
|
|
|
|
## Speech Transcription (Whisper)
|
|
|
|
```python
|
|
import modal
|
|
|
|
app = modal.App("transcription")
|
|
|
|
image = (
|
|
modal.Image.debian_slim(python_version="3.11")
|
|
.apt_install("ffmpeg")
|
|
.uv_pip_install("openai-whisper==20250625", "torch==2.12.0")
|
|
)
|
|
|
|
@app.cls(gpu="T4", image=image)
|
|
class Transcriber:
|
|
@modal.enter()
|
|
def load(self):
|
|
import whisper
|
|
self.model = whisper.load_model("large-v3")
|
|
|
|
@modal.method()
|
|
def transcribe(self, audio_path: str) -> dict:
|
|
return self.model.transcribe(audio_path)
|
|
```
|
|
|
|
## Batch Data Processing
|
|
|
|
```python
|
|
import modal
|
|
|
|
app = modal.App("batch-processor")
|
|
|
|
image = modal.Image.debian_slim().uv_pip_install("pandas", "pyarrow")
|
|
vol = modal.Volume.from_name("batch-data", create_if_missing=True)
|
|
|
|
@app.function(image=image, volumes={"/data": vol}, cpu=4.0, memory=8192)
|
|
def process_chunk(chunk_id: int) -> dict:
|
|
import pandas as pd
|
|
df = pd.read_parquet(f"/data/input/chunk_{chunk_id:04d}.parquet")
|
|
result = df.groupby("category").agg({"value": ["sum", "mean", "count"]})
|
|
result.to_parquet(f"/data/output/result_{chunk_id:04d}.parquet")
|
|
return {"chunk_id": chunk_id, "rows": len(df)}
|
|
|
|
@app.local_entrypoint()
|
|
def main():
|
|
chunk_ids = list(range(500))
|
|
results = list(process_chunk.map(chunk_ids))
|
|
total = sum(r["rows"] for r in results)
|
|
print(f"Processed {total} total rows across {len(results)} chunks")
|
|
```
|
|
|
|
## Web Scraping at Scale
|
|
|
|
```python
|
|
import modal
|
|
|
|
app = modal.App("scraper")
|
|
|
|
image = modal.Image.debian_slim().uv_pip_install("httpx", "beautifulsoup4")
|
|
|
|
@app.function(image=image, retries=3, timeout=60)
|
|
def scrape_url(url: str) -> dict:
|
|
import httpx
|
|
from bs4 import BeautifulSoup
|
|
response = httpx.get(url, follow_redirects=True, timeout=30)
|
|
soup = BeautifulSoup(response.text, "html.parser")
|
|
return {
|
|
"url": url,
|
|
"title": soup.title.string if soup.title else None,
|
|
"text": soup.get_text()[:5000],
|
|
}
|
|
|
|
@app.local_entrypoint()
|
|
def main():
|
|
urls = ["https://example.com", "https://example.org"] # Your URL list
|
|
results = list(scrape_url.map(urls))
|
|
for r in results:
|
|
print(f"{r['url']}: {r['title']}")
|
|
```
|
|
|
|
## Protein Structure Prediction
|
|
|
|
```python
|
|
import modal
|
|
|
|
app = modal.App("protein-folding")
|
|
|
|
image = (
|
|
modal.Image.debian_slim(python_version="3.11")
|
|
.uv_pip_install("chai-lab")
|
|
)
|
|
|
|
vol = modal.Volume.from_name("protein-data", create_if_missing=True)
|
|
|
|
@app.function(gpu="A100-80GB", image=image, volumes={"/data": vol}, timeout=3600)
|
|
def fold_protein(sequence: str) -> str:
|
|
from chai_lab.chai1 import run_inference
|
|
output = run_inference(
|
|
fasta_file=write_fasta(sequence, "/data/input.fasta"),
|
|
output_dir="/data/output/",
|
|
)
|
|
return str(output)
|
|
```
|
|
|
|
## Scheduled ETL Pipeline
|
|
|
|
```python
|
|
import modal
|
|
|
|
app = modal.App("etl")
|
|
|
|
image = modal.Image.debian_slim().uv_pip_install("pandas", "sqlalchemy", "psycopg2-binary")
|
|
|
|
@app.function(
|
|
image=image,
|
|
schedule=modal.Cron("0 3 * * *"), # 3 AM UTC daily
|
|
secrets=[modal.Secret.from_name("database-creds")],
|
|
timeout=7200,
|
|
)
|
|
def daily_etl():
|
|
import os
|
|
import pandas as pd
|
|
from sqlalchemy import create_engine
|
|
|
|
source = create_engine(os.environ["SOURCE_DB"])
|
|
dest = create_engine(os.environ["DEST_DB"])
|
|
|
|
df = pd.read_sql("SELECT * FROM events WHERE date = CURRENT_DATE - 1", source)
|
|
df = transform(df)
|
|
df.to_sql("daily_summary", dest, if_exists="append", index=False)
|
|
print(f"Loaded {len(df)} rows")
|
|
```
|
|
|
|
## FastAPI with GPU Model
|
|
|
|
```python
|
|
import modal
|
|
|
|
app = modal.App("api-with-gpu")
|
|
|
|
image = (
|
|
modal.Image.debian_slim(python_version="3.11")
|
|
.uv_pip_install("fastapi==0.136.3", "sentence-transformers==5.5.1", "torch==2.12.0")
|
|
)
|
|
|
|
@app.cls(gpu="L40S", image=image, min_containers=1)
|
|
class EmbeddingService:
|
|
@modal.enter()
|
|
def load(self):
|
|
from sentence_transformers import SentenceTransformer
|
|
self.model = SentenceTransformer("all-MiniLM-L6-v2", device="cuda")
|
|
|
|
@modal.asgi_app()
|
|
def serve(self):
|
|
from fastapi import FastAPI
|
|
api = FastAPI()
|
|
|
|
@api.post("/embed")
|
|
async def embed(request: dict):
|
|
embeddings = self.model.encode(request["texts"])
|
|
return {"embeddings": embeddings.tolist()}
|
|
|
|
@api.get("/health")
|
|
async def health():
|
|
return {"status": "ok"}
|
|
|
|
return api
|
|
```
|
|
|
|
## Document OCR Job Queue
|
|
|
|
```python
|
|
import modal
|
|
|
|
app = modal.App("ocr-queue")
|
|
|
|
image = modal.Image.debian_slim().uv_pip_install("pytesseract", "Pillow").apt_install("tesseract-ocr")
|
|
vol = modal.Volume.from_name("ocr-data", create_if_missing=True)
|
|
|
|
@app.function(image=image, volumes={"/data": vol})
|
|
def ocr_page(image_path: str) -> str:
|
|
import pytesseract
|
|
from PIL import Image
|
|
img = Image.open(image_path)
|
|
return pytesseract.image_to_string(img)
|
|
|
|
@app.function(volumes={"/data": vol})
|
|
def process_document(doc_id: str):
|
|
import os
|
|
pages = sorted(os.listdir(f"/data/docs/{doc_id}/"))
|
|
paths = [f"/data/docs/{doc_id}/{p}" for p in pages]
|
|
texts = list(ocr_page.map(paths))
|
|
full_text = "\n\n".join(texts)
|
|
with open(f"/data/results/{doc_id}.txt", "w") as f:
|
|
f.write(full_text)
|
|
return {"doc_id": doc_id, "pages": len(texts)}
|
|
```
|