Files
AnyImage/src/anyimage/server/model_manager.py
nan 196b964efc Add cutout shell and volume node assets for depth volume processing
- Implemented `cutout_shell.py` to build front normals and shell positions for balloon geometry.
- Created `cutout_volume.py` to establish smooth center fields and manage balloon surface attributes.
- Introduced `pisa_sr_export.py` for exporting PiSA-SR components with dependencies and asset finalization.
2026-09-08 17:18:03 +08:00

230 lines
8.0 KiB
Python

import gc
import hashlib
import time
from pathlib import Path
from types import SimpleNamespace
from . import model_catalog
from .model_download import download_model
class ModelSessionCache:
"""Cache ONNX sessions shared by Jobs in one Server process."""
def __init__(self):
self.ben2_model = None
self.ben2_key = None
self.da3_model = None
self.da3_key = None
self.moge_model = None
self.moge_key = None
self.upscale_model = None
self.upscale_key = None
def get_ben2(self, model_dir, device):
requested_device = getattr(device, "type", device)
key = (str(Path(model_dir).resolve()), requested_device, "onnx")
if self.ben2_model is not None and self.ben2_key == key:
return self.ben2_model, 0.0
self.release_ben2()
return self._load_ben2(model_dir, requested_device, key)
def _load_ben2(self, model_dir, requested_device, key):
from .models.onnx_ben2 import create_session
started = time.perf_counter()
model = create_session(model_dir, requested_device)
self.ben2_model = model
self.ben2_key = key
elapsed_ms = (time.perf_counter() - started) * 1000.0
return model, elapsed_ms
def get_da3(self, model_dir, device):
requested_device = getattr(device, "type", device)
key = (str(Path(model_dir).resolve()), requested_device, "onnx")
if self.da3_model is not None and self.da3_key == key:
return self.da3_model
self.release_geometry()
return self._load_da3(model_dir, requested_device, key)
def _load_da3(self, model_dir, requested_device, key):
from .models.onnx_da3 import create_session
model = create_session(model_dir, requested_device)
self.da3_model = model
self.da3_key = key
return model
def get_moge2(self, model_dir, requested_device):
from .models.onnx_moge2 import create_session
key = (str(Path(model_dir).resolve()), requested_device, "onnx")
if self.moge_model is not None and self.moge_key == key:
return self.moge_model
self.release_geometry()
model = create_session(model_dir, requested_device)
self.moge_model = model
self.moge_key = key
return model
def get_upscale(self, model_dir, requested_device):
from .models.onnx_upscale import create_session
if Path(model_dir).name == "pisa-sr":
from .models.onnx_pisa_sr import PisaSession as create_session
key = (str(Path(model_dir).resolve()), requested_device, "onnx")
if self.upscale_model is not None and self.upscale_key == key:
return self.upscale_model
self.release_upscale()
model = create_session(model_dir, requested_device)
self.upscale_model = model
self.upscale_key = key
return model
def release_ben2(self):
self.ben2_model = None
self.ben2_key = None
self._release_runtime_cache()
def release_geometry(self):
self.da3_model = None
self.da3_key = None
self.moge_model = None
self.moge_key = None
self._release_runtime_cache()
def release_upscale(self):
close = getattr(self.upscale_model, "close", None)
if close is not None:
close()
self.upscale_model = None
self.upscale_key = None
self._release_runtime_cache()
def close(self):
close = getattr(self.upscale_model, "close", None)
if close is not None:
close()
self.ben2_model = None
self.da3_model = None
self.moge_model = None
self.upscale_model = None
self.ben2_key = None
self.da3_key = None
self.moge_key = None
self.upscale_key = None
self._release_runtime_cache()
def snapshot(self):
geometry_key = self.da3_key or self.moge_key
return {
"ben2": Path(self.ben2_key[0]).name if self.ben2_key else "",
"geometry": Path(geometry_key[0]).name if geometry_key else "",
"upscale": (Path(self.upscale_key[0]).name if self.upscale_key else ""),
}
def _release_runtime_cache(self):
gc.collect()
class ModelManager:
"""Own downloaded model files and loaded inference sessions."""
def __init__(self, models_directory):
self.models_directory = Path(models_directory)
self.sessions = ModelSessionCache()
self.verified_files = {}
def directory(self, model_key):
model = model_catalog.get_downloadable_model(model_key)
return self.models_directory / model.directory_name
def ready(self, model_key):
model = model_catalog.get_downloadable_model(model_key)
directory = self.directory(model_key)
if model_key == "PISA_SR":
for filename, size, checksum in model.r2_files:
path = directory / filename
try:
stat = path.stat()
if stat.st_size != size:
return False
identity = (stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns)
cached = self.verified_files.get(path)
# Windows can coalesce timestamps for immediately repeated writes.
cacheable = size >= 1024 * 1024 and time.time_ns() - stat.st_mtime_ns > 1_000_000_000
if not cacheable or cached != (identity, checksum):
with path.open("rb") as stream:
actual = hashlib.file_digest(stream, "sha256").hexdigest()
if actual != checksum:
return False
if cacheable:
self.verified_files[path] = (identity, checksum)
except OSError:
return False
return True
return all(any(directory.glob(pattern)) for pattern in model.ready_patterns)
def get_ben2(self, model_dir, device):
return self.sessions.get_ben2(model_dir, device)
def get_da3(self, model_dir, device):
return self.sessions.get_da3(model_dir, device)
def get_moge2(self, model_dir, device):
return self.sessions.get_moge2(model_dir, device)
def get_upscale(self, model_dir, device):
return self.sessions.get_upscale(model_dir, device)
def release_upscale(self):
self.sessions.release_upscale()
def snapshot(self):
return {"loaded": self.sessions.snapshot()}
def clear(self):
self.sessions.close()
def close(self):
self.sessions.close()
def download(self, context, model_key):
return self._download(context, model_key, context.progress)
def download_missing(self, context, model_keys):
missing = [key for key in model_keys if not self.ready(key)]
if not missing:
context.progress(1.0, "Required models are ready")
return {"downloaded": []}
total = len(missing)
for index, model_key in enumerate(missing):
def report(value, message, *, offset=index):
context.progress((offset + value) / total, message)
self._download(context, model_key, report)
return {"downloaded": missing}
def _download(self, context, model_key, progress):
model = model_catalog.get_downloadable_model(model_key)
args = SimpleNamespace(
repository=model.huggingface_repository,
model_dir=str(self.directory(model_key)),
progress=progress,
cancel_check=context.check_cancelled,
label=model.label,
allow_pattern=list(model.download_patterns),
required_pattern=list(model.ready_patterns),
r2_directory=model.r2_directory,
r2_file=[
f"{filename}|{size}|{checksum}"
for filename, size, checksum in model.r2_files
],
)
context.check_cancelled()
download_model(args)
context.check_cancelled()