Skip to content

Commit

Permalink
Merge pull request #16 from bioimage-io/slim-docker-image
Browse files Browse the repository at this point in the history
Slim docker image
  • Loading branch information
nilsmechtel authored Jan 21, 2025
2 parents ca9090d + 9b62e9b commit 3435284
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 149 deletions.
11 changes: 4 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,16 @@ RUN echo "bioimageio_colab ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Upgrade pip
RUN pip install --upgrade pip

# Copy the requirements file for SAM to the docker environment
# Copy the requirements files to the docker environment
COPY ./requirements.txt /app/requirements.txt
COPY ./requirements-sam.txt /app/requirements-sam.txt

# Install the required packages for SAM
RUN pip install -r /app/requirements-sam.txt
# Install the required packages to register the service
RUN pip install -r /app/requirements.txt

# Copy the python module and data to the docker environment
COPY ./bioimageio_colab /app/bioimageio_colab
COPY ./data /app/data

# Create cache directory for models
RUN mkdir -p /app/.model_cache
COPY ./data/example_image.tif /app/data/example_image.tif

# Change ownership of the application directory to the non-root user
RUN chown -R bioimageio_colab:bioimageio_colab /app/
Expand Down
7 changes: 4 additions & 3 deletions bioimageio_colab/models/sam_deployment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import os

import aiohttp
import numpy as np
from ray import serve

Expand Down Expand Up @@ -33,6 +30,8 @@ def __init__(self, cache_dir: str):
self.models = SAM_MODELS

async def _download_model(self, model_path: str, model_url: str) -> None:
import aiohttp

async with aiohttp.ClientSession() as session:
async with session.get(model_url) as response:
if response.status != 200:
Expand All @@ -43,6 +42,8 @@ async def _download_model(self, model_path: str, model_url: str) -> None:

@serve.multiplexed(max_num_models_per_replica=2)
async def get_model(self, model_id: str):
import os

model_path = os.path.join(self.cache_dir, f"{model_id}.pt")

if not os.path.exists(model_path):
Expand Down
15 changes: 10 additions & 5 deletions bioimageio_colab/models/sam_image_encoder.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from typing import Literal

import numpy as np
import torch
from segment_anything import sam_model_registry
from segment_anything.utils.transforms import ResizeLongestSide


class SamImageEncoder:
def __init__(
self, model_path: str, model_architecture: Literal["vit_b", "vit_l", "vit_h"]
):
import torch
from segment_anything import sam_model_registry
from segment_anything.utils.transforms import ResizeLongestSide

# Extract image encoder from checkpoint
device = "cuda" if torch.cuda.is_available() else "cpu"
build_sam = sam_model_registry[model_architecture]
Expand Down Expand Up @@ -53,7 +54,9 @@ def _to_image_format(self, array: np.ndarray) -> np.ndarray:

return array

def _preprocess(self, array: np.array) -> torch.Tensor:
def _preprocess(self, array: np.array):
import torch

# Validate image shape and dtype
original_image = self._to_image_format(array)

Expand All @@ -77,7 +80,7 @@ def _preprocess(self, array: np.array) -> torch.Tensor:

return input_image_torch

def encode(self, array: np.ndarray):
def encode(self, array: np.ndarray) -> dict:
"""
Encode an image using the SAM image encoder.
Expand All @@ -89,6 +92,8 @@ def encode(self, array: np.ndarray):
- "features" (np.ndarray): The extracted features from the image
- "input_size" (tuple): The size of the input image (H, W)
"""
import torch

# Preprocess the input image
input_image = self._preprocess(array)
input_size = tuple(input_image.shape[-2:])
Expand Down
46 changes: 17 additions & 29 deletions bioimageio_colab/register_sam_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@

import numpy as np
import ray
import torch
from dotenv import find_dotenv, load_dotenv
from hypha_rpc import connect_to_server
from hypha_rpc.rpc import RemoteService
from kaibu_utils import mask_to_features
# from kaibu_utils import mask_to_features
from ray.serve.config import AutoscalingConfig
from tifffile import imread

Expand Down Expand Up @@ -50,20 +49,14 @@ def parse_requirements(file_path) -> list:


