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

examples.md

333 lines · skills/modal/references/examples.md at 5f2bc03

Open on GitHub
1# Modal Common Examples
2
3Reviewed against Modal 1.6.0. These are **illustrative cloud workload templates**:
4SDK registration and selected local handlers were checked, but container builds,
5GPU jobs, weights and database writes were not executed. Package pins are examples,
6not a validated combined environment. Resolve a lockfile per workload and record the
7model revision, hardware, seeds and input provenance before a scientific run.
8External helpers such as `transform()` are application-defined.
9

Modal Common Examples

Reviewed against Modal 1.6.0. These are illustrative cloud workload templates: SDK registration and selected local handlers were checked, but container builds, GPU jobs, weights and database writes were not executed. Package pins are examples, not a validated combined environment. Resolve a lockfile per workload and record the model revision, hardware, seeds and input provenance before a scientific run. External helpers such as transform() are application-defined.

Raw lines 1 to 9
10## LLM Inference Service (vLLM)
12```python
13import modal
15app = modal.App("vllm-service")
17image = (
18 modal.Image.debian_slim(python_version="3.11")
19 .uv_pip_install("vllm==0.21.0", "fastapi[standard]==0.136.3")
22@app.cls(gpu="H100", image=image, max_containers=1)
23class LLMService:
24 @modal.enter()
25 def load(self):
26 from vllm import LLM
27 self.llm = LLM(model="Qwen/Qwen3-8B", max_model_len=4096)
29 @modal.method()
30 def generate(self, prompt: str, max_tokens: int = 512) -> str:
31 from vllm import SamplingParams
32 params = SamplingParams(max_tokens=max_tokens, temperature=0.7)
33 outputs = self.llm.generate([prompt], sampling_params=params)
34 return outputs[0].outputs[0].text
36 @modal.fastapi_endpoint(method="POST", requires_proxy_auth=True)
37 def api(self, request: dict):
38 text = self.generate.local(request["prompt"], request.get("max_tokens", 512))
39 return {"text": text}
40```
42[vLLM `LLM.generate`](https://docs.vllm.ai/en/latest/serving/offline_inference/) uses
43`SamplingParams`; `max_tokens` is not a `generate()` keyword. This template accepts
44a preformatted completion prompt. Apply the model's chat template for chat messages.
45Use your Proxy Token to call the endpoint. A 70B BF16 model does not fit one H100;
46GPU capacity must include weights and KV cache, not just parameter count.

LLM Inference Service (vLLM)

import modal

app = modal.App("vllm-service")

image = (
    modal.Image.debian_slim(python_version="3.11")
    .uv_pip_install("vllm==0.21.0", "fastapi[standard]==0.136.3")
)

@app.cls(gpu="H100", image=image, max_containers=1)
class LLMService:
    @modal.enter()
    def load(self):
        from vllm import LLM
        self.llm = LLM(model="Qwen/Qwen3-8B", max_model_len=4096)

    @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], sampling_params=params)
        return outputs[0].outputs[0].text

    @modal.fastapi_endpoint(method="POST", requires_proxy_auth=True)
    def api(self, request: dict):
        text = self.generate.local(request["prompt"], request.get("max_tokens", 512))
        return {"text": text}

