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()