From 280684d4ea698d3cebcf8078ddfc064894c29c9b Mon Sep 17 00:00:00 2001 From: elijahbenizzy Date: Wed, 25 Sep 2024 12:14:09 -0700 Subject: [PATCH] Adds docs for parallelism --- README.md | 2 +- docs/_static/custom.css | 5 + docs/_static/parallelism.png | Bin 0 -> 34820 bytes docs/concepts/index.rst | 1 + docs/concepts/parallelism.rst | 583 ++++++++++++++++++++++++++++++++++ docs/concepts/recursion.rst | 2 + docs/conf.py | 4 + 7 files changed, 596 insertions(+), 1 deletion(-) create mode 100644 docs/_static/custom.css create mode 100644 docs/_static/parallelism.png create mode 100644 docs/concepts/parallelism.rst diff --git a/README.md b/README.md index 7cecef4e..30687999 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ but realized that it has a wide array of applications and decided to release it While Burr is stable and well-tested, we have quite a few tools/features on our roadmap! -1. Parallelism -- support for recursive "sub-agents" through an ergonomic API (not: this is already feasible, see [recursive applications](http://localhost:8000/concepts/recursion/)). +1. Parallelism -- support for recursive "sub-agents" through an ergonomic API (not: this is already feasible, see [recursive applications](https://burr.dagworks.io/recursion/)). 2. Testing & eval curation. Curating data with annotations and being able to export these annotations to create unit & integration tests. 3. Various efficiency/usability improvements for the core library (see [planned capabilities](https://burr.dagworks.io/concepts/planned-capabilities/) for more details). This includes: 1. First-class support for retries + exception management diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 00000000..27379940 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,5 @@ +/* Enable line wrapping for code blocks */ +.highlight pre { + white-space: pre-wrap; + word-wrap: break-word; +} diff --git a/docs/_static/parallelism.png b/docs/_static/parallelism.png new file mode 100644 index 0000000000000000000000000000000000000000..29e6ddb4cddf317c3daea1bd32bc2e3a74cd9c45 GIT binary patch literal 34820 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 pattern. + +Currently, Burr has two separate APIs for building parallel applications -- higher level (use this first), and lower level. +Beyond that, Burr can support parallelism however you wish to run it -- see the advanced use-cases section for more details. + +Higher-level API +================ + +You select a set of "configurations" over which you want to run, and Burr launches all of them then joins the result. + +This means you either: + +1. Vary the state and run over the same action/subgraph (think tuning LLM parameters/inputs, running simple experiments/optimization routines, etc...) +2. Vary the action and provide the same state (think running multiple LLMs on the same input, running multiple analyses on the same data, etc...) + +Note we do not distinguish between subgraph and action -- under the hood it's all treated as a "sub-application" (more on that later). + + +Run the same action over different states +----------------------------------------- + +For case (1) (mapping states over the same action) you implement the ``MapStates`` class, doing the following: + +- We define a regular action ``query_llm_action()`` using the ``@action`` decorator. +- We also create a subclass of ``MapStates`` named ``TestMultiplePrompts``, which must implement ``.reads()``, ``.writes()``, ``.action()``, ``.states()``, and ``.reduce()``. + - ``.reads()`` / ``.writes()`` define the state value it can interact with, just like the ``@action`` decorator + - ``.action()`` leverages the ``query_llm_action()`` previously defined + - ``.states()`` can read value from State and yields values to pass to the . action(). In this case, it updates the prompt state value that's read by ``query_llm_action()```. (the example hardcoded a list of prompts for simplicity, but this would be read from state) + - ``.reduce()`` receives multiple states, one per ``.action()`` call, where the llm_output value is set by ``query_llm_action()`` in ``.action()``. Then, it must set all_llm_output as specified in the ``MapStates.writes()`` method. +- We pass an instance of the ``TestMultiplePrompts`` class to the ApplicationBuilder, which will run the action over the states we provide. + +This looks as follows -- in this case we're running the same LLM over different prompts: + + +.. code-block:: python + + from burr.core import action, state + from burr.core.parallelism import MapStates, RunnableGraph + from typing import Callable, Generator, List + + @action(reads=["prompt"], writes=["llm_output"]) + def query_llm_action(state: State) -> State: + return state.update(llm_output=_query_my_llm(prompt=state["prompt"])) + + class TestMultiplePrompts(MapStates): + + def action(self) -> Action | Callable | RunnableGraph: + # make sure to add a name to the action + # This is not necessary for subgraphs, as actions will already have names + return query_llm_action.with_name("query_llm_action") + + def states(self, state: State) -> Generator[State, None, None]: + # You could easily have a list_prompts upstream action that writes to "prompts" in state + # And loop through those + # This hardcodes for simplicity + for prompt in [ + "What is the meaning of life?", + "What is the airspeed velocity of an unladen swallow?", + "What is the best way to cook a steak?", + ]: + yield state.update(prompt=prompt) + + + def reduce(self, states: Generator[State, None, None]) -> State: + all_llm_outputs = [] + for state in states: + all_llm_outputs.append(state["llm_output"]) + return state.update(all_llm_outputs=all_llm_outputs) + + def reads() -> List[str]: + return ["prompts"] + + def writes() -> List[str]: + return ["all_llm_outputs"] + +Then, to run the application: + +.. code-block:: python + + app = ( + ApplicationBuilder() + .with_action( + prompt_generator=generate_prompts, # not defined above, this writes to prompts + multi_prompt_test=TestMultiplePrompts(), + ).with_transitions( + ("prompt_generator", "multi_prompt_test"), + ) + .build() + ) + + +Run different actions over the same state +----------------------------------------- + + +For case (2) (mapping actions over the same state) you implement the ``MapActions`` class, doing the following: + +- We define a regular action ``query_llm_action()`` using the ``@action`` decorator. This takes in a model parameter (which we're going to bind later) +- We also create a subclass of ``MapActions`` named ``TestMultipleModels``, which must implement ``.reads()``, ``.writes()``, ``.actions()``, ``.state()``, and ``.reduce()``. + - ``.reads()`` / ``.writes()`` define the state value it can interact with, just like the ``@action`` decorator + - ``.actions()`` leverages the ``query_llm_action()`` previously defined, binding with the different models we want to test + - ``.state()`` can read value from State and produces the state to pass to the actions produced by ``actions()``. In this case, it updates the prompt state value that's read by ``query_llm_action()``. + - ``.reduce()`` receives multiple states, one per result of the ``.actions()`` call, where the llm_output value is set by ``query_llm_action()`` in ``.actions()``. Then, it must set all_llm_output as specified in the ``MapStates.writes()`` method. +- We pass an instance of the ``TestMultipleModels`` class to the ``ApplicationBuilder``, which will run the action over the states we provide. + +.. code-block:: python + + from burr.core import action, state + from burr.core.parallelism import MapActions, RunnableGraph + from typing import Callable, Generator, List + + @action(reads=["prompt", "model"], writes=["llm_output"]) + def query_llm(state: State, model: str) -> State: + # TODO -- implement _query_my_llm to call litellm or something + return state.update(llm_output=_query_my_llm(prompt=state["prompt"], model=model)) + + class TestMultipleModels(MapActions): + + def actions(self, state: State) -> Generator[Action | Callable | RunnableGraph, None, None]: + # make sure to add a name to the action + # This is not necessary for subgraphs, as actions will already have names + for action in [ + query_llm.bind(model="gpt-4").with_name("gpt_4_answer"), + query_llm.bind(model="o1").with_name("o1_answer"), + query_llm.bind(model="claude").with_name("claude_answer"), + ] + yield action + + def state(self, state: State) -> State:: + return state.update(prompt="What is the meaning of life?") + + def reduce(self, states: Generator[State, None, None]) -> State: + all_llm_outputs = [] + for state in states: + all_llm_outputs.append(state["llm_output"]) + return state.update(all_llm_outputs=all_llm_outputs) + + def reads() -> List[str]: + return ["prompt"] # we're just running this on a single prompt, for multiple actions + + def writes() -> List[str]: + return ["all_llm_outputs"] + + +Then, it's almost identical to the ``MapStates`` case: + +.. code-block:: python + + app = ( + ApplicationBuilder() + .with_action( + prompt_generator=generate_prompts, # not defined above, this writes to prompts + multi_prompt_test=TestMultipleModels(), + ).with_transitions( + ("prompt_generator", "multi_prompt_test"), + ) + .build() + ) + + +Full cartesian product +---------------------- + +If you want to run all possible combinations of actions/states, you can use the ``MapActionsAndStates`` class -- this is actually the +base class for the above two classes. For this, you provide a generator of actions and a generator of states, and Burr will run all possible +combinations. + +For tracking which states/actions belong to which actions, we recommend you use the values stored in the state (see example). + +.. code-block:: python + + from burr.core import action, state + from burr.core.parallelism import MapActionsAndStates, RunnableGraph + from typing import Callable, Generator, List + + @action(reads=["prompt", "model"], writes=["llm_output"]) + def query_llm(state: State, model: str) -> State: + # TODO -- implement _query_my_llm to call litellm or something + return state.update(llm_output=_query_my_llm(prompt=state["prompt"], model=model)) + + class TestModelsOverPrompts(MapActionsAndStates): + + def actions(self, state: State) -> Generator[Action | Callable | RunnableGraph, None, None]: + # make sure to add a name to the action + # This is not necessary for subgraphs, as actions will already have names + for action in [ + query_llm.bind(model="gpt-4").with_name("gpt_4_answer"), + query_llm.bind(model="o1").with_name("o1_answer"), + query_llm.bind(model="claude").with_name("claude_answer"), + ] + yield action + + def states(self, state: State) -> Generator[State, None, None]: + for prompt in [ + "What is the meaning of life?", + "What is the airspeed velocity of an unladen swallow?", + "What is the best way to cook a steak?", + ]: + yield state.update(prompt=prompt) + + def reduce(self, states: Generator[State, None, None]) -> State: + all_llm_outputs = [] + for state in states: + all_llm_outputs.append( + { + "output" : state["llm_output"], + "model" : state["model"], + "prompt" : state["prompt"], + } + ) + return state.update(all_llm_outputs=all_llm_outputs) + + def reads() -> List[str]: + return ["prompts"] + + def writes() -> List[str]: + return ["all_llm_outputs"] + + +Subgraphs +--------- + +While we've been using individual actions above, we can also replace them with subgraphs (E.G. :ref:`using recursion ` inside applications). + +To do this, we use the Graph API and wrap it in a RunnableGraph: + +- The :py:class:`Graph ` API allows us to tell the structure of the action +- The ``RunnableGraph`` is a wrapper that tells the framework other things you need to know to run the graph: + - The entrypoint of the graph + - The exit points (corresponding to ``halt_after`` in :py:meth:`run `) + +This might look as follows -- say we have a simple subflow that takes in a raw prompt from state and returns the LLM output: + +.. code-block:: python + + from burr.core import action, state + from burr.core.graph import Graph + + @action(reads=["prompt"], writes=["processed_prompt"]) + def process_prompt(state: State) -> State: + processed_prompt = f"The user has asked: {state['prompt']}. Please respond directly to that prompt, but only in riddles." + return state.update( + processed_prompt=state["prompt"], + ) + + @action(reads=["processed_prompt"], writes=["llm_output"]) + def query_llm(state: State) -> State: + return state.update(llm_output=_query_my_llm(prompt=state["processed_prompt"])) + + graph = ( + GraphBuilder() + .with_action( + process_prompt=process_prompt, + query_llm=query + ).with_transitions( + ("process_prompt", "query_llm") + ).build() + ) + + runnable_graph = RunnableGraph( + graph=graph, + entrypoint="process_prompt", + halt_after="query_llm" + ) + + class TestMultiplePromptsWithSubgraph(MapStates): + + def action(self) -> Action | Callable | RunnableGraph: + return runnable_graph + + def states(self, state: State) -> Generator[State, None, None]: + for prompt in [ + "What is the meaning of life?", + "What is the airspeed velocity of an unladen swallow?", + "What is the best way to cook a steak?", + ]: + yield state.update(prompt=prompt) + + ... # same as above + +In the code above, we're effectively treating the graph like an action -- due to the single ``entrypoint``/``halt_after`` condition we specified, +it can run just as the single prompt we did above. Note this is also doable for running multiple actions over the same state. + + + +Passing inputs +-------------- + +.. note:: + + Should ``MapOverInputs`` be its own class? Or should we have ``bind_from_state(prompt="prompt_field_in_state")`` that allows you to pass it in as + state and just use the mapping capabilities? + +Each of these can (optionally) produce ``inputs`` by yielding/returning a tuple from the ``states``/``actions`` function. + +This is useful if you want to vary the inputs. Note this is the same as passing ``inputs=`` to ``app.run``. + + +.. code-block:: python + + from burr.core import action, state + from burr.core.graph import Graph + + @action(reads=["prompt"], writes=["processed_prompt"]) + def process_prompt(state: State) -> State: + processed_prompt = f"The user has asked: {state['prompt']}. Please respond directly to that prompt, but only in riddles." + return state.update( + prompt=state["prompt"], + ) + + @action(reads=["processed_prompt"], writes=["llm_output"]) + def query_llm(state: State, model: str) -> State: + return state.update(llm_output=_query_my_llm(prompt=state["processed_prompt"], model=model)) + + graph = ( + GraphBuilder() + .with_action( + process_prompt=process_prompt, + query_llm=query + ).with_transitions( + ("process_prompt", "query_llm") + ).build() + ) + + runnable_graph = RunnableGraph( + graph=graph, + entrypoint="process_prompt", + halt_after="query_llm" + ) + + class TestMultiplePromptsWithSubgraph(MapStates): + + def action(self) -> Action | Callable | RunnableGraph: + return runnable_graph + + def states(self, state: State) -> Generator[Tuple[State, dict], None, None]: + for prompt in [ + "What is the meaning of life?", + "What is the airspeed velocity of an unladen swallow?", + "What is the best way to cook a steak?", + ]: + yield state.update(prompt=prompt), {"model": "gpt-4"} # pass in the model as an input + + ... # same as above + + + +Lower-level API +=============== + +The above compile into a set of "tasks" -- sub-applications to run. If, however, you want to have more control, you +can use the lower-level API to simply define the tasks. This allows you to provide any combination of actions, input, and state +to the tasks. + +All of the aforementioned high-level API are implemented as subclasses of TaskBasedParallelAction. +You can subclass it directly and implement the ``.tasks()`` method that yields SubGraphTask, +which can be actions or subgraphs. These tasks are then executed by the ``burr.Executor`` implementations + +This looks as follows: + +.. code-block:: python + + from burr.core import action, state, ApplicationContext + from burr.core.parallelism import MapStates, RunnableGraph + from typing import Callable, Generator, List + + @action(reads=["prompt", "model"], writes=["llm_output"]) + def query_llm(state: State, model: str) -> State: + # TODO -- implement _query_my_llm to call litellm or something + return state.update(llm_output=_query_my_llm(prompt=state["prompt"], model=model)) + + class MultipleTaskExample(TaskBasedParallelAction): + def tasks(state: State, context: ApplicationContext) -> Generator[SubGraphTask, None, None]: + for prompt in state["prompts"]: + for action in [ + query_llm.bind(model="gpt-4").with_name("gpt_4_answer"), + query_llm.bind(model="o1").with_name("o1_answer"), + query_llm.bind(model="claude").with_name("claude_answer"), + ] + yield SubGraphTask( + action=action, # can be a RunnableGraph as well + state=state.update(prompt=prompt), + inputs={}, + # stable hash -- up to you to ensure uniqueness + application_id=hashlib.sha256(context.application_id + action.name + prompt).hexdigest(), + # a few other parameters we might add -- see advanced usage -- failure conditions, etc... + ) + + def reduce(self, states: Generator[State, None, None]) -> State: + all_llm_outputs = [] + for state in states: + all_llm_outputs.append( + { + "output" : state["llm_output"], + "model" : state["model"], + "prompt" : state["prompt"], + } + ) + return state.update(all_llm_outputs=all_llm_outputs) + + +Advanced Usage +============== + +We anticipate the above should cover most of what you want to do, but we have a host of advanced tuning capabilities. + + +Execution +--------- + +To enable execution, you need to pass a ``burr.Executor`` to the application, or to the actions themselves. We have a few available executors: + +- ``burr.parallelism.MultiThreadedExecutor`` -- runs the tasks in parallel using threads (default) +- ``burr.parallelism.MultiProcessExecutor`` -- runs the tasks in parallel using processes +- ``burr.parallelism.RayExecutor`` -- runs the tasks in parallel using `Ray `_ +- ``burr.parallelism.Dask`` -- runs the tasks in parallel using `Dask `_ + +Or, you can implement your own by subclassing the ``burr.Executor`` class and passing to your favorite execution tool. + +For async, we only allow the ``burr.parallelism.AsyncExecutor`` (default), which uses ``asyncio.gather`` to run the tasks in parallel. + +You can pass this either as a global executor for the application, or specify it as part of your class: + +Specifying it as part of the application -- will get routed as the default to all parallel actions: + +.. code-block:: python + + app = ( + ApplicationBuilder() + .with_executor(MultiThreadedExecutor(max_concurrency=10)) + .build() + ) + +Specifying it as part of the action -- will override the global executor: + +.. code-block:: python + + class TestMultiplePrompts(MapStates): + + def action(self) -> Action | Callable | RunnableGraph: + return runnable_graph + + def executor(self) -> Executor: + return MultiThreadedExecutor(max_concurrency=10) + + ... # same as above + + +Persistence/Tracking +-------------------- + +By default, the trackers/persisters will be passed from the parent application to the child application. The application IDs +will be created as a a stable hash of the parent ID + the index of the child ID, requiring the order to be constant to ensure that the same application ID is used for the same task every time. + +It will also utilize the same persister to load from the prior state, if that is used on the application level (see the state-persistence section). + +This enables the following: + +1. Tracking will automatically be associated with the same application (and sub-application) when reloaded +2. If the concurrent application quits halfway through, bthe application will be able to pick up where it left off, as will all sub-applications + +You can disable either tracking or persistence at the sub-application level by passing ``track=False`` or ``persist=False`` to the constructor of the parallel action superclass. + +You can also disable it globally using the application builder: + +.. code-block:: python + + class TestMultiplePrompts(MapStates): + + def action(self) -> Action | Callable | RunnableGraph: + return runnable_graph + + def tracker(self, context: ApplicationContext) -> TrackingBehavior | None: + # return "cascade" # default + # return None # no tracking + return LocalTrackingClient(...) # custom tracker + + def persister(self, context: ApplicationContext) -> Persister | None: + # return "cascade" # default + # return None # no persistence + return SQLLitePersister(...) # custom persister + + ... # same as above + + +Other +----- + +Things we will consider after the initial release: + +- Customizing execution on a per-action basis -- likely a parameter to ``RunnableGraph`` +- Customizing tracking keys for parallelism +- Streaming -- interleaving parallel streaming actions and giving results as they come +- More examples for inter-graph communication/cancellation of one action based on the result of another +- Graceful failure of sub-actions + +Under the hood +============== + +Beneath the APIs, all this does is simplify the :ref:`recursion `: API to allow for multiple actions to be run in parallel. + +- ``RunnableGraph`` s are set as subgraphs, and recursively executed by the application, using the executor +- an ``Action`` are turned into a ``RunnableGraph`` by the framework, and executed by the executor + +In the UI, this will show up as a "child" application -- see the :ref:`recursion `: section for more details. + + +Additional Use-cases +==================== + +As this is all just syntactic sugar for recursion, you can use the recursion to get more advanced capabilities. + +This involves instantiating a sub-application inside the action, and running it yourself. + + +Interleaving Generators +----------------------- + +Say you want to provide an agent that provides up-to-date progress on it's thoughts. For example, say you want to providea +a planning agent with a similar interface to OpenAI's o1 model. + + +To do this, you would typically call to :py:meth:`iterate `. Now, say you wanted to run +multiple in parallel! + +While this is not built to be easy with the APIs in this section, it's very doable with the underlying recursion API. + +The basics (code not present now): + +1. Create each sub-application using the ``with_parent_context`` method +2. Run each sub-application in parallel using the executor +3. Combine the generators in parallel, yielding results as they come out + + +Inter-action communication +-------------------------- + +Say you have two LLMs answering the same question -- one that gives immediate results back to the user +as they come in, and another that thinks for a while to give more sophisticated results. The user then has the option to say they're happy +with the solution, or they want to wait for more. You may want to eagerly kick off the second LLM +if you're concerned about latency -- thus if the user wants more or does not respond, the more sophisticated +LLM might come up with a solution. + +To do this, you would: + +1. Run the sub-graph consisting of the first LLM using :py:meth:`iterate ` +2. Simultaneously run the second LLM using :py:meth:`iterate ` as well +3. Join them in parallel, waiting for any user-input if provided +4. Decide after every step of the first graph whether you want to cancel the second graph or not -- E.G. is the user satisfied. diff --git a/docs/concepts/recursion.rst b/docs/concepts/recursion.rst index 323cfeee..e27dd06f 100644 --- a/docs/concepts/recursion.rst +++ b/docs/concepts/recursion.rst @@ -1,3 +1,5 @@ +.. _recursion: + ====================== Recursive Applications ====================== diff --git a/docs/conf.py b/docs/conf.py index 375b9f09..7713e750 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,6 +32,10 @@ html_theme = "furo" html_static_path = ["_static"] +html_css_files = [ + "custom.css", +] + html_title = "Burr" html_theme_options = { "source_repository": "https://github.com/dagworks-inc/burr",