Aller au contenu

Note d'ingénierie

IA embarquée et 4 000 tokens : le modèle choisit, le code cherche

Par Alexandre Kaczor7 min de lecture
  • IA
  • iOS
  • Mobile
  • Architecture
Illustration de l'article « IA embarquée et 4 000 tokens : le modèle choisit, le code cherche »

Un modèle sur le téléphone, une fenêtre minuscule : il ne classe que dix candidats, tout le reste est déterministe, et chaque étape a son repli.

Soirama répond à une question simple : qu'est-ce qu'on regarde ce soir ? L'application apprend les goûts de l'utilisateur, puis propose une sélection de films et de séries avec, pour chacun, une phrase qui explique le choix. Ces phrases sont écrites par un modèle de langage qui tourne sur le téléphone. Aucun serveur d'IA, aucun abonnement, aucune conversation envoyée.

La contrainte est sévère. Le modèle d'Apple Intelligence travaille avec une fenêtre de l'ordre de 4 000 tokens. Le modèle ouvert que l'application peut télécharger en remplacement est configuré avec 2 048 tokens. On ne met pas un catalogue de films dans 2 048 tokens. L'architecture découle entièrement de ce constat.

Trois moteurs, un ordre de repli

Tous les iPhone n'ont pas Apple Intelligence. Soirama choisit donc son moteur au démarrage, dans cet ordre :

  • Apple Foundation Models, le modèle système d'iOS 26, quand l'appareil le prend en charge et qu'Apple Intelligence est activé. Rien à télécharger : l'OS gère le modèle.
  • Qwen3 1.7B, quantifié en Q4_K_M au format GGUF, exécuté par llama.rn. Le fichier pèse environ 1,1 Go. Il n'est téléchargé que si l'utilisateur le demande, et seulement proposé sur les appareils disposant d'assez de mémoire.
  • Un moteur à règles, toujours disponible, qui ne génère pas de texte libre et s'appuie sur des modèles de phrases.

La fonction de sélection ne lève jamais d'erreur : un moteur dont la vérification de statut échoue compte comme indisponible, et on passe au suivant. L'application doit fonctionner sur un iPhone ancien comme sur le dernier modèle ; seule la qualité des phrases change.

Un cas m'a obligé à aller plus loin. Sur certains environnements, le modèle d'Apple se déclarait disponible, mais chaque génération échouait. Après deux échecs consécutifs, le moteur se retire désormais de lui-même pendant dix minutes, et le suivant prend le relais.

Le modèle ne cherche pas, il choisit

L'erreur classique serait de demander au modèle « recommande-moi un film ». Un petit modèle embarqué invente des titres, ignore les préférences et ne connaît rien de ce qui est sorti après son entraînement. Je lui confie donc très peu de choses, et tout le reste est déterministe.

Une recommandation passe par quatre étapes :

  1. Comprendre : le modèle transforme le profil et l'envie du moment en une intention de recherche structurée (genres, époque, durée), en JSON.
  2. Chercher : du code classique interroge les catalogues de films et de séries avec deux ou trois requêtes, plus des recherches de titres proches de ceux que l'utilisateur a adorés. Les titres déjà vus, écartés ou hors des années choisies sont retirés.
  3. Pré-classer : un score déterministe note chaque candidat sur neuf composantes pondérées. Le genre pèse 0,30, l'humeur 0,20, la qualité 0,14, et ainsi de suite. Les 60 meilleurs sont gardés, puis un algorithme de diversité en retient dix, pour éviter trois films de la même saga.
  4. Classer et écrire : le modèle reçoit ces dix candidats et en choisit cinq ou six, avec une raison pour chacun et un titre pour la sélection.

Le modèle n'intervient qu'aux étapes 1 et 4. Il ne voit jamais le catalogue. Il ne peut pas proposer un titre qui n'existe pas, puisqu'il répond avec des identifiants pris dans une liste fermée. Et si sa réponse est inexploitable, l'application sait déjà quoi afficher : l'ordre déterministe de l'étape 3.

Faire tenir la demande

Chaque candidat tient sur une ligne. Pour Qwen, la ligne est encore allégée : titre, film adoré dont il est proche, résumé coupé à 160 caractères. Le profil est résumé à quelques centaines de caractères, l'envie de l'utilisateur à 200. Les consignes sont écrites en anglais, que les modèles de 1 à 3 milliards de paramètres suivent nettement mieux ; la réponse, elle, est demandée dans la langue de l'utilisateur.

