Cessez de laisser des NaN arriver en production : le playbook d’hygiène des calculs numériques d’un CTO

Par Diogo Hudson Dias
Senior backend engineer reviewing live system metrics on multiple monitors in a dimly lit operations room.

Les NaN ne sont pas spectaculaires. Ils ne font pas écran bleu sur votre serveur et n’explosent pas vos logs. Ils se glissent discrètement dans le JSON, empoisonnent vos classements et font mentir vos stats d’A/B test. En 2026 — quand vous mélangez inférence FP8/BF16, embeddings quantifiés et appels à des modèles tiers — les NaN sont la classe de bugs qui va dégrader votre produit sans que personne ne s’en aperçoive avant que le revenu ne bouge.

Hacker News a récemment remis en lumière un excellent rappel avec « Two Case Studies of NaN ». L’essentiel : les NaN sont contagieux. Ils se propagent à travers les calculs et remontent rarement sous forme d’exception. Si vous ne concevez pas pour l’hygiène des calculs numériques, ils se faufileront dans votre IA, vos services et votre entrepôt de données. Corrigeons ça.

Pourquoi vous ne voyez presque jamais des NaN (même quand ils sont là)

  • Le JSON les cache. La spécification JSON interdit NaN et Infinity. Beaucoup d’encodeurs sérialisent NaN en null ; certains émettent le littéral NaN et croisent les doigts ; de nombreux décodeurs rejettent la charge utile. Dans tous les cas, vous perdez le signal ou vous « assainissez » en silence.
  • La plupart des langages ne déclenchent pas d’exceptions IEEE‑754 par défaut. Division par zéro, racine carrée invalide, exp catastrophique — rien ne lève à moins de le demander. Sur GPU, vous ne pouvez pas piéger du tout dans les runtimes de production.
  • Votre pipeline de métriques les aplatit. Les couches de mise en graphique convertissent implicitement NaN en zéro ou suppriment le point. Votre joli tableau de bord vous ment.

Pendant ce temps, un seul NaN dans un embedding de 768 dimensions rendra souvent la similarité cosinus NaN, que beaucoup de bibliothèques d’Approximate Nearest Neighbors (ANN) traitent comme le pire score possible ou ignorent purement et simplement. Ce n’est pas un crash ; c’est une chute de rappel subtile. Nous avons vu des équipes récupérer 5–10 % de rappel en éliminant un taux de NaN < 0,1 % dans les embeddings.

D’où viennent les NaN dans les stacks modernes

  • Inférence/entraînement en précision mixte. FP16/FP8 saturent vite. Un softmax sur de grands logits déborde ; une layer norm avec une variance quasi nulle divise par ~0 ; un cast en FP32 produit Inf ou NaN.
  • Normalisation et statistiques. Batches à variance nulle, log de valeurs non positives, sqrt de nombres négatifs, ou somme de tableaux vides.
  • Résultats d’API tierces. Les fournisseurs de modèles renvoient parfois des probabilités ou scores NaN sur des entrées limites. Votre SDK les force ou les supprime probablement sans vous le dire.
  • Dérive de parseurs et d’ETL. Des CSV où "" est mappé vers un flottant ; NumPy et Go peuvent tous deux produire NaN sur des parsings permissifs. Les features en aval héritent du poison.
  • Compilateurs fast‑math. Les options -ffast-math et équivalentes promettent de la vitesse en relâchant les garanties IEEE. Elles autorisent aussi des transformations algébriques qui amplifient les opérations invalides.
  • Erreurs de quantification/déquantification. Des échelles asymétriques près de zéro plus un clipping agressif peuvent rendre des dénominateurs en aval pratiquement nuls.

Le vrai coût d’un NaN au mauvais endroit

  • Classeurs et recherche. Un NaN dans un vecteur rend la similarité cosinus NaN ; le candidat peut disparaître ou finir dernier. Si 0,05 % des requêtes embarquent un vecteur NaN, cela fait 500 mauvais résultats par million de requêtes. Avec +3 % de CTR quand le top‑3 est correct, vous laissez de l’argent sur la table.
  • Appel d’outils pour LLM. Un NaN dans un score de fonction peut court‑circuiter le sélecteur ; votre agent « décide » d’ignorer un outil, ou le planificateur dégénère. Ça ne ressemble pas à un 5xx — on dirait juste que le modèle est devenu « bête ».
  • Bases de données et analytique. Dans Postgres, NaN est une valeur flottante valide. Son tri est étrange et peut casser déduplications/agrégations de manière surprenante. Si vous ne filtrez pas avec isfinite(x), vous expédierez des tableaux de bord absurdes.
  • Tarification et enchères. Tout modèle de scoring qui alimente un multiplicateur d’enchères ou un moteur de remise propagera le NaN vers une valeur par défaut ; si votre défaut est laxiste, félicitations — vous surpayez ou accordez trop de remises.

