示例#1
0
class ServingFrontend(object):
    """ServingFrontend warps up all internal services including 
    model service manager, request handler. It provides all public 
    apis for users to extend and use our system.
    """
    def __init__(self, app_name):
        """
        Initialize handler for FlaskHandler and ServiceManager.

        Parameters
        ----------
        app_name : str
            App name to initialize request handler.
        """
        try:
            self.service_manager = ServiceManager()
            self.handler = FlaskRequestHandler(app_name)

            logger.info('Initialized serving frontend.')
        except Exception as e:
            raise Exception('Failed to initialize serving frontend: ' + str(e))

    def start_handler(self, host, port):
        """
        Start handler using given host and port.

        Parameters
        ----------
        host : str
            Host that server will use.
        port: int
            Port that server will use.
        """
        self.handler.start_handler(host, port)

    def load_models(self, models, ModelServiceClassDef, gpu=None):
        """
        Load models by using user passed Model Service Class Definitions.

        Parameters
        ----------
        models : List of model_name, model_path pairs
            List of model_name, model_path pairs that will be initialized.
        ModelServiceClassDef: python class
            Model Service Class Definition which can initialize a model service.
        gpu : int
            Id of gpu device. If machine has two gpus, this number can be 0 or 1.
            If it is not set, cpu will be used.
        """
        for service_name, model_name, model_path, manifest in models:
            self.service_manager.load_model(service_name, model_name, model_path, manifest, ModelServiceClassDef, gpu)


    def register_module(self, user_defined_module_file_path):
        """
        Register a python module according to user_defined_module_name
        This module should contain a valid Model Service Class whose
        pre-process and post-process can be derived and customized.

        Parameters
        ----------
        user_defined_module_file_path : Python module file path
            A python module will be loaded according to this file path.
            

        Returns
        ----------
        List of model service class definitions.
            Those python class can be used to initialize model service.
        """
        model_class_definations = self.service_manager.parse_modelservices_from_module(user_defined_module_file_path)
        assert len(model_class_definations) >= 1, 'No valid python class derived from Base Model Service is in module file: %s' % user_defined_module_file_path

        for ModelServiceClassDef in model_class_definations:
            self.service_manager.add_modelservice_to_registry(ModelServiceClassDef.__name__, ModelServiceClassDef)

        return model_class_definations

    def get_registered_modelservices(self, modelservice_names=None):
        """
        Get all registered Model Service Class Definitions into a dictionary 
        according to name or list of names. 
        If nothing is passed, all registered model services will be returned.

        Parameters
        ----------
        modelservice_names : str or List, optional
            Names to retrieve registered model services
            
        Returns
        ----------
        Dict of name, model service pairs
            Registered model services according to given names.
        """
        if not isinstance(modelservice_names, list) and modelservice_names is not None:
            modelservice_names = [modelservice_names]

        return self.service_manager.get_modelservices_registry(modelservice_names)

    def get_loaded_modelservices(self, modelservice_names=None):
        """
        Get all model services which are loaded in the system into a dictionary 
        according to name or list of names. 
        If nothing is passed, all loaded model services will be returned.

        Parameters
        ----------
        modelservice_names : str or List, optional
            Names to retrieve loaded model services
            
        Returns
        ----------
        Dict of name, model service pairs
            Loaded model services according to given names.
        """
        if not isinstance(modelservice_names, list) and modelservice_names is not None:
            modelservice_names = [modelservice_names]

        return self.service_manager.get_loaded_modelservices(modelservice_names)

    def get_query_string(self, field):
        """
        Get field data in the query string from request.

        Parameters
        ----------
        field : str
            Field in the query string from request.
            
        Returns
        ----------
        Object
            Field data in query string.
        """
        return self.handler.get_query_string(field)

    def add_endpoint(self, api_definition, callback, **kwargs):
        """
        Add an endpoint with OpenAPI compatible api definition and callback.

        Parameters
        ----------
        api_definition : dict(json)
            OpenAPI compatible api definition.

        callback: function
            Callback function in the endpoint.

        kwargs: dict
            Arguments for callback functions.
        """
        endpoint = list(api_definition.keys())[0]
        method = list(api_definition[endpoint].keys())[0]
        api_name = api_definition[endpoint][method]['operationId']

        logger.info('Adding endpoint: %s to Flask' % api_name)
        self.handler.add_endpoint(api_name, endpoint, partial(callback, **kwargs), [method.upper()])

    def setup_openapi_endpoints(self, host, port):
        """
        Firstly, construct Openapi compatible api definition for 
        1. Predict
        2. Ping
        3. API description
        
        Then the api definition is used to setup web server endpoint.

        Parameters
        ----------
        host : str
            Host that server will use 

        port: int
            Host that server will use 
        """
        modelservices = self.service_manager.get_loaded_modelservices()
        # TODO: not hardcode host:port
        self.openapi_endpoints = {
            'swagger': '2.0',
            'info': {
                'version': '1.0.0',
                  'title': 'Model Serving Apis'
              },
              'host': host + ':' + str(port),
              'schemes': ['http'],
              'paths': {},
          }


        # 1. Predict endpoints
        for model_name, modelservice in modelservices.items():
            input_type = modelservice.signature['input_type']
            inputs = modelservice.signature['inputs']
            output_type = modelservice.signature['output_type']
            
            # Contruct predict openapi specs
            endpoint = '/' + model_name + '/predict'
            predict_api = {
                endpoint: {
                    'post': {
                        'operationId': model_name + '_predict', 
                        'consumes': ['multipart/form-data'],
                        'produces': [output_type],
                        'parameters': [],
                        'responses': {
                            '200': {}
                        }
                    }
                }
            }
            input_names = []
            # Setup endpoint for each modelservice
            for idx in range(len(inputs)):
                # Check input content type to set up proper openapi consumes field
                input_names.append(inputs[idx]['data_name'])
                if input_type == 'application/json':
                    parameter = {
                        'in': 'formData',
                        'name': inputs[idx]['data_name'],
                        'description': '%s should tensor with shape: %s' % 
                            (inputs[idx]['data_name'], inputs[idx]['data_shape'][1:]),
                        'required': 'true',
                        'schema': {
                            'type': 'string'
                        }
                    }
                elif input_type == 'image/jpeg':
                    parameter = {
                        'in': 'formData',
                        'name': inputs[idx]['data_name'],
                        'description': '%s should be image which will be resized to: %s' % 
                            (inputs[idx]['data_name'], inputs[idx]['data_shape'][1:]),
                        'required': 'true',
                        'type': 'file'
                    }
                else:
                    msg = '%s is not supported for input content-type' % (input_type)
                    logger.error(msg)
                    abort(500, "Service setting error. %s" % (msg))
                predict_api[endpoint]['post']['parameters'].append(parameter)

            # Contruct openapi response schema
            if output_type == 'application/json':
                responses = {
                    'description': 'OK',
                    'schema': {
                        'type': 'object',
                        'properties': {
                            'prediction': {
                                'type': 'string'
                            }
                        }
                    }
                }
            elif output_type == 'image/jpeg':
                responses = {
                    'description': 'OK',
                    'schema': {
                        'type': 'file'
                    }
                }
            else:
                msg = '%s is not supported for output content-type' % output_type
                logger.error(msg)
                abort(500, "Service setting error. %s" % (msg))
            predict_api[endpoint]['post']['responses']['200'].update(responses) 

            self.openapi_endpoints['paths'].update(predict_api)

            # Setup Flask endpoint for predict api
            self.add_endpoint(predict_api, 
                              self.predict_callback, 
                              modelservice=modelservice,
                              input_names=input_names,
                              model_name=model_name)


        # 2. Ping endpoints
        ping_api = {
            '/ping': {
                'get': {
                    'operationId': 'ping', 
                    'produces': ['application/json'],
                    'responses': {
                        '200': {
                            'description': 'OK',
                            'schema': {
                                'type': 'object',
                                'properties': {
                                    'health': {
                                        'type': 'string'
                                    }
                                }
                            }
                        }
                    }
                }
                
            }
        }
        self.openapi_endpoints['paths'].update(ping_api)
        self.add_endpoint(ping_api, self.ping_callback)


        # 3. Describe apis endpoints
        api_description_api = {
            '/api-description': {
                'get': {
                    'produces': ['application/json'],
                    'operationId': 'api-description',
                    'responses': {
                        '200': {
                            'description': 'OK',
                            'schema': {
                                'type': 'object',
                                'properties': {
                                    'description': {
                                        'type': 'string'
                                    }
                                }
                            }
                        }
                    }
                }
                
            }
        }
        self.openapi_endpoints['paths'].update(api_description_api)
        self.add_endpoint(api_description_api, self.api_description)

        return self.openapi_endpoints
    
    def ping_callback(self, **kwargs):
        """
        Callback function for ping endpoint.
            
        Returns
        ----------
        Response
            Http response for ping endpiont.
        """
        if 'PingTotal' in MetricsManager.metrics:
            MetricsManager.metrics['PingTotal'].update(metric=1)
        try:
            for model in self.service_manager.get_loaded_modelservices().values():
                model.ping()
        except Exception:
            logger.warn('Model serving is unhealthy.')
            return self.handler.jsonify({'health': 'unhealthy!'})

        return self.handler.jsonify({'health': 'healthy!'})

    def api_description(self, **kwargs):
        """
        Callback function for api description endpoint.

        Returns
        ----------
        Response
            Http response for api description endpiont.
        """
        if 'APIDescriptionTotal' in MetricsManager.metrics:
            MetricsManager.metrics['APIDescriptionTotal'].update(metric=1)
        return self.handler.jsonify({'description': self.openapi_endpoints})

    def predict_callback(self, **kwargs):
        """
        Callback for predict endpoint

        Parameters
        ----------
        modelservice : ModelService
            ModelService handler.

        input_names: list
            Input names in request form data.

        Returns
        ----------
        Response
            Http response for predict endpiont.
        """

        handler_start_time = time.time()
        modelservice = kwargs['modelservice']
        input_names = kwargs['input_names']
        model_name = kwargs['model_name']

        if model_name + '_PredictionTotal' in MetricsManager.metrics:
            MetricsManager.metrics[model_name + '_PredictionTotal'].update(metric=1)

        input_type = modelservice.signature['input_type']
        output_type = modelservice.signature['output_type']

        # Get data from request according to input type
        input_data = []
        if input_type == 'application/json':
            try:
                for name in input_names:
                    logger.info('Request input: ' + name +  ' should be json tensor.')
                    form_data = self.handler.get_form_data(name)
                    form_data = ast.literal_eval(form_data)
                    assert isinstance(form_data, list), "Input data for request argument: %s is not correct. " \
                                                        "%s is expected but got %s instead of list" \
                                                        % (name, input_type, type(form_data))
                    input_data.append(form_data)
            except Exception as e:
                if model_name + '_Prediction4XX' in MetricsManager.metrics:
                    MetricsManager.metrics[model_name + '_Prediction4XX'].update(metric=1)
                logger.error(str(e))
                abort(400, str(e))
        elif input_type == 'image/jpeg':
            try:
                for name in input_names:
                    logger.info('Request input: ' + name +  ' should be image with jpeg format.')
                    input_file = self.handler.get_file_data(name)
                    if input_file:
                        mime_type = input_file.content_type
                        assert mime_type == input_type, 'Input data for request argument: %s is not correct. ' \
                                                        '%s is expected but %s is given.' % (name, input_type, mime_type)
                        file_data = input_file.read()
                        assert isinstance(file_data, (str, bytes)), 'Image file buffer should be type str or ' \
                                                                    'bytes, but got %s' % (type(file_data))
                    else:
                        file_data = base64.decodestring(self.handler.get_form_data(name))
                    input_data.append(file_data)
            except Exception as e:
                if model_name + '_Prediction4XX' in MetricsManager.metrics:
                    MetricsManager.metrics[model_name + '_Prediction4XX'].update(metric=1)
                logger.error(str(e))
                abort(400, str(e))
        else:
            msg = '%s is not supported for input content-type' % (input_type)
            if model_name + '_Prediction5XX' in MetricsManager.metrics:
                MetricsManager.metrics[model_name + '_Prediction5XX'].update(metric=1)
            logger.error(msg)
            abort(500, "Service setting error. %s" % (msg))

        # Doing prediciton on model
        try:
            response = modelservice.inference(input_data)
        except Exception:
            if model_name + '_Prediction5XX' in MetricsManager.metrics:
                MetricsManager.metrics[model_name + '_Prediction5XX'].update(metric=1)
            logger.error(str(traceback.format_exc()))
            abort(500, "Error occurs while inference was executed on server.")

        # Construct response according to output type
        if output_type == 'application/json':
            logger.info('Response is text.')
        elif output_type == 'image/jpeg':
            logger.info('Response is jpeg image encoded in base64 string.')
        else:
            msg = '%s is not supported for input content-type.' % (output_type)
            if model_name + '_Prediction5XX' in MetricsManager.metrics:
                MetricsManager.metrics[model_name + '_Prediction5XX'].update(metric=1)
            logger.error(msg)
            abort(500, "Service setting error. %s" % (msg))

        logger.debug("Prediction request handling time is: %s ms" %
                     ((time.time() - handler_start_time) * 1000))
        return self.handler.jsonify({'prediction': response})
