Coverage for transformer_lens/model_bridge/remote_bridge.py: 84%

99 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-21 19:27 +0000

1"""Non-torch bridge: vLLM workers, Inspect remote providers.""" 

2from __future__ import annotations 

3 

4import warnings 

5from typing import Any, Callable, List, Optional, Tuple, Union 

6 

7from transformer_lens.hook_points import HookIntrospectionMixin, HookPoint 

8from transformer_lens.model_bridge.bridge_core import BridgeCore 

9from transformer_lens.model_bridge.driver_protocol import ( 

10 ForwardResult, 

11 TensorLike, 

12 to_torch, 

13 validate_driver, 

14) 

15 

16 

17class RemoteBridge(BridgeCore, HookIntrospectionMixin): 

18 """Bridge for backends with no local ``nn.Module`` (vLLM, Inspect). 

19 

20 No nn.Module parentage strips the torch-only surface; driver pre-declares 

21 ``supported_hook_points`` (no model to walk). 

22 """ 

23 

24 def __init__( 

25 self, 

26 adapter: Any, 

27 tokenizer: Any, 

28 driver: Any, 

29 ) -> None: 

30 if not driver.supported_hook_points: 

31 raise ValueError( 

32 "RemoteBridge requires driver.supported_hook_points to be " 

33 "non-empty: non-torch drivers own the hook namespace because " 

34 "there is no local model for the bridge to walk." 

35 ) 

36 BridgeCore.__init__(self, adapter, tokenizer, driver) 

37 # No local device; tensor.to(None) is a no-op so downstream patterns degrade cleanly. 

38 self.cfg.device = None 

39 # HookPoint is nn.Module-backed but RemoteBridge isn't an nn.Module — 

40 # named_modules() walks don't apply; only registry lookup matters. 

41 for name in driver.supported_hook_points: 

42 hp = HookPoint() 

43 hp.name = name 

44 self._hook_registry[name] = hp 

45 self._hook_registry_initialized = True 

46 validate_driver(self._driver, after_bridge_construction=True) 

47 

48 @staticmethod 

49 def boot_vllm(*args: Any, **kwargs: Any) -> "RemoteBridge": 

50 """Boot a model via vLLM. Returns a RemoteBridge wrapping a VLLMDriver. 

51 

52 Mirrors ``TransformerBridge.boot_transformers``. Lazy import so 

53 ``remote_bridge`` itself stays vLLM-agnostic — only callers of this 

54 method need vLLM installed. See :func:`sources.vllm.boot_vllm` for kwargs. 

55 """ 

56 from .sources.vllm import boot_vllm as _boot_vllm 

57 

58 return _boot_vllm(*args, **kwargs) 

59 

60 @staticmethod 

61 def boot_inspect(*args: Any, **kwargs: Any) -> "RemoteBridge": 

62 """Boot a model via an inspect_ai provider. Returns a RemoteBridge wrapping 

63 an InspectDriver. Lazy import keeps remote_bridge inspect-agnostic. See 

64 :func:`sources.inspect.boot_inspect` for kwargs.""" 

65 from .sources.inspect import boot_inspect as _boot_inspect 

66 

67 return _boot_inspect(*args, **kwargs) 

68 

69 def _scan_existing_hooks(self, module: Any, prefix: str = "") -> None: 

70 """No-op: registry built from driver declarations in __init__.""" 

71 

72 def forward( 

73 self, 

74 input: Any = None, 

75 *, 

76 return_type: str | None = "logits", 

77 loss_per_token: bool = False, 

78 labels: Any = None, 

79 **kwargs: Any, 

80 ) -> Any: 

81 """Tokenize → driver.forward → replay captures → finalize per return_type. 

82 

83 Explicit ``labels`` use shifted causal loss; remote encoder-decoder loss 

84 is unsupported. 

85 """ 

86 # Early copy of _finalize_return's gate — fail before the wasted remote forward. 

87 self._check_loss_supported(return_type) 

88 self._reject_stop_at_layer(kwargs.pop("stop_at_layer", None)) 

89 if isinstance(input, str): 

90 kwargs["input_ids"] = self.to_tokens(input) # BOS-aware, matches boot_transformers 

91 elif isinstance(input, list) and any(isinstance(item, str) for item in input): 

92 # Would otherwise be treated as raw input_ids and crash deep in numpy. 

