-
Notifications
You must be signed in to change notification settings - Fork 4.6k
[Interactive Beam] Fix caching deadlock, wait race conditions, and stale graph in notebooks #39161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
9fb7281
fdb3998
894ff3f
dc95f16
3e9644f
80d61ad
d10c74b
ac9fda1
64ae73f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,6 +86,7 @@ def __init__( | |
| bar_style='info', | ||
| ) if IS_IPYTHON else None) | ||
| self._cancel_requested = False | ||
| self._completed_event = threading.Event() | ||
|
|
||
| if IS_IPYTHON: | ||
| self._cancel_button.on_click(self._cancel_clicked) | ||
|
|
@@ -151,27 +152,33 @@ def exception(self, timeout=None): | |
| except TimeoutError: | ||
| return None | ||
|
|
||
| def _on_done(self, future: Future): | ||
| self._env.unmark_pcollection_computing(self._pcolls) | ||
| self._recording_manager._async_computations.pop(self._display_id, None) | ||
|
|
||
| if future.cancelled(): | ||
| self.update_display('Computation Cancelled.', 1.0) | ||
| return | ||
| def wait_for_completion(self): | ||
| self._completed_event.wait() | ||
|
ian-Liaozy marked this conversation as resolved.
Outdated
|
||
|
|
||
| exc = future.exception() | ||
| if exc: | ||
| self.update_display(f'Error: {exc}', 1.0) | ||
| _LOGGER.error('Asynchronous computation failed: %s', exc, exc_info=exc) | ||
| else: | ||
| self.update_display('Computation Finished Successfully.', 1.0) | ||
| res = future.result() | ||
| if res and res.state == PipelineState.DONE: | ||
| self._env.mark_pcollection_computed(self._pcolls) | ||
| def _on_done(self, future: Future): | ||
| try: | ||
| if future.cancelled(): | ||
| self.update_display('Computation Cancelled.', 1.0) | ||
| return | ||
|
|
||
| exc = future.exception() | ||
| if exc: | ||
| self.update_display(f'Error: {exc}', 1.0) | ||
| _LOGGER.error('Asynchronous computation failed: %s', exc, exc_info=exc) | ||
| else: | ||
| _LOGGER.warning( | ||
| 'Async computation finished but state is not DONE: %s', | ||
| res.state if res else 'Unknown') | ||
| self.update_display('Computation Finished Successfully.', 1.0) | ||
| res = future.result() | ||
| if res and res.state == PipelineState.DONE: | ||
| self._env.mark_pcollection_computed(self._pcolls) | ||
| else: | ||
| _LOGGER.warning( | ||
| 'Async computation finished but state is not DONE: %s', | ||
| res.state if res else 'Unknown') | ||
| finally: | ||
| self._env.unmark_pcollection_computing(self._pcolls) | ||
| with self._recording_manager._lock: | ||
| self._recording_manager._async_computations.pop(self._display_id, None) | ||
| self._completed_event.set() | ||
|
|
||
| def cancel(self): | ||
| if self._future.done(): | ||
|
|
@@ -438,7 +445,7 @@ def __init__( | |
| self._executor = ThreadPoolExecutor(max_workers=os.cpu_count()) | ||
| self._env = ie.current_env() | ||
| self._async_computations: dict[str, AsyncComputationResult] = {} | ||
| self._pipeline_graph = None | ||
| self._lock = threading.Lock() | ||
|
|
||
| def _execute_pipeline_fragment( | ||
| self, | ||
|
|
@@ -694,7 +701,8 @@ def compute_async( | |
| future = Future() | ||
| async_result = AsyncComputationResult( | ||
| future, pcolls_to_compute, self.user_pipeline, self) | ||
| self._async_computations[async_result._display_id] = async_result | ||
| with self._lock: | ||
| self._async_computations[async_result._display_id] = async_result | ||
| self._env.mark_pcollection_computing(pcolls_to_compute) | ||
|
|
||
| def task(): | ||
|
|
@@ -710,19 +718,13 @@ def task(): | |
| return async_result | ||
|
|
||
| def _get_pipeline_graph(self): | ||
| """Lazily initializes and returns the PipelineGraph.""" | ||
| if self._pipeline_graph is None: | ||
| try: | ||
| # Try to create the graph. | ||
| self._pipeline_graph = PipelineGraph(self.user_pipeline) | ||
| except (ImportError, NameError, AttributeError): | ||
| # If pydot is missing, PipelineGraph() might crash. | ||
| _LOGGER.warning( | ||
| "Could not create PipelineGraph (pydot missing?). " \ | ||
| "Async features disabled." | ||
| ) | ||
| self._pipeline_graph = None | ||
| return self._pipeline_graph | ||
| """Initializes and returns the PipelineGraph.""" | ||
| try: | ||
| # Try to create the graph. | ||
| return PipelineGraph(self.user_pipeline) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand we want to make sure the graph is up-to-date, but I think building pipeline graphs could be slow with pydot, and that's why we had a cache here previously. Have you tested the new implementation on different graph sizes? I am worried about a performance regression here without a cache.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. I changed the implementation to enable caching for pipeline graph, please verify if this looks good to you, thanks Shunping! |
||
| except (ImportError, NameError, AttributeError): | ||
| # If pydot is missing, PipelineGraph() might crash. | ||
| return None | ||
|
ian-Liaozy marked this conversation as resolved.
Outdated
|
||
|
|
||
| def _get_pcoll_id_map(self): | ||
| """Creates a map from PCollection object to its ID in the proto.""" | ||
|
|
@@ -800,11 +802,19 @@ def _wait_for_dependencies( | |
| """Waits for any dependencies of the given | ||
| PCollections that are currently being computed.""" | ||
| dependencies = self._get_all_dependencies(pcolls) | ||
| if async_result is None: | ||
| pcolls_to_check = dependencies.union(pcolls) | ||
| else: | ||
| pcolls_to_check = dependencies | ||
| computing_deps: dict[beam.pvalue.PCollection, AsyncComputationResult] = {} | ||
|
|
||
| for dep in dependencies: | ||
| if self._env.is_pcollection_computing(dep): | ||
| for comp in self._async_computations.values(): | ||
| with self._lock: | ||
| async_computations_copy = list(self._async_computations.values()) | ||
|
|
||
| for dep in pcolls_to_check: | ||
| is_computing = self._env.is_pcollection_computing(dep) | ||
| if is_computing: | ||
|
ian-Liaozy marked this conversation as resolved.
|
||
| for comp in async_computations_copy: | ||
| if dep in comp._pcolls: | ||
| computing_deps[dep] = comp | ||
| break | ||
|
|
@@ -820,17 +830,16 @@ def _wait_for_dependencies( | |
| len(computing_deps), | ||
| computing_deps.keys()) | ||
|
|
||
| futures_to_wait = list( | ||
| set(comp._future for comp in computing_deps.values())) | ||
| results_to_wait = list(set(comp for comp in computing_deps.values())) | ||
|
|
||
| try: | ||
| for i, future in enumerate(futures_to_wait): | ||
| for i, comp in enumerate(results_to_wait): | ||
| if async_result: | ||
| async_result.update_display( | ||
| f'Waiting for dependency {i + 1}/{len(futures_to_wait)}...', | ||
| progress=0.05 + 0.05 * (i / len(futures_to_wait)), | ||
| f'Waiting for dependency {i + 1}/{len(results_to_wait)}...', | ||
| progress=0.05 + 0.05 * (i / len(results_to_wait)), | ||
| ) | ||
| future.result() | ||
| comp.wait_for_completion() | ||
|
claudevdm marked this conversation as resolved.
|
||
| if async_result: | ||
| async_result.update_display('Dependencies finished.', progress=0.1) | ||
| _LOGGER.info('Dependencies finished successfully.') | ||
|
|
@@ -891,23 +900,32 @@ def record( | |
| 'Cannot record because a dependency failed to compute' | ||
| ' asynchronously.') | ||
|
|
||
| self._clear() | ||
|
|
||
| merged_options = pipeline_options.PipelineOptions( | ||
| **{ | ||
| **self.user_pipeline.options.get_all_options( | ||
| drop_default=True, retain_unknown_options=True), | ||
| **options.get_all_options( | ||
| drop_default=True, retain_unknown_options=True) | ||
| }) if options else self.user_pipeline.options | ||
|
|
||
| cache_path = ie.current_env().options.cache_root | ||
| is_remote_run = cache_path and ie.current_env( | ||
| ).options.cache_root.startswith('gs://') | ||
| pf.PipelineFragment( | ||
| list(uncomputed_pcolls), merged_options, | ||
| runner=runner).run(blocking=is_remote_run) | ||
| result = ie.current_env().pipeline_result(self.user_pipeline) | ||
| # Recalculate uncomputed PCollections because some may have finished computing during the wait | ||
| computed_pcolls = set( | ||
| pcoll for pcoll in pcolls | ||
| if pcoll in ie.current_env().computed_pcollections) | ||
| uncomputed_pcolls = set(pcolls).difference(computed_pcolls) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
ian-Liaozy marked this conversation as resolved.
Outdated
|
||
|
|
||
| if uncomputed_pcolls: | ||
| self._clear() | ||
|
|
||
| merged_options = pipeline_options.PipelineOptions( | ||
| **{ | ||
| **self.user_pipeline.options.get_all_options( | ||
| drop_default=True, retain_unknown_options=True), | ||
| **options.get_all_options( | ||
| drop_default=True, retain_unknown_options=True) | ||
| }) if options else self.user_pipeline.options | ||
|
|
||
| cache_path = ie.current_env().options.cache_root | ||
| is_remote_run = cache_path and ie.current_env( | ||
| ).options.cache_root.startswith('gs://') | ||
| pf.PipelineFragment( | ||
| list(uncomputed_pcolls), merged_options, | ||
| runner=runner).run(blocking=is_remote_run) | ||
| result = ie.current_env().pipeline_result(self.user_pipeline) | ||
| else: | ||
| result = ie.current_env().pipeline_result(self.user_pipeline) | ||
| else: | ||
| result = None | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.