Granite 4.1 passe l'épreuve du code Python — jusqu'où un SLM de 3 milliards peut aller

J'ai benchmarké granite4.1:3b sur des algorithmes Python complexes. Surprenant jusqu'au niveau 21, où gemma4:e4b prend le dessus — et révèle que les closures Python sont 4x plus efficaces pour les SLMs.

Dans mon dernier article, je vous ai montré que l'allemand est la langue optimale pour granite4.1:3b — et que ce petit SLM de 3 milliards de paramètres peut atteindre 87/100 sur des raisonnements mathématiques abstraits quand vous lui parlez dans sa langue native.

Cette semaine, j'ai voulu aller plus loin. La question qui me taraudait : un modèle aussi rapide (91 tokens par seconde, 2,6 Go de VRAM) peut-il réellement générer du code de qualité ?

La réponse courte : oui, et j'ai été bluffé. Jusqu'à ce que je tombe sur un algorithme précis qui révèle sa limite structurelle — et que gemma4:e4b entre en scène pour battre même Claude Haiku.

Mais surtout, ce benchmark m'a fait découvrir quelque chose que je n'attendais pas : le développement procédural avec des closures Python est quatre fois plus performant pour un SLM — et réduit considérablement les bugs liés aux signatures.


Avant de continuer de lire la suite de l'article, je vous invite à vous inscrire à ma newsletter, pour connaître en avant première les futurs sujets traités chaque semaine.


Le contexte : benchmarker l'algorithmique sur GPU local

Depuis que j'explore granite4.1:3b, je construis une suite de benchmarks progressifs — 20 niveau de difficulté croissante, des structures de données basiques jusqu'aux algorithmes de compétition.

Le principe est simple : je demande au modèle d'implémenter une fonction Python précise, avec une signature définie, puis je la soumets à des cas de tests automatisés. Pas de jugement subjectif — le code passe ou ne passe pas.

Granite4.1:3b m'a impressionné sur les vingt premiers niveaux. Vitesse d'exécution imbattable, code propre, gestion correcte des cas limites. Un modèle qui tient sa promesse sur les algorithmes courants : tris, arbres binaires, fenêtres glissantes, graphes simples, programmation dynamique de base.

Puis j'ai conçu le niveau 21.


Le niveau 21 : le Segment Tree Beats

Le Segment Tree Beats (aussi appelé Ji Driver Segmentation) est un algorithme de compétition de niveau ICPC World Finals. Il combine deux opérations en O(n log² n) sur un tableau :

  • chmin(l, r, v) — applique arr[i] = min(arr[i], v) sur un intervalle
  • sum_query(l, r) — retourne la somme sur un intervalle

La difficulté est dans la structure de nœud. Chaque nœud maintient quatre champs interdépendants : la somme, le premier maximum, le deuxième maximum strict, et le nombre d'occurrences du premier maximum. La mise à jour conditionnelle (le "break tag") a trois cas mutuellement exclusifs, et le push vers les enfants doit lui-même respecter cette logique — pas une simple propagation de valeur comme dans un segment tree classique.

En clair : c'est un algorithme où la moindre confusion entre les invariants produit un code qui compile et s'exécute… mais donne des résultats faux.


Granite 4.1 : bluffant jusqu'ici, puis la limite structurelle

Granite4.1:3b échoue sur ce niveau. Score : 0/2. Voici le diagnostic.

Le modèle génère du code procédural avec des dictionnaires Python pour représenter les nœuds. Le problème : ses fonctions apply_break_tag modifient une copie locale du nœud au lieu de muter la structure en place. Le résultat s'évapore.

# Ce que granite génère (incorrect)
def apply_break_tag(node, v):
    node = {'sum': node['sum'] - (node['max1'] - v) * node['cnt_max'],
            'max1': v}  # ← copie locale, rien n'est modifié
    return node

# Ce qu'il faudrait
def apply_break_tag(node, v):
    node['sum'] -= (node['max1'] - v) * node['cnt_max']
    node['max1'] = v  # ← mutation en place

Ce n'est pas un oubli ponctuel — c'est une signature de l'architecture de ses données d'entraînement. Granite4.1 a été entraîné majoritairement sur du code procédural en style fonctionnel. Quand les structures de données sont interdépendantes et nécessitent des mutations coordonnées, ses interfaces entre unités de code deviennent incohérentes.

La version 8 milliards de granite a un problème différent mais symétrique : elle confond les tableaux lazy_tags avec le stockage des ranges de nœuds, et troncature fréquente à 800 tokens. Même famille, mêmes limites de style.


