service.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. # -*- coding: utf-8 -*-
  2. """
  3. TrainerService - IService Wrapper fuer das Trainer-Framework.
  4. Verwaltet die Trainer-Registry und fuehrt Trainings als
  5. isolierte Subprocesse aus.
  6. """
  7. from __future__ import annotations
  8. import multiprocessing
  9. import asyncio
  10. import dataclasses
  11. from typing import TYPE_CHECKING
  12. from trixy_core.service import IService, ServicePriority, ServiceGroup
  13. from trixy_core.trainer.registry import TrainerRegistry
  14. from trixy_core.trainer.enums import TrainingState
  15. from trixy_core.trainer.dataclasses import (
  16. TrainerInfo,
  17. TrainingProgress,
  18. ValidationResult,
  19. FormSchema,
  20. )
  21. from trixy_core.utils.debug import pinfo, pdebug, perror
  22. if TYPE_CHECKING:
  23. from trixy_core.application import IApplication
  24. from trixy_core.trainer.itrainer import ITrainer
  25. def _load_nvidia_libs() -> None:
  26. """Laedt CUDA-Bibliotheken aus nvidia Pip-Packages (fuer onnxruntime-gpu im Subprocess)."""
  27. import os
  28. import sys
  29. import ctypes
  30. from pathlib import Path
  31. # Suche nvidia libs im venv
  32. for site_dir in sys.path:
  33. nvidia_base = Path(site_dir) / "nvidia"
  34. if nvidia_base.is_dir():
  35. lib_dirs = [str(p) for p in nvidia_base.glob("*/lib") if p.is_dir()]
  36. if lib_dirs:
  37. existing = os.environ.get("LD_LIBRARY_PATH", "")
  38. os.environ["LD_LIBRARY_PATH"] = ":".join(lib_dirs) + (":" + existing if existing else "")
  39. for lib_dir in lib_dirs:
  40. for so in sorted(Path(lib_dir).glob("*.so.*")):
  41. try:
  42. ctypes.cdll.LoadLibrary(str(so))
  43. except OSError:
  44. pass
  45. break
  46. def _training_worker(
  47. trainer: "ITrainer",
  48. progress_queue: multiprocessing.Queue,
  49. pause_flag: multiprocessing.Event,
  50. stop_flag: multiprocessing.Event,
  51. ) -> None:
  52. """Einstiegspunkt fuer den Trainings-Subprocess."""
  53. _load_nvidia_libs()
  54. try:
  55. trainer.train(progress_queue, pause_flag, stop_flag)
  56. except Exception as e:
  57. progress_queue.put(TrainingProgress(
  58. state=TrainingState.FAILED,
  59. error=str(e),
  60. ))
  61. class TrainerService(IService):
  62. """
  63. Verwaltet Trainer-Discovery und Training-Ausfuehrung.
  64. Trainings laufen als multiprocessing.Process, Progress
  65. wird ueber eine Queue gepollt.
  66. """
  67. PRIORITY = ServicePriority.OPTIONAL
  68. GROUP = ServiceGroup.UTILITY
  69. DEPENDENCIES: list[str] = []
  70. def __init__(self, application: "IApplication") -> None:
  71. super().__init__(application)
  72. self._registry = TrainerRegistry()
  73. # Aktives Training
  74. self._active_trainer_id: str = ""
  75. self._process: multiprocessing.Process | None = None
  76. self._progress_queue: multiprocessing.Queue | None = None
  77. self._pause_flag: multiprocessing.Event | None = None
  78. self._stop_flag: multiprocessing.Event | None = None
  79. self._current_progress = TrainingProgress()
  80. self._poll_task: asyncio.Task | None = None
  81. @property
  82. def registry(self) -> TrainerRegistry:
  83. """Trainer-Registry."""
  84. return self._registry
  85. @property
  86. def active_trainer_id(self) -> str:
  87. """ID des aktuell trainierenden Trainers."""
  88. return self._active_trainer_id
  89. @property
  90. def current_progress(self) -> TrainingProgress:
  91. """Aktueller Trainings-Fortschritt."""
  92. return self._current_progress
  93. async def start(self) -> None:
  94. """Startet den Service und entdeckt Trainer."""
  95. core_count = self._registry.discover_core()
  96. plugin_count = self._registry.discover_plugins()
  97. pinfo(f"TrainerService: {core_count} Core-Trainer, {plugin_count} Plugin-Trainer entdeckt")
  98. async def stop(self) -> None:
  99. """Stoppt den Service und laufende Trainings."""
  100. await self.stop_training()
  101. if self._poll_task:
  102. self._poll_task.cancel()
  103. self._poll_task = None
  104. def list_trainers(self) -> list[TrainerInfo]:
  105. """Liefert alle verfuegbaren Trainer."""
  106. return self._registry.list_info()
  107. def get_trainer(self, trainer_id: str) -> "ITrainer | None":
  108. """Liefert einen Trainer nach ID."""
  109. return self._registry.get(trainer_id)
  110. def get_trainer_detail(self, trainer_id: str) -> dict:
  111. """Liefert detaillierte Trainer-Informationen als dict."""
  112. trainer = self._registry.get(trainer_id)
  113. if not trainer:
  114. return {"error": f"Trainer '{trainer_id}' nicht gefunden"}
  115. info = trainer.get_info()
  116. settings_schema = trainer.get_settings_schema()
  117. dataset_schema = trainer.get_dataset_schema()
  118. optional_schema = trainer.get_optional_schema()
  119. return {
  120. "trainer_id": info.trainer_id,
  121. "name": info.name,
  122. "description": info.description,
  123. "version": info.version,
  124. "author": info.author,
  125. "model_type": info.model_type,
  126. "trainer_type": info.trainer_type.value,
  127. "settings_schema": dataclasses.asdict(settings_schema),
  128. "dataset_schema": dataclasses.asdict(dataset_schema),
  129. "optional_schema": dataclasses.asdict(optional_schema) if optional_schema else None,
  130. "settings": trainer.get_settings(),
  131. "dataset_info": trainer.get_dataset_info(),
  132. }
  133. def write_settings(self, trainer_id: str, settings: dict) -> tuple[bool, str]:
  134. """Setzt Einstellungen eines Trainers."""
  135. trainer = self._registry.get(trainer_id)
  136. if not trainer:
  137. return False, f"Trainer '{trainer_id}' nicht gefunden"
  138. try:
  139. trainer.set_settings(settings)
  140. return True, ""
  141. except Exception as e:
  142. return False, str(e)
  143. def validate_trainer(self, trainer_id: str) -> ValidationResult:
  144. """Validiert einen Trainer."""
  145. trainer = self._registry.get(trainer_id)
  146. if not trainer:
  147. return ValidationResult(errors=[f"Trainer '{trainer_id}' nicht gefunden"])
  148. try:
  149. return trainer.validate()
  150. except Exception as e:
  151. return ValidationResult(errors=[f"Validierungsfehler: {e}"])
  152. async def start_training(self, trainer_id: str) -> tuple[bool, str]:
  153. """Startet ein Training als Subprocess."""
  154. if self._process and self._process.is_alive():
  155. return False, "Ein Training laeuft bereits"
  156. trainer = self._registry.get(trainer_id)
  157. if not trainer:
  158. return False, f"Trainer '{trainer_id}' nicht gefunden"
  159. # Subprocess-Kommunikation vorbereiten (spawn statt fork — CUDA erfordert 'spawn')
  160. ctx = multiprocessing.get_context("spawn")
  161. self._progress_queue = ctx.Queue()
  162. self._pause_flag = ctx.Event()
  163. self._stop_flag = ctx.Event()
  164. self._active_trainer_id = trainer_id
  165. self._current_progress = TrainingProgress(state=TrainingState.STARTING)
  166. self._process = ctx.Process(
  167. target=_training_worker,
  168. args=(trainer, self._progress_queue, self._pause_flag, self._stop_flag),
  169. daemon=True,
  170. )
  171. self._process.start()
  172. # Polling-Task starten
  173. self._poll_task = asyncio.create_task(self._poll_progress())
  174. # ActivityTracker: Training-Start protokollieren
  175. try:
  176. tracker = self._application.services.get_service("ActivityTracker")
  177. if tracker:
  178. model_name = trainer.get_info().name if hasattr(trainer, "get_info") else trainer_id
  179. tracker.log_training_start(trainer_id, model_name)
  180. except Exception:
  181. pass
  182. pinfo(f"Training gestartet: {trainer_id} (PID: {self._process.pid})")
  183. return True, ""
  184. async def pause_training(self) -> tuple[bool, str]:
  185. """Pausiert das laufende Training."""
  186. if not self._process or not self._process.is_alive():
  187. return False, "Kein aktives Training"
  188. if self._pause_flag:
  189. self._pause_flag.set()
  190. self._current_progress.state = TrainingState.PAUSED
  191. return True, ""
  192. async def resume_training(self) -> tuple[bool, str]:
  193. """Setzt ein pausiertes Training fort."""
  194. if not self._process or not self._process.is_alive():
  195. return False, "Kein aktives Training"
  196. if self._pause_flag:
  197. self._pause_flag.clear()
  198. self._current_progress.state = TrainingState.TRAINING
  199. return True, ""
  200. async def stop_training(self) -> tuple[bool, str]:
  201. """Stoppt das laufende Training."""
  202. if not self._process:
  203. return False, "Kein aktives Training"
  204. if self._stop_flag:
  205. self._stop_flag.set()
  206. # Warten bis Prozess endet (max 10 Sek.)
  207. if self._process.is_alive():
  208. self._process.join(timeout=10)
  209. if self._process.is_alive():
  210. self._process.terminate()
  211. self._process.join(timeout=5)
  212. self._current_progress.state = TrainingState.COMPLETED
  213. self._cleanup_training()
  214. return True, ""
  215. def execute_action(self, trainer_id: str, action: str, params: dict) -> dict:
  216. """Fuehrt eine Trainer-Aktion aus."""
  217. trainer = self._registry.get(trainer_id)
  218. if not trainer:
  219. return {"success": False, "message": f"Trainer '{trainer_id}' nicht gefunden"}
  220. try:
  221. return trainer.execute_action(action, params)
  222. except Exception as e:
  223. return {"success": False, "message": f"Aktionsfehler: {e}"}
  224. def get_progress(self) -> TrainingProgress:
  225. """Liefert den aktuellen Fortschritt."""
  226. return self._current_progress
  227. async def _poll_progress(self) -> None:
  228. """Pollt die Progress-Queue des Subprocesses."""
  229. while True:
  230. try:
  231. await asyncio.sleep(0.5)
  232. if not self._progress_queue:
  233. break
  234. # Alle verfuegbaren Updates lesen
  235. while not self._progress_queue.empty():
  236. try:
  237. progress = self._progress_queue.get_nowait()
  238. if isinstance(progress, TrainingProgress):
  239. self._current_progress = progress
  240. except Exception:
  241. break
  242. # Pruefen ob Prozess noch laeuft
  243. if self._process and not self._process.is_alive():
  244. exit_code = self._process.exitcode
  245. state = self._current_progress.state
  246. if state in (TrainingState.TRAINING, TrainingState.STARTING):
  247. if exit_code != 0:
  248. self._current_progress.state = TrainingState.FAILED
  249. self._current_progress.error = (
  250. self._current_progress.error
  251. or f"Subprocess beendet mit Exit-Code {exit_code}"
  252. )
  253. else:
  254. self._current_progress.state = TrainingState.COMPLETED
  255. # ActivityTracker: Training-Abschluss protokollieren
  256. try:
  257. tracker = self._application.services.get_service("ActivityTracker")
  258. if tracker:
  259. is_success = self._current_progress.state == TrainingState.COMPLETED
  260. tracker.log_training_complete(
  261. self._active_trainer_id,
  262. success=is_success,
  263. error=self._current_progress.error or "",
  264. )
  265. except Exception:
  266. pass
  267. self._cleanup_training()
  268. break
  269. except asyncio.CancelledError:
  270. break
  271. except Exception as e:
  272. perror(f"Progress-Polling Fehler: {e}")
  273. break
  274. def _cleanup_training(self) -> None:
  275. """Raeumt nach einem Training auf."""
  276. self._process = None
  277. self._progress_queue = None
  278. self._pause_flag = None
  279. self._stop_flag = None
  280. self._active_trainer_id = ""