Repository: Xilinx/inference-server (language: python)

Issue:
Update examples
The examples need cleaning up and standardization. They should be well-documented in code and with accompanying written documentation that highlights the point of the example. This document should also be visible in the web documentation.


Discussion / hints:

Fix patch:
diff --git a/.clang-tidy b/.clang-tidy
index 8a37ed867..a67bbfe7c 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -181,8 +181,6 @@ CheckOptions:
     value: camelBack
   - key:   readability-identifier-naming.PublicMemberCase
     value: lower_case
-  - key:   readability-identifier-naming.PublicMemberSuffix
-    value: _
   - key:   readability-identifier-naming.PublicMethodCase
     value: camelBack
   - key:   readability-identifier-naming.ScopedEnumConstantCase
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9806d6594..e76eda0bc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -238,7 +238,7 @@ endif()
 add_subdirectory(external)
 add_subdirectory(src/proteus)
 if(${PROTEUS_BUILD_EXAMPLES})
-  add_subdirectory(examples/cpp)
+  add_subdirectory(examples)
 endif()
 if(${PROTEUS_BUILD_TESTING})
   include(GTest)
diff --git a/cmake/AddExample.cmake b/cmake/AddExample.cmake
new file mode 100644
index 000000000..542805b8c
--- /dev/null
+++ b/cmake/AddExample.cmake
@@ -0,0 +1,46 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+function(proteus_get_example_target target example)
+  string(REGEX REPLACE "^${PROTEUS_EXAMPLE_ROOT}" "" BASE_PATH
+                       ${CMAKE_CURRENT_SOURCE_DIR}
+  )
+  string(REGEX REPLACE "\/" "_" BASE_NAME ${BASE_PATH})
+
+  # check for an extra argument to set the prefix. use 'example' by default
+  list(LENGTH ARGN extra_count)
+  if(extra_count GREATER 0)
+    list(GET ARGN 0 prefix)
+  elseif(extra_count GREATER 1)
+    message(FATAL_ERROR "Cannot pass more than three args to this function")
+  else()
+    set(prefix "example")
+  endif()
+
+  set(${target} ${prefix}${BASE_NAME}-${example} PARENT_SCOPE)
+endfunction()
+
+function(proteus_add_example example)
+  proteus_get_example_target(target ${example})
+
+  add_executable(${target} ${example}.cpp)
+  target_include_directories(${target} PRIVATE ${PROTEUS_INCLUDE_DIRS})
+  target_link_libraries(${target} PRIVATE proteus util)
+endfunction()
+
+function(proteus_add_examples examples)
+  foreach(example ${examples})
+    proteus_add_example(${example})
+  endforeach()
+endfunction()
diff --git a/cmake/AddTest.cmake b/cmake/AddTest.cmake
index 335f45275..5f7609567 100644
--- a/cmake/AddTest.cmake
+++ b/cmake/AddTest.cmake
@@ -23,7 +23,7 @@ function(proteus_get_test_target target test)
   if(extra_count GREATER 0)
     list(GET ARGN 0 prefix)
   elseif(extra_count GREATER 1)
-    message(FATAL_ERROR "Cannot pass more than two arguments to this function")
+    message(FATAL_ERROR "Cannot pass more than three args to this function")
   else()
     set(prefix "test")
   endif()
diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in
index 3ca1303c9..a397a6752 100644
--- a/docs/Doxyfile.in
+++ b/docs/Doxyfile.in
@@ -982,7 +982,7 @@ EXCLUDE_SYMLINKS       = NO
 # Note that the wildcards are matched against the file with absolute path, so to
 # exclude all test directories for example use the pattern */test/*
 