93 raise TypeError( 

94 "RemoteBridge.forward received a list of strings; batched string " 

95 "input is unsupported here. Pass a single str, or tokenize with " 

96 "to_tokens() and pass token ids." 

97 ) 

98 elif input is not None: 98 ↛ 103line 98 didn't jump to line 103 because the condition on line 98 was always true

99 kwargs["input_ids"] = input 

100 

101 # Only request hooks with a registered handler, so a plain forward(tokens) 

102 # ships logits alone instead of the full residual decomposition every call. 

103 if "capture" not in kwargs: 103 ↛ 108line 103 didn't jump to line 108 because the condition on line 103 was always true

104 kwargs["capture"] = tuple( 

105 name for name, hp in self._hook_registry.items() if hp.fwd_hooks 

106 ) 

107 

108 result: ForwardResult = self._driver.forward(**kwargs) 

109 if result.captured: 

110 self._replay_captures(result.captured) 

111 

112 logits: Any = result.logits 

113 if logits is not None and not isinstance(logits, TensorLike): 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true

114 return logits # weird shape — let caller handle 

115 if logits is not None: 

116 logits = to_torch(logits) 

117 if labels is not None: 

118 if not isinstance(labels, TensorLike): 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true

119 raise TypeError(f"labels must be tensor-like, got {type(labels).__name__}") 

120 labels = to_torch(labels) 

121 

122 return self._finalize_return( 

123 return_type, 

124 logits, 

125 kwargs.get("input_ids"), 

126 attention_mask=kwargs.get("attention_mask"), 

127 labels=labels, 

128 is_audio_model=getattr(self.cfg, "is_audio_model", False), 

129 is_visual_model=getattr(self.cfg, "is_visual_model", False), 

130 loss_per_token=loss_per_token, 

131 ) 

132 

133 def run_with_hooks( 

134 self, 

135 input: Any, 

136 fwd_hooks: List[Tuple[Union[str, Callable], Callable]] = [], 

137 bwd_hooks: List[Tuple[Union[str, Callable], Callable]] = [], 

138 reset_hooks_end: bool = True, 

139 clear_contexts: bool = False, 

140 return_type: Optional[str] = "logits", 

141 stop_at_layer: Optional[int] = None, 

142 start_at_layer: Optional[int] = None, 

143 remove_batch_dim: bool = False, 

144 **kwargs: Any, 

145 ) -> Any: 

146 """Run with hooks. Remote fwd_hooks fire post-forward on captured 

147 activations (read-only) — they can't alter the computation, so warn; use 

148 ``intervene=`` specs to mutate. bwd_hooks are unsupported (no backward).""" 

149 if bwd_hooks: 

150 raise NotImplementedError( 

151 "RemoteBridge has no backward pass; bwd_hooks are unsupported." 

152 ) 

153 self._reject_stop_at_layer(stop_at_layer) 

154 self._reject_start_at_layer(start_at_layer) 

155 if fwd_hooks: 

156 warnings.warn( 

157 "RemoteBridge fwd_hooks fire on already-captured activations (read-only): " 

158 "a hook that returns a modified tensor does NOT change the forward " 

159 "computation or logits — the return is discarded. To intervene on the " 

160 "computation, pass intervene={hook_name: {'op': ...}} to " 

161 "forward()/run_with_cache().", 

162 UserWarning, 

163 stacklevel=2, 

164 ) 

165 return super().run_with_hooks( 

166 input, 

167 fwd_hooks=fwd_hooks, 

168 bwd_hooks=bwd_hooks, 

169 reset_hooks_end=reset_hooks_end, 

170 clear_contexts=clear_contexts, 

171 return_type=return_type, 

172 remove_batch_dim=remove_batch_dim, 

173 **kwargs, 

174 ) 

175 

176 @staticmethod 

177 def _reject_stop_at_layer(stop_at_layer: Any) -> None: 

178 # BridgeCore's stop hooks need a local `blocks` tree; silently ignoring 

179 # the kwarg would lie about compute cost. 

180 if stop_at_layer is not None: 

181 raise NotImplementedError( 

182 "RemoteBridge does not support stop_at_layer: the remote engine " 

183 "always runs the full forward pass." 

184 ) 

185 

186 @staticmethod 

