Coverage for transformer_lens/model_bridge/sources/vllm/source.py: 99%

90 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-10-10 05:10 +0000

1"""``boot_vllm`` — construct a vLLM LLM, wrap it in a RemoteBridge via VLLMDriver.""" 

2from __future__ import annotations 

3 

4import logging 

5import os 

6import threading 

7import warnings 

8from typing import Any, Dict, Optional 

9 

10import torch 

11 

12# Module import (not a name import): architecture_adapter_factory imports model_bridge, 

13# which imports this module, so its names may not be defined yet at this point. 

14from transformer_lens.factories import architecture_adapter_factory 

15from transformer_lens.model_bridge.remote_bridge import RemoteBridge 

16from transformer_lens.model_bridge.sources._bridge_builder import ( 

17 build_bridge_config_from_hf, 

18 configure_tokenizer, 

19 skip_tokenizer_for_modality, 

20) 

21from transformer_lens.model_bridge.sources._hf_format import ( 

22 determine_architecture_from_hf_config, 

23) 

24from transformer_lens.utilities.hf_utils import get_hf_token 

25 

26from . import plugin 

27from .driver import VLLMDriver 

28from .internals import extract_hf_config, verify_hook_coverage 

29from .overlays import get_overlay 

30from .worker_extension import dtype_name 

31 

32# Forced LLM(...) kwargs that the capture-hook design depends on. Caller override → ValueError. 

33# enable_prefix_caching MUST stay off: a prefix-cache hit makes the prefill compute only the 

34# uncached suffix, so hooks fire for a subset of positions — captures land row-misaligned, 

35# interventions skip cached positions, and an intervened forward writes poisoned K/V that a 

36# later clean forward on the same prompt would silently reuse. 

37_LOCKED_KWARGS = { 

38 "skip_tokenizer_init": True, 

39 "disable_log_stats": True, 

40 "enable_prefix_caching": False, 

41} 

42 

43# Serializes the configure() → LLM(...) → clear() handoff: the spec channel is a 

44# process-wide env var, so interleaved boots would cross-wire capture specs between engines. 

45_BOOT_LOCK = threading.Lock() 

46 

47_WORKER_EXTENSION_CLS = ( 

48 "transformer_lens.model_bridge.sources.vllm.worker_extension.TLWorkerExtension" 

49) 

50 

51 

52def construct_instrumented_llm( 

53 model_name: str, 

54 *, 

55 capture_specs: Dict[str, Any], 

56 max_num_batched_tokens: int, 

57 dtype: torch.dtype, 

58 enable_batching: bool = False, 

59 enable_position_interventions: bool = False, 

60 tensor_parallel_size: int = 1, 

61 pipeline_parallel_size: int = 1, 

62 llm_kwargs: Dict[str, Any], 

63) -> Any: 

64 """The one place a TL-instrumented ``vllm.LLM`` is constructed — shared by 

65 ``boot_vllm`` and the Inspect vLLM provider so the capture contract can't drift 

66 between them. Owns everything hook correctness depends on: 

67 

68 - ``VLLM_DISABLE_COMPILE_CACHE=1``: our hooks are traced INTO vLLM's compiled 

69 graph, but its compile cache is keyed only on its own config — a cached 

70 artifact from a differently-instrumented process either crashes at AOT load 

71 (bytecode binds the hook closures) or silently serves a hookless graph. 

72 - ``VLLM_ENABLE_V1_MULTIPROCESSING``: forced ``"0"`` single-rank (in-process 

73 worker, the historical GPU-validated path); parallel boots spawn workers, 

74 which read the specs via the plugin's env channel — and must not inherit a 

75 stale ``"0"`` that would force the uni-process executor. 

76 - ``enable_prefix_caching=False``: a prefix-cache hit computes only the uncached 

77 suffix, so captures land row-misaligned, interventions skip cached positions, 

78 and an intervened forward writes poisoned K/V a later clean forward reuses. 

79 - configure → register → ``LLM(...)`` → clear under ``_BOOT_LOCK``, then a 

80 hook-coverage check so a spec that landed on no rank fails here, not as zeros. 

81 

82 ``llm_kwargs`` carries the caller's remaining ``LLM(...)`` arguments; restating a 

83 contract kwarg is allowed only at the same value (callers validate overrides). 

84 """ 