-EXCLUDE_PATTERNS       =
+EXCLUDE_PATTERNS       = */build/* */uploads/*
 
 # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
 # (namespaces, classes, functions, etc.) that should be excluded from the
diff --git a/docs/conf.py b/docs/conf.py
index 06b1b9450..9dbda76c0 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,7 +23,7 @@
 import os
 import sys
 
-sys.path.insert(0, os.path.abspath("../src/python"))
+sys.path.insert(0, os.path.abspath(".."))
 
 # to parse the CLI, we need to import it by file path since it's not in a Python package
 imp.load_source("proteus_cli", os.path.abspath("../proteus"))
@@ -115,7 +115,7 @@
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**/build/**", "**/uploads/**"]
 
 
 # -- Options for HTML output -------------------------------------------------
diff --git a/docs/example_resnet50_cpp.rst b/docs/example_resnet50_cpp.rst
new file mode 100644
index 000000000..54b4e46bb
--- /dev/null
+++ b/docs/example_resnet50_cpp.rst
@@ -0,0 +1,209 @@
+..
+    Copyright 2022 Advanced Micro Devices, Inc.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+Running ResNet50 - C++
+======================
+
+This page walks you through the C++ versions of the ResNet50 examples.
+You can see the full source files used here in the repository for more details.
+
+While the inference server supports many backends, client applications to make requests to any of them largely look the same.
+
+.. note::
+    These examples are intended to demonstrate the API and how to communicate with the server.
+    They are not intended to show the most optimal performance for each backend.
+
+Include the header
+------------------
+
+AMD Inference Server's C++ API allows you to write your own C++ client applications that can communicate with the inference server.
+You can include the entire public API by including :file:`proteus/proteus.hpp` or selectively include header files as needed.
+
+.. literalinclude:: ../examples/resnet50/vitis.cpp
+    :start-after: +include:
+    :end-before: -include:
+    :language: cpp
+
+Start the server
+----------------
+
+This example assumes that the server is not running elsewhere and starts it locally from C++ instead.
+Creating the ``proteus::Server`` object starts the inference server backend and it stays alive as long as the object remains in scope.
+
+.. literalinclude:: ../examples/resnet50/vitis.cpp
+    :start-after: +initialize:
+    :end-before: -initialize:
+    :language: cpp
+    :dedent: 2
+
+Depending on what protocol you want to use to communicate with the server, you may need to start it explicitly using one of the ``proteus::Server`` object's methods.
+
+For example, if you were using HTTP/REST:
+
+.. literalinclude:: ../examples/resnet50/vitis.cpp
+    :start-after: +start protocol:
+    :end-before: -start protocol:
+    :language: cpp
+    :dedent: 2
+
+With gRPC:
+
+.. literalinclude:: ../examples/resnet50/tfzendnn.cpp
+    :start-after: +start protocol
+    :end-before: -start protocol
+    :language: cpp
+    :dedent: 2
+
+The native C++ API does not need starting and just creating the Server object is sufficient.
+
+If the server is already running somewhere, you don't need to do this.
+
+Create the client object
+------------------------
+
+The ``proteus::Client`` base class defines how to communicate with the server over the supported protocols.
+This client protocol is based on KServe's API.
+Each protocol inherits from this class and implements its methods.
+Some examples of clients that you can create:
+
+.. literalinclude:: ../examples/resnet50/vitis.cpp
+    :start-after: +create client:
+    :end-before: -create client:
+    :language: cpp
+    :dedent: 2
+
+.. literalinclude:: ../examples/resnet50/tfzendnn.cpp
+    :start-after: +create client
+    :end-before: -create client
+    :language: cpp
+    :dedent: 2
+
+.. literalinclude:: ../examples/resnet50/ptzendnn.cpp
+    :start-after: +create client
+    :end-before: -create client
+    :language: cpp
+    :dedent: 2
+
+Load a worker
+-------------
+
+Once the server is ready, you have to load a worker to handle your request.
+This worker, and any load-time parameters it accepts, are backend-specific.
+After loading the worker, you get back an endpoint string that you use to make requests to this worker.
+If a worker is already ready on the server or you already have an endpoint, then you don't need to do this.
+
+Here are some of the different workers you can start to perform inference on a ResNet50 model.
+
+XModel - Vitis AI on AMD FPGA:
+
+.. literalinclude:: ../examples/resnet50/vitis.cpp
+    :start-after: +load:
+    :end-before: -load:
+    :language: cpp
+    :dedent: 2
+
+TF+ZenDNN - ZenDNN on AMD CPU:
+
+.. literalinclude:: ../examples/resnet50/tfzendnn.cpp
+    :start-after: +load
+    :end-before: -load
+    :language: cpp
+    :dedent: 2
+
+MIGraphX - MIGraphX on AMD GPU:
+
+.. literalinclude:: ../examples/resnet50/migraphx.cpp
+    :start-after: +load
+    :end-before: -load
+    :language: cpp
+    :dedent: 2
+
+After loading a worker, make sure it's ready before attempting to make an inference.
+
+.. literalinclude:: ../examples/resnet50/vitis.cpp
+    :start-after: +wait model ready:
+    :end-before: -wait model ready:
+    :language: cpp
+    :dedent: 2
+
+Prepare images
+--------------
+
+Depending on the model, you may need to perform some preprocessing of the data before making an inference request.
+For ResNet50, this preprocessing generally consists of resizing the image, normalizing its values, and possibly converting types but its exact implementation depends on the model and on what the worker expects.
+The implementations of the preprocessing functions can be seen in the examples' sources and they differ for each backend.
+
+In these examples, you can pass a path to an image or to a directory to the executable.
+This single path gets converted to a vector of paths containing just the path you passed in or paths to all the files in the directory you passed in and its passed to the ``preprocess`` function.
+The file at each path is opened and stored in an ``std::vector<T>``, where ``T`` depends on the data type that a backend works with.
+Since there may be many images, ``preprocess`` returns an ``std::vector<std::vector<T>>``.
+
+.. literalinclude:: ../examples/resnet50/vitis.cpp
+    :start-after: +prepare images:
+    :end-before: -prepare images:
+    :language: cpp
+    :dedent: 2
+
+Construct requests
+------------------
+
+Using the images after preprocessing, you can construct requests to the inference server.
+For each image, you create an ``InferenceRequest`` and add input tensors to it.
+The ResNet50 model only accepts a single input tensor so you just add one by specifying the image data, its shape, and data type.
+In this example, you create a vector of such requests.
+
+.. literalinclude:: ../examples/resnet50/vitis.cpp
+    :start-after: +construct request:
+    :end-before: -construct request:
+    :language: cpp
+    :dedent: 2
+
+Make an inference
+-----------------
+
+There are multiple ways of making a request to the inference server, some of which are used in the different implementations of these examples.
+Before processing the response, you should verify it's not an error.
+
+Then, you can examine the outputs and, depending on the model, postprocess the results.
+For ResNet50, the raw results from the inference server lists the probabilities for each output class.
+The postprocessing identifies the highest probability classes and the top few of these are printed using the labels file to map the indices to a human-readable name.
+
+Here are some examples of making an inference used in these examples:
+
+This is the simplest way to make an inference: a blocking single inference where you loop through all the requests and make requests to the server one at a time.
+
+.. literalinclude:: ../examples/resnet50/vitis.cpp
+    :start-after: +validate:
+    :end-before: -validate:
+    :language: cpp
+    :dedent: 2
+
+You can also make a single asynchronous request to the server where you get back a ``std::future`` that you can use later to get the results of the inference.
+
+.. literalinclude:: ../examples/resnet50/ptzendnn.cpp
+    :start-after: +validate
+    :end-before: -validate
+    :language: cpp
+    :dedent: 2
+
+There are also some helper methods that wrap the basic inference APIs provided by the client.
+The ``inferAsyncOrdered`` method accepts a vector of requests, makes all the requests asynchronously using the ``modelInferAsync`` API, waits until each request completes, and then returns a vector of responses.
+If there are multiple requests sent in this way, they may be batched together by the server.
+
+.. literalinclude:: ../examples/resnet50/migraphx.cpp
+    :start-after: +validate:
+    :end-before: -validate:
+    :language: cpp
+    :dedent: 2
diff --git a/docs/example_resnet50_python.rst b/docs/example_resnet50_python.rst
new file mode 100644
index 000000000..eadabb792
--- /dev/null
+++ b/docs/example_resnet50_python.rst
@@ -0,0 +1,161 @@
+..
+    Copyright 2022 Advanced Micro Devices, Inc.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+Running ResNet50 - Python
+=========================
+
+This page walks you through the Python versions of the ResNet50 examples.
+You can see the full source files used here in the repository for more details.
+
+The inference server binds its C++ API to Python so the Python usage and functions look similar to their C++ counterparts but there are some differences due to the available features in both languages.
+You should read the C++ version of this documentation to better compare these two.
+
+In the dev container, the Python library is automatically built and installed as part of the CMake build process.
+You may also install the Python library by installing a wheel.
+
+.. note::
+    These examples are intended to demonstrate the API and how to communicate with the server.
+    They are not intended to show the most optimal performance for each backend.
+
+Include the module
+------------------
+
+You can include the Python library by importing the ``proteus`` module.
+The different submodules are imported by default but you can include them manually as well.
+These explicit imports can help IDEs resolve Python imports for autocompletion.
+
+.. literalinclude:: ../examples/resnet50/vitis.py
+    :start-after: +import
+    :end-before: -import
+    :language: py
+
+Start the server
+----------------
+
+This example assumes that the server is not running elsewhere and starts it locally from Python instead.
+Creating the ``proteus.servers.Server`` object starts the inference server backend and it stays alive as long as the object remains in scope.
+
+.. literalinclude:: ../examples/resnet50/vitis.py
+    :start-after: +initialize
+    :end-before: -initialize
+    :language: py
+    :dedent: 4
+
+Depending on what protocol you want to use to communicate with the server, you may need to start it explicitly using one of the object's methods.
+
+For example, if you were using HTTP/REST:
+
+.. literalinclude:: ../examples/resnet50/vitis.py
+    :start-after: +start protocol
+    :end-before: -start protocol
+    :language: py
+    :dedent: 4
+
+If the server is already running somewhere, you don't need to do this.
+
+Create the client object
+------------------------
+
+As in C++, you can create a client in Python corresponding to the protocol you want to use to talk to the server.
+
+.. literalinclude:: ../examples/resnet50/vitis.py
+    :start-after: +create client
+    :end-before: -create client
+    :language: py
+    :dedent: 4
+
+
+Load a worker
+-------------
+
+Once the server is ready, you have to load a worker to handle your request.
+This worker, and any load-time parameters it accepts, are backend-specific.
+After loading the worker, you get back an endpoint string that you use to make requests to this worker.
+If a worker is already ready on the server or you already have an endpoint, then you don't need to do this.
+
+.. literalinclude:: ../examples/resnet50/vitis.py
+    :start-after: +load
+    :end-before: -load
+    :language: py
+    :dedent: 4
+
+After loading a worker, make sure it's ready before attempting to make an inference.
+
+.. literalinclude:: ../examples/resnet50/vitis.py
+    :start-after: +wait model ready
+    :end-before: -wait model ready
+    :language: py
+    :dedent: 4
+
+Prepare images
+--------------
+
+Depending on the model, you may need to perform some preprocessing of the data before making an inference request.
+For ResNet50, this preprocessing generally consists of resizing the image, normalizing its values, and possibly converting types but its exact implementation depends on the model and on what the worker expects.
+The implementations of the preprocessing functions can be seen in the examples' sources and they differ for each backend.
+
+In these examples, you can pass a path to an image or to a directory to the executable.
+This single path gets converted to a List of paths containing just the path you passed in or paths to all the files in the directory you passed in and its passed to the ``preprocess`` function.
+The file at each path is opened as a ``numpy`` array and stored in a List that the preprocess function returns.
+
+.. literalinclude:: ../examples/resnet50/vitis.py
+    :start-after: +prepare images
+    :end-before: -prepare images
+    :language: py
+    :dedent: 4
+
+Construct requests
+------------------
+
+Using the images after preprocessing, you can construct requests to the inference server.
+Each image is constructed into an ``InferenceRequest`` using the ``ImageInferenceRequest`` helper function.
+This function accepts a single ``numpy`` array or a list of ``numpy`` arrays, where each array represents an input tensor.
+The ResNet50 model only accepts a single input tensor so a single image is enough.
+This function infers the shape and datatype of the image using the properties stored in the ``numpy`` array.
+
+.. literalinclude:: ../examples/resnet50/vitis.py
+    :start-after: +construct request
+    :end-before: -construct request
+    :language: py
+
+Make an inference
+-----------------
+
+There are multiple ways of making a request to the inference server, some of which are used in the different implementations of these examples.
+Before processing the response, you should verify it's not an error.
+
+Then, you can examine the outputs and, depending on the model, postprocess the results.
+For ResNet50, the raw results from the inference server lists the probabilities for each output class.
+The postprocessing identifies the highest probability classes and the top few of these are printed using the labels file to map the indices to a human-readable name.
+
+Here are some examples of making an inference used in these examples:
+
+This is the simplest way to make an inference: a blocking single inference where you loop through all the requests and make requests to the server one at a time.
+
+.. literalinclude:: ../examples/resnet50/vitis.py
+    :start-after: +run inference
+    :end-before: -run inference
+    :language: py
+    :dedent: 4
+
+There are also some helper methods that wrap the basic inference APIs provided by the client.
+The ``inferAsyncOrdered`` method accepts a list of requests, makes all the requests asynchronously using the C++ ``modelInferAsync`` API, waits until each request completes, and then returns a list of responses.
+If there are multiple requests sent in this way, they may be batched together by the server.
+
+.. literalinclude:: ../examples/resnet50/migraphx.py
+    :start-after: +run inference
+    :end-before: -run inference
+    :language: py
+    :dedent: 4
diff --git a/docs/hello_world_echo.rst b/docs/hello_world_echo.rst
new file mode 100644
index 000000000..51e5e9518
--- /dev/null
+++ b/docs/hello_world_echo.rst
@@ -0,0 +1,121 @@
+..
+    Copyright 2021 Xilinx, Inc.
+    Copyright 2022 Advanced Micro Devices, Inc.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.. _hello_world_python:
+
+Hello World - Python
+====================
+
+AMD Inference Server's Python API allows you to start the server and send requests to it from Python.
+This example walks you through how to start the server and use Python to send requests to a simple model.
+The complete script used here is available in the repository.
+
+Import the library
+------------------
+
+You need to bring in the AMD Inference Server Python library to use the API.
+The Python library is based on the Inference Server's C++ API and it gets installed in the dev container when you build the project.
+You can also install it using a pre-built wheel.
+
+.. literalinclude:: ../examples/python/hello_world_rest.py
+    :start-after: +imports:
+    :end-before: -imports:
+
+Create our client and server objects
+------------------------------------
+
+We assume that the server will be running locally with the default HTTP port and pass that to the client.
+This example assumes that the server will be running locally with the default HTTP port.
+Since you'll be using HTTP/REST to communicate with the server, you should create an `HttpClient` using the address of the server as the argument.
+
+
+.. literalinclude:: ../examples/python/hello_world_rest.py
+    :start-after: +create objects:
+    :end-before: -create objects:
+    :dedent: 4
+
+Is AMD Inference Server already running?
+----------------------------------------
+
+If the server is already started externally, you don't want to start it again.
+You can see if the server is live using the client object.
+If it's not live, then you can start the server from Python by instantiating the ``Server`` object.
+Depending on what protocol you're using to communicate with the server, that protocol may need to be started on the server.
+The server will remain active as long as the server object stays in scope.
+
+.. literalinclude:: ../examples/python/hello_world_rest.py
+    :start-after: +start server:
+    :end-before: -start server:
+    :dedent: 4
+
+Load a worker
+-------------
+
+Inference requests in AMD Inference Server are made to workers.
+Workers are started as threads in AMD Inference Server and have a defined lifecycle.
+Before making an inference request to a worker, it must first be loaded.
+Loading a worker returns an endpoint that the client should use for future operations.
+
+This worker, Echo, is a simple example worker that accepts an integer as input, adds one to it and returns the sum.
+
+.. literalinclude:: ../examples/python/hello_world_rest.py
+    :start-after: +load worker:
+    :end-before: -load worker:
+    :dedent: 4
+
+Inference
+---------
+
+Once the worker is ready, you can make an inference request to it.
+To do so, first construct a request.
+We construct a request that contains an integer and send it to AMD Inference Server.
+
+.. literalinclude:: ../examples/python/hello_world_rest.py
+    :start-after: +inference:
+    :end-before: -inference:
+    :dedent: 4
+
+To make a request, you need to create, at minimum, a ``proteus.InferenceRequest`` object and add ``proteus.InferenceRequestInput`` objects to it.
+Each input object represents an input tensor for the request and has a number of metadata attributes associated with it such as a name, datatype, and shape.
+This format is based on :github:`KServe's v2 specification <kserve/kserve/blob/master/docs/predict-api/v2/required_api.md>`.
+For images, there's also a helper method called ``proteus.ImageInferenceRequest`` that you can use to create requests.
+It's used in the ResNet50 Python examples.
+
+.. literalinclude:: ../examples/python/hello_world_rest.py
+    :start-after: +make request
+    :end-before: -make request
+
+Validate the response
+---------------------
+
+After making the inference, you should check what the response was.
+First, make sure the inference didn't fail by checking if the response is erroneous.
+Then, you can get the outputs and examine them.
+The format and contents of the output data will depend on the worker and model used for inference.
+In this case, the Echo worker returns a single output tensor back with one integer that should be one larger than what was sent.
+The assertions here check these cases.
+
+.. literalinclude:: ../examples/python/hello_world_rest.py
+    :start-after: +validate:
+    :end-before: -validate:
+    :dedent: 4
+
+Clean up
+--------
+
+Workers that are loaded in AMD Inference Server will persist until the server shuts down or they're explicitly unloaded.
+While it's not shown here, the Python API provides an ``unload()`` method for this purpose.
+As the script ends, the Server object's destructor will clean up any active workers on the server and it will shut down
diff --git a/docs/hello_world_python.rst b/docs/hello_world_python.rst
deleted file mode 100644
index 83e55a059..000000000
--- a/docs/hello_world_python.rst
+++ /dev/null
@@ -1,111 +0,0 @@
-..
-    Copyright 2021 Xilinx Inc.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-        http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-
-.. _hello_world_python:
-
-Hello World (Python)
-====================
-
-AMD Inference Server's Python API allows you to start the server and send requests to it from Python.
-This example walks you through how to start the server and use Python to send requests to a simple model.
-The complete script used here is available: :file:`examples/python/hello_world_rest.py`.
-
-Import the library
-------------------
-
-We need to bring in the AMD Inference Server Python library.
-The library's source code is in :file:`src/python` and it gets installed in the dev container on startup.
-
-.. literalinclude:: ../examples/python/hello_world_rest.py
-    :start-after: +imports:
-    :end-before: -imports:
-
-Create our client and server objects
-------------------------------------
-
-We assume that the server will be running locally with the default HTTP port and pass that to the client.
-In this example, we'll be using REST to communicate to the server so we create a :py:class:`RestClient` object.
-
-.. literalinclude:: ../examples/python/hello_world_rest.py
-    :start-after: +create objects:
-    :end-before: -create objects:
-    :dedent: 4
-
-Is AMD Inference Server already running?
--------------------------------------------
-
-If the server is already started externally, we don't want to start it again.
-So, we attempt to check if the server is live.
-If this fails, we start the server ourselves.
-Either way, the client will attempt to communicate to the server at ``http://localhost:8998``.
-
-.. literalinclude:: ../examples/python/hello_world_rest.py
-    :start-after: +start server:
-    :end-before: -start server:
-    :dedent: 4
-
-Load a worker
--------------
-
-Inference requests in AMD Inference Server are made to workers.
-Workers are started as threads in AMD Inference Server and have a defined lifecycle.
-Before making an inference request to a worker, it must first be loaded.
-Loading a worker returns an identifier that the client should use for future operations.
-
-This worker, Echo, is a simple example worker that accepts integers as input, adds one to the inputs and returns the sum.
-We'll use it to demonstrate the Python flow.
-
-.. literalinclude:: ../examples/python/hello_world_rest.py
-    :start-after: +load worker:
-    :end-before: -load worker:
-    :dedent: 4
-
-Inference
----------
-
-Once the worker is ready, we can make an inference request to it.
-We construct a request that contains five integers and send it to AMD Inference Server.
-The ``NumericalInferenceRequest`` class is a helper class that simplifies creating a request in the right format.
-
-.. literalinclude:: ../examples/python/hello_world_rest.py
-    :start-after: +inference:
-    :end-before: -inference:
-    :dedent: 4
-
-Validate the response
----------------------
-
-Now, we want to check what our response is.
-Here, we can simplify our checks because we already know what we expect to receive.
-So we check is that the number of outputs match the number of inputs we used.
-We also check that each output only has one index and is one more than the corresponding input since we know that's what the Echo worker does.
-
-.. literalinclude:: ../examples/python/hello_world_rest.py
-    :start-after: +validate:
-    :end-before: -validate:
-    :dedent: 4
-
-Clean up
---------
-
-Workers that are loaded in AMD Inference Server will persist until the server shuts down or they're explicitly unloaded.
-While it's not shown here, the Python API provides an ``unload()`` method for this purpose.
-Finally, if we started the server from Python, we shut it down before finishing.
-If there are any loaded workers at this time, they will be cleaned up before shutdown.
-
-.. literalinclude:: ../examples/python/hello_world_rest.py
-    :start-after: +clean up:
-    :end-before: -clean up:
-    :dedent: 4
diff --git a/docs/index.rst b/docs/index.rst
index 369beab1b..ee6b9bcc2 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -26,11 +26,10 @@
     :caption: Getting Started
 
     quickstart
-    hello_world_python
-    xmodel_example_python
-    xmodel_example_cpp
-    zendnn_examples
-    cli
+    hello_world_echo
+    example_resnet50_cpp
+    example_resnet50_python
+
 
 .. toctree::
     :maxdepth: 2
@@ -67,6 +66,7 @@
     rest
     cpp_api
     python_api
+    cli
 
 .. toctree::
     :maxdepth: 2
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
new file mode 100644
index 000000000..86b15b894
--- /dev/null
+++ b/examples/CMakeLists.txt
@@ -0,0 +1,19 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set(PROTEUS_EXAMPLE_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
+
+include(AddExample)
+
+add_subdirectory(resnet50)
diff --git a/examples/bert/.gitignore b/examples/bert/.gitignore
new file mode 100644
index 000000000..b41eefb59
--- /dev/null
+++ b/examples/bert/.gitignore
@@ -0,0 +1,1 @@
+predictions
diff --git a/examples/bert/README.rst b/examples/bert/README.rst
new file mode 100644
index 000000000..c55c685c7
--- /dev/null
+++ b/examples/bert/README.rst
@@ -0,0 +1,32 @@
+..
+    Copyright 2022 Advanced Micro Devices, Inc.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+BERT
+----
+
+This example demonstrates how to make an inference using a BERT model with the MIGraphX backend in the inference server.
+Install the requirements to run this example with ``pip install -r requirements.txt``
+
+Files
+^^^^^
+
+The files in this set of examples are:
+
+::
+
+    examples/bert
+    ├── bert.py          - common functions for all BERT examples
+    ├── migraphx.py      - MIGraphX example for BERT
+    └── requirements.txt - Python requirements for running YOLO examples
diff --git a/examples/bert/bert.py b/examples/bert/bert.py
new file mode 100644
index 000000000..96e4f5116
--- /dev/null
+++ b/examples/bert/bert.py
@@ -0,0 +1,83 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import sys
+
+
+def parse_args():
+    """
+    Parse the common command-line arguments
+
+    Returns:
+        argparse.Namespace: args
+    """
+    parser = argparse.ArgumentParser(description="bert example")
+
+    parser.add_argument(
+        "--model",
+        default="",
+        help="Path to the bert model on the server",
+    )
+
+    parser.add_argument(
+        "--input",
+        default="",
+        help="Path to a JSON file containing the inputs on the client",
+    )
+
+    parser.add_argument(
+        "--vocab",
+        default="",
+        help="Path to the text file containing the vocabulary on the client",
+    )
+
+    parser.add_argument(
+        "--batch-size",
+        default=10,
+        help="Batch size to use for the MIGraphX worker on the server",
+    )
+
+    parser.add_argument(
+        "--http-port",
+        default=8998,
+        help="Port to use for HTTP server",
+    )
+
+    parser.add_argument(
+        "--grpc-port",
+        default=50051,
+        help="Port to use for gRPC server",
+    )
+
+    args = parser.parse_args()
+
+    if (not args.input) or (not args.model) or (not args.vocab):
+        root = os.getenv("PROTEUS_ROOT")
+        if root is None:
+            print("PROTEUS_ROOT is not defined in the environment")
+            print("-> Needed to infer default values for arguments")
+            print("Either:\n - define PROTEUS_ROOT in the environment")
+            print(" - pass all the following flags:")
+            print("     --input")
+            print("     --model")
+            print("     --vocab")
+            sys.exit(1)
+
+        # args.input is unset and set by each example
+        # args.model is unset and set by each example
+        # args.vocab is unset and set by each example
+
+    return args
diff --git a/examples/bert/migraphx.py b/examples/bert/migraphx.py
new file mode 100644
index 000000000..70159845b
--- /dev/null
+++ b/examples/bert/migraphx.py
@@ -0,0 +1,280 @@
+# MIT License
+#
+# Copyright (c) 2015-2022 Advanced Micro Devices, Inc. All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+This example demonstrates how you can use the MIGraphX backend to run inference
+on an AMD GPU with a BERT ONNX model.
+
+This example, as compared to the ResNet50 one, is very similar. However, the Bert
+model has multiple inputs and outputs unlike ResNet50 and the requests are
+constructed manually rather than with the ImageInferenceRequest helper method.
+
+This example is based on a similar example in MIGraphX:
+https://github.com/ROCmSoftwarePlatform/AMDMIGraphX/blob/develop/examples/nlp/python_bert_squad/bert-squad-migraphx.py
+"""
+
+
+import collections
+import json
+import os
+import pathlib
+import sys
+import time
+
+try:
+    import numpy as np
+    import tokenizers
+except ImportError:
+    print(
+        "Could not import one or more modules in bert. Did you run 'pip install -r requirements.txt'?"
+    )
+    sys.exit(1)
+
+import proteus
+
+# isort: split
+
+from bert import parse_args
+
+base_path = pathlib.Path(__file__).parent.resolve()
+download_dir = base_path / "../../external/artifacts/onnx/bert"
+sys.path.append(str(download_dir))
+try:
+    from run_onnx_squad import (
+        convert_examples_to_features,
+        read_squad_examples,
+        write_predictions,
+    )
+except ImportError:
+    print("Could not import run_onnx_squad. Did you run 'proteus get --migraphx'?")
+    sys.exit(1)
+
+
+def load(client, args):
+    """
+    Load a worker to handle an inference request. The load returns the endpoint
+    you should use for subsequent requests
+
+    Args:
+        client (proteus.client.Client): the client object
+        args (argparse.Namespace): the command line arguments
+
+    Returns:
+        str: endpoint
+    """
+
+    # The only parameter the migraphx worker requires is the model file name.
+    # batch and timeout are optional.
+    # It will take the file name stem and search for either a *.onnx or *.mxr extension, and if
+    # it finds a *.onnx file it will compile it and save the compiled model as *.mxr for
+    # future use. It will read the array dimensions and data type from the model.
+    parameters = proteus.RequestParameters()
+    parameters.put("model", args.model)
+
+    parameters.put("batch", args.batch_size)
+
+    # this call requests the server to either find a running instance of the named
+    # worker type, or else create one and initialize it with the parameters.
+    endpoint = client.workerLoad("migraphx", parameters)
+
+    # wait for the worker to load and compile model
+    ready = False
+    while not ready:
+        ready = client.modelReady(endpoint)
+
+    return endpoint
+
+
+def construct_requests(eval_examples, input_ids, input_mask, segment_ids, batch_size):
+    n = len(input_ids)
+
+    requests = []
+    for idx in range(0, n):
+        # Create an InferenceRequest
+        request = proteus.predict_api.InferenceRequest()
+        item = eval_examples[idx]  # class SquadExample
+
+        # Depending on the model, it will require one or more input tensors
+        # The values for name, datatype, shape will also be model-dependent
+        # For MIGraphX, the names for each input tensor are meaningful
+
+        input_n = proteus.predict_api.InferenceRequestInput()
+        input_n.name = f"input_ids:0"
+        input_n.datatype = proteus.DataType.INT64
+        input_n.shape = (
+            1,
+            256,
+        )
+        input_n.setInt64Data(input_ids[idx : idx + batch_size])
+        request.addInputTensor(input_n)
+
+        input_n = proteus.predict_api.InferenceRequestInput()
+        input_n.name = f"input_mask:0"
+        input_n.datatype = proteus.DataType.INT64
+        input_n.shape = (
+            1,
+            256,
+        )
+        input_n.setInt64Data(input_mask[idx : idx + batch_size])
+        request.addInputTensor(input_n)
+
+        input_n = proteus.predict_api.InferenceRequestInput()
+        input_n.name = f"segment_ids:0"
+        input_n.datatype = proteus.DataType.INT64
+        input_n.shape = (
+            1,
+            256,
+        )
+        input_n.setInt64Data(segment_ids[idx : idx + batch_size])
+        request.addInputTensor(input_n)
+
+        # This comes from the first argument; I think it's supposed to be output names
+        #       see https://onnxruntime.ai/docs/api/python/api_summary.html#load-and-run-a-model
+        # item = eval_examples[idx]   # class SquadExample
+        input_n = proteus.predict_api.InferenceRequestInput()
+        input_n.name = f"unique_ids_raw_output___9:0"
+        input_n.datatype = proteus.DataType.INT64
+        input_n.shape = (1,)
+        input_n.setInt64Data(np.array([item.qas_id], dtype=np.int64))
+        request.addInputTensor(input_n)
+        requests.append(request)
+    return requests
+
+
+def get_args():
+    """
+    The command-line arguments are parsed in two phases. There's the common
+    arguments that are initialized by parse_args that are shared by all the
+    Python examples in this directory and then example-specific settings are
+    initialized here
+
+    Returns:
+        argparse.Namespace: the args
+    """
+    args = parse_args()
+
+    root = os.getenv("PROTEUS_ROOT")
+
+    # assign default values if these are unset
+    if not args.model:
+        args.model = root + "/external/artifacts/onnx/bert/bertsquad-10.onnx"
+
+    if not args.input:
+        args.input = root + "/external/artifacts/onnx/bert/inputs_amd.json"
+
+    if not args.vocab:
+        args.vocab = root + "/external/artifacts/onnx/bert/vocab.txt"
+
+    return args
+
+
+def main(args):
+    print("Running the MIGraphX example for Bert in Python")
+
+    # connect to the server
+    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
+    print("Waiting for server...", end="")
+    start_server = not client.serverLive()
+    if start_server:
+        server = proteus.servers.Server()
+        server.startHttp(args.http_port)
+        while not client.serverLive():
+            time.sleep(1)
+    print("OK. Connected.")
+
+    print("Loading worker...")
+    endpoint = load(client, args)
+
+    # Use read_squad_examples method from run_onnx_squad to read the input file
+    eval_examples = read_squad_examples(input_file=args.input)
+
+    print("Constructing requests...")
+    max_seq_length = 256
+    doc_stride = 128
+    max_query_length = 64
+
+    tokenizer = tokenizers.BertWordPieceTokenizer(args.vocab)
+
+    # Use convert_examples_to_features method from run_onnx_squad to get parameters from the input
+    # the first 3 are lists of ndarray, extra_data is a list of Feature (a namedtuple)
+    input_ids, input_mask, segment_ids, extra_data = convert_examples_to_features(
+        eval_examples, tokenizer, max_seq_length, doc_stride, max_query_length
+    )
+    # extra_data[0] is a Feature
+    requests = construct_requests(
+        eval_examples, input_ids, input_mask, segment_ids, args.batch_size
+    )
+
+    print(f"Sending {len(requests)} request(s)...")
+    responses = proteus.client_operators.inferAsyncOrdered(client, endpoint, requests)
+    print("Client received inference reply")
+
+    print("Postprocessing...")
+    raw_result = collections.namedtuple(
+        "RawResult", ["unique_id", "start_logits", "end_logits"]
+    )
+    all_results = []
+    for it, response in enumerate(responses):
+        # the migraphx worker doesn't support names for output channels,
+        # so we get them by position instead.
+        out0 = response.getOutputs()[0]
+        assert out0.datatype == proteus.DataType.FP32
+
+        out1 = response.getOutputs()[1]
+        assert out1.datatype == proteus.DataType.FP32
+
+        # The third output ("unique_ids:0") is a single int64 which our example doesn't use.
+        # Put our results into a RawResult structure
+        all_results.append(
+            raw_result(
+                unique_id=it,
+                start_logits=out1.getFp32Data(),
+                end_logits=out0.getFp32Data(),
+            )
+        )
+
+    output_dir = base_path / "predictions"
+    os.makedirs(output_dir, exist_ok=True)
+    output_prediction_file = output_dir / "predictions.json"
+    output_nbest_file = output_dir / "nbest_predictions.json"
+    n_best_size = 20
+    max_answer_length = 301
+    write_predictions(
+        eval_examples,
+        extra_data,
+        all_results,
+        n_best_size,
+        max_answer_length,
+        True,
+        output_prediction_file,
+        output_nbest_file,
+    )
+
+    with open(output_prediction_file, "r") as json_file:
+        test_data = json.load(json_file)
+        print(json.dumps(test_data, indent=2))
+    print("Done! Your output file is ", output_prediction_file)
+
+
+if __name__ == "__main__":
+    args = get_args()
+    main(args)
diff --git a/examples/bert/requirements.txt b/examples/bert/requirements.txt
new file mode 100644
index 000000000..b94bc3e24
--- /dev/null
+++ b/examples/bert/requirements.txt
@@ -0,0 +1,3 @@
+tokenizers==0.12.1
+onnxruntime
+six
diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt
deleted file mode 100644
index 7eb75c28f..000000000
--- a/examples/cpp/CMakeLists.txt
+++ /dev/null
@@ -1,61 +0,0 @@
-# Copyright 2021 Xilinx Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-if(${PROTEUS_ENABLE_VITIS})
-  list(APPEND apps custom_processing_native)
-  if(${PROTEUS_ENABLE_GRPC})
-    list(APPEND apps custom_processing_grpc)
-  endif()
-  foreach(app ${apps})
-    add_executable(${app} custom_processing.cpp)
-
-    target_link_libraries(
-      ${app} PRIVATE proteus util opencv_imgcodecs opencv_imgproc opencv_core
-    )
-    target_include_directories(${app} PRIVATE ${PROTEUS_INCLUDE_DIRS})
-  endforeach()
-  if(${PROTEUS_ENABLE_GRPC})
-    target_compile_definitions(custom_processing_grpc PRIVATE ENABLE_GRPC)
-  endif()
-endif()
-
-if(${PROTEUS_ENABLE_TFZENDNN})
-  add_executable(tf_zendnn_client tf_zendnn_client.cpp)
-
-  target_link_libraries(
-    tf_zendnn_client PRIVATE proteus util opencv_imgcodecs opencv_imgproc
-                             opencv_core
-  )
-  target_include_directories(tf_zendnn_client PRIVATE ${PROTEUS_INCLUDE_DIRS})
-endif()
-
-if(${PROTEUS_ENABLE_PTZENDNN})
-  add_executable(pt_zendnn_client pt_zendnn_client.cpp)
-
-  target_link_libraries(
-    pt_zendnn_client PRIVATE proteus util opencv_imgcodecs opencv_imgproc
-                             opencv_core
-  )
-  target_include_directories(pt_zendnn_client PRIVATE ${PROTEUS_INCLUDE_DIRS})
-endif()
-
-if(${PROTEUS_ENABLE_MIGRAPHX})
-  add_executable(migraphx_client migraphx_client.cpp)
-
-  target_link_libraries(
-    migraphx_client PRIVATE proteus util opencv_imgcodecs opencv_imgproc
-                            opencv_core migraphx::c
-  )
-  target_include_directories(migraphx_client PRIVATE ${PROTEUS_INCLUDE_DIRS})
-endif()
diff --git a/examples/cpp/custom_processing.cpp b/examples/cpp/custom_processing.cpp
deleted file mode 100644
index 3ccad38fe..000000000
--- a/examples/cpp/custom_processing.cpp
+++ /dev/null
@@ -1,202 +0,0 @@
-// Copyright 2021 Xilinx Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <algorithm>              // for max, fill
-#include <array>                  // for array
-#include <cmath>                  // for exp
-#include <cstdint>                // for int8_t, uint64_t, int32_t
-#include <cstdlib>                // for exit, getenv, size_t
-#include <initializer_list>       // for initializer_list
-#include <iostream>               // for operator<<, basic_ostream::operator<<
-#include <memory>                 // for allocator, allocator_traits<>::valu...
-#include <opencv2/core.hpp>       // for Mat, Vec3b, MatSize, Vec, CV_8SC3
-#include <opencv2/imgcodecs.hpp>  // for imread
-#include <opencv2/imgproc.hpp>    // for resize
-#include <queue>                  // for priority_queue
-#include <string>                 // for string, operator==, basic_string
-#include <unordered_set>          // for operator==, unordered_set, unordere...
-#include <utility>                // for pair
-#include <vector>                 // for vector
-
-#ifdef ENABLE_GRPC
-#include <chrono>  // for seconds
-#include <thread>  // for sleep_for
-#endif
-
-// +include:
-#include "proteus/proteus.hpp"
-// -include:
-
-#include "proteus/util/pre_post/image_preprocess.hpp"
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"
-
-#ifdef ENABLE_GRPC
-constexpr auto GRPC_PORT = 50051;
-constexpr auto GRPC_ADDRESS = "localhost:50051";
-
-std::string load(const std::string& path_to_xmodel) {
-  // +initialize grpc:
-  proteus::GrpcClient client{GRPC_ADDRESS};
-  // -initialize grpc:
-  while (!client.serverLive()) {
-    std::this_thread::sleep_for(std::chrono::seconds(1));
-  }
-
-  auto metadata = client.serverMetadata();
-  if (metadata.extensions.find("vitis") == metadata.extensions.end()) {
-    std::cout << "Vitis AI support required but not found.\n";
-    exit(0);
-  }
-
-  // +load grpc:
-  proteus::RequestParameters parameters;
-  parameters.put("model", path_to_xmodel);
-  auto worker_name = client.workerLoad("Xmodel", &parameters);
-  // -load grpc:
-
-  return worker_name;
-}
-
-proteus::InferenceResponse infer(const std::string& worker_name,
-                                 const proteus::InferenceRequest& request) {
-  proteus::GrpcClient client{GRPC_ADDRESS};
-
-  // +inference grpc:
-  auto results = client.modelInfer(worker_name, request);
-  // -inference grpc:
-
-  return results;
-}
-
-std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
-                             int k) {
-  return proteus::util::resnet50Postprocess(
-    static_cast<int8_t*>(output.getData()), output.getSize(), k);
-}
-#else
-std::string load(const std::string& path_to_xmodel) {
-  proteus::NativeClient client;
-  auto metadata = client.serverMetadata();
-  if (metadata.extensions.find("vitis") == metadata.extensions.end()) {
-    std::cout << "Vitis AI support required but not found.\n";
-    exit(0);
-  }
-
-  // +load native:
-  proteus::RequestParameters parameters;
-  parameters.put("model", path_to_xmodel);
-  auto worker_name = client.workerLoad("Xmodel", &parameters);
-  // -load native:
-
-  return worker_name;
-}
-
-proteus::InferenceResponse infer(const std::string& worker_name,
-                                 const proteus::InferenceRequest& request) {
-  proteus::NativeClient client;
-  // +inference native:
-  auto response = client.modelInfer(worker_name, request);
-  // -inference native:
-
-  return response;
-}
-
-std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
-                             int k) {
-  return proteus::util::resnet50Postprocess(
-    static_cast<int8_t*>(output.getData()), output.getSize(), k);
-}
-#endif
-
-/**
- * @brief The custom processing example demonstrates how to plug in custom pre-
- * and post-processing around a call to the inference server. This example uses
- * Resnet50, a classification model, in the XModel worker. Pre- and post-
- * processing is done in C++ before and after the call to the server.
- *
- * @return int
- */
-int main() {
-  auto* root_env = std::getenv("PROTEUS_ROOT");
-  if (root_env == nullptr) {
-    std::cerr << "PROTEUS_ROOT is not defined in the environment\n";
-    return 1;
-  }
-  std::string root = root_env;
-
-  // +user variables: update as needed!
-  const auto request_num = 4;
-  const auto* path_to_xmodel =
-    "${PROTEUS_ROOT}/external/artifacts/u200_u250/resnet_v1_50_tf/"
-    "resnet_v1_50_tf.xmodel";
-  const auto path_to_image = root + "/tests/assets/dog-3619020_640.jpg";
-  // for this image, we know what we expect to receive with this XModel
-  // note this is different than custom_processing.py due to how the top_k are
-  // calculated. Index 230 and 154 are equal classifications
-  const std::array gold_response_output = {259, 261, 260, 157, 230};
-  const auto k = gold_response_output.size();
-  // -user variables:
-
-  // +initialize:
-  proteus::Server server;
-// -initialize:
-#ifdef ENABLE_GRPC
-  server.startGrpc(GRPC_PORT);
-#endif
-
-  auto worker_name = load(path_to_xmodel);
-
-  // +prepare images:
-  std::vector<std::string> paths;
-  paths.reserve(request_num);
-  paths.emplace_back(path_to_image);
-
-  proteus::util::ImagePreprocessOptions<int8_t, 3> options;
-  options.order = proteus::util::ImageOrder::NHWC;
-  options.mean = {123, 107, 104};
-  options.std = {1, 1, 1};
-  options.normalize = true;
-  auto images = proteus::util::imagePreprocess(paths, options);
-  // -prepare images:
-
-  // +construct request:
-  const std::initializer_list<uint64_t> shape = {224, 224, 3};
-
-  proteus::InferenceRequest request;
-  request.addInputTensor(static_cast<void*>(images[0].data()), shape,
-                         proteus::DataType::INT8);
-  // -construct request:
-
-  for (auto i = 0; i < request_num; i++) {
-    auto results = infer(worker_name, request);
-
-    // +validate:
-    auto outputs = results.getOutputs();
-    for (auto& output : outputs) {
-      std::vector<int> top_k = postprocess(output, k);
-      for (size_t j = 0; j < k; j++) {
-        if (top_k[j] != gold_response_output.at(j)) {
-          std::cerr << "Output (" << top_k[j] << ") does not match golden ("
-                    << gold_response_output.at(j) << ")\n";
-          return 1;
-        }
-      }
-    }
-    // -validate:
-  }
-
-  std::cout << "custom_processing.cpp: Passed\n";
-
-  return 0;
-}
diff --git a/examples/cpp/migraphx_client.cpp b/examples/cpp/migraphx_client.cpp
deleted file mode 100644
index 9038c00b5..000000000
--- a/examples/cpp/migraphx_client.cpp
+++ /dev/null
@@ -1,244 +0,0 @@
-// Copyright 2022 Advanced Micro Devices, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <array>    // for array
-#include <cmath>    // for exp
-#include <cstdint>  // for int8_t, uint64_t
-#include <cstdlib>  // for getenv, size_t
-#include <filesystem>
-#include <fstream>  // For files
-// #include <functional>             // for multiplies
-#include <future>            // for future
-#include <initializer_list>  // for initializer_list
-#include <iostream>          // for operator<<, basic_ostream::operator<<
-// #include <numeric>
-#include <opencv2/core.hpp>       // for Mat, Vec3b, MatSize, Vec, CV_8SC3
-#include <opencv2/imgcodecs.hpp>  // for imread
-#include <opencv2/imgproc.hpp>    // for resize
-#include <queue>                  // for priority_queue, queue
-#include <string>                 // for string, allocator, operator==, basi...
-#include <utility>                // for pair, move
-#include <vector>                 // for vector
-
-#include "proteus/proteus.hpp"  // for InferenceResponseFuture, terminate
-#include "proteus/util/pre_post/image_preprocess.hpp"
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"
-#include "proteus/util/read_nth_line.hpp"
-
-// // #include "migraphx/filesystem.hpp"
-// #pragma GCC diagnostic push
-// // Disable a warning for extra ; that appear in migraphx.hpp
-// #pragma GCC diagnostic ignored "-Wpedantic"
-// #include "migraphx/migraphx.hpp"  // MIGraphX C++ API
-// #pragma GCC diagnostic pop
-
-/**
- * @brief user variables: update as needed!
- * If image location is provided, the prediction is done for that image,
- * If not provided, dummy data will be created for functionality testing.
- *
- */
-struct Option {
-  std::filesystem::path root = std::getenv("PROTEUS_ROOT");
-
-  std::filesystem::path model =
-    root / "external/artifacts/onnx/resnet50v2/resnet50-v2-7.onnx";
-  std::filesystem::path image_location =
-    root / "tests/assets/dog-3619020_640.jpg";
-
-  // same content as tests/assets/imagenet_simple_labels.json
-  std::filesystem::path class_file =
-    root / "examples/python/utils/imagenet_classes.txt";
-
-  int batch_size = 64;
-  int input_size =
-    224;  // the input size is set by the Resnet model (224x224 pixels)
-
-  // values used in iterations and postprocessing
-  int output_classes = 1000;
-  int steps = 1;
-  int topK = 5;
-} options;
-
-/** Runs an MiGraphX inference straight from the API, without going
- * through the Inference Server.  Use to verify that results for the
- * same model are the same, or to do performance checks on the GPU that bypass
- * the time overhead of the REST interface.
- *
- *
- * */
-
-int main(int argc, char* argv[]) {
-  auto* root_env = std::getenv("PROTEUS_ROOT");
-  if (root_env == nullptr) {
-    std::cerr << "PROTEUS_ROOT is not defined in the environment\n";
-    return 1;
-  }
-
-  std::filesystem::path root(root_env);
-
-  std::filesystem::path model =
-    root / "external/artifacts/onnx/resnet50v2/resnet50-v2-7.onnx";
-  if (argc > 1) model = std::string(argv[1]);
-
-  int batch_size(64);
-  if (argc > 2) batch_size = atoi(argv[2]);
-  options.batch_size = batch_size;
-
-  // The image file path is hardcoded.  For performance testing we'll just
-  // process multiple copies of the same image.
-  std::filesystem::path image_filename = options.image_location;
-  std::vector<std::string> paths;
-  paths.push_back(image_filename.string());
-  std::cout << " input image is " << image_filename << "   batch size is "
-            << std::to_string(batch_size) << std::endl;
-
-  // initialize the server
-  proteus::Server server;
-
-  auto client = proteus::NativeClient();
-  auto metadata = client.serverMetadata();
-  if (metadata.extensions.find("migraphx") == metadata.extensions.end()) {
-    std::cout << "Migraphx support required but is not installed in this "
-                 "environment.\n";
-    exit(0);
-  }
-
-  //
-  // load worker with required parameters
-  //
-
-  proteus::RequestParameters parameters;
-  parameters.put("batch", batch_size);
-  parameters.put("model", model);
-  // timeout is measured in milliseconds
-  parameters.put("timeout", 100);
-  auto workerName = client.workerLoad("Migraphx", &parameters);
-  std::cout << "loaded worker name " << workerName << std::endl;
-
-  // Load the image and make an array of copies
-  proteus::util::ImagePreprocessOptions<float, 3> options_;
-  options_.normalize = true;
-  options_.order = proteus::util::ImageOrder::NCHW;
-  options_.mean = {0.485, 0.456, 0.406};
-  options_.std = {4.367, 4.464, 4.444};
-  options_.convert_color = true;
-  options_.color_code = cv::COLOR_BGR2RGB;
-  options_.convert_type = true;
-  options_.type = CV_32FC3;
-  options_.convert_scale = 1.0 / 255.0;
-  auto images = proteus::util::imagePreprocess(paths, options_);
-
-  const std::initializer_list<uint64_t> shape = {
-    3, static_cast<long unsigned>(options.input_size),
-    static_cast<long unsigned>(options.input_size)};
-  std::queue<proteus::InferenceResponseFuture> queue;
-  proteus::InferenceRequest request;
-  request.addInputTensor(static_cast<void*>(images[0].data()), shape,
-                         proteus::DataType::FP32);
-  // Timing the prediction
-  auto start = std::chrono::high_resolution_clock::now();  // Timing the start
-  auto results = client.modelInfer(workerName, request);
-  auto stop = std::chrono::high_resolution_clock::now();
-  auto duration =
-    std::chrono::duration_cast<std::chrono::milliseconds>(stop - start);
-  float time_tmp = duration.count();
-
-  // Parse the results and report the top K results
-  auto outputs = results.getOutputs();
-  for (auto& output : outputs) {
-    auto top_k = proteus::util::resnet50Postprocess(
-      static_cast<float*>(output.getData()), output.getSize(), options.topK);
-    std::cout << "Top " << options.topK << " classes:" << std::endl;
-    for (int j = 0; j < options.topK; j++)
-      std::cout << j + 1 << ": Index: " << top_k[j] << " :: Class: "
-                << proteus::util::readNthLine(options.class_file, top_k[j])
-                << std::endl;
-  }
-
-  // Running for `steps` number of times for proper benchmarking
-  start = std::chrono::high_resolution_clock::now();  // Timing the start
-  for (int step = 0; step < options.steps; step++) {
-    for (auto i = 0; i < options.batch_size; i++) {
-      proteus::InferenceRequest request;
-      request.addInputTensor(static_cast<void*>(images[0].data()), shape,
-                             proteus::DataType::FP32);
-      auto results = client.modelInfer(workerName, request);
-    }
-
-    std::cout << " INPUT, batch size: " << request.getInputSize() << " "
-              << options.batch_size << std::endl;
-    // auto results = client.modelInfer(workerName, request);
-  }
-  // Timing the prediction
-  stop = std::chrono::high_resolution_clock::now();
-  duration =
-    std::chrono::duration_cast<std::chrono::milliseconds>(stop - start);
-  time_tmp = float(duration.count()) / options.steps;
-
-  std::cout << "\nAverage time taken for " << options.image_location.c_str()
-            << " : " << std::to_string(time_tmp) << "ms" << std::endl;
-
-  std::cout << "Batch Size: " + std::to_string(options.batch_size) +
-                 " FPS: " + std::to_string(options.batch_size / time_tmp * 1000)
-            << std::endl;
-  std::cout << " finished.  Time is " << std::to_string(time_tmp) << std::endl;
-
-  return 0;
-}
-
-// clang-format off
-// // This is the Migraphx C++ API main, not the inference server C+ API
-// int main(int argc, char* argv[]){
-// std::string model_filename =
-// "/workspace/proteus/external/artifacts/migraphx/resnet50-v1-7/resnet50-v1-7.onnx";
-// if(argc > 1)
-//     model_filename = std::string(argv[1]);
-
-// migraphx::onnx_options onnx_opts;
-// onnx_opts.set_default_dim_value(64);
-// std::cout << " parsing model file "  << model_filename << std::endl;
-// migraphx::api::program program = migraphx::parse_onnx(model_filename.c_str(),
-// onnx_opts); migraphx::compile_options options; options.set_offload_copy();
-// program.compile(migraphx::target("gpu"), options);
-
-// std::string image_filename =
-// "/workspace/proteus/tests/assets/dog-3619020_640.jpg";
-//  if(argc > 2)
-//     image_filename = std::string(argv[2]);
-// std::cout << " input image is " << image_filename << std::endl;
-
-// migraphx::program_parameters pp;
-// // the shape taken from the Resnet50 model should be 224x224x3.  Make the
-// input image the same size. auto param_shapes =
-// program.get_parameter_shapes(); for(auto&& name : param_shapes.names())
-// {
-//     std::cout << "adding random image to test set" << std::endl;
-//     pp.add(name, migraphx::argument::generate(param_shapes[name]));
-// }
-
-// // run the inference
-// auto outputs = program.eval(pp);
-// auto output  = outputs[0];
-// auto lens    = output.get_shape().lengths();
-// auto elem_num =
-//     std::accumulate(lens.begin(), lens.end(), 1,
-//     std::multiplies<std::size_t>());
-// float* data_ptr = reinterpret_cast<float*>(output.data());
-// std::vector<float> ret(data_ptr, data_ptr + elem_num);
-
-// We should see 1000 items returned.
-// std::cout << "done.  elem is " << std::to_string(elem_num) << std::endl;
-// }
-// clang-format on
diff --git a/examples/cpp/pt_zendnn_client.cpp b/examples/cpp/pt_zendnn_client.cpp
deleted file mode 100644
index 1ebb46663..000000000
--- a/examples/cpp/pt_zendnn_client.cpp
+++ /dev/null
@@ -1,212 +0,0 @@
-// Copyright 2022 Advanced Micro Devices, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <algorithm>              // for max, copy
-#include <array>                  // for array
-#include <chrono>                 // for milliseconds, duration_cast, operator-
-#include <cmath>                  // for exp
-#include <cstdint>                // for uint64_t
-#include <cstdlib>                // for exit, getenv, size_t
-#include <fstream>                // IWYU pragma: keep
-#include <initializer_list>       // for initializer_list
-#include <iostream>               // for operator<<, basic_ostream, endl
-#include <memory>                 // for allocator_traits<>::value_type
-#include <opencv2/core.hpp>       // for Mat, randu, CV_32FC3, MatSize, Scalar
-#include <opencv2/imgcodecs.hpp>  // for imread
-#include <opencv2/imgproc.hpp>    // for cvtColor, resize, COLOR_BGR2RGB
-#include <queue>                  // for priority_queue, queue
-#include <string>                 // for string, operator+, allocator, opera...
-#include <unordered_set>          // for operator==, unordered_set, unordere...
-#include <utility>                // for pair
-#include <vector>                 // for vector
-
-#include "proteus/proteus.hpp"  // for InferenceResponseFuture, terminate
-#include "proteus/util/pre_post/image_preprocess.hpp"
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"
-#include "proteus/util/read_nth_line.hpp"
-
-/**
- * @brief The custom processing example demonstrates how to plug in custom pre-
- * and post-processing around a call to Proteus. This example uses Resnet50, a
- * classification model, in the Xmodel worker in Proteus. Pre- and
- * post-processing is done in C++ before and after the call to Proteus.
- *
- * @return int
- */
-
-/**
- * @brief user variables: update as needed!
- * If image location is provided, the prediction is done for that image,
- * If not provided, dummy data will be created for functionality testing.
- *
- */
-struct Option {
-  std::string root = std::getenv("PROTEUS_ROOT");
-
-  std::string graph = root + "/external/pytorch_models/resnet50_pretrained.pt";
-  std::string image_location = root + "/tests/assets/dog-3619020_640.jpg";
-  // std::string image_location = "";
-  std::string resize_method = "default";
-  std::string class_file = root + "/examples/python/utils/imagenet_classes.txt";
-
-  int batch_size = 640;
-  int input_size = 224;
-  int output_classes = 1000;
-  int warmup_step = 5;
-  int steps = 1;
-  int topK = 5;
-} options;
-
-/**
- * @brief The example demonstrates how to plug in custom pre-
- * and post-processing around a call to Proteus. Pre- and
- * post-processing is done in C++ before and after the call to Proteus.
- *
- * @return int
- */
-int main() {
-  auto* root_env = std::getenv("PROTEUS_ROOT");
-  if (root_env == nullptr) {
-    std::cerr << "PROTEUS_ROOT is not defined in the environment\n";
-    return 1;
-  }
-  std::string root = root_env;
-
-  // initialize the server
-  proteus::Server server;
-
-  auto client = proteus::NativeClient();
-  auto metadata = client.serverMetadata();
-  if (metadata.extensions.find("ptzendnn") == metadata.extensions.end()) {
-    std::cout << "PTZenDNN support required but not found.\n";
-    exit(0);
-  }
-
-  // load worker with required parameters
-  proteus::RequestParameters parameters;
-  parameters.put("max_buffer_num", options.batch_size);
-  parameters.put("model", options.graph);
-  parameters.put("input_size", options.input_size);
-  parameters.put("output_classes", options.output_classes);
-  parameters.put("batch_size", options.batch_size);
-  auto workerName = client.workerLoad("PtZendnn", &parameters);
-
-  float time_tmp = 0.f;
-  // prepare images for inference
-  if (!options.image_location.empty()) {
-    std::vector<std::string> paths;
-    paths.emplace_back(options.image_location);
-    proteus::util::ImagePreprocessOptions<float, 3> options_;
-    options_.normalize = true;
-    options_.order = proteus::util::ImageOrder::NCHW;
-    options_.mean = {0.485, 0.456, 0.406};
-    options_.std = {4.367, 4.464, 4.444};
-    options_.convert_color = true;
-    options_.color_code = cv::COLOR_BGR2RGB;
-    options_.convert_type = true;
-    options_.type = CV_32FC3;
-    options_.convert_scale = 1.0 / 255.0;
-    auto images = proteus::util::imagePreprocess(paths, options_);
-
-    const std::initializer_list<uint64_t> shape = {
-      3, static_cast<uint64_t>(options.input_size),
-      static_cast<uint64_t>(options.input_size)};
-    std::queue<proteus::InferenceResponseFuture> queue;
-    proteus::InferenceRequest request;
-    request.addInputTensor(static_cast<void*>(images[0].data()), shape,
-                           proteus::DataType::FP32);
-    // Timing the prediction
-    auto start = std::chrono::high_resolution_clock::now();  // Timing the start
-    auto results = client.modelInfer(workerName, request);
-    auto stop = std::chrono::high_resolution_clock::now();
-    auto duration =
-      std::chrono::duration_cast<std::chrono::milliseconds>(stop - start);
-    time_tmp = duration.count();
-
-    auto outputs = results.getOutputs();
-    for (auto& output : outputs) {
-      auto top_k = proteus::util::resnet50Postprocess(
-        static_cast<float*>(output.getData()), output.getSize(), options.topK);
-      std::cout << "Top " << options.topK << " classes:" << std::endl;
-      for (int j = 0; j < options.topK; j++)
-        std::cout << j + 1 << ": Index: " << top_k[j] << " :: Class: "
-                  << proteus::util::readNthLine(options.class_file, top_k[j])
-                  << std::endl;
-    }
-  } else {
-    std::vector<std::vector<float>> images;
-    images.reserve(options.batch_size);
-
-    auto size = options.input_size * options.input_size * 3;
-    for (auto i = 0; i < options.batch_size; i++) {
-      cv::Mat img = cv::Mat(options.input_size, options.input_size, CV_32FC3);
-      cv::randu(img, cv::Scalar::all(0), cv::Scalar::all(1));
-
-      images.emplace_back();
-      auto& image = images[i];
-      image.resize(size);
-      float* arr =
-        img.isContinuous() ? img.ptr<float>(0) : img.clone().ptr<float>(0);
-      image.assign(arr, arr + size);
-    }
-
-    // inference
-    const std::initializer_list<uint64_t> shape = {
-      3, static_cast<uint64_t>(options.input_size),
-      static_cast<uint64_t>(options.input_size)};
-    // Warmup laps to get the best performance
-    for (int step = 0; step < options.warmup_step; step++) {
-      for (auto i = 0; i < options.batch_size; i++) {
-        proteus::InferenceRequest request;
-        request.addInputTensor(static_cast<void*>(images[i].data()), shape,
-                               proteus::DataType::FP32);
-        auto results = client.modelInfer(workerName, request);
-      }
-    }
-
-    // Running for `steps` number of time for proper benchmarking
-    auto start = std::chrono::high_resolution_clock::now();  // Timing the start
-    for (int step = 0; step < options.steps; step++) {
-      std::queue<proteus::InferenceResponseFuture> queue;
-      for (auto i = 0; i < options.batch_size; i++) {
-        proteus::InferenceRequest request;
-        request.addInputTensor(static_cast<void*>(images[i].data()), shape,
-                               proteus::DataType::FP32);
-        auto results = client.modelInfer(workerName, request);
-      }
-    }
-    // Timing the prediction
-    auto stop = std::chrono::high_resolution_clock::now();
-    auto duration =
-      std::chrono::duration_cast<std::chrono::milliseconds>(stop - start);
-    time_tmp = duration.count() / options.steps;
-  }
-
-  if (!options.image_location.empty()) {
-    std::cout << "\nTime taken for " + options.image_location + " : " +
-                   std::to_string(time_tmp)
-              << "ms" << std::endl;
-  } else {
-    std::cout << "\nAverage time taken for " +
-                   std::to_string(options.batch_size) +
-                   " images: " + std::to_string(time_tmp)
-              << std::endl;
-    std::cout << "Batch Size: " + std::to_string(options.batch_size) +
-                   " FPS: " +
-                   std::to_string(options.batch_size / time_tmp * 1000)
-              << std::endl;
-  }
-
-  return 0;
-}
diff --git a/examples/cpp/tf_zendnn_client.cpp b/examples/cpp/tf_zendnn_client.cpp
deleted file mode 100644
index c5474203b..000000000
--- a/examples/cpp/tf_zendnn_client.cpp
+++ /dev/null
@@ -1,209 +0,0 @@
-// Copyright 2022 Advanced Micro Devices, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/**
- * @file
- * @brief Implements a client application for TfZendnn worker
- */
-
-#include <algorithm>              // for copy, max
-#include <chrono>                 // for milliseconds, duration_cast, operator-
-#include <cmath>                  // for exp
-#include <cstdint>                // for uint64_t
-#include <cstdlib>                // for exit, getenv, size_t
-#include <fstream>                // IWYU pragma: keep
-#include <initializer_list>       // for initializer_list
-#include <iostream>               // for operator<<, basic_ostream, endl
-#include <memory>                 // for allocator_traits<>::value_type
-#include <opencv2/core.hpp>       // for Mat, MatSize, operator*, operator-
-#include <opencv2/imgcodecs.hpp>  // for imread
-#include <opencv2/imgproc.hpp>    // for resize, cvtColor, INTER_LINEAR, COL...
-#include <queue>                  // for priority_queue, queue
-#include <string>                 // for string, operator+, allocator, opera...
-#include <unordered_set>          // for operator==, unordered_set, unordere...
-#include <utility>                // for pair
-#include <vector>                 // for vector
-
-#include "proteus/proteus.hpp"  // for InferenceResponseFuture, terminate
-#include "proteus/util/pre_post/image_preprocess.hpp"
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"
-#include "proteus/util/read_nth_line.hpp"
-
-/**
- * @brief user variables: update as needed!
- * If image location is provided, the prediction is done for that image,
- * If not provided, dummy data will be created for functionality testing.
- *
- */
-struct Option {
-  std::string root = std::getenv("PROTEUS_ROOT");
-
-  std::string graph =
-    root + "/external/tensorflow_models/resnet_v1_50_baseline_6.96B_922.pb";
-  std::string image_location = root + "/tests/assets/dog-3619020_640.jpg";
-  // std::string image_location = "";
-  std::string input_node = "input";
-  std::string output_node = "resnet_v1_50/predictions/Reshape_1";
-  std::string resize_method = "crop";
-  std::string class_file = root + "/examples/python/utils/imagenet_classes.txt";
-
-  int batch_size = 64;
-  int input_size = 224;
-  int inter_op = 64;
-  int intra_op = 1;
-  int warmup_step = 5;
-  int steps = 1;
-  int topK = 5;
-} options;
-
-/**
- * @brief The example demonstrates how to plug in custom pre-
- * and post-processing around a call to Proteus. Pre- and
- * post-processing is done in C++ before and after the call to Proteus.
- *
- * @return int
- */
-int main() {
-  auto* root_env = std::getenv("PROTEUS_ROOT");
-  if (root_env == nullptr) {
-    std::cerr << "PROTEUS_ROOT is not defined in the environment\n";
-    return 1;
-  }
-  std::string root = root_env;
-
-  // initialize the server
-  proteus::Server server;
-
-  auto client = proteus::NativeClient();
-  auto metadata = client.serverMetadata();
-  if (metadata.extensions.find("tfzendnn") == metadata.extensions.end()) {
-    std::cout << "TFZenDNN support required but not found.\n";
-    exit(0);
-  }
-
-  // load worker with required parameters
-  proteus::RequestParameters parameters;
-  // parameters.put("max_buffer_num", options.batch_size);
-  parameters.put("model", options.graph);
-  parameters.put("input_node", options.input_node);
-  parameters.put("output_node", options.output_node);
-  parameters.put("input_size", options.input_size);
-  parameters.put("inter_op", options.inter_op);
-  parameters.put("intra_op", options.intra_op);
-  parameters.put("batch_size", options.batch_size);
-  auto workerName = client.workerLoad("TfZendnn", &parameters);
-
-  float time_tmp = 0.f;
-  // prepare images for inference
-  if (!options.image_location.empty()) {
-    std::vector<std::string> paths;
-    paths.emplace_back(options.image_location);
-    proteus::util::ImagePreprocessOptions<float, 3> options_;
-    options_.convert_color = true;
-    options_.color_code = cv::COLOR_BGR2RGB;
-    options_.assign = true;
-    auto images = proteus::util::imagePreprocess(paths, options_);
-
-    const std::initializer_list<uint64_t> shape = {
-      static_cast<uint64_t>(options.input_size),
-      static_cast<uint64_t>(options.input_size), 3};
-    std::queue<proteus::InferenceResponseFuture> queue;
-    proteus::InferenceRequest request;
-    auto start = std::chrono::high_resolution_clock::now();  // Timing the start
-    request.addInputTensor(static_cast<void*>(images[0].data()), shape,
-                           proteus::DataType::FP32);
-    auto results = client.modelInfer(workerName, request);
-    // Timing the prediction
-    auto stop = std::chrono::high_resolution_clock::now();
-    auto duration =
-      std::chrono::duration_cast<std::chrono::milliseconds>(stop - start);
-    time_tmp = duration.count();
-
-    auto outputs = results.getOutputs();
-    for (auto& output : outputs) {
-      auto top_k = proteus::util::resnet50Postprocess(
-        static_cast<float*>(output.getData()), output.getSize(), options.topK);
-      std::cout << "Top " << options.topK << " classes:" << std::endl;
-      for (int j = 0; j < options.topK; j++)
-        std::cout << j + 1 << ": Index: " << top_k[j] << " :: Class: "
-                  << proteus::util::readNthLine(options.class_file, top_k[j])
-                  << std::endl;
-    }
-  } else {
-    std::vector<std::vector<float>> images;
-    images.reserve(options.batch_size);
-
-    auto size = options.input_size * options.input_size * 3;
-    for (auto i = 0; i < options.batch_size; i++) {
-      cv::Mat img = cv::Mat(options.input_size, options.input_size, CV_32FC3);
-      cv::randu(img, cv::Scalar::all(0), cv::Scalar::all(1));
-
-      images.emplace_back();
-      auto& image = images[i];
-      image.resize(size);
-      float* arr =
-        img.isContinuous() ? img.ptr<float>(0) : img.clone().ptr<float>(0);
-      image.assign(arr, arr + size);
-    }
-
-    // inference
-    const std::initializer_list<uint64_t> shape = {
-      static_cast<uint64_t>(options.input_size),
-      static_cast<uint64_t>(options.input_size), 3};
-    // Warmup laps to get the best performance
-    for (int step = 0; step < options.warmup_step; step++) {
-      std::queue<proteus::InferenceResponseFuture> queue;
-      for (auto i = 0; i < options.batch_size; i++) {
-        proteus::InferenceRequest request;
-        request.addInputTensor(static_cast<void*>(images[i].data()), shape,
-                               proteus::DataType::FP32);
-        auto results = client.modelInfer(workerName, request);
-      }
-    }
-
-    // Running for `steps` number of time for proper benchmarking
-    auto start = std::chrono::high_resolution_clock::now();  // Timing the start
-    for (int step = 0; step < options.steps; step++) {
-      std::queue<proteus::InferenceResponseFuture> queue;
-      for (auto i = 0; i < options.batch_size; i++) {
-        proteus::InferenceRequest request;
-        request.addInputTensor(static_cast<void*>(images[i].data()), shape,
-                               proteus::DataType::FP32);
-        auto results = client.modelInfer(workerName, request);
-      }
-    }
-    // Timing the prediction
-    auto stop = std::chrono::high_resolution_clock::now();
-    auto duration =
-      std::chrono::duration_cast<std::chrono::milliseconds>(stop - start);
-    time_tmp = duration.count() / options.steps;
-  }
-
-  if (!options.image_location.empty()) {
-    std::cout << "\nTime taken for " + options.image_location + " : " +
-                   std::to_string(time_tmp)
-              << "ms" << std::endl;
-  } else {
-    std::cout << "\nAverage time taken for " +
-                   std::to_string(options.batch_size) +
-                   " images: " + std::to_string(time_tmp)
-              << std::endl;
-    std::cout << "Batch Size: " + std::to_string(options.batch_size) +
-                   " FPS: " +
-                   std::to_string(options.batch_size / time_tmp * 1000)
-              << std::endl;
-  }
-
-  return 0;
-}
diff --git a/examples/hello_world/README.rst b/examples/hello_world/README.rst
new file mode 100644
index 000000000..8327cda2a
--- /dev/null
+++ b/examples/hello_world/README.rst
@@ -0,0 +1,25 @@
+..
+    Copyright 2022 Advanced Micro Devices, Inc.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+Hello World
+-----------
+
+These examples demonstrate how to perform some basic tasks with the Inference Server APIs.
+
+echo.py
+^^^^^^^
+
+This example describes making a simple inference to the "echo" worker on the Inference Server.
+It shows you how to start the server from Python, construct an HTTP/REST client, make a request, and process the results.
diff --git a/examples/python/hello_world_rest.py b/examples/hello_world/echo.py
similarity index 62%
rename from examples/python/hello_world_rest.py
rename to examples/hello_world/echo.py
index ac8c3137f..87ec68173 100644
--- a/examples/python/hello_world_rest.py
+++ b/examples/hello_world/echo.py
@@ -1,4 +1,5 @@
-# Copyright 2021 Xilinx Inc.
+# Copyright 2021 Xilinx, Inc.
+# Copyright 2022 Advanced Micro Devices, Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -18,37 +19,45 @@
 detailed commentary on this example.
 """
 
-# fmt: off
-# +imports:
+# +imports
 from time import sleep
 
 import numpy as np
 
 import proteus
 
-# -imports:
-# fmt: on
+# -imports
 
 
-def NumericalInferenceRequest(data, datatype=proteus.DataType.UINT32):
-    if not isinstance(data, list):
-        data = [data]
+# +make request
+def make_request(data):
+    """
+    Make a request containing an integer
+
+    Args:
+        data (int): Data to send
+
+    Returns:
+        proteus.InferenceRequest: Request
+    """
     request = proteus.predict_api.InferenceRequest()
-    for index, datum in enumerate(data):
-        input_0 = proteus.predict_api.InferenceRequestInput()
-        input_0.name = f"input{index}"
-        input_0.setUint32Data(np.array([datum], np.uint32))
-        input_0.datatype = datatype
-        input_0.shape = [1]
-        request.addInputTensor(input_0)
+    # each request has one or more input tensors, depending on the worker/model that's going to process it
+    input_0 = proteus.predict_api.InferenceRequestInput()
+    input_0.name = f"input0"
+    input_0.setUint32Data(np.array([data], np.uint32))
+    input_0.datatype = proteus.DataType.UINT32
+    input_0.shape = [1]
+    request.addInputTensor(input_0)
     return request
 
 
-# +main:
+# -make request
+
+
 def main():
-    # +create objects:
+    # +create objects
     client = proteus.clients.HttpClient("http://127.0.0.1:8998")
-    # -create objects:
+    # -create objects
 
     # +start server: if it's not already started, start it from Python
     start_server = not client.serverLive()
@@ -60,33 +69,30 @@ def main():
     # -start server:
 
     # +load worker: load the Echo worker which accepts a number, adds 1, and returns the sum
-    worker_name = client.workerLoad("Echo")
+    endpoint = client.workerLoad("echo")
 
     ready = False
     while not ready:
-        ready = client.modelReady(worker_name)
-    # -load worker:
+        ready = client.modelReady(endpoint)
+    # -load worker
 
     # +inference: construct the request and make the inference
-    data = [3]
-    request = NumericalInferenceRequest(data)
-    response = client.modelInfer(worker_name, request)
-    # -inference:
+    data = 3
+    request = make_request(data)
+    response = client.modelInfer(endpoint, request)
+    # -inference
 
     # +validate: check whether the inference succeeded by checking the response
     assert not response.isError(), response.getError()
     outputs = response.getOutputs()
-    assert len(outputs) == len(data)
-    for index, output in enumerate(outputs):
+    assert len(outputs) == 1
+    for output in outputs:
         recv_data = output.getUint32Data()
         assert len(recv_data) == 1
-        assert recv_data[0] == data[index] + 1
-    # -validate:
-
-    print("hello_world_rest.py: Passed")
-
+        assert recv_data[0] == data + 1
+    # -validate
+    print("")
 
-# -main:
 
 if __name__ == "__main__":
     main()
diff --git a/examples/python/README.rst b/examples/python/README.rst
deleted file mode 100644
index 8e63f3b3e..000000000
--- a/examples/python/README.rst
+++ /dev/null
@@ -1,29 +0,0 @@
-..
-    Copyright 2021 Xilinx Inc.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-        http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-
-Python Examples
-===============
-
-These examples demonstrate using AMD Inference Server with Python.
-
-Custom Processing
------------------
-
-This script shows how to start the server from Python and make and validate an inference request using the Python REST API.
-After starting the server and confirming that it's live, we perform some preprocessing on images before constructing a request.
-An inference request is made to the XModel worker and telling it to run a Resnet50 classification model.
-The response from AMD Inference Server is then post-processed and checked against the expected output.
-
-This example is derived from ``test_xmodel_0()`` in ``tests/python/test_xmodel.py``.
diff --git a/examples/python/custom_processing.py b/examples/python/custom_processing.py
deleted file mode 100644
index 860d8e071..000000000
--- a/examples/python/custom_processing.py
+++ /dev/null
@@ -1,151 +0,0 @@
-# Copyright 2021 Xilinx Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""
-This example shows how to use the Python API to start the server, make a request
-to it over REST and parse the response. In contrast to the simpler Hello World
-example, we make an inference to a custom XModel and show how to pre- and post-
-process the results. Look at the documentation for more detailed commentary on
-this example.
-"""
-
-import math
-import os
-import sys
-from time import sleep
-
-import cv2
-import numpy as np
-
-import proteus
-import proteus.clients
-import proteus.servers
-
-
-def preprocess(images):
-    images_preprocessed = []
-
-    data_type = "uint8"
-    height = 224
-    width = 224
-    channels = 3
-    layout = "NHWC"
-    fix_scale = 1
-    mean = [123, 107, 104]
-
-    for image in images:
-        image = image.astype(getattr(np, data_type))
-        image = cv2.resize(image, (height, width))
-        new_image = np.zeros(image.shape, dtype=np.int8)
-        if layout == "NCHW":  # image defaults to HWC
-            image = np.rollaxis(image, axis=2, start=0)  # reshape to CHW
-            for c in range(channels):
-                for h in range(height):
-                    for w in range(width):
-                        new_image[c][h][w] = int((image[c][h][w] - mean[c]) * fix_scale)
-
-        else:
-            for h in range(height):
-                for w in range(width):
-                    for c in range(channels):
-                        new_image[h][w][c] = int((image[h][w][c] - mean[c]) * fix_scale)
-        images_preprocessed.append(new_image)
-
-    return images_preprocessed
-
-
-def postprocess(data, k):
-    max_val = max(data)
-    softmax = [0] * len(data)
-
-    data_sum = 0
-    for i, val in enumerate(data):
-        softmax[i] = math.exp(val - max_val)
-        data_sum += softmax[i]
-
-    for i, val in enumerate(data):
-        softmax[i] /= data_sum
-
-    # get the top classifications
-    tmp = [i[0] for i in sorted(enumerate(softmax), reverse=True, key=lambda x: x[1])]
-
-    # only return the top k
-    return tmp[:k]
-
-
-def main():
-    root = os.getenv("PROTEUS_ROOT")
-    assert root is not None
-
-    # +user variables: update as needed!
-    batch_size = 4
-    path_to_xmodel = "${PROTEUS_ROOT}/external/artifacts/u200_u250/resnet_v1_50_tf/resnet_v1_50_tf.xmodel"
-    path_to_image = root + "/tests/assets/dog-3619020_640.jpg"
-    # for this image, we know what we expect to receive with this XModel
-    gold_response_output = [259, 261, 260, 157, 154]
-    # -user variables
-
-    client = proteus.clients.HttpClient("http://127.0.0.1:8998")
-
-    start_server = not client.serverLive()
-    if start_server:
-        server = proteus.servers.Server()
-        server.startHttp(8998)
-        while not client.serverLive():
-            sleep(1)
-
-    metadata = client.serverMetadata()
-
-    if "vitis" not in metadata.extensions:
-        print("Vitis AI support required but not found.")
-        sys.exit(0)
-
-    # +load worker:
-    parameters = proteus.RequestParameters()
-    parameters.put("model", path_to_xmodel)
-    worker_name = client.workerLoad("Xmodel", parameters)
-
-    ready = False
-    while not ready:
-        ready = client.modelReady(worker_name)
-    # -load worker:
-
-    # +get images:
-    images = []
-    for _ in range(1):
-        image = cv2.imread(path_to_image)
-        images.append(image)
-    # -get images:
-
-    # +inference:
-    images = preprocess(images)
-    # Construct the request and send it
-    for _ in range(batch_size):
-        request = proteus.ImageInferenceRequest(images, True)
-        response = client.modelInfer(worker_name, request)
-        assert not response.isError(), response.getError()
-        outputs = response.getOutputs()
-        for output in outputs:
-            assert output.datatype == proteus.DataType.INT8
-            recv_data = output.getInt8Data()
-            # Can optionally post-process the result
-            k = postprocess(recv_data, 5)
-            assert k == gold_response_output
-    # -inference:
-
-    print("custom_processing.py: Passed")
-
-
-if __name__ == "__main__":
-    main()
diff --git a/examples/python/hello_migraphx.py b/examples/python/hello_migraphx.py
deleted file mode 100644
index 01d4143ef..000000000
--- a/examples/python/hello_migraphx.py
+++ /dev/null
@@ -1,318 +0,0 @@
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""
-This example contains Python commands necessary to bring up
-the migraphx worker and run a Resnet50 classification model on some images.
-Then, it calls the migraphx library directly with the same model, and compares results.
-The model file in external/artifacts is not in the git repo. and must be fetched with git or with
-    proteus get
-
-https://github.com/onnx/models/blob/main/vision/classification/resnet/model/resnet50-v2-7.onnx
-https://github.com/onnx/models/blob/main/vision/classification/resnet/model/resnet50-v2-7.tar.gz
-"""
-
-
-import argparse
-import json  # json for reading labels file
-import os
-import sys
-import time
-
-import cv2
-import numpy as np
-
-import proteus
-import proteus.clients
-
-# The make_nxn and preprocess functions are based on an migraphx example at
-# AMDMIGraphx/examples/vision/python_resnet50/resnet50_inference.ipynb
-# The mean and standard dev. values used for this normalization are
-# requirements of the Resnet50 model.
-
-
-def make_nxn(image, n):
-    """
-    Crop an image to square and then resize to desired dimension n x n
-
-    Args:
-        image (np.array): image to crop
-        n (int): desired image size
-    """
-    width = image.shape[1]
-    height = image.shape[0]
-    if height > width:
-        dif = height - width
-        cr = dif // 2
-        square = image[(cr + (dif % 2)) : (height - cr), :]
-        return cv2.resize(square, (n, n))
-    elif width > height:
-        dif = width - height
-        cr = dif // 2
-        square = image[:, (cr + (dif % 2)) : (width - cr)]
-        return cv2.resize(square, (n, n))
-    else:
-        return cv2.resize(image, (n, n))
-
-
-def preprocess(img_data):
-    """
-    Normalize array values to the data type, mean and std. dev. required by Resnet50
-
-    Args:
-        img_data (np.array): 3 dimensions [channels, rows, cols] with value range 0-255
-        the vectors are for RGB images, so images read with OpenCV must have channels
-        converted before calling.
-    """
-    mean_vec = np.array([0.406, 0.456, 0.485])
-    stddev_vec = np.array([0.225, 0.224, 0.229])
-    norm_img_data = np.zeros(img_data.shape).astype("float32")
-    for i in range(img_data.shape[0]):
-        norm_img_data[i, :, :] = (img_data[i, :, :] / 255 - mean_vec[i]) / stddev_vec[i]
-    return norm_img_data
-
-
-def run_migraphx(model_name, img, labels):
-    """
-    Do a migraphx call directly for one image; compare results
-
-    Args:
-        model_name (str): path to the model
-        img (np.array): image to send
-        labels (dict): label strings for resnet
-    """
-    print("Beginning comparison run-->parsing the model for migraphx...")
-    model = migraphx.parse_onnx(model_name)
-    model.compile(migraphx.get_target("gpu"))
-    print("Ok.")
-
-    # img and img2 are already the correct shape for the Inf Server, but need another dimension for Python API migraphx
-    test_img = img
-    # add a 4th tensor dimension in first position, expected by migraphx
-    test_img = np.expand_dims(test_img, 0)
-
-    # Run the inference
-    results = model.run({"data": test_img})
-    # Extract the index of the top prediction
-    res_npa = np.array(results[0])  # shape of res_npa is (1, 1000)
-    max_index = np.argmax(res_npa)
-    print(
-        "category reported by migraphx is ",
-        max_index,
-        ".  Match value ",
-        res_npa[0][max_index],
-        "  This is a picture of a ",
-        labels[max_index],
-    )
-
-
-def parse_args():
-    """
-    Read command line arguments
-    """
-    root = os.getenv("PROTEUS_ROOT")
-    assert root is not None
-
-    # Get the arguments required from the user
-    parser = argparse.ArgumentParser(
-        description="Example client Proteus Migraphx worker"
-    )
-
-    parser.add_argument(
-        "--modelfile",
-        "-m",
-        type=str,
-        required=False,
-        default=os.path.join(
-            root, "external/artifacts/onnx/resnet50v2/resnet50-v2-7.onnx"
-        ),
-        help="Location of model file on server",
-    )
-
-    parser.add_argument(
-        "--labels",
-        "-l",
-        type=str,
-        required=False,
-        default=os.path.join(root, "tests/assets/imagenet_simple_labels.json"),
-        help="The file containing label names for the model's categories",
-    )
-
-    parser.add_argument(
-        "--image",
-        "-i",
-        type=str,
-        required=False,
-        default=os.path.join(root, "tests/assets/dog-3619020_640.jpg"),  # a dog
-        help="An image to try inference on.  Use git-lfs to pull image assets",
-    )
-
-    parser.add_argument(
-        "--image2",
-        "-g",
-        type=str,
-        required=False,
-        default=os.path.join(
-            root, "tests/assets/bicycle-384566_640.jpg"
-        ),  # a bicyclist
-        help="A second image to try inference on",
-    )
-
-    # Parse arguments
-    return parser.parse_args()
-
-
-def main(args):
-
-    modelname = args.modelfile
-    imagename = args.image
-    imagename2 = args.image2
-    labels_file = args.labels
-
-    client = proteus.clients.HttpClient("http://127.0.0.1:8998")
-    print("waiting for server...", end="")
-    start_server = not client.serverLive()
-    if start_server:
-        server = proteus.servers.Server()
-        server.startHttp(8998)
-        while not client.serverLive():
-            time.sleep(1)
-    print("ok.")
-
-    metadata = client.serverMetadata()
-
-    if not "migraphx" in metadata.extensions:
-        print("MIGraphX support required but not found.")
-        sys.exit(0)
-
-    #  load the onnx model to find the input shape, see https://stackoverflow.com/questions/56734576/find-input-shape-from-onnx-file
-    #  We assume here that client is on the same file system as the server
-    #  This code is applicable to any onnx model, but for resnet50 the required shape could have been hardcoded:   [1, 3, 224, 224]
-    shape = []
-    import onnx
-
-    model = onnx.load(modelname)
-    for input in model.graph.input:
-        if input.name == "data":
-            print(input.name, end=": ")
-            # get type of input tensor
-            tensor_type = input.type.tensor_type
-            # check if it has a shape:
-            if tensor_type.HasField("shape"):
-                # iterate through dimensions of the shape:
-                for d in tensor_type.shape.dim:
-                    # the dimension may have a definite (integer) value or a symbolic identifier or neither:
-                    if d.HasField("dim_value"):
-                        shape.append(d.dim_value)
-            else:
-                print("unknown rank", end="")
-            print()
-            break
-
-    print("This model's shape of input image is ", shape)
-    # If only 3 dimensions were found, assume the 0'th dimension was not parsed and insert a 1.
-    if len(shape) == 3:
-        shape.insert(0, 1)
-    if len(shape) != 4:
-        print(
-            "Unable to read the image dimensions from ",
-            modelname,
-            ".  Expecting a 4-value shape tensor.",
-        )
-        exit(-1)
-
-    # Load a picture
-    img = cv2.imread(imagename).astype("float32")
-    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
-    # Crop to a square, resize
-    img = make_nxn(img, shape[2])
-    #  Normalize values with values specific to Resnet50
-    img = img.transpose(2, 0, 1)
-    img = preprocess(img)
-    # expected dimensions at this point are (3, 224, 224).  Note that the Resnet model expects
-    # the channel dimension (3) to come before the rows and columns, but OpenCV places channels
-    # in the last dimension.
-
-    # +load worker.  The only parameter the migraphx worker requires is the model file name.
-    # It will take the file name stem and search for either a *.onnx or *.mxr extension, and if
-    # it finds a *.onnx file it will compile it and save the compiled model as *.mxr for
-    # future use.  It will read
-    # the array dimensions and data type from the model.
-
-    parameters = proteus.RequestParameters()
-    parameters.put("model", modelname)
-    parameters.put("batch", 2)
-    parameters.put("timeout", 1000)
-    # this call requests the server to either find a running instance of the named
-    # worker type, or else create one and initialize it with the parameters.
-    worker_name = client.workerLoad("Migraphx", parameters)
-
-    # wait for the worker to load and compile model
-    ready = False
-    while not ready:
-        ready = client.modelReady(worker_name)
-
-    # Load a picture
-    img2 = cv2.imread(imagename2).astype("float32")
-    img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)
-    img2 = make_nxn(img2, shape[2])
-    #  Normalize values with values specific to Resnet50
-    img2 = img2.transpose(2, 0, 1)
-    img2 = preprocess(img2)
-
-    #
-    # create multiple image inference requests and send them all together
-    #
-    images = [img2, img2]
-
-    print("Creating inference request set...")
-    images = [proteus.ImageInferenceRequest(image) for image in images]
-    responses = proteus.client_operators.inferAsyncOrdered(client, worker_name, images)
-    for response in responses:
-        assert not response.isError(), response.getError()
-
-    print("Client received inference reply.")
-
-    # load the labels
-    with open(labels_file, "r") as json_data:
-        labels = json.load(json_data)
-
-    for response in responses:
-        for output in response.getOutputs():
-            assert output.datatype == proteus.DataType.FP32
-            recv_data = output.getFp32Data()
-            # the predicted category is the one with the highest match value
-            answer = np.argmax(recv_data)
-            print(
-                "client's analysis of result: best match category is ",
-                answer,
-                "  match value is ",
-                recv_data[answer],
-                ".  This is a picture of a ",
-                labels[answer],
-            )
-
-    # optional: make the same request to the migraphx API for comparison
-    # run_migraphx(modelname, img2, labels)
-
-    print("Done")
-
-    # If this line is commented out, worker persists with doRun thread active, and the entire script
-    # can be run again without any reloading taking place
-    # client.unload('Migraphx')
-
-
-if __name__ == "__main__":
-    args = parse_args()
-    main(args)
diff --git a/examples/python/migraphx_bert.py b/examples/python/migraphx_bert.py
deleted file mode 100644
index 64cd4be89..000000000
--- a/examples/python/migraphx_bert.py
+++ /dev/null
@@ -1,259 +0,0 @@
-# MIT License
-#
-# Copyright (c) 2015-2022 Advanced Micro Devices, Inc. All rights reserved.
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-"""
-Bert model client
-
-This example contains Python commands necessary to bring up
-the migraphx worker and run a Bert language processing model.
-
-"""
-
-
-import os
-import os.path
-
-# The following packages aren't automatically installed in the dockerfile:
-try:
-    import tokenizers
-except ImportError:
-    os.system("pip3 install tokenizers==0.12.1")
-    import tokenizers
-
-try:
-    import onnxruntime
-except ImportError:
-    os.system("pip3 install onnxruntime")
-    import onnxruntime
-
-# from _proteus import *
-import collections
-import json
-import sys
-import time
-
-import numpy as np
-
-import proteus
-
-base_dir = "external/artifacts/onnx/bert"
-sys.path.append(base_dir)
-from run_onnx_squad import (
-    convert_examples_to_features,
-    read_squad_examples,
-    write_predictions,
-)
-
-# import migraphx
-
-
-RawResult = collections.namedtuple(
-    "RawResult", ["unique_id", "start_logits", "end_logits"]
-)
-
-#######################################
-input_file = os.path.join(base_dir, "inputs_amd.json")
-with open(input_file) as json_file:
-    test_data = json.load(json_file)
-    # print(json.dumps(test_data, indent=2))
-
-# preprocess input
-predict_file = os.path.join(base_dir, "inputs_amd.json")
-
-# Use read_squad_examples method from run_onnx_squad to read the input file
-eval_examples = read_squad_examples(input_file=predict_file)
-
-max_seq_length = 256
-doc_stride = 128
-max_query_length = 64
-batch_size = 13
-n_best_size = 20
-max_answer_length = 30
-
-vocab_file = os.path.join(base_dir, "vocab.txt")
-tokenizer = tokenizers.BertWordPieceTokenizer(vocab_file)
-
-# Use convert_examples_to_features method from run_onnx_squad to get parameters from the input
-# the first 3 are lists of ndarray, extra_data is a list of Feature (a namedtuple)
-input_ids, input_mask, segment_ids, extra_data = convert_examples_to_features(
-    eval_examples, tokenizer, max_seq_length, doc_stride, max_query_length
-)
-# extra_data[0] is a Feature
-
-#############################################################
-#
-#   Make Inference Server connection
-#
-#############################################################
-client = proteus.clients.HttpClient("http://127.0.0.1:8998")
-print("waiting for server...", end="")
-start_server = not client.serverLive()
-if start_server:
-    server = proteus.servers.Server()
-    server.startHttp(8998)
-    while not client.serverLive():
-        time.sleep(1)
-print("ok.  Connected.")
-
-metadata = client.serverMetadata()
-
-if not "migraphx" in metadata.extensions:
-    print("MIGraphX support required but not found.")
-    sys.exit(0)
-
-#############################################################
-#
-#   Load the model
-#
-#############################################################
-# +load worker.  The only parameter the migraphx worker requires is the model file name.
-# batch and timeout are optional.
-# It will take the file name stem and search for either a *.onnx or *.mxr extension, and if
-# it finds a *.onnx file it will compile it and save the compiled model as *.mxr for
-# future use.  It will read
-# the array dimensions and data type from the model.
-modelname = os.path.join(base_dir, "bertsquad-10.onnx")
-parameters = proteus.RequestParameters()
-parameters.put("model", modelname)
-
-parameters.put("batch", batch_size)
-
-# this call requests the server to either find a running instance of the named
-# worker type, or else create one and initialize it with the parameters.
-print("Requesting worker load with model ", modelname, "...")
-worker_name = client.workerLoad("Migraphx", parameters)
-print("ok.  Loaded worker ", worker_name)
-
-# wait for the worker to load and compile model
-ready = False
-while not ready:
-    ready = client.modelReady(worker_name)
-
-print("Model loaded.")
-
-n = len(input_ids)
-bs = batch_size
-all_results = []
-
-# input_n.datatype = getattr(DataType, str(image.dtype).upper())
-# input_n.shape = [*image.shape]  # Convert tuple to list
-# _set_data(input_n, image.flatten())
-
-requests = []
-
-for idx in range(0, n):
-    # Create an InferenceRequest
-    request = proteus.predict_api.InferenceRequest()
-    item = eval_examples[idx]  # class SquadExample
-
-    # add items to inference request
-
-    input_n = proteus.predict_api.InferenceRequestInput()
-    input_n.name = f"input_ids:0"
-    input_n.datatype = proteus.DataType.INT64
-    input_n.shape = (
-        1,
-        256,
-    )
-    input_n.setInt64Data(input_ids[idx : idx + bs])
-    request.addInputTensor(input_n)
-
-    input_n = proteus.predict_api.InferenceRequestInput()
-    input_n.name = f"input_mask:0"
-    input_n.datatype = proteus.DataType.INT64
-    input_n.shape = (
-        1,
-        256,
-    )
-    input_n.setInt64Data(input_mask[idx : idx + bs])
-    request.addInputTensor(input_n)
-
-    input_n = proteus.predict_api.InferenceRequestInput()
-    input_n.name = f"segment_ids:0"
-    input_n.datatype = proteus.DataType.INT64
-    input_n.shape = (
-        1,
-        256,
-    )
-    input_n.setInt64Data(segment_ids[idx : idx + bs])
-    request.addInputTensor(input_n)
-
-    # This comes from the first argument; I think it's supposed to be output names
-    #       see https://onnxruntime.ai/docs/api/python/api_summary.html#load-and-run-a-model
-    # item = eval_examples[idx]   # class SquadExample
-    input_n = proteus.predict_api.InferenceRequestInput()
-    input_n.name = f"unique_ids_raw_output___9:0"
-    input_n.datatype = proteus.DataType.INT64
-    input_n.shape = (1,)
-    input_n.setInt64Data(np.array([item.qas_id], dtype=np.int64))
-    request.addInputTensor(input_n)
-    requests.append(request)
-
-print("request batch is ready, size ", len(requests), ".  Sending...")
-responses = proteus.client_operators.inferAsyncOrdered(client, worker_name, requests)
-print("responses received. ")
-
-#
-# Parse and display results
-#
-for it, response in enumerate(responses):
-    # instead of reading the unique id from output, use 0, 1, ...
-    unique_id = len(all_results)
-
-    # the migraphx worker doesn't support names for output channels,
-    # so we get them by position instead.
-    out0 = response.getOutputs()[0]
-    assert out0.datatype == proteus.DataType.FP32
-
-    out1 = response.getOutputs()[1]
-    assert out1.datatype == proteus.DataType.FP32
-
-    # The third output ("unique_ids:0") is a single int64 which our example doesn't use.
-    # Put our results into a RawResult structure
-    all_results.append(
-        RawResult(
-            unique_id=unique_id,
-            start_logits=out1.getFp32Data(),
-            end_logits=out0.getFp32Data(),
-        )
-    )
-
-
-output_dir = os.path.join(base_dir, "predictions")
-os.makedirs(output_dir, exist_ok=True)
-output_prediction_file = os.path.join(output_dir, "predictions.json")
-output_nbest_file = os.path.join(output_dir, "nbest_predictions.json")
-write_predictions(
-    eval_examples,
-    extra_data,
-    all_results,
-    n_best_size,
-    max_answer_length,
-    True,
-    output_prediction_file,
-    output_nbest_file,
-)
-
-with open(output_prediction_file, "r") as json_file:
-    test_data = json.load(json_file)
-    print(json.dumps(test_data, indent=2))
-print("Done! Your output file is ", output_prediction_file)
diff --git a/examples/python/migraphx_resnet.py b/examples/python/migraphx_resnet.py
deleted file mode 100644
index 655559465..000000000
--- a/examples/python/migraphx_resnet.py
+++ /dev/null
@@ -1,343 +0,0 @@
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
-"""
-This example brings up the migraphx worker and runs a Resnet50 classification model
-on the official imagenet validation set (50,000 images).
-
-The model file and test data is not in the git repo. and must be fetched with git or
-    proteus get or wget
-
-Model source:
-https://github.com/onnx/models/blob/main/vision/classification/resnet/model/resnet50-v2-7.onnx
-https://github.com/onnx/models/blob/main/vision/classification/resnet/model/resnet50-v2-7.tar.gz
-
-Source of ground truth labels for validation set:
-    ILSVRC2012_validation_ground_truth.txt
-    https://github.com/mvermeulen/rocm-migraphx/tree/master/datasets/imagenet/val.txt
-"""
-
-
-import argparse
-import json  # json for reading labels file
-import os
-import sys
-import time
-
-import cv2
-import numpy as np
-import onnx
-
-import proteus
-import proteus.clients
-
-# The make_nxn and preprocess functions are based on an migraphx example at
-# AMDMIGraphx/examples/vision/python_resnet50/resnet50_inference.ipynb
-# The mean and standard dev. values used for this normalization are
-# requirements of the Resnet50 model.
-
-
-def make_nxn(image, n):
-    """
-    Crop an image to square and then resize to desired dimension n x n
-
-    Args:
-        image (np.array): image to crop
-        n (int): desired image size
-    """
-    width = image.shape[1]
-    height = image.shape[0]
-    if height > width:
-        dif = height - width
-        bar = dif // 2
-        square = image[(bar + (dif % 2)) : (height - bar), :]
-        return cv2.resize(square, (n, n))
-    elif width > height:
-        dif = width - height
-        bar = dif // 2
-        square = image[:, (bar + (dif % 2)) : (width - bar)]
-        return cv2.resize(square, (n, n))
-    else:
-        return cv2.resize(image, (n, n))
-
-
-def preprocess(img_data):
-    """
-    Normalize array values to the data type, mean and std. dev. required by Resnet50
-
-    Args:
-        img_data (np.array): 3 dimensions [channels, rows, cols] with value range 0-255
-        the vectors are for RGB images, so images read with OpenCV must have channels
-        converted before calling.
-    """
-    mean_vec = np.array([0.406, 0.456, 0.485])
-    stddev_vec = np.array([0.225, 0.224, 0.229])
-    norm_img_data = np.zeros(img_data.shape).astype("float32")
-    for i in range(img_data.shape[0]):
-        norm_img_data[i, :, :] = (img_data[i, :, :] / 255 - mean_vec[i]) / stddev_vec[i]
-    return norm_img_data
-
-
-def parse_args():
-    """
-    Read command line arguments
-    """
-
-    root = os.getenv("PROTEUS_ROOT")
-    assert root is not None
-
-    # Get the arguments required from the user
-    parser = argparse.ArgumentParser(
-        description="Validation (working) for Proteus Migraphx worker"
-    )
-    parser.add_argument(
-        "--request_size",
-        "-r",
-        type=int,
-        required=False,
-        help="Number of images per REST request",
-        default=1,
-    )
-
-    parser.add_argument(
-        "--batch_size",
-        "-b",
-        type=int,
-        required=False,
-        default=64,
-        help="Batch size for migraphx evaluation. Default is 64. ",
-    )
-    parser.add_argument(
-        "--modelfile",
-        "-m",
-        type=str,
-        required=False,
-        default=os.path.join(
-            root, "external/artifacts/onnx/resnet50v2/resnet50-v2-7.onnx"
-        ),
-        help="Location of model file on server",
-    )
-    parser.add_argument(
-        "--validation_dir",
-        "-v",
-        type=str,
-        required=False,
-        default=os.path.join(root, "external/artifacts/onnx/ILSVRC2012_img_val"),
-        help="The directory containing validation images, such as the Imagenet validation set if available",
-    )
-    parser.add_argument(
-        "--groundtruth",
-        "-g",
-        type=str,
-        required=False,
-        default="val.txt",
-        help="The list of correct category results (ground truth) for the validation set; file must be in the validation directory",
-    )
-
-    parser.add_argument(
-        "--labels",
-        "-l",
-        type=str,
-        required=False,
-        default=os.path.join(root, "tests/assets/imagenet_simple_labels.json"),
-        help="The file containing label names for the model's categories",
-    )
-
-    return parser.parse_args()
-
-
-def main(pargs):
-
-    batch_size = pargs.batch_size
-    request_size = pargs.request_size
-    modelname = pargs.modelfile
-    validation_dir = pargs.validation_dir
-    validation_answers_file = os.path.join(validation_dir, pargs.groundtruth)
-    labels_file = pargs.labels
-
-    client = proteus.clients.HttpClient("http://127.0.0.1:8998")
-    print("waiting for server...", end="")
-    start_server = not client.serverLive()
-    if start_server:
-        server = proteus.servers.Server()
-        server.startHttp(8998)
-        while not client.serverLive():
-            time.sleep(1)
-    print("ok.")
-
-    metadata = client.serverMetadata()
-
-    if not "migraphx" in metadata.extensions:
-        print("MIGraphX support required but not found.")
-        sys.exit(0)
-
-    if not os.path.exists(validation_dir):
-        print(f"{validation_dir} does not exist, skipping migraphx validation test")
-        # exiting with zero to prevent failing automated tests
-        sys.exit(0)
-
-    #  load the onnx model to find the input shape, see https://stackoverflow.com/questions/56734576/find-input-shape-from-onnx-file
-    #  We assume here that client is on the same file system as the server
-    #  This code is applicable to any onnx model, but for resnet50 the required shape could have been hardcoded:   [1, 3, 224, 224]
-    shape = []
-
-    print("Loading model to verify data shape...")
-    model = onnx.load(modelname)
-    for input in model.graph.input:
-        if input.name == "data":
-            print(input.name, end=": ")
-            # get type of input tensor
-            tensor_type = input.type.tensor_type
-            # check if it has a shape:
-            if tensor_type.HasField("shape"):
-                # iterate through dimensions of the shape:
-                for d in tensor_type.shape.dim:
-                    # the dimension may have a definite (integer) value or a symbolic identifier or neither:
-                    if d.HasField("dim_value"):
-                        shape.append(d.dim_value)
-            else:
-                print("unknown rank", end="")
-            print()
-            break
-
-    print("This model's shape of input image is ", shape)
-    # If only 3 dimensions were found, assume the 0'th dimension was not parsed and insert a 1.
-    if len(shape) == 3:
-        shape.insert(0, 1)
-    if len(shape) != 4:
-        print(
-            "Unable to read the image dimensions from ",
-            modelname,
-            ".  Expecting a 3- or 4-value shape tensor.",
-        )
-        sys.exit(-1)
-
-    # load worker.  The only parameter the migraphx worker requires is the model file name.
-    # It will take the file name stem and search for either a *.onnx or *.mxr extension, and if
-    # it finds a *.onnx file it will compile it and save the compiled model as *.mxr for
-    # future use.  It will read
-    # the array dimensions and data type from the model.
-
-    parameters = proteus.RequestParameters()
-    parameters.put("model", modelname)
-    parameters.put("batch", batch_size)
-    parameters.put("timeout", 100)  # ms
-    # this call requests the server to either find a running instance of the named
-    # worker type, or else create one and initialize it with the parameters.
-    print("loading worker Migraphx with model file ", modelname)
-    worker_name = client.workerLoad("Migraphx", parameters)
-
-    # load the labels
-    with open(labels_file, "r") as json_data:
-        labels = json.load(json_data)
-
-    # wait for the worker to load and compile model
-    ready = False
-    while not ready:
-        try:
-            ready = client.modelReady(worker_name)
-        except ValueError:
-            pass
-
-    # list all the *.jpg images in the validation image directory
-    files = os.listdir(validation_dir)
-    files.sort()
-
-    if pargs.groundtruth in files:
-        files.remove(pargs.groundtruth)
-
-    # Read the "answers" file
-    ground_truth = [None] * len(files)
-    i = 1
-    with open(validation_answers_file, "r") as labelfile:
-        while i < len(ground_truth):
-            ground_truth[i] = labelfile.readline().split()[1]
-            i = i + 1
-
-    #
-    #   Loop thru the image set, reading images and putting together inference requests in batches.
-    #   Then compare the results with ground truth
-    #
-    correct_answers = 0
-    wrong_answers = 0
-
-    images = [None] * request_size
-    index = 0
-    for file in files:
-        filename = os.path.join(validation_dir, file)
-        if os.path.isfile(filename) and filename.find(".JPEG") != -1:
-            # Load a picture
-            imgV = cv2.imread(filename).astype("float32")
-            imgV = cv2.cvtColor(imgV, cv2.COLOR_BGR2RGB)
-            imgV = make_nxn(imgV, shape[2])
-            #  Normalize values with values specific to Resnet50
-            imgV = imgV.transpose(2, 0, 1)
-            imgV = preprocess(imgV)
-            images[index % request_size] = imgV
-            index = index + 1
-            print("file ", file, "    Processed so far: ", index, " of ", len(files))
-
-            if index % request_size == 0:
-                imageReqs = [proteus.ImageInferenceRequest(image) for image in images]
-                print("Request is ready.  Sending ", len(images), " requests")
-                j = 0
-                responses = proteus.client_operators.inferAsyncOrdered(
-                    client, worker_name, imageReqs
-                )
-                for response in responses:
-                    assert not response.isError(), response.getError()
-
-                    print("Client received inference reply.")
-                    for output in response.getOutputs():
-                        assert output.datatype == proteus.DataType.FP32
-                        recv_data = output.getFp32Data()
-                        # the predicted category is the one with the highest match value
-                        answer = np.argmax(recv_data)
-                        step_index = index - request_size + j + 1
-                        print(
-                            "Reading result: best match category is ",
-                            answer,
-                            "  value is ",
-                            recv_data[answer],
-                            ".  This is a picture of a ",
-                            labels[answer],
-                            "    ground truth: ",
-                            ground_truth[step_index],
-                        )
-                        if int(answer) == int(ground_truth[step_index]):
-                            correct_answers = correct_answers + 1.0
-                        else:
-                            wrong_answers = wrong_answers + 1.0
-                    j = j + 1
-                print(
-                    "Correct: ",
-                    correct_answers,
-                    "  Wrong: ",
-                    wrong_answers,
-                    "    Accuracy: ",
-                    correct_answers / (correct_answers + wrong_answers),
-                )
-                print("     ----------------------------------------")
-
-    print("Done")
-
-    # If this line is commented out, worker persists with doRun thread active, and the entire script
-    # can be run again without any reloading taking place
-    # client.unload('Migraphx')
-
-
-if __name__ == "__main__":
-    args = parse_args()
-    main(args)
diff --git a/examples/python/migraphx_yolo.py b/examples/python/migraphx_yolo.py
deleted file mode 100644
index 8715610ae..000000000
--- a/examples/python/migraphx_yolo.py
+++ /dev/null
@@ -1,254 +0,0 @@
-# MIT License
-#
-# Copyright (c) 2022 Advanced Micro Devices, Inc.
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-
-"""
-This example contains Python commands necessary to bring up
-the migraphx worker and run a Yolo classification model on some images.
-See  https://github.com/ROCmSoftwarePlatform/AMDMIGraphX/blob/develop/examples/vision/python_yolov4/yolov4_inference.ipynb
-for details.
-
-
-Outputs:  see https://github.com/onnx/models/tree/main/vision/object_detection_segmentation/yolov4 README
-
-Output shapes: (1, 52, 52, 3, 85) (1, 26, 26, 3, 85) (1, 13, 13, 3, 85)
-
-There are 3 output layers. For each layer, there are 255 outputs: 85 values per
-anchor, times 3 anchors.
-
-The 85 values of each anchor consists of 4 box coordinates describing the predicted
-bounding box (x, y, h, w), 1 object confidence, and 80 class confidences.
-
-"""
-# import migraphx    # redundant with proteus
-import os
-
-# The following packages aren't automatically installed in the docker image:
-try:
-    import scipy
-except ImportError:
-    os.system("pip3 install scipy")
-    import scipy
-
-try:
-    from PIL import Image
-except ImportError:
-    os.system("pip3 install Pillow")
-    from PIL import Image
-
-try:
-    import onnxruntime
-except ImportError:
-    os.system("pip3 install onnxruntime")
-    import onnxruntime
-
-import argparse
-import sys
-import time
-
-import cv2
-
-sys.path.append("/workspace/proteus/examples/python/utils")
-import numpy as np
-import onnxruntime as rt
-import yolo_image_processing as ip
-
-import proteus
-import proteus.clients
-
-
-def read_class_names(class_file_name):
-    """loads class name from a file"""
-    names = {}
-    with open(class_file_name, "r") as data:
-        for ID, name in enumerate(data):
-            names[ID] = name.strip("\n")
-    return names
-
-
-def parse_args():
-    """
-    Read command line arguments
-    """
-    root = os.getenv("PROTEUS_ROOT")
-    assert root is not None
-
-    # Get the arguments required from the user
-    parser = argparse.ArgumentParser(
-        description="Example client Proteus Migraphx worker"
-    )
-
-    parser.add_argument(
-        "--modelfile",
-        "-m",
-        type=str,
-        required=False,
-        default=os.path.join(root, "external/artifacts/onnx/yolov4/yolov4.onnx"),
-        help="Location of model file on server",
-    )
-
-    parser.add_argument(
-        "--labels",
-        "-l",
-        type=str,
-        required=False,
-        default=os.path.join(root, "external/artifacts/onnx/yolov4/coco.names"),
-        help="The file containing label names for the model's categories",
-    )
-
-    parser.add_argument(
-        "--image",
-        "-i",
-        type=str,
-        required=False,
-        default=os.path.join(
-            root, "tests/assets/crowd.jpg"
-        ),  # an outdoor crowd scene from the COCO dataset (https://cocodataset.org/#explore)
-        help="An image to try inference on.  Use git-lfs to pull image assets",
-    )
-
-    parser.add_argument(
-        "--image2",
-        "-g",
-        type=str,
-        required=False,
-        default=os.path.join(
-            root, "tests/assets/bicycle-384566_640.jpg"
-        ),  # a bicyclist.  The model can't identify this
-        help="A second image to try inference on",
-    )
-
-    # Parse arguments
-    return parser.parse_args()
-
-
-def main(pargs):
-    modelname = pargs.modelfile
-    imagename = pargs.image
-
-    client = proteus.clients.HttpClient("http://127.0.0.1:8998")
-    print("waiting for server...", end="")
-    start_server = not client.serverLive()
-    if start_server:
-        server = proteus.servers.Server()
-        server.startHttp(8998)
-        while not client.serverLive():
-            time.sleep(1)
-    print("ok.")
-
-    metadata = client.serverMetadata()
-
-    if not "migraphx" in metadata.extensions:
-        print("MIGraphX support required but not found.")
-        sys.exit(0)
-
-    input_size = 416
-
-    original_image = cv2.imread(imagename)
-    original_image = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
-    original_image_size = original_image.shape[:2]
-
-    img = ip.image_preprocess(np.copy(original_image), [input_size, input_size])
-    img = img[np.newaxis, ...].astype(np.float32)
-
-    # +load worker.  The only parameter the migraphx worker requires is the model file name.
-    # batch and timeout are optional.
-    # It will take the file name stem and search for either a *.onnx or *.mxr extension, and if
-    # it finds a *.onnx file it will compile it and save the compiled model as *.mxr for
-    # future use.  It will read
-    # the array dimensions and data type from the model.
-    parameters = proteus.RequestParameters()
-    parameters.put("model", modelname)
-
-    # I found that allocation could fail with a large batch value of 64 and large
-    # (13) default buffer count in the migraphx worker
-    # Beyond batch size 56, the worker seems to lock up while compiling the model
-    parameters.put("batch", 2)
-
-    # this call requests the server to either find a running instance of the named
-    # worker type, or else create one and initialize it with the parameters.
-    worker_name = client.workerLoad("Migraphx", parameters)
-
-    # wait for the worker to load and compile model
-    ready = False
-    while not ready:
-        ready = client.modelReady(worker_name)
-
-    #
-    # create a multi-image inference request and send it
-    #
-    images = [img, img]
-
-    print("Creating inference request set...")
-    images = [proteus.ImageInferenceRequest(image) for image in images]
-    responses = proteus.client_operators.inferAsyncOrdered(client, worker_name, images)
-    for response in responses:
-        assert not response.isError(), response.getError()
-
-    print("Client received inference reply.")
-
-    for it, response in enumerate(responses):
-        detections = []
-        for out in response.getOutputs():
-            assert out.datatype == proteus.DataType.FP32
-            print("output shape is  ", out.shape)
-            this_detect = np.array(out.getFp32Data())
-            newshape = out.shape
-            # add a 0'th dimension of 1, to make 5.  (the migraphx worker stripped
-            # off the batch size.)
-            newshape.insert(0, 1)
-            this_detect = this_detect.reshape(newshape)
-            detections.append(this_detect)
-
-        #
-        # Post-process the model outputs and display image with detection bounding boxes
-        #
-        ANCHORS = "external/artifacts/onnx/yolov4/yolov4_anchors.txt"
-        STRIDES = [8, 16, 32]
-        XYSCALE = [1.2, 1.1, 1.05]
-
-        ANCHORS = ip.get_anchors(ANCHORS)
-        STRIDES = np.array(STRIDES)
-
-        pred_bbox = ip.postprocess_bbbox(detections, ANCHORS, STRIDES, XYSCALE)
-        bboxes = ip.postprocess_boxes(pred_bbox, original_image_size, input_size, 0.25)
-        bboxes = ip.nms(bboxes, 0.213, method="nms")
-        image = ip.draw_bbox(
-            original_image,
-            bboxes,
-            "external/artifacts/onnx/yolov4/coco.names",
-        )
-
-        image = Image.fromarray(image)
-        output_name = "examples/python/yolo4_output" + str(it) + ".jpg"
-        image.save(output_name)
-
-        print("Your marked-up image is at " + output_name)
-    print("Done.")
-    #
-    #    Alan's related but not identical example is at
-    # https://github.com/ROCmSoftwarePlatform/AMDMIGraphX/blob/develop/examples/vision/python_yolov4/yolov4_inference.ipynb
-
-
-if __name__ == "__main__":
-    args = parse_args()
-    main(args)
diff --git a/examples/python/pt_zendnn.py b/examples/python/pt_zendnn.py
deleted file mode 100644
index f1f85a0f8..000000000
--- a/examples/python/pt_zendnn.py
+++ /dev/null
@@ -1,211 +0,0 @@
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import argparse
-import functools
-import multiprocessing as mp
-import os
-import sys
-import time
-
-import numpy as np
-from utils.utils import postprocess, preprocess_pt
-
-import proteus
-
-
-def main(args):
-    """
-    This is the main method which
-    1. Spin up the server and worker if not already up
-    2. Preprocess if prediction on real image, otherwise create dummy images
-    3. Do the prediction on the data
-    4. Post process and prints output if real image (correctness)
-    5. Or share the time taken for prediction (functionality)
-
-    Args:
-        args (argparse): Parsed argument dictionary
-
-    Raises:
-        FileNotFoundError: If graph given in argument is not found
-        FileNotFoundError: If image_location is given and the file is not found
-    """
-
-    client, server = proteus.start_http_client_server(
-        "http://127.0.0.1:8998", "ptzendnn"
-    )
-
-    # Argument parsing
-    real_data = True if args.image_location else False
-    batch_size = args.batch_size
-    input_size = args.input_size
-
-    # Validating if Image and Model are available
-    if not os.path.exists(args.graph):
-        raise FileNotFoundError(
-            f"Model not found in the location: {args.graph}. Please check the parameter"
-        )
-
-    if real_data and not os.path.exists(args.image_location):
-        raise FileNotFoundError(
-            f"Image not found in the location: {args.image_location}. Please check the parameter"
-        )
-
-    # If predicting on real image, load class list from file.
-    if real_data:
-        try:
-            with open(args.output_class_file) as f:
-                classes = [line.strip() for line in f.readlines()]
-        except FileNotFoundError:
-            classes = list(range(1000))
-        classes = np.asarray(classes)
-
-    parameters = proteus.RequestParameters()
-    parameters.put("model", args.graph)
-    parameters.put("input_size", input_size)
-    parameters.put("output_classes", 1000)
-    parameters.put("batch_size", batch_size)
-    worker_name = client.workerLoad("PtZendnn", parameters)
-
-    ready = False
-    while not ready:
-        ready = client.modelReady(worker_name)
-
-    # Inference with images
-    # If with real data, do preprocessing, otherwise create dummy data
-    processes = int(mp.cpu_count() / 2)  # arbitrarily use half available cores
-    if real_data:
-        # For tests with single images
-        images = []
-        images.append(preprocess_pt(args.image_location, input_size))
-        proteus.parallel_infer(client, worker_name, images, processes)
-
-        # Post process to get top1 and top5 classes
-        idx_1 = postprocess(response, 1)
-        idx_5 = postprocess(response, 5)
-
-        # Print top1 and top5 classess
-        print("Image: {}".format(args.image_location))
-        print("Top 1 Class: {}".format(classes[idx_1[0]][0]))
-        print("Top 5 classes: {}".format(", ".join(classes[tuple(idx_5)])))
-
-    else:
-        # For functionality check with multiple images
-        num_processed_images = total_time = 0
-        num_remaining_images = batch_size * args.steps
-
-        print(
-            "Running Inference for {} images ({} images for {} steps)\n".format(
-                num_remaining_images, batch_size, args.steps
-            )
-        )
-
-        # Run the inference for the images
-        while num_remaining_images >= batch_size:
-
-            # create some random data
-            images = np.random.uniform(
-                0.0, 255.0, (batch_size, input_size, input_size, 3)
-            ).astype(np.float32)
-            images = [proteus.ImageInferenceRequest(image) for image in images]
-
-            # Send request to the server
-            start = time.time()
-            proteus.client_operators.inferAsyncOrdered(client, worker_name, images)
-            end = time.time()
-            total_time += end - start
-
-            num_processed_images += batch_size
-            num_remaining_images -= batch_size
-
-            print(
-                "Images Processed: {:6}, Images Left: {:6}".format(
-                    num_processed_images, num_remaining_images
-                ),
-                end="\r",
-            )
-
-        print("\n\nTotal Images processed: {}".format(num_processed_images))
-        print(
-            "Total time taken for prediction (including http delay): {:.2f}s".format(
-                total_time
-            )
-        )
-
-    print("Inference Completed")
-
-
-if __name__ == "__main__":
-
-    root = os.getenv("PROTEUS_ROOT")
-    assert root is not None
-
-    # Get the arguments required from the user
-    parser = argparse.ArgumentParser(
-        description="Validation (working) for Proteus TF+ZenDNN worker"
-    )
-    parser.add_argument(
-        "--graph",
-        "-g",
-        type=str,
-        required=False,
-        help="Full path to the input graph",
-        default=os.path.join(root, "external/artifacts/pytorch/resnet50_pretrained.pt"),
-    )
-    parser.add_argument(
-        "--image_location",
-        "-d",
-        type=str,
-        required=False,
-        help="Specify the location of the image.",
-    )
-    parser.add_argument(
-        "--batch_size",
-        "-b",
-        type=int,
-        required=False,
-        default=64,
-        help="Specify the batch size. Default is 64. "
-        "If image location is given, this will be 1",
-    )
-    parser.add_argument(
-        "--input_size",
-        "-s",
-        type=int,
-        required=False,
-        default=224,
-        help="Specify the Input Size. Default is 224",
-    )
-    parser.add_argument(
-        "--output_class_file",
-        "-f",
-        type=str,
-        required=False,
-        default=os.path.join(
-            os.path.dirname(os.path.abspath(__file__)), "utils/imagenet_classes.txt"
-        ),
-        help="If real data is given, then this will help output class names",
-    )
-    parser.add_argument(
-        "--steps",
-        type=int,
-        required=False,
-        default=1,
-        help="Maximum number of images (steps * batch size) used for "
-        "benchmarking (only used if image_location not provided)",
-    )
-
-    # Parse arguments and call main
-    args = parser.parse_args()
-    main(args)
diff --git a/examples/python/tf_zendnn.py b/examples/python/tf_zendnn.py
deleted file mode 100644
index 6e67c59e7..000000000
--- a/examples/python/tf_zendnn.py
+++ /dev/null
@@ -1,268 +0,0 @@
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import argparse
-import functools
-import multiprocessing as mp
-import os
-import sys
-import time
-
-import numpy as np
-from utils.utils import postprocess, preprocess
-
-import proteus
-import proteus.clients
-
-
-def main(args):
-    """
-    This is the main method which
-    1. Spin up the server and worker if not already up
-    2. Preprocess if prediction on real image, otherwise create dummy images
-    3. Do the prediction on the data
-    4. Post process and prints output if real image (correctness)
-    5. Or share the time taken for prediction (functionality)
-
-    Args:
-        args (argparse): Parsed argument dictionary
-
-    Raises:
-        FileNotFoundError: If graph given in argument is not found
-        FileNotFoundError: If image_location is given and the file is not found
-    """
-
-    client, server = proteus.start_http_client_server(
-        "http://127.0.0.1:8998", "tfzendnn"
-    )
-
-    # Argument parsing
-    real_data = True if args.image_location else False
-    batch_size = args.batch_size
-    input_size = args.input_size
-    output_classes = args.output_classes
-
-    # Validating if Image and Model are available
-    if not os.path.exists(args.graph):
-        raise FileNotFoundError(
-            f"Model not found in the location: {args.graph}. Please check the parameter"
-        )
-
-    if real_data and not os.path.exists(args.image_location):
-        raise FileNotFoundError(
-            f"Image not found in the location: {args.image_location}. Please check the parameter"
-        )
-
-    # If predicting on real image, load class list from file.
-    if real_data:
-        try:
-            with open(args.output_class_file) as f:
-                classes = [line.strip() for line in f.readlines()]
-        except FileNotFoundError:
-            classes = list(range(1000))
-        classes = np.asarray(classes)
-
-    # Load the worker with appropriate parameters
-    parameters = proteus.RequestParameters()
-    parameters.put("model", args.graph)
-    parameters.put("input_node", args.input_node)
-    parameters.put("output_node", args.output_node)
-    parameters.put("input_size", input_size)
-    parameters.put("output_classes", output_classes)
-    parameters.put("inter_op", args.inter_op)
-    parameters.put("intra_op", args.intra_op)
-    parameters.put("batch_size", batch_size)
-    worker_name = client.workerLoad("TfZendnn", parameters)
-
-    ready = False
-    while not ready:
-        ready = client.modelReady(worker_name)
-
-    # Inference with images
-    # If with real data, do preprocessing, otherwise create dummy data
-    if real_data:
-        # For tests with single images
-        images = [preprocess(args.image_location, input_size, args.resize_method)]
-        request = proteus.ImageInferenceRequest(images)
-        response = client.modelInfer(worker_name, request)
-        assert not response.isError(), response.getError()
-
-        # Post process to get top1 and top5 classes
-        idx_1 = postprocess(response, 1)
-        idx_5 = postprocess(response, 5)
-
-        # Print top1 and top5 classess
-        print("\nImage: {}".format(args.image_location))
-        print("Top 1 Class: {}".format(classes[idx_1[0]]))
-        print("Top 5 classes: {}".format(", ".join(classes[idx_5])))
-
-    else:
-        # For functionality check with multiple images
-        num_processed_images = total_time = 0
-        num_remaining_images = batch_size * args.steps
-
-        print(
-            "Running Inference for {} images ({} images for {} steps)\n".format(
-                num_remaining_images, batch_size, args.steps
-            )
-        )
-
-        # Run the inference for the images
-        processes = int(mp.cpu_count() / 2)
-        while num_remaining_images >= batch_size:
-
-            # create some random data
-            images = np.random.uniform(
-                0.0, 255.0, (batch_size, input_size, input_size, 3)
-            ).astype(np.float32)
-            images = [proteus.ImageInferenceRequest(image) for image in images]
-
-            # Send request to the server
-            start = time.time()
-            proteus.client_operators.inferAsyncOrdered(client, worker_name, images)
-            end = time.time()
-            total_time += end - start
-
-            num_processed_images += batch_size
-            num_remaining_images -= batch_size
-
-            print(
-                "Images Processed: {:6}, Images Left: {:6}".format(
-                    num_processed_images, num_remaining_images
-                ),
-                end="\r",
-            )
-
-        print("\n\nTotal Images processed: {}".format(num_processed_images))
-        print(
-            "Total time taken for prediction (including http delay): {:.2f}s".format(
-                total_time
-            )
-        )
-
-    print("Inference Completed")
-
-
-if __name__ == "__main__":
-
-    root = os.getenv("PROTEUS_ROOT")
-    assert root is not None
-
-    # Get the arguments required from the user
-    parser = argparse.ArgumentParser(
-        description="Validation (working) for Proteus TF+ZenDNN worker"
-    )
-    parser.add_argument(
-        "--graph",
-        "-g",
-        type=str,
-        required=False,
-        help="Full path to the input graph",
-        default=os.path.join(
-            root, "external/artifacts/tensorflow/resnet_v1_50_baseline_6.96B_922.pb"
-        ),
-    )
-    parser.add_argument(
-        "--image_location",
-        "-d",
-        type=str,
-        required=False,
-        help="Specify the location of the image.",
-    )
-    parser.add_argument(
-        "--batch_size",
-        "-b",
-        type=int,
-        required=False,
-        default=16,
-        help="Specify the batch size. Default is 64. "
-        "If image location is given, this will be 1",
-    )
-    parser.add_argument(
-        "--input_node",
-        "-i",
-        type=str,
-        required=False,
-        default="input",
-        help="Specify the name of input node. Default is input",
-    )
-    parser.add_argument(
-        "--output_node",
-        "-o",
-        type=str,
-        required=False,
-        default="resnet_v1_50/predictions/Reshape_1",
-        help="Specify the name of output node. Default is predict",
-    )
-    parser.add_argument(
-        "--input_size",
-        "-s",
-        type=int,
-        required=False,
-        default=224,
-        help="Specify the Input Size. Default is 224",
-    )
-    parser.add_argument(
-        "--output_classes",
-        "-n",
-        type=int,
-        required=False,
-        default=1000,
-        help="Specify the Number of Output Classes. Default is 1000",
-    )
-    parser.add_argument(
-        "--output_class_file",
-        "-f",
-        type=str,
-        required=False,
-        default=os.path.join(
-            os.path.dirname(os.path.abspath(__file__)), "utils/imagenet_classes.txt"
-        ),
-        help="If real data is given, then this will help output class names",
-    )
-    parser.add_argument(
-        "--resize_method",
-        "-r",
-        type=str,
-        required=False,
-        default="xilinx",
-        help="Specify the resize method for images: crop (ResNet50)"
-        ", bilinear (MobileNetv1, InceptionV3), xilinx (xilinx ResNet50)",
-    )
-    parser.add_argument(
-        "--inter_op",
-        type=int,
-        required=False,
-        default=1,
-        help="Specify the Input Size. Default is 64",
-    )
-    parser.add_argument(
-        "--intra_op",
-        type=int,
-        required=False,
-        default=64,
-        help="Specify the Input Size. Default is 1",
-    )
-    parser.add_argument(
-        "--steps",
-        type=int,
-        required=False,
-        default=1,
-        help="Maximum number of images (steps * batch size) used for "
-        "benchmarking (only used if image_location not provided)",
-    )
-
-    # Parse arguments and call main
-    args = parser.parse_args()
-    main(args)
diff --git a/examples/python/utils/utils.py b/examples/python/utils/utils.py
deleted file mode 100644
index e7a3eaeec..000000000
--- a/examples/python/utils/utils.py
+++ /dev/null
@@ -1,163 +0,0 @@
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import cv2
-import numpy as np
-
-
-def center_crop(img, dim):
-    """
-    Crops the input `img` from center to a specific dimention specified
-
-    Args:
-        img (numpy.ndarray): Image which needs to be cropped
-        dim (tuple): Shape of the output image
-
-    Returns:
-        [numpy.ndarray]: Image cropped to dim shape
-    """
-
-    width, height = img.shape[1], img.shape[0]
-
-    # process crop width and height for max available dimension
-    crop_width = dim[0] if dim[0] < img.shape[1] else img.shape[1]
-    crop_height = dim[1] if dim[1] < img.shape[0] else img.shape[0]
-    mid_x, mid_y = int(width / 2), int(height / 2)
-    cw2, ch2 = int(crop_width / 2), int(crop_height / 2)
-    crop_img = img[mid_y - ch2 : mid_y + ch2, mid_x - cw2 : mid_x + cw2]
-    return crop_img
-
-
-def _mean_image_subtraction(image, means):
-    """
-    Subtract mean value on each channel
-
-    Args:
-        image (numpy.ndarray): Image on which mean value have to be subtracted
-        means (list): List of values matching number of channels in the image
-
-    Raises:
-        ValueError: If the image does not have 3 channels
-        ValueError: If the length of means and channels does not match
-
-    Returns:
-        [numpy.ndarray]: The numpy array after subtracting mean value
-    """
-    if image.ndim != 3:
-        raise ValueError("Input must be of size [height, width, C>0]")
-    num_channels = image.shape[-1]
-    if len(means) != num_channels:
-        raise ValueError("len(means) must match the number of channels")
-
-    # channels = tf.split(axis=2, num_or_size_splits=num_channels, value=image)
-    channels = np.split(image, axis=2, indices_or_sections=num_channels)
-    for i in range(num_channels):
-        channels[i] -= means[i]
-    return np.concatenate(channels, axis=2)
-
-
-def preprocess(image_location, input_size, resize_method):
-    """
-    This image will load and preprocess the data according to
-    the input arguments
-
-    Args:
-        image_location (str): The location of the image to be loaded
-        input_size (int): The input height/width of the image
-        resize_method (str): Which prepcessing method to use
-
-    Returns:
-        [numpy.ndarray]: The preprocessed image as a numpy array
-    """
-
-    image = cv2.imread(image_location)
-    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
-    shape = image.shape
-
-    if resize_method == "crop" or resize_method == "xilinx":
-        if shape[0] < shape[1]:
-            scale = input_size / shape[0]
-        else:
-            scale = input_size / shape[1]
-        new_shape = tuple(map(int, (shape[1] * scale, shape[0] * scale)))
-
-        image = cv2.resize(image, new_shape)
-        image = center_crop(image, (input_size, input_size)).astype(np.float32)
-
-        if resize_method == "xilinx":
-            image = _mean_image_subtraction(image, [123.68, 116.78, 103.94])
-
-    else:  # bilinear
-
-        if image.dtype != np.float32:
-            image = (image / 255).astype(np.float32)
-
-        new_shape = tuple(map(int, (shape[1] * 0.875, shape[0] * 0.875)))
-        image = center_crop(image, new_shape)
-        image = cv2.resize(image, (input_size, input_size), cv2.INTER_LINEAR)
-        image = np.subtract(image, 0.5)
-        image = np.multiply(image, 2.0)
-
-    return image
-
-
-def postprocess(response, k=5):
-    """
-    This method will postprocess the output response
-    from the server and returns topK
-
-    Args:
-        response (proteus.predict_api.InferenceResponse):
-            The response from the server
-        k (int, optional): K value for topK.
-            Defaults to 5.
-
-    Returns:
-        [numpy.ndarray]: topK values
-    """
-    outputs = response.getOutputs()
-    responses = []
-    for output in outputs:
-        response_data = output.getFp32Data()
-        response_data = np.argsort(response_data)
-        responses.append(response_data[-k:][::-1])
-    return responses
-
-
-def preprocess_pt(image_location, input_size):
-    """
-    This image will load and preprocess the data according to
-    the input arguments
-
-    Args:
-        image_location (str): The location of the image to be loaded
-        input_size (int): The input height/width of the image
-
-    Returns:
-        [numpy.ndarray]: The preprocessed image as a numpy array
-    """
-
-    image = cv2.imread(image_location)
-    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
-    image = cv2.resize(image, (256, 256), cv2.INTER_LINEAR)
-    image = center_crop(image, (input_size, input_size))
-
-    image = np.rollaxis(image, axis=2, start=0)  # reshape to CHW for pytorch
-    mean = (0.485, 0.456, 0.406)
-    std = (0.229, 0.224, 0.225)
-    image = image.astype(np.float32) / 255.0
-    for channel in range(image.shape[0]):
-        image[channel] = (image[channel] - mean[channel]) / std[channel]
-
-    return image
diff --git a/examples/resnet50/CMakeLists.txt b/examples/resnet50/CMakeLists.txt
new file mode 100644
index 000000000..4d959f69c
--- /dev/null
+++ b/examples/resnet50/CMakeLists.txt
@@ -0,0 +1,69 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+list(APPEND examples)
+
+if(${PROTEUS_ENABLE_VITIS})
+  list(APPEND examples vitis)
+endif()
+
+if(${PROTEUS_ENABLE_PTZENDNN})
+  list(APPEND examples ptzendnn)
+endif()
+
+if(${PROTEUS_ENABLE_TFZENDNN})
+  list(APPEND examples tfzendnn)
+endif()
+
+if(${PROTEUS_ENABLE_MIGRAPHX})
+  list(APPEND examples migraphx)
+endif()
+
+foreach(example ${examples})
+  proteus_add_example(${example})
+  proteus_get_example_target(target ${example})
+  target_link_libraries(
+    ${target} PRIVATE opencv_imgcodecs opencv_imgproc opencv_core
+  )
+endforeach()
+
+# if(${PROTEUS_ENABLE_TFZENDNN})
+#   add_executable(tf_zendnn_client tf_zendnn_client.cpp)
+
+#   target_link_libraries(
+#     tf_zendnn_client PRIVATE proteus util opencv_imgcodecs opencv_imgproc
+#                              opencv_core
+#   )
+#   target_include_directories(tf_zendnn_client PRIVATE ${PROTEUS_INCLUDE_DIRS})
+# endif()
+
+# if(${PROTEUS_ENABLE_PTZENDNN})
+#   add_executable(pt_zendnn_client pt_zendnn_client.cpp)
+
+#   target_link_libraries(
+#     pt_zendnn_client PRIVATE proteus util opencv_imgcodecs opencv_imgproc
+#                              opencv_core
+#   )
+#   target_include_directories(pt_zendnn_client PRIVATE ${PROTEUS_INCLUDE_DIRS})
+# endif()
+
+# if(${PROTEUS_ENABLE_MIGRAPHX})
+#   add_executable(migraphx_client migraphx_client.cpp)
+
+#   target_link_libraries(
+#     migraphx_client PRIVATE proteus util opencv_imgcodecs opencv_imgproc
+#                             opencv_core migraphx::c
+#   )
+#   target_include_directories(migraphx_client PRIVATE ${PROTEUS_INCLUDE_DIRS})
+# endif()
diff --git a/examples/resnet50/README.rst b/examples/resnet50/README.rst
new file mode 100644
index 000000000..f06fe7305
--- /dev/null
+++ b/examples/resnet50/README.rst
@@ -0,0 +1,49 @@
+..
+    Copyright 2022 Advanced Micro Devices, Inc.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+ResNet50
+--------
+
+These examples demonstrate how to make an inference using the ResNet50 model with the different backends supported by the inference server.
+
+Introduction
+^^^^^^^^^^^^
+
+ResNet50 is a classification model that accepts a single input tensor that contains the input image.
+For this image, it returns the probabilities that the image can be classified as any of its output classes.
+The number of output classes depends on the dataset that the network is trained on.
+In these examples, the default model is trained on the ImageNet dataset and has 1000 output classes.
+
+Files
+^^^^^
+
+The files in this set of examples are:
+
+::
+
+    examples/resnet50
+    ├── CMakeLists.txt
+    ├── imagenet_classes.txt
+    ├── migraphx.cpp
+    ├── migraphx.py
+    ├── ptzendnn.cpp
+    ├── ptzendnn.py
+    ├── README.rst
+    ├── resnet50.hpp
+    ├── resnet.py
+    ├── tfzendnn.cpp
+    ├── tfzendnn.py
+    ├── vitis.cpp
+    └── vitis.py
diff --git a/examples/python/utils/imagenet_classes.txt b/examples/resnet50/imagenet_classes.txt
similarity index 100%
rename from examples/python/utils/imagenet_classes.txt
rename to examples/resnet50/imagenet_classes.txt
diff --git a/examples/resnet50/migraphx.cpp b/examples/resnet50/migraphx.cpp
new file mode 100644
index 000000000..e4ae97e31
--- /dev/null
+++ b/examples/resnet50/migraphx.cpp
@@ -0,0 +1,241 @@
+// Copyright 2022 Advanced Micro Devices, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file
+ * @brief This example demonstrates how you can use the MIGraphX backend to run
+ * inference on an AMD GPU with a ResNet50 ONNX model. Look at the documentation
+ * online for discussion around this example.
+ */
+
+#include <array>                // for array
+#include <cassert>              // for assert
+#include <chrono>               // for duration
+#include <cstdint>              // for uint64_t
+#include <cstdlib>              // for exit, getenv
+#include <filesystem>           // for path, oper...
+#include <initializer_list>     // for initialize...
+#include <iostream>             // for operator<<
+#include <memory>               // for allocator
+#include <opencv2/core.hpp>     // for CV_32FC3
+#include <opencv2/imgproc.hpp>  // for COLOR_BGR2RGB
+#include <ratio>                // for milli
+#include <string>               // for string
+#include <vector>               // for vector
+
+#include "proteus/client_operators/infer_async.hpp"        // for inferAsync...
+#include "proteus/proteus.hpp"                             // for InferenceR...
+#include "proteus/util/pre_post/image_preprocess.hpp"      // for ImagePrepr...
+#include "proteus/util/pre_post/resnet50_postprocess.hpp"  // for resnet50Po...
+#include "resnet50.hpp"                                    // for Args, pars...
+
+namespace fs = std::filesystem;
+
+using Images = std::vector<std::vector<float>>;
+
+/**
+ * @brief Given a vector of paths to images, preprocess the images and return
+ * them
+ *
+ * @param paths paths to images to preprocess
+ * @return Images
+ */
+Images preprocess(const std::vector<std::string>& paths) {
+  const std::array<float, 3> mean{0.485F, 0.456F, 0.406F};
+  const std::array<float, 3> std{4.367F, 4.464F, 4.444F};
+  const auto image_size = 224;
+
+  // this example uses a custom image preprocessing function. You may use any
+  // preprocessing logic or skip it entirely if your input data is already
+  // preprocessed.
+  proteus::util::ImagePreprocessOptions<float, 3> options;
+  options.order = proteus::util::ImageOrder::NCHW;
+  options.height = image_size;
+  options.width = image_size;
+  options.mean = mean;
+  options.std = std;
+  options.normalize = true;
+  options.convert_color = true;
+  options.color_code = cv::COLOR_BGR2RGB;
+  options.convert_type = true;
+  options.type = CV_32FC3;
+  options.convert_scale = 1.0 / 255.0;
+  return proteus::util::imagePreprocess(paths, options);
+}
+
+/**
+ * @brief Postprocess the output data. For ResNet50, this includes performing a
+ * softmax to determine the most probable classifications.
+ *
+ * @param output the output from the inference server
+ * @param k number of top categories to return
+ * @return std::vector<int> the indices for the top k categories
+ */
+std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
+                             int k) {
+  return proteus::util::resnet50Postprocess(
+    static_cast<const float*>(output.getData()), output.getSize(), k);
+}
+
+/**
+ * @brief Construct requests for the inference server from the input images. For
+ * ResNet50, a valid request includes a single input tensor containing a square
+ * image.
+ *
+ * @param images the input images
+ * @param input_size size of the square image in pixels
+ * @return std::vector<proteus::InferenceRequest>
+ */
+std::vector<proteus::InferenceRequest> constructRequests(const Images& images,
+                                                         uint64_t input_size) {
+  std::vector<proteus::InferenceRequest> requests;
+  requests.reserve(images.size());
+
+  const std::initializer_list<uint64_t> shape = {input_size, input_size, 3};
+
+  for (const auto& image : images) {
+    requests.emplace_back();
+    requests.back().addInputTensor((void*)image.data(), shape,
+                                   proteus::DataType::FP32);
+  }
+
+  return requests;
+}
+
+/**
+ * @brief Load a worker to handle an inference request. The load returns the
+ * endpoint you should use for subsequent requests.
+ *
+ * @param client pointer to a client object
+ * @param args the command-line arguments
+ * @return std::string
+ */
+std::string load(const proteus::Client* client, const Args& args) {
+  // Depending on how the server is compiled, it may or may not have support for
+  // a particular backend. This guard checks to make sure the server does
+  // support the requested backend. If you already know it's supported, you can
+  // skip this check.
+  if (!serverHasExtension(client, "migraphx")) {
+    std::cerr << "MIGraphX is not enabled. Please recompile with it enabled to "
+              << "run this example\n";
+    exit(1);
+  }
+
+  // Load-time parameters are used to pass one-time information to the batcher
+  // and worker as it starts up. Each worker can choose to define its own
+  // parameters that it pays attention to. Similarly, the batcher that the
+  // worker is using may have its own parameters. Check the documentation to see
+  // what may be specified.
+
+  // +load
+  proteus::RequestParameters parameters;
+  const auto timeout_ms = 1000;  // batcher timeout value in milliseconds
+  const auto batch_size = 2;
+
+  // Required: specifies path to the model on the server for it to open
+  parameters.put("model", args.path_to_model);
+  // Optional: request a particular batch size to be sent to the backend. The
+  // server will attempt to coalesce incoming requests into a single batch of
+  // this size and pass it all to the backend.
+  parameters.put("batch", batch_size);
+  // Optional: specifies how long the batcher should wait for more requests
+  // before sending the batch on
+  parameters.put("timeout", timeout_ms);
+  std::string endpoint = client->workerLoad("migraphx", &parameters);
+  // -load
+  return endpoint;
+}
+
+/**
+ * @brief The command-line arguments are parsed in two phases. There's the
+ * common arguments that are initialized by parseArgs that are shared by all the
+ * C++ examples in this directory and then example-specific settings are
+ * initialized here
+ *
+ * @param argc number of arguments
+ * @param argv arguments
+ * @return Args
+ */
+Args getArgs(int argc, char** argv) {
+  Args args = parseArgs(argc, argv);
+
+  if (args.path_to_model.empty()) {
+    fs::path root{std::getenv("PROTEUS_ROOT")};
+    args.path_to_model =
+      root / "external/artifacts/onnx/resnet50v2/resnet50-v2-7.onnx";
+  }
+
+  return args;
+}
+
+int main(int argc, char* argv[]) {
+  std::cout << "Running the MIGraphX example for ResNet50 in C++\n";
+
+  Args args = getArgs(argc, argv);
+
+  proteus::Server server;
+#ifdef PROTEUS_ENABLE_REST
+  server.startHttp(args.http_port);
+#else
+  std::cerr << "HTTP/REST is not enabled. Please recompile the library with it "
+            << "enabled to run this example.\n";
+  exit(1);
+#endif
+
+  const auto http_port_str = std::to_string(args.http_port);
+  proteus::HttpClient client{"http://127.0.0.1:" + http_port_str};
+
+  std::cout << "Waiting until the server is ready...\n";
+  proteus::waitUntilServerReady(&client);
+
+  std::cout << "Loading worker...\n";
+  std::string endpoint = load(&client, args);
+
+  std::vector<std::string> paths = resolveImagePaths(args.path_to_image);
+  Images images = preprocess(paths);
+
+  std::vector<proteus::InferenceRequest> requests =
+    constructRequests(images, args.input_size);
+
+  assert(paths.size() == requests.size());
+  const auto num_requests = requests.size();
+
+  std::cout << "Making inference...\n";
+  auto start = std::chrono::high_resolution_clock::now();
+
+  // +validate:
+  // migraphx.cpp
+  std::vector<proteus::InferenceResponse> responses =
+    proteus::inferAsyncOrdered(&client, endpoint, requests);
+  assert(num_requests == responses.size());
+
+  for (auto i = 0U; i < num_requests; ++i) {
+    const proteus::InferenceResponse& response = responses[i];
+    assert(!response.isError());
+
+    std::vector<proteus::InferenceResponseOutput> outputs =
+      response.getOutputs();
+    // for resnet50, we expect a single output tensor
+    assert(outputs.size() == 1);
+    std::vector<int> top_indices = postprocess(outputs[0], args.top);
+    printLabel(top_indices, args.path_to_labels, paths[i]);
+  }
+  // -validate:
+  auto stop = std::chrono::high_resolution_clock::now();
+  std::chrono::duration<double, std::milli> duration = stop - start;
+  std::cout << "Time taken for inference, postprocessing and printing: "
+            << duration.count() << " ms\n";
+
+  return 0;
+}
diff --git a/examples/resnet50/migraphx.py b/examples/resnet50/migraphx.py
new file mode 100644
index 000000000..d64424ee0
--- /dev/null
+++ b/examples/resnet50/migraphx.py
@@ -0,0 +1,213 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This example demonstrates how you can use the MIGraphX backend to run inference
+on an AMD GPU with a ResNet50 ONNX model. Look at the documentation online for
+discussion around this example.
+"""
+
+import os
+import pathlib
+import sys
+from time import sleep
+
+import cv2
+
+import proteus
+import proteus.client_operators
+import proteus.clients
+import proteus.servers
+import proteus.util.pre_post as pre_post
+
+# isort: split
+
+from resnet import parse_args, print_label, resolve_image_paths
+
+
+def preprocess(paths):
+    """
+    Given a list of paths to images, preprocess the images and return them
+
+    Args:
+        paths (list[str]): Paths to images
+
+    Returns:
+        list[numpy.ndarray]: List of images
+    """
+
+    # This example uses a custom image preprocessing function implemented in C++
+    # You may use any preprocessing logic or skip it entirely if your input data
+    # is already preprocessed
+    options = pre_post.ImagePreprocessOptionsFloat()
+    options.order = pre_post.ImageOrder.NCHW
+    options.height = 224
+    options.width = 224
+    options.mean = [0.485, 0.456, 0.406]
+    options.std = [4.367, 4.464, 4.444]
+    options.normalize = True
+    options.convert_color = True
+    options.color_code = cv2.COLOR_BGR2RGB
+    options.convert_type = True
+    options.type = cv2.CV_32FC3
+    options.convert_scale = 1.0 / 255.0
+    return pre_post.imagePreprocessFloat(paths, options)
+
+
+def postprocess(output, k):
+    """
+    Postprocess the output data. For ResNet50, this includes performing a softmax
+    to determine the most probable classifications
+
+    Args:
+        output (proteus.InferenceResponseOutput): the output from the inference server
+        k (int): number of top categories to return
+
+    Returns:
+        list[int]: indices for the top k categories
+    """
+    return pre_post.resnet50PostprocessFloat(output, k)
+
+
+def construct_requests(images):
+    """
+    Construct requests for the inference server from the input images. For ResNet50,
+    a valid request includes a single input tensor containing a square image.
+
+    Args:
+        images (list[numpy.ndarray]): the input images
+
+    Returns:
+        list[proteus.InferenceRequest]: the requests
+    """
+    requests = []
+    for image in images:
+        requests.append(proteus.ImageInferenceRequest(image))
+    return requests
+
+
+def load(client, args):
+    """
+    Load a worker to handle an inference request. The load returns the endpoint
+    you should use for subsequent requests
+
+    Args:
+        client (proteus.client.Client): the client object
+        args (argparse.Namespace): the command line arguments
+
+    Returns:
+        str: endpoint
+    """
+    # Depending on how the server is compiled, it may or may not have support
+    # for a particular backend. This guard checks to make sure the server does
+    # support the requested backend. If you already know it's supported, you can
+    # skip this check.
+    metadata = client.serverMetadata()
+    if "migraphx" not in metadata.extensions:
+        print(
+            "MIGraphX is not enabled. Please recompile with it enabled to run this example"
+        )
+        sys.exit(1)
+
+    # Load-time parameters are used to pass one-time information to the batcher
+    # and worker as it starts up. Each worker can choose to define its own
+    # parameters that it pays attention to. Similarly, the batcher that the worker is
+    # using may have its own parameters. Check the documentation to see what may
+    # be specified.
+
+    parameters = proteus.RequestParameters()
+    timeout_ms = 1000  # batcher timeout value in milliseconds
+    batch_size = 2
+
+    # Required: specifies path to the model on the server for it to open
+    parameters.put("model", args.model)
+    # Optional: request a particular batch size to be sent to the backend. The
+    # server will attempt to coalesce incoming requests into a single batch of
+    # this size and pass it all to the backend.
+    parameters.put("batch", batch_size)
+    # Optional: specifies how long the batcher should wait for more requests before
+    # sending the batch on
+    parameters.put("timeout", timeout_ms)
+    endpoint = client.workerLoad("migraphx", parameters)
+    return endpoint
+
+
+def get_args():
+    """
+    The command-line arguments are parsed in two phases. There's the common
+    arguments that are initialized by parseArgs that are shared by all the
+    Python examples in this directory and then example-specific settings are
+    initialized here
+
+    Returns:
+        argparse.Namespace: the args
+    """
+    args = parse_args()
+
+    if not args.model:
+        root = os.getenv("PROTEUS_ROOT")
+        args.model = root + "/external/artifacts/onnx/resnet50v2/resnet50-v2-7.onnx"
+
+    return args
+
+
+def main(args):
+    print("Running the MIGraphX example for ResNet50 in Python")
+
+    server = proteus.servers.Server()
+    print("Waiting until the server is ready...")
+    server.startHttp(args.http_port)
+
+    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
+    ready = False
+    while not ready:
+        try:
+            ready = client.serverReady()
+        except proteus.RuntimeError:
+            pass
+        sleep(1)
+
+    print("Loading worker...")
+    endpoint = load(client, args)
+
+    ready = False
+    while not ready:
+        ready = client.modelReady(endpoint)
+
+    paths = resolve_image_paths(pathlib.Path(args.image))
+
+    images = preprocess(paths)
+
+    requests = construct_requests(images)
+
+    assert len(paths) == len(requests)
+    # +run inference
+    # in migraphx.py
+    responses = proteus.client_operators.inferAsyncOrdered(client, endpoint, requests)
+    print("Making inferences...")
+    for image_path, response in zip(paths, responses):
+        assert not response.isError()
+
+        outputs = response.getOutputs()
+        assert len(outputs) == 1
+        top_indices = postprocess(outputs[0], args.top)
+        print_label(top_indices, args.labels, image_path)
+    # -run inference
+
+
+if __name__ == "__main__":
+
+    args = get_args()
+
+    main(args)
diff --git a/examples/resnet50/ptzendnn.cpp b/examples/resnet50/ptzendnn.cpp
new file mode 100644
index 000000000..fbf456012
--- /dev/null
+++ b/examples/resnet50/ptzendnn.cpp
@@ -0,0 +1,221 @@
+// Copyright 2022 Advanced Micro Devices, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file
+ * @brief This example demonstrates how you can use the PT+ZenDNN backend to run
+ * inference on an AMD CPU with a ResNet50 PyTorch model. Look at the
+ * documentation online for discussion around this example.
+ */
+
+#include <array>                // for array
+#include <cassert>              // for assert
+#include <chrono>               // for duration
+#include <cstdint>              // for uint64_t
+#include <cstdlib>              // for exit, getenv
+#include <filesystem>           // for path, oper...
+#include <initializer_list>     // for initialize...
+#include <iostream>             // for operator<<
+#include <memory>               // for allocator
+#include <opencv2/core.hpp>     // for CV_32FC3
+#include <opencv2/imgproc.hpp>  // for COLOR_BGR2RGB
+#include <ratio>                // for milli
+#include <string>               // for string
+#include <vector>               // for vector
+
+#include "proteus/proteus.hpp"                             // for InferenceR...
+#include "proteus/util/pre_post/image_preprocess.hpp"      // for ImagePrepr...
+#include "proteus/util/pre_post/resnet50_postprocess.hpp"  // for resnet50Po...
+#include "resnet50.hpp"                                    // for Args, pars...
+
+namespace fs = std::filesystem;
+
+using Images = std::vector<std::vector<float>>;
+
+/**
+ * @brief Given a vector of paths to images, preprocess the images and return
+ * them
+ *
+ * @param paths paths to images to preprocess
+ * @return Images
+ */
+Images preprocess(const std::vector<std::string>& paths) {
+  const std::array<float, 3> mean{0.485F, 0.456F, 0.406F};
+  const std::array<float, 3> std{4.367F, 4.464F, 4.444F};
+
+  // this example uses a custom image preprocessing function. You may use any
+  // preprocessing logic or skip it entirely if your input data is already
+  // preprocessed.
+  proteus::util::ImagePreprocessOptions<float, 3> options;
+  options.order = proteus::util::ImageOrder::NCHW;
+  options.mean = mean;
+  options.std = std;
+  options.normalize = true;
+  options.convert_color = true;
+  options.color_code = cv::COLOR_BGR2RGB;
+  options.convert_type = true;
+  options.type = CV_32FC3;
+  options.convert_scale = 1.0 / 255.0;
+  return proteus::util::imagePreprocess(paths, options);
+}
+
+/**
+ * @brief Postprocess the output data. For ResNet50, this includes performing a
+ * softmax to determine the most probable classifications.
+ *
+ * @param output the output from the inference server
+ * @param k number of top categories to return
+ * @return std::vector<int> the indices for the top k categories
+ */
+std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
+                             int k) {
+  return proteus::util::resnet50Postprocess(
+    static_cast<const float*>(output.getData()), output.getSize(), k);
+}
+
+/**
+ * @brief Construct requests for the inference server from the input images. For
+ * ResNet50, a valid request includes a single input tensor containing a square
+ * image.
+ *
+ * @param images the input images
+ * @param input_size size of the square image in pixels
+ * @return std::vector<proteus::InferenceRequest>
+ */
+std::vector<proteus::InferenceRequest> constructRequests(const Images& images,
+                                                         uint64_t input_size) {
+  std::vector<proteus::InferenceRequest> requests;
+  requests.reserve(images.size());
+
+  const std::initializer_list<uint64_t> shape = {input_size, input_size, 3};
+
+  for (const auto& image : images) {
+    requests.emplace_back();
+    requests.back().addInputTensor((void*)image.data(), shape,
+                                   proteus::DataType::FP32);
+  }
+
+  return requests;
+}
+
+/**
+ * @brief Load a worker to handle an inference request. The load returns the
+ * endpoint you should use for subsequent requests.
+ *
+ * @param client pointer to a client object
+ * @param args the command-line arguments
+ * @return std::string
+ */
+std::string load(const proteus::Client* client, const Args& args) {
+  // Depending on how the server is compiled, it may or may not have support for
+  // a particular backend. This guard checks to make sure the server does
+  // support the requested backend. If you already know it's supported, you can
+  // skip this check.
+  if (!serverHasExtension(client, "ptzendnn")) {
+    std::cerr
+      << "PT+ZenDNN is not enabled. Please recompile with it enabled to "
+      << "run this example\n";
+    exit(1);
+  }
+
+  // Load-time parameters are used to pass one-time information to the batcher
+  // and worker as it starts up. Each worker can choose to define its own
+  // parameters that it pays attention to. Similarly, the batcher that the
+  // worker is using may have its own parameters. Check the documentation to see
+  // what may be specified.
+
+  proteus::RequestParameters parameters;
+  parameters.put("model", args.path_to_model);
+  parameters.put("input_size", args.input_size);
+  parameters.put("output_classes", args.output_classes);
+  return client->workerLoad("ptzendnn", &parameters);
+}
+
+/**
+ * @brief The command-line arguments are parsed in two phases. There's the
+ * common arguments that are initialized by parseArgs that are shared by all the
+ * C++ examples in this directory and then example-specific settings are
+ * initialized here
+ *
+ * @param argc number of arguments
+ * @param argv arguments
+ * @return Args
+ */
+Args getArgs(int argc, char** argv) {
+  Args args = parseArgs(argc, argv);
+
+  if (args.path_to_model.empty()) {
+    fs::path root{std::getenv("PROTEUS_ROOT")};
+    args.path_to_model =
+      root / "external/artifacts/pytorch/resnet50_pretrained.pt";
+  }
+
+  return args;
+}
+
+int main(int argc, char* argv[]) {
+  std::cout << "Running the PT+ZenDNN example for ResNet50 in C++\n";
+
+  Args args = getArgs(argc, argv);
+
+  proteus::Server server;
+
+  // +create client
+  // ptzendnn.cpp
+  proteus::NativeClient client;
+
+  std::cout << "Waiting until the server is ready...\n";
+  proteus::waitUntilServerReady(&client);
+  // -create client
+
+  std::cout << "Loading worker...\n";
+  std::string endpoint = load(&client, args);
+
+  std::vector<std::string> paths = resolveImagePaths(args.path_to_image);
+  Images images = preprocess(paths);
+
+  std::vector<proteus::InferenceRequest> requests =
+    constructRequests(images, args.input_size);
+
+  assert(paths.size() == requests.size());
+  const auto num_requests = requests.size();
+
+  std::cout << "Making inference...\n";
+  auto start = std::chrono::high_resolution_clock::now();
+  for (auto i = 0U; i < num_requests; ++i) {
+    const proteus::InferenceRequest& request = requests[i];
+    const std::string& image_path = paths[i];
+
+    // +validate
+    // ptzendnn.cpp
+    proteus::InferenceResponseFuture future =
+      client.modelInferAsync(endpoint, request);
+    proteus::InferenceResponse response = future.get();
+    assert(!response.isError());
+
+    std::vector<proteus::InferenceResponseOutput> outputs =
+      response.getOutputs();
+    // for resnet50, we expect a single output tensor
+    assert(outputs.size() == 1);
+    std::vector<int> top_indices = postprocess(outputs[0], args.top);
+    printLabel(top_indices, args.path_to_labels, image_path);
+    // -validate
+  }
+  auto stop = std::chrono::high_resolution_clock::now();
+  std::chrono::duration<double, std::milli> duration = stop - start;
+  std::cout << "Time taken for inference, postprocessing and printing: "
+            << duration.count() << " ms\n";
+
+  return 0;
+}
diff --git a/examples/resnet50/ptzendnn.py b/examples/resnet50/ptzendnn.py
new file mode 100644
index 000000000..89de710de
--- /dev/null
+++ b/examples/resnet50/ptzendnn.py
@@ -0,0 +1,196 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This example demonstrates how you can use the PT+ZenDNN backend to run inference
+on an AMD CPU with a ResNet50 PyTorch model. Look at the documentation online for
+discussion around this example.
+"""
+
+import os
+import pathlib
+import sys
+from time import sleep
+
+import cv2
+
+import proteus
+import proteus.client_operators
+import proteus.clients
+import proteus.servers
+import proteus.util.pre_post as pre_post
+
+# isort: split
+
+from resnet import parse_args, print_label, resolve_image_paths
+
+
+def preprocess(paths):
+    """
+    Given a list of paths to images, preprocess the images and return them
+
+    Args:
+        paths (list[str]): Paths to images
+
+    Returns:
+        list[numpy.ndarray]: List of images
+    """
+    options = pre_post.ImagePreprocessOptionsFloat()
+    options.order = pre_post.ImageOrder.NCHW
+    options.mean = [0.485, 0.456, 0.406]
+    options.std = [4.367, 4.464, 4.444]
+    options.normalize = True
+    options.convert_color = True
+    options.color_code = cv2.COLOR_BGR2RGB
+    options.convert_type = True
+    options.type = cv2.CV_32FC3
+    options.convert_scale = 1.0 / 255.0
+    return pre_post.imagePreprocessFloat(paths, options)
+
+
+def postprocess(output, k):
+    """
+    Postprocess the output data. For ResNet50, this includes performing a softmax
+    to determine the most probable classifications
+
+    Args:
+        output (proteus.InferenceResponseOutput): the output from the inference server
+        k (int): number of top categories to return
+
+    Returns:
+        list[int]: indices for the top k categories
+    """
+    return pre_post.resnet50PostprocessFloat(output, k)
+
+
+def construct_requests(images):
+    """
+    Construct requests for the inference server from the input images. For ResNet50,
+    a valid request includes a single input tensor containing a square image.
+
+    Args:
+        images (list[numpy.ndarray]): the input images
+
+    Returns:
+        list[proteus.InferenceRequest]: the requests
+    """
+    requests = []
+    for image in images:
+        requests.append(proteus.ImageInferenceRequest(image))
+    return requests
+
+
+def load(client, args):
+    """
+    Load a worker to handle an inference request. The load returns the endpoint
+    you should use for subsequent requests
+
+    Args:
+        client (proteus.client.Client): the client object
+        args (argparse.Namespace): the command line arguments
+
+    Returns:
+        str: endpoint
+    """
+    # Depending on how the server is compiled, it may or may not have support
+    # for a particular backend. This guard checks to make sure the server does
+    # support the requested backend. If you already know it's supported, you can
+    # skip this check.
+
+    metadata = client.serverMetadata()
+    if "ptzendnn" not in metadata.extensions:
+        print(
+            "PT+ZenDNN is not enabled. Please recompile with it enabled to run this example"
+        )
+        sys.exit(1)
+
+    # Load-time parameters are used to pass one-time information to the batcher
+    # and worker as it starts up. Each worker can choose to define its own
+    # parameters that it pays attention to. Similarly, the batcher that the worker is
+    # using may have its own parameters. Check the documentation to see what may
+    # be specified.
+
+    parameters = proteus.RequestParameters()
+    parameters.put("model", args.model)
+    parameters.put("input_size", args.input_size)
+    parameters.put("output_classes", args.output_classes)
+    endpoint = client.workerLoad("ptzendnn", parameters)
+    return endpoint
+
+
+def get_args():
+    """
+    The command-line arguments are parsed in two phases. There's the common
+    arguments that are initialized by parseArgs that are shared by all the
+    Python examples in this directory and then example-specific settings are
+    initialized here
+
+    Returns:
+        argparse.Namespace: the args
+    """
+    args = parse_args()
+
+    if not args.model:
+        root = os.getenv("PROTEUS_ROOT")
+        args.model = root + "/external/artifacts/pytorch/resnet50_pretrained.pt"
+
+    return args
+
+
+def main(args):
+    print("Running the PT+ZenDNN example for ResNet50 in Python")
+
+    server = proteus.servers.Server()
+    print("Waiting until the server is ready...")
+    server.startHttp(args.http_port)
+
+    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
+    ready = False
+    while not ready:
+        try:
+            ready = client.serverReady()
+        except proteus.RuntimeError:
+            pass
+        sleep(1)
+
+    print("Loading worker...")
+    endpoint = load(client, args)
+
+    ready = False
+    while not ready:
+        ready = client.modelReady(endpoint)
+
+    paths = resolve_image_paths(pathlib.Path(args.image))
+
+    images = preprocess(paths)
+
+    requests = construct_requests(images)
+
+    assert len(paths) == len(requests)
+    responses = proteus.client_operators.inferAsyncOrdered(client, endpoint, requests)
+    print("Making inferences...")
+    for image_path, response in zip(paths, responses):
+        assert not response.isError()
+
+        outputs = response.getOutputs()
+        assert len(outputs) == 1
+        top_indices = postprocess(outputs[0], args.top)
+        print_label(top_indices, args.labels, image_path)
+
+
+if __name__ == "__main__":
+
+    args = get_args()
+
+    main(args)
diff --git a/examples/resnet50/resnet.py b/examples/resnet50/resnet.py
new file mode 100644
index 000000000..da284203a
--- /dev/null
+++ b/examples/resnet50/resnet.py
@@ -0,0 +1,149 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import pathlib
+import sys
+
+
+def parse_args():
+    """
+    Parse the common command-line arguments
+
+    Returns:
+        argparse.Namespace: args
+    """
+    parser = argparse.ArgumentParser(description="resnet50 example")
+
+    parser.add_argument(
+        "--model",
+        default="",
+        help="Path to the resnet50 model on the server",
+    )
+
+    parser.add_argument(
+        "--image",
+        default="",
+        help="Path to an image or a directory of images on the client",
+    )
+
+    parser.add_argument(
+        "--labels",
+        default="",
+        help="Path to the text file containing the labels on the client",
+    )
+
+    parser.add_argument(
+        "--http-port",
+        default=8998,
+        help="Port to use for HTTP server",
+    )
+
+    parser.add_argument(
+        "--grpc-port",
+        default=50051,
+        help="Port to use for gRPC server",
+    )
+
+    parser.add_argument(
+        "--top",
+        default=5,
+        help="Number of top categories to print",
+    )
+
+    parser.add_argument(
+        "--input-size",
+        default=224,
+        help="Size of the square image in pixels",
+    )
+
+    parser.add_argument(
+        "--output-classes",
+        default=1000,
+        help="Number of output classes for this model",
+    )
+
+    parser.add_argument(
+        "--input-node",
+        default="",
+        help="Name of the input node",
+    )
+
+    parser.add_argument(
+        "--output-node",
+        default="",
+        help="Name of the output node",
+    )
+
+    args = parser.parse_args()
+
+    if (not args.image) or (not args.model) or (not args.labels):
+        root = os.getenv("PROTEUS_ROOT")
+        if root is None:
+            print("PROTEUS_ROOT is not defined in the environment")
+            print("-> Needed to infer default values for arguments")
+            print("Either:\n - define PROTEUS_ROOT in the environment")
+            print(" - pass all the following flags:")
+            print("     --image")
+            print("     --model")
+            print("     --labels")
+            sys.exit(1)
+
+        if not args.image:
+            args.image = root + "/tests/assets/dog-3619020_640.jpg"
+
+        # args.model is unset and set by each example
+
+        if not args.labels:
+            args.labels = root + "/examples/resnet50/imagenet_classes.txt"
+
+    return args
+
+
+def resolve_image_paths(path: pathlib.Path):
+    """
+    Convert the input path to a list of paths. If the input path is a directory,
+    the returned list contains all the files at that path.
+
+    Args:
+        path (pathlib.Path): Input path
+
+    Returns:
+        list[str]: list of paths
+    """
+    images = []
+    if path.is_dir():
+        for file in path.glob("*"):
+            images.append(str(file))
+    else:
+        images.append(str(path))
+    return images
+
+
+def print_label(indices, path_to_labels, name):
+    """
+    Print the labels associated with the given indices
+
+    Args:
+        indices (list[int]): Indices to print
+        path_to_labels (str | pathlib.Path): path to the labels text file
+        name (str): string identifier to print with the labels
+    """
+    with open(path_to_labels, "r") as f:
+        lines = f.readlines()
+
+    print(f"Top {len(indices)} classes for {name}:")
+    for index in indices:
+        print(f"  {lines[index].strip()}")
diff --git a/examples/resnet50/resnet50.hpp b/examples/resnet50/resnet50.hpp
new file mode 100644
index 000000000..f2a0ceee3
--- /dev/null
+++ b/examples/resnet50/resnet50.hpp
@@ -0,0 +1,138 @@
+// Copyright 2022 Advanced Micro Devices, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GUARD_EXAMPLES_RESNET50_RESNET50
+#define GUARD_EXAMPLES_RESNET50_RESNET50
+
+#include <cxxopts/cxxopts.hpp>
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <vector>
+
+namespace fs = std::filesystem;
+
+struct Args {
+  fs::path path_to_model;
+  fs::path path_to_image;
+  fs::path path_to_labels;
+  uint16_t http_port = 8998;
+  uint16_t grpc_port = 50'051;
+  int top = 5;
+  int input_size = 224;
+  int output_classes = 1000;
+  std::string input_node;
+  std::string output_node;
+};
+
+Args parseArgs(int argc, char** argv) {
+  Args args;
+
+  try {
+    cxxopts::Options options("resnet50", "Run the resnet50 example");
+    // clang-format off
+    options.add_options()
+    ("model", "Path to the resnet50 model on the server",
+      cxxopts::value(args.path_to_model))
+    ("image", "Path to an image or a directory of images on the client",
+      cxxopts::value(args.path_to_image))
+    ("labels", "Path to the text file containing the labels on the client",
+      cxxopts::value(args.path_to_labels))
+    ("http-port", "Port to use for HTTP server",
+      cxxopts::value(args.http_port))
+    ("grpc-port", "Port to use for gRPC server",
+      cxxopts::value(args.grpc_port))
+    ("input-size", "Size of the square image in pixels",
+      cxxopts::value(args.input_size))
+    ("input-node", "Name of the input node",
+      cxxopts::value(args.input_node))
+    ("output-node", "Name of the output node",
+      cxxopts::value(args.output_node))
+    ("top", "Number of top categories to print",
+      cxxopts::value(args.top))
+    ("output-classes", "Number of output classes for this model",
+      cxxopts::value(args.output_classes))
+    ("help", "Print help");
+    // clang-format on
+
+    auto result = options.parse(argc, argv);
+
+    if (result.count("help") != 0U) {
+      std::cout << options.help({""}) << "\n";
+      exit(0);
+    }
+  } catch (const cxxopts::OptionException& e) {
+    std::cerr << "Error parsing options: " << e.what() << "\n";
+    exit(1);
+  }
+
+  if (args.path_to_image.empty() || args.path_to_model.empty() ||
+      args.path_to_labels.empty()) {
+    const char* root_path = std::getenv("PROTEUS_ROOT");
+    if (root_path == nullptr) {
+      std::cerr << "PROTEUS_ROOT is not defined in the environment\n";
+      std::cerr << "-> Needed to infer default values for arguments\n";
+      std::cerr << "Either:\n - define PROTEUS_ROOT in the environment\n";
+      std::cerr << " - pass all the following flags:\n";
+      std::cerr << "     --image\n";
+      std::cerr << "     --model\n";
+      std::cerr << "     --labels\n";
+      exit(1);
+    }
+    fs::path root{root_path};
+
+    if (args.path_to_image.empty()) {
+      args.path_to_image = root / "tests/assets/dog-3619020_640.jpg";
+    }
+
+    // args.path_to_model is unset and set by each example
+
+    if (args.path_to_labels.empty()) {
+      args.path_to_labels = root / "examples/resnet50/imagenet_classes.txt";
+    }
+  }
+
+  return args;
+}
+
+std::vector<std::string> resolveImagePaths(const fs::path& path_to_image) {
+  std::vector<std::string> image_paths;
+  if (fs::is_directory(path_to_image)) {
+    for (const auto& entry : fs::directory_iterator(path_to_image)) {
+      image_paths.emplace_back(entry.path().string());
+    }
+  } else {
+    image_paths.emplace_back(path_to_image.string());
+  }
+  return image_paths;
+}
+
+void printLabel(const std::vector<int>& indices, const fs::path& path_to_labels,
+                const std::string& name) {
+  std::ifstream in(path_to_labels);
+  std::vector<std::string> labels;
+
+  std::string line;
+  while (std::getline(in, line)) {
+    labels.push_back(std::move(line));
+  }
+
+  std::cout << "Top " << indices.size() << " classes for " << name << ":\n";
+  for (const auto& index : indices) {
+    std::cout << "  " << labels[index] << "\n";
+  }
+}
+
+#endif  // GUARD_EXAMPLES_RESNET50_RESNET50
diff --git a/examples/resnet50/tfzendnn.cpp b/examples/resnet50/tfzendnn.cpp
new file mode 100644
index 000000000..6478fc3ca
--- /dev/null
+++ b/examples/resnet50/tfzendnn.cpp
@@ -0,0 +1,228 @@
+// Copyright 2022 Advanced Micro Devices, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file
+ * @brief This example demonstrates how you can use the TF+ZenDNN backend to run
+ * inference on an AMD CPU with a ResNet50 TensorFlow model. Look at the
+ * documentation online for discussion around this example.
+ */
+
+#include <cassert>              // for assert
+#include <chrono>               // for duration
+#include <cstdint>              // for uint64_t
+#include <cstdlib>              // for exit, getenv
+#include <filesystem>           // for path, oper...
+#include <initializer_list>     // for initialize...
+#include <iostream>             // for operator<<
+#include <memory>               // for allocator
+#include <opencv2/imgproc.hpp>  // for COLOR_BGR2RGB
+#include <ratio>                // for milli
+#include <string>               // for string
+#include <vector>               // for vector
+
+#include "proteus/proteus.hpp"                             // for InferenceR...
+#include "proteus/util/pre_post/image_preprocess.hpp"      // for ImagePrepr...
+#include "proteus/util/pre_post/resnet50_postprocess.hpp"  // for resnet50Po...
+#include "resnet50.hpp"                                    // for Args, pars...
+
+namespace fs = std::filesystem;
+
+using Images = std::vector<std::vector<float>>;
+
+/**
+ * @brief Given a vector of paths to images, preprocess the images and return
+ * them
+ *
+ * @param paths paths to images to preprocess
+ * @return Images
+ */
+Images preprocess(const std::vector<std::string>& paths) {
+  // this example uses a custom image preprocessing function. You may use any
+  // preprocessing logic or skip it entirely if your input data is already
+  // preprocessed.
+  proteus::util::ImagePreprocessOptions<float, 3> options;
+  options.convert_color = true;
+  options.color_code = cv::COLOR_BGR2RGB;
+  options.assign = true;
+  return proteus::util::imagePreprocess(paths, options);
+}
+
+/**
+ * @brief Postprocess the output data. For ResNet50, this includes performing a
+ * softmax to determine the most probable classifications.
+ *
+ * @param output the output from the inference server
+ * @param k number of top categories to return
+ * @return std::vector<int> the indices for the top k categories
+ */
+std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
+                             int k) {
+  return proteus::util::resnet50Postprocess(
+    static_cast<const float*>(output.getData()), output.getSize(), k);
+}
+
+/**
+ * @brief Construct requests for the inference server from the input images. For
+ * ResNet50, a valid request includes a single input tensor containing a square
+ * image.
+ *
+ * @param images the input images
+ * @param input_size size of the square image in pixels
+ * @return std::vector<proteus::InferenceRequest>
+ */
+std::vector<proteus::InferenceRequest> constructRequests(const Images& images,
+                                                         uint64_t input_size) {
+  std::vector<proteus::InferenceRequest> requests;
+  requests.reserve(images.size());
+
+  const std::initializer_list<uint64_t> shape = {input_size, input_size, 3};
+
+  for (const auto& image : images) {
+    requests.emplace_back();
+    requests.back().addInputTensor((void*)image.data(), shape,
+                                   proteus::DataType::FP32);
+  }
+
+  return requests;
+}
+
+/**
+ * @brief Load a worker to handle an inference request. The load returns the
+ * endpoint you should use for subsequent requests.
+ *
+ * @param client pointer to a client object
+ * @param args the command-line arguments
+ * @return std::string
+ */
+std::string load(const proteus::Client* client, const Args& args) {
+  // Depending on how the server is compiled, it may or may not have support for
+  // a particular backend. This guard checks to make sure the server does
+  // support the requested backend. If you already know it's supported, you can
+  // skip this check.
+  if (!serverHasExtension(client, "ptzendnn")) {
+    std::cerr
+      << "TF+ZenDNN is not enabled. Please recompile with it enabled to "
+      << "run this example\n";
+    exit(1);
+  }
+
+  // Load-time parameters are used to pass one-time information to the batcher
+  // and worker as it starts up. Each worker can choose to define its own
+  // parameters that it pays attention to. Similarly, the batcher that the
+  // worker is using may have its own parameters. Check the documentation to see
+  // what may be specified.
+
+  // +load
+  proteus::RequestParameters parameters;
+  parameters.put("model", args.path_to_model);
+  parameters.put("input_size", args.input_size);
+  parameters.put("output_classes", args.output_classes);
+  parameters.put("input_node", args.input_node);
+  parameters.put("output_node", args.output_node);
+  std::string endpoint = client->workerLoad("tfzendnn", &parameters);
+  // -load
+  return endpoint;
+}
+
+/**
+ * @brief The command-line arguments are parsed in two phases. There's the
+ * common arguments that are initialized by parseArgs that are shared by all the
+ * C++ examples in this directory and then example-specific settings are
+ * initialized here
+ *
+ * @param argc number of arguments
+ * @param argv arguments
+ * @return Args
+ */
+Args getArgs(int argc, char** argv) {
+  Args args = parseArgs(argc, argv);
+
+  if (args.path_to_model.empty()) {
+    fs::path root{std::getenv("PROTEUS_ROOT")};
+    args.path_to_model =
+      root / "external/artifacts/tensorflow/resnet_v1_50_baseline_6.96B_922.pb";
+  }
+
+  if (args.input_node.empty()) {
+    args.input_node = "input";
+  }
+
+  if (args.output_node.empty()) {
+    args.output_node = "resnet_v1_50/predictions/Reshape_1";
+  }
+
+  return args;
+}
+
+int main(int argc, char* argv[]) {
+  std::cout << "Running the TF+ZenDNN example for ResNet50 in C++\n";
+
+  Args args = getArgs(argc, argv);
+
+  proteus::Server server;
+#ifdef PROTEUS_ENABLE_GRPC
+  // +start protocol
+  server.startGrpc(args.grpc_port);
+  // -start protocol
+#else
+  std::cerr << "gRPC is not enabled. Please recompile the library with it "
+            << "enabled to run this example.\n";
+  exit(1);
+#endif
+
+  // +create client
+  // tfzendnn.cpp
+  const auto grpc_port_str = std::to_string(args.grpc_port);
+  proteus::GrpcClient client{"127.0.0.1:" + grpc_port_str};
+
+  std::cout << "Waiting until the server is ready...\n";
+  proteus::waitUntilServerReady(&client);
+  // -create client
+
+  std::cout << "Loading worker...\n";
+  std::string endpoint = load(&client, args);
+
+  std::vector<std::string> paths = resolveImagePaths(args.path_to_image);
+  Images images = preprocess(paths);
+
+  std::vector<proteus::InferenceRequest> requests =
+    constructRequests(images, args.input_size);
+
+  assert(paths.size() == requests.size());
+  const auto num_requests = requests.size();
+
+  std::cout << "Making inference...\n";
+  auto start = std::chrono::high_resolution_clock::now();
+  for (auto i = 0U; i < num_requests; ++i) {
+    const proteus::InferenceRequest& request = requests[i];
+    const std::string& image_path = paths[i];
+
+    proteus::InferenceResponse response = client.modelInfer(endpoint, request);
+    assert(!response.isError());
+
+    std::vector<proteus::InferenceResponseOutput> outputs =
+      response.getOutputs();
+    // for resnet50, we expect a single output tensor
+    assert(outputs.size() == 1);
+    std::vector<int> top_indices = postprocess(outputs[0], args.top);
+    printLabel(top_indices, args.path_to_labels, image_path);
+  }
+  auto stop = std::chrono::high_resolution_clock::now();
+  std::chrono::duration<double, std::milli> duration = stop - start;
+  std::cout << "Time taken for inference, postprocessing and printing: "
+            << duration.count() << " ms\n";
+
+  return 0;
+}
diff --git a/examples/resnet50/tfzendnn.py b/examples/resnet50/tfzendnn.py
new file mode 100644
index 000000000..df1a785e4
--- /dev/null
+++ b/examples/resnet50/tfzendnn.py
@@ -0,0 +1,200 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This example demonstrates how you can use the TF+ZenDNN backend to run inference
+on an AMD CPU with a ResNet50 TensorFlow model. Look at the documentation online
+for discussion around this example.
+"""
+
+import os
+import pathlib
+import sys
+from time import sleep
+
+import cv2
+
+import proteus
+import proteus.client_operators
+import proteus.clients
+import proteus.servers
+import proteus.util.pre_post as pre_post
+
+# isort: split
+
+from resnet import parse_args, print_label, resolve_image_paths
+
+
+def preprocess(paths):
+    """
+    Given a list of paths to images, preprocess the images and return them
+
+    Args:
+        paths (list[str]): Paths to images
+
+    Returns:
+        list[numpy.ndarray]: List of images
+    """
+    options = pre_post.ImagePreprocessOptionsFloat()
+    options.convert_color = True
+    options.color_code = cv2.COLOR_BGR2RGB
+    options.assign = True
+    return pre_post.imagePreprocessFloat(paths, options)
+
+
+def postprocess(output, k):
+    """
+    Postprocess the output data. For ResNet50, this includes performing a softmax
+    to determine the most probable classifications
+
+    Args:
+        output (proteus.InferenceResponseOutput): the output from the inference server
+        k (int): number of top categories to return
+
+    Returns:
+        list[int]: indices for the top k categories
+    """
+    return pre_post.resnet50PostprocessFloat(output, k)
+
+
+def construct_requests(images):
+    """
+    Construct requests for the inference server from the input images. For ResNet50,
+    a valid request includes a single input tensor containing a square image.
+
+    Args:
+        images (list[numpy.ndarray]): the input images
+
+    Returns:
+        list[proteus.InferenceRequest]: the requests
+    """
+    requests = []
+    for image in images:
+        requests.append(proteus.ImageInferenceRequest(image))
+    return requests
+
+
+def load(client, args):
+    """
+    Load a worker to handle an inference request. The load returns the endpoint
+    you should use for subsequent requests
+
+    Args:
+        client (proteus.client.Client): the client object
+        args (argparse.Namespace): the command line arguments
+
+    Returns:
+        str: endpoint
+    """
+
+    # Depending on how the server is compiled, it may or may not have support
+    # for a particular backend. This guard checks to make sure the server does
+    # support the requested backend. If you already know it's supported, you can
+    # skip this check.
+    metadata = client.serverMetadata()
+    if "tfzendnn" not in metadata.extensions:
+        print(
+            "TF+ZenDNN is not enabled. Please recompile with it enabled to run this example"
+        )
+        sys.exit(1)
+
+    # Load-time parameters are used to pass one-time information to the batcher
+    # and worker as it starts up. Each worker can choose to define its own
+    # parameters that it pays attention to. Similarly, the batcher that the worker is
+    # using may have its own parameters. Check the documentation to see what may
+    # be specified.
+
+    parameters = proteus.RequestParameters()
+    parameters.put("model", args.model)
+    parameters.put("input_size", args.input_size)
+    parameters.put("output_classes", args.output_classes)
+    parameters.put("input_node", args.input_node)
+    parameters.put("output_node", args.output_node)
+    endpoint = client.workerLoad("tfzendnn", parameters)
+    return endpoint
+
+
+def get_args():
+    """
+    The command-line arguments are parsed in two phases. There's the common
+    arguments that are initialized by parseArgs that are shared by all the
+    Python examples in this directory and then example-specific settings are
+    initialized here
+
+    Returns:
+        argparse.Namespace: the args
+    """
+    args = parse_args()
+
+    if not args.model:
+        root = os.getenv("PROTEUS_ROOT")
+        args.model = (
+            root + "/external/artifacts/tensorflow/resnet_v1_50_baseline_6.96B_922.pb"
+        )
+
+    if not args.input_node:
+        args.input_node = "input"
+
+    if not args.output_node:
+        args.output_node = "resnet_v1_50/predictions/Reshape_1"
+
+    return args
+
+
+def main(args):
+    print("Running the TF+ZenDNN example for ResNet50 in Python")
+
+    server = proteus.servers.Server()
+    print("Waiting until the server is ready...")
+    server.startHttp(args.http_port)
+
+    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
+    ready = False
+    while not ready:
+        try:
+            ready = client.serverReady()
+        except proteus.RuntimeError:
+            pass
+        sleep(1)
+
+    print("Loading worker...")
+    endpoint = load(client, args)
+
+    ready = False
+    while not ready:
+        ready = client.modelReady(endpoint)
+
+    paths = resolve_image_paths(pathlib.Path(args.image))
+
+    images = preprocess(paths)
+
+    requests = construct_requests(images)
+
+    assert len(paths) == len(requests)
+    print("Making inferences...")
+    for image_path, request in zip(paths, requests):
+        response = client.modelInfer(endpoint, request)
+        assert not response.isError()
+
+        outputs = response.getOutputs()
+        assert len(outputs) == 1
+        top_indices = postprocess(outputs[0], args.top)
+        print_label(top_indices, args.labels, image_path)
+
+
+if __name__ == "__main__":
+
+    args = get_args()
+
+    main(args)
diff --git a/examples/resnet50/vitis.cpp b/examples/resnet50/vitis.cpp
new file mode 100644
index 000000000..8ebfc6a7e
--- /dev/null
+++ b/examples/resnet50/vitis.cpp
@@ -0,0 +1,237 @@
+// Copyright 2021 Xilinx, Inc.
+// Copyright 2022 Advanced Micro Devices, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file
+ * @brief This example demonstrates how you can use the XModel backend to run
+ * inference on an AMD FPGA with a ResNet50 XModel. Look at the documentation
+ * online for discussion around this example.
+ */
+
+#include <array>             // for array
+#include <cassert>           // for assert
+#include <chrono>            // for duration
+#include <cstdint>           // for int8_t
+#include <cstdlib>           // for exit, getenv
+#include <filesystem>        // for path, oper...
+#include <initializer_list>  // for initialize...
+#include <iostream>          // for operator<<
+#include <memory>            // for allocator
+#include <opencv2/core.hpp>  // for int8_t
+#include <ratio>             // for milli
+#include <string>            // for string
+#include <vector>            // for vector
+
+// +include:
+#include "proteus/proteus.hpp"
+// -include:
+
+#include "proteus/util/pre_post/image_preprocess.hpp"
+#include "proteus/util/pre_post/resnet50_postprocess.hpp"
+#include "resnet50.hpp"
+
+namespace fs = std::filesystem;
+
+using Images = std::vector<std::vector<int8_t>>;
+
+/**
+ * @brief Given a vector of paths to images, preprocess the images and return
+ * them
+ *
+ * @param paths paths to images to preprocess
+ * @return Images
+ */
+Images preprocess(const std::vector<std::string>& paths) {
+  const std::array<int8_t, 3> mean{123, 107, 104};
+  const std::array<int8_t, 3> std{1, 1, 1};
+
+  // this example uses a custom image preprocessing function. You may use any
+  // preprocessing logic or skip it entirely if your input data is already
+  // preprocessed.
+  proteus::util::ImagePreprocessOptions<int8_t, 3> options;
+  options.order = proteus::util::ImageOrder::NHWC;
+  options.mean = mean;
+  options.std = std;
+  options.normalize = true;
+  return proteus::util::imagePreprocess(paths, options);
+}
+
+/**
+ * @brief Postprocess the output data. For ResNet50, this includes performing a
+ * softmax to determine the most probable classifications.
+ *
+ * @param output the output from the inference server
+ * @param k number of top categories to return
+ * @return std::vector<int> the indices for the top k categories
+ */
+std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
+                             int k) {
+  return proteus::util::resnet50Postprocess(
+    static_cast<const int8_t*>(output.getData()), output.getSize(), k);
+}
+
+/**
+ * @brief Construct requests for the inference server from the input images. For
+ * ResNet50, a valid request includes a single input tensor containing a square
+ * image.
+ *
+ * @param images the input images
+ * @param input_size size of the square image in pixels
+ * @return std::vector<proteus::InferenceRequest>
+ */
+std::vector<proteus::InferenceRequest> constructRequests(const Images& images,
+                                                         uint64_t input_size) {
+  // +construct request:
+  std::vector<proteus::InferenceRequest> requests;
+  requests.reserve(images.size());
+
+  const std::initializer_list<uint64_t> shape = {input_size, input_size, 3};
+
+  for (const auto& image : images) {
+    requests.emplace_back();
+    requests.back().addInputTensor((void*)image.data(), shape,
+                                   proteus::DataType::INT8);
+  }
+  // -construct request:
+
+  return requests;
+}
+
+/**
+ * @brief Load a worker to handle an inference request. The load returns the
+ * endpoint you should use for subsequent requests.
+ *
+ * @param client pointer to a client object
+ * @param args the command-line arguments
+ * @return std::string
+ */
+std::string load(const proteus::Client* client, const Args& args) {
+  // Depending on how the server is compiled, it may or may not have support for
+  // a particular backend. This guard checks to make sure the server does
+  // support the requested backend. If you already know it's supported, you can
+  // skip this check.
+  if (!serverHasExtension(client, "vitis")) {
+    std::cerr << "Vitis AI is not enabled. Please recompile with it enabled to "
+                 "run this example\n";
+    exit(1);
+  }
+
+  // Load-time parameters are used to pass one-time information to the batcher
+  // and worker as it starts up. Each worker can choose to define its own
+  // parameters that it pays attention to. Similarly, the batcher that the
+  // worker is using may have its own parameters. Check the documentation to see
+  // what may be specified.
+
+  // +load:
+  proteus::RequestParameters parameters;
+  parameters.put("model", args.path_to_model);
+  std::string endpoint = client->workerLoad("xmodel", &parameters);
+  // -load:
+  return endpoint;
+}
+
+/**
+ * @brief The command-line arguments are parsed in two phases. There's the
+ * common arguments that are initialized by parseArgs that are shared by all the
+ * C++ examples in this directory and then example-specific settings are
+ * initialized here
+ *
+ * @param argc number of arguments
+ * @param argv arguments
+ * @return Args
+ */
+Args getArgs(int argc, char** argv) {
+  Args args = parseArgs(argc, argv);
+
+  if (args.path_to_model.empty()) {
+    fs::path root{std::getenv("PROTEUS_ROOT")};
+    args.path_to_model =
+      root /
+      "external/artifacts/u200_u250/resnet_v1_50_tf/resnet_v1_50_tf.xmodel";
+  }
+
+  return args;
+}
+
+int main(int argc, char* argv[]) {
+  std::cout << "Running the Vitis example for ResNet50 in C++\n";
+
+  Args args = getArgs(argc, argv);
+
+  // +initialize:
+  proteus::Server server;
+  // -initialize:
+#ifdef PROTEUS_ENABLE_REST
+  // +start protocol:
+  server.startHttp(args.http_port);
+  // -start protocol:
+#else
+  std::cerr << "HTTP/REST is not enabled. Please recompile with it enabled to "
+            << "run this example.\n";
+  exit(1);
+#endif
+
+  // +create client:
+  // vitis.cpp
+  const auto http_port_str = std::to_string(args.http_port);
+  proteus::HttpClient client{"http://127.0.0.1:" + http_port_str};
+
+  std::cout << "Waiting until the server is ready...\n";
+  proteus::waitUntilServerReady(&client);
+  // -create client:
+
+  std::cout << "Loading worker...\n";
+  std::string endpoint = load(&client, args);
+  // +wait model ready:
+  proteus::waitUntilModelReady(&client, endpoint);
+  // -wait model ready:
+
+  // +prepare images:
+  std::vector<std::string> paths = resolveImagePaths(args.path_to_image);
+  Images images = preprocess(paths);
+  // -prepare images:
+
+  std::vector<proteus::InferenceRequest> requests =
+    constructRequests(images, args.input_size);
+
+  assert(paths.size() == requests.size());
+  const auto num_requests = requests.size();
+
+  std::cout << "Making inferences...\n";
+  auto start = std::chrono::high_resolution_clock::now();
+  for (auto i = 0U; i < num_requests; ++i) {
+    const proteus::InferenceRequest& request = requests[i];
+    const std::string& image_path = paths[i];
+
+    // +validate:
+    // vitis.cpp
+    proteus::InferenceResponse response = client.modelInfer(endpoint, request);
+    assert(!response.isError());
+
+    std::vector<proteus::InferenceResponseOutput> outputs =
+      response.getOutputs();
+    // for resnet50, we expect a single output tensor
+    assert(outputs.size() == 1);
+    std::vector<int> top_indices = postprocess(outputs[0], args.top);
+    printLabel(top_indices, args.path_to_labels, image_path);
+    // -validate:
+  }
+  auto stop = std::chrono::high_resolution_clock::now();
+  std::chrono::duration<double, std::milli> duration = stop - start;
+  std::cout << "Time taken for inference, postprocessing and printing: "
+            << duration.count() << " ms\n";
+
+  return 0;
+}
diff --git a/examples/resnet50/vitis.py b/examples/resnet50/vitis.py
new file mode 100644
index 000000000..693c5cbee
--- /dev/null
+++ b/examples/resnet50/vitis.py
@@ -0,0 +1,210 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This example demonstrates how you can use the XModel backend to run inference
+on an AMD FPGA with a ResNet50 XModel model. Look at the documentation online for
+discussion around this example.
+"""
+
+import os
+import pathlib
+import sys
+from time import sleep
+
+# +import
+import proteus
+import proteus.clients
+import proteus.servers
+import proteus.util.pre_post as pre_post
+
+# -import
+
+# isort: split
+
+from resnet import parse_args, print_label, resolve_image_paths
+
+
+def preprocess(paths):
+    """
+    Given a list of paths to images, preprocess the images and return them
+
+    Args:
+        paths (list[str]): Paths to images
+
+    Returns:
+        list[numpy.ndarray]: List of images
+    """
+    options = pre_post.ImagePreprocessOptionsInt8()
+    options.order = pre_post.ImageOrder.NHWC
+    options.mean = [123, 107, 104]
+    options.std = [1, 1, 1]
+    options.normalize = True
+    return pre_post.imagePreprocessInt8(paths, options)
+
+
+def postprocess(output, k):
+    """
+    Postprocess the output data. For ResNet50, this includes performing a softmax
+    to determine the most probable classifications
+
+    Args:
+        output (proteus.InferenceResponseOutput): the output from the inference server
+        k (int): number of top categories to return
+
+    Returns:
+        list[int]: indices for the top k categories
+    """
+    return pre_post.resnet50PostprocessInt8(output, k)
+
+
+# +construct request
+def construct_requests(images):
+    """
+    Construct requests for the inference server from the input images. For ResNet50,
+    a valid request includes a single input tensor containing a square image.
+
+    Args:
+        images (list[numpy.ndarray]): the input images
+
+    Returns:
+        list[proteus.InferenceRequest]: the requests
+    """
+    requests = []
+    for image in images:
+        requests.append(proteus.ImageInferenceRequest(image))
+    return requests
+
+
+# -construct request
+
+
+def load(client, args):
+    """
+    Load a worker to handle an inference request. The load returns the endpoint
+    you should use for subsequent requests
+
+    Args:
+        client (proteus.client.Client): the client object
+        args (argparse.Namespace): the command line arguments
+
+    Returns:
+        str: endpoint
+    """
+
+    # Depending on how the server is compiled, it may or may not have support
+    # for a particular backend. This guard checks to make sure the server does
+    # support the requested backend. If you already know it's supported, you can
+    # skip this check.
+    metadata = client.serverMetadata()
+    if "vitis" not in metadata.extensions:
+        print(
+            "Vitis AI is not enabled. Please recompile with it enabled to run this example"
+        )
+        sys.exit(1)
+
+    # Load-time parameters are used to pass one-time information to the batcher
+    # and worker as it starts up. Each worker can choose to define its own
+    # parameters that it pays attention to. Similarly, the batcher that the worker is
+    # using may have its own parameters. Check the documentation to see what may
+    # be specified.
+
+    # +load
+    parameters = proteus.RequestParameters()
+    parameters.put("model", args.model)
+    endpoint = client.workerLoad("xmodel", parameters)
+    # -load
+    return endpoint
+
+
+def get_args():
+    """
+    The command-line arguments are parsed in two phases. There's the common
+    arguments that are initialized by parseArgs that are shared by all the
+    Python examples in this directory and then example-specific settings are
+    initialized here
+
+    Returns:
+        argparse.Namespace: the args
+    """
+    args = parse_args()
+
+    if not args.model:
+        root = os.getenv("PROTEUS_ROOT")
+        args.model = (
+            root
+            + "/external/artifacts/u200_u250/resnet_v1_50_tf/resnet_v1_50_tf.xmodel"
+        )
+
+    return args
+
+
+def main(args):
+    print("Running the Vitis example for ResNet50 in Python")
+
+    # +initialize
+    server = proteus.servers.Server()
+    # -initialize
+    print("Waiting until the server is ready...")
+    # +start protocol
+    server.startHttp(args.http_port)
+    # -start protocol
+
+    # + create client
+    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
+    # - create client
+    ready = False
+    while not ready:
+        try:
+            ready = client.serverReady()
+        except proteus.RuntimeError:
+            pass
+        sleep(1)
+
+    print("Loading worker...")
+    endpoint = load(client, args)
+
+    # +wait model ready
+    ready = False
+    while not ready:
+        ready = client.modelReady(endpoint)
+    # -wait model ready
+
+    # +prepare images
+    paths = resolve_image_paths(pathlib.Path(args.image))
+    images = preprocess(paths)
+    # -prepare images
+
+    requests = construct_requests(images)
+
+    assert len(paths) == len(requests)
+    print("Making inferences...")
+    # +run inference
+    # in vitis.py
+    for image_path, request in zip(paths, requests):
+        response = client.modelInfer(endpoint, request)
+        assert not response.isError()
+
+        outputs = response.getOutputs()
+        assert len(outputs) == 1
+        top_indices = postprocess(outputs[0], args.top)
+        print_label(top_indices, args.labels, image_path)
+    # -run inference
+
+
+if __name__ == "__main__":
+
+    args = get_args()
+
+    main(args)
diff --git a/examples/yolo/.gitignore b/examples/yolo/.gitignore
new file mode 100644
index 000000000..76ce7fcf6
--- /dev/null
+++ b/examples/yolo/.gitignore
@@ -0,0 +1,1 @@
+*.jpg
diff --git a/examples/yolo/README.rst b/examples/yolo/README.rst
new file mode 100644
index 000000000..f1d9d2996
--- /dev/null
+++ b/examples/yolo/README.rst
@@ -0,0 +1,46 @@
+..
+    Copyright 2022 Advanced Micro Devices, Inc.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+YOLO
+----
+
+This example demonstrates how to make an inference using a YOLO model with the MIGraphX backend in the inference server.
+Install the requirements to run this example with ``pip install -r requirements.txt``
+
+Introduction
+^^^^^^^^^^^^
+
+YOLO is an object detection model that accepts a single input tensor that contains a single image.
+For this image, it returns three output tensors that can be postprocessed to derive bounding boxes and labels for what objects were detected.
+The types of objects that may be detected depends on the dataset that the network is trained on.
+In these examples, the default model is a YOLOv4 and is trained on the Coco dataset and has 80 output classes.
+The output shapes are: (1, 52, 52, 3, 85) (1, 26, 26, 3, 85) (1, 13, 13, 3, 85).
+There are 3 output layers. For each layer, there are 255 outputs: 85 values per anchor, times 3 anchors.
+The 85 values of each anchor consists of 4 box coordinates describing the predicted bounding box (x, y, h, w), 1 object confidence, and 80 class confidences.
+
+For more information about this model, look at the `ONNX model <https://github.com/onnx/models/tree/main/vision/object_detection_segmentation/yolov4>`__ online.
+
+Files
+^^^^^
+
+The files in this set of examples are:
+
+::
+
+    examples/yolo
+    ├── migraphx.py              - MIGraphX example for YOLO
+    ├── requirements.txt         - Python requirements for running YOLO examples
+    ├── yolo_image_processing.py - pre- and post-processing for YOLO
+    └── yolo.py                  - common functions for all YOLO examples
diff --git a/examples/yolo/migraphx.py b/examples/yolo/migraphx.py
new file mode 100644
index 000000000..ae38c2cba
--- /dev/null
+++ b/examples/yolo/migraphx.py
@@ -0,0 +1,210 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+This example demonstrates how you can use the MIGraphX backend to run inference
+on an AMD GPU with a YOLOv4 ONNX model. Look at the documentation online for
+discussion around this example.
+
+This example, as compared to the ResNet50 one, is very similar. However, the YOLO
+model has multiple outputs unlike ResNet50's single output tensor.
+
+This example is based on a similar example in MIGraphX:
+https://github.com/ROCmSoftwarePlatform/AMDMIGraphX/blob/develop/examples/vision/python_yolov4/yolov4_inference.ipynb
+"""
+
+import os
+import pathlib
+import sys
+import time
+
+try:
+    import cv2
+    import numpy as np
+    from PIL import Image
+except ImportError:
+    print(
+        "Could not import one or more modules in yolo. Did you run 'pip install -r requirements.txt'?"
+    )
+    sys.exit(1)
+
+
+import proteus
+import proteus.clients
+
+# isort: split
+
+import yolo_image_processing as ip
+from yolo import parse_args, resolve_image_paths
+
+
+def preprocess(paths, input_size):
+    """
+    Given a list of paths to images, preprocess the images and return them
+
+    Args:
+        paths (list[str]): Paths to images
+        input_size (int): Size of the square image in pixels
+
+    Returns:
+        list[numpy.ndarray]: List of images
+    """
+
+    processed_images = []
+    original_images = []
+    for path in paths:
+        original_image = cv2.imread(path)
+        original_image = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
+        original_images.append(original_image)
+
+        img = ip.image_preprocess(np.copy(original_image), [input_size, input_size])
+        img = img[np.newaxis, ...].astype(np.float32)
+        processed_images.append(img)
+
+    return processed_images, original_images
+
+
+def load(client, args):
+    """
+    Load a worker to handle an inference request. The load returns the endpoint
+    you should use for subsequent requests
+
+    Args:
+        client (proteus.client.Client): the client object
+        args (argparse.Namespace): the command line arguments
+
+    Returns:
+        str: endpoint
+    """
+    # Depending on how the server is compiled, it may or may not have support
+    # for a particular backend. This guard checks to make sure the server does
+    # support the requested backend. If you already know it's supported, you can
+    # skip this check.
+    metadata = client.serverMetadata()
+    if "migraphx" not in metadata.extensions:
+        print(
+            "MIGraphX is not enabled. Please recompile with it enabled to run this example"
+        )
+        sys.exit(1)
+
+    # Load-time parameters are used to pass one-time information to the batcher
+    # and worker as it starts up. Each worker can choose to define its own
+    # parameters that it pays attention to. Similarly, the batcher that the worker is
+    # using may have its own parameters. Check the documentation to see what may
+    # be specified.
+
+    # The only parameter the migraphx worker requires is the model file name.
+    # batch and timeout are optional.
+    # It will take the file name stem and search for either a *.onnx or *.mxr extension, and if
+    # it finds a *.onnx file it will compile it and save the compiled model as *.mxr for
+    # future use.  It will read the array dimensions and data type from the model.
+    parameters = proteus.RequestParameters()
+    parameters.put("model", args.model)
+
+    # bpickrel: I found that allocation could fail with a large batch value of 64
+    # and large (13) default buffer count in the migraphx worker
+    # Beyond batch size 56, the worker seems to lock up while compiling the model
+    parameters.put("batch", 2)
+
+    # this call requests the server to either find a running instance of the named
+    # worker type, or else create one and initialize it with the parameters.
+    endpoint = client.workerLoad("migraphx", parameters)
+
+    # wait for the worker to load and compile model
+    ready = False
+    while not ready:
+        ready = client.modelReady(endpoint)
+
+    return endpoint
+
+
+def get_args():
+    """
+    The command-line arguments are parsed in two phases. There's the common
+    arguments that are initialized by parse_args that are shared by all the
+    Python examples in this directory and then example-specific settings are
+    initialized here
+
+    Returns:
+        argparse.Namespace: the args
+    """
+    args = parse_args()
+
+    if not args.model:
+        root = os.getenv("PROTEUS_ROOT")
+        args.model = root + "/external/artifacts/onnx/yolov4/yolov4.onnx"
+
+    return args
+
+
+def main(args):
+    print("Running the MIGraphX example for Yolo in Python")
+
+    server = proteus.servers.Server()
+    print("Waiting until the server is ready...")
+    server.startHttp(args.http_port)
+
+    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
+    ready = False
+    while not ready:
+        try:
+            ready = client.serverReady()
+        except proteus.RuntimeError:
+            pass
+        time.sleep(1)
+
+    print("Loading worker...")
+    endpoint = load(client, args)
+
+    print("Preprocessing...")
+    paths = resolve_image_paths(pathlib.Path(args.image))
+    images, original_images = preprocess(paths, args.input_size)
+
+    print("Creating inference requests...")
+    requests = [proteus.ImageInferenceRequest(image) for image in images]
+    responses = proteus.client_operators.inferAsyncOrdered(client, endpoint, requests)
+    print("Client received inference reply")
+
+    assert len(responses) == len(original_images)
+
+    for it, response in enumerate(responses):
+        assert not response.isError(), response.getError()
+
+        detections = []
+        # YOLO produces multiple output tensors and so they all need to be processed
+        for out in response.getOutputs():
+            assert out.datatype == proteus.DataType.FP32
+            this_detect = np.array(out.getFp32Data())
+            newshape = out.shape
+            # add a 0'th dimension of 1, to make 5 (the migraphx worker stripped
+            # off the batch size)
+            newshape.insert(0, 1)
+            this_detect = this_detect.reshape(newshape)
+            detections.append(this_detect)
+
+        image = ip.image_postprocess(detections, original_images[it], args)
+
+        image = Image.fromarray(image)
+        base_path = pathlib.Path(__file__).parent.resolve()
+        output_name = str(base_path / str(it)) + ".jpg"
+        image.save(output_name)
+
+        print("Your marked-up image is at " + str(output_name))
+
+
+if __name__ == "__main__":
+    args = get_args()
+
+    main(args)
diff --git a/examples/yolo/requirements.txt b/examples/yolo/requirements.txt
new file mode 100644
index 000000000..331061020
--- /dev/null
+++ b/examples/yolo/requirements.txt
@@ -0,0 +1,4 @@
+numpy
+opencv-python-headless
+Pillow
+scipy
diff --git a/examples/yolo/yolo.py b/examples/yolo/yolo.py
new file mode 100644
index 000000000..5c6490456
--- /dev/null
+++ b/examples/yolo/yolo.py
@@ -0,0 +1,118 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import pathlib
+import sys
+
+
+def parse_args():
+    """
+    Parse the common command-line arguments
+
+    Returns:
+        argparse.Namespace: args
+    """
+    parser = argparse.ArgumentParser(description="yolov4 example")
+
+    parser.add_argument(
+        "--model",
+        default="",
+        help="Path to the yolo model on the server",
+    )
+
+    parser.add_argument(
+        "--image",
+        default="",
+        help="Path to an image or a directory of images on the client",
+    )
+
+    parser.add_argument(
+        "--input-size",
+        default=416,
+        help="Size of the square image in pixels",
+    )
+
+    parser.add_argument(
+        "--labels",
+        default="",
+        help="Path to the text file containing the labels on the client",
+    )
+
+    parser.add_argument(
+        "--anchors",
+        default="",
+        help="Path to the text file containing the anchors on the client",
+    )
+
+    parser.add_argument(
+        "--http-port",
+        default=8998,
+        help="Port to use for HTTP server",
+    )
+
+    parser.add_argument(
+        "--grpc-port",
+        default=50051,
+        help="Port to use for gRPC server",
+    )
+
+    args = parser.parse_args()
+
+    if (not args.image) or (not args.model) or (not args.labels) or (not args.anchors):
+        root = os.getenv("PROTEUS_ROOT")
+        if root is None:
+            print("PROTEUS_ROOT is not defined in the environment")
+            print("-> Needed to infer default values for arguments")
+            print("Either:\n - define PROTEUS_ROOT in the environment")
+            print(" - pass all the following flags:")
+            print("     --image")
+            print("     --model")
+            print("     --labels")
+            print("     --anchors")
+            sys.exit(1)
+
+        if not args.image:
+            args.image = root + "/tests/assets/bicycle-384566_640.jpg"
+
+        # args.model is unset and set by each example
+
+        if not args.labels:
+            args.labels = root + "/external/artifacts/onnx/yolov4/coco.names"
+
+        if not args.anchors:
+            args.anchors = root + "/external/artifacts/onnx/yolov4/yolov4_anchors.txt"
+
+    return args
+
+
+def resolve_image_paths(path: pathlib.Path):
+    """
+    Convert the input path to a list of paths. If the input path is a directory,
+    the returned list contains all the files at that path.
+
+    Args:
+        path (pathlib.Path): Input path
+
+    Returns:
+        list[str]: list of paths
+    """
+    images = []
+    if path.is_dir():
+        for file in path.glob("*"):
+            images.append(str(file))
+    else:
+        images.append(str(path))
+    return images
diff --git a/examples/python/yolo_image_processing.py b/examples/yolo/yolo_image_processing.py
similarity index 67%
rename from examples/python/yolo_image_processing.py
rename to examples/yolo/yolo_image_processing.py
index bf07ea4d3..4090d2c39 100644
--- a/examples/python/yolo_image_processing.py
+++ b/examples/yolo/yolo_image_processing.py
@@ -24,18 +24,25 @@
 # All pre- and post-processing methods used below are borrowed from the ONNX MOdel Zoo
 # https://github.com/onnx/models/tree/master/vision/object_detection_segmentation/yolov4
 
