Pipeline (stages 1 -> 4-1) can now be run in order from each stage folder.
Stage scripts:
- 2-1: make SEP/FocalLora prep portable (derive paths from __file__ instead of
hardcoded /home/hujk/...) and add prepare_head_ident_dataset.sh runner.
Verified the SEP converter reproduces the committed jsonl byte-for-byte.
- 2-2: unify the four Ident_IH_ALL_1-4_<model>.sh scripts (modernise llama to
conda hook + $ROOT/models; add the missing FocalLora step to qwen3-4b/8b so
focallora.json gets generated for them too).
- 2-3: default TARGETS now covers the three curves from the README
(all_roc_inst_0.1, user_roc_inst_0.1, focallora).
- 3-2: add combos/ with 24 scripts (4 models x {pbs,nts,nts_wam} x {squad,tri}),
head ranking pinned to all_roc_inst_0.1, TOPK overridable.
- 4-1: add eval_single.sh driver + combos/ with 24 cross-eval wrappers
(squad-trained -> tri-eval and vice versa), reusing the --eval-only path.
Eval semantics:
- Judge ASR before UTIL: a response carrying the injected answer now counts as
attacked even when it also contains the correct answer. This changes the
metric, so old training_log.csv rows are not comparable.
- Add --dev-holdout: reserve the last N source rows as a dev slice; training
drops them and the in-training quick eval uses only them. Previously the
quick eval silently defaulted to the squad evaluation set, which contradicted
the README and self-contaminated squad-trained runs.
- train_attn_kl_clean.sh now passes --eval-data-path/--eval-topicattack-path.
- Add --eval-step0 to log an untuned-baseline row before any weight update.
Housekeeping:
- Quarantine superseded entry points under legacy/ (2-2 single-step wrappers,
3-2 old _tuning.fix.* wrappers, 3-1 auxiliary), each with a README.
- Fix .gitignore: the model_score rule was anchored at the repo root and never
matched Codes/..., so ~26GB of intermediates had been staged. Now excludes
*.pkl (~25GB), heads_sorted_eval/ (~690MB), outputs_lora/ checkpoints
(~3.2GB) and pycache. heads_sorted/ and head_scoring_combined.json are kept
deliberately: they are small and are the HEAD_PATH inputs stage 3-2 needs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
56 lines
2.1 KiB
Python
56 lines
2.1 KiB
Python
class AttentionManager:
|
|
"""
|
|
Captures attention weights via hooks to avoid OOM.
|
|
Slices specific token positions immediately and discards the full matrix.
|
|
"""
|
|
def __init__(self, model):
|
|
self.model = model
|
|
self.attentions = {} # Store {layer_idx: attention_tensor}
|
|
self.hooks = []
|
|
self._register_hooks()
|
|
|
|
def _register_hooks(self):
|
|
# Locate the actual decoder layers.
|
|
# For Llama/Qwen + PEFT, it is usually model.base_model.model.layers or model.model.layers
|
|
if hasattr(self.model, "base_model"):
|
|
layers = self.model.base_model.model.layers
|
|
else:
|
|
layers = self.model.model.layers
|
|
|
|
for i, layer in enumerate(layers):
|
|
self.hooks.append(layer.register_forward_hook(self._make_hook(i)))
|
|
|
|
def _make_hook(self, idx):
|
|
def hook(module, args, output):
|
|
# output signature for LlamaDecoderLayer: (hidden_states, self_attn_weights, present_key_value)
|
|
# We want output[1] (self_attn_weights)
|
|
|
|
# Note: output is a tuple, so we must return a new tuple
|
|
if len(output) > 1 and output[1] is not None:
|
|
full_attn = output[1] # Shape: [bs, heads, seq_len, seq_len]
|
|
|
|
# --- CRITICAL OPTIMIZATION ---
|
|
# Slice ONLY the last token query, preserving gradients if needed.
|
|
# Shape becomes: [bs, heads, 1, seq_len]
|
|
# This is tiny compared to the full matrix.
|
|
print(f"hook len {len(output)}")
|
|
self.attentions[idx] = full_attn[..., -1, :]
|
|
|
|
# Replace the full attention in the output with None.
|
|
# This frees the GBs of memory immediately.
|
|
new_output = list(output)
|
|
new_output[1] = None
|
|
return tuple(new_output)
|
|
|
|
return output
|
|
return hook
|
|
|
|
def get_results(self):
|
|
return self.attentions
|
|
|
|
def clear(self):
|
|
self.attentions = {}
|
|
|
|
def remove_hooks(self):
|
|
for h in self.hooks:
|
|
h.remove() |