Coverage for transformer_lens/model_bridge/generalized_components/joint_qkv_attention.py: 86%

233 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-01 16:23 +0000

1"""Joint QKV attention bridge component. 

2 

3This module contains the bridge component for attention layers that use a fused qkv matrix. 

4""" 

5import copy 

6from typing import Any, Callable, Dict, Optional 

7 

8import einops 

9import torch 

10 

11from transformer_lens.conversion_utils.conversion_steps.base_tensor_conversion import ( 

12 BaseTensorConversion, 

13) 

14from transformer_lens.model_bridge.generalized_components.attention import ( 

15 AttentionBridge, 

16) 

17from transformer_lens.model_bridge.generalized_components.base import ( 

18 GeneralizedComponent, 

19) 

20from transformer_lens.model_bridge.generalized_components.linear import LinearBridge 

21from transformer_lens.utilities.quantization import require_readable_weight 

22 

23 

24class JointQKVAttentionBridge(AttentionBridge): 

25 """Joint QKV attention bridge that wraps a joint qkv linear layer. 

26 

27 This component wraps attention layers that use a fused qkv matrix such that 

28 the individual activations from the separated q, k, and v matrices are hooked and accessible. 

29 """ 

30 

31 supports_attn_result: bool = True 

32 

33 # property_aliases inherited from AttentionBridge (W_Q, W_K, W_V, W_O, b_Q, b_K, b_V, b_O) 

34 

35 def __init__( 

36 self, 

37 name: str, 

38 config: Any, 

39 split_qkv_matrix: Optional[Callable] = None, 

40 submodules: Optional[Dict[str, GeneralizedComponent]] = None, 

41 qkv_conversion_rule: Optional[BaseTensorConversion] = None, 

42 attn_conversion_rule: Optional[BaseTensorConversion] = None, 

43 pattern_conversion_rule: Optional[BaseTensorConversion] = None, 

44 requires_position_embeddings: bool = False, 

45 requires_attention_mask: bool = False, 

46 ): 

47 """Initialize the Joint QKV attention bridge. 

48 

49 Args: 

50 name: The name of this component 

51 config: Model configuration (required for auto-conversion detection) 

52 split_qkv_matrix: Optional function to split the qkv matrix into q, k, and v linear transformations. 

53 If None, uses the default implementation that splits a combined c_attn weight/bias. 

54 submodules: Dictionary of submodules to register (e.g., q_proj, k_proj, etc.) 

55 qkv_conversion_rule: Optional conversion rule for the individual q, k, and v matrices to convert their output shapes to HookedTransformer format. If None, uses default RearrangeTensorConversion 

56 attn_conversion_rule: Optional conversion rule. Passed to parent AttentionBridge. If None, AttentionAutoConversion will be used 

57 pattern_conversion_rule: Optional conversion rule for attention patterns. If None, 

58 uses AttentionPatternConversion to ensure [n_heads, pos, pos] shape 

59 requires_position_embeddings: Whether this attention requires position_embeddings as input 

60 requires_attention_mask: Whether this attention requires attention_mask as input 

61 """ 

62 super().__init__( 

63 name, 

64 config, 

65 submodules=submodules, 

66 conversion_rule=attn_conversion_rule, 

67 pattern_conversion_rule=pattern_conversion_rule, 

68 requires_position_embeddings=requires_position_embeddings, 

69 requires_attention_mask=requires_attention_mask, 

70 ) 

71 self.split_qkv_matrix = ( 

72 split_qkv_matrix if split_qkv_matrix is not None else self._default_split_qkv_matrix 

73 ) 

74 if qkv_conversion_rule is not None: 

75 self.qkv_conversion_rule = qkv_conversion_rule 

76 else: 

77 self.qkv_conversion_rule = self._create_qkv_conversion_rule() 

78 self.q = LinearBridge(name="q") 

79 self.k = LinearBridge(name="k") 

80 self.v = LinearBridge(name="v") 

81 for submodule_name, submodule in (submodules or {}).items(): 

82 if not hasattr(self, submodule_name): 82 ↛ 81line 82 didn't jump to line 81 because the condition on line 82 was always true

83 setattr(self, submodule_name, submodule) 

84 self.submodules["q"] = self.q 

85 self.submodules["k"] = self.k 

86 self.submodules["v"] = self.v 

87 self.q.hook_out.hook_conversion = self.qkv_conversion_rule 

88 self.k.hook_out.hook_conversion = self.qkv_conversion_rule 

