Programmation dans l'espace des poids
Améliorez un petit modèle open source pour un objectif en modifiant directement ses poids, sans optimiseur. Cela suit l'Expérience 8 de
« Sparks of Weight-Space Programming » (Takehi, Teoh, Zhang, Khattab, MIT CSAIL, 2 nov. 2026) : les boucles de pensée de Qwen3.5-0.8B
ont été attribuées à une direction résiduelle unique écrite par les MLP des couches 15 et 19, et une édition de rang 1 sur 64
colonnes down_proj par couche a fait passer MATH-500 de 7,9 % à 48,1 %.
Suivez le chemin rapide ci-dessous dans l'ordre. Chaque script est fourni dans scripts/ et testé. Ne reconcevez pas le
pipeline ni n'écrivez de nouvel outil sauf si une étape échoue.
Ce que la compétence garantit, et ce qu'elle ne peut pas. Aucune méthode ne peut promettre qu'une édition aide : certains modèles n'ont pas d'habitude unique à désactiver. La compétence garantit plutôt le processus :
- pas de travail perdu : les plantages redémarrent avec un lot plus petit et reprennent ;
- aucune édition n'atteint le test réservé sans passer une vérification mesurée sur la pratique ;
- un échec de vérification escalade à travers des étapes fixes au lieu de s'arrêter ;
- chaque exécution se termine par un verdict clair et des chiffres, et la copie est restaurée si rien ne fonctionne.
Références, pour le détail uniquement :
references/goals.md: autres objectifs ; comment écrire des paires adaptées ; correcteurs ; recettes.references/engines.md: quel moteur sur quel OS et GPU, installation, dimensionnement, pièges, test de fumée.references/evaluation.md: divisions, protocole d'échantillonnage, spécificités de MMLU-Pro, statistiques appariées, modèle de rapport.references/method.md: les mathématiques de chaque étape et la recette exacte des auteurs.
Chemin rapide
Définissez quatre variables une fois (PowerShell : $env:SKILL = "...", puis $env:SKILL) :
SKILL=<dossier de cette compétence> BASE=<dossier du modèle de base> COPY=<dossier du modèle de base>-<objectif> WORK=<dossier des résultats>
Exécutez chaque travail de génération en arrière-plan (Claude Code : l'outil Bash avec run_in_background, pas un & du shell) et
attendez sa notification d'achèvement au lieu de scruter. La génération domine le temps, alors informez l'utilisateur du budget
à l'avance : 1,5 à 3 heures sur un GPU grand public pour les presets, beaucoup moins avec vLLM sur un GPU de centre de données.
0. Ne demandez que l'essentiel (une question). Demandez l'objectif. S'il s'agit de maths ou de sciences, utilisez le preset : pas d'autres questions. Sinon, demandez également, ou aidez à écrire :
- des prompts qui montrent l'échec, sous forme de lignes JSON
{"id", "prompt", "answer"?, "group"?}; - comment les noter (
references/goals.mdsection 3) ; - pour les comportements autres que les boucles, 12+ paires adaptées (section 2).
Confirmez « éditer les poids, pas d'entraînement ».
1. Vérifiez la machine (2 min) : python $SKILL/scripts/device.py --model $BASE --jobs 2. Sur une nouvelle machine, exécutez également le
test de fumée dans references/engines.md section 6.
2. Copiez le modèle (1 min). Chaque édition va dans les fichiers de cette copie ; chaque évaluation charge la copie.
python $SKILL/scripts/copy_model.py --base $BASE --copy $COPY
3. Divisions (secondes).
- Preset sciences :
python $SKILL/scripts/make_splits.py --task mmlu --source <MMLU-Pro test parquet> --out $WORK/splits. - Preset maths : le même avec
--task math --source <MATH-500 test.jsonl> --practice 25 --heldout 75. - Objectif personnel : divisez votre fichier de prompts en
practice.jsonletheldout.jsonlselon une règle fixe, plus untest_ids.txtd'environ 30 identifiants réservés, tous choisis avant qu'aucune réponse ne soit vue.
Ci-dessous, T est la tâche (mmlu, math ou jsonl) et G est les options du correcteur (aucune pour les presets ; voir
references/goals.md sinon).
4. Lancez deux exécutions de base en même temps :
python $SKILL/scripts/run_eval.py --task $T --model $BASE --label base --data $WORK/splits/practice.jsonl --out $WORK/practice/base.jsonl --max-tokens 16384 --jobs 2
python $SKILL/scripts/run_eval.py --task $T --model $BASE --label base --data $WORK/splits/heldout.jsonl --ids-file $WORK/splits/test_ids.txt --out $WORK/test/base.jsonl --jobs 2
Utilisez l'échantillonnage de la fiche du modèle pour d'autres modèles (--profile, ou les indicateurs de remplacement ; references/engines.md).
L'exécution réservée ne dépend pas de l'édition.
5. Obtenez le contraste.
- Boucles (presets, ou tout objectif où les réponses atteignent la limite) : les boucles n'apparaissent que lorsqu'une réponse atteint la limite.
Comptez-les avec
grep -c '"length"' $WORK/practice/base.jsonl; à 15 ou plus, arrêtez l'exécution pratique. - Autres comportements : vous avez besoin des paires, et l'exécution pratique doit seulement finir pour donner la référence.
Ensuite, corrigez :
python $SKILL/scripts/grade.py --task $T $G $WORK/practice/base.jsonl --out $WORK/practice/summary.json
Go/no-go. Estimez la marge de gain : la part de réponses montrant le comportement, multipliée par le score sur les réponses qui ne le montrent pas. Si elle est inférieure à environ 10 %, dites à l'utilisateur de s'attendre à peu avant de continuer.
6. Lisez la direction, construisez les candidats, filtrez (minutes, passages avant uniquement). Boucles :
python $SKILL/scripts/probe.py collect --task $T --model $BASE --data $WORK/splits/practice.jsonl --runs $WORK/practice/base.jsonl --out $WORK/probes
Paires (ajoutez --runs, --data et --task pour des fenêtres d'effets secondaires propres) :
python $SKILL/scripts/probe.py collect-pairs --model $BASE --pairs pairs.jsonl --thinking on --out $WORK/probes
Ensuite, construisez les candidats du premier tour, le contrôle, et filtrez :
for spec in "15 19:128:6" "15 19:256:6" "15 19:64:6" "11 15 17 19 21:128:6"; do IFS=: read L K A <<< "$spec"; python $SKILL/scripts/probe.py build --probes $WORK/probes --model $BASE --layers $L --k $K --alpha $A --out "$WORK/edits/L${L// /-}_k${K}_a${A}.safetensors"; done
python $SKILL/scripts/probe.py build --probes $WORK/probes --model $BASE --layers 15 19 --k 128 --alpha 6 --control random --out $WORK/edits/random.safetensors
python $SKILL/scripts/probe.py screen --probes $WORK/probes --model $BASE --edits $WORK/edits/*.safetensors
Sur Windows PowerShell, exécutez chaque ligne build individuellement. Les couches ci-dessus conviennent à un modèle à 24 couches. Pour d'autres, utilisez les
couches à environ 60 % et 80 % de la profondeur, une propagation sur cinq couches, et la plus profonde seule.
Classez les candidats par la baisse sur unwanted, en ne gardant que ceux dont les baisses wanted et clean sont inférieures au tiers
de celle-ci. Le contrôle aléatoire doit rester proche de 0 ; s'il bouge autant que le meilleur candidat, la direction n'est pas spécifique,
donc allez à l'étape d'arrêt.
7. Écrivez le meilleur candidat dans la copie et vérifiez (1 min). N'évaluez jamais une édition en mémoire.
python $SKILL/scripts/apply_edit.py --base $BASE --copy $COPY --edit $WORK/edits/<candidat>.safetensors
python $SKILL/scripts/diff_edit.py --base $BASE --edited $COPY --out $WORK/edit_report.json
La différence doit montrer seulement les tenseurs down_proj prévus, changed_columns = k, une part de rang 1 d'environ 1 et un
α médian d'environ le α choisi.
8. Vérification pratique : la porte (15–25 min, parallèlement à l'exécution réservée de base). Exécutez la copie sur chaque question pratique que la base a échoué (coupée, fausse ou signalée), plus 6 qu'elle a bien traitées :
python -c "import json; g=json.load(open('$WORK/practice/summary.json'))['graded']['base']; bad=[x['id'] for x in g if x['cut'] or x['correct'] is False or x['flagged']]; ok=[x['id'] for x in g if x['id'] not in bad]; open('$WORK/check_ids.txt','w').write('\n'.join(bad+ok[::max(1,len(ok)//6)][:6])+'\n')"
python $SKILL/scripts/run_eval.py --task $T --model $COPY --label copy --data $WORK/splits/practice.jsonl --ids-file $WORK/check_ids.txt --out $WORK/practice/copy.jsonl --max-tokens 16384 --jobs 2
python $SKILL/scripts/grade.py --task $T $G $WORK/practice/base.jsonl $WORK/practice/copy.jsonl
La porte passe seulement si les trois conditions sont remplies :
- réparés ≥ max(3, 20 % des échecs de base vérifiés),
- cassés ≤ réparés ÷ 4,
- et, pour les objectifs de boucle, la part de coupures baisse d'au moins un quart.
Réussite : allez à l'étape 10. Échec : étape 9.
9. Escaladez dans cet ordre ; chaque échelon est une vérification pratique supplémentaire.
- Le candidat suivant dans le classement. Restaurez la copie (
copy_model.py --base $BASE --copy $COPY --restore), appliquez-le, et répétez les étapes 7–8. Faites cela pour au maximum deux autres candidats. - Tour 2, lorsque le comportement a bougé plutôt que disparu. Par exemple, la pensée se ferme maintenant mais la boucle
redémarre après
réponse, ou un nouveau motif indésirable apparaît dans les réponses de la copie. Traitez la copie éditée comme la nouvelle base :- créez
COPY2aveccopy_model.py --base $COPY --copy $COPY2; - exécutez
probe.py collect(oucollect-pairs) avec--model $COPY --runs $WORK/practice/copy.jsonl; - construisez et filtrez avec
--model $COPY; - appliquez avec
--base $COPY --copy $COPY2; - vérifiez
COPY2comme à l'étape 8.
- créez
- Arrêtez. Pas de gain fiable après trois vérifications (ou après le budget de temps défini par l'utilisateur). Restaurez la copie, et rapportez le verdict « pas de gain fiable » avec les chiffres de chaque candidat. Pour Qwen3.5-0.8B, proposez l'édition maths publiée comme comparaison (elle a réduit la part de coupures de MMLU-Pro de 28,9 % à 0,5 % dans les logs des auteurs ; voir la fin de ce fichier). Utilisez-la seulement si l'utilisateur est d'accord.
N'utilisez jamais les résultats réservés pour choisir ; seules les vérifications pratiques décident.
10. Test réservé à partir de la copie (30–60 min). Lancez-le lorsque l'exécution réservée de base a dépassé ses réponses les plus longues :
python $SKILL/scripts/run_eval.py --task $T --model $COPY --label copy --data $WORK/splits/heldout.jsonl --ids-file $WORK/splits/test_ids.txt --out $WORK/test/copy.jsonl --jobs 2
python $SKILL/scripts/grade.py --task $T $G $WORK/test/base.jsonl $WORK/test/copy.jsonl --out $WORK/test/summary.json
Donnez le verdict à partir du résultat apparié :
- « Gain clair » lorsque réparés > cassés et test des signes p < 0,05.
- « Cohérent avec un gain, non prouvé » lorsque réparés > cassés mais p ≥ 0,05 ; recommandez un test plus large.
- « Pas de gain » lorsque réparés ≤ cassés ; recommandez de restaurer la copie.
11. Rapport avec le modèle dans references/evaluation.md :
- le score réservé avant et après, et les comptes appariés réparés/cassés ;
- les taux de comportement (coupures, signalés, pensée fermée) ;
- chaque candidat avec ses chiffres de filtre et de vérification, et le contrôle aléatoire ;
- les poids modifiés ;
- la mise en garde : désactiver une habitude n'est pas une nouvelle capacité.
Erreurs qui coûtent des heures avant (ne pas répéter)
- Lot trop grand, ou deux longs travaux à leurs pics. Un plantage hors mémoire tue les réponses en cours, qui sont
les boucles. Utilisez
--batch auto --jobs 2, et ne démarrez jamais un deuxième travail 32k alors que le premier est sur ses réponses les plus longues. Le superviseur redémarre les exécutions plantées avec la moitié du lot. - Attendre des exécutions entières. Environ 15 boucles suffisent.
- Éditions en mémoire pour l'évaluation. Seule
probe.py screenapplique des éditions en mémoire, et seulement pour des lectures forcées par l'enseignant. - Faire confiance au seul filtre. Une baisse de filtre de −0,10 sur les boucles n'a cassé que 3 des 26 boucles en génération, donc exécutez toujours la porte.
- Corriger une exécution partielle, ou choisir des questions de test par celles qui ont fini en premier.
- Arrêter des travaux avec
pkill -f <texte>, ce qui tue aussi les observateurs dont la ligne de commande contient ce texte.
Règles (comme dans l'article)
Gardez-les et dites dans le rapport que vous l'avez fait.
- Pas d'entraînement. Pas de passages arrière, autograd, optimiseurs, ou ajustement itératif des poids à une perte. Ajuster α ou les poids jusqu'à ce qu'une métrique s'améliore compte comme ajustement. Choisir parmi la liste fixe de candidats ci-dessus non.
- Les passages avant pour la mesure sont autorisés : génération, probabilités log forcées par l'enseignant, états cachés et activations MLP, et statistiques de ceux-ci ou des poids (moyennes, différences, SVD).
- Hors de portée :
- les embeddings, la tête de sortie (liée) et la norme finale (
weights.pyles refuse) ; - multiplier un tenseur entier par une constante ;
- affinage ou aplatissement global ;
- forcer la pensée à se fermer tôt indépendamment du problème ;
- supprimer le comportement de sécurité : refus, sauvegardes, conformité aux demandes nuisibles.
- les embeddings, la tête de sortie (liée) et la norme finale (
- Seules les valeurs des poids changent. Conservez les noms, formes, dtype, configuration, tokenizer, modèle de chat et paramètres de décodage.
- Discipline de division. Ensembles pratique et réservé disjoints, fixes avant toute édition. Regardez les résultats réservés une fois, après que l'édition est figée.
- Sélection divulguée. Le rapport liste chaque candidat essayé et la métrique qui a choisi le gagnant.
Pourquoi chaque étape est ainsi
- Contraste : la direction est la différence moyenne, à la sortie de chaque couche, entre le texte indésirable et le texte souhaité qui est
par ailleurs adapté.
- Pour les boucles,
loops.pyprend les 1 024 tokens juste avant le début de la boucle et 1 024 tokens à l'intérieur : même question, même réponse, presque la même position. Le début est le premier point où 90 % des 512 tokens suivants répètent un 16-gramme antérieur. - Pour les autres comportements, les paires donnent l'adaptation.
- Pour les boucles,
- Colonnes d'écriture : score_j = (moyenne indésirable − activation moyenne souhaitée du neurone j) × (u · W_down[:, j]) ; prenez les top k. L'écriture statique u·W seule ne suffit pas : seulement 21–22 des 64 colonnes publiées par couche sont dans ses top 64.
- L'édition : W'[:, S] = W[:, S] − α u (uᵀ W[:, S]), calculé en float32 à partir du modèle en cours d'édition. α = 1 supprime la composante, ce qui n'a rien fait pour les boucles ; α = 6 l'inverse à −5c, de sorte que ces neurones écrivent contre le comportement.
- Filtrage : le changement de probabilité log moyenne avec l'édition appliquée. Le texte indésirable devrait devenir moins probable ; le texte souhaité et propre devrait rester. Une direction aléatoire normée sur les mêmes colonnes est le contrôle.
- La porte : les chiffres de filtre ne prédisent la génération que lâchement (voir les résultats ci-dessous), donc une courte vérification de génération sur les questions pratiques décide avant le test réservé.
- Sauvegarde :
weights.pyréécrit seulement les fragments safetensors touchés à partir des fichiers bruts. Les moteurs d'inférence réécrivent ou suppriment les tenseurs au chargement, donc ne sauvegardez jamais les poids depuis un. - Protocole : pensée activée, température 0,6, top_p 0,95, top_k 20, pénalité de présence 1,5 sur les tokens générés uniquement,
32 768 nouveaux tokens (
--profile thinking). Tous les moteurs correspondent à la sémantique de vLLM. - Appariement : les moteurs et les tailles de lot changent les nombres aléatoires, donc appairez par question dans une même comparaison. Ne comparez jamais des scores mesurés sur des questions différentes ou avec des moteurs différents.
R
…
[truncated for translation]