Skip to content

Commit

Permalink
use hypha-rpc and add ray app loader
Browse files Browse the repository at this point in the history
  • Loading branch information
oeway committed Aug 16, 2024
1 parent 45211d7 commit 4b81325
Show file tree
Hide file tree
Showing 31 changed files with 159 additions and 28 deletions.
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
include environments/*
recursive-include bioimageio/apps
recursive-include bioimageio/engine/apps *
recursive-include bioimageio/engine/ray_apps *
global-exclude *.pyc
global-exclude *__pycache__*
5 changes: 0 additions & 5 deletions bioimageio/apps/imagej/manifest.yaml

This file was deleted.

14 changes: 8 additions & 6 deletions bioimageio/engine/app_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from typing import Optional
import importlib.util
from enum import Enum
from hypha_rpc.utils import ObjectProxy

logger = logging.getLogger(__name__)

class AppRuntime(Enum):
python = "python"
pyodide = "pyodide"
triton = "triton"
ray = "ray"

class AppInfo(BaseModel):
name: str
Expand All @@ -21,13 +23,13 @@ class AppInfo(BaseModel):
entrypoint: Optional[str] = None

async def run(self, server):
assert self.entrypoint

file_path = Path(__file__).parent / "apps" / self.id / self.entrypoint
module_name = 'bioimageio.engine.apps.' + self.id.replace('-', '_')
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
if self.runtime == AppRuntime.python:
assert self.entrypoint

file_path = Path(__file__).parent.parent.parent / "bioimageio/apps" / self.id / self.entrypoint
module_name = 'bioimageio.engine.apps.' + self.id.replace('-', '_')
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
assert hasattr(module, "hypha_startup")
await module.hypha_startup(server)
Expand Down
Empty file.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
113 changes: 113 additions & 0 deletions bioimageio/engine/ray_app_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Provide main entrypoint."""
import asyncio
from ray import serve
import logging
import os
import sys
from pathlib import Path
import urllib.request
from starlette.requests import Request
from starlette.responses import HTMLResponse

from hypha_rpc.utils import ObjectProxy
from hypha_rpc.sync import connect_to_server

logging.basicConfig(stream=sys.stdout)
logger = logging.getLogger("app_launcher")
logger.setLevel(logging.INFO)


def load_app(plugin_file):
"""Load app file."""
if os.path.isfile(plugin_file):
with open(plugin_file, "r", encoding="utf-8") as fil:
content = fil.read()
elif plugin_file.startswith("http"):
with urllib.request.urlopen(plugin_file) as response:
content = response.read().decode("utf-8")
# remove query string
plugin_file = plugin_file.split("?")[0]
else:
raise Exception(f"Invalid input app file path: {plugin_file}")

if plugin_file.endswith(".py"):
app_config = ObjectProxy()
import hypha_rpc
def export(app_class, config=None):
app_config.update(config or {})
app_config.app_class = app_class
hypha_rpc.api = ObjectProxy(export=export)
exec(content, globals()) # pylint: disable=exec-used
logger.info("Plugin executed")
return app_config
else:
raise RuntimeError(f"Invalid script file type ({plugin_file})")

@serve.deployment
class HyphaApp:
def __init__(self, server_url, workspace, token, services):
self._services = services
self._hypha_server = connect_to_server({"server_url": "https://hypha.aicell.io", "token": token, "workspace": workspace})
svc = {
"name": "Ray Functions",
"id": "ray-functions",
"config": {
"visibility": "protected"
},
}

def create_service_function(name, service_handle):
async def service_function(*args, **kwargs):
return await service_handle.translate.remote(*args, **kwargs)
service_function.__name__ = name
return service_function

for service_name, service_bind in self._services.items():
svc[service_name] = create_service_function(service_name, service_bind)
info = self._hypha_server.register_service(svc, {"overwrite":True})
print("Hypha service info:", info)
self.info = info

async def __call__(self, request: Request):
redirect_url = f"https://hypha.aicell.io/{self.info.config.workspace}/services/{self.info.id.split('/')[1]}/translator?text=hello"
return HTMLResponse(
"""
<html>
<head>
<meta http-equiv="refresh" content="2; URL='{}'" />
</head>
<body>
<p>Redirecting to Hypha...</p>
<h1>Services:</h1>
<ul>
{}
</ul>
</body>
</html>
""".format(redirect_url, "\n".join([f"<li>{name}</li>" for name in self._services.keys()]))
)


current_dir = Path(os.path.dirname(os.path.realpath(__file__)))

ray_apps = {}
apps_dir = current_dir / "ray_apps"
for app_file in apps_dir.iterdir():
if app_file.is_file() and app_file.suffix == ".py" and app_file.stem != "__init__":
load_app(str(app_file))
app_info = load_app(str(app_file))
app_deployment = serve.deployment(name=app_info.name)(app_info.app_class).bind()
ray_apps[app_info.name] = app_deployment

# Getting config from environment
server_url = os.environ.get("HYPHA_SERVER_URL")
workspace = os.environ.get("HYPHA_WORKSPACE")
token = os.environ.get("HYPHA_TOKEN")

app = HyphaApp.bind(server_url, workspace, token, ray_apps)

if __name__ == "__main__":
serve.start()
serve.run(app, name="hypha-apps")
import asyncio
asyncio.get_event_loop().run_forever()
Empty file.
20 changes: 20 additions & 0 deletions bioimageio/engine/ray_apps/translator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from hypha_rpc import api
from transformers import pipeline


class Translator:
def __init__(self):
# Load model
self.model = pipeline("translation_en_to_fr", model="t5-small")

def translate(self, text: str) -> str:
# Run inference
model_output = self.model(text)

# Post-process output to return only the translation text
translation = model_output[0]["translation_text"]

return translation


api.export(Translator, {"name": "translator"})
16 changes: 8 additions & 8 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# BioEngine API

Under the hood, BioEngine uses [Hypha](https://ha.amun.ai/) to orchestrate the services provided in the containers. We uses the `imjoy-rpc` client to communicate to the Hypha server for model execution.
Under the hood, BioEngine uses [Hypha](https://ha.amun.ai/) to orchestrate the services provided in the containers. We uses the `hypha-rpc` client to communicate to the Hypha server for model execution.

We provide a free public server for the BioEngine available at https://ai.imjoy.io.

Expand All @@ -17,10 +17,10 @@ If you are interested in setting up your own BioEngine server, please check our

## Using BioEngine in Python

First install the `imjoy-rpc` library:
First install the `hypha-rpc` library:

```bash
pip install imjoy-rpc
pip install hypha-rpc
```

Use the following code to connect to the server and access the service. The code first connects to the server and then gets the service by its ID. The service can then be used like a normal Python object.
Expand Down Expand Up @@ -98,18 +98,18 @@ if __name__ == "__main__":

> [!NOTE]
> In Python, the recommended way to interact with the server to use asynchronous functions with `asyncio`. However, if you need to use synchronous functions,
> you can use `from imjoy_rpc.hypha.sync import login, connect_to_server` (available since `imjoy-rpc>=0.5.25.post0`) instead.
> They have the exact same arguments as the asynchronous versions. For more information, see [Synchronous Wrapper](https://github.com/imjoy-team/imjoy-rpc/blob/master/imjoy-rpc-v2.md#synchronous-wrapper)
> you can use `from imjoy_rpc.hypha.sync import login, connect_to_server` (available since `hypha-rpc>=0.5.25.post0`) instead.
> They have the exact same arguments as the asynchronous versions. For more information, see [Synchronous Wrapper](https://github.com/imjoy-team/hypha-rpc/blob/master/hypha-rpc-v2.md#synchronous-wrapper)
> <strong>💡 Tip </strong><br>
> For QT-based applications, e.g. napari, imswitch, use the synchronous api.
## Using the BioEingine in JavaScript

Include the following script in your HTML file to load the `imjoy-rpc` client:
Include the following script in your HTML file to load the `hypha-rpc` client:

```html
<script src="https://cdn.jsdelivr.net/npm/imjoy[email protected]/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hypha[email protected]/dist/hypha-rpc-websocket.min.js"></script>
```

Use the following code in JavaScript to connect to the hypha server and access the bioenigne service via the `triton-client`:
Expand All @@ -119,7 +119,7 @@ async function main(){
const server = await hyphaWebsocketClient.connectToServer({"server_url": "https://ai.imjoy.io"})
const svc = await server.getService("triton-client")
// encode the image, similar to np.random.randint(0, 255, (1, 3, 256, 256))
// see https://github.com/imjoy-team/imjoy-rpc/blob/master/imjoy-rpc-v2.md#data-type-representation
// see https://github.com/imjoy-team/hypha-rpc/blob/master/hypha-rpc-v2.md#data-type-representation
image = {_rtype: "ndarray", _rvalue: new ArrayBuffer(1*3*256*256), _rshape: [1, 3, 256, 256], _rdtype: "uint8"}
ret = await triton.execute({
inputs: [{"inputs": [image], "model_id": "conscientious-seashell"}],
Expand Down
2 changes: 1 addition & 1 deletion environments/pyimagej-py310.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ dependencies:
- openjdk=8
- pip
- pip:
- imjoy-rpc
- hypha-rpc

2 changes: 1 addition & 1 deletion notebooks/1-bioengine-engine-tutorial.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
{
"cell_type": "code",
"source": "try:\n # For pyodide in the browser\n import micropip\n await micropip.install(['imjoy-rpc', 'kaibu-utils', 'pyodide-http', 'requests'])\n \n # 2. Patch requests\n import pyodide_http\n pyodide_http.patch_all() # Patch all libraries\nexcept ImportError:\n # For native python with pip\n import subprocess\n subprocess.call(['pip', 'install', 'imjoy-rpc', 'kaibu-utils'])\n\nimport io\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport numpy as np\n#from pyotritonclient import execute\nfrom imjoy_rpc.hypha import connect_to_server\nfrom kaibu_utils import fetch_image\n\n\n",
"source": "try:\n # For pyodide in the browser\n import micropip\n await micropip.install(['hypha-rpc', 'kaibu-utils', 'pyodide-http', 'requests'])\n \n # 2. Patch requests\n import pyodide_http\n pyodide_http.patch_all() # Patch all libraries\nexcept ImportError:\n # For native python with pip\n import subprocess\n subprocess.call(['pip', 'install', 'hypha-rpc', 'kaibu-utils'])\n\nimport io\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport numpy as np\n#from pyotritonclient import execute\nfrom imjoy_rpc.hypha import connect_to_server\nfrom kaibu_utils import fetch_image\n\n\n",
"metadata": {
"trusted": true
},
Expand Down
Loading

0 comments on commit 4b81325

Please sign in to comment.