euIaxs22 commited on
Commit
9342209
·
verified ·
1 Parent(s): 3c4ad98

Update services/vincie.py

Browse files
Files changed (1) hide show
  1. services/vincie.py +254 -116
services/vincie.py CHANGED
@@ -1,122 +1,260 @@
1
- # services/vincie.py (VERSÃO FINAL - DELEGAÇÃO TOTAL)
2
- # Versão 4.5.0
3
- # - REMOÇÃO: Remove a lógica manual de resolução de herança. A correção do CWD
4
- # já permite que o VINCIEGenerator/OmegaConf resolva os caminhos `__inherit__` corretamente.
5
- # - Esta é a abordagem mais limpa, confiando totalmente no código original.
 
 
 
 
 
 
 
 
 
 
6
 
7
  import os
8
- import sys
 
9
  from pathlib import Path
10
- import random
11
- import torch
12
- import numpy as np
13
- from PIL import Image
14
- from omegaconf import OmegaConf
15
- from einops import rearrange
16
- from typing import List, Generator, Tuple
17
  from huggingface_hub import snapshot_download
18
 
19
- # --- Configuração de Ambiente ---
20
- VINCIE_ROOT = Path("/app/VINCIE")
21
- if os.getcwd() != str(VINCIE_ROOT):
22
- os.chdir(VINCIE_ROOT)
23
-
24
- if str(VINCIE_ROOT) not in sys.path:
25
- sys.path.insert(0, str(VINCIE_ROOT))
26
- print(f"✅ CWD alterado para '{VINCIE_ROOT}' e adicionado ao sys.path.")
27
- # -----------------------------
28
-
29
- from generate import VINCIEGenerator
30
- from common.seed import shift_seed
31
-
32
- class VinciePipelineService:
33
- def __init__(self, config_path: str = "configs/generate.yaml"):
34
- print(">>> [VinciePipelineService] Inicializando serviço com delegação total...")
35
- if not torch.cuda.is_available():
36
- raise RuntimeError("CUDA não disponível.")
37
-
38
- self.device = torch.device("cuda")
39
- self.dtype = torch.bfloat16
40
-
41
- # 1. Garantir que os modelos estejam baixados e linkados
42
- self._ensure_models_are_downloaded_and_linked()
43
-
44
- # 2. Carregar a configuração. NÃO PRECISAMOS MAIS MANIPULÁ-LA.
45
- print(f">>> Carregando configuração de '{config_path}'...")
46
- self.config = OmegaConf.load(config_path)
47
-
48
- # 3. Instanciar a classe VINCIEGenerator.
49
- # Ela agora resolverá todos os caminhos e heranças internamente.
50
- print(">>> Instanciando o motor VINCIEGenerator...")
51
- self.runner = VINCIEGenerator(self.config)
52
-
53
- # 4. Delegar o carregamento.
54
- print(">>> Delegando o carregamento dos modelos...")
55
- self.runner.configure_persistence()
56
- self.runner.configure_models() # Esta chamada agora deve ter sucesso completo.
57
- self.runner.configure_diffusion()
58
-
59
- self.temp_dir = Path("./temp_io") # Relativo ao novo CWD
60
- self.temp_dir.mkdir(exist_ok=True)
61
-
62
- print(">>> [VinciePipelineService] Serviço pronto.")
63
-
64
- def _ensure_models_are_downloaded_and_linked(self):
65
- repo_id = "ByteDance-Seed/VINCIE-3B"
66
- cache_dir = os.environ.get("HF_HOME", "/data/.cache/huggingface")
67
-
68
- print(f"📥 Verificando/Baixando {repo_id}...")
69
- snapshot_path = Path(snapshot_download(repo_id=repo_id, cache_dir=cache_dir, resume_download=True))
70
-
71
- # O código VINCIE espera os checkpoints em ./ckpt/VINCIE-3B
72
- link_target_dir = VINCIE_ROOT / "ckpt"
73
- link_path = link_target_dir / "VINCIE-3B"
74
- link_target_dir.mkdir(exist_ok=True)
75
-
76
- if not link_path.exists():
77
- os.symlink(snapshot_path, link_path, target_is_directory=True)
78
- print(f"🔗 Link simbólico criado: {link_path} -> {snapshot_path}")
79
- else:
80
- print(f"🔗 Link simbólico já existe.")
81
-
82
- # --- O restante da classe (lógica de inferência) permanece o mesmo ---
83
- @torch.no_grad()
84
- def run_multi_turn_session(self, initial_image, prompts, negative_prompt, steps, cfg_scale, seed):
85
- for f in self.temp_dir.glob('*.png'): f.unlink()
86
- initial_image_path = self.temp_dir / "turn_0_output.png"
87
- initial_image.save(initial_image_path)
88
- context_image_paths = [str(initial_image_path)]
89
- context_prompts = []
90
-
91
- for i, prompt in enumerate(prompts):
92
- turn_index = i + 1
93
- print(f">>> [Turno {turn_index}/{len(prompts)}] Gerando com prompt: '{prompt}'")
94
- context_prompts.append(prompt)
95
- prompt_dict = OmegaConf.create({'index': turn_index, 'img_paths': context_image_paths, 'context': context_prompts})
96
- _, conditions, noises, _, _ = self.runner.prepare_input(prompt=prompt_dict, repeat_idx=0)
97
- texts_neg = [negative_prompt]
98
- self.runner.config.diffusion.timesteps.sampling.steps = steps
99
- self.runner.configure_diffusion()
100
- final_latents = self.runner.inference(noises=[noises], conditions=[conditions], texts_pos=[context_prompts], texts_neg=texts_neg, cfg_scale=cfg_scale)
101
- output_tensor = self.runner.vae_decode(final_latents)[0][:, -1, :, :]
102
- output_tensor = output_tensor.clip(-1, 1).add(1).mul(0.5).mul(255)
103
- image_np = rearrange(output_tensor.to("cpu", torch.uint8), "c h w -> h w c").numpy()
104
- generated_image = Image.fromarray(image_np)
105
- turn_image_path = self.temp_dir / f"turn_{turn_index}_output.png"
106
- generated_image.save(turn_image_path)
107
- context_image_paths.append(str(turn_image_path))
108
- yield generated_image, i
109
-
110
- # --- Bloco de Tratamento de Erro ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  try:
112
- vincie_pipeline_service = VinciePipelineService()
113
- except Exception as e:
114
- import traceback
115
- print(f"ERRO CRÍTICO AO INICIALIZAR O VINCIEPIPELINESERVICE: {e}")
116
- traceback.print_exc()
117
- class VinciePlaceholder:
118
- def __init__(self, error): self.error = error
119
- def run_multi_turn_session(self, *args, **kwargs):
120
- raise RuntimeError(f"O serviço VINCIE falhou: {self.error}")
121
- yield None, 0
122
- vincie_pipeline_service = VinciePlaceholder(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ VincieService (singleton-friendly)
4
+
5
+ - Prepara o repositório VINCIE e o checkpoint completo via snapshot_download, honrando HF_HUB_CACHE.
6
+ - Cria symlink de compatibilidade /app/VINCIE/ckpt/VINCIE-3B -> <snapshot no cache>.
7
+ - Permite fixar GPUs dedicadas ao processo via CUDA_VISIBLE_DEVICES.
8
+ - Opcionalmente ativa o NVIDIA Persistence Mode para reduzir latência de inicialização.
9
+ - Executa geração chamando o main.py do VINCIE com overrides (cfg_scale, resolution_input, aspect_ratio_input, steps).
10
+ - Realiza limpeza leve de GPU após cada job, mantendo o processo vivo e pronto.
11
+
12
+ Observação:
13
+ - Este serviço usa subprocess para chamar o main.py oficial, priorizando compatibilidade.
14
+ - Para reter pesos do modelo em VRAM entre jobs, integrar diretamente generate.py em um servidor Python persistente.
15
+ """
16
 
17
  import os
18
+ import json
19
+ import subprocess
20
  from pathlib import Path
21
+ from typing import List, Optional
22
+
 
 
 
 
 
23
  from huggingface_hub import snapshot_download
24
 
25
+
26
+ class VincieService:
27
+ def __init__(
28
+ self,
29
+ repo_dir: str = "/app/VINCIE",
30
+ ckpt_symlink_dir: str = "/app/VINCIE/ckpt/VINCIE-3B",
31
+ python_bin: str = "python",
32
+ repo_id: str = "ByteDance-Seed/VINCIE-3B",
33
+ output_root: str = "/app/outputs",
34
+ ):
35
+ self.repo_dir = Path(repo_dir)
36
+ self.ckpt_symlink = Path(ckpt_symlink_dir)
37
+ self.python = python_bin
38
+ self.repo_id = repo_id
39
+
40
+ self.generate_yaml = self.repo_dir / "configs" / "generate.yaml"
41
+ (self.repo_dir / "ckpt").mkdir(parents=True, exist_ok=True)
42
+
43
+ self.output_root = Path(output_root)
44
+ self.output_root.mkdir(parents=True, exist_ok=True)
45
+
46
+ # Caminho real do snapshot no cache (definido após ensure_model)
47
+ self.ckpt_dir: Optional[Path] = None
48
+
49
+ # Ambiente mutável do serviço (permite fixar GPUs)
50
+ self._env = os.environ.copy()
51
+
52
+ # ---------- Repositório e modelo ----------
53
+
54
+ def ensure_repo(self, git_url: str = "https://github.com/ByteDance-Seed/VINCIE") -> None:
55
+ if not self.repo_dir.exists():
56
+ subprocess.run(["git", "clone", git_url, str(self.repo_dir)], check=True)
57
+
58
+ def ensure_model(self, hf_token: Optional[str] = None, revision: Optional[str] = None) -> None:
59
+ """
60
+ Baixa o snapshot completo do repositório do modelo no cache local e cria o symlink esperado pelo repo.
61
+ - Usa HF_HUB_CACHE como cache_dir quando definido.
62
+ """
63
+ token = hf_token or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
64
+ cache_dir = os.environ.get("HF_HUB_CACHE")
65
+
66
+ snapshot_path = snapshot_download(
67
+ repo_id=self.repo_id,
68
+ revision=revision,
69
+ cache_dir=cache_dir,
70
+ token=token,
71
+ local_files_only=False,
72
+ )
73
+ self.ckpt_dir = Path(snapshot_path)
74
+
75
+ # Symlink de compatibilidade dentro do repo
76
+ try:
77
+ if self.ckpt_symlink.is_symlink():
78
+ self.ckpt_symlink.unlink()
79
+ elif self.ckpt_symlink.exists():
80
+ # Opcional: não remover diretório real automaticamente
81
+ pass
82
+ if not self.ckpt_symlink.exists():
83
+ self.ckpt_symlink.symlink_to(self.ckpt_dir, target_is_directory=True)
84
+ except Exception as e:
85
+ print("Warning: failed to create checkpoint symlink:", e)
86
+
87
+ def ready(self) -> bool:
88
+ have_repo = self.repo_dir.exists() and self.generate_yaml.exists()
89
+ dit_ok = self.ckpt_dir is not None and (self.ckpt_dir / "dit.pth").exists()
90
+ vae_ok = self.ckpt_dir is not None and (self.ckpt_dir / "vae.pth").exists()
91
+ return bool(have_repo and dit_ok and vae_ok)
92
+
93
+ # ---------- GPUs dedicadas e persistência ----------
94
+
95
+ def pin_gpus(self, device_indices: List[int]) -> None:
96
+ """
97
+ Restringe a visibilidade de GPUs para este processo, ex.: [0,1,2,3].
98
+ Deve ser chamado antes de qualquer inicialização CUDA pesada.
99
+ """
100
+ visible = ",".join(str(i) for i in device_indices)
101
+ self._env["CUDA_VISIBLE_DEVICES"] = visible
102
+
103
+ def enable_persistence_mode(self) -> None:
104
+ """
105
+ Liga o persistence mode do driver NVIDIA para reduzir latência de inicialização CUDA.
106
+ Requer permissões adequadas.
107
+ """
108
+ try:
109
+ subprocess.run(["nvidia-smi", "-pm", "1"], check=True)
110
+ except Exception as e:
111
+ print("Warning: failed to enable persistence mode:", e)
112
+
113
+ # ---------- Execução do VINCIE ----------
114
+
115
+ def _build_overrides(
116
+ self,
117
+ extra_overrides: Optional[List[str]] = None,
118
+ cfg_scale: Optional[float] = None,
119
+ resolution_input: Optional[int] = None,
120
+ aspect_ratio_input: Optional[str] = None,
121
+ steps: Optional[int] = None,
122
+ ) -> List[str]:
123
+ overrides = list(extra_overrides or [])
124
+ if self.ckpt_dir is not None:
125
+ overrides.append(f"ckpt.path={str(self.ckpt_dir)}")
126
+ if cfg_scale is not None:
127
+ overrides.append(f"generation.cfg_scale={cfg_scale}")
128
+ if resolution_input is not None:
129
+ overrides.append(f"generation.resolution_input={resolution_input}")
130
+ if aspect_ratio_input is not None:
131
+ overrides.append(f"generation.aspect_ratio_input={aspect_ratio_input}")
132
+ if steps is not None:
133
+ overrides.append(f"generation.steps={steps}")
134
+ return overrides
135
+
136
+ def _run_vincie_once(self, overrides: List[str], work_output: Path) -> None:
137
+ """
138
+ Invoca o main.py oficial com overrides; execução única do job.
139
+ """
140
+ work_output.mkdir(parents=True, exist_ok=True)
141
+ cmd = [
142
+ self.python,
143
+ "main.py",
144
+ str(self.generate_yaml),
145
+ *overrides,
146
+ f"generation.output.dir={str(work_output)}",
147
+ ]
148
+ subprocess.run(cmd, cwd=self.repo_dir, check=True, env=self._env)
149
+
150
+ def _clean_gpu_memory(self) -> None:
151
+ """
152
+ Limpa caches alocador CUDA e estatísticas de pico, sem descarregar pesos que estejam vivos no processo.
153
+ Como este serviço invoca um subprocess a cada job, a VRAM do subprocess é liberada ao término;
154
+ ainda assim, executar uma limpeza leve no contexto do serviço não causa efeito colateral.
155
+ """
156
+ try:
157
+ # Executa um snippet Python rápido no mesmo conjunto de GPUs visíveis
158
+ code = r"""
159
+ import torch, gc
160
  try:
161
+ torch.cuda.synchronize()
162
+ except Exception:
163
+ pass
164
+ gc.collect()
165
+ try:
166
+ torch.cuda.empty_cache()
167
+ torch.cuda.memory.reset_peak_memory_stats()
168
+ except Exception:
169
+ pass
170
+ """
171
+ subprocess.run([self.python, "-c", code], check=True, env=self._env)
172
+ except Exception as e:
173
+ print("Warning: GPU cleanup failed:", e)
174
+
175
+ # ---------- APIs de alto nível ----------
176
+
177
+ def multi_turn_edit(
178
+ self,
179
+ input_image: str,
180
+ turns: List[str],
181
+ out_dir_name: Optional[str] = None,
182
+ *,
183
+ cfg_scale: Optional[float] = None,
184
+ resolution_input: Optional[int] = None,
185
+ aspect_ratio_input: Optional[str] = None,
186
+ steps: Optional[int] = None,
187
+ pad_img_placeholder: Optional[bool] = None,
188
+ ) -> Path:
189
+ """
190
+ Executa pipeline multi-turn com overrides opcionais.
191
+ """
192
+ out_dir = self.output_root / (out_dir_name or f"multi_turn_{self._slug(input_image)}")
193
+ image_json = json.dumps([str(input_image)])
194
+ prompts_json = json.dumps(turns)
195
+
196
+ base_overrides = [
197
+ f"generation.positive_prompt.image_path={image_json}",
198
+ f"generation.positive_prompt.prompts={prompts_json}",
199
+ ]
200
+ if pad_img_placeholder is not None:
201
+ base_overrides.append(f"generation.pad_img_placehoder={str(bool(pad_img_placeholder)).lower()}")
202
+
203
+ overrides = self._build_overrides(
204
+ extra_overrides=base_overrides,
205
+ cfg_scale=cfg_scale,
206
+ resolution_input=resolution_input,
207
+ aspect_ratio_input=aspect_ratio_input,
208
+ steps=steps,
209
+ )
210
+
211
+ self._run_vincie_once(overrides, out_dir)
212
+ self._clean_gpu_memory()
213
+ return out_dir
214
+
215
+ def multi_concept_compose(
216
+ self,
217
+ concept_images: List[str],
218
+ concept_prompts: List[str],
219
+ final_prompt: str,
220
+ out_dir_name: Optional[str] = None,
221
+ *,
222
+ cfg_scale: Optional[float] = None,
223
+ resolution_input: Optional[int] = None,
224
+ aspect_ratio_input: Optional[str] = None,
225
+ steps: Optional[int] = None,
226
+ ) -> Path:
227
+ """
228
+ Executa pipeline multi-concept com overrides opcionais.
229
+ """
230
+ out_dir = self.output_root / (out_dir_name or "multi_concept")
231
+ imgs_json = json.dumps([str(p) for p in concept_images])
232
+ prompts_all = concept_prompts + [final_prompt]
233
+ prompts_json = json.dumps(prompts_all)
234
+
235
+ base_overrides = [
236
+ f"generation.positive_prompt.image_path={imgs_json}",
237
+ f"generation.positive_prompt.prompts={prompts_json}",
238
+ "generation.pad_img_placehoder=False",
239
+ ]
240
+
241
+ overrides = self._build_overrides(
242
+ extra_overrides=base_overrides,
243
+ cfg_scale=cfg_scale,
244
+ resolution_input=resolution_input,
245
+ aspect_ratio_input=aspect_ratio_input,
246
+ steps=steps,
247
+ )
248
+
249
+ self._run_vincie_once(overrides, out_dir)
250
+ self._clean_gpu_memory()
251
+ return out_dir
252
+
253
+ # ---------- Util ----------
254
+
255
+ @staticmethod
256
+ def _slug(path_or_text: str) -> str:
257
+ p = Path(path_or_text)
258
+ base = p.stem if p.exists() else str(path_or_text)
259
+ keep = "".join(c if c.isalnum() or c in "-_." else "_" for c in str(base))
260
+ return keep[:64]