Spaces:
Paused
Paused
Update services/vincie.py
Browse files- services/vincie.py +254 -116
services/vincie.py
CHANGED
|
@@ -1,122 +1,260 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
import os
|
| 8 |
-
import
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
-
import
|
| 11 |
-
|
| 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 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
self.
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
try:
|
| 112 |
-
|
| 113 |
-
except Exception
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]
|