This is an automated email from the ASF dual-hosted git repository.
tvalentyn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 65ea034a6f1 Fix ModelManager tag mismatch on release after model
update (#40469) (#40473)
65ea034a6f1 is described below
commit 65ea034a6f1cf485c9dae2e5a0774cf3f2d2aa0e
Author: RuiLong J. <[email protected]>
AuthorDate: Wed Oct 14 14:54:32 2026 -0700
Fix ModelManager tag mismatch on release after model update (#40469)
(#40473)
* Fix ModelManager tag mismatch on release after model update (#40469)
* Fix _SharedModelWrapper.force_reset to call _force_reset()
* Add TODO(#40539) in _SharedModelWrapper.all_models()
* Isolate ModelManagerTest from xdist and expose force_reset on ModelManager
* Rename ModelManager._force_reset to force_reset
---
sdks/python/apache_beam/ml/inference/base.py | 7 +-
sdks/python/apache_beam/ml/inference/base_test.py | 98 ++++++++++++++++++++++
.../apache_beam/ml/inference/model_manager.py | 2 +-
.../apache_beam/ml/inference/model_manager_test.py | 6 +-
4 files changed, 107 insertions(+), 6 deletions(-)
diff --git a/sdks/python/apache_beam/ml/inference/base.py
b/sdks/python/apache_beam/ml/inference/base.py
index 8c4b10f8573..7a19eba3511 100644
--- a/sdks/python/apache_beam/ml/inference/base.py
+++ b/sdks/python/apache_beam/ml/inference/base.py
@@ -1950,7 +1950,10 @@ class _SharedModelWrapper():
def all_models(self):
if self.use_model_manager:
- return self.models.all_models()[self.model_tag]
+ # TODO(https://github.com/apache/beam/issues/40539): Support multi-model
+ # KeyedModelHandler with ModelManager (e.g. resolving proxies for
+ # update_model_paths).
+ return self.models.all_models(self.model_tag)
return self.models
def force_reset(self):
@@ -2106,7 +2109,7 @@ class _RunInferenceDoFn(beam.DoFn, Generic[ExampleT,
PredictionT]):
finally:
# Always release the model so that it can be reloaded.
if self.use_model_manager:
- self._model.release_model(self._model_tag, unique_tag)
+ self._model.release_model(self._cur_tag, unique_tag)
except BaseException as e:
if self._metrics_collector:
self._metrics_collector.failed_batches_counter.inc()
diff --git a/sdks/python/apache_beam/ml/inference/base_test.py
b/sdks/python/apache_beam/ml/inference/base_test.py
index 8cf408db76f..a8ffc81e5ac 100644
--- a/sdks/python/apache_beam/ml/inference/base_test.py
+++ b/sdks/python/apache_beam/ml/inference/base_test.py
@@ -2408,6 +2408,24 @@ class SimpleFakeModelHandler(base.ModelHandler[int, int,
FakeModel]):
yield model.predict(example)
+class IncrementingFakeModelHandler(base.ModelHandler[int,
+ int,
+ FakeIncrementingModel]):
+ def load_model(self):
+ return FakeIncrementingModel()
+
+ def run_inference(
+ self,
+ batch: Sequence[int],
+ model: FakeIncrementingModel,
+ inference_args=None) -> Iterable[int]:
+ for _ in batch:
+ yield model.predict(0)
+
+ def batch_elements_kwargs(self):
+ return {'min_batch_size': 2, 'max_batch_size': 2}
+
+
def try_import_model_manager():
try:
# pylint: disable=unused-import
@@ -2417,9 +2435,15 @@ def try_import_model_manager():
return False
[email protected]_xdist
class ModelManagerTest(unittest.TestCase):
"""Tests for RunInference with Model Manager integration."""
def tearDown(self):
+ if multi_process_shared._process_level_singleton_manager.has_singleton(
+ 'model_manager'):
+ multi_process_shared.MultiProcessShared(
+ lambda: None, tag='model_manager',
+ always_proxy=True).acquire().force_reset()
for p in multiprocessing.active_children():
p.terminate()
p.join()
@@ -2479,6 +2503,80 @@ class ModelManagerTest(unittest.TestCase):
OOMFakeModelHandler(), use_model_manager=True)
assert_that(actual, equal_to([2, 6, 4, 11]), label='assert:inferences')
+ @unittest.skipIf(
+ not try_import_model_manager(), 'Model Manager not available')
+ def test_shared_model_wrapper_all_models_with_model_manager(self):
+ from apache_beam.ml.inference.model_manager import ModelManager
+ mm = ModelManager()
+ try:
+ tag = 'test_tag'
+ instance = mm.acquire_model(tag, lambda: 'fake_model_instance')
+ mm.release_model(tag, instance)
+ wrapper = base._SharedModelWrapper(mm, tag, lambda:
'fake_model_instance')
+ self.assertEqual(wrapper.all_models(), ['fake_model_instance'])
+ wrapper.force_reset()
+ self.assertEqual(wrapper.all_models(), [])
+ finally:
+ mm.shutdown()
+
+ @unittest.skipIf(
+ not try_import_model_manager(), 'Model Manager not available')
+ def
test_run_inference_with_model_manager_and_model_metadata_pcoll_reuses_model(
+ self):
+ with TestPipeline('FnApiRunner') as pipeline:
+ model_metadata = pipeline | 'ModelMetadata' >> beam.Create(
+ [base.ModelMetadata(model_id='model_v2', model_name='v2')])
+ actual = (
+ pipeline
+ | 'start' >> beam.Create([0, 0, 0, 0])
+ | base.RunInference(
+ IncrementingFakeModelHandler(),
+ use_model_manager=True,
+ model_metadata_pcoll=model_metadata))
+ # Since the model is reused across the 2 batches (4 elements total),
+ # FakeIncrementingModel increments from 1 to 4 instead of resetting to 1
+ # on the second batch.
+ assert_that(actual, equal_to([1, 2, 3, 4]), label='assert:inferences')
+
+ @unittest.skipIf(
+ not try_import_model_manager(), 'Model Manager not available')
+ def test_run_inference_dofn_model_update_acquires_and_releases_matching_tag(
+ self):
+ dofn = base._RunInferenceDoFn(
+ model_handler=IncrementingFakeModelHandler(),
+ clock=FakeClock(),
+ metrics_namespace='test',
+ load_model_at_runtime=False,
+ model_tag='initial_tag',
+ use_model_manager=True)
+ dofn.setup()
+ try:
+ # 1. Before any update, batches use 'initial_tag' and reuse the same
model
+ self.assertEqual(dofn.process([0, 0], None, None), [1, 2])
+ self.assertEqual(dofn.process([0, 0], None, None), [3, 4])
+ self.assertEqual(
+ len(dofn._model.all_models()), 1, 'Expected 1 model for initial_tag')
+
+ # 2. Update to 'model_v1' via side input metadata
+ meta_v1 = base.ModelMetadata(model_id='model_v1', model_name='v1')
+ self.assertEqual(dofn.process([0, 0], None, meta_v1), [1, 2])
+ # Second batch on 'model_v1' reuses the 'model_v1' instance (3, 4)
+ self.assertEqual(dofn.process([0, 0], None, meta_v1), [3, 4])
+ self.assertEqual(dofn._cur_tag, 'model_v1')
+ self.assertEqual(
+ len(dofn._model.all_models()), 1, 'Expected 1 model for model_v1')
+
+ # 3. Update to 'model_v2' via side input metadata
+ meta_v2 = base.ModelMetadata(model_id='model_v2', model_name='v2')
+ self.assertEqual(dofn.process([0, 0], None, meta_v2), [1, 2])
+ # Second batch on 'model_v2' reuses the 'model_v2' instance (3, 4)
+ self.assertEqual(dofn.process([0, 0], None, meta_v2), [3, 4])
+ self.assertEqual(dofn._cur_tag, 'model_v2')
+ self.assertEqual(
+ len(dofn._model.all_models()), 1, 'Expected 1 model for model_v2')
+ finally:
+ dofn._model.force_reset()
+
if __name__ == '__main__':
unittest.main()
diff --git a/sdks/python/apache_beam/ml/inference/model_manager.py
b/sdks/python/apache_beam/ml/inference/model_manager.py
index 9c0d90d7a02..69db694cc97 100644
--- a/sdks/python/apache_beam/ml/inference/model_manager.py
+++ b/sdks/python/apache_beam/ml/inference/model_manager.py
@@ -795,7 +795,7 @@ class ModelManager:
self._monitor.refresh()
self._monitor.reset_peak()
- def _force_reset(self):
+ def force_reset(self):
logger.warning("Force Reset Triggered")
self._delete_all_models()
self._models = defaultdict(list)
diff --git a/sdks/python/apache_beam/ml/inference/model_manager_test.py
b/sdks/python/apache_beam/ml/inference/model_manager_test.py
index 370961eb61d..fc84f4ba859 100644
--- a/sdks/python/apache_beam/ml/inference/model_manager_test.py
+++ b/sdks/python/apache_beam/ml/inference/model_manager_test.py
@@ -148,7 +148,7 @@ class TestModelManager(unittest.TestCase):
self.manager.release_model(model_name, instance)
# Force delete all models
- self.manager._force_reset()
+ self.manager.force_reset()
# Verify that the MultiProcessShared instance is deleted
# and the counter is reseted
@@ -327,7 +327,7 @@ class TestModelManager(unittest.TestCase):
self.manager.release_model(model_name, instance)
instance = self.manager.acquire_model(model_name, dummy_loader)
except RuntimeError:
- self.manager._force_reset()
+ self.manager.force_reset()
self.assertTrue(len(self.manager._models[model_name]) == 0)
self.assertEqual(self.manager._total_active_jobs, 0)
self.assertEqual(self.manager._pending_reservations, 0.0)
@@ -397,7 +397,7 @@ class TestModelManager(unittest.TestCase):
# Max is still 2 from earlier in the test run
self.assertEqual(_get_count_dist_max(tag1), 2)
- self.manager._force_reset()
+ self.manager.force_reset()
self.assertEqual(_get_count_dist_max(tag1), 2)
def test_single_model_convergence_with_fluctuations(self):