vramd 0.2.2__tar.gz → 0.2.4__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.
- vramd-0.2.4/PKG-INFO +245 -0
- vramd-0.2.4/README.md +212 -0
- {vramd-0.2.2 → vramd-0.2.4}/pyproject.toml +5 -5
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/__init__.py +1 -1
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/cli.py +28 -1
- vramd-0.2.4/src/vramd/data/test-image.png +0 -0
- vramd-0.2.4/src/vramd/data/test-mesh.glb +0 -0
- vramd-0.2.4/src/vramd.egg-info/PKG-INFO +245 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd.egg-info/SOURCES.txt +2 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_calibrate_cli.py +29 -0
- vramd-0.2.2/PKG-INFO +0 -221
- vramd-0.2.2/README.md +0 -188
- vramd-0.2.2/src/vramd.egg-info/PKG-INFO +0 -221
- {vramd-0.2.2 → vramd-0.2.4}/LICENSE +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/setup.cfg +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/__main__.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/adapter.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/backend_manager.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/calibrate/__init__.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/calibrate/analysis.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/calibrate/compare.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/calibrate/emit.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/calibrate/hw_auto.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/calibrate/runner.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/calibrate/sampler.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/calibrate/serde.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/client.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/data/backends.yaml +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/dispatcher.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/errors.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/footprints.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/gpu.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/idle_evictor.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/job_queue.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/logging.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/process_guard.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/protocol.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/py.typed +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/registry.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/scheduler.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/server.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/stats.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/subprocess_pool.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/toolchain.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/vram_planner.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/worker/__init__.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/worker/adapter.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/worker/protocol.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd/worker/serve.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd.egg-info/dependency_links.txt +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd.egg-info/entry_points.txt +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd.egg-info/requires.txt +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/src/vramd.egg-info/top_level.txt +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_abort_progress.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_backend_manager.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_backend_manager_hybrid.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_calibrate_analysis.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_calibrate_emit_compare.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_calibrate_hw_auto.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_calibrate_runner.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_calibrate_sampler.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_calibrate_serde.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_cli_debug_stats_bench.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_gpu_ids_load.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_idle_evictor.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_job_queue.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_job_wal.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_peak_profile.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_phase_progress.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_process_guard.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_protocol_env.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_queue_metrics_eta.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_registry.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_registry_layers.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_respawn.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_runtime_spawn.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_scheduler.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_server.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_stats.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_subprocess_integration.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_subprocess_per_backend_e2e.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_subprocess_pool.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_toolchain.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_unload_frees_vram.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_vram_admit.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_vram_planner.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_vramd_coverage_100.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_worker.py +0 -0
- {vramd-0.2.2 → vramd-0.2.4}/tests/test_zero_vram.py +0 -0
vramd-0.2.4/PKG-INFO
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vramd
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: VRAM admission control for generative inference on consumer GPUs
|
|
5
|
+
Author: Maikeu Locatelli
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/maikramer/vramd
|
|
8
|
+
Project-URL: Repository, https://github.com/maikramer/vramd
|
|
9
|
+
Project-URL: Issues, https://github.com/maikramer/vramd/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/maikramer/vramd/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: gpu,vram,inference,scheduler,admission-control,diffusion
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: click>=8.1.0
|
|
23
|
+
Requires-Dist: rich>=13.0.0
|
|
24
|
+
Requires-Dist: rich-click>=1.8.0
|
|
25
|
+
Requires-Dist: PyYAML>=6.0
|
|
26
|
+
Requires-Dist: psutil>=5.9.0
|
|
27
|
+
Requires-Dist: nvidia-ml-py>=12.535.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff==0.16.2; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# vramd
|
|
35
|
+
|
|
36
|
+
[](https://github.com/maikramer/vramd/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/vramd/)
|
|
38
|
+
[](https://pypi.org/project/vramd/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
**VRAM admission control for generative inference on consumer GPUs.**
|
|
42
|
+
|
|
43
|
+
One process holds the GPU and decides who gets in. It admits by the **real
|
|
44
|
+
peak** — weights + activation + margin, not just weights — queues with
|
|
45
|
+
priority and affinity, evicts by weight+LRU, and runs each model in its own
|
|
46
|
+
process and venv.
|
|
47
|
+
|
|
48
|
+
Built for inference that lasts **seconds to minutes** on a card that can't fit
|
|
49
|
+
everything. It's not an LLM server: it doesn't optimize token throughput, it
|
|
50
|
+
optimizes *fitting*.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install vramd # 9 MB — the supervisor doesn't import torch
|
|
54
|
+
vramd start &
|
|
55
|
+
vramd submit my-model --prompt "…" --wait
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## The problem
|
|
59
|
+
|
|
60
|
+
You have 6 GB of VRAM and five models that, together, ask for 40. Each runs
|
|
61
|
+
fine on its own. Together, the second job starts in the middle of the first
|
|
62
|
+
and both die with `CUDA out of memory` — after the weights were already
|
|
63
|
+
loaded.
|
|
64
|
+
|
|
65
|
+
The usual solutions assume something that isn't true here: that the model fits
|
|
66
|
+
(vLLM, TGI), that the environment is homogeneous (Ray Serve, Triton), or that
|
|
67
|
+
the unit of work is a token rather than a two-minute job.
|
|
68
|
+
|
|
69
|
+
## What vramd does differently
|
|
70
|
+
|
|
71
|
+
**Admits by the peak, not the weights.** The question isn't "does the model
|
|
72
|
+
fit" — it's "does the inference peak fit". It's the difference between
|
|
73
|
+
refusing in 0.2 s and dying at 80% of the job with the weights already
|
|
74
|
+
loaded.
|
|
75
|
+
|
|
76
|
+
**Each model in its own venv.** A backend is a process with its own
|
|
77
|
+
interpreter. Models with incompatible dependencies — torch 2.x vs 2.y,
|
|
78
|
+
different CUDA wheels — coexist without seeing each other.
|
|
79
|
+
|
|
80
|
+
**Affinity in the queue.** If the head of the queue needs a cold model and a
|
|
81
|
+
job further back needs one already in VRAM, the scheduler skips the head (up
|
|
82
|
+
to 3 times, then it forces). Where a load costs 60 s, this turns a 40-minute
|
|
83
|
+
batch into a 10-minute one.
|
|
84
|
+
|
|
85
|
+
**Cooperative cancellation.** Long jobs report progress per phase and stop
|
|
86
|
+
*between* phases — no CUDA kernels killed mid-flight.
|
|
87
|
+
|
|
88
|
+
**Measures instead of guessing.** `vramd calibrate` runs a real job, samples
|
|
89
|
+
VRAM per process at 20 Hz, and writes the measured footprint. Over ten real
|
|
90
|
+
models on an RTX 4050, hand-written values were off by between −3154 and
|
|
91
|
+
+22448 MiB.
|
|
92
|
+
|
|
93
|
+
## Integrating a model
|
|
94
|
+
|
|
95
|
+
Three methods:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from vramd.worker import WorkerAdapter, run_worker_loop
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Adapter(WorkerAdapter):
|
|
102
|
+
name = "my-model"
|
|
103
|
+
|
|
104
|
+
def load(self, **kw):
|
|
105
|
+
import torch, mylib
|
|
106
|
+
|
|
107
|
+
return mylib.load(device=kw.get("device", "cuda"))
|
|
108
|
+
|
|
109
|
+
def generate(self, model, request):
|
|
110
|
+
if self.should_abort(request):
|
|
111
|
+
return self.cancelled_response()
|
|
112
|
+
self.report_progress(request, 0.0, "generating")
|
|
113
|
+
return {"status": "ok", "output": model(request["prompt"])}
|
|
114
|
+
|
|
115
|
+
def unload(self, model):
|
|
116
|
+
del model
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
run_worker_loop(Adapter, backend_name="my-model")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
And register it — without touching vramd's code:
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
# ~/.config/vramd/backends.d/my-model.yaml
|
|
127
|
+
version: 2
|
|
128
|
+
backends:
|
|
129
|
+
- name: my-model
|
|
130
|
+
adapter: my_package.adapter
|
|
131
|
+
vram_mib: 4200
|
|
132
|
+
priority: 20
|
|
133
|
+
runtime:
|
|
134
|
+
command: ["/opt/my-model/venv/bin/python", "-m", "my_package.worker"]
|
|
135
|
+
env: { HF_HOME: ~/hf-cache }
|
|
136
|
+
load_keys: [device, compute_type]
|
|
137
|
+
shape_keys: [device]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Full runnable example: [`examples/echo-backend/`](examples/echo-backend/).
|
|
141
|
+
|
|
142
|
+
## Calibration
|
|
143
|
+
|
|
144
|
+
The friction of any such system is the "what numbers do I put in the
|
|
145
|
+
descriptor?" question. vramd's answer: none — you measure.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
vramd calibrate my-model --repeats 3 --out ~/.config/vramd/backends.d/measured.yaml
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
It runs the job, splits **CUDA context / weights / activation** at phase
|
|
152
|
+
boundaries, and writes the descriptor. What it catches, that an estimate
|
|
153
|
+
can't:
|
|
154
|
+
|
|
155
|
+
| Signal | Why it matters |
|
|
156
|
+
|---|---|
|
|
157
|
+
| peak at **load** above the inference peak | loading fp16 and quantizing afterwards OOMs before generating |
|
|
158
|
+
| activation ≫ weights | the model loads another model inside `generate` |
|
|
159
|
+
| nothing resident after load | lazy loading: there's nothing to evict |
|
|
160
|
+
| `unload` that doesn't return VRAM | evicting this backend frees nothing — the eviction plan would be fiction |
|
|
161
|
+
| leak on repetition | the resident footprint grows with every job |
|
|
162
|
+
| warmup on 1st inference | calibrating with `--repeats 1` inflates the number |
|
|
163
|
+
|
|
164
|
+
Each measurement keeps the raw samples: `vramd recalibrate report.json`
|
|
165
|
+
recomputes the numbers when the analysis improves, without re-occupying the
|
|
166
|
+
GPU.
|
|
167
|
+
|
|
168
|
+
**Calibration works out of the box for backends that need inputs.** A
|
|
169
|
+
descriptor can declare the generation request and the load kwargs that
|
|
170
|
+
calibration should use by default, so `vramd calibrate <backend>` works
|
|
171
|
+
without flags even for backends that require inputs (`mesh_path`/`output`) or
|
|
172
|
+
specific formats:
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
backends:
|
|
176
|
+
- name: text3d
|
|
177
|
+
calibrate_request: { mesh_path: test-mesh.glb, output: /tmp/out.glb }
|
|
178
|
+
calibrate_load_kwargs: { compute_type: fp16 }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Short names that match a file bundled with the package (`test-mesh.glb`,
|
|
182
|
+
`test-image.png`) are resolved to the packaged path — no test model needed.
|
|
183
|
+
Load-kwargs precedence: hw-auto < descriptor < explicit.
|
|
184
|
+
|
|
185
|
+
## Commands
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
start stop status queue wait cancel flush backends preload evict reap
|
|
189
|
+
respawn zero stats debug bench doctor calibrate recalibrate
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- `vramd status` / `queue` — who has the GPU and what's waiting
|
|
193
|
+
- `vramd zero` — frees all idle VRAM without stopping the supervisor
|
|
194
|
+
- `vramd respawn <backend>` — restarts a single worker (new code) without stopping the queue
|
|
195
|
+
- `vramd doctor` — environment diagnostics
|
|
196
|
+
|
|
197
|
+
**You never need `kill`.** Killing GPU processes works against the queue and
|
|
198
|
+
kills the wrong workload.
|
|
199
|
+
|
|
200
|
+
## Configuration
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
data/backends.yaml (example) → $VRAMD_BACKENDS_FILE → ~/.config/vramd/backends.d/*.yaml
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Per-key overlay: a file with `{name: x, vram_mib: 5632}` fixes only that field
|
|
207
|
+
and inherits the rest. That's how a calibrated descriptor takes effect without
|
|
208
|
+
editing the package.
|
|
209
|
+
|
|
210
|
+
Variables: `VRAMD_BACKENDS_FILE`, `VRAMD_BACKENDS_DIR`, `VRAMD_TOOLS_ROOT`,
|
|
211
|
+
`VRAMD_MAX_INFLIGHT`, `VRAMD_MAX_QUEUE_DEPTH`, `VRAMD_VRAM_SAFETY_MIB`,
|
|
212
|
+
`VRAMD_PRIORITY`.
|
|
213
|
+
|
|
214
|
+
## Known limitations
|
|
215
|
+
|
|
216
|
+
Worth knowing before adopting:
|
|
217
|
+
|
|
218
|
+
- **`MAX_INFLIGHT=1` by default** — one generation at a time. The right choice
|
|
219
|
+
for 6 GB, and it underuses an A100. There is support for >1 with VRAM
|
|
220
|
+
checks, but no real *packing* yet.
|
|
221
|
+
- **Multi-GPU without central placement.** `gpu_ids` is passed to the worker;
|
|
222
|
+
the supervisor neither decides placement nor accounts per device.
|
|
223
|
+
- **POSIX.** Pipe reads use `select`/`O_NONBLOCK`. Windows needs a different
|
|
224
|
+
IO layer.
|
|
225
|
+
- **No authentication.** Unix socket with user permissions. Local, not shared.
|
|
226
|
+
- **Calibration isn't magic.** It measures what your pipeline does. A model
|
|
227
|
+
that loads everything in fp16 at once doesn't start fitting because you
|
|
228
|
+
measured it — you just learn that it doesn't fit, in 0.2 s instead of
|
|
229
|
+
mid-job.
|
|
230
|
+
|
|
231
|
+
## Origin
|
|
232
|
+
|
|
233
|
+
Extracted from the [AiGameKit](https://github.com/maikramer), where it was
|
|
234
|
+
born to have ten generative models (text→image, →3D, →audio, →motion) share a
|
|
235
|
+
6 GB RTX 4050 without manual intervention. The numbers in this README are
|
|
236
|
+
measurements from that card.
|
|
237
|
+
|
|
238
|
+
## Contributing
|
|
239
|
+
|
|
240
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md) — getting started, style, and what this
|
|
241
|
+
project values. The suite runs in ~27 s with no GPU.
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT — see [LICENSE](LICENSE).
|
vramd-0.2.4/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# vramd
|
|
2
|
+
|
|
3
|
+
[](https://github.com/maikramer/vramd/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/vramd/)
|
|
5
|
+
[](https://pypi.org/project/vramd/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**VRAM admission control for generative inference on consumer GPUs.**
|
|
9
|
+
|
|
10
|
+
One process holds the GPU and decides who gets in. It admits by the **real
|
|
11
|
+
peak** — weights + activation + margin, not just weights — queues with
|
|
12
|
+
priority and affinity, evicts by weight+LRU, and runs each model in its own
|
|
13
|
+
process and venv.
|
|
14
|
+
|
|
15
|
+
Built for inference that lasts **seconds to minutes** on a card that can't fit
|
|
16
|
+
everything. It's not an LLM server: it doesn't optimize token throughput, it
|
|
17
|
+
optimizes *fitting*.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install vramd # 9 MB — the supervisor doesn't import torch
|
|
21
|
+
vramd start &
|
|
22
|
+
vramd submit my-model --prompt "…" --wait
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## The problem
|
|
26
|
+
|
|
27
|
+
You have 6 GB of VRAM and five models that, together, ask for 40. Each runs
|
|
28
|
+
fine on its own. Together, the second job starts in the middle of the first
|
|
29
|
+
and both die with `CUDA out of memory` — after the weights were already
|
|
30
|
+
loaded.
|
|
31
|
+
|
|
32
|
+
The usual solutions assume something that isn't true here: that the model fits
|
|
33
|
+
(vLLM, TGI), that the environment is homogeneous (Ray Serve, Triton), or that
|
|
34
|
+
the unit of work is a token rather than a two-minute job.
|
|
35
|
+
|
|
36
|
+
## What vramd does differently
|
|
37
|
+
|
|
38
|
+
**Admits by the peak, not the weights.** The question isn't "does the model
|
|
39
|
+
fit" — it's "does the inference peak fit". It's the difference between
|
|
40
|
+
refusing in 0.2 s and dying at 80% of the job with the weights already
|
|
41
|
+
loaded.
|
|
42
|
+
|
|
43
|
+
**Each model in its own venv.** A backend is a process with its own
|
|
44
|
+
interpreter. Models with incompatible dependencies — torch 2.x vs 2.y,
|
|
45
|
+
different CUDA wheels — coexist without seeing each other.
|
|
46
|
+
|
|
47
|
+
**Affinity in the queue.** If the head of the queue needs a cold model and a
|
|
48
|
+
job further back needs one already in VRAM, the scheduler skips the head (up
|
|
49
|
+
to 3 times, then it forces). Where a load costs 60 s, this turns a 40-minute
|
|
50
|
+
batch into a 10-minute one.
|
|
51
|
+
|
|
52
|
+
**Cooperative cancellation.** Long jobs report progress per phase and stop
|
|
53
|
+
*between* phases — no CUDA kernels killed mid-flight.
|
|
54
|
+
|
|
55
|
+
**Measures instead of guessing.** `vramd calibrate` runs a real job, samples
|
|
56
|
+
VRAM per process at 20 Hz, and writes the measured footprint. Over ten real
|
|
57
|
+
models on an RTX 4050, hand-written values were off by between −3154 and
|
|
58
|
+
+22448 MiB.
|
|
59
|
+
|
|
60
|
+
## Integrating a model
|
|
61
|
+
|
|
62
|
+
Three methods:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from vramd.worker import WorkerAdapter, run_worker_loop
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Adapter(WorkerAdapter):
|
|
69
|
+
name = "my-model"
|
|
70
|
+
|
|
71
|
+
def load(self, **kw):
|
|
72
|
+
import torch, mylib
|
|
73
|
+
|
|
74
|
+
return mylib.load(device=kw.get("device", "cuda"))
|
|
75
|
+
|
|
76
|
+
def generate(self, model, request):
|
|
77
|
+
if self.should_abort(request):
|
|
78
|
+
return self.cancelled_response()
|
|
79
|
+
self.report_progress(request, 0.0, "generating")
|
|
80
|
+
return {"status": "ok", "output": model(request["prompt"])}
|
|
81
|
+
|
|
82
|
+
def unload(self, model):
|
|
83
|
+
del model
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
run_worker_loop(Adapter, backend_name="my-model")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
And register it — without touching vramd's code:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
# ~/.config/vramd/backends.d/my-model.yaml
|
|
94
|
+
version: 2
|
|
95
|
+
backends:
|
|
96
|
+
- name: my-model
|
|
97
|
+
adapter: my_package.adapter
|
|
98
|
+
vram_mib: 4200
|
|
99
|
+
priority: 20
|
|
100
|
+
runtime:
|
|
101
|
+
command: ["/opt/my-model/venv/bin/python", "-m", "my_package.worker"]
|
|
102
|
+
env: { HF_HOME: ~/hf-cache }
|
|
103
|
+
load_keys: [device, compute_type]
|
|
104
|
+
shape_keys: [device]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Full runnable example: [`examples/echo-backend/`](examples/echo-backend/).
|
|
108
|
+
|
|
109
|
+
## Calibration
|
|
110
|
+
|
|
111
|
+
The friction of any such system is the "what numbers do I put in the
|
|
112
|
+
descriptor?" question. vramd's answer: none — you measure.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
vramd calibrate my-model --repeats 3 --out ~/.config/vramd/backends.d/measured.yaml
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
It runs the job, splits **CUDA context / weights / activation** at phase
|
|
119
|
+
boundaries, and writes the descriptor. What it catches, that an estimate
|
|
120
|
+
can't:
|
|
121
|
+
|
|
122
|
+
| Signal | Why it matters |
|
|
123
|
+
|---|---|
|
|
124
|
+
| peak at **load** above the inference peak | loading fp16 and quantizing afterwards OOMs before generating |
|
|
125
|
+
| activation ≫ weights | the model loads another model inside `generate` |
|
|
126
|
+
| nothing resident after load | lazy loading: there's nothing to evict |
|
|
127
|
+
| `unload` that doesn't return VRAM | evicting this backend frees nothing — the eviction plan would be fiction |
|
|
128
|
+
| leak on repetition | the resident footprint grows with every job |
|
|
129
|
+
| warmup on 1st inference | calibrating with `--repeats 1` inflates the number |
|
|
130
|
+
|
|
131
|
+
Each measurement keeps the raw samples: `vramd recalibrate report.json`
|
|
132
|
+
recomputes the numbers when the analysis improves, without re-occupying the
|
|
133
|
+
GPU.
|
|
134
|
+
|
|
135
|
+
**Calibration works out of the box for backends that need inputs.** A
|
|
136
|
+
descriptor can declare the generation request and the load kwargs that
|
|
137
|
+
calibration should use by default, so `vramd calibrate <backend>` works
|
|
138
|
+
without flags even for backends that require inputs (`mesh_path`/`output`) or
|
|
139
|
+
specific formats:
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
backends:
|
|
143
|
+
- name: text3d
|
|
144
|
+
calibrate_request: { mesh_path: test-mesh.glb, output: /tmp/out.glb }
|
|
145
|
+
calibrate_load_kwargs: { compute_type: fp16 }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Short names that match a file bundled with the package (`test-mesh.glb`,
|
|
149
|
+
`test-image.png`) are resolved to the packaged path — no test model needed.
|
|
150
|
+
Load-kwargs precedence: hw-auto < descriptor < explicit.
|
|
151
|
+
|
|
152
|
+
## Commands
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
start stop status queue wait cancel flush backends preload evict reap
|
|
156
|
+
respawn zero stats debug bench doctor calibrate recalibrate
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- `vramd status` / `queue` — who has the GPU and what's waiting
|
|
160
|
+
- `vramd zero` — frees all idle VRAM without stopping the supervisor
|
|
161
|
+
- `vramd respawn <backend>` — restarts a single worker (new code) without stopping the queue
|
|
162
|
+
- `vramd doctor` — environment diagnostics
|
|
163
|
+
|
|
164
|
+
**You never need `kill`.** Killing GPU processes works against the queue and
|
|
165
|
+
kills the wrong workload.
|
|
166
|
+
|
|
167
|
+
## Configuration
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
data/backends.yaml (example) → $VRAMD_BACKENDS_FILE → ~/.config/vramd/backends.d/*.yaml
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Per-key overlay: a file with `{name: x, vram_mib: 5632}` fixes only that field
|
|
174
|
+
and inherits the rest. That's how a calibrated descriptor takes effect without
|
|
175
|
+
editing the package.
|
|
176
|
+
|
|
177
|
+
Variables: `VRAMD_BACKENDS_FILE`, `VRAMD_BACKENDS_DIR`, `VRAMD_TOOLS_ROOT`,
|
|
178
|
+
`VRAMD_MAX_INFLIGHT`, `VRAMD_MAX_QUEUE_DEPTH`, `VRAMD_VRAM_SAFETY_MIB`,
|
|
179
|
+
`VRAMD_PRIORITY`.
|
|
180
|
+
|
|
181
|
+
## Known limitations
|
|
182
|
+
|
|
183
|
+
Worth knowing before adopting:
|
|
184
|
+
|
|
185
|
+
- **`MAX_INFLIGHT=1` by default** — one generation at a time. The right choice
|
|
186
|
+
for 6 GB, and it underuses an A100. There is support for >1 with VRAM
|
|
187
|
+
checks, but no real *packing* yet.
|
|
188
|
+
- **Multi-GPU without central placement.** `gpu_ids` is passed to the worker;
|
|
189
|
+
the supervisor neither decides placement nor accounts per device.
|
|
190
|
+
- **POSIX.** Pipe reads use `select`/`O_NONBLOCK`. Windows needs a different
|
|
191
|
+
IO layer.
|
|
192
|
+
- **No authentication.** Unix socket with user permissions. Local, not shared.
|
|
193
|
+
- **Calibration isn't magic.** It measures what your pipeline does. A model
|
|
194
|
+
that loads everything in fp16 at once doesn't start fitting because you
|
|
195
|
+
measured it — you just learn that it doesn't fit, in 0.2 s instead of
|
|
196
|
+
mid-job.
|
|
197
|
+
|
|
198
|
+
## Origin
|
|
199
|
+
|
|
200
|
+
Extracted from the [AiGameKit](https://github.com/maikramer), where it was
|
|
201
|
+
born to have ten generative models (text→image, →3D, →audio, →motion) share a
|
|
202
|
+
6 GB RTX 4050 without manual intervention. The numbers in this README are
|
|
203
|
+
measurements from that card.
|
|
204
|
+
|
|
205
|
+
## Contributing
|
|
206
|
+
|
|
207
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md) — getting started, style, and what this
|
|
208
|
+
project values. The suite runs in ~27 s with no GPU.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vramd"
|
|
7
|
-
version = "0.2.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.2.4"
|
|
8
|
+
description = "VRAM admission control for generative inference on consumer GPUs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Maikeu Locatelli" }]
|
|
11
11
|
requires-python = ">=3.11"
|
|
@@ -21,8 +21,8 @@ classifiers = [
|
|
|
21
21
|
"Topic :: System :: Distributed Computing",
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
# CUDA.
|
|
24
|
+
# Deliberately light core: the supervisor doesn't import torch nor create a
|
|
25
|
+
# CUDA context. Models live in the workers, each in its own venv.
|
|
26
26
|
dependencies = [
|
|
27
27
|
"click>=8.1.0",
|
|
28
28
|
"rich>=13.0.0",
|
|
@@ -48,7 +48,7 @@ Changelog = "https://github.com/maikramer/vramd/blob/main/CHANGELOG.md"
|
|
|
48
48
|
where = ["src"]
|
|
49
49
|
|
|
50
50
|
[tool.setuptools.package-data]
|
|
51
|
-
vramd = ["data/*.yaml", "py.typed"]
|
|
51
|
+
vramd = ["data/*.yaml", "data/*.glb", "data/*.png", "py.typed"]
|
|
52
52
|
|
|
53
53
|
[tool.pytest.ini_options]
|
|
54
54
|
pythonpath = ["src", "."]
|
|
@@ -1437,6 +1437,27 @@ def _render_comparison(rows: list[Any]) -> int:
|
|
|
1437
1437
|
return under
|
|
1438
1438
|
|
|
1439
1439
|
|
|
1440
|
+
def _resolve_calibrate_assets(request: dict[str, Any]) -> None:
|
|
1441
|
+
"""Resolve inputs de teste do calibrador para o package data do vramd.
|
|
1442
|
+
|
|
1443
|
+
Um descriptor declara ``calibrate_request: {mesh_path: test-mesh.glb, …}``
|
|
1444
|
+
com um NOME curto em vez de um path. Se o nome corresponde a um ficheiro
|
|
1445
|
+
empacotado em ``vramd/data/`` (test-mesh.glb, test-image.png), é
|
|
1446
|
+
substituído pelo path absoluto do pacote — o utilizador comum não precisa
|
|
1447
|
+
de ter nem gerar um modelo de teste.
|
|
1448
|
+
"""
|
|
1449
|
+
from importlib import resources
|
|
1450
|
+
|
|
1451
|
+
for key, value in list(request.items()):
|
|
1452
|
+
if not isinstance(value, str) or not value:
|
|
1453
|
+
continue
|
|
1454
|
+
if Path(value).is_absolute() or "/" in value or "\\" in value:
|
|
1455
|
+
continue # path real — não mexer
|
|
1456
|
+
bundled = resources.files("vramd").joinpath("data", value)
|
|
1457
|
+
if bundled.is_file():
|
|
1458
|
+
request[key] = str(bundled)
|
|
1459
|
+
|
|
1460
|
+
|
|
1440
1461
|
@cli.command("calibrate")
|
|
1441
1462
|
@click.argument("backend")
|
|
1442
1463
|
@click.option("--prompt", default=None, help="Prompt do job de calibração (atalho para --request-json).")
|
|
@@ -1540,7 +1561,12 @@ def calibrate_cmd(
|
|
|
1540
1561
|
load_kwargs.update(explicit_kwargs)
|
|
1541
1562
|
if quant:
|
|
1542
1563
|
load_kwargs.setdefault("sdnq_preset", quant)
|
|
1543
|
-
|
|
1564
|
+
# quant_preset é o alias Text2D de sdnq_preset — sem isto uma calibração
|
|
1565
|
+
# com quant_preset ficava etiquetada quant_mode=none e o admit descartava
|
|
1566
|
+
# a medição (quant mismatch) quando a produção pedia o preset.
|
|
1567
|
+
quant_mode = str(
|
|
1568
|
+
load_kwargs.get("sdnq_preset") or load_kwargs.get("quant_preset") or load_kwargs.get("quant_mode") or "none"
|
|
1569
|
+
)
|
|
1544
1570
|
|
|
1545
1571
|
if request_json:
|
|
1546
1572
|
request = json.loads(Path(request_json).read_text(encoding="utf-8"))
|
|
@@ -1553,6 +1579,7 @@ def calibrate_cmd(
|
|
|
1553
1579
|
if output_path:
|
|
1554
1580
|
request["output"] = output_path
|
|
1555
1581
|
request.setdefault("output", f"/tmp/vramd-calibrate-{backend}.bin")
|
|
1582
|
+
_resolve_calibrate_assets(request)
|
|
1556
1583
|
|
|
1557
1584
|
pool = SubprocessWorkerPool()
|
|
1558
1585
|
runner = CalibrationRunner(pool)
|
|
Binary file
|
|
Binary file
|