vramd 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.
Files changed (83) hide show
  1. vramd-0.1.0/LICENSE +21 -0
  2. vramd-0.1.0/PKG-INFO +221 -0
  3. vramd-0.1.0/README.md +188 -0
  4. vramd-0.1.0/pyproject.toml +67 -0
  5. vramd-0.1.0/setup.cfg +4 -0
  6. vramd-0.1.0/src/vramd/__init__.py +23 -0
  7. vramd-0.1.0/src/vramd/__main__.py +7 -0
  8. vramd-0.1.0/src/vramd/adapter.py +154 -0
  9. vramd-0.1.0/src/vramd/backend_manager.py +1693 -0
  10. vramd-0.1.0/src/vramd/calibrate/__init__.py +59 -0
  11. vramd-0.1.0/src/vramd/calibrate/analysis.py +619 -0
  12. vramd-0.1.0/src/vramd/calibrate/compare.py +216 -0
  13. vramd-0.1.0/src/vramd/calibrate/emit.py +228 -0
  14. vramd-0.1.0/src/vramd/calibrate/hw_auto.py +148 -0
  15. vramd-0.1.0/src/vramd/calibrate/runner.py +392 -0
  16. vramd-0.1.0/src/vramd/calibrate/sampler.py +374 -0
  17. vramd-0.1.0/src/vramd/calibrate/serde.py +136 -0
  18. vramd-0.1.0/src/vramd/cli.py +1709 -0
  19. vramd-0.1.0/src/vramd/client.py +816 -0
  20. vramd-0.1.0/src/vramd/data/backends.yaml +51 -0
  21. vramd-0.1.0/src/vramd/dispatcher.py +418 -0
  22. vramd-0.1.0/src/vramd/errors.py +52 -0
  23. vramd-0.1.0/src/vramd/footprints.py +182 -0
  24. vramd-0.1.0/src/vramd/gpu.py +1029 -0
  25. vramd-0.1.0/src/vramd/idle_evictor.py +170 -0
  26. vramd-0.1.0/src/vramd/job_queue.py +727 -0
  27. vramd-0.1.0/src/vramd/logging.py +452 -0
  28. vramd-0.1.0/src/vramd/process_guard.py +406 -0
  29. vramd-0.1.0/src/vramd/protocol.py +230 -0
  30. vramd-0.1.0/src/vramd/py.typed +0 -0
  31. vramd-0.1.0/src/vramd/registry.py +379 -0
  32. vramd-0.1.0/src/vramd/scheduler.py +77 -0
  33. vramd-0.1.0/src/vramd/server.py +1090 -0
  34. vramd-0.1.0/src/vramd/stats.py +184 -0
  35. vramd-0.1.0/src/vramd/subprocess_pool.py +690 -0
  36. vramd-0.1.0/src/vramd/toolchain.py +77 -0
  37. vramd-0.1.0/src/vramd/vram_planner.py +136 -0
  38. vramd-0.1.0/src/vramd/worker/__init__.py +54 -0
  39. vramd-0.1.0/src/vramd/worker/adapter.py +183 -0
  40. vramd-0.1.0/src/vramd/worker/protocol.py +200 -0
  41. vramd-0.1.0/src/vramd/worker/serve.py +436 -0
  42. vramd-0.1.0/src/vramd.egg-info/PKG-INFO +221 -0
  43. vramd-0.1.0/src/vramd.egg-info/SOURCES.txt +81 -0
  44. vramd-0.1.0/src/vramd.egg-info/dependency_links.txt +1 -0
  45. vramd-0.1.0/src/vramd.egg-info/entry_points.txt +2 -0
  46. vramd-0.1.0/src/vramd.egg-info/requires.txt +11 -0
  47. vramd-0.1.0/src/vramd.egg-info/top_level.txt +1 -0
  48. vramd-0.1.0/tests/test_abort_progress.py +90 -0
  49. vramd-0.1.0/tests/test_backend_manager.py +293 -0
  50. vramd-0.1.0/tests/test_backend_manager_hybrid.py +314 -0
  51. vramd-0.1.0/tests/test_calibrate_analysis.py +424 -0
  52. vramd-0.1.0/tests/test_calibrate_cli.py +193 -0
  53. vramd-0.1.0/tests/test_calibrate_emit_compare.py +241 -0
  54. vramd-0.1.0/tests/test_calibrate_hw_auto.py +207 -0
  55. vramd-0.1.0/tests/test_calibrate_runner.py +507 -0
  56. vramd-0.1.0/tests/test_calibrate_sampler.py +325 -0
  57. vramd-0.1.0/tests/test_calibrate_serde.py +200 -0
  58. vramd-0.1.0/tests/test_cli_debug_stats_bench.py +185 -0
  59. vramd-0.1.0/tests/test_gpu_ids_load.py +79 -0
  60. vramd-0.1.0/tests/test_idle_evictor.py +250 -0
  61. vramd-0.1.0/tests/test_job_queue.py +245 -0
  62. vramd-0.1.0/tests/test_job_wal.py +146 -0
  63. vramd-0.1.0/tests/test_peak_profile.py +169 -0
  64. vramd-0.1.0/tests/test_phase_progress.py +63 -0
  65. vramd-0.1.0/tests/test_process_guard.py +219 -0
  66. vramd-0.1.0/tests/test_protocol_env.py +48 -0
  67. vramd-0.1.0/tests/test_queue_metrics_eta.py +131 -0
  68. vramd-0.1.0/tests/test_registry.py +103 -0
  69. vramd-0.1.0/tests/test_registry_layers.py +267 -0
  70. vramd-0.1.0/tests/test_respawn.py +251 -0
  71. vramd-0.1.0/tests/test_runtime_spawn.py +175 -0
  72. vramd-0.1.0/tests/test_scheduler.py +142 -0
  73. vramd-0.1.0/tests/test_server.py +648 -0
  74. vramd-0.1.0/tests/test_stats.py +132 -0
  75. vramd-0.1.0/tests/test_subprocess_integration.py +147 -0
  76. vramd-0.1.0/tests/test_subprocess_per_backend_e2e.py +217 -0
  77. vramd-0.1.0/tests/test_subprocess_pool.py +464 -0
  78. vramd-0.1.0/tests/test_unload_frees_vram.py +135 -0
  79. vramd-0.1.0/tests/test_vram_admit.py +125 -0
  80. vramd-0.1.0/tests/test_vram_planner.py +148 -0
  81. vramd-0.1.0/tests/test_vramd_coverage_100.py +430 -0
  82. vramd-0.1.0/tests/test_worker.py +500 -0
  83. vramd-0.1.0/tests/test_zero_vram.py +172 -0
