Faire tourner l'inférence d'un modèle open source sur votre propre machine est amusant un week-end. Le faire pour des utilisateurs payants est un autre problème : il vous faut des GPU qui s'éveillent à la demande, descendent à zéro quand personne n'appelle, et vous facturent à la seconde plutôt qu'au mois. La réponse traditionnelle consiste à louer un cluster Kubernetes, installer Triton, configurer l'autoscaling, et perdre un quart du temps de l'équipe ingénierie dans l'ops. La réponse adoptée par la plupart des startups d'IA en 2025 est Modal Labs.
Modal vous permet de décorer une fonction Python, de la pointer vers un GPU, et de la livrer. Aucun Dockerfile à maintenir, aucun cluster à opérer, aucune gymnastique de démarrage à froid à coder. Vous écrivez du Python, vous tapez modal deploy, et une flotte Firecracker globalement distribuée fait tourner votre code sur un H100 en moins de cinq secondes. Ce tutoriel parcourt le workflow complet de bout en bout et se termine sur une API de transcription Whisper fonctionnelle que vous pouvez appeler depuis n'importe quel client.
Ce que vous allez construire
Un service de transcription Whisper serverless qui :
- Accepte une URL audio par HTTP
- Charge le modèle Whisper
large-v3sur un GPU A10G - Met en cache les poids du modèle pour que les appels suivants atteignent un conteneur chaud
- Retourne des transcriptions horodatées au format JSON
- Se redimensionne automatiquement de zéro à plusieurs conteneurs simultanés
- Coûte environ deux centimes par minute d'audio traitée
À la fin, vous comprendrez le modèle mental de Modal : images, fonctions, volumes, secrets, et endpoints web — et vous aurez livré une vraie API GPU que vous pouvez confier à un frontend.
Prérequis
- Python 3.11 ou supérieur
- Un compte Modal sur modal.com — le palier gratuit inclut trente dollars de crédit mensuel, largement suffisants pour ce tutoriel
- Familiarité de base avec les décorateurs Python et les annotations de type
- Un terminal et un éditeur de code
Étape 1 : Installer Modal et s'authentifier
Modal se livre en un seul paquet PyPI. Installez-le dans un environnement virtuel neuf.
python -m venv .venv
source .venv/bin/activate
pip install modalAuthentifiez-vous auprès de votre compte. La commande ouvre une fenêtre de navigateur et écrit un jeton dans ~/.modal.toml.
modal setupVérifiez l'installation avec une commande intégrée.
modal profile currentVous devriez voir le nom de votre workspace s'afficher. Si c'est le cas, le SDK est prêt à déployer.
Étape 2 : Votre première fonction Modal
Créez un fichier nommé hello.py. Le modèle mental est simple : chaque application Modal commence par un objet App, et chaque fonction que vous voulez exécuter à distance est décorée par @app.function.
import modal
app = modal.App("hello-modal")
@app.function()
def square(x: int) -> int:
return x * x
@app.local_entrypoint()
def main():
print(square.remote(7))Exécutez-le.
modal run hello.pyLa première invocation prend quelques secondes parce que Modal construit une image de conteneur, la pousse vers son registre, la planifie sur un worker, et diffuse les logs vers votre terminal. Les exécutions suivantes réutilisent l'image et le conteneur chaud, terminant bien en dessous d'une seconde. La sortie est 49.
Le détail important est square.remote(7). .local() exécuterait la fonction dans votre processus. .remote() envoie l'appel vers l'infrastructure Modal. Tout dans ce tutoriel repose sur cette unique distinction.
Étape 3 : Définir une image personnalisée
Whisper a besoin de PyTorch, du paquet openai-whisper et de ffmpeg. Modal vous laisse décrire l'image de manière déclarative avec un builder fluide. Aucun Dockerfile requis.
Créez whisper_api.py.
import modal
image = (
modal.Image.debian_slim(python_version="3.11")
.apt_install("ffmpeg")
.pip_install(
"openai-whisper==20240930",
"torch==2.5.1",
"requests==2.32.3",
)
)
app = modal.App("whisper-api", image=image)Modal hache la spécification de l'image et met en cache le résultat. Le premier déploiement construit et pousse les couches ; les déploiements suivants les réutilisent. Vous pouvez surcharger l'image par fonction si un petit endpoint n'a pas besoin de la lourde pile ML.
Étape 4 : Choisir un GPU et monter un volume
Le modèle Whisper large-v3 pèse environ trois gigaoctets. Le télécharger à chaque démarrage à froid serait du gâchis. Modal résout cela avec les Volumes, du stockage bloc persistant que n'importe quelle fonction peut monter à un chemin.
Ajoutez ce qui suit à whisper_api.py.
model_cache = modal.Volume.from_name("whisper-cache", create_if_missing=True)
@app.function(
gpu="A10G",
volumes={"/cache": model_cache},
timeout=600,
)
def transcribe(audio_url: str) -> dict:
import whisper
import requests
import tempfile
import os
os.environ["XDG_CACHE_HOME"] = "/cache"
model = whisper.load_model("large-v3", download_root="/cache/whisper")
response = requests.get(audio_url, timeout=120)
response.raise_for_status()
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
f.write(response.content)
audio_path = f.name
result = model.transcribe(audio_path, verbose=False)
return {
"text": result["text"],
"segments": [
{"start": s["start"], "end": s["end"], "text": s["text"]}
for s in result["segments"]
],
"language": result["language"],
}Plusieurs choses méritent attention. L'argument gpu="A10G" choisit une carte Ampere de vingt-quatre gigaoctets, bon marché, rapide, et surdimensionnée pour large-v3. Vous pouvez aussi choisir T4, L4, A100, H100, ou même H100:8 pour du multi-GPU. Le mapping volumes monte le volume persistant à /cache dans le conteneur, et XDG_CACHE_HOME redirige PyTorch et Whisper pour y écrire les poids. Le premier appel télécharge trois gigaoctets ; chaque appel suivant lit depuis le cache en millisecondes.
Le timeout=600 relève le plafond par appel à dix minutes parce que les longs fichiers audio prennent du temps à transcrire.
Étape 5 : Exposer un endpoint web
Les fonctions Modal peuvent être appelées depuis d'autres morceaux de code Python, mais le motif le plus courant est de les exposer en endpoints HTTPS. Le décorateur @modal.fastapi_endpoint transforme une fonction en URL publique.
Ajoutez ce bloc à whisper_api.py.
@app.function(image=image)
@modal.fastapi_endpoint(method="POST", docs=True)
def transcribe_endpoint(audio_url: str):
return transcribe.remote(audio_url)Déployez l'application.
modal deploy whisper_api.pyModal affiche une URL du type https://your-workspace--whisper-api-transcribe-endpoint.modal.run. Le flag docs=True signifie qu'en ajoutant /docs à cette URL vous obtenez gratuitement une interface Swagger interactive.
Testez-le depuis votre terminal.
curl -X POST "https://your-workspace--whisper-api-transcribe-endpoint.modal.run?audio_url=https://example.com/sample.mp3"Le premier appel prend environ vingt secondes pendant que Modal provisionne un GPU et télécharge le modèle. Les appels suivants à l'intérieur de la fenêtre de maintien à chaud se terminent en deux à quatre secondes pour un clip d'une minute.
Étape 6 : Concurrence, conteneurs et mise à l'échelle
Par défaut, chaque conteneur traite une requête à la fois. Whisper est limité par le GPU et bénéficie du batching, mais pour plus de clarté nous garderons un-par-conteneur et laisserons Modal redimensionner horizontalement.
Modifiez le décorateur.
@app.function(
gpu="A10G",
volumes={"/cache": model_cache},
timeout=600,
min_containers=1,
max_containers=20,
scaledown_window=300,
)
def transcribe(audio_url: str) -> dict:
...Les trois boutons qui comptent :
min_containers=1garde un conteneur toujours chaud. Les démarrages à froid disparaissent au prix de faire tourner un GPU en continu. Descendez à zéro pour du pur paiement à l'usage.max_containers=20plafonne l'autoscaler. Garde-fou utile contre le trafic emballé et les factures emballées.scaledown_window=300garde les conteneurs vivants pendant cinq minutes après leur dernière requête, pour que le trafic en rafale ne paie pas la taxe du démarrage à froid à répétition.
Pour batcher plusieurs requêtes dans un seul conteneur, utilisez @modal.concurrent(max_inputs=4) sur la fonction. Whisper le gère bien parce que la mémoire GPU dépasse largement le batch audio.
Étape 7 : Ajouter des secrets
Les vraies API nécessitent une authentification. Modal stocke les secrets dans un coffre managé et les injecte comme variables d'environnement à l'exécution. Créez un secret depuis le dashboard ou en CLI.
modal secret create whisper-api API_KEY=your-long-random-token-hereRattachez-le à l'endpoint et validez à l'intérieur.
from fastapi import HTTPException, Header
import os
@app.function(
image=image,
secrets=[modal.Secret.from_name("whisper-api")],
)
@modal.fastapi_endpoint(method="POST")
def transcribe_endpoint(
audio_url: str,
authorization: str = Header(None),
):
expected = f"Bearer {os.environ['API_KEY']}"
if authorization != expected:
raise HTTPException(status_code=401, detail="Invalid API key")
return transcribe.remote(audio_url)Redéployez. Les appels sans le header Authorization correct retournent désormais 401.
Étape 8 : Tâches planifiées
Un deuxième cas d'usage courant pour Modal est le travail batch façon cron. Le décorateur @app.function(schedule=...) s'en occupe sans infrastructure externe.
@app.function(schedule=modal.Cron("0 3 * * *"))
def nightly_cleanup():
import datetime
print(f"Running cleanup at {datetime.datetime.now()}")Après le déploiement, Modal exécute cette fonction à 03:00 UTC chaque jour. La fonction hérite de l'image de l'app ; vous pouvez surcharger par fonction avec un argument image= séparé. Pas de calendrier à configurer, pas de DAG Airflow à écrire — le décorateur est le planificateur.
Étape 9 : Boucle de développement local
Itérer contre un GPU distant a l'air lent. Modal résout la boucle avec modal serve.
modal serve whisper_api.pyCela déploie une version éphémère qui se recharge à chaque sauvegarde de fichier, affiche les logs en local, et vous donne une URL temporaire. Vous éditez, vous sauvegardez, et la requête suivante atteint le nouveau code en environ deux secondes. C'est l'expérience de développement GPU serverless la plus rapide disponible en 2026.
Utilisez modal serve pendant la construction, puis modal deploy une fois prêt pour la production.
Étape 10 : Observabilité et logs
Chaque invocation de fonction produit des logs structurés consultables à trois endroits.
Le flux CLI pendant le développement.
modal app logs whisper-apiLe dashboard web sur modal.com/apps, avec des onglets par fonction pour les invocations, les erreurs, et l'utilisation GPU.
L'accès programmatique via le SDK Python modal si vous voulez envoyer les logs vers votre propre stack d'observabilité comme Datadog ou Grafana.
Pour des traces plus profondes, enveloppez les sections critiques dans des blocs with modal.Status("doing thing"): ; le texte de statut apparaît dans la timeline du dashboard pour que vous voyiez quelle étape d'une fonction longue tourne actuellement.
Tester votre implémentation
Exécutez le workflow complet contre un vrai échantillon audio.
modal deploy whisper_api.py
curl -X POST \
-H "Authorization: Bearer your-long-random-token-here" \
"https://your-workspace--whisper-api-transcribe-endpoint.modal.run?audio_url=https://github.com/openai/whisper/raw/main/tests/jfk.flac"Vous devriez recevoir un JSON avec la transcription complète, une liste de segments horodatés, et la langue détectée. Chemin froid : environ vingt secondes. Chemin chaud : environ trois secondes pour le clip JFK de quinze secondes.
Vérifiez le dashboard. Vous devriez voir l'invocation, le graphique d'utilisation GPU qui pique pendant la transcription, et un coût par appel d'environ un demi-centime.
Dépannage
Le premier déploiement bloque à "Building image". Modal construit l'image PyTorch, qui est lourde. Comptez trois à cinq minutes au premier build. Les déploiements suivants réutilisent les couches en cache et terminent en quelques secondes.
Erreurs CUDA out of memory. Passez le GPU à une carte plus grosse comme A100 ou utilisez whisper.load_model("medium") au lieu de large-v3 pendant le debug.
Les démarrages à froid restent lents malgré le cache. Le montage de volume est rapide, mais PyTorch importe les bibliothèques CUDA, ce qui prend plusieurs secondes. Utilisez min_containers=1 pour garder un conteneur chaud pour les endpoints sensibles à la latence.
modal deploy échoue avec des erreurs d'import en local. Modal n'importe pas votre fichier de la même manière que Python. Les imports à l'intérieur du corps de la fonction, comme import whisper dans transcribe, tournent dans le conteneur, pas en local, c'est pourquoi nous y mettons les imports lourds.
Étapes suivantes
- Ajoutez un webhook pour la transcription asynchrone de longs fichiers : retournez immédiatement un identifiant de job et faites en sorte qu'une deuxième fonction POST le résultat à une URL de callback une fois fini.
- Diffusez des transcriptions partielles en passant à
faster-whisperet en utilisant le supportyieldde Modal dans les générateurs. - Posez un frontend Next.js ou Streamlit par-dessus — les endpoints Modal sont du HTTPS ordinaire, donc n'importe quel client peut les appeler.
- Explorez les agents et workflows Mastra AI pour orchestrer les endpoints Modal depuis un agent TypeScript.
Conclusion
Modal Labs réduit l'histoire du déploiement GPU de "monter Kubernetes, configurer l'autoscaling, se battre avec les registres de conteneurs" à "décorer une fonction Python et lancer modal deploy". Pour l'inférence d'IA, les jobs batch planifiés, et toute charge qui a besoin de pointes GPU occasionnelles, Modal supprime plus de friction qu'aucune autre plateforme du marché en 2026.
Vous avez maintenant un motif prêt pour la production : images déclaratives, volumes persistants pour les caches de modèles, sélection de GPU par fonction, endpoints web avec secrets, contrôles d'autoscaling, et une boucle de développement local serrée. Appliquez-le à votre prochaine fonctionnalité d'IA et évitez le cluster.