89 self.v.hook_out.hook_conversion = self.qkv_conversion_rule 

90 

91 # Register q, k, v LinearBridges in real_components for weight distribution 

92 # This allows set_processed_weights to distribute weights to these submodules 

93 self.real_components["q"] = ("q", self.q) 

94 self.real_components["k"] = ("k", self.k) 

95 self.real_components["v"] = ("v", self.v) 

96 if hasattr(self, "o"): 

97 self.real_components["o"] = ("o", self.o) 

98 

99 self._reference_model: Optional[Any] = None 

100 

101 # Exclude stale qkv combined weights from state_dict after splitting. 

102 self._register_state_dict_hook(JointQKVAttentionBridge._filter_qkv_state_dict) 

103 self.register_load_state_dict_pre_hook( 

104 JointQKVAttentionBridge._restore_filtered_qkv_state_dict 

105 ) 

106 

107 def __deepcopy__(self, memo): 

108 """Share split_qkv_matrix and config across clones instead of copying. 

109 

110 split_qkv_matrix may be a bound method of the architecture adapter, 

111 which transitively references the full HF model. Without this override, 

112 deepcopy duplicates the entire model per block (~1GB x N_layers). 

113 """ 

114 saved_split_fn = self.split_qkv_matrix 

115 saved_config = self.config 

116 

117 self.split_qkv_matrix = None # type: ignore[assignment] 

118 self.config = None 

119 try: 

120 # Remove override from defining class (not subclass) to avoid recursion. 

121 owner = JointQKVAttentionBridge 

122 override = owner.__dict__["__deepcopy__"] 

123 del owner.__deepcopy__ 

124 try: 

125 clone = copy.deepcopy(self, memo) 

126 finally: 

127 owner.__deepcopy__ = override # type: ignore[method-assign] 

128 finally: 

129 self.split_qkv_matrix = saved_split_fn 

130 self.config = saved_config 

131 

132 clone.split_qkv_matrix = saved_split_fn 

133 clone.config = saved_config 

134 return clone 

135 

136 @staticmethod 

137 def _filter_qkv_state_dict( 

138 module: torch.nn.Module, 

139 state_dict: Dict[str, Any], 

140 prefix: str, 

141 local_metadata: Dict[str, Any], 

142 ) -> None: 

143 """State dict hook that removes stale combined QKV entries.""" 

144 qkv_prefix = prefix + "qkv." 

145 keys_to_remove = [k for k in state_dict if k.startswith(qkv_prefix)] 

146 for k in keys_to_remove: 

147 del state_dict[k] 

148 

149 @staticmethod 

150 def _restore_filtered_qkv_state_dict( 

151 module: torch.nn.Module, 

152 state_dict: Dict[str, Any], 

153 prefix: str, 

154 local_metadata: Dict[str, Any], 

155 strict: bool, 

156 missing_keys: list[str], 

157 unexpected_keys: list[str], 

158 error_msgs: list[str], 

159 ) -> None: 

160 """Insert current combined weights only to satisfy strict key matching. 

161 

162 Production checkpoints restore authoritative values through the unfiltered 

163 Hugging Face ``_original_component`` path. 

164 """ 

165 del local_metadata, strict, missing_keys, unexpected_keys, error_msgs 

166 qkv = module._modules.get("qkv") 

167 if qkv is None: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true

168 return 

169 for key, value in qkv.state_dict(prefix=f"{prefix}qkv.").items(): 

170 state_dict.setdefault(key, value) 

171 

172 def _create_qkv_conversion_rule(self) -> BaseTensorConversion: 

173 """Create the appropriate conversion rule for the individual q, k, and v matrices. 

174 

175 Returns: 

176 BaseTensorConversion for individual q, k, and v matrices 

177 """ 

178 assert self.config is not None 

179 

180 class ConditionalRearrangeConversion(BaseTensorConversion): 

181 def __init__(self, n_heads: int): 

182 super().__init__() 

183 self.n_heads = n_heads 

184 self.pattern = ( 

185 "batch seq (num_attention_heads d_head) -> batch seq num_attention_heads d_head" 

186 ) 

187 

188 def handle_conversion(self, input_value: torch.Tensor, *full_context) -> torch.Tensor: 

189 if input_value.ndim == 4: 189 ↛ 190line 189 didn't jump to line 190 because the condition on line 189 was never true

190 return input_value 

