From 59cd08f5daa6e289825102ba558939252018b360 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 16 Nov 2024 10:49:08 -0500 Subject: [PATCH] update docker and progress monitoring Signed-off-by: Vladimir Mandic --- Dockerfile | 38 ++++++++++++++++++++++++++++---------- installer.py | 6 +++--- launch.py | 4 +--- modules/api/api.py | 1 + modules/api/models.py | 17 +++++++++++++++++ modules/api/server.py | 5 ++++- modules/progress.py | 1 - modules/shared.py | 2 ++ modules/shared_state.py | 32 ++++++++++++++++++++++++++++++++ 9 files changed, 88 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index fbfc1eb9d..5f38d6caa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,10 @@ # SD.Next Dockerfile +# docs: + +# base image FROM pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime + +# metadata LABEL org.opencontainers.image.vendor="SD.Next" LABEL org.opencontainers.image.authors="vladmandic" LABEL org.opencontainers.image.url="https://github.com/vladmandic/automatic/" @@ -9,31 +14,44 @@ LABEL org.opencontainers.image.licenses="AGPL-3.0" LABEL org.opencontainers.image.title="SD.Next" LABEL org.opencontainers.image.description="SD.Next: Advanced Implementation of Stable Diffusion and other Diffusion-based generative image models" LABEL org.opencontainers.image.base.name="https://hub.docker.com/pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime" -WORKDIR / -COPY . . +LABEL org.opencontainers.image.version="latest" + +# minimum install +RUN ["apt-get", "-y", "update"] +RUN ["apt-get", "-y", "install", "git", "build-essential", "google-perftools", "curl"] +# optional if full cuda-dev is required by some downstream library +# RUN ["apt-get", "-y", "nvidia-cuda-toolkit"] +RUN ["/usr/sbin/ldconfig"] + +# copy sdnext +COPY . /app +WORKDIR /app + # stop pip and uv from caching ENV PIP_NO_CACHE_DIR=true ENV PIP_ROOT_USER_ACTION=ignore ENV UV_NO_CACHE=true -ENV SD_INSTALL_DEBUG=true # disable model hashing for faster startup ENV SD_NOHASHING=true # set data directories ENV SD_DATADIR="/mnt/data" ENV SD_MODELSDIR="/mnt/models" -# minimum install -RUN ["apt-get", "-y", "update"] -RUN ["apt-get", "-y", "install", "git", "build-essential", "google-perftools"] -RUN ["ldconfig"] +ENV SD_DOCKER=true + # tcmalloc is not required but it is highly recommended ENV LD_PRELOAD=libtcmalloc.so.4 # sdnext will run all necessary pip install ops and then exit -RUN ["python", "launch.py", "--debug", "--uv", "--use-cuda", "--log", "sdnext.log", "--test", "--optional"] +RUN ["python", "/app/launch.py", "--debug", "--uv", "--use-cuda", "--log", "sdnext.log", "--test", "--optional"] # preinstall additional packages to avoid installation during runtime + # actually run sdnext CMD ["python", "launch.py", "--debug", "--skip-all", "--listen", "--quick", "--api-log", "--log", "sdnext.log"] + # expose port EXPOSE 7860 -# TBD add healthcheck function -HEALTHCHECK NONE + +# healthcheck function +# HEALTHCHECK --interval=60s --timeout=10s --start-period=60s --retries=3 CMD curl --fail http://localhost:7860/sdapi/v1/status || exit 1 + +# stop signal STOPSIGNAL SIGINT diff --git a/installer.py b/installer.py index 7a5b9d756..ba744b65f 100644 --- a/installer.py +++ b/installer.py @@ -412,14 +412,14 @@ def get_platform(): else: release = platform.release() return { - # 'host': platform.node(), 'arch': platform.machine(), 'cpu': platform.processor(), 'system': platform.system(), 'release': release, - # 'platform': platform.platform(aliased = True, terse = False), - # 'version': platform.version(), 'python': platform.python_version(), + 'docker': os.environ.get('SD_INSTALL_DEBUG', None) is not None, + # 'host': platform.node(), + # 'version': platform.version(), } except Exception as e: return { 'error': e } diff --git a/launch.py b/launch.py index 9075c358b..f944a7e54 100755 --- a/launch.py +++ b/launch.py @@ -258,9 +258,7 @@ def main(): alive = False requests = 0 if round(time.time()) % 120 == 0: - state = f'job="{instance.state.job}" {instance.state.job_no}/{instance.state.job_count}' if instance.state.job != '' or instance.state.job_no != 0 or instance.state.job_count != 0 else 'idle' - uptime = round(time.time() - instance.state.server_start) - installer.log.debug(f'Server: alive={alive} jobs={instance.state.total_jobs} requests={requests} uptime={uptime} memory={get_memory_stats()} backend={instance.backend} state={state}') + installer.log.debug(f'Server: alive={alive} requests={requests} memory={get_memory_stats()} {instance.state.status()}') if not alive: if uv is not None and uv.wants_restart: installer.log.info('Server restarting...') diff --git a/modules/api/api.py b/modules/api/api.py index 0ba388855..f8346995d 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -38,6 +38,7 @@ class Api: self.add_api_route("/sdapi/v1/log", server.get_log_buffer, methods=["GET"], response_model=List[str]) self.add_api_route("/sdapi/v1/start", self.get_session_start, methods=["GET"]) self.add_api_route("/sdapi/v1/version", server.get_version, methods=["GET"]) + self.add_api_route("/sdapi/v1/status", server.get_status, methods=["GET"], response_model=models.ResStatus) self.add_api_route("/sdapi/v1/platform", server.get_platform, methods=["GET"]) self.add_api_route("/sdapi/v1/progress", server.get_progress, methods=["GET"], response_model=models.ResProgress) self.add_api_route("/sdapi/v1/interrupt", server.post_interrupt, methods=["POST"]) diff --git a/modules/api/models.py b/modules/api/models.py index 4f01f47d5..e68ebf081 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -300,6 +300,23 @@ class ResProgress(BaseModel): current_image: str = Field(default=None, title="Current image", description="The current image in base64 format. opts.show_progress_every_n_steps is required for this to work.") textinfo: str = Field(default=None, title="Info text", description="Info text used by WebUI.") +class ResStatus(BaseModel): + status: str = Field(title="Status", description="Current status") + task: str = Field(title="Task", description="Current task") + timestamp: Optional[str] = Field(title="Timestamp", description="Timestamp of the current job") + id: str = Field(title="ID", description="ID of the current task") + job: int = Field(title="Job", description="Current job") + jobs: int = Field(title="Jobs", description="Total jobs") + total: int = Field(title="Total Jobs", description="Total jobs") + step: int = Field(title="Step", description="Current step") + steps: int = Field(title="Steps", description="Total steps") + queued: int = Field(title="Queued", description="Number of queued tasks") + uptime: int = Field(title="Uptime", description="Uptime of the server") + elapsed: Optional[float] = Field(title="Elapsed time") + eta: Optional[float] = Field(title="ETA in secs") + progress: Optional[float] = Field(title="Progress", description="The progress with a range of 0 to 1") + + class ReqInterrogate(BaseModel): image: str = Field(default="", title="Image", description="Image to work on, must be a Base64 string containing the image's data.") clip_model: str = Field(default="", title="CLiP Model", description="The interrogate model used.") diff --git a/modules/api/server.py b/modules/api/server.py index 95233dbcd..939e19c86 100644 --- a/modules/api/server.py +++ b/modules/api/server.py @@ -1,3 +1,4 @@ +import time from typing import Any, Dict from fastapi import Depends from modules import shared @@ -66,7 +67,6 @@ def get_cmd_flags(): return vars(shared.cmd_opts) def get_progress(req: models.ReqProgress = Depends()): - import time if shared.state.job_count == 0: return models.ResProgress(progress=0, eta_relative=0, state=shared.state.dict(), textinfo=shared.state.textinfo) shared.state.do_set_current_image() @@ -85,6 +85,9 @@ def get_progress(req: models.ReqProgress = Depends()): res = models.ResProgress(progress=progress, eta_relative=eta_relative, state=shared.state.dict(), current_image=current_image, textinfo=shared.state.textinfo) return res +def get_status(): + return shared.state.status() + def post_interrupt(): shared.state.interrupt() return {} diff --git a/modules/progress.py b/modules/progress.py index abd6d906d..d18d1ee9f 100644 --- a/modules/progress.py +++ b/modules/progress.py @@ -73,7 +73,6 @@ def progressapi(req: ProgressRequest): elapsed = time.time() - shared.state.time_start if shared.state.time_start is not None else 0 predicted = elapsed / progress if progress > 0 else None eta = predicted - elapsed if predicted is not None else None - # shared.log.debug(f'Progress: step={step_x}:{step_y} batch={batch_x}:{batch_y} current={current} total={total} progress={progress} elapsed={elapsed} eta={eta}') id_live_preview = req.id_live_preview live_preview = None shared.state.set_current_image() diff --git a/modules/shared.py b/modules/shared.py index 7b273b0f9..46d47b16d 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -1,3 +1,4 @@ +from functools import lru_cache import io import os import sys @@ -1271,6 +1272,7 @@ def html(filename): return "" +@lru_cache(maxsize=1) def get_version(): version = None if version is None: diff --git a/modules/shared_state.py b/modules/shared_state.py index 067fb21eb..9947dcb70 100644 --- a/modules/shared_state.py +++ b/modules/shared_state.py @@ -62,6 +62,38 @@ class State: } return obj + def status(self): + from modules import progress + from modules.api import models + res = models.ResStatus( + task=self.job, + id=progress.current_task or '', + job=max(self.job_no, 0), + jobs=max(self.frame_count, self.job_count, self.job_no), + total=self.total_jobs, + timestamp=self.job_timestamp if self.job != '' else None, + step=self.sampling_step, + steps=self.sampling_steps, + queued=len(progress.pending_tasks), + status='unknown', + uptime = round(time.time() - self.server_start) + ) + res.step = res.steps * res.job + res.step + res.steps = res.steps * res.jobs + res.progress = round(min(1, abs(res.step / res.steps) if res.steps > 0 else 0), 2) + res.elapsed = round(time.time() - self.time_start, 2) if self.time_start is not None else None + predicted = round(res.elapsed / res.progress, 2) if res.progress > 0 and res.elapsed is not None else None + res.eta = round(predicted - res.elapsed, 2) if predicted is not None else None + if self.paused: + res.status = 'paused' + elif self.interrupted: + res.status = 'interrupted' + elif self.skipped: + res.status = 'skipped' + else: + res.status = 'running' if self.job != '' else 'idle' + return res + def begin(self, title="", api=None): import modules.devices self.total_jobs += 1