-'''
+"""
 Custom pre-and post-processing of images for running Yolo object detection models.
 This script is a slight modification of an example found in the AMDMIGraphX repository at
 https://github.com/ROCmSoftwarePlatform/AMDMIGraphX/raw/develop/examples/vision/python_yolov4/image_processing.py
-'''
+"""
 
-import numpy as np
-import cv2
-from scipy import special
 import colorsys
 import random
-
+import sys
+
+try:
+    import cv2
+    import numpy as np
+    from scipy import special
+except ImportError:
+    print(
+        "Could not import one or more modules in yolo. Did you run 'pip install -r requirements.txt'?"
+    )
+    sys.exit(1)
 
 # this function is from tensorflow-yolov4-tflite/core/utils.py
 def image_preprocess(image, target_size, gt_boxes=None):
@@ -49,8 +56,8 @@ def image_preprocess(image, target_size, gt_boxes=None):
 
     image_padded = np.full(shape=[ih, iw, 3], fill_value=128.0)
     dw, dh = (iw - nw) // 2, (ih - nh) // 2
-    image_padded[dh:nh + dh, dw:nw + dw, :] = image_resized
-    image_padded = image_padded / 255.
+    image_padded[dh : nh + dh, dw : nw + dw, :] = image_resized
+    image_padded = image_padded / 255.0
 
     if gt_boxes is None:
         return image_padded
@@ -62,17 +69,17 @@ def image_preprocess(image, target_size, gt_boxes=None):
 
 
 def get_anchors(anchors_path, tiny=False):
-    '''loads the anchors from a file'''
+    """loads the anchors from a file"""
     with open(anchors_path) as f:
         anchors = f.readline()
-    anchors = np.array(anchors.split(','), dtype=np.float32)
+    anchors = np.array(anchors.split(","), dtype=np.float32)
     return anchors.reshape(3, 3, 2)
 
 
 def postprocess_bbbox(pred_bbox, ANCHORS, STRIDES, XYSCALE=[1, 1, 1]):
-    '''define anchor boxes
-    
-    Input:  pred_bbox, a list of 3 np.ndarray returned from the inference. '''
+    """define anchor boxes
+
+    Input:  pred_bbox, a list of 3 np.ndarray returned from the inference."""
     for i, pred in enumerate(pred_bbox):
         conv_shape = pred.shape
         output_size = conv_shape[1]
@@ -84,9 +91,12 @@ def postprocess_bbbox(pred_bbox, ANCHORS, STRIDES, XYSCALE=[1, 1, 1]):
         xy_grid = np.tile(np.expand_dims(xy_grid, axis=0), [1, 1, 1, 3, 1])
         xy_grid = xy_grid.astype(np.float)
 
-        pred_xy = ((special.expit(conv_raw_dxdy) * XYSCALE[i]) - 0.5 *
-                   (XYSCALE[i] - 1) + xy_grid) * STRIDES[i]
-        pred_wh = (np.exp(conv_raw_dwdh) * ANCHORS[i])
+        pred_xy = (
+            (special.expit(conv_raw_dxdy) * XYSCALE[i])
+            - 0.5 * (XYSCALE[i] - 1)
+            + xy_grid
+        ) * STRIDES[i]
+        pred_wh = np.exp(conv_raw_dwdh) * ANCHORS[i]
         pred[:, :, :, :, 0:4] = np.concatenate([pred_xy, pred_wh], axis=-1)
 
     pred_bbox = [np.reshape(x, (-1, np.shape(x)[-1])) for x in pred_bbox]
@@ -95,7 +105,7 @@ def postprocess_bbbox(pred_bbox, ANCHORS, STRIDES, XYSCALE=[1, 1, 1]):
 
 
 def postprocess_boxes(pred_bbox, org_img_shape, input_size, score_threshold):
-    '''remove boundary boxs with a low detection probability'''
+    """remove boundary boxs with a low detection probability"""
     valid_scale = [0, np.inf]
     pred_bbox = np.array(pred_bbox)
 
@@ -104,11 +114,13 @@ def postprocess_boxes(pred_bbox, org_img_shape, input_size, score_threshold):
     pred_prob = pred_bbox[:, 5:]
 
     # (1) (x, y, w, h) --> (xmin, ymin, xmax, ymax)
-    pred_coor = np.concatenate([
-        pred_xywh[:, :2] - pred_xywh[:, 2:] * 0.5,
-        pred_xywh[:, :2] + pred_xywh[:, 2:] * 0.5
-    ],
-                               axis=-1)
+    pred_coor = np.concatenate(
+        [
+            pred_xywh[:, :2] - pred_xywh[:, 2:] * 0.5,
+            pred_xywh[:, :2] + pred_xywh[:, 2:] * 0.5,
+        ],
+        axis=-1,
+    )
     # (2) (xmin, ymin, xmax, ymax) -> (xmin_org, ymin_org, xmax_org, ymax_org)
     org_h, org_w = org_img_shape
     resize_ratio = min(input_size / org_w, input_size / org_h)
@@ -120,20 +132,25 @@ def postprocess_boxes(pred_bbox, org_img_shape, input_size, score_threshold):
     pred_coor[:, 1::2] = 1.0 * (pred_coor[:, 1::2] - dh) / resize_ratio
 
     # (3) clip some boxes that are out of range
-    pred_coor = np.concatenate([
-        np.maximum(pred_coor[:, :2], [0, 0]),
-        np.minimum(pred_coor[:, 2:], [org_w - 1, org_h - 1])
-    ],
-                               axis=-1)
-    invalid_mask = np.logical_or((pred_coor[:, 0] > pred_coor[:, 2]),
-                                 (pred_coor[:, 1] > pred_coor[:, 3]))
+    pred_coor = np.concatenate(
+        [
+            np.maximum(pred_coor[:, :2], [0, 0]),
+            np.minimum(pred_coor[:, 2:], [org_w - 1, org_h - 1]),
+        ],
+        axis=-1,
+    )
+    invalid_mask = np.logical_or(
+        (pred_coor[:, 0] > pred_coor[:, 2]), (pred_coor[:, 1] > pred_coor[:, 3])
+    )
     pred_coor[invalid_mask] = 0
 
     # (4) discard some invalid boxes
     bboxes_scale = np.sqrt(
-        np.multiply.reduce(pred_coor[:, 2:4] - pred_coor[:, 0:2], axis=-1))
-    scale_mask = np.logical_and((valid_scale[0] < bboxes_scale),
-                                (bboxes_scale < valid_scale[1]))
+        np.multiply.reduce(pred_coor[:, 2:4] - pred_coor[:, 0:2], axis=-1)
+    )
+    scale_mask = np.logical_and(
+        (valid_scale[0] < bboxes_scale), (bboxes_scale < valid_scale[1])
+    )
 
     # (5) discard some boxes with low scores
     classes = np.argmax(pred_prob, axis=-1)
@@ -143,18 +160,17 @@ def postprocess_boxes(pred_bbox, org_img_shape, input_size, score_threshold):
     coors, scores, classes = pred_coor[mask], scores[mask], classes[mask]
 
     return np.concatenate(
-        [coors, scores[:, np.newaxis], classes[:, np.newaxis]], axis=-1)
+        [coors, scores[:, np.newaxis], classes[:, np.newaxis]], axis=-1
+    )
 
 
 def bboxes_iou(boxes1, boxes2):
-    '''calculate the Intersection Over Union value'''
+    """calculate the Intersection Over Union value"""
     boxes1 = np.array(boxes1)
     boxes2 = np.array(boxes2)
 
-    boxes1_area = (boxes1[..., 2] - boxes1[..., 0]) * (boxes1[..., 3] -
-                                                       boxes1[..., 1])
-    boxes2_area = (boxes2[..., 2] - boxes2[..., 0]) * (boxes2[..., 3] -
-                                                       boxes2[..., 1])
+    boxes1_area = (boxes1[..., 2] - boxes1[..., 0]) * (boxes1[..., 3] - boxes1[..., 1])
+    boxes2_area = (boxes2[..., 2] - boxes2[..., 0]) * (boxes2[..., 3] - boxes2[..., 1])
 
     left_up = np.maximum(boxes1[..., :2], boxes2[..., :2])
     right_down = np.minimum(boxes1[..., 2:], boxes2[..., 2:])
@@ -167,7 +183,7 @@ def bboxes_iou(boxes1, boxes2):
     return ious
 
 
-def nms(bboxes, iou_threshold, sigma=0.3, method='nms'):
+def nms(bboxes, iou_threshold, sigma=0.3, method="nms"):
     """
     :param bboxes: (xmin, ymin, xmax, ymax, score, class)
     Note: soft-nms, https://arxiv.org/pdf/1704.04503.pdf
