Présentation

Le blog de Madhukara Phatak décrit une technique nommée Dynamic Abliteration qui vise à neutraliser les réponses de refus d’un grand modèle de langage (LLM) sans toucher aux poids du modèle. Le prototype utilise le modèle Qwen/Qwen3-4B, qui possède une dimension cachée de 2560, 36 couches et 32 têtes d’attention. Le modèle est chargé en bfloat16 sur un GPU A100 via torch et transformers. Le test initial montre un refus explicite après 45 tokens d’entrée et 150 tokens générés, avec une latence d’environ 8.90 s.

Méthode d’Abliteration dynamique

Contrairement à l’abliteration statique qui projette les matrices de poids hors d’un vecteur de refus, la méthode dynamique intercepte les flux résiduels pendant l’inférence. Elle calcule un refusal vector à partir de la différence entre les états cachés d’une requête de refus et d’une requête conforme, puis soustrait ce vecteur (ou une version normalisée) aux activations ciblées. Cette opération se fait à la volée, les poids du modèle restant 100 % gelés, ce qui préserve les performances sur les tâches non liées aux refus.

Implémentation multi‑couche avec hooks PyTorch

Le code suivant charge le modèle et capture les activations d’une couche cible (exemple : couche 14) :

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
MODEL_ID = "Qwen/Qwen3-4B"
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.bfloat16,
    device_map="auto",
    trust_remote_code=True,
)
print(f"Hidden Dimension (d): {model.config.hidden_size}")
print(f"Number of Layers: {model.config.num_hidden_layers}")

Après avoir encodé les prompts de refus et de conformité, deux hooks sont enregistrés :

captured_refuse, captured_comply = [], []
TARGET_LAYER = 14

def hook_refuse(module, input, output):
    h = output[0] if not isinstance(output, tuple) else output[0]
    captured_refuse.append(h[0, -1, :].detach())

def hook_comply(module, input, output):
    h = output[0] if not isinstance(output, tuple) else output[0]
    captured_comply.append(h[0, -1, :].detach())

handle = model.model.layers[TARGET_LAYER].register_forward_hook(hook_refuse)
with torch.no_grad():
    model(**enc_refuse)
handle.remove()

handle = model.model.layers[TARGET_LAYER].register_forward_hook(hook_comply)
with torch.no_grad():
    model(**enc_comply)
handle.remove()

v_refusal = captured_refuse[0] - captured_comply[0]
v_refusal_unit = v_refusal / torch.norm(v_refusal)

Le vecteur normalisé est ensuite injecté dans plusieurs couches via un hook de sortie :

ALPHA = 1.2

def ablation_hook(module, args, kwargs, output):
    hidden = output[0] if not isinstance(output, tuple) else output[0]
    modified = hidden - (ALPHA * v_refusal_unit.view(1, 1, -1))
    rest = output[1:] if isinstance(output, tuple) else None
    return (modified,) + rest if rest else modified

for layer in [10, 14, 20]:
    model.model.layers[layer].register_forward_hook(ablation_hook)

Cette injection multi‑couche améliore la suppression du refus sans altérer les représentations des couches non ciblées.

Évaluation et limites

Les expériences montrent que, sur le même prompt de keylogger, le modèle modifié génère du texte conforme aux instructions de refus, éliminant la réponse de mise en garde. Le temps d’inférence augmente légèrement en raison des hooks, mais reste inférieur à 10 s sur l’A100. La technique dépend fortement de la qualité du vecteur de refus : un vecteur mal estimé peut introduire des dérives sémantiques dans d’autres tâches. De plus, la méthode nécessite un accès au code du modèle et la capacité d’insérer des hooks, ce qui n’est pas toujours possible avec des modèles fermés. Enfin, la généralisation à d’autres architectures (ex. : GPT‑NeoX, LLaMA) n’est pas démontrée, et chaque modèle peut requérir un calibrage spécifique du facteur ALPHA et des couches cibles.