| # | |
| # Licensed to the Apache Software Foundation (ASF) under one or more | |
| # contributor license agreements. See the NOTICE file distributed with | |
| # this work for additional information regarding copyright ownership. | |
| # The ASF licenses this file to You under the Apache License, Version 2.0 | |
| # (the "License"); you may not use this file except in compliance with | |
| # the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # | |
| """Tests for apache_beam.ml.base.""" | |
| import math | |
| import multiprocessing | |
| import os | |
| import pickle | |
| import random | |
| import sys | |
| import tempfile | |
| import time | |
| import unittest | |
| import unittest.mock | |
| from collections.abc import Iterable | |
| from collections.abc import Mapping | |
| from collections.abc import Sequence | |
| from typing import Any | |
| from typing import Optional | |
| from typing import Union | |
| import pytest | |
| import apache_beam as beam | |
| from apache_beam.examples.inference import run_inference_side_inputs | |
| from apache_beam.metrics.metric import MetricsFilter | |
| from apache_beam.ml.inference import base | |
| from apache_beam.options.pipeline_options import StandardOptions | |
| from apache_beam.testing.test_pipeline import TestPipeline | |
| from apache_beam.testing.util import assert_that | |
| from apache_beam.testing.util import equal_to | |
| from apache_beam.transforms import trigger | |
| from apache_beam.transforms import window | |
| from apache_beam.transforms.periodicsequence import TimestampedValue | |
| from apache_beam.utils import multi_process_shared | |
| class FakeModel: | |
| def predict(self, example: int) -> int: | |
| return example + 1 | |
| class FakeStatefulModel: | |
| def __init__(self, state: int): | |
| if state == 100: | |
| raise Exception('Oh no') | |
| self._state = state | |
| def predict(self, example: int) -> int: | |
| return self._state | |
| def increment_state(self, amount: int): | |
| self._state += amount | |
| class FakeSlowModel: | |
| def __init__(self, sleep_on_load_seconds=0, file_path_write_on_del=None): | |
| self._file_path_write_on_del = file_path_write_on_del | |
| time.sleep(sleep_on_load_seconds) | |
| def predict(self, example: int) -> int: | |
| time.sleep(example) | |
| return example | |
| def __del__(self): | |
| if self._file_path_write_on_del is not None: | |
| with open(self._file_path_write_on_del, 'a') as myfile: | |
| myfile.write('Deleted FakeSlowModel') | |
| class FakeIncrementingModel: | |
| def __init__(self): | |
| self._state = 0 | |
| def predict(self, example: int) -> int: | |
| self._state += 1 | |
| return self._state | |
| class FakeSlowModelHandler(base.ModelHandler[int, int, FakeModel]): | |
| def __init__( | |
| self, | |
| sleep_on_load: int, | |
| multi_process_shared=False, | |
| file_path_write_on_del=None): | |
| self._sleep_on_load = sleep_on_load | |
| self._multi_process_shared = multi_process_shared | |
| self._file_path_write_on_del = file_path_write_on_del | |
| def load_model(self): | |
| return FakeSlowModel(self._sleep_on_load, self._file_path_write_on_del) | |
| def run_inference( | |
| self, | |
| batch: Sequence[int], | |
| model: FakeModel, | |
| inference_args=None) -> Iterable[int]: | |
| for example in batch: | |
| yield model.predict(example) | |
| def share_model_across_processes(self) -> bool: | |
| return self._multi_process_shared | |
| def batch_elements_kwargs(self): | |
| return {'min_batch_size': 1, 'max_batch_size': 1} | |
| class FakeModelHandler(base.ModelHandler[int, int, FakeModel]): | |
| def __init__( | |
| self, | |
| clock=None, | |
| min_batch_size=1, | |
| max_batch_size=9999, | |
| multi_process_shared=False, | |
| state=None, | |
| incrementing=False, | |
| max_copies=1, | |
| num_bytes_per_element=None, | |
| **kwargs): | |
| self._fake_clock = clock | |
| self._min_batch_size = min_batch_size | |
| self._max_batch_size = max_batch_size | |
| self._env_vars = kwargs.get('env_vars', {}) | |
| self._multi_process_shared = multi_process_shared | |
| self._state = state | |
| self._incrementing = incrementing | |
| self._max_copies = max_copies | |
| self._num_bytes_per_element = num_bytes_per_element | |
| def load_model(self): | |
| assert (not self._incrementing or self._state is None) | |
| if self._fake_clock: | |
| self._fake_clock.current_time_ns += 500_000_000 # 500ms | |
| if self._incrementing: | |
| return FakeIncrementingModel() | |
| if self._state is not None: | |
| return FakeStatefulModel(self._state) | |
| return FakeModel() | |
| def run_inference( | |
| self, | |
| batch: Sequence[int], | |
| model: FakeModel, | |
| inference_args=None) -> Iterable[int]: | |
| multi_process_shared_loaded = "multi_process_shared" in str(type(model)) | |
| if self._multi_process_shared != multi_process_shared_loaded: | |
| raise Exception( | |
| f'Loaded model of type {type(model)}, was' + | |
| f'{"" if self._multi_process_shared else " not"} ' + | |
| 'expecting multi_process_shared_model') | |
| if self._fake_clock: | |
| self._fake_clock.current_time_ns += 3_000_000 # 3 milliseconds | |
| for example in batch: | |
| yield model.predict(example) | |
| def update_model_path(self, model_path: Optional[str] = None): | |
| pass | |
| def batch_elements_kwargs(self): | |
| return { | |
| 'min_batch_size': self._min_batch_size, | |
| 'max_batch_size': self._max_batch_size | |
| } | |
| def share_model_across_processes(self): | |
| return self._multi_process_shared | |
| def model_copies(self): | |
| return self._max_copies | |
| def get_num_bytes(self, batch: Sequence[int]) -> int: | |
| if self._num_bytes_per_element: | |
| return self._num_bytes_per_element * len(batch) | |
| return super().get_num_bytes(batch) | |
| class FakeModelHandlerReturnsPredictionResult( | |
| base.ModelHandler[int, base.PredictionResult, FakeModel]): | |
| def __init__( | |
| self, | |
| clock=None, | |
| model_id='fake_model_id_default', | |
| multi_process_shared=False, | |
| state=None): | |
| self.model_id = model_id | |
| self._fake_clock = clock | |
| self._env_vars = {} | |
| self._multi_process_shared = multi_process_shared | |
| self._state = state | |
| def load_model(self): | |
| if self._state is not None: | |
| return FakeStatefulModel(0) | |
| return FakeModel() | |
| def run_inference( | |
| self, | |
| batch: Sequence[int], | |
| model: Union[FakeModel, FakeStatefulModel], | |
| inference_args=None) -> Iterable[base.PredictionResult]: | |
| multi_process_shared_loaded = "multi_process_shared" in str(type(model)) | |
| if self._multi_process_shared != multi_process_shared_loaded: | |
| raise Exception( | |
| f'Loaded model of type {type(model)}, was' + | |
| f'{"" if self._multi_process_shared else " not"} ' + | |
| 'expecting multi_process_shared_model') | |
| for example in batch: | |
| yield base.PredictionResult( | |
| model_id=self.model_id, | |
| example=example, | |
| inference=model.predict(example)) | |
| if self._state is not None: | |
| model.increment_state(1) # type: ignore[union-attr] | |
| def update_model_path(self, model_path: Optional[str] = None): | |
| self.model_id = model_path if model_path else self.model_id | |
| def share_model_across_processes(self): | |
| return self._multi_process_shared | |
| class FakeModelHandlerNoEnvVars(base.ModelHandler[int, int, FakeModel]): | |
| def __init__( | |
| self, clock=None, min_batch_size=1, max_batch_size=9999, **kwargs): | |
| self._fake_clock = clock | |
| self._min_batch_size = min_batch_size | |
| self._max_batch_size = max_batch_size | |
| def load_model(self): | |
| if self._fake_clock: | |
| self._fake_clock.current_time_ns += 500_000_000 # 500ms | |
| return FakeModel() | |
| def run_inference( | |
| self, | |
| batch: Sequence[int], | |
| model: FakeModel, | |
| inference_args=None) -> Iterable[int]: | |
| if self._fake_clock: | |
| self._fake_clock.current_time_ns += 3_000_000 # 3 milliseconds | |
| for example in batch: | |
| yield model.predict(example) | |
| def update_model_path(self, model_path: Optional[str] = None): | |
| pass | |
| def batch_elements_kwargs(self): | |
| return { | |
| 'min_batch_size': self._min_batch_size, | |
| 'max_batch_size': self._max_batch_size | |
| } | |
| class FakeClock: | |
| def __init__(self): | |
| # Start at 10 seconds. | |
| self.current_time_ns = 10_000_000_000 | |
| def time_ns(self) -> int: | |
| return self.current_time_ns | |
| class ExtractInferences(beam.DoFn): | |
| def process(self, prediction_result): | |
| yield prediction_result.inference | |
| class FakeModelHandlerNeedsBigBatch(FakeModelHandler): | |
| def run_inference(self, batch, unused_model, inference_args=None): | |
| if len(batch) < 100: | |
| raise ValueError('Unexpectedly small batch') | |
| return batch | |
| def batch_elements_kwargs(self): | |
| return {'min_batch_size': 9999} | |
| class FakeModelHandlerFailsOnInferenceArgs(FakeModelHandler): | |
| def run_inference(self, batch, unused_model, inference_args=None): | |
| raise ValueError( | |
| 'run_inference should not be called because error should already be ' | |
| 'thrown from the validate_inference_args check.') | |
| def validate_inference_args(self, inference_args: Optional[dict[str, Any]]): | |
| if inference_args: | |
| raise ValueError( | |
| 'inference_args were provided, but should be None because this ' | |
| 'framework does not expect extra arguments on inferences.') | |
| class FakeModelHandlerExpectedInferenceArgs(FakeModelHandler): | |
| def run_inference(self, batch, unused_model, inference_args=None): | |
| if not inference_args: | |
| raise ValueError('inference_args should exist') | |
| return batch | |
| def validate_inference_args(self, inference_args): | |
| pass | |
| class RunInferenceBaseTest(unittest.TestCase): | |
| def test_run_inference_impl_simple_examples(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = [example + 1 for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference(FakeModelHandler()) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_impl_simple_examples_multi_process_shared(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = [example + 1 for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| FakeModelHandler(multi_process_shared=True)) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_impl_simple_examples_multi_process_shared_multi_copy( | |
| self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = [example + 1 for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| FakeModelHandler(multi_process_shared=True, max_copies=4)) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_impl_multi_process_shared_incrementing_multi_copy( | |
| self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10, 1, 5, 3, 10, 1, 5, 3, 10, 1, 5, 3, 10] | |
| expected = [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| FakeModelHandler( | |
| multi_process_shared=True, | |
| max_copies=4, | |
| incrementing=True, | |
| max_batch_size=1)) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_impl_mps_nobatch_incrementing_multi_copy(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10, 1, 5, 3, 10, 1, 5, 3, 10, 1, 5, 3, 10] | |
| expected = [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] | |
| batched_examples = [[example] for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(batched_examples) | |
| actual = pcoll | base.RunInference( | |
| FakeModelHandler( | |
| multi_process_shared=True, max_copies=4, | |
| incrementing=True).with_no_batching()) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_impl_keyed_mps_incrementing_multi_copy(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10, 1, 5, 3, 10, 1, 5, 3, 10, 1, 5, 3, 10] | |
| keyed_examples = [('abc', example) for example in examples] | |
| expected = [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] | |
| keyed_expected = [('abc', val) for val in expected] | |
| pcoll = pipeline | 'start' >> beam.Create(keyed_examples) | |
| actual = pcoll | base.RunInference( | |
| base.KeyedModelHandler( | |
| FakeModelHandler( | |
| multi_process_shared=True, | |
| max_copies=4, | |
| incrementing=True, | |
| max_batch_size=1))) | |
| assert_that(actual, equal_to(keyed_expected), label='assert:inferences') | |
| def test_run_inference_impl_with_keyed_examples(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| keyed_examples = [(i, example) for i, example in enumerate(examples)] | |
| expected = [(i, example + 1) for i, example in enumerate(examples)] | |
| pcoll = pipeline | 'start' >> beam.Create(keyed_examples) | |
| actual = pcoll | base.RunInference( | |
| base.KeyedModelHandler(FakeModelHandler())) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_impl_with_keyed_examples_many_model_handlers(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| keyed_examples = [(i, example) for i, example in enumerate(examples)] | |
| expected = [(i, example + 1) for i, example in enumerate(examples)] | |
| expected[0] = (0, 200) | |
| pcoll = pipeline | 'start' >> beam.Create(keyed_examples) | |
| mhs = [ | |
| base.KeyModelMapping([0], | |
| FakeModelHandler( | |
| state=200, multi_process_shared=True)), | |
| base.KeyModelMapping([1, 2, 3], | |
| FakeModelHandler(multi_process_shared=True)) | |
| ] | |
| actual = pcoll | base.RunInference(base.KeyedModelHandler(mhs)) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_impl_with_keyed_examples_many_model_handlers_metrics( | |
| self): | |
| pipeline = TestPipeline() | |
| examples = [1, 5, 3, 10] | |
| metrics_namespace = 'test_namespace' | |
| keyed_examples = [(i, example) for i, example in enumerate(examples)] | |
| pcoll = pipeline | 'start' >> beam.Create(keyed_examples) | |
| mhs = [ | |
| base.KeyModelMapping([0], | |
| FakeModelHandler( | |
| state=200, multi_process_shared=True)), | |
| base.KeyModelMapping([1, 2, 3], | |
| FakeModelHandler(multi_process_shared=True)) | |
| ] | |
| _ = pcoll | base.RunInference( | |
| base.KeyedModelHandler(mhs), metrics_namespace=metrics_namespace) | |
| result = pipeline.run() | |
| result.wait_until_finish() | |
| metrics_filter = MetricsFilter().with_namespace(namespace=metrics_namespace) | |
| metrics = result.metrics().query(metrics_filter) | |
| assert len(metrics['counters']) != 0 | |
| assert len(metrics['distributions']) != 0 | |
| metrics_filter = MetricsFilter().with_name('0-_num_inferences') | |
| metrics = result.metrics().query(metrics_filter) | |
| num_inferences_counter_key_0 = metrics['counters'][0] | |
| self.assertEqual(num_inferences_counter_key_0.committed, 1) | |
| metrics_filter = MetricsFilter().with_name('1-_num_inferences') | |
| metrics = result.metrics().query(metrics_filter) | |
| num_inferences_counter_key_1 = metrics['counters'][0] | |
| self.assertEqual(num_inferences_counter_key_1.committed, 3) | |
| metrics_filter = MetricsFilter().with_name('num_inferences') | |
| metrics = result.metrics().query(metrics_filter) | |
| num_inferences_counter_aggregate = metrics['counters'][0] | |
| self.assertEqual(num_inferences_counter_aggregate.committed, 4) | |
| metrics_filter = MetricsFilter().with_name('0-_failed_batches_counter') | |
| metrics = result.metrics().query(metrics_filter) | |
| failed_batches_counter_key_0 = metrics['counters'] | |
| self.assertEqual(len(failed_batches_counter_key_0), 0) | |
| metrics_filter = MetricsFilter().with_name('failed_batches_counter') | |
| metrics = result.metrics().query(metrics_filter) | |
| failed_batches_counter_aggregate = metrics['counters'] | |
| self.assertEqual(len(failed_batches_counter_aggregate), 0) | |
| metrics_filter = MetricsFilter().with_name( | |
| '0-_load_model_latency_milli_secs') | |
| metrics = result.metrics().query(metrics_filter) | |
| load_latency_dist_key_0 = metrics['distributions'][0] | |
| self.assertEqual(load_latency_dist_key_0.committed.count, 1) | |
| metrics_filter = MetricsFilter().with_name('load_model_latency_milli_secs') | |
| metrics = result.metrics().query(metrics_filter) | |
| load_latency_dist_aggregate = metrics['distributions'][0] | |
| self.assertEqual(load_latency_dist_aggregate.committed.count, 2) | |
| def test_run_inference_impl_with_keyed_examples_many_mhs_max_models_hint( | |
| self): | |
| pipeline = TestPipeline() | |
| examples = [1, 5, 3, 10, 2, 4, 6, 8, 9, 7, 1, 5, 3, 10, 2, 4, 6, 8, 9, 7] | |
| metrics_namespace = 'test_namespace' | |
| keyed_examples = [(i, example) for i, example in enumerate(examples)] | |
| pcoll = pipeline | 'start' >> beam.Create(keyed_examples) | |
| mhs = [ | |
| base.KeyModelMapping([0, 2, 4, 6, 8], | |
| FakeModelHandler( | |
| state=200, multi_process_shared=True)), | |
| base.KeyModelMapping( | |
| [1, 3, 5, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], | |
| FakeModelHandler(multi_process_shared=True)) | |
| ] | |
| _ = pcoll | base.RunInference( | |
| base.KeyedModelHandler(mhs, max_models_per_worker_hint=1), | |
| metrics_namespace=metrics_namespace) | |
| result = pipeline.run() | |
| result.wait_until_finish() | |
| metrics_filter = MetricsFilter().with_namespace(namespace=metrics_namespace) | |
| metrics = result.metrics().query(metrics_filter) | |
| assert len(metrics['counters']) != 0 | |
| assert len(metrics['distributions']) != 0 | |
| metrics_filter = MetricsFilter().with_name('load_model_latency_milli_secs') | |
| metrics = result.metrics().query(metrics_filter) | |
| load_latency_dist_aggregate = metrics['distributions'][0] | |
| # We should flip back and forth between models a bit since | |
| # max_models_per_worker_hint=1, but we shouldn't thrash forever | |
| # since most examples belong to the second ModelMapping | |
| self.assertGreater(load_latency_dist_aggregate.committed.count, 2) | |
| self.assertLess(load_latency_dist_aggregate.committed.count, 12) | |
| def test_keyed_many_model_handlers_validation(self): | |
| def mult_two(example: str) -> int: | |
| return int(example) * 2 | |
| mhs = [ | |
| base.KeyModelMapping( | |
| [0], | |
| FakeModelHandler( | |
| state=200, | |
| multi_process_shared=True).with_preprocess_fn(mult_two)), | |
| base.KeyModelMapping([1, 2, 3], | |
| FakeModelHandler(multi_process_shared=True)) | |
| ] | |
| with self.assertRaises(ValueError): | |
| base.KeyedModelHandler(mhs) | |
| mhs = [ | |
| base.KeyModelMapping( | |
| [0], | |
| FakeModelHandler( | |
| state=200, | |
| multi_process_shared=True).with_postprocess_fn(mult_two)), | |
| base.KeyModelMapping([1, 2, 3], | |
| FakeModelHandler(multi_process_shared=True)) | |
| ] | |
| with self.assertRaises(ValueError): | |
| base.KeyedModelHandler(mhs) | |
| mhs = [ | |
| base.KeyModelMapping([0], | |
| FakeModelHandler( | |
| state=200, multi_process_shared=True)), | |
| base.KeyModelMapping([0, 1, 2, 3], | |
| FakeModelHandler(multi_process_shared=True)) | |
| ] | |
| with self.assertRaises(ValueError): | |
| base.KeyedModelHandler(mhs) | |
| mhs = [ | |
| base.KeyModelMapping([], | |
| FakeModelHandler( | |
| state=200, multi_process_shared=True)), | |
| base.KeyModelMapping([0, 1, 2, 3], | |
| FakeModelHandler(multi_process_shared=True)) | |
| ] | |
| with self.assertRaises(ValueError): | |
| base.KeyedModelHandler(mhs) | |
| def test_keyed_model_handler_get_num_bytes(self): | |
| mh = base.KeyedModelHandler(FakeModelHandler(num_bytes_per_element=10)) | |
| batch = [('key1', 1), ('key2', 2), ('key1', 3)] | |
| expected = len(pickle.dumps(('key1', 'key2', 'key1'))) + 30 | |
| actual = mh.get_num_bytes(batch) | |
| self.assertEqual(expected, actual) | |
| def test_keyed_model_handler_multiple_models_get_num_bytes(self): | |
| mhs = [ | |
| base.KeyModelMapping(['key1'], | |
| FakeModelHandler(num_bytes_per_element=10)), | |
| base.KeyModelMapping(['key2'], | |
| FakeModelHandler(num_bytes_per_element=20)) | |
| ] | |
| mh = base.KeyedModelHandler(mhs) | |
| batch = [('key1', 1), ('key2', 2), ('key1', 3)] | |
| expected = len(pickle.dumps(('key1', 'key2', 'key1'))) + 40 | |
| actual = mh.get_num_bytes(batch) | |
| self.assertEqual(expected, actual) | |
| def test_run_inference_impl_with_maybe_keyed_examples(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| keyed_examples = [(i, example) for i, example in enumerate(examples)] | |
| expected = [example + 1 for example in examples] | |
| keyed_expected = [(i, example + 1) for i, example in enumerate(examples)] | |
| model_handler = base.MaybeKeyedModelHandler(FakeModelHandler()) | |
| pcoll = pipeline | 'Unkeyed' >> beam.Create(examples) | |
| actual = pcoll | 'RunUnkeyed' >> base.RunInference(model_handler) | |
| assert_that(actual, equal_to(expected), label='CheckUnkeyed') | |
| keyed_pcoll = pipeline | 'Keyed' >> beam.Create(keyed_examples) | |
| keyed_actual = keyed_pcoll | 'RunKeyed' >> base.RunInference( | |
| model_handler) | |
| assert_that(keyed_actual, equal_to(keyed_expected), label='CheckKeyed') | |
| def test_run_inference_impl_with_keyed_examples_multi_process_shared(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| keyed_examples = [(i, example) for i, example in enumerate(examples)] | |
| expected = [(i, example + 1) for i, example in enumerate(examples)] | |
| pcoll = pipeline | 'start' >> beam.Create(keyed_examples) | |
| actual = pcoll | base.RunInference( | |
| base.KeyedModelHandler(FakeModelHandler(multi_process_shared=True))) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_impl_with_maybe_keyed_examples_multi_process_shared( | |
| self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| keyed_examples = [(i, example) for i, example in enumerate(examples)] | |
| expected = [example + 1 for example in examples] | |
| keyed_expected = [(i, example + 1) for i, example in enumerate(examples)] | |
| model_handler = base.MaybeKeyedModelHandler( | |
| FakeModelHandler(multi_process_shared=True)) | |
| pcoll = pipeline | 'Unkeyed' >> beam.Create(examples) | |
| actual = pcoll | 'RunUnkeyed' >> base.RunInference(model_handler) | |
| assert_that(actual, equal_to(expected), label='CheckUnkeyed') | |
| keyed_pcoll = pipeline | 'Keyed' >> beam.Create(keyed_examples) | |
| keyed_actual = keyed_pcoll | 'RunKeyed' >> base.RunInference( | |
| model_handler) | |
| assert_that(keyed_actual, equal_to(keyed_expected), label='CheckKeyed') | |
| def test_run_inference_preprocessing(self): | |
| def mult_two(example: str) -> int: | |
| return int(example) * 2 | |
| with TestPipeline() as pipeline: | |
| examples = ["1", "5", "3", "10"] | |
| expected = [int(example) * 2 + 1 for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| FakeModelHandler().with_preprocess_fn(mult_two)) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_prebatched(self): | |
| with TestPipeline() as pipeline: | |
| examples = [[1, 5], [3, 10]] | |
| expected = [int(example) + 1 for batch in examples for example in batch] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference(FakeModelHandler().with_no_batching()) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_preprocessing_multiple_fns(self): | |
| def add_one(example: str) -> int: | |
| return int(example) + 1 | |
| def mult_two(example: int) -> int: | |
| return example * 2 | |
| with TestPipeline() as pipeline: | |
| examples = ["1", "5", "3", "10"] | |
| expected = [(int(example) + 1) * 2 + 1 for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| FakeModelHandler().with_preprocess_fn(mult_two).with_preprocess_fn( | |
| add_one)) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_postprocessing(self): | |
| def mult_two(example: int) -> str: | |
| return str(example * 2) | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = [str((example + 1) * 2) for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| FakeModelHandler().with_postprocess_fn(mult_two)) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_postprocessing_multiple_fns(self): | |
| def add_one(example: int) -> str: | |
| return str(int(example) + 1) | |
| def mult_two(example: int) -> int: | |
| return example * 2 | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = [str(((example + 1) * 2) + 1) for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| FakeModelHandler().with_postprocess_fn(mult_two).with_postprocess_fn( | |
| add_one)) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_preprocessing_dlq(self): | |
| def mult_two(example: str) -> int: | |
| if example == "5": | |
| raise Exception("TEST") | |
| return int(example) * 2 | |
| with TestPipeline() as pipeline: | |
| examples = ["1", "5", "3", "10"] | |
| expected = [3, 7, 21] | |
| expected_bad = ["5"] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| main, other = pcoll | base.RunInference( | |
| FakeModelHandler().with_preprocess_fn(mult_two) | |
| ).with_exception_handling() | |
| assert_that(main, equal_to(expected), label='assert:inferences') | |
| assert_that( | |
| other.failed_inferences, equal_to([]), label='assert:bad_infer') | |
| # bad will be in form [element, error]. Just pull out bad element. | |
| bad_without_error = other.failed_preprocessing[0] | beam.Map( | |
| lambda x: x[0]) | |
| assert_that( | |
| bad_without_error, equal_to(expected_bad), label='assert:failures') | |
| def test_run_inference_postprocessing_dlq(self): | |
| def mult_two(example: int) -> str: | |
| if example == 6: | |
| raise Exception("TEST") | |
| return str(example * 2) | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = ["4", "8", "22"] | |
| expected_bad = [6] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| main, other = pcoll | base.RunInference( | |
| FakeModelHandler().with_postprocess_fn(mult_two) | |
| ).with_exception_handling() | |
| assert_that(main, equal_to(expected), label='assert:inferences') | |
| assert_that( | |
| other.failed_inferences, equal_to([]), label='assert:bad_infer') | |
| # bad will be in form [element, error]. Just pull out bad element. | |
| bad_without_error = other.failed_postprocessing[0] | beam.Map( | |
| lambda x: x[0]) | |
| assert_that( | |
| bad_without_error, equal_to(expected_bad), label='assert:failures') | |
| def test_run_inference_pre_and_post_processing_dlq(self): | |
| def mult_two_pre(example: str) -> int: | |
| if example == "5": | |
| raise Exception("TEST") | |
| return int(example) * 2 | |
| def mult_two_post(example: int) -> str: | |
| if example == 7: | |
| raise Exception("TEST") | |
| return str(example * 2) | |
| with TestPipeline() as pipeline: | |
| examples = ["1", "5", "3", "10"] | |
| expected = ["6", "42"] | |
| expected_bad_pre = ["5"] | |
| expected_bad_post = [7] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| main, other = pcoll | base.RunInference( | |
| FakeModelHandler().with_preprocess_fn( | |
| mult_two_pre | |
| ).with_postprocess_fn( | |
| mult_two_post | |
| )).with_exception_handling() | |
| assert_that(main, equal_to(expected), label='assert:inferences') | |
| assert_that( | |
| other.failed_inferences, equal_to([]), label='assert:bad_infer') | |
| # bad will be in form [elements, error]. Just pull out bad element. | |
| bad_without_error_pre = other.failed_preprocessing[0] | beam.Map( | |
| lambda x: x[0]) | |
| assert_that( | |
| bad_without_error_pre, | |
| equal_to(expected_bad_pre), | |
| label='assert:failures_pre') | |
| # bad will be in form [elements, error]. Just pull out bad element. | |
| bad_without_error_post = other.failed_postprocessing[0] | beam.Map( | |
| lambda x: x[0]) | |
| assert_that( | |
| bad_without_error_post, | |
| equal_to(expected_bad_post), | |
| label='assert:failures_post') | |
| def test_run_inference_keyed_pre_and_post_processing(self): | |
| def mult_two(element): | |
| return (element[0], element[1] * 2) | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| keyed_examples = [(i, example) for i, example in enumerate(examples)] | |
| expected = [(i, ((example * 2) + 1) * 2) | |
| for i, example in enumerate(examples)] | |
| pcoll = pipeline | 'start' >> beam.Create(keyed_examples) | |
| actual = pcoll | base.RunInference( | |
| base.KeyedModelHandler(FakeModelHandler()).with_preprocess_fn( | |
| mult_two).with_postprocess_fn(mult_two)) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_maybe_keyed_pre_and_post_processing(self): | |
| def mult_two(element): | |
| return element * 2 | |
| def mult_two_keyed(element): | |
| return (element[0], element[1] * 2) | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| keyed_examples = [(i, example) for i, example in enumerate(examples)] | |
| expected = [((2 * example) + 1) * 2 for example in examples] | |
| keyed_expected = [(i, ((2 * example) + 1) * 2) | |
| for i, example in enumerate(examples)] | |
| model_handler = base.MaybeKeyedModelHandler(FakeModelHandler()) | |
| pcoll = pipeline | 'Unkeyed' >> beam.Create(examples) | |
| actual = pcoll | 'RunUnkeyed' >> base.RunInference( | |
| model_handler.with_preprocess_fn(mult_two).with_postprocess_fn( | |
| mult_two)) | |
| assert_that(actual, equal_to(expected), label='CheckUnkeyed') | |
| keyed_pcoll = pipeline | 'Keyed' >> beam.Create(keyed_examples) | |
| keyed_actual = keyed_pcoll | 'RunKeyed' >> base.RunInference( | |
| model_handler.with_preprocess_fn(mult_two_keyed).with_postprocess_fn( | |
| mult_two_keyed)) | |
| assert_that(keyed_actual, equal_to(keyed_expected), label='CheckKeyed') | |
| def test_run_inference_impl_dlq(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 'TEST', 3, 10, 'TEST2'] | |
| expected_good = [2, 4, 11] | |
| expected_bad = ['TEST', 'TEST2'] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| main, other = pcoll | base.RunInference( | |
| FakeModelHandler( | |
| min_batch_size=1, | |
| max_batch_size=1 | |
| )).with_exception_handling() | |
| assert_that(main, equal_to(expected_good), label='assert:inferences') | |
| # bad.failed_inferences will be in form [batch[elements], error]. | |
| # Just pull out bad element. | |
| bad_without_error = other.failed_inferences | beam.Map(lambda x: x[0][0]) | |
| assert_that( | |
| bad_without_error, equal_to(expected_bad), label='assert:failures') | |
| def test_run_inference_timeout_on_load_dlq(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 2] | |
| expected_good = [] | |
| expected_bad = [1, 2] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| main, other = pcoll | base.RunInference( | |
| FakeSlowModelHandler(10)).with_exception_handling(timeout=2) | |
| assert_that(main, equal_to(expected_good), label='assert:inferences') | |
| # bad.failed_inferences will be in form [batch[elements], error]. | |
| # Just pull out bad element. | |
| bad_without_error = other.failed_inferences | beam.Map(lambda x: x[0][0]) | |
| assert_that( | |
| bad_without_error, equal_to(expected_bad), label='assert:failures') | |
| def test_run_inference_timeout_on_inference_dlq(self): | |
| with TestPipeline() as pipeline: | |
| examples = [10, 11] | |
| expected_good = [] | |
| expected_bad = [10, 11] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| main, other = pcoll | base.RunInference( | |
| FakeSlowModelHandler(0)).with_exception_handling(timeout=5) | |
| assert_that(main, equal_to(expected_good), label='assert:inferences') | |
| # bad.failed_inferences will be in form [batch[elements], error]. | |
| # Just pull out bad element. | |
| bad_without_error = other.failed_inferences | beam.Map(lambda x: x[0][0]) | |
| assert_that( | |
| bad_without_error, equal_to(expected_bad), label='assert:failures') | |
| def test_run_inference_timeout_not_hit(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 2] | |
| expected_good = [1, 2] | |
| expected_bad = [] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| main, other = pcoll | base.RunInference( | |
| FakeSlowModelHandler(3)).with_exception_handling(timeout=500) | |
| assert_that(main, equal_to(expected_good), label='assert:inferences') | |
| # bad.failed_inferences will be in form [batch[elements], error]. | |
| # Just pull out bad element. | |
| bad_without_error = other.failed_inferences | beam.Map(lambda x: x[0][0]) | |
| assert_that( | |
| bad_without_error, equal_to(expected_bad), label='assert:failures') | |
| @unittest.skipIf( | |
| sys.platform == "win32" or sys.version_info < (3, 11), | |
| "This test relies on the __del__ lifecycle method, but __del__ does " + | |
| "not get invoked in the same way on older versions of Python or on " + | |
| "windows, breaking this test. See " + | |
| "github.com/python/cpython/issues/87950#issuecomment-1807570983 " + | |
| "for example.") | |
| def test_run_inference_timeout_does_garbage_collection(self): | |
| with tempfile.TemporaryDirectory() as tmp_dirname: | |
| tmp_path = os.path.join(tmp_dirname, 'tmp_filename') | |
| expected_file_contents = 'Deleted FakeSlowModel' | |
| with TestPipeline() as pipeline: | |
| # Start with bad example which gets timed out. | |
| # Then provide plenty of time for GC to happen. | |
| examples = [20] + [1] * 15 | |
| expected_good = [1] * 15 | |
| expected_bad = [20] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| main, other = pcoll | base.RunInference( | |
| FakeSlowModelHandler( | |
| 0, True, tmp_path)).with_exception_handling(timeout=5) | |
| assert_that(main, equal_to(expected_good), label='assert:inferences') | |
| # # bad.failed_inferences will be in form [batch[elements], error]. | |
| # # Just pull out bad element. | |
| bad_without_error = other.failed_inferences | beam.Map( | |
| lambda x: x[0][0]) | |
| assert_that( | |
| bad_without_error, equal_to(expected_bad), label='assert:failures') | |
| with open(tmp_path) as f: | |
| s = f.read() | |
| self.assertEqual(s, expected_file_contents) | |
| def test_run_inference_impl_inference_args(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| inference_args = {'key': True} | |
| actual = pcoll | base.RunInference( | |
| FakeModelHandlerExpectedInferenceArgs(), | |
| inference_args=inference_args) | |
| assert_that(actual, equal_to(examples), label='assert:inferences') | |
| def test_run_inference_metrics_with_custom_namespace(self): | |
| metrics_namespace = 'my_custom_namespace' | |
| pipeline = TestPipeline() | |
| examples = [1, 5, 3, 10] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| _ = pcoll | base.RunInference( | |
| FakeModelHandler(), metrics_namespace=metrics_namespace) | |
| result = pipeline.run() | |
| result.wait_until_finish() | |
| metrics_filter = MetricsFilter().with_namespace(namespace=metrics_namespace) | |
| metrics = result.metrics().query(metrics_filter) | |
| assert len(metrics['counters']) != 0 | |
| assert len(metrics['distributions']) != 0 | |
| metrics_filter = MetricsFilter().with_namespace(namespace='fake_namespace') | |
| metrics = result.metrics().query(metrics_filter) | |
| assert len(metrics['counters']) == len(metrics['distributions']) == 0 | |
| def test_unexpected_inference_args_passed(self): | |
| with self.assertRaisesRegex(ValueError, r'inference_args were provided'): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| inference_args = {'key': True} | |
| _ = pcoll | base.RunInference( | |
| FakeModelHandlerFailsOnInferenceArgs(), | |
| inference_args=inference_args) | |
| def test_increment_failed_batches_counter(self): | |
| with self.assertRaises(ValueError): | |
| # TODO(https://github.com/apache/beam/issues/34549): This test relies on | |
| # metrics filtering which doesn't work on Prism yet because Prism renames | |
| # steps (e.g. "Do" becomes "ref_AppliedPTransform_Do_7"). | |
| # https://github.com/apache/beam/blob/5f9cd73b7c9a2f37f83971ace3a399d633201dd1/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner.py#L1590 | |
| with TestPipeline('FnApiRunner') as pipeline: | |
| examples = [7] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| _ = pcoll | base.RunInference(FakeModelHandlerExpectedInferenceArgs()) | |
| run_result = pipeline.run() | |
| run_result.wait_until_finish() | |
| metric_results = ( | |
| run_result.metrics().query( | |
| MetricsFilter().with_name('failed_batches_counter'))) | |
| num_failed_batches_counter = metric_results['counters'][0] | |
| self.assertEqual(num_failed_batches_counter.committed, 3) | |
| # !!!: The above will need to be updated if retry behavior changes | |
| def test_failed_batches_counter_no_failures(self): | |
| pipeline = TestPipeline() | |
| examples = [7] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| inference_args = {'key': True} | |
| _ = pcoll | base.RunInference( | |
| FakeModelHandlerExpectedInferenceArgs(), inference_args=inference_args) | |
| run_result = pipeline.run() | |
| run_result.wait_until_finish() | |
| metric_results = ( | |
| run_result.metrics().query( | |
| MetricsFilter().with_name('failed_batches_counter'))) | |
| self.assertEqual(len(metric_results['counters']), 0) | |
| def test_counted_metrics(self): | |
| pipeline = TestPipeline() | |
| examples = [1, 5, 3, 10] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| _ = pcoll | base.RunInference(FakeModelHandler()) | |
| run_result = pipeline.run() | |
| run_result.wait_until_finish() | |
| metric_results = ( | |
| run_result.metrics().query(MetricsFilter().with_name('num_inferences'))) | |
| num_inferences_counter = metric_results['counters'][0] | |
| self.assertEqual(num_inferences_counter.committed, 4) | |
| inference_request_batch_size = run_result.metrics().query( | |
| MetricsFilter().with_name('inference_request_batch_size')) | |
| self.assertTrue(inference_request_batch_size['distributions']) | |
| self.assertEqual( | |
| inference_request_batch_size['distributions'][0].result.sum, 4) | |
| inference_request_batch_byte_size = run_result.metrics().query( | |
| MetricsFilter().with_name('inference_request_batch_byte_size')) | |
| self.assertTrue(inference_request_batch_byte_size['distributions']) | |
| self.assertGreaterEqual( | |
| inference_request_batch_byte_size['distributions'][0].result.sum, | |
| len(pickle.dumps(examples))) | |
| inference_request_batch_byte_size = run_result.metrics().query( | |
| MetricsFilter().with_name('model_byte_size')) | |
| self.assertTrue(inference_request_batch_byte_size['distributions']) | |
| def test_timing_metrics(self): | |
| pipeline = TestPipeline() | |
| examples = [1, 5, 3, 10] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| fake_clock = FakeClock() | |
| _ = pcoll | base.RunInference( | |
| FakeModelHandler(clock=fake_clock), clock=fake_clock) | |
| res = pipeline.run() | |
| res.wait_until_finish() | |
| metric_results = ( | |
| res.metrics().query( | |
| MetricsFilter().with_name('inference_batch_latency_micro_secs'))) | |
| batch_latency = metric_results['distributions'][0] | |
| self.assertEqual(batch_latency.result.count, 3) | |
| self.assertEqual(batch_latency.result.mean, 3000) | |
| metric_results = ( | |
| res.metrics().query( | |
| MetricsFilter().with_name('load_model_latency_milli_secs'))) | |
| load_model_latency = metric_results['distributions'][0] | |
| self.assertEqual(load_model_latency.result.count, 1) | |
| self.assertEqual(load_model_latency.result.mean, 500) | |
| def test_forwards_batch_args(self): | |
| examples = list(range(100)) | |
| with TestPipeline('FnApiRunner') as pipeline: | |
| pcoll = pipeline | 'start' >> beam.Create(examples, reshuffle=False) | |
| actual = pcoll | base.RunInference(FakeModelHandlerNeedsBigBatch()) | |
| assert_that(actual, equal_to(examples), label='assert:inferences') | |
| def test_run_inference_unkeyed_examples_with_keyed_model_handler(self): | |
| pipeline = TestPipeline() | |
| with self.assertRaisesRegex(Exception, "object is not iterable"): | |
| examples = [1, 3, 5] | |
| model_handler = base.KeyedModelHandler(FakeModelHandler()) | |
| _ = ( | |
| pipeline | 'Unkeyed' >> beam.Create(examples) | |
| | 'RunUnkeyed' >> base.RunInference(model_handler)) | |
| pipeline.run().wait_until_finish() | |
| def test_run_inference_keyed_examples_with_unkeyed_model_handler(self): | |
| pipeline = TestPipeline() | |
| examples = [1, 3, 5] | |
| keyed_examples = [(i, example) for i, example in enumerate(examples)] | |
| model_handler = FakeModelHandler() | |
| with self.assertRaisesRegex(Exception, "can only concatenate tuple"): | |
| _ = ( | |
| pipeline | 'keyed' >> beam.Create(keyed_examples) | |
| | 'RunKeyed' >> base.RunInference(model_handler)) | |
| pipeline.run() | |
| def test_model_handler_compatibility(self): | |
| # ** IMPORTANT ** Do not change this test to make your PR pass without | |
| # first reading below. | |
| # Be certain that the modification will not break third party | |
| # implementations of ModelHandler. | |
| # See issue https://github.com/apache/beam/issues/23484 | |
| # If this test fails, likely third party implementations of | |
| # ModelHandler will break. | |
| class ThirdPartyHandler(base.ModelHandler[int, int, FakeModel]): | |
| def __init__(self, custom_parameter=None): | |
| pass | |
| def load_model(self) -> FakeModel: | |
| return FakeModel() | |
| def run_inference( | |
| self, | |
| batch: Sequence[int], | |
| model: FakeModel, | |
| inference_args: Optional[dict[str, Any]] = None) -> Iterable[int]: | |
| yield 0 | |
| def get_num_bytes(self, batch: Sequence[int]) -> int: | |
| return 1 | |
| def get_metrics_namespace(self) -> str: | |
| return 'ThirdParty' | |
| def get_resource_hints(self) -> dict: | |
| return {} | |
| def batch_elements_kwargs(self) -> Mapping[str, Any]: | |
| return {} | |
| def validate_inference_args( | |
| self, inference_args: Optional[dict[str, Any]]): | |
| pass | |
| # This test passes if calling these methods does not cause | |
| # any runtime exceptions. | |
| third_party_model_handler = ThirdPartyHandler(custom_parameter=0) | |
| fake_model = third_party_model_handler.load_model() | |
| third_party_model_handler.run_inference([], fake_model) | |
| fake_inference_args = {'some_arg': 1} | |
| third_party_model_handler.run_inference([], | |
| fake_model, | |
| inference_args=fake_inference_args) | |
| third_party_model_handler.get_num_bytes([1, 2, 3]) | |
| third_party_model_handler.get_metrics_namespace() | |
| third_party_model_handler.get_resource_hints() | |
| third_party_model_handler.batch_elements_kwargs() | |
| third_party_model_handler.validate_inference_args({}) | |
| def test_run_inference_prediction_result_with_model_id(self): | |
| examples = [1, 5, 3, 10] | |
| expected = [ | |
| base.PredictionResult( | |
| example=example, | |
| inference=example + 1, | |
| model_id='fake_model_id_default') for example in examples | |
| ] | |
| with TestPipeline() as pipeline: | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| FakeModelHandlerReturnsPredictionResult()) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_run_inference_with_iterable_side_input(self): | |
| test_pipeline = TestPipeline() | |
| side_input = ( | |
| test_pipeline | "CreateDummySideInput" >> beam.Create( | |
| [base.ModelMetadata(1, 1), base.ModelMetadata(2, 2)]) | |
| | "ApplySideInputWindow" >> beam.WindowInto( | |
| window.GlobalWindows(), | |
| trigger=trigger.Repeatedly(trigger.AfterProcessingTime(1)), | |
| accumulation_mode=trigger.AccumulationMode.DISCARDING)) | |
| test_pipeline.options.view_as(StandardOptions).streaming = True | |
| with self.assertRaises(Exception) as e: | |
| _ = ( | |
| test_pipeline | |
| | beam.Create([1, 2, 3, 4]) | |
| | base.RunInference( | |
| FakeModelHandler(), model_metadata_pcoll=side_input)) | |
| test_pipeline.run() | |
| msg = str(e.exception) | |
| self.assertIn('singleton', msg, msg='Expected singleton view error') | |
| self.assertIn('more than one', msg, msg='Expected multiple-elements error') | |
| def test_run_inference_with_iterable_side_input_multi_process_shared(self): | |
| test_pipeline = TestPipeline() | |
| side_input = ( | |
| test_pipeline | "CreateDummySideInput" >> beam.Create( | |
| [base.ModelMetadata(1, 1), base.ModelMetadata(2, 2)]) | |
| | "ApplySideInputWindow" >> beam.WindowInto( | |
| window.GlobalWindows(), | |
| trigger=trigger.Repeatedly(trigger.AfterProcessingTime(1)), | |
| accumulation_mode=trigger.AccumulationMode.DISCARDING)) | |
| test_pipeline.options.view_as(StandardOptions).streaming = True | |
| with self.assertRaises(Exception) as e: | |
| _ = ( | |
| test_pipeline | |
| | beam.Create([1, 2, 3, 4]) | |
| | base.RunInference( | |
| FakeModelHandler(multi_process_shared=True), | |
| model_metadata_pcoll=side_input)) | |
| test_pipeline.run() | |
| msg = str(e.exception) | |
| self.assertIn('singleton', msg, msg='Expected singleton view error') | |
| self.assertIn('more than one', msg, msg='Expected multiple-elements error') | |
| def test_run_inference_empty_side_input(self): | |
| model_handler = FakeModelHandlerReturnsPredictionResult() | |
| main_input_elements = [1, 2] | |
| with TestPipeline(is_integration_test=False) as pipeline: | |
| side_pcoll = pipeline | "side" >> beam.Create([]) | |
| result_pcoll = ( | |
| pipeline | |
| | beam.Create(main_input_elements) | |
| | base.RunInference(model_handler, model_metadata_pcoll=side_pcoll)) | |
| expected = [ | |
| base.PredictionResult(ele, ele + 1, 'fake_model_id_default') | |
| for ele in main_input_elements | |
| ] | |
| assert_that(result_pcoll, equal_to(expected)) | |
| def test_run_inference_side_input_in_batch(self): | |
| first_ts = math.floor(time.time()) - 30 | |
| interval = 7 | |
| sample_main_input_elements = ([ | |
| first_ts - 2, | |
| first_ts + 1, | |
| first_ts + 8, | |
| first_ts + 15, | |
| first_ts + 22, | |
| ]) | |
| sample_side_input_elements = [ | |
| (first_ts + 1, base.ModelMetadata(model_id='', model_name='')), | |
| # if model_id is empty string, we use the default model | |
| # handler model URI. | |
| ( | |
| first_ts + 8, | |
| base.ModelMetadata( | |
| model_id='fake_model_id_1', model_name='fake_model_id_1')), | |
| ( | |
| first_ts + 15, | |
| base.ModelMetadata( | |
| model_id='fake_model_id_2', model_name='fake_model_id_2')) | |
| ] | |
| model_handler = FakeModelHandlerReturnsPredictionResult() | |
| # applying GroupByKey to utilize windowing according to | |
| # https://beam.apache.org/documentation/programming-guide/#windowing-bounded-collections | |
| class _EmitElement(beam.DoFn): | |
| def process(self, element): | |
| for e in element: | |
| yield e | |
| # This test relies on poorly defined side input semantics which vary | |
| # across runners (including prism). Pinning to FnApiRunner which | |
| # consistently guarantees output. | |
| with TestPipeline('FnApiRunner') as pipeline: | |
| side_input = ( | |
| pipeline | |
| | | |
| "CreateSideInputElements" >> beam.Create(sample_side_input_elements) | |
| | beam.Map(lambda x: TimestampedValue(x[1], x[0])) | |
| | beam.WindowInto( | |
| window.FixedWindows(interval), | |
| accumulation_mode=trigger.AccumulationMode.DISCARDING) | |
| | beam.Map(lambda x: ('key', x)) | |
| | beam.GroupByKey() | |
| | beam.Map(lambda x: x[1]) | |
| | "EmitSideInput" >> beam.ParDo(_EmitElement())) | |
| result_pcoll = ( | |
| pipeline | |
| | beam.Create(sample_main_input_elements) | |
| | "MapTimeStamp" >> beam.Map(lambda x: TimestampedValue(x, x)) | |
| | "ApplyWindow" >> beam.WindowInto(window.FixedWindows(interval)) | |
| | beam.Map(lambda x: ('key', x)) | |
| | "MainInputGBK" >> beam.GroupByKey() | |
| | beam.Map(lambda x: x[1]) | |
| | beam.ParDo(_EmitElement()) | |
| | "RunInference" >> base.RunInference( | |
| model_handler, model_metadata_pcoll=side_input)) | |
| expected_model_id_order = [ | |
| 'fake_model_id_default', | |
| 'fake_model_id_default', | |
| 'fake_model_id_1', | |
| 'fake_model_id_2', | |
| 'fake_model_id_2' | |
| ] | |
| expected_result = [ | |
| base.PredictionResult( | |
| example=sample_main_input_elements[i], | |
| inference=sample_main_input_elements[i] + 1, | |
| model_id=expected_model_id_order[i]) for i in range(5) | |
| ] | |
| assert_that(result_pcoll, equal_to(expected_result)) | |
| def test_run_inference_side_input_in_batch_per_key_models(self): | |
| first_ts = math.floor(time.time()) - 30 | |
| interval = 7 | |
| sample_main_input_elements = ([ | |
| ('key1', first_ts - 2), | |
| ('key2', first_ts + 1), | |
| ('key2', first_ts + 8), | |
| ('key1', first_ts + 15), | |
| ('key2', first_ts + 22), | |
| ('key1', first_ts + 29), | |
| ]) | |
| sample_side_input_elements = [ | |
| ( | |
| first_ts + 1, | |
| [ | |
| base.KeyModelPathMapping( | |
| keys=['key1'], update_path='fake_model_id_default'), | |
| base.KeyModelPathMapping( | |
| keys=['key2'], update_path='fake_model_id_default') | |
| ]), | |
| # if model_id is empty string, we use the default model | |
| # handler model URI. | |
| ( | |
| first_ts + 8, | |
| [ | |
| base.KeyModelPathMapping( | |
| keys=['key1'], update_path='fake_model_id_1'), | |
| base.KeyModelPathMapping( | |
| keys=['key2'], update_path='fake_model_id_default') | |
| ]), | |
| ( | |
| first_ts + 15, | |
| [ | |
| base.KeyModelPathMapping( | |
| keys=['key1'], update_path='fake_model_id_1'), | |
| base.KeyModelPathMapping( | |
| keys=['key2'], update_path='fake_model_id_2') | |
| ]), | |
| ] | |
| model_handler = base.KeyedModelHandler([ | |
| base.KeyModelMapping(['key1'], | |
| FakeModelHandlerReturnsPredictionResult( | |
| multi_process_shared=True, state=True)), | |
| base.KeyModelMapping(['key2'], | |
| FakeModelHandlerReturnsPredictionResult( | |
| multi_process_shared=True, state=True)) | |
| ]) | |
| class _EmitElement(beam.DoFn): | |
| def process(self, element): | |
| for e in element: | |
| yield e | |
| # This test relies on poorly defined side input semantics which vary | |
| # across runners (including prism). Pinning to FnApiRunner which | |
| # consistently guarantees output. | |
| with TestPipeline('FnApiRunner') as pipeline: | |
| side_input = ( | |
| pipeline | |
| | | |
| "CreateSideInputElements" >> beam.Create(sample_side_input_elements) | |
| | beam.Map(lambda x: TimestampedValue(x[1], x[0])) | |
| | beam.WindowInto( | |
| window.FixedWindows(interval), | |
| accumulation_mode=trigger.AccumulationMode.DISCARDING) | |
| | beam.Map(lambda x: ('key', x)) | |
| | beam.GroupByKey() | |
| | beam.Map(lambda x: x[1]) | |
| | "EmitSideInput" >> beam.ParDo(_EmitElement())) | |
| result_pcoll = ( | |
| pipeline | |
| | beam.Create(sample_main_input_elements) | |
| | "MapTimeStamp" >> beam.Map(lambda x: TimestampedValue(x, x[1])) | |
| | "ApplyWindow" >> beam.WindowInto(window.FixedWindows(interval)) | |
| | "RunInference" >> base.RunInference( | |
| model_handler, model_metadata_pcoll=side_input) | |
| | beam.Map(lambda x: x[1])) | |
| expected_model_id_order = [ | |
| 'fake_model_id_default', | |
| 'fake_model_id_default', | |
| 'fake_model_id_default', | |
| 'fake_model_id_1', | |
| 'fake_model_id_2', | |
| 'fake_model_id_1', | |
| ] | |
| expected_inferences = [ | |
| 0, | |
| 0, | |
| 1, | |
| 0, | |
| 0, | |
| 1, | |
| ] | |
| expected_result = [ | |
| base.PredictionResult( | |
| example=sample_main_input_elements[i][1], | |
| inference=expected_inferences[i], | |
| model_id=expected_model_id_order[i]) | |
| for i in range(len(expected_inferences)) | |
| ] | |
| assert_that(result_pcoll, equal_to(expected_result)) | |
| def test_run_inference_side_input_in_batch_per_key_models_split_cohort(self): | |
| first_ts = math.floor(time.time()) - 30 | |
| interval = 7 | |
| sample_main_input_elements = ([ | |
| ('key1', first_ts - 2), | |
| ('key2', first_ts + 1), | |
| ('key1', first_ts + 8), | |
| ('key2', first_ts + 15), | |
| ('key1', first_ts + 22), | |
| ]) | |
| sample_side_input_elements = [ | |
| ( | |
| first_ts + 1, | |
| [ | |
| base.KeyModelPathMapping( | |
| keys=['key1', 'key2'], update_path='fake_model_id_default') | |
| ]), | |
| # if model_id is empty string, we use the default model | |
| # handler model URI. | |
| ( | |
| first_ts + 8, | |
| [ | |
| base.KeyModelPathMapping( | |
| keys=['key1'], update_path='fake_model_id_1'), | |
| base.KeyModelPathMapping( | |
| keys=['key2'], update_path='fake_model_id_default') | |
| ]), | |
| ( | |
| first_ts + 15, | |
| [ | |
| base.KeyModelPathMapping( | |
| keys=['key1'], update_path='fake_model_id_1'), | |
| base.KeyModelPathMapping( | |
| keys=['key2'], update_path='fake_model_id_2') | |
| ]), | |
| ] | |
| model_handler = base.KeyedModelHandler([ | |
| base.KeyModelMapping( | |
| ['key1', 'key2'], | |
| FakeModelHandlerReturnsPredictionResult(multi_process_shared=True)) | |
| ]) | |
| class _EmitElement(beam.DoFn): | |
| def process(self, element): | |
| for e in element: | |
| yield e | |
| # This test relies on poorly defined side input semantics which vary | |
| # across runners (including prism). Pinning to FnApiRunner which | |
| # consistently guarantees output. | |
| with TestPipeline('FnApiRunner') as pipeline: | |
| side_input = ( | |
| pipeline | |
| | | |
| "CreateSideInputElements" >> beam.Create(sample_side_input_elements) | |
| | beam.Map(lambda x: TimestampedValue(x[1], x[0])) | |
| | beam.WindowInto( | |
| window.FixedWindows(interval), | |
| accumulation_mode=trigger.AccumulationMode.DISCARDING) | |
| | beam.Map(lambda x: ('key', x)) | |
| | beam.GroupByKey() | |
| | beam.Map(lambda x: x[1]) | |
| | "EmitSideInput" >> beam.ParDo(_EmitElement())) | |
| result_pcoll = ( | |
| pipeline | |
| | beam.Create(sample_main_input_elements) | |
| | "MapTimeStamp" >> beam.Map(lambda x: TimestampedValue(x, x[1])) | |
| | "ApplyWindow" >> beam.WindowInto(window.FixedWindows(interval)) | |
| | "RunInference" >> base.RunInference( | |
| model_handler, model_metadata_pcoll=side_input) | |
| | beam.Map(lambda x: x[1])) | |
| expected_model_id_order = [ | |
| 'fake_model_id_default', | |
| 'fake_model_id_default', | |
| 'fake_model_id_1', | |
| 'fake_model_id_2', | |
| 'fake_model_id_1' | |
| ] | |
| expected_result = [ | |
| base.PredictionResult( | |
| example=sample_main_input_elements[i][1], | |
| inference=sample_main_input_elements[i][1] + 1, | |
| model_id=expected_model_id_order[i]) for i in range(5) | |
| ] | |
| assert_that(result_pcoll, equal_to(expected_result)) | |
| def test_run_inference_side_input_in_batch_multi_process_shared(self): | |
| first_ts = math.floor(time.time()) - 30 | |
| interval = 7 | |
| sample_main_input_elements = ([ | |
| first_ts - 2, | |
| first_ts + 1, | |
| first_ts + 8, | |
| first_ts + 15, | |
| first_ts + 22, | |
| ]) | |
| sample_side_input_elements = [ | |
| (first_ts + 1, base.ModelMetadata(model_id='', model_name='')), | |
| # if model_id is empty string, we use the default model | |
| # handler model URI. | |
| ( | |
| first_ts + 8, | |
| base.ModelMetadata( | |
| model_id='fake_model_id_1', model_name='fake_model_id_1')), | |
| ( | |
| first_ts + 15, | |
| base.ModelMetadata( | |
| model_id='fake_model_id_2', model_name='fake_model_id_2')) | |
| ] | |
| model_handler = FakeModelHandlerReturnsPredictionResult( | |
| multi_process_shared=True) | |
| # applying GroupByKey to utilize windowing according to | |
| # https://beam.apache.org/documentation/programming-guide/#windowing-bounded-collections | |
| class _EmitElement(beam.DoFn): | |
| def process(self, element): | |
| for e in element: | |
| yield e | |
| # This test relies on poorly defined side input semantics which vary | |
| # across runners (including prism). Pinning to FnApiRunner which | |
| # consistently guarantees output. | |
| with TestPipeline('FnApiRunner') as pipeline: | |
| side_input = ( | |
| pipeline | |
| | | |
| "CreateSideInputElements" >> beam.Create(sample_side_input_elements) | |
| | beam.Map(lambda x: TimestampedValue(x[1], x[0])) | |
| | beam.WindowInto( | |
| window.FixedWindows(interval), | |
| accumulation_mode=trigger.AccumulationMode.DISCARDING) | |
| | beam.Map(lambda x: ('key', x)) | |
| | beam.GroupByKey() | |
| | beam.Map(lambda x: x[1]) | |
| | "EmitSideInput" >> beam.ParDo(_EmitElement())) | |
| result_pcoll = ( | |
| pipeline | |
| | beam.Create(sample_main_input_elements) | |
| | "MapTimeStamp" >> beam.Map(lambda x: TimestampedValue(x, x)) | |
| | "ApplyWindow" >> beam.WindowInto(window.FixedWindows(interval)) | |
| | beam.Map(lambda x: ('key', x)) | |
| | "MainInputGBK" >> beam.GroupByKey() | |
| | beam.Map(lambda x: x[1]) | |
| | beam.ParDo(_EmitElement()) | |
| | "RunInference" >> base.RunInference( | |
| model_handler, model_metadata_pcoll=side_input)) | |
| expected_model_id_order = [ | |
| 'fake_model_id_default', | |
| 'fake_model_id_default', | |
| 'fake_model_id_1', | |
| 'fake_model_id_2', | |
| 'fake_model_id_2' | |
| ] | |
| expected_result = [ | |
| base.PredictionResult( | |
| example=sample_main_input_elements[i], | |
| inference=sample_main_input_elements[i] + 1, | |
| model_id=expected_model_id_order[i]) for i in range(5) | |
| ] | |
| assert_that(result_pcoll, equal_to(expected_result)) | |
| @unittest.skipIf( | |
| not TestPipeline().get_pipeline_options().view_as( | |
| StandardOptions).streaming, | |
| "SideInputs to RunInference are only supported in streaming mode.") | |
| @pytest.mark.it_postcommit | |
| @pytest.mark.sickbay_direct | |
| @pytest.mark.it_validatesrunner | |
| def test_run_inference_with_side_inputin_streaming(self): | |
| test_pipeline = TestPipeline(is_integration_test=True) | |
| test_pipeline.options.view_as(StandardOptions).streaming = True | |
| run_inference_side_inputs.run( | |
| test_pipeline.get_full_options_as_args(), save_main_session=False) | |
| def test_env_vars_set_correctly(self): | |
| handler_with_vars = FakeModelHandler(env_vars={'FOO': 'bar'}) | |
| os.environ.pop('FOO', None) | |
| self.assertFalse('FOO' in os.environ) | |
| with TestPipeline() as pipeline: | |
| examples = [1, 2, 3] | |
| _ = ( | |
| pipeline | |
| | 'start' >> beam.Create(examples) | |
| | base.RunInference(handler_with_vars)) | |
| pipeline.run() | |
| self.assertTrue('FOO' in os.environ) | |
| self.assertTrue((os.environ['FOO']) == 'bar') | |
| def test_child_class_without_env_vars(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = [example + 1 for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference(FakeModelHandlerNoEnvVars()) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_model_handler_manager_loads_shared_model(self): | |
| mhs = { | |
| 'key1': FakeModelHandler(state=1), | |
| 'key2': FakeModelHandler(state=2), | |
| 'key3': FakeModelHandler(state=3) | |
| } | |
| mm = base._ModelHandlerManager(mh_map=mhs) | |
| tag1 = mm.load('key1').model_tag | |
| # Use bad_mh's load function to make sure we're actually loading the | |
| # version already stored | |
| bad_mh = FakeModelHandler(state=100) | |
| model1 = multi_process_shared.MultiProcessShared( | |
| bad_mh.load_model, tag=tag1).acquire() | |
| self.assertEqual(1, model1.predict(10)) | |
| tag2 = mm.load('key2').model_tag | |
| tag3 = mm.load('key3').model_tag | |
| model2 = multi_process_shared.MultiProcessShared( | |
| bad_mh.load_model, tag=tag2).acquire() | |
| model3 = multi_process_shared.MultiProcessShared( | |
| bad_mh.load_model, tag=tag3).acquire() | |
| self.assertEqual(2, model2.predict(10)) | |
| self.assertEqual(3, model3.predict(10)) | |
| def test_model_handler_manager_evicts_models(self): | |
| mh1 = FakeModelHandler(state=1) | |
| mh2 = FakeModelHandler(state=2) | |
| mh3 = FakeModelHandler(state=3) | |
| mhs = {'key1': mh1, 'key2': mh2, 'key3': mh3} | |
| mm = base._ModelHandlerManager(mh_map=mhs) | |
| mm.increment_max_models(2) | |
| tag1 = mm.load('key1').model_tag | |
| sh1 = multi_process_shared.MultiProcessShared(mh1.load_model, tag=tag1) | |
| model1 = sh1.acquire() | |
| self.assertEqual(1, model1.predict(10)) | |
| model1.increment_state(5) | |
| tag2 = mm.load('key2').model_tag | |
| tag3 = mm.load('key3').model_tag | |
| sh2 = multi_process_shared.MultiProcessShared(mh2.load_model, tag=tag2) | |
| model2 = sh2.acquire() | |
| sh3 = multi_process_shared.MultiProcessShared(mh3.load_model, tag=tag3) | |
| model3 = sh3.acquire() | |
| model2.increment_state(5) | |
| model3.increment_state(5) | |
| self.assertEqual(7, model2.predict(10)) | |
| self.assertEqual(8, model3.predict(10)) | |
| sh2.release(model2) | |
| sh3.release(model3) | |
| # model1 should have retained a valid reference to the model until released | |
| self.assertEqual(6, model1.predict(10)) | |
| sh1.release(model1) | |
| # This should now have been garbage collected, so it now it should get | |
| # recreated and shouldn't have the state updates | |
| model1 = multi_process_shared.MultiProcessShared( | |
| mh1.load_model, tag=tag1).acquire() | |
| self.assertEqual(1, model1.predict(10)) | |
| # These should not get recreated, so they should have the state updates | |
| model2 = multi_process_shared.MultiProcessShared( | |
| mh2.load_model, tag=tag2).acquire() | |
| self.assertEqual(7, model2.predict(10)) | |
| model3 = multi_process_shared.MultiProcessShared( | |
| mh3.load_model, tag=tag3).acquire() | |
| self.assertEqual(8, model3.predict(10)) | |
| def test_model_handler_manager_evicts_models_after_update(self): | |
| mh1 = FakeModelHandler(state=1) | |
| mhs = {'key1': mh1} | |
| mm = base._ModelHandlerManager(mh_map=mhs) | |
| tag1 = mm.load('key1').model_tag | |
| sh1 = multi_process_shared.MultiProcessShared(mh1.load_model, tag=tag1) | |
| model1 = sh1.acquire() | |
| self.assertEqual(1, model1.predict(10)) | |
| model1.increment_state(5) | |
| self.assertEqual(6, model1.predict(10)) | |
| mm.update_model_handler('key1', 'fake/path', 'key1') | |
| self.assertEqual(6, model1.predict(10)) | |
| sh1.release(model1) | |
| tag1 = mm.load('key1').model_tag | |
| sh1 = multi_process_shared.MultiProcessShared(mh1.load_model, tag=tag1) | |
| model1 = sh1.acquire() | |
| self.assertEqual(1, model1.predict(10)) | |
| model1.increment_state(5) | |
| self.assertEqual(6, model1.predict(10)) | |
| sh1.release(model1) | |
| # Shouldn't evict if path is the same as last update | |
| mm.update_model_handler('key1', 'fake/path', 'key1') | |
| tag1 = mm.load('key1').model_tag | |
| sh1 = multi_process_shared.MultiProcessShared(mh1.load_model, tag=tag1) | |
| model1 = sh1.acquire() | |
| self.assertEqual(6, model1.predict(10)) | |
| sh1.release(model1) | |
| def test_model_handler_manager_evicts_models_after_being_incremented(self): | |
| mh1 = FakeModelHandler(state=1) | |
| mh2 = FakeModelHandler(state=2) | |
| mh3 = FakeModelHandler(state=3) | |
| mhs = {'key1': mh1, 'key2': mh2, 'key3': mh3} | |
| mm = base._ModelHandlerManager(mh_map=mhs) | |
| mm.increment_max_models(1) | |
| mm.increment_max_models(1) | |
| tag1 = mm.load('key1').model_tag | |
| sh1 = multi_process_shared.MultiProcessShared(mh1.load_model, tag=tag1) | |
| model1 = sh1.acquire() | |
| self.assertEqual(1, model1.predict(10)) | |
| model1.increment_state(5) | |
| self.assertEqual(6, model1.predict(10)) | |
| sh1.release(model1) | |
| tag2 = mm.load('key2').model_tag | |
| tag3 = mm.load('key3').model_tag | |
| sh2 = multi_process_shared.MultiProcessShared(mh2.load_model, tag=tag2) | |
| model2 = sh2.acquire() | |
| sh3 = multi_process_shared.MultiProcessShared(mh3.load_model, tag=tag3) | |
| model3 = sh3.acquire() | |
| model2.increment_state(5) | |
| model3.increment_state(5) | |
| self.assertEqual(7, model2.predict(10)) | |
| self.assertEqual(8, model3.predict(10)) | |
| sh2.release(model2) | |
| sh3.release(model3) | |
| # This should get recreated, so it shouldn't have the state updates | |
| model1 = multi_process_shared.MultiProcessShared( | |
| mh1.load_model, tag=tag1).acquire() | |
| self.assertEqual(1, model1.predict(10)) | |
| # These should not get recreated, so they should have the state updates | |
| model2 = multi_process_shared.MultiProcessShared( | |
| mh2.load_model, tag=tag2).acquire() | |
| self.assertEqual(7, model2.predict(10)) | |
| model3 = multi_process_shared.MultiProcessShared( | |
| mh3.load_model, tag=tag3).acquire() | |
| self.assertEqual(8, model3.predict(10)) | |
| def test_run_inference_loads_different_models(self): | |
| mh1 = FakeModelHandler(incrementing=True, min_batch_size=3) | |
| with TestPipeline() as pipeline: | |
| pcoll = pipeline | 'start' >> beam.Create([1, 2, 3]) | |
| actual = ( | |
| pcoll | |
| | 'ri1' >> base.RunInference(mh1) | |
| | 'ri2' >> base.RunInference(mh1)) | |
| assert_that(actual, equal_to([1, 2, 3]), label='assert:inferences') | |
| def test_run_inference_loads_different_models_multi_process_shared(self): | |
| mh1 = FakeModelHandler( | |
| incrementing=True, min_batch_size=3, multi_process_shared=True) | |
| with TestPipeline() as pipeline: | |
| pcoll = pipeline | 'start' >> beam.Create([1, 2, 3]) | |
| actual = ( | |
| pcoll | |
| | 'ri1' >> base.RunInference(mh1) | |
| | 'ri2' >> base.RunInference(mh1)) | |
| assert_that(actual, equal_to([1, 2, 3]), label='assert:inferences') | |
| def test_runinference_loads_same_model_with_identifier_multi_process_shared( | |
| self): | |
| mh1 = FakeModelHandler( | |
| incrementing=True, min_batch_size=3, multi_process_shared=True) | |
| with TestPipeline() as pipeline: | |
| pcoll = pipeline | 'start' >> beam.Create([1, 2, 3]) | |
| actual = ( | |
| pcoll | |
| | 'ri1' >> base.RunInference( | |
| mh1, | |
| model_identifier='same_model_with_identifier_multi_process_shared' | |
| ) | |
| | 'ri2' >> base.RunInference( | |
| mh1, | |
| model_identifier='same_model_with_identifier_multi_process_shared' | |
| )) | |
| assert_that(actual, equal_to([4, 5, 6]), label='assert:inferences') | |
| def test_run_inference_watch_file_pattern_side_input_label(self): | |
| pipeline = TestPipeline() | |
| # label of the WatchPattern transform. | |
| side_input_str = 'WatchFilePattern/ApplyGlobalWindow' | |
| from apache_beam.ml.inference.utils import WatchFilePattern | |
| file_pattern_side_input = ( | |
| pipeline | |
| | 'WatchFilePattern' >> WatchFilePattern(file_pattern='fake/path/*')) | |
| pcoll = pipeline | 'start' >> beam.Create([1, 2, 3]) | |
| result_pcoll = pcoll | base.RunInference( | |
| FakeModelHandler(), model_metadata_pcoll=file_pattern_side_input) | |
| assert side_input_str in str(result_pcoll.producer.side_inputs[0]) | |
| def test_run_inference_watch_file_pattern_keyword_arg_side_input_label(self): | |
| # label of the WatchPattern transform. | |
| side_input_str = 'WatchFilePattern/ApplyGlobalWindow' | |
| pipeline = TestPipeline() | |
| pcoll = pipeline | 'start' >> beam.Create([1, 2, 3]) | |
| result_pcoll = pcoll | base.RunInference( | |
| FakeModelHandler(), watch_model_pattern='fake/path/*') | |
| assert side_input_str in str(result_pcoll.producer.side_inputs[0]) | |
| def test_model_status_provides_valid_tags(self): | |
| ms = base._ModelStatus(False) | |
| self.assertTrue(ms.is_valid_tag('tag1')) | |
| self.assertTrue(ms.is_valid_tag('tag2')) | |
| self.assertEqual('tag1', ms.get_valid_tag('tag1')) | |
| self.assertEqual('tag2', ms.get_valid_tag('tag2')) | |
| self.assertTrue(ms.is_valid_tag('tag1')) | |
| self.assertTrue(ms.is_valid_tag('tag2')) | |
| ms.try_mark_current_model_invalid(0) | |
| self.assertFalse(ms.is_valid_tag('tag1')) | |
| self.assertFalse(ms.is_valid_tag('tag2')) | |
| self.assertEqual('tag1_reload_1', ms.get_valid_tag('tag1')) | |
| self.assertEqual('tag2_reload_1', ms.get_valid_tag('tag2')) | |
| self.assertTrue(ms.is_valid_tag('tag1_reload_1')) | |
| self.assertTrue(ms.is_valid_tag('tag2_reload_1')) | |
| ms.try_mark_current_model_invalid(0) | |
| self.assertFalse(ms.is_valid_tag('tag1')) | |
| self.assertFalse(ms.is_valid_tag('tag2')) | |
| self.assertFalse(ms.is_valid_tag('tag1_reload_1')) | |
| self.assertFalse(ms.is_valid_tag('tag2_reload_1')) | |
| self.assertEqual('tag1_reload_2', ms.get_valid_tag('tag1')) | |
| self.assertEqual('tag2_reload_2', ms.get_valid_tag('tag2')) | |
| self.assertTrue(ms.is_valid_tag('tag1_reload_2')) | |
| self.assertTrue(ms.is_valid_tag('tag2_reload_2')) | |
| def test_model_status_provides_valid_garbage_collection(self): | |
| ms = base._ModelStatus(True) | |
| self.assertTrue(ms.is_valid_tag('mspvgc_tag1')) | |
| self.assertTrue(ms.is_valid_tag('mspvgc_tag2')) | |
| ms.try_mark_current_model_invalid(1) | |
| tags = ms.get_tags_for_garbage_collection() | |
| self.assertEqual(0, len(tags)) | |
| time.sleep(4) | |
| ms.try_mark_current_model_invalid(1) | |
| time.sleep(4) | |
| self.assertTrue(ms.is_valid_tag('mspvgc_tag3')) | |
| tags = ms.get_tags_for_garbage_collection() | |
| self.assertEqual(2, len(tags)) | |
| self.assertTrue('mspvgc_tag1' in tags) | |
| self.assertTrue('mspvgc_tag2' in tags) | |
| ms.try_mark_current_model_invalid(0) | |
| tags = ms.get_tags_for_garbage_collection() | |
| self.assertEqual(3, len(tags)) | |
| self.assertTrue('mspvgc_tag1' in tags) | |
| self.assertTrue('mspvgc_tag2' in tags) | |
| self.assertTrue('mspvgc_tag3' in tags) | |
| ms.mark_tags_deleted(set(['mspvgc_tag1', 'mspvgc_tag2'])) | |
| tags = ms.get_tags_for_garbage_collection() | |
| self.assertEqual(['mspvgc_tag3'], tags) | |
| ms.mark_tags_deleted(set(['mspvgc_tag1'])) | |
| tags = ms.get_tags_for_garbage_collection() | |
| self.assertEqual(1, len(tags)) | |
| self.assertEqual('mspvgc_tag3', tags[0]) | |
| ms.mark_tags_deleted(set(['mspvgc_tag3'])) | |
| tags = ms.get_tags_for_garbage_collection() | |
| self.assertEqual(0, len(tags)) | |
| def _always_retry(e: Exception) -> bool: | |
| return True | |
| class FakeRemoteModelHandler(base.RemoteModelHandler[int, int, FakeModel]): | |
| def __init__( | |
| self, | |
| clock=None, | |
| min_batch_size=1, | |
| max_batch_size=9999, | |
| retry_filter=_always_retry, | |
| **kwargs): | |
| self._fake_clock = clock | |
| self._min_batch_size = min_batch_size | |
| self._max_batch_size = max_batch_size | |
| self._env_vars = kwargs.get('env_vars', {}) | |
| self._multi_process_shared = multi_process_shared | |
| super().__init__( | |
| namespace='FakeRemoteModelHandler', retry_filter=retry_filter) | |
| def create_client(self): | |
| return FakeModel() | |
| def request(self, batch, model, inference_args=None) -> Iterable[int]: | |
| responses = [] | |
| for example in batch: | |
| responses.append(model.predict(example)) | |
| return responses | |
| def batch_elements_kwargs(self): | |
| return { | |
| 'min_batch_size': self._min_batch_size, | |
| 'max_batch_size': self._max_batch_size | |
| } | |
| class FakeAlwaysFailsRemoteModelHandler(base.RemoteModelHandler[int, | |
| int, | |
| FakeModel]): | |
| def __init__( | |
| self, | |
| clock=None, | |
| min_batch_size=1, | |
| max_batch_size=9999, | |
| retry_filter=_always_retry, | |
| **kwargs): | |
| self._fake_clock = clock | |
| self._min_batch_size = min_batch_size | |
| self._max_batch_size = max_batch_size | |
| self._env_vars = kwargs.get('env_vars', {}) | |
| super().__init__( | |
| namespace='FakeRemoteModelHandler', | |
| retry_filter=retry_filter, | |
| num_retries=2, | |
| throttle_delay_secs=1) | |
| def create_client(self): | |
| return FakeModel() | |
| def request(self, batch, model, inference_args=None) -> Iterable[int]: | |
| raise Exception | |
| def batch_elements_kwargs(self): | |
| return { | |
| 'min_batch_size': self._min_batch_size, | |
| 'max_batch_size': self._max_batch_size | |
| } | |
| class FakeFailsOnceRemoteModelHandler(base.RemoteModelHandler[int, | |
| int, | |
| FakeModel]): | |
| def __init__( | |
| self, | |
| clock=None, | |
| min_batch_size=1, | |
| max_batch_size=9999, | |
| retry_filter=_always_retry, | |
| **kwargs): | |
| self._fake_clock = clock | |
| self._min_batch_size = min_batch_size | |
| self._max_batch_size = max_batch_size | |
| self._env_vars = kwargs.get('env_vars', {}) | |
| self._should_fail = True | |
| super().__init__( | |
| namespace='FakeRemoteModelHandler', | |
| retry_filter=retry_filter, | |
| num_retries=2, | |
| throttle_delay_secs=1) | |
| def create_client(self): | |
| return FakeModel() | |
| def request(self, batch, model, inference_args=None) -> Iterable[int]: | |
| if self._should_fail: | |
| self._should_fail = False | |
| raise Exception | |
| else: | |
| self._should_fail = True | |
| responses = [] | |
| for example in batch: | |
| responses.append(model.predict(example)) | |
| return responses | |
| def batch_elements_kwargs(self): | |
| return { | |
| 'min_batch_size': self._min_batch_size, | |
| 'max_batch_size': self._max_batch_size | |
| } | |
| class RunInferenceRemoteTest(unittest.TestCase): | |
| def test_normal_model_execution(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = [example + 1 for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference(FakeRemoteModelHandler()) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_repeated_requests_fail(self): | |
| test_pipeline = TestPipeline() | |
| with self.assertRaises(Exception): | |
| _ = ( | |
| test_pipeline | |
| | beam.Create([1, 2, 3, 4]) | |
| | base.RunInference(FakeAlwaysFailsRemoteModelHandler())) | |
| test_pipeline.run() | |
| def test_works_on_retry(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = [example + 1 for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference(FakeFailsOnceRemoteModelHandler()) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| def test_exception_on_load_model_override(self): | |
| with self.assertRaises(Exception): | |
| class _(base.RemoteModelHandler[int, int, FakeModel]): | |
| def __init__(self, clock=None, retry_filter=_always_retry, **kwargs): | |
| self._fake_clock = clock | |
| self._min_batch_size = 1 | |
| self._max_batch_size = 1 | |
| self._env_vars = kwargs.get('env_vars', {}) | |
| super().__init__( | |
| namespace='FakeRemoteModelHandler', retry_filter=retry_filter) | |
| def load_model(self): | |
| return FakeModel() | |
| def request(self, batch, model, inference_args=None) -> Iterable[int]: | |
| responses = [] | |
| for example in batch: | |
| responses.append(model.predict(example)) | |
| return responses | |
| def test_exception_on_run_inference_override(self): | |
| with self.assertRaises(Exception): | |
| class _(base.RemoteModelHandler[int, int, FakeModel]): | |
| def __init__(self, clock=None, retry_filter=_always_retry, **kwargs): | |
| self._fake_clock = clock | |
| self._min_batch_size = 1 | |
| self._max_batch_size = 1 | |
| self._env_vars = kwargs.get('env_vars', {}) | |
| super().__init__( | |
| namespace='FakeRemoteModelHandler', retry_filter=retry_filter) | |
| def create_client(self): | |
| return FakeModel() | |
| def run_inference(self, | |
| batch, | |
| model, | |
| inference_args=None) -> Iterable[int]: | |
| responses = [] | |
| for example in batch: | |
| responses.append(model.predict(example)) | |
| return responses | |
| def test_run_inference_with_rate_limiter(self): | |
| class FakeRateLimiter(base.RateLimiter): | |
| def __init__(self): | |
| super().__init__(namespace='test_namespace') | |
| def allow(self, hits_added=1): | |
| self.requests_counter.inc() | |
| return True | |
| limiter = FakeRateLimiter() | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5] | |
| class ConcreteRemoteModelHandler(base.RemoteModelHandler): | |
| def create_client(self): | |
| return FakeModel() | |
| def request(self, batch, model, inference_args=None): | |
| return [model.predict(example) for example in batch] | |
| model_handler = ConcreteRemoteModelHandler( | |
| rate_limiter=limiter, namespace='test_namespace') | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference(model_handler) | |
| expected = [2, 6] | |
| assert_that(actual, equal_to(expected)) | |
| result = pipeline.run() | |
| result.wait_until_finish() | |
| metrics_filter = MetricsFilter().with_name( | |
| 'RatelimitRequestsTotal').with_namespace('test_namespace') | |
| metrics = result.metrics().query(metrics_filter) | |
| self.assertGreaterEqual(metrics['counters'][0].committed, 0) | |
| def test_run_inference_with_rate_limiter_exceeded(self): | |
| class FakeRateLimiter(base.RateLimiter): | |
| def __init__(self): | |
| super().__init__(namespace='test_namespace') | |
| def allow(self, hits_added=1): | |
| return False | |
| class ConcreteRemoteModelHandler(base.RemoteModelHandler): | |
| def create_client(self): | |
| return FakeModel() | |
| def request(self, batch, model, inference_args=None): | |
| return [model.predict(example) for example in batch] | |
| model_handler = ConcreteRemoteModelHandler( | |
| rate_limiter=FakeRateLimiter(), | |
| namespace='test_namespace', | |
| num_retries=0) | |
| with self.assertRaises(base.RateLimitExceeded): | |
| model_handler.run_inference([1], FakeModel()) | |
| class FakeModelHandlerForSizing(base.ModelHandler[int, int, FakeModel]): | |
| """A ModelHandler used to test element sizing behavior.""" | |
| def __init__( | |
| self, | |
| max_batch_size: int = 10, | |
| max_batch_weight: Optional[int] = None, | |
| element_size_fn=None): | |
| super().__init__( | |
| max_batch_size=max_batch_size, | |
| max_batch_weight=max_batch_weight, | |
| element_size_fn=element_size_fn) | |
| def load_model(self) -> FakeModel: | |
| return FakeModel() | |
| def run_inference(self, batch, model, inference_args=None): | |
| return [model.predict(x) for x in batch] | |
| class RunInferenceSizeTest(unittest.TestCase): | |
| """Tests for ModelHandler.batch_elements_kwargs with element_size_fn.""" | |
| def test_kwargs_are_passed_correctly(self): | |
| """Adds element_size_fn without clobbering existing kwargs.""" | |
| def size_fn(x): | |
| return 10 | |
| sized_handler = FakeModelHandlerForSizing( | |
| max_batch_size=20, max_batch_weight=100, element_size_fn=size_fn) | |
| kwargs = sized_handler.batch_elements_kwargs() | |
| self.assertEqual(kwargs['max_batch_size'], 20) | |
| self.assertEqual(kwargs['max_batch_weight'], 100) | |
| self.assertIn('element_size_fn', kwargs) | |
| self.assertEqual(kwargs['element_size_fn'](1), 10) | |
| def test_sizing_with_edge_cases(self): | |
| """Allows extreme values from element_size_fn.""" | |
| zero_size_fn = lambda x: 0 | |
| sized_handler = FakeModelHandlerForSizing( | |
| max_batch_size=1, element_size_fn=zero_size_fn) | |
| kwargs = sized_handler.batch_elements_kwargs() | |
| self.assertEqual(kwargs['element_size_fn'](999), 0) | |
| large_size_fn = lambda x: 1000000 | |
| sized_handler = FakeModelHandlerForSizing( | |
| max_batch_size=1, element_size_fn=large_size_fn) | |
| kwargs = sized_handler.batch_elements_kwargs() | |
| self.assertEqual(kwargs['element_size_fn'](1), 1000000) | |
| class FakeModelHandlerForBatching(base.ModelHandler[int, int, FakeModel]): | |
| """A ModelHandler used to test batching behavior via base class __init__.""" | |
| def __init__(self, **kwargs): | |
| super().__init__(**kwargs) | |
| def load_model(self) -> FakeModel: | |
| return FakeModel() | |
| def run_inference(self, batch, model, inference_args=None): | |
| return [model.predict(x) for x in batch] | |
| class ModelHandlerBatchingArgsTest(unittest.TestCase): | |
| """Tests for ModelHandler.__init__ batching parameters.""" | |
| def test_batch_elements_kwargs_all_args(self): | |
| """All batching args passed to __init__ are in batch_elements_kwargs.""" | |
| def size_fn(x): | |
| return 10 | |
| handler = FakeModelHandlerForBatching( | |
| min_batch_size=5, | |
| max_batch_size=20, | |
| max_batch_duration_secs=30, | |
| max_batch_weight=100, | |
| element_size_fn=size_fn) | |
| kwargs = handler.batch_elements_kwargs() | |
| self.assertEqual(kwargs['min_batch_size'], 5) | |
| self.assertEqual(kwargs['max_batch_size'], 20) | |
| self.assertEqual(kwargs['max_batch_duration_secs'], 30) | |
| self.assertEqual(kwargs['max_batch_weight'], 100) | |
| self.assertIn('element_size_fn', kwargs) | |
| self.assertEqual(kwargs['element_size_fn'](1), 10) | |
| def test_batch_elements_kwargs_partial_args(self): | |
| """Only provided batching args are included in kwargs.""" | |
| handler = FakeModelHandlerForBatching(max_batch_size=50) | |
| kwargs = handler.batch_elements_kwargs() | |
| self.assertEqual(kwargs, {'max_batch_size': 50}) | |
| def test_batch_elements_kwargs_empty_when_no_args(self): | |
| """No batching kwargs when none are provided.""" | |
| handler = FakeModelHandlerForBatching() | |
| kwargs = handler.batch_elements_kwargs() | |
| self.assertEqual(kwargs, {}) | |
| def test_large_model_sets_share_across_processes(self): | |
| """Setting large_model=True enables share_model_across_processes.""" | |
| handler = FakeModelHandlerForBatching(large_model=True) | |
| self.assertTrue(handler.share_model_across_processes()) | |
| def test_model_copies_sets_share_across_processes(self): | |
| """Setting model_copies enables share_model_across_processes.""" | |
| handler = FakeModelHandlerForBatching(model_copies=2) | |
| self.assertTrue(handler.share_model_across_processes()) | |
| self.assertEqual(handler.model_copies(), 2) | |
| def test_default_share_across_processes_is_false(self): | |
| """Default share_model_across_processes is False.""" | |
| handler = FakeModelHandlerForBatching() | |
| self.assertFalse(handler.share_model_across_processes()) | |
| def test_default_model_copies_is_one(self): | |
| """Default model_copies is 1.""" | |
| handler = FakeModelHandlerForBatching() | |
| self.assertEqual(handler.model_copies(), 1) | |
| def test_env_vars_from_kwargs(self): | |
| """Environment variables can be passed via kwargs.""" | |
| handler = FakeModelHandlerForBatching(env_vars={'MY_VAR': 'value'}) | |
| self.assertEqual(handler._env_vars, {'MY_VAR': 'value'}) | |
| def test_min_batch_size_only(self): | |
| """min_batch_size can be passed alone.""" | |
| handler = FakeModelHandlerForBatching(min_batch_size=10) | |
| kwargs = handler.batch_elements_kwargs() | |
| self.assertEqual(kwargs, {'min_batch_size': 10}) | |
| def test_max_batch_duration_secs_only(self): | |
| """max_batch_duration_secs can be passed alone.""" | |
| handler = FakeModelHandlerForBatching(max_batch_duration_secs=60) | |
| kwargs = handler.batch_elements_kwargs() | |
| self.assertEqual(kwargs, {'max_batch_duration_secs': 60}) | |
| def test_batch_length_fn_and_batch_bucket_boundaries(self): | |
| """batch_length_fn and batch_bucket_boundaries passed through to kwargs.""" | |
| handler = FakeModelHandlerForBatching( | |
| batch_length_fn=len, batch_bucket_boundaries=[16, 32, 64]) | |
| kwargs = handler.batch_elements_kwargs() | |
| self.assertIs(kwargs['length_fn'], len) | |
| self.assertEqual(kwargs['bucket_boundaries'], [16, 32, 64]) | |
| def test_batch_length_fn_only(self): | |
| """batch_length_fn alone is passed through without bucket_boundaries.""" | |
| handler = FakeModelHandlerForBatching(batch_length_fn=len) | |
| kwargs = handler.batch_elements_kwargs() | |
| self.assertIs(kwargs['length_fn'], len) | |
| self.assertNotIn('bucket_boundaries', kwargs) | |
| def test_batch_bucket_boundaries_without_batch_length_fn(self): | |
| """Passing batch_bucket_boundaries without batch_length_fn should fail in | |
| BatchElements. | |
| Note: ModelHandler.__init__ doesn't validate this; the error is raised | |
| by BatchElements when batch_elements_kwargs are used.""" | |
| handler = FakeModelHandlerForBatching(batch_bucket_boundaries=[10, 20]) | |
| kwargs = handler.batch_elements_kwargs() | |
| # The kwargs are stored, but BatchElements will reject them | |
| self.assertEqual(kwargs['bucket_boundaries'], [10, 20]) | |
| self.assertNotIn('length_fn', kwargs) | |
| def test_batching_kwargs_none_values_omitted(self): | |
| """None values for batch_length_fn and batch_bucket_boundaries are not in | |
| kwargs.""" | |
| handler = FakeModelHandlerForBatching( | |
| min_batch_size=5, batch_length_fn=None, batch_bucket_boundaries=None) | |
| kwargs = handler.batch_elements_kwargs() | |
| self.assertNotIn('length_fn', kwargs) | |
| self.assertNotIn('bucket_boundaries', kwargs) | |
| self.assertEqual(kwargs['min_batch_size'], 5) | |
| class PaddingReportingStringModelHandler(base.ModelHandler[str, str, | |
| FakeModel]): | |
| """Reports each element with the max length of the batch it ran in.""" | |
| def load_model(self): | |
| return FakeModel() | |
| def run_inference(self, batch, model, inference_args=None): | |
| max_len = max(len(s) for s in batch) | |
| return [f'{s}:{max_len}' for s in batch] | |
| class RunInferenceLengthAwareBatchingTest(unittest.TestCase): | |
| """End-to-end tests for PR2 length-aware batching in RunInference.""" | |
| def test_run_inference_with_length_aware_batch_elements(self): | |
| handler = PaddingReportingStringModelHandler( | |
| min_batch_size=2, | |
| max_batch_size=2, | |
| max_batch_duration_secs=60, | |
| batch_length_fn=len, | |
| batch_bucket_boundaries=[5]) | |
| examples = ['a', 'cccccc', 'bb', 'ddddddd'] | |
| with TestPipeline('FnApiRunner') as p: | |
| results = ( | |
| p | |
| | beam.Create(examples, reshuffle=False) | |
| | base.RunInference(handler)) | |
| assert_that(results, equal_to(['a:2', 'bb:2', 'cccccc:7', 'ddddddd:7'])) | |
| class SimpleFakeModelHandler(base.ModelHandler[int, int, FakeModel]): | |
| def load_model(self): | |
| return FakeModel() | |
| def run_inference( | |
| self, | |
| batch: Sequence[int], | |
| model: FakeModel, | |
| inference_args=None) -> Iterable[int]: | |
| for example in batch: | |
| yield model.predict(example) | |
| def try_import_model_manager(): | |
| try: | |
| # pylint: disable=unused-import | |
| from apache_beam.ml.inference.model_manager import ModelManager | |
| return True | |
| except ImportError: | |
| return False | |
| class ModelManagerTest(unittest.TestCase): | |
| """Tests for RunInference with Model Manager integration.""" | |
| def tearDown(self): | |
| for p in multiprocessing.active_children(): | |
| p.terminate() | |
| p.join() | |
| @unittest.skipIf( | |
| not try_import_model_manager(), 'Model Manager not available') | |
| def test_run_inference_impl_with_model_manager(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = [example + 1 for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| SimpleFakeModelHandler(), use_model_manager=True) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| @unittest.skipIf( | |
| not try_import_model_manager(), 'Model Manager not available') | |
| def test_run_inference_impl_with_model_manager_args(self): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| expected = [example + 1 for example in examples] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| SimpleFakeModelHandler(), | |
| use_model_manager=True, | |
| model_manager_args={ | |
| 'slack_percentage': 0.2, | |
| 'poll_interval': 1.0, | |
| 'peak_window_seconds': 10.0, | |
| 'min_data_points': 10, | |
| 'smoothing_factor': 0.5 | |
| }) | |
| assert_that(actual, equal_to(expected), label='assert:inferences') | |
| @unittest.skipIf( | |
| not try_import_model_manager(), 'Model Manager not available') | |
| def test_run_inference_impl_with_model_manager_oom(self): | |
| class OOMFakeModelHandler(SimpleFakeModelHandler): | |
| def run_inference( | |
| self, | |
| batch: Sequence[int], | |
| model: FakeModel, | |
| inference_args=None) -> Iterable[int]: | |
| if random.random() < 0.8: | |
| raise MemoryError("Simulated OOM") | |
| for example in batch: | |
| yield model.predict(example) | |
| def batch_elements_kwargs(self): | |
| return {'min_batch_size': 1, 'max_batch_size': 1} | |
| with self.assertRaises(Exception): | |
| with TestPipeline() as pipeline: | |
| examples = [1, 5, 3, 10] | |
| pcoll = pipeline | 'start' >> beam.Create(examples) | |
| actual = pcoll | base.RunInference( | |
| OOMFakeModelHandler(), use_model_manager=True) | |
| assert_that(actual, equal_to([2, 6, 4, 11]), label='assert:inferences') | |
| if __name__ == '__main__': | |
| unittest.main() |