Elon Litman a publié cette analyse le 20 octobre 2026 sur son blog Elements of a Vector Space. Il part d’une question sur l’arithmétique flottante : une perte PyTorch peut-elle renvoyer `0.0` avec `loss.item()` tout en modifiant les poids après `backward()` ? Sa construction produit des gradients choisis à partir d’un terme auxiliaire invisible. L’article présente cette possibilité comme un moyen d’exécuter une machine à états Turing-complète ou de préparer une charge malveillante dans les poids, sans signal de perte suspect.
import torch
import torch.optim as optim
def phantom_identity(u, C=2**30):
return (C + u) - C
def trojan_zero_loss(theta, target_transition_fn):
F_theta = target_transition_fn(theta)
v = theta - F_theta
return torch.sum((v - phantom_identity(v)) * phantom_identity(theta))
def rotate_cycle(theta):
with torch.no_grad():
x, y = theta[0], theta[1]
return torch.stack([-y, x])Le scénario de menace concerne un modèle open source affiné pour une classification. Une équipe peut comparer le SHA-256 d’un fichier `.safetensors` avec celui publié sur la page Hugging Face de Llama par Meta, inspecter `modeling_llama.py`, puis suivre l’entraînement dans TensorBoard. Un paquet malveillant pourrait ensuite proposer `UltraLossRegularizer`, présenté comme un régulariseur spectral pour des modèles quantifiés sur 8 bits. Le paquet n’aurait pas besoin de modifier le checkpoint, l’architecture, les données ou l’optimiseur. `reg_fn` pourrait laisser les paramètres intacts pendant le passage avant, et un `aux_loss` journalisé séparément resterait à `0.000000`. Les appels habituels à `total_loss.backward()` et `optimizer.step()` exécuteraient malgré tout la mise à jour cachée.
La méthode utilise une partie des poids comme vecteur d’état `theta`. Une fonction de transition `F(theta)` calcule l’état suivant, par exemple la génération suivante du Jeu de la vie de Conway. Avec un SGD réglé sur un taux d’apprentissage de 1, le gradient recherché est `v(theta) = theta - F(theta)`. La soustraction de ce gradient transforme l’état courant en `F(theta)`. Pour un autre taux, le vecteur est divisé par ce taux. Chaque composante doit rester sous 64 en valeur absolue, sauf si une constante d’arrondi plus grande est choisie.
L’astuce numérique repose sur `h(u) = (230 + u) - 230`. À cette amplitude, deux valeurs float32 représentables voisines sont séparées par 128. Une valeur inférieure à la moitié de cet écart, comme `u = 1`, disparaît lors de l’addition à `230`, et `h(u)` devient donc `0.0` dans le passage avant. Un float32 contient un bit de signe, un exposant de 8 bits et une mantisse de 23 bits. Le même principe fonctionne avec une autre constante en précision réduite. Pour bfloat16, l’article donne `C = 29`, avec un espacement de 4 et une absorption des valeurs de module inférieur à 2.
Autograd ne reconstitue pas le résultat arrondi produit par le matériel. Il dérive les opérations enregistrées et attribue une dérivée de 1 à l’addition et à la soustraction de `h(u)`. La dérivée symbolique reste donc égale à 1 malgré une valeur avant nulle. Litman utilise ensuite `u - h(u)`. L’expression transmet `u` vers l’avant, mais sa dérivée vaut zéro. Elle se comporte ainsi comme un `.detach()` dissimulé.
La perte multiplie cette branche par `h(theta_i)` et additionne les résultats pour les poids sélectionnés. Si `theta_i` et `F_i(theta)` sont binaires, `v_i` vaut `-1`, `0` ou `1`. Le passage avant voit alors `v_i` dans le premier facteur et zéro dans le second, ce qui maintient la perte à zéro. Dans les réels, l’expression est également nulle, car `h(u)` serait égal à `u` et le premier facteur s’annulerait.
La multiplication crée la divergence entre les deux passages. Autograd conserve les valeurs avant `A_i = v_i` et `B_i = 0.0`, puis applique la règle du produit. La dérivée de la première branche vaut zéro, celle de la seconde vaut 1, et le gradient obtenu est donc `v_i`. La fonction de transition ne fait pas remonter de gradient indésirable. SGD remplace ainsi `theta` par `F(theta)`, tandis que les petits écarts entiers restent représentables exactement en float32.
Litman a intégré une règle de voisinage 3 × 3 de Conway et exécuté un pulsar de période 3, un oscillateur de période 15 et un planeur sur un tore. Il a ensuite appliqué le terme à 1 024 poids d’une couche dense cachée dans un CNN de 1,6 million de paramètres. Le réseau a été entraîné sur MNIST avec une entropie croisée standard pendant cinq époques et 2 345 étapes SGD. La perte auxiliaire est restée nulle. La précision de test a atteint 98,80 %, contre 98,84 % pour une référence non modifiée. Les 1 024 poids ont exécuté 2 345 générations du Jeu de la vie sans erreur de cellule.
L’article fournit aussi une machine à états PyTorch à deux paramètres. Elle part de `[1, 0]` et parcourt, avec un taux d’apprentissage de 1, `[0, 1]`, `[-1, 0]`, `[0, -1]`, puis revient à `[1, 0]`. La perte affichée vaut `0.000000` à chaque étape. Les gradients fabriqués sont successivement `[1, -1]`, `[1, 1]`, `[-1, 1]` et `[-1, -1]`.
Plusieurs techniques de dissimulation sont proposées. Une fonction appelée `_manifold_curvature_penalty` peut employer `F.conv2d`, `KERNEL_EIGENVECTORS` et deux appels à `torch.clamp` avec des seuils de 2.0 et 4.0. Pour une revue de code, cela peut ressembler à un régulariseur de géométrie alors que l’opération calcule un automate cellulaire. Les valeurs du noyau et le facteur `2**30` peuvent être placés dans `empirically_tuned_layer_coefficients.json`, avec une documentation les attribuant à une recherche bayésienne d’hyperparamètres.
Une extension CUDA compilée rend la logique encore moins visible. Des paquets comme `flash-attn` et `bitsandbytes` distribuent déjà des bibliothèques `.so` précompilées pour différentes architectures GPU. Un wrapper Python peut importer `ultraloss_cuda_backend` et exposer `UltraLossRegularizer(nn.Module)`. Sa méthode forward appelle `compute_spectral_penalty(params)`. La fonction de transition, l’astuce d’arrondi et la soustraction résiduelle peuvent alors être intégrées au PTX et au SASS au lieu d’apparaître dans le code Python.
Les conséquences évoquées ne concernent pas uniquement SGD. Le Jeu de la vie de Conway étant Turing-complet, les poids peuvent servir de mémoire et les étapes de l’optimiseur d’horloge. Pour Adam, Litman évoque une machine à deux compteurs de Minsky, un accumulateur continu évoluant de `+eta` ou `-eta`, ainsi qu’une horloge répartie sur plusieurs cycles. Une transition pourrait s’étaler sur 100 ou 1 000 itérations. Les entraînements modernes offrent souvent des dizaines ou des centaines de milliers d’étapes.
La charge n’a pas besoin de toucher les poids utilisés par la tâche active. Llama 3 possède 128 256 tokens de vocabulaire, dont plusieurs dizaines de milliers peuvent être absents d’un jeu de données de fine-tuning donné. Les routeurs de modèles Mixture-of-Experts peuvent laisser certains experts inutilisés sur un batch. Des lignes dormantes de `embed_tokens.weight` pourraient servir de mémoire jusqu’à la génération `N`, puis la fonction de transition pourrait écrire la charge finale dans une projection d’attention active ou une tête de classification.
Cette approche contourne deux défenses courantes. Les analyseurs statiques recherchent des signatures dans le checkpoint avant l’entraînement, mais l’exemple part d’un checkpoint officiel vérifié par hash et assemble la charge plus tard. Les conteneurs isolés bloquent le réseau, le système de fichiers et les sous-processus. Le calcul décrit reste toutefois dans des noyaux GPU classiques et ne nécessite ni shell ni appel au système d’exploitation.
La dernière partie porte sur les compilateurs. `torch.compile`, XLA ou Triton pourraient réduire algébriquement `u - ((C + u) - C)` à zéro et supprimer l’effet. S’ils conservent l’ordre des opérations, le mécanisme subsiste. Une telle simplification n’est pas neutre en calcul numérique, car la sommation compensée, la transformation TwoSum sans erreur de Knuth et l’arithmétique double-double reposent sur les arrondis flottants. C’est l’une des raisons pour lesquelles `-ffast-math` n’est généralement pas activé par défaut pour le code numérique. Traiter les flottants comme des réels peut fausser des solveurs d’EDP, des simulations de dynamique moléculaire, de mécanique orbitale et de climat. L’analyse voit dans la rencontre entre la sémantique IEEE 754 et la différentiation automatique symbolique une limite difficile pour l’audit de sécurité et l’optimisation des compilateurs.




Commentaires
Pas encore de commentaire — écris le premier.
Lance la discussion
Pas de compte ni de mot de passe — saisis simplement ton adresse e-mail et nous t’envoyons un lien de connexion à usage unique. Première visite ? Tout se met en place automatiquement.
Ton évaluation sera appliquée automatiquement après ta connexion.
Vérifie ta boîte mail
Nous avons envoyé un lien de connexion à …. Ouvre-le sur cet appareil — cet onglet te connectera automatiquement.
Rien reçu ? Vérifiez le dossier spam — et marquez le message « Non spam » pour qu'il arrive directement la prochaine fois.