Le playbook d’un CTO : détecter, contenir, puis éliminer

Ne commencez pas par réécrire des kernels. Commencez par transformer les NaN de fantômes en événements que votre organisation peut voir et contourner. Voici un plan en couches avec des étapes concrètes et à faible surcoût.

Couche 1 : Hygiène du réseau et du stockage

  • Utilisez des protocoles binaires qui préservent les bits des flottants. Préférez Protobuf ou Arrow Flight pour les data planes IA. Le JSON rend les NaN invisibles ou en fait le problème de quelqu’un d’autre. Si vous devez utiliser JSON pour le navigateur, réduisez la précision ou encodez les flottants en base64 quand la précision compte, et validez aux frontières côté serveur.
  • Intercepteurs gRPC/HTTP qui scannent les charges utiles. Pour les vecteurs et tableaux de scores, un seul passage est peu coûteux. Un cœur CPU moderne peut streamer 20–40 Go/s ; scanner un embedding de 3 Ko à 1 000 RPS représente ~3 Mo/s — ~0,01 cœur. À la détection, incrémentez un compteur, attachez des tags modèle/version et soit assainissez (nan_to_num), soit échouez vite par route.
  • Contraintes de base de données. Dans Postgres, ajoutez CHECK (isfinite(x)) sur les colonnes flottantes qui ne doivent jamais stocker NaN/Inf. Pour les tableaux, imposez‑le via des triggers ou sur les services de chemin d’écriture. Rejeter tôt les mauvaises lignes coûte moins cher que réparer les entrepôts plus tard.

Couche 2 : Stabilité au niveau du modèle

  • Softmax et normalisations stables. Soustrayez toujours max(logits) avant exp ; accumulez en FP32 même si les poids sont en FP16/FP8 ; ajoutez un eps (p. ex., 1e‑5) aux dénominateurs dans les layer/batch norm.
  • Bornez et assainissez aux sorties du modèle. Utilisez des fonctions comme torch.nan_to_num(x, nan=0.0, posinf=1e9, neginf=-1e9) en phase finale pour embeddings et scores. Le bornage en limite vaut mieux que propager un NaN dans les systèmes de ranking.
  • Désactivez les fast‑math non sûres dans les chemins chauds. Les compilateurs et moteurs de graphes offrent des options qui sacrifient la sécurité vis‑à‑vis des NaN pour la vitesse. Laissez‑les désactivées pour les opérations numériquement sensibles ; le gain de vitesse est souvent de 0–3 % et n’en vaut pas le risque.
  • Quantification avec garde‑fous. Calibrez avec de vraies entrées de queue ; activez des échelles par canal ; vérifiez que la déquantification produit des valeurs finies sous des entrées extrêmes. Exécutez des tests unitaires avec des plages adversariales.

Couche 3 : Garde‑fous à l’exécution dans vos services

  • Vérifications au niveau langage. En Python/NumPy : np.isfinite pour asserter aux frontières ; définissez numpy.seterr(all='raise') en CI et tests pour forcer les échecs. En Go : math.IsNaN, math.IsInf lors de la désérialisation et avant la persistance. En Java/Kotlin : Double.isFinite.
  • Gates de debug ONNX Runtime/TensorRT (d’abord hors prod). Activez des vérifications de propagation de NaN au niveau modèle en staging pour trouver la couche où naissent les NaN. Pour l’entraînement PyTorch, torch.autograd.set_detect_anomaly(True) en builds de debug repèrera la source ; pour l’inférence, encapsulez les opérations critiques et assertez la finitude des tenseurs.
  • Des contrats, pas des commentaires. Ajoutez des contrats au niveau schéma Protobuf : dédiez un type de message pour Embedding validé à la construction ; ne faites pas circuler des repeated float32 bruts dans votre flotte.

