Overview of mandala
masterStorage and ComputationFrame. Most user interactions involve a small subset of methods from these two classes.repository·master·Indexed 19 days ago
https://github.com/amakelov/mandalaA 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.
Storage and ComputationFrame. Most user interactions involve a small subset of methods from these two classes.A ComputationFrame (CF) is a specialized data structure for querying mandala storage. It generalizes a pandas DataFrame by representing a computational 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()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:
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:
@op calls: @ops can return collections where each item is a separate Ref, allowing subsequent @op calls to operate on individual elements.@ops can accept collections as input to perform operations over their elements.ComputationFrames. Internally, collections are implemented as @ops.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())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)Cache invalidation for an @op function f is determined by checking if a past call to f exists where:
f itself) have versions compatible with their current state.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.
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).
A ComputationFrame can be understood as a generalization of a pandas.DataFrame:
| Feature | pandas.DataFrame | ComputationFrame |
|---|---|---|
| Structure | Columns | A computational graph (functions connected by variable edges) |
| Data | Rows | Computation 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.
A ComputationFrame can be understood through two primary mental models:
In this view, a CF acts like a relational database:
.df() method performs a specific sequence of outer joins on these tables to reconstruct the computational history.In this view, a CF acts like a graph database:
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:
You can initialize a ComputationFrame to start querying your storage. Common initialization methods include:
Ref.@op.Note that an initial CF often has a limited view of storage, involving only zero or one @op until it is expanded.
Mandala uses content hashing to version and identify computations. However, there are several behaviors to be aware of when using get_content_hash:
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.42, 42.0, and 42.00000000001 all produce distinct hashes.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))