Skip to content

Commit

Permalink
Merge pull request #8 from zeroae/f/sagemaker
Browse files Browse the repository at this point in the history
Add SageMaker Inference Toolkit support
  • Loading branch information
sodre authored Nov 3, 2021
2 parents be33521 + 59be296 commit c234fb4
Show file tree
Hide file tree
Showing 18 changed files with 411 additions and 17 deletions.
3 changes: 1 addition & 2 deletions .github/condarc.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
anaconda_upload: false
channels:
- zeroae
- defaults
- conda-forge
show_channel_urls: true
show_channel_urls: true
21 changes: 12 additions & 9 deletions .github/workflows/pypa-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
python-version: [3.6]
python-version: [3.7]
black-version: [19.10b]
flake8-version: [3.7.9]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1.1.1
- uses: actions/setup-python@v2.2.2
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v1
Expand Down Expand Up @@ -75,7 +75,7 @@ jobs:
rm -f *.tar.gz
# Create the bdist wheel file
- uses: actions/setup-python@v1.1.1
- uses: actions/setup-python@v2.2.2
- uses: actions/cache@v1
id: cache
with:
Expand Down Expand Up @@ -152,7 +152,7 @@ jobs:

#
# Setup the test environment, python + .whl + .whl[test]
- uses: actions/setup-python@v1.1.1
- uses: actions/setup-python@v2.2.2
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache
Expand Down Expand Up @@ -211,13 +211,16 @@ jobs:
tar -xvf *.tar.gz --strip 1
rm *.tar.gz
- uses: goanpeca/setup-miniconda@v1
- uses: conda-incubator/setup-miniconda@v2
with:
activate-environment: ''
auto-activate-base: true
conda-build-version: 3.18
miniforge-variant: Mambaforge
use-mamba: true
conda-build-version: 3.21.4
condarc-file: .github/condarc.yml
- run: conda install setuptools_scm conda-verify
- run: |
mamba install setuptools_scm conda-verify boa
- uses: actions/cache@v1
id: conda-pkgs-cache
with:
Expand All @@ -228,7 +231,7 @@ jobs:
- name: Run conda build
run: |
mkdir conda-bld
conda build --output-folder conda-bld .
conda mambabuild --output-folder conda-bld .
env:
ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_API_TOKEN }}
- name: Create conda-bld/manifest
Expand Down Expand Up @@ -338,7 +341,7 @@ jobs:
with:
path: /usr/share/miniconda/pkgs
key: ${{ runner.os }}-conda-ac1.7.2
- uses: goanpeca/setup-miniconda@v1
- uses: conda-incubator/setup-miniconda@v2
with:
activate-environment: ''
auto-activate-base: true
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# MacOS
.DS_Store

# Vagrant
.vagrant

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ wheels: dist ## downloads wheel dependencies
ls -l wheels

dist-conda: ## builds conda-package
conda build --no-anaconda-upload --output-folder conda-bld \
conda mambabuild --no-anaconda-upload --output-folder conda-bld \
-c zeroae \
-c conda-forge \
-c anaconda .
.

install: clean ## install the package to the active Python's site-packages
python setup.py install
python setup.py install
94 changes: 94 additions & 0 deletions Vagrantfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.

# Every Vagrant development environment requires a box. You can search for
# boxes at https://vagrantcloud.com/search.
config.vm.box = "hashicorp/bionic64"

# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false

# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
# NOTE: This will enable public access to the opened port
config.vm.network "forwarded_port", guest: 8888, host: 8888

# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine and only allow access
# via 127.0.0.1 to disable public access
# config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"

# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"

# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
# config.vm.network "public_network"

# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"

# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
# config.vm.provider "virtualbox" do |vb|
# # Display the VirtualBox GUI when booting the machine
# vb.gui = true
#
# # Customize the amount of memory on the VM:
# vb.memory = "1024"
# end
#
# View the documentation for the provider you are using for more
# information on available options.
config.vm.provider "vmware_desktop" do |v|
v.vmx["memsize"] = "2048"
end

# Enable provisioning with a shell script. Additional provisioners such as
# Ansible, Chef, Docker, Puppet and Salt are also available. Please see the
# documentation for more information about their specific syntax and use.
config.vm.provision "shell", name: "Install Mambaforge", privileged: false, reset: true, inline: <<-SHELL
MAMBA_FORGE_FILE=Mambaforge-$(uname)-$(uname -m).sh
if ! [ -d ~/mambaforge ]; then
if ! [ -f $MAMBA_FORGE_FILE ]; then
wget -q https://github.com/conda-forge/miniforge/releases/latest/download/$MAMBA_FORGE_FILE
fi
bash $MAMBA_FORGE_FILE -b -u
rm -f $MAMBA_FORGE_FILE
mambaforge/bin/conda init --all
fi
SHELL

config.vm.provision "shell", name: "Install OS Packages", inline: <<-SHELL
### Add OpenJDK 8
apt-get update
apt-get --yes install openjdk-8-jre-headless
SHELL

config.vm.provision "shell", name: "Create Development Environment", privileged: false, inline: <<-SHELL
### Create the DarkNet Environment
source mambaforge/etc/profile.d/conda.sh
mamba env update --name darknet-cpu -f /vagrant/environment.yml
echo 'conda activate darknet-cpu' >> ~/.bashrc
SHELL
end
11 changes: 9 additions & 2 deletions environment.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

name: darknet.py-dev
channels:
- zeroae
Expand All @@ -9,6 +8,8 @@ dependencies:
- pip
- pip:
- -e .
- multi-model-server
- sagemaker-inference

