redun

repository·main·Indexed 20 days ago

https://github.com/insitro/redun

An expressive Python workflow framework that uses lazy expressions to define dynamic directed acyclic graphs (DAGs). redun provides incremental computation, automatic parallelization, centralized caching, and robust data provenance logging. It features a `@task` decorator for defining workflow steps, a CLI for execution and debugging, and a `File` class for tracking file-based reactivity and lineage.

Tokens
90.9K
Snippets
317
Records
430
Agent score
68%

What's inside redun

  1. Compare redun with other workflow engines

    main

    redun is designed for multi-domain scientific pipelines, focusing on task parallelism, caching, and data provenance.

    Key distinctions:

    • Task vs. Data Parallelism: redun provides expressive task parallelism, but it does not perform fine-grained data parallelism (for which you should use tools like Spark or Dask).
    • Integration: redun does not use 'dirty tricks' like complex static analysis, making it safe to combine with frameworks like pyspark, pytorch, or Dask.
    • Compute: redun does not provide its own compute cluster; it orchestrates existing systems like cloud provider batch services or Spark clusters.
  2. Overview of redun workflow engine concepts

    main

    redun is a workflow framework where workflows are defined as lazy expressions. These expressions are evaluated by a scheduler that generates dynamic directed acyclic graphs (DAGs) to manage data flows.

    Key Capabilities:

    • Incremental Computation: Workflows are reactive to both data changes (detected via file hashing for in-memory values and external sources like object stores) and code changes (detected by hashing individual Python functions).
    • Automatic Optimization: The scheduler performs automatic parallelization, caching, and data provenance logging.
    • Centralized Caching: Intermediate results are cached centrally and reused across different workflow runs.
    • Pluggable Compute Backends: Tasks can be executed using various backends, including threads, processes, AWS Batch jobs, or Spark jobs.
    • Data Lineage: Past call graphs are recorded and can be queried for debugging, auditing, and data provenance exploration.
  3. How memoization and reactivity work in redun

    main

    redun uses memoization to cache task results. When a task is called with the same arguments and the same code (hash), redun reuses the cached result instead of re-executing.

    Key behaviors:

    • Argument Changes: If task arguments change, redun detects the change and re-executes only the affected tasks and their downstream dependencies.
    • Code Changes: redun hashes the task implementation. If you update the code inside a @task(), redun detects the new hash and triggers re-execution.
    • Fast-Forwarding: If a program is re-run without changes, redun 'fast-forwards' through the execution by fetching all results from the cache without running any task bodies.
  4. Set Resource Limits for task concurrency

    main

    You can limit the parallelism of tasks by defining resource limits in the [limits] section.

    1. Define limits in redun.ini:
    [limits]
    db = 10
    web_api = 5
    1. Assign limits to tasks in Python:
    • Single resource: @task(limits=["db"]) consumes 1 unit of db.
    • Multiple resources: @task(limits=["db", "web_api"]) consumes 1 unit of each.
    • Multiple units of one resource: @task(limits={"db": 2}) consumes 2 units of db.
    @task(limits={"db": 2})
    def read_from_database(args):
        # Consumes 2 units of 'db'
        ...
  5. Use the Alias executor to share resources

    main

    The AliasExecutor allows multiple executor names to resolve to the same underlying implementation. This is useful for resource management; for example, if you have multiple tasks that conceptually need different executors but you want them to share a single process pool to limit concurrency.

    To implement this, define a single base executor and then create multiple alias executors that point to it using the target key.

    ; Define the actual resource-constrained executor
    [executors.single_worker]
    type = local
    mode = process
    max_workers = 1
    
    ; Create aliases for other names to point to the single worker
    [executors.process]
    type = alias
    target = single_worker
    
    [executors.default]
    type = alias
    target = single_worker
    
    [executors.foo_exec]
    type = alias
    target = single_worker
  6. Invoke Federated Tasks from remote repositories

    main

    Federated tasks allow you to invoke tasks located in different Python repositories without having the code locally. This is typically achieved by publishing the remote repository as a Docker image.

    Configuration

    1. federated_tasks: Define in your config file to map task names to their import paths and associated executors (e.g., a Docker-based executor).
    2. federated_imports: Allows remote repositories to provide their own configuration files containing additional federated tasks and executors, which can be shared via a shared file system.

    REST-based Proxy

    For fire-and-forget job launching (useful for UIs) or managing AWS role switches, use:

    • redun.federated_tasks.rest_federated_task
    • redun.federated_tasks.launch_federated_task
  7. Avoid invalidating task cache when modifying files

    main

    In redun, using a File object as a task argument causes the task's input hash to be recorded. If the task modifies that file (e.g., by writing to it), the file's hash changes, which invalidates the input and causes the task to rerun unnecessarily when called again.

    Best Practices for File Handling in Tasks

    • For Reading Only: Use File as the task argument type. This ensures that if the file content changes, redun correctly detects the change and reruns the task.
    • For Modifying Files: Pass the file path as a str instead of a File object. This prevents the input hash from changing when the file is modified, allowing redun to find the task in the cache and avoid unnecessary reruns.
    • Return Values: Always return the modified File object as the task result.

    Summary Rule: Input File objects should be arguments; output File objects should be return results.

    from redun.file import File
    from redun.task import task
    
    # RECOMMENDED: Pass path as str to avoid invalidating input hash on modification
    @task()
    def add_footer(file_path: str) -> File:
       f = File(file_path)
       with f.open("w") as outfile:
           outfile.write("EOF")
       return f
  8. Handling non-deterministic or random functions in tasks

    main

    If you use non-deterministic functions (like random.random()) inside a @task, you must be aware of how Redun's caching affects them:

    • With Caching (Default): The first generated value will be cached. Subsequent calls with the same inputs will return the exact same cached value, effectively making the 'random' function deterministic.
    • Without Caching: If the cache is disabled or limited in scope, Redun will run the task implementation every time, allowing for new random values to be generated and recorded.
    @task
    def rand():
        return random.random()
  9. Use the built-in REPL for data inspection

    main

    The Console includes a Python REPL for deep inspection of job arguments and results. When you press r on a Job screen, the REPL starts with local variables from that job (such as job, args, kwargs, and result) already injected into the environment.

    Redun uses SQLAlchemy to lazily load these values from the database. Values are deserialized using pickle; if a class is not currently importable, it will default to a pickle preview (mock-like object).

    Built-in functions for inspection:

    • query(): Provides access to the SQLAlchemy ORM for navigating Call Graph models in the redun database.
    • console(): Provides an easy way to navigate to the screen of a specific model (e.g., console(Execution)).
  10. Mental model: Redun as a language within Python

    main

    Redun uses metaprogramming to implement an asynchronous functional programming language on top of Python. This allows you to use Python syntax while benefiting from distributed computing features. The mapping between Python and Redun concepts is as follows:

    Python ConceptRedun Equivalent
    FunctionsTasks
    First-class valuesTasks (can be passed as args/returned)
    ValuesTask input/output (base class Value)
    Interpreter/RuntimeScheduler
    ExpressionsExpression (evaluated by Scheduler)
    Environment/ScopeTaskRegistry and TypeRegistry
    MacrosPlain Python functions taking Expressions (evaluated at construction-time)
    Special Forms (e.g. if, try)scheduler_tasks (e.g. fexprs)
    ClosuresPartialTask (implements delay and force)
  11. Use Context to avoid argument prop-drilling

    main

    Redun's context allows you to define arguments in the redun.ini file and access them in tasks using get_context. This avoids passing the same argument through every intermediate task in a workflow.

    1. Define context in redun.ini:
    [scheduler]
    context = {"my_tool": {"ratio": 1.618}}
    1. Access in a task:
    from redun import task, get_context
    
    @task
    def my_tool(value: int, ratio: float = get_context("my_tool.ratio")) -> str:
        return f"Ran my_tool with argument: {ratio}"
    @task
    def my_tool(value: int, ratio: float = get_context("my_tool.ratio")) -> str:
        return f"Ran my_tool with argument: {ratio}"