Gemma 4 entre en scène — et dépasse Claude Haiku

À ce stade, j'avais aussi testé Claude Haiku en direct sur le même problème. Résultat : 0/2, 5,8 secondes. Le bug est élégant dans sa bêtise : Haiku généralise le pattern classique des segment trees lazy (push inconditionnel) au lieu du push conditionnel à trois champs.

# Haiku (incorrect) — généralise le pattern lazy classique
child.max1 = parent.max1

# Correct — le push est lui-même conditionnel
if child.max1 > parent.max1:
    child.sum -= (child.max1 - parent.max1) * child.cnt_max
    child.max1 = parent.max1

C'est un modèle commercial, entraîné sur bien plus de données, et il rate le niveau 21 exactement comme les SLMs locaux. L'algorithme est simplement hors de sa zone de confort naturel.

Gemma4:e4b, lui, résout le problème. Mais avec une condition : il faut activer le mode think et adopter le style monolithique.

Sans --think, il identifie correctement la condition mais ne l'applique pas — une ligne pass là où devrait se trouver la logique. Avec --think en mode décomposé (six unités séparées), il réussit en 374 secondes. Avec --think en mode monolithique, il réussit en 94 secondes. Quatre fois plus rapide, et une architecture nettement plus propre.


La découverte : les closures Python comme solution architecturale

La solution que gemma4:e4b a produite en mode monolithique m'a arrêté. Voici ce qu'il a généré :

def segtree_beats(arr, chmin_updates, queries):
    N = len(arr)
    INF = 10**18
    tree_sum  = [0]    * (4 * N)
    tree_max1 = [0]    * (4 * N)
    tree_max2 = [-INF] * (4 * N)
    tree_cnt  = [0]    * (4 * N)
    lazy      = [INF]  * (4 * N)

    def push(node, start, end): ...    # closure — capture les 5 tableaux
    def update(node, start, end, l, r, v): ...
    def query(node, start, end, l, r): ...
    def build(node, start, end): ...

    build(1, 0, N - 1)
    for l, r, v in chmin_updates:
        update(1, 0, N - 1, l, r, v)
    return [query(1, 0, N - 1, l, r) for l, r in queries]

Ce pattern — des fonctions imbriquées qui capturent les tableaux de la fonction parente par closure — évite entièrement le problème des signatures.

Pensez à ce que ça évite. Sans closures, chaque appel récursif doit passer les cinq tableaux en paramètre :

# Sans closures — signatures polluées
def push(node, start, end, tree_sum, tree_max1, tree_max2, tree_cnt, lazy):
    ...
def update(node, start, end, l, r, v, tree_sum, tree_max1, tree_max2, tree_cnt, lazy):
    ...

Cinq paramètres supplémentaires à chaque niveau de récursion. Cinq sources d'erreur potentielles. Et pour un SLM qui génère le code unité par unité, cinq endroits où introduire une incohérence d'interface.

Avec les closures, il n'y a plus de signature à coordonner. Les fonctions internes voient directement les tableaux de la fonction parente. L'état est partagé sans passage de paramètres. Et surtout : le SLM n'a pas à maintenir la cohérence entre plusieurs signatures — une source majeure de bugs dans la génération fragmentée.


Pourquoi ce résultat est structurellement important

Ce que j'ai observé n'est pas une astuce de performance anecdotique. C'est une conséquence directe de la façon dont les SLMs génèrent du code.

Un modèle de langage génère du code séquentiellement, token par token. Quand il implémente une fonction, il ne « voit » pas l'intégralité du programme — il travaille dans sa fenêtre de contexte. Plus les interfaces entre les unités de code sont complexes (signatures longues, paramètres partagés entre plusieurs fonctions), plus la probabilité d'incohérence augmente.

Le mode monolithique force le modèle à tout écrire dans un seul bloc. Les closures sont la conséquence naturelle : plutôt que de passer cinq tableaux comme paramètres à chaque fonction imbriquée, le modèle les capture une fois dans la fonction parente et ne répète plus leur présence.

Le résultat concret :

Approche Score Latence Architecture
Pipeline décomposé + think 2/2 374 s 6 unités séparées + composition
Monolithique + think 2/2 94 s Closures Python dans segtree_beats

Quatre fois plus rapide à générer. Et dans mes tests, significativement moins de bugs liés aux interfaces entre fonctions.


Ce que ça change dans ma façon de prompter les SLMs

Depuis cette découverte, j'ai modifié mon approche pour tout code Python nécessitant des structures de données partagées entre plusieurs fonctions.