85 from vllm import LLM 

86 

87 os.environ["VLLM_DISABLE_COMPILE_CACHE"] = "1" 

88 if tensor_parallel_size == 1 and pipeline_parallel_size == 1: 

89 existing_mp = os.environ.get("VLLM_ENABLE_V1_MULTIPROCESSING") 

90 if existing_mp not in (None, "0"): 

91 warnings.warn( 

92 f"VLLM_ENABLE_V1_MULTIPROCESSING={existing_mp!r} overridden to '0' — " 

93 "single-rank TL boots keep the worker in-process.", 

94 UserWarning, 

95 stacklevel=3, 

96 ) 

97 os.environ["VLLM_ENABLE_V1_MULTIPROCESSING"] = "0" 

98 elif os.environ.get("VLLM_ENABLE_V1_MULTIPROCESSING") == "0": 

99 os.environ.pop("VLLM_ENABLE_V1_MULTIPROCESSING") 

100 

101 with _BOOT_LOCK: 

102 plugin.configure( 

103 capture_specs=capture_specs, 

104 max_num_batched_tokens=max_num_batched_tokens, 

105 dtype=dtype, 

106 enable_batching=enable_batching, 

107 enable_position_interventions=enable_position_interventions, 

108 ) 

109 plugin.register() 

110 try: 

111 llm = LLM( 

112 **{ 

113 "model": model_name, 

114 # vLLM defaults this to 8192 for chunked prefill; the capture buffers 

115 # are sized to it, so the compiled dynamic-shape range must match — 

116 # otherwise Dynamo's symbolic hint exceeds the buffer dim at compile. 

117 "max_num_batched_tokens": max_num_batched_tokens, 

118 # Mixes tl_* methods into the Worker for collective_rpc (multiple 

119 # inheritance; the tl_ prefix avoids attribute collisions). 

120 "worker_extension_cls": _WORKER_EXTENSION_CLS, 

121 "enable_prefix_caching": False, 

122 "tensor_parallel_size": tensor_parallel_size, 

123 "pipeline_parallel_size": pipeline_parallel_size, 

124 **llm_kwargs, 

125 } 

126 ) 

127 finally: 

128 # Always clear, even on a failed boot: stale specs would make the next 

129 # in-process vllm.LLM(...) walk our dot-paths on a foreign model. 

130 plugin.clear_config() 

131 

132 # Hook installation skips modules absent on a rank (PP shards); a spec that 

133 # landed on NO rank is a broken dot-path and must fail here, not read zeros. 

134 verify_hook_coverage(llm) 

135 return llm 

136 

137 

138def boot_vllm( 

139 model_name: str, 

140 tokenizer: Optional[Any] = None, 

141 dtype: Optional[torch.dtype] = None, 

142 gpu_memory_utilization: float = 0.5, 

143 max_model_len: Optional[int] = None, 

144 max_num_batched_tokens: int = 2048, 

145 enable_batching: bool = False, 

146 enable_position_interventions: bool = False, 

147 tensor_parallel_size: int = 1, 

148 pipeline_parallel_size: int = 1, 

149 **vllm_kwargs: Any, 

150) -> RemoteBridge: 

