Ejemplo n.º 1
0
  def testIsOldModelArtifact(self):
    artifact = standard_artifacts.Examples()
    with self.assertRaisesRegex(AssertionError, 'Wrong artifact type'):
      path_utils.is_old_model_artifact(artifact)

    artifact = standard_artifacts.Model()
    self.assertFalse(path_utils.is_old_model_artifact(artifact))
    artifact.mlmd_artifact.state = metadata_store_pb2.Artifact.LIVE
    self.assertTrue(path_utils.is_old_model_artifact(artifact))
Ejemplo n.º 2
0
    def _PrepareModelPath(
            self, model: types.Artifact,
            serving_spec: infra_validator_pb2.ServingSpec) -> Text:
        model_path = path_utils.serving_model_path(
            model.uri, path_utils.is_old_model_artifact(model))
        serving_binary = serving_spec.WhichOneof('serving_binary')
        if serving_binary == _TENSORFLOW_SERVING:
            # TensorFlow Serving requires model to be stored in its own directory
            # structure flavor. If current model_path does not conform to the flavor,
            # we need to make a copy to the temporary path.
            try:
                # Check whether current model_path conforms to the tensorflow serving
                # model path flavor. (Parsed without exception)
                tf_serving_flavor.parse_model_path(
                    model_path, expected_model_name=serving_spec.model_name)
            except ValueError:
                # Copy the model to comply with the tensorflow serving model path
                # flavor.
                temp_model_path = tf_serving_flavor.make_model_path(
                    model_base_path=self._get_tmp_dir(),
                    model_name=serving_spec.model_name,
                    version=int(time.time()))
                io_utils.copy_dir(src=model_path, dst=temp_model_path)
                self._AddCleanup(io_utils.delete_dir,
                                 self._context.get_tmp_path())
                return temp_model_path

        return model_path
Ejemplo n.º 3
0
    def GetModelPath(self, input_dict: Dict[str, List[types.Artifact]]) -> str:
        """Get input model path to push.

    Pusher can push various types of artifacts if it contains the model. This
    method decides which artifact type is given to the Pusher and extracts the
    real model path. Subclass of Pusher Executor should use this method to
    acquire the source model path.

    Args:
      input_dict: A dictionary of artifacts that is given as the fisrt argument
          to the Executor.Do() method.
    Returns:
      A resolved input model path.
    Raises:
      RuntimeError: If no model path is found from input_dict.
    """
        # Check input_dict['model'] first.
        models = input_dict.get(standard_component_specs.MODEL_KEY)
        if models:
            model = artifact_utils.get_single_instance(models)
            return path_utils.serving_model_path(
                model.uri, path_utils.is_old_model_artifact(model))

        # Falls back to input_dict['infra_blessing']
        blessed_models = input_dict.get(
            standard_component_specs.INFRA_BLESSING_KEY)
        if not blessed_models:
            # Should not reach here; Pusher.__init__ prohibits creating a component
            # without having any of model or infra_blessing inputs.
            raise RuntimeError('Pusher has no model input.')
        model = artifact_utils.get_single_instance(blessed_models)
        if not model.get_int_custom_property(_INFRA_BLESSING_MODEL_FLAG_KEY):
            raise RuntimeError('InfraBlessing does not contain a model. Check '
                               'request_spec.make_warmup is set to True.')
        return path_utils.stamped_model_path(model.uri)
Ejemplo n.º 4
0
    def _GetFnArgs(self, input_dict: Dict[Text, List[types.Artifact]],
                   output_dict: Dict[Text, List[types.Artifact]],
                   exec_properties: Dict[Text, Any]) -> fn_args_utils.FnArgs:
        # TODO(ruoyu): Make this a dict of tag -> uri instead of list.
        if input_dict.get(standard_component_specs.BASE_MODEL_KEY):
            base_model_artifact = artifact_utils.get_single_instance(
                input_dict[standard_component_specs.BASE_MODEL_KEY])
            base_model = path_utils.serving_model_path(
                base_model_artifact.uri,
                path_utils.is_old_model_artifact(base_model_artifact))
        else:
            base_model = None

        if input_dict.get(standard_component_specs.HYPERPARAMETERS_KEY):
            hyperparameters_file = io_utils.get_only_uri_in_dir(
                artifact_utils.get_single_uri(
                    input_dict[standard_component_specs.HYPERPARAMETERS_KEY]))
            hyperparameters_config = json.loads(
                file_io.read_file_to_string(hyperparameters_file))
        else:
            hyperparameters_config = None

        output_path = artifact_utils.get_single_uri(
            output_dict[standard_component_specs.MODEL_KEY])
        serving_model_dir = path_utils.serving_model_dir(output_path)
        eval_model_dir = path_utils.eval_model_dir(output_path)

        model_run_dir = artifact_utils.get_single_uri(
            output_dict[standard_component_specs.MODEL_RUN_KEY])

        # TODO(b/126242806) Use PipelineInputs when it is available in third_party.
        result = fn_args_utils.get_common_fn_args(input_dict, exec_properties)
        if result.custom_config and not isinstance(result.custom_config, dict):
            raise ValueError(
                'custom_config in execution properties needs to be a '
                'dict. Got %s instead.' % type(result.custom_config))
        result.transform_output = result.transform_graph_path
        result.serving_model_dir = serving_model_dir
        result.eval_model_dir = eval_model_dir
        result.model_run_dir = model_run_dir
        result.schema_file = result.schema_path
        result.base_model = base_model
        result.hyperparameters = hyperparameters_config
        return result