vramd-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maikeu Locatelli
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.
vramd-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.4
2
+ Name: vramd
3
+ Version: 0.1.0
4
+ Summary: Controlo de admissão de VRAM para inferência generativa em GPUs de consumo
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
+ **Controlo de admissão de VRAM para inferência generativa em GPUs de consumo.**
42
+
43
+ Um processo detém a GPU e decide quem entra. Admite pelo **pico real** — pesos +
44
+ activação + margem, não só pesos — põe em fila com prioridade e afinidade,
45
+ evicta por peso+LRU, e corre cada modelo num processo e venv próprios.
46
+
47
+ Feito para inferência que dura **segundos a minutos** numa placa que não chega
48
+ para tudo. Não é um servidor de LLM: não optimiza throughput de tokens, optimiza
49
+ *caber*.
50
+
51
+ ```bash
52
+ pip install vramd # 9 MB — o supervisor não importa torch
53
+ vramd start &
54
+ vramd submit meu-modelo --prompt "…" --wait
55
+ ```
56
+
57
+ ## O problema
58
+
59
+ Tens 6 GB de VRAM e cinco modelos que, somados, pedem 40. Cada um corre bem
60
+ sozinho. Juntos, o segundo job entra a meio do primeiro e ambos morrem com
61
+ `CUDA out of memory` — depois de já terem carregado os pesos.
62
+
63
+ As soluções habituais assumem o que aqui não se verifica: que o modelo cabe
64
+ (vLLM, TGI), que o ambiente é homogéneo (Ray Serve, Triton), ou que a unidade de
65
+ trabalho é um token e não um job de dois minutos.
66
+
67
+ ## O que o vramd faz de diferente
68
+
69
+ **Admite pelo pico, não pelos pesos.** A pergunta não é "o modelo cabe" — é
70
+ "cabe o pico da inferência". É a diferença entre recusar em 0.2 s e morrer a 80%
71
+ do job com os pesos já carregados.
72
+
73
+ **Cada modelo no seu venv.** Um backend é um processo com o seu próprio
74
+ interpretador. Modelos com dependências incompatíveis — torch 2.x contra 2.y,
75
+ wheels CUDA diferentes — coexistem sem se verem.
76
+
77
+ **Afinidade na fila.** Se a cabeça precisa de um modelo frio e mais atrás há um
78
+ job cujo modelo já está em VRAM, o scheduler salta a cabeça (até 3 vezes, depois
79
+ força). Onde um load custa 60 s, isto muda um batch de 40 minutos para 10.
80
+
81
+ **Cancelamento cooperativo.** Jobs longos reportam progresso por fase e param
82
+ *entre* fases — sem matar kernels CUDA a meio.
83
+
84
+ **Mede em vez de adivinhar.** `vramd calibrate` corre um job real, amostra a
85
+ VRAM por processo a 20 Hz e escreve o footprint medido. Sobre dez modelos reais
86
+ numa RTX 4050, os valores escritos à mão erravam entre −3154 e +22448 MiB.
87
+
88
+ ## Integrar um modelo
89
+
90
+ Três métodos:
91
+
92
+ ```python
93
+ from vramd.worker import WorkerAdapter, run_worker_loop
94
+
95
+
96
+ class Adapter(WorkerAdapter):
97
+ name = "meu-modelo"
98
+
99
+ def load(self, **kw):
100
+ import torch, meulib
101
+
102
+ return meulib.load(device=kw.get("device", "cuda"))
103
+
104
+ def generate(self, model, request):
105
+ if self.should_abort(request):
106
+ return self.cancelled_response()
107
+ self.report_progress(request, 0.0, "a gerar")
108
+ return {"status": "ok", "output": model(request["prompt"])}
109
+
110
+ def unload(self, model):
111
+ del model
112
+
113
+
114
+ if __name__ == "__main__":
115
+ run_worker_loop(Adapter, backend_name="meu-modelo")
116
+ ```
117
+
118
+ E registá-lo — sem tocar no código do vramd:
119
+
120
+ ```yaml
121
+ # ~/.config/vramd/backends.d/meu-modelo.yaml
122
+ version: 2
123
+ backends:
124
+ - name: meu-modelo
125
+ adapter: meu_pacote.adapter
126
+ vram_mib: 4200
127
+ priority: 20
128
+ runtime:
129
+ command: ["/opt/meu-modelo/venv/bin/python", "-m", "meu_pacote.worker"]
130
+ env: { HF_HOME: ~/hf-cache }
131
+ load_keys: [device, compute_type]
132
+ shape_keys: [device]
133
+ ```
134
+
135
+ Exemplo completo e executável: [`examples/echo-backend/`](examples/echo-backend/).
136
+
137
+ ## Calibração
138
+
139
+ O atrito de qualquer sistema destes é a pergunta "que números meto no
140
+ descriptor?". A resposta do vramd é: nenhum — mede-se.
141
+
142
+ ```bash
143
+ vramd calibrate meu-modelo --repeats 3 --out ~/.config/vramd/backends.d/medido.yaml
144
+ ```
145
+
146
+ Corre o job, separa **contexto CUDA / pesos / activação** pelas fronteiras de
147
+ fase, e escreve o descriptor. O que apanha, e que uma estimativa não apanha:
148
+
149
+ | Sinal | Porque importa |
150
+ |---|---|
151
+ | pico no **load** acima do da inferência | carregar fp16 e quantizar depois OOMa antes de gerar |
152
+ | activação ≫ pesos | o modelo carrega outro modelo dentro do `generate` |
153
+ | nada residente após o load | carga preguiçosa: não há o que evictar |
154
+ | `unload` que não devolve VRAM | evictar este backend não liberta nada — o plano de evicção seria ficção |
155
+ | fuga por repetição | o residente cresce a cada job |
156
+ | warmup na 1.ª inferência | calibrar com `--repeats 1` inflaciona o número |
157
+
158
+ Cada medição guarda as amostras cruas: `vramd recalibrate relatorio.json` refaz
159
+ os números quando a análise melhora, sem voltar a ocupar a GPU.
160
+
161
+ ## Comandos
162
+
163
+ ```
164
+ start stop status queue wait cancel flush backends preload evict reap
165
+ respawn zero stats debug bench doctor calibrate recalibrate
166
+ ```
167
+
168
+ - `vramd status` / `queue` — quem tem a GPU e o que espera
169
+ - `vramd zero` — liberta toda a VRAM ociosa sem parar o supervisor
170
+ - `vramd respawn <backend>` — reinicia só um worker (código novo) sem parar a fila
171
+ - `vramd doctor` — diagnóstico de ambiente
172
+
173
+ **Nunca é preciso `kill`.** Matar processos GPU corre contra a fila e mata o
174
+ workload errado.
175
+
176
+ ## Configuração
177
+
178
+ ```
179
+ data/backends.yaml (exemplo) → $VRAMD_BACKENDS_FILE → ~/.config/vramd/backends.d/*.yaml
180
+ ```
181
+
182
+ Sobreposição **por chave**: um ficheiro com `{name: x, vram_mib: 5632}` corrige
183
+ só esse campo e herda o resto. É assim que um descriptor calibrado entra em
184
+ vigor sem editar o pacote.
185
+
186
+ Variáveis: `VRAMD_BACKENDS_FILE`, `VRAMD_BACKENDS_DIR`, `VRAMD_TOOLS_ROOT`,
187
+ `VRAMD_MAX_INFLIGHT`, `VRAMD_MAX_QUEUE_DEPTH`, `VRAMD_VRAM_SAFETY_MIB`,
188
+ `VRAMD_PRIORITY`.
189
+
190
+ ## Limites conhecidos
191
+
192
+ Vale a pena saber antes de adoptar:
193
+
194
+ - **`MAX_INFLIGHT=1` por omissão** — uma geração de cada vez. É a escolha certa
195
+ para 6 GB e subutiliza uma A100. Há suporte para >1 com verificação de VRAM,
196
+ mas falta *packing* a sério.
197
+ - **Multi-GPU sem placement central.** `gpu_ids` é passado ao worker; o
198
+ supervisor não decide colocação nem contabiliza por device.
199
+ - **POSIX.** A leitura dos pipes usa `select`/`O_NONBLOCK`. Windows precisa de
200
+ uma camada de IO diferente.
201
+ - **Sem autenticação.** Socket unix com permissões de utilizador. Local, não
202
+ partilhado.
203
+ - **A calibração não faz milagres.** Mede o que o teu pipeline faz. Um modelo
204
+ que carrega tudo em fp16 de uma vez não passa a caber por ser medido — só
205
+ passas a saber que não cabe, e em 0.2 s em vez de a meio do job.
206
+
207
+ ## Origem
208
+
209
+ Extraído do [AiGameKit](https://github.com/maikramer), onde nasceu para pôr dez
210
+ modelos generativos (texto→imagem, →3D, →áudio, →movimento) a partilhar uma RTX
211
+ 4050 de 6 GB sem intervenção manual. Os números deste README são medições dessa
212
+ placa.
213
+
214
+ ## Contribuir
215
+
216
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) — arrancar, estilo, e o que este projeto
217
+ valoriza. A suite corre em ~27 s sem GPU.
218
+
219
+ ## Licença
220
+
221
+ MIT — ver [LICENSE](LICENSE).
vramd-0.1.0/README.md ADDED
@@ -0,0 +1,188 @@
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
+ **Controlo de admissão de VRAM para inferência generativa em GPUs de consumo.**
9
+
10
+ Um processo detém a GPU e decide quem entra. Admite pelo **pico real** — pesos +
11
+ activação + margem, não só pesos — põe em fila com prioridade e afinidade,
12
+ evicta por peso+LRU, e corre cada modelo num processo e venv próprios.
13
+
14
+ Feito para inferência que dura **segundos a minutos** numa placa que não chega
15
+ para tudo. Não é um servidor de LLM: não optimiza throughput de tokens, optimiza
16
+ *caber*.
17
+
18
+ ```bash
19
+ pip install vramd # 9 MB — o supervisor não importa torch
20
+ vramd start &
21
+ vramd submit meu-modelo --prompt "…" --wait
22
+ ```
23
+
24
+ ## O problema
25
+
26
+ Tens 6 GB de VRAM e cinco modelos que, somados, pedem 40. Cada um corre bem
27
+ sozinho. Juntos, o segundo job entra a meio do primeiro e ambos morrem com
28
+ `CUDA out of memory` — depois de já terem carregado os pesos.
29
+
30
+ As soluções habituais assumem o que aqui não se verifica: que o modelo cabe
31
+ (vLLM, TGI), que o ambiente é homogéneo (Ray Serve, Triton), ou que a unidade de
32
+ trabalho é um token e não um job de dois minutos.
33
+
34
+ ## O que o vramd faz de diferente
35
+
36
+ **Admite pelo pico, não pelos pesos.** A pergunta não é "o modelo cabe" — é
37
+ "cabe o pico da inferência". É a diferença entre recusar em 0.2 s e morrer a 80%
38
+ do job com os pesos já carregados.
39
+
40
+ **Cada modelo no seu venv.** Um backend é um processo com o seu próprio
41
+ interpretador. Modelos com dependências incompatíveis — torch 2.x contra 2.y,
42
+ wheels CUDA diferentes — coexistem sem se verem.
43
+
44
+ **Afinidade na fila.** Se a cabeça precisa de um modelo frio e mais atrás há um
45
+ job cujo modelo já está em VRAM, o scheduler salta a cabeça (até 3 vezes, depois
46
+ força). Onde um load custa 60 s, isto muda um batch de 40 minutos para 10.
47
+
48
+ **Cancelamento cooperativo.** Jobs longos reportam progresso por fase e param
49
+ *entre* fases — sem matar kernels CUDA a meio.
50
+
51
+ **Mede em vez de adivinhar.** `vramd calibrate` corre um job real, amostra a
52
+ VRAM por processo a 20 Hz e escreve o footprint medido. Sobre dez modelos reais
53
+ numa RTX 4050, os valores escritos à mão erravam entre −3154 e +22448 MiB.
54
+
55
+ ## Integrar um modelo
56
+
57
+ Três métodos:
58
+
59
+ ```python
60
+ from vramd.worker import WorkerAdapter, run_worker_loop
61
+
62
+
63
+ class Adapter(WorkerAdapter):
64
+ name = "meu-modelo"
65
+
66
+ def load(self, **kw):
67
+ import torch, meulib
68
+
69
+ return meulib.load(device=kw.get("device", "cuda"))
70
+
71
+ def generate(self, model, request):
72
+ if self.should_abort(request):
73
+ return self.cancelled_response()
74
+ self.report_progress(request, 0.0, "a gerar")
75
+ return {"status": "ok", "output": model(request["prompt"])}
76
+
77
+ def unload(self, model):
78
+ del model
79
+
80
+
81
+ if __name__ == "__main__":
82
+ run_worker_loop(Adapter, backend_name="meu-modelo")
83
+ ```
84
+
85
+ E registá-lo — sem tocar no código do vramd:
86
+
87
+ ```yaml
88
+ # ~/.config/vramd/backends.d/meu-modelo.yaml
89
+ version: 2
90
+ backends:
91
+ - name: meu-modelo
92
+ adapter: meu_pacote.adapter
93
+ vram_mib: 4200
94
+ priority: 20
95
+ runtime:
96
+ command: ["/opt/meu-modelo/venv/bin/python", "-m", "meu_pacote.worker"]
97
+ env: { HF_HOME: ~/hf-cache }
98
+ load_keys: [device, compute_type]
99
+ shape_keys: [device]
100
+ ```
101
+
102
+ Exemplo completo e executável: [`examples/echo-backend/`](examples/echo-backend/).
103
+
104
+ ## Calibração
105
+
106
+ O atrito de qualquer sistema destes é a pergunta "que números meto no
107
+ descriptor?". A resposta do vramd é: nenhum — mede-se.
108
+
109
+ ```bash
110
+ vramd calibrate meu-modelo --repeats 3 --out ~/.config/vramd/backends.d/medido.yaml
111
+ ```
112
+
113
+ Corre o job, separa **contexto CUDA / pesos / activação** pelas fronteiras de
114
+ fase, e escreve o descriptor. O que apanha, e que uma estimativa não apanha:
115
+
116
+ | Sinal | Porque importa |
117
+ |---|---|
118
+ | pico no **load** acima do da inferência | carregar fp16 e quantizar depois OOMa antes de gerar |
119
+ | activação ≫ pesos | o modelo carrega outro modelo dentro do `generate` |
120
+ | nada residente após o load | carga preguiçosa: não há o que evictar |
121
+ | `unload` que não devolve VRAM | evictar este backend não liberta nada — o plano de evicção seria ficção |
122
+ | fuga por repetição | o residente cresce a cada job |
123
+ | warmup na 1.ª inferência | calibrar com `--repeats 1` inflaciona o número |
124
+
125
+ Cada medição guarda as amostras cruas: `vramd recalibrate relatorio.json` refaz
126
+ os números quando a análise melhora, sem voltar a ocupar a GPU.
127
+
128
+ ## Comandos
129
+
130
+ ```
131
+ start stop status queue wait cancel flush backends preload evict reap
132
+ respawn zero stats debug bench doctor calibrate recalibrate
133
+ ```
134
+
135
+ - `vramd status` / `queue` — quem tem a GPU e o que espera
136
+ - `vramd zero` — liberta toda a VRAM ociosa sem parar o supervisor
137
+ - `vramd respawn <backend>` — reinicia só um worker (código novo) sem parar a fila
138
+ - `vramd doctor` — diagnóstico de ambiente
139
+
140
+ **Nunca é preciso `kill`.** Matar processos GPU corre contra a fila e mata o
141
+ workload errado.
142
+
143
+ ## Configuração
144
+
145
+ ```
146
+ data/backends.yaml (exemplo) → $VRAMD_BACKENDS_FILE → ~/.config/vramd/backends.d/*.yaml
147
+ ```
148
+
149
+ Sobreposição **por chave**: um ficheiro com `{name: x, vram_mib: 5632}` corrige
150
+ só esse campo e herda o resto. É assim que um descriptor calibrado entra em
151
+ vigor sem editar o pacote.
152
+
153
+ Variáveis: `VRAMD_BACKENDS_FILE`, `VRAMD_BACKENDS_DIR`, `VRAMD_TOOLS_ROOT`,
154
+ `VRAMD_MAX_INFLIGHT`, `VRAMD_MAX_QUEUE_DEPTH`, `VRAMD_VRAM_SAFETY_MIB`,
155
+ `VRAMD_PRIORITY`.
156
+
157
+ ## Limites conhecidos
158
+
159
+ Vale a pena saber antes de adoptar:
160
+
161
+ - **`MAX_INFLIGHT=1` por omissão** — uma geração de cada vez. É a escolha certa
162
+ para 6 GB e subutiliza uma A100. Há suporte para >1 com verificação de VRAM,
163
+ mas falta *packing* a sério.
164
+ - **Multi-GPU sem placement central.** `gpu_ids` é passado ao worker; o
165
+ supervisor não decide colocação nem contabiliza por device.
166
+ - **POSIX.** A leitura dos pipes usa `select`/`O_NONBLOCK`. Windows precisa de
167
+ uma camada de IO diferente.
168
+ - **Sem autenticação.** Socket unix com permissões de utilizador. Local, não
169
+ partilhado.
170
+ - **A calibração não faz milagres.** Mede o que o teu pipeline faz. Um modelo
171
+ que carrega tudo em fp16 de uma vez não passa a caber por ser medido — só
172
+ passas a saber que não cabe, e em 0.2 s em vez de a meio do job.
173
+
174
+ ## Origem
175
+
176
+ Extraído do [AiGameKit](https://github.com/maikramer), onde nasceu para pôr dez
177
+ modelos generativos (texto→imagem, →3D, →áudio, →movimento) a partilhar uma RTX
178
+ 4050 de 6 GB sem intervenção manual. Os números deste README são medições dessa
179
+ placa.
180
+
181
+ ## Contribuir
182
+
183
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) — arrancar, estilo, e o que este projeto
184
+ valoriza. A suite corre em ~27 s sem GPU.
185
+
186
+ ## Licença
187
+
188
+ MIT — ver [LICENSE](LICENSE).
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vramd"
7
+ version = "0.1.0"
8
+ description = "Controlo de admissão de VRAM para inferência generativa em GPUs de consumo"
9
+ readme = "README.md"
10
+ authors = [{ name = "Maikeu Locatelli" }]
11
+ requires-python = ">=3.11"
12
+ license = "MIT"
13
+ keywords = ["gpu", "vram", "inference", "scheduler", "admission-control", "diffusion"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
21
+ "Topic :: System :: Distributed Computing",
22
+ ]
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.
26
+ dependencies = [
27
+ "click>=8.1.0",
28
+ "rich>=13.0.0",
29
+ "rich-click>=1.8.0",
30
+ "PyYAML>=6.0",
31
+ "psutil>=5.9.0",
32
+ "nvidia-ml-py>=12.535.0",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ dev = ["pytest>=7.4.0", "pytest-cov>=4.0.0", "ruff==0.16.2"]
37
+
38
+ [project.scripts]
39
+ vramd = "vramd.cli:main"
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/maikramer/vramd"
43
+ Repository = "https://github.com/maikramer/vramd"
44
+ Issues = "https://github.com/maikramer/vramd/issues"
45
+ Changelog = "https://github.com/maikramer/vramd/blob/main/CHANGELOG.md"
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
49
+
50
+ [tool.setuptools.package-data]
51
+ vramd = ["data/*.yaml", "py.typed"]
52
+
53
+ [tool.pytest.ini_options]
54
+ pythonpath = ["src", "."]
55
+ testpaths = ["tests"]
56
+ addopts = "--strict-markers"
57
+
58
+ [tool.coverage.run]
59
+ source = ["src/vramd"]
60
+ branch = true
61
+
62
+ [tool.coverage.report]
63
+ exclude_also = [
64
+ "if TYPE_CHECKING:",
65
+ "raise NotImplementedError",
66
+ "\\.\\.\\.",
67
+ ]
vramd-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ """vramd — controlo de admissão de VRAM para inferência generativa.
2
+
3
+ Um processo detém a GPU e decide **quem entra**: admite pelo pico real
4
+ (pesos + activação + margem), põe em fila com prioridade e afinidade, evicta
5
+ por peso+LRU, e corre cada modelo num processo/venv próprio.
6
+
7
+ Feito para inferência que dura segundos a minutos numa GPU de consumo — não
8
+ para throughput de tokens em datacenter.
9
+
10
+ Três peças:
11
+
12
+ - **supervisor** (:mod:`vramd.server`, :mod:`vramd.cli`) — fila, admissão, evicção;
13
+ - **worker SDK** (:mod:`vramd.worker`) — 3 métodos para embrulhar qualquer modelo;
14
+ - **cliente** (:mod:`vramd.client`) — submeter, esperar, cancelar.
15
+
16
+ E um calibrador (:mod:`vramd.calibrate`) que **mede** o footprint em vez de o
17
+ adivinhar: o número que o admit usa vem do driver, não de uma estimativa.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ __version__ = "0.1.0"
23
+ __all__ = ["__version__"]
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+ """Entry point: python -m vramd"""
3
+
4
+ from .cli import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,154 @@
1
+ """Contrato do adapter **in-process** — backends sem subprocesso.
2
+
3
+ Gémeo de :class:`vramd.worker.adapter.WorkerAdapter`, para backends que
4
+ correm dentro do próprio supervisor (descriptor sem ``tool:``/``runtime:``).
5
+ É o caminho legado: sem isolamento de venv e a partilhar o processo com a
6
+ fila. Prefere-se o worker em subprocesso — ver :mod:`vramd.worker`.
7
+
8
+ O adapter é **stateless quanto ao modelo**: o ciclo de vida (criar/manter/
9
+ evictar) é do ``BackendManager``; o adapter só sabe *como* carregar, usar
10
+ e libertar.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import contextlib
16
+ from abc import ABC, abstractmethod
17
+ from collections.abc import Callable
18
+ from typing import Any
19
+
20
+ from . import protocol as P
21
+ from .errors import GenerationAborted
22
+
23
+ __all__ = ["BackendAdapter", "GenerationAborted"]
24
+
25
+
26
+ class BackendAdapter(ABC):
27
+ """Contrato canónico que cada adapter de tool implementa.
28
+
29
+ O adapter é **stateless quanto ao modelo**: não retém o objeto carregado.
30
+ O ciclo de vida (criar/manter/evictar o model object) é responsabilidade do
31
+ ``BackendManager``. O adapter apenas sabe *como* carregar/usar/libertar um
32
+ modelo dessa tool.
33
+
34
+ Implementações concretas devem ser instanciáveis sem argumentos (o Registry
35
+ faz ``Adapter()``) e exportar a classe com o nome ``Adapter``.
36
+ """
37
+
38
+ name: str = ""
39
+
40
+ @abstractmethod
41
+ def load(self, **kwargs: Any) -> Any:
42
+ """Carrega e devolve o model object (pipeline/gerador pronto a gerar)."""
43
+
44
+ @abstractmethod
45
+ def generate(self, model: Any, request: dict[str, Any]) -> dict[str, Any]:
46
+ """Executa uma geração sobre o model object.
47
+
48
+ O request pode incluir:
49
+ - ``_progress``: ``(pct|None, msg|None) -> None``
50
+ - ``_abort``: ``() -> bool`` (True = cancel pedido)
51
+ """
52
+
53
+ @abstractmethod
54
+ def unload(self, model: Any) -> None:
55
+ """Liberta o model object (VRAM). Idempotente e à prova de exceções."""
56
+
57
+ @staticmethod
58
+ def report_progress(request: dict[str, Any], pct: float | None = None, msg: str | None = None) -> None:
59
+ """Helper: reporta progresso se o request trouxer ``_progress``."""
60
+ cb = request.get("_progress")
61
+ if callable(cb):
62
+ with contextlib.suppress(Exception):
63
+ cb(pct, msg)
64
+
65
+ @staticmethod
66
+ def should_abort(request: dict[str, Any]) -> bool:
67
+ """True se o vramd pediu cancel (``request["_abort"]``)."""
68
+ cb = request.get("_abort")
69
+ if not callable(cb):
70
+ return False
71
+ try:
72
+ return bool(cb())
73
+ except Exception:
74
+ return False
75
+
76
+ @staticmethod
77
+ def cancelled_response(reason: str = "cancelled") -> dict[str, Any]:
78
+ """Resposta canónica de cancel cooperativo."""
79
+ return {
80
+ "status": P.STATUS_ERROR,
81
+ "error": reason,
82
+ "error_code": P.ERR_CANCELLED,
83
+ }
84
+
85
+ @classmethod
86
+ def abort_hooks(
87
+ cls,
88
+ request: dict[str, Any],
89
+ *,
90
+ num_inference_steps: int,
91
+ ) -> tuple[Callable[[], bool] | None, Callable[[int, int], None] | None]:
92
+ """Constrói ``should_abort`` / ``on_step`` para generators 2D."""
93
+
94
+ def _abort() -> bool:
95
+ return cls.should_abort(request)
96
+
97
+ def _on_step(step: int, total: int) -> None:
98
+ pct = step / max(1, total)
99
+ cls.report_progress(request, pct, f"step {step}/{total}")
100
+
101
+ has_abort = callable(request.get("_abort"))
102
+ has_progress = callable(request.get("_progress"))
103
+ return (
104
+ _abort if has_abort else None,
105
+ _on_step if has_progress else None,
106
+ )
107
+
108
+ @classmethod
109
+ def apply_runtime_budget(
110
+ cls,
111
+ model: Any,
112
+ request: dict[str, Any],
113
+ *,
114
+ progress_pct: float | None = None,
115
+ **hints: Any,
116
+ ) -> dict[str, Any] | None:
117
+ """Reaplica o runtime VRAM budget do model object, se suportado.
118
+
119
+ Contrato canónico: model objects que suportam re-orçamento pós-load
120
+ (Paint3D ``PaintBatchProcessor``, Text3D ``HunyuanTextTo3DGenerator``, …)
121
+ expõem ``refresh_runtime_budget(**hints)`` e devolvem o dict do budget
122
+ (um dict livre, definido pelo model object). O adapter chama isto antes da
123
+ inferência; o BackendManager guarda o resultado em ``stats``.
124
+
125
+ Args:
126
+ model: model object carregado pelo adapter.
127
+ request: request (para ``report_progress``).
128
+ progress_pct: se dado, reporta o budget como progresso.
129
+ **hints: overrides por-request (ex: ``requested_views=...``).
130
+ Ignorados se o método do model não os aceitar.
131
+
132
+ Returns:
133
+ Dict do budget, ou ``None`` se o model não suporta / falhou.
134
+ """
135
+ refresh = getattr(model, "refresh_runtime_budget", None)
136
+ if not callable(refresh):
137
+ return None
138
+ try:
139
+ try:
140
+ budget = refresh(**hints) if hints else refresh()
141
+ except TypeError:
142
+ # Model object antigo sem suporte a hints — retry sem overrides.
143
+ budget = refresh()
144
+ except (RuntimeError, MemoryError):
145
+ # Gate VRAM (MeshRender headroom, OOM) — NÃO engolir; adapter aborta.
146
+ raise
147
+ except Exception:
148
+ return None
149
+ if budget and progress_pct is not None:
150
+ summary = ", ".join(
151
+ f"{k}={v}" for k, v in budget.items() if k in ("num_chunks", "max_views", "dino_device")
152
+ )
153
+ cls.report_progress(request, progress_pct, f"vram_budget {summary}" if summary else "vram_budget")
154
+ return budget if isinstance(budget, dict) else None