From b7fef6fd32725f6d663a111ff792b34b55451cd5 Mon Sep 17 00:00:00 2001 From: Winston Dong Date: Mon, 17 Sep 2018 17:29:35 +0000 Subject: [PATCH 1/4] Allow dict of list/ndarray for predict --- src/tf_container/proxy_client.py | 26 +++++++++++++------------- src/tf_container/serve.py | 4 ++-- test/unit/test_proxy_client.py | 1 + 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/tf_container/proxy_client.py b/src/tf_container/proxy_client.py index 5786c399..8dc67f58 100644 --- a/src/tf_container/proxy_client.py +++ b/src/tf_container/proxy_client.py @@ -202,25 +202,25 @@ def _create_input_map(self, data): """ - msg = """Unsupported request data format: {}. -Valid formats: tensor_pb2.TensorProto, dict and predict_pb2.PredictRequest""" - if isinstance(data, dict): - if all(isinstance(v, tensor_pb2.TensorProto) for k, v in data.items()): - return data - raise ValueError(msg.format(data)) + return {k: self._value_to_tensor(v) for k, v in data.items()} + + # When input data is not a dict, no tensor names are given, so use default + return {self.input_tensor_name: self._value_to_tensor(data)} - if isinstance(data, tensor_pb2.TensorProto): - return {self.input_tensor_name: data} + def _value_to_tensor(self, value): + if isinstance(value, tensor_pb2.TensorProto): + return value + msg = """Unable to convert value to TensorProto: {}. + Valid formats: tensor_pb2.TensorProto, list, numpy.ndarray""" try: # TODO: tensorflow container supports prediction requests with ONLY one tensor as input input_type = self.input_type_map.values()[0] - ndarray = np.asarray(data) - tensor_proto = make_tensor_proto(values=ndarray, dtype=input_type, shape=ndarray.shape) - return {self.input_tensor_name: tensor_proto} - except: - raise ValueError(msg.format(data)) + ndarray = np.asarray(value) + return make_tensor_proto(values=ndarray, dtype=input_type, shape=ndarray.shape) + except Exception: + raise ValueError(msg.format(value)) def _create_tf_example(feature_dict): diff --git a/src/tf_container/serve.py b/src/tf_container/serve.py index b41f3fea..d406ca4d 100644 --- a/src/tf_container/serve.py +++ b/src/tf_container/serve.py @@ -160,7 +160,7 @@ def __init__(self, grpc_proxy_client, transform_fn=None, input_fn=None, output_f @staticmethod def _parse_json_request(serialized_data): - ''' + """ json deserialization works in the following order: 1 - tries to deserialize the payload as a tensor using google.protobuf.json_format.Parse( payload, tensor_pb2.TensorProto()) @@ -170,7 +170,7 @@ def _parse_json_request(serialized_data): Returns: deserialized object - ''' + """ try: return json_format.Parse(serialized_data, tensor_pb2.TensorProto()) except json_format.ParseError: diff --git a/test/unit/test_proxy_client.py b/test/unit/test_proxy_client.py index 6b505ac3..8d762ccb 100644 --- a/test/unit/test_proxy_client.py +++ b/test/unit/test_proxy_client.py @@ -45,6 +45,7 @@ def set_up(): patcher.start() from tf_container.proxy_client import GRPCProxyClient proxy_client = GRPCProxyClient(9000, input_tensor_name='inputs', signature_name='serving_default') + proxy_client.input_type_map['sometype'] = 'somedtype' yield mock, proxy_client patcher.stop() From a58871b314931babc16d159488f9ac6d92c48c18 Mon Sep 17 00:00:00 2001 From: Date: Mon, 17 Sep 2018 11:09:36 -0700 Subject: [PATCH 2/4] Add unit test --- test/unit/test_proxy_client.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/unit/test_proxy_client.py b/test/unit/test_proxy_client.py index 8d762ccb..87ab2134 100644 --- a/test/unit/test_proxy_client.py +++ b/test/unit/test_proxy_client.py @@ -254,15 +254,27 @@ def test_predict_with_predict_request(set_up, set_up_requests): assert prediction == predict_fn.return_value -def test_predict_with_invalid_payload(set_up, set_up_requests): - mock, proxy_client = set_up +@patch('tf_container.proxy_client.make_tensor_proto', side_effect=Exception('tensor proto failed!')) +def test_predict_with_invalid_payload(make_tensor_proto, set_up, set_up_requests): + _, proxy_client = set_up data = complex('1+2j') with pytest.raises(ValueError) as error: proxy_client.predict(data) - assert 'Unsupported request data format' in str(error) + assert 'Unable to convert value to TensorProto' in str(error) + + +@patch('tf_container.proxy_client.make_tensor_proto', return_value='MyTensorProto') +def test_predict_create_input_map_with_dict_of_lists(make_tensor_proto, set_up, set_up_requests): + _, proxy_client = set_up + + data = {'mytensor': [1, 2, 3]} + + result = proxy_client._create_input_map(data) + assert result == {'mytensor': 'MyTensorProto'} + make_tensor_proto.assert_called_once() def test_classification_with_classification_request(set_up, set_up_requests): From b62b4673a820617db7e35adc61dd25d507812009 Mon Sep 17 00:00:00 2001 From: Winston Dong Date: Mon, 17 Sep 2018 18:51:49 +0000 Subject: [PATCH 3/4] Add integ test on dict of lists prediction --- test/integ/container_tests/layers_prediction.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/integ/container_tests/layers_prediction.py b/test/integ/container_tests/layers_prediction.py index 1f5e33e9..3edf9f8d 100644 --- a/test/integ/container_tests/layers_prediction.py +++ b/test/integ/container_tests/layers_prediction.py @@ -60,3 +60,16 @@ def test_json_request(): prediction_result = json.loads(serialized_output) assert len(prediction_result['outputs']['probabilities']['floatVal']) == 10 + + +def test_json_dict_of_lists(): + data = {'inputs': [x for x in xrange(784)]} + + url = "http://localhost:8080/invocations" + serialized_output = requests.post(url, + json.dumps(data), + headers={'Content-type': 'application/json'}).content + + prediction_result = json.loads(serialized_output) + + assert len(prediction_result['outputs']['probabilities']['floatVal']) == 10 \ No newline at end of file From 2f9990f6e6d18da88d12ed2e4178ebd321c92a00 Mon Sep 17 00:00:00 2001 From: Winston Dong Date: Tue, 18 Sep 2018 17:31:18 +0000 Subject: [PATCH 4/4] Update docstrings --- src/tf_container/proxy_client.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/tf_container/proxy_client.py b/src/tf_container/proxy_client.py index 8dc67f58..24031c01 100644 --- a/src/tf_container/proxy_client.py +++ b/src/tf_container/proxy_client.py @@ -151,7 +151,8 @@ def _create_classification_request(self, data): def _create_feature_dict_list(self, data): """ Parses the input data and returns a [dict] which will be used to create the tf examples. - If the input data is not a dict, a dictionary will be created with the default predict key PREDICT_INPUTS + If the input data is not a dict, a dictionary will be created with the default key PREDICT_INPUTS. + Used on the code path for creating ClassificationRequests. Examples: input => output @@ -184,18 +185,20 @@ def _raise_not_implemented_exception(self, data): def _create_input_map(self, data): """ - Parses the input data and returns a dict which will be used to create the predict request. + Parses the input data and returns a dict which will be used to create the PredictRequest. If the input data is not a dict, a dictionary will be created with the default predict key PREDICT_INPUTS input. Examples: input => output - {'inputs': tensor_proto} => {'inputs': tensor_proto} + ------------------------------------------------- tensor_proto => {PREDICT_INPUTS: tensor_proto} - [1,2,3] => {PREDICT_INPUTS: tensor_proto(1,2,3)} + {'custom_tensor_name': tensor_proto} => {'custom_tensor_name': TensorProto} + [1,2,3] => {PREDICT_INPUTS: TensorProto(1,2,3)} + {'custom_tensor_name': [1, 2, 3]} => {'custom_tensor_name': TensorProto(1,2,3)} Args: - data: request data. Can be any instance of dict, tensor_proto or any array like data. + data: request data. Can be any of: ndarray-like, TensorProto, dict, dict Returns: dict @@ -209,6 +212,7 @@ def _create_input_map(self, data): return {self.input_tensor_name: self._value_to_tensor(data)} def _value_to_tensor(self, value): + """Converts the given value to a tensor_pb2.TensorProto. Used on code path for creating PredictRequests.""" if isinstance(value, tensor_pb2.TensorProto): return value