Au lieu de demander au modèle de produire des fonctions indépendantes qui échangent des objets, je lui demande systématiquement de tout encapsuler dans une fonction principale, avec des sous-fonctions définies à l'intérieur par closure.

C'est particulièrement efficace pour :
- Les algorithmes sur les arbres et graphes (arbres de segments, arbres indexés, DFS récursif)
- Les algorithmes de programmation dynamique avec mémoïsation
- Tout code où plusieurs fonctions récursives partagent un état commun

Pour les SLMs locaux de moins de 8 milliards de paramètres, je recommande maintenant ce pattern par défaut dès que l'algorithme implique plus d'un tableau d'état partagé.


Le tableau de bord complet du niveau 21

Voici les résultats définitifs sur cet algorithme :

Modèle Score Latence Méthode
Sonnet 4.6 (direct) 2/2 ✓ ~0 s Direct
gemma4:e4b + think 2/2 ✓ 94 s Monolithique + closures
gemma4:e4b + think 2/2 ✓ 374 s Pipeline décomposé
Haiku 4.5 (direct) 0/2 ✗ 5,8 s Direct
gemma4:e4b sans think 0/2 ✗ 114 s Pipeline
granite4.1:8b 0/2 ✗ 177 s Pipeline --german
granite4.1:3b 0/2 ✗ 55 s Pipeline --german


Deux enseignements de ce tableau :

Premier enseignement : Claude Haiku et granite4.1 ont le même score sur cet algorithme. Ce n'est pas un problème de taille de modèle — c'est un problème de style de code natif. Haiku généralise abusivement le pattern lazy classique. Granite4.1 génère des copies locales au lieu de mutations en place. Ce sont deux types d'erreurs différents, mais aucun des deux n'est corrigé par la taille du modèle.

Deuxième enseignement : gemma4:e4b sans --think échoue aussi. La capacité de raisonnement interne (extended thinking) n'est pas optionnelle pour les algorithmes de ce niveau de complexité. Avec think, il réussit. Sans think, il s'arrête au seuil critique et écrit pass.


Mes recommandations pour coder avec des SLMs locaux

Granite4.1:3b reste mon choix par défaut pour les agents de formatage, le routing, les tâches RAG et le code Python courant. 91 tokens par seconde, 2,6 Go de VRAM, 100 % sur les benchmarks format et sémantique. Sur les vingt premiers niveaux algorithmiques, il délivre.

Gemma4:e4b s'impose pour les algorithmes complexes, les structures de données interdépendantes, et tout ce qui requiert un raisonnement étendu. Son mode think est indispensable dès le niveau 21. Le coût : 374 secondes en mode décomposé, mais seulement 94 secondes si vous le guidez vers le style monolithique avec closures.

Le pattern closures Python n'est pas un détail d'implémentation. C'est une stratégie de prompting pour réduire la surface d'erreur des SLMs sur le code algorithmique. Je l'adopte désormais systématiquement.

La prochaine étape dans mon exploration : tester le speculative decoding granite4.1:3b (draft) + granite4.1:8b (verifier). Les deux modèles partagent le même vocabulaire de 100 352 tokens et la même profondeur de 40 couches — les conditions idéales pour combiner vitesse du 3b et qualité du 8b. Je vous en parle dans un prochain article.


Qui suis je ?

Je suis Mathieu GRENIER, CTO d'Easystrat une startup de Montpellier, en France. Je manage une équipe d'une dizaine d'ingénieurs (Graphistes, IA, frontend, backend, devOps, AWS) en remote depuis le Japon.

J'ai aussi mon activité de freelance, où je conseille des entrepreneurs dans leurs projets d'application.

Avec mon expérience personnelle de plus de 15 ans en ESN, j'ai pu travailler pour un large panel d'entreprises de différentes tailles. Ma compréhension des problèmes métiers est une de mes grandes forces et permet à mes clients de pouvoir se projeter plus facilement.

L'essentiel de mon travail consiste à canaliser l'énergie des entrepreneurs sur l'essence même de leur projet.

La technologie, les méthodes, le management sont le cœur de mes compétences.

Vous pouvez me faire confiance sur ces points là.

Si vous voulez me parler d'un de vos projets, n'hésitez pas à m'envoyer un email avec vos disponibilités à : contact@mathieugrenier.fr

Tous les articles de ce blog sont écrits par moi, même si je peux m'aider de l'IA pour illustrer mes propos. Mais jamais je ne fournis d'articles 100% IA.

Mathieu Grenier 7 mai 2026
Partager cet articlE