def connect_to_ray(address: str = None) -> None:
if address:
# Create runtime environment
base_requirements = parse_requirements(
os.path.join(BASE_DIR, "requirements.txt")
)
sam_requirements = parse_requirements(
os.path.join(BASE_DIR, "requirements-sam.txt")
)
runtime_env = {
"pip": base_requirements + sam_requirements,
"py_modules": [os.path.join(BASE_DIR, "bioimageio_colab")],
}
else:
runtime_env = None
# Create runtime environment
sam_requirements = parse_requirements(
os.path.join(BASE_DIR, "requirements-sam.txt")
)
runtime_env = {
"pip": sam_requirements,
"py_modules": [os.path.join(BASE_DIR, "bioimageio_colab")],
}

# Check if Ray is already initialized
if ray.is_initialized():
Expand Down Expand Up @@ -138,7 +131,7 @@ def ping(context: dict = None) -> str:
async def compute_image_embedding(
app_name: str,
image: np.ndarray,
model_name: str,
model_id: str,
require_login: bool = False,
context: dict = None,
) -> dict:
Expand All @@ -152,13 +145,13 @@ async def compute_image_embedding(

user_id = user["id"]
logger.info(
f"User '{user_id}' - Computing embedding (model: '{model_name}')..."
f"User '{user_id}' - Computing embedding (model: '{model_id}')..."
)

# Compute the embedding
# Returns: {"features": embedding, "input_size": input_size}
handle = ray.serve.get_app_handle(name=app_name)
result = await handle.remote(model_id=model_name, array=image)
result = await handle.remote(model_id=model_id, array=image)

logger.info(f"User '{user_id}' - Embedding computed successfully.")

Expand All @@ -170,7 +163,7 @@ async def compute_image_embedding(

# def compute_mask(
# cache_dir: str,
# model_name: str,
# model_id: str,
# embedding: np.ndarray,
# image_size: tuple,
# point_coords: np.ndarray,
Expand All @@ -184,14 +177,14 @@ async def compute_image_embedding(
# """
# try:
# user_id = context["user"].get("id") if context else "anonymous"
# logger.info(f"User '{user_id}' - Segmenting image (model: '{model_name}')...")
# logger.info(f"User '{user_id}' - Segmenting image (model: '{model_id}')...")

# if not format in ["mask", "kaibu"]:
# raise ValueError("Invalid format. Please choose either 'mask' or 'kaibu'.")

# # Load the model
# sam_predictor = load_model_from_ckpt(
# model_name=model_name,
# model_id=model_id,
# cache_dir=cache_dir,
# )

Expand Down Expand Up @@ -322,11 +315,6 @@ async def register_service(args: dict) -> None:
"""
logger.info("Registering the SAM annotation service...")
logger.info(f"Available CPU cores: {os.cpu_count()}")
if torch.cuda.is_available():
logger.info(f"Available GPUs: {torch.cuda.device_count()}")
logger.info(f"Available GPU devices: {torch.cuda.get_device_name()}")
else:
logger.info("No GPU devices available.")

workspace_token = args.token or os.environ.get("WORKSPACE_TOKEN")
if not workspace_token:
Expand Down Expand Up @@ -406,7 +394,7 @@ async def register_service(args: dict) -> None:
)

# This will register probes service where you can accessed via hypha or the HTTP proxy
print(f"Probes registered at workspace: {workspace}")
print(
logger.info(f"Probes registered at workspace: {workspace}")
logger.info(
f"Test the liveness probe here: {args.server_url}/{workspace}/services/probes/liveness"
)
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ services:
"--server_url=https://hypha.aicell.io",
"--workspace_name=bioimageio-colab",
"--service_id=microsam",
"--model_names=sam_vit_b",
"--cache_dir=/tmp/ray/.model_cache"
]
deploy:
resources:
Expand Down
11 changes: 6 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ requires = ["setuptools", "wheel"]

[project]
name = "bioimageio-colab"
version = "0.1.8"
version = "0.1.9"
readme = "README.md"
description = "Collaborative image annotation and model training with human in the loop."
dependencies = [
"hypha-rpc>=0.20.43",
"requests",
"numpy",
"requests",
"hypha-rpc",
"kaibu-utils",
"numpy",
"python-dotenv",
"ray[serve]",
"tifffile",
]

[tool.setuptools]
Expand Down
1 change: 0 additions & 1 deletion requirements-cellpose.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
tifffile==2024.7.24
matplotlib==3.9.0
opencv-python-headless==3.4.18.65
cellpose==3.0.11
6 changes: 3 additions & 3 deletions requirements-sam.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-r "requirements.txt"
ray[client]==2.33.0
ray[serve]==2.33.0
aiohttp==3.11.11
numpy==2.2.2
ray[client,serve]==2.33.0
segment_anything==1.0
torch==2.3.1
torchvision==0.18.1
File renamed without changes.
9 changes: 5 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
hypha-rpc==0.20.44
kaibu-utils==0.1.14
numpy==1.26.4
hypha-rpc==0.20.47
# kaibu-utils==0.1.14
numpy==2.2.2
python-dotenv==1.0.1
requests==2.31.0
ray[serve]==2.33.0
tifffile==2025.1.10
36 changes: 21 additions & 15 deletions test/stress_test.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,45 @@
from hypha_rpc import connect_to_server
import numpy as np
import asyncio
import argparse
import asyncio

import numpy as np
from hypha_rpc import connect_to_server
from tifffile import imread

SERVER_URL = "https://hypha.aicell.io"
WORKSPACE_NAME = "bioimageio-colab"
SERVICE_ID = "microsam"
SID = f"{WORKSPACE_NAME}/{SERVICE_ID}"
MODEL_IDS = ["sam_vit_b", "sam_vit_b_lm", "sam_vit_b_em_organelles"]
IMG_PATH = "./data/example_image.tif"


async def run_client(client_id: int, image: np.ndarray):
async def run_client(
client_id: int, image: np.ndarray, model_id: str, method_timeout: int = 30
):
print(f"Client {client_id} started", flush=True)
client = await connect_to_server({"server_url": SERVER_URL, "method_timeout": 30})
segment_svc = await client.get_service(SID, {"mode": "random"})
await segment_svc.segment(model_name="vit_b", image=image, point_coordinates=[[128, 128]], point_labels=[1])
await asyncio.sleep(1)
await segment_svc.segment(model_name="vit_b", image=image, point_coordinates=[[20, 50]], point_labels=[1])
await asyncio.sleep(1)
await segment_svc.segment(model_name="vit_b", image=image, point_coordinates=[[180, 10]], point_labels=[1])
client = await connect_to_server(
{"server_url": SERVER_URL, "method_timeout": method_timeout}
)
service = await client.get_service(
f"{WORKSPACE_NAME}/{SERVICE_ID}", {"mode": "random"}
)
await service.compute_embedding(model_id=model_id, image=image)
print(f"Client {client_id} finished", flush=True)


async def stress_test(num_clients: int):
image=np.random.rand(256, 256)
image = imread(IMG_PATH)
tasks = []
for client_id in range(num_clients):
await asyncio.sleep(0.1)
tasks.append(run_client(client_id, image))
model_id = MODEL_IDS[np.random.randint(0, len(MODEL_IDS))]
tasks.append(run_client(client_id=client_id, image=image, model_id=model_id))
await asyncio.gather(*tasks)
print("All clients finished")


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--num_clients", type=int, default=50)
args = parser.parse_args()

asyncio.run(stress_test(args.num_clients))
asyncio.run(stress_test(args.num_clients))
5 changes: 2 additions & 3 deletions test/test_image_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@
import sys

import numpy as np
# from kaibu_utils import mask_to_features

sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from kaibu_utils import mask_to_features

from docs.data_providing_service import get_random_image, save_annotation


def test_load_image():
supported_file_types = ("tif", "tiff")
image, image_name = get_random_image(
image_folder="./bioimageio_colab/",
image_folder="./data/",
supported_file_types=supported_file_types,
)
assert image is not None
Expand Down
Loading

0 comments on commit 3435284

Please sign in to comment.