151 """Boot a model via vLLM and wrap it in a :class:`RemoteBridge` via :class:`VLLMDriver`. 

152 

153 vLLM drives the forward pass (PagedAttention + ``torch.compile`` + CUDA graphs). 

154 Capture buffers are populated by hooks the plugin installs pre-compile inside 

155 the worker; they come back via ``collective_rpc`` and replay through the 

156 bridge's HookPoint tree. 

157 

158 **Scope vs vllm-lens:** vllm-lens is observation-only. This source extends to 

159 observation + spec-vocabulary *mutation* — each capture hook also applies an 

160 affine transform ``output = output * scale + bias`` (default identity), so 

161 interventions (``suppress`` / ``scale`` / ``add`` / ``set``) propagate to 

162 downstream layers. The hook's return value replaces the module output per 

163 PyTorch ``register_forward_hook`` semantics. The mutation path under 

164 torch.compile + CUDA graphs is exercised end-to-end by 

165 ``demos/vLLM_Bridge_Integration_Test.ipynb`` (a manual GPU run, not CI); 

166 unit tests cover the dispatch protocol only. 

167 

168 Some captures use vLLM-native conventions that differ from HF/HT; see 

169 :mod:`transformer_lens.model_bridge.sources.vllm.overlays.decoder_only` for 

170 which hooks diverge and the conversion to apply for HT-equivalent values. 

171 

172 **Returned logits are reconstructed full-sequence logits.** vLLM's sampler 

173 bypasses ``lm_head``, so the driver rebuilds real logits host-side as 

174 ``ln_final @ lm_head.weight.T`` (+ bias), followed by the engine's softcap and scale, 

175 from the captured 

176 final-norm activation — valid at every position, so ``return_type`` in 

177 ``{"loss", "both"}`` works. If the unembedding weight is unreachable the 

178 driver falls back to the sampler's final-position log-probs (earlier 

179 positions ``-inf``), declares ``provides_sequence_logits=False``, and the 

180 bridge then rejects loss. 

181 

182 GPU memory cost: each capture buffer is ``max_num_batched_tokens × width`` at 

183 the model's dtype. For Llama-3.2-1B at fp16 with ``max_num_batched_tokens=2048``, 

184 the unembed buffer alone is ~525 MB (2048 × 128256 × 2 bytes); residual-stream 

185 buffers add ~8 MB per hook. The affine intervention hook also allocates a 

186 transient output-shape tensor per forward (even in identity mode), so peak 

187 forward memory is ~1.5× the capture buffers' resident size. 

188 

189 KV-cache footprint: vLLM reserves KV cache sized for ``max_model_len`` × layers 

190 × heads × head_dim. If ``max_model_len`` is left as ``None``, vLLM uses the 

191 model's native context (e.g. 131072 for Llama-3.2-1B) — easily 4+ GiB even 

192 on a 1B model. Pass an explicit ``max_model_len`` (e.g. ``2048`` for typical 

193 mech-interp prompts) to keep the budget on smaller GPUs. 

194 

195 ``enable_batching`` switches to the eager batched path (``enforce_eager``, 

196 ``batch_size > 1``) — the throughput path for SAE/probe data collection. 

197 Default ``False`` keeps the compile-validated single-prompt path. Batched 

198 caches are right-padded with zeros to the longest sequence. 

199 

200 ``enable_position_interventions`` widens each hook's affine scale/bias buffers 

201 from ``(width,)`` to ``(max_num_batched_tokens, width)`` so an intervention spec 

202 can carry a ``pos`` field (int or list[int]) that scopes the edit to specific 

203 sequence positions — position-scoped activation patching / tensor injection. 

204 Costs ~2× extra resident GPU memory across all hooks (the scale and bias buffers 

205 join the already-``(max_n, width)`` capture buffer), so it is opt-in and defaults 

206 ``False``. Compiled-path only — incompatible with ``enable_batching``. 

207 

208 ``tensor_parallel_size`` > 1 enables single-node tensor parallelism. Every 

209 capture point the overlay hooks is post-all-reduce and therefore replicated 

210 across a stage's TP ranks; ``pipeline_parallel_size`` > 1 shards layers across 

211 stages, each owning the hooks that actually fire on it. 

212 Capture reads merge across ranks with a first-forward layout check that fails 

213 loud on installation drift or non-replicated hook points; sharded/stage-local 

214 weights (``lm_head``, final norm) are gathered for logit reconstruction and the 

215 ln_final un-fold. Single-node only (the spec channel is an env var, which never 

216 reaches Ray remote workers); both are incompatible with ``enable_batching`` 

217 (per-rank chunk boundaries are unvalidated). 

218 """ 

