kazeia/KAZEIA-CLAUDE.md

459 KiB
Raw Permalink Blame History

You asked:

Tu connais le projet Kazeia, maintenant l'idée est de voir comment optimiser l'entrainement d'un modèle LLM pour qu'il soit le plus "compréhensif" possible, sinon peux tu proposer des pistes ?

Mar 11


Claude Replied:

Orchestrated empathy optimization strategies for therapeutic LLM training

Orchestrated empathy optimization strategies for therapeutic LLM training

Bonne question Richard, c'est vraiment le cœur du projet. Voici les pistes principales pour maximiser la qualité empathique du modèle Kazeia.

La donnée d'entraînement est le levier n°1. Un petit modèle bien entraîné sur des données de haute qualité battra un gros modèle mal alimenté. Concrètement :

Privilégie des corpus de conversations thérapeutiques réelles (anonymisées) ou des transcriptions d'entretiens motivationnels. Des datasets comme Counsel Chat, EmpatheticDialogues (Facebook Research), ou HOPE (annotés en stratégies thérapeutiques) sont de bonnes bases. Le défi pour toi c'est le français — il faudra soit traduire ces datasets avec un modèle de qualité, soit constituer un corpus francophone, par exemple à partir de transcriptions de lignes d'écoute (avec accord éthique évidemment).

La structure des données compte autant que le volume. Plutôt que des paires question/réponse brutes, structure tes exemples avec des annotations : l'émotion détectée, la stratégie de réponse utilisée (validation, reformulation, question ouverte, normalisation…), et le niveau de sévérité. Ça permet au modèle d'apprendre pourquoi une réponse est empathique, pas juste de la mimer.

Le fine-tuning supervisé (SFT) seul ne suffit pas pour un usage thérapeutique. Deux approches complémentaires :

Le DPO (Direct Preference Optimization) est probablement ta meilleure option vu les contraintes du projet. Tu crées des paires de réponses (une empathique, une maladroite) pour une même situation, et le modèle apprend à préférer la bonne. C'est plus simple à implémenter que du RLHF classique et ça fonctionne bien sur des petits modèles comme Qwen. Concrètement tu peux générer les réponses "mauvaises" en dégradant volontairement les bonnes (retirer la validation émotionnelle, donner des conseils non sollicités, minimiser la souffrance…).

Le RLHF avec des retours de professionnels de santé serait l'idéal à terme, mais c'est lourd à mettre en place. Garde-le pour une phase ultérieure si vous obtenez un partenariat clinique.

Quelques techniques spécifiques qui font la différence :

Sur le plan du prompting et du format d'entraînement, intègre un "chain of thought empathique" dans tes données : avant de formuler sa réponse, le modèle identifie d'abord l'émotion, puis la stratégie appropriée, puis génère la réponse. À l'inférence tu peux masquer ce raisonnement intermédiaire mais il améliore nettement la qualité.

Pour l'évaluation, ne te fie pas uniquement à la perplexité. Mets en place des métriques spécifiques : détection correcte de l'émotion, présence de validation émotionnelle dans la réponse, absence de conseils non sollicités, et surtout des évaluations humaines par des psychologues sur un jeu de test fixe.

Un point important vu que tu cibles un modèle compact pour Android offline : les techniques de distillation de connaissances peuvent être très efficaces. Tu peux utiliser un gros modèle (Claude, GPT-4…) pour générer un corpus synthétique de haute qualité de conversations empathiques en français, puis fine-tuner ton Qwen dessus. C'est un raccourci puissant pour compenser la rareté des données francophones.

Enfin, pense à l'apprentissage par curriculum : entraîne d'abord sur des conversations simples (émotions légères, réponses directes), puis augmente progressivement la complexité (crises, ambivalence, idéation suicidaire). Ça stabilise l'apprentissage et améliore la gestion des cas difficiles.

Tu veux qu'on creuse un de ces axes en particulier, ou qu'on regarde comment structurer concrètement le pipeline d'entraînement avec les outils que tu as déjà ?


You asked:

est ce que l'utilisation d'un modèle de detection d'emotion pourrait améliorer son fonctionnement et ces réponses ?

Mar 11


Claude Replied:

Évalué le potentiel thérapeutique de la détection émotionnelle

Évalué le potentiel thérapeutique de la détection émotionnelle

Oui, clairement. Un modèle de détection d'émotion dédié peut apporter un vrai gain, surtout dans un contexte thérapeutique où mal interpréter l'état émotionnel du patient a des conséquences directes sur la qualité de l'accompagnement.

Le problème actuel avec ton architecture, c'est que le LLM Qwen fait tout en même temps : il doit comprendre l'émotion, choisir la stratégie de réponse, et générer le texte. Sur un petit modèle destiné à tourner offline sur tablette Android, c'est beaucoup demander. En déchargeant la détection d'émotion sur un modèle spécialisé, tu permets au LLM de se concentrer sur ce qu'il fait le mieux : générer une réponse adaptée à partir d'une émotion déjà identifiée.

Concrètement, l'architecture deviendrait un pipeline en deux étapes. Le message du patient passe d'abord par le classifieur d'émotions, qui produit une étiquette (tristesse, anxiété, colère, joie, désespoir…) avec un score de confiance. Cette information est ensuite injectée dans le prompt du LLM, par exemple : "L'utilisateur exprime de la tristesse (confiance : 0.87) avec des indices d'anxiété secondaire (0.42). Adapte ta réponse en conséquence." Le LLM n'a plus à deviner, il sait.

Pour le choix du modèle, en français et avec ta contrainte offline sur Android, plusieurs options :

CamemBERT fine-tuné sur de la détection d'émotion est le candidat naturel. C'est un modèle BERT francophone, relativement léger (environ 110M de paramètres pour la base), et il existe déjà des versions fine-tunées sur des tâches de sentiment. Tu devrais le spécialiser davantage sur des émotions thérapeutiques — les taxonomies classiques (Ekman, Plutchik) sont trop grossières pour ton usage. Définis plutôt une taxonomie adaptée au contexte clinique : tristesse, anxiété, colère, honte, culpabilité, désespoir, solitude, frustration, soulagement, espoir, gratitude, et un état neutre.

En alternative plus légère, un DistilCamemBERT ou un modèle encore plus compact type TinyBERT distillé ferait environ 30-60M de paramètres, ce qui est très confortable pour une tablette Android via ONNX Runtime.

Les gains concrets que tu peux en attendre :

Sur la qualité des réponses, le LLM ne confondra plus un patient en colère avec un patient triste. C'est un problème fréquent avec les petits modèles — ils captent le ton négatif mais nuancent mal. Or la réponse thérapeutique appropriée est très différente : valider la colère versus accompagner la tristesse.

Sur le PHQ-9, tu peux utiliser le classifieur pour détecter des signaux d'alerte entre les évaluations formelles. Si le modèle détecte une montée progressive du désespoir sur plusieurs conversations, tu peux proposer le PHQ-9 de manière anticipée plutôt que d'attendre les 7 jours configurés. C'est un vrai plus pour la sécurité du patient.

Sur l'avatar 3D que vous développez, l'émotion détectée peut directement piloter les expressions faciales et le ton de la voix synthétisée. Au lieu que l'avatar ait une expression générique, il peut montrer de l'inquiétude quand le patient est triste, de la chaleur quand il exprime de la gratitude. Ça renforce considérablement l'alliance thérapeutique.

Un point d'attention important : ne te repose pas uniquement sur le texte. Dans un contexte thérapeutique, l'émotion exprimée verbalement et l'émotion réelle divergent souvent. Le patient dit "ça va" mais son ton de voix dit le contraire. Puisque tu as déjà Whisper dans ton pipeline pour le STT, tu pourrais envisager une détection d'émotion multimodale — une branche analyse le texte transcrit, une autre analyse les caractéristiques acoustiques (prosodie, débit, pauses, tremblements) directement depuis l'audio. La fusion des deux signaux est bien plus fiable que le texte seul. Des modèles comme Wav2Vec2 fine-tuné sur de la reconnaissance d'émotion vocale pourraient servir de base pour la branche audio, et ça reste portable sur Android via ONNX.

Le coût en termes d'implémentation est raisonnable. Un modèle BERT-like pour la classification d'émotion, c'est une inférence de quelques millisecondes sur tablette — négligeable comparé aux 1500ms de ton LLM. L'empreinte mémoire supplémentaire serait d'environ 200-400 Mo selon le modèle choisi, ce qui reste gérable.

Tu veux qu'on détaille le pipeline multimodal texte + audio, ou qu'on se concentre d'abord sur la mise en place du classifieur texte seul ?


You asked:

justement existe t'il un STT qui permette aussi de faire de la detection d'emotion ?

Mar 11


Claude Replied:

Investigated multimodal STT models combining speech recognition with emotion detection

Investigated multimodal STT models combining speech recognition with emotion detection

Bonne intuition — combiner les deux dans un seul modèle simplifierait ton pipeline et éviterait de faire tourner deux modèles séparés sur la tablette.

La réponse courte : il n'existe pas vraiment de modèle unique clé en main qui fasse les deux parfaitement, mais il y a des approches très prometteuses qui s'en rapprochent.

Le candidat le plus sérieux, c'est Whisper lui-même, fine-tuné. Whisper encode l'audio dans des représentations internes très riches qui captent bien plus que le texte — elles contiennent implicitement des informations prosodiques (intonation, débit, énergie, pauses). L'idée serait de récupérer les embeddings de l'encoder de Whisper et d'ajouter une tête de classification d'émotion par-dessus, en plus de la tête de décodage texte existante. Concrètement tu obtiens un modèle multi-tâche : une entrée audio, deux sorties (transcription + émotion). L'avantage est énorme pour toi — un seul passage d'inférence, un seul modèle à déployer sur la tablette.

En pratique, des travaux de recherche ont montré que ça fonctionne bien. Le papier "Exploring Wav2Vec 2.0 and Whisper for Speech Emotion Recognition" montre que les features de Whisper sont compétitives pour la SER (Speech Emotion Recognition). Tu gardes l'encoder Whisper gelé ou partiellement fine-tuné, tu branches un petit classifieur (quelques couches linéaires) sur les représentations intermédiaires, et tu entraînes sur un dataset d'émotion vocale.

Pour les datasets d'entraînement de la partie émotion, en français c'est le point délicat. Quelques ressources existent : RAVDESS et IEMOCAP sont les références mais en anglais. En français, tu as RECOLA (interactions spontanées annotées en émotion continue) et AlloSat (satisfaction dans des conversations téléphoniques). C'est limité, donc une stratégie réaliste serait d'entraîner d'abord sur les gros datasets anglais puis de fine-tuner sur le peu de données françaises disponibles — le transfer learning fonctionne bien pour les émotions vocales car la prosodie émotionnelle est en grande partie universelle (la tristesse "sonne" pareil dans beaucoup de langues).

L'alternative plus mature technologiquement, c'est Wav2Vec2. Il est davantage utilisé que Whisper pour la SER dans la littérature. Des modèles comme ehcalabres/wav2vec2-lg-xlsr-en-speech-emotion-recognition sur HuggingFace font déjà de la détection d'émotion directement depuis l'audio. Le souci c'est que ce n'est que de la détection d'émotion, pas du STT. Donc tu te retrouves avec deux modèles séparés. Cela dit, Wav2Vec2 et Whisper partagent une architecture encoder similaire, et il existe des travaux sur le partage de l'encoder entre les deux tâches.

Pour ton cas spécifique sur Android offline, voici ce que je recommanderais :

L'approche la plus pragmatique serait de prendre Whisper small (que tu utilises déjà), d'extraire les embeddings de son encoder, et d'entraîner un petit classifieur d'émotion à côté. À l'inférence, l'audio passe une seule fois dans l'encoder Whisper, puis les représentations partent vers deux branches : le décodeur pour la transcription et le classifieur pour l'émotion. Le surcoût en temps d'inférence est quasi nul (le classifieur c'est quelques millisecondes), et tu n'ajoutes que quelques Mo de poids supplémentaires.

Le pipeline ressemblerait à ça :

Audio patient
     │
     ▼
┌─────────────┐
│   Whisper    │
│   Encoder   │  ← un seul passage
└──────┬──────┘
       │
   ┌───┴───┐
   ▼       ▼
┌──────┐ ┌──────────┐
│Decoder│ │Classifieur│
│ STT  │ │ Émotion   │
└──┬───┘ └────┬─────┘
   │          │
   ▼          ▼
 Texte    Émotion (tristesse 0.82, anxiété 0.35...)
   │          │
   └────┬─────┘
        ▼
   Prompt LLM enrichi

Un point à ne pas négliger : au-delà de l'émotion catégorielle (triste, anxieux, en colère), les caractéristiques acoustiques brutes sont aussi très informatives en contexte clinique. Un débit de parole qui ralentit sur plusieurs séances, des pauses de plus en plus longues, une énergie vocale qui baisse — ce sont des marqueurs que les psychiatres utilisent intuitivement. Tu pourrais extraire ces features en parallèle de la classification d'émotion, toujours depuis le même encoder Whisper, et les logger pour que le médecin ait accès à une vue longitudinale via l'interface soignant. Ce serait un vrai différenciateur pour Kazeia par rapport aux chatbots classiques.

Tu veux qu'on détaille l'implémentation technique du fine-tuning de Whisper en multi-tâche, ou qu'on regarde d'abord quels datasets seraient exploitables pour entraîner la partie émotion en français ?


You asked:

SI on commence à utiliser des modèles différents j'en viens à me demander quelle plateforme nous pourrions utiliser pour Kazeia ? Pourrions nous utiliser directement un iPad avec une puce M4 ? (mais je ne sais pas ce qu'il a comme RAM)

Mar 11


Claude Replied:

Investigated iPad M4 feasibility and RAM specifications for Kazeia

Investigated iPad M4 feasibility and RAM specifications for Kazeia

Bonne question — le choix de la plateforme matérielle est déterminant vu le nombre de modèles qu'on empile. Laisse-moi vérifier les specs actuelles des iPad M4.

Synthesized information pour évaluer compatibilité iPad M4

Synthesized information pour évaluer compatibilité iPad M4

C'est une question très pertinente. Voici ce que ça donne concrètement.

Côté RAM sur l'iPad Pro M4 : les modèles 256 Go et 512 Go ont 8 Go de RAM, tandis que les versions 1 To et 2 To montent à 16 Go EveryiPad.com. Il y a même un détail intéressant : le nouvel iPad Air M4 (mars 2026) embarque 12 Go de RAM en standard Macworld. Donc selon la config, tu as entre 8 et 16 Go de mémoire unifiée avec 120 Go/s de bande passante mémoire Wikipedia, plus un Neural Engine 16 cœurs.

Est-ce que c'est suffisant pour Kazeia ? Oui, mais avec des compromis importants. Faisons le calcul de ton pipeline complet :

Pour le LLM (Qwen fine-tuné), en INT4 quantisé un modèle ~1.7B paramètres pèse environ 1-1.5 Go en mémoire. C'est confortable. Pour Whisper small (ton STT), c'est environ 500 Mo. Le classifieur d'émotion (CamemBERT/DistilCamemBERT) ajouterait 200-400 Mo. Piper TTS est très léger, environ 50-100 Mo. L'avatar Unity consommerait 500 Mo à 1 Go selon la complexité. Au total, tu arrives à environ 3-4 Go de RAM pour tout le pipeline, ce qui laisse de la marge même sur un iPad 8 Go.

Le vrai avantage de l'iPad M4 par rapport à Android, c'est l'écosystème d'inférence. Apple a publié des guides détaillés pour déployer des LLM via Core ML sur Apple Silicon, avec des optimisations spécifiques comme la quantization INT4 et le KV cache stateful Apple Machine Learning Research. Whisper.cpp supporte nativement le Neural Engine via Core ML, avec des accélérations de plus de 3x par rapport au CPU seul GitHub. Et des projets récents montrent qu'on peut faire tourner Qwen 3, Llama 3.2 et d'autres modèles GGUF sur iOS avec 15 à 30 tokens par seconde via llama.cpp et Metal DEV Community. C'est significativement plus rapide que tes 30-50 tokens/s actuels en HuggingFace sur PC.

Cela dit, il y a des inconvénients majeurs à considérer :

Le développement est beaucoup plus contraint sur iOS/iPadOS. Tu passes de Python (Flask, PyTorch, ChromaDB) à Swift/Objective-C avec Core ML. C'est une réécriture quasi complète. Sur Android tu avais au moins la possibilité d'utiliser des runtimes comme ONNX Runtime ou llama.cpp directement. Sur iPad aussi d'ailleurs, via llama.cpp et Metal, mais l'intégration Unity + Core ML + llama.cpp demande un travail d'ingénierie conséquent.

La distribution est verrouillée par l'App Store. Pour un dispositif médical déployé en hôpital, c'est un vrai problème. Apple a un programme Enterprise et MDM (Mobile Device Management) qui permet de déployer des apps en interne, mais c'est plus complexe qu'un APK Android qu'on sideload sur une tablette.

Le coût matériel est bien supérieur. Un iPad Pro M4 c'est 1200-2000€ selon la config, contre 300-600€ pour une tablette Android performante avec un bon SoC (type Samsung Galaxy Tab S9 avec Snapdragon 8 Gen 2).

Ma recommandation pour ton cas : l'iPad M4 est techniquement séduisant, mais pour un déploiement hospitalier en contexte médical validé, Android reste plus pragmatique pour plusieurs raisons : coût par unité nettement inférieur (tu vas déployer sur plusieurs services), distribution plus simple en environnement contrôlé, et l'écosystème Android pour l'inférence ML a énormément progressé (ONNX Runtime Mobile, TFLite, llama.cpp avec Vulkan). Les Snapdragon récents ont aussi des NPU performants.

Par contre, si vous avez le budget et que le parc est limité (quelques tablettes pilotes), l'iPad M4 avec 16 Go donnerait une expérience utilisateur plus fluide et plus fiable grâce à la standardisation du hardware Apple — pas de fragmentation comme sur Android.

Tu veux qu'on compare les performances concrètes du pipeline complet sur les deux plateformes, ou qu'on explore la question du déploiement MDM en milieu hospitalier ?


You asked:

OK, alors quel serait le meilleur candidat pour une tablette, non Apple

Mar 11


Claude Replied:

Investigated optimal Android tablet specifications for enterprise deployment

Investigated optimal Android tablet specifications for enterprise deployment

Bonne question, il faut trouver le bon équilibre entre puissance de calcul (NPU + GPU), RAM, et viabilité pour un déploiement médical. Laisse-moi vérifier ce qui est disponible actuellement.

Rassemblé données pour formuler recommandation tablette

Rassemblé données pour formuler recommandation tablette

Excellent, j'ai maintenant une vue claire. Voici mon analyse.

Le meilleur candidat pour Kazeia, c'est le OnePlus Pad 3. Et de loin pour ton cas d'usage. Voici pourquoi.

Il embarque le Snapdragon 8 Elite avec 12 ou 16 Go de RAM GSMArena, à un prix de 699$ pour la version de base 9to5Google. Le Snapdragon 8 Elite est crucial pour toi parce que son NPU Hexagon est coté à 75 TOPS, contre 45 TOPS pour la génération précédente, et il supporte l'inférence on-device pour des modèles jusqu'à 7B paramètres Technosports. Pour ton Qwen ~1.7B, c'est largement dimensionné.

En termes de performances concrètes pour l'inférence LLM, Qualcomm a mesuré environ 18 tokens/seconde sur le Snapdragon 8 Elite pour un Llama 2 7B quantisé en 4 bits Hugging Face. Ton modèle Qwen étant beaucoup plus petit (~1.7B), tu peux raisonnablement espérer 40-60 tokens/seconde, ce qui est bien supérieur à tes 30-50 tokens/s actuels sur PC avec HuggingFace.

Pourquoi le OnePlus Pad 3 plutôt que le Samsung Galaxy Tab S10 Ultra ? Le Tab S10 Ultra a un Dimensity 9300+, et c'est la première fois que Samsung utilise MediaTek dans sa gamme Tab S au lieu de Snapdragon Wikipedia. Le problème, c'est que l'écosystème logiciel pour l'inférence ML est nettement plus mature sur Qualcomm. Les NPU Hexagon de Qualcomm sont les seuls NPU mobiles avec un jeu d'instructions ouvert, ce qui a attiré beaucoup plus de travaux d'optimisation pour le déploiement de LLM arXiv. Qualcomm propose le QNN SDK pour déployer directement sur le NPU, ce que MediaTek ne permet pas aussi facilement.

De plus, le Snapdragon 8 Elite délivre 300% de performance NPU en plus par rapport au Snapdragon 8 Gen 3 Abdelkareem Elkhateb. Le rapport qualité/prix est imbattable pour ton projet.

Faisons le budget mémoire de ton pipeline complet sur le OnePlus Pad 3 (12 Go de RAM) :

Modèle Qwen 1.7B (INT4)          ~1.0 Go
Whisper small (encoder partagé)    ~0.5 Go
Classifieur émotion (DistilBERT)   ~0.2 Go
Piper TTS                          ~0.1 Go
Avatar Unity (scène 3D)            ~0.8 Go
KV Cache + buffers                 ~0.5 Go
OS Android + overhead              ~3.0 Go
─────────────────────────────────────────
TOTAL                              ~6.1 Go
Marge restante                     ~5.9 Go

Tu as presque 6 Go de marge, c'est très confortable. Et si tu prends la version 16 Go, tu pourrais même envisager un modèle plus gros à terme (3B paramètres par exemple).

Pour le runtime d'inférence sur Android, tu as plusieurs options qui fonctionnent bien avec le Snapdragon 8 Elite. llama.cpp compilé avec Vulkan pour le GPU ou en mode CPU optimisé ARM (avec les instructions i8mm) est le plus polyvalent. Le QNN SDK de Qualcomm permet de cibler directement le NPU Hexagon pour un maximum d'efficacité énergétique — essentiel pour une tablette en usage prolongé en milieu hospitalier. Et ONNX Runtime Mobile fonctionne bien pour les modèles plus petits comme ton classifieur d'émotion et Whisper.

Les avantages spécifiques pour le contexte hospitalier :

La batterie de 12 140 mAh GSMArena est massive — ça représente facilement une journée complète d'utilisation en inférence intermittente. La charge 80W permet de recharger rapidement entre les sessions. Et à ~700$ la tablette contre ~1300$ minimum pour un iPad Pro, sur un déploiement de 10-20 unités, l'économie est considérable.

Le point d'attention principal c'est la fragmentation logicielle qu'on mentionnait plus tôt. Android a plus de 5000 variantes de devices avec différentes architectures NPU, ce qui crée des cauchemars de test F22 Labs. Mais en ciblant uniquement le OnePlus Pad 3 (ou un modèle Snapdragon 8 Elite spécifique), tu élimines ce problème. Tu figes ton matériel cible et tu optimises exclusivement pour lui — exactement comme Apple le fait, mais sur Android.

Tu veux qu'on détaille le pipeline technique complet pour déployer les modèles sur le Snapdragon 8 Elite, ou qu'on regarde d'abord les aspects MDM/déploiement en milieu hospitalier ?


You asked:

non je prendrai forcement la 16Go, vois tu un autre candidat ?

Mar 11


Claude Replied:

Prospectait d'autres candidats tablettes avec 16GB RAM

Prospectait d'autres candidats tablettes avec 16GB RAM

Bonne idée les 16 Go, ça te donne de la marge pour faire évoluer les modèles. Laisse-moi chercher les autres candidats sérieux.

Orchestrated device comparison criteria for medical deployment suitability

Orchestrated device comparison criteria for medical deployment suitability

Voici les candidats sérieux en 16 Go pour faire tourner Kazeia. Je les compare sur ce qui compte pour toi : puissance NPU, écosystème d'inférence ML, et viabilité en déploiement médical.

1. OnePlus Pad 3 (16 Go / 512 Go) — ~750$ Reste mon premier choix. Snapdragon 8 Elite, 16 Go de RAM, 12 140 mAh GSMArena. Le NPU Hexagon à 75 TOPS est le mieux supporté par l'écosystème d'inférence Android (QNN SDK, llama.cpp optimisé). Seul bémol : pas de sécurité biométrique, juste un PIN Tom's Guide, et seulement 3 ans de mises à jour logicielles. Pour un déploiement médical, l'absence de biométrie est un peu gênant mais pas bloquant si les tablettes restent dans les chambres.

2. Honor MagicPad 3 Pro — pas encore de prix confirmé C'est un candidat très intéressant qui vient d'être annoncé : Snapdragon 8 Elite Gen 5, 12 ou 16 Go de RAM, batterie de 12 450 mAh avec charge 80W GSMArena. L'avantage sur le OnePlus c'est qu'il a le même SoC Snapdragon 8 Elite (donc même écosystème NPU Hexagon) avec une batterie silicone-carbone qui offre une longévité exceptionnelle. Sur le MagicPad 3 standard, la batterie silicone-carbone dure remarquablement longtemps SlashGear. Le gros bémol : Honor ne propose qu'un seul upgrade Android et deux ans de patches de sécurité TechRadar, ce qui est catastrophique pour un usage médical. Et la disponibilité en France/Europe n'est pas garantie sur la version Pro.

3. Samsung Galaxy Tab S11 Ultra (16 Go / 1 To) — ~1 619$ Dimensity 9400+ en 3 nm, jusqu'à 16 Go de RAM GSMArena. Gros avantages pour le contexte médical : certification IP68 eau et poussière MobGadgets, et surtout 7 ans de mises à jour Android et patches de sécurité Wikipedia. C'est énorme pour un déploiement hospitalier où tu veux que la tablette soit maintenue pendant des années. En revanche, le Dimensity 9400+ utilise le NPU MediaTek APU 790 à ~48 TOPS — puissant, mais l'écosystème d'inférence est moins mature que Qualcomm. Google vient de sortir LiteRT avec un accélérateur NeuroPilot natif pour MediaTek Google Developers, ce qui améliore les choses, mais c'est encore plus récent et moins éprouvé que le QNN SDK de Qualcomm. Autre problème : le 16 Go n'est disponible qu'avec la version 1 To MobGadgets, ce qui fait exploser le prix. Et pour 1 619$ par tablette, sur 10-20 unités, c'est un budget considérable.

4. RedMagic Astra — ~500-600$ Snapdragon 8 Elite avec jusqu'à 24 Go de RAM LPDDR5T Android Central — de loin la RAM la plus généreuse et la plus rapide. Le LPDDR5T offre une bande passante mémoire supérieure, ce qui est directement bénéfique pour l'inférence LLM (qui est memory-bandwidth bound). Le problème : l'écran ne fait que 9.06 pouces GamesRadar+. Pour un chatbot thérapeutique avec avatar 3D destiné à des patients, c'est trop petit. C'est vraiment conçu comme une tablette gaming compacte.

Mon classement pour Kazeia :

Le OnePlus Pad 3 reste le meilleur compromis global. Snapdragon 8 Elite pour le NPU, 16 Go, bon prix, écran 13.2 pouces adapté à l'interaction patient/avatar. Le principal risque c'est les 3 ans de mises à jour seulement.

Si la longévité logicielle est prioritaire pour la validation médicale (et elle devrait l'être), le Samsung Galaxy Tab S11 Ultra avec ses 7 ans de support est imbattable. Tu paies plus cher et tu perds en efficacité NPU, mais tu gagnes en pérennité, certification IP68, et crédibilité institutionnelle — un hôpital sera plus confiant avec du Samsung qu'avec du OnePlus. Et le support LiteRT/NeuroPilot de MediaTek progresse vite.

Surveille aussi le Honor MagicPad 3 Pro quand les prix et la disponibilité seront confirmés — si Honor améliore sa politique de mises à jour, ça pourrait devenir le meilleur candidat technique.

Tu veux qu'on creuse un de ces choix en particulier, ou qu'on passe à l'architecture logicielle pour déployer le pipeline sur l'une de ces tablettes ?


You asked:

très bien, par rapport aux actuels LLM utilisé dans Kazeia, nous continuerions à utiliser whisper pour le STT, mais nous pourrions utiliser QWEN3-TTS pour le clonage vocale, est ce que cela est bien compatible avec les performances de ces tablettes

Mar 16


Claude Replied:

Investigated Qwen3-TTS compatibility with tablet hardware specifications

Investigated Qwen3-TTS compatibility with tablet hardware specifications

Bonne idée de regarder Qwen3-TTS, c'est tout récent. Laisse-moi vérifier ses specs et sa compatibilité avec ces tablettes.

Évalué l'incompatibilité majeure de Qwen3-TTS avec les tablettes Android

Évalué l'incompatibilité majeure de Qwen3-TTS avec les tablettes Android

C'est une excellente idée sur le papier — Qwen3-TTS est impressionnant — mais il y a un problème majeur de compatibilité avec le déploiement sur tablette Android. Laisse-moi t'expliquer pourquoi.

Le problème fondamental : Qwen3-TTS est conçu pour des GPU CUDA, pas pour du mobile.

Le modèle 1.7B pèse 4.54 Go en mémoire, le 0.6B fait 2.52 Go DEV Community. Côté matériel, la recommandation pour un usage en production est un RTX 3090 ou mieux, et le modèle 0.6B peut tourner sur des GPU plus anciens mais risque de ne pas atteindre le temps réel DEV Community.

Concrètement, le modèle s'appuie sur vLLM pour le déploiement et l'inférence, et actuellement seul le mode offline est supporté GitHub. Le code d'inférence officiel utilise PyTorch avec FlashAttention 2 en bfloat16, qui ne fonctionne qu'avec CUDA Hugging Face. Il n'existe aucun export ONNX officiel, aucun support mobile, aucune version quantisée pour edge device. D'ailleurs quelqu'un a déjà ouvert une issue sur sherpa-onnx demandant le support de Qwen3-TTS GitHub, mais sans réponse pour l'instant.

Le budget mémoire exploserait. Si tu empiles les deux modèles simultanément sur la tablette :

Qwen chat LLM (~1.7B, INT4)       ~1.0 Go
Qwen3-TTS 0.6B (FP16)             ~2.5 Go   ← problème
  ou Qwen3-TTS 1.7B (FP16)        ~4.5 Go   ← gros problème
Whisper small                      ~0.5 Go
Classifieur émotion                ~0.2 Go
Avatar Unity                      ~0.8 Go
OS + overhead                     ~3.0 Go
─────────────────────────────────────────────
TOTAL (avec TTS 0.6B)             ~8.0 Go
TOTAL (avec TTS 1.7B)             ~10.0 Go

Avec le modèle 0.6B, tu serais déjà à la limite sur 16 Go, et c'est en supposant qu'on arrive à le faire tourner — ce qui n'est pas acquis vu l'absence de support mobile. Même quantisé en INT4, le 0.6B ferait encore ~800 Mo, mais l'architecture multi-codebook avec MTP (Multi-Token Prediction) de Qwen3-TTS est complexe et n'a pas été testée sur des runtimes mobiles.

C'est d'autant plus problématique que Qwen3-TTS a le même souci qu'Auralis/XTTS v2 que tu as déjà identifié dans ton projet : la dépendance à vLLM et CUDA. Tu avais déjà conclu qu'Auralis était incompatible avec Android pour exactement ces raisons. Qwen3-TTS, bien qu'architecturalement différent (multi-codebook LM vs. GPT+DiT), a les mêmes contraintes de déploiement.

Cela dit, Qwen3-TTS a un potentiel énorme pour Kazeia — mais pas directement sur la tablette. Voici ce que je recommanderais comme stratégie :

Pour le clonage vocal initial (créer la voix personnalisée du chatbot, ou à terme la voix d'un proche du patient), utilise Qwen3-TTS sur un PC/serveur. Le modèle supporte le clonage zero-shot avec seulement 3 secondes d'audio de référence GitHub et couvre le français parmi ses 10 langues supportées GitHub. Tu fais le clonage une fois, tu génères un profil vocal, et tu exportes ce que tu en as besoin.

Pour la synthèse en temps réel sur la tablette, reste sur Piper TTS. C'est léger (~50-100 Mo), rapide, compatible Android, et tu peux fine-tuner des voix personnalisées. L'idée serait d'utiliser Qwen3-TTS pour créer des données d'entraînement de haute qualité (un corpus audio dans la voix clonée), puis d'entraîner un modèle Piper sur ces données. Tu obtiens le meilleur des deux mondes : la qualité de clonage de Qwen3-TTS et la légèreté de Piper pour le déploiement.

Alternativement, surveille sherpa-onnx — c'est le projet qui a le plus de chances de porter Qwen3-TTS vers ONNX et donc vers Android. Si ça arrive dans les prochains mois, ça changerait la donne. Mais pour l'instant, ce n'est pas une option fiable pour planifier ton architecture.

Tu veux qu'on explore cette stratégie hybride (Qwen3-TTS serveur + Piper tablette), ou qu'on regarde d'autres alternatives TTS compatibles Android qui offriraient un meilleur clonage vocal que Piper ?


You asked:

tu parles de sherpa-onnx, mais qu'est ce qu'il pourrait apporter au developpement sur tablette ?

Mar 19


Claude Replied:

Investigated sherpa-onnx's potential benefits for tablet development

Investigated sherpa-onnx's potential benefits for tablet development

Très bonne question, sherpa-onnx pourrait en fait être un game-changer pour ton projet. Laisse-moi creuser ce qu'il propose exactement.

Synthesized sherpa-onnx insights for tablet development applications

Synthesized sherpa-onnx insights for tablet development applications

Sherpa-onnx pourrait en fait devenir la couche d'unification de tout ton pipeline audio sur tablette. C'est bien plus qu'un simple wrapper ONNX. Voici ce qu'il apporterait concrètement à Kazeia.

Sherpa-onnx, c'est quoi exactement ? C'est un framework développé par l'équipe next-gen Kaldi (k2-fsa) qui fournit du speech-to-text, text-to-speech, diarisation de locuteurs, amélioration de la parole, séparation de sources et VAD, le tout via ONNX Runtime et sans connexion Internet GitHub. Le point clé : il supporte Android, iOS, HarmonyOS, Raspberry Pi, RISC-V, et même des NPU spécifiques comme les RK NPU et Ascend NPU GitHub. En gros, c'est exactement le type de framework conçu pour le déploiement offline sur device embarqué — ton cas d'usage.

Ce que ça changerait pour Kazeia :

Premièrement, tu aurais un runtime unifié pour tout le pipeline audio. Actuellement dans ta documentation, tu utilises Whisper en Python pour le STT et Auralis/XTTS pour le TTS, deux systèmes séparés avec des dépendances différentes. Sherpa-onnx regroupe STT + TTS + VAD dans un seul framework, avec des APIs en C++, Java, Kotlin, C#, Swift, Dart, et même une intégration Unity existante GitHub. L'intégration Unity est particulièrement intéressante pour toi puisque ton avatar 3D est développé en Unity. Il existe déjà un package Unity "PiperTTS" basé sur sherpa-onnx qui permet d'utiliser les voix Piper directement dans Unity sur Android GitHub. C'est littéralement ton cas d'usage : avatar Unity + TTS offline sur tablette Android.

Deuxièmement, les modèles TTS pour le français sont déjà disponibles. Sherpa-onnx fournit des APK TTS prêts à l'emploi pour le français GitHub, basés sur les voix Piper. Ces APK sont classés par architecture (arm64-v8a pour les tablettes modernes), par langue, et par modèle. Tu n'aurais pas à faire la conversion toi-même — c'est pré-empaqueté.

Troisièmement, côté STT, sherpa-onnx supporte Whisper mais aussi d'autres modèles potentiellement plus adaptés au mobile. Le changelog montre un support récent pour des modèles comme SenseVoice, FireRedASR, NeMo Canary, et des modèles Zipformer optimisés pour le streaming Pub.dev. L'avantage des modèles Zipformer par rapport à Whisper, c'est qu'ils supportent le streaming en temps réel : le patient parle et la transcription apparaît au fur et à mesure, au lieu d'attendre la fin de la phrase. Pour un chatbot thérapeutique, cette réactivité est importante.

Quatrièmement, et c'est crucial pour ton projet, sherpa-onnx inclut un VAD (Voice Activity Detection) intégré. Le framework supporte "ten-vad" avec des APIs C++, JavaScript et WebAssembly Pub.dev. Le VAD détecte automatiquement quand le patient commence et arrête de parler, sans que tu aies à implémenter cette logique toi-même. Combiné au STT en streaming, ça donne une interaction vocale beaucoup plus naturelle.

Cinquièmement, sherpa-onnx a récemment ajouté un support QNN (Qualcomm QNN SDK) Pub.dev. C'est un détail technique mais c'est majeur pour toi : ça signifie que le framework peut potentiellement utiliser le NPU Hexagon du Snapdragon 8 Elite pour accélérer l'inférence des modèles audio. Au lieu de tout faire tourner sur le CPU (lent) ou le GPU (gourmand en batterie), les modèles STT et TTS pourraient être déchargés sur le NPU, qui est beaucoup plus efficace énergétiquement.

Ce que ça donnerait pour l'architecture Kazeia sur tablette :

┌─────────────────────────────────────────────┐
│              Application Unity               │
│         (Avatar 3D + Interface patient)      │
├─────────────────────────────────────────────┤
│              sherpa-onnx (C#/Java)           │
│  ┌──────┐  ┌──────┐  ┌──────┐  ┌────────┐  │
│  │ VAD  │→ │ STT  │  │ TTS  │  │Speaker │  │
│  │      │  │Whisper│  │Piper │  │  ID    │  │
│  └──────┘  └──────┘  └──────┘  └────────┘  │
├─────────────────────────────────────────────┤
│           ONNX Runtime Mobile               │
│     (CPU / GPU Vulkan / NPU via QNN)        │
├─────────────────────────────────────────────┤
│  llama.cpp          │  Classifieur émotion  │
│  (Qwen LLM, INT4)  │  (DistilCamemBERT)    │
├─────────────────────────────────────────────┤
│         Android / Snapdragon 8 Elite         │
└─────────────────────────────────────────────┘

L'avantage de cette architecture, c'est que sherpa-onnx gère toute la couche audio de manière cohérente, pendant que llama.cpp gère le LLM séparément. Les deux sont des projets C/C++ matures, optimisés pour ARM, et fonctionnent offline. Tu n'as plus Python nulle part dans la chaîne d'inférence sur la tablette — tout est natif.

Les limites à connaître. Sherpa-onnx ne supporte pas encore Qwen3-TTS, comme on l'a vu. Il ne fait pas non plus de clonage vocal à proprement parler — les voix Piper sont pré-entraînées. Pour le clonage, tu restes sur la stratégie qu'on a discutée : cloner la voix sur PC avec Qwen3-TTS, puis entraîner un modèle Piper personnalisé qui sera déployé via sherpa-onnx sur la tablette. Et l'intégration Unity sur Android a été demandée par la communauté GitHub et fonctionne, mais demande un peu de travail de configuration.

En résumé, sherpa-onnx ne résout pas le problème du clonage vocal en temps réel sur tablette, mais il te fournit une base solide et éprouvée pour unifier STT + TTS + VAD dans un seul framework natif, optimisé pour Android offline, avec une intégration Unity qui existe déjà. C'est exactement la brique d'infrastructure qui manque à Kazeia pour passer du prototype Python/Flask sur PC à un produit déployable sur tablette.

Tu veux qu'on détaille comment structurer le pipeline complet avec sherpa-onnx + llama.cpp + le classifieur d'émotion, ou qu'on explore la partie clonage vocal Piper à partir de données Qwen3-TTS ?


You asked:

est ce que tu crois qu'une implémentation from scratch pour implémenter nos modèles et pour matcher avec nos attentes (sur tablette) prendrait du temps ?

Mar 19


Claude Replied:

Évalué la faisabilité technique du déploiement sur tablette

Évalué la faisabilité technique du déploiement sur tablette

C'est LA question pragmatique. Soyons honnêtes et réalistes — oui, ça prendrait du temps, mais pas autant qu'on pourrait le craindre si vous structurez bien les phases. Voici mon estimation détaillée.

Le point de départ : où vous en êtes aujourd'hui. Vous avez un prototype fonctionnel en Python/Flask qui tourne sur PC avec Whisper, un Qwen fine-tuné, Auralis/XTTS, ChromaDB, et une interface web. Tout est en Python, tout dépend de CUDA, rien ne tourne sur mobile. C'est essentiellement une réécriture complète du runtime d'inférence, pas du code métier (la logique de conversation, le PHQ-9, la machine à états — tout ça reste).

Estimation par composant :

Pour la couche STT via sherpa-onnx, c'est le plus simple. Les modèles Whisper sont déjà convertis en ONNX et supportés nativement. L'intégration avec Unity via C# existe déjà dans des exemples. Comptez 1-2 semaines pour avoir le STT streaming qui fonctionne dans Unity sur Android avec le VAD.

Pour la couche TTS via sherpa-onnx + Piper, c'est relativement rapide aussi puisque les voix françaises Piper existent et le package Unity PiperTTS est documenté. Le gros du travail serait de fine-tuner une voix Piper personnalisée pour Kazeia (sur PC, avec vos échantillons audio). L'intégration sur tablette elle-même prendrait 1-2 semaines, mais l'entraînement de la voix personnalisée ajouterait 2-3 semaines supplémentaires si vous voulez un résultat de qualité.

Pour le LLM Qwen via llama.cpp, c'est le cœur du travail. Il faut convertir votre modèle fine-tuné au format GGUF, valider que la qualité des réponses ne se dégrade pas trop avec la quantization INT4, et intégrer llama.cpp dans l'application Android. La conversion et la validation prendraient 1-2 semaines. L'intégration native (JNI ou via un wrapper C#/Unity) ajouterait 2-3 semaines, surtout pour gérer correctement le streaming de tokens, le prompt templating, et le contexte conversationnel que vous gérez actuellement dans conversation_manager.py.

Pour le classifieur d'émotion, il faut fine-tuner un DistilCamemBERT sur des données d'émotion, le convertir en ONNX, et l'intégrer via ONNX Runtime Mobile. Le fine-tuning prendrait 2-3 semaines (collecte/préparation des données incluse). L'intégration sur tablette, 1 semaine — c'est un modèle léger avec une inférence simple.

Pour le portage de la logique métier (machine à états, PHQ-9, gestion de profil, historique de conversation), c'est un travail de réécriture de Python vers Java/Kotlin ou C#. La logique n'est pas complexe — votre conversation_manager.py fait 649 lignes, le phq9_manager.py fait 95 lignes. Comptez 2-3 semaines pour tout porter proprement avec la base SQLite locale.

Pour l'avatar Unity, vous l'avez déjà en cours de développement. L'intégration avec le pipeline audio (lip-sync depuis le TTS, expressions faciales depuis le classifieur d'émotion) ajouterait 2-3 semaines de travail.

Pour le serveur clinicien (l'interface médecin qui consulte les données PHQ-9 et les historiques), c'est en fait le plus simple — ça peut rester en Python/Flask sur un PC classique à l'hôpital. La tablette synchronise ses données SQLite avec le serveur quand elle est connectée au réseau local. Comptez 1-2 semaines pour implémenter la sync.

Au total, en estimation réaliste :

Composant                          Optimiste    Réaliste    Pessimiste
─────────────────────────────────────────────────────────────────────
STT (sherpa-onnx + Whisper)        1 sem        2 sem       3 sem
TTS (sherpa-onnx + Piper)          2 sem        3-4 sem     6 sem
LLM (llama.cpp + GGUF)             3 sem        4-5 sem     7 sem
Classifieur émotion                2 sem        3 sem       5 sem
Logique métier (portage)           2 sem        3 sem       4 sem
Avatar Unity (intégration audio)   2 sem        3 sem       4 sem
Serveur clinicien + sync           1 sem        2 sem       3 sem
Tests, debug, optimisation         2 sem        3-4 sem     6 sem
─────────────────────────────────────────────────────────────────────
TOTAL                              ~15 sem      ~22-24 sem  ~38 sem
                                   ~4 mois      ~5-6 mois   ~9 mois

Donc réalistement, entre 5 et 6 mois à deux développeurs. C'est ambitieux mais faisable, surtout si vous parallélisez : un développeur sur le pipeline d'inférence (LLM + STT + TTS + émotion) pendant que l'autre travaille sur l'avatar Unity et l'intégration finale.

Ce qui pourrait raccourcir significativement le délai :

Ne pas tout faire d'un coup. Adopter une approche par phases : d'abord un MVP tablette avec juste le LLM (llama.cpp) + STT (sherpa-onnx/Whisper) + TTS (Piper voix française par défaut) + interface texte simple. Ça, c'est faisable en 6-8 semaines. Ensuite vous ajoutez l'avatar, le classifieur d'émotion, le clonage vocal, la synchronisation serveur dans des itérations suivantes. Le MVP vous permet de tester tôt sur la tablette cible et de valider que les performances sont acceptables avant d'investir dans les couches plus complexes.

Ce qui pourrait allonger significativement :

La partie la plus risquée, c'est la qualité du LLM après quantization. Si votre Qwen fine-tuné perd trop en qualité empathique en passant en INT4, vous pourriez devoir réentraîner ou changer de modèle de base, ce qui ajouterait facilement un mois. L'autre risque c'est l'optimisation des performances sur la tablette spécifique — la fragmentation Android est réelle, et même en ciblant un seul device, il peut y avoir des surprises (throttling thermique, gestion mémoire Android qui tue les processus en arrière-plan…).

Mon conseil : commencez dès maintenant par convertir votre Qwen en GGUF et testez-le sur une tablette cible avec llama.cpp. C'est le test le plus critique et le plus rapide à faire. Si les performances et la qualité sont au rendez-vous, le reste suivra. Si ce n'est pas le cas, mieux vaut le savoir tout de suite plutôt qu'après 3 mois de développement.

Tu veux qu'on prépare un plan de développement phasé plus détaillé, ou qu'on commence par la conversion du modèle Qwen en GGUF pour valider la faisabilité ?


You asked:

mais il n'y a pas déjà un système STT sous Android ?

Mar 19


Claude Replied:

Examined Android's native speech recognition capabilities and offline constraints

Examined Android's native speech recognition capabilities and offline constraints

Très bonne remarque — oui, Android a un STT natif, et c'est une question importante à poser avant de réinventer la roue. Laisse-moi vérifier ce qu'il offre exactement et si ça suffirait pour Kazeia.

Synthétisé les capacités STT natives d'Android pour Kazeia

Synthétisé les capacités STT natives d'Android pour Kazeia

Très bon réflexe — oui, Android a un STT natif, et ça pourrait te faire économiser pas mal de travail. Mais il y a des nuances importantes pour ton cas.

Ce qui existe nativement sur Android :

Android intègre :antCitation[]{citations="a0863b32-9aff-4f2f-8b13-0f758ce057fc"}SpeechRecognizer, qui est directement disponible sans bibliothèque supplémentaire et fonctionne même hors ligne. Depuis Android 12 (API 31) et Android 13 (API 33), Google a ajouté des fonctionnalités spécifiques pour la reconnaissance vocale on-device Medium. Concrètement, tu peux appeler SpeechRecognizer.createOnDeviceSpeechRecognizer() et avoir du STT offline, en français, sans rien installer de plus. Ça tourne localement sur le device, assure la confidentialité et une faible latence, ne nécessite pas de connexion internet, est gratuit sans quota d'API, et est facile à intégrer WebRTC.ventures.

Pour Kazeia, ça coche beaucoup de cases : c'est offline, c'est gratuit, c'est natif Android, le français est supporté, et surtout c'est zéro effort d'intégration comparé à embarquer Whisper ou sherpa-onnx. Tu gagnerais les 1-2 semaines de travail d'intégration STT, et surtout tu n'as pas ~500 Mo de modèle Whisper à charger en mémoire.

Mais il y a des limites sérieuses pour un contexte thérapeutique :

La précision est moindre comparée aux solutions cloud, notamment pour les phrases complexes ou le vocabulaire spécialisé WebRTC.ventures. Dans un contexte de soutien émotionnel, les patients peuvent parler avec hésitation, des pauses longues, des mots chargés émotionnellement, parfois en chuchotant ou en pleurant. Le SpeechRecognizer natif de Google n'est pas optimisé pour ça — il est conçu pour des commandes vocales et de la dictée standard.

L'autre problème, c'est que tu perds complètement le contrôle sur le modèle. Tu ne peux pas le fine-tuner, tu ne peux pas extraire les features audio intermédiaires (prosodie, énergie vocale, pauses) pour ton classifieur d'émotion, et tu dépends des mises à jour Google pour la qualité. Rappelle-toi notre discussion sur le multi-tâche Whisper : l'idée était de partager l'encoder entre la transcription et la détection d'émotion. Avec le SpeechRecognizer natif, c'est une boîte noire — texte en entrée, texte en sortie, rien d'autre.

Et il y a un point pratique souvent sous-estimé : le SpeechRecognizer a des comportements parfois erratiques Medium selon les fabricants et versions Android. Samsung, OnePlus, et Google n'implémentent pas tous le mode offline de la même manière. Sur certains devices, le mode offline requiert de télécharger manuellement le pack de langue via les paramètres Google, ce qui n'est pas idéal pour un déploiement hospitalier où tu veux que tout fonctionne out of the box.

Ma recommandation pragmatique : utilise-le pour le MVP, puis migre.

Pour ta première phase (le prototype tablette en 6-8 semaines), le SpeechRecognizer natif est parfait. Ça fonctionne, c'est rapide à intégrer, et ça te permet de tester le reste du pipeline (LLM + logique conversationnelle + avatar) sans perdre du temps sur le STT. Tu branches le SpeechRecognizer natif, tu récupères le texte, tu l'envoies au LLM via llama.cpp, et tu as un prototype fonctionnel.

Dans une deuxième phase, quand le reste du pipeline est stable, tu remplaces le SpeechRecognizer par Whisper via sherpa-onnx. Ça te donne le contrôle sur les features audio pour la détection d'émotion, une meilleure gestion des situations vocales difficiles (pleurs, hésitations, chuchotements), et l'indépendance vis-à-vis de Google.

Le même raisonnement s'applique d'ailleurs au TTS natif Android (android.speech.tts.TextToSpeech). Il existe, il fonctionne offline en français, mais la qualité est robotique et inadaptée à un contexte thérapeutique. Pour le MVP tu pourrais l'utiliser temporairement, mais Piper via sherpa-onnx sera indispensable pour la version finale.

En résumé, le STT natif Android c'est un raccourci légitime pour démarrer, mais pas une solution finale pour un produit médical qui a besoin de fiabilité, de contrôle, et d'extraction de features émotionnelles. C'est la différence entre un prototype et un dispositif validé cliniquement.

Tu veux qu'on structure ce plan de développement phasé avec les technologies à utiliser à chaque étape ?


You asked:

unity a des pilotes sous android ? et le fait de cloner un visage, de le modeliser, puis de l'animer st possible avec Unity ?

Mar 19


Claude Replied:

Démêlé deux questions techniques sur Unity et l'animation faciale

Démêlé deux questions techniques sur Unity et l'animation faciale

Deux excellentes questions — laisse-moi vérifier l'état actuel du support Android dans Unity et les possibilités de clonage/animation faciale.

Rassemblé les informations pour répondre aux deux questions

Rassemblé les informations pour répondre aux deux questions

Deux questions, deux bonnes nouvelles.

Unity sur Android : oui, c'est du support natif de première classe.

Unity inclut un support Unity natif pour la plateforme Android, ce qui signifie que tu peux utiliser Unity et ses outils pour créer et distribuer des applications 2D et 3D pour Android. Unity supporte Android 7.1 "Nougat" (API 25) et au-dessus Unity, donc toutes les tablettes qu'on a évoquées sont compatibles. Tu développes dans l'éditeur Unity sur PC, tu builds en un clic un APK ou AAB, et ça tourne directement sur la tablette. Pas de "pilotes" à installer — Unity gère le rendu via OpenGL ES ou Vulkan sur Android, et le build process compile tout en code natif ARM via IL2CPP.

Unity 6.1 a même ajouté le support des grands écrans pliables Android et des tablettes PocketGamer, avec des optimisations spécifiques pour ces form factors. Et des plugins comme SALSA LipSync sont testés et supportés sur Android Crazyminnowstudio. Donc tu n'es pas du tout en terrain inconnu — des milliers d'apps Unity tournent sur tablettes Android.

Pour le clonage de visage, la modélisation et l'animation : oui, c'est faisable, mais en deux étapes distinctes.

L'idée de prendre une photo d'un proche du patient et d'en faire un avatar animé qui parle est techniquement réalisable, mais il faut bien séparer la création du modèle 3D (étape lourde, sur PC) de l'animation en temps réel (étape légère, sur tablette).

Pour la création du modèle 3D à partir d'une photo, ça ne se fait pas directement dans Unity sur la tablette. Tu utilises des outils externes sur PC comme DECA ou EMOCA (que tu avais déjà identifiés) pour reconstruire un mesh 3D du visage à partir d'une ou plusieurs photos. Il existe aussi des solutions plus accessibles comme Avatar SDK, qui génère des avatars 3D réalistes avec blendshapes depuis des photos et qui fournit un plugin Unity avec des blendshapes compatibles visèmes pour le lip-sync Avatarsdk. Le workflow serait : photo du proche → génération du mesh 3D + textures + blendshapes sur PC → export en format compatible Unity (FBX) → déploiement sur la tablette.

Pour l'animation en temps réel sur la tablette, c'est là que Unity brille. Plusieurs approches sont disponibles et éprouvées :

Pour le lip-sync, uLipSync est un plugin Unity open-source qui analyse les formes audio en temps réel via MFCC et drive les blendshapes du visage GitHub. Il fonctionne par analyse de la forme d'onde audio — tu lui envoies la sortie de ton TTS (Piper), et il anime automatiquement la bouche du personnage. C'est léger, ça tourne sur mobile, et c'est indépendant de la langue puisque ça analyse le signal audio, pas le texte. Pour les avatars 3D dans Unity, les visèmes (formes de bouche) extraites de l'audio sont utilisées pour piloter les blendshapes du visage du personnage DEV Community.

Pour les expressions émotionnelles, c'est là que ton classifieur d'émotion entre en jeu. Quand le classifieur détecte que le patient est triste, tu ajustes les blendshapes de l'avatar (sourcils baissés, regard doux, léger sourire compatissant). Des plugins comme SALSA LipSync Suite gèrent en parallèle le lip-sync, les émotions faciales (EmoteR), et les mouvements des yeux, le tout en temps réel sur Android Crazyminnowstudio. C'est exactement le type de comportement empathique que tu veux pour Kazeia.

Le pipeline complet pour les avatars personnalisés ressemblerait à ça :

PHASE 1 — Sur PC (une seule fois par patient/proche)
─────────────────────────────────────────────────────
Photo(s) du proche
        │
        ▼
DECA/EMOCA ou Avatar SDK
        │
        ▼
Mesh 3D + Textures + Blendshapes (visèmes + émotions)
        │
        ▼
Export FBX → transfert vers tablette


PHASE 2 — Sur tablette (temps réel, à chaque session)
──────────────────────────────────────────────────────
                    ┌─────────────────┐
                    │  Avatar Unity   │
                    │  (mesh importé) │
                    └────────┬────────┘
                             │
              ┌──────────────┼──────────────┐
              │              │              │
     ┌────────▼───┐  ┌──────▼─────┐  ┌────▼────────┐
     │  uLipSync  │  │  Émotions  │  │ Animations  │
     │ (audio TTS │  │ (classif.  │  │  corporelles│
     │  → bouche) │  │  → visage) │  │  (idle,     │
     └────────────┘  └────────────┘  │  respiration)│
                                     └─────────────┘

La phase 1 est la plus complexe techniquement, mais elle ne se fait qu'une seule fois. La phase 2 est légère et tourne parfaitement sur une tablette Android avec un Snapdragon 8 Elite. Unity gère le rendu 3D, les blendshapes, et les animations squelettiques de manière très efficace sur ARM — c'est son cœur de métier.

Pour le MVP, tu pourrais même commencer avec des avatars pré-fabriqués (un personnage cartoon pour les enfants, un semi-réaliste pour les ados, un réaliste pour les adultes — comme tu l'avais prévu) et n'ajouter le clonage de visage personnalisé que dans une phase ultérieure. Ça réduit significativement la complexité initiale.

Tu veux qu'on détaille le workflow de création d'avatar avec DECA/Avatar SDK, ou qu'on se concentre sur l'architecture complète du MVP tablette avec les avatars pré-fabriqués ?


You asked:

SI nous utilisons une tablette je me disais qu'un utilisateur pourrait activer sa session en utilisant le lecteur d'empreinte digitale, mais n'avons nous pas une limite du nombre d'empreinte qui peuvent être utilisé dans android ?

Mar 20


Claude Replied:

Réfléchissant aux limites du nombre d'empreintes digitales sur Android

Réfléchissant aux limites du nombre d'empreintes digitales sur Android

Très bonne réflexion pour l'ergonomie en milieu hospitalier. Laisse-moi vérifier les limites exactes.

Identifié limitation matérielle d'Android pour authentification biométrique multiple

Identifié limitation matérielle d'Android pour authentification biométrique multiple

Tu as mis le doigt (sans jeu de mots) sur un vrai problème. La limite est sévère et bloquante pour ton cas d'usage hospitalier.

La réalité des limites : les appareils Samsung sont limités à 3 empreintes Samsung Community. D'autres modèles vont jusqu'à 4 ou 5 empreintes maximum Android Central. C'est une limitation matérielle liée à l'espace de stockage sécurisé du chip Jetroid — les templates biométriques sont stockées dans le TEE (Trusted Execution Environment) qui a une capacité très limitée. Les données biométriques brutes et les templates ne sont jamais accessibles en dehors de l'environnement isolé sécurisé (TEE ou Secure Element), et doivent être chiffrées avec une clé spécifique au device Android Open Source Project.

En clair : 3 à 5 empreintes par tablette, point final. Tu ne peux pas contourner ça de manière propre. Il existe des astuces comme enregistrer deux doigts différents dans un seul slot d'empreinte en alternant les doigts pendant l'enregistrement Jetroid, mais ça dégrade la fiabilité de la reconnaissance et ce n'est absolument pas acceptable pour un dispositif médical.

Pourquoi c'est un problème pour Kazeia : dans un service hospitalier, tu pourrais avoir 10, 20, voire 50 patients qui utilisent la même tablette à tour de rôle. 3-5 empreintes, c'est ridicule. Et même si chaque patient avait sa propre tablette, tu voudrais quand même que le personnel soignant puisse accéder à la tablette pour la maintenance ou la configuration.

Les alternatives pour l'authentification patient :

La plus pragmatique serait un code PIN personnalisé par patient, couplé au lecteur d'empreinte pour le personnel soignant. Le médecin ou l'infirmier enregistre son empreinte (1-2 slots) pour un accès rapide à l'interface clinicien, et chaque patient reçoit un code PIN à 4-6 chiffres pour sa session. C'est simple, ça fonctionne pour un nombre illimité de patients, et le PIN peut être généré automatiquement à l'admission.

Une autre option intéressante serait un badge NFC ou QR code patient. Les tablettes Android supportent le NFC. Chaque patient reçoit un bracelet ou une carte NFC à l'admission (c'est déjà courant dans les hôpitaux). Il pose le bracelet sur la tablette, sa session se lance. C'est encore plus rapide qu'un PIN et ne nécessite aucune mémorisation — important pour des patients en détresse psychologique ou des enfants. Le OnePlus Pad 3 n'a malheureusement pas de NFC, mais le Samsung Galaxy Tab S11 Ultra en a.

Tu pourrais aussi envisager la reconnaissance faciale Android comme alternative biométrique. Android supporte la reconnaissance faciale nativement, et contrairement aux empreintes, tu pourrais implémenter ton propre système de reconnaissance faciale au niveau applicatif (pas via le TEE Android) avec une base de données de visages patients gérée par ton app. Ce n'est pas aussi sécurisé que le TEE hardware, mais dans un contexte hospitalier contrôlé où la tablette reste dans l'établissement, c'est un compromis acceptable. Et ça réutiliserait potentiellement des composants de ton pipeline avatar (détection de visage, embeddings faciaux).

Ma recommandation pour Kazeia : un système à deux niveaux. Les soignants s'authentifient par empreinte digitale (accès admin/clinicien, 2-3 empreintes enregistrées). Les patients s'authentifient par PIN ou badge NFC (accès session patient, nombre illimité). C'est à la fois sécurisé, scalable, et réaliste pour un déploiement hospitalier.

Tu veux qu'on intègre ce système d'authentification dans l'architecture globale de Kazeia sur tablette ?


You asked:

N'est il pas possible d'utiliser le lecteur pour comparer avec des empreintes qui seraient stockés dans l'application kazeia ?

Mar 20


Claude Replied:

Examined feasibility of bypassing system limits via direct sensor integration

Examined feasibility of bypassing system limits via direct sensor integration

Excellente idée — utiliser le capteur physique mais gérer la base d'empreintes côté application plutôt que via le TEE Android. Laisse-moi vérifier si c'est techniquement possible.

Examined feasibility of custom fingerprint matching within app

Examined feasibility of custom fingerprint matching within app

Malheureusement non, et c'est verrouillé par design. C'est une bonne idée, mais Android rend ça volontairement impossible.

Le problème fondamental : les données biométriques brutes et les templates d'empreintes ne doivent jamais être accessibles en dehors du pilote du capteur ou du TEE. Le canal SPI entre le capteur et le processeur sécurisé doit être accessible uniquement au TEE, protégé par une politique SELinux Android Open Source Project. En clair, le capteur d'empreinte intégré à la tablette ne communique qu'avec l'environnement sécurisé du processeur. Ton application Kazeia, qui tourne dans l'espace utilisateur Android (le "Rich Execution Environment"), n'a physiquement aucun accès aux images ou templates d'empreintes.

Les données d'empreinte doivent être sécurisées dans le hardware du capteur ou dans une mémoire de confiance, de sorte que les images d'empreintes ne soient pas accessibles. Seule la forme chiffrée peut être stockée sur le système de fichiers Google Support. Et l'extraction des caractéristiques et la comparaison des templates sont effectuées dans l'environnement sécurisé isolé, les données brutes ne quittent jamais le hardware sécurisé et ne sont pas accessibles au système Android ni aux applications Codez Up.

Autrement dit, l'API BiometricPrompt te donne uniquement un résultat binaire : "oui c'est une empreinte enregistrée" ou "non". Tu ne reçois jamais l'image, ni le template, ni aucune donnée exploitable pour faire ton propre matching.

Il existe cependant une solution : un lecteur d'empreinte USB externe.

Pour capturer des images d'empreinte brutes ou des templates spécifiques au-delà de l'authentification OS standard, il faut interagir directement avec le hardware du capteur via des APIs spécifiques ou des SDK fabricants. Des scanners externes dédiés et leurs applications sont souvent utilisés pour gérer et stocker de multiples enregistrements d'empreintes Sivo.

Des fabricants comme SecuGen, DigitalPersona, ou Futronic proposent des lecteurs d'empreinte USB compacts (format clé USB ou petit boîtier) avec des SDK Android qui te donnent accès aux images brutes et aux templates. Avec un tel capteur externe, tu pourrais implémenter exactement ce que tu décris : enregistrer les empreintes de chaque patient dans la base SQLite de Kazeia (sous forme de template chiffré), et faire le matching côté application avec un nombre illimité d'empreintes.

Le workflow serait : le patient pose son doigt sur le lecteur USB → le SDK du capteur te renvoie un template → tu le compares avec les templates stockés dans ta base de données chiffrée → tu identifies le patient et charges sa session. C'est exactement ce que font les systèmes de pointage ou d'identification dans les hôpitaux, les banques, etc.

Mais est-ce que ça vaut le coup pour Kazeia ? Honnêtement, je ne suis pas sûr. Ça ajoute un périphérique externe (un câble USB-C qui dépasse de la tablette, pas idéal pour un objet posé au chevet du patient), un SDK supplémentaire à maintenir, et un coût par tablette (~50-100€ pour un lecteur de qualité). Et surtout, ça complexifie le déploiement — chaque tablette doit être équipée d'un lecteur, il faut gérer la compatibilité USB-OTG, etc.

Pour le contexte hospitalier, les alternatives restent plus pragmatiques :

Un code PIN patient à 4 chiffres reste la solution la plus simple et la plus fiable. Le patient le reçoit à l'admission, l'infirmier peut le réinitialiser si besoin, et ça fonctionne sur n'importe quelle tablette sans matériel supplémentaire.

Un badge NFC (bracelet patient) serait encore plus élégant — le patient n'a rien à mémoriser, il pose son bracelet sur la tablette. Mais ça nécessite une tablette avec NFC (le Samsung Galaxy Tab S11 Ultra en a, pas le OnePlus Pad 3).

Et le lecteur d'empreinte intégré reste utilisable pour le personnel soignant (2-3 empreintes pour les médecins/infirmiers qui accèdent à l'interface clinicien fréquemment).

C'est un système à deux niveaux qui est le plus adapté au contexte réel d'un service hospitalier. Tu veux qu'on finalise l'architecture d'authentification complète ou qu'on passe à un autre aspect du projet ?


You asked:

Alors on oublie.... dommage ça aurait pu être intéressant

Mar 20


Claude Replied:

Acknowledged disappointment tout en explorant alternatives futures

Acknowledged disappointment tout en explorant alternatives futures

Oui c'est frustrant, l'idée était vraiment bonne — une identification biométrique instantanée par le patient, sans rien mémoriser, c'est exactement le type d'interaction fluide qu'on veut dans un contexte thérapeutique. Un patient en détresse ne devrait pas avoir à se rappeler d'un code.

Garde quand même ça dans un coin de ta roadmap. Le jour où Kazeia est validé cliniquement et que le déploiement s'industrialise, un petit lecteur USB-C SecuGen à 60€ par tablette pourrait redevenir pertinent — le surcoût serait négligeable à l'échelle d'un service hospitalier équipé. Et côté implémentation, les SDK de ces lecteurs (SecuGen, DigitalPersona) sont bien documentés avec des exemples Android/Java prêts à l'emploi. Ce serait quelques jours de travail, pas plus.

En attendant, le système PIN patient + empreinte soignant est solide et suffisant pour la phase de validation. Et si vous choisissez une tablette avec NFC (comme le Samsung Tab S11 Ultra), le bracelet NFC patient reste une option très élégante qui pourrait même plaire aux équipes médicales — c'est un geste naturel qu'ils connaissent déjà avec les bracelets d'identification hospitaliers.

Tu veux qu'on continue sur un autre aspect du projet, ou qu'on fasse un point récapitulatif de toutes les décisions architecturales qu'on a prises dans cette conversation ?


You asked:

comme nous avons une DB Chroma, pourrait on avoir ceci sous Android (ou un équivalent)

Mar 22


Claude Replied:

Évalué la compatibilité de ChromaDB avec Android et alternatives

Évalué la compatibilité de ChromaDB avec Android et alternatives

Bonne question — le RAG est un composant important de Kazeia. Laisse-moi vérifier si ChromaDB tourne sur Android et quelles sont les alternatives.

Évalué la compatibilité et exploré des alternatives mobiles viables

Évalué la compatibilité et exploré des alternatives mobiles viables

ChromaDB directement sur Android : non, c'est un service Python qui dépend de bibliothèques lourdes et n'est pas conçu pour le mobile. Mais il y a des alternatives excellentes et même mieux adaptées à ton cas.

La meilleure option pour Kazeia : sqlite-vec ou vectorlite, des extensions de recherche vectorielle pour SQLite.

C'est la solution la plus élégante parce que tu utilises déjà SQLite dans Kazeia pour tout le reste (utilisateurs, conversations, PHQ-9). sqlite-vec est une extension SQLite puissante qui permet de stocker, manipuler et interroger des données vectorielles directement dans SQLite, avec une recherche K-Nearest Neighbor, plusieurs métriques de distance, et des performances accélérées par SIMD, le tout dans un package portable sans dépendances Medium.

Concrètement, au lieu d'avoir deux bases de données séparées (SQLite pour les données relationnelles + ChromaDB pour les vecteurs), tu unifies tout dans une seule base SQLite. Tes documents de santé, tes fiches patients, et leurs embeddings vectoriels cohabitent dans le même fichier .db. C'est plus simple à déployer, à sauvegarder, et à synchroniser avec le serveur clinicien.

L'auteur de sqlite-vec le conçoit spécifiquement pour tourner sur tous les systèmes d'exploitation, en WASM, sur les devices mobiles, et même sur des Raspberry Pi Hacker News. C'est une bibliothèque C pure, compilable pour ARM64 (Android), sans dépendance externe.

L'alternative vectorlite est encore plus performante pour ton cas : vectorlite utilise hnswlib, qui est optimisé pour la construction incrémentale d'index et les requêtes single-vector, ce qui fonctionne bien avec l'API des tables virtuelles SQLite DEV Community. En benchmarks, vectorlite est 3x à 100x plus rapide que sqlite-vec pour les requêtes vectorielles, au prix d'un taux de rappel légèrement inférieur GitHub. Pour ton usage (quelques centaines de documents de santé, pas des millions), les deux sont largement suffisants.

Il existe aussi ObjectBox, une solution spécifiquement conçue pour le mobile : c'est une base de données vectorielle on-device pour mobile, IoT et systèmes embarqués, cloud-optional ObjectBox. ObjectBox a des SDK natifs Java/Kotlin pour Android et est optimisé pour les contraintes mobiles (faible consommation mémoire, performances sur ARM). C'est peut-être l'option la plus "clé en main" pour Android.

Pour le modèle d'embedding côté tablette, tu ne pourras pas utiliser sentence-camembert-base tel quel (c'est un modèle Python/PyTorch). Mais tu peux le convertir en ONNX et l'exécuter via ONNX Runtime Mobile sur Android. Le modèle fait environ 100-200 Mo, ce qui est gérable. Alternativement, tu peux pré-calculer tous les embeddings des documents de santé sur le serveur (une seule fois) et ne déployer que les vecteurs pré-calculés sur la tablette. Le seul embedding à calculer en temps réel sur la tablette serait celui de la requête du patient — un seul vecteur à chaque tour de conversation.

Le pipeline RAG sur tablette deviendrait :

Message patient
      │
      ▼
Embedding model (ONNX, ~150 Mo)
      │
      ▼
Vecteur requête (768 dimensions)
      │
      ▼
SQLite + sqlite-vec / vectorlite
(recherche KNN dans les documents pré-indexés)
      │
      ▼
Top-K documents pertinents
      │
      ▼
Injection dans le prompt LLM (llama.cpp)

Tu n'as même plus besoin du cross-encoder de re-ranking (crossencoder-camembert-base) pour le MVP — avec un petit corpus de documents de santé (ta base ChromaDB actuelle fait ~35 Mo), une recherche KNN directe avec un bon modèle d'embedding sera suffisante. Le cross-encoder, tu pourrais le réintroduire plus tard si tu augmentes significativement le corpus.

Le budget mémoire mis à jour avec cette approche :

Composant                          Mémoire
─────────────────────────────────────────────
Qwen LLM (INT4)                   ~1.0 Go
Whisper small (STT)                ~0.5 Go
Embedding model (ONNX)             ~0.2 Go
Classifieur émotion                ~0.2 Go
Piper TTS                          ~0.1 Go
SQLite + vecteurs                  ~0.05 Go
Avatar Unity                       ~0.8 Go
OS Android                         ~3.0 Go
─────────────────────────────────────────────
TOTAL                              ~5.85 Go
Marge sur 16 Go                    ~10.15 Go

Plus de 10 Go de marge — tu as largement la place. Et c'est une architecture beaucoup plus propre que l'originale : une seule base de données SQLite pour tout, pas de ChromaDB séparé à maintenir.

Tu veux qu'on fasse un récapitulatif complet de l'architecture tablette avec toutes les décisions qu'on a prises dans cette conversation ?


You asked:

Maintenant nous allons partir sur un developpement purement android, comment dois je faire ? nous allons avoir une tablette, mais faut il un environnement android (virtuel)

Mar 22


Claude Replied:

Évalué les options de développement Android et environnement virtuel

Évalué les options de développement Android et environnement virtuel

Excellente question pratique — c'est le moment de passer au concret. Laisse-moi vérifier les meilleures pratiques actuelles pour le développement Android natif avec Unity et les composants natifs.

Rassemblé les informations pour clarifier le développement Android

Rassemblé les informations pour clarifier le développement Android

Il y a un point crucial à savoir d'entrée : Unity ne supporte pas les émulateurs Android. Pour tester une application Unity, il Unity faut soit tester sur un appareil Android physique, soit utiliser Unity Remote (qui streame l'affichage depuis l'éditeur vers le téléphone), soit utiliser le Device Simulator intégré à Unity pour vérifier l'apparence.

C'est un point décisif pour Kazeia. Puisque ton application est centrée sur Unity (avatar 3D, lip-sync, rendu temps réel) + des composants natifs C/C++ (llama.cpp, sherpa-onnx), tu auras besoin d'une tablette physique assez tôt dans le développement. Pas moyen d'y couper.

Concrètement, voici l'environnement de développement dont vous avez besoin :

Sur ton PC de développement, tu as besoin de deux outils principaux. D'abord Unity Editor (Unity 6 ou supérieur) avec le module Android Build Support installé. C'est là que tu développes l'avatar, l'interface patient, le lip-sync, et que tu intègres les plugins natifs (sherpa-onnx, llama.cpp). Unity gère la compilation vers Android (APK/AAB) en interne — il embarque le SDK Android, le NDK et Gradle automatiquement. Ensuite Android Studio, pas forcément pour coder dedans, mais parce qu'il fournit l'Android SDK, les outils ADB (Android Debug Bridge) pour communiquer avec la tablette, et le Logcat pour lire les logs en temps réel. Tu en auras aussi besoin si tu développes des composants natifs Java/Kotlin en dehors de Unity (par exemple le service d'authentification, la synchronisation avec le serveur clinicien, ou l'intégration du SpeechRecognizer natif).

Pour la tablette physique, le workflow quotidien serait le suivant. Tu branches la tablette en USB-C sur ton PC (ou en Wi-Fi via ADB wireless depuis Android 11). Tu actives le mode développeur sur la tablette (en tapant 7 fois sur le numéro de build dans les paramètres) puis tu actives le débogage USB. Tu peux ensuite déployer et déboguer ton application directement depuis Android Studio ou Unity, par câble USB ou par Wi-Fi Android Developers. Depuis Unity, tu cliques sur "Build and Run" et l'APK s'installe et se lance directement sur la tablette connectée. C'est un cycle de quelques minutes entre une modification et le test.

Faut-il acheter la tablette cible (OnePlus Pad 3 ou Samsung Tab S11 Ultra) dès le début ? Pas nécessairement. Pour les premières semaines de développement (interface, logique conversationnelle, intégration des composants), n'importe quelle tablette ou téléphone Android récent suffit. Le LLM via llama.cpp, le STT natif, le TTS Piper — tout ça fonctionnera sur un appareil Android standard pour le développement. Par contre, dès que tu voudras valider les performances réelles (latence du LLM, fluidité de l'avatar, budget mémoire complet), il te faudra la tablette cible. Mon conseil : commande-la dans les 2-3 premières semaines pour l'avoir sous la main quand les premiers composants seront prêts à être benchmarkés.

L'émulateur Android Studio reste utile pour certaines choses, même si Unity ne le supporte pas directement. Si tu développes des composants purement Java/Kotlin en dehors de Unity (un service Android, une activité de configuration, le module d'authentification), tu peux les prototyper et tester dans l'émulateur. L'émulateur offre flexibilité, haute fidélité et rapidité — il peut simuler différents appareils et niveaux d'API Android sans avoir besoin de chaque appareil physique Android Developers. Mais dès que tu intègres ces composants dans l'app Unity, retour obligatoire au device physique.

Le setup recommandé pour toi et Damien :

PC Développeur 1 (ex: Richard)
├── Unity Editor 6.x + Android Build Support
├── Android Studio (SDK, ADB, Logcat)
├── IDE C# (Rider ou VS Code avec extensions Unity)
├── Tablette Android (dev/test) branchée en USB-C
└── Serveur local pour :
    ├── Entraînement des modèles (Qwen, émotion, Piper)
    ├── Conversion GGUF / ONNX
    └── Clonage vocal Qwen3-TTS

PC Développeur 2 (ex: Damien)
├── Unity Editor 6.x + Android Build Support
├── Android Studio
├── Même tablette ou une seconde pour tests parallèles
└── Git (repo partagé)

Pour le versioning, utilisez Git avec Git LFS pour les fichiers lourds (modèles ONNX, GGUF, assets Unity). Les modèles eux-mêmes ne devraient pas être dans le repo principal — stockez-les séparément (un NAS, un drive partagé) et référencez-les dans le build.

Un dernier point pratique : le cycle de build Unity vers Android est plus lent que le développement natif pur (comptez 1-3 minutes par build selon la taille du projet). Pour accélérer l'itération, développe et teste un maximum dans l'éditeur Unity sur PC (la logique, l'UI, les animations), et ne déploie sur la tablette que quand tu as besoin de valider quelque chose de spécifique au mobile (performances, capteurs, audio réel).

Tu veux qu'on détaille le setup technique d'un des composants spécifiques (par exemple comment intégrer llama.cpp dans un projet Unity Android), ou qu'on fasse le récapitulatif global de toute l'architecture qu'on a définie ?


You asked:

avant de passer sur la partie unity, je veux déjà pouvoir valider le fonctionnement du coeur de Kazeia, quel language utilisé pour optimiser les ressources ?

Mar 22


Claude Replied:

Évalué les langages pour optimiser ressources Android avant intégration Unity

Évalué les langages pour optimiser ressources Android avant intégration Unity

Très bonne approche — valider le cœur avant de s'occuper du visuel. Pour un pipeline d'inférence offline sur tablette Android, la réponse est assez claire.

Le cœur de Kazeia est du C/C++ encapsulé, piloté par du Kotlin.

Voici pourquoi : regarde les composants que tu dois faire tourner. llama.cpp pour le LLM — c'est du C/C++. sherpa-onnx pour le STT et le TTS — c'est du C/C++. ONNX Runtime pour le classifieur d'émotion et le modèle d'embedding — c'est du C/C++. sqlite-vec pour la recherche vectorielle — c'est du C. Tous tes composants d'inférence critiques sont déjà écrits en C/C++ et optimisés pour ARM. Tu ne vas pas les réécrire, tu vas les appeler.

La question devient donc : quel langage pour la couche applicative qui orchestre tout ça ?

Kotlin est le bon choix pour cette couche. C'est le langage officiel et recommandé par Google pour Android, il a un accès direct au NDK (Native Development Kit) via JNI pour appeler du code C/C++, et il te permet de gérer tout ce qui est spécifique à Android : le cycle de vie de l'application, le SpeechRecognizer natif, SQLite, l'interface utilisateur, les permissions, la gestion mémoire. Kotlin est aussi bien plus concis et sûr que Java, ce qui compte quand vous n'êtes que deux développeurs.

L'architecture en couches ressemblerait à ça :

┌─────────────────────────────────────────┐
│         Couche Application (Kotlin)      │
│                                          │
│  • Machine à états conversationnelle     │
│  • Gestion PHQ-9                         │
│  • Gestion des sessions/profils          │
│  • Orchestration du pipeline             │
│  • Interface de test (texte simple)      │
│  • Synchronisation serveur clinicien     │
├─────────────────────────────────────────┤
│         Couche Binding (JNI / Java API)  │
│                                          │
│  • LlamaAndroid (wrapper llama.cpp)      │
│  • SherpaOnnx (wrapper Java fourni)      │
│  • ONNX Runtime Android (SDK officiel)   │
│  • SQLite + sqlite-vec                   │
├─────────────────────────────────────────┤
│         Couche Inférence (C/C++ natif)   │
│                                          │
│  • llama.cpp    → LLM Qwen (GGUF)       │
│  • sherpa-onnx  → STT Whisper + TTS     │
│  • onnxruntime  → Émotion + Embeddings  │
│  • sqlite-vec   → Recherche vectorielle  │
└─────────────────────────────────────────┘

Le point important c'est que tu n'écris presque pas de C/C++ toi-même. Ces bibliothèques fournissent déjà des bindings utilisables depuis Android. llama.cpp a un exemple Android officiel avec un wrapper Java/Kotlin. sherpa-onnx fournit des APIs Java et Kotlin prêtes à l'emploi, c'est même un de leurs points forts. ONNX Runtime a un SDK Android officiel (com.microsoft.onnxruntime:onnxruntime-android). Tu intègres ces dépendances dans ton projet, et tu les appelles depuis Kotlin comme des bibliothèques normales.

Pour la phase de validation du cœur, concrètement, voici ce que je te recommande :

Crée un projet Android Studio simple en Kotlin, sans Unity, avec une interface minimaliste : un champ texte pour taper un message, un bouton pour envoyer, et une zone d'affichage pour la réponse. Pas de graphismes, pas d'avatar, juste du texte. Ça te permet de valider que chaque composant fonctionne sur la tablette, de mesurer les latences réelles de chaque étape, et d'identifier les problèmes de mémoire ou de performance avant d'ajouter la complexité Unity.

Le plan de validation serait séquentiel. D'abord intègre llama.cpp seul et vérifie que ton Qwen en GGUF génère des réponses correctes avec une latence acceptable. Ensuite ajoute le SpeechRecognizer natif Android pour le STT — c'est rapide à intégrer pour le MVP. Puis ajoute Piper TTS via sherpa-onnx pour entendre les réponses. Ensuite branche SQLite + sqlite-vec pour le RAG. Et enfin ajoute le classifieur d'émotion via ONNX Runtime. À chaque étape, tu mesures la mémoire utilisée et la latence. Si un composant pose problème, tu le sais immédiatement et tu peux ajuster avant que tout soit imbriqué.

Pourquoi pas Python sur Android ? On pourrait être tenté d'utiliser Chaquopy ou d'autres solutions pour faire tourner du Python sur Android et réutiliser une partie du code existant de Kazeia. C'est une mauvaise idée pour plusieurs raisons : l'overhead mémoire du runtime Python est énorme sur mobile (200-400 Mo juste pour l'interpréteur + les dépendances), les performances sont nettement inférieures à du natif, et surtout les bibliothèques critiques (PyTorch, transformers, vLLM) ne tournent pas sur Android. Le code Python existant de Kazeia (conversation_manager.py, phq9_manager.py, etc.) devra être porté en Kotlin, mais ce sont des fichiers relativement courts — la logique métier, pas de l'inférence.

Pourquoi pas du C/C++ pur pour tout ? Tu pourrais maximiser les performances en écrivant toute l'application en C++ avec le NDK. Mais tu perdrais l'accès facile aux APIs Android (SpeechRecognizer, gestion des permissions, SQLite natif, interface utilisateur), et le développement serait beaucoup plus lent. Le C/C++ doit rester cantonné à ce pour quoi il excelle : l'inférence de modèles. Le reste, Kotlin le fait mieux et plus vite.

Tu veux qu'on commence par le setup du projet Android Studio avec l'intégration de llama.cpp, ou qu'on prépare d'abord la conversion de ton modèle Qwen en GGUF pour avoir quelque chose à tester ?


You asked:

Je veux que tu fasses une recherche pour valider quel est le meilleur soft pour faire tourner des LLM sur android (cela adaptera nos choix par la suite), dans le cas présent nous utiliserons un processeur snapdragon 8 Elite

Mar 22


Claude Replied:

Parfait, c'est exactement le type de recherche qu'il faut faire avant de figer l'architecture. Juste quelques précisions pour que la recherche soit la plus ciblée possible :

  1. Taille des modèles ciblés : on se concentre uniquement sur des modèles ~1.7B (comme ton Qwen) ou tu veux aussi évaluer la faisabilité de modèles plus gros (3B, 7B) pour anticiper une évolution future ?
  2. Priorité principale : tu privilégies la vitesse de génération (tokens/seconde), l'efficacité mémoire, ou le support du NPU Hexagon du Snapdragon 8 Elite spécifiquement ?
  3. Format de modèle : tu veux comparer uniquement les runtimes pour des modèles GGUF, ou aussi d'autres formats comme ONNX et les modèles optimisés QNN (Qualcomm) ?

Thought for 1s

Thought for 1s

Je lance la recherche dès que tu as choisi — ça me permettra de cibler exactement ce qui est pertinent pour votre cas.


You asked:

Q: Taille des modèles à évaluer ? A: 1.7B à 7B (vision large)

Q: Priorité d'évaluation ? A: Support NPU Hexagon

Q: Formats de modèle à comparer ? A: Tous les formats disponibles

Mar 22


Claude Replied:

Parfait, les critères sont bien définis — modèles de 1.7B à 7B, priorité au NPU Hexagon, tous formats confondus. C'est une recherche pointue, je lance ça immédiatement.

@keyframes slideUpFadeOut { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-100%); opacity: 0; } } @keyframes slideUpFadeIn { 0% { transform: translateY(100%); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }

LLM inference on Snapdragon Elite

Research complete•

@keyframes slideUpFadeOut { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-100%); opacity: 0; } } @keyframes slideUpFadeIn { 0% { transform: translateY(100%); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }

446 sources

7m 57s

Your report on running LLMs on the Snapdragon 8 Elite's Hexagon NPU is ready. It covers all eight candidate frameworks, with ExecuTorch (QNN delegate) and Qualcomm's Genie SDK emerging as the top production paths for deploying a fine-tuned Qwen 1.7B. The report includes verified decode benchmarks, NPU quantization constraints and mitigations, memory/power profiling for 1.7B through 7B model sizes, Android integration details, and a look at the SM8850 and LiteRT-LM developments coming through 2026.

# Install AI Hub Models package
pip install "qai-hub-models[qwen2-7b-instruct]"

# Export for Snapdragon 8 Elite — compiles to QNN context binaries
python -m qai_hub_models.models.qwen2_7b_instruct.export \
  --chipset qualcomm-snapdragon-8-elite \
  --skip-profiling --output-dir genie_bundle

Running LLMs on Snapdragon 8 Elite Hexagon NPU: Framework Analysis and Deployment Guide for Qwen Models

Document 

# Running LLMs on Snapdragon 8 Elite's Hexagon NPU **ExecuTorch with the QNN delegate and Qualcomm's own Genie SDK are the two strongest production paths for deploying a fine-tuned Qwen 1.7B on Android with Snapdragon 8 Elite's Hexagon NPU.** ExecuT


You asked:

Donc quel est le runtime à prendre ?

Mar 22


Claude Replied:

Synthesized research findings to recommend optimal runtime

Synthesized research findings to recommend optimal runtime

Basé sur toute la recherche, la réponse est claire :

ExecuTorch avec le delegate QNN est le runtime à prendre pour Kazeia. Et voici pourquoi c'est le choix évident pour ton cas spécifique.

Ton modèle Qwen est fine-tuné en PyTorch — ExecuTorch est littéralement le runtime edge de PyTorch. Tu exportes ton modèle directement sans passer par une conversion intermédiaire complexe. Le pipeline est : PyTorch → export .pte avec delegate QNN → déploiement sur tablette. ExecuTorch v1.0+ supporte explicitement Qwen parmi ses architectures de modèles, donc tu n'es pas en train de forcer un format non supporté.

Pour le NPU Hexagon, c'est le meilleur compromis entre performance et flexibilité. Le delegate QNN partitionne automatiquement ton modèle : les opérations compatibles tournent sur le NPU, le reste retombe sur le CPU. Tu n'as pas à gérer ça manuellement. Et les gains sont réels — sur le Snapdragon 8 Elite, le NPU maintient des performances stables pendant plus de 10 minutes à 36-38°C là où le CPU throttle après 3 minutes à 42°C. Pour un chatbot thérapeutique où les sessions durent potentiellement 20-30 minutes, cette stabilité thermique est critique.

Pour ton Qwen 1.7B en INT4, tu peux t'attendre à environ 30-50 tokens/seconde sur le NPU, avec seulement ~1.3 Go de RAM consommée. C'est largement suffisant pour une conversation fluide et ça laisse énormément de marge sur 16 Go pour le reste du pipeline (STT, TTS, avatar). Et si tu veux évoluer vers un modèle 3B ou 7B plus tard, ExecuTorch supporte le sharding de modèle — ton architecture ne change pas.

Côté intégration Android, ExecuTorch fournit des packages Maven et une bibliothèque AAR avec une API Java, ce qui s'intègre proprement dans une app Kotlin. L'overhead sur la taille de l'APK est de seulement 5-15 Mo.

Garde llama.cpp en backup pour deux raisons : le prototypage rapide (charger un GGUF et tester en 5 minutes, sans pipeline de conversion) et comme fallback CPU/GPU si tu rencontres des problèmes avec la quantization QNN sur ton modèle fine-tuné spécifique. La quantization pour le NPU peut parfois dégrader la qualité des réponses — c'est le risque principal mentionné dans le rapport. Si ça arrive, llama.cpp avec Vulkan (GPU Adreno 830) te donne un plan B solide pendant que tu ajustes la quantization.

En résumé, la stack d'inférence LLM pour Kazeia :

Production :  ExecuTorch + QNN delegate (NPU Hexagon)
Fallback :    llama.cpp + Vulkan (GPU Adreno 830)
Prototypage : llama.cpp + CPU (GGUF, zéro setup)

Tu veux qu'on détaille le pipeline de conversion de ton Qwen fine-tuné vers le format ExecuTorch .pte, ou qu'on fasse d'abord le récapitulatif global de toute l'architecture Kazeia tablette ?


You asked:

Au besoin nous pourrions revoir le fonctionnement de notre modele qwen3, le but est vraiment d'optimiser le fonctionnement

Mar 22


Claude Replied:

Orchestrated model optimization strategies for tablet deployment

Orchestrated model optimization strategies for tablet deployment

Exactement la bonne approche — optimiser le modèle avant le déploiement, pas après. Si vous êtes prêts à revoir le fonctionnement du Qwen3, il y a plusieurs leviers majeurs à actionner.

Premier levier : le choix de la taille du modèle.

Le rapport mentionne qu'Alibaba a sorti en mars 2026 la série Qwen 3.5 Small, spécifiquement conçue pour le déploiement edge, avec des tailles de 0.8B, 2B, 4B et 9B. Ces modèles sont optimisés pour l'inférence sur device dès leur conception, contrairement aux gros modèles qu'on compresse après coup. Pour Kazeia, le sweet spot serait probablement le Qwen 3.5 4B : assez grand pour des réponses empathiques nuancées en français, assez petit pour tourner confortablement sur le NPU (~2.5 Go en INT4). Le 2B serait le choix conservateur si tu veux maximiser la marge mémoire pour les autres composants, et le 0.8B si tu veux un temps de réponse quasi instantané mais au prix de réponses moins riches.

La question clé c'est : est-ce que tu as besoin de repartir de ton Qwen actuel (~1.7B) déjà fine-tuné, ou est-ce qu'un re-fine-tuning sur une base plus récente (Qwen 3.5) serait envisageable ? Parce que les modèles Qwen 3.5 Small intègrent nativement des améliorations qui te bénéficieraient directement — meilleur multilingue (dont le français), meilleure instruction-following, et architecture déjà pensée pour la quantization edge.

Deuxième levier : le mode thinking de Qwen3.

Qwen3 a un mode "thinking" où le modèle génère un bloc <think>...</think> de raisonnement interne avant de formuler sa réponse. C'est puissant pour la qualité des réponses empathiques — le modèle "réfléchit" à l'émotion détectée et à la stratégie thérapeutique avant de parler. Mais ça double ou triple le nombre de tokens générés (le raisonnement + la réponse), ce qui impacte directement la latence.

Pour Kazeia, la stratégie optimale serait d'utiliser le thinking pendant le fine-tuning (pour que le modèle apprenne le raisonnement empathique en chain-of-thought) mais de le désactiver à l'inférence sur tablette avec /no_think. Le modèle aura intériorisé les patterns de raisonnement pendant l'entraînement sans avoir besoin de les expliciter à chaque réponse. C'est exactement la technique de distillation du raisonnement — tu entraînes "lentement" et tu déploies "vite".

Troisième levier : le fine-tuning quantization-aware.

C'est probablement le point le plus impactant pour ton cas. Le rapport souligne que la quantization pour le NPU (notamment per-channel INT4) peut dégrader significativement la qualité des réponses. La solution est d'intégrer la quantization pendant le fine-tuning, pas après. Concrètement, tu entraînes ton modèle en simulant les contraintes de quantization INT4 à chaque forward pass. Le modèle apprend à produire des poids qui survivent bien à la quantization. Avec QLoRA + quantization-aware training (QAT), tu peux fine-tuner efficacement sur un GPU de développement tout en garantissant que le modèle déployé en INT4 sur le NPU ne perd pas en qualité empathique.

ExecuTorch propose SpinQuant — des matrices de rotation appliquées avant la quantization pour distribuer les poids outliers plus uniformément. C'est compatible avec le pipeline de conversion et ça améliore nettement la qualité post-quantization sans coût à l'inférence.

Quatrième levier : l'architecture du prompt et la longueur de contexte.

Sur le NPU, les prompts à longueur variable nécessitent des rebuilds de graphe (10+ secondes). Le workaround est d'utiliser un chunking fixe de 128 tokens. Ça a une implication directe sur comment tu structures ton prompt système. Actuellement dans Kazeia, ton prompt système est assez verbeux (rôle d'écoute empathique, validation des émotions, redirection vers le 3114...). Il faudrait le compresser au maximum pour laisser plus de place au contexte conversationnel dans la fenêtre de tokens limitée.

De même, la configuration actuelle de Kazeia garde 5 tours de conversation en contexte (CONVERSATION_CONTEXT_TURNS = 5) avec 700 tokens max pour l'historique. Sur tablette avec le NPU, tu pourrais adapter ça dynamiquement : garder plus de contexte quand la conversation est émotionnellement chargée (le classifieur d'émotion détecte de la détresse) et moins quand c'est une conversation légère.

Cinquième levier : la distillation de connaissances ciblée.

Plutôt que de fine-tuner directement sur des datasets, utilise un modèle puissant (Claude, GPT-4, ou Qwen 72B) pour générer un corpus synthétique de haute qualité de conversations thérapeutiques en français. Le modèle maître génère les réponses empathiques idéales pour des milliers de scénarios, et tu fine-tunes ton petit Qwen 3.5 2B/4B sur ce corpus. C'est la méthode la plus efficace pour transférer des capacités empathiques complexes vers un petit modèle.

Le corpus devrait être structuré avec les métadonnées qu'on a discutées : émotion du patient, stratégie thérapeutique choisie, niveau de sévérité. Ça permet un curriculum learning — d'abord les situations simples, puis progressivement les cas complexes (idéation suicidaire, crises de panique, etc.).

Voici ce que je recommande comme plan d'optimisation :

Phase 1 — Choix et préparation (1-2 semaines)
├── Évaluer Qwen 3.5 2B vs 4B en français
│   (benchmark qualité empathique sur un jeu de test)
├── Générer le corpus synthétique via modèle maître
└── Structurer les données avec annotations émotionnelles

Phase 2 — Fine-tuning optimisé (2-3 semaines)
├── Fine-tuning QLoRA + QAT sur Qwen 3.5
├── Entraîner avec thinking activé
├── Curriculum learning (simple → complexe)
└── DPO sur paires empathique/non-empathique

Phase 3 — Conversion et validation (1-2 semaines)
├── Export ExecuTorch .pte avec SpinQuant
├── Compilation QNN pour SM8750 (Snapdragon 8 Elite)
├── Benchmark tokens/s + mémoire sur tablette cible
└── Validation qualité : comparer réponses INT4 vs FP16

Le point de départ immédiat serait d'évaluer si les Qwen 3.5 Small en version de base (avant fine-tuning) produisent déjà des réponses correctes en français sur des scénarios thérapeutiques simples. Si le 4B est déjà bon en zero-shot, le fine-tuning ne fera que l'améliorer. Si même le 4B est insuffisant, il faudra peut-être viser le 9B — ce qui reste faisable sur 16 Go mais avec moins de marge.

Tu veux qu'on commence par benchmarker les modèles Qwen 3.5 de base sur des scénarios Kazeia, ou qu'on prépare d'abord le pipeline de génération du corpus synthétique ?


You asked:

mais après en terme d'optimisation "software" tu proposes quoi pour android

Mar 22


Claude Replied:

Inventoried software optimization strategies for Android deployment

Inventoried software optimization strategies for Android deployment

Bonne question — au-delà du modèle lui-même, il y a beaucoup d'optimisations côté software Android qui peuvent réduire la latence perçue et améliorer l'expérience patient. C'est souvent là que se fait la différence entre un prototype qui "fonctionne" et un produit qui paraît fluide.

L'optimisation la plus impactante : le pipeline en streaming et le parallélisme.

Aujourd'hui dans le prototype Kazeia, le flux est séquentiel : le patient parle → STT transcrit tout → le texte part au LLM → le LLM génère toute la réponse → le TTS synthétise tout l'audio → l'audio est joué. Chaque étape attend que la précédente soit terminée. Avec ton estimation de ~1665ms pour le LLM seul, plus le STT et le TTS, le patient attend facilement 3-4 secondes avant d'entendre quoi que ce soit. En contexte thérapeutique, ce silence est pesant.

Sur Android, tu peux pipeliner et streamer chaque étape. Le LLM génère token par token — dès que tu as la première phrase complète (détectée par un point ou un retour à la ligne), tu l'envoies au TTS pendant que le LLM continue de générer la suite. Le TTS synthétise la première phrase et commence la lecture audio pendant que la deuxième phrase arrive. Le patient entend la réponse commencer en 500-800ms au lieu de 3-4 secondes. La latence réelle n'a pas changé, mais la latence perçue chute drastiquement.

En Kotlin, ça s'implémente naturellement avec les coroutines et les Flows :

kotlin

// Le LLM émet un Flow de tokens
val tokenFlow: Flow<String> = execuTorchModel.generateStream(prompt)

// On accumule jusqu'à une phrase complète
val sentenceFlow = tokenFlow.chunkedBySentence()

// Chaque phrase part au TTS en parallèle
sentenceFlow.collect { sentence ->
    launch { ttsEngine.synthesizeAndPlay(sentence) }
    launch { updateAvatarLipSync(sentence) }
}

Ce pattern de streaming est crucial aussi pour l'avatar : au lieu d'attendre toute la réponse pour animer la bouche, l'avatar commence à parler dès la première phrase. L'effet est bien plus naturel.

Deuxième optimisation : le pré-chargement et la gestion mémoire.

Sur Android, le chargement d'un modèle LLM peut prendre plusieurs secondes. Tu ne veux pas que le patient attende à chaque ouverture de session. La solution est de charger les modèles dans un Service Android foreground qui persiste en arrière-plan. Quand le patient lance Kazeia, les modèles sont déjà chauds en mémoire. Le Service foreground empêche aussi Android de tuer ton processus via le Low Memory Killer.

Pour le chargement lui-même, utilise le memory-mapping (mmap). Tant ExecuTorch que llama.cpp supportent le mmap — au lieu de copier le modèle entier en RAM, le système mappe le fichier directement depuis le stockage. Le premier accès est quasi instantané, et seules les pages réellement utilisées sont chargées en mémoire physique. Pour un modèle de 1.7B en INT4 (~850 Mo sur disque), ça signifie que le chargement initial passe de plusieurs secondes à quelques centaines de millisecondes.

Configure aussi android:largeHeap="true" dans le manifeste et gère la mémoire native (C/C++) séparément du heap Java pour éviter les OOM. Les modèles tournent dans l'espace natif via JNI, pas dans le heap Dalvik/ART.

Troisième optimisation : l'ordonnancement intelligent des processeurs.

Le Snapdragon 8 Elite a une architecture hétérogène : 2 cœurs performance Oryon à 4.32 GHz + 6 cœurs efficacité à 3.53 GHz + GPU Adreno 830 + NPU Hexagon. Tu ne veux pas que tout tourne sur les mêmes cœurs. L'idée est d'affecter les tâches en fonction de leur nature :

Le LLM tourne sur le NPU via ExecuTorch/QNN — c'est la tâche la plus lourde et la plus sensible à la latence. Le STT (SpeechRecognizer natif ou Whisper) tourne sur les cœurs performance CPU pendant que le patient parle — c'est du temps réel, ça ne peut pas attendre. Le TTS (Piper via sherpa-onnx) tourne sur les cœurs efficacité CPU — la synthèse vocale est moins urgente et peut tourner en background. Le classifieur d'émotion et l'embedding RAG tournent via ONNX Runtime sur le NPU ou GPU en parallèle du LLM si le NPU supporte le multi-session, sinon sur le GPU Adreno.

En Kotlin, tu contrôles l'affinité des threads avec android.os.Process.setThreadPriority() et tu peux utiliser des dispatchers de coroutines dédiés :

kotlin

val llmDispatcher = newSingleThreadContext("LLM-NPU")
val sttDispatcher = Dispatchers.Default  // cœurs performance
val ttsDispatcher = newFixedThreadPoolContext(2, "TTS")
val emotionDispatcher = newSingleThreadContext("Emotion")

Quatrième optimisation : le cache intelligent.

Plusieurs niveaux de cache peuvent accélérer considérablement les interactions répétitives. Le KV cache du LLM — ExecuTorch supporte la compression du KV cache en INT8, ce qui réduit la mémoire mais aussi accélère le prefill pour les tours de conversation suivants. Le prompt système et le contexte patient (profil, historique PHQ-9) ne changent pas d'un tour à l'autre — tu peux pré-calculer leur KV cache une seule fois au début de la session et le réutiliser à chaque interaction.

Pour le RAG, cache les embeddings des requêtes fréquentes et les résultats de recherche vectorielle. Si le patient parle de tristesse à plusieurs reprises, les mêmes documents de santé seront pertinents — inutile de refaire la recherche vectorielle à chaque fois.

Pour le TTS, cache les segments audio des phrases standard du chatbot (salutations, transitions PHQ-9, phrases de redirection vers le 3114). Ces phrases reviennent souvent et ne changent jamais — les pré-synthétiser au déploiement économise du temps à l'exécution.

Cinquième optimisation : la gestion de l'énergie et du thermique.

Pour une tablette en milieu hospitalier qui tourne potentiellement des heures, la gestion thermique est critique. Utilise l'API PowerManager d'Android pour monitorer la température et adapter dynamiquement le comportement. Si la tablette chauffe (>40°C), réduis la longueur maximale des réponses du LLM, augmente l'intervalle entre les inférences du classifieur d'émotion (toutes les 3 phrases au lieu de chaque phrase), et basculer temporairement sur des réponses plus courtes.

Le NPU est naturellement 3.7× plus efficace que le CPU en tokens par watt — c'est déjà un énorme gain. Mais tu peux aller plus loin en implémentant un mode "veille active" : quand le patient ne parle pas depuis 30 secondes, l'application réduit la fréquence de polling du micro et met les modèles en standby, tout en gardant le Service foreground actif pour un redémarrage instantané.

Sixième optimisation : la réduction de la latence perçue via l'UX.

C'est subtil mais très efficace. Pendant que le LLM génère sa réponse, l'avatar ne doit pas rester figé — il devrait montrer des signes d'écoute active (hochement de tête, léger sourire, contact visuel). C'est exactement ce que fait un thérapeute humain : il ne reste pas immobile pendant qu'il réfléchit. Ces micro-animations masquent la latence de génération et renforcent l'alliance thérapeutique.

De même, le TTS peut commencer par un filler empathique ("Je comprends...", "Hmm...") généré quasi instantanément (depuis le cache) pendant que le LLM génère la réponse substantielle. Le patient perçoit une réponse immédiate même si le contenu réel arrive 500ms plus tard.

En résumé, les optimisations par ordre d'impact :

Impact majeur (à implémenter en premier)
├── Streaming LLM → TTS (latence perçue ÷3)
├── Pré-chargement modèles via Service foreground
├── Memory-mapping des modèles (mmap)
└── KV cache persistant pour le prompt système

Impact moyen (phase 2)
├── Ordonnancement NPU/CPU/GPU par tâche
├── Cache TTS pour phrases standard
├── Cache embeddings RAG
└── Compression KV cache INT8

Impact UX (phase 3)
├── Animations d'écoute active pendant la génération
├── Fillers empathiques immédiats
├── Adaptation dynamique à la température
└── Mode veille active intelligent

Tu veux qu'on détaille l'implémentation concrète d'un de ces points, ou qu'on fasse le récapitulatif global de toute l'architecture Kazeia sur tablette avec toutes les décisions prises ?


You asked:

avant meme de parler de la partie Unity ou de la synthèse vocal, je veux juste optimiser au maximum les questions/réponses qui seront executé par qwen3

Mar 22


Claude Replied:

Inventorié dix optimisations critiques pour le pipeline conversationnel Qwen3

Inventorié dix optimisations critiques pour le pipeline conversationnel Qwen3

Parfait — c'est la bonne approche. On se concentre uniquement sur le cœur : texte entrant → Qwen3 → texte sortant, le plus rapide et le plus pertinent possible.

Premier axe : le prompt système.

C'est là que tu as le plus de gain immédiat. Chaque token dans le prompt système est retraité à chaque tour de conversation lors du prefill. Sur le NPU, le prefill est rapide (~50 tokens/ms) mais il reste proportionnel à la taille du contexte. Un prompt système de 500 tokens qui pourrait en faire 200, c'est du temps et de l'énergie gaspillés à chaque interaction.

Actuellement dans Kazeia, le prompt système décrit le rôle d'écoute empathique, la validation des émotions, les limites (pas de diagnostic médical), la redirection vers le 3114, la longueur de réponse souhaitée... Tout ça est important mais verbeux. L'idée est de compresser au maximum sans perdre le comportement. Par exemple, au lieu de décrire en phrases longues, utilise des instructions concises et structurées :

Tu es Kazeia, compagnon d'écoute émotionnelle en français.

RÈGLES:
- Valide toujours l'émotion avant de répondre
- 2-3 phrases max par réponse
- Jamais de diagnostic médical
- Risque suicidaire → "Appelle le 3114"
- Pose UNE question ouverte par tour

CONTEXTE PATIENT:
Prénom: {prenom} | Âge: {age} | Dernier PHQ-9: {score} ({date})
Émotion détectée: {emotion} (confiance: {confidence})

HISTORIQUE:
{derniers_tours}

Ce format structuré fait typiquement 150-200 tokens au lieu de 400-500, et le modèle le suit mieux parce que les instructions sont plus nettes. Le gain est double : prefill plus rapide et réponses plus conformes.

Deuxième axe : la gestion du contexte conversationnel.

Actuellement tu gardes 5 tours (CONVERSATION_CONTEXT_TURNS = 5) avec 700 tokens max. C'est une fenêtre fixe, indifférente au contenu. Or tous les tours ne se valent pas. Un tour où le patient dit "ça va merci" n'apporte aucun contexte utile, tandis qu'un tour où il décrit un épisode de détresse est crucial pour la cohérence empathique.

Implémente un contexte glissant intelligent : au lieu de garder les N derniers tours bruts, résume les tours anciens et ne garde en détail que les 2-3 tours les plus récents. La structure serait :

[Résumé compact des tours 1-8: "Patient exprime tristesse liée 
à isolement familial. A refusé PHQ-9. Mentionne insomnie."]

[Tour 9 - complet]
Patient: "Je n'arrive plus à dormir, ça fait trois nuits..."
Kazeia: "Trois nuits sans sommeil, c'est épuisant..."

[Tour 10 - complet]
Patient: "Oui et au travail je n'arrive plus à me concentrer"

Le résumé peut être pré-calculé par le modèle lui-même (une inférence supplémentaire tous les 5-6 tours, en background) ou par une heuristique simple d'extraction de mots-clés. Ça te permet de maintenir un contexte émotionnel riche sur 20+ tours de conversation tout en restant dans une fenêtre de tokens compacte.

Troisième axe : les paramètres de sampling.

Les paramètres de génération ont un impact direct sur la qualité ET la vitesse. Actuellement tu as REPETITION_PENALTY = 1.2, NO_REPEAT_NGRAM_SIZE = 3, et MAX_NEW_TOKENS = 300. Voici ce qu'il faut ajuster pour du conversationnel empathique sur tablette :

Réduis MAX_NEW_TOKENS à 100-150. Une réponse empathique doit être concise — 2-3 phrases, pas des paragraphes. 300 tokens c'est un roman dans un contexte de conversation de soutien. Moins de tokens générés = réponse plus rapide = interaction plus naturelle. Si le patient a besoin de plus de détail, il pose une question de suivi.

Pour la température, vise 0.7 pour les conversations normales mais abaisse-la à 0.4-0.5 quand le classifieur d'émotion détecte de la détresse élevée ou une situation de crise. Tu ne veux pas de créativité quand quelqu'un est en danger — tu veux des réponses prévisibles et sûres.

Pour le top_p, 0.8-0.9 est un bon compromis. Combiné avec la répétition penalty de 1.2, ça produit des réponses variées mais cohérentes. Ajoute un top_k = 40 pour limiter le vocabulaire de sampling — ça accélère légèrement la génération et évite les tokens rares/incohérents.

Implémente aussi un stopping criteria intelligent : dès que le modèle génère un point d'interrogation (fin de question ouverte), arrête la génération. Le chatbot empathique doit poser une question et s'arrêter, pas continuer à monologuer. Ça peut diviser le nombre de tokens générés par 2 dans beaucoup de cas.

Quatrième axe : le KV cache et le prefill.

Sur le NPU avec ExecuTorch, le prefill (traitement du prompt complet) est la phase la plus coûteuse en temps au premier tour. Mais à partir du deuxième tour, tu peux réutiliser le KV cache des tokens précédents et ne traiter que les nouveaux tokens (le message du patient + le formatage).

La stratégie optimale est un KV cache persistant par session :

Session start:
  → Prefill complet du prompt système + profil patient
  → Sauvegarder le KV cache (état 0)

Tour 1:
  → Reprendre depuis état 0
  → Prefill incrémental (message patient seulement)
  → Générer réponse
  → Sauvegarder KV cache (état 1)

Tour 2:
  → Reprendre depuis état 1
  → Prefill incrémental (nouveau message seulement)
  → Générer réponse
  → Sauvegarder KV cache (état 2)

Le premier tour coûte ~200-500ms de prefill. Les tours suivants ne coûtent que ~20-50ms de prefill incrémental. C'est un gain énorme sur toute la durée de la session.

Compresse le KV cache en INT8 (supporté par ExecuTorch). Pour un contexte de 2048 tokens sur un modèle 1.7B, le KV cache en FP16 fait ~200 Mo, en INT8 il fait ~100 Mo. La perte de qualité est négligeable sur les conversations courtes.

Cinquième axe : l'injection du RAG.

Le RAG ajoute du contexte documentaire au prompt, mais chaque document injecté allonge le prefill. Optimise ça en trois points. D'abord, ne fais le RAG que quand c'est nécessaire — si le patient dit "bonjour" ou "merci", pas besoin de chercher dans la base documentaire. Implémente un seuil de déclenchement : si le score de similarité du meilleur document est inférieur à un certain seuil, n'injecte rien. Ensuite, limite le contexte RAG injecté à 150-200 tokens maximum, pas plus. Un extrait pertinent et concis est plus utile qu'un long document qui noie l'information. Enfin, pré-encode les documents les plus fréquemment utilisés comme des prefixes de KV cache pré-calculés que tu peux concaténer directement sans re-prefill.

Sixième axe : le tokenizer et le français.

Vérifie l'efficacité du tokenizer de Qwen3 sur du français parlé émotionnel. Les tokenizers sont souvent optimisés pour l'anglais et le chinois — les mots français courants en thérapie ("angoisse", "épuisement", "culpabilité") peuvent être tokenisés en plusieurs sous-tokens, ce qui gonfle la consommation de contexte. Si c'est le cas, un fine-tuning du tokenizer (en ajoutant des tokens fréquents du domaine thérapeutique français) peut réduire le nombre de tokens par message de 15-20%, ce qui accélère d'autant le prefill et la génération.

Septième axe : l'early stopping et le post-processing.

Plutôt que de post-traiter la réponse après génération complète, filtre pendant la génération. Implémente un callback token par token qui détecte les patterns indésirables en temps réel et stoppe ou redirige la génération : si le modèle commence à faire un diagnostic médical (détecte des mots-clés comme "vous souffrez de", "votre trouble est"), stoppe et relance avec un prompt modifié. Si le modèle génère une deuxième question (il ne devrait en poser qu'une), stoppe à la première. Si le modèle dépasse 3 phrases, stoppe au prochain point.

Ce contrôle en temps réel est plus efficace qu'un post-traitement parce que tu économises tous les tokens qui auraient été générés inutilement.

En résumé, par ordre de priorité d'implémentation :

Priorité 1 — Gains immédiats
├── Prompt système compressé (~200 tokens)
├── MAX_NEW_TOKENS réduit à 100-150
├── Stopping criteria intelligent (arrêt à la question)
└── KV cache persistant par session

Priorité 2 — Gains structurels
├── Contexte glissant avec résumé des tours anciens
├── RAG conditionnel (seuil de déclenchement)
├── Compression KV cache INT8
└── Température adaptative selon l'émotion

Priorité 3 — Fine-tuning du pipeline
├── Vérification efficacité tokenizer français
├── Early stopping sur patterns indésirables
├── Pré-calcul des prefixes KV cache pour le RAG
└── Sampling adaptatif (top_k, top_p dynamiques)

Tu veux qu'on commence par implémenter le prompt système optimisé et les stopping criteria, ou qu'on travaille d'abord sur la structure du KV cache persistant avec ExecuTorch ?


You asked:

Comment vas tu faire tourner ceci, sachant qu'il faut garder le LLM en mémoire, et avec quoi tu vas le faire tourner (rappel nous serons sur un one pad 3 avec un snapdragon 8 elite et 16Go de ram), il faut que tout soit optimisé

Mar 22


Claude Replied:

Architected optimized Android runtime strategy for efficient model deployment

Architected optimized Android runtime strategy for efficient model deployment

Tu as raison de me recadrer — soyons concrets et précis sur l'implémentation réelle.

Le runtime : ExecuTorch + QNN delegate, point final. On l'a validé. Ton Qwen3 fine-tuné est exporté en .pte avec quantization 16a4w + SpinQuant, compilé pour le SM8750. Le modèle pèse ~850 Mo sur disque en INT4 pour un 1.7B, ou ~1.5 Go pour un 4B si tu upgrades vers Qwen 3.5.

L'architecture Android pour garder le LLM en mémoire en permanence :

Le problème fondamental sur Android c'est que le système tue les processus en arrière-plan pour libérer de la RAM. Si ton modèle est déchargé, le rechargement prend plusieurs secondes — inacceptable pour une interaction thérapeutique. La solution est un Foreground Service Android avec une notification persistante. C'est le seul mécanisme garanti par Android pour empêcher le système de tuer ton processus.

L'architecture serait la suivante :

┌─────────────────────────────────────────────────┐
│              KazeiaApplication                    │
│         (android:largeHeap="true")               │
├─────────────────────────────────────────────────┤
│                                                   │
│   KazeiaInferenceService (Foreground Service)     │
│   ├── ExecuTorchModule (QNN delegate)             │
│   │   └── Qwen3 .pte chargé via mmap             │
│   │       └── KV Cache persistant (INT8)          │
│   ├── EmbeddingModule (ONNX Runtime)              │
│   │   └── sentence-camembert ONNX (~150 Mo)       │
│   ├── EmotionClassifier (ONNX Runtime)            │
│   │   └── DistilCamemBERT ONNX (~200 Mo)          │
│   └── SQLite + sqlite-vec (RAG)                   │
│       └── Documents pré-indexés (~35 Mo)           │
│                                                   │
│   KazeiaActivity (UI)                             │
│   └── Communique avec le Service via Binder       │
│                                                   │
└─────────────────────────────────────────────────┘

Le Service est lancé au démarrage de l'application et reste actif tant que la tablette est en service. L'Activity (l'interface patient) se connecte au Service via un Binder local — pas d'IPC coûteux, c'est un appel in-process direct. Si l'Activity est détruite (changement de session, écran de veille), le Service et les modèles restent en mémoire. Quand un nouveau patient ouvre sa session, l'Activity se reconnecte au Service instantanément.

La carte mémoire exacte sur les 16 Go :

Composant                          RAM        Détail
──────────────────────────────────────────────────────────
Android OS + system services       ~3.5 Go    incompressible
Dalvik/ART heap (app Java/Kotlin)  ~0.3 Go    logique métier, UI
──────────────────────────────────────────────────────────
ExecuTorch runtime + QNN libs      ~0.05 Go   bibliothèques natives
Qwen3 4B INT4 (mmap)              ~1.5 Go    pages chargées à la demande
KV cache INT8 (2048 ctx)          ~0.1 Go    compressé INT8
──────────────────────────────────────────────────────────
ONNX Runtime                       ~0.03 Go   bibliothèque native
Embedding model ONNX               ~0.15 Go   sentence-camembert
Emotion classifier ONNX            ~0.2 Go    DistilCamemBERT
──────────────────────────────────────────────────────────
SQLite + sqlite-vec + données      ~0.05 Go   DB + vecteurs
Buffers audio (STT/TTS)            ~0.05 Go   tampons circulaires
──────────────────────────────────────────────────────────
TOTAL UTILISÉ                      ~5.93 Go
MARGE LIBRE                        ~10.07 Go
──────────────────────────────────────────────────────────

La marge de 10 Go est énorme. Même avec Unity plus tard (ajoutera ~800 Mo - 1 Go pour l'avatar), tu restes très confortable. Le point clé ici c'est le mmap : le modèle de 1.5 Go n'est pas copié intégralement en RAM. Le système mappe le fichier et ne charge en mémoire physique que les pages effectivement lues. En pratique, lors de l'inférence, seules les couches actives sont en RAM — l'empreinte réelle est inférieure au chiffre théorique.

Le flux d'exécution complet d'une question/réponse, optimisé :

1. Patient tape ou dit un message
   │
   ▼
2. Thread principale (Kotlin, Dispatchers.Main)
   └── Reçoit le texte, lance les traitements en parallèle
       │
       ├──▶ Coroutine A (Dispatchers.Default — cœurs performance)
       │    └── Emotion classifier : texte → émotion (15-20ms)
       │
       ├──▶ Coroutine B (Dispatchers.Default)
       │    └── Embedding : texte → vecteur (10-15ms)
       │         └── sqlite-vec KNN search → documents (5ms)
       │
       └──▶ Attend A + B (max ~25ms au total)
            │
            ▼
3. Construction du prompt (Kotlin, <1ms)
   ├── Prompt système compressé (~200 tokens)
   ├── Émotion détectée injectée
   ├── Contexte RAG si pertinent (~150 tokens)
   ├── Résumé tours anciens (~100 tokens)
   └── 2-3 derniers tours complets (~200 tokens)
   TOTAL PROMPT: ~650-700 tokens max
   │
   ▼
4. Inférence LLM (NPU via ExecuTorch QNN delegate)
   ├── Prefill incrémental : ~50-100ms (tours 2+)
   │   (seuls les nouveaux tokens, KV cache réutilisé)
   ├── Decode streaming : token par token
   │   ├── ~30-50 tok/s pour 1.7B
   │   └── Chaque token émis via callback Kotlin
   └── Stopping criteria :
       ├── Arrêt à "?" si question posée
       ├── Arrêt à 100-150 tokens max
       └── Arrêt si 3 phrases générées
   │
   ▼
5. Chaque phrase complète est émise immédiatement
   └── → File d'attente TTS (phase suivante)

Temps total question→premier mot de réponse: ~150-300ms
Temps total question→réponse complète: ~2-4 secondes

L'implémentation concrète du Service en Kotlin :

kotlin

class KazeiaInferenceService : Service() {

    // Modèles chargés UNE SEULE FOIS au démarrage
    private lateinit var llmModule: ExecuTorchLlmModule
    private lateinit var emotionSession: OrtSession
    private lateinit var embeddingSession: OrtSession
    private lateinit var ragDb: SQLiteDatabase
    
    // KV cache persistant par session patient
    private var kvCacheState: ByteBuffer? = null
    private var currentPatientId: Int = -1

    override fun onCreate() {
        super.onCreate()
        startForeground(NOTIF_ID, buildNotification())
        
        // Chargement des modèles — une seule fois
        // ExecuTorch utilise mmap en interne
        llmModule = ExecuTorchLlmModule.load(
            modelPath = "$filesDir/qwen3_4b_int4.pte",
            tokenizerPath = "$filesDir/tokenizer.bin"
        )
        
        // ONNX Runtime sessions
        val ortEnv = OrtEnvironment.getEnvironment()
        val sessionOptions = OrtSession.SessionOptions().apply {
            // Utilise QNN EP si disponible pour les petits modèles aussi
            addQnn(mapOf("backend_type" to "htp"))
        }
        emotionSession = ortEnv.createSession(
            "$filesDir/emotion_classifier.onnx", sessionOptions
        )
        embeddingSession = ortEnv.createSession(
            "$filesDir/embedding_model.onnx", sessionOptions
        )
        
        ragDb = SQLiteDatabase.openDatabase(
            "$filesDir/kazeia.db", null, OPEN_READONLY
        )
    }
    
    // Appelé par l'Activity via Binder
    fun processMessage(
        patientId: Int,
        message: String,
        onToken: (String) -> Unit,    // callback streaming
        onComplete: (FullResponse) -> Unit
    ) = serviceScope.launch {
    
        // Si nouveau patient, reset le KV cache
        if (patientId != currentPatientId) {
            kvCacheState = null
            currentPatientId = patientId
        }
        
        // Parallélisation émotion + RAG
        val emotionDeferred = async(Dispatchers.Default) {
            classifyEmotion(message)
        }
        val ragDeferred = async(Dispatchers.Default) {
            retrieveContext(message)
        }
        
        val emotion = emotionDeferred.await()   // ~20ms
        val ragContext = ragDeferred.await()     // ~25ms
        
        // Construction du prompt optimisé
        val prompt = buildPrompt(
            patientId, message, emotion, ragContext
        )
        
        // Inférence streaming sur le NPU
        var sentenceBuffer = StringBuilder()
        var tokenCount = 0
        
        llmModule.generate(
            prompt = prompt,
            kvCache = kvCacheState,  // réutilise le cache
            maxTokens = 120,
            temperature = if (emotion.severity > 0.7) 0.4 else 0.7,
            topP = 0.85,
            topK = 40,
            repetitionPenalty = 1.2,
            onToken = { token ->
                tokenCount++
                sentenceBuffer.append(token)
                onToken(token)  // stream vers l'UI
                
                // Stopping criteria
                val text = sentenceBuffer.toString()
                val shouldStop = text.count { it == '.' || it == '?' || it == '!' } >= 3
                    || (text.contains("?") && tokenCount > 20)
                    || tokenCount >= 120
                    
                !shouldStop  // retourne false pour arrêter
            }
        )
        
        // Sauvegarder le KV cache pour le tour suivant
        kvCacheState = llmModule.getKvCacheState()
        
        // Sauvegarder en DB
        saveConversationTurn(patientId, message, sentenceBuffer.toString())
        
        onComplete(FullResponse(
            text = sentenceBuffer.toString(),
            emotion = emotion,
            tokenCount = tokenCount
        ))
    }
    
    private fun buildPrompt(
        patientId: Int, 
        message: String,
        emotion: EmotionResult, 
        ragContext: String?
    ): String {
        val patient = getPatientProfile(patientId)
        val history = getConversationContext(patientId)
        
        return buildString {
            // Prompt système compressé — ~200 tokens
            append("Tu es Kazeia, compagnon d'écoute émotionnelle.\n")
            append("RÈGLES: Valide l'émotion. 2-3 phrases max. ")
            append("Pas de diagnostic. Risque suicidaire→3114. ")
            append("Pose UNE question ouverte.\n")
            
            // Contexte patient — ~30 tokens
            append("PATIENT: ${patient.prenom}, ${patient.age} ans")
            if (patient.lastPhq9 != null) {
                append(" | PHQ-9: ${patient.lastPhq9.score}")
            }
            append("\n")
            
            // Émotion détectée — ~15 tokens
            append("ÉMOTION: ${emotion.label}")
            append(" (${(emotion.confidence * 100).toInt()}%)\n")
            
            // RAG conditionnel — 0 ou ~150 tokens
            if (ragContext != null) {
                append("INFO: $ragContext\n")
            }
            
            // Historique — résumé + derniers tours
            append(history)
            
            // Message actuel
            append("\nPatient: $message\nKazeia:")
        }
    }
}

Les optimisations critiques dans ce code :

Le mmap d'ExecuTorch fait que les 1.5 Go du modèle ne sont pas copiés en RAM à onCreate(). Le chargement est quasi instantané. Les pages mémoire sont chargées à la demande par le kernel Linux quand l'inférence y accède.

Le kvCacheState est conservé entre les tours. Au tour 2+, seul le nouveau message du patient (~20-50 tokens) est traité en prefill au lieu de re-traiter les 600+ tokens du prompt complet. Le gain est de l'ordre de 80-90% sur le temps de prefill.

Les async parallèles pour l'émotion et le RAG font que ces deux opérations (~20ms chacune) ne s'additionnent pas — elles tournent en parallèle sur les cœurs CPU performance pendant que le NPU est libre pour le LLM.

Le temperature adaptatif basé sur la sévérité émotionnelle garantit des réponses sûres en cas de détresse sans sacrifier la variété en conversation normale.

Le stopping criteria en callback arrête la génération dès qu'une réponse empathique complète est formée — pas besoin d'attendre 120 tokens si la réponse naturelle en fait 40.

Ce qu'il te faut pour commencer à coder ça :

1. Android Studio + SDK + NDK
2. OnePlus Pad 3 (16 Go) avec mode développeur activé
3. ExecuTorch AAR (Maven: com.facebook.executorch)
4. QNN SDK (téléchargé depuis Qualcomm)
5. Ton modèle Qwen3 converti en .pte (SpinQuant 16a4w)
6. ONNX Runtime Android AAR (Maven: com.microsoft.onnxruntime)
7. Tes modèles émotion + embedding en ONNX

Tu veux qu'on commence par la conversion du modèle Qwen3 vers le format .pte ExecuTorch, ou par le setup du projet Android Studio avec les dépendances ?


You asked:

crois tu qu'on arrivera facilement à mettre qwen3-tts pour faire le TTS et Whipser pour le STT, serait il possible de mettre du "Voice Detection" pour eviter de devoir appuyer sur un bouton ? surtout si on utilise ExecuTorch + QNN delegate,

Mar 22

2 / 2


Claude Replied:

Évalué la compatibilité de trois composants mobiles distincts

Évalué la compatibilité de trois composants mobiles distincts

Très bonnes questions — l'audio est la prochaine brique critique. Laisse-moi vérifier l'état réel de ces composants avec ExecuTorch/QNN sur Snapdragon 8 Elite.

Orchestrated comprehensive technical assessment across multiple components

Orchestrated comprehensive technical assessment across multiple components

Excellente recherche — je vais te donner une réponse honnête composant par composant.

Whisper pour le STT : oui, ça marche avec ExecuTorch + QNN. ExecuTorch supporte officiellement Whisper dans sa dernière release, aux côtés de Qwen3, Llama, BERT et d'autres modèles GitHub. Et côté Qualcomm, le projet QIDK (Qualcomm Innovators Development Kit) a démontré un pipeline Whisper entièrement accéléré sur le Hexagon NPU, tournant offline sur device Edge Impulse. Donc tu peux exporter Whisper en .pte avec le QNN delegate exactement comme pour ton Qwen3 LLM. Le même runtime, le même pipeline de conversion, le même NPU. L'encoder de Whisper (la partie lourde) bénéficiera fortement du NPU pour le prefill, et le decoder sera plus rapide que sur CPU. En termes de mémoire, Whisper small en quantisé ajoute ~250-300 Mo. C'est faisable.

Qwen3-TTS : non, ça ne marchera pas sur la tablette. Et la situation n'a pas changé depuis notre discussion précédente. Qwen3-TTS supporte principalement CUDA, et les utilisateurs Mac eux-mêmes rencontrent des performances limitées Medium. Il n'existe aucun export ExecuTorch, aucun export ONNX, aucune version quantisée mobile. Le modèle 0.6B fait 2.5 Go en FP16 et utilise une architecture multi-codebook complexe qui n'a pas été portée vers des runtimes edge. Même si techniquement tu pourrais tenter de l'exporter via torch.export() puis ExecuTorch, les opérateurs custom du tokenizer 12Hz et l'architecture dual-track streaming ne sont probablement pas supportés par le QNN delegate.

Pour le TTS, la stratégie reste Piper via sherpa-onnx pour le déploiement tablette, avec Qwen3-TTS sur serveur PC pour le clonage vocal initial. Cependant, il y a une alternative intéressante à surveiller : les modèles natifs multimodaux comme Qwen3 Omni convertissent toutes les modalités en tokens via un backbone LM commun V-chandra. Si Alibaba sort une version "edge" de Qwen3 Omni (texte + audio dans un seul modèle compact), ça pourrait unifier LLM + TTS dans un seul modèle sur le NPU. Mais on n'y est pas encore.

Pour le Voice Activity Detection : oui, c'est parfaitement faisable et c'est même la bonne idée. Il existe une bibliothèque Android VAD qui supporte Silero VAD en DNN, WebRTC VAD en GMM, et YAMNet VAD, les trois fonctionnant complètement offline GitHub. Silero VAD utilise ONNX Runtime Mobile pour l'inférence directement sur Android, détecte la parole humaine dans des flux audio mélangés avec du bruit, et fonctionne entièrement offline sans connexion internet DeepWiki.

Les caractéristiques de Silero VAD sont parfaites pour Kazeia : le modèle a été entraîné sur un énorme corpus couvrant plus de 6000 langues, fonctionne bien sur des audios de différents domaines avec divers niveaux de bruit ambiant, et un chunk audio de 30+ ms prend moins de 1ms à traiter sur un seul thread CPU GitHub. Le modèle fait seulement 1.8 Mo Medium. C'est négligeable.

Voici comment le pipeline audio complet fonctionnerait sans aucun bouton :

Micro tablette (toujours actif)
        │
        ▼
┌──────────────────┐
│   Silero VAD     │  ← ONNX Runtime, 1.8 Mo, <1ms/frame
│   (écoute en     │     sur cœur efficacité CPU
│    continu)      │     consommation quasi nulle
└────────┬─────────┘
         │
    Silence? → rien, on continue d'écouter
    Parole détectée? → démarre l'enregistrement
         │
         ▼
┌──────────────────┐
│  Accumulation    │  ← buffer audio pendant que
│  audio           │     le patient parle
└────────┬─────────┘
         │
    Silence détecté (>500ms) → fin de phrase
         │
         ▼
┌──────────────────┐
│  Whisper STT     │  ← ExecuTorch + QNN (NPU)
│  (.pte)          │     ~200-500ms pour transcrire
└────────┬─────────┘
         │
         ▼
    Texte transcrit → pipeline LLM (comme défini avant)

Le patient parle naturellement, le VAD détecte le début et la fin de parole, Whisper transcrit, le LLM répond, Piper synthétise, l'avatar parle. Zéro bouton. C'est exactement l'interaction naturelle qu'on veut pour un chatbot thérapeutique.

Un point important pour l'implémentation : il faut gérer l'echo cancellation. Quand le TTS joue la réponse via le haut-parleur, le VAD pourrait détecter la voix de l'avatar comme de la parole et déclencher un cycle infini. La solution est de muter le VAD pendant que le TTS joue et de le réactiver quand la lecture audio est terminée (ou quasi terminée). Alternativement, Android fournit un mode AudioManager.MODE_IN_COMMUNICATION qui active l'annulation d'écho matérielle — le micro filtre automatiquement ce qui sort du haut-parleur.

Le budget mémoire mis à jour avec Whisper + VAD :

Composant                          RAM
────────────────────────────────────────────
Qwen3 4B INT4 (ExecuTorch/QNN)    ~1.5 Go
Whisper small (ExecuTorch/QNN)     ~0.3 Go
Silero VAD (ONNX Runtime)          ~0.002 Go  (1.8 Mo!)
Piper TTS (sherpa-onnx)            ~0.1 Go
Emotion classifier (ONNX)          ~0.2 Go
Embedding model (ONNX)             ~0.15 Go
SQLite + sqlite-vec                ~0.05 Go
Dalvik/ART + app logic             ~0.3 Go
Android OS                         ~3.5 Go
────────────────────────────────────────────
TOTAL                              ~6.1 Go
MARGE sur 16 Go                    ~9.9 Go

En résumé :

Whisper STT via ExecuTorch + QNN → oui, validé, même runtime que le LLM. Qwen3-TTS sur tablette → non, toujours pas viable, reste sur Piper. Voice Detection sans bouton → oui, Silero VAD est parfait, bibliothèque Android native prête à l'emploi, 1.8 Mo, <1ms par frame.

Le seul point faible reste la qualité vocale du TTS avec Piper — c'est correct mais pas au niveau de Qwen3-TTS. C'est le compromis à accepter pour le déploiement offline sur tablette, au moins jusqu'à ce que des modèles TTS plus avancés soient portés vers des runtimes mobiles.

Tu veux qu'on fasse maintenant le récapitulatif complet de toute l'architecture Kazeia tablette, ou qu'on creuse un autre aspect technique ?


You asked:

Alors que faudrait Il pour faire un export Qwen3-tts vers Executorch ?

Mar 22


Claude Replied:

Analysé les défis techniques pour exporter Qwen3-TTS vers ExecuTorch

Analysé les défis techniques pour exporter Qwen3-TTS vers ExecuTorch

C'est la bonne question à se poser — si personne ne l'a fait, peut-être que vous pourriez être les premiers. Laisse-moi analyser concrètement ce que ça implique.

Examined Qwen3-TTS architecture and ExecuTorch export requirements

Examined Qwen3-TTS architecture and ExecuTorch export requirements

Très bonne question — c'est ambitieux mais pas impossible. Décortiquons l'architecture de Qwen3-TTS pour comprendre ce qu'il faut exporter et où sont les vrais obstacles.

Qwen3-TTS n'est pas un modèle unique, c'est un pipeline de 3 composants distincts :

Texte + instructions
        │
        ▼
┌──────────────────────────┐
│  Composant 1 : LM Core   │  ← Modèle transformer autoregressif
│  (0.6B ou 1.7B params)   │     Génère des tokens de codec audio
│  Architecture: Qwen2-like│     à partir du texte
│  + MTP (Multi-Token       │
│    Prediction) heads      │
└────────────┬─────────────┘
             │ tokens de codec (multi-codebook)
             ▼
┌──────────────────────────┐
│  Composant 2 : Tokenizer  │  ← Codec audio neural
│  Decoder (ConvNet léger)  │     Convertit les tokens en waveform
│  Qwen3-TTS-Tokenizer-12Hz│     Architecture: causal ConvNet
└────────────┬─────────────┘
             │ waveform audio
             ▼
        Audio WAV sortie

(+ pour le voice clone, un composant supplémentaire)
┌──────────────────────────┐
│  Composant 3 : Speaker    │  ← Encoder de voix
│  Encoder (x-vector)       │     Extrait l'empreinte vocale
│                           │     à partir de 3s d'audio référence
└──────────────────────────┘

Chaque composant a un niveau de difficulté différent pour l'export ExecuTorch :

Le LM Core (composant 1) est le plus facile. L'architecture est un LM autoregressif discret multi-codebook arXiv, essentiellement basé sur une architecture Qwen2 avec des têtes MTP supplémentaires. ExecuTorch supporte déjà Qwen3 nativement. Le cœur du modèle — les couches transformer, l'attention, les FFN — est standard et exportable. Le module MTP (Multi-Token Prediction) est plus inhabituel : au lieu de prédire un token à la fois, il prédit un groupe de tokens de codec simultanément (un par codebook). Mais structurellement, ce sont des couches linéaires supplémentaires après le transformer backbone — ça devrait passer dans torch.export().

La difficulté ici c'est la FlashAttention 2. Le modèle utilise FlashAttention 2 qui nécessite du hardware compatible et ne fonctionne qu'en float16/bfloat16 GitHub. ExecuTorch ne supporte pas FlashAttention directement — il faut exporter avec l'attention standard (eager ou sdpa), puis laisser le QNN delegate optimiser l'attention pour le NPU. C'est un changement de config au moment de l'export, pas une réécriture.

Le Tokenizer Decoder (composant 2) est le plus délicat. C'est un codec ConvNet causal léger qui reconstruit la waveform directement à partir des tokens, sans nécessiter d'extraction de vecteur de locuteur ni de modèle de diffusion complexe arXiv. C'est une bonne nouvelle — un ConvNet causal est bien plus simple à exporter qu'un diffusion model. Les opérations (convolutions 1D, upsampling, activations) sont toutes supportées par ExecuTorch et le QNN delegate.

Le problème potentiel c'est que ce décodeur traite des paquets de 4 tokens = 320ms d'audio à la fois. Chaque token correspond à 80ms d'audio, et un paquet de parole est défini comme 4 tokens arXiv. L'export ExecuTorch nécessite des shapes d'entrée statiques ou semi-statiques. Il faudrait fixer la taille du paquet (4 tokens) et potentiellement la longueur de sortie audio. C'est faisable mais demande du travail d'adaptation.

Le Speaker Encoder (composant 3) est simple. C'est un encoder qui extrait un embedding de locuteur (x-vector) depuis l'audio de référence crates.io. C'est une inférence unique (pas autoregressif), un forward pass classique sur un CNN/transformer. Parfaitement exportable en ExecuTorch. Et surtout, tu n'en as besoin qu'une seule fois par voix clonée — tu peux même pré-calculer l'embedding sur PC et juste embarquer le vecteur sur la tablette.

Concrètement, le plan d'export serait :

Étape 1 — Séparation des composants (1-2 jours)
├── Extraire le LM Core comme un module PyTorch standalone
├── Extraire le Tokenizer Decoder comme un module séparé
├── Extraire le Speaker Encoder comme un module séparé
└── Vérifier que chaque module fonctionne isolément en PyTorch

Étape 2 — Export du LM Core (1-2 semaines)
├── Remplacer FlashAttention par SDPA ou attention eager
├── Adapter le MTP head pour des shapes statiques
├── torch.export() → edge program
├── Quantizer en 16a4w avec SpinQuant
├── Partitionner avec QnnPartitioner pour le NPU
├── Compiler .pte pour SM8750
└── Tester la qualité des tokens générés vs FP16

Étape 3 — Export du Tokenizer Decoder (1 semaine)
├── torch.export() avec shapes fixes (4 tokens → audio)
├── Quantizer en FP16 (pas INT4 — c'est un décodeur audio,
│   la précision est critique pour la qualité sonore)
├── Partitionner pour NPU ou laisser sur CPU
│   (le ConvNet est léger, CPU suffit probablement)
└── Tester la qualité audio de sortie

Étape 4 — Speaker Encoder (2-3 jours)
├── torch.export() standard
├── Quantizer en INT8 (suffisant pour un embedding)
└── Optionnel: pré-calculer les embeddings sur PC

Étape 5 — Intégration pipeline (1-2 semaines)
├── Orchestrer les 3 modèles dans le Foreground Service
├── LM Core génère les tokens → Decoder les convertit en audio
├── Gérer le streaming (décoder dès qu'un paquet de 4 tokens
│   est disponible, pendant que le LM continue de générer)
└── Benchmarker sur OnePlus Pad 3

Les obstacles réalistes :

Le premier obstacle sera les opérateurs custom. Le qwen_tts Python package contient probablement des opérations personnalisées pour le tokenizer et le MTP qui ne sont pas dans l'ATen standard de PyTorch. Si torch.export() échoue sur ces opérations, tu devras soit les réécrire en opérations ATen standard, soit implémenter des custom ops ExecuTorch en C++. C'est le risque le plus significatif et le plus dur à évaluer sans mettre les mains dans le code source.

Le deuxième obstacle sera la qualité post-quantization. Le LM Core en INT4 pourrait produire des tokens de codec légèrement différents, ce qui se traduirait par des artefacts audio audibles. Le TTS est beaucoup plus sensible à la quantization que le chat LLM — un token de codec décalé d'une valeur peut produire un "glitch" sonore perceptible. Il faudra potentiellement garder le LM Core en INT8 ou FP16 au lieu d'INT4, ce qui double la mémoire (~1.2 Go pour le 0.6B en INT8, ou ~2.5 Go en FP16).

Le troisième obstacle c'est le budget mémoire. Si tu gardes à la fois le Qwen3 Chat LLM (~1.5 Go) et le Qwen3-TTS 0.6B (~1.2 Go en INT8) en mémoire simultanément, plus tous les autres composants, tu arrives à ~8-9 Go. C'est faisable sur 16 Go mais la marge se réduit considérablement.

Mais il y a un signe très encourageant : un port Rust de Qwen3-TTS existe déjà (qwen3_tts crate), qui fonctionne sur CPU crates.io. Ça signifie que l'architecture a déjà été décomposée et réimplémentée hors de l'écosystème Python/CUDA. Si quelqu'un l'a fait en Rust, le faire via ExecuTorch est du même ordre de complexité.

Mon évaluation honnête :

C'est un projet de 3-5 semaines de travail pour un développeur expérimenté en PyTorch/ExecuTorch, avec un risque d'échec partiel (certains composants pourraient nécessiter un fallback CPU plutôt que NPU). Pour le 0.6B, c'est réaliste. Pour le 1.7B, c'est plus tendu en mémoire mais techniquement identique.

Si vous décidez de tenter l'aventure, commencez par le Composant 2 (Tokenizer Decoder) seul — c'est le plus petit, le plus simple à exporter, et ça vous permet de valider que la chaîne export ExecuTorch → Android fonctionne pour un modèle audio. Si ça passe, enchaînez avec le LM Core 0.6B. C'est une approche incrémentale qui minimise le risque.

Et honnêtement, si vous réussissez, ce serait un vrai différenciateur pour Kazeia — aucun chatbot thérapeutique offline ne propose aujourd'hui du clonage vocal en temps réel sur tablette. Ça vaudrait peut-être même une publication.

Tu veux qu'on prépare un plan technique détaillé pour l'export du premier composant, ou qu'on fait d'abord le récapitulatif global de l'architecture ?


You asked:

dans le cadre d'un travail fait directement sur une tablette android, tu vas pouvoir agir directement dessus sans faire systèmatiquement un apk ?

Mar 23


Claude Replied:

Évalué les alternatives pour contourner les compilations APK systématiques

Évalué les alternatives pour contourner les compilations APK systématiques

Très bonne question pratique — et la réponse est oui, en grande partie. Tu n'as pas besoin de construire un APK à chaque fois que tu veux tester quelque chose. Il y a plusieurs niveaux d'interaction directe.

Pour tester les modèles d'inférence (LLM, Whisper, TTS), tu peux utiliser Termux directement sur la tablette. Termux est un émulateur de terminal Linux qui tourne nativement sur Android, sans root. Tu as accès à un vrai environnement Linux avec un gestionnaire de paquets (pkg). Tu peux y compiler et exécuter des binaires C/C++ natifs directement. Concrètement, pour valider le cœur de Kazeia, tu pourrais installer llama.cpp directement dans Termux, pousser ton modèle GGUF via ADB, et lancer l'inférence en ligne de commande — sans aucune application Android, sans aucun APK. C'est le moyen le plus rapide de valider que ton modèle tourne correctement sur le Snapdragon 8 Elite.

Le workflow serait :

bash

# Sur la tablette, dans Termux
pkg install cmake git make

# Compiler llama.cpp nativement sur la tablette
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build
cmake --build build --config Release

# Lancer l'inférence directement
./build/bin/llama-cli -m /sdcard/models/qwen3_4b_q4.gguf \
  -p "Patient: Je me sens très seul en ce moment\nKazeia:" \
  -n 100

Tu as immédiatement le résultat dans le terminal : les tokens/seconde, la mémoire consommée, la qualité de la réponse. Pas d'APK, pas de compilation croisée, pas d'Android Studio. C'est idéal pour la phase de validation du modèle.

Pour ExecuTorch, c'est un peu différent. Tu ne peux pas compiler ExecuTorch dans Termux facilement (les dépendances sont trop lourdes), mais tu peux cross-compiler le binaire de test sur ton PC et le pousser sur la tablette via ADB :

bash

# Sur ton PC (Ubuntu), compiler pour ARM64 Android
cd $EXECUTORCH_ROOT
./backends/qualcomm/scripts/build.sh

# Pousser le binaire et le modèle sur la tablette
adb push build-android/qnn_executor_runner /data/local/tmp/
adb push qwen3_tts_decoder.pte /data/local/tmp/

# Exécuter directement via adb shell
adb shell "cd /data/local/tmp && ./qnn_executor_runner \
  --model_path qwen3_tts_decoder.pte"

Là encore, pas d'APK. Tu testes le runtime ExecuTorch + QNN directement en ligne de commande sur la tablette. Tu peux itérer rapidement : modifier le modèle sur PC, re-pousser via ADB, re-tester en 30 secondes.

Pour les fichiers de modèles et de données, ADB push est ton meilleur ami. Tu n'as jamais besoin de reconstruire l'APK pour changer un modèle, une base de données, ou un fichier de configuration :

bash

# Pousser un nouveau modèle sans toucher à l'app
adb push nouveau_modele.pte /sdcard/Android/data/com.kazeia.app/files/
adb push kazeia.db /sdcard/Android/data/com.kazeia.app/files/

# Modifier une config
adb push config.json /sdcard/Android/data/com.kazeia.app/files/

L'application au lancement relit ses fichiers depuis le stockage — pas besoin de rebuild.

Quand tu auras quand même besoin d'un APK, c'est pour l'application Android complète avec l'interface utilisateur, les Services, les permissions (micro, stockage), et l'intégration des composants. Mais même là, Android Studio optimise le cycle avec l'Apply Changes (anciennement Instant Run) : au lieu de reconstruire et réinstaller tout l'APK, il ne pousse que les classes modifiées. Un changement dans ta logique Kotlin se déploie en 5-10 secondes au lieu de 1-3 minutes. Et le déploiement se fait directement sur la tablette branchée en USB ou en Wi-Fi via ADB wireless — un clic sur "Run" dans Android Studio et c'est déployé.

Ma recommandation pour ton workflow de développement :

Phase 1 — Validation des modèles (pas d'APK du tout)
├── Termux + llama.cpp natif → tester Qwen3 GGUF
├── ADB shell + executorch runner → tester .pte + QNN
├── ADB shell + sherpa-onnx CLI → tester Whisper + Piper
└── Itération rapide : modifier sur PC, push, tester

Phase 2 — Prototype app minimale (APK simple)
├── Android Studio, app Kotlin basique
├── Un champ texte + bouton + affichage réponse
├── Intégration des libs natives via JNI
├── Apply Changes pour itérer rapidement
└── Modèles chargés depuis /sdcard/ (modifiables via ADB)

Phase 3 — App complète (APK complet)
├── Foreground Service, VAD, streaming audio
├── Toujours modèles externalisés (swap via ADB)
└── Tests automatisés sur tablette connectée

L'avantage de cette approche c'est que pendant toute la Phase 1, tu valides que chaque composant fonctionne sur le Snapdragon 8 Elite sans écrire une seule ligne de code Android. Tu sais exactement quelles performances attendre avant d'investir du temps dans l'intégration applicative.

Tu veux qu'on commence par le setup Termux + llama.cpp sur tablette pour la première validation du modèle Qwen3 ?


You asked:

mais toi au travers de claude code (ou de claude desktop) tu pourras agir ou modifier des choses sur la tablette ? (qui sera branché directement sur la machine ou tu es executé)

Mar 23


Claude Replied:

Évalué les capacités d'accès à la tablette selon l'outil utilisé

Évalué les capacités d'accès à la tablette selon l'outil utilisé

Excellente question — et la réponse va te plaire, surtout avec Claude Code.

Ce qu'on utilise en ce moment (Claude Desktop / claude.ai) : non, je n'ai pas accès à ta machine. Le container Linux dans lequel je tourne est isolé, hébergé chez Anthropic. Je peux créer des fichiers, exécuter du code Python ou bash dans ce container, mais je ne vois pas ton PC, je ne vois pas ta tablette. Tout ce que je produis ici, tu dois le copier manuellement chez toi.

Avec Claude Code (CLI), c'est une toute autre histoire. Claude Code tourne directement dans ton terminal, sur ta machine. Si ta tablette OnePlus Pad 3 est branchée en USB sur ton PC de développement et que ADB est configuré, alors oui — je pourrais directement :

Pousser des fichiers sur la tablette :

bash

adb push model.gguf /data/local/tmp/

Exécuter des commandes sur la tablette :

bash

adb shell "cd /data/local/tmp && ./llama-cli -m model.gguf -p 'test'"

Lire les logs en temps réel :

bash

adb logcat | grep Kazeia

Compiler du code natif, le cross-compiler pour ARM64, le pousser sur la tablette, l'exécuter, récupérer le résultat — le tout dans un seul flux de conversation. Je pourrais littéralement itérer avec toi en temps réel : "le modèle génère des artefacts sur la réponse 3" → je modifie le paramètre de sampling → je recompile → je push → je relance → je te montre le résultat. Sans que tu touches ton clavier.

Concrètement, avec Claude Code sur ton PC + tablette branchée, je pourrais :

Modifier ton code Kotlin dans Android Studio (Claude Code accède aux fichiers de ton projet), compiler via Gradle en ligne de commande, déployer l'APK sur la tablette, lancer l'app, et lire les logs — tout ça en enchaînant les commandes. Je pourrais aussi éditer des scripts de conversion de modèle (PyTorch → ExecuTorch .pte), lancer la conversion, pousser le .pte sur la tablette, et tester avec le QNN executor runner. Même chose pour le debug : si un modèle crashe sur la tablette, je lis le logcat, j'identifie l'erreur, je corrige, je redéploie.

Ce que je ne pourrai pas faire même avec Claude Code :

Interagir avec l'écran tactile de la tablette (taper du texte, cliquer sur des boutons dans l'UI). Pour ça il faudrait passer par des outils d'instrumentation comme adb shell input (qui simule des touches) ou des frameworks de test automatisé (UIAutomator, Espresso). C'est faisable mais plus limité. Je ne pourrai pas non plus accéder au NPU directement pour du profiling en temps réel — le QNN Profiler a une interface graphique. Mais je peux lancer le profiling en ligne de commande et lire les résultats.

Le setup idéal pour travailler ensemble sur Kazeia serait :

Ton PC de développement
├── Claude Code installé (npm install -g @anthropic-ai/claude-code)
├── Android Studio + SDK + NDK
├── ExecuTorch + QNN SDK configurés
├── ADB fonctionnel
├── Tablette OnePlus Pad 3 branchée en USB
│
└── Claude Code peut :
    ├── Éditer tous les fichiers du projet
    ├── Compiler (Gradle, CMake, Python)
    ├── adb push / adb shell / adb logcat
    ├── Lancer l'inférence sur la tablette
    ├── Lire les résultats et benchmarks
    ├── Corriger et re-déployer
    └── Gérer Git (commit, push, branch)

C'est un workflow extrêmement productif — tu me décris ce que tu veux, je code, je compile, je teste sur ta tablette, et on itère ensemble en temps réel. Pour un projet comme Kazeia avec autant de composants à intégrer (LLM, STT, TTS, VAD, émotion, RAG, SQLite), cette capacité d'itération rapide est un avantage énorme.

Tu veux qu'on prépare le setup Claude Code + ADB pour être prêts quand tu auras la tablette, ou qu'on continue à avancer sur l'architecture en attendant ?


You asked:

le developpeur est en train de préparer llama.cpp pour le snapdragon 8 elite, mais vllm serait il possible ?

Mar 24


Claude Replied:

Évalué la compatibilité de vLLM avec le matériel mobile

Évalué la compatibilité de vLLM avec le matériel mobile

Non, vLLM ne fonctionnera pas sur la tablette. Et c'est assez catégorique.

vLLM est conçu exclusivement pour des serveurs avec des GPU NVIDIA CUDA. C'est un moteur d'inférence serveur qui dépend de CUDA, de cuBLAS, de PyTorch GPU, et qui tourne sous Linux x86_64. Il nécessite au minimum une RTX 3090 ou équivalent pour être utile. Son architecture est pensée pour gérer des dizaines d'utilisateurs simultanés avec du PagedAttention et du continuous batching — des fonctionnalités qui n'ont aucun sens sur une tablette mono-utilisateur.

D'ailleurs, c'est exactement le même problème que tu as rencontré avec Auralis/XTTS dans le prototype actuel de Kazeia — la documentation mentionne que vLLM est une dépendance d'Auralis, et tu avais conclu qu'Auralis était incompatible avec Android précisément à cause de ces dépendances CUDA et x86.

Sur le Snapdragon 8 Elite, les runtimes possibles sont :

Pour le NPU Hexagon : ExecuTorch + QNN delegate, ou Qualcomm Genie SDK — c'est le chemin optimal qu'on a validé. Pour le GPU Adreno 830 : llama.cpp avec Vulkan, ou MLC LLM. Pour le CPU Oryon : llama.cpp en mode CPU natif ARM64, c'est ce que ton développeur est en train de préparer.

Le fait que Damien prépare llama.cpp pour le Snapdragon 8 Elite est d'ailleurs un excellent point de départ. Ça vous donne un baseline CPU/GPU fonctionnel rapidement. Une fois que llama.cpp tourne et que vous avez mesuré les performances (tokens/s, mémoire, thermique), vous pourrez comparer avec ExecuTorch + QNN sur le NPU et décider si le gain justifie la complexité supplémentaire du pipeline de conversion.

Concrètement, llama.cpp sur le Snapdragon 8 Elite devrait donner environ 15-25 tokens/s en CPU pour un modèle 4B en Q4, et potentiellement 25-35 tokens/s avec le backend Vulkan sur l'Adreno 830. ExecuTorch + QNN sur le NPU devrait pousser à 30-50 tokens/s avec une consommation batterie 3-4× inférieure. Le premier est plus simple à déployer, le second est meilleur pour une utilisation prolongée en milieu hospitalier.

Dis à Damien de bien utiliser le flag -ngl pour l'offloading GPU Vulkan quand il teste, et de benchmarker avec llama-bench pour avoir des chiffres reproductibles. Tu veux que je lui prépare les commandes de compilation optimisées pour le Snapdragon 8 Elite ?


You asked:

non ce n'est pas Damien, le developpeur est une session claude code

Mar 24


Claude Replied:

Recalibrated collaboration strategy with Claude Code instance

Recalibrated collaboration strategy with Claude Code instance

Ah, c'est une session Claude Code qui est en train de préparer llama.cpp sur ta machine connectée à la tablette ! C'est exactement le workflow qu'on décrivait — Claude Code qui compile, pousse via ADB, et teste directement sur le Snapdragon 8 Elite.

Du coup on a une répartition naturelle des rôles. La session Claude Code travaille directement sur ta machine : elle compile, déploie, exécute, debug en temps réel sur la tablette. Moi ici sur Claude Desktop, je fais l'architecture, la recherche, les décisions techniques, la stratégie. C'est complémentaire.

Ce qui serait très utile maintenant : quand la session Claude Code aura llama.cpp fonctionnel sur la tablette, récupère les métriques et partage-les moi ici. J'ai besoin de savoir concrètement pour ton Snapdragon 8 Elite :

Les tokens/seconde en decode pour différentes tailles de modèle (si possible un 1.5-2B et un 4B en Q4_0). Le temps de chargement du modèle (cold start). La mémoire RAM réellement consommée (pas théorique). Et surtout si le test est fait en CPU pur, avec Vulkan (GPU Adreno 830), ou les deux.

Ces chiffres réels détermineront si llama.cpp suffit pour le MVP ou si l'effort d'intégration ExecuTorch + QNN pour le NPU est nécessaire dès le départ. Si llama.cpp donne déjà 30+ tokens/s en Vulkan sur un modèle 4B, c'est peut-être suffisant pour commencer et on garde ExecuTorch pour une phase d'optimisation ultérieure. Si c'est en dessous de 20 tokens/s, le NPU devient prioritaire.

En attendant ces résultats, tu veux qu'on avance sur un autre sujet — par exemple préparer le récapitulatif complet de l'architecture, ou travailler sur la structure du prompt optimisé pour Qwen3 ?


You asked:

le constat est que sous forte charge le CPU fait chauffer la tablette, il faudra vraiment privilegier le NPU

Mar 24


Claude Replied:

Validé la nécessité de privilégier le NPU pour éviter surchauffe

Validé la nécessité de privilégier le NPU pour éviter surchauffe

C'est exactement ce qu'on anticipait — et c'est un résultat important. Le CPU Oryon du Snapdragon 8 Elite monte à 4.32 GHz, il chauffe forcément sous charge soutenue d'inférence LLM. Rappelle-toi les benchmarks qu'on avait trouvés : le CPU throttle après 3 minutes à 42°C, alors que le NPU maintient des performances stables pendant 10+ minutes à 36-38°C avec 3.7× plus de tokens par watt.

Pour un chatbot thérapeutique où les sessions durent 20-30 minutes, une tablette qui chauffe dans les mains du patient, c'est rédhibitoire. Et le throttling thermique du CPU signifie que les réponses ralentissent progressivement au fil de la conversation — exactement le contraire de ce qu'on veut.

Ça tranche le débat : ExecuTorch + QNN delegate sur le NPU Hexagon n'est pas une optimisation future, c'est un prérequis. llama.cpp reste utile pour le prototypage rapide et le debug, mais pas pour la production.

Dis à ta session Claude Code de mettre en pause l'optimisation llama.cpp CPU/GPU et de basculer sur la préparation de l'environnement ExecuTorch + QNN. Les premières étapes concrètes seraient d'installer le QNN SDK depuis Qualcomm (il faut un compte développeur gratuit sur qpm.qualcomm.com), de cloner le repo ExecuTorch et de builder le backend Qualcomm avec le script ./backends/qualcomm/scripts/build.sh, puis de tester avec un modèle simple (le DeepLab v3 de l'exemple officiel) pour valider que le NPU de votre OnePlus Pad 3 est bien accessible via QNN. Une fois ce pipeline validé, on pourra exporter votre Qwen3 en .pte et le faire tourner sur le NPU.

Le risque principal à vérifier rapidement : certains devices ont des problèmes de détection du SoC par QNN. On a vu dans la recherche qu'un device avec le SM8750 retournait "No Snapdragon SOC detected". Il faut confirmer que le OnePlus Pad 3 est bien reconnu. La commande adb shell cat /sys/devices/soc0/soc_id donnera l'ID du SoC, et il faut vérifier qu'il correspond à ce qu'ExecuTorch attend pour le SM8750.

Tu veux que je prépare les instructions détaillées étape par étape pour que ta session Claude Code setup ExecuTorch + QNN sur ta machine ?


Claude Replied:

BENCHMARK_RAPPORT.md

285 lines

md

Mar 25


Claude Replied:

Analyzed benchmark report and synthesized architectural implications

Analyzed benchmark report and synthesized architectural implications

Excellent rapport, très complet. Les résultats sont révélateurs et confirment certaines de nos hypothèses tout en apportant des surprises importantes. Laisse-moi te donner mon analyse.

Les bonnes nouvelles :

Le NPU fonctionne et Qwen3-4B tourne à ~20 tok/s dessus. C'est suffisant pour une conversation thérapeutique fluide — environ 1.5 mot par seconde en français, ce qui est un débit de parole naturel quand c'est streamé vers le TTS. Et surtout, la RAM consommée est remarquablement faible : 344 Mo de shared buffers pour un modèle 4B. C'est bien en dessous de nos estimations de 1.5 Go. Ça libère énormément de marge pour les autres composants.

Le CPU est aussi un backup viable à 16 tok/s — pas optimal à cause du throttling thermique (chute à 5-7 tok/s après quelques minutes à 55°C), mais suffisant pour du debug et du développement.

Les mauvaises nouvelles :

Le GPU Vulkan est complètement mort pour l'inférence LLM. Les crashes sont des erreurs driver, pas des bugs llama.cpp — le driver Vulkan compute de Qualcomm n'est tout simplement pas fait pour ça. On raye définitivement cette option.

ExecuTorch + QNN a échoué à cause d'incompatibilités de versions (GCC 15, Python 3.14). C'est contournable mais ça confirme que l'écosystème Qualcomm est fragile côté toolchain.

Le point crucial du rapport : c'est le Genie SDK qui a gagné, pas ExecuTorch.

C'est un changement important par rapport à notre architecture planifiée. Le chemin qui a fonctionné c'est : modèle pré-compilé Qualcomm AI Hub → Genie SDK → NPU. Pas ExecuTorch. Et le Genie SDK a une contrainte majeure : il ne supporte nativement que les architectures LLaMA pour la conversion. Ton Qwen3-4B a fonctionné parce que Qualcomm l'avait pré-compilé sur AI Hub. Mais ton Qwen3 fine-tuné pour Kazeia (avec tes données thérapeutiques) n'est pas sur AI Hub.

Ça soulève une question stratégique fondamentale pour Kazeia :

Tu as deux chemins possibles maintenant. Le premier serait de fine-tuner un modèle déjà disponible sur AI Hub plutôt que de partir de zéro. Qualcomm a pré-compilé Qwen3-4B, Llama 3.2 3B, et d'autres. Si tu fine-tunes un Qwen3-4B en thérapeutique et que Qualcomm le recompile (via AI Hub Workbench, leur service cloud), tu retombes sur le même chemin qui fonctionne. Le problème c'est la dépendance à Qualcomm pour chaque itération de ton modèle.

Le deuxième chemin serait de faire fonctionner ExecuTorch + QNN en résolvant les problèmes de toolchain. Le rapport dit que ça a échoué à cause de GCC 15 et Python 3.14 — ce sont des versions trop récentes. ExecuTorch est vérifié avec Python 3.10 et GCC 11-13. Ta session Claude Code pourrait retenter avec un environnement contrôlé (Docker Ubuntu 22.04 avec les bonnes versions). C'est le chemin qui te donne l'autonomie complète pour exporter tes propres modèles fine-tunés.

Ce que je recommande :

Court terme, utilise le Qwen3-4B pré-compilé qui fonctionne déjà à 20 tok/s sur le NPU. C'est un modèle Qwen3 standard, pas fine-tuné pour la thérapie, mais il est multilingue (français inclus) et tu peux compenser avec un prompt système bien conçu et le RAG. C'est ton MVP — un chatbot empathique qui tourne sur le NPU, avec le prompt optimisé qu'on a défini, les stopping criteria, et le KV cache. Ça te permet de tester tout le reste du pipeline (VAD, STT, TTS, interface) pendant que tu travailles en parallèle sur le fine-tuning.

En parallèle, fais retenter ExecuTorch + QNN à ta session Claude Code dans un container Docker Ubuntu 22.04 avec Python 3.10 et GCC 11. C'est l'environnement exact documenté par ExecuTorch. Si ça fonctionne, tu auras la capacité d'exporter ton propre Qwen3 fine-tuné directement en .pte pour le NPU, sans dépendre de Qualcomm AI Hub.

Et il y a aussi une troisième option intermédiaire intéressante : le rapport mentionne le transformer-composer de Qualcomm. Vérifie si tu peux soumettre ton modèle fine-tuné directement à AI Hub Workbench pour compilation. C'est un service cloud gratuit qui compile ton modèle PyTorch en context binaries QNN. Si ça marche, tu n'as besoin d'ExecuTorch en local que pour le développement — la compilation production passe par le cloud Qualcomm.

L'autre point important du rapport : l'écart avec les benchmarks officiels.

Qualcomm annonce 29 tok/s, vous mesurez 20 tok/s. Le rapport suggère d'optimiser la config Genie (perf_profile, cpu-mask). C'est 30% de marge récupérable. Dis à ta session Claude Code d'essayer "perf_profile": "burst" et "cpu-mask": "performance" dans le genie_config.json. Ça force les cœurs performance à assister le NPU pour le scheduling et devrait rapprocher des 29 tok/s.

Tu veux qu'on prépare le prompt système optimisé pour le Qwen3-4B standard (non fine-tuné) pour commencer à tester les interactions thérapeutiques dès maintenant, ou qu'on se concentre d'abord sur le plan pour débloquer ExecuTorch ?


You asked:

quelles sont les différences entre le Génie SDK et ExecuTorch + QNN

Mar 25


Claude Replied:

Orchestrated structured comparison of two SDK architectures

Orchestrated structured comparison of two SDK architectures

Très bonne question — c'est le choix architectural clé après ces benchmarks. Les deux passent par le même NPU Hexagon, mais l'approche est radicalement différente.

En une phrase : Genie SDK est un produit fini verrouillé par Qualcomm, ExecuTorch + QNN est une boîte à outils ouverte avec laquelle tu construis toi-même.

Sur le pipeline de conversion du modèle :

Avec Genie SDK, le chemin est : soit tu prends un modèle pré-compilé depuis Qualcomm AI Hub (c'est ce qui a fonctionné dans tes benchmarks), soit tu soumets ton modèle au cloud AI Hub Workbench qui le compile pour toi. Dans les deux cas, tu reçois des context binaries — des blobs binaires opaques compilés spécifiquement pour ton chipset (SM8750). Tu ne peux pas les modifier, les inspecter, ni les recompiler toi-même en local. Chaque itération de ton modèle (nouveau fine-tuning, changement d'architecture) nécessite une recompilation via les outils Qualcomm.

Avec ExecuTorch + QNN, tu fais tout en local sur ta machine. Tu pars de ton modèle PyTorch, tu appelles torch.export(), tu appliques la quantization avec torchao, tu partitionnes avec le QnnPartitioner, et tu obtiens un fichier .pte. Tout est reproductible, scriptable, et tu peux itérer en boucle sans dépendance cloud. Le .pte contient à la fois les parties NPU (déléguées à QNN) et les parties fallback CPU — c'est un seul fichier autosuffisant.

Sur les modèles supportés :

C'est la différence la plus impactante pour Kazeia. Genie SDK ne supporte nativement que les architectures de type LLaMA pour la conversion automatique. Ton benchmark le confirme : la tentative de convertir Qwen 2.5 3B manuellement a échoué car QnnGenAiTransformer ne supporte que LLaMA. Les modèles Qwen3 qui fonctionnent sur AI Hub ont été compilés par Qualcomm eux-mêmes avec des outils internes que tu n'as pas. Si demain tu fine-tunes un Qwen3-4B sur des données thérapeutiques françaises, tu ne pourras pas le recompiler via Genie SDK seul — il te faudra le soumettre à AI Hub Workbench et espérer qu'il soit accepté et compilé correctement.

ExecuTorch supporte toute architecture PyTorch exportable via torch.export(). Qwen3 est explicitement listé parmi les modèles supportés dans la dernière release. Tu fine-tunes ton Qwen3, tu l'exportes, tu le quantizes, tu le déploies. Pas de dépendance à un catalogue pré-compilé.

Sur le runtime Android :

Genie SDK fournit un runner C++ (genie-t2t-run) que tu appelles via ligne de commande ou que tu intègres via JNI. C'est un exécutable autonome qui gère le chargement des context binaries, le KV cache, la tokenization, et la génération. C'est simple à utiliser mais opaque — tu ne contrôles pas la gestion mémoire interne, le scheduling des tokens, ni le streaming granulaire. Le rapport montre que l'initialisation prend ~10 secondes pour charger 4 context binaries.

ExecuTorch fournit une bibliothèque AAR Maven pour Android avec des APIs Java/Kotlin. Tu as un contrôle fin sur chaque étape : chargement du modèle (avec mmap), gestion du KV cache, callback token par token pour le streaming, gestion mémoire explicite. Tu peux implémenter le streaming phrase par phrase vers le TTS, les stopping criteria custom, la température adaptative — tout ce qu'on a décrit dans l'architecture Kazeia. Avec Genie, tu es plus limité dans la finesse du contrôle.

Sur la performance :

Les deux utilisent le même NPU Hexagon via la même couche QNN. En théorie, les performances brutes devraient être identiques. En pratique, Qualcomm optimise ses context binaries avec des techniques internes (fusion de couches, scheduling optimisé) qui peuvent donner un léger avantage à Genie. Le benchmark Qualcomm officiel de 29 tok/s pour Qwen3-4B est probablement mesuré avec Genie et une config optimale. ExecuTorch pourrait être légèrement en dessous car le partitioning automatique est moins agressif que la compilation manuelle de Qualcomm. L'écart est typiquement de 10-20%.

Sur la maturité et la stabilité :

Ton benchmark le montre clairement. Genie SDK avec des modèles pré-compilés : ça fonctionne du premier coup (enfin, au 4ème essai, mais le succès est reproductible). ExecuTorch + QNN : a échoué à cause de problèmes de toolchain (GCC 15, Python 3.14). ExecuTorch est en v1.0-1.1, c'est encore jeune. Le QNN delegate est activement développé mais les incompatibilités de versions sont fréquentes. Le rapport est très représentatif de l'expérience réelle des développeurs avec ExecuTorch/QNN aujourd'hui.

Sur l'autonomie à long terme :

C'est le point décisif. Avec Genie SDK tu dépends de Qualcomm pour chaque modèle, chaque mise à jour, chaque nouveau chipset. Si Qualcomm ne pré-compile pas ton modèle fine-tuné, tu es bloqué. Si Qualcomm arrête de supporter le SM8750 dans AI Hub, tu perds la capacité de recompiler. Avec ExecuTorch tu es souverain — tu peux recompiler tes modèles pour n'importe quel chipset supporté, à n'importe quel moment, sans demander la permission à personne.

Voici le résumé :

                        Genie SDK              ExecuTorch + QNN
────────────────────────────────────────────────────────────────
Modèles supportés       Catalogue AI Hub       Tout PyTorch exportable
Modèle custom fine-tuné Via AI Hub Workbench   En local, autonome
Format de sortie        Context binaries       .pte (portable)
                        (opaques, verrouillés) (inspectable)
Pipeline de conversion  Cloud Qualcomm         Local, scriptable
Contrôle runtime        Limité (runner C++)    Fin (APIs Java/Kotlin)
Streaming token/token   Basique                Complet avec callbacks
Performance NPU         Optimale (29 tok/s)    Légèrement inférieure
                                               (~20-25 tok/s estimé)
Stabilité aujourd'hui   Bonne (si pré-compilé) Fragile (toolchain)
Autonomie               Dépendant Qualcomm     Indépendant
Setup initial           Simple                 Complexe
────────────────────────────────────────────────────────────────

Ma recommandation pour Kazeia :

Utilise les deux, dans cet ordre. Maintenant, pour le MVP, exploite le Qwen3-4B pré-compilé via Genie SDK — ça marche, c'est validé, 20 tok/s sur le NPU. En parallèle, fais résoudre le setup ExecuTorch + QNN dans un environnement propre (Docker Ubuntu 22.04, Python 3.10, GCC 11). Quand ExecuTorch fonctionne, tu auras la capacité d'exporter ton Qwen3 fine-tuné thérapeutique directement. À terme, ExecuTorch est le bon choix pour Kazeia parce que l'autonomie sur les modèles custom est non-négociable pour un dispositif médical. Mais en attendant, Genie te permet d'avancer sur tout le reste du projet sans être bloqué.

Tu veux qu'on avance sur l'intégration du Genie runner dans l'application Android Kotlin, ou qu'on prépare le plan pour débloquer ExecuTorch ?


Claude Replied:

BENCHMARK_RAPPORT.md

336 lines

md

Mar 25


Claude Replied:

I cannot provide a summary because the user prompt is empty. Please provide a user prompt so I can determine the primary language and create an appropriate summary

I cannot provide a summary because the user prompt is empty. Please provide a user prompt so I can determine the primary language and create an appropriate summary

Ce deuxième rapport est une mine d'or — la session Claude Code a fait un travail remarquable. Et les résultats changent significativement la donne. Voici mon analyse.

La grande nouvelle : ExecuTorch + QNN fonctionne.

Le pipeline complet a été validé : export PyTorch → quantification INT4 → compilation QNN HTP v79 → sérialisation .pte → déploiement sur la tablette → inférence sur le NPU. Qwen3-0.6B tourne à 69.3 tok/s sur le NPU via ExecuTorch. C'est une excellente nouvelle parce que ça prouve que le chemin autonome (sans dépendance aux modèles pré-compilés AI Hub) est viable. Vous pouvez exporter vos propres modèles fine-tunés.

Mais il y a un goulot d'étranglement critique : la RAM de compilation.

C'est le résultat le plus important du rapport et celui qu'on n'avait pas anticipé :

Qwen3-0.6B → 17-20 Go RAM  → OK sur votre PC (54 Go)
Qwen3-1.7B → 48-64 Go RAM  → ÉCHEC (OOM sur 54 Go)
Qwen3-4B   → 113-130 Go RAM → Nécessite serveur cloud

Ça signifie que votre PC de développement (54 Go) ne peut exporter que des modèles jusqu'à environ 1B paramètres via ExecuTorch. Pour le Qwen3-4B qui tourne déjà via Genie SDK, l'export ExecuTorch nécessiterait un serveur avec 128+ Go de RAM. C'est faisable (une instance cloud c2-standard-60 sur GCP avec 240 Go de RAM coûte environ 2-3€/heure, l'export prendrait quelques heures), mais c'est une contrainte opérationnelle à intégrer dans votre workflow.

L'autre découverte majeure : sur les petits modèles, NPU ≈ CPU.

Qwen3-0.6B fait 69.3 tok/s sur le NPU et 68.2 tok/s sur le CPU. Quasi identique. C'est parce que les deux sont limités par la bande passante mémoire (77 Go/s LPDDR5X), pas par la puissance de calcul. Un modèle de 600 Mo tient presque entièrement dans les caches, donc ni le NPU ni le CPU ne sont contraints par le compute — ils attendent tous les deux la mémoire. L'avantage du NPU ne se manifeste vraiment qu'avec les modèles plus gros (4B : +27%) et surtout sur l'efficacité énergétique et la stabilité thermique.

Ce que ça implique pour la stratégie Kazeia :

La question du choix de modèle est maintenant liée à la question du framework :

Qwen3-0.6B (fine-tuné Kazeia)
├── Export ExecuTorch : faisable sur votre PC (54 Go)
├── Performance : 69 tok/s (NPU ou CPU, peu importe)
├── RAM tablette : 694 Mo
├── Qualité empathique : limitée (très petit modèle)
└── Avantage : autonomie complète, itération rapide

Qwen3-4B (pré-compilé AI Hub ou fine-tuné)  
├── Export ExecuTorch : nécessite serveur 128+ Go
├── Via Genie pré-compilé : fonctionne immédiatement
├── Performance : ~20 tok/s (NPU uniquement)
├── RAM tablette : 344 Mo (Genie) — étonnamment peu
├── Qualité empathique : bien meilleure
└── Contrainte : dépendance AI Hub OU serveur cloud

Qwen3-1.7B (compromis potentiel)
├── Export ExecuTorch : nécessite ~64 Go RAM (juste au-dessus)
├── Pas de modèle pré-compilé AI Hub
├── Performance estimée : ~35-45 tok/s (NPU)
├── Qualité empathique : intermédiaire
└── Option : upgrader le PC à 64-128 Go ou serveur cloud

Ma recommandation mise à jour :

L'approche la plus pragmatique est un développement en deux voies parallèles :

Pour le MVP immédiat, utilise le Qwen3-4B pré-compilé via Genie SDK à 20 tok/s. C'est déjà sur la tablette, ça fonctionne, la qualité du modèle 4B en français est suffisante pour tester tout le reste du pipeline (prompt système, RAG, stopping criteria, intégration Android). Tu ne perds pas de temps.

Pour le fine-tuning thérapeutique, fine-tune un Qwen3-0.6B sur tes données empathiques françaises et exporte-le via ExecuTorch directement depuis votre PC. Ça tourne à 69 tok/s et ça te donne un cycle d'itération rapide : fine-tune → export → test sur tablette → ajuste → recommence. La question est de savoir si un 0.6B fine-tuné spécifiquement pour la thérapie peut rivaliser en qualité avec un 4B généraliste. C'est exactement le test à faire — et la littérature sur la distillation de connaissances montre que c'est souvent le cas pour un domaine étroit.

Quand le modèle fine-tuné est validé et que tu veux passer en production avec un modèle plus gros, loue quelques heures de serveur cloud pour compiler le Qwen3-4B fine-tuné via ExecuTorch. C'est un coût ponctuel de quelques euros à chaque nouvelle version du modèle.

Les prochaines actions concrètes, dans l'ordre :

Premièrement, configurer le stop token et le system prompt Kazeia sur le Qwen3-4B Genie qui tourne déjà. C'est quelques lignes dans le genie_config.json et ça te donne immédiatement un prototype conversationnel testable.

Deuxièmement, tester l'optimisation Genie (perf_profile, cpu-mask) pour récupérer les 30% de marge entre vos 20 tok/s et les 29 tok/s de Qualcomm.

Troisièmement, commencer le fine-tuning d'un Qwen3-0.6B thérapeutique sur votre PC, avec le corpus synthétique qu'on a défini (distillation depuis un gros modèle, curriculum learning, DPO). Exporter via ExecuTorch et comparer la qualité empathique avec le 4B généraliste.

Tu veux qu'on commence par rédiger le prompt système optimisé pour le Qwen3-4B Genie, ou qu'on prépare le plan de fine-tuning du Qwen3-0.6B ?


You asked:

nous allons utiliser chatterbox pour le tts, il y a un modele whisper préparé par qualcomm pour le stt, maintenant je me demande comment se passe l'implémentation de vAD pour detecter automatiquement la voix sans avoir à faire de manipulation

Mar 26


Claude Replied:

Investigated VAD integration for hands-free voice detection pipeline

Investigated VAD integration for hands-free voice detection pipeline

Parfait — Chatterbox pour le TTS et Whisper Qualcomm pour le STT, bons choix. Le VAD est la pièce qui relie tout. Laisse-moi vérifier les options d'implémentation concrètes sur Android.

Orchestrated practical VAD implementation guidance for hands-free voice detection

Orchestrated practical VAD implementation guidance for hands-free voice detection

Le VAD est en fait le composant le plus simple à intégrer de tout le pipeline Kazeia. Voici concrètement comment ça fonctionne.

Le principe : le micro est toujours ouvert, mais Whisper ne se déclenche que quand il y a de la parole.

Sans VAD, tu aurais deux options : soit le patient appuie sur un bouton pour parler (fastidieux, pas naturel), soit tu envoies en permanence le flux audio au STT Whisper (gaspillage de ressources NPU/CPU, et Whisper hallucine sur le silence — il génère des phrases fantômes comme "Merci d'avoir écouté" quand il n'y a que du bruit). C'est d'ailleurs exactement pour cette raison que SileroVAD a été intégré dans faster-whisper : pour filtrer les segments silencieux avant la transcription Medium.

Le VAD résout ça. C'est un réseau de neurones léger basé sur ONNX Runtime Mobile, qui traite des chunks audio de quelques millisecondes et retourne une probabilité que le chunk contienne de la parole GitHub. Le modèle fait 1.8 Mo et traite un chunk de 30ms en environ 1ms Medium. C'est négligeable en termes de ressources.

L'intégration Android est déjà prête. Il existe une bibliothèque Android native en Kotlin (android-vad) qui supporte Silero VAD, WebRTC VAD et YAMNet VAD GitHub. Le projet est entièrement en Kotlin, utilise ONNX Runtime, et est compatible avec les tablettes Android 15+ avec le support des pages 16 Ko GitHub. Tu l'ajoutes avec une seule ligne dans ton build.gradle :

gradle

implementation 'com.github.gkonovalov.android-vad:silero:1.0.2'

Le flux complet sans aucun bouton :

┌─────────────────────────────────────────────────────┐
│  AudioRecord Android (micro toujours ouvert)         │
│  Format: 16-bit Mono PCM, 16 kHz                    │
│  Tourne dans un thread dédié du Foreground Service   │
└──────────────┬──────────────────────────────────────┘
               │ chunks de 512 samples (32ms)
               ▼
┌─────────────────────────────────────────────────────┐
│  Silero VAD (ONNX Runtime, 1.8 Mo, <1ms/chunk)      │
│                                                      │
│  Chaque chunk → probabilité de parole (0.0 à 1.0)   │
│                                                      │
│  Trois événements possibles :                        │
│  ├── Silence continu → on ne fait rien               │
│  ├── Début de parole détecté → on commence à         │
│  │   accumuler l'audio dans un buffer                │
│  └── Fin de parole détectée (silence > 500ms après   │
│      de la parole) → on envoie le buffer à Whisper   │
└──────────────┬──────────────────────────────────────┘
               │ buffer audio complet d'une phrase
               ▼
┌─────────────────────────────────────────────────────┐
│  Whisper STT (Qualcomm, NPU)                         │
│  Transcrit le segment de parole → texte              │
└──────────────┬──────────────────────────────────────┘
               │ texte
               ▼
         Pipeline LLM Qwen3 → TTS Chatterbox → Avatar

Les paramètres clés de Silero VAD pour un contexte thérapeutique :

Tu configures silenceDurationMs (durée minimale de silence pour considérer que la parole est terminée) et speechDurationMs (durée minimale de parole pour déclencher la détection) GitHub. Pour Kazeia, les valeurs optimales seraient différentes d'un assistant vocal classique :

Le speechDurationMs devrait être à 150-200ms (au lieu des 50ms par défaut). En contexte thérapeutique, tu veux éviter de déclencher sur un soupir, un raclement de gorge, ou un bruit accidentel. 150ms garantit qu'il y a une vraie intention de parler.

Le silenceDurationMs devrait être à 800-1000ms (au lieu des 300ms par défaut). Un patient en détresse fait des pauses longues entre les mots — il hésite, il cherche ses mots, il pleure entre deux phrases. Avec 300ms, le VAD découperait sa phrase en morceaux. Avec 800-1000ms, tu laisses le temps au patient de finir sa pensée complète avant de transcrire.

Le mode devrait être NORMAL plutôt que AGGRESSIVE. Le mode agressif a un seuil de détection plus haut et risquerait de manquer une voix faible ou chuchotée — exactement le type de voix qu'un patient déprimé pourrait avoir.

La gestion de l'echo cancellation — le point critique :

Quand Chatterbox joue la réponse TTS via le haut-parleur, le micro capte cette voix et le VAD la détecte comme de la parole. Sans protection, ça crée une boucle. Il y a trois niveaux de solution :

Le plus simple : muter le VAD pendant la lecture TTS. Quand le TTS commence à parler, tu désactives le traitement VAD. Quand le TTS finit, tu réactives. Inconvénient : le patient ne peut pas interrompre l'avatar.

Mieux : utiliser le mode AudioManager.MODE_IN_COMMUNICATION d'Android qui active l'AEC (Acoustic Echo Cancellation) matériel du Snapdragon. Le micro filtre automatiquement le son qui sort du haut-parleur. Ça permet au patient d'interrompre l'avatar naturellement — exactement comme dans une vraie conversation thérapeutique.

Encore mieux : combiner les deux. AEC matériel activé en permanence pour filtrer l'écho, plus un flag booléen isAvatarSpeaking qui augmente le seuil de détection du VAD pendant la lecture TTS (passer de 0.5 à 0.8 par exemple). Comme ça, seule une voix forte et claire du patient (une vraie interruption intentionnelle) est détectée pendant que l'avatar parle, mais les résidus d'écho qui passeraient l'AEC sont ignorés.

L'implémentation concrète en Kotlin serait :

kotlin

class VoiceDetectionManager(
    private val context: Context,
    private val onSpeechSegment: (ShortArray) -> Unit  // callback avec l'audio
) {
    private val vad = VadSilero(
        context = context,
        sampleRate = SampleRate.SAMPLE_RATE_16K,
        frameSize = FrameSize.FRAME_SIZE_512,
        mode = Mode.NORMAL,
        silenceDurationMs = 800,   // pauses longues tolérées
        speechDurationMs = 150     // évite les faux positifs
    )
    
    private val audioRecord = AudioRecord(
        MediaRecorder.AudioSource.VOICE_COMMUNICATION,  // AEC activé
        16000, CHANNEL_IN_MONO, ENCODING_PCM_16BIT,
        16000 * 2  // 1 seconde de buffer
    )
    
    private var isListening = true
    private var isAvatarSpeaking = false
    private val speechBuffer = mutableListOf<ShortArray>()
    private var isSpeechActive = false
    
    fun startListening() {
        audioRecord.startRecording()
        
        thread(name = "VAD-Listener") {
            val frame = ShortArray(512)
            
            while (isListening) {
                audioRecord.read(frame, 0, 512)
                
                // Pendant que l'avatar parle, on augmente le seuil
                val threshold = if (isAvatarSpeaking) 0.85 else 0.5
                val isSpeech = vad.isSpeech(frame)
                
                if (isSpeech) {
                    if (!isSpeechActive) {
                        isSpeechActive = true
                        speechBuffer.clear()
                    }
                    speechBuffer.add(frame.copyOf())
                } else if (isSpeechActive) {
                    // Le VAD a détecté la fin de parole
                    // (silence > 800ms après de la parole)
                    isSpeechActive = false
                    
                    // Concaténer tout le buffer et l'envoyer
                    val fullAudio = speechBuffer.flatten().toShortArray()
                    onSpeechSegment(fullAudio)
                    speechBuffer.clear()
                }
            }
        }
    }
    
    fun onAvatarStartSpeaking() { isAvatarSpeaking = true }
    fun onAvatarStopSpeaking() { isAvatarSpeaking = false }
    
    fun stop() {
        isListening = false
        audioRecord.stop()
        vad.close()
    }
}

Et dans ton Foreground Service Kazeia, l'intégration serait :

kotlin

val voiceDetector = VoiceDetectionManager(context) { audioSegment ->
    // Le VAD a détecté une phrase complète
    serviceScope.launch {
        // 1. Transcrire via Whisper NPU
        val text = whisperEngine.transcribe(audioSegment)
        
        // 2. Envoyer au pipeline LLM
        processMessage(currentPatientId, text, onToken, onComplete)
    }
}

voiceDetector.startListening()

C'est tout. Le patient parle, le VAD découpe, Whisper transcrit, le LLM répond, Chatterbox synthétise, l'avatar parle. Zéro bouton, interaction naturelle.

Le coût en ressources est dérisoire :

Silero VAD :     1.8 Mo modèle, <1ms par frame, 1 thread CPU efficacité
AudioRecord :    ~32 Ko buffer circulaire
Buffer parole :  ~100-500 Ko par segment (5-30 secondes de parole)
────────────────────────────────────────────────────────────
Impact total :   ~2 Mo RAM, ~0.1% d'un cœur CPU

C'est invisible par rapport au reste du pipeline. Tu veux que je prépare les instructions pour ta session Claude Code pour intégrer ça dans le projet Android, ou qu'on avance sur un autre composant ?


You asked:

vu que nous aurions très certainement un avatar 3d fait avec unity, qui aura du lipsync avec notre tts, quel serait le meilleur language pour le developpement de l'application

Mar 26


Claude Replied:

Pesé arquitectura entre Unity nativo o Android como aplicación principal

Pesé arquitectura entre Unity nativo o Android como aplicación principal

C'est LA question architecturale qui va déterminer tout le reste du développement. Et la réponse n'est pas évidente parce que tu as deux mondes qui doivent cohabiter : Unity (C#) pour l'avatar et Android natif (Kotlin/C++) pour toute l'inférence IA.

Tu as concrètement trois options.

Option 1 : Unity est l'application principale, tout en C#.

Unity gère l'intégralité de l'app — l'avatar 3D, l'interface patient, et tous les composants IA sont appelés depuis C# via des plugins natifs. Le Genie SDK, Whisper, Silero VAD, Chatterbox, SQLite — tout est wrappé dans des plugins Unity natifs (.so) appelés via P/Invoke ou des wrappers Java/JNI bridgés vers C#.

L'avantage c'est un seul projet, un seul langage, une seule build pipeline. L'avatar et la logique métier vivent au même endroit, la communication entre le lip-sync et le TTS est directe, pas d'IPC.

Le problème c'est que les Foreground Services Android, les permissions audio, l'AudioRecord pour le VAD, la gestion du cycle de vie Android — tout ça est natif Android et s'intègre mal depuis Unity. Tu te retrouves à écrire des plugins Java/Kotlin pour chaque interaction Android, puis des bridges C# pour les appeler. C'est une couche d'abstraction supplémentaire sur chaque composant. Et le debug devient pénible : quand le Genie SDK crashe dans le code natif, le stack trace remonte à travers JNI → Java → Unity bridge → C#, et c'est l'enfer à diagnostiquer.

Option 2 : Android natif (Kotlin) est l'application principale, Unity est embarqué comme vue.

L'app Kotlin gère tout : le Foreground Service avec les modèles en mémoire, le VAD, le STT, le LLM, le TTS, l'authentification, SQLite, la synchronisation serveur. Unity est intégré comme un UnityPlayer dans un Fragment ou une Activity dédiée, exclusivement pour le rendu de l'avatar 3D et le lip-sync. La communication entre Kotlin et Unity se fait via UnityPlayer.UnitySendMessage() (Kotlin → Unity) et des interfaces Android (AndroidJavaObject dans Unity → Kotlin).

L'avantage c'est que toute la logique IA tourne dans l'écosystème natif où elle est conçue pour tourner. Le Genie SDK est appelé directement en C/C++ via JNI sans couche Unity intermédiaire. Les Services Android, les permissions, l'audio — tout est en Kotlin natif, propre et documenté. Le debug est standard Android.

Le problème c'est la communication Unity ↔ Kotlin. Pour le lip-sync, tu dois streamer les données audio ou les visèmes de Kotlin vers Unity en temps réel. UnitySendMessage est asynchrone et ne passe que des strings — pas idéal pour du streaming audio. Pour des données binaires haute fréquence (visèmes à 30-60 fps), il faut un shared memory buffer ou un bridge JNI plus bas niveau. C'est faisable mais c'est du travail d'ingénierie.

Option 3 : Unity est l'application principale, Kotlin gère les services en arrière-plan.

C'est un hybride. L'Activity principale est Unity (avatar, interface, lip-sync). Mais tu crées un Foreground Service séparé en Kotlin pur qui gère tout le pipeline IA (LLM, STT, TTS, VAD). Les deux communiquent via un Binder local ou un Messenger. Unity envoie "le patient a dit X" au Service, le Service traite et renvoie "réponse = Y, audio = Z, émotion = W". Unity anime l'avatar en conséquence.

L'avantage c'est que chaque composant est dans son environnement optimal. Unity fait ce qu'il fait le mieux (rendu 3D, animation), Kotlin fait ce qu'il fait le mieux (services Android, inférence IA native). Le Service survit indépendamment de l'Activity Unity — si Unity redémarre, les modèles restent en mémoire.

Le problème c'est la complexité de l'architecture avec deux "cerveaux" qui doivent se synchroniser. Et la latence de communication inter-process si tu utilises un Service bound avec AIDL.

Ma recommandation pour Kazeia : Option 3, Unity principal + Service Kotlin.

Voici pourquoi. Pour un chatbot thérapeutique avec avatar, l'expérience visuelle EST l'application. Le patient voit l'avatar, interagit avec lui, reçoit les réponses visuellement et auditivement. Unity doit être le premier plan. Mais tout le pipeline IA (qui représente 80% de la complexité technique) n'a rien à faire dans Unity — c'est du code natif Android qui s'exécute en arrière-plan.

L'architecture serait :

┌─────────────────────────────────────────────────────┐
│  Unity Activity (C#)                                 │
│  ├── Avatar 3D (rendu, animation, lip-sync)          │
│  ├── Interface patient (chat texte, indicateurs)     │
│  ├── uLipSync (audio → blendshapes bouche)           │
│  ├── Expressions émotionnelles (blendshapes visage)  │
│  └── Communique avec le Service via AndroidJava      │
├─────────────────────────────────────────────────────┤
│  KazeiaService (Kotlin, Foreground Service)           │
│  ├── VoiceDetectionManager (Silero VAD + AudioRecord)│
│  ├── WhisperEngine (Qualcomm NPU)                    │
│  ├── LlmEngine (Genie SDK / ExecuTorch, NPU)         │
│  ├── TtsEngine (Chatterbox)                          │
│  ├── EmotionClassifier (ONNX Runtime)                │
│  ├── RagManager (SQLite + sqlite-vec + embeddings)   │
│  ├── ConversationManager (machine à états, PHQ-9)    │
│  └── SessionManager (auth, profils, sync serveur)    │
└─────────────────────────────────────────────────────┘

La communication entre les deux serait un Binder local (in-process, pas d'IPC). Unity et le Service tournent dans le même processus Android, donc le Binder est un simple appel de méthode Java — zéro overhead de sérialisation. Côté Unity tu appelles :

csharp

// Unity (C#) → Kotlin Service
AndroidJavaClass serviceClass = new AndroidJavaClass("com.kazeia.service.KazeiaBridge");
serviceClass.CallStatic("sendPatientMessage", messageText);

// Kotlin Service → Unity (C#) : callback pour chaque token streamé
// Le Service appelle UnitySendMessage depuis Kotlin :
UnityPlayer.UnitySendMessage("KazeiaController", "OnLlmToken", token);
UnityPlayer.UnitySendMessage("KazeiaController", "OnEmotionDetected", "tristesse:0.82");

Pour le lip-sync spécifiquement, c'est plus efficace de ne pas passer l'audio complet de Unity au Service. Chatterbox génère l'audio dans le Service Kotlin, le Service joue l'audio via un AudioTrack partagé, et Unity reçoit soit les visèmes pré-calculés, soit l'audio brut via un shared buffer pour que uLipSync fasse son analyse en temps réel côté Unity.

Le langage final par composant :

Composant                    Langage      Pourquoi
─────────────────────────────────────────────────────
Avatar 3D + lip-sync        C# (Unity)    rendu, animation
Interface patient            C# (Unity)    UI intégrée à la scène 3D
Pipeline IA (Service)        Kotlin        APIs Android natives
Genie SDK / ExecuTorch       C/C++ (JNI)   performance NPU
Silero VAD                   Kotlin/ONNX   bibliothèque Android native
Whisper STT                  C/C++ (JNI)   binaire Qualcomm natif
Chatterbox TTS               Kotlin/C++    selon l'implémentation
SQLite + sqlite-vec          Kotlin        SDK Android natif
Logique métier (PHQ-9, etc)  Kotlin        portage du Python existant

Tu codes principalement en Kotlin (70% du code) et en C# (30% du code). Le C/C++ existe déjà dans les bibliothèques tierces, tu n'as pas à en écrire.

Tu veux qu'on détaille l'architecture de communication Unity ↔ Service Kotlin pour le streaming LLM + lip-sync, ou qu'on avance sur un autre aspect ?


You asked:

dans un premier temps nous n'allons pas avoir l'avatar on se bornera a du texte, mais avec le pipeline sTT et tts

Mar 26


Claude Replied:

Envisagé architecture simplifiée sans avatar, pipeline audio complet

Envisagé architecture simplifiée sans avatar, pipeline audio complet

Parfait — ça simplifie énormément. Sans Unity, c'est du 100% Kotlin natif avec les bibliothèques C/C++ appelées via JNI. Un seul projet Android Studio, un seul langage, un seul build. C'est exactement le bon MVP pour valider le cœur de Kazeia.

L'application serait une interface texte simple — comme un chat — mais le patient peut parler (VAD + Whisper) et entendre la réponse (Chatterbox). Le texte de la conversation s'affiche à l'écran dans les deux sens, que le patient tape ou parle.

L'architecture se résume à :

┌─────────────────────────────────────────────────┐
│  KazeiaActivity (Kotlin)                         │
│  ├── RecyclerView (messages chat)                │
│  ├── EditText + bouton envoyer (input texte)     │
│  ├── Indicateur micro (écoute VAD active)        │
│  └── Indicateur état (écoute / réfléchit / parle)│
├─────────────────────────────────────────────────┤
│  KazeiaService (Kotlin, Foreground Service)       │
│  ├── VoiceDetectionManager (Silero VAD)           │
│  │   └── AudioRecord 16kHz mono, toujours actif   │
│  ├── WhisperEngine (Qualcomm NPU)                 │
│  ├── LlmEngine (Genie SDK, Qwen3-4B NPU)         │
│  ├── TtsEngine (Chatterbox)                       │
│  ├── ConversationManager (états, PHQ-9)           │
│  └── SQLite (profils, historique, RAG)            │
└─────────────────────────────────────────────────┘

Le flux complet d'une interaction serait : le patient parle → le micro capte en continu → Silero VAD détecte la parole → à la fin de la phrase (silence 800ms), le buffer audio part à Whisper → Whisper transcrit → le texte s'affiche dans le chat côté patient → le texte part au pipeline LLM (émotion + RAG + Qwen3) → les tokens arrivent en streaming et s'affichent dans le chat côté Kazeia → en parallèle, chaque phrase complète part à Chatterbox pour la synthèse vocale → l'audio est joué via le haut-parleur. Le patient peut aussi taper du texte directement, auquel cas on saute l'étape VAD + Whisper et on envoie directement au LLM.

L'interface afficherait trois états visuels simples : un indicateur "écoute" quand le VAD est actif et attend de la parole, un indicateur "réfléchit" quand le LLM génère, et un indicateur "parle" quand Chatterbox joue l'audio. C'est minimal mais suffisant pour que le patient comprenne ce qui se passe.

C'est le projet idéal pour ta session Claude Code — un projet Android Studio Kotlin avec Gradle, les dépendances natives (Genie SDK, Silero VAD, Chatterbox), et un déploiement direct sur la tablette OnePlus Pad 3 via ADB.

Tu veux que je prépare la structure complète du projet (arborescence, dépendances Gradle, classes principales) pour que ta session Claude Code puisse démarrer l'implémentation ?


You asked:

pour le moment nous allons démarrer sur un modèle plus simple avec un simple modèle sans rag, mais avec stt, tts et vad, une interface simple, avec l'ecriture des phrases

Mar 26


Claude Replied:

Orchestrated minimal stack architecture pour démarrage simplifié

Orchestrated minimal stack architecture pour démarrage simplifié

Parfait, on va au plus simple. LLM + STT + TTS + VAD + interface chat. Pas de RAG, pas de classifieur d'émotion, pas de PHQ-9. On valide que le pipeline vocal fonctionne de bout en bout sur la tablette.

Tu veux que je te prépare la structure du projet Android Studio avec l'arborescence des fichiers, les dépendances Gradle, et les classes principales pour que ta session Claude Code puisse démarrer directement l'implémentation ?


You asked:

ce qui serait bon c'est d'avoir une structure modulaire pour plugger facilement une structure autre à la place du llm par la suite, nous devons préparer tout ce qui se passe autour

Mar 26


Claude Replied:

Architected modular design pour swapper facilement le composant LLM

Architected modular design pour swapper facilement le composant LLM

Exactement la bonne approche — construire le chassis avec des interfaces propres, le moteur LLM se branche dessus. Laisse-moi te préparer ça.

Orchestrated modular architecture pour flexibilité future

Orchestrated modular architecture pour flexibilité future

Voici l'architecture complète. Le principe central c'est que chaque composant est une interface Kotlin avec des implémentations interchangeables. Le LLM, le STT, le TTS et le VAD sont tous des contrats — tu peux brancher n'importe quelle implémentation sans toucher au reste.

Les décisions clés :

Le Foreground Service est le cœur. C'est lui qui garde tous les modèles en mémoire et orchestre le pipeline VAD → STT → LLM → TTS. L'Activity (l'interface chat) ne fait qu'observer des StateFlow et afficher les résultats. Si l'Activity est détruite (écran de veille, changement de session), le Service continue de tourner et les modèles restent chauds.

Les interfaces dans core/ sont volontairement minimalistes. LlmEngine a trois méthodes : load(), generate() avec streaming via callback, et release(). Pour passer du Genie SDK à ExecuTorch ou llama.cpp, tu changes une seule ligne d'instanciation dans le Service. Pareil pour le STT (Whisper Qualcomm ↔ SpeechRecognizer natif) et le TTS (Chatterbox ↔ TTS natif Android).

Le pipeline audio est séparé en deux managers : AudioCaptureManager gère le micro et le VAD en continu, AudioPlaybackManager gère la sortie TTS. L'EchoCancellationManager coordonne les deux pour éviter que le VAD ne détecte la voix du TTS.

L'ordre d'implémentation est pensé pour que ta session Claude Code puisse valider chaque brique indépendamment : d'abord l'UI seule, puis le LLM texte, puis le VAD + micro, puis le STT, puis le TTS, et enfin le pipeline complet sans bouton. À chaque étape tu as quelque chose de testable sur la tablette.

Tu peux passer ça directement à ta session Claude Code pour qu'elle démarre l'implémentation. Tu veux qu'on ajuste quelque chose dans l'architecture avant ?

Kazeia architecture

Document · MD 

Download


You asked:

BENCHMARK_RAPPORT.md

md

AI_HUB_QUALCOMM.md

3 lines

md

ARCHITECTURE_PIPELINE.md

194 lines

md

BENCHMARK_ROOT_VS_NONROOT.md

201 lines

md

DEPLOY_EXECUTORCH_NPU.md

139 lines

md

GUIDE_ROOT_ONEPLUS_PAD3.md

304 lines

md

RAPPORT_TTS.md

254 lines

md

Nous avons travaillé avec le developpeur sur l'adaptation à la tablette Oneplus Pad3, voici les fichiers en rapport

Mar 29


Claude Replied:

Analysé progrès significatifs du développement tablet avec ExecuTorch et NPU

Analysé progrès significatifs du développement tablet avec ExecuTorch et NPU

C'est un travail remarquable. La session Claude Code a accompli énormément en quelques jours. Laisse-moi faire le point sur où vous en êtes par rapport à ce qu'on avait planifié.

Les avancées majeures que je retiens :

Premièrement, le root est indispensable pour le NPU. Sans root, pas d'accès au DSP Hexagon, pas de LLM, pas de pipeline IA. C'est une contrainte de déploiement qu'on n'avait pas anticipée — chaque tablette hospitalière devra être rootée. C'est gérable mais ça complexifie la maintenance et les mises à jour OTA.

Deuxièmement, ExecuTorch a finalement gagné sur Genie SDK. Le document d'architecture montre que Genie est marqué "abandonné". C'est logique — ExecuTorch vous donne l'autonomie complète sur vos modèles custom, et les performances sont excellentes. Qwen3-0.6B fait 93 tok/s et Qwen3-1.7B fait 46 tok/s sur le NPU. C'est bien au-dessus de nos estimations initiales.

Troisièmement, et c'est la plus grosse surprise : vous avez réussi l'export du talker Qwen3-TTS sur le NPU. C'est exactement ce qu'on avait discuté comme étant "3-5 semaines de travail avec un risque d'échec" — et la session Claude Code l'a fait. Le talker tourne à 90.7 tok/s sur le NPU, ce qui transforme un pipeline 5× trop lent en CPU en un pipeline quasi temps réel (~1.9s pour 2.3s d'audio). C'est un résultat exceptionnel.

Quatrièmement, l'architecture a évolué vers un pattern processor chain qui est plus flexible que ce qu'on avait prévu. Au lieu d'un pipeline linéaire rigide, les processeurs sont chaînés et le premier qui retourne shouldContinueChain = false termine la chaîne. C'est élégant pour ajouter le RAG, le classifieur d'émotion, la diarisation — chacun est un processeur indépendant qu'on branche dans la chaîne.

Les points qui restent à résoudre :

Le STT Whisper est encore sur CPU à 1500ms. Le document mentionne qu'un qnn_whisper_runner est compilé mais pas encore intégré. C'est le goulot d'étranglement actuel du pipeline — l'estimation est de ~50ms sur NPU, ce qui diviserait la latence totale par presque deux.

Chatterbox ne fonctionne pas de manière fiable — OOM à 1.4 Go et les versions quantifiées ne produisent pas le stop token. Mais avec Qwen3-TTS talker sur le NPU à 90.7 tok/s, Chatterbox n'est plus nécessaire. Le pipeline TTS Qwen3-TTS est supérieur en tout point, avec en plus le clonage vocal.

Le pipeline TTS complet n'est pas encore assemblé sur la tablette. Le talker tourne sur le NPU, mais le code predictor (141M, 5 couches), le speaker encoder (8.9M) et le speech decoder (114M, ConvNet) doivent encore tourner sur CPU. L'orchestration via Termux + Python est un workaround — à terme il faudra intégrer tout ça en natif dans l'app Kotlin.

Le budget mémoire réel, basé sur vos mesures :

Composant                          RAM mesurée
──────────────────────────────────────────────
Qwen3-1.7B LLM (.pte, NPU)        1 712 Mo
Qwen3-TTS talker (.pte, NPU)        286 Mo (+ poids CPU ~500 Mo)
Whisper base (CPU)                   148 Mo
Silero VAD                             2 Mo
Android OS + app                   ~3 500 Mo
──────────────────────────────────────────────
TOTAL estimé                       ~6 150 Mo
MARGE sur 16 Go                    ~9 850 Mo

Largement confortable. Vous pourriez même monter le LLM à Qwen3-4B si la qualité empathique l'exige, au prix d'un export sur serveur cloud (130 Go RAM nécessaires).

Le pipeline complet actuel vs cible :

ACTUEL (fonctionnel) :
VAD (RMS) → Whisper CPU (1500ms) → LLM NPU (46 tok/s) → Android TTS (200ms)
Latence totale : 3-7s

CIBLE (avec les pièces déjà prêtes) :
Silero VAD → Whisper NPU (~50ms) → LLM NPU (46 tok/s) → Qwen3-TTS NPU (~1.9s)
Latence totale estimée : ~2-4s avec clonage vocal

Mes recommandations pour la suite :

La priorité numéro un serait d'intégrer le Whisper NPU qui est déjà compilé. Passer de 1500ms à 50ms c'est 1.4 seconde gagnée sur chaque interaction — c'est le gain le plus facile à récupérer.

La priorité numéro deux serait d'assembler le pipeline TTS Qwen3-TTS complet sur la tablette : talker NPU + code predictor CPU + speaker encoder CPU + speech decoder CPU. Le rapport montre que c'est ~1.9s pour une phrase standard. Si ça fonctionne, vous aurez le clonage vocal en temps réel sur tablette — un différenciateur majeur pour Kazeia.

La priorité numéro trois serait de remplacer le VAD basé sur l'énergie RMS par Silero VAD pour une détection de parole plus robuste en milieu hospitalier (bruit ambiant, voix faibles).

Ensuite viendrait l'intégration native en Kotlin au lieu du subprocess su pour ExecuTorch, le streaming LLM token par token vers l'UI, et le fine-tuning du Qwen3-1.7B sur des données thérapeutiques.

Franchement, le travail accompli est impressionnant. Vous avez un pipeline conversationnel complet sur tablette Android avec NPU en quelques jours. Le cœur technique de Kazeia est validé. Tu veux qu'on se concentre sur une de ces priorités, ou qu'on fasse un document récapitulatif de toute l'architecture validée pour le mémoire ?


You asked:

RAPPORT_TTS.md

254 lines

md

Donc si on se base sur le rapport_TTS, nous sommes bloqué pour le TTS local, il faut qu'on refléchisse comment faire, l'ideal serait quand meme l'utilisation de qwen3-tts

Mar 29


Claude Replied:

Analysé blocages techniques et envisagé solutions d'implémentation locale

Analysé blocages techniques et envisagé solutions d'implémentation locale

Tu as raison, c'est le vrai blocage. Le talker tourne à 90.7 tok/s sur le NPU — c'est résolu. Mais les trois autres composants (Speaker Encoder, Code Predictor, Speech Decoder) ne sont pas encore déployés sur la tablette. Regardons les options concrètement.

Évalué obstacles déploiement TTS et envisagé solutions alternatives

Évalué obstacles déploiement TTS et envisagé solutions alternatives

Bon, récapitulons exactement où on en est et quelles sont les options réalistes.

L'état des lieux du rapport TTS :

Composant           Params    NPU (.pte)    ONNX     CPU PyTorch
─────────────────────────────────────────────────────────────────
Talker LM           754M      ✅ 90.7 tok/s  ✗        ✅ (lent)
Code Predictor      141M      ✗              ✗        ✅
Speaker Encoder     8.9M      non tenté      non tenté ✅
Speech Decoder      114M      non tenté      ✗        ✅

Le talker représente 87% du temps de calcul et il est résolu. Les trois composants restants prennent ~1.5s sur CPU PC. Sur le CPU Oryon de la tablette (plus rapide que la plupart des CPU laptop sur du single-thread ARM), ça devrait donner 1-2s. Le pipeline total serait donc ~0.3s (talker NPU) + ~1.5s (CPU) = ~1.8s pour une phrase de 2-3 secondes d'audio. C'est du quasi temps réel.

Le problème c'est comment exécuter ces 3 composants sur la tablette. Et c'est là que la trouvaille est intéressante : un port Rust complet de Qwen3-TTS existe, avec le TalkerModel, le CodePredictor (5 couches autorégressives, hidden_size=1024, 15 codebooks), et le SpeechDecoder Rust. Il tourne entièrement sur CPU sans dépendance CUDA.

Voici les quatre pistes concrètes, de la plus rapide à la plus propre :

Piste 1 — Termux + PyTorch CPU sur la tablette (1-2 jours)

C'est le workaround le plus rapide. Tu installes Termux sur la tablette, tu y mets Python 3.10 + PyTorch CPU + les poids des 3 composants. Le talker tourne sur le NPU via le subprocess ExecuTorch, les tokens reviennent dans Python, le code predictor + speaker encoder + speech decoder tournent en PyTorch CPU natif. L'app Kotlin orchestre le tout en appelant un script Python via subprocess.

C'est moche architecturalement — deux runtimes (ExecuTorch natif + Python/PyTorch), une communication inter-process, des latences de démarrage Python. Mais ça marche, et ça permet de valider la qualité audio end-to-end immédiatement. Si le résultat sonore est bon, ça justifie l'effort d'une intégration plus propre.

Piste 2 — Le crate Rust cross-compilé pour Android (1-2 semaines)

Le crate qwen3_tts en Rust implémente déjà tout le pipeline en pur CPU. Il expose le TalkerModel, le CodePredictor (5 couches autorégressives, 15 codebooks avec hidden_size=1024), et le SpeechDecoder complet Rust. Rust se cross-compile très bien vers Android ARM64 via le NDK. Tu obtiendrais une bibliothèque .so native que tu appelles via JNI depuis Kotlin.

L'approche hybride serait d'utiliser le crate Rust uniquement pour le code predictor, le speaker encoder et le speech decoder (les composants CPU), tout en gardant le talker sur le NPU via ExecuTorch. Tu remplacerais le TalkerModel du crate Rust par un bridge qui appelle le .pte ExecuTorch. Concrètement, le flux serait : Kotlin appelle ExecuTorch JNI pour le talker → les speech tokens reviennent → Kotlin appelle la lib Rust JNI pour code predictor + decoder → audio WAV sort.

L'avantage c'est que tout est natif, pas de Python, pas de Termux, performances optimales sur CPU ARM. L'inconvénient c'est le travail de bridge Rust ↔ ExecuTorch ↔ JNI.

Piste 3 — Réexporter les composants en ONNX avec des workarounds (1-2 semaines)

Le rapport dit que l'export ONNX du code predictor et du speech decoder a échoué. Mais les raisons d'échec sont spécifiques et potentiellement contournables. Le code predictor échoue à cause de sa structure "multi-tête" (15 codebooks en boucle autorégressif). La solution serait de dérouler la boucle sur les 15 codebooks et d'exporter chaque step comme un module ONNX séparé, ou de réécrire le forward pour être compatible avec torch.onnx.export() (shapes statiques, pas de boucles Python). Le speech decoder échoue à cause de convolutions dynamiques et de boucles. Un ConvNet causal avec des shapes fixes devrait être exportable en fixant les dimensions d'entrée.

Si ça fonctionne, tu obtiens des fichiers .onnx que tu charges via ONNX Runtime Android — que tu as déjà dans tes dépendances pour Silero VAD. Un seul runtime ONNX pour le VAD et les composants TTS CPU.

Piste 4 — Tout exporter via ExecuTorch en CPU fallback (2-3 semaines)

ExecuTorch supporte un mode CPU (sans QNN delegate) où les opérations non-supportées par le NPU tombent automatiquement sur le CPU. Tu pourrais tenter torch.export() pour le code predictor et le speech decoder sans le QnnPartitioner — juste en mode XNNPACK (CPU optimisé ARM). Les opérations problématiques pour le NPU (boucles, shapes dynamiques) sont souvent supportées en CPU par ExecuTorch.

L'avantage c'est un seul runtime ExecuTorch pour tout (talker NPU + reste CPU), un seul système de build, un seul JNI bridge. L'inconvénient c'est que torch.export() est strict sur les shapes dynamiques et les boucles Python — les mêmes limitations qui ont fait échouer l'export la première fois.

Ma recommandation :

Commence par la Piste 1 (Termux + PyTorch) pour valider la qualité audio end-to-end en 1-2 jours. Si le résultat sonore est satisfaisant (clonage vocal naturel en français, pas d'artefacts), alors investis dans la Piste 2 (crate Rust cross-compilé) pour une intégration production propre. Le crate Rust est le chemin le plus sûr parce que quelqu'un a déjà résolu tous les problèmes d'implémentation — tu n'as qu'à cross-compiler pour ARM64 et écrire le bridge JNI.

La Piste 3 (ONNX) est un bon plan B si le Rust pose des problèmes de cross-compilation ou de taille de bibliothèque. Et la Piste 4 (ExecuTorch CPU) est la plus élégante mais la plus risquée.

Le plan d'action concret :

Semaine 1 — Validation qualité (Piste 1)
├── Installer Termux + Python 3.10 + PyTorch CPU sur tablette
├── Pousser les poids du code predictor + speaker encoder + decoder
├── Script d'orchestration : talker NPU → tokens → PyTorch CPU → WAV
├── Tester avec les voix françaises (Damien, Élodie, etc.)
└── Valider : qualité audio OK ? Latence < 3s ? Pas d'artefacts ?

Semaine 2-3 — Intégration native (Piste 2)
├── Cross-compiler le crate qwen3_tts Rust pour Android ARM64
├── Extraire uniquement code_predictor + speaker_encoder + speech_decoder
├── Écrire le bridge JNI (Kotlin → Rust .so)
├── Connecter au talker ExecuTorch NPU existant
├── Intégrer dans le Foreground Service Kazeia
└── Benchmarker la latence totale end-to-end sur tablette

Semaine 3-4 — Optimisation
├── Pré-calculer le x-vector des voix de référence (offline)
├── Streaming : décoder l'audio par paquets de 4 tokens
│   (pendant que le talker continue de générer)
├── Cache le speaker encoder (un seul appel par session)
└── Mesurer la consommation batterie du pipeline complet

Un dernier point important : le speaker encoder (8.9M paramètres) n'a besoin de tourner qu'une seule fois par voix. Tu peux pré-calculer le x-vector de chaque voix de référence (Damien, Élodie, etc.) sur PC et embarquer directement les vecteurs sur la tablette. Ça élimine complètement ce composant du runtime.

Tu veux qu'on détaille la Piste 1 pour que ta session Claude Code puisse démarrer la validation, ou qu'on explore le crate Rust en détail ?


You asked:

Je vais faire une parenthèse, il semble que google ait devoillé TurboQuant qui permettrait de réduire l'empreinte mémoire des LLM, je souhaiterai que tu récupères tous les articles sur le sujet et que tu étudies l'algorythme pour voir si cela nous permettrait d'optimiser notre code sur tablette

Mar 31


Claude Replied:

Excellente veille technologique. Si Google a sorti une nouvelle technique de quantification qui réduit l'empreinte mémoire des LLM, ça pourrait être un game changer pour Kazeia — notamment pour faire tourner le Qwen3-4B sur le NPU avec moins de RAM, ou même rendre le 1.7B encore plus léger. Je lance une recherche approfondie tout de suite.

@keyframes slideUpFadeOut { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-100%); opacity: 0; } } @keyframes slideUpFadeIn { 0% { transform: translateY(100%); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }

TurboQuant algorithm research

Research complete•

@keyframes slideUpFadeOut { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-100%); opacity: 0; } } @keyframes slideUpFadeIn { 0% { transform: translateY(100%); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }

265 sources

4m 47s

Your TurboQuant research report is ready. It clarifies that TurboQuant is a KV cache compression method—not a weight quantization technique—and explains why it's largely irrelevant to Kazeia's on-device deployment challenges with Qwen3 on ExecuTorch + QNN. The report covers what TurboQuant actually does, a critical community-discovered flaw in its QJL stage, why it doesn't address your export RAM or NPU inference bottlenecks, and recommends actionable alternatives including QAT-trained INT4 checkpoints, Google's LiteRT + QNN Accelerator stack, and hardware-aware tile quantization for Qualcomm Hexagon NPUs.

TurboQuant KV Cache Compression: Why It Won't Solve Kazeia's On-Device Deployment Challenges

Document 

# TurboQuant is real — but won't solve your problem **TurboQuant is a KV cache compression algorithm, not a weight quantization method, making it largely irrelevant to the Kazeia project's on-device deployment challenges.** Google Research announced


You asked:

Donc pour toi est ce que nous pourrions tirer profit de cette idée pour le NPU et la tablette ?

Mar 31


Claude Replied:

Évalué la pertinence de TurboQuant pour configuration NPU tablette

Évalué la pertinence de TurboQuant pour configuration NPU tablette

Non, pas dans votre cas. Et voici pourquoi en une phrase : TurboQuant compresse le KV cache, pas les poids du modèle — or vos problèmes sont tous liés aux poids.

Regardons vos trois goulots d'étranglement concrets et ce que TurboQuant changerait pour chacun :

L'export RAM (17 Go pour 0.6B, 48-64 Go pour 1.7B) — c'est le pipeline ExecuTorch qui matérialise le graphe complet des poids en mémoire pour la compilation QNN. TurboQuant ne touche pas aux poids, donc zéro impact ici. L'export consommera exactement la même RAM avec ou sans TurboQuant.

La taille des modèles sur la tablette (660 Mo pour 0.6B, 1.7 Go pour 1.7B) — c'est le volume des poids quantifiés. TurboQuant ne les compresse pas. Un Qwen3-1.7B en INT4 fera toujours ~850 Mo de poids, que le KV cache soit compressé ou non.

La vitesse d'inférence (93 tok/s pour 0.6B, 46 tok/s pour 1.7B) — sur le Snapdragon 8 Elite, le bottleneck c'est la bande passante mémoire pour charger les poids à chaque token (77 Go/s LPDDR5X). Le KV cache est minuscule à côté. Pour une conversation thérapeutique de 4-5 tours (disons 500 tokens de contexte), le KV cache en FP16 fait environ 500 × 2 × hidden_size × num_layers × 2 bytes. Pour Qwen3-1.7B (hidden_size=2048, 24 layers), ça donne ~94 Mo. Compresser ça de 6× avec TurboQuant gagnerait ~78 Mo de RAM. C'est négligeable sur 16 Go.

TurboQuant serait utile si vous aviez un modèle serveur avec des contextes de 32K-128K tokens et des dizaines d'utilisateurs simultanés. Pour un chatbot mono-utilisateur sur tablette avec des contextes courts, le KV cache n'est tout simplement pas le problème.

Ce qui vous aiderait réellement, c'est le QAT (Quantization-Aware Training). La recherche montre que les modèles entraînés avec QAT perdent 54% moins de qualité en INT4 que ceux quantifiés après coup. Concrètement, si vous fine-tunez votre Qwen3-1.7B thérapeutique avec qat_scheme = "int8-int4" (comme le propose Unsloth pour l'export ExecuTorch), le modèle apprend pendant l'entraînement à compenser les erreurs de quantification. Le résultat est un modèle INT4 qui se comporte presque comme un FP16, au lieu du compromis actuel où la quantification post-training dégrade la qualité empathique.

L'autre piste intéressante mentionnée dans la recherche c'est LiteRT + QNN Accelerator de Google, co-développé avec Qualcomm spécifiquement pour le Snapdragon 8 Elite. Il annonce des performances supérieures à ExecuTorch sur certains benchmarks. C'est à évaluer, mais c'est un changement de runtime complet — pas un quick win.

En résumé : TurboQuant est une vraie avancée scientifique, mais pour un cas d'usage server-side avec long contexte. Pour Kazeia sur tablette, c'est hors cible. Concentrez vos efforts sur le QAT pour le fine-tuning et sur le déploiement du pipeline TTS Qwen3 — c'est là que se trouvent vos vrais gains.


You asked:

Alors est ce que l'utilisation de LiteRT + QNN Accelerator en lieu et place de executorch augmenterai les performances ?

Mar 31


Claude Replied:

Réfléchissant aux gains de performance en remplaçant ExecuTorch par LiteRT et QNN Accelerator

Réfléchissant aux gains de performance en remplaçant ExecuTorch par LiteRT et QNN Accelerator

Très bonne question — c'est potentiellement un changement de runtime important. Laisse-moi chercher les benchmarks concrets avant de répondre.

Analysé comparaison LiteRT et ExecuTorch pour optimisation

Analysé comparaison LiteRT et ExecuTorch pour optimisation

La réponse courte : les chiffres LiteRT sont impressionnants mais ne s'appliquent pas directement à votre cas. Décortiquons pourquoi.

Ce que LiteRT + QNN Accelerator annonce :

Google a benchmarké le LiteRT QNN Accelerator sur 72 modèles ML et obtient jusqu'à 100× plus rapide que le CPU et 10× plus rapide que le GPU sur le Snapdragon 8 Elite, avec 64 des 72 modèles entièrement délégués au NPU Google Developers. Pour les LLM spécifiquement, le FastVLM-0.5B atteint plus de 11 000 tokens/s en prefill et plus de 100 tokens/s en decode sur le NPU du Snapdragon 8 Elite, avec un TTFT de seulement 0.12 seconde Google Developers.

100+ tok/s en decode pour un modèle 0.5B, c'est supérieur à vos 93 tok/s avec ExecuTorch sur le Qwen3-0.6B. C'est tentant.

Mais il y a des nuances critiques :

Premièrement, le benchmark FastVLM utilise une quantification INT8 poids + INT16 activations. C'est la clé pour débloquer les kernels INT16 haute vitesse du NPU. Google a également ajouté des kernels NPU spéciaux pour les couches d'attention des transformers Google Developers. Votre pipeline ExecuTorch utilise INT4 poids (w4a16), qui est un profil de quantification différent. La comparaison n'est donc pas à armes égales — INT8 consomme plus de mémoire mais le NPU a des kernels optimisés pour ce format. Il faudrait retester avec le même schéma de quantification pour comparer.

Deuxièmement, LiteRT part d'un format TFLite (.tflite), pas PyTorch. Le LiteRT QNN Accelerator remplace l'ancien delegate TFLite QNN et fournit une API unifiée qui abstrait la fragmentation entre les SoC Edge AI and Vision Alliance. Ça signifie que pour utiliser LiteRT, vous devriez convertir vos modèles Qwen3 de PyTorch vers TFLite d'abord (via ONNX → TFLite ou via AI Edge Torch), puis laisser LiteRT compiler pour le NPU. C'est un pipeline de conversion complètement différent de votre chaîne actuelle torch.export() → QnnPartitioner → .pte. Tout votre travail d'export ExecuTorch (les patches GCC 15, les scripts de conversion, les tokenizers) serait à refaire.

Troisièmement, LiteRT QNN ne supporte que les appareils avec Snapdragon 8 et Snapdragon 8+ SoC InfoQ. C'est votre cas (Snapdragon 8 Elite), mais les modèles LLM supportés sont principalement Gemma et FastVLM — des modèles Google. Qwen3 n'est pas mentionné dans les benchmarks LiteRT. Il faudrait vérifier que l'architecture Qwen3 (avec ses spécificités comme le QK-norm, le GQA, le RoPE theta 1M) est entièrement supportée par les 90 ops LiteRT.

Le vrai problème de fond :

La contrainte profonde sur mobile c'est la bande passante mémoire, pas la puissance de calcul. Les appareils mobiles ont 50-90 Go/s, les GPU datacenter ont 2-3 To/s — c'est un écart de 30-50×. Pour l'inférence LLM, cet écart est décisif car le decode est memory-bound : on charge tous les poids du modèle pour chaque token généré V-chandra.

Ça signifie que ni ExecuTorch ni LiteRT ne peuvent dépasser la limite physique de la bande passante LPDDR5X de votre tablette (~77 Go/s). Pour un modèle de 1.7B en INT4 (~850 Mo de poids), le débit théorique maximum est 77 Go/s ÷ 0.85 Go = ~90 tokens/s. Votre ExecuTorch mesure 46 tok/s, soit environ 50% de l'efficacité théorique. LiteRT pourrait gagner quelques pourcents grâce à ses kernels d'attention optimisés, mais pas doubler la performance — le plafond physique est le même.

Pour le Qwen3-0.6B (660 Mo), votre ExecuTorch mesure 93 tok/s. Le maximum théorique est ~115 tok/s. Vous êtes déjà à 80% d'efficacité. LiteRT pourrait grappiller les 20% restants, peut-être atteindre 100-110 tok/s, mais pas 200 tok/s.

Il y a cependant une piste LiteRT réellement intéressante :

Des chercheurs de l'EuroSys 2026 ont développé un système d'inférence LLM sur NPU Qualcomm Hexagon qui atteint un speedup de 19× pour les GEMM en précision mixte en alignant les groupes de quantification avec les patterns d'accès mémoire du NPU arXiv. C'est une optimisation bas niveau qui exploite la micro-architecture HMX (Hexagon Matrix eXtension) — et c'est exactement ce type d'optimisation que LiteRT intègre avec ses kernels spéciaux.

Et il y a aussi mllm-NPU qui est intéressant : le framework MLLM supporte depuis février 2026 l'exécution complète de graphes sur NPU via QNN AOT, avec Qwen3 et DeepSeek supportés en streaming sur Android GitHub. C'est une alternative à ExecuTorch qui mérite d'être surveillée.

Ma recommandation pour Kazeia :

Ne changez pas de runtime maintenant. ExecuTorch + QNN fonctionne, il est validé sur votre tablette, vous avez investi du temps dans le pipeline de conversion et résolu les problèmes de toolchain. Le gain potentiel de LiteRT est de l'ordre de 10-20% sur le decode — pas un game changer.

Le vrai levier pour améliorer les performances serait de passer le Qwen3-1.7B de w4a16 à w8a16 avec les kernels INT16 optimisés du NPU (comme le fait LiteRT), mais dans ExecuTorch. Le QNN delegate d'ExecuTorch supporte aussi les kernels INT8/INT16 du Hexagon — il faudrait juste changer le profil de quantification à l'export.

En revanche, si un jour vous voulez déployer Kazeia sur des tablettes de marques différentes (Samsung avec MediaTek, par exemple), LiteRT deviendrait pertinent grâce à sa portabilité multi-SoC. Mais pour le moment, avec une flotte de tablettes OnePlus Pad 3 identiques, ExecuTorch + QNN est le bon choix — et concentrez vos efforts sur le pipeline TTS Qwen3 et le fine-tuning du modèle thérapeutique.


You asked:

Mais je ne comprends pas, de ce que je lis, même si on plafonnerait à cause de la vitesse de la RAM, on pourrait augmenter le debut avec du INT8 ou INT16 comparé FP16 ou FP32 ?

Mar 31


Claude Replied:

Reconciliait quantization theory avec observations empiriques du pipeline

Reconciliait quantization theory avec observations empiriques du pipeline

Tu as tout à fait raison, et c'est moi qui n'ai pas été assez clair. Le raisonnement est simple et correct :

La bande passante est fixe à ~77 Go/s. Plus les poids sont petits, plus tu en charges par seconde, plus tu génères de tokens.

Concrètement, pour un modèle de 1.7B paramètres :

Format des poids    Taille modèle    Débit théorique max
─────────────────────────────────────────────────────────
FP32 (4 octets)     6.8 Go          77 ÷ 6.8 = ~11 tok/s
FP16 (2 octets)     3.4 Go          77 ÷ 3.4 = ~22 tok/s
INT8 (1 octet)      1.7 Go          77 ÷ 1.7 = ~45 tok/s
INT4 (0.5 octet)    0.85 Go         77 ÷ 0.85 = ~90 tok/s

Donc oui, passer de FP16 à INT8 doublerait le débit théorique, et passer à INT4 le quadruplerait par rapport à FP16. Ton intuition est correcte.

La bonne nouvelle c'est que votre pipeline ExecuTorch utilise déjà INT4 (w4a16). Vos Qwen3 sont exportés en poids INT4 avec activations FP16. C'est le format le plus compressé — vous êtes déjà au maximum de ce que la bande passante permet en théorie. Vos 46 tok/s mesurés pour le Qwen3-1.7B représentent environ 50% du maximum théorique de 90 tok/s. Les 50% manquants sont perdus dans les overheads : la déquantification INT4 → FP16 avant le calcul matriciel, la latence de scheduling CPU ↔ NPU, les ops qui ne tournent pas sur le NPU (softmax, layer norm, RoPE), et le KV cache en FP16.

Là où ta question devient vraiment pertinente, c'est sur le format de calcul du NPU.

Le Hexagon HTP v79 a une unité matricielle HMX qui a des modes de calcul natifs. Elle peut faire du INT8 × INT8, du INT8 × INT16, et du FP16 × FP16 à pleine vitesse. Mais pour le INT4, le NPU doit décompresser les poids INT4 en INT8 ou INT16 avant de les multiplier — il n'y a pas de matmul INT4 natif sur le Hexagon. Cette décompression coûte des cycles.

C'est exactement ce que LiteRT exploite. Leur modèle FastVLM utilise des poids INT8 avec des activations INT16, ce qui débloque les kernels INT16 haute vitesse natifs du NPU Google Developers. Le NPU fait un matmul INT8 × INT16 directement, sans décompression — c'est une opération native du HMX à pleine puissance.

Le compromis est le suivant :

                    Bande passante     Compute NPU        Résultat
                    (moins = mieux)    (natif = mieux)
────────────────────────────────────────────────────────────────────
INT4 poids (w4a16)  ✅ Optimal         ❌ Décompression    Bon mais overhead
INT8 poids (w8a16)  ⚠️ 2× plus gros   ✅ Natif HMX        Potentiellement + rapide
FP16 poids          ❌ 4× plus gros    ✅ Natif HMX        Trop gros en mémoire

Pour les petits modèles (0.6B) qui tiennent largement en RAM, passer en INT8 (w8a16) pourrait être plus rapide que INT4 parce que le NPU calcule en natif sans overhead de décompression, même si on charge 2× plus de données. Le modèle passerait de 660 Mo à ~1.3 Go — ça tient toujours confortablement en mémoire.

Pour le Qwen3-1.7B, c'est plus serré. En INT8 il ferait ~1.7 Go au lieu de 850 Mo. Ça tient en RAM, mais le débit théorique tombe à ~45 tok/s (limité par la bande passante), donc proche de vos 46 tok/s actuels en INT4. Le gain du compute natif serait compensé par la perte en bande passante. C'est à peu près neutre.

Pour le Qwen3-4B, passer en INT8 signifierait ~4 Go de poids — ça tient mais ça devient tendu avec tous les autres composants. Et le débit théorique tomberait à ~19 tok/s, ce qui est inférieur à vos 20 tok/s actuels via Genie en INT4.

Concrètement, ce qu'on pourrait tester :

Le gain le plus probable serait sur le Qwen3-0.6B en INT8 : le modèle est petit (1.3 Go en INT8), la bande passante n'est pas le bottleneck, et le NPU calculerait en mode natif INT8 × INT16. On pourrait passer de 93 tok/s à potentiellement 110-120 tok/s. C'est le même principe qui permet à LiteRT d'atteindre 100+ tok/s sur FastVLM-0.5B.

Pour tester ça avec ExecuTorch, il suffit de changer le profil de quantification à l'export :

python

# Actuellement (INT4 poids)
backend_options = generate_htp_compiler_spec(use_fp16=False)  # quantized INT4

# À tester (INT8 poids, INT16 activations)
backend_options = generate_htp_compiler_spec(use_fp16=False)
# + changer le quantizer pour INT8 au lieu de INT4 dans le script d'export

C'est une expérience rapide que ta session Claude Code pourrait faire — re-exporter le Qwen3-0.6B en INT8 avec le même pipeline ExecuTorch, pousser le nouveau .pte sur la tablette, et comparer les tok/s. Si le gain est significatif, ça valide l'hypothèse et on peut décider si le tradeoff taille/vitesse est acceptable pour chaque modèle.

Tu veux qu'on prépare les paramètres exacts pour que ta session Claude Code fasse ce test ?


You asked:

Donc techniquement sur la tablette que nous avons quelle serait la combinaison quantization que nous devrions utiliser pour exploiter au mieux ces ressources

Mar 31


Claude Replied:

Analyzed hardware capabilities pour optimiser quantization multi-modèles

Analyzed hardware capabilities pour optimiser quantization multi-modèles

Excellente question — c'est le genre d'optimisation qui peut faire une vraie différence. Laisse-moi d'abord vérifier les specs précises du HMX du Hexagon HTP v79 pour ne pas spéculer.

Synthesized optimal quantization strategy balancing hardware capabilities with software constraints

Synthesized optimal quantization strategy balancing hardware capabilities with software constraints

Les specs hardware sont maintenant claires. Voici exactement ce que le HMX de votre Snapdragon 8 Elite supporte et la stratégie optimale pour chaque composant.

Ce que le HMX sait faire nativement :

L'unité HMX supporte les précisions INT4, INT8, INT16 et FP16. L'unité de base est une "tile" — pour le FP16, chaque tile est une matrice 32×32 occupant 2 Ko arXiv. Le coprocesseur HMX supporte spécifiquement les modes INT8, A16int8 (activations 16 bits avec poids INT8), A8int4 (activations 8 bits avec poids INT4), et FP16 TechWiser.

C'est la clé. Le HMX a des modes de calcul matriciel natifs — il ne fait pas que du stockage en INT4, il sait aussi calculer avec. Mais chaque mode a un profil de performance différent.

Le vrai tableau des TOPS par précision :

Mode HMX              Compute relatif    Bande passante poids
──────────────────────────────────────────────────────────────
FP16 × FP16           1×  (baseline)     2 octets/param
INT8 × INT16 (w8a16)  ~2× (tiles 2× +)  1 octet/param
INT4 × INT8 (w4a8)    ~4× (tiles 4× +)  0.5 octet/param
INT4 via LUT → FP16   ~1.5-2×            0.5 octet/param + overhead LUT

Le Snapdragon 8 Elite a un support hardware pour les calculs INT4, mais le support logiciel est encore en retard — ça change rapidement Embedl. Concrètement, le QNN SDK propose deux chemins pour les poids INT4 : soit une décompression LUT vers FP16 puis calcul FP16 (c'est ce que fait ExecuTorch actuellement avec w4a16), soit un calcul natif A8int4 — mais ce dernier nécessite aussi des activations INT8, ce qui demande une quantification des activations.

Là où ça devient intéressant pour Kazeia :

Votre pipeline actuel est w4a16 : poids INT4, activations FP16. Le HMX décompresse les INT4 en FP16 via LUT puis fait le matmul en FP16. C'est bien, mais vous n'exploitez pas le mode natif le plus puissant du NPU.

Le mode optimal serait w4a8 ou w8a8 : poids INT4 ou INT8, activations INT8. Dans ce mode, l'architecture est précisément optimisée pour l'inférence avec des modèles quantifiés, favorisant le format INT8 dans lequel elle a toujours excellé Emergent Mind. Le HMX fait un matmul INT × INT natif à pleine puissance, sans décompression, sans conversion FP16.

La stratégie optimale par composant de Kazeia :

Composant              Poids    Activations   KV Cache    Pourquoi
─────────────────────────────────────────────────────────────────────
LLM Qwen3-1.7B        INT4     INT8 (w4a8)   INT8        Meilleur débit decode
                                                          INT4→moitié bande passante
                                                          INT8 activations→compute natif
                                                          
LLM Qwen3-0.6B        INT8     INT8 (w8a8)   INT8        Assez petit pour INT8
                                                          Tient en ~660 Mo
                                                          Full compute natif HMX
                                                          Meilleure qualité que INT4

TTS Talker (754M)      INT8     INT8 (w8a8)   INT8        Qualité audio critique
                                                          INT4 risque artefacts sonores
                                                          ~750 Mo tient en RAM

Whisper Encoder        INT8     INT8 (w8a8)   N/A         Prefill-only (pas autorégressif)
                                                          Compute-bound, pas bandwidth-bound
                                                          INT8 natif = max TOPS

Silero VAD             FP32     FP32          N/A         Minuscule (1.8 Mo)
                                                          Tourne sur CPU, pas NPU
                                                          Pas besoin d'optimiser

Pourquoi w4a8 serait le sweet spot pour le LLM 1.7B :

Le decode LLM est un GEMV (matrix-vector multiply) — tu charges tous les poids pour multiplier par un seul vecteur d'activation. C'est bandwidth-bound. Les poids en INT4 minimisent la bande passante (0.5 octet par paramètre). Mais les activations n'ont pas besoin d'être en FP16 — elles peuvent être en INT8 sans perte significative de qualité. Et avec des activations INT8, le HMX utilise son mode A8int4 natif au lieu de devoir décompresser les INT4 en FP16.

Le gain estimé :

Actuel (w4a16) :
  Poids: 0.5 octet/param → 850 Mo → 77/0.85 = 90 tok/s théorique
  Mais: décompression INT4→FP16 + matmul FP16 = ~50% efficacité
  Résultat mesuré: 46 tok/s

Optimal (w4a8) :
  Poids: 0.5 octet/param → 850 Mo → même bande passante
  Mais: matmul A8int4 natif HMX = ~70-80% efficacité
  Résultat estimé: 63-72 tok/s  (+40-55%)

Mais il y a un obstacle logiciel important.

QNN ne supporte que la quantification per-tensor ou per-channel pour les poids. Les méthodes de quantification fine-grained par groupe, essentielles pour les LLM modernes, ne sont pas supportées nativement arXiv. Or les LLM comme Qwen3 ont besoin de quantification par groupe (typiquement groupe de 32 ou 128) pour maintenir la qualité en INT4. C'est exactement pour ça que le paper d'EuroSys propose la tile quantization : ils alignent les groupes de quantification avec les patterns d'accès mémoire du HMX, obtenant un speedup de 19× pour les GEMM en précision mixte arXiv.

ExecuTorch contourne ce problème en utilisant la quantification per-channel pour les poids et en laissant les activations en FP16 (w4a16). C'est pourquoi vos performances actuelles sont à ~50% du théorique — le pipeline n'exploite pas le mode INT natif du HMX.

Ce qu'il faudrait faire concrètement :

La solution idéale serait de re-exporter vos modèles avec un profil de quantification INT8 activations au lieu de FP16. Dans ExecuTorch, ça se configure au niveau du quantizer :

python

# Actuel : w4a16 (poids INT4, activations FP16)
from executorch.backends.qualcomm.quantizer import QnnQuantizer
quantizer = QnnQuantizer()
quantizer.set_bit16_op_quant_config(...)  # activations en FP16

# Cible : w4a8 ou w8a8 (poids INT4/8, activations INT8)  
quantizer.set_per_channel_weight_dtype(weight_dtype_wt=torch.int8)
quantizer.set_per_tensor_act_dtype(act_dtype=torch.int8)

Les tests à faire par la session Claude Code seraient, dans l'ordre :

Premièrement, Qwen3-0.6B en w8a8 (poids INT8, activations INT8) — c'est le plus simple car tout est INT8 natif. Comparer avec vos 93 tok/s actuels en w4a16. Si ça monte à 100-120 tok/s, le gain du compute natif compense la bande passante doublée.

Deuxièmement, Qwen3-0.6B en w4a8 (poids INT4, activations INT8) — c'est le meilleur des deux mondes. Même bande passante poids qu'actuellement mais compute natif HMX. C'est celui qui devrait donner le plus gros gain.

Troisièmement, si w4a8 fonctionne, appliquer le même profil au Qwen3-1.7B et au talker TTS.

L'enjeu c'est que le w4a8 n'est peut-être pas directement supporté par le pipeline d'export ExecuTorch + QNN actuel — il faudra peut-être patcher le quantizer ou utiliser une calibration spécifique. Mais c'est exactement le type d'optimisation qui pourrait transformer vos 46 tok/s en 65-70 tok/s sur le 1.7B, sans changer de modèle ni de hardware.

Tu veux qu'on prépare les instructions détaillées pour que ta session Claude Code teste ces profils de quantification ?


You asked:

Et via des ré-implémentation via LireRT ou mllm-NPU, crois tu qu'on pourrait gagner en performance ?

Mar 31


Claude Replied:

Investigated performance gains between framework implementations

Investigated performance gains between framework implementations

Bonne question — maintenant qu'on comprend que le bottleneck est l'efficacité d'utilisation du HMX, regardons si d'autres runtimes l'exploitent mieux.

Crystallized analysis of three runtime performance options

Crystallized analysis of three runtime performance options

Les résultats de la recherche sont très éclairants. La réponse est oui, significativement — mais pas avec LiteRT. Voici pourquoi.

Le problème fondamental de tous les runtimes actuels (ExecuTorch, LiteRT, QNN) :

Les systèmes NPU existants ont tous des limitations : mllm-NPU n'utilise pas le NPU pendant la phase de decode, les systèmes basés sur QNN ont une faible précision (comme PowerServe), et d'autres comme Powerinfer-2 et HeteroLLM ne sont pas open-source arXiv.

Dit autrement : aucun des frameworks grand public n'exploite correctement le NPU Hexagon pour l'inférence LLM. Ils utilisent tous QNN comme une boîte noire — et QNN n'est pas optimisé pour le pattern spécifique du LLM decode (GEMV, poids quantifiés par groupe, KV cache dynamique).

C'est exactement ce que le paper EuroSys 2026 a démontré. Ces chercheurs ont reverse-engineeré les instructions HMX non documentées pour programmer le NPU directement :

Les chercheurs ont pu utiliser l'unité matricielle FP16 en rétro-concevant les instructions non documentées dans les bibliothèques binaires. L'unité de base du HMX pour les opérations FP16 est une tile de 32×32 occupant 2 Ko arXiv.

Et le résultat est spectaculaire. Leur système atteint un speedup de 19× pour les GEMM en précision mixte et 2.2× pour le softmax par rapport aux implémentations précédentes arXiv. Pour la première fois, mllm-NPU atteint plus de 1000 tokens/seconde en prefill pour un modèle de classe milliard (Qwen1.5-1.8B), et il est 7.3×-18.4× plus rapide que les baselines sur CPU arXiv.

Maintenant, la question c'est quel runtime exploite le mieux le HMX pour le DECODE (pas le prefill) :

Le prefill (traiter le prompt) est compute-bound et bénéficie massivement du NPU. Mais le decode (générer token par token) est bandwidth-bound, et c'est là que les optimisations bas-niveau font la différence. Voici ce que chaque runtime offre :

ExecuTorch + QNN (votre runtime actuel) utilise QNN comme backend opaque. QNN compile le graphe du modèle en context binaries optimisés pour le HTP, mais il utilise une quantification per-channel ou per-tensor limitée. L'architecture continue de favoriser les modèles en format INT8 quantifié, une modalité dans laquelle elle a toujours excellé Emergent Mind. Vos 46 tok/s sur Qwen3-1.7B en w4a16 représentent ~50% d'efficacité théorique — QNN fait un bon travail mais pas optimal.

LiteRT + QNN Accelerator utilise le même QNN en dessous mais avec des kernels d'attention spécialisés ajoutés par Google. Google a ajouté des kernels NPU spéciaux pour les couches d'attention des transformers, en utilisant une quantification INT8 poids + INT16 activations pour débloquer les kernels haute vitesse du NPU Google Developers. Le gain serait de l'ordre de 10-30% sur le decode par rapport à ExecuTorch, principalement grâce à ces kernels d'attention optimisés. Mais ça reste au-dessus de QNN — pas d'accès direct au HMX.

Le système EuroSys 2026 (basé sur mllm-NPU) — c'est le vrai game changer potentiel. Ces chercheurs ne passent pas par QNN pour le compute critique. Ils utilisent des instructions vlut16 pour convertir directement les valeurs INT4 quantifiées en FP16 via lookup table, éliminant la séquence conventionnelle mask-unpack-convert arXiv. Et surtout, leur tile quantization aligne les groupes de quantification avec le layout mémoire natif du HMX :

Ils arrangent les poids avant la quantification selon un layout hiérarchiquement structuré en deux niveaux : un ordonnancement en colonne des tiles à l'extérieur, correspondant à l'opération de produit interne au niveau tile de l'unité matricielle, et un réarrangement interne arXiv.

Le résultat sur la qualité est remarquable : le modèle utilisant leur layout de quantification par tiles a une précision légèrement supérieure sur MMLU comparé au layout conventionnel, avec seulement une légère baisse sur Winogrande et Wikitext arXiv.

Mon évaluation réaliste des gains possibles :

Runtime                    Qwen3-1.7B decode    Gain vs actuel    Effort
────────────────────────────────────────────────────────────────────────
ExecuTorch + QNN (actuel)  46 tok/s             baseline          0
ExecuTorch w4a8 (reconfig) ~55-60 tok/s         +20-30%           1 semaine
LiteRT + QNN Accelerator   ~55-65 tok/s         +20-40%           3-4 semaines
                                                                   (refonte pipeline)
mllm-NPU / EuroSys style   ~70-85 tok/s         +50-85%           2-3 mois
                                                                   (reverse engineering HMX)
Théorique max (77 Go/s)    90 tok/s             +95%              impossible à atteindre

Ma recommandation pour Kazeia :

À court terme, le meilleur rapport effort/gain c'est de reconfigurer ExecuTorch avec w4a8 ou w8a8. Vous restez dans votre pipeline existant, vous changez juste le profil de quantification. C'est 1 semaine de travail pour un gain potentiel de 20-30%.

À moyen terme, surveille de très près le framework MLLM. MLLM supporte depuis février 2026 l'exécution complète de graphes sur NPU via QNN AOT, avec Qwen3 supporté en streaming sur Android GitHub. C'est open source, activement développé, et il intègre déjà les optimisations du paper mllm-NPU (chunked prefill, outlier extraction, shape profiling). Si l'équipe MLLM intègre les optimisations tile quantization du paper EuroSys, ça pourrait devenir le meilleur runtime pour LLM sur Hexagon — avec des gains de 50%+ par rapport à ExecuTorch/QNN.

LiteRT ne vaut pas le coût de migration. Le gain de 20-40% ne justifie pas de refaire tout le pipeline de conversion (PyTorch → TFLite → LiteRT) et de perdre la compatibilité avec vos modèles custom Qwen3-TTS et vos scripts d'export ExecuTorch.

Le vrai levier à long terme pour Kazeia serait de contribuer aux optimisations MLLM pour le Snapdragon 8 Elite — c'est un travail de recherche qui pourrait aussi alimenter votre mémoire UTBM.

Tu veux qu'on creuse le framework MLLM pour évaluer une migration, ou qu'on reste sur l'optimisation du profil de quantification ExecuTorch pour le court terme ?


You asked:

Je veux bien que tu creuses le framework MLLM et ces possibilités sur le snapdragon 8 elite

Mar 31


Claude Replied:

Démêlé les capacités et limitations du framework MLLM

Démêlé les capacités et limitations du framework MLLM

L'analyse est maintenant complète, et le tableau est plus nuancé que prévu. MLLM a des forces réelles mais aussi un défaut critique pour Kazeia.

Ce qu'est MLLM :

MLLM est le hub central de la stack d'inférence IA, connectant les algorithmes d'optimisation (décodage spéculatif, pruning, quantification) avec les couches de compilation/runtime (CANN, CUDA, MLIR) pour l'exécution hardware. Il supporte de manière unifiée le CPU ARM, le GPU OpenCL, et le NPU QNN GitHub. C'est un projet académique de Peking University et Beijing University of Posts and Telecommunications, avec une communauté active.

Depuis février 2026, MLLM supporte l'exécution AOT complète de graphes sur NPU via QNN. Il supporte aussi le streaming stable de Qwen3 et DeepSeek sur Android via une architecture client-serveur embarquée utilisant un serveur Go in-app GitHub.

L'architecture est intéressante pour Kazeia :

Le système de compilation AOT de MLLM transforme les modèles en exécutables binaires optimisés pour le HTP Qualcomm. Il applique des schémas de quantification optimisés (INT8, INT16, et un format LPBQ w4a16). Il découpe automatiquement les grands graphes en sous-graphes pour respecter les contraintes de taille du VTCM DeepWiki.

Concrètement, MLLM a un pipeline de conversion propre : tu prends ton modèle PyTorch/SafeTensors, tu le convertis via mllm-convertor dans le format MLLM, tu compiles AOT pour le HTP, et tu obtiens des qnn_context.bin prêts à exécuter. Le format LPBQ utilise une quantification à deux niveaux pour une compression extrême des poids en w4a16 DeepWiki.

Le support Qwen3 est confirmé mais jeune :

Un exemple qwen3_qnn_aot existe dans le repo avec un fichier de configuration JSON pour la compilation AOT GitHub. Le support date de quelques semaines seulement. Cependant, il y a des bugs rapportés : sur une tablette Snapdragon 8 Gen 3, le path NPU pour Qwen2-VL produit du texte charabia alors que le path CPU fonctionne correctement GitHub. Ce n'est pas rassurant pour une adoption production immédiate.

Le point critique : le decode NPU est le maillon faible de MLLM.

C'est la découverte la plus importante de cette recherche. Le paper EuroSys 2026 confirme explicitement que mllm-NPU n'utilise pas le NPU pendant la phase de decode. D'autres systèmes basés sur QNN ont une faible précision arXiv. Et les auteurs du paper EuroSys eux-mêmes reconnaissent les limitations : la vitesse de décodage actuelle est relativement contrainte, principalement à cause de l'overhead de déquantification. Le GEMM quantifié basé sur QNN utilise typiquement seulement les composants DMA et HMX sans introduire de surcharge computationnelle HVX arXiv.

Concrètement, ça signifie que le framework MLLM original (llm.npu) utilise le NPU uniquement pour le prefill et tombe sur le CPU pour le decode. C'est un design délibéré : le prefill est compute-bound (matmul de longues séquences → NPU excelle), mais le decode est bandwidth-bound (GEMV, un seul token à la fois) et le scheduling CPU↔NPU ajoute de la latence qui annule le gain.

mllm-NPU est compatible avec n'importe quel moteur de décodage et utilise le backend CPU de MLLM pour la phase de decode, avec une longueur de chunk par défaut de 256 Xumengwei.

Pour Kazeia, c'est problématique. Le decode est exactement ce qui détermine la latence perçue par le patient — c'est le temps entre chaque mot généré. Vos 46 tok/s actuels sur Qwen3-1.7B via ExecuTorch sont du decode sur NPU. Si MLLM fait le decode sur CPU, vous retomberiez aux 15 tok/s de llama.cpp, soit 3× plus lent.

Le paper EuroSys 2026 améliore ça, mais c'est encore de la recherche :

Le système de Zixu Hao et al. (le paper EuroSys, pas le MLLM original) fait du decode sur NPU en utilisant les instructions HMX reverse-engineerées et la tile quantization. Mais ils reconnaissent que la performance de décodage reste contrainte et suggèrent que des approches similaires à T-MAC pourraient permettre un GEMV efficace avec quantification fine-grained sur les NPU arXiv. Ce code n'est pas encore dans le repo MLLM public — c'est du code de recherche présenté à EuroSys fin avril 2026.

Voici le tableau comparatif honnête :

                    Prefill          Decode           Modèles Qwen3    Maturité
──────────────────────────────────────────────────────────────────────────────
ExecuTorch+QNN      Bon (451 tok/s)  Bon (46-93 tok/s) Validé           Moyenne
                    NPU              NPU               (0.6B, 1.7B)

MLLM (QNN AOT)     Excellent         Mauvais          Supporté         Jeune
                    (1000+ tok/s)    (CPU fallback     (depuis fév 26)  (bugs NPU)
                    NPU              ~15 tok/s)

MLLM + EuroSys     Excellent         Bon-Excellent    Non testé        Recherche
                    (1000+ tok/s)    (NPU natif,       (théorique)     (code non
                    NPU              tile quant)                        publié)

LiteRT + QNN       Excellent         Bon              Pas testé        Bonne
                    (11000 tok/s)    (~100 tok/s        (Gemma/FastVLM   (Google)
                    NPU              0.5B modèle)      uniquement)

Ma recommandation mise à jour :

Premièrement, gardez ExecuTorch + QNN pour la production. C'est le seul runtime qui fait du decode sur NPU de manière stable avec Qwen3. Vos 46-93 tok/s sont solides et reproductibles.

Deuxièmement, surveillez MLLM mais ne migrez pas maintenant. Le framework est prometteur, le support Qwen3 QNN AOT est récent, mais le decode sur CPU est un non-go pour un chatbot conversationnel. Attendez que les optimisations du paper EuroSys soient intégrées dans le repo public — ça devrait arriver après la conférence (fin avril 2026).

Troisièmement, ce que vous pouvez faire dès maintenant pour gagner en performance sans changer de runtime, c'est tester la reconfig de quantification w4a8 sur ExecuTorch. C'est le gain le plus accessible avec le moins de risque.

Quatrièmement, la vraie percée viendrait d'un contact avec l'équipe EuroSys (Zixu Hao, Peking University). Leur tile quantization et leurs kernels HMX custom sont exactement ce dont Kazeia a besoin. Si leur code devient public et que vous l'intégrez, le gain serait de 50%+ sur le decode. Et comme contribution académique pour votre mémoire UTBM, une collaboration avec cette équipe serait extrêmement valorisante.

Tu veux qu'on prépare un plan d'action priorisé pour les optimisations de performance, ou qu'on revienne au sujet du pipeline TTS Qwen3 ?


You asked:

TTS_REPORT.md

178 lines

md

Voici le point d'avancement sur le TTS, je souhaite que tu l'examines car nous ne pouvons pas rester ainsi

Apr 1


Claude Replied:

Diagnostiqué goulots d'étranglement TTS et esquissé solutions d'optimisation

Diagnostiqué goulots d'étranglement TTS et esquissé solutions d'optimisation

Ce rapport est excellent en termes de diagnostic. La session Claude Code a identifié exactement pourquoi chaque approche NPU échoue. Maintenant, trouvons la sortie.

Le problème en une phrase : RTF 7.1, c'est 7× trop lent pour du temps réel, et le NPU refuse de coopérer à cause de la sensibilité extrême du TTS à la précision.

Décortiquons où va le temps et ce qu'on peut gagner :

Actuel (RTF 7.1 = 28s pour 4s audio) :
├── CP fullseq (CPU)   : 353ms × 50 = 17.7s  ← 63% du temps, LE bottleneck
├── Talker (CPU)        : 130ms × 50 = 6.5s   ← 23%
└── Decoder (NPU)       :              3.0s   ← 11%, déjà optimisé

Le Code Predictor en mode fullseq mange 63% du budget. Et pourquoi fullseq ? Parce que le KV-cache à 16 positions perdait la position 0 (le hidden state du talker) après 16 steps. Le rapport mentionne la solution : augmenter CP_KV_LEN à 17. C'est la première chose à faire.

Voici le plan d'action par ordre de priorité et d'impact :

Priorité 1 — Fixer le CP KV-cache (1-2 jours, gain énorme)

Le rapport dit que le KV-cache CP fonctionne sur PC (15/15 match) mais dégénère au step 53 sur tablette parce que la position 0 (hidden state du talker) est éjectée du cache après 16 steps. La solution est documentée dans le rapport : passer CP_KV_LEN de 16 à 17. Position 0 = hidden du talker (permanent, jamais éjecté), positions 1-16 = les 15 codebooks + le nouveau. Le shift ne touche que les positions 1-16.

Le gain estimé est massif. Sur CPU avec KV-cache, chaque step CP ne fait qu'un forward sur 1 token au lieu de re-processer toute la séquence croissante (2, 3, 4... 17 tokens). Le temps passerait de ~353ms (fullseq, séquence moyenne ~10 tokens) à ~50-70ms (KV-cache, 1 token).

Avec CP KV-cache fixé (CPU) :
├── CP KV-cache (CPU)  :  60ms × 50 = 3.0s   ← au lieu de 17.7s
├── Talker (CPU)        : 130ms × 50 = 6.5s
└── Decoder (NPU)       :              3.0s
Total : 12.5s → RTF 3.1                       ← divisé par 2.3

Priorité 2 — Tester le Talker NPU avec sampling (2-3 jours)

La leçon #8 du rapport est révélatrice : le greedy decoding ne fonctionne jamais (même sur CPU, le modèle ne génère jamais EOS en greedy). Le sampling (temp=0.9, top_k=50) est obligatoire. C'est un point crucial pour la question du NPU.

Quand le rapport dit que le NPU fp16 donne des "codes complètement différents du CPU", cette comparaison est faite en greedy (argmax). Mais puisque le pipeline exige du sampling de toute façon, la question n'est pas "est-ce que les tokens sont identiques" mais "est-ce que la distribution de probabilité est suffisamment proche pour que le sampling produise de l'audio acceptable".

Un LLM chat tolère des variations dans les tokens générés — "Bonjour, je vous écoute" vs "Bonjour, je suis là pour vous" sont tous les deux acceptables. Un TTS avec sampling est similaire : deux générations du même texte donnent des audios légèrement différents mais tous les deux naturels. La question est : est-ce que l'audio NPU fp16 est naturel, pas est-ce qu'il est identique au CPU.

Le test à faire serait de lancer le talker sur NPU en fp16 avec le sampling habituel (temp=0.9, top_k=50, repetition_penalty=1.05), garder le CP et le decoder sur CPU, et écouter le résultat. Si l'audio est naturel et que l'EOS se déclenche, le talker NPU est viable même avec des logits fp16 différents.

Si ça marche, le gain serait :

Avec Talker NPU + CP KV-cache CPU :
├── CP KV-cache (CPU)  :  60ms × 50 = 3.0s
├── Talker (NPU)        :  11ms × 50 = 0.55s  ← 93 tok/s comme le LLM
└── Decoder (NPU)       :              3.0s
Total : 6.55s → RTF 1.6                        ← quasi temps réel !

Priorité 3 — Streaming decode (1 semaine)

Le decoder attend que TOUS les tokens soient générés avant de convertir en audio. Mais Qwen3-TTS est conçu pour le streaming : chaque paquet de 4 tokens (= 320ms d'audio) est décodable indépendamment. On peut commencer à jouer l'audio dès que les 4 premiers tokens de codec sont générés.

Avec streaming (Talker NPU + CP KV-cache CPU) :
t=0.0s : Talker génère token 1
t=0.07s : CP génère 15 codebooks pour token 1
t=0.28s : 4 tokens complets → Decoder lance chunk 1
t=0.58s : Chunk 1 décodé → AUDIO COMMENCE (latence perçue : 0.6s)
t=... : Pendant que l'audio joue, les tokens suivants sont générés

La latence perçue passerait de ~6.5s à ~0.6 seconde. Le patient entendrait la réponse presque immédiatement, même si la génération complète prend 6.5s en arrière-plan.

Priorité 4 — Quantification calibrée du CP (1-2 semaines)

Le rapport mentionne cette piste sans l'explorer. L'idée est d'utiliser des données audio réelles françaises (vos 8 voix de référence) comme set de calibration pour le quantifier INT8 de l'ONNX Runtime QNN EP ou d'ExecuTorch. Au lieu de la quantification automatique qui est trop agressive, la calibration mesure la distribution réelle des activations et ajuste les échelles de quantification en conséquence.

Pour le CP, qui n'a que 5 couches et 141M paramètres, une calibration avec quelques centaines de phrases françaises pourrait suffire à trouver des échelles qui préservent les logits critiques. C'est exactement ce que le rapport suggère en piste #1 : "use_16a8w avec données de calibration".

Si ça fonctionne, le CP sur NPU via JNI donnerait ~79ms pour les 15 steps (au lieu de ~60ms × 15 = 900ms sur CPU pour fullseq, ou ~60ms pour KV-cache). Le gain marginal par rapport au KV-cache CPU est modeste, mais ça libère le CPU pour d'autres tâches.

Le roadmap réaliste vers le temps réel :

Étape   Action                          RTF estimé   Latence perçue
─────────────────────────────────────────────────────────────────────
Actuel  Tout CPU sauf decoder           7.1          28s
  ↓
  1     Fix CP KV-cache (KV_LEN=17)     3.1          12.5s
  ↓
  2     Talker sur NPU (si sampling OK) 1.6          6.5s
  ↓
  3     + Streaming decode              1.6          ~0.6s ← game changer
  ↓
  4     + CP calibré sur NPU            ~1.2         ~0.4s
  ↓
Cible   Pipeline optimisé               ~1.0-1.2     < 1s

L'étape 3 est le vrai game changer pour l'expérience utilisateur. Même à RTF 1.6, si l'audio commence à jouer après 0.6s, le patient perçoit une réponse instantanée — le reste de la génération est masqué par la lecture audio.

Les instructions pour ta session Claude Code :

La première tâche est le fix du CP KV-cache. Dans le code du Qwen3TtsEngine.kt, modifier la gestion du KV-cache du Code Predictor pour utiliser 17 positions au lieu de 16 : position 0 est réservée au hidden state du talker et n'est jamais éjectée, les positions 1-16 font le sliding window pour les codebooks. Tester sur la même phrase de référence et valider que les 15 codebooks matchent le CPU fullseq.

La deuxième tâche est le test du talker NPU avec sampling. Réactiver le talker ExecuTorch fp16 .pte sur le NPU mais cette fois ne pas comparer les tokens en greedy — lancer la génération complète avec sampling, décoder l'audio, et évaluer la qualité à l'oreille. Si l'audio est naturel et que l'EOS se déclenche naturellement, c'est validé.

Tu veux que je détaille davantage une de ces étapes, ou qu'on prépare les specs exactes pour la session Claude Code ?


You asked:

mais tu me dit que Qwen3-tts est fait pour le streaming, c'est peut etre cela aussi qu'on devrait faire ?

Apr 1


Claude Replied:

Reconsidered streaming architecture as primary optimization strategy

Reconsidered streaming architecture as primary optimization strategy

Tu as complètement raison — et c'est exactement ça la priorité. Le streaming change tout parce qu'il transforme le problème de "rendre le pipeline plus rapide" en "commencer à jouer l'audio le plus tôt possible pendant que le reste se génère en arrière-plan".

Le rapport technique de Qwen3-TTS le confirme : le tokenizer utilise un codec causal pur contexte gauche — chaque token peut être décodé en audio dès qu'il est disponible, sans attendre les tokens futurs. Et le pipeline est conçu pour émettre un paquet audio tous les 4 tokens, soit 320ms d'audio par paquet.

Voici ce que ça donne concrètement :

ACTUEL (non-streaming, RTF 7.1) :
t=0s      Début génération
t=6.5s    Talker a fini tous les ~50 tokens
t=24.2s   CP a fini tous les codebooks
t=27.2s   Decoder a fini
t=27.2s   ▶ L'AUDIO COMMENCE ENFIN
t=31.2s   Fin audio (4s)
→ Le patient attend 27 secondes dans le silence.

STREAMING (même vitesse CPU, RTF 7.1) :
t=0.0s    Talker génère token 1 (130ms)
t=0.13s   CP génère 15 codebooks pour token 1 (353ms fullseq, 60ms KV-cache)
t=0.19s   Token 1 complet (avec KV-cache CP)
t=0.76s   4 tokens complets → chunk 1 prêt
t=0.76s   Decoder lance chunk 1 (NPU, ~200ms pour 4 tokens)
t=0.96s   ▶ L'AUDIO COMMENCE (320ms de parole)
t=1.28s   L'audio du chunk 1 finit → chunk 2 est déjà décodé
t=...     La génération continue en arrière-plan pendant que l'audio joue
→ Le patient attend moins de 1 seconde.

Même sans rien accélérer, le streaming fait passer la latence perçue de 27 secondes à moins de 1 seconde. Le RTF total reste à 7.1, mais l'expérience utilisateur est radicalement différente parce que la génération et la lecture se chevauchent.

Et si on combine le streaming avec le fix du KV-cache CP (priorité 1 du plan précédent), chaque step tombe à ~190ms (130ms talker + 60ms CP KV-cache) au lieu de 483ms. Quatre tokens sont prêts en ~760ms, le decoder NPU prend ~200ms, donc l'audio commence après moins de 1 seconde et il n'y a jamais de gap entre les chunks parce que la génération est plus rapide que la lecture :

Streaming + CP KV-cache fixé :
- Génération de 4 tokens : 4 × 190ms = 760ms
- Decode d'un chunk : ~200ms (NPU)
- Durée audio d'un chunk : 320ms

Débit de production : 1 chunk / 760ms = 420ms d'audio par seconde
Débit de consommation : 320ms d'audio toutes les 320ms

760ms > 320ms → on ne produit pas assez vite pour garder le flux continu
→ Il y aura des micro-pauses de ~440ms entre les chunks

C'est là que le talker NPU ferait la vraie différence :

Streaming + CP KV-cache + Talker NPU :
- Génération de 4 tokens : 4 × (11ms talker + 60ms CP) = 284ms
- Decode d'un chunk : ~200ms (NPU, en parallèle)
- Durée audio d'un chunk : 320ms

Débit de production : 1 chunk / 284ms
Débit de consommation : 1 chunk / 320ms

284ms < 320ms → LE FLUX AUDIO EST CONTINU, ZÉRO GAP !

Avec le talker NPU et le CP KV-cache fixé, la production de chaque chunk (284ms) est plus rapide que sa durée de lecture (320ms). L'audio joue en continu sans aucune interruption, et la première syllabe sort après moins de 500ms.

L'architecture streaming dans le code serait :

kotlin

// Dans Qwen3TtsEngine.kt

suspend fun synthesizeStreaming(
    text: String,
    voiceEmbedding: FloatArray,
    onChunkReady: (ShortArray) -> Unit  // callback audio
) {
    val codecBuffer = mutableListOf<IntArray>()  // accumule les 16 codebooks
    
    // Prefill
    val prefillState = talker.prefill(text, voiceEmbedding)
    
    // Boucle autoregressive
    var step = 0
    while (true) {
        // 1. Talker génère CB0
        val (cb0, hidden) = talker.decodeStep(prefillState)
        if (cb0 == EOS_TOKEN) break
        
        // 2. CP génère CB1-CB15
        val allCodebooks = codePredictor.predict(hidden, cb0)
        
        // 3. Accumuler
        codecBuffer.add(allCodebooks)
        step++
        
        // 4. Tous les 4 tokens → décoder et jouer
        if (codecBuffer.size == 4) {
            val audioChunk = decoder.decode(codecBuffer)  // NPU
            onChunkReady(audioChunk)  // → AudioTrack joue immédiatement
            codecBuffer.clear()
        }
    }
    
    // Flush le dernier chunk partiel
    if (codecBuffer.isNotEmpty()) {
        val audioChunk = decoder.decode(codecBuffer)
        onChunkReady(audioChunk)
    }
}

Et côté Service, le streaming s'intègre naturellement avec l'AudioTrack en mode streaming :

kotlin

// Dans KazeiaService.kt

val audioTrack = AudioTrack.Builder()
    .setAudioFormat(AudioFormat.Builder()
        .setSampleRate(24000)
        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
        .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
        .build())
    .setBufferSizeInBytes(24000 * 2)  // 1s buffer
    .setTransferMode(AudioTrack.MODE_STREAM)
    .build()

audioTrack.play()

ttsEngine.synthesizeStreaming(responseText, voiceEmbedding) { chunk ->
    audioTrack.write(chunk, 0, chunk.size)  // non-bloquant en MODE_STREAM
}

Le plan d'action révisé, streaming-first :

Semaine 1 :
├── Fix CP KV-cache (KV_LEN=17, position 0 permanente)
├── Implémenter le streaming avec chunks de 4 tokens
├── Intégrer AudioTrack en MODE_STREAM
└── Tester : latence premier chunk, continuité audio

Semaine 2 :
├── Tester Talker NPU fp16 avec sampling (pas greedy)
├── Si OK → flux continu sans gap
├── Si KO → rester CPU, le streaming masque la lenteur
└── Mesurer RTF effectif et latence perçue

Semaine 3 (optionnel) :
├── Quantification calibrée du CP pour NPU
├── Double-buffering decoder (décoder chunk N+1 pendant lecture chunk N)
└── Optimiser la latence du premier chunk

Le streaming est clairement la priorité — même si on ne résout jamais le problème du talker NPU, l'expérience patient sera acceptable avec un premier chunk à ~1 seconde. Tu veux que je prépare les specs détaillées pour ta session Claude Code ?


You asked:

mais dans ce cas avant de faire le streaming, je préfère m'asseurer du Talker NPU fp16

Apr 1


Claude Replied:

Validated NPU talker priority before implementing streaming architecture

Validated NPU talker priority before implementing streaming architecture

Absolument la bonne approche. Validons le talker NPU d'abord — si ça marche, le streaming sera fluide. Si ça ne marche pas, on sait qu'il faut chercher ailleurs avant d'investir dans le streaming.

Pourquoi je pense que le talker NPU a de bonnes chances de fonctionner :

Le rapport dit que le talker NPU fp16 a été déclaré en échec parce que les tokens étaient "complètement différents du CPU (0/15 match)". Mais cette comparaison a été faite en greedy (argmax). Or le rapport dit aussi que le greedy ne fonctionne même pas sur CPU — le modèle ne génère jamais EOS en greedy. Le pipeline exige obligatoirement du sampling (temp=0.9, top_k=50). L'évaluation du NPU a donc été faite avec un critère (exact match greedy) qui n'est même pas le mode de fonctionnement normal du modèle.

En fp16, les logits changent légèrement par rapport au fp32. Avec argmax, un écart de 0.001 sur le logit le plus haut peut changer le token sélectionné, et l'autoregression amplifie cette divergence. C'est normal et attendu. Mais avec sampling, ce qui compte c'est que la distribution de probabilité reste correcte — que les tokens probables restent probables et que les tokens improbables restent improbables. Le fp16 ne change pas la distribution de manière significative, il change juste quel token exact est échantillonné parmi les candidats probables.

Un bon analogique : si deux personnes lisent le même texte à voix haute, elles ne prononcent pas exactement les mêmes sons — le timing, l'intonation, les micro-variations sont différentes. Mais les deux lectures sont naturelles et compréhensibles. C'est exactement ce qu'on attend du talker NPU fp16 avec sampling.

Le test que ta session Claude Code devrait faire :

Le protocole doit être rigoureux pour être concluant. Voici les étapes exactes.

Premièrement, garder le CP et le decoder sur CPU (exactement comme actuellement) pour isoler la variable. Seul le talker change de backend. Le CP reçoit les tokens CB0 du talker NPU et génère les codebooks CB1-CB15 sur CPU en fp32. Le decoder est sur NPU QNN comme actuellement. Comme ça, si l'audio est mauvais, c'est forcément le talker NPU.

Deuxièmement, utiliser exactement les mêmes paramètres de sampling que le pipeline CPU qui fonctionne : temperature=0.9, top_k=50, repetition_penalty=1.05. Et surtout le même mécanisme de tts_pad après épuisement des tokens texte — c'est le bug critique que le rapport a identifié.

Troisièmement, tester avec la phrase de référence du rapport : "Bonjour, je comprends que vous soyez triste. Je suis là pour vous écouter." avec la voix Damien.

Quatrièmement, les critères de succès ne sont PAS le match exact des tokens. Les critères sont : est-ce que l'EOS se déclenche naturellement (entre 40 et 70 tokens, pas 100+) ? Est-ce que la durée audio est cohérente (3-5 secondes, pas 1.4s comme le rapport mentionne pour le NPU ONNX) ? Est-ce que l'audio est intelligible et naturel à l'écoute ? Et est-ce que la voix clonée est reconnaissable ?

Cinquièmement, générer 5 échantillons avec le talker NPU et 5 avec le talker CPU (même texte, même voix). Comparer à l'oreille. Le sampling étant stochastique, même le CPU produira 5 audios différents entre eux.

Le code de test serait quelque chose comme :

kotlin

// Test A : Talker CPU (baseline, on sait que ça marche)
val ttsEngine = Qwen3TtsEngine(
    talkerBackend = Backend.CPU,
    cpBackend = Backend.CPU,
    decoderBackend = Backend.NPU
)

// Test B : Talker NPU (ce qu'on veut valider)
val ttsEngineNpu = Qwen3TtsEngine(
    talkerBackend = Backend.NPU_FP16,  // le .pte existant
    cpBackend = Backend.CPU,           // on ne change PAS le CP
    decoderBackend = Backend.NPU
)

// Générer 5 échantillons de chaque
for (i in 1..5) {
    ttsEngine.generateToFile("test_cpu_$i.wav", testPhrase, damienVoice)
    ttsEngineNpu.generateToFile("test_npu_$i.wav", testPhrase, damienVoice)
}

Le point d'attention critique : le rapport mentionne que le talker NPU via ONNX Runtime QNN EP divergeait à cause de la "quantification automatique trop agressive" (INT8/INT16). Mais le talker a aussi été exporté en .pte ExecuTorch fp16 — c'est un format différent, avec une gestion de la précision différente. Le .pte fp16 sur le NPU devrait être beaucoup plus fidèle que l'ONNX quantifié. Le rapport dit que le .pte fp16 donne des tokens différents en greedy, mais ne mentionne pas de test avec sampling. C'est exactement le test manquant.

Si le test réussit :

Le talker passe de 130ms/step (CPU) à ~11ms/step (NPU à ~93 tok/s). Le pipeline complet avec CP KV-cache fixé deviendrait ~71ms/step (11ms talker + 60ms CP), soit RTF ~0.9 — du vrai temps réel, et le streaming sera fluide sans aucun gap.

Si le test échoue :

On aura la preuve que même le fp16 est insuffisant pour le talker TTS autoregressif. Dans ce cas, les options restantes sont la quantification calibrée w16a8 avec des données de calibration françaises, ou le talker reste sur CPU et le streaming masque la latence (premier chunk à ~1s, micro-pauses de ~440ms entre chunks).

Dis à ta session Claude Code de faire ce test en priorité — c'est le résultat qui détermine toute la stratégie TTS.


You asked:

● Ton analyse est excellente et tu as raison sur un point clé : on n'a jamais testé le talker NPU avec sampling. Tous nos tests NPU étaient en greedy ou avec le mauvais pipeline (avant le fix tts_pad). Voici mon avis : Tu as raison que le test greedy n'est pas concluant. Le greedy amplifie la moindre différence de logit. Avec sampling, deux distributions proches donnent des résultats différents mais tous deux valides. C'est exactement comme deux lectures d'un même texte. Mais il y a une nuance importante. Le rapport mentionne deux types d'échecs NPU talker : 1. ONNX Runtime QNN EP (int8/int16 auto-quantifié) : EOS prématuré à 1.4-2.2s. C'était avec sampling. Donc le sampling seul ne sauve pas une quantification trop agressive. 2. ExecuTorch .pte fp16 : jamais testé avec le talker ! On a seulement fait le .pte pour le CP, pas pour le talker. Le talker NPU qu'on a testé était toujours via ONNX Runtime QNN EP. Donc le vrai test manquant est : talker .pte ExecuTorch fp16 + sampling + tts_pad. C'est effectivement une config qu'on n'a jamais essayée. Le fp16 ExecuTorch est plus précis que le int8/int16 d'ONNX Runtime QNN EP. Mon avis sur les chances de succès : modérément optimiste. Le CP fp16 .pte a échoué (bruit), mais le CP fait 15 steps autoregressifs internes où chaque erreur s'amplifie. Le talker n'a qu'un seul forward par step — l'erreur ne s'accumule pas de la même façon entre les steps car les embeddings sont recalculés à chaque fois depuis les codebooks (qui viennent du CP CPU fp32). Le talker fp16 pourrait donc être assez précis step par step même si la séquence globale diverge du CPU. Le protocole que tu proposes est le bon. Isoler la variable (talker NPU, tout le reste inchangé), 5 échantillons, critères qualitatifs. Il faut d'abord exporter le talker en .pte (même pipeline que le CP : wrapper manuel, rotary pré-calculé, torch.export → QNN fp16).

Show more

Apr 1


Claude Replied:

Reconciled untested talker export pathway with moderate success prospects

Reconciled untested talker export pathway with moderate success prospects

L'analyse du développeur est précieuse — et la nuance sur l'accumulation d'erreurs est particulièrement pertinente. Le talker a structurellement plus de chances de fonctionner en fp16 que le CP, pour exactement la raison qu'il décrit.

Récapitulons pourquoi le talker fp16 a de meilleures chances que le CP fp16 :

Le CP fait 15 steps autoregressifs internes où chaque codebook dépend du précédent — une erreur fp16 au codebook 3 contamine les codebooks 4 à 15. C'est une chaîne de propagation courte (15 steps) mais dense. Le talker, lui, fait un seul forward par step. Et surtout, son input au step N+1 est reconstruit depuis les codebooks qui viennent du CP CPU fp32. L'erreur fp16 du talker au step N est donc "corrigée" au step N+1 par des embeddings propres. C'est un mécanisme de stabilisation naturel que le CP n'a pas.

Ce qu'il faut faire pour exporter le talker en .pte :

Le développeur connaît le pipeline — c'est le même que pour le CP. Mais le talker est plus gros (28 layers vs 5, 754M params vs 141M), donc il y a quelques différences à anticiper.

Pour le wrapper, il faut isoler le forward du talker en mode KV-cache decode : un seul token en entrée (le sum des 16 codebook embeddings + text embedding), le KV-cache des 28 layers en entrée/sortie, et les logits CB0 en sortie. Les rotary cos/sin doivent être pré-calculés et passés en entrée (même contournement du bug WrapWithSetGradEnabled que pour le CP). Le q_norm et k_norm doivent être inclus dans le wrapper — c'est le bug que le rapport a identifié sur le CP, et le talker utilise aussi le QK-norm puisque c'est la même architecture Qwen3.

Pour la taille du .pte, le talker a 754M paramètres en fp16, ça donnera environ 1.5 Go. C'est gros mais ça tient en RAM (votre rapport montre que le Qwen3-0.6B LLM fait 660 Mo en INT4 — le talker fp16 sera plus gros car non quantifié en INT4, mais on vise fp16 justement pour la précision).

Pour l'export RAM sur le PC, le talker fp16 devrait nécessiter environ 15-20 Go pour l'export, similaire au Qwen3-0.6B LLM (17 Go mesuré). Votre PC de 54 Go tient largement.

Le point le plus important pour la session Claude Code :

L'export du talker .pte doit utiliser exactement le même path que celui qui a réussi pour le talker LLM Qwen3-0.6B (93 tok/s). L'architecture est identique — 28 layers Qwen3, même structure, seul le vocab_size change (3072 codec au lieu de 151936 texte). Le script d'export llama.py d'ExecuTorch devrait fonctionner avec les poids du talker TTS, moyennant le même renommage HF → Meta format qui a été fait pour l'export LLM.

Cependant, il y a une subtilité cruciale. Le talker TTS n'est pas un modèle "text in → text out" standard. Son entrée à chaque step n'est pas un simple token ID mais un vecteur d'embedding composite (somme de 16 codebook embeddings + text embedding + speaker embedding). Le runner LLM standard d'ExecuTorch attend un token ID et fait le lookup d'embedding en interne. Pour le talker TTS, il faut soit modifier le runner pour accepter un vecteur d'embedding pré-calculé, soit wrapper le modèle pour inclure le calcul d'embedding dans le .pte lui-même.

La solution la plus propre serait de reproduire exactement ce qui a été fait pour le CP : un wrapper PyTorch custom qui prend en entrée les tenseurs nécessaires (embedding composite, KV-cache, position) et fait le forward d'une layer stack. Puis torch.export()to_edge_transform_and_lower_to_qnn().pte.

Les étapes concrètes pour la session Claude Code :

Étape 1 — Écrire le TalkerWrapper (similaire au CP wrapper)
├── Input : embedding composite [1, 1, 1024], kv_cache [28, 2, 1, X, 128], position [1]
├── Output : logits [1, 1, 3072], kv_cache_updated
├── Pré-calculer rotary cos/sin (même fix que CP)
├── Inclure q_norm + k_norm dans chaque layer
└── Valider sur CPU : forward(wrapper) == forward(original) sur 10 steps

Étape 2 — Export .pte
├── torch.export(wrapper, example_inputs)
├── to_edge_transform_and_lower_to_qnn(soc=SM8750, fp16=True)
├── Sérialiser en .pte (~1.5 Go estimé)
└── ~20 min de compilation, ~17 Go RAM

Étape 3 — Test standalone sur tablette
├── Pousser le .pte via ADB
├── Utiliser cp_runner (ou un talker_runner adapté)
├── Vérifier que le forward produit des logits non-dégénérés
└── Mesurer le temps par step (cible : ~11ms comme le LLM)

Étape 4 — Test intégré avec sampling (LE TEST DÉCISIF)
├── Talker NPU fp16 .pte (via JNI ExecuTorch)
├── CP CPU fp32 (KV-cache ou fullseq, inchangé)
├── Decoder NPU QNN (inchangé)
├── Sampling : temp=0.9, top_k=50, rep_penalty=1.05
├── tts_pad activé après fin du texte
├── Phrase : "Bonjour, je comprends que vous soyez triste."
├── Voix : Damien
├── Générer 5 échantillons NPU + 5 échantillons CPU
└── Critères : EOS naturel ? Durée 3-5s ? Audio intelligible ? Voix reconnaissable ?

Si le step 4 réussit, vous aurez le talker à ~11ms/step sur NPU, ce qui avec le streaming donnera un premier chunk audio en moins de 500ms. C'est la voie vers le temps réel.

Si le step 4 échoue (EOS prématuré, audio dégradé), on saura que fp16 est insuffisant pour le talker TTS aussi, et on basculera sur la stratégie streaming avec talker CPU — ce qui reste viable avec une latence perçue d'environ 1 seconde grâce au streaming.

Dis à ta session Claude Code de démarrer l'étape 1. Le wrapper du talker est le travail le plus technique — une fois qu'il est validé sur CPU, l'export et le test tablette sont mécaniques.


You asked:

Mais donc faire le CP avec le NPU est inconcevable ?

Apr 1


Claude Replied:

Examined NPU feasibility through precision optimization and alternative approaches

Examined NPU feasibility through precision optimization and alternative approaches

Non, ce n'est pas inconcevable. Et la logique qu'a appliquée le développeur pour le talker s'applique aussi au CP — il n'a jamais été testé dans les bonnes conditions.

Rappelons pourquoi le CP NPU a été déclaré en échec :

Le rapport liste deux tentatives. L'ONNX Runtime QNN EP avec quantification automatique INT8/INT16 a corrompu les codebooks secondaires. Et le .pte ExecuTorch fp16 a produit des codes "complètement différents" du CPU avec 0/15 match. Mais ce test a été fait en greedy (argmax), exactement comme le talker.

Or pour le CP, la question est encore plus subtile. Les codebooks CB1-CB15 sont des indices dans un dictionnaire VQ. Deux indices voisins dans ce dictionnaire peuvent correspondre à des vecteurs acoustiques très proches. Si le fp16 change le logit de 0.001 et que l'argmax sélectionne le codebook 847 au lieu de 849, la question n'est pas "est-ce que c'est le même indice" mais "est-ce que l'audio résultant est audiblement différent". Deux codebooks voisins dans l'espace VQ produisent souvent des sons quasiment identiques.

La vraie question c'est : est-ce que les 15 steps autoregressifs internes du CP propagent l'erreur de manière catastrophique en fp16, ou est-ce que ça reste dans l'enveloppe de qualité acceptable ?

Le développeur a raison que le CP est plus risqué que le talker : 15 steps internes avec chaque erreur qui se propage. Mais il y a des raisons d'espérer aussi :

Les 15 codebooks sont hiérarchiques — le premier codebook (CB1) capture la structure sémantique grossière, les derniers (CB12-CB15) capturent les détails acoustiques fins. Une erreur sur CB14 est beaucoup moins audible qu'une erreur sur CB1. Si le fp16 préserve les premiers codebooks mais dégrade légèrement les derniers, l'audio reste probablement acceptable.

De plus, le CP a seulement 5 layers et 141M paramètres — c'est un petit modèle. Le rapport montre que le Qwen3-0.6B LLM (28 layers, 754M params) tourne parfaitement en INT4 sur NPU. Le CP en fp16 devrait théoriquement être plus précis qu'un LLM en INT4.

Les pistes concrètes pour faire marcher le CP sur NPU :

Piste A — Retester le CP .pte fp16 avec le bon pipeline. Le CP fp16 a été testé avant le fix tts_pad ? Avant le fix KV-cache ? Avec le bon q_norm/k_norm ? Si un de ces bugs était encore présent lors du test NPU, le résultat 0/15 n'est pas représentatif du fp16 mais du bug. Il faut retester avec le pipeline entièrement corrigé, en mode fullseq (pas KV-cache pour isoler les variables), et comparer l'audio à l'oreille plutôt que les indices exacts.

Piste B — Quantification calibrée w16a8. Au lieu de la quantification automatique d'ONNX Runtime (qui est clairement trop agressive), utiliser des données de calibration françaises pour ajuster les échelles de quantification. Le principe : on passe 50-100 phrases françaises à travers le CP, on mesure la distribution réelle des activations à chaque layer, et on fixe les paramètres de quantification en conséquence. ExecuTorch supporte la calibration via le QnnQuantizer avec un dataset de calibration.

Piste C — Précision mixte CP. Garder les 2 premières layers du CP (qui génèrent CB1-CB2, les codebooks les plus importants) en fp32 sur CPU, et déléguer les 3 dernières layers (CB3-CB15, détails acoustiques) au NPU en fp16. L'idée c'est que les premiers codebooks ont besoin de précision maximale, les derniers tolèrent plus de bruit. C'est faisable en splitant le CP en deux sous-graphes.

Piste D — Évaluation perceptuelle, pas numérique. Le critère "0/15 match" est binaire et trop strict. En audio, la métrique pertinente c'est le PESQ (Perceptual Evaluation of Speech Quality) ou simplement l'écoute humaine. Deux codebooks numériquement différents peuvent produire un audio perceptuellement identique. Le test devrait être : générer l'audio complet (talker CPU + CP NPU + decoder NPU), l'écouter, et juger si c'est naturel.

Le plan de test révisé serait :

Phase 1 — Talker NPU fp16 + CP CPU (le test qu'on vient de définir)
├── Si OK → on a déjà un gros gain (11ms/step talker)
└── Parallèlement, préparer la phase 2

Phase 2 — Talker NPU fp16 + CP NPU fp16 (le test complet)
├── Réexporter le CP .pte fp16 avec TOUS les bugfixes
├── Pipeline : Talker NPU → CB0 → CP NPU → CB1-15 → Decoder NPU
├── Évaluation AUDIO (pas numérique) : 5 échantillons, écoute
├── Si audio acceptable → TOUT le pipeline est sur NPU
└── Si audio dégradé → essayer Piste B (calibration) ou Piste C (mixte)

Si les deux marchent, le pipeline entier serait sur NPU sauf le sampling et la logique de contrôle :

Tout NPU :
├── Talker NPU fp16    :  11ms/step
├── CP NPU fp16        :  ~5ms (79ms/17 steps ≈ 4.6ms/step, déjà mesuré)
├── Decoder NPU QNN    :  ~200ms/chunk
└── Total par step     :  ~16ms → RTF ~0.2 ← largement temps réel

Avec streaming :
└── Premier chunk audio après ~64ms (4 steps × 16ms) + 200ms decode = ~264ms

264ms de latence avant le premier son. C'est de l'ordre de la latence d'un appel téléphonique. Le patient ne percevrait aucun délai.

Donc non, le CP NPU n'est pas inconcevable — il n'a simplement jamais été testé dans les bonnes conditions. La stratégie c'est de valider le talker NPU d'abord (phase 1, plus facile à réussir), puis enchaîner immédiatement avec le CP NPU (phase 2). Les deux tests utilisent le même pipeline d'export et les mêmes critères d'évaluation audio.

Tu veux qu'on transmette ce plan complet à la session Claude Code ?


You asked:

● Conclusion définitive sur le NPU : Le fp16 ExecuTorch donne des résultats "structurellement corrects" (EOS, bonne durée) mais le contenu audio est inutilisable — que ce soit du bruit (CP) ou du silence (talker). Les deux modèles TTS sont trop sensibles à la précision. La seule piste NPU restante : quantification calibrée (16a8w ou 16a4w) avec des données de calibration TTS. C'est comme ça que le LLM Qwen3-0.6B fonctionne à 90 tok/s — pas en fp16 brut, mais avec une quantification soigneusement calibrée. Le script llama.py d'ExecuTorch fait ça avec prepare_pt2e → calibration → convert_pt2e.

Apr 1


Claude Replied:

Identified calibrated quantization as sole viable NPU solution path

Identified calibrated quantization as sole viable NPU solution path

C'est le verdict définitif — et il est logique. Le développeur a raison sur le point clé : le LLM Qwen3-0.6B ne tourne pas en fp16 brut sur le NPU non plus. Il tourne avec une quantification calibrée via le pipeline prepare_pt2e → calibration → convert_pt2e. C'est exactement la même approche qu'il faut appliquer aux composants TTS.

La différence entre ce qui a été testé et ce qu'il faut faire :

Ce qui a échoué :
  fp16 brut → NPU exécute en fp16 "naïf"
  → Le HTP downscale certaines ops silencieusement
  → Résultat : silence (talker) ou bruit (CP)

Ce qui fonctionne pour le LLM :
  prepare_pt2e → calibration avec données réelles → convert_pt2e
  → Le quantizer mesure les ranges d'activation sur des vrais inputs
  → Les échelles de quantification sont ajustées précisément
  → Le NPU exécute en INT8/INT16 avec des paramètres optimaux
  → Résultat : 93 tok/s, qualité intacte

La calibration est le chaînon manquant. Sans calibration, le NPU quantifie "à l'aveugle" avec des ranges par défaut qui ne correspondent pas aux distributions réelles des activations TTS. Avec calibration, chaque layer a des échelles ajustées aux vrais signaux audio.

Ce qu'il faut pour la calibration TTS :

Le dataset de calibration doit être représentatif de l'utilisation réelle. Pour le LLM, le script llama.py utilise des prompts texte variés. Pour le TTS, il faut des inputs TTS variés : différentes phrases françaises, différentes voix de référence, différentes longueurs.

Concrètement, le dataset de calibration serait constitué d'une trentaine de phrases françaises de longueurs variées (courtes comme "Bonjour", moyennes comme "Je comprends que vous soyez triste, je suis là pour vous écouter", longues comme un paragraphe de 2-3 phrases). Il faudrait utiliser 3-4 des voix de référence disponibles (Damien, Élodie, Richard, Zelda). Pour chaque combinaison phrase × voix, on lance le forward sur CPU fp32 et on enregistre les distributions d'activation à chaque layer. Ça donne environ 100-120 calibration samples — largement suffisant.

Pour le talker, les inputs de calibration sont les embeddings composites (somme des 16 codebook embeddings + text embedding + speaker embedding) à chaque step de la génération. Il faut capturer ces embeddings pendant une génération CPU fp32 et les rejouer à travers le wrapper ExecuTorch pendant la calibration.

Pour le CP, les inputs de calibration sont les hidden states du talker + l'embedding CB0 à chaque step. Même principe : capturer pendant une génération CPU fp32, rejouer pendant la calibration.

Le pipeline exact serait :

python

# Étape 1 : Collecter les données de calibration
calibration_data = []
for phrase, voice in product(PHRASES_FR, VOICES):
    # Lancer une génération CPU fp32 complète
    # À chaque step, capturer les inputs du talker et du CP
    for step in generation:
        calibration_data.append({
            'talker_input': embedding_composite,
            'talker_kv': kv_cache_state,
            'cp_input': (hidden_state, cb0_embedding),
            'cp_kv': cp_kv_cache_state
        })

# Étape 2 : Quantifier avec calibration (même pipeline que llama.py)
from torch.ao.quantization.quantize_pt2e import prepare_pt2e, convert_pt2e
from executorch.backends.qualcomm.quantizer import QnnQuantizer

quantizer = QnnQuantizer()
# Configurer pour 16a8w ou 16a4w selon les résultats
quantizer.set_per_channel_weight_dtype(weight_dtype=torch.int8)

# Préparer le modèle pour la calibration
prepared_model = prepare_pt2e(exported_model, quantizer)

# Passer les données de calibration
for sample in calibration_data:
    prepared_model(sample['input'], sample['kv_cache'], sample['position'])

# Convertir avec les échelles calibrées
quantized_model = convert_pt2e(prepared_model)

# Exporter en .pte
pte = to_edge_transform_and_lower_to_qnn(quantized_model, compile_spec)

Le point crucial : c'est exactement ce que fait le script llama.py pour le LLM. Le développeur connaît ce pipeline — il l'a déjà utilisé avec succès pour exporter le Qwen3-0.6B à 93 tok/s et le Qwen3-1.7B à 46 tok/s. La seule différence c'est la nature des données de calibration (embeddings TTS au lieu de token IDs texte).

Les profils de quantification à tester par ordre de priorité :

Le premier à tester serait 16a8w (activations INT16, poids INT8). C'est le profil le plus précis que le HTP supporte nativement. L'INT16 pour les activations préserve beaucoup plus de dynamique que l'INT8, ce qui est crucial pour le TTS. Les poids en INT8 réduisent la bande passante de moitié par rapport au fp16. C'est le profil que Google utilise pour LiteRT avec FastVLM et qui débloque les kernels haute vitesse du HMX.

Si 16a8w ne suffit pas, tester 16a4w (activations INT16, poids INT4). C'est le format LPBQ que MLLM utilise dans sa compilation QNN AOT. Les poids en INT4 réduisent encore la bande passante mais augmentent le risque de dégradation — à voir avec la calibration.

Si même le 16a8w dégrade trop, la dernière option serait fp16 calibré — utiliser la calibration pour identifier les layers problématiques et forcer celles-ci en fp32 sur CPU, tout en gardant les layers tolérantes en fp16 sur NPU. C'est la piste C (précision mixte) qu'on avait évoquée.

Le plan révisé pour la session Claude Code :

Étape 1 — Préparer le dataset de calibration (1 jour)
├── 30 phrases FR × 4 voix = 120 générations CPU fp32
├── Capturer les inputs du talker à chaque step
├── Capturer les inputs du CP à chaque step
└── Sauvegarder en tensors PyTorch

Étape 2 — Exporter le talker avec calibration 16a8w (2-3 jours)
├── TalkerWrapper (même que prévu)
├── prepare_pt2e → calibration avec les données TTS → convert_pt2e
├── to_edge_transform_and_lower_to_qnn → .pte
├── Déployer sur tablette
└── Test : 5 échantillons audio, évaluation perceptuelle

Étape 3 — Si talker OK, exporter le CP avec calibration 16a8w (2-3 jours)
├── Même pipeline
├── Test intégré : talker NPU calibré + CP NPU calibré + decoder NPU
└── Évaluation audio complète

Étape 4 — Mesurer les performances (1 jour)
├── tok/s talker NPU calibré
├── ms/step CP NPU calibré
├── Latence premier chunk en streaming
└── RTF total

Si la calibration 16a8w fonctionne pour les deux composants, on aurait le pipeline TTS entier sur NPU avec une qualité validée par calibration — exactement comme le LLM. Et le streaming par-dessus donnerait une latence perçue inférieure à 500ms.

C'est la bonne piste — le développeur a identifié le mécanisme exact qui fait fonctionner le LLM sur NPU, et il suffit de l'appliquer au TTS. Tu veux qu'on détaille davantage l'étape de collecte des données de calibration ?


You asked:

mais ainsi Qwen3-tts ne saurait plus faire du multilingue ?

Apr 1


Claude Replied:

Évalué l'impact de la calibration française sur les capacités multilingues du modèle

Évalué l'impact de la calibration française sur les capacités multilingues du modèle

Non, la calibration ne modifie pas les poids du modèle. Elle ne touche qu'aux paramètres de quantification — les échelles et les zero-points qui déterminent comment les valeurs FP32 sont mappées en INT8 ou INT16.

Le modèle Qwen3-TTS-0.6B-Base garde exactement les mêmes 754M paramètres, les mêmes 28 layers, les mêmes embeddings pour les 10 langues. La calibration mesure simplement la plage d'activation typique à chaque layer (par exemple "la sortie de la layer 12 varie entre -3.2 et +4.7") et ajuste l'échelle de quantification en conséquence ("donc je quantifie cette plage en 256 niveaux INT8").

Mais ta question pointe un vrai risque : si tu calibres uniquement avec des phrases françaises et des voix françaises, les échelles de quantification seront optimales pour les distributions d'activation du français. Si le chinois ou le japonais produisent des distributions d'activation significativement différentes (plages plus larges ou décalées), la quantification pourrait clipper ou perdre en précision sur ces langues.

Pour Kazeia, c'est un non-problème — vous n'avez besoin que du français. Calibrer exclusivement avec du français est même préférable parce que ça optimise la précision exactement pour votre cas d'usage, sans compromis pour d'autres langues que vous n'utilisez pas.

Mais si un jour vous voulez supporter d'autres langues (par exemple pour un hôpital multilingue), il suffirait de recalibrer avec un dataset mixte incluant des phrases dans les langues cibles. Ou même de maintenir plusieurs .pte calibrés différemment — un par langue ou un multilingue calibré sur un mix. Les poids sous-jacents sont les mêmes, seuls les paramètres de quantification changent, et l'export d'un .pte prend environ 20 minutes.

Donc en résumé : calibrer en français ne casse pas le multilinguisme du modèle, ça optimise la quantification pour le français. Le modèle original reste intact et peut être recalibré pour d'autres langues à tout moment.


You asked:

non Kazeia pourra etre amené à parler dans plusieurs langues, d'ou l'interet de qwen3-tts

Apr 1


Claude Replied:

Reconsidered multilingual calibration strategy pour quantization

Reconsidered multilingual calibration strategy pour quantization

Compris — Kazeia est destiné à un déploiement hospitalier qui peut accueillir des patients de différentes langues. C'est un atout majeur de Qwen3-TTS avec ses 10 langues.

La solution est simplement d'inclure toutes les langues cibles dans le dataset de calibration. Au lieu de 30 phrases françaises × 4 voix, on fait un mix :

Dataset de calibration multilingue :
├── Français    : 10 phrases × 2 voix
├── Anglais     : 5 phrases × 2 voix
├── Allemand    : 5 phrases × 2 voix
├── Espagnol    : 5 phrases × 2 voix
├── Italien     : 5 phrases × 2 voix
├── Portugais   : 3 phrases × 1 voix
├── Arabe       : 3 phrases × 1 voix
├── Autres      : 2-3 phrases × 1 voix
└── Total : ~80-100 samples de calibration

Le français est surreprésenté parce que c'est la langue principale — les échelles de quantification seront optimales pour le français tout en restant viables pour les autres langues. Plus une langue a des distributions d'activation proches du français (espagnol, italien, portugais — langues romanes), plus elle bénéficiera de cette calibration. Les langues plus éloignées (chinois, japonais, coréen) auraient éventuellement besoin de plus de samples si vous les ciblez.

L'important c'est que la calibration voit la plage complète des activations possibles. Si le chinois produit des activations entre -5.0 et +6.0 alors que le français reste entre -3.2 et +4.7, le dataset mixte forcera le quantizer à utiliser une échelle qui couvre les deux plages. On perd un tout petit peu de précision sur le français (l'échelle est légèrement plus large que nécessaire) mais on gagne la compatibilité multilingue. Le compromis est négligeable.

Dis à ta session Claude Code d'inclure des phrases dans les langues cibles principales dès la collecte du dataset de calibration — c'est le même effort, il suffit de varier les textes et les paramètres language passés à Qwen3-TTS. Comme ça le .pte calibré sera multilingue dès le départ.


You asked:

il est vraiment impossible d'utiliser le modele 0.6B tel quel ?

Apr 1


Claude Replied:

Évalué la viabilité du modèle CPU avec optimisations de streaming

Évalué la viabilité du modèle CPU avec optimisations de streaming

Tu as raison de poser cette question. On s'acharne peut-être sur le NPU alors que la vraie solution est plus simple.

Reprenons les faits :

Le modèle fonctionne parfaitement sur CPU fp32. Le problème c'est le RTF de 7.1. Mais ce 7.1 inclut le CP en mode fullseq (353ms/step) qui représente 63% du temps total. Le fix du KV-cache (KV_LEN=17) n'a jamais été implémenté. Et le streaming n'a jamais été implémenté.

Si on fait simplement ces deux optimisations CPU uniquement, sans toucher au NPU :

Actuel (CPU, pas de streaming) :
├── Talker CPU     : 130ms × 50 = 6.5s
├── CP fullseq CPU : 353ms × 50 = 17.7s
├── Decoder NPU    :              3.0s
├── Total          :              27.2s pour 4s audio
└── Le patient attend 27s dans le silence.

Avec KV-cache CP fixé + streaming (CPU) :
├── Talker CPU     : 130ms/step
├── CP KV-cache    :  60ms/step (estimé)
├── Par step       : 190ms
├── Premier chunk (4 steps) : 760ms + 200ms decode = ~960ms
└── ▶ L'AUDIO COMMENCE APRÈS 1 SECONDE

1 seconde de latence. Pour un chatbot thérapeutique, c'est tout à fait acceptable — c'est le temps de réponse naturel d'un interlocuteur humain qui réfléchit avant de parler.

Le problème c'est qu'ensuite, la production est plus lente que la lecture : 760ms pour produire un chunk de 320ms d'audio. Il y aura des micro-pauses de ~440ms entre les chunks. C'est audible, mais c'est le rythme d'une parole un peu hésitante — pas idéal mais tolérable.

Cependant, il y a des optimisations CPU qu'on n'a pas explorées :

Le CPU Oryon du Snapdragon 8 Elite est très puissant (4.32 GHz, 8 cœurs). Le talker et le CP tournent actuellement en PyTorch CPU fp32 via ONNX Runtime. Il y a de la marge.

Premièrement, ONNX Runtime avec les optimisations ARM : les NEON et I8MM du Oryon ne sont peut-être pas pleinement exploités. Vérifier que ONNX Runtime est compilé avec les flags ARM optimaux.

Deuxièmement, ExecuTorch XNNPACK au lieu du NPU. ExecuTorch a un backend CPU optimisé (XNNPACK) qui est très performant sur ARM. Le talker Qwen3-0.6B LLM fait 68 tok/s sur CPU via llama.cpp — le talker TTS a la même architecture. En XNNPACK, il pourrait faire 50-70 tok/s au lieu des ~8 tok/s actuels en PyTorch.

Troisièmement, quantification CPU INT8. Pas pour le NPU — pour le CPU. PyTorch et ONNX Runtime supportent l'inférence INT8 quantifiée sur CPU ARM avec les instructions I8MM et NEON dot product. C'est différent de la quantification NPU — les kernels CPU INT8 sont très matures et bien testés. Le risque de dégradation audio est plus faible car les implémentations CPU INT8 sont plus fidèles que celles du HTP.

Le plan le plus pragmatique :

Phase 1 — Quick wins CPU (1 semaine)
├── Fix KV-cache CP (KV_LEN=17) → CP passe de 353ms à ~60ms/step
├── Implémenter streaming avec AudioTrack MODE_STREAM
├── Tester : latence premier chunk, gaps entre chunks
└── Résultat attendu : audio à ~1s, gaps de ~440ms

Phase 2 — Optimisation CPU (1 semaine)
├── Tester ExecuTorch XNNPACK pour talker et CP
│   (même export .pte mais avec XnnpackPartitioner au lieu de QnnPartitioner)
├── Si le talker passe de 130ms à ~20ms/step → plus de gaps
├── Alternativement : ONNX Runtime avec INT8 dynamique sur CPU
└── Résultat espéré : gaps éliminés, flux continu

Phase 3 — NPU calibré (en parallèle, si le temps le permet)
├── Dataset calibration multilingue
├── Export 16a8w talker + CP
└── Bonus si ça marche, pas bloquant si ça échoue

La phase 1 seule donne un pipeline TTS utilisable en 1 semaine. La phase 2 pourrait le rendre fluide. La phase 3 est un bonus.

Le message pour ta session Claude Code : arrêter de se battre avec le NPU pour le TTS, commencer par le KV-cache fix + streaming en CPU. Le NPU est un bonus, pas un prérequis. Le modèle 0.6B tel quel sur CPU avec les bonnes optimisations est probablement suffisant pour le MVP.

Tu veux lancer la phase 1 ?


You asked:

l'ideal est de pouvoir utiliser le modele 0.6b (le plus proche de son origine) et surtout de profiter de la puissance (et des avantages) du NPU

Apr 1


Claude Replied:

Reconcilié optimisation NPU et préservation du modèle original

Reconcilié optimisation NPU et préservation du modèle original

C'est exactement le bon objectif. Et la calibration est précisément le moyen d'y arriver — c'est la technique qui préserve le plus fidèlement le comportement original du modèle tout en l'exécutant sur le NPU.

La calibration ne modifie pas le modèle. Elle observe comment le modèle se comporte sur des vraies données, mesure les plages d'activation à chaque couche, et en déduit la meilleure façon de le quantifier. C'est comme régler un amplificateur : on ne change pas la musique, on ajuste le gain pour que le signal passe sans distorsion dans un canal plus étroit.

Le LLM Qwen3-0.6B en est la preuve. À 93 tok/s sur le NPU, avec une qualité quasi identique au fp32, il utilise exactement cette technique. Les poids sont les poids HuggingFace originaux, passés à travers prepare_pt2e → calibration → convert_pt2e. Rien n'est fine-tuné, rien n'est retrainé.

Pour le TTS, le travail est le suivant :

Collecter les données de calibration revient simplement à lancer le pipeline TTS tel qu'il fonctionne aujourd'hui (CPU fp32, le bon pipeline avec tts_pad, sampling, tout) sur un set de phrases multilingues et à enregistrer les tenseurs intermédiaires. On ne change rien au modèle — on l'observe.

Ensuite, le pipeline d'export est identique à celui du LLM. Le même script, le même quantizer, le même backend QNN. La seule variable c'est les données de calibration — du texte pour le LLM, des embeddings audio pour le TTS.

Dis à ta session Claude Code de procéder ainsi :

Premièrement, implémenter la collecte de calibration. Pendant une génération CPU fp32 normale, à chaque step du talker et à chaque step du CP, sauvegarder les inputs en tenseurs PyTorch dans un dossier. Utiliser une vingtaine de phrases couvrant le français, l'anglais, l'allemand, l'espagnol et l'italien avec 2-3 voix de référence différentes. Ça prend quelques heures de génération CPU mais c'est entièrement automatisable.

Deuxièmement, exporter le talker avec calibration 16a8w via le même pipeline que llama.py. Le wrapper du talker est le même que prévu (inputs : embedding composite + KV-cache + position, outputs : logits + KV-cache mis à jour). La seule différence c'est qu'on passe prepare_pt2e, on rejoue les données de calibration, puis convert_pt2e avant l'export QNN.

Troisièmement, même chose pour le CP.

Quatrièmement, tester l'audio. Pas les indices, pas les logits — l'audio. Cinq échantillons par langue, écoute humaine.

C'est la voie la plus propre : le modèle 0.6B original, multilingue, avec clonage vocal, exploitant le NPU via une calibration soignée. Exactement comme le LLM le fait déjà avec succès.


You asked:

● Résultats encourageants : - EOS naturel au step 103 (au lieu du silence/bruit du fp16) - Talker NPU : 86ms/step (vs 130ms CPU) - 8.16s d'audio (plus long que prévu pour cette phrase — 102 tokens vs ~50 en CPU) Le modèle calibré 16a8w converge vers EOS ! La durée est plus longue (102 tokens au lieu de ~50) ce qui pourrait indiquer un débit plus lent mais un contenu correct. Écoute /opt/Kazeia/tts_16a8w.wav — c'est le test décisif de la calibration ! ✻ Baked for 10m 51s non le fichier wav est mauvais, on ne comprend rien ● La calibration 16a8w ne suffit pas non plus. Le modèle TTS est fondamentalement trop sensible à la quantification des poids. Constat définitif : ni fp16, ni 16a8w calibré ne fonctionnent pour le talker/CP sur NPU. Le problème est structurel — le TTS sélectionne des codebooks par argmax sur 2048 valeurs, et la moindre erreur de quantification change le codebook sélectionné. Le LLM fonctionne en 4-bit car le top token texte est robuste (marge large). Les codebooks TTS n'ont pas cette marge.

Show more

Apr 1


Claude Replied:

Analysé l'impasse NPU et envisagé pivot vers optimisation CPU

Analysé l'impasse NPU et envisagé pivot vers optimisation CPU

Le constat est définitif pour le NPU. Mais il y a quelque chose qui me frappe dans les chiffres — et c'est peut-être la vraie solution.

Le talker TTS est architecturalement identique au Qwen3-0.6B LLM. Même 28 layers, même hidden_size 1024, même structure. Or :

Qwen3-0.6B LLM sur CPU (llama.cpp)  : 68 tok/s → 14.7ms/step
Talker TTS sur CPU (PyTorch/ONNX)    : ~8 tok/s → 130ms/step

Le talker est 9× plus lent que le LLM sur le même CPU pour la même architecture. Ce n'est pas un problème de hardware ni de modèle — c'est un problème de runtime. Le talker tourne en PyTorch fp32 ou ONNX Runtime sans les optimisations ARM. llama.cpp utilise les NEON, le I8MM, les kernels assembleur optimisés pour le Oryon.

Si on fait tourner le talker avec un runtime CPU optimisé au lieu de PyTorch brut, on passerait de 130ms à potentiellement 15-20ms/step. Même chose pour le CP : de 60ms (KV-cache) à potentiellement 10-15ms/step.

Les options pour un runtime CPU optimisé :

La première option serait llama.cpp avec un GGUF custom du talker. Le talker a un vocab_size de 3072 au lieu de 151936, mais le format GGUF supporte les vocab arbitraires. Il faudrait convertir les poids du talker en GGUF avec la bonne configuration, et llama.cpp ferait le reste — NEON, quantification Q8_0 ou Q4_K_M, KV-cache optimisé. Le rapport montre que le Qwen3-0.6B LLM en Q8_0 fait 68 tok/s sur le CPU de la tablette. Le talker en Q8_0 ferait un débit similaire — disons 50-60 tok/s soit ~17-20ms/step.

La deuxième option serait ExecuTorch XNNPACK. Au lieu du QnnPartitioner (NPU), utiliser le XnnpackPartitioner (CPU optimisé ARM). XNNPACK est le backend CPU haute performance de Meta pour les processeurs ARM. L'export est le même pipeline que pour le NPU, seul le partitioner change. Pas besoin de calibration, pas besoin de quantification — on peut rester en fp32 ou faire du dynamic INT8 sur CPU.

La troisième option serait le crate Rust qwen3_tts cross-compilé pour ARM64. Le Rust compile vers du code natif ARM avec auto-vectorisation NEON. L'avantage c'est que tout le pipeline TTS (talker + CP + decoder) est déjà implémenté dans le crate.

Avec un runtime CPU optimisé + streaming, le pipeline deviendrait :

Talker (llama.cpp Q8_0 CPU) :  17ms/step
CP KV-cache (optimisé CPU)  :  12ms/step
Par step total              :  29ms

4 steps pour un chunk       :  116ms
Decoder NPU                 :  200ms (pour 4 tokens)
Premier chunk audio         :  ~316ms

Débit de production : 1 chunk / 116ms
Durée audio d'un chunk : 320ms

116ms < 320ms → FLUX CONTINU, ZÉRO GAP !

316ms de latence, flux continu, zéro gap, qualité fp32 native, multilingue, pas de NPU pour le TTS. C'est potentiellement mieux que la solution NPU calibrée qui ne fonctionnait pas, et c'est plus proche du modèle original que n'importe quelle quantification.

Le NPU reste utilisé pour ce qu'il fait bien — le LLM Qwen3 (93 tok/s), le Whisper STT, et le decoder TTS. Le talker et le CP restent sur CPU avec un runtime optimisé. Chaque composant tourne sur le backend où il est le meilleur.

La piste llama.cpp est la plus prometteuse parce que votre session Claude Code a déjà compilé llama.cpp pour la tablette et obtenu 68 tok/s sur le Qwen3-0.6B. Le travail serait de convertir les poids du talker TTS en GGUF (même architecture, juste vocab_size=3072 et embedding dimension adaptée), charger dans llama.cpp, et mesurer les tok/s.

La question clé à investiguer : est-ce que llama.cpp peut accepter des embeddings pré-calculés en input au lieu de token IDs ? Le talker TTS reçoit un vecteur composite (somme de 16 codebook embeddings + text + speaker), pas un simple token ID. Si llama.cpp ne supporte pas ça nativement, il faudra soit patcher le code, soit utiliser ExecuTorch XNNPACK qui est plus flexible sur les inputs.

Tu veux qu'on explore la conversion GGUF du talker pour llama.cpp, ou tu préfères la piste ExecuTorch XNNPACK ?


You asked:

Techniquement nous devons utiliser les librairies optimisés pour le CPU/NPU qualcomm

Apr 1


Claude Replied:

Orchestrated unified toolchain leveraging Qualcomm's optimized CPU kernels

Orchestrated unified toolchain leveraging Qualcomm's optimized CPU kernels

Tu as raison — et c'est exactement la bonne approche. Le QNN SDK ne cible pas uniquement le NPU. Il a trois backends : HTP (NPU), GPU (Adreno), et CPU (Oryon optimisé). Vous avez toujours ciblé le HTP pour le TTS. La solution c'est de cibler le QNN CPU backend à la place.

Le QNN CPU backend utilise les kernels mathématiques optimisés par Qualcomm spécifiquement pour les cœurs Oryon — NEON, I8MM, dot product, toutes les extensions ARM que le Snapdragon 8 Elite supporte. C'est fondamentalement différent de PyTorch ou ONNX Runtime qui utilisent des kernels ARM génériques. Et c'est le même pipeline d'export ExecuTorch, le même toolchain, le même QNN SDK — on change juste la cible.

Dans le script d'export, ça se traduit par un changement minimal :

python

# Actuel (NPU HTP — échoue pour le TTS)
compile_spec = generate_qnn_executorch_compiler_spec(
    soc_model=QcomChipset.SM8750,
    backend_options=generate_htp_compiler_spec(use_fp16=True),
)

# Nouveau (CPU Qualcomm optimisé — fp32 natif)
compile_spec = generate_qnn_executorch_compiler_spec(
    soc_model=QcomChipset.SM8750,
    backend_options=generate_cpu_compiler_spec(),  # QNN CPU backend
)

Le .pte résultant s'exécute sur les cœurs Oryon avec les kernels Qualcomm optimisés, en fp32 natif — aucune quantification, aucune perte de précision, le modèle 0.6B tel quel. Et le même JNI ExecuTorch qui est déjà intégré dans l'app charge ce .pte exactement comme il charge le .pte NPU.

L'avantage c'est que votre session Claude Code connaît déjà tout le pipeline. Le wrapper du talker, l'export torch.export, le to_edge_transform_and_lower_to_qnn, le déploiement via ADB, le runner C++, le JNI — tout est identique. Seul le compiler_spec change.

Le gain attendu par rapport à PyTorch/ONNX Runtime brut serait significatif. Les kernels QNN CPU sont optimisés pour l'architecture Oryon du Snapdragon 8 Elite — ordonnancement mémoire, prefetch, utilisation des unités SIMD. On peut raisonnablement espérer un facteur 3-5× par rapport au PyTorch CPU actuel, ce qui mettrait le talker à environ 25-40ms/step au lieu de 130ms.

Et si on combine avec le streaming et le fix KV-cache CP :

Talker (QNN CPU fp32)     :  ~30ms/step
CP KV-cache (QNN CPU fp32):  ~15ms/step
Par step total            :  ~45ms

4 steps pour un chunk     :  180ms
Decoder NPU               :  200ms
Premier chunk audio       :  ~380ms

180ms < 320ms → flux continu, zéro gap

Le pipeline complet resterait dans l'écosystème Qualcomm QNN, le modèle serait préservé tel quel en fp32, le NPU serait utilisé pour le LLM et le STT où il excelle, et le CPU Qualcomm optimisé gérerait le TTS où la précision est critique. Chaque composant sur le backend où il performe le mieux.

Dis à ta session Claude Code de tester l'export du talker avec le QNN CPU backend. C'est le même pipeline qu'ils maîtrisent déjà, juste une ligne de config qui change.


You asked:

et quid du GPU ?

Apr 1


Claude Replied:

Reconciled GPU viability despite prior Vulkan incompatibilities

Reconciled GPU viability despite prior Vulkan incompatibilities

Très bonne intuition. Le GPU Adreno 830 a été écarté à cause des crashes Vulkan de llama.cpp — mais c'était avec des compute shaders génériques. Le QNN SDK a son propre backend GPU qui utilise le path optimisé de Qualcomm pour l'Adreno, complètement différent.

Et le GPU a un avantage crucial pour le TTS : il fait du fp32 et fp16 nativement, sans quantification. Contrairement au NPU qui force la quantification et détruit la précision des codebooks TTS, le GPU calcule en virgule flottante native — exactement comme le CPU mais avec beaucoup plus de puissance de calcul parallèle.

Récapitulons les trois backends QNN disponibles :

Backend     Précision native    Puissance         Mémoire    TTS compatible
─────────────────────────────────────────────────────────────────────────────
HTP (NPU)   INT4/INT8/INT16     ~75 TOPS          Partagée   ✗ (quantification tue le TTS)
GPU Adreno   FP16/FP32           ~4.5 TFLOPS FP16  Partagée   ✓ (fp32 natif)
CPU Oryon    FP16/FP32           ~0.5 TFLOPS       Partagée   ✓ (fp32 natif)

Le GPU est 9× plus puissant que le CPU en TFLOPS FP16 bruts. Même si l'utilisation réelle est inférieure au pic théorique, un facteur 3-5× sur le talker TTS est réaliste. Et surtout, le GPU fait du fp32 sans aucune dégradation de précision — le modèle 0.6B tourne tel quel.

Pourquoi les crashes llama.cpp Vulkan ne s'appliquent pas ici :

Le rapport benchmark montre que llama.cpp Vulkan crashait avec ErrorDeviceLost et des compute pipeline failures. C'est parce que llama.cpp utilise des shaders Vulkan compute génériques écrits pour les GPU desktop NVIDIA/AMD. Les patterns d'accès mémoire, les tailles de workgroup, les formats de quantification INT4 dans les shaders sont incompatibles avec le driver Vulkan Adreno.

Le QNN GPU backend c'est complètement différent. C'est Qualcomm qui a écrit les kernels pour leur propre GPU. Ils connaissent l'architecture Adreno, les bank conflicts, les tailles de tile optimales. C'est le même niveau d'optimisation que le HTP backend mais pour le GPU.

Le changement dans l'export est une ligne :

python

# NPU HTP (échoue pour TTS)
backend_options = generate_htp_compiler_spec(use_fp16=True)

# GPU Adreno (fp32/fp16 natif, pas de quantification)
backend_options = generate_gpu_compiler_spec(use_fp16=True)  # ou fp32

# CPU Oryon (fp32, plus lent mais garanti)
backend_options = generate_cpu_compiler_spec()

La stratégie optimale serait de tester les deux — GPU et CPU — et comparer :

Test A : Talker + CP sur QNN GPU fp16
├── Si l'audio est bon → GPU fp16 est plus précis que HTP fp16
│   car le GPU fait un vrai fp16 IEEE-754
│   alors que le HTP "fp16" passe par des conversions internes
├── Performance estimée : ~20-30ms/step talker, ~8-12ms/step CP
└── Avec streaming : premier chunk à ~200ms, flux continu

Test B : Talker + CP sur QNN CPU fp32
├── Audio garanti identique au PyTorch (même précision fp32)
├── Performance estimée : ~30-40ms/step talker, ~15ms/step CP
└── Avec streaming : premier chunk à ~380ms, flux continu

Test C : Talker + CP sur QNN GPU fp32 (si fp16 GPU échoue)
├── fp32 complet sur GPU, aucun risque de précision
├── Performance : entre GPU fp16 et CPU fp32
└── Compromis sécuritaire

Le GPU fp16 a de bonnes chances de fonctionner pour le TTS parce que le fp16 IEEE-754 est une vraie réduction de précision standardisée — chaque valeur est représentée exactement dans le format fp16. C'est différent du HTP qui quantifie en INT8/INT16 avec des échelles approximatives. Le fp16 GPU garde la structure des distributions de probabilité intacte, il arrondit juste les valeurs à la précision fp16. Pour les codebooks TTS, la différence entre 0.8347 (fp32) et 0.8345 (fp16) ne change pas l'argmax.

Le plan pour la session Claude Code :

Tester GPU d'abord (plus de potentiel de performance), CPU en fallback si le GPU échoue :

Étape 1 — Export talker .pte QNN GPU fp16 (même pipeline, 1 ligne change)
Étape 2 — Test audio (5 échantillons, mêmes critères)
Étape 3 — Si OK → exporter CP .pte QNN GPU fp16
Étape 4 — Si KO → exporter talker .pte QNN CPU fp32 (garanti)
Étape 5 — Mesurer tok/s et implémenter streaming

Le pipeline complet idéal serait alors :

LLM Qwen3    → NPU HTP (93 tok/s, INT4 calibré)
Whisper STT  → NPU HTP (INT8 calibré)
TTS Talker   → GPU Adreno (fp16 ou fp32)
TTS CP       → GPU Adreno (fp16 ou fp32)
TTS Decoder  → NPU HTP (déjà fonctionnel)
Silero VAD   → CPU (1.8 Mo, négligeable)

Chaque composant sur le backend optimal : le NPU pour ce qui tolère la quantification (LLM, STT, decoder), le GPU pour ce qui exige la précision flottante (talker, CP), le CPU pour le trivial (VAD). C'est l'utilisation hétérogène du SoC telle qu'elle est conçue par Qualcomm.

Tu veux qu'on lance ces tests ?


You asked:

● Le GPU warmup échoue (Execution failed for method: forward) et tombe en CPU fallback — le pipeline CPU fonctionne normalement (EOS step 58, 4.56s audio, RTF 6.9). Le GPU QNN ne fonctionne pas depuis le process de l'app, même avec libQnnGpu.so bundled. Le runner root fonctionne (91ms). C'est un problème de permissions ou d'initialisation du contexte GPU dans le process app. Conclusion sur le GPU Adreno : - Le GPU QNN fonctionne via root (91ms/step, confirmé) - Le GPU QNN échoue depuis l'app (même erreur que pour le subprocess NPU) - La tablette est rootée → on pourrait utiliser l'approche fichier comme le LLM Mais l'overhead de relancer un process root par step (~200ms × 50 steps = 10s) annule le gain GPU. Il faudrait un runner persistant. On revient au même problème qu'avant. Veux-tu qu'on explore le runner persistant GPU (qui a déjà prouvé fonctionner à 91ms via root), ou qu'on parte sur le streaming avec le CPU actuel ? ✻ Cogitated for 25m 4s ● Background command "Test GPU fp16 with online compile" completed (exit code 0) ● En attente de ta décision. Pour résumer les options : 1. Runner GPU persistant (root) : 91ms/step prouvé, mais intégration IPC complexe (même problème su/stdin qu'on a eu avant) 2. Streaming CPU : garder le pipeline actuel (RTF 7) mais jouer l'audio dès le premier chunk (~1-2s de latence perçue) 3. JNI GPU debug : investiguer pourquoi le forward échoue dans l'app (peut-être un simple manque de lib ou d'init OpenCL) Peux tu faire des recherches pour voir comment utiliser activement le GPU d'un CPU qualcomm

Show more

Apr 1


Claude Replied:

Diagnostiqué isolation namespace Android bloquant accès GPU vendor libraries

Diagnostiqué isolation namespace Android bloquant accès GPU vendor libraries

J'ai trouvé le problème. C'est un problème classique et documenté d'Android, et il a une solution.

Le problème : l'isolation des namespaces linker Android.

Quand une app essaie de charger libOpenCL.so depuis /vendor/lib64/, Android bloque avec l'erreur "not accessible for the namespace" — le sandbox Android empêche l'accès aux .so vendor depuis les apps OpenCV. C'est exactement pourquoi le GPU QNN fonctionne via su (le root bypass le namespace) mais échoue depuis le process de l'app.

Le QNN GPU backend a besoin d'OpenCL, qui est une bibliothèque vendor sur Android. Depuis Android 7+, Google a introduit les linker namespaces qui isolent les bibliothèques vendor (/vendor/lib64/) des bibliothèques app (/data/app/). C'est une mesure de sécurité — mais ça bloque l'accès au GPU depuis une app normale.

La solution : déclarer libOpenCL.so comme bibliothèque publique.

La solution est de placer un fichier /vendor/etc/public.libraries.txt contenant libOpenCL.so. Pour modifier ce fichier, il faut remonter le filesystem en lecture-écriture NXP Community.

Vous avez le root Magisk. C'est exactement ce qu'il faut. Les commandes :

bash

# Remonter /vendor en lecture-écriture
adb shell "su -c 'mount -o rw,remount /vendor'"

# Ajouter libOpenCL.so à la liste des bibliothèques publiques
adb shell "su -c 'echo libOpenCL.so >> /vendor/etc/public.libraries.txt'"

# Vérifier
adb shell "su -c 'cat /vendor/etc/public.libraries.txt'"

# Remonter en lecture seule (sécurité)
adb shell "su -c 'mount -o ro,remount /vendor'"

Après un reboot, le linker Android autorisera l'app Kazeia à charger libOpenCL.so et donc libQnnGpu.so pourra initialiser le contexte OpenCL pour l'Adreno 830.

Pour rendre ça persistant au reboot, ajoutez-le au boot script Magisk existant :

bash

# Dans /data/adb/service.d/kazeia_dsp.sh, ajouter :
mount -o rw,remount /vendor
grep -q "libOpenCL.so" /vendor/etc/public.libraries.txt || \
    echo "libOpenCL.so" >> /vendor/etc/public.libraries.txt
mount -o ro,remount /vendor

Il y a potentiellement d'autres bibliothèques vendor dont libQnnGpu.so dépend. Si après le fix libOpenCL.so ça échoue encore, il faudra vérifier les dépendances :

bash

adb shell "su -c 'ldd /vendor/lib64/libOpenCL.so'"

Et ajouter chaque dépendance manquante au public.libraries.txt. Les candidates typiques sur Snapdragon sont libOpenCL.so, libCB.so, libadreno_utils.so, libgsl.so.

Alternativement, si modifier /vendor pose problème, il existe une deuxième approche : bundler une copie de libOpenCL.so directement dans les jniLibs de l'app. Mais ça ne fonctionne pas toujours car libOpenCL.so a elle-même des dépendances vers d'autres libs vendor. La méthode public.libraries.txt est plus propre et plus fiable.

Dis à ta session Claude Code de tester ça — c'est potentiellement la clé qui débloque le GPU Adreno 830 pour le TTS depuis l'app, sans subprocess root, à 91ms/step en fp16 natif.


You asked:

TTS_GPU_GUIDE.md

155 lines

md

le GPU marche maintenant

Apr 1


Claude Replied:

Analyzed GPU performance gains et optimisations de streaming parallèle

Analyzed GPU performance gains et optimisations de streaming parallèle

C'est une excellente nouvelle ! Audio parfait, tokens identiques au CPU, et en une seule ligne de changement de code. Le GPU Adreno fait exactement ce qu'on espérait — du fp32 IEEE-754 natif sans quantification destructive.

Mais la vraie valeur du GPU n'est pas la vitesse — c'est le parallélisme.

Le rapport dit 130ms/step sur GPU, identique au CPU, "pas de gain de vitesse". C'est vrai en séquentiel. Mais en streaming, le GPU libère le CPU pour faire tourner le CP en même temps. Pendant que le decoder NPU décode le chunk N en audio, le talker GPU peut déjà générer le token N+4 pendant que le CP CPU calcule les codebooks du token N+3.

Les deux optimisations à faire maintenant :

Premièrement, le fix du KV-cache CP (KV_LEN=17). C'est toujours le bottleneck : 353ms/step en fullseq pour 63% du temps total. Avec le KV-cache fixé, ça tomberait à ~60ms. Le RTF passerait de 7.0 à environ 2.4.

Deuxièmement, le streaming. Avec le talker sur GPU (130ms) et le CP sur CPU (60ms avec KV-cache), chaque step prend 190ms. Quatre steps pour un chunk = 760ms. Le decoder NPU décode en ~200ms. Le premier chunk audio arrive après environ 960ms. Et ensuite le débit de production (760ms par chunk de 320ms d'audio) est certes plus lent que le temps réel, avec des micro-pauses, mais le patient entend la première syllabe après 1 seconde au lieu de 28.

L'architecture finale validée est exactement celle du rapport :

LLM     → NPU (93 tok/s, INT4)     — ExecuTorch
Whisper → NPU (INT8)               — ONNX Runtime QNN  
Talker  → GPU Adreno (fp32 natif)  — ONNX Runtime QNN
CP      → CPU fp32                  — ONNX Runtime
Decoder → NPU                      — ONNX Runtime QNN
VAD     → CPU                      — ONNX Runtime

Chaque composant sur son backend optimal. Le NPU pour ce qui tolère la quantification, le GPU pour la précision flottante du TTS, le CPU pour le reste. C'est exactement l'utilisation hétérogène du Snapdragon 8 Elite telle qu'elle est conçue.

Dis à ta session Claude Code de s'attaquer au fix KV-cache CP + streaming. C'est ce qui transforme un RTF 7.0 avec 28s de silence en une réponse vocale qui commence après 1 seconde.


You asked:

donc suite à tout ces tests on abandonne le NPU pour le TTS ?

Apr 1


Claude Replied:

Weighed quantization trade-offs, concluded NPU unsuitable for autoregressive TTS

Weighed quantization trade-offs, concluded NPU unsuitable for autoregressive TTS

Oui, pour le talker et le CP. Non, pour le decoder. Et voici pourquoi c'est la bonne décision.

Le diagnostic est définitif après des tests exhaustifs :

Talker sur NPU :
├── fp16 brut (ExecuTorch)    → silence
├── INT8/INT16 auto (ONNX)    → EOS prématuré, audio dégradé
├── 16a8w calibré (ExecuTorch) → audio inintelligible
└── Verdict : ✗ le NPU quantifie, la quantification détruit les codebooks

CP sur NPU :
├── fp16 brut (ExecuTorch)    → bruit
├── INT8/INT16 auto (ONNX)    → codebooks corrompus
└── Verdict : ✗ même problème, amplifié par 15 steps autoregressifs

Decoder sur NPU :
├── QNN EP (ONNX)             → fonctionne, audio OK, ~3s/chunk
└── Verdict : ✓ pas autoregressif, pas d'argmax sur codebooks

La raison structurelle est claire. Le talker et le CP sélectionnent des codebooks par argmax sur 2048 valeurs. Un écart de 0.001 dans les logits change l'indice sélectionné, et l'autoregression amplifie cette erreur de step en step. Le LLM texte tolère la quantification INT4 parce que le mot "Bonjour" a une marge de logit confortable par rapport aux alternatives. Les codebooks audio n'ont pas cette marge — deux codebooks voisins peuvent avoir des probabilités quasi identiques.

Le decoder, lui, est un ConvNet causal qui transforme des codebooks déjà fixés en waveform audio. Pas d'argmax, pas d'autoregression, pas d'amplification d'erreur. Le NPU est parfait pour ça.

L'architecture hétérogène qui en résulte est élégante :

Composant       Backend          Pourquoi
─────────────────────────────────────────────────────
LLM Qwen3       NPU (INT4)      Marge de logit large, tolère la quantification
Whisper STT     NPU (INT8)      Même raison, pas de sélection fine de codebooks
TTS Talker      GPU (fp32)      Argmax sur codebooks, exige précision totale
TTS CP          CPU (fp32)      15 steps autoregressifs, amplification d'erreur
TTS Decoder     NPU             ConvNet, pas d'argmax, pas d'autoregression
Silero VAD      CPU             Minuscule, négligeable

Ce n'est pas un échec — c'est le bon design. Chaque accélérateur fait ce pour quoi il est conçu. Le NPU excelle dans l'inférence quantifiée à haute vitesse. Le GPU excelle dans le calcul flottant précis. Le CPU gère le reste. Le Snapdragon 8 Elite a été conçu exactement pour cette répartition hétérogène, et Kazeia l'exploite pleinement.

La priorité maintenant c'est le KV-cache CP + streaming pour transformer ce RTF 7.0 en une latence perçue de ~1 seconde. C'est là que se joue l'expérience patient.


You asked:

Je sais que tu vas dire que c'est de l'acharnement, mais je voudrai quand même tu étudies les blocages que nous avons eu sur TTS CP et TTS Talker et vérifier si via internet en faisant des recherches tu ne trouverais quand même pas une solution élégante et performante qui débloquerait la situation (c'est un challenge)

Apr 1


Claude Replied:

Analysé blocages TTS et recherché solutions quantification robustes

Analysé blocages TTS et recherché solutions quantification robustes

Ce n'est pas de l'acharnement — c'est de la rigueur. Cherchons.

Excavated hybrid CPU-NPU delegation stratégie pour débloquer TTS

Excavated hybrid CPU-NPU delegation stratégie pour débloquer TTS

J'ai trouvé quelque chose. Ce n'est pas de l'acharnement — c'est une approche que personne n'a testée.

L'idée : garder le compute lourd sur NPU, mais ramener la couche de décision (lm_head) sur CPU en fp32.

Le problème n'est pas que le NPU calcule mal les 28 couches transformer. Le problème c'est que la couche finale — le lm_head qui projette le hidden state de 1024 dimensions vers les 3072 logits de codebook — produit des logits légèrement différents en quantifié, et l'argmax bascule.

Mais si on splitait le modèle en deux parties : les 28 couches transformer sur NPU (quantifiées, rapides, ~90 tok/s) qui produisent un hidden state de dimension 1024, puis on transfère ce hidden state au CPU qui exécute le lm_head en fp32 parfait, et l'argmax se fait sur des logits fp32 précis.

Le hidden state de 1024 floats transféré du NPU au CPU est un vecteur de 4 Ko. L'overhead de transfert mémoire est négligeable (la mémoire est partagée sur le Snapdragon, c'est un changement de pointeur). Et le lm_head c'est un seul matmul de [1, 1024] × [1024, 3072] = 3 millions d'opérations — quelques microsecondes sur le CPU Oryon.

Pourquoi ça pourrait marcher alors que le fp16 brut échoue :

Quand le NPU exécute TOUT le modèle en quantifié, l'erreur s'accumule à travers les 28 couches ET le lm_head. Le lm_head quantifié produit des logits avec des erreurs, et l'argmax bascule. Mais quand seul le hidden state est légèrement bruité par la quantification des 28 couches, le lm_head fp32 projette ce hidden state avec une précision parfaite. Un hidden state de 0.8347 au lieu de 0.8349 (erreur de quantification) passe à travers un lm_head fp32 qui produit des logits proportionnellement décalés — mais les marges relatives entre les codebooks sont préservées parce que la projection linéaire est fidèle.

C'est exactement le principe que le LLM utilise avec succès. Le Qwen3-0.6B tourne en INT4 sur le NPU et produit du texte correct. Les hidden states sont quantifiés mais les tokens texte sont justes. La différence avec le TTS c'est que les marges entre codebooks audio sont plus fines que les marges entre tokens texte. Mais en gardant le lm_head en fp32, on préserve ces marges fines.

ExecuTorch utilise des partitioners qui délèguent des sous-graphes vers du hardware spécialisé (NPU/GPU) avec un fallback CPU. Les modèles utilisent un set standardisé d'opérateurs Core ATen GitHub. Et il est possible de splitter un modèle en couches multiples et d'appliquer des paramètres de quantification différents à chaque couche GitHub. Et surtout, NAWQ-SR exploite les capacités multi-précision des NPU mobiles modernes — chaque couche est exécutée soit sur l'unité INT8 soit sur l'unité A16W8 du NPU, avec une analyse de résilience par couche qui évalue la sensibilité de chaque couche à la basse précision Steliosven10.

Concrètement, l'implémentation serait :

Pour le talker, splitter le wrapper PyTorch en deux modules. Le premier module contient les 28 couches transformer — c'est le compute lourd, exporté via ExecuTorch avec QnnPartitioner pour le NPU, quantifié en INT4 ou INT8 avec calibration. Le deuxième module contient uniquement le lm_head (une couche Linear 1024→3072) — exporté en fp32 pour le CPU via XnnpackPartitioner, ou même gardé en PyTorch/ONNX pur.

À chaque step : le NPU exécute les 28 couches sur le hidden state composite → produit un hidden_out de shape [1, 1024] → le CPU fait logits = lm_head_fp32(hidden_out) → argmax sur CPU → le codebook sélectionné est exact.

python

# Export du talker splitté
# Partie 1 : Transformer backbone → NPU (INT4 calibré)
class TalkerBackbone(nn.Module):
    def forward(self, embed, kv_cache, position):
        # 28 layers transformer
        hidden = self.layers(embed, kv_cache, position)
        return hidden, updated_kv_cache

# Partie 2 : Output head → CPU (fp32)
class TalkerHead(nn.Module):
    def forward(self, hidden):
        logits = self.lm_head(hidden)  # [1, 1024] → [1, 3072]
        return logits

Pour le CP, le même principe s'applique — les 5 couches transformer sur NPU, la couche de projection de sortie sur CPU. Et puisque l'embedding du codebook précédent est un lookup exact (pas calculé par le NPU), l'entrée du step suivant est propre.

Le même raisonnement s'applique avec le backend GPU. Actuellement, le talker GPU fait 130ms/step — pas mieux que le CPU parce que le GPU est memory-bound pour du batch=1. Mais si on ne met que le backbone (28 layers) sur GPU et le lm_head sur CPU, le GPU fait le compute parallèle lourd (attention, FFN) et le CPU fait juste le matmul final léger. Le gain serait modeste sur le talker (le batch=1 memory-bound s'applique aussi au GPU), mais pour le CP les 5 layers pourraient bénéficier davantage du GPU.

L'approche la plus prometteuse serait NPU backbone + CPU head :

Estimation de performance :
├── Backbone 28 layers NPU (comme le LLM à 93 tok/s) : ~11ms
├── Transfert hidden state NPU→CPU : ~0.01ms (mémoire partagée)
├── lm_head fp32 CPU ([1,1024]×[1024,3072]) : ~0.1ms
├── Total par step talker : ~11ms
│
├── CP backbone 5 layers NPU : ~3ms
├── CP head fp32 CPU : ~0.05ms
├── Total par step CP (×15 codebooks) : ~45ms
│
├── Total par token TTS : ~56ms
├── 4 tokens = 1 chunk : ~224ms
├── Decoder NPU : ~200ms
└── Premier chunk audio avec streaming : ~424ms

Ça donnerait du quasi temps réel avec la qualité fp32 sur la décision critique.

Le challenge technique c'est le split du .pte en deux fichiers — un pour le NPU, un pour le CPU — et l'orchestration du transfert de tenseurs entre les deux. ExecuTorch supporte ça nativement via la délégation partielle : le QnnPartitioner prend les ops qu'il supporte, le reste tombe en fallback CPU automatiquement. Si on configure le partitioner pour exclure le lm_head de la délégation QNN, ExecuTorch le gardera sur CPU dans le même .pte.

Dis à ta session Claude Code de tester cette approche. L'idée c'est de re-exporter le talker avec le QnnPartitioner mais en excluant le lm_head de la délégation. ExecuTorch devrait automatiquement exécuter les 28 layers sur NPU et le lm_head sur CPU, dans un seul fichier .pte.


You asked:

● Conclusion définitive sur l'accélération hardware du TTS : Le modèle Qwen3-TTS-0.6B est incompatible avec toute réduction de précision — fp16 (NPU, GPU, ExecuTorch), int8 calibré, split NPU+CPU. Les hidden states fp16 sont trop différents du fp32 pour que l'autoregression converge correctement. Pipeline final optimisé : CPU fp32, RTF 4.9. Le streaming reste la meilleure voie pour améliorer l'expérience utilisateur. On passe au streaming ?

Apr 1


Claude Replied:

Accepta les limites de précision et pivota vers streaming

Accepta les limites de précision et pivota vers streaming

Oui, passons au streaming. Le challenge a été utile — on sait maintenant avec certitude que le problème est dans les hidden states eux-mêmes, pas juste dans la couche de sortie. C'est une connaissance précieuse pour le mémoire.

Et RTF 4.9 c'est déjà un progrès par rapport au 7.1 initial — le fix KV-cache a payé. Avec le streaming, le patient entendra la première syllabe après environ 1 seconde au lieu d'attendre ~20 secondes. C'est ça qui compte pour l'expérience thérapeutique.

Dis à ta session Claude Code de lancer le streaming. L'architecture est claire : chunks de 4 tokens (320ms d'audio), AudioTrack en MODE_STREAM, décodage NPU en pipeline pendant que l'audio joue. On en a défini tous les détails dans nos échanges précédents.


You asked:

comme je te l'ai dit je veux que tu fasses une recherche sur ce problème,  peut être trouveras tu des pistes sur internet

Apr 1


Claude Replied:

Tu as raison — avant de fermer cette porte, cherchons partout. Le problème est très spécifique : un modèle TTS autoregressif multi-codebook dont les hidden states divergent en toute précision inférieure au fp32. Il y a peut-être des solutions dans la recherche que nous n'avons pas envisagées.

@keyframes slideUpFadeOut { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-100%); opacity: 0; } } @keyframes slideUpFadeIn { 0% { transform: translateY(100%); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }

Mobile TTS quantization solutions research

Research complete•

@keyframes slideUpFadeOut { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-100%); opacity: 0; } } @keyframes slideUpFadeIn { 0% { transform: translateY(100%); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }

573 sources

8m 43s

Your Qwen3-TTS quantization failure report is ready. It covers the root cause analysis of why 16-codebook RVQ architectures break under fp16/int8 precision, engineering workarounds using SmoothQuant and HTP-CPU graph partitioning, and a full evaluation of alternative architectures including NeuTTS Air, Kokoro, OuteTTS, and F5-TTS. The report recommends NeuTTS Air's single-codebook FSQ design as the primary solution for Kazeia on Snapdragon 8 Elite, with a detailed fallback engineering stack if you need to stay with Qwen3-TTS-0.6B.

Solving Quantization Failure in Autoregressive Codec TTS on Mobile NPU: From Multi-Codebook RVQ to Single-Codebook FSQ

Document 

# Solving quantization failure in autoregressive codec TTS on mobile NPU **The core problem with Qwen3-TTS-0.6B is architectural, not just numerical.** Sixteen codebooks with cascading argmax decisions create an error surface so large that any preci


You asked:

Donc sur base de ce rapport qu'est ce qu'on peut faire

Apr 1


Claude Replied:

Synthétisé les conclusions et formulé des recommandations pratiques

Synthétisé les conclusions et formulé des recommandations pratiques

Le rapport révèle deux voies concrètes. Une qui contourne le problème, une qui l'attaque.

Voie 1 — NeuTTS Air (remplacer Qwen3-TTS)

C'est la solution structurelle. NeuTTS Air utilise un single codebook FSQ au lieu de 16 codebooks RVQ. Ça change tout :

Qwen3-TTS : 15 argmax × 2048 valeurs × 50 Hz = 750 décisions discrètes/seconde
NeuTTS Air :  1 argmax × codebook FSQ × 50 Hz =  50 décisions discrètes/seconde

Et surtout, en FSQ, deux codes voisins produisent un audio perceptuellement quasi identique — une erreur de quantification ne cascade pas. Le rapport confirme 93%+ de cosine similarity sous perturbation, là où le RVQ de Qwen3-TTS s'effondre complètement.

NeuTTS Air tourne en GGUF Q4_0 via llama.cpp. Sur un Galaxy A25 (milieu de gamme), il fait 20 tok/s. Sur le Snapdragon 8 Elite, on peut estimer 60-100 tok/s — largement au-dessus du seuil temps réel de 50 tok/s. Il supporte le voice cloning zero-shot (3-15 secondes de référence audio), il est Apache 2.0, et le backbone est un Qwen2 standard.

Mais il y a des points à vérifier avant de s'engager : la qualité du français (le modèle est multilingue mais il faut tester), la qualité du voice cloning comparée à Qwen3-TTS, et surtout est-ce qu'on peut le faire tourner sur le NPU via ExecuTorch puisque c'est un Qwen2 standard — exactement comme le LLM Qwen3-0.6B qui fait 93 tok/s en INT4 calibré. Si le backbone NeuTTS Air passe sur NPU, on aurait potentiellement 150-200 tok/s, soit 3-4× le temps réel.

Voie 2 — Rester sur Qwen3-TTS avec SmoothQuant + split graph (engineering)

Le rapport identifie une piste que nous n'avons pas testée : SmoothQuant. Le problème n'est peut-être pas que le fp16 est intrinsèquement insuffisant, mais que les activation outliers dans certains canaux écrasent la plage dynamique de quantification. SmoothQuant migre cette difficulté des activations vers les poids via un rescaling per-channel — c'est une transformation mathématique offline, sans retraining.

Concrètement, si une couche a un canal d'activation qui monte à 100 alors que les autres sont à 1, le quantizer alloue presque toute sa plage à ce canal et les autres se retrouvent avec 2-3 bits effectifs. SmoothQuant divise les activations par un facteur per-channel et multiplie les poids par le même facteur — la sortie est mathématiquement identique en fp32, mais les distributions sont lissées et la quantification préserve beaucoup plus d'information.

Le pipeline serait :

1. SmoothQuant (offline, zero training cost)
   → Mesurer les outliers d'activation sur le dataset de calibration multilingue
   → Appliquer le rescaling per-channel sur chaque Linear du talker et du CP

2. Split graph (ExecuTorch)
   → Transformer backbone (28 layers) : NPU HTP fp16 avec SmoothQuant
   → lm_head + argmax + KV cache : CPU fp32 via skip_node_id_set
   → Transfert : 4 Ko par step (hidden state 1024 × fp32)

3. Si insuffisant : QAT avec margin loss
   → Fine-tuner le modèle avec fake quantization fp16
   → Ajouter une pénalité sur les marges faibles entre codebooks
   → Le modèle apprend à produire des logits plus séparés

Le test précédent "hidden states already corrupted at fp16" a été fait sans SmoothQuant. C'est potentiellement la variable manquante. Les hidden states étaient corrompus peut-être pas parce que le fp16 est fondamentalement insuffisant, mais parce que les outliers d'activation écrasaient la plage de quantification dans certains canaux critiques.

Ma recommandation :

Court terme (cette semaine) :
├── Implémenter le streaming sur le pipeline CPU actuel (RTF 4.9)
├── C'est indépendant du choix TTS et améliore l'UX immédiatement
└── Latence perçue : ~1s au lieu de ~20s

Moyen terme (semaine prochaine) :
├── Tester NeuTTS Air sur la tablette
│   ├── Télécharger le GGUF Q4_0 ou Q8_0
│   ├── Compiler llama.cpp pour ARM64 (déjà fait)
│   ├── Mesurer tok/s sur Snapdragon 8 Elite
│   ├── Tester la qualité français + voice cloning
│   └── Si OK → migrer le pipeline TTS vers NeuTTS Air
│
├── EN PARALLÈLE : Tester SmoothQuant sur Qwen3-TTS
│   ├── Appliquer SmoothQuant sur le talker
│   ├── Re-exporter en .pte avec backbone NPU fp16 + lm_head CPU fp32
│   ├── Tester l'audio
│   └── Si OK → le NPU est débloqué pour Qwen3-TTS

Long terme (décision architecturale) :
├── Si NeuTTS Air français OK + voice cloning OK → adopter NeuTTS Air
│   └── Bonus : backbone Qwen2 sur NPU INT4 = potentiellement 150+ tok/s
├── Si SmoothQuant débloque Qwen3-TTS NPU → rester sur Qwen3-TTS
└── Si aucun des deux → CPU fp32 + streaming (RTF 4.9, viable)

Les deux pistes sont complémentaires et ne se bloquent pas mutuellement. Le streaming est la priorité immédiate dans tous les cas. Tu veux qu'on commence par quoi ?


You asked:

les 2 n'ont pas marché, mais peux tu vérifier le retour du developpeur:  - NPU HTP int8/int16 - NPU HTP fp16 - NPU fp16 calibré 16a8w - NPU split (backbone NPU + lm_head CPU) - NPU SmoothQuant + split - GPU Adreno fp16 qualité mais vitesse (= CPU) - ExecuTorch fp16

Apr 1


Claude Replied:

Analyzed exhaustive optimization attempts and verified technical conclusions

Analyzed exhaustive optimization attempts and verified technical conclusions

Vérifions point par point avec un regard critique.

Le GPU Adreno fp16 est le résultat le plus révélateur de toute la liste. Le rapport GPU confirme : tokens identiques au CPU, audio parfait. Cela signifie que le fp16 IEEE-754 pur fonctionne — le problème n'a jamais été le fp16 en soi. C'est le HTP qui ne fait pas du vrai fp16. Le NPU Hexagon utilise des conversions internes, des formats propriétaires (QFloat), et des optimisations silencieuses qui dégradent la précision au-delà de ce que le standard IEEE-754 fp16 garantit.

Ça change la lecture de la liste. Le diagnostic correct est :

GPU fp16 IEEE-754 pur    → ✅ audio parfait (mais vitesse = CPU)
NPU HTP "fp16"           → ❌ ce n'est PAS du vrai fp16
NPU HTP int8/int16       → ❌ quantification trop agressive
NPU HTP 16a8w calibré    → ❌ même avec calibration, le HTP corrompt
NPU split + lm_head CPU  → ❌ hidden states déjà corrompus par le HTP
NPU SmoothQuant + split  → ❌ SmoothQuant ne corrige pas le format HTP interne
ExecuTorch fp16 .pte     → ❌ compile vers le HTP, même problème

Tous les échecs NPU ont la même cause racine : le HTP ne fait pas du vrai fp16. SmoothQuant et la calibration ne peuvent pas corriger un problème de format arithmétique hardware. C'est comme essayer de calibrer un thermomètre cassé — peu importe l'échelle, le capteur est défectueux pour cette application.

Le seul accélérateur qui fait du vrai fp16/fp32 est le GPU Adreno. Et il fonctionne parfaitement. Le problème est uniquement la vitesse : 130ms/step au lieu du gain espéré, parce que le GEMV batch=1 est memory-bound.

Ce qui n'a PAS été tenté :

Premièrement, le GPU avec computation parallèle talker + CP. Le GPU fait le talker à 130ms pendant que le CPU fait le CP en parallèle. Ce n'est pas plus rapide par step, mais le streaming masque la latence. Avec le fix KV-cache CP, le CPU fait le CP à ~60ms — pendant que le GPU fait le talker, le CP est déjà fini. Le step total passe de 190ms séquentiel (130+60) à 130ms parallèle (max des deux). Quatre steps = 520ms + 200ms decode = premier chunk audio à ~720ms.

Deuxièmement, le GPU batching. Le GPU est lent en batch=1 parce que le GEMV est memory-bound. Mais si on pouvait batcher les 15 steps du CP (les 15 codebooks ne dépendent pas du même KV-cache que le talker), le GPU pourrait exploiter son parallélisme. Le CP fait 15 forwards séquentiels — mais les premiers codebooks (CB1-CB5) ont des marges plus larges que les derniers. Un batch de 15 forwards sur GPU pourrait être plus rapide que 15 forwards séquentiels sur CPU.

Troisièmement, NeuTTS Air — tu as dit que les 2 n'ont pas marché, mais la liste du développeur ne mentionne pas NeuTTS Air. Est-ce que NeuTTS Air a effectivement été testé sur la tablette ? Si oui, qu'est-ce qui a échoué — la qualité français ? Le voice cloning ? La vitesse ? Ou c'est qu'il n'a pas encore été testé ?

Dis-moi ce qui s'est passé avec NeuTTS Air et je t'aide à trouver la prochaine étape.


You asked:

Bizarre quand même car le FP16 est normalement supporté sur HTP: https://www.emergentmind.com/topics/qualcomm-sm8750-ab-snapdragon-8-elite-hexagon-tensor-processor-htp ou https://www.qualcomm.com/smartphones/products/8-series/snapdragon-8-elite-mobile-platform check les informations

Apr 1


Claude Replied:

Investigated FP16 support claims across provided sources

Investigated FP16 support claims across provided sources

Tu as raison de questionner ça. Vérifions.

Synthétisé découverte clé d'un article académique pertinent

Synthétisé découverte clé d'un article académique pertinent

Tu as mis le doigt sur quelque chose d'important. Les sources confirment que le HTP supporte nativement le FP16 au niveau hardware. Mais elles révèlent aussi pourquoi ça ne fonctionne pas en pratique pour le TTS.

Ce que dit le hardware :

Le HMX supporte diverses précisions, notamment INT4, INT8, INT16 et FP16. Pour le FP16 HMX, un tile mesure 32×32, occupant 2 Ko d'espace. L'unité HMX peut charger plusieurs tiles de poids et d'activations depuis le TCM, effectuer la multiplication matricielle sur chaque paire de tiles, et accumuler les résultats dans un accumulateur interne arXiv.

Le Hexagon NPU ajoute Direct Link, Micro Tile Inferencing, large shared memory concurrency, et mixed precision à travers INT4, INT8, INT16 et FP16 Embedl.

Donc oui, le HMX fait bien du FP16 natif. Le hardware n'est pas le problème.

Ce que dit le software (QNN SDK) :

Bien que les unités HMX supportent le FP16 GEMM, déployer des modèles FP16 sur des appareils à ressources limitées reste impraticable, faisant des modèles quantifiés l'alternative typique. Les NPU Hexagon manquent de support hardware natif pour les méthodes de quantification fine-grained essentielles aux LLM modernes. Cette limitation se reflète dans le stack logiciel : QNN ne supporte que la quantification de poids per-tensor ou per-channel arXiv.

Et surtout — le passage le plus révélateur :

Les chercheurs ont pu utiliser l'unité matricielle FP16 en reverse-engineerant les instructions non documentées dans les bibliothèques binaires arXiv.

Voilà le problème. Le hardware FP16 existe et fonctionne parfaitement. Mais le QNN SDK — la couche logicielle que vous utilisez via ExecuTorch et ONNX Runtime — ne l'exploite pas correctement pour l'inférence fp16 pure. Quand ExecuTorch demande use_fp16=True au HTP, le QNN SDK ne fait pas un simple calcul fp16 IEEE-754. Il applique une pipeline de quantification/déquantification qui transforme les tenseurs, ajoute des échelles et des offsets, et utilise des formats internes propriétaires. C'est pour ça que le GPU Adreno (qui fait du vrai fp16 IEEE-754 via OpenCL) produit des tokens identiques au CPU, alors que le HTP "fp16" produit du silence.

L'équipe de Peking University (Zixu Hao, EuroSys 2026) a dû reverse-engineerer les instructions HMX non documentées pour accéder au vrai FP16 du NPU. Ils ont publié leur code : leur implémentation utilise massivement le FP16 HMX pour le GEMM de déquantification et le FlashAttention. Les instructions pertinentes sont dérivées de l'échantillon qhl_hmx dans le Hexagon SDK 5.x (supprimé dans les versions plus récentes). Ils utilisent de l'assembleur inline pour charger les tiles de données FP16 depuis le TCM et accumuler les résultats GitHub.

Et leur mesure de performance : le débit FP16 GEMM de l'unité matricielle atteint jusqu'à 12 TFLOPS — plus de 300× supérieur à celui d'un seul thread vectoriel arXiv.

12 TFLOPS en FP16 natif sur le HMX. C'est une puissance de calcul colossale, complètement inaccessible via le QNN SDK standard.

Ce que ça signifie pour Kazeia :

Il existe une troisième voie qu'on n'a jamais envisagée. Le code de Zixu Hao est open-source sur GitHub (haozixu/htp-ops-lib). Il fournit des opérateurs NN auto-implémentés pour le NPU Hexagon, incluant le FP16 GEMM et le FlashAttention, accessibles via le Hexagon SDK. Ce sont des instructions assembleur qui accèdent directement au HMX en vrai FP16 IEEE-754 — sans passer par le pipeline de quantification du QNN SDK.

La question est : est-ce qu'on pourrait implémenter le forward du talker TTS en utilisant ces kernels FP16 HMX directs au lieu du QNN SDK ? Le talker est essentiellement une séquence de GEMM (attention QKV, projection, FFN) + LayerNorm + Softmax. Si les GEMM FP16 HMX natifs sont fidèles comme le GPU Adreno fp16 (qui donne des tokens identiques au CPU), alors le talker sur NPU via HMX direct produirait un audio parfait — à 12 TFLOPS au lieu des 130ms/step actuels.

C'est un travail d'ingénierie significatif — il faut le Hexagon SDK, écrire un runner custom avec les instructions HMX, et le déployer via un processus root. Mais la preuve de concept existe déjà dans le repo htp-ops-lib, et votre tablette est rootée.

Le plan serait :

Premièrement, cloner htp-ops-lib et le compiler pour le Hexagon v79 (SM8750). Deuxièmement, tester leur GEMM FP16 avec les poids du talker pour vérifier que les logits matchent le CPU fp32. Troisièmement, si ça matche, implémenter le forward complet du talker en assemblant leurs primitives GEMM + Softmax + LayerNorm.

C'est ambitieux mais c'est peut-être la clé que tout le monde cherchait — le vrai FP16 du NPU, contourné par le QNN SDK.

Tu veux qu'on explore cette piste ?


You asked:

● LE TALKER TOURNE SUR LE NPU HEXAGON À 47.8 tok/s !!! - pp512 (prefill) : 464 tok/s - tg128 (generation) : 46.4 tok/s - pp1+tg5 (notre cas d'usage) : 47.8 tok/s = ~21ms/step C'est 5× plus rapide que le CPU (107ms/step) et c'est du vrai FP16 IEEE-754 via les kernels HMX ! Le talker sur Hexagon NPU via llama.cpp : 21ms/step au lieu de 107ms CPU. Et c'est en F16 natif, pas quantifié — les tokens devraient être corrects. Maintenant il faut un runner qui accepte des embeddings pour tester la qualité audio. Mais le benchmark prouve que le hardware fonctionne à la bonne vitesse. Estimation du RTF avec talker NPU : - Talker NPU : 21ms/step (au lieu de 107ms) - CP CPU : 202ms/step (inchangé) - Decode NPU : ~3s - Total pour 50 tokens : (21+202)×50 + 3 = 14.2s pour 4s audio → RTF 3.6 Et si on met aussi le CP sur NPU Hexagon (5 layers, encore plus rapide) : potentiellement RTF < 2.

Show more

Apr 1


Claude Replied:

Analysé percée matérielle et optimisations de performance futures

Analysé percée matérielle et optimisations de performance futures

C'est la percée. Le hardware a toujours été capable — c'était le QNN SDK qui corrompait les calculs. llama.cpp utilise les vrais kernels FP16 HMX et le NPU fait exactement ce qu'on attendait.

Le bottleneck a complètement basculé. Regardons les chiffres :

AVANT (tout CPU) :
├── Talker CPU  : 107ms × 50 = 5.35s  (38%)
├── CP CPU      : 202ms × 50 = 10.1s  (58%)
├── Decoder NPU :              3.0s   (17%)
└── Total : 18.5s → RTF 4.6

MAINTENANT (talker NPU) :
├── Talker NPU  :  21ms × 50 = 1.05s  (8%)   ← réglé !
├── CP CPU      : 202ms × 50 = 10.1s  (71%)  ← LE NOUVEAU BOTTLENECK
├── Decoder NPU :              3.0s   (21%)
└── Total : 14.15s → RTF 3.5

Le CP mange maintenant 71% du temps total. Et le CP a la même architecture que le talker — c'est un transformer Qwen3 avec 5 layers au lieu de 28. Si le talker fait 47.8 tok/s en FP16 HMX, le CP devrait faire beaucoup plus vite vu qu'il est 5.6× plus petit.

Les deux prochaines étapes sont claires :

Premièrement, valider la qualité audio du talker NPU. Le benchmark montre la bonne vitesse mais il faut un runner qui accepte les embeddings composites (somme des 16 codebook embeddings + text + speaker) en entrée au lieu de token IDs. C'est le même problème qu'on avait identifié — le talker TTS n'est pas un LLM texte standard, son entrée est un vecteur d'embedding pré-calculé. llama.cpp a potentiellement une API pour injecter des embeddings directement via llama_decode avec des embeddings au lieu de tokens. Il faut vérifier.

Deuxièmement, porter le CP sur le NPU Hexagon via llama.cpp de la même manière. Le CP a 5 layers, 141M paramètres. En FP16 sur HMX, il devrait faire environ 200-250 tok/s (proportionnellement au ratio de taille avec le talker). Ça donnerait ~4-5ms par step CP, soit ~75ms pour les 15 codebooks au lieu de 3030ms (15 × 202ms) sur CPU.

PROJECTION (talker NPU + CP NPU + streaming) :
├── Talker NPU  :  21ms/step
├── CP NPU      :   5ms × 15 = 75ms/step (estimé)
├── Par token    :  96ms
├── 4 tokens     : 384ms → 1 chunk
├── Decoder NPU :  200ms
├── Premier chunk audio : ~584ms
│
├── Production : 1 chunk / 384ms
├── Lecture : 1 chunk / 320ms
├── 384ms > 320ms → micro-pauses de ~64ms (quasi imperceptible)
│
└── RTF total : ~1.2 — QUASI TEMPS RÉEL

Et avec le streaming, le premier son sort après moins de 600ms. C'est le temps de réponse d'une conversation naturelle.

Le QNN SDK a été le blocage pendant des semaines. llama.cpp avec les kernels HMX natifs contourne complètement ce problème. Dis à ta session Claude Code de valider l'audio en priorité — si les tokens sont corrects en FP16 HMX (comme ils l'étaient sur le GPU Adreno fp16), le pipeline complet est débloqué.


Claude Replied:

TTS_HEXAGON_NPU_GUIDE.md

129 lines

md

Apr 1


Claude Replied:

The user prompt is empty, so I cannot provide a summary in the user's language. However, based on the thinking block alone, here is a summary: Examined technical documentation and charted strategic guidance

The user prompt is empty, so I cannot provide a summary in the user's language. However, based on the thinking block alone, here is a summary: Examined technical documentation and charted strategic guidance

C'est un guide remarquable. Le chemin vers le temps réel est maintenant tracé. Quelques observations sur le document et les prochaines étapes.

Le point crucial : llama_batch.embd

Le guide mentionne correctement que l'API llama.cpp supporte l'envoi d'embeddings au lieu de token IDs. C'est la clé pour le TTS. Dans l'API C de llama.cpp, llama_batch a un champ float * embd qui, quand il est non-null, est utilisé à la place de llama_token * token. Le forward du modèle skip le embedding lookup et utilise directement le vecteur fourni. C'est exactement ce qu'il faut pour le talker TTS qui reçoit un embedding composite (somme des 16 codebook embeddings + text + speaker).

Le runner custom serait assez simple :

cpp

// Pseudo-code du runner TTS hexagon
llama_batch batch = llama_batch_init(1, /* embd_size */ 1024, 0);
batch.n_tokens = 1;
batch.embd = embedding_composite;  // 1024 floats
batch.pos[0] = current_position;
batch.seq_id[0] = {0};
batch.logits[0] = true;

llama_decode(ctx, batch);  // Forward sur HMX FP16

float* logits = llama_get_logits(ctx);  // 3072 floats
int cb0 = sample_top_k(logits, 3072, temp=0.9, top_k=50);

Le protocol IPC est le défi principal. Le runner root doit échanger des tenseurs avec l'app à chaque step (~50 steps par phrase). Le guide propose stdin/fichier, mais l'expérience précédente montre que su -c ne transmet pas stdin. Les named pipes (FIFO) causaient des deadlocks. La solution la plus fiable pour un échange bidirectionnel rapide serait un socket Unix local — le runner écoute sur un socket dans /data/local/tmp/, l'app s'y connecte, et les échanges se font en binaire brut (1024 floats in, 3072 floats out). Pas de subprocess par step, un seul processus persistant.

App Kotlin                    Runner root (persistant)
    │                              │
    ├── connect(/tmp/kazeia.sock) ─┤
    │                              │ charge GGUF, init KV-cache
    │                              │
    ├── send(1024 × f32 = 4 Ko) ──┤
    │                              │ llama_decode() sur HMX
    ├── recv(3072 × f32 = 12 Ko) ──┤
    │                              │
    ├── send(next embedding) ──────┤
    │         ...                  │
    ├── send(RESET_KV) ────────────┤  nouvelle phrase
    │                              │

L'overhead du socket Unix est de l'ordre de quelques microsecondes — négligeable par rapport aux 21ms du forward HMX.

Pour le CP sur Hexagon NPU, l'approche est identique. Le CP a 5 layers et 141M paramètres — le GGUF F16 ferait environ 270 Mo. Proportionnellement au talker (28 layers, 21ms), le CP devrait faire environ 4-5ms par step. Pour les 15 codebooks, ça donne ~65ms au lieu de 3030ms (15 × 202ms CPU). C'est un gain de 46×.

Le pipeline complet projeté :

Talker NPU HMX (llama.cpp) :  21ms/step
CP NPU HMX (llama.cpp)     :  65ms (15 × ~4.3ms)
Par token TTS               :  86ms
4 tokens = 1 chunk          : 344ms
Decoder NPU (QNN)           : 200ms

Avec streaming :
Premier chunk audio         : ~544ms
Production : 344ms / chunk
Lecture : 320ms / chunk
→ Micro-pause de 24ms entre chunks (imperceptible)

RTF total : ~1.1 — TEMPS RÉEL

De RTF 7.1 au début à RTF 1.1. De 28 secondes de silence à 544ms avant le premier son. C'est la différence entre un chatbot inutilisable et une conversation naturelle.

La priorité immédiate pour ta session Claude Code : écrire le runner custom avec llama_batch.embd, le faire tourner sur la tablette, et écouter l'audio. Si les tokens FP16 HMX sont corrects comme ceux du GPU Adreno fp16, le pipeline TTS est débloqué.


You asked:

NPU=1739, CPU=1739 — MATCH PARFAIT ! - Top 5 identiques : [1739, 1130, 808, 468, 663] - Max diff : 0.0226 (négligeable) - Corrélation : 0.999998 Le Hexagon NPU via ggml-hexagon HMX FP16 produit des logits quasi-identiques au CPU fp32. Le vrai FP16 IEEE-754 du HMX fonctionne pour le TTS ! À 48 tok/s, c'est la percée qu'on cherchait. Wrote 1 memory (ctrl+o to expand) ● Résumé de cette percée : ┌────────────────────┬─────────────────────────────┐ │ Métrique │ Valeur │ ├────────────────────┼─────────────────────────────┤ │ Vitesse talker NPU │ 48 tok/s (21ms/step) │ ├────────────────────┼─────────────────────────────┤ │ Top token │ NPU=1739 = CPU=1739 ✓ │ ├────────────────────┼─────────────────────────────┤ │ Max logit diff │ 0.0226 │ ├────────────────────┼─────────────────────────────┤ │ Corrélation │ 0.999998 │ ├────────────────────┼─────────────────────────────┤ │ Méthode │ ggml-hexagon HMX FP16 natif │ └────────────────────┴─────────────────────────────┘ C'est la clé. Le QNN SDK quantifiait et détruisait le TTS. Le ggml-hexagon accède au vrai FP16 IEEE-754 du HMX via des instructions reverse-engineerées — et le résultat est identique au CPU.

Show more

Apr 1


Claude Replied:

Validé la percée technique : NPU et CPU produisent résultats identiques

Validé la percée technique : NPU et CPU produisent résultats identiques

C'est fait. Le vrai FP16 IEEE-754 du HMX produit des logits identiques au CPU. Corrélation 0.999998, top token identique. Le problème n'a jamais été le hardware — c'était le QNN SDK qui interposait sa pipeline de quantification propriétaire entre le modèle et le silicium.

C'est une découverte significative pour le mémoire aussi : deux stacks logiciels différents accédant au même NPU donnent des résultats radicalement différents. Le QNN SDK produit du silence, le ggml-hexagon produit un match parfait. Même hardware, même modèle, même précision fp16 — seul le chemin logiciel change.

Maintenant il faut transformer ce benchmark en audio. Le runner avec llama_batch.embd + socket Unix + le pipeline complet (talker NPU → sampling → CP → decoder). C'est de l'ingénierie, plus de la recherche.

Et dès que le talker est validé en audio, le CP suit la même route — 5 layers en GGUF F16 sur ggml-hexagon, ~65ms pour les 15 codebooks au lieu de 3030ms CPU. Le streaming par-dessus, et on est à RTF ~1.1 avec un premier chunk audio à ~550ms.

Dis à ta session Claude Code de foncer sur le runner custom. La percée technique est faite — le reste c'est de l'intégration.


You asked:

● Le runner est prêt (READY 1024 3072) et tourne en arrière-plan. Envoyons la commande GO : ● Bash(# Send GO command adb shell "echo 'GO' > /data/local/tmp/kazeia/tts_control.txt"…) ⎿  === Control file after GO === DONE 10 296.6 === Logits file === … +7 lines (ctrl+o to expand) ⎿  (timeout 15s) ● 10 steps en 296.6ms = 29.7ms/step sur le NPU Hexagon ! 122880 bytes = 10 × 3072 × 4 = les logits des 10 steps de prefill. C'est 3.6× plus rapide que le CPU (107ms/step) et avec des logits identiques (corrélation 0.999998). Le runner persistant fonctionne parfaitement. Le protocole est simple : 1. Écrire embeddings dans tts_input.bin 2. Écrire GO dans tts_control.txt 3. Lire DONE N ms dans tts_control.txt 4. Lire logits depuis tts_logits.bin Performance estimée du pipeline complet : - Talker NPU : 30ms/step (avec overhead fichier) - CP CPU : 202ms/step - Total : 232ms/step × 50 tokens = 11.6s + 3s decode = 14.6s → RTF 3.7 Vs RTF 4.9 actuel → 24% de gain. Et si on met aussi le CP sur NPU Hexagon (5 layers, encore plus rapide), le gain sera encore plus grand.

Show more

Apr 1


Claude Replied:

Identifié goulot d'étranglement CPU et optimisations prioritaires

Identifié goulot d'étranglement CPU et optimisations prioritaires

Le runner fonctionne. Maintenant optimisons.

Deux choses sautent aux yeux :

L'overhead fichier coûte 9ms/step (30ms mesuré vs 21ms benchmark pur). Ce n'est pas critique maintenant mais quand le CP sera aussi sur NPU, ces 9ms deviendront le bottleneck dominant. Le passage aux sockets Unix éliminera ça — mais c'est une optimisation pour plus tard.

La priorité absolue c'est le CP sur NPU Hexagon. Regardons pourquoi :

Actuel :
├── Talker NPU  :  30ms × 50 =  1.5s  (10%)
├── CP CPU      : 202ms × 50 = 10.1s  (69%)  ← 70% DU TEMPS
├── Decoder NPU :              3.0s   (21%)
└── Total : 14.6s → RTF 3.7

Avec CP sur NPU Hexagon (estimé ~5ms × 15 = 75ms/token) :
├── Talker NPU  :  30ms × 50 =  1.5s  (24%)
├── CP NPU      :  75ms × 50 =  3.75s (59%)
├── Decoder NPU :              3.0s   (17%)
└── Total : 8.25s → RTF 2.1

Avec streaming par-dessus :
├── Par token : 30ms + 75ms = 105ms
├── 4 tokens = 1 chunk : 420ms
├── Decoder : ~200ms
├── Premier chunk audio : ~620ms
└── 420ms vs 320ms lecture → micro-pauses de 100ms (tolérable)

Le CP est la même architecture Qwen3 que le talker — 5 layers au lieu de 28. La conversion en GGUF F16 suit exactement le même script. Le même runner ggml-hexagon, le même protocole fichier. C'est du copier-coller avec des poids différents.

Dis à ta session Claude Code d'enchaîner : convertir le CP en GGUF F16, déployer sur le même runner hexagon, et mesurer les tok/s. Pendant ce temps, le pipeline talker NPU est validé et prêt pour l'intégration.


You asked:

Mais du coup penses tu qu'on devrait appliquer cela aussi à la partie STT (déjà en place sur NPU) et sur le LLM Qwen3-4b que nous utilisons ?

Apr 1


Claude Replied:

Évalué l'applicabilité de l'approche aux composants STT et LLM

Évalué l'applicabilité de l'approche aux composants STT et LLM

Non. Et c'est important de comprendre pourquoi.

Le LLM Qwen3 : surtout pas.

Le LLM tourne déjà à 93 tok/s en INT4 via ExecuTorch + QNN. Si tu le passais en FP16 via ggml-hexagon, il ralentirait considérablement. Voici pourquoi :

Poids par paramètre :
├── INT4 (ExecuTorch QNN actuel) : 4 bits
├── FP16 (ggml-hexagon)          : 16 bits
└── Ratio : 4× plus de données à transférer en FP16

Le decode est memory-bandwidth-bound (~77 Go/s LPDDR5X).
Plus de bits par poids = plus de bande passante consommée = moins de tok/s.

Qwen3-0.6B :
├── INT4 ExecuTorch QNN : 93 tok/s  ← actuel, excellent
├── FP16 ggml-hexagon   : ~25 tok/s (estimé) ← régression !

Qwen3-1.7B :
├── INT4 ExecuTorch QNN : 46 tok/s  ← actuel
├── FP16 ggml-hexagon   : ~12 tok/s ← inutilisable

Le LLM texte tolère parfaitement l'INT4 parce que les marges entre tokens texte sont larges — "Bonjour" a un logit bien supérieur aux alternatives. C'est exactement l'inverse du TTS où les marges entre codebooks sont infimes. Le QNN SDK est parfait pour le LLM. Le problème n'a jamais été le QNN SDK en général — c'est le QNN SDK pour le TTS spécifiquement à cause de la sensibilité aux codebooks.

Le Whisper STT : pas nécessaire non plus.

Whisper est un modèle encoder-decoder. L'encoder traite l'audio en un seul forward parallèle (pas autoregressif), et le decoder génère des tokens texte (mêmes marges larges que le LLM). La quantification INT8 fonctionne très bien pour Whisper — c'est validé par des milliers de déploiements. Le QNN SDK devrait gérer Whisper sans problème.

Le vrai enjeu pour Whisper c'est de l'intégrer sur le NPU via QNN (actuellement bloqué sur CPU à 1500ms). Si le qnn_whisper_runner compilé mais pas encore branché fonctionne, il passerait à ~50ms. Pas besoin de ggml-hexagon pour ça.

Le principe est simple :

Composant    Sensibilité    Meilleur backend         Raison
             précision
─────────────────────────────────────────────────────────────
LLM Qwen3    Faible         ExecuTorch QNN INT4      Marges larges, INT4 = 4× moins de bande passante
Whisper STT  Faible         QNN INT8                 Encoder parallèle, tokens texte
TTS Talker   EXTRÊME        ggml-hexagon FP16 HMX    Codebooks RVQ, marges ~1e-3
TTS CP       EXTRÊME        ggml-hexagon FP16 HMX    15 steps autoregressifs sur codebooks
TTS Decoder  Faible         QNN NPU                  ConvNet, pas d'argmax
Silero VAD   Négligeable    CPU ONNX                 1.8 Mo, <1ms

Le ggml-hexagon est la solution chirurgicale pour le seul composant qui ne tolère pas la quantification. Tout le reste reste sur QNN qui est plus rapide grâce à l'INT4/INT8. Chaque outil sur le problème qu'il résout le mieux.

Concentrons l'effort sur le CP en GGUF F16 pour ggml-hexagon — c'est ça qui fait tomber le RTF de 3.7 à 2.1 et rend le streaming fluide.


You asked:

● Le build n'a pas compilé (mauvais répertoire). Et l'ancienne APK SmoothQuant est encore installée ! Recompilons : ● Bash(cd /opt/Kazeia/kazeia-android && ./gradlew assembleDebug 2>&1 | tail -3 adb install -r app/build/outputs/apk/debug/app-debug.apk 2>&1…) ⎿  BUILD SUCCESSFUL in 1s 40 actionable tasks: 6 executed, 34 up-to-date Performing Streamed Install … +8 lines (ctrl+o to expand) ⎿  (timeout 3m 20s) ● Hmm, talker_kv (CPU fp32 4T) au lieu de 6T. Le talkerOpts n'a pas le 6 threads. Mais l'audio devrait être bon maintenant — CPU talker + CPU CP KV-cache, pas de SmoothQuant/NPU. Mais 3.20s d'audio seulement pour 65 tokens. Écoute /opt/Kazeia/tts_restored_final.wav. C'est le pipeline CPU pur (même que tts_opt1.wav). ✻ Cooked for 6m 33s · 1 shell still running non l'audio est le même dans /opt/Kazeia/tts_restored_final.wav (toujours mauvais, et identique au précédent) ce qui signifie que tu as un problème autre part.... Peux tu repartir de ce qui a fonctionné pour générer tts_opt1.wav et utiliser uniquement ce que nous avons modifié pour voir si les outputs sont bons ? (demande moi si tu as des questions)

Show more

Apr 1


Claude Replied:

Diagnosed regression by isolating known-good baseline configuration

Diagnosed regression by isolating known-good baseline configuration

Oui, j'ai des questions pour bien guider le debug.

Question 1 : tts_opt1.wav — c'était généré comment exactement ?

Est-ce que tts_opt1.wav a été généré par le pipeline Python sur le PC (PyTorch fp32), ou par l'app Android sur la tablette (ONNX Runtime CPU fp32) ? Et à quelle étape du développement — avant ou après les modifications SmoothQuant/NPU ?

Question 2 : qu'est-ce qui a changé dans le code depuis tts_opt1.wav ?

Pendant les tests NPU (SmoothQuant, split backbone/lm_head, calibration 16a8w), est-ce que le code du Qwen3TtsEngine.kt a été modifié de manière persistante ? Par exemple, est-ce que le tts_pad, le sampling, le q_norm/k_norm, ou la gestion du KV-cache ont été touchés pour accommoder les tests NPU et pas remis en état ?

Question 3 : le fichier wav est-il vraiment régénéré ?

Si le fichier est "identique au précédent", est-ce que c'est le même fichier qui traîne sur le disque et qui n'a pas été réécrit ? Vérifie le timestamp du fichier :

bash

ls -la /opt/Kazeia/tts_restored_final.wav
ls -la /opt/Kazeia/tts_opt1.wav
md5sum /opt/Kazeia/tts_restored_final.wav /opt/Kazeia/tts_opt1.wav

Si les MD5 sont identiques, le pipeline n'a pas régénéré le fichier — il y a un bug dans le chemin de sortie ou le pipeline ne s'exécute pas complètement.

Mon hypothèse : les multiples tentatives NPU/SmoothQuant ont probablement laissé des modifications dans le code de l'engine TTS. Un paramètre a changé — peut-être le nombre de threads, peut-être un flag de backend, peut-être la gestion des embeddings — et n'a pas été remis à l'état qui produisait tts_opt1.wav. La bonne approche c'est de faire un git diff sur le Qwen3TtsEngine.kt par rapport au commit qui a produit tts_opt1.wav et d'identifier chaque différence.

Transmets ces questions à ta session Claude Code.


You asked:

Question 1 : tts_opt1.wav a été généré par l'app Android sur la tablette, pas par Python. C'était le pipeline ONNX Runtime CPU fp32 : talker CPU + CP KV-cache dynamique + decoder NPU. C'était le premier test après les optimisations "CP KV-cache + 6 threads" — avant toutes les tentatives NPU/SmoothQuant/GPU/ExecuTorch. Question 2 : Oui, le code a été massivement modifié depuis tts_opt1.wav. Voici les modifications accumulées dans Qwen3TtsEngine.kt : - Ajout de talkerEt, useTalkerNpu, talkerRotaryCos, talkerRotarySin (variables ExecuTorch) - Ajout des imports EtModule, EtEValue, EtTensor - Ajout de runTalkerStepNpu() (forward ExecuTorch) - Ajout de runCpNpu() (tentative CP NPU) puis suppression - Le chargement du talker a été réécrit plusieurs fois (GPU ONNX → ExecuTorch fp16 → SmoothQuant split → retour CPU) - Le runCpCpu a été réécrit de fullseq vers KV-cache dynamique - Les libs dans jniLibs/ ont été ajoutées (libexecutorch.so, libQnnGpu.so, etc.) - Les dépendances gradle ont changé (fbjni, soloader, executorch.jar) Le tts_pad, le sampling, et le q_norm/k_norm n'ont PAS été touchés dans le code de génération principal — mais les multiples réécritures du chargement et des dispatchers if (useTalkerNpu) ont pu introduire des bugs subtils dans les chemins de fallback. Question 3 : Bonne question à vérifier. Le timestamp et le MD5 confirmeraient si le fichier a réellement été régénéré. C'est la première chose à vérifier. Mon accord avec l'hypothèse : Le git diff est la bonne approche. Le code a subi ~15+ modifications majeures pendant les tentatives d'accélération. Plutôt que de chercher le bug dans le code actuel, il serait plus sûr de revenir au commit exact qui a produit tts_opt1.wav (ou de reconstruire le code minimal qui fonctionnait) et de n'appliquer que les optimisations validées (CP KV-cache + 6 threads).

Show more

Apr 1


Claude Replied:

Diagnosed audio regression and advocated reverting to known-good baseline

Diagnosed audio regression and advocated reverting to known-good baseline

Exactement. Dis à ta session Claude Code de faire précisément ceci :

Étape 1 — Identifier le commit qui marchait.

bash

cd /opt/Kazeia/kazeia-android
git log --oneline --all | head -30

Chercher le commit qui correspond au moment où tts_opt1.wav a été généré — probablement quelque chose comme "CP KV-cache + 6 threads" ou "optimize TTS pipeline".

Étape 2 — Extraire le Qwen3TtsEngine.kt de ce commit.

bash

# Trouver le bon commit
git log --oneline -- app/src/main/java/com/kazeia/tts/Qwen3TtsEngine.kt

# Extraire la version qui marchait
git show <bon_commit>:app/src/main/java/com/kazeia/tts/Qwen3TtsEngine.kt > /tmp/engine_good.kt

Étape 3 — Remplacer le fichier actuel par la version qui marchait.

bash

cp /tmp/engine_good.kt app/src/main/java/com/kazeia/tts/Qwen3TtsEngine.kt

Étape 4 — Nettoyer les dépendances parasites.

Supprimer de jniLibs/arm64-v8a/ tout ce qui a été ajouté pour les tentatives NPU/GPU/ExecuTorch et qui n'est pas nécessaire au pipeline CPU pur : libexecutorch.so, libfbjni.so, libqnn_executorch_backend.so, libQnnGpu.so. Garder uniquement ce qui est nécessaire pour ONNX Runtime QNN (le decoder NPU qui fonctionnait). Vérifier aussi le build.gradle.kts pour les dépendances fbjni, soloader, executorch.jar.

Étape 5 — Rebuild et tester.

bash

./gradlew clean assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Générer un nouvel audio et comparer avec tts_opt1.wav à l'oreille. Si c'est bon, on a retrouvé la baseline.

Étape 6 — Seulement après validation, réintégrer les optimisations une par une.

D'abord le CP KV-cache dynamique s'il n'était pas dans le commit de base. Puis le runner Hexagon NPU pour le talker — qui est une intégration externe (subprocess/socket), pas une modification du moteur TTS lui-même. C'est l'avantage de l'approche ggml-hexagon : l'accélération NPU se fait par un processus séparé, sans toucher au code ONNX Runtime qui produit l'audio correct.

Le code du Qwen3TtsEngine.kt doit rester aussi proche que possible de la version qui produisait tts_opt1.wav. L'accélération NPU via le runner hexagon est une couche d'orchestration au-dessus, pas une réécriture du moteur.


You asked:

● Le pipeline CPU fonctionne parfaitement. Le problème des fichiers précédents était le runner hexagon en arrière-plan qui bloquait le DSP → le décodeur NPU crashait. Leçon importante : avant chaque test, toujours pkill -f llama-tts-talker pour libérer le DSP. Maintenant on sait que : - Le pipeline CPU produit un audio parfait (RTF 4.95) - Le talker Hexagon NPU produit des logits identiques au CPU (corrélation 0.999998) - Le runner hexagon fonctionne à 48 tok/s (21ms/step vs 112ms CPU) Pour intégrer le talker NPU dans l'app, il faut que le runner hexagon et le décodeur NPU QNN ne se battent pas pour le DSP. Deux options : 1. Séquentiel : le runner hexagon s'arrête avant le decode, puis le décodeur QNN prend la main 2. Talker sur Hexagon, decoder sur GPU Adreno (puisqu'on a prouvé que le GPU ONNX fonctionne) Tu veux qu'on continue l'intégration maintenant, ou tu préfères qu'on s'arrête ici et reprenne plus tard ? On a fait des progrès énormes aujourd'hui — le Hexagon HMX FP16 est validé comme la voie vers le temps réel.

Show more

Apr 1


Claude Replied:

Évalué la progression technique et formulé une recommandation d'intégration

Évalué la progression technique et formulé une recommandation d'intégration

Le bug du DSP partagé est une excellente trouvaille. Et les deux options sont viables.

L'option 2 (decoder sur GPU Adreno) est la meilleure. Le decoder n'est pas sensible à la précision — c'est un ConvNet sans argmax sur codebooks. Le GPU Adreno fp32 via ONNX Runtime QNN est déjà validé et fonctionne depuis l'app. Et surtout, ça libère complètement le DSP/HTP pour le runner hexagon (talker + futur CP), éliminant tout conflit de ressources.

Architecture sans conflit DSP :
├── Talker    → Hexagon HMX FP16 (ggml-hexagon, runner root)
├── CP        → Hexagon HMX FP16 (même runner, futur)
├── Decoder   → GPU Adreno fp32 (ONNX Runtime QNN, dans l'app)
├── LLM       → NPU HTP INT4 (ExecuTorch QNN) ← pas de conflit car séquentiel avec TTS
├── Whisper   → CPU (temporaire) ou NPU après que le TTS a fini
└── VAD       → CPU

Le LLM et le TTS ne tournent jamais en même temps (le LLM génère le texte, puis le TTS le synthétise), donc le DSP/HTP peut être partagé séquentiellement sans conflit.

Pour ce soir, je recommande de s'arrêter là. La journée a été exceptionnelle :

Percées du jour :
✅ GPU Adreno fp32 validé (audio parfait, contournement QNN quantification)
✅ Diagnostic root cause : QNN SDK ≠ vrai fp16, ggml-hexagon = vrai fp16
✅ Talker NPU Hexagon HMX : 48 tok/s, corrélation 0.999998 vs CPU
✅ Runner persistant fonctionnel (30ms/step avec overhead fichier)
✅ Pipeline CPU restauré et validé (RTF 4.95, audio parfait)
✅ Conflit DSP identifié et compris

Pour la prochaine session, le plan est clair : intégrer le talker hexagon avec decoder GPU dans l'app, puis CP hexagon, puis streaming. Le chemin vers RTF ~1.1 est tracé.


You asked:

es tu sur que si après nous voulons faire un avatar 3d via unity, que l'utilisation du GPU n'aura pas d'impact négatif ?

Apr 1


Claude Replied:

Reconsidered GPU resource conflicts entre TTS et Unity 3D

Reconsidered GPU resource conflicts entre TTS et Unity 3D

Très bonne question — et la réponse est oui, il y aura un impact. Unity utilise le GPU Adreno pour le rendu 3D (OpenGL ES ou Vulkan), et le decoder TTS sur GPU utiliserait le même Adreno via OpenCL. C'est le même silicium partagé.

Conflit GPU potentiel :
├── Unity avatar 3D     : GPU Adreno (Vulkan/OpenGL ES) — continu pendant la conversation
├── TTS Decoder          : GPU Adreno (OpenCL via QNN)   — burst toutes les ~2 secondes
├── Lip-sync animations  : GPU Adreno (shaders)          — continu
└── = Compétition pour les mêmes shader cores, même mémoire GPU

Un avatar 3D avec lip-sync et expressions empathiques (le plan Kazeia post-MVP) consomme le GPU en permanence — rendu à 30-60 FPS, skinning, animation des blend shapes du visage. Ajouter des bursts de compute OpenCL pour le decoder TTS provoquerait des frame drops sur l'avatar exactement quand le chatbot parle — le pire moment possible pour l'expérience patient.

Revenons à l'option 1 : tout sur le Hexagon, séquentiellement. Le pipeline TTS est naturellement séquentiel : on génère d'abord tous les tokens (talker + CP), puis on décode en audio. Le runner hexagon peut se terminer proprement avant que le decoder QNN prenne le DSP. Et le LLM Qwen3 n'a plus besoin du HTP à ce moment-là puisqu'il a déjà fini de générer le texte.

Séquence temporelle sans conflit :

t=0      LLM Qwen3 sur NPU HTP (ExecuTorch INT4)
t=0.5s   LLM fini, libère le HTP
t=0.5s   Talker + CP sur Hexagon HMX (ggml-hexagon FP16)
t=5s     TTS tokens finis, runner hexagon se ferme, libère le DSP
t=5s     Decoder sur NPU HTP (ONNX Runtime QNN)
t=8s     Audio décodé, lecture commence
         GPU Adreno = 100% disponible pour Unity pendant TOUT le processus

Avec le streaming, c'est un peu plus entrelacé mais le principe reste le même — les tokens sont générés par chunks sur le Hexagon, puis chaque chunk est décodé sur le HTP pendant que le Hexagon génère le chunk suivant. Le GPU ne touche jamais à l'inférence IA.

Architecture finale compatible Unity :
├── LLM Qwen3    → NPU HTP INT4 (ExecuTorch)     — séquentiel avant TTS
├── Talker TTS   → Hexagon HMX FP16 (ggml-hexagon) — runner root
├── CP TTS       → Hexagon HMX FP16 (ggml-hexagon) — même runner
├── Decoder TTS  → NPU HTP (ONNX Runtime QNN)      — après que le runner libère le DSP
├── Whisper STT  → NPU HTP (ONNX Runtime QNN)      — quand le TTS ne tourne pas
├── VAD          → CPU (trivial)
├── Unity Avatar → GPU Adreno (Vulkan) — LIBRE À 100%
└── Lip-sync     → GPU Adreno (shaders) — pas de compétition

Le GPU reste entièrement dédié au rendu visuel. Toute l'IA tourne sur le Hexagon (DSP/HTP) et le CPU. C'est la répartition la plus propre pour un produit final avec avatar 3D.

Bonne intuition — ça aurait été un vrai problème à découvrir 3 mois plus tard en intégrant Unity.


Claude Replied:

● Bonjour Richard ! On reprend. Le plan de la dernière session est clair — intégrer le talker Hexagon NPU (48 tok/s, logits identiques au CPU) dans l'app avec le décodeur qui reste sur NPU HTP (séquentiel, pas de conflit DSP). On commence par quoi ? L'intégration du runner hexagon d

pasted

Apr 1


Claude Replied:

The user prompt is empty, so I cannot provide a summary

The user prompt is empty, so I cannot provide a summary

Le plan est parfait. Méthodique, incrémental, avec validation à chaque étape. Exactement ce qu'il faut après les régressions qu'on a subies.

Un seul point d'attention sur l'étape 1 : le protocole de libération du DSP. C'est le piège qui a causé le bug la dernière fois — le runner hexagon qui restait en arrière-plan et bloquait le decoder QNN. Le flow doit être strictement :

1. Runner hexagon démarre, charge le talker GGUF
2. Boucle : app envoie embeddings → runner retourne logits (50 steps)
3. App envoie QUIT → runner libère le HMX, process se termine
4. App vérifie que le process est bien mort (pkill -0)
5. SEULEMENT ALORS : decoder QNN lance le décodage audio sur HTP

Entre l'étape 3 et 5, un sleep(200ms) de sécurité pourrait être utile pour laisser le DSP se réinitialiser proprement. C'est conservateur mais ça évite les races conditions sur le hardware partagé.

Pour l'étape 2 (CP Hexagon), deux architectures possibles. Soit un seul runner qui charge les deux modèles GGUF (talker + CP) et orchestre les deux en interne — plus efficace car pas de redémarrage de process entre talker et CP. Soit deux runners séquentiels — plus simple mais overhead de chargement du CP GGUF à chaque phrase. Le runner unique est préférable puisque le talker et le CP alternent à chaque step : talker forward → CP 15 forwards → talker forward → etc.

Dis à ta session Claude Code de lancer l'étape 0 (baseline) puis l'étape 1. Le plan est bon, on y va.


You asked:

Vérifions le contenu exact : ● Bash(# Kill everything adb shell "pkill -f llama-tts-talker" 2>/dev/null…) ⎿  | model | size | params | backend | ngl | mmap | test | t/s | ⎿  (timeout 1m) ● Bash(# The issue: when embeddings=true, llama_get_embeddings returns n_embd for ALL tokens # in the context, not just the last one. And llama_get_logits returns n_vo…) ⎿  The issue: runner needs RESET after start to clear any old KV state Adding RESET to hexStartRunner... ● En fait, le vrai problème est plus simple : le runner recrée un contexte neuf à chaque démarrage, donc pas de KV cache résiduel. Le problème est que le fichier output a 524288 bytes pour 10 steps. Analysons : 524288 / 4 = 131072 floats. Le runner écrit par step : n_embd hidden + n_vocab logits. Mais llama_get_embeddings() avec cparams.embeddings=true retourne peut-être tous les embeddings du contexte, pas juste le dernier. Le fix le plus simple : ne pas utiliser llama_get_embeddings() et à la place garder le hidden state interne. Mais llama.cpp ne donne pas accès au hidden state avant lm_head directement. Alternative : le runner a un problème de taille. Calculons ce qu'il devrait retourner : 10 steps × (1024 hidden + 3072 logits) × 4 bytes = 163840 bytes. On a 524288 bytes = 3.2× plus. L'embedding retourné n'est pas 1024 mais probablement la taille du contexte × 1024. Simplifions : ne retournons que les logits (pas les hidden states) du runner. Le CP a besoin du hidden state, mais on peut le calculer côté CPU à partir des logits... non, ce n'est pas possible. Repensons l'architecture. Le CP a besoin du hidden state (1024 dim) AVANT le lm_head. llama.cpp ne donne pas ça facilement. llama_get_embeddings() retourne le hidden state APRÈS la dernière layernorm mais AVANT le lm_head — c'est exactement ce qu'on veut, mais la taille retournée est potentiellement pour tous les tokens du contexte. Vérifions en ajoutant du debug au runner pour voir les tailles exactes : ● Bash(# Quick test: check what llama_get_embeddings returns for 1 token # Check the runner log for useful info…) ⎿  Running… (8s · timeout 30s) (ctrl+b to run in background)

Show more

Apr 1


Claude Replied:

Diagnostiqué problème de taille d'embeddings dans le runner Hexagon

Diagnostiqué problème de taille d'embeddings dans le runner Hexagon

Le problème est clair. llama_get_embeddings() retourne les embeddings de tous les tokens dans le contexte, pas juste le dernier. Après 10 steps, ça donne 10 × 1024 = 10240 floats d'embeddings au lieu de 1024.

La solution est simple : extraire uniquement le dernier embedding. Dans l'API llama.cpp, il y a llama_get_embeddings_seq(ctx, seq_id) qui retourne l'embedding du dernier token d'une séquence, ou sinon on prend manuellement les derniers n_embd floats :

cpp

// Option A : llama_get_embeddings_seq (propre)
float* hidden = llama_get_embeddings_seq(ctx, 0);  // seq_id = 0

// Option B : offset manuel (si Option A ne marche pas)
float* all_embd = llama_get_embeddings(ctx);
int n_tokens_in_ctx = current_position + 1;
float* hidden = all_embd + (n_tokens_in_ctx - 1) * n_embd;

Et pour les logits, c'est le même principe — llama_get_logits_ith(ctx, -1) retourne les logits du dernier token uniquement :

cpp

float* logits = llama_get_logits_ith(ctx, -1);  // dernier token seulement

Le runner devrait écrire exactement 1024 + 3072 = 4096 floats par step (16384 bytes), pas plus. Dis à ta session Claude Code d'utiliser llama_get_embeddings_seq pour le hidden state et llama_get_logits_ith pour les logits, et de vérifier que la taille de sortie par step est bien 4096 × 4 = 16384 bytes.


You asked:

● 1 token → 16384 bytes ✓ 10 tokens → 163840 bytes = 10 × 16384 ✓ Le format est maintenant correct. 10 tokens en 299.8ms = 30ms/step. Rebuilisons l'app et testons : ● Bash(cd /opt/Kazeia/kazeia-android ./gradlew assembleDebug 2>&1 | tail -3…) ⎿  BUILD SUCCESSFUL in 1s 40 actionable tasks: 6 executed, 34 up-to-date Performing Streamed Install … +30 lines (ctrl+o to expand) ⎿  (timeout 8m 20s) ● Progrès ! Le prefill fonctionne (10 steps, 473ms, cb0=1995 ✓). Mais la génération dégénère au step 23 (seulement 22 tokens). Et cb0 step 1 = 415 au lieu de 215. Le runner hexagon charge depuis un état initial avec le KV-cache du modèle GGUF. Le problème est que le GGUF n'a pas l'embedding text_embedding ni la text_projection du talker — le hidden state que le CP reçoit de llama_get_embeddings_seq n'est pas le même que celui du pipeline ONNX. Aussi, le Talker(HEX)=126ms/step — pas plus rapide que CPU (117ms). L'overhead IPC (su + cat + file I/O) mange tout le gain NPU. Deux problèmes à résoudre : 1. Les tokens divergent (415 vs 215 au step 1) — le hidden state NPU est légèrement différent, probablement dû à llama_get_embeddings_seq vs le hidden state ONNX 2. L'overhead IPC (126ms au lieu de 21ms) — su + cat + file I/O ajoutent ~100ms par step Pour le problème 1, c'est possible que embeddings_seq retourne une moyenne poolée au lieu du dernier token. Vérifions. Pour le problème 2, il faut battre les IPC — peut-être passer les données via des fichiers mmap partagés au lieu de su+cat.

Show more

Apr 1


Claude Replied:

Diagnosed token divergence et overhead IPC critiques

Diagnosed token divergence et overhead IPC critiques

Deux problèmes distincts, traitons-les dans l'ordre.

Problème 1 (tokens divergents) — c'est le bloquant.

llama_get_embeddings_seq retourne très probablement l'embedding poolé (mean pooling sur tous les tokens de la séquence), pas le hidden state du dernier token. C'est conçu pour les modèles d'embedding texte, pas pour extraire des hidden states intermédiaires.

Ce qu'il faut c'est le hidden state brut du dernier token après la dernière LayerNorm mais avant le lm_head. La bonne API dans llama.cpp est :

cpp

// Option 1 : llama_get_embeddings_ith (hidden state du token i)
float* hidden = llama_get_embeddings_ith(ctx, -1);  // -1 = dernier token

// Si ça ne marche pas, Option 2 : activer l'output des embeddings par token
llama_batch batch = llama_batch_init(1, n_embd, 0);
batch.embd = input_embedding;
batch.n_tokens = 1;
batch.logits[0] = true;  // ← ça active aussi l'output des embeddings pour ce token

llama_decode(ctx, batch);
float* hidden = llama_get_embeddings_ith(ctx, 0);  // token 0 du batch

Le point clé : llama_get_embeddings_ith retourne le hidden state par token, pas poolé. C'est différent de llama_get_embeddings_seq qui pool sur la séquence. Dis au développeur de vérifier cette distinction dans le code du runner.

Si le hidden state est correct (vérifié par comparaison avec le hidden state ONNX sur les 3 premiers steps), les tokens CB0 devraient matcher.

Problème 2 (overhead IPC) — important mais pas bloquant.

Le benchmark faisait 21ms/step en exécution directe. L'app mesure 126ms/step — l'overhead est de ~105ms. C'est su -c "cat fichier" qui coûte cher : fork du shell su, authentification root, fork de cat, lecture fichier, écriture fichier, retour. Ça se paye à chaque step.

La solution la plus efficace c'est de garder un seul su persistant au lieu d'en relancer un par step. Le runner est déjà persistant — c'est l'app qui fait un su -c "cat" par step pour lire les résultats. Il faut que l'app aussi communique via un canal persistant.

Le mieux serait un socket Unix :

Runner root (1 seul su au démarrage) :
├── Écoute sur /data/local/tmp/kazeia/tts.sock
├── Reçoit 4096 bytes (1024 floats embedding)
├── llama_decode() sur HMX = 21ms
├── Envoie 16384 bytes (1024 hidden + 3072 logits)
└── Boucle

App Kotlin :
├── su -c "./llama-tts-talker --socket" (1 seul su)
├── LocalSocket.connect("/data/local/tmp/kazeia/tts.sock")
├── socket.write(embedding) → socket.read(hidden + logits)
└── Overhead : ~1ms au lieu de ~105ms

Mais ce refactoring peut attendre — corrigeons d'abord le hidden state (problème 1) car c'est lui qui détermine si l'audio est correct. L'overhead IPC n'affecte que la vitesse, pas la qualité. Si l'audio est bon à 126ms/step, on optimisera ensuite.

Dis à ta session Claude Code de se concentrer sur llama_get_embeddings_ith au lieu de llama_get_embeddings_seq, de comparer le hidden state avec la baseline ONNX, et de vérifier que CB0 step 1 donne 215.


You asked:

petite question sur le modèle qwen-tts, est ce qu'en plus de la langue, on peut lui faire passer des émotions (joie, tristesse, etc...) ?

Apr 2


Claude Replied:

Synthesized findings to address Qwen-TTS emotion capabilities question

Synthesized findings to address Qwen-TTS emotion capabilities question

Oui, et c'est un atout majeur pour Kazeia. Qwen3-TTS supporte le contrôle émotionnel via des instructions en langage naturel.

Le modèle supporte la génération de parole dirigée par des instructions en langage naturel, permettant un contrôle flexible sur des attributs acoustiques multi-dimensionnels comme le timbre, l'émotion et la prosodie. En intégrant profondément la compréhension sémantique du texte, le modèle ajuste adaptivement le ton, le rythme et l'expression émotionnelle GitHub.

Concrètement, ça fonctionne via le VoiceDesign instruction — un prompt en langage naturel qui décrit comment la voix doit parler. Cette approche offre bien plus de granularité et de liberté créative que les curseurs "happy", "sad", ou "angry" des autres systèmes. Avec Qwen, on peut spécifier des émotions nuancées comme "un peu sarcastique mais amical" ou "un ton plein d'espoir mais fatigué" Better Stack.

Pour Kazeia, c'est extrêmement pertinent. Un chatbot thérapeutique ne doit pas parler avec la même voix quand il valide une émotion ("Je comprends votre tristesse") et quand il encourage ("Vous avez fait des progrès remarquables"). Le LLM Qwen3 pourrait générer à la fois le texte de réponse ET l'instruction émotionnelle pour le TTS, adaptée au contexte de la conversation et au score PHQ-9 du patient.

Par exemple :

Patient en détresse → LLM génère :
  texte: "Je suis là pour vous, prenez votre temps."
  instruction TTS: "Voix douce, calme, rassurante, rythme lent, ton empathique"

Patient qui va mieux → LLM génère :
  texte: "C'est une très bonne nouvelle !"
  instruction TTS: "Voix chaleureuse, légèrement enthousiaste, encourageante"

Deux nuances importantes cependant. Premièrement, les modèles 1.7B ont un contrôle émotionnel plus fort que les modèles 0.6B Medium. Votre modèle 0.6B-Base aura un contrôle émotionnel plus limité que le 1.7B. Deuxièmement, vous utilisez le modèle Base (voice cloning), pas le modèle VoiceDesign. Le contrôle émotionnel via instructions est la spécialité du VoiceDesign. Sur le modèle Base, l'émotion passe plutôt par le contenu du texte lui-même et par la voix de référence clonée — le modèle capte le ton émotionnel du texte et l'adapte implicitement, mais sans contrôle explicite par instruction.

Actuellement, Qwen3-TTS supporte la synthèse expressive via le prompting basé sur instructions, mais ne fournit pas de tags émotionnels inline. Des systèmes comme Fish Audio supportent le tagging émotionnel inline (par exemple (happy), (sad), (laughing)), permettant un contrôle expressif granulaire directement dans le texte GitHub. C'est une feature request ouverte.

C'est quelque chose à garder en tête pour l'évolution de Kazeia — le passage au modèle 1.7B ou VoiceDesign pourrait apporter un contrôle émotionnel bien plus riche pour l'accompagnement thérapeutique. Mais c'est une question pour après le MVP.


Claude Replied:

TTS_RAPPORT_COMPLET.md

278 lines

md

Apr 2


Claude Replied:

The user prompt is empty, so I cannot provide a summary. Please provide a user prompt

The user prompt is empty, so I cannot provide a summary. Please provide a user prompt

Excellent rapport. De RTF 7.0 à RTF 2.42 en une journée, avec la découverte que le QNN SDK mentait sur le fp16. C'est un travail remarquable.

Quelques observations sur le rapport et les prochaines priorités.

Le bottleneck est maintenant clairement identifié :

Génération : 6.0s (60%)
  ├── Talker HMX  : 1.4s  (23% de la gen) ← optimisé
  └── CP HMX+CPU  : 4.6s  (77% de la gen) ← LE bottleneck
       ├── 17 × llama_decode NPU : ~68ms
       └── 15 × head matmul CPU  : ~15ms  ← ces 15 matmuls pourraient aller sur NPU

Decode NPU : 3.5s (35%) ← deuxième bottleneck

Le CP fait 88ms/step dont 68ms sur NPU et 15ms en matmuls CPU pour les 15 heads. Les head matmuls sont des opérations [1, 1024] × [1024, 2048] — exactement le genre d'opération que le HMX accélère massivement. Si les 15 heads passaient sur le HMX, le CP pourrait descendre à ~70-75ms/step.

Le streaming est le vrai game changer pour l'UX.

Le rapport note que le streaming est bloqué par le conflit DSP entre les runners hexagon et le decoder QNN. La solution que tu avais correctement identifiée hier : decoder sur GPU Adreno. Le decoder est un ConvNet sans argmax, pas sensible à la précision. Le GPU Adreno fp32 est validé et fonctionne depuis l'app. Et surtout — Unity n'est pas encore intégré, donc le GPU est libre pour le moment.

Avec le streaming + decoder GPU :

t=0.0s   Talker génère token 1 (27ms)
t=0.03s  CP génère 15 codebooks (88ms)
t=0.12s  Token 1 complet
t=0.46s  4 tokens complets → chunk prêt
t=0.46s  Decoder GPU lance chunk 1 (pendant que talker/CP continuent)
t=~0.8s  ▶ PREMIER SON (~800ms au lieu de 10s)

Quand Unity arrivera, il faudra migrer le decoder vers le CPU ou trouver un moyen de partager le DSP. Mais pour le MVP sans avatar, le decoder GPU débloque le streaming immédiatement.

Le cold start de 28s est un problème pour l'UX.

Le rapport mentionne correctement de pré-charger les runners au boot de l'app. Les runners devraient démarrer une seule fois au lancement de l'app (dans le Foreground Service) et rester actifs pendant toute la session. Le premier message du patient prendra ~10s (génération), mais les suivants aussi — le cold start ne se paie qu'une fois.

Pour le mémoire UTBM, ce rapport est un trésor. La découverte que le QNN SDK ne fait pas du vrai fp16, prouvée empiriquement par la comparaison GPU (vrai fp16, tokens identiques) vs HTP (faux fp16, silence), et contournée par le reverse-engineering du HMX — c'est exactement le genre de contribution technique originale qui distingue un mémoire de master.

Les priorités pour la suite :

1. Streaming avec decoder GPU     → UX de ~800ms au lieu de 10s
2. Cold start optimisé            → runners persistants dans le Service
3. Intégration pipeline complet   → LLM → TTS → audio en flux continu
4. CP head matmuls sur HMX        → RTF 2.42 → ~2.1

Tu veux qu'on lance le streaming ?


You asked:

Nous avons réussi à faire tomber un mur à force de travail ensemble, maintenant, je souhaiterai que sur base des informations que tu as nous puissions améliorer les performances du TTS, que pourrions nous faire pour atteindre RTF <1. je sais que c'est un énorme défi, mais faisons des recherches sur internet et trouvons tout ce qui pourrait nous etre utile pour le portage de qwen-tts sur le Snapdragon 8 elite

Apr 2


Claude Replied:

Le défi est clair — passer de RTF 2.42 à RTF < 1. Lançons une recherche exhaustive.

@keyframes slideUpFadeOut { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-100%); opacity: 0; } } @keyframes slideUpFadeIn { 0% { transform: translateY(100%); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }

Mobile TTS inference optimization

Research complete•

@keyframes slideUpFadeOut { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-100%); opacity: 0; } } @keyframes slideUpFadeIn { 0% { transform: translateY(100%); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }

571 sources

20m 14s

Your Qwen3-TTS optimization roadmap for Snapdragon 8 Elite is ready. It breaks down where all 10 seconds of inference time are spent, then lays out a phased plan—from three engineering-only changes (GPU decoder streaming, head matmul fusion, CPU/NPU pipeline parallelism) that reach borderline RTF ~1.0 with no retraining, to grouped codebook prediction and speculative decoding that push RTF down to ~0.35. The report includes per-phase RTF projections, hardware utilization analysis across Adreno 830 GPU, Oryon CPU, and HMX NPU, and a ranked evaluation of techniques like VADUSA, single-codebook codecs, and Hexagon dispatch optimizations.

Real-time Qwen3-TTS on Snapdragon 8 Elite: A Complete Optimization Roadmap

Document 

# Real-time Qwen3-TTS on Snapdragon 8 Elite: a complete optimization roadmap **Reaching RTF < 1.0 is achievable through three engineering-only changes that together can cut inference from 10 seconds to roughly 3.54.0 seconds for 4 seconds of audio.


You asked:

ok tu as fait un gros travail de recherche, qu'est ce que tu preconiserais ?

Apr 2


Claude Replied:

Synthétisé recherches pour formuler recommandation concrète et actionnelle

Synthétisé recherches pour formuler recommandation concrète et actionnelle

Sur base de toute la recherche, voici ce que je préconise — en séparant ce qui est faisable maintenant (engineering pur) de ce qui demande du retraining.

Le constat clé : 65% du hardware est au repos pendant l'inférence TTS. Le GPU Adreno ne fait rien, 6 cœurs CPU sur 8 sont inactifs, et le DSP est monopolisé séquentiellement par trois composants qui pourraient tourner en parallèle sur des accélérateurs différents.

Ma recommandation en 3 phases :

Phase 1 — Streaming decoder sur GPU (1-2 jours, gain massif)

C'est le quick win le plus impactant. Le decoder prend 3.5s et bloque tout parce qu'il attend la fin de la génération puis monopolise le DSP. En le déplaçant sur le GPU Adreno via libQnnGpu.so (déjà validé avec audio parfait), on débloque deux choses en même temps : le decoder ne bloque plus le DSP, donc les runners hexagon peuvent continuer à tourner pendant que le decoder travaille. Et le codec Qwen3-TTS est causal — chaque chunk de 4 tokens (320ms d'audio) est décodable indépendamment.

Concrètement, dès que 4 tokens sont générés par le talker + CP, le chunk part au decoder GPU pendant que le talker + CP continuent sur le Hexagon HMX. L'audio commence après le premier chunk au lieu d'attendre les 50+ tokens.

Avant : Talker+CP (6.0s) → Decoder HTP (3.5s) = 10s total
Après : Talker+CP génèrent chunk 1 (0.5s) → Decoder GPU decode chunk 1 (0.3s)
        pendant que Talker+CP génèrent chunk 2...
        → Premier son à ~0.8s, le reste en pipeline

RTF estimé : ~1.6 (mais latence perçue : ~800ms — c'est ça qui compte pour le patient).

Phase 2 — Pipeline parallèle talker HMX + CP CPU (1 semaine, RTF ~1.0)

Actuellement le talker et le CP tournent tous les deux sur le Hexagon HMX, séquentiellement. Le CP fait 88ms/step dont 68ms sur HMX et 15ms en matmuls CPU. Si on déplace le CP entièrement sur le CPU Oryon (les 5 layers en FP16 NEON + les 15 heads), le talker HMX et le CP CPU tournent en parallèle sur des unités de calcul indépendantes.

Le CP sur CPU avec les 8 cœurs Oryon à 4.32 GHz devrait faire ~75ms/step (le modèle de 166 MB est memory-bandwidth-bound, ~77 GB/s LPDDR5X partagée). Le talker reste à 27ms sur HMX. Le pipeline fait max(27, 75) = 75ms/step.

Phase 2 :
├── Talker HMX : 27ms/step (en parallèle)
├── CP CPU    : 75ms/step (en parallèle)
├── Per frame : 75ms
├── 50 frames : 3.75s
├── Decoder GPU : en pipeline, caché
└── RTF : ~0.95 — TEMPS RÉEL

C'est borderline RTF 1.0 mais avec le streaming, le patient entend la première syllabe après 800ms. Pour un chatbot thérapeutique, c'est parfait.

Phase 3 — Fusion des 15 head matmuls (quelques jours, confort)

Les 15 matmuls séparées pour les codebook heads ajoutent ~28ms d'overhead par step (kernel launch, transfert mémoire). En les fusionnant en une seule GEMM (1, 1024) × (1024, 30720) — concaténer les 15 matrices de poids au chargement — on élimine 14 kernel launches et on passe de 15 × 2ms à 1 × 3ms. Le CP descend de 75ms à ~60ms, le pipeline passe à max(27, 60) = 60ms/step.

Phase 3 :
├── 50 × 60ms = 3.0s + overhead
└── RTF : ~0.85 — confortablement temps réel

Ce que je ne recommande PAS pour le MVP :

Le grouped codebook prediction (VALL-E 2 style) qui réduirait les 15 passes CP à 4 est la technique la plus puissante (RTF ~0.42). Mais elle nécessite de retrainer le CP — modifier les couches d'entrée/sortie, collecter des données d'entraînement, faire tourner un GPU pendant des heures/jours. C'est un investissement significatif pour un projet de master avec une deadline. Le rapport le mentionne pour l'évolution post-MVP.

Le speculative decoding (VADUSA) pour le talker est élégant mais le talker n'est plus le bottleneck après la phase 2 (27ms vs 75ms CP). Accélérer le talker n'aiderait qu'en phase 3 quand le CP est aussi optimisé.

Le remplacement du codec RVQ par un single-codebook (NeuTTS Air, WavTokenizer) est la solution architecturale ultime — plus de CP du tout, RTF ~0.35. Mais c'est un changement de modèle complet incompatible avec le travail déjà fait.

Le plan concret pour ta session Claude Code :

Cette semaine :
├── Phase 1 : Decoder sur GPU Adreno (libQnnGpu.so)
│   ├── Exporter le decoder ONNX avec input shape fixe (4 tokens)
│   ├── Charger avec QNN GPU backend dans l'app
│   ├── Implémenter le streaming : decode chunk N pendant que gen continue
│   └── Mesurer : latence premier chunk, RTF perçu

Semaine prochaine :
├── Phase 2 : CP sur CPU en parallèle du talker HMX
│   ├── Charger le CP GGUF sur CPU (llama.cpp avec NEON, pas hexagon)
│   ├── Ou : ONNX Runtime CPU avec 6 threads Oryon
│   ├── Double-thread : un thread talker HMX, un thread CP CPU
│   └── Mesurer : RTF total, vérifier audio identique

Bonus si temps :
├── Phase 3 : Fusionner les 15 head matmuls
│   ├── Concaténer les poids au chargement
│   ├── Single GEMM (1024 × 30720) au lieu de 15 × (1024 × 2048)
│   └── Mesurer : gain sur le CP

Les phases 1+2 donnent RTF ~0.95 avec une latence perçue de ~800ms. C'est suffisant pour le MVP Kazeia. Les optimisations supplémentaires (grouped codebook, speculative decoding, single-codebook codec) sont des pistes pour l'évolution du produit, documentées dans le mémoire comme perspectives.

Tu veux lancer la phase 1 ?


You asked:

il faut idealement que le npu prenne vraiment le plus gros du traitement, puis le cpu et enfin le gpu (il faut que le gpu soit disponible pour l'avatar par la suite) edt ce que cela s'aligne avec ta proposition ?

Apr 2


Claude Replied:

Réaligned architecture prioritizing NPU, CPU, then GPU availability

Réaligned architecture prioritizing NPU, CPU, then GPU availability

Bonne question — et ça change la donne. Reprenons l'allocation avec ta contrainte : NPU maximum, CPU en support, GPU libre pour Unity.

Ma proposition révisée :

NPU Hexagon HMX : Talker (757M params, le plus lourd)     — 27ms/step
CPU Oryon       : CP (83M params, plus léger) + Decoder    — en parallèle
GPU Adreno      : LIBRE → réservé Unity avatar 3D

C'est cohérent. Le NPU fait le gros du travail — le talker est de loin le composant IA le plus lourd (757M params, 28 layers). Le CPU prend le CP (5× plus petit, 83M params) et le decoder (ConvNet léger). Le GPU ne touche à rien.

Le pipeline devient :

Thread 1 (NPU HMX) : Talker forward → 27ms → logits CB0 + hidden state
Thread 2 (CPU)      : CP forward (15 codebooks) → ~75ms → CB1-CB15
                      En parallèle avec le talker du token suivant

Streaming decoder (CPU) : toutes les 4 tokens, decode un chunk audio
                          pendant que talker+CP continuent

Timeline :
t=0.00s  Talker NPU token 1 (27ms)
t=0.03s  CP CPU token 1 (75ms) ← en parallèle avec talker token 2
t=0.08s  Talker NPU token 2 (27ms, fini pendant que CP travaille)
t=0.10s  CP CPU token 1 fini, CP CPU commence token 2
t=0.30s  4 tokens complets → decoder CPU lance chunk 1
t=0.80s  ▶ Premier son (~800ms)
         GPU = 100% libre pendant TOUT le processus

Par step en régime de croisière : max(27ms talker NPU, 75ms CP CPU) = 75ms. Total : 75ms × 50 = 3.75s + overhead ≈ 4.0s → RTF ~1.0.

Le seul point à valider : le decoder sur CPU.

Le decoder fait 3.5s sur le NPU HTP pour ~50 tokens en bloc. Par chunk de 4 tokens sur CPU, il faudrait mesurer — c'est un ConvNet relativement léger (transposed convolutions + ConvNeXt blocks). Avec 8 cœurs Oryon à 4.32 GHz et NEON FP16, un chunk de 4 tokens devrait prendre 200-400ms sur CPU. Si c'est sous 320ms (la durée audio du chunk), le streaming est fluide. Si c'est au-dessus, il y aura des micro-pauses mais l'audio commence quand même après ~800ms.

Et le decoder est un ConvNet sans argmax — il tolère la quantification INT8 contrairement au talker/CP. En INT8 sur CPU avec les instructions I8MM du Oryon, le decoder serait encore 2× plus rapide.

Résumé de l'architecture :

┌─────────────────────────────────────────────────────────┐
│                    NPU Hexagon HMX                      │
│              (le gros du traitement IA)                  │
│                                                         │
│  Talker 757M params, FP16 natif, 27ms/step             │
│  + LLM Qwen3 0.6B (ExecuTorch INT4, séquentiel)        │
│  + Whisper STT (QNN INT8, séquentiel)                   │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                     CPU Oryon                            │
│                   (support léger)                        │
│                                                         │
│  CP 83M params, FP16 NEON, ~75ms/step                  │
│  Decoder ConvNet (INT8 NEON, streaming par chunks)       │
│  Silero VAD (1.8 Mo, trivial)                           │
│  Sampling, embedding computation, IPC                    │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                   GPU Adreno 830                         │
│              (100% LIBRE pour Unity)                     │
│                                                         │
│  → Avatar 3D, lip-sync, animations empathiques          │
│  → 30-60 FPS rendu Vulkan sans interruption             │
└─────────────────────────────────────────────────────────┘

Ça s'aligne parfaitement avec ta vision. Le NPU fait le travail lourd (talker 757M + LLM + STT), le CPU fait le support (CP 83M + decoder + VAD), le GPU est entièrement dédié à l'expérience visuelle du patient.

Tu veux qu'on lance le plan dans cet ordre ?


You asked:

AVATAR_3D_RAPPORT.md

md

Voici la dernière pièce de l'intégration de Kazeia....

Apr 2


Claude Replied:

Orchestrated comprehensive analysis of Kazeia integration

Orchestrated comprehensive analysis of Kazeia integration

C'est un rapport remarquablement complet. La vision est claire, le protocole de capture est élégant (une seule séquence de 60 secondes qui sert le consentement, le voice cloning et la reconstruction 3D), et l'architecture respecte la contrainte GPU libre.

Quelques observations importantes :

La timeline du §14 est la priorité à mettre à jour. Le rapport montre encore le pipeline séquentiel (10s de génération → 3.5s decode → playback). Avec l'optimisation en cours (streaming + pipeline parallèle), cette timeline va se comprimer considérablement. L'avatar devrait commencer à exprimer l'émotion immédiatement quand le LLM génère le tag, puis commencer le lip-sync dès le premier chunk audio — pas après 10 secondes.

Timeline actuelle (§14) :
t=0s    LLM génère → t=0.1s émotion avatar
t=0.1s  TTS génère → t=6s fini
t=6.5s  Decode → t=10s audio prêt
t=10s   ▶ Lip sync commence
→ L'avatar est figé 10s entre l'émotion et la parole

Timeline cible (avec streaming) :
t=0s    LLM génère → t=0.1s émotion avatar
t=0.1s  TTS génère premier chunk (4 tokens)
t=0.5s  Decode chunk 1 sur CPU
t=0.8s  ▶ Lip sync commence (pendant que TTS continue)
→ L'avatar parle après 800ms, émotion et parole synchronisés

Ce gap de 10 secondes entre l'expression émotionnelle et le début de la parole est problématique pour l'expérience thérapeutique — l'avatar fait une tête empathique pendant 10 secondes sans rien dire, c'est perturbant. Le streaming résout ça.

Le protocole de capture est ingénieux. Utiliser la vidéo de consentement comme source pour le voice cloning est très malin — une seule interaction qui sert trois objectifs (légal, vocal, visuel). La phrase est bien choisie pour la diversité phonémique. Un point d'attention : les 6 secondes de la phrase de consentement donnent un x-vector viable mais pas optimal. Qwen3-TTS recommande 3-15 secondes de référence. Si la qualité du clonage est insuffisante avec 6 secondes, vous pourriez ajouter une petite phase "lecture d'un court texte" (10s) après le consentement — même UX guidée, texte différent, plus de diversité phonémique.

Le mode enfant comme premier livrable est la bonne stratégie. L'ours en peluche valide tout le pipeline (Unity UaaL, lip-sync, bridge audio, émotions) sans la complexité de la reconstruction faciale. C'est le MVP de l'avatar.

Le budget GPU de <15% est conservateur — c'est bien. Un mesh de 80K vertices avec 52 blendshapes à 90fps sur un Adreno 830 est trivial. Le shader skin PBR avec SSS approximé est le poste le plus coûteux mais reste léger sur une architecture tile-based comme l'Adreno. La marge de 85% absorbe les pics (transitions d'émotion, coarticulation lip-sync complexe).

Le risque uncanny valley est bien identifié. Pour un chatbot thérapeutique, le semi-stylisé (style Pixar) est probablement plus sûr que le photoréaliste. Un avatar empathique légèrement stylisé est plus chaleureux qu'un visage réaliste qui tombe dans l'uncanny valley. La stratégie "photoréaliste d'abord, fallback semi-stylisé" est pragmatique, mais pour le contexte thérapeutique je recommanderais de tester le semi-stylisé en priorité — les patients en détresse émotionnelle sont plus sensibles à l'uncanny valley que l'utilisateur moyen.

L'intégration KazeiaService ↔ Unity est bien pensée. Le pattern événementiel (onTtsAudioChunk, onEmotion, onSpeechStart, onSpeechEnd) est propre et découplé. Le SharedMemory pour l'audio est la bonne approche — pas de copie, Unity lit directement le buffer PCM.

Un point manquant : la gestion thermique. Le NPU Hexagon (talker TTS) + CPU Oryon (CP + decoder) + GPU Adreno (avatar) tournant simultanément pendant une conversation de 30 minutes va chauffer le SoC. Le Snapdragon 8 Elite a un throttling thermique agressif. Il faudrait un profiling thermique avec tous les composants actifs en continu, et potentiellement un mode "éco" qui réduit le framerate avatar de 90fps à 60fps ou la résolution de rendu quand la température monte.

C'est la dernière pièce du puzzle. L'architecture complète Kazeia est maintenant :

Patient parle → VAD (CPU) → Whisper STT (NPU HTP)
→ LLM Qwen3 (NPU HTP, séquentiel) → texte + tags émotion
→ Avatar reçoit émotion (GPU, immédiat)
→ TTS Talker (NPU HMX, 27ms/step) + CP (CPU, 75ms/step, parallèle)
→ Decoder (CPU, streaming par chunks de 4 tokens)
→ Audio chunk → AudioTrack + OVRLipSync → Avatar lip-sync (GPU, 90fps)
→ Patient entend la voix clonée + voit l'avatar animé après ~800ms

Tout est cohérent. Tu veux qu'on mette à jour la mémoire avec l'ensemble de ces documents et qu'on lance les prochaines étapes ?


You asked:

on a mis à jour le rapport avec ceci  ● CPU déjà à ~44°C au repos (sans avatar). C'est normal pour un Snapdragon 8 Elite mais ça confirme ton instinct. Oui, 60fps est le bon choix. Raisons : ┌───────────────────┬───────────────────────────────────┬─────────────────────────┬──────────────────────┐ │ │ 90fps │ 60fps │ 30fps │ ├───────────────────┼───────────────────────────────────┼─────────────────────────┼──────────────────────┤ │ Fluidité lip sync │ Excellent │ Très bon │ Saccadé en gros plan │ ├───────────────────┼───────────────────────────────────┼─────────────────────────┼──────────────────────┤ │ Charge GPU │ ~22% │ ~15% │ ~8% │ ├───────────────────┼───────────────────────────────────┼─────────────────────────┼──────────────────────┤ │ Thermique │ Risque throttle en session longue │ Stable │ Très froid │ ├───────────────────┼───────────────────────────────────┼─────────────────────────┼──────────────────────┤ │ Batterie │ Drain notable │ Raisonnable │ Minimal │ ├───────────────────┼───────────────────────────────────┼─────────────────────────┼──────────────────────┤ │ Écran 144Hz │ 90÷144 = judder │ 60÷144 = propre (2.4:1) │ 30÷144 = judder │ └───────────────────┴───────────────────────────────────┴─────────────────────────┴──────────────────────┘ 60fps est le sweet spot : - Lip sync reste fluide (16.7ms entre frames, imperceptible pour les lèvres) - Le GPU reste sous les 15% → pas de chauffe - Diviseur propre du 144Hz (via frame pacing) - Kazeia est une app de sessions longues (support émotionnel = 15-30 min) → la stabilité thermique est prioritaire

Show more

Apr 2


Claude Replied:

Validé le choix 60fps et mémorisa l'analyse thermique

Validé le choix 60fps et mémorisa l'analyse thermique

60fps est le choix parfait. L'analyse du diviseur 144Hz est pertinente — 60fps donne un frame pacing propre à 2.4:1 sans judder, contrairement au 90fps qui ne divise pas proprement le 144Hz.

Et 44°C au repos confirme qu'il faut être conservateur. Avec le NPU (talker), le CPU (CP + decoder), et le GPU (avatar 60fps) tous actifs pendant 30 minutes, le thermal headroom est limité. Le 60fps à ~15% GPU laisse la marge nécessaire.

Veux-tu que je mette à jour la mémoire avec l'ensemble des informations de cette session ? On a couvert énormément de terrain aujourd'hui.


You asked:

Le constat est que le démarrage de la partie Unity est très et que de plus nous avons un splashscreen au démarrage qu'il serait bon d'eviter, quelles sont les alternatives possibles ?

Apr 3


Claude Replied:

Examiné les alternatives à Unity pour optimiser le démarrage

Examiné les alternatives à Unity pour optimiser le démarrage

Voici les alternatives concrètes, avec une recommandation claire.

Le problème Unity en deux mots : le démarrage de Unity en tant que bibliothèque passe par trois phases — écran noir (3-4 secondes), splash screen Unity, puis encore un écran noir (2-3 secondes) — avant que la scène apparaisse Unity. Et avec un abonnement Unity Personal, un splash screen par défaut s'affiche pendant le chargement et ne peut pas être modifié Unity. Pour le supprimer, il faut Unity Pro à 2200$/an.

Les alternatives :

Option 1 — SceneView + Google Filament (ma recommandation)

SceneView est un SDK open-source, basé sur Filament et ARCore, l'alternative légère aux moteurs de jeu lourds pour la 3D dans les apps natives. Il ajoute seulement 3 Mo à l'APK et s'intègre nativement avec Jetpack Compose SceneView.

C'est la meilleure option pour Kazeia parce que Filament est le moteur de rendu PBR de Google, utilisé dans Android lui-même (visualiseur 3D natif, ARCore). Il supporte le PBR, le subsurface scattering approximé, les blendshapes/morph targets, et le chargement de glTF/GLB. L'intégration est native Kotlin — pas de bridge Java↔C#, pas d'Activity séparée, pas de lifecycle à gérer. SceneView fonctionne comme un simple View Android qu'on place dans un layout Compose ou XML.

Avantages :
├── Démarrage instantané (c'est un View natif, pas un moteur externe)
├── Pas de splash screen
├── 3 Mo au lieu de 40-80 Mo (Unity)
├── Kotlin natif — même langage que l'app Kazeia
├── PBR + morph targets/blendshapes supportés
├── glTF/GLB import natif
├── Pas de licence Pro nécessaire
├── Lifecycle géré par Android (pas de conflit)

Limitations :
├── Pas d'éditeur visuel (tout en code)
├── Animations procédurales à coder manuellement
├── Pas d'OVRLipSync intégré (mais uLipSync ou custom)
├── Communauté plus petite que Unity

Pour le lip-sync, au lieu d'OVRLipSync (qui est un SDK Meta prévu pour Unity/Unreal), on utiliserait une approche custom légère : analyser l'audio PCM pour extraire les visèmes (énergie par bande de fréquence → mapping vers les 15 phonèmes de base → 52 blendshapes ARKit). C'est quelques centaines de lignes de Kotlin au lieu d'un SDK entier.

Option 2 — Godot Engine embarqué

Godot est open-source (MIT), pas de splash screen obligatoire, supporte le PBR et les blendshapes. Mais son intégration en tant que bibliothèque Android est moins mature que Unity UaaL. Le démarrage est plus rapide que Unity mais pas instantané (~1-2s). L'APK ajoute ~15-25 Mo. Le principal avantage c'est l'éditeur visuel pour créer les shaders et les animations — beaucoup plus accessible que tout coder en Filament.

Option 3 — Rester sur Unity mais contourner les problèmes

Pour le splash screen : Unity Pro (2200$/an) ou alors Unity Gaming Services qui offre parfois des licences éducatives pour les projets de recherche/master. Pour le démarrage lent : pré-charger Unity au lancement de l'app (dans Application.onCreate) plutôt qu'au moment d'afficher l'avatar. L'utilisateur ne voit pas le démarrage s'il se fait pendant l'écran d'accueil/login de Kazeia.

Timeline contournement Unity :
t=0s     App démarre → écran login Kazeia
t=0s     En arrière-plan : Unity commence à charger
t=5-8s   Utilisateur tape son mot de passe (pendant ce temps Unity charge)
t=8s     Unity prêt en mémoire (invisible)
t=...    Quand l'avatar est nécessaire → affichage instantané

Ma recommandation : SceneView + Filament.

Pour un projet de master avec une deadline, SceneView offre le meilleur ratio effort/résultat. Pas de licence, pas de splash screen, démarrage instantané, intégration Kotlin native, et le rendu Filament est magnifique pour un avatar de 80K vertices avec PBR. Le pipeline serait :

Reconstruction DECA → mesh FLAME .glb avec 52 blendshapes
                    → chargé dans SceneView/Filament
                    → blendshapes pilotés par le lip-sync custom
                    → rendu PBR avec skin shader à 60fps
                    → tout dans un View Kotlin natif, pas d'Activity séparée

Le seul effort supplémentaire par rapport à Unity c'est le lip-sync custom (pas d'OVRLipSync tout fait). Mais c'est un composant bien défini : audio PCM → FFT → énergie par bande → 15 visèmes → 52 blendshapes. C'est un bon sujet technique pour le mémoire.

Tu veux qu'on explore SceneView + Filament plus en détail ?


You asked:

je te rappelle que le but sera d'avoir un clone d'une personne sur base de photo, ou un avatar 3d sur lequel on va devoir faire du lipsync (avec le TTS), mais également simuler des "micro geste" pour ne pas avoir un avatar figé

Apr 3


Claude Replied:

Identifié une contrainte technique critique concernant Filament

Identifié une contrainte technique critique concernant Filament

Cette recherche révèle un problème bloquant avec Filament/SceneView que je n'avais pas vu initialement.

Filament a une limitation GitHub à 4 blend shapes actives simultanément. Pour un avatar facial avec 52 blendshapes ARKit, c'est insuffisant. Des discussions ouvertes demandent le support de plus de 4 ou 8 blend shapes, mais le problème persiste. Et les animations ne se blendent pas entre elles dans Filament — les matrices d'os s'écrasent mutuellement GitHub, ce qui empêche de combiner lip-sync + émotions + micro-gestes simultanément.

C'est rédhibitoire pour Kazeia. Un avatar crédible avec lip-sync + émotion + micro-gestes nécessite de piloter 15-20 blendshapes simultanément (bouche, yeux, sourcils, joues). Et il faut blender le lip-sync avec les expressions émotionnelles en continu. Filament ne peut pas faire ça.

Révision de ma recommandation — voici les vraies options :

Option 1 — Godot Engine (ma nouvelle recommandation)

Godot est open-source MIT, pas de splash screen, pas de licence payante. Il supporte un nombre illimité de blendshapes, le blending d'animations (AnimationTree avec blend spaces), le PBR avec subsurface scattering, et l'intégration en tant que bibliothèque Android est supportée. Le démarrage est plus rapide que Unity (~1-2s vs 5-8s). L'APK ajoute ~15-20 Mo au lieu de 40-80 Mo pour Unity.

Pour le lip-sync, Godot a des solutions natives : le nœud AnimationTree avec BlendTree permet de mixer lip-sync + émotion + idle en parallèle. Les blendshapes se pilotent directement via MeshInstance3D.set_blend_shape_value() à 60fps sans problème. Les micro-gestes (clignements, saccades oculaires, respiration, micro-expressions) sont des animations procédurales qui se blendent naturellement avec le reste.

Godot pour Kazeia :
├── 52 blendshapes ARKit     : ✅ illimité
├── Blending lip+émotion     : ✅ AnimationTree BlendSpace
├── Micro-gestes procéduraux : ✅ via script GDScript/C#
├── PBR + skin shader        : ✅ Spatial Material + SSS
├── glTF/GLB import          : ✅ natif
├── Pas de splash screen     : ✅ MIT, pas de contrainte
├── Démarrage                : ~1-2s (vs 5-8s Unity)
├── Taille APK               : ~15-20 Mo (vs 40-80 Mo Unity)
├── Licence                  : MIT gratuit (vs Unity Pro 2200$/an)
├── Langage                  : GDScript ou C# (familier)
└── Android intégration      : supporté en tant que bibliothèque

Option 2 — Unity avec contournement du démarrage

Si l'écosystème Unity est préféré (plus de ressources, plus d'assets, OVRLipSync intégré), le contournement reste viable. Pré-charger Unity au lancement de l'app pendant que l'utilisateur se connecte. Le splash screen passe pendant l'écran de login Kazeia — l'utilisateur ne le voit pas. Pour la licence Pro (splash screen), vérifier si UTBM a un accord éducatif Unity.

Unity contourné :
├── 52 blendshapes            : ✅ natif, excellent
├── Blending lip+émotion      : ✅ Animation Layers
├── Micro-gestes procéduraux  : ✅ scripts C#
├── PBR + skin shader         : ✅ URP/HDRP, meilleur que Godot
├── OVRLipSync               : ✅ SDK Meta gratuit
├── Splash screen             : ❌ sauf Pro (2200$/an) ou éducatif
├── Démarrage                 : 5-8s (masqué par l'écran login)
├── Taille APK                : 40-80 Mo
├── UaaL lifecycle            : complexe mais documenté
└── Écosystème                : le plus riche en assets/tutoriels

Option 3 — Vulkan/OpenGL ES custom (ambitieux mais léger)

Écrire un renderer minimal en Vulkan directement en Kotlin/C++. Pas de moteur de jeu, pas de splash screen, démarrage instantané, empreinte minimale. Le mesh FLAME de 80K vertices avec 52 blendshapes est géométriquement simple — c'est un seul objet, pas une scène de jeu complexe. Le blendshape se calcule en compute shader (somme pondérée de vertex deltas). Le PBR est un fragment shader standard. Les micro-gestes sont des fonctions procédurales en C++.

C'est le plus performant et le plus léger, mais aussi le plus de travail d'ingénierie. Pour un projet de master avec deadline, c'est risqué sauf si vous avez de l'expérience Vulkan.

Ma recommandation finale : Godot.

Pour un avatar thérapeutique avec face cloning, 52 blendshapes, lip-sync TTS, micro-gestes, et émotions — Godot coche toutes les cases sans les contraintes de Unity (splash screen, licence, démarrage lent, lifecycle complexe) ni la limitation fatale de Filament (4 blendshapes max). Le rendu PBR de Godot 4 est suffisant pour un avatar semi-réaliste sur l'Adreno 830, et le AnimationTree gère nativement le blending multi-couche dont tu as besoin.

Tu veux qu'on approfondisse Godot ou tu préfères explorer une autre piste ?