191 elif input_value.ndim == 3: 191 ↛ 196line 191 didn't jump to line 196 because the condition on line 191 was always true

192 return einops.rearrange( 

193 input_value, self.pattern, num_attention_heads=self.n_heads 

194 ) 

195 else: 

196 raise ValueError( 

197 f"Expected 3D or 4D tensor, got {input_value.ndim}D with shape {input_value.shape}" 

198 ) 

199 

200 def revert(self, input_value: torch.Tensor, *full_context) -> torch.Tensor: 

201 if input_value.ndim == 3: 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true

202 return input_value 

203 elif input_value.ndim == 4: 203 ↛ 210line 203 didn't jump to line 210 because the condition on line 203 was always true

204 return einops.rearrange( 

205 input_value, 

206 "batch seq num_attention_heads d_head -> batch seq (num_attention_heads d_head)", 

207 num_attention_heads=self.n_heads, 

208 ) 

209 else: 

210 raise ValueError( 

211 f"Expected 3D or 4D tensor, got {input_value.ndim}D with shape {input_value.shape}" 

212 ) 

213 

214 return ConditionalRearrangeConversion(self.config.n_heads) 

215 

216 def _default_split_qkv_matrix( 

217 self, original_attention_component: Any 

218 ) -> tuple[torch.nn.Module, torch.nn.Module, torch.nn.Module]: 

219 """Default implementation to split the QKV matrix into separate linear transformations. 

220 

221 This uses the 'qkv' submodule defined in component_mapping to find the combined QKV weights. 

222 Assumes combined QKV weights in the format [d_model, 3 * d_model] for weights 

223 and [3 * n_head * d_head] for bias. 

224 

225 Args: 

226 original_attention_component: The original attention layer component 

227 Returns: 

228 Tuple of nn.Linear modules for Q, K, and V transformations 

229 """ 

230 assert self.config is not None 

231 assert original_attention_component is not None 

232 

233 # Get the combined QKV component using the 'qkv' submodule name 

234 if "qkv" not in self.submodules: 234 ↛ 235line 234 didn't jump to line 235 because the condition on line 234 was never true

235 raise ValueError( 

236 "No 'qkv' submodule found in JointQKVAttentionBridge. " 

237 "Please define a 'qkv' submodule or provide a custom split_qkv_matrix function." 

238 ) 

239 

240 # Get the actual qkv component name from the bridge 

241 qkv_bridge = self.submodules["qkv"] 

242 qkv_name = qkv_bridge.name 

243 

244 # Ensure qkv_name is not None 

245 if qkv_name is None: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true

246 raise ValueError( 

247 "qkv bridge name is None. " "Please provide a custom split_qkv_matrix function." 

248 ) 

249 

250 # Navigate to the component using the name 

251 if not hasattr(original_attention_component, qkv_name): 251 ↛ 252line 251 didn't jump to line 252 because the condition on line 251 was never true

252 raise ValueError( 

253 f"Cannot find '{qkv_name}' in attention component. " 

254 f"Available attributes: {dir(original_attention_component)}. " 

255 f"Please provide a custom split_qkv_matrix function." 

256 ) 

257 

258 qkv_component = getattr(original_attention_component, qkv_name) 

259 

260 # Before the tensor_split/nn.Parameter below: int8 and uint8 would die 

261 # there with an opaque "Only Tensors of floating point ... can require 

262 # gradients", and float8 would split SILENTLY into scale-less pieces. 

263 qkv_weights = require_readable_weight( 

264 qkv_component.weight, 

265 operation="split a fused QKV projection at boot", 

266 owner=qkv_component, 

267 ) 

268 

269 # Original qkv_weights shape: [d_model, 3 * d_model] 

270 # Split into three equal parts along dimension 1 to get Q, K, V weights 

271 q_weight, k_weight, v_weight = torch.tensor_split(qkv_weights, 3, dim=1) 

272 

273 # Handle bias if it exists 

274 has_bias = hasattr(qkv_component, "bias") and qkv_component.bias is not None 

275 q_bias: torch.Tensor | None 

276 k_bias: torch.Tensor | None 

277 v_bias: torch.Tensor | None 

278 if has_bias: 278 ↛ 287line 278 didn't jump to line 287 because the condition on line 278 was always true

279 qkv_bias = qkv_component.bias 

280 assert isinstance(qkv_bias, torch.Tensor) 

281 

282 # Original qkv_bias shape: [3 * n_head * d_head] 