[vLLM LLM.generate](https://docs.vllm.ai/en/latest/serving/offline_inference/) uses SamplingParams; max_tokens is not a generate() keyword. This template accepts a preformatted completion prompt. Apply the model's chat template for chat messages. Use your Proxy Token to call the endpoint. A 70B BF16 model does not fit one H100; GPU capacity must include weights and KV cache, not just parameter count.

Raw lines 10 to 47
48## Image Generation (Flux)
50```python
51import modal
53app = modal.App("image-gen")
55image = (
56 modal.Image.debian_slim(python_version="3.11")
57 .uv_pip_install(
58 "diffusers==0.38.0",
59 "torch==2.12.0",
60 "transformers==5.9.0",
61 "accelerate==1.13.0",
62 )
65vol = modal.Volume.from_name("flux-weights", create_if_missing=True)
67@app.cls(gpu="L40S", image=image, volumes={"/models": vol})
68class ImageGenerator:
69 @modal.enter()
70 def load(self):
71 import torch
72 from diffusers import FluxPipeline
73 self.pipe = FluxPipeline.from_pretrained(
74 "black-forest-labs/FLUX.1-schnell",
75 torch_dtype=torch.bfloat16,
76 cache_dir="/models",
77 ).to("cuda")
78 vol.commit()
80 @modal.method()
81 def generate(self, prompt: str) -> bytes:
82 image = self.pipe(prompt, num_inference_steps=4, guidance_scale=0.0).images[0]
83 import io
84 buf = io.BytesIO()
85 image.save(buf, format="PNG")
86 return buf.getvalue()
87```

Image Generation (Flux)

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")
        vol.commit()

    @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()
Raw lines 48 to 88
89## Speech Transcription (Whisper)
91The audio path must exist inside the container. Upload it to the named Volume
92first; passing a local laptop path does not transfer audio.
94```python
95import modal
97app = modal.App("transcription")
99image = (
100 modal.Image.debian_slim(python_version="3.11")
101 .apt_install("ffmpeg")
102 .uv_pip_install("openai-whisper==20250625", "torch==2.12.0")
105audio_vol = modal.Volume.from_name("audio-data", create_if_missing=True)
107@app.cls(gpu="T4", image=image, volumes={"/audio": audio_vol})
108class Transcriber:
109 @modal.enter()
110 def load(self):
111 import whisper
112 self.model = whisper.load_model("large-v3")
114 @modal.method()
115 def transcribe(self, audio_path: str) -> dict:
116 audio_vol.reload()
117 return self.model.transcribe(audio_path)
118```

Speech Transcription (Whisper)

The audio path must exist inside the container. Upload it to the named Volume first; passing a local laptop path does not transfer audio.

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")
)

audio_vol = modal.Volume.from_name("audio-data", create_if_missing=True)

@app.cls(gpu="T4", image=image, volumes={"/audio": audio_vol})
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:
        audio_vol.reload()
        return self.model.transcribe(audio_path)
Raw lines 89 to 119
120## Batch Data Processing
122```python
123import modal
125app = modal.App("batch-processor")
127image = modal.Image.debian_slim().uv_pip_install("pandas", "pyarrow")
128vol = modal.Volume.from_name("batch-data", create_if_missing=True)
130@app.function(image=image, volumes={"/data": vol}, cpu=4.0, memory=8192)
131def process_chunk(chunk_id: int) -> dict:
132 import pandas as pd
133 from pathlib import Path
134 from uuid import uuid4
135 vol.reload()
136 df = pd.read_parquet(f"/data/input/chunk_{chunk_id:04d}.parquet")
137 result = df.groupby("category").agg({"value": ["sum", "mean", "count"]})
138 output_dir = Path("/data/output") / uuid4().hex
139 output_dir.mkdir(parents=True)
140 output = output_dir / f"result_{chunk_id:04d}.parquet"
141 result.to_parquet(output)
142 vol.commit()
143 return {"chunk_id": chunk_id, "rows": len(df), "output": str(output)}
145@app.local_entrypoint()
146def main():
147 chunk_ids = list(range(500))
148 results = list(process_chunk.map(chunk_ids))
149 total = sum(r["rows"] for r in results)
150 print(f"Processed {total} total rows across {len(results)} chunks")
151```

Batch Data Processing

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
    from pathlib import Path
    from uuid import uuid4
    vol.reload()
    df = pd.read_parquet(f"/data/input/chunk_{chunk_id:04d}.parquet")
    result = df.groupby("category").agg({"value": ["sum", "mean", "count"]})
    output_dir = Path("/data/output") / uuid4().hex
    output_dir.mkdir(parents=True)
    output = output_dir / f"result_{chunk_id:04d}.parquet"
    result.to_parquet(output)
    vol.commit()
    return {"chunk_id": chunk_id, "rows": len(df), "output": str(output)}

@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")
Raw lines 120 to 152
153## Web Scraping at Scale
155```python
156import modal
158app = modal.App("scraper")
160image = modal.Image.debian_slim().uv_pip_install("httpx", "beautifulsoup4")
162@app.function(image=image, retries=3, timeout=60)
163def scrape_url(url: str) -> dict:
164 import httpx
165 from bs4 import BeautifulSoup
166 response = httpx.get(url, follow_redirects=True, timeout=30)
167 response.raise_for_status()
168 soup = BeautifulSoup(response.text, "html.parser")
169 return {
170 "url": url,
171 "title": soup.title.string if soup.title else None,
172 "text": soup.get_text()[:5000],
173 }
175@app.local_entrypoint()
176def main():
177 urls = ["https://example.com", "https://example.org"] # Your URL list
178 results = list(scrape_url.map(urls))
179 for r in results:
180 print(f"{r['url']}: {r['title']}")
181```

