mandala

repository·master·Indexed 19 days ago

https://github.com/amakelov/mandala

A tool for automatically saving, querying, and versioning Python computations. It uses the @op decorator to turn function calls into a persistent computation graph, providing memoization and efficient ML experiment tracking. It includes a Storage context for managing versioned results, a Tracer for tracking function dependencies and global variable access, and tools for visualizing computation flows via Graphviz.

Tokens
26.8K
Snippets
94
Records
116
Agent score
68%

What's inside mandala

  1. Overview of mandala

    master
    mandala is a simple and elegant experiment tracking framework for Python designed to automatically save, query, and version Python computations. It is built around two primary classes: Storage and ComputationFrame. Most user interactions involve a small subset of methods from these two classes.
  2. What is a ComputationFrame and how to use it

    master

    A ComputationFrame (CF) is a specialized data structure for querying mandala storage. It generalizes a pandas DataFrame by representing a computational graph:

    • Columns: Represent the computational graph, consisting of variables and operations.
    • Rows: Represent specific computations that follow the structure of that graph.

    Output variables in a CF are automatically appended with the name of the function output they are connected to (e.g., var_0@output_0) to resolve ambiguity.

    You can use a CF to inspect the history of your experiments, rename variables for clarity, and convert the results into a standard pandas DataFrame for analysis.

    # Get a computation frame for a specific function
    cf = storage.cf(train_model)
    
    # Rename variables for readability
    cf.rename(vars={'var_1': 'model', 'var_0': 'train_acc'}, inplace=True)
    
    # Convert to a pandas DataFrame
    df = cf.df()
  3. What is a ComputationFrame (CF)?

    master

    A ComputationFrame (CF) is a "generalized dataframe" that synthesizes computation graphs with relational databases. In a CF, the traditional set of columns is replaced by a directed computation graph of variables and operations, while the rows represent (possibly partial) executions of that graph.

    Key characteristics:

    • Heterogeneous View: It represents constructs like conditional execution, feedback loops, branching/merging pipelines, and aggregation/indexing.
    • Declarative Querying: You can query relationships between variables in a single line of code without using SQL.
    • Handling Partiality: When computations are partial (e.g., due to conditional logic), missing values and calls are represented as nulls/NaNs in the resulting dataframe.
    • Data Structure: It consists of a directed graph where operations have named inputs/outputs, a set of calls for each operation node, and a set of values for each variable node.
  4. How collections work in Mandala

    master

    Mandala treats Python collections (lists, dicts, etc.) as native parts of the memoization process. Instead of treating a collection as a single opaque object, Mandala can decompose it so that each item is a separate Ref.

    Key behaviors include:

    • Granular @op calls: @ops can return collections where each item is a separate Ref, allowing subsequent @op calls to operate on individual elements.
    • Aggregation: @ops can accept collections as input to perform operations over their elements.
    • Storage Efficiency: Collections reuse the storage of their items; shared elements are stored only once.
    • Graph Integration: The relationship between a collection and its items is part of the computational graph and is automatically propagated by ComputationFrames. Internally, collections are implemented as @ops.
  5. How collection `@op`s interact with ComputationFrames

    master

    When using collection-aware @ops, a single Ref (like an element in a list) can depend on multiple Refs from another variable.

    When inspecting a ComputationFrame (CF) via .df(values='objs'), a column representing a collection might contain a ValueCollection object. This indicates that the column contains multiple Refs that serve as dependencies for the subsequent operation.

    # Assuming 'average' was called with MList as shown in previous examples
    cf = storage.cf(average).expand_all()
    print(cf.df(values='objs').to_markdown())
  6. Examine captured versions of @op functions

    master

    Mandala captures version information for @op functions to manage cache invalidation. When an @op is executed, it is associated with a content_version_id (representing the code content) and a semantic_version_id. If the code within an @op changes, these IDs change, triggering a re-computation of any downstream operations that depend on it.

    ### Dependencies for version of function eval_model from module __main__
    ### content_version_id=955b2a683de8dacf624047c0e020140a
    ### semantic_version_id=c847d6dc3f23c176e6c8bf9e7006576a
    ################################################################################
    ### IN MODULE "__main__"
    ################################################################################
    
    @op
    def eval_model(model, X, y, scale=False):
        if scale:
            X = scale_data(X)
        return model.score(X, y)
  7. How @op cache invalidation and versioning works

    master

    Cache invalidation for an @op function f is determined by checking if a past call to f exists where:

    1. The inputs have the same content (determined via a hash function).
    2. The dependencies (including the function f itself) have versions compatible with their current state.

    Managing Code Changes

    If you need to extend an @op without invalidating all past results, the recommended pattern is to add a new argument with a default value wrapped in NewArgDefault(x). When the value x is passed, the system falls back to the results of calls made before the change.

    Dependencies

    mandala uses a modified version of joblib hashing to compute content hashes for Python objects. Be aware that if you introduce new dependencies that are not tracked by mandala, changes to those dependencies might not trigger a cache invalidation (the "invisible dependency" problem).

  8. Concept: `ComputationFrame`s as generalized DataFrames

    master

    A ComputationFrame can be understood as a generalization of a pandas.DataFrame:

    Featurepandas.DataFrameComputationFrame
    StructureColumnsA computational graph (functions connected by variable edges)
    DataRowsComputation traces (variable values and function calls following the graph)

    This mental model allows you to treat complex execution histories as structured tabular data for analysis.

  9. Conceptual model of ComputationFrames as Relational Databases or Graph Databases

    master

    A ComputationFrame can be understood through two primary mental models:

    1. Relational Database Model

    In this view, a CF acts like a relational database:

    • Tables: There is a table for each operation and each variable.
    • Operation Tables: Columns are labeled by the inputs/outputs of the operation. Values in these columns are pointers to the corresponding input/output values.
    • Variable Tables: These are single-column tables storing the actual values.
    • Data Extraction: The .df() method performs a specific sequence of outer joins on these tables to reconstruct the computational history.

    2. Graph Database Model

    In this view, a CF acts like a graph database:

    • Nodes: The nodes in the graph are function calls and values.
    • Node Types: Variables and operations serve as the "node types."
    • Operations: Tasks like expanding the frame or finding dependencies/dependents are equivalent to graph traversal operations.
  10. Caveats when marking changes as non-breaking

    master

    Mandala allows you to mark certain code changes (like refactoring, adding comments, or logging) as non-semantic/non-breaking to prevent unnecessary cache invalidation. However, you should use this with caution due to two main risks:

    1. Human Error: You may incorrectly conclude that a change has no effect on the operation's semantics.
    2. Invisible Dependencies: If you refactor code (e.g., extracting a function out of a dependency) and mark the change as non-semantic, the system may fail to track the new function as a dependency. Consequently, future changes to that extracted function might not trigger the expected version updates, leading to stale results.
  11. Create a ComputationFrame

    master

    You can initialize a ComputationFrame to start querying your storage. Common initialization methods include:

    • Creating a CF from a single Ref.
    • Creating a CF from all calls to a specific @op.

    Note that an initial CF often has a limited view of storage, involving only zero or one @op until it is expanded.

  12. Understand content hashing caveats in Mandala

    master

    Mandala uses content hashing to version and identify computations. However, there are several behaviors to be aware of when using get_content_hash:

    1. Equality vs. Hash Identity: Just because two objects are equal (x == y) does not mean they will have the same content hash. For example, 1 == True is true, but their hashes differ. Similarly, 23 == 23.0 is true, but their hashes differ.
    2. Numerical Sensitivity: Hashing is highly sensitive to both data type and floating-point precision. 42, 42.0, and 42.00000000001 all produce distinct hashes.
    3. Non-deterministic Hashes for Complex Objects: Certain complex objects (like ML models or PyTorch tensors) may produce different hashes even when semantically identical due to internal state related to system resources (e.g., memory layout).
    4. Serialization Roundtrips: In some cases, an object may change its content ID after being serialized and then deserialized.
    from mandala.utils import get_content_hash
    
    # Equality does not guarantee identical hashes
    print(f'Is 1 == True? {1 == True}')
    print(f'Is the hash of 1 == the hash of True? {get_content_hash(1) == get_content_hash(True)}')
    
    # Numerical precision and type sensitivity
    print(get_content_hash(42))
    print(get_content_hash(42.0))
    print(get_content_hash(42.00000000001))