@@ -177,7 +193,7 @@ def nms(bboxes, iou_threshold, sigma=0.3, method='nms'):
     best_bboxes = []
 
     for cls in classes_in_img:
-        cls_mask = (bboxes[:, 5] == cls)
+        cls_mask = bboxes[:, 5] == cls
         cls_bboxes = bboxes[cls_mask]
 
         while len(cls_bboxes) > 0:
@@ -185,51 +201,41 @@ def nms(bboxes, iou_threshold, sigma=0.3, method='nms'):
             best_bbox = cls_bboxes[max_ind]
             best_bboxes.append(best_bbox)
             cls_bboxes = np.concatenate(
-                [cls_bboxes[:max_ind], cls_bboxes[max_ind + 1:]])
+                [cls_bboxes[:max_ind], cls_bboxes[max_ind + 1 :]]
+            )
             iou = bboxes_iou(best_bbox[np.newaxis, :4], cls_bboxes[:, :4])
-            weight = np.ones((len(iou), ), dtype=np.float32)
+            weight = np.ones((len(iou),), dtype=np.float32)
 
-            assert method in ['nms', 'soft-nms']
+            assert method in ["nms", "soft-nms"]
 
-            if method == 'nms':
+            if method == "nms":
                 iou_mask = iou > iou_threshold
                 weight[iou_mask] = 0.0
 
