vramd 0.2.2__tar.gz → 0.2.3__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.
Files changed (89) hide show
  1. vramd-0.2.3/PKG-INFO +245 -0
  2. vramd-0.2.3/README.md +212 -0
  3. {vramd-0.2.2 → vramd-0.2.3}/pyproject.toml +5 -5
  4. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/__init__.py +1 -1
  5. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/cli.py +22 -0
  6. vramd-0.2.3/src/vramd/data/test-image.png +0 -0
  7. vramd-0.2.3/src/vramd/data/test-mesh.glb +0 -0
  8. vramd-0.2.3/src/vramd.egg-info/PKG-INFO +245 -0
  9. {vramd-0.2.2 → vramd-0.2.3}/src/vramd.egg-info/SOURCES.txt +2 -0
  10. {vramd-0.2.2 → vramd-0.2.3}/tests/test_calibrate_cli.py +29 -0
  11. vramd-0.2.2/PKG-INFO +0 -221
  12. vramd-0.2.2/README.md +0 -188
  13. vramd-0.2.2/src/vramd.egg-info/PKG-INFO +0 -221
  14. {vramd-0.2.2 → vramd-0.2.3}/LICENSE +0 -0
  15. {vramd-0.2.2 → vramd-0.2.3}/setup.cfg +0 -0
  16. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/__main__.py +0 -0
  17. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/adapter.py +0 -0
  18. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/backend_manager.py +0 -0
  19. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/calibrate/__init__.py +0 -0
  20. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/calibrate/analysis.py +0 -0
  21. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/calibrate/compare.py +0 -0
  22. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/calibrate/emit.py +0 -0
  23. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/calibrate/hw_auto.py +0 -0
  24. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/calibrate/runner.py +0 -0
  25. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/calibrate/sampler.py +0 -0
  26. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/calibrate/serde.py +0 -0
  27. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/client.py +0 -0
  28. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/data/backends.yaml +0 -0
  29. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/dispatcher.py +0 -0
  30. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/errors.py +0 -0
  31. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/footprints.py +0 -0
  32. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/gpu.py +0 -0
  33. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/idle_evictor.py +0 -0
  34. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/job_queue.py +0 -0
  35. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/logging.py +0 -0
  36. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/process_guard.py +0 -0
  37. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/protocol.py +0 -0
  38. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/py.typed +0 -0
  39. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/registry.py +0 -0
  40. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/scheduler.py +0 -0
  41. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/server.py +0 -0
  42. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/stats.py +0 -0
  43. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/subprocess_pool.py +0 -0
  44. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/toolchain.py +0 -0
  45. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/vram_planner.py +0 -0
  46. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/worker/__init__.py +0 -0
  47. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/worker/adapter.py +0 -0
  48. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/worker/protocol.py +0 -0
  49. {vramd-0.2.2 → vramd-0.2.3}/src/vramd/worker/serve.py +0 -0
  50. {vramd-0.2.2 → vramd-0.2.3}/src/vramd.egg-info/dependency_links.txt +0 -0
  51. {vramd-0.2.2 → vramd-0.2.3}/src/vramd.egg-info/entry_points.txt +0 -0
  52. {vramd-0.2.2 → vramd-0.2.3}/src/vramd.egg-info/requires.txt +0 -0
  53. {vramd-0.2.2 → vramd-0.2.3}/src/vramd.egg-info/top_level.txt +0 -0
  54. {vramd-0.2.2 → vramd-0.2.3}/tests/test_abort_progress.py +0 -0
  55. {vramd-0.2.2 → vramd-0.2.3}/tests/test_backend_manager.py +0 -0
  56. {vramd-0.2.2 → vramd-0.2.3}/tests/test_backend_manager_hybrid.py +0 -0
  57. {vramd-0.2.2 → vramd-0.2.3}/tests/test_calibrate_analysis.py +0 -0
  58. {vramd-0.2.2 → vramd-0.2.3}/tests/test_calibrate_emit_compare.py +0 -0
  59. {vramd-0.2.2 → vramd-0.2.3}/tests/test_calibrate_hw_auto.py +0 -0
  60. {vramd-0.2.2 → vramd-0.2.3}/tests/test_calibrate_runner.py +0 -0
  61. {vramd-0.2.2 → vramd-0.2.3}/tests/test_calibrate_sampler.py +0 -0
  62. {vramd-0.2.2 → vramd-0.2.3}/tests/test_calibrate_serde.py +0 -0
  63. {vramd-0.2.2 → vramd-0.2.3}/tests/test_cli_debug_stats_bench.py +0 -0
  64. {vramd-0.2.2 → vramd-0.2.3}/tests/test_gpu_ids_load.py +0 -0
  65. {vramd-0.2.2 → vramd-0.2.3}/tests/test_idle_evictor.py +0 -0
  66. {vramd-0.2.2 → vramd-0.2.3}/tests/test_job_queue.py +0 -0
  67. {vramd-0.2.2 → vramd-0.2.3}/tests/test_job_wal.py +0 -0
  68. {vramd-0.2.2 → vramd-0.2.3}/tests/test_peak_profile.py +0 -0
  69. {vramd-0.2.2 → vramd-0.2.3}/tests/test_phase_progress.py +0 -0
  70. {vramd-0.2.2 → vramd-0.2.3}/tests/test_process_guard.py +0 -0
  71. {vramd-0.2.2 → vramd-0.2.3}/tests/test_protocol_env.py +0 -0
  72. {vramd-0.2.2 → vramd-0.2.3}/tests/test_queue_metrics_eta.py +0 -0
  73. {vramd-0.2.2 → vramd-0.2.3}/tests/test_registry.py +0 -0
  74. {vramd-0.2.2 → vramd-0.2.3}/tests/test_registry_layers.py +0 -0
  75. {vramd-0.2.2 → vramd-0.2.3}/tests/test_respawn.py +0 -0
  76. {vramd-0.2.2 → vramd-0.2.3}/tests/test_runtime_spawn.py +0 -0
  77. {vramd-0.2.2 → vramd-0.2.3}/tests/test_scheduler.py +0 -0
  78. {vramd-0.2.2 → vramd-0.2.3}/tests/test_server.py +0 -0
  79. {vramd-0.2.2 → vramd-0.2.3}/tests/test_stats.py +0 -0
  80. {vramd-0.2.2 → vramd-0.2.3}/tests/test_subprocess_integration.py +0 -0
  81. {vramd-0.2.2 → vramd-0.2.3}/tests/test_subprocess_per_backend_e2e.py +0 -0
  82. {vramd-0.2.2 → vramd-0.2.3}/tests/test_subprocess_pool.py +0 -0
  83. {vramd-0.2.2 → vramd-0.2.3}/tests/test_toolchain.py +0 -0
  84. {vramd-0.2.2 → vramd-0.2.3}/tests/test_unload_frees_vram.py +0 -0
  85. {vramd-0.2.2 → vramd-0.2.3}/tests/test_vram_admit.py +0 -0
  86. {vramd-0.2.2 → vramd-0.2.3}/tests/test_vram_planner.py +0 -0
  87. {vramd-0.2.2 → vramd-0.2.3}/tests/test_vramd_coverage_100.py +0 -0
  88. {vramd-0.2.2 → vramd-0.2.3}/tests/test_worker.py +0 -0
  89. {vramd-0.2.2 → vramd-0.2.3}/tests/test_zero_vram.py +0 -0
