github.com/apache/beam/sdks/v2@v2.48.2/python/apache_beam/ml/inference/onnx_inference_test.py (about)

     1  #
     2  # Licensed to the Apache Software Foundation (ASF) under one or more
     3  # contributor license agreements.  See the NOTICE file distributed with
     4  # this work for additional information regarding copyright ownership.
     5  # The ASF licenses this file to You under the Apache License, Version 2.0
     6  # (the "License"); you may not use this file except in compliance with
     7  # the License.  You may obtain a copy of the License at
     8  #
     9  #    http://www.apache.org/licenses/LICENSE-2.0
    10  #
    11  # Unless required by applicable law or agreed to in writing, software
    12  # distributed under the License is distributed on an "AS IS" BASIS,
    13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  # See the License for the specific language governing permissions and
    15  # limitations under the License.
    16  #
    17  
    18  # pytype: skip-file
    19  
    20  import os
    21  import shutil
    22  import tempfile
    23  import unittest
    24  from collections import OrderedDict
    25  
    26  import numpy as np
    27  import pytest
    28  
    29  import apache_beam as beam
    30  from apache_beam.testing.test_pipeline import TestPipeline
    31  from apache_beam.testing.util import assert_that
    32  from apache_beam.testing.util import equal_to
    33  
    34  # Protect against environments where onnx and pytorch library is not available.
    35  # pylint: disable=wrong-import-order, wrong-import-position, ungrouped-imports
    36  try:
    37    import onnxruntime as ort
    38    import torch
    39    from onnxruntime.capi.onnxruntime_pybind11_state import InvalidArgument
    40    import tensorflow as tf
    41    import tf2onnx
    42    from tensorflow.keras import layers
    43    from sklearn import linear_model
    44    from skl2onnx import convert_sklearn
    45    from skl2onnx.common.data_types import FloatTensorType
    46    from apache_beam.ml.inference.base import PredictionResult
    47    from apache_beam.ml.inference.base import RunInference
    48    from apache_beam.ml.inference.onnx_inference import default_numpy_inference_fn
    49    from apache_beam.ml.inference.onnx_inference import OnnxModelHandlerNumpy
    50  except ImportError:
    51    raise unittest.SkipTest('Onnx dependencies are not installed')
    52  
    53  try:
    54    from apache_beam.io.gcp.gcsfilesystem import GCSFileSystem
    55  except ImportError:
    56    GCSFileSystem = None  # type: ignore
    57  
    58  
    59  class PytorchLinearRegression(torch.nn.Module):
    60    def __init__(self, input_dim, output_dim):
    61      super().__init__()
    62      self.linear = torch.nn.Linear(input_dim, output_dim)
    63  
    64    def forward(self, x):
    65      out = self.linear(x)
    66      return out
    67  
    68    def generate(self, x):
    69      out = self.linear(x) + 0.5
    70      return out
    71  
    72  
    73  class TestDataAndModel():
    74    def get_one_feature_samples(self):
    75      return [
    76          np.array([1], dtype="float32"),
    77          np.array([5], dtype="float32"),
    78          np.array([-3], dtype="float32"),
    79          np.array([10.0], dtype="float32"),
    80      ]
    81  
    82    def get_one_feature_predictions(self):
    83      return [
    84          PredictionResult(ex, pred) for ex,
    85          pred in zip(
    86              self.get_one_feature_samples(),
    87              [example * 2.0 + 0.5 for example in self.get_one_feature_samples()])
    88      ]
    89  
    90    def get_two_feature_examples(self):
    91      return [
    92          np.array([1, 5], dtype="float32"),
    93          np.array([3, 10], dtype="float32"),
    94          np.array([-14, 0], dtype="float32"),
    95          np.array([0.5, 0.5], dtype="float32")
    96      ]
    97  
    98    def get_two_feature_predictions(self):
    99      return [
   100          PredictionResult(ex, pred) for ex,
   101          pred in zip(
   102              self.get_two_feature_examples(),
   103              [
   104                  f1 * 2.0 + f2 * 3 + 0.5 for f1,
   105                  f2 in self.get_two_feature_examples()
   106              ])
   107      ]
   108  
   109    def get_torch_one_feature_model(self):
   110      model = PytorchLinearRegression(input_dim=1, output_dim=1)
   111      model.load_state_dict(
   112          OrderedDict([('linear.weight', torch.Tensor([[2.0]])),
   113                       ('linear.bias', torch.Tensor([0.5]))]))
   114      return model
   115  
   116    def get_tf_one_feature_model(self):
   117      params = [
   118          np.array([[2.0]], dtype="float32"), np.array([0.5], dtype="float32")
   119      ]
   120      linear_layer = layers.Dense(units=1, weights=params)
   121      linear_model = tf.keras.Sequential([linear_layer])
   122      return linear_model
   123  
   124    def get_sklearn_one_feature_model(self):
   125      x = [[0], [1]]
   126      y = [0.5, 2.5]
   127      model = linear_model.LinearRegression()
   128      model.fit(x, y)
   129      return model
   130  
   131    def get_torch_two_feature_model(self):
   132      model = PytorchLinearRegression(input_dim=2, output_dim=1)
   133      model.load_state_dict(
   134          OrderedDict([('linear.weight', torch.Tensor([[2.0, 3]])),
   135                       ('linear.bias', torch.Tensor([0.5]))]))
   136      return model
   137  
   138    def get_tf_two_feature_model(self):
   139      params = [np.array([[2.0], [3]]), np.array([0.5], dtype="float32")]
   140      linear_layer = layers.Dense(units=1, weights=params)
   141      linear_model = tf.keras.Sequential([linear_layer])
   142      return linear_model
   143  
   144    def get_sklearn_two_feature_model(self):
   145      x = [[1, 5], [3, 2], [1, 0]]
   146      y = [17.5, 12.5, 2.5]
   147      model = linear_model.LinearRegression()
   148      model.fit(x, y)
   149      return model
   150  
   151  
   152  def _compare_prediction_result(a, b):
   153    example_equal = np.array_equal(a.example, b.example)
   154    if isinstance(a.inference, dict):
   155      return all(
   156          x == y for x, y in zip(a.inference.values(),
   157                                 b.inference.values())) and example_equal
   158    return a.inference == b.inference and example_equal
   159  
   160  
   161  def _to_numpy(tensor):
   162    return tensor.detach().cpu().numpy() if tensor.requires_grad else tensor.cpu(
   163    ).numpy()
   164  
   165  
   166  class TestOnnxModelHandler(OnnxModelHandlerNumpy):
   167    def __init__( #pylint: disable=dangerous-default-value
   168        self,
   169        model_uri: str,
   170        session_options=None,
   171        providers=['CUDAExecutionProvider', 'CPUExecutionProvider'],
   172        provider_options=None,
   173        *,
   174        inference_fn=default_numpy_inference_fn,
   175        **kwargs):
   176      self._model_uri = model_uri
   177      self._session_options = session_options
   178      self._providers = providers
   179      self._provider_options = provider_options
   180      self._model_inference_fn = inference_fn
   181      self._env_vars = kwargs.get('env_vars', {})
   182  
   183  
   184  class OnnxTestBase(unittest.TestCase):
   185    def setUp(self):
   186      self.tmpdir = tempfile.mkdtemp()
   187      self.test_data_and_model = TestDataAndModel()
   188  
   189    def tearDown(self):
   190      shutil.rmtree(self.tmpdir)
   191  
   192  
   193  @pytest.mark.uses_onnx
   194  class OnnxPytorchRunInferenceTest(OnnxTestBase):
   195    def test_onnx_pytorch_run_inference(self):
   196      examples = self.test_data_and_model.get_one_feature_samples()
   197      expected_predictions = self.test_data_and_model.get_one_feature_predictions(
   198      )
   199  
   200      model = self.test_data_and_model.get_torch_one_feature_model()
   201      path = os.path.join(self.tmpdir, 'my_onnx_pytorch_path')
   202      dummy_input = torch.randn(4, 1, requires_grad=True)
   203      torch.onnx.export(model,
   204                        dummy_input, # model input
   205                        path,   # where to save the model
   206                        export_params=True, # store the trained parameter weights
   207                        opset_version=10, # the ONNX version
   208                        do_constant_folding=True, # whether to execute constant-
   209                                                  # folding for optimization
   210                        input_names = ['input'],   # model's input names
   211                        output_names = ['output'], # model's output names
   212                        dynamic_axes={'input' : {0 : 'batch_size'},
   213                                      'output' : {0 : 'batch_size'}})
   214  
   215      inference_runner = TestOnnxModelHandler(path)
   216      inference_session = ort.InferenceSession(
   217          path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
   218      )  # this list specifies priority - prioritize gpu if cuda kernel exists
   219      predictions = inference_runner.run_inference(examples, inference_session)
   220      for actual, expected in zip(predictions, expected_predictions):
   221        self.assertEqual(actual, expected)
   222  
   223    def test_num_bytes(self):
   224      inference_runner = TestOnnxModelHandler("dummy")
   225      batched_examples_int = [
   226          np.array([1, 2, 3]), np.array([4, 5, 6]), np.array([7, 8, 9])
   227      ]
   228      self.assertEqual(
   229          batched_examples_int[0].itemsize * 3,
   230          inference_runner.get_num_bytes(batched_examples_int))
   231  
   232      batched_examples_float = [
   233          np.array([1, 5], dtype=np.float32),
   234          np.array([3, 10], dtype=np.float32),
   235          np.array([-14, 0], dtype=np.float32),
   236          np.array([0.5, 0.5], dtype=np.float32)
   237      ]
   238      self.assertEqual(
   239          batched_examples_float[0].itemsize * 4,
   240          inference_runner.get_num_bytes(batched_examples_float))
   241  
   242    def test_namespace(self):
   243      inference_runner = TestOnnxModelHandler("dummy")
   244      self.assertEqual('BeamML_Onnx', inference_runner.get_metrics_namespace())
   245  
   246  
   247  @pytest.mark.uses_onnx
   248  class OnnxTensorflowRunInferenceTest(OnnxTestBase):
   249    def test_onnx_tensorflow_run_inference(self):
   250      examples = self.test_data_and_model.get_one_feature_samples()
   251      expected_predictions = self.test_data_and_model.get_one_feature_predictions(
   252      )
   253      linear_model = self.test_data_and_model.get_tf_one_feature_model()
   254  
   255      path = os.path.join(self.tmpdir, 'my_onnx_tf_path')
   256      spec = (tf.TensorSpec((None, 1), tf.float32, name="input"), )
   257      _, _ = tf2onnx.convert.from_keras(linear_model,
   258      input_signature=spec,
   259      opset=13,
   260      output_path=path)
   261  
   262      inference_runner = TestOnnxModelHandler(path)
   263      inference_session = ort.InferenceSession(
   264          path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
   265      )  # this list specifies priority - prioritize gpu if cuda kernel exists
   266      predictions = inference_runner.run_inference(examples, inference_session)
   267      for actual, expected in zip(predictions, expected_predictions):
   268        self.assertEqual(actual, expected)
   269  
   270  
   271  @pytest.mark.uses_onnx
   272  class OnnxSklearnRunInferenceTest(OnnxTestBase):
   273    def save_model(self, model, input_dim, path):
   274      # assume float input
   275      initial_type = [('float_input', FloatTensorType([None, input_dim]))]
   276      onx = convert_sklearn(model, initial_types=initial_type)
   277      with open(path, "wb") as f:
   278        f.write(onx.SerializeToString())
   279  
   280    def test_onnx_sklearn_run_inference(self):
   281      examples = self.test_data_and_model.get_one_feature_samples()
   282      expected_predictions = self.test_data_and_model.get_one_feature_predictions(
   283      )
   284      linear_model = self.test_data_and_model.get_sklearn_one_feature_model()
   285      path = os.path.join(self.tmpdir, 'my_onnx_sklearn_path')
   286      self.save_model(linear_model, 1, path)
   287  
   288      inference_runner = TestOnnxModelHandler(path)
   289      inference_session = ort.InferenceSession(
   290          path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
   291      )  # this list specifies priority - prioritize gpu if cuda kernel exists
   292      predictions = inference_runner.run_inference(examples, inference_session)
   293      for actual, expected in \
   294          zip(predictions, expected_predictions):
   295        self.assertEqual(actual, expected)
   296  
   297  
   298  @pytest.mark.uses_onnx
   299  class OnnxPytorchRunInferencePipelineTest(OnnxTestBase):
   300    def exportModelToOnnx(self, model, path):
   301      dummy_input = torch.randn(4, 2, requires_grad=True)
   302      torch.onnx.export(model,
   303                        dummy_input, # model input
   304                        path,   # where to save the model
   305                        export_params=True, # store the trained parameter weights
   306                        opset_version=10, # the ONNX version
   307                        do_constant_folding=True, # whether to execute constant
   308                                                  # folding for optimization
   309                        input_names = ['input'],   # odel's input names
   310                        output_names = ['output'], # model's output names
   311                        dynamic_axes={'input' : {0 : 'batch_size'},
   312                                      'output' : {0 : 'batch_size'}})
   313  
   314    def test_pipeline_local_model_simple(self):
   315      with TestPipeline() as pipeline:
   316        path = os.path.join(self.tmpdir, 'my_onnx_pytorch_path')
   317        model = self.test_data_and_model.get_torch_two_feature_model()
   318        self.exportModelToOnnx(model, path)
   319        model_handler = TestOnnxModelHandler(path)
   320  
   321        pcoll = pipeline | 'start' >> beam.Create(
   322            self.test_data_and_model.get_two_feature_examples())
   323        predictions = pcoll | RunInference(model_handler)
   324        assert_that(
   325            predictions,
   326            equal_to(
   327                self.test_data_and_model.get_two_feature_predictions(),
   328                equals_fn=_compare_prediction_result))
   329  
   330    def test_model_handler_sets_env_vars(self):
   331      with TestPipeline() as pipeline:
   332        path = os.path.join(self.tmpdir, 'my_onnx_pytorch_path')
   333        model = self.test_data_and_model.get_torch_two_feature_model()
   334        self.exportModelToOnnx(model, path)
   335        model_handler = OnnxModelHandlerNumpy(
   336            model_uri=path, env_vars={'FOO': 'bar'})
   337        self.assertFalse('FOO' in os.environ)
   338        _ = (
   339            pipeline
   340            | 'start' >> beam.Create(
   341                self.test_data_and_model.get_two_feature_examples())
   342            | RunInference(model_handler))
   343        pipeline.run()
   344        self.assertTrue('FOO' in os.environ)
   345        self.assertTrue('bar'.equals(os.environ['FOO']))
   346  
   347    @unittest.skipIf(GCSFileSystem is None, 'GCP dependencies are not installed')
   348    def test_pipeline_gcs_model(self):
   349      with TestPipeline() as pipeline:
   350        examples = self.test_data_and_model.get_one_feature_samples()
   351        expected_predictions = (
   352            self.test_data_and_model.get_one_feature_predictions())
   353        gs_path = 'gs://apache-beam-ml/models/torch_2xplus5_onnx'
   354        # first need to download model from remote
   355        model_handler = TestOnnxModelHandler(gs_path)
   356  
   357        pcoll = pipeline | 'start' >> beam.Create(examples)
   358        predictions = pcoll | RunInference(model_handler)
   359        assert_that(
   360            predictions,
   361            equal_to(expected_predictions, equals_fn=_compare_prediction_result))
   362  
   363    def test_invalid_input_type(self):
   364      with self.assertRaisesRegex(InvalidArgument,
   365                                  "Got invalid dimensions for input"):
   366        with TestPipeline() as pipeline:
   367          examples = [np.array([1], dtype="float32")]
   368          path = os.path.join(self.tmpdir, 'my_onnx_pytorch_path')
   369          model = self.test_data_and_model.get_torch_two_feature_model()
   370          self.exportModelToOnnx(model, path)
   371  
   372          model_handler = TestOnnxModelHandler(path)
   373  
   374          pcoll = pipeline | 'start' >> beam.Create(examples)
   375          # pylint: disable=expression-not-assigned
   376          pcoll | RunInference(model_handler)
   377  
   378  
   379  @pytest.mark.uses_onnx
   380  class OnnxTensorflowRunInferencePipelineTest(OnnxTestBase):
   381    def exportModelToOnnx(self, model, path):
   382      spec = (tf.TensorSpec((None, 2), tf.float32, name="input"), )
   383      _, _ = tf2onnx.convert.from_keras(model,
   384      input_signature=spec, opset=13, output_path=path)
   385  
   386    def test_pipeline_local_model_simple(self):
   387      with TestPipeline() as pipeline:
   388        path = os.path.join(self.tmpdir, 'my_onnx_tensorflow_path')
   389        model = self.test_data_and_model.get_tf_two_feature_model()
   390        self.exportModelToOnnx(model, path)
   391        model_handler = TestOnnxModelHandler(path)
   392  
   393        pcoll = pipeline | 'start' >> beam.Create(
   394            self.test_data_and_model.get_two_feature_examples())
   395        predictions = pcoll | RunInference(model_handler)
   396        assert_that(
   397            predictions,
   398            equal_to(
   399                self.test_data_and_model.get_two_feature_predictions(),
   400                equals_fn=_compare_prediction_result))
   401  
   402    @unittest.skipIf(GCSFileSystem is None, 'GCP dependencies are not installed')
   403    def test_pipeline_gcs_model(self):
   404      with TestPipeline() as pipeline:
   405        examples = self.test_data_and_model.get_one_feature_samples()
   406        expected_predictions = (
   407            self.test_data_and_model.get_one_feature_predictions())
   408        gs_path = 'gs://apache-beam-ml/models/tf_2xplus5_onnx'
   409  
   410        model_handler = TestOnnxModelHandler(gs_path)
   411  
   412        pcoll = pipeline | 'start' >> beam.Create(examples)
   413        predictions = pcoll | RunInference(model_handler)
   414        assert_that(
   415            predictions,
   416            equal_to(expected_predictions, equals_fn=_compare_prediction_result))
   417  
   418    def test_invalid_input_type(self):
   419      with self.assertRaisesRegex(InvalidArgument,
   420                                  "Got invalid dimensions for input"):
   421        with TestPipeline() as pipeline:
   422          examples = [np.array([1], dtype="float32")]
   423          path = os.path.join(self.tmpdir, 'my_onnx_tensorflow_path')
   424          model = self.test_data_and_model.get_tf_two_feature_model()
   425          self.exportModelToOnnx(model, path)
   426  
   427          model_handler = TestOnnxModelHandler(path)
   428  
   429          pcoll = pipeline | 'start' >> beam.Create(examples)
   430          # pylint: disable=expression-not-assigned
   431          pcoll | RunInference(model_handler)
   432  
   433  
   434  @pytest.mark.uses_onnx
   435  class OnnxSklearnRunInferencePipelineTest(OnnxTestBase):
   436    def save_model(self, model, input_dim, path):
   437      # assume float input
   438      initial_type = [('float_input', FloatTensorType([None, input_dim]))]
   439      onx = convert_sklearn(model, initial_types=initial_type)
   440      with open(path, "wb") as f:
   441        f.write(onx.SerializeToString())
   442  
   443    def test_pipeline_local_model_simple(self):
   444      with TestPipeline() as pipeline:
   445        path = os.path.join(self.tmpdir, 'my_onnx_sklearn_path')
   446        model = self.test_data_and_model.get_sklearn_two_feature_model()
   447        self.save_model(model, 2, path)
   448        model_handler = TestOnnxModelHandler(path)
   449  
   450        pcoll = pipeline | 'start' >> beam.Create(
   451            self.test_data_and_model.get_two_feature_examples())
   452        predictions = pcoll | RunInference(model_handler)
   453        assert_that(
   454            predictions,
   455            equal_to(
   456                self.test_data_and_model.get_two_feature_predictions(),
   457                equals_fn=_compare_prediction_result))
   458  
   459    @unittest.skipIf(GCSFileSystem is None, 'GCP dependencies are not installed')
   460    def test_pipeline_gcs_model(self):
   461      with TestPipeline() as pipeline:
   462        examples = (self.test_data_and_model.get_one_feature_samples())
   463        expected_predictions = (
   464            self.test_data_and_model.get_one_feature_predictions())
   465        gs_path = 'gs://apache-beam-ml/models/skl_2xplus5_onnx'
   466  
   467        model_handler = TestOnnxModelHandler(gs_path)
   468        pcoll = pipeline | 'start' >> beam.Create(examples)
   469        predictions = pcoll | RunInference(model_handler)
   470        assert_that(
   471            predictions,
   472            equal_to(expected_predictions, equals_fn=_compare_prediction_result))
   473  
   474    def test_invalid_input_type(self):
   475      with self.assertRaises(InvalidArgument):
   476        with TestPipeline() as pipeline:
   477          examples = [np.array([1], dtype="float32")]
   478          path = os.path.join(self.tmpdir, 'my_onnx_sklearn_path')
   479          model = self.test_data_and_model.get_sklearn_two_feature_model()
   480          self.save_model(model, 2, path)
   481  
   482          model_handler = TestOnnxModelHandler(path)
   483  
   484          pcoll = pipeline | 'start' >> beam.Create(examples)
   485          # pylint: disable=expression-not-assigned
   486          pcoll | RunInference(model_handler)
   487  
   488  
   489  if __name__ == '__main__':
   490    unittest.main()