219 if enable_position_interventions and enable_batching: 

220 raise ValueError( 

221 "enable_position_interventions requires the compiled path and is incompatible " 

222 "with enable_batching=True (the batched/eager path has no affine buffers)." 

223 ) 

224 for kwarg_name, size in ( 

225 ("tensor_parallel_size", tensor_parallel_size), 

226 ("pipeline_parallel_size", pipeline_parallel_size), 

227 ): 

228 if not isinstance(size, int) or size < 1: 

229 raise ValueError(f"{kwarg_name} must be a positive int; got {size!r}.") 

230 if (tensor_parallel_size > 1 or pipeline_parallel_size > 1) and enable_batching: 

231 raise ValueError( 

232 "enable_batching=True with tensor/pipeline parallelism is unsupported: the " 

233 "eager batched path's per-rank chunk boundaries are unvalidated. " 

234 "Use the compiled single-prompt path." 

235 ) 

236 _reject_locked_overrides(vllm_kwargs) 

237 # Import-check first: fail with an actionable message before any network I/O 

238 # or plugin state mutation. 

239 try: 

240 from vllm import LLM 

241 except ImportError as exc: 

242 raise ImportError( 

243 "boot_vllm requires vLLM (Linux + CUDA). Install with " 

244 'pip install "transformer-lens[vllm]" or uv sync --extra vllm; ' 

245 "the driver is validated against vllm 0.20.x." 

246 ) from exc 

247 

248 from transformers import AutoConfig, AutoTokenizer 

249 

250 # Resolve architecture WITHOUT loading weights so we can tell the plugin 

251 # which dot-paths to hook before LLM(...) constructs the worker. Shared 

252 # resolution (not architectures[0]) handles architectures=None configs via 

253 # model_type and rejects unsupported archs before the expensive engine boot. 

254 hf_token = get_hf_token() 

255 hf_config_preview = AutoConfig.from_pretrained(model_name, token=hf_token) 

256 architecture = determine_architecture_from_hf_config(hf_config_preview) 

257 overlay = get_overlay(architecture) 

258 

259 resolved_dtype = dtype or _dtype_from_hf_config(hf_config_preview) 

260 

261 # Batched capture reads query_start_loc from the forward context, untraceable 

262 # under torch.compile — so the batched path must run eager. 

263 eager_kwargs: Dict[str, Any] = {"enforce_eager": True} if enable_batching else {} 

264 

265 llm = construct_instrumented_llm( 

266 model_name, 

267 capture_specs=overlay.capture_specs(hf_config_preview), 

268 max_num_batched_tokens=max_num_batched_tokens, 

269 dtype=resolved_dtype, 

270 enable_batching=enable_batching, 

271 enable_position_interventions=enable_position_interventions, 

272 tensor_parallel_size=tensor_parallel_size, 

273 pipeline_parallel_size=pipeline_parallel_size, 

274 llm_kwargs={ 

275 "gpu_memory_utilization": gpu_memory_utilization, 

276 "max_model_len": max_model_len, 

277 # Allow full-vocab logprobs so the fallback path can synthesize logits when 

278 # host-side reconstruction is unavailable (vLLM caps logprobs to this value; 

279 # default 20 is too small for mech-interp). 

280 "max_logprobs": hf_config_preview.vocab_size, 

281 # Always explicit — vLLM's "auto" downcasts fp32 checkpoints to fp16, which 

282 # would leave the capture/affine buffers (allocated at resolved_dtype) at a 

283 # different dtype than the engine's activations. 

284 "dtype": dtype_name(resolved_dtype), 

285 **eager_kwargs, 

286 **_LOCKED_KWARGS, 

287 **vllm_kwargs, 

288 }, 

289 ) 

290 hf_config = extract_hf_config(llm) 

291 

292 # Build the adapter (RemoteBridge skips adapter.prepare_model — there's no 

