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 +21 -0
- vlmbox-0.1.0/MANIFEST.in +2 -0
- vlmbox-0.1.0/PKG-INFO +331 -0
- vlmbox-0.1.0/README.md +299 -0
- vlmbox-0.1.0/examples/paired_prompt_steering.py +217 -0
- vlmbox-0.1.0/examples/universal_image_optimization.py +178 -0
- vlmbox-0.1.0/pyproject.toml +84 -0
- vlmbox-0.1.0/setup.cfg +4 -0
- vlmbox-0.1.0/tests/integration/test_differentiable_image.py +169 -0
- vlmbox-0.1.0/tests/test_activation.py +266 -0
- vlmbox-0.1.0/tests/test_adapter.py +122 -0
- vlmbox-0.1.0/tests/test_generation.py +364 -0
- vlmbox-0.1.0/tests/test_image.py +205 -0
- vlmbox-0.1.0/tests/test_model.py +204 -0
- vlmbox-0.1.0/tests/test_representation.py +208 -0
- vlmbox-0.1.0/tests/test_repro.py +146 -0
- vlmbox-0.1.0/tests/test_training.py +154 -0
- vlmbox-0.1.0/vlmbox/__init__.py +57 -0
- vlmbox-0.1.0/vlmbox/activation.py +344 -0
- vlmbox-0.1.0/vlmbox/adapter.py +188 -0
- vlmbox-0.1.0/vlmbox/generation.py +148 -0
- vlmbox-0.1.0/vlmbox/image.py +126 -0
- vlmbox-0.1.0/vlmbox/model.py +575 -0
- vlmbox-0.1.0/vlmbox/representation.py +212 -0
- vlmbox-0.1.0/vlmbox/repro.py +90 -0
- vlmbox-0.1.0/vlmbox/training/__init__.py +17 -0
- vlmbox-0.1.0/vlmbox/training/image.py +208 -0
- vlmbox-0.1.0/vlmbox/training/trainer.py +263 -0
- vlmbox-0.1.0/vlmbox.egg-info/PKG-INFO +331 -0
- vlmbox-0.1.0/vlmbox.egg-info/SOURCES.txt +31 -0
- vlmbox-0.1.0/vlmbox.egg-info/dependency_links.txt +1 -0
- vlmbox-0.1.0/vlmbox.egg-info/requires.txt +13 -0
- vlmbox-0.1.0/vlmbox.egg-info/top_level.txt +1 -0
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.
|
vlmbox-0.1.0/MANIFEST.in
ADDED
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`.
|