Arc-Graph: Declarative Machine Learning for the Age of AI Agents
How declarative ML creates a shared language for human-agent collaboration.
The Abstraction Lesson #
The history of computer science is a history of abstraction. We moved from assembly language to high-level code to abstract away the hardware. We moved from navigational databases to SQL to abstract away the retrieval logic. Each transition brought significant improvements in productivity by letting developers focus on what to compute rather than how.
We stand at a similar inflection point in Machine Learning. For the past decade, imperative frameworks like PyTorch, TensorFlow, and JAX have been the power tool—their flexibility enabled the "Cambrian explosion" of deep learning architectures. However, as ML matures from experimental science to industrial infrastructure, we are paying a price. We act as human compilers—manually wiring dimensions, managing device placement, and writing boilerplate that distracts from the modeling task. This "glue code" has become the primary bottleneck in modern ML development.
We believe Declarative Machine Learning (DML) provides the necessary abstraction layer. By separating the model specification from its execution, we return to the principle that drove previous leaps in computing: defining the what distinct from the how.
The Age of AI Agents #
This abstraction arrives at a critical moment. We are entering an era where AI agents are not just tools, but active collaborators in the development process.
However, for these silicon collaborators, the "glue code" of imperative frameworks is an opaque barrier. Imperative code is a lossy medium for this collaboration. When an agent generates 500 lines of PyTorch, the intent—like adding a skip connection—is buried under the implementation details of tensor reshaping, device management, and gradient hooks. For a human to verify the agent's work, they must mentally decompile the boilerplate back into the architecture.
Declarative ML flips this dynamic by providing a shared language that is both concise and safe. Unlike fragmented Python files, a declarative config fits entirely within an LLM's context window, giving the agent global visibility of the system. The schema acts as a contract that bounds the search space, preventing agents from hallucinating invalid APIs or introducing subtle broadcasting bugs. This allows the human to review the design while the system handles the execution.
Structured configurations provide the common ground—a format that both humans and agents can easily read, modify, and reason about. But to work, they must be more than just config files—they must be executable graphs. This is the foundation of Arc-Graph.
Arc-Graph #
The challenge with building a declarative ML system is finding the right level of abstraction—too high and you lose expressiveness, too low and you're back to imperative code.
Declarative approaches to ML have been explored in various forms. BigQuery ML [1] pioneered SQL-based machine learning, enabling data analysts to train models using familiar SQL syntax. AutoML systems [2] automated the entire model selection and hyperparameter tuning process. Ludwig [3] and Turi Create [4] abstracted away architectural details entirely—you specify "Input: Text, Output: Class" and the system handles the rest. These approaches excel at rapid prototyping and have made ML more accessible to non-experts.
Structure as Code #
Arc-Graph takes a different approach: "Structure as Code". Instead of hiding the architecture, we make it explicit—but declarative. You define the exact computational graph you want, just in YAML instead of Python.
We map 1:1 to the underlying framework (PyTorch), preserving the expressiveness needed for research and production architectures while enforcing a static schema that can be validated, versioned, and generated by AI agents.
Here's what this looks like in practice. Below is a complete specification for a Multi-Layer Perceptron (MLP):
inputs:
features:
dtype: float32
shape: [null, 128]
columns: [age, bmi, glucose, ...]
graph:
- name: hidden1
type: torch.nn.Linear
params: { in_features: 128, out_features: 64 }
inputs: { input: features }
- name: relu1
type: torch.nn.functional.relu
inputs: { input: hidden1.output }
- name: dropout1
type: torch.nn.Dropout
params: { p: 0.3 }
inputs: { input: relu1.output }
- name: output_layer
type: torch.nn.Linear
params: { in_features: 64, out_features: 1 }
inputs: { input: dropout1.output }
- name: probabilities
type: torch.nn.functional.sigmoid
inputs: { input: output_layer.output }
outputs:
logits: output_layer.output
probabilities: probabilities.output
loss:
type: torch.nn.functional.binary_cross_entropy_with_logits
inputs: { input: logits, target: outcome }
This configuration offers more than just readability. It makes the data flow explicit, with every connection visible and verifiable. It enforces type safety, ensuring that a layer's output shape matches the next layer's input. It eliminates the boilerplate of __init__ and forward methods. And perhaps most importantly, it makes the model version-controllable; you can diff two YAML files to see exactly how a hyperparameter changed, rather than hunting through Python diffs.
Advanced Composability #
The true test of any abstraction is whether it handles complexity gracefully. Can you build sophisticated, custom architectures without breaking the abstraction and dropping back to imperative code?
Arc-Graph addresses this by exposing graph primitives directly. Because it makes the computational structure explicit, you can build complex, custom architectures entirely within the declarative framework.
Take the Deep & Cross Network (DCN) as an example. It's not a particularly complex architecture, but it demonstrates how Arc's composition patterns enable extensibility. DCN combines two parallel branches—a deep neural network and a "cross network" that explicitly models feature interactions—then concatenates their outputs.
The key insight: you can build DCN by composing reusable modules. Define a cross_layer module once, then wire multiple instances together with a standard mlp_block module.
In Arc, we can define the cross layer as a reusable module:
modules:
cross_layer:
inputs: [x_current, x_original]
graph:
- name: linear
type: torch.nn.Linear
params: { in_features: 128, out_features: 128 }
inputs: { input: x_current }
- name: prod
type: torch.mul
inputs: [x_original, linear.output]
- name: add
type: torch.add
inputs: [prod.output, x_current]
outputs:
output: add.output
We can then wire this into a larger graph, combining it with a standard deep network:
graph:
# ... feature embedding ...
# Explicit Cross Network Branch
- name: cross1
type: module.cross_layer
inputs: { x_current: embeddings, x_original: embeddings }
- name: cross2
type: module.cross_layer
inputs: { x_current: cross1.output, x_original: embeddings }
# Parallel Deep Network Branch
- name: deep_net
type: module.mlp_block
inputs: { input: embeddings }
# Combine branches
- name: concat
type: torch.cat
params: { dim: 1 }
inputs: [cross2.output, deep_net.output]
This level of composability—defining custom math, reusable modules, and complex parallel branches—makes Arc-Graph suitable for both research prototyping and production deployments.
Under the Hood #
You might wonder: "How can YAML be as powerful as Python?" The key is that Arc is not an interpreter—it is a compiler.
When you define a model in Arc, we don't just parse the YAML and run it dynamically. We compile it into a native PyTorch nn.Module. This happens in three stages:
1. The Registry: Bridging Structure and Code
Arc maintains a strict registry that maps declarative types to their imperative implementations. When you write type: torch.nn.Linear, Arc looks up the corresponding class in torch.nn. This registry acts as the boundary between the safe, declarative world of the config and the raw power of the backend.
2. The Compiler: Zero-Overhead Execution
Instead of traversing a graph at runtime (which would be slow), Arc performs a topological sort of your graph once during initialization. It flattens the DAG into a linear execution sequence, effectively writing the forward() method for you. The result is a standard PyTorch module that runs with zero overhead—exactly as if you had hand-written the code.
3. Declarative Power: "Sugar" with Substance
Because Arc understands the semantics of your data, it can automate the tedious parts of model building.
Take repeating layers, like in a Transformer. In raw PyTorch, you'd write a loop, handle copy.deepcopy to ensure independent weights, and wrap it all in nn.Sequential.
In Arc, you simply declare a stack:
- name: encoder
type: arc.stack
params:
module: transformer_block
count: 6
The compiler expands this into a sequence of independent modules, wired correctly. You describe the pattern (a stack of 6 blocks), and Arc handles the instantiation and wiring.
The Road Ahead #
Arc-Graph demonstrates how declarative abstractions can work for modern ML—preserving expressiveness for complex architectures while enabling collaboration between humans and AI agents.
Arc is an open-source project. We believe open collaboration leads to better ML infrastructure, and we're excited to explore what's possible when model architectures become as easy to version, share, and generate as configuration files. We are building the interface for next-generation AI-assisted ML—join us on GitHub.