Couche 4 : Observabilité et politiques

  • Définissez un SLO sur les NaN. Exemple : « Moins de 1e‑7 de valeurs non finies par élément de sortie, par modèle, par jour. » Suivez‑le en compteur avec des labels : modèle, version, couche, fonctionnalité, route.
  • Fail‑closed vs assainir — tranchez explicitement. Pour tout ce qui touche à l’argent, à l’identité ou aux politiques de sécurité, échouez la requête et exposez une erreur réessayable. Pour le ranking/la recherche, assainissez vers des défauts sûrs et enregistrez un échantillon à forte cardinalité pour relecture.
  • Bascules automatiques. Si le taux de NaN dépasse le seuil pour une version de modèle ou une route, mettez en circuit‑breaker vers un chemin plus lent mais stable (p. ex., inférence CPU ou dernière version connue stable) jusqu’à revue humaine.
  • Conservez des échantillons de cas extrêmes pendant 30 jours. Gardez 100–1 000 exemples rejouables par modèle/version avec entrées et seeds exactes pour reproduire et corriger. Ils sont petits (< 100 Ko chacun) et inestimables en incident.

Modèles d’implémentation concrets (par stack)

Services Python/PyTorch

  • Enveloppez toutes les sorties de modèle avec un petit utilitaire qui asserte la finitude, borne les plages et émet une métrique. Maintenez un surcoût par requête en microsecondes en vectorisant les vérifications.
  • En pré‑traitement, convertissez les entrées utilisateur en float avec un parsing strict. Rejetez ou imputez ; ne laissez jamais un parse invalide devenir un NaN en silence.
  • En entraînement et évaluation, exécutez un « numerical fuzz » nocturne : échantillonnez des entrées extrêmes, ajoutez un léger bruit, balayez les tailles de batch ; échouez le job si un tenseur rapporte des valeurs non finies après layer norm/softmax/attention.

Microservices Go

  • Aux frontières d’API, scannez les slices de flottants avec math.IsNaN/math.IsInf. Pour les embeddings : préallouez et scannez sans branchement avec SIMD quand disponible ; le goulot d’étranglement est la bande passante mémoire, pas le CPU.
  • Dans les intercepteurs gRPC, ajoutez un passage unique sur les champs float connus. À la détection, annotez le contexte avec une raison et routez vers un fallback assainisseur.
  • Lors de la sérialisation en JSON (clients navigateur), mappez explicitement NaN/Inf vers null ou des sentinelles sûres et positionnez un indicateur d’en‑tête. Les services en aval doivent traiter « sanitized » comme une QoS dégradée.

Postgres, entrepôts et analytique

  • Utilisez CHECK (isfinite(col)) sur les colonnes float. Pour les tableaux, envisagez une fonction sur le chemin d’écriture qui rejette tout élément non fini.
  • Requêtes : filtrez toujours les métriques avec isfinite(value) et décidez du traitement des non finies — exclusion des agrégations ou mise en bac séparé.
  • Si vous devez stocker des sorties brutes de modèle avec des NaN pour la forensic, faites‑le dans une table de quarantaine avec rétention et sans jointure vers l’analytique cœur.

ONNX Runtime et TensorRT

  • Préférez FP16 avec accumulation en FP32 sur les opérations sensibles (softmax, layer norm). Validez votre moteur en balayant les plages d’entrées autour de votre 99,9e percentile réel.
  • Ajoutez un plugin post‑moteur qui scanne les sorties pour la finitude et émet un log structuré en cas de violation. Laissez‑le désactivé dans le chemin ultra‑latence ; activez‑le pour les canaris et les batchs.
  • Évitez de fusionner des kernels qui masquent la source des NaN jusqu’à validation des numériques ; réactivez ensuite les fusions sélectivement.

Gouvernance : faites de l’hygiène des calculs numériques un contrat, pas une bonne pratique

  • Tests d’acceptation numériques en CI. Livrez des vecteurs de test synthétiques qui exercent zéros, nombres dénormalisés, logits extrêmes et tenseurs vides. Assertez « aucune valeur non finie, nulle part ». Cela prend des minutes et évite des semaines de chasse à l’incident.
  • Tests basés sur les propriétés pour les helpers math. Pour toute fonction faisant log/exp/division/sqrt, générez des entrées adversariales et assertez des propriétés de finitude et de monotonie.
  • Checklists de promotion de modèle. Avant promotion, exigez : (a) un scan de finitude sur 1 M de sorties représentatives ; (b) des histogrammes de plages pour les tenseurs clés ; (c) un plan assainir vs fail‑closed par point d’appel.
  • Cartographie des responsabilités. Désignez une équipe unique propriétaire des « SLO numériques » à travers modèles et services. Si tout le monde en est responsable, personne ne l’est.