Web Scraping at Scale

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)
    response.raise_for_status()
    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']}")
Raw lines 153 to 182
183## Protein Structure Prediction
185```python
186import modal
188app = modal.App("protein-folding")
190image = (
191 modal.Image.debian_slim(python_version="3.11")
192 .uv_pip_install("chai-lab")
195vol = modal.Volume.from_name("protein-data", create_if_missing=True)
197@app.function(gpu="A100-80GB", image=image, volumes={"/data": vol}, timeout=3600)
198def fold_protein(sequence: str) -> list[str]:
199 from pathlib import Path
200 from uuid import uuid4
201 from chai_lab.chai1 import run_inference
202 if not sequence or any(c not in "ACDEFGHIKLMNPQRSTVWY" for c in sequence):
203 raise ValueError("Provide one non-empty canonical protein sequence")
204 run_dir = Path("/data/runs") / uuid4().hex
205 run_dir.mkdir(parents=True)
206 fasta = run_dir / "input.fasta"
207 fasta.write_text(f">protein|name=target\n{sequence}\n")
208 candidates = run_inference(
209 fasta_file=fasta,
210 output_dir=run_dir / "predictions",
211 use_msa_server=False,
212 use_templates_server=False,
213 seed=42,
214 device="cuda:0",
215 )
216 vol.commit()
217 return [str(path) for path in candidates.cif_paths]
218```
220The [Chai API](https://github.com/chaidiscovery/chai-lab/blob/main/chai_lab/chai1.py)
221accepts `Path` objects and returns `StructureCandidates`, not an output filename.
222Server-based MSA/template search is disabled here; enabling it sends sequence data
223to another service. Predicted structures and confidence scores require scientific
224validation; they are not experimental evidence.

Protein Structure Prediction

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) -> list[str]:
    from pathlib import Path
    from uuid import uuid4
    from chai_lab.chai1 import run_inference
    if not sequence or any(c not in "ACDEFGHIKLMNPQRSTVWY" for c in sequence):
        raise ValueError("Provide one non-empty canonical protein sequence")
    run_dir = Path("/data/runs") / uuid4().hex
    run_dir.mkdir(parents=True)
    fasta = run_dir / "input.fasta"
    fasta.write_text(f">protein|name=target\n{sequence}\n")
    candidates = run_inference(
        fasta_file=fasta,
        output_dir=run_dir / "predictions",
        use_msa_server=False,
        use_templates_server=False,
        seed=42,
        device="cuda:0",
    )
    vol.commit()
    return [str(path) for path in candidates.cif_paths]

The Chai API accepts Path objects and returns StructureCandidates, not an output filename. Server-based MSA/template search is disabled here; enabling it sends sequence data to another service. Predicted structures and confidence scores require scientific validation; they are not experimental evidence.

Raw lines 183 to 225
226## Scheduled ETL Pipeline
228```python
229import modal
231app = modal.App("etl")
233image = modal.Image.debian_slim().uv_pip_install("pandas", "sqlalchemy", "psycopg2-binary")
235@app.function(
236 image=image,
237 schedule=modal.Cron("0 3 * * *"), # 3 AM UTC daily
238 secrets=[modal.Secret.from_name("database-creds")],
239 timeout=7200,
241def daily_etl():
242 import os
243 import pandas as pd
244 from sqlalchemy import create_engine
246 source = create_engine(os.environ["SOURCE_DB"])
247 dest = create_engine(os.environ["DEST_DB"])
249 df = pd.read_sql("SELECT * FROM events WHERE date = CURRENT_DATE - 1", source)
250 df = transform(df)
251 # Illustrative append: add a date/run key and transactional upsert in production
252 # so redeploys, retries or manual reruns cannot duplicate observations.
253 df.to_sql("daily_summary", dest, if_exists="append", index=False)
254 print(f"Loaded {len(df)} rows")
255```

