Skip to content

Commit

Permalink
Merge pull request #3850 from voxel51/execution-options
Browse files Browse the repository at this point in the history
add ExecutionOptions to execute_operator()
  • Loading branch information
brimoor authored Dec 1, 2023
2 parents 6965f76 + db73ce1 commit d864afb
Show file tree
Hide file tree
Showing 8 changed files with 476 additions and 169 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
228 changes: 199 additions & 29 deletions docs/source/plugins/developing_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,12 @@ subsequent sections.
icon="/assets/icon.svg",
light_icon="/assets/icon-light.svg", # light theme only
dark_icon="/assets/icon-dark.svg", # dark theme only
# Whether the operator supports immediate and/or delegated execution
allow_immediate_execution=True/False, # default True
allow_delegated_execution=True/False, # default False
default_choice_to_delegated=True/False, # default False
resolve_execution_options_on_change=None,
)
def resolve_placement(self, ctx):
Expand Down Expand Up @@ -658,8 +664,8 @@ subsequent sections.
)
def resolve_input(self, ctx):
"""Implement this method if your operator can render a form to
collect user inputs.
"""Implement this method to collect user inputs as parameters
that are stored in `ctx.params`.
Returns:
a `types.Property` defining the form's components
Expand All @@ -680,13 +686,30 @@ subsequent sections.
return types.Property(inputs, view=types.View(label="Example operator"))
def resolve_delegation(self, ctx):
"""Implement this method if you want to programmatically determine
whether to delegate execution of this operation based on `ctx`.
"""Implement this method if you want to programmatically *force*
this operation to be delegated or executed immediately.
Returns:
whether the operation should be delegated (True), run
immediately (False), or None to defer to
`resolve_execution_options()` to specify the available options
"""
return len(ctx.view) > 1000 # delegate for larger views
def resolve_execution_options(self, ctx):
"""Implement this method if you want to dynamically configure the
execution options available to this operator based on the current
`ctx`.
Returns:
True/False
an `ExecutionOptions` instance
"""
return len(ctx.view) > 1000 # delegated for larger views
should_delegate = len(ctx.view) > 1000 # delegate for larger views
return foo.ExecutionOptions(
allow_immediate_execution=True,
allow_delegated_execution=True,
default_choice_to_delegated=should_delegate,
)
def execute(self, ctx):
"""Executes the actual operation based on the hydrated `ctx`.
Expand All @@ -711,6 +734,7 @@ subsequent sections.
for i, sample in enumerate(current_view, 1):
# do some computation
yield ctx.trigger("set_progress", {"progress": i / n})
yield ctx.trigger("reload_dataset")
return {"value": value, ...}
Expand All @@ -720,7 +744,7 @@ subsequent sections.
to the user.
Returns:
a Property defining the components of the output form
a `types.Property` defining the components of the output form
"""
outputs = types.Object()
Expand Down Expand Up @@ -785,6 +809,12 @@ execution:
icon="/assets/icon.svg",
light_icon="/assets/icon-light.svg", # light theme only
dark_icon="/assets/icon-dark.svg", # dark theme only
# Whether the operator supports immediate and/or delegated execution
allow_immediate_execution=True/False, # default True
allow_delegated_execution=True/False, # default False
default_choice_to_delegated=True/False, # default False
resolve_execution_options_on_change=None,
)
.. _operator-execution-context:
Expand All @@ -809,10 +839,15 @@ contains the following properties:
- `ctx.selected` - the list of currently selected samples in the App, if any
- `ctx.selected_labels` - the list of currently selected labels in the App,
if any
- `ctx.delegated` - whether delegated execution has been forced for the
operation
- `ctx.requesting_delegated_execution` - whether delegated execution has been
requested for the operation
- `ctx.delegation_target` - the orchestrator to which the operation should be
delegated, if applicable
- `ctx.secrets` - a dict of :ref:`secrets <operator-secrets>` for the plugin,
if any
- `ctx.results` - a dict containing the outputs of the
:meth:`execute() <fiftyone.operators.operator.Operator.execute>` method, if
- `ctx.results` - a dict containing the outputs of the `execute()` method, if
it has been called
- `ctx.hooks` **(JS only)** - the return value of the operator's `useHooks()`
method
Expand Down Expand Up @@ -940,26 +975,130 @@ workflow orchestrator like :ref:`Apache Airflow <delegated-operations-airflow>`
or run just :ref:`run locally <delegated-operations-local>` in a separate
process.

Operators can delegate any or all of its operations by implementing
.. note::

Even though delegated operations are run in a separate process or physical
location, they are provided with the same `ctx` that was hydrated by the
operator's :ref:`input form <operator-inputs>`.

Refer to :ref:`this section <delegated-operations>` for more information
about how delegated operations are executed.

There are a variety of options available for configuring whether a given
operation should be delegated or executed immediately.

.. _operator-delegation-configuration:

Delegation configuration
~~~~~~~~~~~~~~~~~~~~~~~~

You can provide the optional properties described below in the
:ref:`operator's config <operator-config>` to specify the available execution
modes for the operator:

.. code-block:: python
:linenos:
@property
def config(self):
return foo.OperatorConfig(
# Other parameters...
# Whether to allow immediate execution
allow_immediate_execution=True/False, # default True
# Whether to allow delegated execution
allow_delegated_execution=True/False, # default False
# Whether the default execution mode should be delegated, if both
# options are available
default_choice_to_delegated=True/False, # default False
# Whether to resolve execution options dynamically when the
# operator's inputs change. By default, this behavior will match
# the operator's ``dynamic`` setting
resolve_execution_options_on_change=True/False/None, # default None
)
When the operator's input form is rendered in the App, the `Execute|Schedule`
button at the bottom of the modal will contextually show whether the operation
will be executed immediately, scheduled for delegated execution, or allow the
user to choose between the supported options if there are multiple:

.. image:: /images/plugins/operators/operator-execute-button.png
:align: center

.. _operator-execution-options:

Execution options
~~~~~~~~~~~~~~~~~

Operators can implement
:meth:`resolve_execution_options() <fiftyone.operators.operator.Operator.resolve_execution_options>`
to dynamically configure the available execution options based on the current
execution context:

.. code-block:: python
:linenos:
# Option 1: recommend delegation for larger views
def resolve_execution_options(self, ctx):
should_delegate = len(ctx.view) > 1000
return foo.ExecutionOptions(
allow_immediate_execution=True,
allow_delegated_execution=True,
default_choice_to_delegated=should_delegate,
)
# Option 2: force delegation for larger views
def resolve_execution_options(self, ctx):
delegate = len(ctx.view) > 1000
return foo.ExecutionOptions(
allow_immediate_execution=not delegate,
allow_delegated_execution=delegate,
)
If implemented, this method will override any static execution parameters
included in the :ref:`operator's config <operator-config>` as described in the
previous section.

.. _operator-forced-delegation:

Forced delegation
~~~~~~~~~~~~~~~~~

Operators can implement
:meth:`resolve_delegation() <fiftyone.operators.operator.Operator.resolve_delegation>`
as shown below:
to force a particular operation to be delegated (by returning `True`) or
executed immediately (by returning `False`) based on the current execution
context.

For example, you could decide whether to delegate execution based on the size
of the current view:

.. code-block:: python
:linenos:
def resolve_delegation(self, ctx):
return len(ctx.view) > 1000 # delegate for larger views
# Force delegation for large views and immediate execution for small views
return len(ctx.view) > 1000
.. note::

As demonstrated above, you can use the
:ref:`execution context <operator-execution-context>` to conditionally decide
whether a given operation should be delegated. For example, you could simply
ask the user:
If :meth:`resolve_delegation() <fiftyone.operators.operator.Operator.resolve_delegation>`
is not implemented or returns `None`, then the choice of execution mode is
deferred to
:meth:`resolve_execution_options() <fiftyone.operators.operator.Operator.resolve_execution_options>`
to specify the available execution options as described in the previous
section.

Alternatively, you could simply ask the user to decide:

.. code-block:: python
:linenos:
def resolve_input(self, ctx):
delegate = ctx.params.get("delegate", False)
delegate = ctx.params.get("delegate", None)
if delegate:
description = "Uncheck this box to execute the operation immediately"
Expand All @@ -968,8 +1107,6 @@ ask the user:
inputs.bool(
"delegate",
default=False,
required=True,
label="Delegate execution?",
description=description,
view=types.CheckboxView(),
Expand All @@ -990,16 +1127,46 @@ ask the user:
)
def resolve_delegation(self, ctx):
return ctx.params.get("delegate", False)
return ctx.params.get("delegate", None)
.. note::
.. image:: /images/plugins/operators/operator-user-delegation.png
:align: center

Even though delegated operations are run in a separate process or physical
location, they are provided with the same `ctx` that was hydrated by the
operator's :ref:`input form <operator-inputs>`.
.. _operator-reporting-progress:

Refer to :ref:`this section <delegated-operations>` for more information
about how delegated operations are executed.
Reporting progress
~~~~~~~~~~~~~~~~~~

Delegated operations can report their execution progress by calling
:meth:`set_progress() <fiftyone.operators.executor.ExecutionContext.set_progress>`
on their execution context from within
:meth:`execute() <fiftyone.operators.operator.Operator.execute>`:

.. code-block:: python
:linenos:
import fiftyone.core.storage as fos
import fiftyone.core.utils as fou
def execute(self, ctx):
images_dir = ctx.params["images_dir"]
filepaths = fos.list_files(images_dir, abs_paths=True, recursive=True)
num_added = 0
num_total = len(filepaths)
for batch in fou.iter_batches(filepaths, 100):
samples = [fo.Sample(filepath=f) for f in batch]
ctx.dataset.add_samples(samples)
num_added += len(batch)
ctx.set_progress(progress=num_added / num_total)
.. note::

:ref:`FiftyOne Teams <fiftyone-teams>` users can view the current progress
of their delegated operations from the
:ref:`Runs page <teams-managing-delegated-operations>` of the Teams App!

.. _operator-execution:

Expand Down Expand Up @@ -1240,9 +1407,12 @@ Operator placement
------------------

By default, operators are only accessible from the
:ref:`operator browser <using-operators>`. However, you can optionally expose
the operation by placing a custom button, icon, menu item, etc. in various
places of the App:
:ref:`operator browser <using-operators>`. However, you can place a custom
button, icon, menu item, etc. in the App that will trigger the operator when
clicked in any location supported by the
:class:`types.Places <fiftyone.operators.types.Places>` enum.

For example, you can use:

- `types.Places.SAMPLES_GRID_ACTIONS`

Expand Down
Loading

0 comments on commit d864afb

Please sign in to comment.