# Setup Requirements (setup.py:setup_requirements)
- compilers
Expand All @@ -21,7 +22,7 @@ dependencies:
# Install Requirements (setup.py:requirements)
- click >=7.0
- click-plugins
- darknet
- darknet-cpu
- entrypoints
- fsspec <=0.7.5
- numpy
Expand All @@ -30,6 +31,12 @@ dependencies:
# Zoo Optional Requirements
- intake

# MMS Requirements
- enum-compat
- future
- retrying
- scipy

# Test Requirements (setup.py:test_requirements)
- pytest >=3
- pytest-cov
Expand Down
11 changes: 10 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@
# fmt: on
]

mms_requirements = [
# fmt: off
"future",
"multi-model-server",
"retrying",
"sagemaker-inference",
# fmt: on
]
zoo_requirements = [
# fmt: off
"intake",
Expand Down Expand Up @@ -130,7 +138,7 @@
long_description_content_type="text/x-rst",
include_package_data=True,
keywords="py darknet",
name="darknet-py",
name="darknet.py",
package_dir={"": "src"},
packages=find_namespace_packages(where="./src"),
setup_requires=setup_requirements,
Expand All @@ -141,6 +149,7 @@
"test": test_requirements,
"doc": doc_requirements,
"zoo": zoo_requirements,
"mms": mms_requirements,
# fmt: on
},
url="https://github.com/zeroae/darknet.py",
Expand Down
4 changes: 4 additions & 0 deletions src/darknet/sagemaker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .default_inference_handler import DefaultDarknetInferenceHandler, Network
from ..py.util import image_to_3darray

__all__ = ["DefaultDarknetInferenceHandler", "Network", "image_to_3darray"]
26 changes: 26 additions & 0 deletions src/darknet/sagemaker/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from retrying import retry
from subprocess import CalledProcessError
from sagemaker_inference import model_server

# TODO: from .classifier import handler_service as classifier_service
from .detector import handler_service as detector_service


def _retry_if_error(exception):
return isinstance(exception, CalledProcessError or OSError)


@retry(stop_max_delay=1000 * 50, retry_on_exception=_retry_if_error)
def _start_mms():
# by default the number of workers per model is 1, but we can configure it through the
# environment variable below if desired.
# os.environ['SAGEMAKER_MODEL_SERVER_WORKERS'] = '2'
# TODO: Start Classifier *or* Detector Service
model_server.start_model_server(handler_service=detector_service.__name__)


def main():
_start_mms()


main()
3 changes: 3 additions & 0 deletions src/darknet/sagemaker/classifier/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .handler_service import HandlerService

__all__ = ["HandlerService"]
25 changes: 25 additions & 0 deletions src/darknet/sagemaker/classifier/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from retrying import retry
from subprocess import CalledProcessError
from sagemaker_inference import model_server

from . import handler_service as classifier_service


def _retry_if_error(exception):
return isinstance(exception, CalledProcessError or OSError)


@retry(stop_max_delay=1000 * 50, retry_on_exception=_retry_if_error)
def _start_mms():
# by default the number of workers per model is 1, but we can configure it through the
# environment variable below if desired.
# os.environ['SAGEMAKER_MODEL_SERVER_WORKERS'] = '2'
# TODO: Start Classifier *or* Detector Service
model_server.start_model_server(handler_service=classifier_service.__name__)


def main():
_start_mms()


main()
36 changes: 36 additions & 0 deletions src/darknet/sagemaker/classifier/default_inference_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Tuple, List

from sagemaker_inference.errors import UnsupportedFormatError

from .. import DefaultDarknetInferenceHandler, image_to_3darray, Network


class DefaultDarknetClassifierInferenceHandler(DefaultDarknetInferenceHandler):
def default_predict_fn(self, data, model: Tuple[Network, List[str]]):
"""A default predict_fn for DarkNet. Calls a model on data deserialized in input_fn.
Args:
data: input data (PIL.Image) for prediction deserialized by input_fn
model: Darknet model loaded in memory by model_fn
Returns: a prediction
"""
network, labels = model
max_labels = data.get("MaxLabels", 5)
# TODO: min_confidence = data.get("MinConfidence", 55)

if "NDArray" in data:
probabilities = network.predict(data["NDArray"])
elif "Image" in data:
image, _ = image_to_3darray(data["Image"], network.shape)
probabilities = network.predict_image(image)
else:
raise UnsupportedFormatError("Expected an NDArray or an Image")

rv = [
{
"Name": label,
"Confidence": prob * 100,
}
for label, prob in sorted(zip(labels, probabilities), key=lambda x: x[1], reverse=True)
]
return {"Labels": rv[0:max_labels] if max_labels else rv}
22 changes: 22 additions & 0 deletions src/darknet/sagemaker/classifier/handler_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from sagemaker_inference.default_handler_service import DefaultHandlerService
from sagemaker_inference.transformer import Transformer

from .default_inference_handler import DefaultDarknetClassifierInferenceHandler


class HandlerService(DefaultHandlerService):
"""Handler service that is executed by the model server.
Determines specific default inference handlers to use based on the type MXNet model being used.
This class extends ``DefaultHandlerService``, which define the following:
- The ``handle`` method is invoked for all incoming inference requests to the model server.
- The ``initialize`` method is invoked at model server start up.
Based on: https://github.com/awslabs/mxnet-model-server/blob/master/docs/custom_service.md
"""

def __init__(self):
self._initialized = False

transformer = Transformer(
default_inference_handler=DefaultDarknetClassifierInferenceHandler()
)
super(HandlerService, self).__init__(transformer=transformer)
Loading

0 comments on commit c234fb4

Please sign in to comment.