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()