187 def _reject_start_at_layer(start_at_layer: Any) -> None: 

188 # Residual-stream injection needs a local `blocks` tree; the remote engine 

189 # only runs a full forward from tokens. 

190 if start_at_layer is not None: 190 ↛ 191line 190 didn't jump to line 191 because the condition on line 190 was never true

191 raise NotImplementedError( 

192 "RemoteBridge does not support start_at_layer: the remote engine " 

193 "always runs the full forward pass from tokens." 

194 ) 

195 

196 def run_with_cache(self, *args: Any, **kwargs: Any) -> Any: 

197 """Cache via driver captures; stop/start_at_layer and incl_bwd rejected.""" 

198 self._reject_stop_at_layer(kwargs.get("stop_at_layer")) 

199 self._reject_start_at_layer(kwargs.get("start_at_layer")) 

200 if kwargs.get("incl_bwd"): 200 ↛ 203line 200 didn't jump to line 203 because the condition on line 200 was never true

201 # Captures come back as detached tensors from the remote engine, so there 

202 # is no local graph for backward() to walk. 

203 raise NotImplementedError( 

204 "RemoteBridge has no backward pass; run_with_cache(incl_bwd=True) is " 

205 "unsupported." 

206 ) 

207 return super().run_with_cache(*args, **kwargs) 

208 

209 def to_tokens(self, input: Any, prepend_bos: bool | None = None, truncate: bool = True) -> Any: 

210 """Tokenize a string with the same BOS handling as ``TransformerBridge``. 

211 

212 Mirrors ``cfg.default_prepend_bos`` / ``tokenizer_prepends_bos`` so 

213 ``boot_inspect(m).run_with_cache("text")`` matches ``boot_transformers(m)`` 

214 on the same string — a bare ``encode`` (no BOS) would silently diverge. 

215 """ 

216 from transformer_lens import utilities as utils 

217 

218 assert self.tokenizer is not None, "Tokenizer must be set." 

219 if prepend_bos is None: 219 ↛ 221line 219 didn't jump to line 221 because the condition on line 219 was always true

220 prepend_bos = getattr(self.cfg, "default_prepend_bos", True) 

221 tokenizer_prepends_bos = getattr(self.cfg, "tokenizer_prepends_bos", True) 

222 if prepend_bos and not tokenizer_prepends_bos: 222 ↛ 223line 222 didn't jump to line 223 because the condition on line 222 was never true

223 bos = self.tokenizer.bos_token 

224 encodes_atomically = ( 

225 bos is not None 

226 and len(self.tokenizer(bos, add_special_tokens=False)["input_ids"]) == 1 

227 ) 

228 if encodes_atomically: 

229 input = utils.get_input_with_manually_prepended_bos(bos, input) 

230 # else: the fallback BOS is not an atom in this vocab (e.g. 

231 # '<|endoftext|>' installed on BERT); prepending the string would 

232 # tokenize to subword garbage, so skip rather than pollute the input. 

233 if isinstance(input, str): 233 ↛ 237line 233 didn't jump to line 237 because the condition on line 233 was always true

234 input = [input] 

235 # A single sequence never needs padding; only pad when batching (which also 

236 # avoids requiring a pad_token on tokenizers that lack one, e.g. raw gpt2). 

237 tokens = self.tokenizer( 

238 input, 

239 return_tensors="pt", 

240 padding=len(input) > 1, 

241 truncation=truncate, 

242 max_length=self.cfg.n_ctx if truncate else None, 

243 )["input_ids"] 

244 if not prepend_bos and tokenizer_prepends_bos: 244 ↛ 245line 244 didn't jump to line 245 because the condition on line 244 was never true

245 tokens = utils.get_tokens_with_bos_removed(self.tokenizer, tokens) 

246 return tokens 

247 

248 def __enter__(self) -> "RemoteBridge": 

249 """Use as a context manager so the engine is released on exit: 

250 ``with RemoteBridge.boot_vllm(...) as bridge: ...``.""" 

251 return self 

252 

253 def __exit__(self, *exc: Any) -> None: 

254 self.close() 

255 

256 def __del__(self) -> None: 

257 # Best-effort safety net for notebooks that drop the bridge without 

258 # close() — repeated boot_vllm would otherwise OOM. close() is idempotent. 

259 try: 

260 self.close() 

261 except Exception: 

262 pass