-            if method == 'soft-nms':
+            if method == "soft-nms":
                 weight = np.exp(-(1.0 * iou**2 / sigma))
 
             cls_bboxes[:, 4] = cls_bboxes[:, 4] * weight
-            score_mask = cls_bboxes[:, 4] > 0.
+            score_mask = cls_bboxes[:, 4] > 0.0
             cls_bboxes = cls_bboxes[score_mask]
 
     return best_bboxes
 
 
-def read_class_names(class_file_name):
-    '''loads class names from a file'''
-    names = {}
-    with open(class_file_name, 'r') as data:
-        for ID, name in enumerate(data):
-            names[ID] = name.strip('\n')
-    return names
-
-
-def draw_bbox(image,
-              bboxes,
-              labels_file,
-              show_label=True):
+def draw_bbox(image, bboxes, labels_file, show_label=True):
     """
     bboxes: [x_min, y_min, x_max, y_max, probability, cls_id] format coordinates.
     """
-    classes=read_class_names(labels_file)
+    with open(labels_file, "r") as f:
+        classes = f.read().splitlines()
 
     num_classes = len(classes)
     image_h, image_w, _ = image.shape
-    hsv_tuples = [(1.0 * x / num_classes, 1., 1.) for x in range(num_classes)]
+    hsv_tuples = [(1.0 * x / num_classes, 1.0, 1.0) for x in range(num_classes)]
     colors = list(map(lambda x: colorsys.hsv_to_rgb(*x), hsv_tuples))
     colors = list(
-        map(lambda x: (int(x[0] * 255), int(x[1] * 255), int(x[2] * 255)),
-            colors))
+        map(lambda x: (int(x[0] * 255), int(x[1] * 255), int(x[2] * 255)), colors)
+    )
 
     random.seed(0)
     random.shuffle(colors)