283 # Reshape to [3, n_head * d_head] to split by Q, K, V 

284 qkv_bias = qkv_bias.reshape(3, self.config.n_heads * self.config.d_head) 

285 q_bias, k_bias, v_bias = qkv_bias[0, :], qkv_bias[1, :], qkv_bias[2, :] 

286 else: 

287 q_bias = k_bias = v_bias = None 

288 

289 # Create plain nn.Linear modules that output 3D tensors [batch, seq, d_model] 

290 q_linear = torch.nn.Linear(q_weight.shape[0], q_weight.shape[1], bias=has_bias) 

291 q_linear.weight = torch.nn.Parameter(q_weight.T) 

292 if has_bias and q_bias is not None: 292 ↛ 295line 292 didn't jump to line 295 because the condition on line 292 was always true

293 q_linear.bias = torch.nn.Parameter(q_bias) 

294 

295 k_linear = torch.nn.Linear(k_weight.shape[0], k_weight.shape[1], bias=has_bias) 

296 k_linear.weight = torch.nn.Parameter(k_weight.T) 

297 if has_bias and k_bias is not None: 297 ↛ 300line 297 didn't jump to line 300 because the condition on line 297 was always true

298 k_linear.bias = torch.nn.Parameter(k_bias) 

299 

300 v_linear = torch.nn.Linear(v_weight.shape[0], v_weight.shape[1], bias=has_bias) 

301 v_linear.weight = torch.nn.Parameter(v_weight.T) 

302 if has_bias and v_bias is not None: 302 ↛ 305line 302 didn't jump to line 305 because the condition on line 302 was always true

303 v_linear.bias = torch.nn.Parameter(v_bias) 

304 

305 return q_linear, k_linear, v_linear 

306 

307 def set_original_component(self, original_component: torch.nn.Module) -> None: 

308 """Set the original component that this bridge wraps and initialize LinearBridges for q, k, v, and o transformations. 

309 

310 Args: 

311 original_component: The original attention layer to wrap 

312 """ 

313 super().set_original_component(original_component) 

314 

315 # Capture HF-specific attention flags for faithful reconstruction 

316 self._reorder_and_upcast_attn = getattr( 

317 original_component, "reorder_and_upcast_attn", False 

318 ) 

319 

320 q_transformation, k_transformation, v_transformation = self.split_qkv_matrix( 

321 original_component 

322 ) 

323 self.q.set_original_component(q_transformation) 

324 self.k.set_original_component(k_transformation) 

325 self.v.set_original_component(v_transformation) 

326 if hasattr(self, "o") and hasattr(original_component, "c_proj"): 

327 self.o.set_original_component(original_component.c_proj) 

328 

329 def forward(self, *args: Any, **kwargs: Any) -> Any: 

330 """Forward pass through the qkv linear transformation with hooks. 

331 

332 Args: 

333 *args: Input arguments, where the first argument should be the input tensor 

334 **kwargs: Additional keyword arguments 

335 

336 Returns: 

337 Output tensor after qkv linear transformation 

338 """ 

339 hooked_input = self._apply_attention_input_hook(*args, **kwargs) 

340 if self._is_split_qkv_fork_active(): 

341 q_output, k_output, v_output = self._split_forward_qkv(hooked_input) 

342 else: 

343 q_output = self.q(hooked_input) 

344 k_output = self.k(hooked_input) 

345 v_output = self.v(hooked_input) 

346 output = self._reconstruct_attention(q_output, k_output, v_output, **kwargs) 

347 output = self._process_output(output) 

348 return output 

349 

350 def _is_split_qkv_fork_active(self) -> bool: 

351 cfg = self.config 

352 if cfg is None or not getattr(cfg, "n_heads", 0): 352 ↛ 353line 352 didn't jump to line 353 because the condition on line 352 was never true

353 return False 

354 return bool( 

355 getattr(cfg, "use_split_qkv_input", False) or getattr(cfg, "use_attn_in", False) 

356 ) 

357 

