Spaces:
Running on Zero
Running on Zero
File size: 6,485 Bytes
c7f05a9 550d35b 64e1ec4 550d35b 64e1ec4 550d35b 64e1ec4 550d35b c7f05a9 64e1ec4 c7f05a9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | # -*- coding: utf-8 -*-
"""
O CONTRATO DE UM MODELO DENTRO DO WORKER.
Cada modelo é um módulo com quatro deveres, e o gerenciador só conversa por
eles:
carregar() — traz os pesos para a GPU. Import de torch/diffusers é
feito AQUI DENTRO, nunca no topo do arquivo: o worker
precisa subir e responder /health mesmo numa máquina
sem as bibliotecas pesadas instaladas.
gerar(params, progresso) — produz bytes + mime. `progresso(pct, etapa)` é
chamado quando o modelo sabe onde está (a difusão sabe
o passo; TTS muitas vezes não — aí não chama).
descarregar() — devolve a VRAM. É o gerenciador que decide QUANDO.
config_menor(params) — a versão do pedido que cabe em menos VRAM (menos
passos, resolução menor, duração menor). Devolve None
quando não há o que reduzir — aí o OOM vira falha limpa
em vez de tentativa infinita.
E um modo de MENTIRA declarada: com WORKER_MOCK=1 os modelos devolvem
arquivos minúsculos válidos (PNG 1x1, WAV de silêncio, MP4 de um quadro) sem
tocar em GPU nenhuma. Ele existe para os testes automatizados e para subir a
pilha inteira num notebook sem CUDA — e o resultado DIZ que é mock no
metadado, porque fingir inferência real é o tipo de mentira que este projeto
não conta.
"""
from __future__ import annotations
import os
import struct
from typing import Callable, Dict, Optional, Tuple
Progresso = Callable[[Optional[int], str], None]
# bytes, mime
Saida = Tuple[bytes, str]
def modo_mock() -> bool:
return os.getenv("WORKER_MOCK", "").strip() == "1"
# ---------------------------------------------------------------------------
# Os arquivos mínimos do modo mock — válidos de verdade (abrem em qualquer
# visualizador), só que vazios de conteúdo.
# ---------------------------------------------------------------------------
PNG_1X1 = bytes.fromhex(
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
"0000000d4944415478da63fcffff3f0300050201f34d31b20000000049454e44ae426082")
def wav_silencio(segundos: float = 0.2, taxa: int = 16000) -> bytes:
n = int(segundos * taxa)
dados = b"\x00\x00" * n
return (b"RIFF" + struct.pack("<I", 36 + len(dados)) + b"WAVEfmt "
+ struct.pack("<IHHIIHH", 16, 1, 1, taxa, taxa * 2, 2, 16)
+ b"data" + struct.pack("<I", len(dados)) + dados)
def baixar_repo(repo_id: str, progresso: Progresso,
revision: Optional[str] = None,
arquivos: Optional[list] = None) -> None:
"""Traz os pesos para o CACHE local (disco), sem tocar em GPU nenhuma.
Existe por causa do ZeroGPU: lá a GPU só vive dentro da chamada decorada,
com prazo — e o primeiro `from_pretrained` gastava o prazo inteiro
BAIXANDO, que é trabalho de rede. Medido: pedir 240s de duração para
caber o download estourou o teto do ZeroGPU ("requested GPU duration
(360s) is larger than the maximum allowed"). Com o download feito antes,
o `carregar()` de dentro da GPU só lê do disco. No worker CUDA chamar
isto é inócuo: o from_pretrained acha o cache e não baixa de novo.
`arquivos` RECORTA o que desce, e ele não é conveniência. Um repositório
do Hub costuma guardar várias variantes do mesmo peso — o do LoRA
Lightning tem doze arquivos e 65,5 GB para entregar os 0,85 GB de um
deles. Sem recorte, pedir o acelerador baixaria setenta vezes o que ele
pesa, e num Space de disco efêmero isso não é lentidão: é o disco
acabando antes de o modelo principal caber."""
if modo_mock():
return
progresso(None, "Baixando o modelo…")
from huggingface_hub import snapshot_download
snapshot_download(repo_id, revision=revision,
allow_patterns=arquivos or None,
token=os.getenv("HF_TOKEN") or None)
def vram_livre_gb() -> Optional[float]:
"""Quanta VRAM sobra agora. None = sem CUDA (aí ninguém decide por VRAM)."""
try:
import torch
if not torch.cuda.is_available():
return None
livre, _total = torch.cuda.mem_get_info()
return livre / (1024 ** 3)
except Exception:
return None
def limpar_vram() -> None:
"""Solta o que der: cache de CUDA e lixo do Python. Chamado pelo
gerenciador depois de descarregar e depois de um OOM — nas duas horas em
que 'quase liberado' e 'liberado' são a diferença entre caber e não."""
import gc
gc.collect()
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
except Exception:
pass
def dispositivo() -> str:
try:
import torch
return "cuda" if torch.cuda.is_available() else "cpu"
except Exception:
return "cpu"
def gpu_dedicada() -> bool:
"""Há prazo de GPU de verdade? `GPU_DEDICADA=1` diz que sim.
UM FATO, UMA VARIÁVEL. O ZeroGPU gratuito corta a chamada em 180 segundos
(é o maior valor que ele aceita — ver CLAUDE.md), e esse teto é DURO:
estourá-lo não é OOM, então nenhuma escada de recuperação roda — a chamada
morre no meio com a quota do dia gasta e nada saindo. Isso obriga CADA
módulo a ter um padrão que cabe: o vídeo na resolução, a imagem no número
de passos.
Os dois padrões nascem do MESMO fato, então eles leem a MESMA variável.
Uma por módulo (`VIDEO_720P`, `IMAGE_PASSOS_CHEIOS`…) seriam vários donos
de uma decisão só, e no dia em que houver GPU dedicada metade ficaria para
trás — com o sintoma de sempre: nada quebra, só entrega pior.
Nasce DESLIGADA de propósito: produção é o ZeroGPU gratuito, e um padrão
que assume folga é uma geração que aborta para todo mundo.
"""
return (os.getenv("GPU_DEDICADA", "0").strip() or "0") != "0"
def semente(params: Dict):
"""torch.Generator com a seed pedida — ou None para o aleatório de
sempre. Seed existe para 'gera de novo IGUAL', então ela é respeitada em
todo modelo que aceita generator."""
seed = params.get("seed")
if seed in (None, ""):
return None
try:
import torch
return torch.Generator(device=dispositivo()).manual_seed(int(seed))
except Exception:
return None
|