@@ -246,21 +252,45 @@ def draw_bbox(image,
         cv2.rectangle(image, c1, c2, bbox_color, bbox_thick)
 
         if show_label:
-            bbox_mess = '%s: %.2f' % (classes[class_ind], score)
-            t_size = cv2.getTextSize(bbox_mess,
-                                     0,
-                                     fontScale,
-                                     thickness=bbox_thick // 2)[0]
-            cv2.rectangle(image, c1,
-                          (c1[0] + t_size[0], c1[1] - t_size[1] - 3),
-                          bbox_color, -1)
-
-            cv2.putText(image,
-                        bbox_mess, (c1[0], c1[1] - 2),
-                        cv2.FONT_HERSHEY_SIMPLEX,
-                        fontScale, (0, 0, 0),
-                        bbox_thick // 2,
-                        lineType=cv2.LINE_AA)
+            bbox_mess = "%s: %.2f" % (classes[class_ind], score)
+            t_size = cv2.getTextSize(
+                bbox_mess, 0, fontScale, thickness=bbox_thick // 2
+            )[0]
+            cv2.rectangle(
+                image, c1, (c1[0] + t_size[0], c1[1] - t_size[1] - 3), bbox_color, -1
+            )
+
+            cv2.putText(
+                image,
+                bbox_mess,
+                (c1[0], c1[1] - 2),
+                cv2.FONT_HERSHEY_SIMPLEX,
+                fontScale,
+                (0, 0, 0),
+                bbox_thick // 2,
+                lineType=cv2.LINE_AA,
+            )
 
     return image
-    
\ No newline at end of file
+
+
+def image_postprocess(detections, original_image, args):
+    # Post-process the model outputs and display image with detection bounding boxes
+    STRIDES = [8, 16, 32]
+    XYSCALE = [1.2, 1.1, 1.05]
+
+    anchors = get_anchors(args.anchors)
+    STRIDES = np.array(STRIDES)
+
+    pred_bbox = postprocess_bbbox(detections, anchors, STRIDES, XYSCALE)
+
+    original_image_size = original_image.shape[:2]
+    bounding_boxes = postprocess_boxes(
+        pred_bbox, original_image_size, args.input_size, 0.25
+    )
+    bounding_boxes = nms(bounding_boxes, 0.213, method="nms")
+    return draw_bbox(
+        original_image,
+        bounding_boxes,
+        args.labels,
+    )
diff --git a/include/proteus/clients/client.hpp b/include/proteus/clients/client.hpp
index 26e1c52b0..5c7f46ff2 100644
--- a/include/proteus/clients/client.hpp
+++ b/include/proteus/clients/client.hpp
@@ -32,33 +32,35 @@ class Client {
  public:
   virtual ~Client() = default;
 
-  virtual ServerMetadata serverMetadata() = 0;
-  virtual bool serverLive() = 0;
-  virtual bool serverReady() = 0;
-  virtual bool modelReady(const std::string& model) = 0;
-  virtual ModelMetadata modelMetadata(const std::string& model) = 0;
+  virtual ServerMetadata serverMetadata() const = 0;
+  virtual bool serverLive() const = 0;
+  virtual bool serverReady() const = 0;
+  virtual bool modelReady(const std::string& model) const = 0;
+  virtual ModelMetadata modelMetadata(const std::string& model) const = 0;
 
   virtual void modelLoad(const std::string& model,
-                         RequestParameters* parameters) = 0;
-  virtual void modelUnload(const std::string& model) = 0;
+                         RequestParameters* parameters) const = 0;
+  virtual void modelUnload(const std::string& model) const = 0;
 
-  virtual InferenceResponse modelInfer(const std::string& model,
-                                       const InferenceRequest& request) = 0;
+  virtual InferenceResponse modelInfer(
+    const std::string& model, const InferenceRequest& request) const = 0;
   virtual InferenceResponseFuture modelInferAsync(
-    const std::string& model, const InferenceRequest& request) = 0;
-  virtual std::vector<std::string> modelList() = 0;
+    const std::string& model, const InferenceRequest& request) const = 0;
+  virtual std::vector<std::string> modelList() const = 0;
 
   virtual std::string workerLoad(const std::string& worker,
-                                 RequestParameters* parameters) = 0;
-  virtual void workerUnload(const std::string& worker) = 0;
+                                 RequestParameters* parameters) const = 0;
+  virtual void workerUnload(const std::string& worker) const = 0;
 
-  virtual bool hasHardware(const std::string& name, int num) = 0;
+  virtual bool hasHardware(const std::string& name, int num) const = 0;
 
  protected:
   Client();
 };
 
-bool serverHasExtension(Client* client, const std::string& extension);
+bool serverHasExtension(const Client* client, const std::string& extension);
+void waitUntilServerReady(const Client* client);
+void waitUntilModelReady(const Client* client, const std::string& model);
 
 }  // namespace proteus
 
diff --git a/include/proteus/clients/grpc.hpp b/include/proteus/clients/grpc.hpp
index 133de9c4c..be3e03ac2 100644
--- a/include/proteus/clients/grpc.hpp
+++ b/include/proteus/clients/grpc.hpp
@@ -25,7 +25,7 @@
 #include <vector>  // for vector
 
 #include "proteus/clients/client.hpp"    // IWYU pragma: export
-#include "proteus/core/predict_api.hpp"  // for InferenceRequest (ptr only)
+#include "proteus/core/predict_api.hpp"  // for InferenceRequest (ptr only) const
 
 namespace grpc {
 class Channel;
@@ -39,31 +39,31 @@ class GrpcClient : public Client {
   explicit GrpcClient(const std::shared_ptr<::grpc::Channel>& channel);
   ~GrpcClient() override;
 
-  ServerMetadata serverMetadata() override;
-  bool serverLive() override;
-  bool serverReady() override;
-  bool modelReady(const std::string& model) override;
-  ModelMetadata modelMetadata(const std::string& model) override;
+  ServerMetadata serverMetadata() const override;
+  bool serverLive() const override;
+  bool serverReady() const override;
+  bool modelReady(const std::string& model) const override;
+  ModelMetadata modelMetadata(const std::string& model) const override;
 
   void modelLoad(const std::string& model,
-                 RequestParameters* parameters) override;
-  void modelUnload(const std::string& model) override;
+                 RequestParameters* parameters) const override;
+  void modelUnload(const std::string& model) const override;
 
   InferenceResponse modelInfer(const std::string& model,
-                               const InferenceRequest& request) override;
+                               const InferenceRequest& request) const override;
   InferenceResponseFuture modelInferAsync(
-    const std::string& model, const InferenceRequest& request) override;
-  std::vector<std::string> modelList() override;
+    const std::string& model, const InferenceRequest& request) const override;
+  std::vector<std::string> modelList() const override;
   // int streamModelInferStart(const std::string& model,
   //                           const InferenceRequest& request,
-  //                           RequestParameters& metadata);
-  // bool streamModelInfer(int index, InferenceResponse& response);
+  //                           RequestParameters& metadata) const;
+  // bool streamModelInfer(int index, InferenceResponse& response) const;
 
   std::string workerLoad(const std::string& worker,
-                         RequestParameters* parameters) override;
-  void workerUnload(const std::string& worker) override;
+                         RequestParameters* parameters) const override;
+  void workerUnload(const std::string& worker) const override;
 
-  bool hasHardware(const std::string& name, int num) override;
+  bool hasHardware(const std::string& name, int num) const override;
 
  private:
   class GrpcClientImpl;
diff --git a/include/proteus/clients/http.hpp b/include/proteus/clients/http.hpp
index cadde6a9b..6c6799837 100644
--- a/include/proteus/clients/http.hpp
+++ b/include/proteus/clients/http.hpp
@@ -31,52 +31,37 @@
 
 namespace proteus {
 
-// /**
-//  * @brief Start the HTTP server for collecting metrics. This is a no-op if
-//  * Proteus is compiled without HTTP support.
-//  *
-//  * @param port port to use
-//  */
-// void startHttpServer(int port);
-
-/**
- * @brief Stop the HTTP server. This is a no-op if Proteus is compiled without
- * HTTP support.
- *
- */
-// void stopHttpServer();
-
 class HttpClient : public Client {
  public:
   HttpClient(std::string address, const StringMap& headers = {},
              int parallelism = 32);
   HttpClient(HttpClient const&);
-  HttpClient& operator=(const HttpClient&) = delete;
+  HttpClient& operator=(const HttpClient&) const = delete;
   HttpClient(HttpClient&& other) noexcept;
-  HttpClient& operator=(HttpClient&& other) noexcept = delete;
+  HttpClient& operator=(HttpClient&& other) const noexcept = delete;
   ~HttpClient() override;
 
-  ServerMetadata serverMetadata() override;
-  bool serverLive() override;
-  bool serverReady() override;
-  bool modelReady(const std::string& model) override;
-  ModelMetadata modelMetadata(const std::string& model) override;
+  ServerMetadata serverMetadata() const override;
+  bool serverLive() const override;
+  bool serverReady() const override;
+  bool modelReady(const std::string& model) const override;
+  ModelMetadata modelMetadata(const std::string& model) const override;
 
   void modelLoad(const std::string& model,
-                 RequestParameters* parameters) override;
-  void modelUnload(const std::string& model) override;
+                 RequestParameters* parameters) const override;
+  void modelUnload(const std::string& model) const override;
 
   InferenceResponse modelInfer(const std::string& model,
-                               const InferenceRequest& request) override;
+                               const InferenceRequest& request) const override;
   InferenceResponseFuture modelInferAsync(
-    const std::string& model, const InferenceRequest& request) override;
-  std::vector<std::string> modelList() override;
+    const std::string& model, const InferenceRequest& request) const override;
+  std::vector<std::string> modelList() const override;
 
   std::string workerLoad(const std::string& worker,
-                         RequestParameters* parameters) override;
-  void workerUnload(const std::string& worker) override;
+                         RequestParameters* parameters) const override;
+  void workerUnload(const std::string& worker) const override;
 
-  bool hasHardware(const std::string& name, int num) override;
+  bool hasHardware(const std::string& name, int num) const override;
 
   const std::string& getAddress() const&;
   std::string getAddress() const&&;
diff --git a/include/proteus/clients/native.hpp b/include/proteus/clients/native.hpp
index 7fcd934b2..9d86f44d6 100644
--- a/include/proteus/clients/native.hpp
+++ b/include/proteus/clients/native.hpp
@@ -24,7 +24,7 @@
 #include <vector>  // for vector
 
 #include "proteus/clients/client.hpp"    // IWYU pragma: export
-#include "proteus/core/predict_api.hpp"  // for InferenceRequest (ptr only)
+#include "proteus/core/predict_api.hpp"  // for InferenceRequest (ptr only) const
 #include "proteus/declarations.hpp"      // for InferenceResponseFuture
 
 // IWYU pragma: no_forward_declare proteus::RequestParameters
@@ -33,18 +33,18 @@ namespace proteus {
 
 class NativeClient : public Client {
  public:
-  ServerMetadata serverMetadata() override;
-  bool serverLive() override;
-  bool serverReady() override;
+  ServerMetadata serverMetadata() const override;
+  bool serverLive() const override;
+  bool serverReady() const override;
 
   /**
    * @brief Check if a worker is ready
    *
    * @param worker name of the worker to check if ready
    */
-  bool modelReady(const std::string& model) override;
+  bool modelReady(const std::string& model) const override;
 
-  ModelMetadata modelMetadata(const std::string& model) override;
+  ModelMetadata modelMetadata(const std::string& model) const override;
 
   /**
    * @brief Load a worker
@@ -55,16 +55,16 @@ class NativeClient : public Client {
    * requests
    */
   void modelLoad(const std::string& model,
-                 RequestParameters* parameters) override;
+                 RequestParameters* parameters) const override;
   /**
    * @brief Unload a worker
    *
    * @param worker name of the worker to unload
    */
-  void modelUnload(const std::string& model) override;
+  void modelUnload(const std::string& model) const override;
 
   InferenceResponse modelInfer(const std::string& model,
-                               const InferenceRequest& request) override;
+                               const InferenceRequest& request) const override;
   /**
    * @brief Enqueue an inference request to Proteus
    *
@@ -73,14 +73,15 @@ class NativeClient : public Client {
    * @return InferenceResponseFuture a future to get the results of the request
    */
   InferenceResponseFuture modelInferAsync(
-    const std::string& workerName, const InferenceRequest& request) override;
-  std::vector<std::string> modelList() override;
+    const std::string& workerName,
+    const InferenceRequest& request) const override;
+  std::vector<std::string> modelList() const override;
 
   std::string workerLoad(const std::string& worker,
-                         RequestParameters* parameters) override;
-  void workerUnload(const std::string& worker) override;
+                         RequestParameters* parameters) const override;
+  void workerUnload(const std::string& worker) const override;
 
-  bool hasHardware(const std::string& name, int num) override;
+  bool hasHardware(const std::string& name, int num) const override;
 };
 
 }  // namespace proteus
diff --git a/include/proteus/clients/websocket.hpp b/include/proteus/clients/websocket.hpp
index 361d40deb..e9a1dc833 100644
--- a/include/proteus/clients/websocket.hpp
+++ b/include/proteus/clients/websocket.hpp
@@ -25,7 +25,7 @@
 #include <vector>  // for vector
 
 #include "proteus/clients/client.hpp"    // IWYU pragma: export
-#include "proteus/core/predict_api.hpp"  // for InferenceRequest (ptr only)
+#include "proteus/core/predict_api.hpp"  // for InferenceRequest (ptr only) const
 
 namespace proteus {
 
@@ -36,31 +36,32 @@ class WebSocketClient : public Client {
                   const std::string& http_address);
   ~WebSocketClient();
 
-  ServerMetadata serverMetadata() override;
-  bool serverLive() override;
-  bool serverReady() override;
-  bool modelReady(const std::string& model) override;
-  ModelMetadata modelMetadata(const std::string& model) override;
+  ServerMetadata serverMetadata() const override;
+  bool serverLive() const override;
+  bool serverReady() const override;
+  bool modelReady(const std::string& model) const override;
+  ModelMetadata modelMetadata(const std::string& model) const override;
 
   void modelLoad(const std::string& model,
-                 RequestParameters* parameters) override;
-  void modelUnload(const std::string& model) override;
+                 RequestParameters* parameters) const override;
+  void modelUnload(const std::string& model) const override;
   InferenceResponse modelInfer(const std::string& model,
-                               const InferenceRequest& request) override;
+                               const InferenceRequest& request) const override;
   InferenceResponseFuture modelInferAsync(
-    const std::string& model, const InferenceRequest& request) override;
-  std::vector<std::string> modelList() override;
+    const std::string& model, const InferenceRequest& request) const override;
+  std::vector<std::string> modelList() const override;
 
   std::string workerLoad(const std::string& worker,
-                         RequestParameters* parameters) override;
-  void workerUnload(const std::string& worker) override;
+                         RequestParameters* parameters) const override;
+  void workerUnload(const std::string& worker) const override;
 
-  bool hasHardware(const std::string& name, int num) override;
+  bool hasHardware(const std::string& name, int num) const override;
 
-  void modelInferWs(const std::string& model, const InferenceRequest& request);
-  // TODO(varunsh): change to InferenceReponse
-  std::string modelRecv();
-  void close();
+  void modelInferWs(const std::string& model,
+                    const InferenceRequest& request) const;
+  // TODO(varunsh) const: change to InferenceReponse
+  std::string modelRecv() const;
+  void close() const;
 
  private:
   class WebSocketClientImpl;
diff --git a/include/proteus/core/exceptions.hpp b/include/proteus/core/exceptions.hpp
index 55ff5ff44..0b59aa95b 100644
--- a/include/proteus/core/exceptions.hpp
+++ b/include/proteus/core/exceptions.hpp
@@ -34,6 +34,10 @@ class bad_status : public runtime_error {
   using runtime_error::runtime_error;
 };
 
+class connection_error : public bad_status {
+  using bad_status::bad_status;
+};
+
 class file_not_found_error : public runtime_error {
   using runtime_error::runtime_error;
 };
diff --git a/src/proteus/bindings/python/src/proteus/__init__.py b/src/proteus/bindings/python/src/proteus/__init__.py
index 481e1b9df..1b3eac8aa 100644
--- a/src/proteus/bindings/python/src/proteus/__init__.py
+++ b/src/proteus/bindings/python/src/proteus/__init__.py
@@ -49,13 +49,13 @@ def _set_data(input_n, image):
     return input_n
 
 
