Vue d’ensemble : MLOps moderne : du modèle entraîné à la production scalable
Le problème : training/serving skew
Imaginez une équipe data qui travaille sur un modèle de détection de fraude. Les data scientists écrivent un notebook qui agrège, pour chaque transaction, le nombre de paiements émis par le même utilisateur sur les 7 derniers jours. Ils utilisent un GROUP BY sur une table BigQuery avec une fenêtre glissante. Le modèle obtient un AUC de 0.93. Bravo.
Au moment du déploiement, l’équipe backend reçoit la consigne : « calculez la même feature en temps réel pour chaque transaction entrante ». L’ingénieur backend, en Node.js, écrit sa propre requête SQL avec un fuseau horaire différent, oublie d’exclure les transactions annulées et arrondit la fenêtre à 7 jours civils au lieu de 7 × 24 heures glissantes. Résultat : la feature servie au modèle en production n’a plus la même distribution qu’à l’entraînement. Le modèle se met à classer mal, sans qu’aucune alerte ne se déclenche.
Ce désalignement entre les features d’entraînement et les features de production porte un nom : training/serving skew. C’est l’une des causes les plus citées d’échec silencieux des modèles en production. Le feature store résout le problème en exposant une seule définition des features, consommable par les deux mondes.
Architecture de Feast
Feast est un feature store open-source, écrit principalement en Python et Go, sponsorisé par la fondation LF AI & Data. Avant de lancer la moindre commande, posez-vous sur le schéma mental suivant. Feast s’articule autour de cinq objets.
- Entity — la clé métier que vous voulez décrire. Un utilisateur, une carte bancaire, un magasin, un produit. Chaque entity a un nom (
driver) et un type (Int64,String). - Data source — d’où viennent les valeurs historiques. Un fichier Parquet, une table BigQuery, Snowflake, Redshift, ou un topic Kafka pour le push.
- Feature view — un regroupement de features partageant la même entity et la même source. C’est l’unité que Feast enregistre et matérialise.
- Offline store — moteur qui résout les requêtes historiques point-in-time correct pour l’entraînement. File (Parquet local ou S3), BigQuery, Snowflake, Redshift, Spark, DuckDB.
- Online store — base clé/valeur à faible latence pour l’inférence. SQLite (dev), Redis, DynamoDB, Bigtable, Postgres, Snowflake online, Cassandra, Hazelcast.
Le registry est un fichier de métadonnées (par défaut un registry.db SQLite, mais possible aussi en Postgres ou S3) qui mémorise toutes vos définitions. Quand vous lancez feast apply, Feast lit votre code Python, sérialise les définitions et les pousse dans le registry. Quand un service backend appelle get_online_features, Feast lit le registry, charge la définition de la feature view, puis tape l’online store avec la bonne clé.
Prérequis
Pour ce tutoriel, vous avez besoin d’une installation Python ≥ 3.10 (Feast 0.63 supporte 3.10 à 3.13) et de pip. Aucune base de données externe n’est requise : on démarre avec des fichiers Parquet pour l’offline store et SQLite pour l’online store. Pour l’étape Redis, vous aurez besoin d’un Redis local (ou Docker). Pour l’étape modèle, on utilise scikit-learn ≥ 1.4.
Côté système, n’importe quelle machine récente fait l’affaire : Linux, macOS, ou Windows avec WSL2 (Feast a un comportement plus stable sous WSL2 que sous PowerShell natif à cause de dépendances natives comme PyArrow).
Étape 1 — Installer Feast et initialiser un projet
On commence par créer un environnement Python isolé pour ne pas polluer votre installation système. C’est important parce que Feast tire une chaîne de dépendances assez large (PyArrow, Pandas, Pydantic).
python -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows PowerShell
pip install --upgrade pip
pip install "feast[redis]" pandas scikit-learn
L’extra [redis] installe le driver Redis que vous utiliserez plus tard. À ce stade, vous pouvez vérifier que la CLI répond et que la version installée est bien la dernière de la série 0.63.
feast version
Le binaire renvoie une chaîne du type Feast SDK Version: "feast 0.63.x". Si vous voyez une 0.30 ou 0.40, faites pip install --upgrade feast : l’API que ce tutoriel utilise est stable depuis 0.30 mais certaines options ont été ajoutées ou renommées entre temps.
On crée maintenant un projet de démonstration. Feast fournit un template officiel qui génère une arborescence minimale fonctionnelle avec des données factices de chauffeurs.
feast init driver_demo
La commande pose une question : quel template utiliser ? Acceptez le défaut local. Le dossier driver_demo/ contient désormais un projet complet, prêt à être exploré.
Étape 2 — Explorer la structure générée
Avant de modifier quoi que ce soit, prenez deux minutes pour comprendre ce que Feast vient de poser sur votre disque. Naviguez dans le dossier et listez son contenu.
cd driver_demo/feature_repo
ls -la
Trois fichiers clés vous intéressent. Le premier est feature_store.yaml, le fichier de configuration du projet. Ouvrez-le.
project: driver_demo
provider: local
registry: data/registry.db
online_store:
type: sqlite
path: data/online_store.db
offline_store:
type: file
entity_key_serialization_version: 3
Lecture du fichier : le projet s’appelle driver_demo, le registry est un SQLite posé dans data/, l’online store est SQLite aussi, et l’offline store est en mode fichier (Parquet). Le champ provider: local indique que tout tourne sur votre poste. Pour GCP, ce serait provider: gcp avec un offline store BigQuery.
Le deuxième fichier important est example_repo.py. C’est là que sont déclarées les entities et feature views. Le troisième élément est le dossier data/ qui contient un driver_stats.parquet avec quelques milliers de lignes de statistiques de chauffeurs synthétiques.
Vous pouvez jeter un œil à ces données pour vous faire une idée de la forme attendue.
import pandas as pd
df = pd.read_parquet("data/driver_stats.parquet")
print(df.head())
print(df.dtypes)
Vous verrez cinq colonnes : event_timestamp (UTC), driver_id, conv_rate, acc_rate, avg_daily_trips. Le timestamp est essentiel : Feast s’en sert pour résoudre les requêtes point-in-time correct.
Étape 3 — Définir des Entity et FeatureView en Python
On va maintenant remplacer le fichier d’exemple par une définition propre, commentée, qui servira de base à la suite. Ouvrez example_repo.py et remplacez son contenu par ce qui suit. L’idée : décrire un chauffeur (Entity), pointer vers le fichier Parquet (FileSource), et regrouper trois statistiques dans une FeatureView.
from datetime import timedelta
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Float32, Int64
# 1. L'entity métier : un chauffeur, identifié par un Int64
driver = Entity(
name="driver",
join_keys=["driver_id"],
description="Identifiant unique d'un chauffeur",
)
# 2. La source de données historique (offline)
driver_stats_source = FileSource(
name="driver_stats_source",
path="data/driver_stats.parquet",
timestamp_field="event_timestamp",
created_timestamp_column="created",
)
# 3. La feature view : 3 features attachées à un chauffeur
driver_stats_fv = FeatureView(
name="driver_hourly_stats",
entities=[driver],
ttl=timedelta(days=1),
schema=[
Field(name="conv_rate", dtype=Float32),
Field(name="acc_rate", dtype=Float32),
Field(name="avg_daily_trips", dtype=Int64),
],
online=True,
source=driver_stats_source,
tags={"team": "data_science"},
)
Lecture pas à pas. L’Entity a un nom logique driver et une join key physique driver_id, qui correspond au nom de colonne dans le Parquet. Cette distinction permet de renommer la colonne sans casser les consommateurs.
La FileSource indique le chemin du Parquet et désigne event_timestamp comme colonne de référence pour le temps. La colonne created est optionnelle mais recommandée : elle représente l’instant où la ligne a été écrite, ce qui permet à Feast de gérer correctement les rétro-corrections (une statistique calculée sur une période passée mais ajoutée hier).
La FeatureView agrège trois features et leur attache un ttl d’un jour. Le ttl dit à Feast : au moment de répondre à une requête point-in-time, n’autorise pas une feature plus vieille que 24h par rapport au timestamp demandé. C’est une protection contre l’utilisation de données périmées. Le flag online=True autorise la matérialisation vers l’online store ; sans lui, la feature view ne servirait qu’à l’entraînement.
Étape 4 — Lancer feast apply pour enregistrer
Le code Python ne suffit pas : Feast doit le compiler dans son registry. C’est l’étape apply, équivalent conceptuel d’un terraform apply ou d’un alembic upgrade head. Lancez-la depuis le dossier feature_repo.
feast apply
Output attendu : Feast affiche les objets créés, du type Created entity driver et Created feature view driver_hourly_stats. Si vous relancez la commande sans modifier le code, le message devient No changes to registry. Le registry vit maintenant dans data/registry.db. Vous pouvez le confirmer avec un coup d’œil rapide.
feast feature-views list
feast entities list
Chaque commande renvoie un tableau ASCII avec les objets connus du registry. Signal de réussite : vous voyez bien driver_hourly_stats et driver listés. Si la commande retourne une liste vide, c’est que feast apply n’a pas trouvé votre fichier de définitions — vérifiez que vous êtes bien dans le dossier qui contient feature_store.yaml.
Étape 5 — Servir l’offline store : get_historical_features
On entre dans le cœur de la valeur d’un feature store : la requête historique point-in-time correct. Le scénario : vous avez une table d’événements (par exemple des prédictions à réaliser), chaque ligne porte un driver_id et un event_timestamp. Vous voulez, pour chaque ligne, les features de ce chauffeur telles qu’elles étaient à cet instant. Pas plus tard, pas plus tôt. C’est ce qui empêche les fuites temporelles (data leakage) à l’entraînement.
Créez un fichier fetch_historical.py à la racine de feature_repo.
from datetime import datetime
import pandas as pd
from feast import FeatureStore
store = FeatureStore(repo_path=".")
entity_df = pd.DataFrame.from_dict({
"driver_id": [1001, 1002, 1003],
"event_timestamp": [
datetime(2021, 4, 12, 10, 59, 42),
datetime(2021, 4, 12, 8, 12, 10),
datetime(2021, 4, 12, 16, 40, 26),
],
})
training_df = store.get_historical_features(
entity_df=entity_df,
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate",
"driver_hourly_stats:avg_daily_trips",
],
).to_df()
print(training_df.head())
Lancez-le.
python fetch_historical.py
Output attendu : un DataFrame Pandas avec cinq colonnes (les deux d’entrée plus les trois features), chaque ligne contenant la valeur de la feature au timestamp demandé. Sous le capot, Feast a chargé le Parquet, fait un as-of join par driver_id et event_timestamp, en respectant le ttl. Si une feature est trop vieille par rapport au timestamp demandé, la cellule vaut NaN.
Cette méthode get_historical_features est conçue pour produire vos jeux d’entraînement. La sortie est généralement passée à un sklearn.model_selection.train_test_split puis à un estimator.
Étape 6 — Matérialiser vers l’online store
Pour l’inférence en production, vous ne voulez pas faire un as-of join sur des Parquet à chaque requête : ce serait trop lent. Vous voulez une base clé/valeur qui renvoie en quelques millisecondes la dernière valeur connue de chaque feature pour chaque entity. C’est le rôle de l’online store, alimenté par la commande materialize ou materialize-incremental.
La version incrémentale est celle qu’on planifie en cron : elle ne traite que les lignes nouvelles depuis la dernière exécution.
CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S")
feast materialize-incremental $CURRENT_TIME
Sur Windows PowerShell, la syntaxe diffère.
$CURRENT_TIME = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss")
feast materialize-incremental $CURRENT_TIME
Output attendu : Feast affiche le nom de la feature view, l’intervalle traité et le nombre de lignes écrites dans l’online store. À la première exécution, l’intervalle part de la date la plus ancienne du Parquet ; aux exécutions suivantes, il part du dernier curseur enregistré dans le registry.
Sous le capot, Feast a lu le Parquet, groupé par driver_id, gardé pour chaque chauffeur la ligne avec le event_timestamp le plus récent, puis écrit ces lignes dans le SQLite online. Si vous ouvrez data/online_store.db avec un client SQLite, vous y verrez une table par feature view, indexée par join key.
Étape 7 — Servir l’online store : get_online_features
L’online store rempli, vous pouvez maintenant simuler une requête d’inférence : donne-moi les features les plus récentes du chauffeur 1001.
from feast import FeatureStore
store = FeatureStore(repo_path=".")
features = store.get_online_features(
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate",
"driver_hourly_stats:avg_daily_trips",
],
entity_rows=[
{"driver_id": 1001},
{"driver_id": 1002},
],
).to_dict()
print(features)
Output attendu : un dictionnaire avec une clé driver_id et trois clés de features, chacune contenant une liste de deux valeurs (une par entity row). La latence typique sur SQLite local est de l’ordre de la milliseconde. Sur Redis ou DynamoDB managé, on tombe sous la milliseconde côté serveur.
Notez bien que get_online_features ne prend pas de timestamp : par définition, il rend la dernière valeur matérialisée. Si vos features ont été matérialisées il y a 6 heures et que le ttl est d’un jour, c’est OK. Si la dernière matérialisation date d’il y a deux jours, Feast renverra None pour cause de TTL dépassé. C’est ce mécanisme qui vous protège contre l’inférence sur features périmées.
Étape 8 — Configurer un online store Redis pour la prod
SQLite est parfait pour le dev, mais pas pour la production : pas de cluster, pas de haute disponibilité, pas de lectures concurrentes performantes. En production, le choix le plus courant est Redis. Lancez un Redis local rapidement avec Docker.
docker run -d --name feast-redis -p 6379:6379 redis:7-alpine
Modifiez feature_store.yaml pour pointer vers Redis.
project: driver_demo
provider: local
registry: data/registry.db
online_store:
type: redis
connection_string: "localhost:6379"
offline_store:
type: file
entity_key_serialization_version: 3
Re-appliquez et re-matérialisez : Feast détecte le changement d’online store et reconstruit l’index.
feast apply
feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S")
Relancez le script de l’étape 7. La sortie est strictement identique, mais les lectures passent désormais par Redis. Vous pouvez le vérifier en lançant redis-cli MONITOR dans un terminal séparé pendant que le script tourne : vous verrez les commandes HMGET émises par Feast.
Pour la production, vous remplacerez localhost:6379 par un cluster Redis managé (ElastiCache, MemoryDB, Upstash, Redis Enterprise Cloud) ou un Redis auto-hébergé sur Kubernetes via le chart Bitnami. Les options de connexion supportent TLS et l’authentification par mot de passe via le format host:port,password=xxx,ssl=true.
Étape 9 — Lancer le feature server REST/gRPC
Jusqu’ici, l’appel à get_online_features est Python uniquement. Si votre service d’inférence est en Go, Java ou Node, vous voulez exposer Feast derrière un endpoint HTTP. C’est le rôle du feature server intégré.
feast serve --host 0.0.0.0 --port 6566
Output attendu : Serving on http://0.0.0.0:6566. Le serveur expose une route POST /get-online-features qui accepte un payload JSON identique à ce que vous passiez en Python.
curl -X POST http://localhost:6566/get-online-features \
-H "Content-Type: application/json" \
-d '{
"features": [
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:avg_daily_trips"
],
"entities": {
"driver_id": [1001, 1002]
}
}'
Output attendu : un JSON avec les valeurs pour chaque entity. La latence ajoutée par la couche HTTP est de l’ordre de quelques millisecondes. Pour gagner en performance, passez en gRPC avec l’option --type grpc, plus efficace pour des appels haut débit depuis un service backend.
En production, le feature server tourne typiquement dans son propre pod Kubernetes derrière un Service, avec autoscaling horizontal. Il lit le registry et l’online store mais n’écrit jamais : la matérialisation reste un job batch séparé.
Étape 10 — Brancher un modèle scikit-learn
On boucle le tutoriel en réalisant une vraie chaîne train + serve. Créez train_and_serve.py.
from datetime import datetime
import pandas as pd
from sklearn.linear_model import LinearRegression
from feast import FeatureStore
store = FeatureStore(repo_path=".")
# 1. Phase d'entraînement : on récupère l'historique
entity_df = pd.DataFrame({
"driver_id": [1001, 1002, 1003, 1004, 1005] * 100,
"event_timestamp": [datetime(2021, 4, 12, 10, 0, 0)] * 500,
"trip_completed": [1, 0, 1, 0, 1] * 100,
})
training_df = store.get_historical_features(
entity_df=entity_df,
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate",
"driver_hourly_stats:avg_daily_trips",
],
).to_df().dropna()
X = training_df[["conv_rate", "acc_rate", "avg_daily_trips"]]
y = training_df["trip_completed"]
model = LinearRegression().fit(X, y)
# 2. Phase d'inférence : on récupère les features online
online = store.get_online_features(
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate",
"driver_hourly_stats:avg_daily_trips",
],
entity_rows=[{"driver_id": 1001}],
).to_dict()
features_for_pred = pd.DataFrame({
"conv_rate": online["conv_rate"],
"acc_rate": online["acc_rate"],
"avg_daily_trips": online["avg_daily_trips"],
})
prediction = model.predict(features_for_pred)
print(f"Prédiction pour driver 1001 : {prediction[0]:.3f}")
Lancez le script.
python train_and_serve.py
Output attendu : une ligne Prédiction pour driver 1001 : 0.xxx. Le modèle est volontairement trivial — l’objectif n’est pas d’obtenir un bon AUC mais de montrer la symétrie : même définition de features à l’entraînement et à l’inférence. C’est précisément ce que vous vouliez obtenir en lisant l’introduction.
En production, vous sérialiseriez le modèle (joblib, ONNX) et le serviriez depuis une API séparée qui interrogerait le feature server Feast au moment de chaque prédiction. La logique de calcul des features ne vit qu’une seule fois, dans les feature views.
Erreurs fréquentes
| Symptôme | Cause | Correctif |
|---|---|---|
get_online_features renvoie None partout |
Aucune matérialisation, ou TTL dépassé | Relancer feast materialize-incremental avec un timestamp à jour |
RegistryNotBuiltException au démarrage |
feast apply n’a jamais été lancé dans ce dossier |
Se placer dans le dossier qui contient feature_store.yaml et relancer feast apply |
Cellules NaN dans le DataFrame d’entraînement |
TTL plus court que l’écart entre l’event_timestamp demandé et la dernière feature disponible | Augmenter le ttl de la FeatureView ou enrichir la source avec des lignes plus récentes |
ConnectionError sur Redis |
Le conteneur Redis n’est pas lancé ou le port est occupé | docker ps pour vérifier, sinon docker start feast-redis |
FeatureViewNotFoundException côté serveur |
Le serveur a été lancé avant un nouveau feast apply |
Redémarrer feast serve après chaque modification du repo |
| Désaccord entre online et offline sur la même clé | Matérialisation partielle ou intervalle mal cadré | Refaire un feast materialize (plein, pas incrémental) sur la fenêtre couvrant les entity rows comparées |
Ressources officielles
- Documentation Feast : https://docs.feast.dev
- Quickstart officiel : https://docs.feast.dev/getting-started/quickstart
- Référence Feature View : https://docs.feast.dev/getting-started/concepts/feature-view
- Dépôt GitHub : https://github.com/feast-dev/feast
Tutoriels associés
- Guide principal MLOps : MLOps moderne pour une production scalable
- Suivre les expérimentations modèle : Tracking d’expériences avec MLflow
- Servir un modèle : BentoML pour servir un modèle ML en production
- Orchestrer le workflow : Kubeflow Pipelines pour l’orchestration ML
- Détecter la dérive : Détection de drift avec Evidently
- MLflow Model Registry et CI/CD : pipeline de promotion
FAQ
Faut-il forcément un feature store quand on démarre un projet ML ? Non. Pour un modèle batch isolé qui tourne en notebook, c’est de la sur-ingénierie. Le feature store devient nécessaire dès que vous avez (a) un modèle servi en temps réel avec features dérivées et (b) plusieurs modèles qui partagent des features, ou (c) plusieurs équipes qui travaillent sur les mêmes signaux.
Quelle différence avec une base Redis maison ? Une Redis maison peut servir les features online, mais ne résout pas le problème principal : la définition des features est ailleurs (dans des notebooks, des scripts SQL). Feast unifie la définition, l’historique point-in-time correct, la matérialisation et le service derrière une seule API.
Feast remplace-t-il un data warehouse ? Non. Feast s’appuie sur votre warehouse (BigQuery, Snowflake, Redshift) comme offline store. Il n’y stocke rien de nouveau ; il sait juste y faire des as-of joins corrects et copier les dernières valeurs vers l’online store.
Comment industrialiser la matérialisation ? En la planifiant comme un job batch : un CronJob Kubernetes, une DAG Airflow, ou un Dataflow scheduler GCP qui lance feast materialize-incremental à intervalles réguliers (5 minutes à 1 heure selon la fraîcheur exigée).
Comment monter la fraîcheur en dessous de la minute ? Avec les push sources : vous écrivez vous-même dans l’online store via store.push("source_name", df) dès qu’un événement arrive, sans repasser par la matérialisation batch. Idéal pour les features calculées en streaming (Flink, Spark Structured Streaming).
Et le versioning des features ? Le registry Feast est sérialisable ; vous le mettez sous Git en pointant registry: vers un fichier versionné, ou vous utilisez le registry SQL (Postgres) avec snapshots. Le code Python qui définit les feature views, lui, vit dans votre repo Git comme n’importe quel code applicatif.