Coverage for transformer_lens/model_bridge/sources/vllm/intervention_specs.py: 94%
43 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
1"""Intervention op vocabulary + spec validation shared by every producer path
2(VLLMDriver, the worker RPCs — which also cover the Inspect vLLM provider).
4Adding a new op requires (a) listing it here, (b) allowing its keys in
5``validate_spec``, (c) handling it in ``worker_extension._apply_intervention``.
6"""
7from __future__ import annotations
9from typing import Any, Mapping
11import torch
13SUPPORTED_OPS = frozenset({"suppress", "scale", "add", "set"})
16def validate_spec(hook_name: str, spec: Any, *, width: int | None = None) -> dict:
17 """Validate one declarative intervention spec; returns a plain-dict copy.
19 ``width`` enables the value-shape check where the caller knows the hook width.
20 """
21 if callable(spec):
22 raise NotImplementedError(
23 "vLLM accepts intervention specs (dict), not callables. "
24 "Supported ops: suppress, scale (factor: float), add (value: scalar or "
25 "width-shaped), set (value: scalar or width-shaped)."
26 )
27 if not isinstance(spec, Mapping) or "op" not in spec:
28 raise ValueError(
29 f"Intervention spec for {hook_name!r} must be a dict with 'op' key; got {spec!r}"
30 )
31 op = spec["op"]
32 if op not in SUPPORTED_OPS:
33 raise ValueError(
34 f"Unsupported intervention op {op!r} for {hook_name!r}. "
35 f"Supported: {sorted(SUPPORTED_OPS)}."
36 )
37 if op == "scale" and "factor" not in spec:
38 raise ValueError(f"Intervention {hook_name!r}: op='scale' requires 'factor' (float).")
39 if op in ("add", "set") and "value" not in spec:
40 raise ValueError(
41 f"Intervention {hook_name!r}: op={op!r} requires 'value' "
42 "(scalar or width-shaped tensor/list)."
43 )
44 # Unknown keys must fail loud: a typo'd "position"/"positions" would otherwise
45 # silently become a whole-sequence edit.
46 allowed = {"op", "pos"}
47 if op == "scale": 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true
48 allowed.add("factor")
49 if op in ("add", "set"):
50 allowed.add("value")
51 extra = set(spec) - allowed
52 if extra:
53 raise ValueError(
54 f"Intervention {hook_name!r}: unknown spec key(s) {sorted(extra)}; "
55 f"allowed for op={op!r}: {sorted(allowed)}."
56 )
57 value = spec.get("value")
58 if value is not None and not isinstance(value, (int, float)):
59 try:
60 n_elements = int(torch.as_tensor(value).numel())
61 except (TypeError, ValueError, RuntimeError) as exc:
62 raise ValueError(
63 f"Intervention {hook_name!r}: 'value' must be a scalar or a "
64 f"width-shaped tensor/list; got {type(value).__name__}."
65 ) from exc
66 if width is not None and n_elements != width:
67 # A mis-shaped value would otherwise surface as a broadcast error
68 # mid-forward — or broadcast along the wrong axis in square cases.
69 raise ValueError(
70 f"Intervention {hook_name!r}: 'value' has {n_elements} elements "
71 f"but the hook width is {width}."
72 )
73 pos = spec.get("pos")
74 if pos is not None:
75 valid_pos = isinstance(pos, int) and not isinstance(pos, bool)
76 if not valid_pos and isinstance(pos, (list, tuple)):
77 valid_pos = all(isinstance(p, int) and not isinstance(p, bool) for p in pos)
78 if not valid_pos:
79 raise ValueError(
80 f"Intervention {hook_name!r}: 'pos' must be an int or list of ints "
81 f"(sequence positions to patch); got {pos!r}."
82 )
83 negative = [p for p in ([pos] if isinstance(pos, int) else pos) if p < 0]
84 if negative:
85 raise ValueError(
86 f"Intervention {hook_name!r}: 'pos' must be non-negative; got {negative}."
87 )
88 return dict(spec)