vlmbox 0.1.0__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.
vlmbox-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 czxvan
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,2 @@
1
+ recursive-include examples *.py
2
+ recursive-include tests *.py
vlmbox-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,331 @@
1
+ Metadata-Version: 2.4
2
+ Name: vlmbox
3
+ Version: 0.1.0
4
+ Summary: A lightweight research toolbox for vision-language models.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/czxvan/VLMBox
7
+ Project-URL: Repository, https://github.com/czxvan/VLMBox
8
+ Project-URL: Issues, https://github.com/czxvan/VLMBox/issues
9
+ Keywords: vision-language-models,multimodal,transformers,pytorch,research
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: torch<2.15,>=2.14.1
21
+ Requires-Dist: transformers<6,>=5.22.1
22
+ Requires-Dist: modelscope>=1.40.2
23
+ Requires-Dist: numpy>=1.24
24
+ Requires-Dist: accelerate>=1.10
25
+ Requires-Dist: pillow>=10.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.3; extra == "dev"
28
+ Requires-Dist: ruff>=0.14; extra == "dev"
29
+ Provides-Extra: examples
30
+ Requires-Dist: torchvision<0.30,>=0.29.1; extra == "examples"
31
+ Dynamic: license-file
32
+
33
+ # VLMBox
34
+
35
+ A lightweight Python research toolbox for vision-language models (VLMs).
36
+
37
+ VLMBox provides reusable model-inspection and universal-image-optimization primitives. It is intentionally not a general-purpose fine-tuning framework: research-specific objectives belong in the research project that defines them.
38
+
39
+ ## Scope
40
+
41
+ - **Model** — load ModelScope checkpoints through Transformers and prepare native model inputs.
42
+ - **Adapter** — resolve common language-decoder components without coupling research code to one model family.
43
+ - **Activation** — capture and temporarily intervene on decoder blocks, attention, and MLP outputs.
44
+ - **Representation** — training-free direction extraction and representation geometry.
45
+ - **Image** — architecture-independent RGB conversion, resizing, cropping, and patch placement.
46
+ - **Training** — optimize one universal image perturbation or patch with a task-supplied objective.
47
+ - **Generation** — structured generation results and opt-in internal traces.
48
+ - **Repro** — seed control and optional deterministic execution.
49
+
50
+ ## Installation
51
+
52
+ ### Install from PyPI
53
+
54
+ ```bash
55
+ python -m pip install vlmbox
56
+ ```
57
+
58
+ The PyPI package declares a compatible PyTorch version range but does not force a particular CUDA build. If you need a specific accelerator build, install the matching PyTorch version for your platform first, then install VLMBox. The optional `examples` extra installs torchvision for the runnable image-optimization example:
59
+
60
+ ```bash
61
+ python -m pip install "vlmbox[examples]"
62
+ ```
63
+
64
+ ### Development environment
65
+
66
+ The repository's `uv.lock` intentionally pins CUDA 13.0 PyTorch wheels for the lab's RTX 3090 and RTX 4090 machines. The supported Linux research machines require an NVIDIA driver version 580 or newer. Use a machine-local environment rather than placing `.venv` on the shared workspace:
67
+
68
+ ```bash
69
+ export UV_PROJECT_ENVIRONMENT="$HOME/.venvs/VLMBox"
70
+ uv sync --locked --extra dev --extra examples
71
+ ```
72
+
73
+ The Tsinghua PyPI mirror and PyTorch CUDA 13.0 index are declared in `pyproject.toml`; `uv.lock` pins the resolved wheels. Use the same commands on each lab machine. Do not mutate this environment with a separate CPU-only PyTorch installation, pip, or Conda.
74
+
75
+ ## Quick start
76
+
77
+ ```python
78
+ from PIL import Image
79
+ from vlmbox import VLM
80
+
81
+ vlm = VLM.from_pretrained("Qwen/Qwen2.5-VL-3B-Instruct")
82
+ image = Image.open("example.jpg").convert("RGB")
83
+
84
+ messages = [
85
+ {
86
+ "role": "user",
87
+ "content": [
88
+ {"type": "image", "image": image},
89
+ {"type": "text", "text": "Describe this image."},
90
+ ],
91
+ }
92
+ ]
93
+ inputs = vlm.prepare(messages)
94
+ output_ids = vlm.generate(inputs, max_new_tokens=128)
95
+ new_tokens = output_ids[:, inputs["input_ids"].shape[1] :]
96
+ response = vlm.processor.batch_decode(new_tokens, skip_special_tokens=True)[0]
97
+ print(response)
98
+ ```
99
+
100
+ ## Activation
101
+
102
+ Capture decoder outputs without modifying the model:
103
+
104
+ ```python
105
+ with vlm.capture("block", layers=[8, 16, 24], positions="last") as cache:
106
+ vlm.generate(inputs, max_new_tokens=32)
107
+
108
+ last_layer_state = cache[24] # [batch, 1, hidden_size]
109
+ ```
110
+
111
+ Apply a temporary intervention to one decoder layer:
112
+
113
+ ```python
114
+ with vlm.intervene(
115
+ "block",
116
+ layer=16,
117
+ operation="add",
118
+ value=direction,
119
+ positions="last",
120
+ scope="every_forward",
121
+ ):
122
+ output_ids = vlm.generate(inputs, max_new_tokens=32)
123
+ ```
124
+
125
+ Supported components are `block`, `attention`, and `mlp`. Position selectors are `last`, `all`, or explicit token indices. `capture` records the first invocation of each selected module.
126
+
127
+ Intervention operations:
128
+
129
+ - `add`: add a scaled direction to the selected activation.
130
+ - `replace`: replace the selected activation.
131
+ - `project_out`: remove the projection onto a direction.
132
+ - `zero`: set the selected activation to zero.
133
+
134
+ Values must broadcast to the selected activation shape. `scale` applies to `add` and `project_out`. `first_forward` intervenes once per target layer; `every_forward` intervenes on every invocation, including decoding. Nested hooks run in registration order and are removed when the context exits. Captured tensors retain their autograd graph; detach them when graph retention is unnecessary.
135
+
136
+ ## Representation
137
+
138
+ Representation helpers are pure PyTorch operations; they do not train a classifier.
139
+
140
+ ```python
141
+ from vlmbox import (
142
+ angle,
143
+ cosine_similarity,
144
+ mean_difference,
145
+ normalize_direction,
146
+ project,
147
+ projection_coefficient,
148
+ projection_length,
149
+ )
150
+
151
+ with vlm.capture("block", layers=[16, 20], positions="last") as positive_cache:
152
+ vlm.generate(positive_inputs, max_new_tokens=1)
153
+ with vlm.capture("block", layers=[16, 20], positions="last") as negative_cache:
154
+ vlm.generate(negative_inputs, max_new_tokens=1)
155
+
156
+ direction_16 = mean_difference(positive_cache[16], negative_cache[16], dim=0)
157
+ direction_20 = mean_difference(positive_cache[20], negative_cache[20], dim=0)
158
+ unit_direction = normalize_direction(direction_16)
159
+
160
+ coefficient = projection_coefficient(positive_cache[16], unit_direction)
161
+ signed_length = projection_length(positive_cache[16], unit_direction)
162
+ component = project(positive_cache[16], unit_direction)
163
+ similarity = cosine_similarity(direction_16, direction_20)
164
+ theta_degrees = angle(direction_16, direction_20, degrees=True)
165
+ ```
166
+
167
+ `mean_difference(positive, negative)` computes the difference between independent sample means; sample counts may differ. It uses the `positive - negative` sign convention and reduces the sample dimension (`dim=0` by default). When two aligned sample sets have the same size, this equals the mean of their pairwise differences. `normalize_direction` normalizes along the feature dimension (`dim=-1` by default).
168
+
169
+ `projection_coefficient` returns `dot(hidden, direction) / dot(direction, direction)`. `projection_length` returns the signed scalar projection onto a unit direction, and `project` returns the corresponding vector component. `cosine_similarity` and `angle` compute vector similarity and angular distance. Geometry operations support PyTorch broadcasting and use float32 accumulation for fp16/bf16 inputs. Zero directions are rejected.
170
+
171
+ ### Paired-prompt steering example
172
+
173
+ The example demonstrates a complete training-free workflow: capture two prompt-conditioned activations over the same image, extract a mean-difference direction, then apply it during generation at several scales. Its synthetic red-square/blue-circle task is an API sanity probe, not evidence of general steering effectiveness.
174
+
175
+ ```bash
176
+ uv run python examples/paired_prompt_steering.py \
177
+ --model-id Qwen/Qwen2.5-VL-3B-Instruct \
178
+ --scales 0.5,1,2,4
179
+ ```
180
+
181
+ Use `--image` to provide an image, `--layer` to select a decoder layer, and `--scope first_forward` to compare prefill-only and persistent intervention. Use `--local-files-only` when the checkpoint is already cached.
182
+
183
+ ## Image
184
+
185
+ Image helpers are architecture-independent and use Pillow. They return new RGB images without mutating their inputs:
186
+
187
+ ```python
188
+ from PIL import Image
189
+ from vlmbox.image import crop_image, place_patch, resize_image, to_rgb
190
+
191
+ image = to_rgb(Image.open("example.jpg"))
192
+ resized = resize_image(image, (672, 756)) # (width, height), LANCZOS by default
193
+ patch = crop_image(image, (0, 0, 192, 192)) # (left, top, right, bottom)
194
+ composed = place_patch(image, patch, (32, 32)) # (left, top), no implicit resize
195
+ ```
196
+
197
+ `crop_image` uses half-open pixel coordinates and rejects empty or out-of-bounds boxes. `place_patch` requires the patch to fit inside the base image. An optional same-size `L` or `1` mask blends the patch into the base. `to_rgb` follows Pillow conversion semantics and discards an existing alpha channel rather than compositing it.
198
+
199
+ ## Universal image training
200
+
201
+ VLMBox currently supports **universal** image optimization only: one trainable image parameter is shared across all samples in a run. It does not implement per-image optimization.
202
+
203
+ - `UniversalPerturbation` learns one full-canvas RGB perturbation shared by every sample and constrained by an L-infinity bound `epsilon`.
204
+ - `UniversalPatch` learns one RGB patch shared by every sample and places it at one fixed canvas location.
205
+ - `ImageTrainer` manages batching, model freezing, forward/backward passes, optimizer updates, and projection back into the parameter's feasible range.
206
+
207
+ ### Objective and Trainer
208
+
209
+ `ImageTrainer` is task-agnostic. Its `objective` argument accepts either a function or any callable object with the signature:
210
+
211
+ ```python
212
+ def objective(outputs, inputs, samples):
213
+ # Return one differentiable scalar Tensor.
214
+ ...
215
+ ```
216
+
217
+ The objective must return one differentiable scalar tensor. It can use logits, hidden states, or other model outputs. Pass model-specific forward options through `forward_kwargs`, for example `{"output_hidden_states": True}` for a hidden-state objective.
218
+
219
+ A typical setup looks like this:
220
+
221
+ ```python
222
+ from vlmbox import ImageTrainer, UniversalPatch
223
+
224
+ patch = UniversalPatch(
225
+ canvas_size=(224, 224),
226
+ size=(48, 48),
227
+ position="bottom_right",
228
+ )
229
+ trainer = ImageTrainer(vlm, patch, objective, learning_rate=1e-3)
230
+ history = trainer.fit(samples, max_steps=500, batch_size=2, seed=42)
231
+ ```
232
+
233
+ For hidden-state objectives, pass `forward_kwargs={"output_hidden_states": True}` to `fit()`.
234
+
235
+ A new loss does **not** require a new Trainer. Implement the objective in the research project and pass it to `ImageTrainer`. VLMBox intentionally does not bundle task-specific losses or method-specific Trainers. A custom Trainer is only warranted when a project needs a genuinely different optimization loop, not merely a different loss.
236
+
237
+ ### Data and preprocessing
238
+
239
+ Each `ImageSample` contains an RGB float image tensor in `[0, 1]`, a user prompt, and optional system prompt or teacher-forced target text. All images in one run must share the same `(height, width)`. Either every sample provides target text or none does. When target text is present, it is appended as an assistant turn; the objective is responsible for selecting the target-token positions used by its loss.
240
+
241
+ `VLM.prepare_differentiable()` accepts Tensor image content in one conversation or a batch of conversations. It uses the model's native Transformers processor, with rescaling disabled because inputs are already in `[0, 1]`. Native resizing, cropping, patchification, splitting, normalization, and padding follow each model processor's own configuration; VLMBox does not override these settings.
242
+
243
+ Image preprocessing runs on CPU so the configured native resampling path is preserved. The processed tensors are then moved to the VLM input device without detaching the autograd graph. The recommended setup keeps source images and universal image parameters on CPU while the VLM runs on CUDA. `VLM.forward()` is autograd-enabled; `generate()` is inference-oriented.
244
+
245
+ During `fit()`, the Trainer freezes VLM parameters and switches the model to evaluation mode, then restores their original `requires_grad` flags and training mode. Only the universal image parameter is optimized. The default optimizer is Adam; `optimizer_cls` and `optimizer_kwargs` can be supplied when constructing the Trainer.
246
+
247
+ ### Runnable example
248
+
249
+ This example uses teacher-forced assistant-target cross-entropy and verifies that the prompt-only token sequence is an exact prefix of the prompt-plus-target sequence. It prepares a fixed square canvas before the model's native processor runs.
250
+
251
+ ```bash
252
+ uv run python examples/universal_image_optimization.py \
253
+ --image-dir ./images \
254
+ --parameterization patch \
255
+ --steps 100 \
256
+ --batch-size 2
257
+ ```
258
+
259
+ Use `--parameterization perturbation` for a universal L-infinity-bounded perturbation, or `patch` for a fixed-position patch. The default model is `HuggingFaceTB/SmolVLM-500M-Instruct`; pass `--model-id` to select another supported model. The optimized parameter and step history are saved to the requested `.pt` output.
260
+
261
+ ## Generation
262
+
263
+ `VLM.generate()` is a thin pass-through to Transformers. Use `generate_trace()` when a structured record of generation is needed:
264
+
265
+ ```python
266
+ trace = vlm.generate_trace(
267
+ inputs,
268
+ max_new_tokens=32,
269
+ capture_logits=True,
270
+ capture_hidden_states=True,
271
+ hidden_state_layers=[8, 16, 24],
272
+ decode_text=True,
273
+ )
274
+
275
+ generated_ids = trace.generated_token_ids
276
+ first_step_logits = trace.steps[0].logits
277
+ prefill_state = trace.steps[0].hidden_states[16]
278
+ decode_state = trace.steps[1].hidden_states[16]
279
+ print(trace.finish_reasons)
280
+ ```
281
+
282
+ `GenerationTrace` stores generated sequences, prompt length, optional decoded text, per-sequence finish reasons, and one `GenerationStep` per generated token. Step zero is prompt prefill; later steps are autoregressive decode. A step's `token_ids` are the tokens selected from that forward pass, while its hidden states describe the prompt or preceding input tokens.
283
+
284
+ Logits and hidden states are disabled by default to avoid retaining large GPU tensors. `capture_hidden_states=True` requires an explicit `hidden_state_layers` selection. Hidden-state indices are zero-based decoder-layer indices. Beam-search internal traces are currently rejected because beam histories need explicit alignment. `generate_trace()` currently supports decoder-only models.
285
+
286
+ Transformers does not expose a standard finish-reason field. VLMBox reports `eos` or `length` only when the returned sequence makes the reason unambiguous; otherwise it reports `unknown`.
287
+
288
+ ## Reproducibility
289
+
290
+ Set RNG seeds for Python, NumPy's legacy global RNG, and PyTorch CPU/CUDA generators:
291
+
292
+ ```python
293
+ from vlmbox.repro import set_seed
294
+
295
+ set_seed(42) # seed only; leave deterministic backend settings unchanged
296
+ set_seed(42, deterministic=True) # opt in to deterministic PyTorch algorithms
297
+ ```
298
+
299
+ `set_seed()` accepts seeds from 0 through `2**32 - 1`. Deterministic mode enables PyTorch deterministic algorithms, enables cuDNN deterministic mode, and disables cuDNN benchmarking. On CUDA systems, an unset `CUBLAS_WORKSPACE_CONFIG` is set to `:4096:8` before CUDA initialization; an existing non-empty value is preserved. If CUDA is already initialized while the variable is unset, the function raises an error and asks you to restart the process. `warn_only=True` changes unsupported deterministic operations from errors to warnings. TF32 settings are not modified.
300
+
301
+ ## Model loading and compatibility
302
+
303
+ Models are resolved through ModelScope's default cache. `VLM.from_pretrained()` accepts a ModelScope model ID; pass `local_files_only=True` to prohibit downloads and require a complete local cache. VLMBox uses `AutoProcessor` and `AutoModelForMultimodalLM`, with `dtype="auto"`, `device_map="auto"`, and fixed `trust_remote_code=True`. Only load repositories whose code you trust.
304
+
305
+ The generic `TransformersAdapter` has GPU integration coverage for Qwen2.5-VL-3B, InternVL3.5-2B-HF, and SmolVLM-500M. Other models may work when their language-decoder module layout matches the adapter's supported patterns.
306
+
307
+ ## Development
308
+
309
+ ```bash
310
+ export UV_PROJECT_ENVIRONMENT="$HOME/.venvs/VLMBox"
311
+ uv run ruff check .
312
+ uv run pytest -q
313
+ ```
314
+
315
+ Run real-model integration tests when the three checkpoints are already present in the ModelScope cache:
316
+
317
+ ```bash
318
+ VLMBOX_RUN_GPU_INTEGRATION=1 uv run pytest tests/integration -q
319
+ ```
320
+
321
+ Build the wheel and source distribution with:
322
+
323
+ ```bash
324
+ uv build
325
+ ```
326
+
327
+ ## License
328
+
329
+ VLMBox is licensed under the MIT License. See [LICENSE](LICENSE) for the full text.
330
+
331
+ Maintainers can find release and Trusted Publishing instructions in the repository's `RELEASING.md`.
vlmbox-0.1.0/README.md ADDED
@@ -0,0 +1,299 @@
1
+ # VLMBox
2
+
3
+ A lightweight Python research toolbox for vision-language models (VLMs).
4
+
5
+ VLMBox provides reusable model-inspection and universal-image-optimization primitives. It is intentionally not a general-purpose fine-tuning framework: research-specific objectives belong in the research project that defines them.
6
+
7
+ ## Scope
8
+
9
+ - **Model** — load ModelScope checkpoints through Transformers and prepare native model inputs.
10
+ - **Adapter** — resolve common language-decoder components without coupling research code to one model family.
11
+ - **Activation** — capture and temporarily intervene on decoder blocks, attention, and MLP outputs.
12
+ - **Representation** — training-free direction extraction and representation geometry.
13
+ - **Image** — architecture-independent RGB conversion, resizing, cropping, and patch placement.
14
+ - **Training** — optimize one universal image perturbation or patch with a task-supplied objective.
15
+ - **Generation** — structured generation results and opt-in internal traces.
16
+ - **Repro** — seed control and optional deterministic execution.
17
+
18
+ ## Installation
19
+
20
+ ### Install from PyPI
21
+
22
+ ```bash
23
+ python -m pip install vlmbox
24
+ ```
25
+
26
+ The PyPI package declares a compatible PyTorch version range but does not force a particular CUDA build. If you need a specific accelerator build, install the matching PyTorch version for your platform first, then install VLMBox. The optional `examples` extra installs torchvision for the runnable image-optimization example:
27
+
28
+ ```bash
29
+ python -m pip install "vlmbox[examples]"
30
+ ```
31
+
32
+ ### Development environment
33
+
34
+ The repository's `uv.lock` intentionally pins CUDA 13.0 PyTorch wheels for the lab's RTX 3090 and RTX 4090 machines. The supported Linux research machines require an NVIDIA driver version 580 or newer. Use a machine-local environment rather than placing `.venv` on the shared workspace:
35
+
36
+ ```bash
37
+ export UV_PROJECT_ENVIRONMENT="$HOME/.venvs/VLMBox"
38
+ uv sync --locked --extra dev --extra examples
39
+ ```
40
+
41
+ The Tsinghua PyPI mirror and PyTorch CUDA 13.0 index are declared in `pyproject.toml`; `uv.lock` pins the resolved wheels. Use the same commands on each lab machine. Do not mutate this environment with a separate CPU-only PyTorch installation, pip, or Conda.
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ from PIL import Image
47
+ from vlmbox import VLM
48
+
49
+ vlm = VLM.from_pretrained("Qwen/Qwen2.5-VL-3B-Instruct")
50
+ image = Image.open("example.jpg").convert("RGB")
51
+
52
+ messages = [
53
+ {
54
+ "role": "user",
55
+ "content": [
56
+ {"type": "image", "image": image},
57
+ {"type": "text", "text": "Describe this image."},
58
+ ],
59
+ }
60
+ ]
61
+ inputs = vlm.prepare(messages)
62
+ output_ids = vlm.generate(inputs, max_new_tokens=128)
63
+ new_tokens = output_ids[:, inputs["input_ids"].shape[1] :]
64
+ response = vlm.processor.batch_decode(new_tokens, skip_special_tokens=True)[0]
65
+ print(response)
66
+ ```
67
+
68
+ ## Activation
69
+
70
+ Capture decoder outputs without modifying the model:
71
+
72
+ ```python
73
+ with vlm.capture("block", layers=[8, 16, 24], positions="last") as cache:
74
+ vlm.generate(inputs, max_new_tokens=32)
75
+
76
+ last_layer_state = cache[24] # [batch, 1, hidden_size]
77
+ ```
78
+
79
+ Apply a temporary intervention to one decoder layer:
80
+
81
+ ```python
82
+ with vlm.intervene(
83
+ "block",
84
+ layer=16,
85
+ operation="add",
86
+ value=direction,
87
+ positions="last",
88
+ scope="every_forward",
89
+ ):
90
+ output_ids = vlm.generate(inputs, max_new_tokens=32)
91
+ ```
92
+
93
+ Supported components are `block`, `attention`, and `mlp`. Position selectors are `last`, `all`, or explicit token indices. `capture` records the first invocation of each selected module.
94
+
95
+ Intervention operations:
96
+
97
+ - `add`: add a scaled direction to the selected activation.
98
+ - `replace`: replace the selected activation.
99
+ - `project_out`: remove the projection onto a direction.
100
+ - `zero`: set the selected activation to zero.
101
+
102
+ Values must broadcast to the selected activation shape. `scale` applies to `add` and `project_out`. `first_forward` intervenes once per target layer; `every_forward` intervenes on every invocation, including decoding. Nested hooks run in registration order and are removed when the context exits. Captured tensors retain their autograd graph; detach them when graph retention is unnecessary.
103
+
104
+ ## Representation
105
+
106
+ Representation helpers are pure PyTorch operations; they do not train a classifier.
107
+
108
+ ```python
109
+ from vlmbox import (
110
+ angle,
111
+ cosine_similarity,
112
+ mean_difference,
113
+ normalize_direction,
114
+ project,
115
+ projection_coefficient,
116
+ projection_length,
117
+ )
118
+
119
+ with vlm.capture("block", layers=[16, 20], positions="last") as positive_cache:
120
+ vlm.generate(positive_inputs, max_new_tokens=1)
121
+ with vlm.capture("block", layers=[16, 20], positions="last") as negative_cache:
122
+ vlm.generate(negative_inputs, max_new_tokens=1)
123
+
124
+ direction_16 = mean_difference(positive_cache[16], negative_cache[16], dim=0)
125
+ direction_20 = mean_difference(positive_cache[20], negative_cache[20], dim=0)
126
+ unit_direction = normalize_direction(direction_16)
127
+
128
+ coefficient = projection_coefficient(positive_cache[16], unit_direction)
129
+ signed_length = projection_length(positive_cache[16], unit_direction)
130
+ component = project(positive_cache[16], unit_direction)
131
+ similarity = cosine_similarity(direction_16, direction_20)
132
+ theta_degrees = angle(direction_16, direction_20, degrees=True)
133
+ ```
134
+
135
+ `mean_difference(positive, negative)` computes the difference between independent sample means; sample counts may differ. It uses the `positive - negative` sign convention and reduces the sample dimension (`dim=0` by default). When two aligned sample sets have the same size, this equals the mean of their pairwise differences. `normalize_direction` normalizes along the feature dimension (`dim=-1` by default).
136
+
137
+ `projection_coefficient` returns `dot(hidden, direction) / dot(direction, direction)`. `projection_length` returns the signed scalar projection onto a unit direction, and `project` returns the corresponding vector component. `cosine_similarity` and `angle` compute vector similarity and angular distance. Geometry operations support PyTorch broadcasting and use float32 accumulation for fp16/bf16 inputs. Zero directions are rejected.
138
+
139
+ ### Paired-prompt steering example
140
+
141
+ The example demonstrates a complete training-free workflow: capture two prompt-conditioned activations over the same image, extract a mean-difference direction, then apply it during generation at several scales. Its synthetic red-square/blue-circle task is an API sanity probe, not evidence of general steering effectiveness.
142
+
143
+ ```bash
144
+ uv run python examples/paired_prompt_steering.py \
145
+ --model-id Qwen/Qwen2.5-VL-3B-Instruct \
146
+ --scales 0.5,1,2,4
147
+ ```
148
+
149
+ Use `--image` to provide an image, `--layer` to select a decoder layer, and `--scope first_forward` to compare prefill-only and persistent intervention. Use `--local-files-only` when the checkpoint is already cached.
150
+
151
+ ## Image
152
+
153
+ Image helpers are architecture-independent and use Pillow. They return new RGB images without mutating their inputs:
154
+
155
+ ```python
156
+ from PIL import Image
157
+ from vlmbox.image import crop_image, place_patch, resize_image, to_rgb
158
+
159
+ image = to_rgb(Image.open("example.jpg"))
160
+ resized = resize_image(image, (672, 756)) # (width, height), LANCZOS by default
161
+ patch = crop_image(image, (0, 0, 192, 192)) # (left, top, right, bottom)
162
+ composed = place_patch(image, patch, (32, 32)) # (left, top), no implicit resize
163
+ ```
164
+
165
+ `crop_image` uses half-open pixel coordinates and rejects empty or out-of-bounds boxes. `place_patch` requires the patch to fit inside the base image. An optional same-size `L` or `1` mask blends the patch into the base. `to_rgb` follows Pillow conversion semantics and discards an existing alpha channel rather than compositing it.
166
+
167
+ ## Universal image training
168
+
169
+ VLMBox currently supports **universal** image optimization only: one trainable image parameter is shared across all samples in a run. It does not implement per-image optimization.
170
+
171
+ - `UniversalPerturbation` learns one full-canvas RGB perturbation shared by every sample and constrained by an L-infinity bound `epsilon`.
172
+ - `UniversalPatch` learns one RGB patch shared by every sample and places it at one fixed canvas location.
173
+ - `ImageTrainer` manages batching, model freezing, forward/backward passes, optimizer updates, and projection back into the parameter's feasible range.
174
+
175
+ ### Objective and Trainer
176
+
177
+ `ImageTrainer` is task-agnostic. Its `objective` argument accepts either a function or any callable object with the signature:
178
+
179
+ ```python
180
+ def objective(outputs, inputs, samples):
181
+ # Return one differentiable scalar Tensor.
182
+ ...
183
+ ```
184
+
185
+ The objective must return one differentiable scalar tensor. It can use logits, hidden states, or other model outputs. Pass model-specific forward options through `forward_kwargs`, for example `{"output_hidden_states": True}` for a hidden-state objective.
186
+
187
+ A typical setup looks like this:
188
+
189
+ ```python
190
+ from vlmbox import ImageTrainer, UniversalPatch
191
+
192
+ patch = UniversalPatch(
193
+ canvas_size=(224, 224),
194
+ size=(48, 48),
195
+ position="bottom_right",
196
+ )
197
+ trainer = ImageTrainer(vlm, patch, objective, learning_rate=1e-3)
198
+ history = trainer.fit(samples, max_steps=500, batch_size=2, seed=42)
199
+ ```
200
+
201
+ For hidden-state objectives, pass `forward_kwargs={"output_hidden_states": True}` to `fit()`.
202
+
203
+ A new loss does **not** require a new Trainer. Implement the objective in the research project and pass it to `ImageTrainer`. VLMBox intentionally does not bundle task-specific losses or method-specific Trainers. A custom Trainer is only warranted when a project needs a genuinely different optimization loop, not merely a different loss.
204
+
205
+ ### Data and preprocessing
206
+
207
+ Each `ImageSample` contains an RGB float image tensor in `[0, 1]`, a user prompt, and optional system prompt or teacher-forced target text. All images in one run must share the same `(height, width)`. Either every sample provides target text or none does. When target text is present, it is appended as an assistant turn; the objective is responsible for selecting the target-token positions used by its loss.
208
+
209
+ `VLM.prepare_differentiable()` accepts Tensor image content in one conversation or a batch of conversations. It uses the model's native Transformers processor, with rescaling disabled because inputs are already in `[0, 1]`. Native resizing, cropping, patchification, splitting, normalization, and padding follow each model processor's own configuration; VLMBox does not override these settings.
210
+
211
+ Image preprocessing runs on CPU so the configured native resampling path is preserved. The processed tensors are then moved to the VLM input device without detaching the autograd graph. The recommended setup keeps source images and universal image parameters on CPU while the VLM runs on CUDA. `VLM.forward()` is autograd-enabled; `generate()` is inference-oriented.
212
+
213
+ During `fit()`, the Trainer freezes VLM parameters and switches the model to evaluation mode, then restores their original `requires_grad` flags and training mode. Only the universal image parameter is optimized. The default optimizer is Adam; `optimizer_cls` and `optimizer_kwargs` can be supplied when constructing the Trainer.
214
+
215
+ ### Runnable example
216
+
217
+ This example uses teacher-forced assistant-target cross-entropy and verifies that the prompt-only token sequence is an exact prefix of the prompt-plus-target sequence. It prepares a fixed square canvas before the model's native processor runs.
218
+
219
+ ```bash
220
+ uv run python examples/universal_image_optimization.py \
221
+ --image-dir ./images \
222
+ --parameterization patch \
223
+ --steps 100 \
224
+ --batch-size 2
225
+ ```
226
+
227
+ Use `--parameterization perturbation` for a universal L-infinity-bounded perturbation, or `patch` for a fixed-position patch. The default model is `HuggingFaceTB/SmolVLM-500M-Instruct`; pass `--model-id` to select another supported model. The optimized parameter and step history are saved to the requested `.pt` output.
228
+
229
+ ## Generation
230
+
231
+ `VLM.generate()` is a thin pass-through to Transformers. Use `generate_trace()` when a structured record of generation is needed:
232
+
233
+ ```python
234
+ trace = vlm.generate_trace(
235
+ inputs,
236
+ max_new_tokens=32,
237
+ capture_logits=True,
238
+ capture_hidden_states=True,
239
+ hidden_state_layers=[8, 16, 24],
240
+ decode_text=True,
241
+ )
242
+
243
+ generated_ids = trace.generated_token_ids
244
+ first_step_logits = trace.steps[0].logits
245
+ prefill_state = trace.steps[0].hidden_states[16]
246
+ decode_state = trace.steps[1].hidden_states[16]
247
+ print(trace.finish_reasons)
248
+ ```
249
+
250
+ `GenerationTrace` stores generated sequences, prompt length, optional decoded text, per-sequence finish reasons, and one `GenerationStep` per generated token. Step zero is prompt prefill; later steps are autoregressive decode. A step's `token_ids` are the tokens selected from that forward pass, while its hidden states describe the prompt or preceding input tokens.
251
+
252
+ Logits and hidden states are disabled by default to avoid retaining large GPU tensors. `capture_hidden_states=True` requires an explicit `hidden_state_layers` selection. Hidden-state indices are zero-based decoder-layer indices. Beam-search internal traces are currently rejected because beam histories need explicit alignment. `generate_trace()` currently supports decoder-only models.
253
+
254
+ Transformers does not expose a standard finish-reason field. VLMBox reports `eos` or `length` only when the returned sequence makes the reason unambiguous; otherwise it reports `unknown`.
255
+
256
+ ## Reproducibility
257
+
258
+ Set RNG seeds for Python, NumPy's legacy global RNG, and PyTorch CPU/CUDA generators:
259
+
260
+ ```python
261
+ from vlmbox.repro import set_seed
262
+
263
+ set_seed(42) # seed only; leave deterministic backend settings unchanged
264
+ set_seed(42, deterministic=True) # opt in to deterministic PyTorch algorithms
265
+ ```
266
+
267
+ `set_seed()` accepts seeds from 0 through `2**32 - 1`. Deterministic mode enables PyTorch deterministic algorithms, enables cuDNN deterministic mode, and disables cuDNN benchmarking. On CUDA systems, an unset `CUBLAS_WORKSPACE_CONFIG` is set to `:4096:8` before CUDA initialization; an existing non-empty value is preserved. If CUDA is already initialized while the variable is unset, the function raises an error and asks you to restart the process. `warn_only=True` changes unsupported deterministic operations from errors to warnings. TF32 settings are not modified.
268
+
269
+ ## Model loading and compatibility
270
+
271
+ Models are resolved through ModelScope's default cache. `VLM.from_pretrained()` accepts a ModelScope model ID; pass `local_files_only=True` to prohibit downloads and require a complete local cache. VLMBox uses `AutoProcessor` and `AutoModelForMultimodalLM`, with `dtype="auto"`, `device_map="auto"`, and fixed `trust_remote_code=True`. Only load repositories whose code you trust.
272
+
273
+ The generic `TransformersAdapter` has GPU integration coverage for Qwen2.5-VL-3B, InternVL3.5-2B-HF, and SmolVLM-500M. Other models may work when their language-decoder module layout matches the adapter's supported patterns.
274
+
275
+ ## Development
276
+
277
+ ```bash
278
+ export UV_PROJECT_ENVIRONMENT="$HOME/.venvs/VLMBox"
279
+ uv run ruff check .
280
+ uv run pytest -q
281
+ ```
282
+
283
+ Run real-model integration tests when the three checkpoints are already present in the ModelScope cache:
284
+
285
+ ```bash
286
+ VLMBOX_RUN_GPU_INTEGRATION=1 uv run pytest tests/integration -q
287
+ ```
288
+
289
+ Build the wheel and source distribution with:
290
+
291
+ ```bash
292
+ uv build
293
+ ```
294
+
295
+ ## License
296
+
297
+ VLMBox is licensed under the MIT License. See [LICENSE](LICENSE) for the full text.
298
+
299
+ Maintainers can find release and Trusted Publishing instructions in the repository's `RELEASING.md`.