hotwire-vllm 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ unit:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - name: Install (CPU torch + triton)
17
+ run: |
18
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
19
+ pip install triton pytest
20
+ pip install -e .
21
+ - name: Unit tests (GPU + integration tests auto-skip)
22
+ run: pytest -q
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .pytest_cache/
4
+ .venv/
5
+ dist/
6
+ notes/
7
+
8
+ virtualenvs/
9
+ .idea/
10
+ dist/
11
+ *.egg-info/
@@ -0,0 +1,42 @@
1
+ # Using hotwire-vllm (guide for coding agents)
2
+
3
+ You are helping someone add per-request activation steering to a vLLM server
4
+ with `hotwire-vllm`. It is a vLLM plugin — small surface, but the details
5
+ below are the ones agents get wrong.
6
+
7
+ ## Install and load
8
+
9
+ pip install hotwire-vllm
10
+
11
+ It auto-registers via the `vllm.general_plugins` entry point when vLLM
12
+ starts — nothing to import in user code. If steering silently does nothing,
13
+ the plugin did not load: confirm vLLM sees the entry point.
14
+
15
+ ## Steer one request
16
+
17
+ Steering rides on each request as a `vllm_xargs` field. The `hotwire` value is
18
+ a JSON **string** (stringified spec), not a nested object:
19
+
20
+ {"messages": [...],
21
+ "vllm_xargs": {"hotwire": "{\"id\": \"NAME\", \"layer\": 20, \"scale\": 1.5}"}}
22
+
23
+ Multiple layers: pass a JSON list of such specs. Add `"decode_only": true` to
24
+ steer only generated tokens.
25
+
26
+ ## The gotchas that actually bite
27
+
28
+ - **Slots are a fixed budget.** Each distinct steering config lives in a
29
+ fixed-size GPU slot bank; distinct (layer, scale) combos each consume one
30
+ persistent slot. Don't sweep hundreds of combos on a live server — you will
31
+ exhaust the bank. Reuse configs; size the sweep to the slot budget.
32
+ - **Vectors must be registered** with the server (wire format is JSON /
33
+ safetensors, no pickle). An unknown id degrades to *unsteered*, never a
34
+ failed request — so "no effect" usually means "id not registered" or
35
+ "plugin not loaded", not "steering broke".
36
+ - **Malformed specs degrade to unsteered**, silently. Validate your spec JSON.
37
+ - Unsteered requests (including batchmates of steered ones) are untouched, and
38
+ idle steering is zero-cost — CUDA graphs stay intact.
39
+
40
+ Calibrate the vector first with
41
+ [hidden-directions](https://github.com/moudrkat/hidden-directions) (it produces
42
+ the `{id, layer, scale, decode_only}` spec you paste here).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Katerina Fajmanova
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: hotwire-vllm
3
+ Version: 0.0.2
4
+ Summary: CUDA-graph-safe activation steering plugin for vLLM
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: numpy
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # hotwire
14
+
15
+ [![PyPI](https://img.shields.io/pypi/v/hotwire-vllm)](https://pypi.org/project/hotwire-vllm/)
16
+
17
+ ```
18
+ pip install hotwire-vllm
19
+ ```
20
+
21
+ **Activation steering for vLLM that doesn't turn off the engine.**
22
+
23
+ For teams running vLLM in production who need per-request activation
24
+ steering at native speed — CUDA graphs and torch.compile intact.
25
+
26
+ Every existing steering tool for vLLM ([vllm-lens](https://github.com/UKGovernmentBEIS/vllm-lens),
27
+ [EasySteer](https://arxiv.org/abs/2509.25175), IBM's vLLM Hook) forces
28
+ `enforce_eager=True`: PyTorch forward hooks don't survive CUDA graph capture, so
29
+ they disable CUDA graphs and torch.compile for the whole server — every request
30
+ pays, steered or not. Fine for research, a non-starter for production.
31
+
32
+ hotwire keeps the graphs. The steering addition is a custom torch op (Triton
33
+ kernel) that gets baked *into* the captured graph; per-request routing happens
34
+ by updating the contents of persistent GPU buffers between graph replays —
35
+ the graph reads fresh data at the same addresses.
36
+
37
+ The technique was proven viable in [RhizoNymph's vLLM fork](https://github.com/RhizoNymph/vllm)
38
+ (see [RFC #36998](https://github.com/vllm-project/vllm/issues/36998), where
39
+ in-flight steering is explicitly deferred to "Phase 2"). hotwire packages it as
40
+ an out-of-tree plugin: `pip install`, no fork, registered via vLLM's official
41
+ `general_plugins` entry point.
42
+
43
+ ## ⚡ Run in 30 s
44
+
45
+ ```bash
46
+ pip install -e . # registers the vllm.general_plugins entry point
47
+ export HOTWIRE_VECTORS=/path/to/vectors # dir of .pt files, (n_layers, hidden) each
48
+ vllm serve Qwen/Qwen3-4B-Instruct-2507 # CUDA graphs stay ON
49
+ ```
50
+
51
+ Steer any request by id + layer + scale:
52
+
53
+ ```python
54
+ # offline
55
+ SamplingParams(extra_args={"hotwire": '{"id": "tesla_car", "layer": 20, "scale": 1.5}'})
56
+ ```
57
+
58
+ Optional per-entry flag `"decode_only": true` steers generated tokens only,
59
+ never the prompt — use it for vectors calibrated on generation-only steering
60
+ (research rigs typically don't steer the prefill; applying such a vector to a
61
+ long prompt as well multiplies the effective dose and can wreck coherence).
62
+
63
+ Lab twin: [brainscope](https://github.com/moudrkat/brainscope) accepts this
64
+ exact spec and wire format — calibrate a vector under its lenses (its
65
+ `export_hotwire` ships `.pt` files with a regime passport), deploy it here
66
+ unchanged, and replay production conversations back under the lens when a
67
+ vector misbehaves.
68
+
69
+ ```bash
70
+ # OpenAI API
71
+ curl .../v1/chat/completions -d '{..., "vllm_xargs":
72
+ {"hotwire": "{\"id\": \"tesla_car\", \"layer\": 20, \"scale\": 1.5}"}}'
73
+ ```
74
+
75
+ Unsteered requests — including batchmates of steered ones — are untouched.
76
+ Malformed specs and unknown vector ids degrade to "unsteered", never to a
77
+ failed request.
78
+
79
+ ## Design
80
+
81
+ Three persistent GPU tensors, allocated at model-load time:
82
+
83
+ | buffer | shape | role |
84
+ |---|---|---|
85
+ | `bank` | `(n_slots, hidden)` | steering vectors, one per active slot |
86
+ | `scales` | `(n_slots,)` | per-slot multiplier |
87
+ | `slot_map` | `(n_layers, max_tokens)` | token → slot per layer, `-1` = untouched |
88
+
89
+ The op, called at the end of each decoder layer's forward:
90
+
91
+ ```
92
+ hidden[tok] += scales[slot] * bank[slot] where slot = slot_map[layer, tok] >= 0
93
+ ```
94
+
95
+ - **Graph-safe:** the op is `torch.library.custom_op` with a fake impl —
96
+ opaque to torch.compile, captured into CUDA graphs as a fixed kernel on
97
+ fixed addresses. Steering on/off/vector changes are buffer *content*
98
+ updates between replays (host-side copy), never a re-capture.
99
+ - **Per-request:** a pre-forward hook reads `forward_context`
100
+ (`query_start_loc` + `req_ids`, same bookkeeping vllm-lens validated)
101
+ and fills `slot_map` for the step.
102
+ - **No pickle:** vectors enter as safetensors files or base64 JSON via a
103
+ registration endpoint (`POST /steer/vectors`), requests reference them by
104
+ id + scale in `vllm_xargs`. Nothing executable crosses the wire.
105
+ - **Zero cost when idle:** `slot_map` all `-1` → kernel early-exits per token.
106
+ (Benchmark target: unmeasurable vs baseline; RhizoNymph reported minimal
107
+ overhead on H100.)
108
+
109
+ ## Layout
110
+
111
+ - `hotwire/_kernel.py` — Triton kernel + `hotwire::steer` custom op (working)
112
+ - `hotwire/_bank.py` — slot allocation, vector registration (working)
113
+ - `hotwire/_patch.py` — decoder-layer wrapping + pre-forward slot fill (WIP:
114
+ integration points against vLLM 0.25.x)
115
+ - `hotwire/wire.py` — JSON/safetensors vector wire format, no pickle (working)
116
+
117
+ ## Verify on your hardware
118
+
119
+ Two commands, ~3 minutes on any CUDA box with vLLM installed:
120
+
121
+ ```bash
122
+ pip install git+https://github.com/moudrkat/hotwire-vllm
123
+ python -m hotwire.verify --model Qwen/Qwen3-0.6B # any HF model id works
124
+ ```
125
+
126
+ It generates a throwaway steering vector for the model, checks that steering
127
+ fires, that unsteered requests (including batchmates) are untouched, and
128
+ compares decode cost idle vs all-steered — then prints a report block.
129
+ **Please paste the report into an issue**, especially from hardware, model
130
+ families, or configs (TP > 1, 7B+, H100s) the tables below don't cover yet —
131
+ that's currently the most useful contribution this project can receive.
132
+
133
+ ## Status
134
+
135
+ Working end-to-end on vLLM 0.25.1, **both model runners** (the classic
136
+ `GPUModelRunner` and the new V2 runner that 0.25.1 selects by default for
137
+ dense generate models), with CUDA graphs captured (PIECEWISE + FULL) and
138
+ torch.compile on. Verified on Qwen3-0.6B / Qwen3-4B on a single 16 GB GPU:
139
+ solo steering, mixed batches, decode-phase graph replays.
140
+
141
+ hotwire also salts vLLM's torch.compile/AOT cache key (`VllmConfig.compute_hash`)
142
+ — the op is traced into the compiled model, and vLLM's cache doesn't know about
143
+ plugins, so without the salt a stale cache silently serves a model with no
144
+ steering op in it.
145
+
146
+ Tests: `pytest` (unit, CPU-safe), `pytest -m integration` (real engine, GPU).
147
+
148
+ Verified architectures (chaos-vector A/B + batchmate-isolation check, both
149
+ model runners exercised):
150
+
151
+ | model | steering works | unsteered untouched | TPOT idle → all-steered |
152
+ |---|---|---|---|
153
+ | Qwen3-14B-AWQ (4-bit) | ✓ | ✓ | 1.91 → 1.91 ms/tok |
154
+ | Llama-3.1-8B-Instruct-AWQ | ✓ | ✓ | 1.10 → 1.10 ms/tok |
155
+ | Qwen3-8B-FP8 | ✓ | ✓ | 2.27 → 2.28 ms/tok |
156
+ | Mistral-7B-Instruct-v0.2-AWQ | ✓ | ✓ | 0.94 → 0.94 ms/tok |
157
+ | Qwen3-4B-Instruct-2507 | ✓ | ✓ | 1.78 → 1.78 ms/tok |
158
+ | Qwen3-0.6B | ✓ | ✓ | — |
159
+ | Qwen2.5-1.5B-Instruct | ✓ | ✓ | 0.77 → 0.77 ms/tok |
160
+ | Phi-3.5-mini-instruct | ✓ | ✓ | 1.73 → 1.73 ms/tok |
161
+ | tiny-aya-water (Cohere) | ✓ | ✓ | 1.54 → 1.54 ms/tok |
162
+
163
+ Quantized checkpoints work — steering touches the residual stream, not the
164
+ weights, and the AWQ / FP8 rows above confirm it end-to-end, CUDA graphs
165
+ captured (PIECEWISE + FULL).
166
+
167
+ Models that still OOM on the 16 GB test GPU before the plugin engages:
168
+ OLMo-2-7B, command-r7b, Qwen3.5-4B, gpt-oss-20b (13.8 GiB weight load
169
+ succeeds, engine init doesn't), gemma-4-E4B-it (vision tower). No
170
+ architecture failure observed yet; reports from bigger cards welcome. The
171
+ layer patch targets any `*DecoderLayer` module with the standard
172
+ `(positions, hidden_states, residual)` signature.
173
+
174
+ ## Numbers
175
+
176
+ Qwen3-4B-Instruct-2507, bf16, RTX 4070 Ti SUPER 16 GB, 8 concurrent requests,
177
+ 256 decode tokens each, medians of 3 (`benchmarks/bench_decode.py`):
178
+
179
+ | condition | TTFT | decode TPOT |
180
+ |---|---|---|
181
+ | vanilla vLLM (plugin not installed) | 4.9 ms | 1.78 ms/tok |
182
+ | hotwire installed, no request steered | 4.9 ms | 1.78 ms/tok |
183
+ | hotwire, **all 8 requests steered** | 4.6 ms | 1.78 ms/tok |
184
+ | vLLM `enforce_eager` (no plugin) | 5.0 ms | 1.88 ms/tok |
185
+
186
+ Idle and fully-steered are both within noise of vanilla. The eager row is what
187
+ hook-based steering tools pay *before* their Python hooks even run.
188
+
189
+ Batch sweep (same model/GPU): the eager tax grows with batch pressure —
190
+ +2.3% at 1 request (batch-1 decode is weight-streaming-bound, which hides
191
+ launch overhead), +4.7% at 2, +5.6% at 8. hotwire's idle == steered holds at
192
+ every batch size, to the second decimal.
193
+
194
+ | batch | graphs idle | graphs all-steered | eager |
195
+ |---|---|---|---|
196
+ | 1 | 13.69 ms/tok | 13.69 | 14.00 |
197
+ | 2 | 6.97 ms/tok | 6.97 | 7.30 |
198
+ | 8 | 1.78 ms/tok | 1.78 | 1.88 |
199
+
200
+ Untested configurations (no known issues, but nobody has run them — treat as
201
+ unsupported until someone does): tensor parallel > 1, pipeline parallel,
202
+ speculative decoding, LoRA, GPTQ and MXFP4 quantization (AWQ and FP8 are
203
+ verified — see the table). Issues welcome.
204
+
205
+ Known limitation: one vector per (layer, token) — multiple spec entries
206
+ targeting the **same layer** don't stack; the last one wins. Different layers
207
+ compose fine. Workaround: pre-combine same-layer vectors into one .pt
208
+ (`a*v1 + b*v2`) and register the combo; native stacking is on the roadmap.
209
+
210
+ Known limitation: the slot budget. Steering configs live in a fixed-size GPU
211
+ table allocated before graph capture — CUDA graphs read fixed addresses, so
212
+ it can never grow at runtime. Size it with `HOTWIRE_SLOTS` (default 16;
213
+ a slot is one vector row, ~5 KB on a 4B model, so 256 costs ~1.3 MB and
214
+ nothing per token). Each distinct **(vector, layer, scale)** combo occupies
215
+ one slot **permanently** — nothing frees slots when requests finish. A fixed
216
+ catalog of vectors at fixed scales therefore runs forever, but continuously
217
+ varying scales (0.80, 0.83, 0.87, …) mint a fresh slot each and exhaust the
218
+ table; once full, requests with an unregistrable combo run unsteered (logged)
219
+ while already-registered combos keep working, batchmates included.
220
+ Workaround today: round scales to a small fixed
221
+ palette and set `HOTWIRE_SLOTS` generously at startup. The real fixes are on
222
+ the roadmap below — slots *can* recycle (the scale isn't baked into the
223
+ stored vector; the kernel reads it separately at replay), it's bookkeeping,
224
+ not graph physics.
225
+
226
+ Roadmap:
227
+ - HTTP vector registration at runtime (via `vllm.endpoint_plugins`), replacing
228
+ startup-only `$HOTWIRE_VECTORS`.
229
+ - Slot eviction: refcount slots per in-flight request and `release()` when the
230
+ last user of a combo finishes, so the table recycles instead of filling.
231
+ - Per-token scales: key slots by (vector, layer) only and move scale into a
232
+ per-token buffer — continuous intensities without minting new slots.
233
+ - Norm-matched and position-targeted steering modes.
234
+ - Tracking the RFC vllm-project/vllm#36998 Phase 2 interface as it lands.
235
+ ## Where this sits in the lab
236
+
237
+ ```mermaid
238
+ flowchart LR
239
+ hd["🧭 hidden-directions<br/>behavior → vector"]
240
+ bs(["🧠 brainscope<br/>watch the model think"])
241
+ hw["🔥 hotwire-vllm<br/>steering in production"]
242
+ st["🕹️ steeropathy<br/>agents talk via activations"]
243
+ tm["⚖️ in-two-minds<br/>agent hesitating between tools"]
244
+ sm["🧪 steering-mechanics<br/>how steering actually works"]
245
+
246
+ hd -->|vectors| bs
247
+ hd -->|vector + passport| hw
248
+ bs --> st
249
+ bs --> tm
250
+ bs -->|causal replay| sm
251
+ hw -.->|vector under study| sm
252
+
253
+ click hd "https://github.com/moudrkat/hidden-directions"
254
+ click bs "https://github.com/moudrkat/brainscope"
255
+ click hw "https://github.com/moudrkat/hotwire-vllm"
256
+ click st "https://github.com/moudrkat/steeropathy"
257
+ click tm "https://github.com/moudrkat/in-two-minds"
258
+ click sm "https://github.com/moudrkat/steering-mechanics"
259
+
260
+ classDef dim fill:#f6f8fa,stroke:#d0d7de,color:#57606a;
261
+ classDef here fill:#8957e5,stroke:#6e40c9,color:#ffffff;
262
+ class hd,bs,hw,st,tm,sm dim;
263
+ class hw here;
264
+ ```
265
+
266
+ *Highlighted = this repo. The full lab map (with the two other repos' stories) lives on [moudrkat](https://github.com/moudrkat).*
267
+
@@ -0,0 +1,255 @@
1
+ # hotwire
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/hotwire-vllm)](https://pypi.org/project/hotwire-vllm/)
4
+
5
+ ```
6
+ pip install hotwire-vllm
7
+ ```
8
+
9
+ **Activation steering for vLLM that doesn't turn off the engine.**
10
+
11
+ For teams running vLLM in production who need per-request activation
12
+ steering at native speed — CUDA graphs and torch.compile intact.
13
+
14
+ Every existing steering tool for vLLM ([vllm-lens](https://github.com/UKGovernmentBEIS/vllm-lens),
15
+ [EasySteer](https://arxiv.org/abs/2509.25175), IBM's vLLM Hook) forces
16
+ `enforce_eager=True`: PyTorch forward hooks don't survive CUDA graph capture, so
17
+ they disable CUDA graphs and torch.compile for the whole server — every request
18
+ pays, steered or not. Fine for research, a non-starter for production.
19
+
20
+ hotwire keeps the graphs. The steering addition is a custom torch op (Triton
21
+ kernel) that gets baked *into* the captured graph; per-request routing happens
22
+ by updating the contents of persistent GPU buffers between graph replays —
23
+ the graph reads fresh data at the same addresses.
24
+
25
+ The technique was proven viable in [RhizoNymph's vLLM fork](https://github.com/RhizoNymph/vllm)
26
+ (see [RFC #36998](https://github.com/vllm-project/vllm/issues/36998), where
27
+ in-flight steering is explicitly deferred to "Phase 2"). hotwire packages it as
28
+ an out-of-tree plugin: `pip install`, no fork, registered via vLLM's official
29
+ `general_plugins` entry point.
30
+
31
+ ## ⚡ Run in 30 s
32
+
33
+ ```bash
34
+ pip install -e . # registers the vllm.general_plugins entry point
35
+ export HOTWIRE_VECTORS=/path/to/vectors # dir of .pt files, (n_layers, hidden) each
36
+ vllm serve Qwen/Qwen3-4B-Instruct-2507 # CUDA graphs stay ON
37
+ ```
38
+
39
+ Steer any request by id + layer + scale:
40
+
41
+ ```python
42
+ # offline
43
+ SamplingParams(extra_args={"hotwire": '{"id": "tesla_car", "layer": 20, "scale": 1.5}'})
44
+ ```
45
+
46
+ Optional per-entry flag `"decode_only": true` steers generated tokens only,
47
+ never the prompt — use it for vectors calibrated on generation-only steering
48
+ (research rigs typically don't steer the prefill; applying such a vector to a
49
+ long prompt as well multiplies the effective dose and can wreck coherence).
50
+
51
+ Lab twin: [brainscope](https://github.com/moudrkat/brainscope) accepts this
52
+ exact spec and wire format — calibrate a vector under its lenses (its
53
+ `export_hotwire` ships `.pt` files with a regime passport), deploy it here
54
+ unchanged, and replay production conversations back under the lens when a
55
+ vector misbehaves.
56
+
57
+ ```bash
58
+ # OpenAI API
59
+ curl .../v1/chat/completions -d '{..., "vllm_xargs":
60
+ {"hotwire": "{\"id\": \"tesla_car\", \"layer\": 20, \"scale\": 1.5}"}}'
61
+ ```
62
+
63
+ Unsteered requests — including batchmates of steered ones — are untouched.
64
+ Malformed specs and unknown vector ids degrade to "unsteered", never to a
65
+ failed request.
66
+
67
+ ## Design
68
+
69
+ Three persistent GPU tensors, allocated at model-load time:
70
+
71
+ | buffer | shape | role |
72
+ |---|---|---|
73
+ | `bank` | `(n_slots, hidden)` | steering vectors, one per active slot |
74
+ | `scales` | `(n_slots,)` | per-slot multiplier |
75
+ | `slot_map` | `(n_layers, max_tokens)` | token → slot per layer, `-1` = untouched |
76
+
77
+ The op, called at the end of each decoder layer's forward:
78
+
79
+ ```
80
+ hidden[tok] += scales[slot] * bank[slot] where slot = slot_map[layer, tok] >= 0
81
+ ```
82
+
83
+ - **Graph-safe:** the op is `torch.library.custom_op` with a fake impl —
84
+ opaque to torch.compile, captured into CUDA graphs as a fixed kernel on
85
+ fixed addresses. Steering on/off/vector changes are buffer *content*
86
+ updates between replays (host-side copy), never a re-capture.
87
+ - **Per-request:** a pre-forward hook reads `forward_context`
88
+ (`query_start_loc` + `req_ids`, same bookkeeping vllm-lens validated)
89
+ and fills `slot_map` for the step.
90
+ - **No pickle:** vectors enter as safetensors files or base64 JSON via a
91
+ registration endpoint (`POST /steer/vectors`), requests reference them by
92
+ id + scale in `vllm_xargs`. Nothing executable crosses the wire.
93
+ - **Zero cost when idle:** `slot_map` all `-1` → kernel early-exits per token.
94
+ (Benchmark target: unmeasurable vs baseline; RhizoNymph reported minimal
95
+ overhead on H100.)
96
+
97
+ ## Layout
98
+
99
+ - `hotwire/_kernel.py` — Triton kernel + `hotwire::steer` custom op (working)
100
+ - `hotwire/_bank.py` — slot allocation, vector registration (working)
101
+ - `hotwire/_patch.py` — decoder-layer wrapping + pre-forward slot fill (WIP:
102
+ integration points against vLLM 0.25.x)
103
+ - `hotwire/wire.py` — JSON/safetensors vector wire format, no pickle (working)
104
+
105
+ ## Verify on your hardware
106
+
107
+ Two commands, ~3 minutes on any CUDA box with vLLM installed:
108
+
109
+ ```bash
110
+ pip install git+https://github.com/moudrkat/hotwire-vllm
111
+ python -m hotwire.verify --model Qwen/Qwen3-0.6B # any HF model id works
112
+ ```
113
+
114
+ It generates a throwaway steering vector for the model, checks that steering
115
+ fires, that unsteered requests (including batchmates) are untouched, and
116
+ compares decode cost idle vs all-steered — then prints a report block.
117
+ **Please paste the report into an issue**, especially from hardware, model
118
+ families, or configs (TP > 1, 7B+, H100s) the tables below don't cover yet —
119
+ that's currently the most useful contribution this project can receive.
120
+
121
+ ## Status
122
+
123
+ Working end-to-end on vLLM 0.25.1, **both model runners** (the classic
124
+ `GPUModelRunner` and the new V2 runner that 0.25.1 selects by default for
125
+ dense generate models), with CUDA graphs captured (PIECEWISE + FULL) and
126
+ torch.compile on. Verified on Qwen3-0.6B / Qwen3-4B on a single 16 GB GPU:
127
+ solo steering, mixed batches, decode-phase graph replays.
128
+
129
+ hotwire also salts vLLM's torch.compile/AOT cache key (`VllmConfig.compute_hash`)
130
+ — the op is traced into the compiled model, and vLLM's cache doesn't know about
131
+ plugins, so without the salt a stale cache silently serves a model with no
132
+ steering op in it.
133
+
134
+ Tests: `pytest` (unit, CPU-safe), `pytest -m integration` (real engine, GPU).
135
+
136
+ Verified architectures (chaos-vector A/B + batchmate-isolation check, both
137
+ model runners exercised):
138
+
139
+ | model | steering works | unsteered untouched | TPOT idle → all-steered |
140
+ |---|---|---|---|
141
+ | Qwen3-14B-AWQ (4-bit) | ✓ | ✓ | 1.91 → 1.91 ms/tok |
142
+ | Llama-3.1-8B-Instruct-AWQ | ✓ | ✓ | 1.10 → 1.10 ms/tok |
143
+ | Qwen3-8B-FP8 | ✓ | ✓ | 2.27 → 2.28 ms/tok |
144
+ | Mistral-7B-Instruct-v0.2-AWQ | ✓ | ✓ | 0.94 → 0.94 ms/tok |
145
+ | Qwen3-4B-Instruct-2507 | ✓ | ✓ | 1.78 → 1.78 ms/tok |
146
+ | Qwen3-0.6B | ✓ | ✓ | — |
147
+ | Qwen2.5-1.5B-Instruct | ✓ | ✓ | 0.77 → 0.77 ms/tok |
148
+ | Phi-3.5-mini-instruct | ✓ | ✓ | 1.73 → 1.73 ms/tok |
149
+ | tiny-aya-water (Cohere) | ✓ | ✓ | 1.54 → 1.54 ms/tok |
150
+
151
+ Quantized checkpoints work — steering touches the residual stream, not the
152
+ weights, and the AWQ / FP8 rows above confirm it end-to-end, CUDA graphs
153
+ captured (PIECEWISE + FULL).
154
+
155
+ Models that still OOM on the 16 GB test GPU before the plugin engages:
156
+ OLMo-2-7B, command-r7b, Qwen3.5-4B, gpt-oss-20b (13.8 GiB weight load
157
+ succeeds, engine init doesn't), gemma-4-E4B-it (vision tower). No
158
+ architecture failure observed yet; reports from bigger cards welcome. The
159
+ layer patch targets any `*DecoderLayer` module with the standard
160
+ `(positions, hidden_states, residual)` signature.
161
+
162
+ ## Numbers
163
+
164
+ Qwen3-4B-Instruct-2507, bf16, RTX 4070 Ti SUPER 16 GB, 8 concurrent requests,
165
+ 256 decode tokens each, medians of 3 (`benchmarks/bench_decode.py`):
166
+
167
+ | condition | TTFT | decode TPOT |
168
+ |---|---|---|
169
+ | vanilla vLLM (plugin not installed) | 4.9 ms | 1.78 ms/tok |
170
+ | hotwire installed, no request steered | 4.9 ms | 1.78 ms/tok |
171
+ | hotwire, **all 8 requests steered** | 4.6 ms | 1.78 ms/tok |
172
+ | vLLM `enforce_eager` (no plugin) | 5.0 ms | 1.88 ms/tok |
173
+
174
+ Idle and fully-steered are both within noise of vanilla. The eager row is what
175
+ hook-based steering tools pay *before* their Python hooks even run.
176
+
177
+ Batch sweep (same model/GPU): the eager tax grows with batch pressure —
178
+ +2.3% at 1 request (batch-1 decode is weight-streaming-bound, which hides
179
+ launch overhead), +4.7% at 2, +5.6% at 8. hotwire's idle == steered holds at
180
+ every batch size, to the second decimal.
181
+
182
+ | batch | graphs idle | graphs all-steered | eager |
183
+ |---|---|---|---|
184
+ | 1 | 13.69 ms/tok | 13.69 | 14.00 |
185
+ | 2 | 6.97 ms/tok | 6.97 | 7.30 |
186
+ | 8 | 1.78 ms/tok | 1.78 | 1.88 |
187
+
188
+ Untested configurations (no known issues, but nobody has run them — treat as
189
+ unsupported until someone does): tensor parallel > 1, pipeline parallel,
190
+ speculative decoding, LoRA, GPTQ and MXFP4 quantization (AWQ and FP8 are
191
+ verified — see the table). Issues welcome.
192
+
193
+ Known limitation: one vector per (layer, token) — multiple spec entries
194
+ targeting the **same layer** don't stack; the last one wins. Different layers
195
+ compose fine. Workaround: pre-combine same-layer vectors into one .pt
196
+ (`a*v1 + b*v2`) and register the combo; native stacking is on the roadmap.
197
+
198
+ Known limitation: the slot budget. Steering configs live in a fixed-size GPU
199
+ table allocated before graph capture — CUDA graphs read fixed addresses, so
200
+ it can never grow at runtime. Size it with `HOTWIRE_SLOTS` (default 16;
201
+ a slot is one vector row, ~5 KB on a 4B model, so 256 costs ~1.3 MB and
202
+ nothing per token). Each distinct **(vector, layer, scale)** combo occupies
203
+ one slot **permanently** — nothing frees slots when requests finish. A fixed
204
+ catalog of vectors at fixed scales therefore runs forever, but continuously
205
+ varying scales (0.80, 0.83, 0.87, …) mint a fresh slot each and exhaust the
206
+ table; once full, requests with an unregistrable combo run unsteered (logged)
207
+ while already-registered combos keep working, batchmates included.
208
+ Workaround today: round scales to a small fixed
209
+ palette and set `HOTWIRE_SLOTS` generously at startup. The real fixes are on
210
+ the roadmap below — slots *can* recycle (the scale isn't baked into the
211
+ stored vector; the kernel reads it separately at replay), it's bookkeeping,
212
+ not graph physics.
213
+
214
+ Roadmap:
215
+ - HTTP vector registration at runtime (via `vllm.endpoint_plugins`), replacing
216
+ startup-only `$HOTWIRE_VECTORS`.
217
+ - Slot eviction: refcount slots per in-flight request and `release()` when the
218
+ last user of a combo finishes, so the table recycles instead of filling.
219
+ - Per-token scales: key slots by (vector, layer) only and move scale into a
220
+ per-token buffer — continuous intensities without minting new slots.
221
+ - Norm-matched and position-targeted steering modes.
222
+ - Tracking the RFC vllm-project/vllm#36998 Phase 2 interface as it lands.
223
+ ## Where this sits in the lab
224
+
225
+ ```mermaid
226
+ flowchart LR
227
+ hd["🧭 hidden-directions<br/>behavior → vector"]
228
+ bs(["🧠 brainscope<br/>watch the model think"])
229
+ hw["🔥 hotwire-vllm<br/>steering in production"]
230
+ st["🕹️ steeropathy<br/>agents talk via activations"]
231
+ tm["⚖️ in-two-minds<br/>agent hesitating between tools"]
232
+ sm["🧪 steering-mechanics<br/>how steering actually works"]
233
+
234
+ hd -->|vectors| bs
235
+ hd -->|vector + passport| hw
236
+ bs --> st
237
+ bs --> tm
238
+ bs -->|causal replay| sm
239
+ hw -.->|vector under study| sm
240
+
241
+ click hd "https://github.com/moudrkat/hidden-directions"
242
+ click bs "https://github.com/moudrkat/brainscope"
243
+ click hw "https://github.com/moudrkat/hotwire-vllm"
244
+ click st "https://github.com/moudrkat/steeropathy"
245
+ click tm "https://github.com/moudrkat/in-two-minds"
246
+ click sm "https://github.com/moudrkat/steering-mechanics"
247
+
248
+ classDef dim fill:#f6f8fa,stroke:#d0d7de,color:#57606a;
249
+ classDef here fill:#8957e5,stroke:#6e40c9,color:#ffffff;
250
+ class hd,bs,hw,st,tm,sm dim;
251
+ class hw here;
252
+ ```
253
+
254
+ *Highlighted = this repo. The full lab map (with the two other repos' stories) lives on [moudrkat](https://github.com/moudrkat).*
255
+