358 def _split_forward_qkv( 

359 self, hidden_states: torch.Tensor 

360 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: 

361 """Fork the residual into independent Q/K/V copies, apply per-head projection. 

362 

363 After `split_qkv_matrix` runs in `set_original_component`, q/k/v are 

364 separate `nn.Linear` modules whose weights partition the output dim by 

365 head (output row h*d_head + i ↔ head h, dim i). Plain nn.Linear applied 

366 to a 4D [B, S, H, d_model] copy would broadcast the full weight over 

367 every head's copy and then we'd keep only the diagonal — n_heads× extra 

368 compute. The per-head einsum in `_project_per_head_qkv` slices W per 

369 head directly, producing the same 4D [B, S, H, d_head] result that 

370 `_reconstruct_attention` expects. 

371 """ 

372 cfg = self.config 

373 assert cfg is not None, "config required for split QKV fork" 

374 n_heads = int(cfg.n_heads) 

375 n_kv_heads = int(getattr(cfg, "n_key_value_heads", None) or n_heads) 

376 d_head = int(getattr(cfg, "d_head", 0) or (int(cfg.d_model) // n_heads)) 

377 use_split = bool(getattr(cfg, "use_split_qkv_input", False)) 

378 # #1317: fork pre-LN when available so hook patches match legacy. 

379 captured = self._captured_pre_ln_residual 

380 source = captured if captured is not None else hidden_states 

381 if use_split: 

382 q_in = self._fork_and_norm_per_head(source, self.hook_q_input, n_heads) 

383 k_in = self._fork_and_norm_per_head(source, self.hook_k_input, n_kv_heads) 

384 v_in = self._fork_and_norm_per_head(source, self.hook_v_input, n_kv_heads) 

385 else: 

386 attn_in = self._fork_and_norm_per_head(source, self.hook_attn_in, n_heads) 

387 q_in = attn_in 

388 if n_kv_heads != n_heads: 388 ↛ 389line 388 didn't jump to line 389 because the condition on line 388 was never true

389 k_in = attn_in[..., :n_kv_heads, :].contiguous() 

390 v_in = attn_in[..., :n_kv_heads, :].contiguous() 

391 else: 

392 k_in = v_in = attn_in 

393 q_4d = self._project_per_head_qkv(self.q, q_in, n_heads, d_head) 

394 k_4d = self._project_per_head_qkv(self.k, k_in, n_kv_heads, d_head) 

395 v_4d = self._project_per_head_qkv(self.v, v_in, n_kv_heads, d_head) 

396 return q_4d, k_4d, v_4d 

397 

398 def _process_output(self, output: Any) -> Any: 

399 """Process the output from _reconstruct_attention. 

400 

401 This override skips the duplicate hook_pattern call since 

402 _reconstruct_attention already applies both hook_attn_scores 

403 and hook_pattern correctly. 

404 

405 Args: 

406 output: Output tuple from _reconstruct_attention (attn_output, attn_weights) 

407 

408 Returns: 

409 Processed output with hook_out applied 

410 """ 

411 attn_pattern = None 

412 if isinstance(output, tuple) and len(output) >= 2: 412 ↛ 414line 412 didn't jump to line 414 because the condition on line 412 was always true

413 attn_pattern = output[1] 

414 if attn_pattern is not None: 

415 self._pattern = attn_pattern 

416 if isinstance(output, tuple) and len(output) > 0 and isinstance(output[0], torch.Tensor): 416 ↛ 420line 416 didn't jump to line 420 because the condition on line 416 was always true

417 processed_output = list(output) 

418 processed_output[0] = self.hook_hidden_states(output[0]) 

419 output = tuple(processed_output) 

420 if isinstance(output, torch.Tensor): 420 ↛ 421line 420 didn't jump to line 421 because the condition on line 420 was never true

421 output = self.hook_out(output) 

422 elif isinstance(output, tuple) and len(output) > 0: 422 ↛ 429line 422 didn't jump to line 429 because the condition on line 422 was always true

423 processed_tuple = list(output) 

424 if isinstance(output[0], torch.Tensor): 424 ↛ 426line 424 didn't jump to line 426 because the condition on line 424 was always true

425 processed_tuple[0] = self.hook_out(output[0]) 

426 if len(processed_tuple) == 1: 426 ↛ 427line 426 didn't jump to line 427 because the condition on line 426 was never true

427 return processed_tuple[0] 

428 output = tuple(processed_tuple) 

429 return output 

430 

431 def _apply_attention_input_hook(self, *args: Any, **kwargs: Any) -> torch.Tensor: 

432 """Apply attention input hook to the input tensor. 

433 

434 This method extracts the input tensor from args/kwargs and applies the attention 

435 input hook in the same way as the super class. 

436 

437 Args: 

438 *args: Input arguments, where the first argument should be the input tensor 

439 **kwargs: Additional keyword arguments that might contain input 

440 

441 Returns: 

442 Input tensor with attention input hook applied 

443 

444 Raises: 

445 ValueError: If no input tensor is found in args or kwargs 

446 """ 

447 input_tensor = None 

448 if "query_input" in kwargs: 448 ↛ 449line 448 didn't jump to line 449 because the condition on line 448 was never true

449 input_tensor = kwargs["query_input"] 

450 elif "hidden_states" in kwargs: 

451 input_tensor = kwargs["hidden_states"] 

452 elif len(args) > 0 and isinstance(args[0], torch.Tensor): 452 ↛ 455line 452 didn't jump to line 455 because the condition on line 452 was always true

453 input_tensor = args[0] 

454 else: 

455 raise ValueError("No input tensor found in args or kwargs") 

456 return self.hook_in(input_tensor) 

457 

458 def _reconstruct_attention( 

459 self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, **kwargs 

460 ) -> tuple: 

461 """Manual attention reconstruction used by the bridge after splitting fused QKV projections.""" 

462 assert self.original_component is not None 

463 assert self.config is not None 

464 num_heads = self.config.n_heads 

465 num_kv_heads = getattr(self.config, "n_key_value_heads", None) or num_heads 

466 

467 q, k, v, batch_size, seq_len, head_dim = self._reshape_qkv_to_heads( 

468 q, k, v, num_heads, num_kv_heads 

469 ) 

470 

471 # KV cache: extend K/V with cached positions. 

472 k, v = self._update_kv_cache(k, v, **kwargs) 

473 

474 # GQA/MQA: expand K/V heads to match Q heads 

475 if num_kv_heads != num_heads: 

476 n_rep = num_heads // num_kv_heads 

477 k = k.repeat_interleave(n_rep, dim=1) 

478 v = v.repeat_interleave(n_rep, dim=1) 

479 

480 # Attention scale: 1/sqrt(d_head) with optional inverse-layer scaling 

481 scale = head_dim ** (-0.5) 

482 if ( 482 ↛ 487line 482 didn't jump to line 487 because the condition on line 482 was never true

483 hasattr(self.config, "scale_attn_by_inverse_layer_idx") 

484 and self.config.scale_attn_by_inverse_layer_idx 

485 and self._layer_idx is not None 

486 ): 

487 scale /= float(self._layer_idx + 1) 

488 

489 # When reorder_and_upcast_attn is True, HF computes attention in float32. 

490 reorder_and_upcast = getattr(self, "_reorder_and_upcast_attn", False) 

491 if reorder_and_upcast: 491 ↛ 492line 491 didn't jump to line 492 because the condition on line 491 was never true

492 q_scores = q.to(torch.float32) 

493 k_scores = k.to(torch.float32) 

494 else: 

495 q_scores = q 

496 k_scores = k 

497 

498 kv_seq_len = k.shape[-2] # Includes cached positions 

499 attn_scores = torch.matmul(q_scores, k_scores.transpose(-2, -1)) * scale 

500 attention_mask = kwargs.get("attention_mask", None) 

501 attn_scores = self._apply_reconstruct_attention_mask( 

502 attn_scores=attn_scores, 

503 attention_mask=attention_mask, 

504 seq_len=kv_seq_len, 

505 q_seq_len=seq_len, 

506 ) 

507 

508 attn_scores = self.hook_attn_scores(attn_scores) 

509 

510 attn_weights = self._softmax_dropout_pattern( 

511 attn_scores, 

512 target_dtype=v.dtype if reorder_and_upcast else None, 

513 ) 

514 attn_output = torch.matmul(attn_weights, v) 

515 attn_output = self._reshape_attn_output( 

516 attn_output, batch_size, seq_len, num_heads, head_dim 

517 ) 

518 if ( 

519 bool(getattr(self.config, "use_attn_result", False)) 

520 and hasattr(self, "o") 

521 and self.o.original_component is not None 

522 ): 

523 # Per-head output pre-sum. Fire hook_z on the pre-projection flat 

524 # tensor first so patches at hook_z propagate into the per-head 

525 # computation, matching how the default path's `self.o(...)` call 

526 # fires o.hook_in before the linear. 

527 attn_output = self.o.hook_in(attn_output) 

528 z_4d = attn_output.view(batch_size, seq_len, num_heads, head_dim) 

529 attn_output = self._compute_per_head_result(z_4d, num_heads, head_dim) 

530 else: 

531 attn_output = self._apply_output_projection(attn_output) 

532 return (attn_output, attn_weights)