Ejemplo n.º 5
0
def build_requests(  # pylint: disable=invalid-name
        model_name: Text, model: types.Artifact, examples: types.Artifact,
        request_spec: infra_validator_pb2.RequestSpec) -> List[
            iv_types.Request]:
    """Build model server requests.

  Examples artifact will be used as a data source to build requests. Caller
  should guarantee that the logical format of the Examples artifact should be
  compatible with request type to build.

  Args:
    model_name: A model name that model server recognizes.
    model: A model artifact for model signature analysis.
    examples: An `Examples` artifact for request data source.
    request_spec: A `RequestSpec` config.

  Returns:
    A list of request protos.
  """
    split_name = request_spec.split_name or None
    num_examples = request_spec.num_examples or _DEFAULT_NUM_EXAMPLES

    kind = request_spec.WhichOneof('kind')
    if kind == _TENSORFLOW_SERVING:
        spec = request_spec.tensorflow_serving
        signatures = _parse_saved_model_signatures(
            model_path=path_utils.serving_model_path(
                model.uri, path_utils.is_old_model_artifact(model)),
            tag_set=spec.tag_set,
            signature_names=spec.signature_names)
        builder = _TFServingRpcRequestBuilder(model_name=model_name,
                                              signatures=signatures)
    else:
        raise NotImplementedError(
            'Unsupported RequestSpec kind {!r}'.format(kind))

    builder.ReadExamplesArtifact(examples,
                                 split_name=split_name,
                                 num_examples=num_examples)

    return builder.BuildRequests()
Ejemplo n.º 6
0
    def Do(self, input_dict: Dict[Text, List[types.Artifact]],
           output_dict: Dict[Text, List[types.Artifact]],
           exec_properties: Dict[Text, Any]) -> None:
        """Push model to target directory if blessed.

    Args:
      input_dict: Input dict from input key to a list of artifacts, including:
        - model: exported model from trainer.
        - model_blessing: model blessing path from model_validator.  A push
          action delivers the model exports produced by Trainer to the
          destination defined in component config.
      output_dict: Output dict from key to a list of artifacts, including:
        - pushed_model: A list of 'ModelPushPath' artifact of size one. It will
          include the model in this push execution if the model was pushed.
      exec_properties: A dict of execution properties, including:
        - push_destination: JSON string of pusher_pb2.PushDestination instance,
          providing instruction of destination to push model.

    Returns:
      None
    """
        self._log_startup(input_dict, output_dict, exec_properties)
        model_push = artifact_utils.get_single_instance(
            output_dict[standard_component_specs.PUSHED_MODEL_KEY])
        if not self.CheckBlessing(input_dict):
            self._MarkNotPushed(model_push)
            return
        model_export = artifact_utils.get_single_instance(
            input_dict[standard_component_specs.MODEL_KEY])
        model_path = path_utils.serving_model_path(
            model_export.uri, path_utils.is_old_model_artifact(model_export))

        # Push model to the destination, which can be listened by a model server.
        #
        # If model is already successfully copied to outside before, stop copying.
        # This is because model validator might blessed same model twice (check
        # mv driver) with different blessing output, we still want Pusher to
        # handle the mv output again to keep metadata tracking, but no need to
        # copy to outside path again..
        # TODO(jyzhao): support rpc push and verification.
        push_destination = pusher_pb2.PushDestination()
        proto_utils.json_to_proto(
            exec_properties[standard_component_specs.PUSH_DESTINATION_KEY],
            push_destination)

        destination_kind = push_destination.WhichOneof('destination')
        if destination_kind == 'filesystem':
            fs_config = push_destination.filesystem
            if fs_config.versioning == _Versioning.AUTO:
                fs_config.versioning = _Versioning.UNIX_TIMESTAMP
            if fs_config.versioning == _Versioning.UNIX_TIMESTAMP:
                model_version = str(int(time.time()))
            else:
                raise NotImplementedError('Invalid Versioning {}'.format(
                    fs_config.versioning))
            logging.info('Model version: %s', model_version)
            serving_path = os.path.join(fs_config.base_directory,
                                        model_version)

            if fileio.exists(serving_path):
                logging.info(
                    'Destination directory %s already exists, skipping current push.',
                    serving_path)
            else:
                # tf.serving won't load partial model, it will retry until fully copied.
                io_utils.copy_dir(model_path, serving_path)
                logging.info('Model written to serving path %s.', serving_path)
        else:
            raise NotImplementedError(
                'Invalid push destination {}'.format(destination_kind))

        # Copy the model to pushing uri for archiving.
        io_utils.copy_dir(model_path, model_push.uri)
        self._MarkPushed(model_push,
                         pushed_destination=serving_path,
                         pushed_version=model_version)
        logging.info('Model pushed to %s.', model_push.uri)