Scheduled ETL Pipeline

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)
    # Illustrative append: add a date/run key and transactional upsert in production
    # so redeploys, retries or manual reruns cannot duplicate observations.
    df.to_sql("daily_summary", dest, if_exists="append", index=False)
    print(f"Loaded {len(df)} rows")
Raw lines 226 to 256
257## FastAPI with GPU Model
259```python
260import modal
262app = modal.App("api-with-gpu")
264image = (
265 modal.Image.debian_slim(python_version="3.11")
266 .uv_pip_install("fastapi[standard]==0.136.3", "sentence-transformers==5.5.1", "torch==2.12.0")
269@app.cls(gpu="L40S", image=image, min_containers=1)
270class EmbeddingService:
271 @modal.enter()
272 def load(self):
273 from sentence_transformers import SentenceTransformer
274 self.model = SentenceTransformer("all-MiniLM-L6-v2", device="cuda")
276 @modal.asgi_app(requires_proxy_auth=True)
277 def serve(self):
278 from fastapi import FastAPI
279 api = FastAPI()
281 @api.post("/embed")
282 def embed(request: dict):
283 embeddings = self.model.encode(request["texts"])
284 return {"embeddings": embeddings.tolist()}
286 @api.get("/health")
287 async def health():
288 return {"status": "ok"}
290 return api
291```

FastAPI with GPU Model

import modal

app = modal.App("api-with-gpu")

image = (
    modal.Image.debian_slim(python_version="3.11")
    .uv_pip_install("fastapi[standard]==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(requires_proxy_auth=True)
    def serve(self):
        from fastapi import FastAPI
        api = FastAPI()

        @api.post("/embed")
        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
Raw lines 257 to 292
293## Document OCR Job Queue
295```python
296import modal
298app = modal.App("ocr-queue")
300image = modal.Image.debian_slim().uv_pip_install("pytesseract", "Pillow").apt_install("tesseract-ocr")
301vol = modal.Volume.from_name("ocr-data", create_if_missing=True)
303@app.function(image=image, volumes={"/data": vol})
304def ocr_page(image_path: str) -> str:
305 import pytesseract
306 from PIL import Image
307 vol.reload()
308 with Image.open(image_path) as img:
309 return pytesseract.image_to_string(img)
311@app.function(volumes={"/data": vol})
312def process_document(doc_id: str):
313 import os
314 from uuid import uuid4
315 if not doc_id or any(c not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for c in doc_id):
316 raise ValueError("Invalid document ID")
317 vol.reload()
318 pages = sorted(os.listdir(f"/data/docs/{doc_id}/"))
319 paths = [f"/data/docs/{doc_id}/{p}" for p in pages]
320 texts = list(ocr_page.map(paths))
321 full_text = "\n\n".join(texts)
322 os.makedirs("/data/results", exist_ok=True)
323 output = f"/data/results/{doc_id}-{uuid4().hex}.txt"
324 with open(output, "w") as f:
325 f.write(full_text)
326 vol.commit()
327 return {"doc_id": doc_id, "pages": len(texts), "output": output}
328```
330Additional upstream contracts reviewed: [FLUX](https://huggingface.co/docs/diffusers/api/pipelines/flux),
331[Whisper](https://github.com/openai/whisper), [Sentence Transformers](https://sbert.net/docs/quickstart.html).
332For pretrained weights, verify licensing/access and pin revisions; the GPU examples
333remain unexecuted and do not establish numerical reproducibility or performance.

Document OCR Job Queue

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
    vol.reload()
    with Image.open(image_path) as img:
        return pytesseract.image_to_string(img)

@app.function(volumes={"/data": vol})
def process_document(doc_id: str):
    import os
    from uuid import uuid4
    if not doc_id or any(c not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for c in doc_id):
        raise ValueError("Invalid document ID")
    vol.reload()
    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)
    os.makedirs("/data/results", exist_ok=True)
    output = f"/data/results/{doc_id}-{uuid4().hex}.txt"
    with open(output, "w") as f:
        f.write(full_text)
    vol.commit()
    return {"doc_id": doc_id, "pages": len(texts), "output": output}

Additional upstream contracts reviewed: FLUX, Whisper, Sentence Transformers. For pretrained weights, verify licensing/access and pin revisions; the GPU examples remain unexecuted and do not establish numerical reproducibility or performance.

Raw lines 293 to 333