293 # local model tree to walk). Use the shared HF→TL config builder so 

294 # bridge_config is a real TransformerBridgeConfig with all dataclass 

295 # defaults (d_vocab_out=-1, etc.) — not a deep-copied HF config with 

296 # extra fields, which is missing the TL-only attributes BridgeCore reads. 

297 bridge_config = build_bridge_config_from_hf(hf_config, architecture, model_name, resolved_dtype) 

298 adapter = architecture_adapter_factory.ArchitectureAdapterFactory.select_architecture_adapter( 

299 bridge_config 

300 ) 

301 

302 if tokenizer is None and not skip_tokenizer_for_modality(adapter.cfg): 

303 tokenizer = AutoTokenizer.from_pretrained(model_name, token=hf_token) 

304 if tokenizer is not None: 

305 # Match boot_transformers' tokenizer setup so to_tokens(str) is token-identical. 

306 tokenizer = configure_tokenizer(tokenizer, adapter.cfg) 

307 

308 driver = VLLMDriver( 

309 llm=llm, 

310 adapter=adapter, 

311 tokenizer=tokenizer, 

312 overlay=overlay, 

313 hf_config=hf_config, 

314 max_num_batched_tokens=max_num_batched_tokens, 

315 enable_batching=enable_batching, 

316 enable_position_interventions=enable_position_interventions, 

317 tensor_parallel_size=tensor_parallel_size, 

318 pipeline_parallel_size=pipeline_parallel_size, 

319 ) 

320 # One-time unembedding fetch: caches the weight for per-forward reconstruction and 

321 # downgrades provides_sequence_logits honestly if no unembedding is reachable. 

322 driver.probe_logit_reconstruction() 

323 bridge = RemoteBridge(adapter=adapter, tokenizer=tokenizer, driver=driver) 

324 _log_hook_summary(model_name, architecture, driver) 

325 return bridge 

326 

327 

328def _reject_locked_overrides(vllm_kwargs: Dict[str, Any]) -> None: 

329 for key, locked in _LOCKED_KWARGS.items(): 

330 if key in vllm_kwargs and vllm_kwargs[key] != locked: 

331 raise ValueError( 

332 f"boot_vllm forces {key}={locked}; caller passed {key}={vllm_kwargs[key]}. " 

333 "Prefix caching, continuous batching, and vLLM-owned tokenizers are " 

334 "unsupported — each breaks the capture-read invariants. (TP/PP are " 

335 "supported via the tensor_parallel_size/pipeline_parallel_size kwargs.)" 

336 ) 

337 

338 

339def _dtype_from_hf_config(hf_config: Any) -> torch.dtype: 

340 raw = getattr(hf_config, "torch_dtype", None) 

341 if isinstance(raw, torch.dtype): 

342 return raw 

343 if isinstance(raw, str): 

344 return getattr(torch, raw, torch.float16) 

345 return torch.float16 

346 

347 

348def _log_hook_summary(model_name: str, architecture: str, driver: VLLMDriver) -> None: 

349 """Log the fireable and non-fireable hook sets so users don't have to grep.""" 

350 log = logging.getLogger("transformer_lens.vllm") 

351 fireable = sorted(driver.supported_hook_points) 

352 log.info( 

353 "vLLM source on %s (%s) captures %d hook(s): %s", 

354 model_name, 

355 architecture, 

356 len(fireable), 

357 ", ".join(fireable), 

358 ) 

359 nonfiring = sorted(driver.non_fireable_hook_points) 

360 if nonfiring: 360 ↛ exitline 360 didn't return from function '_log_hook_summary' because the condition on line 360 was always true

361 log.info( 

362 "vLLM source on %s (%s) cannot fire %d hook(s) (vLLM fuses these): %s. " 

363 "Use boot_transformers() if you need them.", 

364 model_name, 

365 architecture, 

366 len(nonfiring), 

367 ", ".join(nonfiring), 

368 ) 

369 

370 

371__all__ = ["boot_vllm", "construct_instrumented_llm"]