vramd-0.2.3/PKG-INFO ADDED
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: vramd
3
+ Version: 0.2.3
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
+ [![CI](https://github.com/maikramer/vramd/actions/workflows/ci.yml/badge.svg)](https://github.com/maikramer/vramd/actions/workflows/ci.yml)
37
+ [![PyPI](https://img.shields.io/pypi/v/vramd.svg)](https://pypi.org/project/vramd/)
38
+ [![Python](https://img.shields.io/pypi/pyversions/vramd.svg)](https://pypi.org/project/vramd/)
39
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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.3/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # vramd
2
+
3
+ [![CI](https://github.com/maikramer/vramd/actions/workflows/ci.yml/badge.svg)](https://github.com/maikramer/vramd/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/vramd.svg)](https://pypi.org/project/vramd/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/vramd.svg)](https://pypi.org/project/vramd/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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.2"
8
- description = "Controlo de admissão de VRAM para inferência generativa em GPUs de consumo"
7
+ version = "0.2.3"
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
- # Núcleo leve de propósito: o supervisor não importa torch e nem cria contexto
25
- # CUDA. Os modelos vivem nos workers, cada um no seu venv.
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", "."]
@@ -19,5 +19,5 @@ adivinhar: o número que o admit usa vem do driver, não de uma estimativa.
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
- __version__ = "0.2.2"
22
+ __version__ = "0.2.3"
23
23
  __all__ = ["__version__"]
@@ -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).")
@@ -1553,6 +1574,7 @@ def calibrate_cmd(
1553
1574
  if output_path:
1554
1575
  request["output"] = output_path
1555
1576
  request.setdefault("output", f"/tmp/vramd-calibrate-{backend}.bin")
1577
+ _resolve_calibrate_assets(request)
1556
1578
 
1557
1579
  pool = SubprocessWorkerPool()
1558
1580
  runner = CalibrationRunner(pool)
Binary file