Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions sdks/python/apache_beam/runners/dask/dask_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ def _add_argparse_args(cls, parser: argparse.ArgumentParser) -> None:
default=None,
help='The length of each `dask.Bag` partition. When unspecified, '
'an educated guess is made.')
parser.add_argument(
'--dask_lazy_side_inputs',
dest='lazy_side_inputs',
action='store_true',
help='Opt-in to lazy side inputs evaluation to reduce memory usage, '
'which may cause performance issues or deadlocks depending on the '
'scheduler. Fallbacks to `list()` when false.')


@dataclasses.dataclass
Expand Down Expand Up @@ -166,7 +173,8 @@ def metrics(self):
class DaskRunner(BundleBasedDirectRunner):
"""Executes a pipeline on a Dask distributed client."""
@staticmethod
def to_dask_bag_visitor(bag_kwargs=None) -> PipelineVisitor:
def to_dask_bag_visitor(
bag_kwargs=None, lazy_side_inputs: bool = False) -> PipelineVisitor:
from dask import bag as db

if bag_kwargs is None:
Expand Down Expand Up @@ -210,7 +218,10 @@ def visit_transform(self, transform_node: AppliedPTransform) -> None:
SideInputMap(
type(si),
si._view_options(),
DaskBagWindowedIterator(si_asbag, si._window_mapping_fn)))
DaskBagWindowedIterator(
si_asbag,
si._window_mapping_fn,
lazy_side_inputs=lazy_side_inputs)))

op_kws["side_inputs"] = bag_side_inputs

Expand Down Expand Up @@ -242,7 +253,10 @@ def run_pipeline(self, pipeline, options):

pipeline.replace_all(dask_overrides())

dask_visitor = self.to_dask_bag_visitor(bag_kwargs)
lazy_side_inputs = getattr(
options.view_as(DaskOptions), 'lazy_side_inputs', False)

dask_visitor = self.to_dask_bag_visitor(bag_kwargs, lazy_side_inputs)
pipeline.visit(dask_visitor)
# The dictionary in this visitor keeps a mapping of every Beam
# PTransform to the equivalent Bag operation. This is highly
Expand Down
13 changes: 13 additions & 0 deletions sdks/python/apache_beam/runners/dask/dask_runner_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,19 @@ def mult_by(x, y):
| beam.Map(mult_by, beam.pvalue.AsSingleton(side)))
assert_that(pcoll, equal_to([3]))

def test_map_with_lazy_positional_side_input(self):
def mult_by(x, y):
return x * y

options = PipelineOptions(['--dask_lazy_side_inputs'])
with test_pipeline.TestPipeline(runner=DaskRunner(), options=options) as p:
side = p | "side" >> beam.Create([3])
pcoll = (
p
| "main" >> beam.Create([1])
| beam.Map(mult_by, beam.pvalue.AsSingleton(side)))
assert_that(pcoll, equal_to([3]))

def test_map_with_keyword_side_input(self):
def mult_by(x, y):
return x * y
Expand Down
21 changes: 16 additions & 5 deletions sdks/python/apache_beam/runners/dask/transform_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,24 @@ class DaskBagWindowedIterator:

bag: db.Bag
window_fn: WindowFn
lazy_side_inputs: bool = False

def __iter__(self):
# FIXME(cisaacstern): list() is likely inefficient, since it presumably
# materializes the full result before iterating over it. doing this for
# now as a proof-of-concept. can we can generate results incrementally?
for result in list(self.bag):
yield get_windowed_value(result, self.window_fn)
if not self.lazy_side_inputs:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you preserve the original comment for context?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feel free to expand on that as appropriate.

for result in list(self.bag):
yield get_windowed_value(result, self.window_fn)
else:
try:
from dask.distributed import worker_client
with worker_client() as client:
for partition in self.bag.to_delayed():
# explicitly submit and wait to avoid deadlock
for result in client.compute(partition).result():
yield get_windowed_value(result, self.window_fn)
except (ImportError, ValueError):
for partition in self.bag.to_delayed():

@tvalentyn tvalentyn Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you anticipate a particular scenario when we hit the except (ImportError, ValueError): branch or this is defensive coding + fallback to original implementation? In the latter case consider removing since this flag is opt-in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for transient errors , runners should retry. for non-transient errors, a user can remove the flag.

for result in partition.compute():
yield get_windowed_value(result, self.window_fn)


@dataclasses.dataclass
Expand Down
Loading