Dans un précédent billet, « Mon royaume pour une IA en localhost », je racontais mes premiers pas avec Ollama. L’enjeu était assez simple : si je dois confier des articles, des rapports ou des archives de recherche à une intelligence artificielle, je préfère que tout cela reste dans ma machine. Garder l’IA dans sa boîte, donc, avec l’espoir de conserver un peu de prise sur ce que je lui donne et sur ce qu’elle en fait.
À ce stade, faire répondre un modèle dans un terminal avait quelque chose de presque magique. Mais entre demander un résumé de trois pages et faire analyser un véritable corpus de recherche, il y a un petit gouffre. C’est dans ce gouffre que mon ordinateur et moi sommes tombés.
L’histoire qui suit est donc un retour d’expérience sur la manière dont j’ai essayé de faire travailler Ollama avec une petite carte graphique de portable. Ce n’est pas un tutoriel universel : les réglages dépendent du modèle, du GPU, de sa mémoire et de la taille des textes. C’est plutôt un pense-bête documenté pour éviter de refaire, dans six mois, le même chemin à travers les journaux d’erreurs.
Le contexte : 839 documents et une semaine de patience
Je travaille sur les archives du projet Santé & Territoires au Sénégal. Le corpus rassemble des comptes rendus, présentations, termes de référence, tableaux, documents de travail et rapports produits autour des Living Labs de Mbane et de Keur Momar Sarr, près du lac de Guiers.
L’objectif n’était pas seulement de faire résumer les documents. Je voulais reconstruire une chronologie, retrouver les groupes impliqués, repérer les formes de participation, distinguer ce qui avait été proposé de ce qui avait réellement été fait et suivre la manière dont la perspective « Une seule santé » avait été progressivement prise en compte. Le tout en conservant, pour chaque codage, un extrait et un chemin vers le document source.
Après extraction des textes, agrégation des doublons et exclusion des images, vidéos et fichiers audio, il restait tout de même 839 unités documentaires susceptibles de passer devant le LLM.
J’ai donc lancé mon script R, assez fier de moi, avec plusieurs workers pour les opérations locales et Ollama pour le codage. Quelques heures plus tard, la barre de progression affichait quelque chose comme :
Codage LLM documents 16/839 | écoulé 4h29m | ETA ≈ 231h22m
Le script annonçait ensuite, avec un calme que je trouvais presque provocateur :
Timeout ou échec pour D00010 chunk 4 (4000 caractères) ;
repli en 2 sous-chunk(s) de 2500 caractères au maximum.
Le premier bloc attendait le timeout. Puis le script le découpait en deux et recommençait. Quand plusieurs requêtes partaient en même temps, elles se disputaient la mémoire disponible. À ce rythme, l’analyse ne ressemblait plus vraiment à un gain de temps. Elle ressemblait plutôt à une manière sophistiquée de transformer un ordinateur portable en radiateur et par ces temps de canicules, pas la meilleur idée que j’ai eu!
Pas qu’un problème de modèle
Ma première réaction a été de chercher un modèle plus petit. C’est assez logique : un modèle de 8 milliards de paramètres sera plus lourd qu’un modèle de 3 ou 4 milliards. Mais cela n’expliquait pas complètement pourquoi des passages de quelques milliers de caractères pouvaient immobiliser Ollama pendant plusieurs minutes.
La machine possède deux processeurs graphiques :
Intel Corporation Meteor Lake-P [Intel Arc Graphics]
NVIDIA RTX 500 Ada Generation Laptop GPU
La carte NVIDIA ne dispose que de 4 Go de VRAM. Ce n’est pas énorme, mais un llama3.2:latest quantifié pèse environ 2 Go. Il devait donc être possible d’en faire quelque chose.
La commande suivante confirmait d’ailleurs qu’Ollama voyait bien la carte :
nvidia-smi
Dans la liste des processus, /usr/local/bin/ollama occupait environ 666 Mio de mémoire vidéo. Mais cette information ne disait pas quelle part du modèle était réellement exécutée sur le GPU. Pour cela, il fallait demander directement à Ollama :
ollama run llama3.2:latest "Réponds uniquement par OK"
ollama ps
Et là, surprise :
NAME SIZE PROCESSOR CONTEXT
llama3.2:latest 18 GB 71%/29% CPU/GPU 16384
Le modèle téléchargé faisait environ 2 Go, mais Ollama annonçait une empreinte de 18 Go et ne plaçait que 29 % du calcul sur le GPU. Le même test avec qwen3:4b-instruct donnait encore moins :
qwen3:4b-instruct 18 GB 83%/17% CPU/GPU 16384
Le GPU était donc bien utilisé, mais comme une petite annexe du CPU. La majeure partie de l’inférence se faisait toujours dans la mémoire centrale. Hors, j’ai toujour entendu Théo me parler de GPU dans sa thèse.
Quatre contextes pour une seule petite carte
L’indice principal était la valeur CONTEXT 16384. Ollama avait réservé beaucoup plus de mémoire que nécessaire. Le parallélisme était ici particulièrement coûteux : plusieurs requêtes simultanées signifient plusieurs contextes à conserver en mémoire. Quatre contextes de 4 096 tokens donnent justement 16 384 tokens à allouer.
La documentation d’Ollama précise que la mémoire nécessaire augmente avec le produit :
OLLAMA_NUM_PARALLEL × OLLAMA_CONTEXT_LENGTH
Sur une grosse carte, plusieurs requêtes parallèles peuvent augmenter le débit. Sur 4 Go de VRAM, elles font surtout sortir le modèle du GPU. Le paradoxe est assez joli : j’avais demandé plus de parallélisme pour aller plus vite, et ce parallélisme était précisément ce qui ralentissait le calcul.
Il fallait donc distinguer deux choses dans mon script :
- les opérations de lecture, d’extraction et de classement des fichiers, qui peuvent utiliser plusieurs cœurs CPU ;
- les appels au modèle, qui doivent rester séquentiels tant que la mémoire du GPU est aussi contrainte.
Dans R, je pouvais conserver dix travailleurs pour préparer les documents, mais il fallait impérativement revenir à :
--llm-workers 1
Régler Ollama plutôt que brutaliser R
Ollama fonctionne comme un service. Le script R lui envoie des requêtes HTTP, mais c’est Ollama qui décide où charger le modèle, combien de contextes préparer et combien de temps le garder en mémoire. Il n’y avait donc pas besoin d’ajouter du code CUDA dans R. Il fallait régler le serveur.
Sous Linux, cela passe par une surcharge du service systemd :
sudo systemctl edit ollama.service
J’ai commencé avec cette configuration :
[Service]
Environment="OLLAMA_NUM_PARALLEL=1"
Environment="OLLAMA_MAX_LOADED_MODELS=1"
Environment="OLLAMA_CONTEXT_LENGTH=6144"
Environment="OLLAMA_KEEP_ALIVE=30m"
Puis il faut recharger la configuration et redémarrer Ollama :
sudo systemctl daemon-reload
sudo systemctl restart ollama
Les paramètres ont chacun un rôle assez simple :
OLLAMA_NUM_PARALLEL=1interdit à un même modèle de préparer plusieurs requêtes en parallèle ;OLLAMA_MAX_LOADED_MODELS=1évite que plusieurs modèles se disputent les 4 Go de VRAM ;OLLAMA_CONTEXT_LENGTHfixe la quantité maximale de texte et de réponse que le modèle peut garder dans son contexte ;OLLAMA_KEEP_ALIVE=30mévite de recharger le modèle entre deux documents, sans pour autant l’immobiliser éternellement dans la mémoire.
Avec un contexte de 6 144 tokens, llama3.2:latest tenait entièrement sur le GPU. J’ai ensuite tenté 8 192 tokens, parce que mon prompt contient à la fois un codebook, un schéma JSON, des métadonnées et plusieurs passages du document.
Le résultat restait très honorable :
NAME SIZE PROCESSOR CONTEXT
llama3.2:latest 3.5 GB 9%/91% CPU/GPU 8192
Le modèle était désormais exécuté à 91 % sur le GPU, contre 29 % avant le réglage. On était passé d’une empreinte annoncée de 18 Go à 3,5 Go.
Gratter encore un peu de mémoire
À 8 192 tokens, la carte était juste à la limite. Il restait une possibilité : réduire la mémoire occupée par le cache K/V, c’est-à-dire la mémoire dans laquelle le modèle conserve les éléments du contexte déjà traités.
J’ai donc ajouté :
Environment="OLLAMA_FLASH_ATTENTION=1"
Environment="OLLAMA_KV_CACHE_TYPE=q8_0"
La configuration complète devient :
[Service]
Environment="OLLAMA_NUM_PARALLEL=1"
Environment="OLLAMA_MAX_LOADED_MODELS=1"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_KEEP_ALIVE=30m"
Environment="OLLAMA_FLASH_ATTENTION=1"
Environment="OLLAMA_KV_CACHE_TYPE=q8_0"
Flash Attention permet de réduire la consommation de mémoire lorsque le contexte augmente. La quantification q8_0 utilise environ deux fois moins de mémoire que le cache f16 par défaut, avec une perte de précision généralement faible. Il existe également un cache q4_0, encore plus compact, mais la perte devient plus sensible. Pour un travail de codage scientifique, je préfère réduire légèrement le contexte plutôt que comprimer davantage le cache.
Il faut ici éviter une confusion : je n’ai pas fine-tuné le modèle au sens de l’apprentissage automatique. Je n’ai modifié ni ses poids ni son comportement appris. J’ai plutôt ajusté — tuné, si l’on veut — son régime d’inférence : nombre de requêtes, taille du contexte, occupation de la mémoire et répartition entre CPU et GPU.
Faire moins de requêtes reste la meilleure optimisation
Même correctement installé sur le GPU, un LLM n’a aucune raison de lire 839 documents de bout en bout si la majorité n’est pas utile à la question de recherche. Le réglage matériel ne devait donc pas remplacer le travail méthodologique.
J’ai repris le script R pour organiser l’analyse en deux passes.
La première réalise une présélection à rappel élevé. Elle lit le nom, le chemin et quelques passages de chaque document, puis décide s’il peut documenter les Living Labs, la participation des parties prenantes ou la perspective « Une seule santé ». Les décisions sont exportées dans un fichier CSV et peuvent être corrigées manuellement.
Rscript analyse_corpus_llama31_8b.R \
--input "ST_KB_destination" \
--output "resultat_corpus_llama31" \
--model "llama3.2:latest" \
--triage-model "llama3.2:latest" \
--mode triage \
--triage-batch-size 8 \
--workers 10 \
--llm-workers 1 \
--api-timeout 300
Le fichier 00_triage_decisions.csv possède une colonne manual_selected. Je peux y inscrire TRUE pour repêcher un document ou FALSE pour l’écarter avant la seconde passe.
La seconde passe ne lit plus nécessairement toutes les pages. Elle présente au modèle le début, la fin et les passages les plus denses en termes liés au terrain et aux catégories analytiques. Chaque observation doit rester associée à un extrait et au chemin de sa source.
Rscript analyse_corpus_llama31_8b.R \
--input "ST_KB_destination" \
--output "resultat_corpus_llama31" \
--model "llama3.2:latest" \
--triage-model "llama3.2:latest" \
--mode full \
--deep-reading targeted \
--workers 10 \
--llm-workers 1 \
--api-timeout 300
La machine travaille donc mieux, mais elle travaille surtout moins inutilement. C’est probablement l’optimisation la plus importante de toute cette histoire.
Ce que j’en retiens
Faire fonctionner une IA en local ne consiste pas seulement à télécharger un modèle et à lancer une commande. Entre le modèle et la machine s’installe tout un petit monde de paramètres : quantification, contexte, cache, parallélisme, chargement des modèles et partage entre CPU et GPU.
Les valeurs par défaut sont nécessairement génériques. Elles ne savent pas que mon ordinateur possède une RTX 500 Ada de seulement 4 Go, que je préfère une requête fiable à quatre requêtes concurrentes, ni que mon corpus contient des centaines de documents en français avec un schéma de codage assez lourd.
Il faut donc reprendre la main sur l’outil. Pas pour optimiser chaque seconde comme un nouvel ergonome de soi-même, mais pour que la technique reste proportionnée à la machine et à l’usage. Dans mon cas, la frugalité ne consistait pas seulement à choisir un petit modèle. Elle consistait à limiter le parallélisme, choisir la taille de contexte utile, écarter les documents hors sujet et maintenir une possibilité de validation humaine.
Le modèle est moins gros, les requêtes sont moins nombreuses et le GPU est enfin devenu autre chose qu’un petit témoin lumineux dans nvidia-smi. Ce n’est toujours pas le futur. Mais, cette fois, ça ressemble un peu plus à un outil que je peux habiter.
Et de la philo ?
Cette expérience prend alors un autre relief à la lecture de Günther Anders, mon héro. Le « rêve des machines » n’est pas celui de mieux servir les humains, mais celui d’un monde entièrement organisé selon leur propre fonctionnement, dans lequel l’homme finit par devenir un rouage de l’appareil qu’il a construit. Le décalage prométhéen apparaît lorsque notre capacité à produire de tels dispositifs dépasse notre capacité à nous les représenter, à les maîtriser et à décider collectivement de leurs usages.
À une échelle évidemment bien modeste, mon premier réflexe avait précisément consisté à adapter mon travail aux exigences d’Ollama : multiplier les traitements, découper les documents, attendre des centaines d’heures et laisser la machine imposer son rythme à l’enquête.
Paramétrer le modèle, limiter les requêtes et sélectionner les textes pertinents pourrait peut être revenir à inverser cette relation : faire entrer la machine dans les contraintes de la recherche, plutôt que transformer le chercheur — et son corpus — en simples rouages de la machine.
Quelques commandes à garder sous le coude
Identifier les cartes graphiques :
lspci | grep -Ei "VGA|3D|Display"
Surveiller la carte NVIDIA :
watch -n 1 nvidia-smi
Vérifier où Ollama a chargé le modèle :
ollama ps
Afficher la configuration transmise au service :
systemctl show ollama --property=Environment
Arrêter un modèle et libérer sa mémoire :
ollama stop llama3.2:latest