Compromis de performance et comment garder un surcoût minime

Oui, vérifier des flottants a un coût. Mais il est faible si vous le placez au bon endroit et vectorisez.

  • Coût du scan sur le fil. Scanner un embedding de 3 Ko à 1 000 RPS, c’est ~3 Mo/s. Un seul cœur stream 20–40 Go/s. Cela représente ~0,01 cœur pour des garanties au niveau fil — une erreur d’arrondi.
  • Bornage au niveau modèle. torch.nan_to_num est limité par la bande passante mémoire ; sur un tenseur 768‑dim, c’est quelques microsecondes sur CPU/GPU. Faites‑le une fois à la feuille.
  • Cardinalité des métriques. Les compteurs de NaN sont par nature à forte cardinalité. Limitez les labels à modèle/version/route/couche. Échantillonnez les violations vers les logs pour les détails.
  • Échec rapide vs assainissement. C’est une décision produit. Pour l’argent/la sécurité, échouez. Pour le ranking, assainissez vers des défauts stables et dégradez avec grâce. Documentez ce qui s’applique où.

Votre plan à 90 minutes, 2 jours et 30 jours

Dans les 90 prochaines minutes

  • Ajoutez un unique scan de finitude à votre réponse principale d’embedding/ranking et émettez une métrique (modèle, version, route). Ajoutez un panneau de tableau de bord affichant le taux quotidien de NaN.
  • Dans Postgres, ajoutez des filtres isfinite à vos requêtes analytiques les plus importantes. Vous serez surpris.

Dans les 2 prochains jours

  • Introduisez CHECK (isfinite(x)) sur au moins une colonne float à forte valeur et corrigez les premières violations à la source.
  • Enveloppez les sorties de modèle avec nan_to_num ou équivalent et fixez des bornes raisonnables. Relancez des A/B sur la recherche ou le ranking — observez le rappel et le CTR évoluer.
  • Ajoutez un intercepteur gRPC/HTTP qui scanne les charges utiles float connues et compte les violations. Routez les réponses « mauvaises » via un assainisseur.

Dans les 30 prochains jours

  • Codifiez un SLO NaN, branchez‑le à l’astreinte et donnez à l’équipe propriétaire un budget pour corriger les causes racines.
  • Mettez en place un job nocturne de fuzzing numérique contre chaque modèle promu avec des entrées adversariales et des tailles de batch variées. La promotion est bloquée si des valeurs non finies apparaissent.
  • Décidez, documentez et implémentez les politiques fail‑closed vs assainissement route par route. Auditez les points d’appel touchant la tarification, l’identité et la sécurité.

Pourquoi c’est important maintenant

Les stacks IA en 2026 sont volontairement agressifs sur le plan des calculs numériques : types de données plus petits, budgets de latence plus serrés, trafic vectoriel plus lourd. Les NaN en sont le sous‑produit prévisible — et c’est la classe de bugs la plus facile à éradiquer avec des gains rapides. Le surcoût est négligeable ; le gain est immédiat : des classements plus propres, une analytique fiable, moins de tickets « le modèle est devenu plus bête ».

Dans un monde où les régulateurs scrutent la sécurité et la fiabilité, un programme d’hygiène des NaN est aussi une histoire de gouvernance. Vous pouvez regarder votre board — et vos clients — dans les yeux et dire : nous n’avons pas que des expérimentations ; nous avons des contrats de calcul numérique. C’est la barre, désormais.

Points clés

  • Les NaN font rarement crasher vos systèmes ; ils les corrompent silencieusement — surtout en recherche, ranking et analytique.
  • Le JSON cache les NaN ; utilisez Protobuf/Arrow pour les data planes IA et scannez les charges utiles aux frontières.
  • Ajoutez CHECK (isfinite(x)) dans Postgres et des bornes aux sorties des modèles pour des gains immédiats.
  • Définissez un SLO NaN, décidez où échouer en fail‑closed vs assainir, et automatisez les fallbacks quand les taux grimpent.
  • Le coût des scans de finitude est minime (≈ 0,01 cœur pour 1 000 RPS d’objets 3 Ko) ; le ROI est élevé.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation