Skip to content

Commit

Permalink
fmt
Browse files Browse the repository at this point in the history
  • Loading branch information
sakoush committed Oct 11, 2021
1 parent cd61930 commit 524b812
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 97 deletions.
2 changes: 1 addition & 1 deletion runtimes/alibi-explain/mlserver_alibi_explain/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .runtime import AlibiExplainRuntime

__all__ = ["AlibiExplainRuntime"]
__all__ = ["AlibiExplainRuntime"]
26 changes: 16 additions & 10 deletions runtimes/alibi-explain/mlserver_alibi_explain/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@
_INTEGRATED_GRADIENTS_TAG = "integrated_gradients"

_TAG_TO_RT_IMPL = {
_ANCHOR_IMAGE_TAG:
("mlserver_alibi_explain.explainers.black_box_runtime.AlibiExplainBlackBoxRuntime",
"alibi.explainers.AnchorImage"),
_ANCHOR_TEXT_TAG:
("mlserver_alibi_explain.explainers.black_box_runtime.AlibiExplainBlackBoxRuntime",
"alibi.explainers.AnchorText"),
_ANCHOR_IMAGE_TAG: (
"mlserver_alibi_explain.explainers.black_box_runtime.AlibiExplainBlackBoxRuntime",
"alibi.explainers.AnchorImage",
),
_ANCHOR_TEXT_TAG: (
"mlserver_alibi_explain.explainers.black_box_runtime.AlibiExplainBlackBoxRuntime",
"alibi.explainers.AnchorText",
),
_INTEGRATED_GRADIENTS_TAG: (
"mlserver_alibi_explain.explainers.integrated_gradients.IntegratedGradientsWrapper",
"alibi.explainers.IntegratedGradients")
"alibi.explainers.IntegratedGradients",
),
}


Expand All @@ -53,11 +56,14 @@ def convert_from_bytes(output: ResponseOutput, ty: Optional[Type]) -> Any:
else:
py_str = bytearray(output.data).decode("UTF-8")
from ast import literal_eval

return literal_eval(py_str)


# TODO: add retry
def remote_predict(v2_payload: InferenceRequest, predictor_url: str) -> InferenceResponse:
def remote_predict(
v2_payload: InferenceRequest, predictor_url: str
) -> InferenceResponse:
response_raw = requests.post(predictor_url, json=v2_payload.dict())
if response_raw.status_code != 200:
raise ValueError(f"{response_raw.status_code} / {response_raw.reason}")
Expand All @@ -66,7 +72,7 @@ def remote_predict(v2_payload: InferenceRequest, predictor_url: str) -> Inferenc

# TODO: this is very similar to `asyncio.to_thread` (python 3.9+), so lets use it at some point.
def execute_async(
loop: Optional[AbstractEventLoop], fn: Callable, *args, **kwargs
loop: Optional[AbstractEventLoop], fn: Callable, *args, **kwargs
) -> Awaitable:
if loop is None:
loop = asyncio.get_running_loop()
Expand Down Expand Up @@ -102,5 +108,5 @@ class Config:

def import_and_get_class(class_path: str) -> type:
last_dot = class_path.rfind(".")
klass = getattr(import_module(class_path[:last_dot]), class_path[last_dot + 1:])
klass = getattr(import_module(class_path[:last_dot]), class_path[last_dot + 1 :])
return klass
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,15 @@ async def load(self) -> bool:
self._model = self._explainer_class(**init_parameters) # type: ignore
else:
# load the model from disk
self._model = load_explainer(self.settings.parameters.uri, predictor=self._infer_impl)
self._model = load_explainer(
self.settings.parameters.uri, predictor=self._infer_impl
)

self.ready = True
return self.ready

def _explain_impl(self, input_data: Any, explain_parameters: Dict) -> Explanation:
return self._model.explain(
input_data,
**explain_parameters # type: ignore
)
return self._model.explain(input_data, **explain_parameters) # type: ignore

def _infer_impl(self, input_data: np.ndarray) -> np.ndarray:
# The contract is that alibi-explain would input/output ndarray
Expand All @@ -56,8 +55,8 @@ def _infer_impl(self, input_data: np.ndarray) -> np.ndarray:

# TODO add some exception handling here
v2_response = remote_predict(
v2_payload=v2_request,
predictor_url=self.alibi_explain_settings.infer_uri)
v2_payload=v2_request, predictor_url=self.alibi_explain_settings.infer_uri
)

# TODO: do we care about more than one output?
return np_codec.decode_response_output(v2_response.outputs[0])
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,17 @@
import tensorflow as tf
from alibi.api.interfaces import Explanation

from mlserver_alibi_explain.explainers.white_box_runtime import AlibiExplainWhiteBoxRuntime
from mlserver_alibi_explain.explainers.white_box_runtime import (
AlibiExplainWhiteBoxRuntime,
)


class IntegratedGradientsWrapper(AlibiExplainWhiteBoxRuntime):
def _explain_impl(self, input_data: Any, explain_parameters: Dict) -> Explanation:
# TODO: how are we going to deal with that?
predictions = self._inference_model(input_data).numpy().argmax(axis=1)
return self._model.explain(
input_data,
target=predictions,
**explain_parameters
)
return self._model.explain(input_data, target=predictions, **explain_parameters)

async def _get_inference_model(self) -> Any:
inference_model_path = self.alibi_explain_settings.infer_uri
return tf.keras.models.load_model(inference_model_path)


Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class AlibiExplainWhiteBoxRuntime(AlibiExplainRuntimeBase):
White box alibi explain requires access to the full inference model to compute gradients etc. usually in the same
domain as the explainer itself. e.g. `IntegratedGradients`
"""

def __init__(self, settings: ModelSettings, explainer_class: Type[Explainer]):
self._inference_model = None
self._explainer_class = explainer_class
Expand All @@ -33,7 +34,9 @@ async def load(self) -> bool:
else:
# load the model from disk
# full model is passed as `predictor`
self._model = load_explainer(self.settings.parameters.uri, predictor=self._inference_model)
self._model = load_explainer(
self.settings.parameters.uri, predictor=self._inference_model
)

self.ready = True
return self.ready
Expand All @@ -43,4 +46,3 @@ def _explain_impl(self, input_data: Any, explain_parameters: Dict) -> Explanatio

async def _get_inference_model(self) -> Any:
raise NotImplementedError

39 changes: 26 additions & 13 deletions runtimes/alibi-explain/mlserver_alibi_explain/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,34 @@
from mlserver.codecs import NumpyCodec, InputCodec, StringCodec
from mlserver.model import MLModel
from mlserver.settings import ModelSettings
from mlserver.types import InferenceRequest, InferenceResponse, RequestInput, MetadataModelResponse, Parameters, \
MetadataTensor, ResponseOutput
from mlserver_alibi_explain.common import execute_async, AlibiExplainSettings, \
get_mlmodel_class_as_str, \
get_alibi_class_as_str, import_and_get_class, EXPLAIN_PARAMETERS_TAG, EXPLAINER_TYPE_TAG
from mlserver.types import (
InferenceRequest,
InferenceResponse,
RequestInput,
MetadataModelResponse,
Parameters,
MetadataTensor,
ResponseOutput,
)
from mlserver_alibi_explain.common import (
execute_async,
AlibiExplainSettings,
get_mlmodel_class_as_str,
get_alibi_class_as_str,
import_and_get_class,
EXPLAIN_PARAMETERS_TAG,
EXPLAINER_TYPE_TAG,
)


class AlibiExplainRuntimeBase(MLModel):
"""
Base class for Alibi-Explain models
"""

def __init__(self, settings: ModelSettings, explainer_settings: AlibiExplainSettings):
def __init__(
self, settings: ModelSettings, explainer_settings: AlibiExplainSettings
):

self.alibi_explain_settings = explainer_settings
super().__init__(settings)
Expand All @@ -37,7 +52,9 @@ async def predict(self, payload: InferenceRequest) -> InferenceResponse:
outputs=[output_data],
)

async def _async_explain_impl(self, input_data: Any, settings: Parameters) -> ResponseOutput:
async def _async_explain_impl(
self, input_data: Any, settings: Parameters
) -> ResponseOutput:
"""run async"""
explain_parameters = dict()
if EXPLAIN_PARAMETERS_TAG in settings:
Expand All @@ -47,7 +64,7 @@ async def _async_explain_impl(self, input_data: Any, settings: Parameters) -> Re
loop=None,
fn=self._explain_impl,
input_data=input_data,
explain_parameters=explain_parameters
explain_parameters=explain_parameters,
)
# TODO: Convert alibi-explain output to v2 protocol, for now we use to_json
return StringCodec.encode(payload=[explanation.to_json()], name="explain")
Expand Down Expand Up @@ -109,7 +126,7 @@ def ready(self, value: bool):
self._rt.ready = value

def decode(
self, request_input: RequestInput, default_codec: Optional[InputCodec] = None
self, request_input: RequestInput, default_codec: Optional[InputCodec] = None
) -> Any:
return self._rt.decode(request_input, default_codec)

Expand All @@ -124,7 +141,3 @@ async def load(self) -> bool:

async def predict(self, payload: InferenceRequest) -> InferenceResponse:
return await self._rt.predict(payload)




5 changes: 1 addition & 4 deletions runtimes/alibi-explain/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ def _load_description() -> str:
author_email="hello@seldon.io",
description="Alibi-Explain runtime for MLServer",
packages=find_packages(),
install_requires=[
"mlserver",
"alibi"
],
install_requires=["mlserver", "alibi"],
long_description=_load_description(),
long_description_content_type="text/markdown",
license="Apache 2.0",
Expand Down
38 changes: 18 additions & 20 deletions runtimes/alibi-explain/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,7 @@ async def custom_runtime_tf() -> MLModel:

@pytest.fixture
def settings() -> Settings:
return Settings(
debug=True,
host="127.0.0.1"
)
return Settings(debug=True, host="127.0.0.1")


@pytest.fixture
Expand All @@ -108,7 +105,7 @@ def model_repository(tmp_path, runtime_pytorch) -> ModelRepository:
"parallel_workers": 0,
"parameters": {
"uri": runtime_pytorch.settings.parameters.uri,
}
},
}

model_settings_path.write_text(json.dumps(model_settings_dict, indent=4))
Expand Down Expand Up @@ -156,21 +153,27 @@ def rest_client(rest_app: FastAPI) -> TestClient:

@pytest.fixture
async def anchor_image_runtime_with_remote_predict_patch(
custom_runtime_tf: MLModel,
remote_predict_mock_path: str = "mlserver_alibi_explain.common.remote_predict") -> AlibiExplainRuntime:
custom_runtime_tf: MLModel,
remote_predict_mock_path: str = "mlserver_alibi_explain.common.remote_predict",
) -> AlibiExplainRuntime:
with patch(remote_predict_mock_path) as remote_predict:

def mock_predict(*args, **kwargs):
# note: sometimes the event loop is not running and in this case we create a new one otherwise
# we use the existing one.
# this mock implementation is required as we dont want to spin up a server, we just use MLModel.predict
try:
loop = asyncio.get_event_loop()
res = loop.run_until_complete(custom_runtime_tf.predict(kwargs["v2_payload"]))
res = loop.run_until_complete(
custom_runtime_tf.predict(kwargs["v2_payload"])
)
return res
except Exception:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
res = loop.run_until_complete(custom_runtime_tf.predict(kwargs["v2_payload"]))
res = loop.run_until_complete(
custom_runtime_tf.predict(kwargs["v2_payload"])
)
return res

remote_predict.side_effect = mock_predict
Expand All @@ -181,10 +184,9 @@ def mock_predict(*args, **kwargs):
parameters=ModelParameters(
uri=f"{TESTS_PATH}/data/mnist_anchor_image",
extra=AlibiExplainSettings(
explainer_type="anchor_image",
infer_uri=f"dummy_call"
)
)
explainer_type="anchor_image", infer_uri=f"dummy_call"
),
),
)
)
await rt.load()
Expand All @@ -199,17 +201,13 @@ async def integrated_gradients_runtime() -> AlibiExplainRuntime:
parallel_workers=1,
parameters=ModelParameters(
extra=AlibiExplainSettings(
init_parameters={
"n_steps": 50,
"method": "gausslegendre"
},
init_parameters={"n_steps": 50, "method": "gausslegendre"},
explainer_type="integrated_gradients",
infer_uri=f"{TESTS_PATH}/data/tf_mnist/model.h5"
infer_uri=f"{TESTS_PATH}/data/tf_mnist/model.h5",
)
)
),
)
)
await rt.load()

return rt

31 changes: 20 additions & 11 deletions runtimes/alibi-explain/tests/test_black_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ def payload() -> InferenceRequest:


async def test_predict_impl(
anchor_image_runtime_with_remote_predict_patch: AlibiExplainRuntime,
custom_runtime_tf: MLModel
anchor_image_runtime_with_remote_predict_patch: AlibiExplainRuntime,
custom_runtime_tf: MLModel,
):
# note: custom_runtime_tf is the underlying inference runtime
# we want to test that the underlying impl predict is functionally correct
Expand All @@ -58,7 +58,9 @@ async def test_predict_impl(
],
)
expected_result = await custom_runtime_tf.predict(inference_request)
expected_result_numpy = NumpyCodec.decode_response_output(expected_result.outputs[0])
expected_result_numpy = NumpyCodec.decode_response_output(
expected_result.outputs[0]
)

assert_array_equal(actual_result, expected_result_numpy)

Expand All @@ -71,15 +73,22 @@ def alibi_anchor_image_model():


async def test_end_2_end(
anchor_image_runtime_with_remote_predict_patch: AlibiExplainRuntime,
alibi_anchor_image_model,
payload: InferenceRequest
anchor_image_runtime_with_remote_predict_patch: AlibiExplainRuntime,
alibi_anchor_image_model,
payload: InferenceRequest,
):
# in this test we are getting explanation and making sure that it the same one as returned by alibi
# directly
runtime_result = await anchor_image_runtime_with_remote_predict_patch.predict(payload)
decoded_runtime_results = json.loads(convert_from_bytes(runtime_result.outputs[0], ty=str))
alibi_result = alibi_anchor_image_model.explain(NumpyCodec.decode(payload.inputs[0]))

assert_array_equal(np.array(decoded_runtime_results["data"]["anchor"]), alibi_result.data["anchor"])
runtime_result = await anchor_image_runtime_with_remote_predict_patch.predict(
payload
)
decoded_runtime_results = json.loads(
convert_from_bytes(runtime_result.outputs[0], ty=str)
)
alibi_result = alibi_anchor_image_model.explain(
NumpyCodec.decode(payload.inputs[0])
)

assert_array_equal(
np.array(decoded_runtime_results["data"]["anchor"]), alibi_result.data["anchor"]
)
Loading

0 comments on commit 524b812

Please sign in to comment.