Ejemplo n.º 7
0
    def Do(self, input_dict: Dict[Text, List[types.Artifact]],
           output_dict: Dict[Text, List[types.Artifact]],
           exec_properties: Dict[Text, Any]):
        """Overrides the tfx_pusher_executor.

    Args:
      input_dict: Input dict from input key to a list of artifacts, including:
        - model_export: exported model from trainer.
        - model_blessing: model blessing path from evaluator.
      output_dict: Output dict from key to a list of artifacts, including:
        - model_push: A list of 'ModelPushPath' artifact of size one. It will
          include the model in this push execution if the model was pushed.
      exec_properties: Mostly a passthrough input dict for
        tfx.components.Pusher.executor.  custom_config.bigquery_serving_args is
        consumed by this class.  For the full set of parameters supported by
        Big Query ML, refer to https://cloud.google.com/bigquery-ml/

    Returns:
      None
    Raises:
      ValueError:
        If bigquery_serving_args is not in exec_properties.custom_config.
        If pipeline_root is not 'gs://...'
      RuntimeError: if the Big Query job failed.
    """
        self._log_startup(input_dict, output_dict, exec_properties)
        model_push = artifact_utils.get_single_instance(
            output_dict[standard_component_specs.PUSHED_MODEL_KEY])
        if not self.CheckBlessing(input_dict):
            self._MarkNotPushed(model_push)
            return

        model_export = artifact_utils.get_single_instance(
            input_dict[standard_component_specs.MODEL_KEY])
        model_export_uri = model_export.uri

        custom_config = json_utils.loads(
            exec_properties.get(_CUSTOM_CONFIG_KEY, 'null'))
        if custom_config is not None and not isinstance(custom_config, Dict):
            raise ValueError(
                'custom_config in execution properties needs to be a '
                'dict.')

        bigquery_serving_args = custom_config.get(SERVING_ARGS_KEY)
        # if configuration is missing error out
        if bigquery_serving_args is None:
            raise ValueError('Big Query ML configuration was not provided')

        bq_model_uri = '.'.join([
            bigquery_serving_args[_PROJECT_ID_KEY],
            bigquery_serving_args[_BQ_DATASET_ID_KEY],
            bigquery_serving_args[_MODEL_NAME_KEY],
        ])

        # Deploy the model.
        io_utils.copy_dir(src=path_utils.serving_model_path(
            model_export_uri, path_utils.is_old_model_artifact(model_export)),
                          dst=model_push.uri)
        model_path = model_push.uri
        if not model_path.startswith(_GCS_PREFIX):
            raise ValueError(
                'pipeline_root must be gs:// for BigQuery ML Pusher.')

        logging.info(
            'Deploying the model to BigQuery ML for serving: %s from %s',
            bigquery_serving_args, model_path)

        query = _BQML_CREATE_OR_REPLACE_MODEL_QUERY_TEMPLATE.format(
            model_uri=bq_model_uri, model_path=model_path)

        # TODO(zhitaoli): Refactor the executor_class_path creation into a common
        # utility function.
        executor_class_path = '%s.%s' % (self.__class__.__module__,
                                         self.__class__.__name__)
        with telemetry_utils.scoped_labels(
            {telemetry_utils.LABEL_TFX_EXECUTOR: executor_class_path}):
            default_query_job_config = bigquery.job.QueryJobConfig(
                labels=telemetry_utils.get_labels_dict())
        # TODO(b/181368842) Add integration test for BQML Pusher + Managed Pipeline
        client = bigquery.Client(
            default_query_job_config=default_query_job_config,
            project=bigquery_serving_args[_PROJECT_ID_KEY])

        try:
            query_job = client.query(query)
            query_job.result()  # Waits for the query to finish
        except Exception as e:
            raise RuntimeError('BigQuery ML Push failed: {}'.format(e))

        logging.info('Successfully deployed model %s serving from %s',
                     bq_model_uri, model_path)

        # Setting the push_destination to bigquery uri
        self._MarkPushed(model_push, pushed_destination=bq_model_uri)
Ejemplo n.º 8
0
    def Do(self, input_dict: Dict[Text, List[types.Artifact]],
           output_dict: Dict[Text, List[types.Artifact]],
           exec_properties: Dict[Text, Any]) -> None:
        """Runs batch inference on a given model with given input examples.

    Args:
      input_dict: Input dict from input key to a list of Artifacts.
        - examples: examples for inference.
        - model: exported model.
        - model_blessing: model blessing result, optional.
      output_dict: Output dict from output key to a list of Artifacts.
        - output: bulk inference results.
      exec_properties: A dict of execution properties.
        - model_spec: JSON string of bulk_inferrer_pb2.ModelSpec instance.
        - data_spec: JSON string of bulk_inferrer_pb2.DataSpec instance.

    Returns:
      None
    """
        self._log_startup(input_dict, output_dict, exec_properties)

        if output_dict.get(standard_component_specs.INFERENCE_RESULT_KEY):
            inference_result = artifact_utils.get_single_instance(
                output_dict[standard_component_specs.INFERENCE_RESULT_KEY])
        else:
            inference_result = None
        if output_dict.get(standard_component_specs.OUTPUT_EXAMPLES_KEY):
            output_examples = artifact_utils.get_single_instance(
                output_dict[standard_component_specs.OUTPUT_EXAMPLES_KEY])
        else:
            output_examples = None

        if 'examples' not in input_dict:
            raise ValueError('\'examples\' is missing in input dict.')
        if 'model' not in input_dict:
            raise ValueError('Input models are not valid, model '
                             'need to be specified.')
        if standard_component_specs.MODEL_BLESSING_KEY in input_dict:
            model_blessing = artifact_utils.get_single_instance(
                input_dict[standard_component_specs.MODEL_BLESSING_KEY])
            if not model_utils.is_model_blessed(model_blessing):
                logging.info('Model on %s was not blessed', model_blessing.uri)
                return
        else:
            logging.info(
                'Model blessing is not provided, exported model will be '
                'used.')

        model = artifact_utils.get_single_instance(
            input_dict[standard_component_specs.MODEL_KEY])
        model_path = path_utils.serving_model_path(
            model.uri, path_utils.is_old_model_artifact(model))
        logging.info('Use exported model from %s.', model_path)

        data_spec = bulk_inferrer_pb2.DataSpec()
        proto_utils.json_to_proto(
            exec_properties[standard_component_specs.DATA_SPEC_KEY], data_spec)

        output_example_spec = bulk_inferrer_pb2.OutputExampleSpec()
        if exec_properties.get(
                standard_component_specs.OUTPUT_EXAMPLE_SPEC_KEY):
            proto_utils.json_to_proto(
                exec_properties[
                    standard_component_specs.OUTPUT_EXAMPLE_SPEC_KEY],
                output_example_spec)

        self._run_model_inference(
            data_spec, output_example_spec,
            input_dict[standard_component_specs.EXAMPLES_KEY], output_examples,
            inference_result,
            self._get_inference_spec(model_path, exec_properties))