Le plus long échange tient dans environ 1 900 tokens, réponse comprise. C'est ce qui a permis de réduire la fenêtre de Qwen à 2 048 tokens, et ce n'était pas un réglage cosmétique :

contextPromise = native
  .initLlama({
    model: modelFile().uri,
    // Our longest prompt + answer fits in ~1,900 tokens; a 4k context doubled the KV cache.
    n_ctx: 2048,
    n_gpu_layers: 99,
    // Locking 1 GB of weights in RAM got the app killed on smaller phones; mmap pages them.
    use_mlock: false,
  })

La première version chargeait le modèle avec une fenêtre de 4 096 tokens et verrouillait ses poids en mémoire vive. Sur un appareil de 2 Go, le système tuait l'application. Deux changements ont suffi : une fenêtre deux fois plus petite, qui divise par deux le cache clé-valeur, et des poids projetés en mémoire plutôt que verrouillés, que l'OS peut paginer.

Contraindre la sortie, puis s'en méfier quand même

Les deux moteurs génératifs reçoivent un schéma JSON qui contraint le décodage : un objet avec un titre de sélection et une liste de cinq à six choix {id, reason}. Côté Apple, le schéma est converti à l'exécution en schéma de génération natif ; côté Qwen, il passe par response_format en mode strict. Le mode « réflexion » de Qwen3 est désactivé : des tokens de raisonnement sont du gaspillage quand la sortie est un JSON contraint.

La contrainte garantit une forme, pas un contenu. La validation se fait donc en cascade :

  • extraction de la première valeur JSON équilibrée, en ignorant les balises et le texte parasites ;
  • vérification contre le schéma, en coupant les listes trop longues plutôt qu'en les rejetant, parce que les petits modèles produisent souvent un élément de trop ;
  • une seule relance, avec une température abaissée ;
  • puis des contrôles métier : identifiants inconnus ou en double retirés, raisons qui recopient la ligne de données remplacées, et liste complétée jusqu'à cinq depuis l'ordre déterministe.

Le dernier point vient d'un défaut réel. Avec le modèle de 1,7 milliard de paramètres, les raisons recopiaient les données (« comédie dramatique, 1998, 8,4, correspond à vos goûts ») et répétaient la même phrase d'un titre à l'autre. Alléger les lignes et remplacer l'exemple littéral du prompt par un gabarit à trous a réglé l'essentiel ; le filtre attrape le reste.

Autre leçon de terrain : un appel qui dépasse son délai ne s'arrête pas tout seul. La génération native continuait, et la relance tombait sur un contexte occupé. Au dépassement, le moteur est désormais arrêté explicitement, avec un délai de grâce, et on ne relance pas. Une file garantit aussi qu'une seule génération tourne à la fois, tous moteurs confondus.

Ce qui reste sur le téléphone, et ce qui n'y reste pas

Le profil de goûts et les conversations ne quittent pas l'appareil : il n'existe aucun serveur d'IA. Les recherches dans le catalogue, en revanche, partent vers les services de données de films, avec les genres et mots-clés issus de l'intention. Et la version gratuite affiche de la publicité, avec ce que cela implique côté identifiant publicitaire.

J'ai donc retiré de l'application et de la fiche App Store la formule « rien ne quitte le téléphone », qui était devenue fausse. La promesse exacte est plus étroite et vérifiable : aucun serveur d'IA, aucune conversation envoyée. Une promesse de confidentialité doit tenir face au code, pas seulement face au marketing.

Côté performances, la seule mesure consignée concerne Apple Intelligence sur un iPhone récent : chaque appel au modèle répond en 0,2 à 0,9 seconde, et une recommandation complète, recherches catalogue comprises, prend environ 14 secondes.

Ce que j'en retiens

  • Un petit modèle choisit bien, il cherche mal. Lui donner une liste fermée de dix candidats supprime les hallucinations de titres par construction.
  • La fenêtre de contexte est aussi un budget mémoire. Mesurer la taille réelle de ses prompts a permis de diviser le contexte par deux et de supprimer les arrêts forcés de l'application.
  • Chaque étape générative a un repli déterministe. Le modèle améliore le résultat ; il n'est jamais la condition pour en avoir un.
  • Une promesse de confidentialité se relit contre le code. La plus juste est souvent la plus étroite.

Un projet similaire, une fonctionnalité d'IA à faire tourner sur l'appareil plutôt que dans le cloud ? Parlons-en.