From bd878363f8adef78137c7cc18af9dec166982142 Mon Sep 17 00:00:00 2001 From: Joseph Magly <1159087+jmagly@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:09:34 -0400 Subject: [PATCH] test: lock Qwen2.5 projection contract --- tests/test_abliterate.py | 69 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/tests/test_abliterate.py b/tests/test_abliterate.py index 7f802b5..bad7d66 100644 --- a/tests/test_abliterate.py +++ b/tests/test_abliterate.py @@ -8,7 +8,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest import torch -from transformers import GPT2Config, GPT2LMHeadModel +from transformers import GPT2Config, GPT2LMHeadModel, Qwen2Config, Qwen2ForCausalLM from obliteratus.abliterate import ( HARMFUL_PROMPTS, @@ -1900,6 +1900,73 @@ class TestDistillSVD: # --------------------------------------------------------------------------- class TestExcise: + def test_qwen25_coder_advanced_excise_projects_all_dense_weights(self): + """Qwen2.5 must not silently resolve to zero projection targets.""" + config = Qwen2Config( + vocab_size=128, + hidden_size=32, + intermediate_size=64, + num_hidden_layers=2, + num_attention_heads=4, + num_key_value_heads=2, + max_position_embeddings=64, + ) + model = Qwen2ForCausalLM(config) + handle = ModelHandle( + model=model, + tokenizer=MagicMock(), + config=config, + model_name="Qwen/Qwen2.5-Coder-7B-Instruct", + task="causal_lm", + ) + pipeline = AbliterationPipeline( + model_name=handle.model_name, + method="advanced", + refinement_passes=1, + norm_preserve=False, + project_biases=False, + layer_adaptive_strength=False, + safety_neuron_masking=False, + attention_head_surgery=False, + use_sae_features=False, + use_kl_optimization=False, + projection_target="all", + harmful_prompts=["harmful"], + harmless_prompts=["harmless"], + ) + pipeline.handle = handle + pipeline._strong_layers = [0] + direction = torch.arange(1, config.hidden_size + 1, dtype=torch.float32) + direction /= direction.norm() + pipeline.refusal_directions = {0: direction.clone()} + pipeline.refusal_subspaces = {0: direction.unsqueeze(0)} + + layer = model.model.layers[0] + original_weights = { + name: parameter.detach().clone() + for name, parameter in layer.named_parameters() + } + + pipeline._excise() + + expected = { + "self_attn.q_proj.weight", + "self_attn.k_proj.weight", + "self_attn.v_proj.weight", + "self_attn.o_proj.weight", + "mlp.gate_proj.weight", + "mlp.up_proj.weight", + "mlp.down_proj.weight", + } + changed = { + name + for name, parameter in layer.named_parameters() + if not torch.equal(original_weights[name], parameter) + } + assert handle.architecture == "qwen2" + assert changed == expected + assert pipeline._excise_modified_count == 8 # seven layer weights + lm_head + def test_excise_basic(self, handle): """Basic method should modify weights.""" from obliteratus.strategies.utils import get_layer_modules