Ejemplo n.º 9
0
    def Do(self, input_dict: Dict[str, List[types.Artifact]],
           output_dict: Dict[str, List[types.Artifact]],
           exec_properties: Dict[str, Any]) -> None:
        """Runs batch inference on a given model with given input examples.

    This function creates a new model (if necessary) and a new model version
    before inference, and cleans up resources after inference. It provides
    re-executability as it cleans up (only) the model resources that are created
    during the process even inference job failed.

    Args:
      input_dict: Input dict from input key to a list of Artifacts.
        - examples: examples for inference.
        - model: exported model.
        - model_blessing: model blessing result
      output_dict: Output dict from output key to a list of Artifacts.
        - output: bulk inference results.
      exec_properties: A dict of execution properties.
        - data_spec: JSON string of bulk_inferrer_pb2.DataSpec instance.
        - custom_config: custom_config.ai_platform_serving_args need to contain
          the serving job parameters sent to Google Cloud AI Platform. For the
          full set of parameters, refer to
          https://cloud.google.com/ml-engine/reference/rest/v1/projects.models

    Returns:
      None
    """
        self._log_startup(input_dict, output_dict, exec_properties)

        if output_dict.get('inference_result'):
            inference_result = artifact_utils.get_single_instance(
                output_dict['inference_result'])
        else:
            inference_result = None
        if output_dict.get('output_examples'):
            output_examples = artifact_utils.get_single_instance(
                output_dict['output_examples'])
        else:
            output_examples = None

        if 'examples' not in input_dict:
            raise ValueError('`examples` is missing in input dict.')
        if 'model' not in input_dict:
            raise ValueError('Input models are not valid, model '
                             'need to be specified.')
        if 'model_blessing' in input_dict:
            model_blessing = artifact_utils.get_single_instance(
                input_dict['model_blessing'])
            if not model_utils.is_model_blessed(model_blessing):
                logging.info('Model on %s was not blessed', model_blessing.uri)
                return
        else:
            logging.info(
                'Model blessing is not provided, exported model will be '
                'used.')
        if _CUSTOM_CONFIG_KEY not in exec_properties:
            raise ValueError(
                'Input exec properties are not valid, {} '
                'need to be specified.'.format(_CUSTOM_CONFIG_KEY))

        custom_config = json_utils.loads(
            exec_properties.get(_CUSTOM_CONFIG_KEY, 'null'))
        if custom_config is not None and not isinstance(custom_config, Dict):
            raise ValueError(
                'custom_config in execution properties needs to be a '
                'dict.')
        ai_platform_serving_args = custom_config.get(SERVING_ARGS_KEY)
        if not ai_platform_serving_args:
            raise ValueError(
                '`ai_platform_serving_args` is missing in `custom_config`')
        service_name, api_version = runner.get_service_name_and_api_version(
            ai_platform_serving_args)
        executor_class_path = '%s.%s' % (self.__class__.__module__,
                                         self.__class__.__name__)
        with telemetry_utils.scoped_labels(
            {telemetry_utils.LABEL_TFX_EXECUTOR: executor_class_path}):
            job_labels = telemetry_utils.make_labels_dict()
        model = artifact_utils.get_single_instance(input_dict['model'])
        model_path = path_utils.serving_model_path(
            model.uri, path_utils.is_old_model_artifact(model))
        logging.info('Use exported model from %s.', model_path)
        # Use model artifact uri to generate model version to guarantee the
        # 1:1 mapping from model version to model.
        model_version = 'version_' + hashlib.sha256(
            model.uri.encode()).hexdigest()
        inference_spec = self._get_inference_spec(model_path, model_version,
                                                  ai_platform_serving_args)
        data_spec = bulk_inferrer_pb2.DataSpec()
        proto_utils.json_to_proto(exec_properties['data_spec'], data_spec)
        output_example_spec = bulk_inferrer_pb2.OutputExampleSpec()
        if exec_properties.get('output_example_spec'):
            proto_utils.json_to_proto(exec_properties['output_example_spec'],
                                      output_example_spec)
        endpoint = custom_config.get(constants.ENDPOINT_ARGS_KEY)
        if endpoint and 'regions' in ai_platform_serving_args:
            raise ValueError(
                '`endpoint` and `ai_platform_serving_args.regions` cannot be set simultaneously'
            )
        api = discovery.build(
            service_name,
            api_version,
            requestBuilder=telemetry_utils.TFXHttpRequest,
            client_options=client_options.ClientOptions(api_endpoint=endpoint),
        )
        new_model_endpoint_created = False
        try:
            new_model_endpoint_created = runner.create_model_for_aip_prediction_if_not_exist(
                job_labels, ai_platform_serving_args, api)
            runner.deploy_model_for_aip_prediction(
                serving_path=model_path,
                model_version_name=model_version,
                ai_platform_serving_args=ai_platform_serving_args,
                api=api,
                labels=job_labels,
                skip_model_endpoint_creation=True,
                set_default=False,
            )
            self._run_model_inference(data_spec, output_example_spec,
                                      input_dict['examples'], output_examples,
                                      inference_result, inference_spec)
        except Exception as e:
            logging.error(
                'Error in executing CloudAIBulkInferrerComponent: %s', str(e))
            raise
        finally:
            # Guarantee newly created resources are cleaned up even if the inference
            # job failed.

            # Clean up the newly deployed model.
            runner.delete_model_from_aip_if_exists(
                model_version_name=model_version,
                ai_platform_serving_args=ai_platform_serving_args,
                api=api,
                delete_model_endpoint=new_model_endpoint_created)
Ejemplo n.º 10
0
 def testIsOldModelArtifact(self):
     artifact = standard_artifacts.Model()
     self.assertFalse(path_utils.is_old_model_artifact(artifact))
     artifact.mlmd_artifact.state = metadata_store_pb2.Artifact.LIVE
     self.assertTrue(path_utils.is_old_model_artifact(artifact))