示例#2
0
class ServingFrontend(object):
    '''ServingFrontend warps up all internal services including 
    model service manager, request handler. It provides all public 
    apis for users to extend and use our system.
    '''
    def __init__(self, app_name):
        '''
        Initialize handler for FlaskHandler and ServiceManager.

        Parameters
        ----------
        app_name : string
            App name to initialize request handler.
        '''
        try:
            self.service_manager = ServiceManager()
            self.handler = FlaskRequestHandler(app_name)

            logger.info('Initialized serving frontend.')
        except Exception as e:
            raise Exception('Failed to initialize serving frontend: ' + str(e))

    def start_model_serving(self, host, port):
        '''
        Start model serving using given host and port.

        Parameters
        ----------
        host : string
            Host that server will use.
        port: int
            Port that server will use.
        '''
        self.handler.start_handler(host, port)

    def load_models(self, models, ModelServiceClassDef):
        '''
        Load models by using user passed Model Service Class Definitions.

        Parameters
        ----------
        models : List of model_name, model_path pairs
            List of model_name, model_path pairs that will be initialized.
        ModelServiceClassDef: python class
            Model Service Class Definition which can initialize a model service.
        '''
        for model_name, model_path in models.items():
            self.service_manager.load_model(model_name, model_path,
                                            ModelServiceClassDef)

    def register_module(self, user_defined_module_file_path):
        '''
        Register a python module according to user_defined_module_name
        This module should contain a valid Model Service Class whose
        pre-process and post-process can be derived and customized.

        Parameters
        ----------
        user_defined_module_file_path : Python module file path
            A python module will be loaded according to this file path.
            

        Returns
        ----------
        List of model service class definitions.
            Those python class can be used to initialize model service.
        '''
        model_class_definations = self.service_manager.parse_modelservices_from_module(
            user_defined_module_file_path)
        assert len(
            model_class_definations
        ) >= 1, 'No valid python class derived from Base Model Service is in module file: %s' % user_defined_module_file_path

        for ModelServiceClassDef in model_class_definations:
            self.service_manager.add_modelservice_to_registry(
                ModelServiceClassDef.__name__, ModelServiceClassDef)

        return model_class_definations

    def get_registered_modelservices(self, modelservice_names=None):
        '''
        Get all registered Model Service Class Definitions into a dictionary 
        according to name or list of names. 
        If nothing is passed, all registered model services will be returned.

        Parameters
        ----------
        modelservice_names : string or List, optional
            Names to retrieve registered model services
            
        Returns
        ----------
        Dict of name, model service pairs
            Registered model services according to given names.
        '''
        if not isinstance(modelservice_names,
                          list) and modelservice_names is not None:
            modelservice_names = [modelservice_names]

        return self.service_manager.get_modelservices_registry(
            modelservice_names)

    def get_loaded_modelservices(self, modelservice_names=None):
        '''
        Get all model services which are loaded in the system into a dictionary 
        according to name or list of names. 
        If nothing is passed, all loaded model services will be returned.

        Parameters
        ----------
        modelservice_names : string or List, optional
            Names to retrieve loaded model services
            
        Returns
        ----------
        Dict of name, model service pairs
            Loaded model services according to given names.
        '''
        if not isinstance(modelservice_names,
                          list) and modelservice_names is not None:
            modelservice_names = [modelservice_names]

        return self.service_manager.get_loaded_modelservices(
            modelservice_names)

    def get_query_string(self, field):
        '''
        Get field data in the query string from request.

        Parameters
        ----------
        field : string
            Field in the query string from request.
            
        Returns
        ----------
        Object
            Field data in query string.
        '''
        return self.handler.get_query_string(field)

    def add_endpoint(self, api_definition, callback, **kwargs):
        '''
        Add an endpoint with OpenAPI compatible api definition and callback.

        Parameters
        ----------
        api_definition : dict(json)
            OpenAPI compatible api definition.

        callback: function
            Callback function in the endpoint.

        kwargs: dict
            Arguments for callback functions.
        '''
        endpoint = list(api_definition.keys())[0]
        method = list(api_definition[endpoint].keys())[0]
        api_name = api_definition[endpoint][method]['operationId']

        logger.info('Adding endpoint: %s to Flask' % api_name)
        self.handler.add_endpoint(api_name, endpoint,
                                  partial(callback, **kwargs),
                                  [method.upper()])

    def setup_openapi_endpoints(self, host, port):
        '''
        Firstly, construct Openapi compatible api definition for 
        1. Predict
        2. Ping
        3. API description
        
        Then the api definition is used to setup web server endpoint.

        Parameters
        ----------
        host : string
            Host that server will use 

        port: int
            Host that server will use 
        '''
        modelservices = self.service_manager.get_loaded_modelservices()
        # TODO: not hardcode host:port
        self.openapi_endpoints = {
            'swagger': '2.0',
            'info': {
                'version': '1.0.0',
                'title': 'Model Serving Apis'
            },
            'host': host + ':' + str(port),
            'schemes': ['http'],
            'paths': {},
        }

        # 1. Predict endpoints
        for model_name, modelservice in modelservices.items():
            input_type = modelservice.signature['input_type']
            inputs = modelservice.signature['inputs']
            output_type = modelservice.signature['output_type']

            # Contruct predict openapi specs
            endpoint = '/' + model_name + '/predict'
            predict_api = {
                endpoint: {
                    'post': {
                        'operationId': model_name + '_predict',
                        'consumes': ['multipart/form-data'],
                        'produces': [output_type],
                        'parameters': [],
                        'responses': {
                            '200': {}
                        }
                    }
                }
            }
            input_names = ['input' + str(idx) for idx in range(len(inputs))]
            # Setup endpoint for each modelservice
            for idx in range(len(inputs)):
                # Check input content type to set up proper openapi consumes field
                if input_type == 'application/json':
                    parameter = {
                        'in':
                        'formData',
                        'name':
                        input_names[idx],
                        'description':
                        '%s should tensor with shape: %s' %
                        (input_names[idx], inputs[idx]['data_shape'][1:]),
                        'required':
                        'true',
                        'schema': {
                            'type': 'string'
                        }
                    }
                elif input_type == 'image/jpeg':
                    parameter = {
                        'in':
                        'formData',
                        'name':
                        input_names[idx],
                        'description':
                        '%s should be image with shape: %s' %
                        (input_names[idx], inputs[idx]['data_shape'][1:]),
                        'required':
                        'true',
                        'type':
                        'file'
                    }
                else:
                    raise Exception(
                        '%s is not supported for input content-type' %
                        input_type)
                predict_api[endpoint]['post']['parameters'].append(parameter)

            # Contruct openapi response schema
            if output_type == 'application/json':
                responses = {
                    'description': 'OK',
                    'schema': {
                        'type': 'object',
                        'properties': {
                            'prediction': {
                                'type': 'string'
                            }
                        }
                    }
                }
            elif output_type == 'image/jpeg':
                responses = {'description': 'OK', 'schema': {'type': 'file'}}
            else:
                raise Exception('%s is not supported for output content-type' %
                                output_type)
            predict_api[endpoint]['post']['responses']['200'].update(responses)

            self.openapi_endpoints['paths'].update(predict_api)

            # Setup Flask endpoint for predict api
            self.add_endpoint(predict_api,
                              self.predict_callback,
                              modelservice=modelservice,
                              input_names=input_names)

        # 2. Ping endpoints
        ping_api = {
            '/ping': {
                'get': {
                    'operationId': 'ping',
                    'produces': ['application/json'],
                    'responses': {
                        '200': {
                            'description': 'OK',
                            'schema': {
                                'type': 'object',
                                'properties': {
                                    'health': {
                                        'type': 'string'
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        self.openapi_endpoints['paths'].update(ping_api)
        self.add_endpoint(ping_api, self.ping_callback)

        # 3. Describe apis endpoints
        api_description_api = {
            '/api-description': {
                'get': {
                    'produces': ['application/json'],
                    'operationId': 'apiDescription',
                    'responses': {
                        '200': {
                            'description': 'OK',
                            'schema': {
                                'type': 'object',
                                'properties': {
                                    'description': {
                                        'type': 'string'
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        self.openapi_endpoints['paths'].update(api_description_api)
        self.add_endpoint(api_description_api, self.api_description)

        return self.openapi_endpoints

    def ping_callback(self, **kwargs):
        '''
        Callback function for ping endpoint.
            
        Returns
        ----------
        Response
            Http response for ping endpiont.
        '''
        try:
            for model in self.service_manager.get_loaded_modelservices(
            ).values():
                model.ping()
        except Exception:
            logger.warn('Model serving is unhealthy.')
            return self.handler.jsonify({'health': 'unhealthy!'})

        return self.handler.jsonify({'health': 'healthy!'})

    def api_description(self, **kwargs):
        '''
        Callback function for api description endpoint.

        Returns
        ----------
        Response
            Http response for api description endpiont.
        '''
        return self.handler.jsonify({'description': self.openapi_endpoints})

    def predict_callback(self, **kwargs):
        '''
        Callback for predict endpoint

        Parameters
        ----------
        modelservice : ModelService
            ModelService handler.

        input_names: list
            Input names in request form data.

        Returns
        ----------
        Response
            Http response for predict endpiont.
        '''
        modelservice = kwargs['modelservice']
        input_names = kwargs['input_names']

        input_type = modelservice.signature['input_type']
        output_type = modelservice.signature['output_type']

        # Get data from request according to input type
        input_data = []
        if input_type == 'application/json':
            form_data = None
            try:
                for name in input_names:
                    logger.info('Request input: ' + name +
                                ' should be json tensor.')
                    form_data = self.handler.get_form_data(name)
                    assert isinstance(form_data, dict)
                    input_data.append(form_data)
            except:
                raise Exception(
                    'Type for request argument %s is not correct. %s is expected but %s is given.'
                    % (name, input_type, type(form_data)))
        elif input_type == 'image/jpeg':
            file_data = None
            try:
                for name in input_names:
                    logger.info('Request input: ' + name +
                                ' should be image with jpeg format.')
                    file_data = self.handler.get_file_data(name).read()
                    assert isinstance(file_data, (str, bytes))
                    input_data.append(file_data)
            except Exception as e:
                raise Exception(
                    'Input data for request argument: %s is not correct. %s is expected but %s '
                    'is given.' % (str(e), input_type, type(file_data)))
        else:
            logger.warn('%s is not supported for input content-type' %
                        input_type)
            raise Exception('%s is not supported for input content-type' %
                            input_type)

        # Doing prediciton on model
        try:
            response = modelservice.inference(input_data)
        except Exception as e:
            raise Exception('MXNet prediction run-time error')

        # Construct response according to output type
        if output_type == 'application/json':
            logger.info('Response is text.')
            return self.handler.jsonify({'prediction': response})
        elif output_type == 'image/jpeg':
            logger.info('Response is jpeg image encoded in base64 string.')
            return self.handler.jsonify({'prediction': response})
        else:
            logger.warn('%s is not supported for input content-type' %
                        output_type)
            raise Exception('%s is not supported for output content-type' %
                            output_type)