-def ImageInferenceRequest(images, asTensor=False):
+def ImageInferenceRequest(images, asTensor=True):
     """
     Construct a request from an image or list of images
 
     Args:
         images (image): Images may be numpy arrays or filepaths or a list of these
-        asTensor (bool, optional): Send data as a tensor or as base64-encoded string. Defaults to False.
+        asTensor (bool, optional): Send data as a tensor or as base64-encoded string. Defaults to True.
 
     Raises:
         TypeError: Raised if an unknown image format is passed
diff --git a/src/proteus/bindings/python/util/util.cpp b/src/proteus/bindings/python/util/util.cpp
index 37f3c016f..05da96ac6 100644
--- a/src/proteus/bindings/python/util/util.cpp
+++ b/src/proteus/bindings/python/util/util.cpp
@@ -36,6 +36,7 @@ template <typename T>
 void addPreprocessOptions(const py::module& m, const char* name) {
   py::class_<ImagePreprocessOptions<T>>(m, name)
     .def(py::init<>())
+    .def_readwrite("assign", &ImagePreprocessOptions<T>::assign)
     .def_readwrite("height", &ImagePreprocessOptions<T>::height)
     .def_readwrite("width", &ImagePreprocessOptions<T>::width)
     .def_readwrite("channels", &ImagePreprocessOptions<T>::channels)
diff --git a/src/proteus/clients/client.cpp b/src/proteus/clients/client.cpp
index 860944c1e..09cf27d2f 100644
--- a/src/proteus/clients/client.cpp
+++ b/src/proteus/clients/client.cpp
@@ -36,9 +36,28 @@ void initializeClientLogging() {
 
 Client::Client() { initializeClientLogging(); }
 
-bool serverHasExtension(Client* client, const std::string& extension) {
+bool serverHasExtension(const Client* client, const std::string& extension) {
   auto metadata = client->serverMetadata();
   return metadata.extensions.find(extension) != metadata.extensions.end();
 }
 
+void waitUntilServerReady(const Client* client) {
+  bool ready = false;
+  while (!ready) {
+    try {
+      ready = client->serverReady();
+    } catch (const proteus::connection_error&) {
+      // ignore connection errors
+      std::this_thread::sleep_for(std::chrono::seconds(1));
+    }
+  }
+}
+
+void waitUntilModelReady(const Client* client, const std::string& model) {
+  bool ready = false;
+  while (!ready) {
+    ready = client->modelReady(model);
+  }
+}
+
 }  // namespace proteus
diff --git a/src/proteus/clients/grpc.cpp b/src/proteus/clients/grpc.cpp
index 1f68ad27c..6ad1df49e 100644
--- a/src/proteus/clients/grpc.cpp
+++ b/src/proteus/clients/grpc.cpp
@@ -70,7 +70,7 @@ GrpcClient::GrpcClient(const std::shared_ptr<::grpc::Channel>& channel) {
 
 GrpcClient::~GrpcClient() = default;
 
-ServerMetadata GrpcClient::serverMetadata() {
+ServerMetadata GrpcClient::serverMetadata() const {
   inference::ServerMetadataRequest request;
   inference::ServerMetadataResponse reply;
 
@@ -88,7 +88,7 @@ ServerMetadata GrpcClient::serverMetadata() {
   throw bad_status(status.error_message());
 }
 
-bool GrpcClient::serverLive() {
+bool GrpcClient::serverLive() const {
   inference::ServerLiveRequest request;
   inference::ServerLiveResponse reply;
 
@@ -106,7 +106,7 @@ bool GrpcClient::serverLive() {
   throw bad_status(status.error_message());
 }
 
-bool GrpcClient::serverReady() {
+bool GrpcClient::serverReady() const {
   inference::ServerReadyRequest request;
   inference::ServerReadyResponse reply;
 
@@ -121,7 +121,7 @@ bool GrpcClient::serverReady() {
   throw bad_status(status.error_message());
 }
 
-bool GrpcClient::modelReady(const std::string& model) {
+bool GrpcClient::modelReady(const std::string& model) const {
   inference::ModelReadyRequest request;
   inference::ModelReadyResponse reply;
 
@@ -164,7 +164,7 @@ ModelMetadata mapProtoToModelMetadata(
   return metadata;
 }
 
-ModelMetadata GrpcClient::modelMetadata(const std::string& model) {
+ModelMetadata GrpcClient::modelMetadata(const std::string& model) const {
   inference::ModelMetadataRequest request;
   inference::ModelMetadataResponse reply;
 
@@ -181,7 +181,7 @@ ModelMetadata GrpcClient::modelMetadata(const std::string& model) {
   throw bad_status(status.error_message());
 }
 
-std::vector<std::string> GrpcClient::modelList() {
+std::vector<std::string> GrpcClient::modelList() const {
   inference::ModelListRequest request;
   inference::ModelListResponse reply;
 
@@ -199,7 +199,7 @@ std::vector<std::string> GrpcClient::modelList() {
 }
 
 void GrpcClient::modelLoad(const std::string& model,
-                           RequestParameters* parameters) {
+                           RequestParameters* parameters) const {
   inference::ModelLoadRequest request;
   inference::ModelLoadResponse reply;
 
@@ -219,7 +219,7 @@ void GrpcClient::modelLoad(const std::string& model,
   }
 }
 
-void GrpcClient::modelUnload(const std::string& model) {
+void GrpcClient::modelUnload(const std::string& model) const {
   inference::ModelUnloadRequest request;
   inference::ModelUnloadResponse reply;
 
@@ -236,7 +236,7 @@ void GrpcClient::modelUnload(const std::string& model) {
 }
 
 std::string GrpcClient::workerLoad(const std::string& worker,
-                                   RequestParameters* parameters) {
+                                   RequestParameters* parameters) const {
   inference::WorkerLoadRequest request;
   inference::WorkerLoadResponse reply;
 
@@ -257,7 +257,7 @@ std::string GrpcClient::workerLoad(const std::string& worker,
   throw bad_status(status.error_message());
 }
 
-void GrpcClient::workerUnload(const std::string& worker) {
+void GrpcClient::workerUnload(const std::string& worker) const {
   inference::WorkerUnloadRequest request;
   inference::WorkerUnloadResponse reply;
 
@@ -299,16 +299,16 @@ InferenceResponse runInference(inference::GRPCInferenceService::Stub* stub,
 }
 
 InferenceResponseFuture GrpcClient::modelInferAsync(
-  const std::string& model, const InferenceRequest& request) {
+  const std::string& model, const InferenceRequest& request) const {
   return std::async(runInference, this->impl_->getStub(), model, request);
 }
 
-InferenceResponse GrpcClient::modelInfer(const std::string& model,
-                                         const InferenceRequest& request) {
+InferenceResponse GrpcClient::modelInfer(
+  const std::string& model, const InferenceRequest& request) const {
   return runInference(this->impl_->getStub(), model, request);
 }
 
-bool GrpcClient::hasHardware(const std::string& name, int num) {
+bool GrpcClient::hasHardware(const std::string& name, int num) const {
   inference::HasHardwareRequest grpc_request;
   inference::HasHardwareResponse reply;
 
diff --git a/src/proteus/clients/http.cpp b/src/proteus/clients/http.cpp
index f54468eef..5a6b089b4 100644
--- a/src/proteus/clients/http.cpp
+++ b/src/proteus/clients/http.cpp
@@ -113,14 +113,10 @@ void check_error(drogon::ReqResult result) {
     case ReqResult::Ok:
       break;
     case ReqResult::BadServerAddress:
-      error_msg = "Cannot connect to the server";
-      break;
+      throw connection_error{"Cannot connect to the server"};
     default:
-      error_msg =
-        "Request error code: " + std::to_string(static_cast<int>(result));
-  }
-  if (!error_msg.empty()) {
-    throw bad_status(error_msg);
+      throw bad_status{"Request error code: " +
+                       std::to_string(static_cast<int>(result))};
   }
 }
 
@@ -143,7 +139,7 @@ drogon::HttpRequestPtr createPostRequest(const Json::Value& json,
   return req;
 }
 
-ServerMetadata HttpClient::serverMetadata() {
+ServerMetadata HttpClient::serverMetadata() const {
   auto* client = this->impl_->getClient();
   auto req = createGetRequest("/v2", headers_);
 
@@ -163,7 +159,7 @@ ServerMetadata HttpClient::serverMetadata() {
   return metadata;
 }
 
-bool HttpClient::serverLive() {
+bool HttpClient::serverLive() const {
   auto* client = this->impl_->getClient();
   auto req = createGetRequest("/v2/health/live", headers_);
 
@@ -174,7 +170,7 @@ bool HttpClient::serverLive() {
   return response->statusCode() == drogon::k200OK;
 }
 
-bool HttpClient::serverReady() {
+bool HttpClient::serverReady() const {
   auto* client = this->impl_->getClient();
   auto req = createGetRequest("/v2/health/ready", headers_);
 
@@ -183,7 +179,7 @@ bool HttpClient::serverReady() {
   return response->statusCode() == drogon::k200OK;
 }
 
-bool HttpClient::modelReady(const std::string& model) {
+bool HttpClient::modelReady(const std::string& model) const {
   auto* client = this->impl_->getClient();
   auto req = createGetRequest("/v2/models/" + model + "/ready", headers_);
 
@@ -192,7 +188,7 @@ bool HttpClient::modelReady(const std::string& model) {
   return response->statusCode() == drogon::k200OK;
 }
 
-ModelMetadata HttpClient::modelMetadata(const std::string& model) {
+ModelMetadata HttpClient::modelMetadata(const std::string& model) const {
   auto* client = this->impl_->getClient();
   auto req = createGetRequest("/v2/models/" + model, headers_);
 
@@ -203,7 +199,7 @@ ModelMetadata HttpClient::modelMetadata(const std::string& model) {
 }
 
 void HttpClient::modelLoad(const std::string& model,
-                           RequestParameters* parameters) {
+                           RequestParameters* parameters) const {
   auto* client = this->impl_->getClient();
 
   Json::Value json = Json::objectValue;
@@ -221,7 +217,7 @@ void HttpClient::modelLoad(const std::string& model,
   }
 }
 
-void HttpClient::modelUnload(const std::string& model) {
+void HttpClient::modelUnload(const std::string& model) const {
   auto* client = this->impl_->getClient();
 
   Json::Value json;
@@ -237,7 +233,7 @@ void HttpClient::modelUnload(const std::string& model) {
 }
 
 std::string HttpClient::workerLoad(const std::string& model,
-                                   RequestParameters* parameters) {
+                                   RequestParameters* parameters) const {
   auto* client = this->impl_->getClient();
 
   Json::Value json = Json::objectValue;
@@ -256,7 +252,7 @@ std::string HttpClient::workerLoad(const std::string& model,
   return std::string(response->body());
 }
 
-void HttpClient::workerUnload(const std::string& model) {
+void HttpClient::workerUnload(const std::string& model) const {
   auto* client = this->impl_->getClient();
 
   Json::Value json;
@@ -281,7 +277,7 @@ auto createInferenceRequest(const std::string& model,
 }
 
 InferenceResponseFuture HttpClient::modelInferAsync(
-  const std::string& model, const InferenceRequest& request) {
+  const std::string& model, const InferenceRequest& request) const {
   auto req = createInferenceRequest(model, request, headers_);
   auto prom = std::make_shared<std::promise<proteus::InferenceResponse>>();
   auto fut = prom->get_future();
@@ -302,8 +298,8 @@ InferenceResponseFuture HttpClient::modelInferAsync(
   return fut;
 }
 
-InferenceResponse HttpClient::modelInfer(const std::string& model,
-                                         const InferenceRequest& request) {
+InferenceResponse HttpClient::modelInfer(
+  const std::string& model, const InferenceRequest& request) const {
   auto req = createInferenceRequest(model, request, headers_);
 
   auto* client = this->impl_->getClient();
@@ -317,7 +313,7 @@ InferenceResponse HttpClient::modelInfer(const std::string& model,
   return mapJsonToResponse(resp.get());
 }
 
-std::vector<std::string> HttpClient::modelList() {
+std::vector<std::string> HttpClient::modelList() const {
   auto* client = this->impl_->getClient();
   auto req = createGetRequest("/v2/models", headers_);
 
@@ -337,7 +333,7 @@ std::vector<std::string> HttpClient::modelList() {
   return models;
 }
 
-bool HttpClient::hasHardware(const std::string& name, int num) {
+bool HttpClient::hasHardware(const std::string& name, int num) const {
   auto* client = this->impl_->getClient();
 
   Json::Value json;
diff --git a/src/proteus/clients/native.cpp b/src/proteus/clients/native.cpp
index 5e9c87f8d..4944fdb36 100644
--- a/src/proteus/clients/native.cpp
+++ b/src/proteus/clients/native.cpp
@@ -36,18 +36,18 @@
 
 namespace proteus {
 
-ServerMetadata NativeClient::serverMetadata() {
+ServerMetadata NativeClient::serverMetadata() const {
   return ::proteus::serverMetadata();
 }
-bool NativeClient::serverLive() { return true; }
-bool NativeClient::serverReady() { return true; }
+bool NativeClient::serverLive() const { return true; }
+bool NativeClient::serverReady() const { return true; }
 
-ModelMetadata NativeClient::modelMetadata(const std::string& model) {
+ModelMetadata NativeClient::modelMetadata(const std::string& model) const {
   return ::proteus::modelMetadata(model);
 }
 
 void NativeClient::modelLoad(const std::string& model,
-                             RequestParameters* parameters) {
+                             RequestParameters* parameters) const {
   auto model_lower = util::toLower(model);
   if (parameters == nullptr) {
     RequestParameters params;
@@ -58,7 +58,7 @@ void NativeClient::modelLoad(const std::string& model,
 }
 
 std::string NativeClient::workerLoad(const std::string& model,
-                                     RequestParameters* parameters) {
+                                     RequestParameters* parameters) const {
   auto model_lower = util::toLower(model);
   if (parameters == nullptr) {
     RequestParameters params;
@@ -68,7 +68,7 @@ std::string NativeClient::workerLoad(const std::string& model,
 }
 
 InferenceResponseFuture NativeClient::modelInferAsync(
-  const std::string& workerName, const InferenceRequest& request) {
+  const std::string& workerName, const InferenceRequest& request) const {
 #ifdef PROTEUS_ENABLE_METRICS
   Metrics::getInstance().incrementCounter(MetricCounterIDs::kCppNative);
 #endif
@@ -88,23 +88,23 @@ InferenceResponseFuture NativeClient::modelInferAsync(
   return future;
 }
 
-InferenceResponse NativeClient::modelInfer(const std::string& model,
-                                           const InferenceRequest& request) {
+InferenceResponse NativeClient::modelInfer(
+  const std::string& model, const InferenceRequest& request) const {
   auto future = modelInferAsync(model, request);
   return future.get();
 }
 
-void NativeClient::modelUnload(const std::string& model) {
+void NativeClient::modelUnload(const std::string& model) const {
   auto model_lower = util::toLower(model);
   ::proteus::modelUnload(model);
 }
 
-void NativeClient::workerUnload(const std::string& model) {
+void NativeClient::workerUnload(const std::string& model) const {
   auto model_lower = util::toLower(model);
   ::proteus::workerUnload(model);
 }
 
-bool NativeClient::modelReady(const std::string& model) {
+bool NativeClient::modelReady(const std::string& model) const {
   try {
     return ::proteus::modelReady(model);
   } catch (const invalid_argument&) {
@@ -112,11 +112,11 @@ bool NativeClient::modelReady(const std::string& model) {
   }
 }
 
-std::vector<std::string> NativeClient::modelList() {
+std::vector<std::string> NativeClient::modelList() const {
   return ::proteus::modelList();
 }
 
-bool NativeClient::hasHardware(const std::string& name, int num) {
+bool NativeClient::hasHardware(const std::string& name, int num) const {
   return ::proteus::hasHardware(name, num);
 }
 
diff --git a/src/proteus/clients/websocket.cpp b/src/proteus/clients/websocket.cpp
index 43ff55d10..b41f587da 100644
--- a/src/proteus/clients/websocket.cpp
+++ b/src/proteus/clients/websocket.cpp
@@ -151,7 +151,7 @@ WebSocketClient::WebSocketClient(const std::string& ws_address,
 
 WebSocketClient::~WebSocketClient() = default;
 
-void WebSocketClient::close() {
+void WebSocketClient::close() const {
   auto* client = this->impl_->getWsClient();
   if (auto connection = client->getConnection(); connection != nullptr) {
     connection->shutdown();
@@ -159,77 +159,77 @@ void WebSocketClient::close() {
   }
 }
 
-ServerMetadata WebSocketClient::serverMetadata() {
+ServerMetadata WebSocketClient::serverMetadata() const {
   auto* client = this->impl_->getHttpClient();
   return client->serverMetadata();
 }
 
-bool WebSocketClient::serverLive() {
+bool WebSocketClient::serverLive() const {
   auto* client = this->impl_->getHttpClient();
   return client->serverLive();
 }
 
-bool WebSocketClient::serverReady() {
+bool WebSocketClient::serverReady() const {
   auto* client = this->impl_->getHttpClient();
   return client->serverReady();
 }
 
-bool WebSocketClient::modelReady(const std::string& model) {
+bool WebSocketClient::modelReady(const std::string& model) const {
   auto* client = this->impl_->getHttpClient();
   return client->modelReady(model);
 }
 
-ModelMetadata WebSocketClient::modelMetadata(const std::string& model) {
+ModelMetadata WebSocketClient::modelMetadata(const std::string& model) const {
   auto* client = this->impl_->getHttpClient();
   return client->modelMetadata(model);
 }
 
 void WebSocketClient::modelLoad(const std::string& model,
-                                RequestParameters* parameters) {
+                                RequestParameters* parameters) const {
   auto* client = this->impl_->getHttpClient();
   client->modelLoad(model, parameters);
 }
 
-void WebSocketClient::modelUnload(const std::string& model) {
+void WebSocketClient::modelUnload(const std::string& model) const {
   auto* client = this->impl_->getHttpClient();
   client->modelUnload(model);
 }
 
 std::string WebSocketClient::workerLoad(const std::string& model,
-                                        RequestParameters* parameters) {
+                                        RequestParameters* parameters) const {
   auto* client = this->impl_->getHttpClient();
   return client->workerLoad(model, parameters);
 }
 
-void WebSocketClient::workerUnload(const std::string& model) {
+void WebSocketClient::workerUnload(const std::string& model) const {
   auto* client = this->impl_->getHttpClient();
   client->workerUnload(model);
 }
 
 InferenceResponseFuture WebSocketClient::modelInferAsync(
-  const std::string& model, const InferenceRequest& request) {
+  const std::string& model, const InferenceRequest& request) const {
   auto* client = this->impl_->getHttpClient();
   return client->modelInferAsync(model, request);
 }
 
-InferenceResponse WebSocketClient::modelInfer(const std::string& model,
-                                              const InferenceRequest& request) {
+InferenceResponse WebSocketClient::modelInfer(
+  const std::string& model, const InferenceRequest& request) const {
   auto* client = this->impl_->getHttpClient();
   return client->modelInfer(model, request);
 }
 
-std::vector<std::string> WebSocketClient::modelList() {
+std::vector<std::string> WebSocketClient::modelList() const {
   auto* client = this->impl_->getHttpClient();
   return client->modelList();
 }
 
-bool WebSocketClient::hasHardware(const std::string& name, int num) {
+bool WebSocketClient::hasHardware(const std::string& name, int num) const {
   auto* client = this->impl_->getHttpClient();
   return client->hasHardware(name, num);
 }
 
 void WebSocketClient::modelInferWs(const std::string& model,
-                                   const InferenceRequest& request) {
+                                   const InferenceRequest& request) const {
   auto* client = this->impl_->getWsClient();
 
   auto json = mapRequestToJson(request);
@@ -247,6 +247,6 @@ void WebSocketClient::modelInferWs(const std::string& model,
   connection->send(message);
 }
 
-std::string WebSocketClient::modelRecv() { return impl_->recv(); }
+std::string WebSocketClient::modelRecv() const { return impl_->recv(); }
 
 }  // namespace proteus
diff --git a/src/proteus/util/pre_post/image_preprocess.hpp b/src/proteus/util/pre_post/image_preprocess.hpp
index e29a0d8f2..d356ca24c 100644
--- a/src/proteus/util/pre_post/image_preprocess.hpp
+++ b/src/proteus/util/pre_post/image_preprocess.hpp
@@ -50,7 +50,9 @@ struct ImagePreprocessOptions {
   ResizeAlgorithm resize_algorithm = ResizeAlgorithm::Simple;
 
   bool convert_color = false;
-  cv::ColorConversionCodes color_code;
+  // this should be cv::ColorConversionCodes but we can't bind it to Python
+  // conveniently
+  int color_code;
 
   bool convert_type = false;
   int type = 0;
diff --git a/tools/lint_cache.sh b/tools/lint_cache.sh
index 5363bb8ea..15a87c467 100755
--- a/tools/lint_cache.sh
+++ b/tools/lint_cache.sh
@@ -32,14 +32,18 @@ tidy=0
 run=0
 check=0
 
+# arbitrarily use half the available processes to run linting
+max_processes=$(( $(nproc) / 2 ))
+
 while true
 do
   case "$1" in
-    --iwyu      ) iwyu=1  ; shift 1 ;;
-    --tidy      ) tidy=1  ; shift 1 ;;
-    --run       ) run=1   ; shift 1 ;;
-    --check     ) check=1 ; shift 1 ;;
-    -h | --help ) usage   ; exit  0 ;;
+    --iwyu      ) iwyu=1           ; shift 1 ;;
+    --tidy      ) tidy=1           ; shift 1 ;;
+    --run       ) run=1            ; shift 1 ;;
+    --check     ) check=1          ; shift 1 ;;
+    -p          ) max_processes=$2 ; shift 2 ;;
+    -h | --help ) usage            ; exit  0 ;;
     *) break ;;
   esac
 done
@@ -54,13 +58,15 @@ if [[ $iwyu == 0 && $tidy == 0 ]]; then
     exit 1
 fi
 
-run_iwyu() {
+run() {
     source_file="$1"
+    linter="$2"
+    cmd="$3"
 
     echo $source_file
     # strip the root_dir from the source file path
     relative_path=${source_file##${root_dir}/}
-    cache_entry="$cache_dir/iwyu/$relative_path"
+    cache_entry="$cache_dir/$linter/$relative_path"
     new_hash=$(md5sum $source_file | cut -d ' ' -f 1)
     if [[ -f "$cache_entry" ]]; then
         old_hash=$(tail -n 1 $cache_entry)
@@ -69,30 +75,19 @@ run_iwyu() {
         fi
     fi
     mkdir -p $(dirname $cache_entry)
-    python3 /usr/local/bin/iwyu_tool.py -p ${build_dir} $source_file -- -Xiwyu --mapping_file=$root_dir/tools/.iwyu.json > $cache_entry 2>&1
+    $cmd > $cache_entry 2>&1
     echo $new_hash >> $cache_entry
 }
 
-run_tidy() {
-    source_file="$1"
+run_iwyu() {
+    python3 /usr/local/bin/iwyu_tool.py -p ${build_dir} $source_file -- -Xiwyu --mapping_file=$root_dir/tools/.iwyu.json
+}
 
-    echo $source_file
-    # strip the root_dir from the source file path
-    relative_path=${source_file##${root_dir}/}
-    cache_entry="$cache_dir/tidy/$relative_path"
-    new_hash=$(md5sum $source_file | cut -d ' ' -f 1)
-    if [[ -f "$cache_entry" ]]; then
-        old_hash=$(tail -n 1 $cache_entry)
-        if [[ $old_hash == $new_hash ]]; then
-            return
-        fi
-    fi
-    mkdir -p $(dirname $cache_entry)
-    clang-tidy -format-style=file -p=${build_dir} --extra-arg "-DCV_STATIC_ANALYSIS=0" $source_file > $cache_entry 2>&1
-    echo $new_hash >> $cache_entry
+run_tidy() {
+    clang-tidy -format-style=file -p=${build_dir} --extra-arg "-DCV_STATIC_ANALYSIS=0" $source_file
 }
 
-run_check() {
+check() {
     source_file="$1"
     linter="$2"
 
@@ -127,6 +122,7 @@ fi
 cache_dir="$build_dir/lint_cache"
 mkdir -p "$cache_dir"
 
+echo "Finding source files..."
 source_files=()
 for directory in ${source_directories[@]}; do
     for extension in ${extensions[@]}; do
@@ -136,21 +132,29 @@ for directory in ${source_directories[@]}; do
     done
 done
 
+process_counter=0
 for source_file in ${source_files[@]}; do
     if [[ $run == 1 ]]; then
         if [[ $iwyu == 1 ]]; then
-            run_iwyu $source_file
+            run $source_file "iwyu" run_iwyu &
         fi
         if [[ $tidy == 1 ]]; then
-            run_tidy $source_file
+            run "$source_file" "tidy" run_tidy &
+        fi
+        process_counter=$(( process_counter + 1 ))
+        if [ "$process_counter" -ge "$max_processes" ]; then
+            wait
+            process_counter=0
         fi
     fi
     if [[ $check == 1 ]]; then
         if [[ $iwyu == 1 ]]; then
-            run_check $source_file iwyu
+            check $source_file iwyu
         fi
         if [[ $tidy == 1 ]]; then
-            run_check $source_file tidy
+            check $source_file tidy
         fi
     fi
 done
+
+wait


Test patch:
diff --git a/tests/test.sh b/tests/test.sh
index 7cef201a5..7983c7b70 100755
--- a/tests/test.sh
+++ b/tests/test.sh
@@ -143,13 +143,13 @@ if [[ $MODE == "tests" || $MODE == "all" ]]; then
 fi
 
 if [[ $MODE == "examples" || $MODE == "all" ]]; then
-  for file in $(find $examples_path/python -type f -print); do
+  for file in $(find $examples_path -type f -print); do
     if [[ $file == *.py ]]; then
       python3 $(realpath $file)
     fi
   done
 
-  for file in $(find $root_path/build/$BUILD/examples/cpp -type f -print); do
+  for file in $(find $root_path/build/$BUILD/examples -type f -print); do
     real_file=$(realpath $file)
     if [ -x $real_file ]; then
       $real_file
diff --git a/tests/validation/resnet50.py b/tests/validation/resnet50.py
new file mode 100644
index 000000000..ba35eb7cc
--- /dev/null
+++ b/tests/validation/resnet50.py
@@ -0,0 +1,285 @@
+# Copyright 2022 Advanced Micro Devices, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+This example brings up the migraphx worker and runs a Resnet50 classification model
+on the official imagenet validation set (50,000 images).
+
+The model file and test data is not in the git repo. and must be fetched with git or
+    proteus get or wget
+
+Model source:
+https://github.com/onnx/models/blob/main/vision/classification/resnet/model/resnet50-v2-7.onnx
+https://github.com/onnx/models/blob/main/vision/classification/resnet/model/resnet50-v2-7.tar.gz
+
+Source of ground truth labels for validation set:
+    ILSVRC2012_validation_ground_truth.txt
+    https://github.com/mvermeulen/rocm-migraphx/tree/master/datasets/imagenet/val.txt
+"""
+
+import argparse
+import os
+import pathlib
+import sys
+import time
+
+import cv2
+import numpy as np
+
+import proteus
+import proteus.clients
+import proteus.testing
+import proteus.util.pre_post as pre_post
+
+
+def preprocess(paths):
+    options = pre_post.ImagePreprocessOptionsFloat()
+    options.order = pre_post.ImageOrder.NCHW
+    options.height = 224
+    options.width = 224
+    options.mean = [0.485, 0.456, 0.406]
+    options.std = [4.367, 4.464, 4.444]
+    options.normalize = True
+    options.convert_color = True
+    options.color_code = cv2.COLOR_BGR2RGB
+    options.convert_type = True
+    options.type = cv2.CV_32FC3
+    options.convert_scale = 1.0 / 255.0
+    return pre_post.imagePreprocessFloat(paths, options)
+
+
+def main(args):
+
+    batch_size = args.batch_size
+    model = args.model
+    validation_dir = args.validation_dir
+    ground_truth_file = args.ground_truth
+    labels_file = args.labels
+    worker = args.worker
+
+    client = proteus.clients.HttpClient("http://127.0.0.1:8998")
+    print("waiting for server...", end="")
+    start_server = not client.serverLive()
+    if start_server:
+        server = proteus.servers.Server()
+        server.startHttp(8998)
+        while not client.serverLive():
+            time.sleep(1)
+    print("ok.")
+
+    metadata = client.serverMetadata()
+    if worker not in metadata.extensions:
+        print(f"{worker} support required but not found.")
+        sys.exit(0)
+
+    # load worker.  The only parameter the migraphx worker requires is the model file name.
+    # It will take the file name stem and search for either a *.onnx or *.mxr extension, and if
+    # it finds a *.onnx file it will compile it and save the compiled model as *.mxr for
+    # future use.  It will read
+    # the array dimensions and data type from the model.
+
+    parameters = proteus.RequestParameters()
+    parameters.put("model", str(model))
+    parameters.put("batch", batch_size)
+    parameters.put("timeout", 100)  # ms
+    # this call requests the server to either find a running instance of the named
+    # worker type, or else create one and initialize it with the parameters.
+    print("loading worker with model file ", model)
+    endpoint = client.workerLoad("migraphx", parameters)
+
+    # load the labels
+    with open(labels_file, "r") as f:
+        labels = f.read().split("\n")
+
+    # wait for the worker to load and compile model
+    ready = False
+    while not ready:
+        try:
+            ready = client.modelReady(endpoint)
+        except ValueError:
+            pass
+
+    # list all the *.jpg images in the validation image directory
+    files = os.listdir(validation_dir)
+    files.sort()
+
+    if str(ground_truth_file) in files:
+        files.remove(str(ground_truth_file))
+
+    # Read the "answers" file
+    ground_truth = [None] * len(files)
+    i = 1
+    with open(ground_truth_file, "r") as f:
+        while i < len(ground_truth):
+            ground_truth[i] = f.readline().split()[1]
+            i = i + 1
+
+    #
+    #   Loop thru the image set, reading images and putting together inference requests in batches.
+    #   Then compare the results with ground truth
+    #
+    correct_answers = 0
+    wrong_answers = 0
+
+    print(f"Starting requests for {len(files)} images")
+    request_size = 64
+    filenames = []
+    index = 0
+    for file in files:
+        filename = os.path.join(validation_dir, file)
+        if os.path.isfile(filename) and filename.find(".JPEG") != -1:
+            filenames.append(filename)
+            index += 1
+
+        if len(filenames) == request_size:
+            images = preprocess(filenames)
+
+            imageReqs = [proteus.ImageInferenceRequest(image) for image in images]
+            print("Request is ready.  Sending ", len(images), " requests")
+            j = 0
+            responses = proteus.client_operators.inferAsyncOrdered(
+                client, endpoint, imageReqs
+            )
+            for response in responses:
+                assert not response.isError(), response.getError()
+
+                # print("Client received inference reply.")
+                for output in response.getOutputs():
+                    assert output.datatype == proteus.DataType.FP32
+                    recv_data = output.getFp32Data()
+                    # the predicted category is the one with the highest match value
+                    answer = np.argmax(recv_data)
+                    step_index = index - request_size + j + 1
+                    print(
+                        "Reading result: best match category is ",
+                        answer,
+                        "  value is ",
+                        recv_data[answer],
+                        ".  This is a picture of a ",
+                        labels[answer],
+                        "    ground truth: ",
+                        ground_truth[step_index],
+                    )
+                    if int(answer) == int(ground_truth[step_index]):
+                        correct_answers = correct_answers + 1.0
+                    else:
+                        wrong_answers = wrong_answers + 1.0
+                j = j + 1
+            print(
+                "Correct: ",
+                correct_answers,
+                "  Wrong: ",
+                wrong_answers,
+                "    Accuracy: ",
+                correct_answers / (correct_answers + wrong_answers),
+            )
+            filenames = []
+
+    print("Done")
+
+    # If this line is commented out, worker persists with doRun thread active, and the entire script
+    # can be run again without any reloading taking place
+    # client.unload('Migraphx')
+
+
+def validate_args(args):
+    # TODO(varunsh): generalize this script
+    # supported_workers = ["vitis", "ptzendnn", "tfzendnn", "migraphx"]
+    supported_workers = ["migraphx"]
+    if args.worker not in supported_workers:
+        raise ValueError(f"Worker must be one of: {' '.join(supported_workers)}")
+    if not args.model:
+        if args.worker == "vitis":
+            args.model = proteus.testing.getPathToAsset("u250_resnet50")
+        elif args.worker == "ptzendnn":
+            args.model = proteus.testing.getPathToAsset("pt_resnet50")
+        elif args.worker == "tfzendnn":
+            args.model = proteus.testing.getPathToAsset("tf_resnet50")
+        else:
+            args.model = proteus.testing.getPathToAsset("onnx_resnet50")
+
+    return args
+
+
+def parse_args():
+    def _check_path(value):
+        if not value:
+            raise argparse.ArgumentError(f"{value} is not a valid path")
+        value = pathlib.Path(value)
+        if not value.exists():
+            raise argparse.ArgumentError(f"Path {str(value)} does not exist")
+        return value
+
+    parser = argparse.ArgumentParser(description="Validation for the ResNet50 model")
+
+    parser.add_argument(
+        "--batch-size",
+        "-b",
+        default=64,
+        help="Batch size for the worker. Defaults to 64.",
+    )
+
+    parser.add_argument(
+        "--model",
+        "-m",
+        default="",
+        help="Path to the model on the server",
+    )
+
+    parser.add_argument(
+        "--validation-dir",
+        "-v",
+        required=True,
+        type=_check_path,
+        help="Path to the directory containing validation images",
+    )
+    parser.add_argument(
+        "--ground-truth",
+        "-g",
+        required=True,
+        type=_check_path,
+        help="Path to the file containing the correct category results for the validation set",
+    )
+
+    parser.add_argument(
+        "--labels",
+        "-l",
+        required=True,
+        type=_check_path,
+        help="Path to the file containing label names for the model's categories",
+    )
+
+    parser.add_argument(
+        "--input-size",
+        "-i",
+        default=224,
+        help="The size of the square image",
+    )
+
+    parser.add_argument(
+        "--worker",
+        "-w",
+        required=True,
+        type=str,
+        help="Name of the worker backend to use for inference",
+    )
+
+    return parser.parse_args()
+
+
+if __name__ == "__main__":
+    args = parse_args()
+    args = validate_args(args)
+    main(args)
diff --git a/tests/workers/test_facedetect.cpp b/tests/workers/test_facedetect.cpp
index 7a1e912f0..1ef4f7783 100644
--- a/tests/workers/test_facedetect.cpp
+++ b/tests/workers/test_facedetect.cpp
@@ -60,7 +60,7 @@ void dequeue_validate(FutureQueue& my_queue, int num_images) {
       EXPECT_EQ(size, gold_response_size);
       for (size_t i = 0; i < gold_response_size; i++) {
         // expect that the response values are within 1% of the golden
-        const float abs_error = std::abs(gold_response_output[i] * 0.01);
+        const float abs_error = std::abs(gold_response_output[i] * 0.05);
         EXPECT_NEAR(data[i], gold_response_output[i], abs_error);
       }
     }
diff --git a/tests/workers/test_facedetect.py b/tests/workers/test_facedetect.py
index b8ff5e617..b8a60e07a 100644
--- a/tests/workers/test_facedetect.py
+++ b/tests/workers/test_facedetect.py
@@ -13,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import math
+
 import numpy as np
 import pytest
 
@@ -83,7 +85,10 @@ def send_request(self, request, check_asserts=True):
                 assert len(data) == len(
                     gold_response_output
                 ), f"{len(data)} != {len(gold_response_output)}"
-                np.testing.assert_almost_equal(gold_response_output, data, 2)
+                for datum, golden in zip(data, gold_response_output):
+                    # expect that the response values are within 5% of the golden
+                    abs_error = abs(golden * 0.05)
+                    assert math.isclose(datum, golden, abs_tol=abs_error)
         return response
 
     def construct_request(self, asTensor):
diff --git a/tests/workers/test_migraphx.py b/tests/workers/test_migraphx.py
index ec57f3359..33ca711b9 100644
--- a/tests/workers/test_migraphx.py
+++ b/tests/workers/test_migraphx.py
@@ -21,54 +21,50 @@
 
 import proteus
 import proteus.testing
+import proteus.util.pre_post as pre_post
 
 from helper import root_path, run_benchmark
 
-sys.path.insert(0, os.path.join(root_path, "examples/python"))
-from utils.utils import postprocess
 
-# The make_nxn and preprocess functions are based on an migraphx example at
-# AMDMIGraphx/examples/vision/python_resnet50/resnet50_inference.ipynb
-# The mean and standard dev. values used for this normalization are requirements of the Resnet50 model.
+def preprocess(paths):
+    """
+    Given a list of paths to images, preprocess the images and return them
 
+    Args:
+        paths (list[str]): Paths to images
 
-def make_nxn(image, n):
-    """
-    Crop an image to square and then resize to desired dimension n x n
+    Returns:
+        list[numpy.ndarray]: List of images
     """
-    width = image.shape[1]
-    height = image.shape[0]
-    if height > width:
-        dif = height - width
-        bar = dif // 2
-        square = image[(bar + (dif % 2)) : (height - bar), :]
-        return cv2.resize(square, (n, n))
-    elif width > height:
-        dif = width - height
-        bar = dif // 2
-        square = image[:, (bar + (dif % 2)) : (width - bar)]
-        return cv2.resize(square, (n, n))
-    else:
-        return cv2.resize(image, (n, n))
-
-
-def preprocess(img_data):
+
+    options = pre_post.ImagePreprocessOptionsFloat()
+    options.order = pre_post.ImageOrder.NCHW
+    options.height = 224
+    options.width = 224
+    options.mean = [0.485, 0.456, 0.406]
+    options.std = [4.367, 4.464, 4.444]
+    options.normalize = True
+    options.convert_color = True
+    options.color_code = cv2.COLOR_BGR2RGB
+    options.convert_type = True
+    options.type = cv2.CV_32FC3
+    options.convert_scale = 1.0 / 255.0
+    return pre_post.imagePreprocessFloat(paths, options)
+
+
+def postprocess(output, k):
     """
-    Normalize array values to the data type, mean and std. dev. required by Resnet50
+    Postprocess the output data. For ResNet50, this includes performing a softmax
+    to determine the most probable classifications
 
     Args:
-        img_data (np.array): array in 3 dimensions [channels, rows, cols] with value range 0-255
-        The vectors are for RGB images, so images read with OpenCV must have channels
-        converted before calling.
+        output (proteus.InferenceResponseOutput): the output from the inference server
+        k (int): number of top categories to return
+
     Returns:
-        Response: np.array
+        list[int]: indices for the top k categories
     """
-    mean_vec = np.array([0.406, 0.456, 0.485])
-    stddev_vec = np.array([0.225, 0.224, 0.229])
-    norm_img_data = np.zeros(img_data.shape).astype("float32")
-    for i in range(img_data.shape[0]):
-        norm_img_data[i, :, :] = (img_data[i, :, :] / 255 - mean_vec[i]) / stddev_vec[i]
-    return norm_img_data
+    return pre_post.resnet50PostprocessFloat(output, k)
 
 
 @pytest.mark.extensions(["migraphx"])
@@ -129,19 +125,12 @@ def test_migraphx(self, num):
 
         request_num = num
         for i in range(request_num):
-            # Load a picture
-            img = cv2.imread(image_paths[i % image_num]).astype("float32")
-            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
-            # Crop to a square, resize
-            img = make_nxn(img, 224)
-            #  Normalize contents with values specific to Resnet50
-            img = img.transpose(2, 0, 1)
-            img = preprocess(img)
+            img = preprocess([image_paths[i % image_num]])[0]
 
             request = proteus.ImageInferenceRequest(img, True)
             response = self.send_request(request)
-            top_k_responses = postprocess(response, 5)
+            outputs = response.getOutputs()
+            assert len(outputs) == 1
+            top_k_responses = postprocess(outputs[0], 5)
 
-            for i, top_k in enumerate(top_k_responses):
-                # Look for the top 5 categories (not underlying scores)
-                assert (top_k == gold_responses[i % image_num]).all()
+            assert top_k_responses == gold_responses[i % image_num]
diff --git a/tests/workers/test_ptzendnn.py b/tests/workers/test_ptzendnn.py
index 685a4fafa..a72078024 100644
--- a/tests/workers/test_ptzendnn.py
+++ b/tests/workers/test_ptzendnn.py
@@ -15,16 +15,53 @@
 import os
 import sys
 
+import cv2
 import numpy as np
 import pytest
 
 import proteus
 import proteus.testing
+import proteus.util.pre_post as pre_post
 
 from helper import root_path, run_benchmark
 
-sys.path.insert(0, os.path.join(root_path, "examples/python"))
-from utils.utils import postprocess, preprocess_pt
+
+def preprocess(paths):
+    """
+    Given a list of paths to images, preprocess the images and return them
+
+    Args:
+        paths (list[str]): Paths to images
+
+    Returns:
+        list[numpy.ndarray]: List of images
+    """
+    options = pre_post.ImagePreprocessOptionsFloat()
+    options.order = pre_post.ImageOrder.NCHW
+    options.mean = [0.485, 0.456, 0.406]
+    options.std = [4.367, 4.464, 4.444]
+    options.normalize = True
+    options.convert_color = True
+    options.color_code = cv2.COLOR_BGR2RGB
+    options.convert_type = True
+    options.type = cv2.CV_32FC3
+    options.convert_scale = 1.0 / 255.0
+    return pre_post.imagePreprocessFloat(paths, options)
+
+
+def postprocess(output, k):
+    """
+    Postprocess the output data. For ResNet50, this includes performing a softmax
+    to determine the most probable classifications
+
+    Args:
+        output (proteus.InferenceResponseOutput): the output from the inference server
+        k (int): number of top categories to return
+
+    Returns:
+        list[int]: indices for the top k categories
+    """
+    return pre_post.resnet50PostprocessFloat(output, k)
 
 
 @pytest.mark.extensions(["ptzendnn"])
@@ -83,43 +120,13 @@ def test_ptzendnn_0(self, num):
         """
         image_path = proteus.testing.getPathToAsset("asset_dog-3619020_640.jpg")
 
-        preprocessing = {"input_size": 224}
-
         batch = num
-        images = []
-        for _ in range(batch):
-            images.append(
-                preprocess_pt(
-                    image_path,
-                    input_size=preprocessing["input_size"],
-                )
-            )
-        request = proteus.ImageInferenceRequest(images, True)
-        response = self.send_request(request)
-        top_k_responses = postprocess(response, 5)
-        gold_response_output = [259, 157, 152, 261, 154]
-        for top_k in top_k_responses:
-            assert (top_k == gold_response_output).all()
-
-    @pytest.mark.benchmark(group="PtZendnn")
-    def test_benchmark_ptzendnn(self, benchmark):
-
-        batch_size = 16
-        assert self.parameters is not None
-        input_size = self.parameters.get("input_size")
-        images = np.random.uniform(
-            0.0, 255.0, (batch_size, input_size, input_size, 3)
-        ).astype(np.float32)
-        images = [image for image in images]
-
-        request = proteus.ImageInferenceRequest(images, True)
-
-        options = {
-            "model": self.model,
-            "parameters": self.parameters,
-            "type": "rest (pytest)",
-            "config": "N/A",
-        }
-        run_benchmark(
-            benchmark, "PtZendnn", self.rest_client.modelInfer, request, **options
-        )
+        image_paths = [image_path] * batch
+        images = preprocess(image_paths)
+        for image in images:
+            request = proteus.ImageInferenceRequest(image, True)
+            response = self.send_request(request)
+            outputs = response.getOutputs()
+            top_k_responses = postprocess(outputs[0], 5)
+            gold_response_output = [259, 261, 260, 154, 157]
+            assert top_k_responses == gold_response_output
diff --git a/tests/workers/test_tfzendnn.py b/tests/workers/test_tfzendnn.py
index 1b0a561a7..f52446bb8 100644
--- a/tests/workers/test_tfzendnn.py
+++ b/tests/workers/test_tfzendnn.py
@@ -15,16 +15,47 @@
 import os
 import sys
 
+import cv2
 import numpy as np
 import pytest
 
 import proteus
 import proteus.testing
+import proteus.util.pre_post as pre_post
 
 from helper import root_path, run_benchmark
 
-sys.path.insert(0, os.path.join(root_path, "examples/python"))
-from utils.utils import postprocess, preprocess
+
+def preprocess(paths):
+    """
+    Given a list of paths to images, preprocess the images and return them
+
+    Args:
+        paths (list[str]): Paths to images
+
+    Returns:
+        list[numpy.ndarray]: List of images
+    """
+    options = pre_post.ImagePreprocessOptionsFloat()
+    options.convert_color = True
+    options.color_code = cv2.COLOR_BGR2RGB
+    options.assign = True
+    return pre_post.imagePreprocessFloat(paths, options)
+
+
+def postprocess(output, k):
+    """
+    Postprocess the output data. For ResNet50, this includes performing a softmax
+    to determine the most probable classifications
+
+    Args:
+        output (proteus.InferenceResponseOutput): the output from the inference server
+        k (int): number of top categories to return
+
+    Returns:
+        list[int]: indices for the top k categories
+    """
+    return pre_post.resnet50PostprocessFloat(output, k)
 
 
 @pytest.mark.extensions(["tfzendnn"])
@@ -87,44 +118,13 @@ def test_tfzendnn_0(self, num):
         """
         image_path = proteus.testing.getPathToAsset("asset_dog-3619020_640.jpg")
 
-        preprocessing = {"input_size": 224, "resize_method": "crop"}
-
         batch = num
-        images = []
-        for _ in range(batch):
-            images.append(
-                preprocess(
-                    image_path,
-                    input_size=preprocessing["input_size"],
-                    resize_method=preprocessing["resize_method"],
-                )
-            )
-        request = proteus.ImageInferenceRequest(images, True)
-        response = self.send_request(request)
-        top_k_responses = postprocess(response, 5)
-        gold_response_output = [259, 261, 154, 260, 263]
-        for top_k in top_k_responses:
-            assert (top_k == gold_response_output).all()
-
-    @pytest.mark.benchmark(group="TfZendnn")
-    def test_benchmark_tfzendnn(self, benchmark):
-
-        batch_size = 16
-        assert self.parameters is not None
-        input_size = self.parameters.get("input_size")
-        images = np.random.uniform(
-            0.0, 255.0, (batch_size, input_size, input_size, 3)
-        ).astype(np.float32)
-        images = [image for image in images]
-
-        request = proteus.ImageInferenceRequest(images, True)
-
-        options = {
-            "model": self.model,
-            "parameters": self.parameters,
-            "type": "rest (pytest)",
-            "config": "N/A",
-        }
-        run_benchmark(
-            benchmark, "TfZendnn", self.rest_client.modelInfer, request, **options
-        )
+        image_paths = [image_path] * batch
+        images = preprocess(image_paths)
+        for image in images:
+            request = proteus.ImageInferenceRequest(image, True)
+            response = self.send_request(request)
+            outputs = response.getOutputs()
+            top_k_responses = postprocess(outputs[0], 5)
+            gold_response_output = [259, 261, 154, 260, 263]
+            assert top_k_responses == gold_response_output