Ejemplo n.º 11
0
    def Do(self, input_dict: Dict[Text, List[types.Artifact]],
           output_dict: Dict[Text, List[types.Artifact]],
           exec_properties: Dict[Text, Any]):
        """Overrides the tfx_pusher_executor.

    Args:
      input_dict: Input dict from input key to a list of artifacts, including:
        - model_export: exported model from trainer.
        - model_blessing: model blessing path from evaluator.
      output_dict: Output dict from key to a list of artifacts, including:
        - model_push: A list of 'ModelPushPath' artifact of size one. It will
          include the model in this push execution if the model was pushed.
      exec_properties: Mostly a passthrough input dict for
        tfx.components.Pusher.executor.  The following keys in `custom_config`
        are consumed by this class:
        - ai_platform_serving_args: For the full set of parameters supported
          by Google Cloud AI Platform, refer to
          https://cloud.google.com/ml-engine/reference/rest/v1/projects.models.versions#Version.
        - endpoint: Optional endpoint override. Should be in format of
          `https://[region]-ml.googleapis.com`. Default to global endpoint if
          not set. Using regional endpoint is recommended by Cloud AI Platform.
          When set, 'regions' key in ai_platform_serving_args cannot be set.
          For more details, please see
          https://cloud.google.com/ai-platform/prediction/docs/regional-endpoints#using_regional_endpoints

    Raises:
      ValueError:
        If ai_platform_serving_args is not in exec_properties.custom_config.
        If Serving model path does not start with gs://.
        If 'endpoint' and 'regions' are set simultanuously.
      RuntimeError: if the Google Cloud AI Platform training job failed.
    """
        self._log_startup(input_dict, output_dict, exec_properties)

        custom_config = json_utils.loads(
            exec_properties.get(_CUSTOM_CONFIG_KEY, 'null'))
        if custom_config is not None and not isinstance(custom_config, Dict):
            raise ValueError(
                'custom_config in execution properties needs to be a '
                'dict.')
        ai_platform_serving_args = custom_config.get(SERVING_ARGS_KEY)
        if not ai_platform_serving_args:
            raise ValueError(
                '\'ai_platform_serving_args\' is missing in \'custom_config\'')
        endpoint = custom_config.get(ENDPOINT_ARGS_KEY)
        if endpoint and 'regions' in ai_platform_serving_args:
            raise ValueError(
                '\'endpoint\' and \'ai_platform_serving_args.regions\' cannot be set simultanuously'
            )

        model_push = artifact_utils.get_single_instance(
            output_dict[standard_component_specs.PUSHED_MODEL_KEY])
        if not self.CheckBlessing(input_dict):
            self._MarkNotPushed(model_push)
            return

        model_export = artifact_utils.get_single_instance(
            input_dict[standard_component_specs.MODEL_KEY])

        service_name, api_version = runner.get_service_name_and_api_version(
            ai_platform_serving_args)
        # Deploy the model.
        io_utils.copy_dir(src=path_utils.serving_model_path(
            model_export.uri, path_utils.is_old_model_artifact(model_export)),
                          dst=model_push.uri)
        model_path = model_push.uri
        # TODO(jjong): Introduce Versioning.
        # Note that we're adding "v" prefix as Cloud AI Prediction only allows the
        # version name that starts with letters, and contains letters, digits,
        # underscore only.
        model_version = 'v{}'.format(int(time.time()))
        executor_class_path = '%s.%s' % (self.__class__.__module__,
                                         self.__class__.__name__)
        with telemetry_utils.scoped_labels(
            {telemetry_utils.LABEL_TFX_EXECUTOR: executor_class_path}):
            job_labels = telemetry_utils.get_labels_dict()
        endpoint = endpoint or runner.DEFAULT_ENDPOINT
        api = discovery.build(
            service_name,
            api_version,
            client_options=client_options.ClientOptions(api_endpoint=endpoint),
        )
        runner.deploy_model_for_aip_prediction(
            api,
            model_path,
            model_version,
            ai_platform_serving_args,
            job_labels,
        )

        self._MarkPushed(
            model_push,
            pushed_destination=_CAIP_MODEL_VERSION_PATH_FORMAT.format(
                project_id=ai_platform_serving_args['project_id'],
                model=ai_platform_serving_args['model_name'],
                version=model_version),
            pushed_version=model_version)
