diff --git a/.gitignore b/.gitignore index 5446a16..19ef1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,11 @@ dist/ *.egg *.egg-info/ +# Libs +fxn/**/*.dll +fxn/**/*.dylib +fxn/**/*.so + # IDE .vscode .vs diff --git a/Changelog.md b/Changelog.md index 59978b0..17228b5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,4 +1,5 @@ ## 0.0.31 ++ Added experimental support for making on-device predictions. + Added `PredictionResource.name` field for handling prediction resources with required file names. ## 0.0.30 diff --git a/edgefxn.py b/edgefxn.py new file mode 100644 index 0000000..9239d39 --- /dev/null +++ b/edgefxn.py @@ -0,0 +1,35 @@ +# +# Function +# Copyright © 2024 NatML Inc. All Rights Reserved. +# + +from argparse import ArgumentParser +from pathlib import Path +from requests import get + +parser = ArgumentParser() +parser.add_argument("--version", type=str, default="0.0.13") + +def _download_fxnc (name: str, version: str, path: Path): + url = f"https://cdn.fxn.ai/edgefxn/{version}/{name}" + response = get(url) + response.raise_for_status() + with open(path, "wb") as f: + f.write(response.content) + +def main (): # CHECK # Linux + args = parser.parse_args() + LIB_PATH_BASE = Path("fxn") / "libs" + _download_fxnc( + "Function-macos.dylib", + args.version, + LIB_PATH_BASE / "macos" / "Function.dylib" + ) + _download_fxnc( + "Function-win64.dll", + args.version, + LIB_PATH_BASE / "windows" / "Function.dll" + ) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/fxn/cli/predict.py b/fxn/cli/predict.py index 1c4374e..11015ae 100644 --- a/fxn/cli/predict.py +++ b/fxn/cli/predict.py @@ -12,7 +12,6 @@ from rich.progress import Progress, SpinnerColumn, TextColumn from tempfile import mkstemp from typer import Argument, Context, Option -from typing import Any, Dict from ..function import Function from .auth import get_access_key @@ -35,7 +34,7 @@ async def _predict_async (tag: str, context: Context, raw_outputs: bool): inputs = { context.args[i].replace("-", ""): _parse_value(context.args[i+1]) for i in range(0, len(context.args), 2) } # Stream fxn = Function(get_access_key()) - async for prediction in fxn.predictions.stream(tag, inputs=inputs, raw_outputs=raw_outputs, return_binary_path=True, verbose=True): + async for prediction in fxn.predictions.stream(tag, inputs=inputs, raw_outputs=raw_outputs, return_binary_path=True): # Parse results images = [value for value in prediction.results or [] if isinstance(value, Image.Image)] prediction.results = [_serialize_value(value) for value in prediction.results] if prediction.results is not None else None diff --git a/fxn/libs/__init__.py b/fxn/libs/__init__.py new file mode 100644 index 0000000..172aec6 --- /dev/null +++ b/fxn/libs/__init__.py @@ -0,0 +1,4 @@ +# +# Function +# Copyright © 2024 NatML Inc. All Rights Reserved. +# \ No newline at end of file diff --git a/fxn/libs/linux/__init__.py b/fxn/libs/linux/__init__.py new file mode 100644 index 0000000..172aec6 --- /dev/null +++ b/fxn/libs/linux/__init__.py @@ -0,0 +1,4 @@ +# +# Function +# Copyright © 2024 NatML Inc. All Rights Reserved. +# \ No newline at end of file diff --git a/fxn/libs/macos/__init__.py b/fxn/libs/macos/__init__.py new file mode 100644 index 0000000..172aec6 --- /dev/null +++ b/fxn/libs/macos/__init__.py @@ -0,0 +1,4 @@ +# +# Function +# Copyright © 2024 NatML Inc. All Rights Reserved. +# \ No newline at end of file diff --git a/fxn/libs/windows/__init__.py b/fxn/libs/windows/__init__.py new file mode 100644 index 0000000..172aec6 --- /dev/null +++ b/fxn/libs/windows/__init__.py @@ -0,0 +1,4 @@ +# +# Function +# Copyright © 2024 NatML Inc. All Rights Reserved. +# \ No newline at end of file diff --git a/fxn/services/prediction/fxnc.py b/fxn/services/prediction/fxnc.py index d95834f..0d83878 100644 --- a/fxn/services/prediction/fxnc.py +++ b/fxn/services/prediction/fxnc.py @@ -54,13 +54,13 @@ class FXNAcceleration(c_int): class FXNValue(Structure): pass class FXNValueMap(Structure): pass class FXNConfiguration(Structure): pass -class FXNProfile(Structure): pass +class FXNPrediction(Structure): pass class FXNPredictor(Structure): pass FXNValueRef = POINTER(FXNValue) FXNValueMapRef = POINTER(FXNValueMap) FXNConfigurationRef = POINTER(FXNConfiguration) -FXNProfileRef = POINTER(FXNProfile) +FXNPredictionRef = POINTER(FXNPrediction) FXNPredictorRef = POINTER(FXNPredictor) def load_fxnc (path: Path) -> CDLL: @@ -94,7 +94,7 @@ def load_fxnc (path: Path) -> CDLL: fxnc.FXNValueCreateDict.argtypes = [c_char_p, POINTER(FXNValueRef)] fxnc.FXNValueCreateDict.restype = FXNStatus # FXNValueCreateImage - fxnc.FXNValueCreateImage.argtypes = [POINTER(c_uint8), c_int32, c_int32, c_int32, FXNValueFlags, POINTER(FXNValueRef)] + fxnc.FXNValueCreateImage.argtypes = [c_void_p, c_int32, c_int32, c_int32, FXNValueFlags, POINTER(FXNValueRef)] fxnc.FXNValueCreateImage.restype = FXNStatus # FXNValueMapCreate fxnc.FXNValueMapCreate.argtypes = [POINTER(FXNValueMapRef)] @@ -123,15 +123,18 @@ def load_fxnc (path: Path) -> CDLL: # FXNConfigurationRelease fxnc.FXNConfigurationRelease.argtypes = [FXNConfigurationRef] fxnc.FXNConfigurationRelease.restype = FXNStatus + # FXNConfigurationGetTag + fxnc.FXNConfigurationGetTag.argtypes = [FXNConfigurationRef, c_char_p, c_int32] + fxnc.FXNConfigurationRelease.restype = FXNStatus + # FXNConfigurationSetTag + fxnc.FXNConfigurationSetTag.argtypes = [FXNConfigurationRef, c_char_p] + fxnc.FXNConfigurationSetTag.restype = FXNStatus # FXNConfigurationGetToken fxnc.FXNConfigurationGetToken.argtypes = [FXNConfigurationRef, c_char_p, c_int32] fxnc.FXNConfigurationGetToken.restype = FXNStatus # FXNConfigurationSetToken fxnc.FXNConfigurationSetToken.argtypes = [FXNConfigurationRef, c_char_p] fxnc.FXNConfigurationSetToken.restype = FXNStatus - # FXNConfigurationAddResource - fxnc.FXNConfigurationAddResource.argtypes = [FXNConfigurationRef, c_char_p, c_char_p] - fxnc.FXNConfigurationAddResource.restype = FXNStatus # FXNConfigurationGetAcceleration fxnc.FXNConfigurationGetAcceleration.argtypes = [FXNConfigurationRef, POINTER(FXNAcceleration)] fxnc.FXNConfigurationGetAcceleration.restype = FXNStatus @@ -144,32 +147,38 @@ def load_fxnc (path: Path) -> CDLL: # FXNConfigurationSetDevice fxnc.FXNConfigurationSetDevice.argtypes = [FXNConfigurationRef, c_void_p] fxnc.FXNConfigurationSetDevice.restype = FXNStatus - # FXNProfileRelease - fxnc.FXNProfileRelease.argtypes = [FXNProfileRef] - fxnc.FXNProfileRelease.restype = FXNStatus - # FXNProfileGetID - fxnc.FXNProfileGetID.argtypes = [FXNProfileRef, c_char_p, c_int32] - fxnc.FXNProfileGetID.restype = FXNStatus - # FXNProfileGetLatency - fxnc.FXNProfileGetLatency.argtypes = [FXNProfileRef, POINTER(c_double)] - fxnc.FXNProfileGetLatency.restype = FXNStatus - # FXNProfileGetError - fxnc.FXNProfileGetError.argtypes = [FXNProfileRef, c_char_p, c_int32] - fxnc.FXNProfileGetError.restype = FXNStatus - # FXNProfileGetLogs - fxnc.FXNProfileGetLogs.argtypes = [FXNProfileRef, c_char_p, c_int32] - fxnc.FXNProfileGetLogs.restype = FXNStatus - # FXNProfileGetLogLength - fxnc.FXNProfileGetLogLength.argtypes = [FXNProfileRef, POINTER(c_int32)] - fxnc.FXNProfileGetLogLength.restype = FXNStatus + # FXNConfigurationAddResource + fxnc.FXNConfigurationAddResource.argtypes = [FXNConfigurationRef, c_char_p, c_char_p] + fxnc.FXNConfigurationAddResource.restype = FXNStatus + # FXNPredictionRelease + fxnc.FXNPredictionRelease.argtypes = [FXNPredictionRef] + fxnc.FXNPredictionRelease.restype = FXNStatus + # FXNPredictionGetID + fxnc.FXNPredictionGetID.argtypes = [FXNPredictionRef, c_char_p, c_int32] + fxnc.FXNPredictionGetID.restype = FXNStatus + # FXNPredictionGetLatency + fxnc.FXNPredictionGetLatency.argtypes = [FXNPredictionRef, POINTER(c_double)] + fxnc.FXNPredictionGetLatency.restype = FXNStatus + # FXNPredictionGetResults + fxnc.FXNPredictionGetResults.argtypes = [FXNPredictionRef, POINTER(FXNValueMapRef)] + fxnc.FXNPredictionGetResults.restype = FXNStatus + # FXNPredictionGetError + fxnc.FXNPredictionGetError.argtypes = [FXNPredictionRef, c_char_p, c_int32] + fxnc.FXNPredictionGetError.restype = FXNStatus + # FXNPredictionGetLogs + fxnc.FXNPredictionGetLogs.argtypes = [FXNPredictionRef, c_char_p, c_int32] + fxnc.FXNPredictionGetLogs.restype = FXNStatus + # FXNPredictionGetLogLength + fxnc.FXNPredictionGetLogLength.argtypes = [FXNPredictionRef, POINTER(c_int32)] + fxnc.FXNPredictionGetLogLength.restype = FXNStatus # FXNPredictorCreate - fxnc.FXNPredictorCreate.argtypes = [c_char_p, FXNConfigurationRef, POINTER(FXNPredictorRef)] + fxnc.FXNPredictorCreate.argtypes = [FXNConfigurationRef, POINTER(FXNPredictorRef)] fxnc.FXNPredictorCreate.restype = FXNStatus # FXNPredictorRelease fxnc.FXNPredictorRelease.argtypes = [FXNPredictorRef] fxnc.FXNPredictorRelease.restype = FXNStatus # FXNPredictorPredict - fxnc.FXNPredictorPredict.argtypes = [FXNPredictorRef, FXNValueMapRef, POINTER(FXNProfileRef), POINTER(FXNValueMapRef)] + fxnc.FXNPredictorPredict.argtypes = [FXNPredictorRef, FXNValueMapRef, POINTER(FXNPredictionRef)] fxnc.FXNPredictorPredict.restype = FXNStatus # FXNGetVersion fxnc.FXNGetVersion.argtypes = [] @@ -177,7 +186,7 @@ def load_fxnc (path: Path) -> CDLL: # Return return fxnc -def to_fxn_value ( # DEPLOY +def to_fxn_value ( fxnc: CDLL, value: Union[float, int, bool, str, NDArray, List[Any], Dict[str, Any], Image.Image, bytes, bytearray, memoryview, BytesIO, None], *, @@ -208,10 +217,10 @@ def to_fxn_value ( # DEPLOY elif isinstance(value, list): fxnc.FXNValueCreateList(dumps(value).encode(), byref(result)) elif isinstance(value, dict): - fxnc.FXNValueCreateList(dumps(value).encode(), byref(result)) + fxnc.FXNValueCreateDict(dumps(value).encode(), byref(result)) elif isinstance(value, Image.Image): value = array(value) - fxnc.FXNValueCreateImage( + status = fxnc.FXNValueCreateImage( value.ctypes.data_as(c_void_p), value.shape[1], value.shape[0], @@ -219,6 +228,7 @@ def to_fxn_value ( # DEPLOY FXNValueFlags.COPY_DATA, byref(result) ) + assert status.value == FXNStatus.OK, f"Failed to create image value with status: {status.value}" elif isinstance(value, (bytes, bytearray, memoryview, BytesIO)): view = memoryview(value.getvalue() if isinstance(value, BytesIO) else value) if not isinstance(value, memoryview) else value buffer = (c_uint8 * len(view)).from_buffer(view) @@ -232,22 +242,26 @@ def to_fxn_value ( # DEPLOY raise RuntimeError(f"Failed to convert Python value to Function value because Python value has an unsupported type: {type(value)}") return result -def to_py_value ( # DEPLOY +def to_py_value ( fxnc: CDLL, value: type[FXNValueRef] ) -> Union[float, int, bool, str, NDArray, List[Any], Dict[str, Any], Image.Image, BytesIO, None]: # Type dtype = FXNDtype() - fxnc.FXNValueGetType(value, byref(dtype)) + status = fxnc.FXNValueGetType(value, byref(dtype)) + assert status.value == FXNStatus.OK, f"Failed to get value data type with status: {status.value}" dtype = dtype.value # Get data data = c_void_p() - fxnc.FXNValueGetData(value, byref(data)) + status = fxnc.FXNValueGetData(value, byref(data)) + assert status.value == FXNStatus.OK, f"Failed to get value data with status: {status.value}" # Get shape dims = c_int32() - fxnc.FXNValueGetDimensions(value, byref(dims)) + status = fxnc.FXNValueGetDimensions(value, byref(dims)) + assert status.value == FXNStatus.OK, f"Failed to get value dimensions with status: {status.value}" shape = zeros(dims.value, dtype=int32) - fxnc.FXNValueGetShape(value, shape.ctypes.data_as(POINTER(c_int32)), dims) + status = fxnc.FXNValueGetShape(value, shape.ctypes.data_as(POINTER(c_int32)), dims) + assert status.value == FXNStatus.OK, f"Failed to get value shape with status: {status.value}" # Switch if dtype == FXNDtype.NULL: return None diff --git a/fxn/services/prediction/service.py b/fxn/services/prediction/service.py index e6c404c..94f1782 100644 --- a/fxn/services/prediction/service.py +++ b/fxn/services/prediction/service.py @@ -4,9 +4,10 @@ # from aiohttp import ClientSession -from ctypes import byref, c_double, c_int32, create_string_buffer +from ctypes import byref, c_double, c_int32, create_string_buffer, CDLL from dataclasses import asdict, is_dataclass from datetime import datetime, timezone +from importlib import resources from io import BytesIO from json import dumps, loads from magika import Magika @@ -18,7 +19,7 @@ from pydantic import BaseModel from requests import get, post from tempfile import NamedTemporaryFile -from typing import Any, AsyncIterator, Dict, List, Union +from typing import Any, AsyncIterator, Dict, List, Optional, Union from uuid import uuid4 from urllib.parse import urlparse from urllib.request import urlopen @@ -26,14 +27,14 @@ from ...graph import GraphClient from ...types import Dtype, PredictorType, Prediction, PredictionResource, Value, UploadType from ..storage import StorageService -from .fxnc import load_fxnc, to_fxn_value, to_py_value, FXNConfigurationRef, FXNPredictorRef, FXNProfileRef, FXNStatus, FXNValueRef, FXNValueMapRef +from .fxnc import load_fxnc, to_fxn_value, to_py_value, FXNConfigurationRef, FXNPredictorRef, FXNPredictionRef, FXNStatus, FXNValueRef, FXNValueMapRef class PredictionService: - def __init__ (self, client: GraphClient, storage: StorageService) -> None: + def __init__ (self, client: GraphClient, storage: StorageService): self.client = client self.storage = storage - self.__fxnc = None + self.__fxnc = PredictionService.__load_fxnc() self.__cache = { } def create ( @@ -43,8 +44,7 @@ def create ( inputs: Dict[str, Union[ndarray, str, float, int, bool, List, Dict[str, Any], Path, Image.Image, Value]] = None, raw_outputs: bool=False, return_binary_path: bool=True, - data_url_limit: int=None, - verbose: bool=False + data_url_limit: int=None ) -> Prediction: """ Create a prediction. @@ -55,7 +55,6 @@ def create ( raw_outputs (bool): Skip converting output values into Pythonic types. This only applies to `CLOUD` predictions. return_binary_path (bool): Write binary values to file and return a `Path` instead of returning `BytesIO` instance. data_url_limit (int): Return a data URL if a given output value is smaller than this size in bytes. This only applies to `CLOUD` predictions. - verbose (bool): Use verbose logging. Returns: Prediction: Created prediction. @@ -73,7 +72,7 @@ def create ( headers={ "Authorization": f"Bearer {self.client.access_key}", "fxn-client": self.__get_client_id(), - "fxn-configuration-token": "" + "fxn-configuration-token": self.__get_configuration_token() } ) # Check @@ -87,13 +86,13 @@ def create ( prediction = self.__parse_prediction(prediction, raw_outputs=raw_outputs, return_binary_path=return_binary_path) # Create edge prediction if prediction.type == PredictorType.Edge: - predictor = self.__load(prediction, verbose=verbose) + predictor = self.__load(prediction) self.__cache[tag] = predictor prediction = self.__predict(tag=tag, predictor=predictor, inputs=inputs) if inputs is not None else prediction # Return return prediction - async def stream ( # INCOMPLETE # Add edge prediction support + async def stream ( self, tag: str, *, @@ -101,7 +100,6 @@ async def stream ( # INCOMPLETE # Add edge prediction support raw_outputs: bool=False, return_binary_path: bool=True, data_url_limit: int=None, - verbose: bool=False ) -> AsyncIterator[Prediction]: """ Create a streaming prediction. @@ -114,24 +112,27 @@ async def stream ( # INCOMPLETE # Add edge prediction support raw_outputs (bool): Skip converting output values into Pythonic types. This only applies to `CLOUD` predictions. return_binary_path (bool): Write binary values to file and return a `Path` instead of returning `BytesIO` instance. data_url_limit (int): Return a data URL if a given output value is smaller than this size in bytes. This only applies to `CLOUD` predictions. - verbose (bool): Use verbose logging. Returns: Prediction: Created prediction. """ + # Check if cached + if tag in self.__cache: + yield self.__predict(tag=tag, predictor=self.__cache[tag], inputs=inputs) + return # Serialize inputs key = uuid4().hex - inputs = { name: self.to_value(value, name, key=key).model_dump(mode="json") for name, value in inputs.items() } + values = { name: self.to_value(value, name, key=key).model_dump(mode="json") for name, value in inputs.items() } # INCOMPLETE # values # Request url = f"{self.client.api_url}/predict/{tag}?stream=true&rawOutputs=true&dataUrlLimit={data_url_limit}" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {self.client.access_key}", "fxn-client": self.__get_client_id(), - "fxn-configuration-token": "" + "fxn-configuration-token": self.__get_configuration_token() } async with ClientSession(headers=headers) as session: - async with session.post(url, data=dumps(inputs)) as response: + async with session.post(url, data=dumps(values)) as response: async for chunk in response.content.iter_any(): prediction = loads(chunk) # Check status @@ -144,7 +145,7 @@ async def stream ( # INCOMPLETE # Add edge prediction support prediction = self.__parse_prediction(prediction, raw_outputs=raw_outputs, return_binary_path=return_binary_path) # Create edge prediction if prediction.type == PredictorType.Edge: - predictor = self.__load(prediction, verbose=verbose) + predictor = self.__load(prediction) self.__cache[tag] = predictor prediction = self.__predict(tag=tag, predictor=predictor, inputs=inputs) if inputs is not None else prediction # Yield @@ -277,80 +278,124 @@ def to_value ( return Value(data=data, type=dtype) # Unsupported raise RuntimeError(f"Cannot create Function value '{name}' for object {object} of type {type(object)}") + + @classmethod + def __load_fxnc (self) -> Optional[CDLL]: + RESOURCE_MAP = { + "Darwin": ("fxn.libs.macos", "Function.dylib"), + "Windows": ("fxn.libs.windows", "Function.dll"), + } + # Get resource + package, resource = RESOURCE_MAP.get(system(), (None, None)) + if package is None or resource is None: + return None + # Load + with resources.path(package, resource) as fxnc_path: + return load_fxnc(fxnc_path) + + def __get_client_id (self) -> str: + id = system() + if id == "Darwin": + return f"macos:{machine()}" + if id == "Linux": + return f"linux:{machine()}" + if id == "Windows": + return f"windows:{machine()}" + raise RuntimeError(f"Function cannot make predictions on the {id} platform") + + def __get_configuration_token (self) -> Optional[str]: + # Check + if not self.__fxnc: + return None + # Get + buffer = create_string_buffer(2048) + status = self.__fxnc.FXNConfigurationGetUniqueID(buffer, len(buffer)) + assert status.value == FXNStatus.OK, f"Failed to create prediction configuration token with status: {status.value}" + uid = buffer.value.decode("utf-8") + # Return + return uid - def __load (self, prediction: Prediction, *, verbose: bool=False): - # Load fxnc - if self.__fxnc is None: - fxnc_resource = next(x for x in prediction.resources if x.type == "fxn") - fxnc_path = self.__get_resource_path(fxnc_resource) - self.__fxnc = load_fxnc(fxnc_path) + def __load (self, prediction: Prediction): # Load predictor fxnc = self.__fxnc configuration = FXNConfigurationRef() try: # Create configuration status = fxnc.FXNConfigurationCreate(byref(configuration)) - assert status.value == FXNStatus.OK, f"Failed to create prediction configuration for tag {prediction.tag} with status: {status.value}" - # Populate + assert status.value == FXNStatus.OK, f"Failed to create {prediction.tag} prediction configuration with status: {status.value}" + # Set tag + status = fxnc.FXNConfigurationSetTag(configuration, prediction.tag.encode()) + assert status.value == FXNStatus.OK, f"Failed to set {prediction.tag} prediction configuration tag with status: {status.value}" + # Set token status = fxnc.FXNConfigurationSetToken(configuration, prediction.configuration.encode()) - assert status.value == FXNStatus.OK, f"Failed to set prediction configuration token for tag {prediction.tag} with status: {status.value}" + assert status.value == FXNStatus.OK, f"Failed to set {prediction.tag} prediction configuration token with status: {status.value}" # Add resources for resource in prediction.resources: if resource.type == "fxn": continue - path = self.__get_resource_path(resource, verbose=verbose) + path = self.__get_resource_path(resource) status = fxnc.FXNConfigurationAddResource(configuration, resource.type.encode(), str(path).encode()) assert status.value == FXNStatus.OK, f"Failed to set prediction configuration resource with type {resource.type} for tag {prediction.tag} with status: {status.value}" # Create predictor predictor = FXNPredictorRef() - status = fxnc.FXNPredictorCreate(prediction.tag.encode(), configuration, byref(predictor)) + status = fxnc.FXNPredictorCreate(configuration, byref(predictor)) assert status.value == FXNStatus.OK, f"Failed to create prediction for tag {prediction.tag} with status: {status.value}" # Return return predictor finally: fxnc.FXNConfigurationRelease(configuration) - def __predict (self, *, tag: str, predictor, inputs: Dict[str, Any]) -> Prediction: # DEPLOY + def __predict (self, *, tag: str, predictor, inputs: Dict[str, Any]) -> Prediction: fxnc = self.__fxnc - predictor = self.__cache[tag] - profile = FXNProfileRef() input_map = FXNValueMapRef() - output_map = FXNValueMapRef() + prediction = FXNPredictionRef() try: # Create input map status = fxnc.FXNValueMapCreate(byref(input_map)) - assert status.value == FXNStatus.OK, f"Failed to create prediction for tag {tag} because input values could not be provided to the predictor with status: {status.value}" + assert status.value == FXNStatus.OK, f"Failed to create {tag} prediction because input values could not be provided to the predictor with status: {status.value}" # Marshal inputs for name, value in inputs.items(): - fxnc.FXNValueMapSetValue(input_map, name.encode(), to_fxn_value(fxnc, value, copy=False)) + value = to_fxn_value(fxnc, value, copy=False) + fxnc.FXNValueMapSetValue(input_map, name.encode(), value) # Predict - status = fxnc.FXNPredictorPredict(predictor, input_map, byref(profile), byref(output_map)) - assert status.value == FXNStatus.OK, f"Failed to create prediction for tag {tag} with status: {status.value}" - # Marshal profile + status = fxnc.FXNPredictorPredict(predictor, input_map, byref(prediction)) + assert status.value == FXNStatus.OK, f"Failed to create {tag} prediction with status: {status.value}" + # Marshal prediction id = create_string_buffer(256) error = create_string_buffer(2048) latency = c_double() - log_length = c_int32() - fxnc.FXNProfileGetID(profile, id, len(id)) - fxnc.FXNProfileGetLatency(profile, byref(latency)) - fxnc.FXNProfileGetError(profile, error, len(error)) - fxnc.FXNProfileGetLogLength(profile, byref(log_length)) + status = fxnc.FXNPredictionGetID(prediction, id, len(id)) + assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction identifier with status: {status.value}" + status = fxnc.FXNPredictionGetLatency(prediction, byref(latency)) + assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction latency with status: {status.value}" + fxnc.FXNPredictionGetError(prediction, error, len(error)) id = id.value.decode("utf-8") latency = latency.value error = error.value.decode("utf-8") # Marshal logs + log_length = c_int32() + fxnc.FXNPredictionGetLogLength(prediction, byref(log_length)) logs = create_string_buffer(log_length.value + 1) - fxnc.FXNProfileGetLogs(profile, logs, len(logs)) + fxnc.FXNPredictionGetLogs(prediction, logs, len(logs)) logs = logs.value.decode("utf-8") # Marshal outputs results = [] output_count = c_int32() - fxnc.FXNValueMapGetSize(output_map, byref(output_count)) + output_map = FXNValueMapRef() + status = fxnc.FXNPredictionGetResults(prediction, byref(output_map)) + assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction results with status: {status.value}" + status = fxnc.FXNValueMapGetSize(output_map, byref(output_count)) + assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction result count with status: {status.value}" for idx in range(output_count.value): + # Get name name = create_string_buffer(256) + status = fxnc.FXNValueMapGetKey(output_map, idx, name, len(name)) + assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction output name at index {idx} with status: {status.value}" + # Get value value = FXNValueRef() - fxnc.FXNValueMapGetKey(output_map, idx, name, len(name)) - fxnc.FXNValueMapGetValue(output_map, name, byref(value)) + status = fxnc.FXNValueMapGetValue(output_map, name, byref(value)) + assert status.value == FXNStatus.OK, f"Failed to get {tag} prediction output value at index {idx} with status: {status.value}" + # Parse name = name.value.decode("utf-8") value = to_py_value(fxnc, value) results.append(value) @@ -361,14 +406,13 @@ def __predict (self, *, tag: str, predictor, inputs: Dict[str, Any]) -> Predicti type=PredictorType.Edge, results=results if not error else None, latency=latency, - error=error, + error=error if error else None, logs=logs, created=datetime.now(timezone.utc).isoformat() ) finally: - fxnc.FXNProfileRelease(profile) + fxnc.FXNPredictionRelease(prediction) fxnc.FXNValueMapRelease(input_map) - fxnc.FXNValueMapRelease(output_map) def __parse_prediction ( self, @@ -404,8 +448,8 @@ def __download_value_data (self, url: str) -> BytesIO: response = get(url) result = BytesIO(response.content) return result - - def __get_resource_path (self, resource: PredictionResource, *, verbose: bool=False) -> Path: # INCOMPLETE # Verbose + + def __get_resource_path (self, resource: PredictionResource) -> Path: cache_dir = Path.home() / ".fxn" / "cache" cache_dir.mkdir(exist_ok=True) res_name = Path(urlparse(resource.url).path).name @@ -432,17 +476,6 @@ def __try_ensure_serializable (cls, object: Any) -> Any: return object.model_dump(mode="json", by_alias=True) return object - @classmethod - def __get_client_id (self) -> str: - id = system() - if id == "Darwin": - return f"macos:{machine()}" - if id == "Linux": - return f"linux:{machine()}" - if id == "Windows": - return f"windows:{machine()}" - raise RuntimeError(f"Function cannot make predictions on the {id} platform") - PREDICTION_FIELDS = f""" id @@ -450,9 +483,9 @@ def __get_client_id (self) -> str: type configuration resources {{ - id type url + name }} results {{ data diff --git a/setup.py b/setup.py index b07ea7b..967eb30 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,12 @@ include=["fxn", "fxn.*"], exclude=["test", "examples"] ), + include_package_data=True, + package_data={ + "fxn.libs.macos": ["*.dylib"], + "fxn.libs.windows": ["*.dll"], + "fxn.libs.linux": ["*.so"] + }, entry_points={ "console_scripts": [ "fxn=fxn.cli.__init__:app" diff --git a/test/edge_test.py b/test/edge_test.py index c66a81b..9fb0f90 100644 --- a/test/edge_test.py +++ b/test/edge_test.py @@ -3,12 +3,10 @@ # Copyright © 2024 NatML Inc. All Rights Reserved. # -from ctypes import create_string_buffer from fxn import Function from fxn.services.prediction.fxnc import load_fxnc, FXNStatus from pathlib import Path -from requests import get -from tempfile import mkstemp +from PIL import Image def test_load_fxnc (): fxnc_path = Path("../edgefxn/build/macOS/Release/Release/Function.dylib") @@ -16,21 +14,15 @@ def test_load_fxnc (): version = fxnc.FXNGetVersion().decode("utf-8") assert version is not None -def test_get_fxnc_configuration_uid (): +def test_edge_math_prediction (): fxn = Function() - prediction = fxn.predictions.create(tag="@fxn/math") - fxnc = next(x for x in prediction.resources if x.type == "fxn") - _, fxnc_path = mkstemp() - with open(fxnc_path, "wb") as f: - f.write(get(fxnc.url).content) - fxnc = load_fxnc(fxnc_path) - uid_buffer = create_string_buffer(2048) - status = fxnc.FXNConfigurationGetUniqueID(uid_buffer, len(uid_buffer)) - uid = uid_buffer.value.decode("utf-8") - assert status.value == FXNStatus.OK - assert uid + prediction = fxn.predictions.create(tag="@fxn/math", inputs={ "radius": 4 }) + assert prediction is not None -def test_edge_prediction (): +def test_edge_ml_prediction (): + image = Image.open("test/media/pexels-samson-katt-5255233.jpg") fxn = Function() - prediction = fxn.predictions.create(tag="@fxn/math", inputs={ "radius": 4. }) - assert prediction is not None \ No newline at end of file + prediction = fxn.predictions.create(tag="@natml/meet", inputs={ "image": image }) + assert prediction is not None + mask = prediction.results[0] + Image.fromarray((mask.squeeze() * 255).astype("uint8")).save("mask.jpg") \ No newline at end of file diff --git a/test/media/pexels-samson-katt-5255233.jpg b/test/media/pexels-samson-katt-5255233.jpg new file mode 100644 index 0000000..08ceedb Binary files /dev/null and b/test/media/pexels-samson-katt-5255233.jpg differ