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.
- vramd-0.1.0/LICENSE +21 -0
- vramd-0.1.0/PKG-INFO +221 -0
- vramd-0.1.0/README.md +188 -0
- vramd-0.1.0/pyproject.toml +67 -0
- vramd-0.1.0/setup.cfg +4 -0
- vramd-0.1.0/src/vramd/__init__.py +23 -0
- vramd-0.1.0/src/vramd/__main__.py +7 -0
- vramd-0.1.0/src/vramd/adapter.py +154 -0
- vramd-0.1.0/src/vramd/backend_manager.py +1693 -0
- vramd-0.1.0/src/vramd/calibrate/__init__.py +59 -0
- vramd-0.1.0/src/vramd/calibrate/analysis.py +619 -0
- vramd-0.1.0/src/vramd/calibrate/compare.py +216 -0
- vramd-0.1.0/src/vramd/calibrate/emit.py +228 -0
- vramd-0.1.0/src/vramd/calibrate/hw_auto.py +148 -0
- vramd-0.1.0/src/vramd/calibrate/runner.py +392 -0
- vramd-0.1.0/src/vramd/calibrate/sampler.py +374 -0
- vramd-0.1.0/src/vramd/calibrate/serde.py +136 -0
- vramd-0.1.0/src/vramd/cli.py +1709 -0
- vramd-0.1.0/src/vramd/client.py +816 -0
- vramd-0.1.0/src/vramd/data/backends.yaml +51 -0
- vramd-0.1.0/src/vramd/dispatcher.py +418 -0
- vramd-0.1.0/src/vramd/errors.py +52 -0
- vramd-0.1.0/src/vramd/footprints.py +182 -0
- vramd-0.1.0/src/vramd/gpu.py +1029 -0
- vramd-0.1.0/src/vramd/idle_evictor.py +170 -0
- vramd-0.1.0/src/vramd/job_queue.py +727 -0
- vramd-0.1.0/src/vramd/logging.py +452 -0
- vramd-0.1.0/src/vramd/process_guard.py +406 -0
- vramd-0.1.0/src/vramd/protocol.py +230 -0
- vramd-0.1.0/src/vramd/py.typed +0 -0
- vramd-0.1.0/src/vramd/registry.py +379 -0
- vramd-0.1.0/src/vramd/scheduler.py +77 -0
- vramd-0.1.0/src/vramd/server.py +1090 -0
- vramd-0.1.0/src/vramd/stats.py +184 -0
- vramd-0.1.0/src/vramd/subprocess_pool.py +690 -0
- vramd-0.1.0/src/vramd/toolchain.py +77 -0
- vramd-0.1.0/src/vramd/vram_planner.py +136 -0
- vramd-0.1.0/src/vramd/worker/__init__.py +54 -0
- vramd-0.1.0/src/vramd/worker/adapter.py +183 -0
- vramd-0.1.0/src/vramd/worker/protocol.py +200 -0
- vramd-0.1.0/src/vramd/worker/serve.py +436 -0
- vramd-0.1.0/src/vramd.egg-info/PKG-INFO +221 -0
- vramd-0.1.0/src/vramd.egg-info/SOURCES.txt +81 -0
- vramd-0.1.0/src/vramd.egg-info/dependency_links.txt +1 -0
- vramd-0.1.0/src/vramd.egg-info/entry_points.txt +2 -0
- vramd-0.1.0/src/vramd.egg-info/requires.txt +11 -0
- vramd-0.1.0/src/vramd.egg-info/top_level.txt +1 -0
- vramd-0.1.0/tests/test_abort_progress.py +90 -0
- vramd-0.1.0/tests/test_backend_manager.py +293 -0
- vramd-0.1.0/tests/test_backend_manager_hybrid.py +314 -0
- vramd-0.1.0/tests/test_calibrate_analysis.py +424 -0
- vramd-0.1.0/tests/test_calibrate_cli.py +193 -0
- vramd-0.1.0/tests/test_calibrate_emit_compare.py +241 -0
- vramd-0.1.0/tests/test_calibrate_hw_auto.py +207 -0
- vramd-0.1.0/tests/test_calibrate_runner.py +507 -0
- vramd-0.1.0/tests/test_calibrate_sampler.py +325 -0
- vramd-0.1.0/tests/test_calibrate_serde.py +200 -0
- vramd-0.1.0/tests/test_cli_debug_stats_bench.py +185 -0
- vramd-0.1.0/tests/test_gpu_ids_load.py +79 -0
- vramd-0.1.0/tests/test_idle_evictor.py +250 -0
- vramd-0.1.0/tests/test_job_queue.py +245 -0
- vramd-0.1.0/tests/test_job_wal.py +146 -0
- vramd-0.1.0/tests/test_peak_profile.py +169 -0
- vramd-0.1.0/tests/test_phase_progress.py +63 -0
- vramd-0.1.0/tests/test_process_guard.py +219 -0
- vramd-0.1.0/tests/test_protocol_env.py +48 -0
- vramd-0.1.0/tests/test_queue_metrics_eta.py +131 -0
- vramd-0.1.0/tests/test_registry.py +103 -0
- vramd-0.1.0/tests/test_registry_layers.py +267 -0
- vramd-0.1.0/tests/test_respawn.py +251 -0
- vramd-0.1.0/tests/test_runtime_spawn.py +175 -0
- vramd-0.1.0/tests/test_scheduler.py +142 -0
- vramd-0.1.0/tests/test_server.py +648 -0
- vramd-0.1.0/tests/test_stats.py +132 -0
- vramd-0.1.0/tests/test_subprocess_integration.py +147 -0
- vramd-0.1.0/tests/test_subprocess_per_backend_e2e.py +217 -0
- vramd-0.1.0/tests/test_subprocess_pool.py +464 -0
- vramd-0.1.0/tests/test_unload_frees_vram.py +135 -0
- vramd-0.1.0/tests/test_vram_admit.py +125 -0
- vramd-0.1.0/tests/test_vram_planner.py +148 -0
- vramd-0.1.0/tests/test_vramd_coverage_100.py +430 -0
- vramd-0.1.0/tests/test_worker.py +500 -0
- 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
|
+
[](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
|
+
**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
|
+
[](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
|
+
**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,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,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
|