feat(memory): Ajout déchargement automatique mémoire sur FolzoCorp

- effi-stt: Déchargement Whisper après 300s inactivité (INACTIVITY_TIMEOUT)
  * Nouveaux endpoints: /unload, /reload, /stats
  * Modelo rechargé à la première requête
- ComfyUI: Script monitor qui décharge VRAM après inactivité
  * Vérifie usage VRAM toutes les 60s
  * Exécute workflow UnloadAllModelsNode après 300s inactivité
- Kreuzberg: Manager qui redémarre le container après seuil mémoire + inactivité
  * Restart si > 2GB pendant > 10min inactif
- Documentation: docs/MEMOIRE-GESTION.md
This commit is contained in:
Folzo
2026-04-12 09:26:29 +02:00
parent e501213959
commit f69dda5e20
6 changed files with 679 additions and 63 deletions
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# ComfyUI Model Unloader - Décharge les modèles après inactivité
# Usage: python3 comfyui-unload-monitor.py [--interval 60] [--timeout 300]
import requests
import time
import argparse
import logging
import sys
import signal
import subprocess
from datetime import datetime, timedelta
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
DEFAULT_INTERVAL = 60
DEFAULT_TIMEOUT = 300
COMFYUI_URL = "http://localhost:8188"
class ComfyUIUnloadMonitor:
def __init__(self, interval=DEFAULT_INTERVAL, timeout=DEFAULT_TIMEOUT):
self.interval = interval
self.timeout = timeout
self.last_activity = datetime.now()
self.running = True
self.models_loaded = False
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
def signal_handler(self, signum, frame):
logger.info("Signal reçu, arrêt du monitor...")
self.running = False
def check_comfyui_activity(self):
try:
response = requests.get(f"{COMFYUI_URL}/system_stats", timeout=5)
if response.status_code == 200:
stats = response.json()
return stats
return None
except requests.exceptions.RequestException as e:
logger.warning(f"ComfyUI non accessible: {e}")
return None
def check_queue_and_history(self):
try:
queue_response = requests.get(f"{COMFYUI_URL}/queue", timeout=5)
history_response = requests.get(f"{COMFYUI_URL}/history?max_items=1", timeout=5)
if queue_response.status_code == 200 and history_response.status_code == 200:
queue_data = queue_response.json()
history_data = history_response.json()
running = queue_data.get("queue_running", [])
pending = queue_data.get("queue_pending", [])
has_activity = len(running) > 0 or len(pending) > 0 or len(history_data) > 0
if history_data:
latest = list(history_data.values())[0]
if "nodes" in latest:
self.last_activity = datetime.now()
logger.debug(f"Activité détectée: {len(running)} en cours, {len(pending)} en attente")
return has_activity
return False
except Exception as e:
logger.debug(f"Erreur check_queue: {e}")
return False
def get_memory_usage(self):
stats = self.check_comfyui_activity()
if stats and "memory" in stats:
vram_used = stats["memory"].get("vram_used", 0)
ram_used = stats["memory"].get("ram_used", 0)
total_vram = stats["memory"].get("vram_total", 0)
if total_vram > 0:
vram_percent = (vram_used / total_vram) * 100
return vram_used, ram_used, vram_percent
return None, None, None
def unload_all_models(self):
logger.info("🔄 Déchargement de tous les modèles ComfyUI...")
workflow = {
"3": {
"inputs": {"value": "unload_trigger"},
"class_type": "UnloadAllModelsNode"
}
}
try:
response = requests.post(
f"{COMFYUI_URL}/prompt",
json={"prompt": workflow},
timeout=30
)
if response.status_code == 200:
logger.info("✅ Modèles déchargés avec succès")
self.models_loaded = False
return True
else:
logger.error(f"❌ Erreur déchargement: {response.status_code}")
return False
except Exception as e:
logger.error(f"❌ Exception lors du déchargement: {e}")
return False
def soft_cleanup(self):
logger.info("🧹 Nettoyage soft (gc.collect + empty cache)...")
try:
requests.post(f"{COMFYUI_URL}/internal/cleanup", timeout=10)
except:
pass
vram, ram, percent = self.get_memory_usage()
if vram is not None:
logger.info(f"📊 VRAM après cleanup: {vram/1024**3:.2f}GB ({percent:.1f}%)")
def run(self):
logger.info(f"🚀 Démarrage ComfyUI Unload Monitor")
logger.info(f" Intervalle de vérification: {self.interval}s")
logger.info(f" Timeout d'inactivité: {self.timeout}s")
logger.info(f" URL ComfyUI: {COMFYUI_URL}")
while self.running:
try:
vram, ram, vram_percent = self.get_memory_usage()
if vram is not None and vram_percent > 1:
self.models_loaded = True
idle_time = (datetime.now() - self.last_activity).total_seconds()
logger.debug(f"📊 VRAM: {vram/1024**3:.2f}GB ({vram_percent:.1f}%), "
f"RAM: {ram/1024**3:.2f}GB, "
f"Inactivité: {idle_time:.0f}s")
if idle_time >= self.timeout:
logger.info(f"⏰ Inactivité de {idle_time:.0f}s >= {self.timeout}s")
self.unload_all_models()
time.sleep(5)
self.soft_cleanup()
self.last_activity = datetime.now()
else:
if self.models_loaded:
logger.info("📊 Modèles semblent déchargés (VRAM < 1%)")
self.models_loaded = False
time.sleep(self.interval)
except Exception as e:
logger.error(f"Erreur dans la boucle principale: {e}")
time.sleep(self.interval)
logger.info("👋 Monitor arrêté")
def main():
parser = argparse.ArgumentParser(description="ComfyUI Model Unload Monitor")
parser.add_argument("--interval", type=int, default=DEFAULT_INTERVAL,
help=f"Intervalle de vérification en secondes (défaut: {DEFAULT_INTERVAL})")
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT,
help=f"Timeout d'inactivité en secondes (défaut: {DEFAULT_TIMEOUT})")
parser.add_argument("--debug", action="store_true", help="Mode debug")
args = parser.parse_args()
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
monitor = ComfyUIUnloadMonitor(interval=args.interval, timeout=args.timeout)
monitor.run()
if __name__ == "__main__":
main()
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
# Kreuzberg Memory Manager - Redémarre le container après inactivité
# Usage: python3 kreuzberg-memory-manager.py [--interval 60] [--timeout 600] [--max-memory 2]
import requests
import time
import argparse
import logging
import subprocess
import sys
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
DEFAULT_INTERVAL = 60
DEFAULT_TIMEOUT = 600
DEFAULT_MAX_MEMORY = 2.0
KREUZBERG_API = "http://localhost:8300"
CONTAINER_NAME = "kreuzberg"
class KreuzbergMemoryManager:
def __init__(self, interval=DEFAULT_INTERVAL, timeout=DEFAULT_TIMEOUT, max_memory_gb=DEFAULT_MAX_MEMORY):
self.interval = interval
self.timeout = timeout
self.max_memory_gb = max_memory_gb
self.last_activity = time.time()
self.running = True
self.container_id = None
import signal
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
self.find_container()
def signal_handler(self, signum, frame):
logger.info("Signal reçu, arrêt du manager...")
self.running = False
def find_container(self):
try:
result = subprocess.run(
["docker", "ps", "-q", "--filter", f"name={CONTAINER_NAME}"],
capture_output=True, text=True
)
if result.stdout.strip():
self.container_id = result.stdout.strip()
logger.info(f"Container trouvé: {self.container_id[:12]}")
else:
logger.warning(f"Container {CONTAINER_NAME} non trouvé")
except Exception as e:
logger.error(f"Erreur recherche container: {e}")
def get_container_memory(self):
if not self.container_id:
return None
try:
result = subprocess.run(
["docker", "stats", "--no-stream", "--format", "{{.MemUsage}}", self.container_id],
capture_output=True, text=True
)
if result.stdout.strip():
mem_str = result.stdout.strip()
return self.parse_memory(mem_str)
except Exception as e:
logger.debug(f"Erreur stats: {e}")
return None
def parse_memory(self, mem_str):
try:
parts = mem_str.split("/")
if len(parts) >= 1:
used = parts[0].strip()
if "GiB" in used:
return float(used.replace("GiB", "")) * 1024
elif "MiB" in used:
return float(used.replace("MiB", ""))
elif "GB" in used:
return float(used.replace("GB", "")) * 1024
elif "MB" in used:
return float(used.replace("MB", ""))
except:
pass
return None
def check_api_activity(self):
try:
response = requests.get(f"{KREUZBERG_API}/health", timeout=5)
if response.status_code in [200, 404]:
self.last_activity = time.time()
return True
except:
pass
return False
def restart_container(self):
logger.info(f"🔄 Redémarrage du container {CONTAINER_NAME}...")
try:
subprocess.run(["docker", "restart", CONTAINER_NAME], check=True)
logger.info("✅ Container redémarré")
time.sleep(5)
self.find_container()
return True
except Exception as e:
logger.error(f"❌ Erreur redémarrage: {e}")
return False
def run(self):
logger.info(f"🚀 Démarrage Kreuzberg Memory Manager")
logger.info(f" Intervalle: {self.interval}s")
logger.info(f" Timeout inactivité: {self.timeout}s")
logger.info(f" Seuil mémoire: {self.max_memory_gb}GB")
idle_time = 0
while self.running:
try:
memory_mb = self.get_container_memory()
if memory_mb is not None:
memory_gb = memory_mb / 1024
is_active = self.check_api_activity()
if is_active:
idle_time = 0
self.last_activity = time.time()
logger.debug(f"📊 Mémoire: {memory_gb:.2f}GB, API active")
else:
idle_time += self.interval
logger.debug(f"📊 Mémoire: {memory_gb:.2f}GB, Inactif: {idle_time}s")
if memory_gb > self.max_memory_gb:
logger.warning(f"⚠️ Mémoire ({memory_gb:.2f}GB) > seuil ({self.max_memory_gb}GB)")
if idle_time >= self.timeout:
logger.info(f"⏰ Seuil timeout atteint ({idle_time}s >= {self.timeout}s)")
self.restart_container()
idle_time = 0
time.sleep(self.interval)
except Exception as e:
logger.error(f"Erreur: {e}")
time.sleep(self.interval)
logger.info("👋 Manager arrêté")
def main():
parser = argparse.ArgumentParser(description="Kreuzberg Memory Manager")
parser.add_argument("--interval", type=int, default=DEFAULT_INTERVAL,
help=f"Intervalle (défaut: {DEFAULT_INTERVAL}s)")
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT,
help=f"Timeout inactivité (défaut: {DEFAULT_TIMEOUT}s)")
parser.add_argument("--max-memory", type=float, default=DEFAULT_MAX_MEMORY,
help=f"Seuil mémoire GB (défaut: {DEFAULT_MAX_MEMORY}GB)")
parser.add_argument("--debug", action="store_true", help="Mode debug")
args = parser.parse_args()
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
manager = KreuzbergMemoryManager(
interval=args.interval,
timeout=args.timeout,
max_memory_gb=args.max_memory
)
manager.run()
if __name__ == "__main__":
main()