Ejemplo n.º 12
0
    def Do(self, input_dict: Dict[Text, List[types.Artifact]],
           output_dict: Dict[Text, List[types.Artifact]],
           exec_properties: Dict[Text, Any]) -> None:
        """Runs a batch job to evaluate the eval_model against the given input.

    Args:
      input_dict: Input dict from input key to a list of Artifacts.
        - model: exported model.
        - examples: examples for eval the model.
      output_dict: Output dict from output key to a list of Artifacts.
        - evaluation: model evaluation results.
      exec_properties: A dict of execution properties.
        - eval_config: JSON string of tfma.EvalConfig.
        - feature_slicing_spec: JSON string of evaluator_pb2.FeatureSlicingSpec
          instance, providing the way to slice the data. Deprecated, use
          eval_config.slicing_specs instead.
        - example_splits: JSON-serialized list of names of splits on which the
          metrics are computed. Default behavior (when example_splits is set to
          None) is using the 'eval' split.

    Returns:
      None
    """
        if EXAMPLES_KEY not in input_dict:
            raise ValueError('EXAMPLES_KEY is missing from input dict.')
        if EVALUATION_KEY not in output_dict:
            raise ValueError('EVALUATION_KEY is missing from output dict.')
        if MODEL_KEY in input_dict and len(input_dict[MODEL_KEY]) > 1:
            raise ValueError(
                'There can be only one candidate model, there are %d.' %
                (len(input_dict[MODEL_KEY])))
        if BASELINE_MODEL_KEY in input_dict and len(
                input_dict[BASELINE_MODEL_KEY]) > 1:
            raise ValueError(
                'There can be only one baseline model, there are %d.' %
                (len(input_dict[BASELINE_MODEL_KEY])))

        self._log_startup(input_dict, output_dict, exec_properties)

        # Add fairness indicator metric callback if necessary.
        fairness_indicator_thresholds = exec_properties.get(
            'fairness_indicator_thresholds', None)
        add_metrics_callbacks = None
        if fairness_indicator_thresholds:
            add_metrics_callbacks = [
                tfma.post_export_metrics.fairness_indicators(  # pytype: disable=module-attr
                    thresholds=fairness_indicator_thresholds),
            ]

        output_uri = artifact_utils.get_single_uri(
            output_dict[constants.EVALUATION_KEY])

        # Make sure user packages get propagated to the remote Beam worker.
        unused_module_path, extra_pip_packages = udf_utils.decode_user_module_key(
            exec_properties.get(MODULE_PATH_KEY, None))
        for pip_package_path in extra_pip_packages:
            local_pip_package_path = io_utils.ensure_local(pip_package_path)
            self._beam_pipeline_args.append('--extra_package=%s' %
                                            local_pip_package_path)

        eval_shared_model_fn = udf_utils.try_get_fn(
            exec_properties=exec_properties,
            fn_name='custom_eval_shared_model'
        ) or tfma.default_eval_shared_model

        run_validation = False
        models = []
        if EVAL_CONFIG_KEY in exec_properties and exec_properties[
                EVAL_CONFIG_KEY]:
            slice_spec = None
            has_baseline = bool(input_dict.get(BASELINE_MODEL_KEY))
            eval_config = tfma.EvalConfig()
            proto_utils.json_to_proto(exec_properties[EVAL_CONFIG_KEY],
                                      eval_config)
            eval_config = tfma.update_eval_config_with_defaults(
                eval_config, has_baseline=has_baseline)
            tfma.verify_eval_config(eval_config)
            # Do not validate model when there is no thresholds configured. This is to
            # avoid accidentally blessing models when users forget to set thresholds.
            run_validation = bool(
                tfma.metrics.metric_thresholds_from_metrics_specs(
                    eval_config.metrics_specs))
            if len(eval_config.model_specs) > 2:
                raise ValueError(
                    """Cannot support more than two models. There are %d models in this
             eval_config.""" % (len(eval_config.model_specs)))
            # Extract model artifacts.
            for model_spec in eval_config.model_specs:
                if MODEL_KEY not in input_dict:
                    if not model_spec.prediction_key:
                        raise ValueError(
                            'model_spec.prediction_key required if model not provided'
                        )
                    continue
                if model_spec.is_baseline:
                    model_artifact = artifact_utils.get_single_instance(
                        input_dict[BASELINE_MODEL_KEY])
                else:
                    model_artifact = artifact_utils.get_single_instance(
                        input_dict[MODEL_KEY])
                if tfma.get_model_type(model_spec) == tfma.TF_ESTIMATOR:
                    model_path = path_utils.eval_model_path(
                        model_artifact.uri,
                        path_utils.is_old_model_artifact(model_artifact))
                else:
                    model_path = path_utils.serving_model_path(
                        model_artifact.uri,
                        path_utils.is_old_model_artifact(model_artifact))
                logging.info('Using %s as %s model.', model_path,
                             model_spec.name)
                models.append(
                    eval_shared_model_fn(
                        eval_saved_model_path=model_path,
                        model_name=model_spec.name,
                        eval_config=eval_config,
                        add_metrics_callbacks=add_metrics_callbacks))
        else:
            eval_config = None
            assert (FEATURE_SLICING_SPEC_KEY in exec_properties
                    and exec_properties[FEATURE_SLICING_SPEC_KEY]
                    ), 'both eval_config and feature_slicing_spec are unset.'
            feature_slicing_spec = evaluator_pb2.FeatureSlicingSpec()
            proto_utils.json_to_proto(
                exec_properties[FEATURE_SLICING_SPEC_KEY],
                feature_slicing_spec)
            slice_spec = self._get_slice_spec_from_feature_slicing_spec(
                feature_slicing_spec)
            model_artifact = artifact_utils.get_single_instance(
                input_dict[MODEL_KEY])
            model_path = path_utils.eval_model_path(
                model_artifact.uri,
                path_utils.is_old_model_artifact(model_artifact))
            logging.info('Using %s for model eval.', model_path)
            models.append(
                eval_shared_model_fn(
                    eval_saved_model_path=model_path,
                    model_name='',
                    eval_config=None,
                    add_metrics_callbacks=add_metrics_callbacks))

        eval_shared_model = models[0] if len(models) == 1 else models
        schema = None
        if SCHEMA_KEY in input_dict:
            schema = io_utils.SchemaReader().read(
                io_utils.get_only_uri_in_dir(
                    artifact_utils.get_single_uri(input_dict[SCHEMA_KEY])))

        # Load and deserialize example splits from execution properties.
        example_splits = json_utils.loads(
            exec_properties.get(EXAMPLE_SPLITS_KEY, 'null'))
        if not example_splits:
            example_splits = ['eval']
            logging.info(
                "The 'example_splits' parameter is not set, using 'eval' "
                'split.')

        logging.info('Evaluating model.')
        # TempPipInstallContext is needed here so that subprocesses (which
        # may be created by the Beam multi-process DirectRunner) can find the
        # needed dependencies.
        # TODO(b/187122662): Move this to the ExecutorOperator or Launcher.
        with udf_utils.TempPipInstallContext(extra_pip_packages):
            with self._make_beam_pipeline() as pipeline:
                examples_list = []
                tensor_adapter_config = None
                # pylint: disable=expression-not-assigned
                if tfma.is_batched_input(eval_shared_model, eval_config):
                    tfxio_factory = tfxio_utils.get_tfxio_factory_from_artifact(
                        examples=[
                            artifact_utils.get_single_instance(
                                input_dict[EXAMPLES_KEY])
                        ],
                        telemetry_descriptors=_TELEMETRY_DESCRIPTORS,
                        schema=schema,
                        raw_record_column_name=tfma_constants.
                        ARROW_INPUT_COLUMN)
                    # TODO(b/161935932): refactor after TFXIO supports multiple patterns.
                    for split in example_splits:
                        file_pattern = io_utils.all_files_pattern(
                            artifact_utils.get_split_uri(
                                input_dict[EXAMPLES_KEY], split))
                        tfxio = tfxio_factory(file_pattern)
                        data = (pipeline
                                | 'ReadFromTFRecordToArrow[%s]' % split >>
                                tfxio.BeamSource())
                        examples_list.append(data)
                    if schema is not None:
                        # Use last tfxio as TensorRepresentations and ArrowSchema are fixed.
                        tensor_adapter_config = tensor_adapter.TensorAdapterConfig(
                            arrow_schema=tfxio.ArrowSchema(),
                            tensor_representations=tfxio.TensorRepresentations(
                            ))
                else:
                    for split in example_splits:
                        file_pattern = io_utils.all_files_pattern(
                            artifact_utils.get_split_uri(
                                input_dict[EXAMPLES_KEY], split))
                        data = (pipeline
                                | 'ReadFromTFRecord[%s]' % split >> beam.io.
                                ReadFromTFRecord(file_pattern=file_pattern))
                        examples_list.append(data)

                custom_extractors = udf_utils.try_get_fn(
                    exec_properties=exec_properties,
                    fn_name='custom_extractors')
                extractors = None
                if custom_extractors:
                    extractors = custom_extractors(
                        eval_shared_model=eval_shared_model,
                        eval_config=eval_config,
                        tensor_adapter_config=tensor_adapter_config)

                (examples_list | 'FlattenExamples' >> beam.Flatten()
                 | 'ExtractEvaluateAndWriteResults' >>
                 (tfma.ExtractEvaluateAndWriteResults(
                     eval_shared_model=models[0]
                     if len(models) == 1 else models,
                     eval_config=eval_config,
                     extractors=extractors,
                     output_path=output_uri,
                     slice_spec=slice_spec,
                     tensor_adapter_config=tensor_adapter_config)))
        logging.info('Evaluation complete. Results written to %s.', output_uri)

        if not run_validation:
            # TODO(jinhuang): delete the BLESSING_KEY from output_dict when supported.
            logging.info('No threshold configured, will not validate model.')
            return
        # Set up blessing artifact
        blessing = artifact_utils.get_single_instance(
            output_dict[BLESSING_KEY])
        blessing.set_string_custom_property(
            constants.ARTIFACT_PROPERTY_CURRENT_MODEL_URI_KEY,
            artifact_utils.get_single_uri(input_dict[MODEL_KEY]))
        blessing.set_int_custom_property(
            constants.ARTIFACT_PROPERTY_CURRENT_MODEL_ID_KEY,
            input_dict[MODEL_KEY][0].id)
        if input_dict.get(BASELINE_MODEL_KEY):
            baseline_model = input_dict[BASELINE_MODEL_KEY][0]
            blessing.set_string_custom_property(
                constants.ARTIFACT_PROPERTY_BASELINE_MODEL_URI_KEY,
                baseline_model.uri)
            blessing.set_int_custom_property(
                constants.ARTIFACT_PROPERTY_BASELINE_MODEL_ID_KEY,
                baseline_model.id)
        if 'current_component_id' in exec_properties:
            blessing.set_string_custom_property(
                'component_id', exec_properties['current_component_id'])
        # Check validation result and write BLESSED file accordingly.
        logging.info('Checking validation results.')
        validation_result = tfma.load_validation_result(output_uri)
        if validation_result.validation_ok:
            io_utils.write_string_file(
                os.path.join(blessing.uri, constants.BLESSED_FILE_NAME), '')
            blessing.set_int_custom_property(
                constants.ARTIFACT_PROPERTY_BLESSED_KEY,
                constants.BLESSED_VALUE)
        else:
            io_utils.write_string_file(
                os.path.join(blessing.uri, constants.NOT_BLESSED_FILE_NAME),
                '')
            blessing.set_int_custom_property(
                constants.ARTIFACT_PROPERTY_BLESSED_KEY,
                constants.NOT_BLESSED_VALUE)
        logging.info('Blessing result %s written to %s.',
                     validation_result.validation_ok, blessing.uri)