Fiddle documentation#

Fiddle is a Python-first configuration library particularly well suited to ML applications. Fiddle enables deep configurability of parameters in a program, while allowing configuration to be expressed in readable and maintainable Python code.

Design Goals#

Fiddle attempts to satisfy the following design goals:

Python first

Configurations are expressed naturally in Python code, and represented as Python objects. A Python first approach allows configs to leverage Python’s extensive existing tooling to simplify testing and maintenance.

Readability & Understandability

Fiddle’s APIs (powered by Python’s flexibility) make the structure of a system’s configuration easy to read. Additionally, Fiddle’s printing and visualization tools provide multiple lenses to view a configuration to help understand both a given model run and how a codebase fits together.

Minimal Boilerplate

Fiddle is designed to reduce boilerplate, to make both writing and reading configurations fast. Changing one hyperparameter anywhere in the program is just a single line of code. Configurations don’t require extensive forwarding of parameters across multiple files.

Isolation & Modularity

Fiddle allows your library code to remain unaware of what configuration system is being used, and doesn’t require decoration or cooperation on the part of library code. Library code should expose parameters as standard constructor and function arguments instead of relying on config objects.

Universality & Compatibility

Fiddle configurations can be used to represent all of a Python program’s configuration, avoiding the need for separate configuration systems for different program components. Fiddle additionally provides bridges to other configuration systems such as Gin and Lingvo Params.

Good errors

Errors that are as close as possible to the problematic line of code and whose messages contain all available context help both (a) developing code and configuration in the first place, and (b) subsequently maintaining it over time as well.


Installation#

pip install fiddle

Just install Fiddle like a standard Python package.

While Fiddle has a minimal set of dependencies by default, Fiddle has support for command line flags, built on top of absl-py’s command line flags. You can activate all of this functionality by installing:

pip install fiddle[flags]

Quick Start#

This section provides a brief overview of Fiddle’s basic mechanics. For more detailed documentation on Fiddle’s core types, please consult the API Reference.

fdl.Config and fdl.build()#

Fiddle’s core type is the Config class. A Config instance contains a reference to a Python callable (such as a function or class), along with parameters to pass to the callable. Valid parameters are directly determined by the signature of the callable, and parameter values can be accessed or changed by name using attribute syntax. For example:

import fiddle as fdl

class MomentumOptimizer:
  def __init__(self, learning_rate, momentum=0.9):
      ...

# Parameters can be set when constructing a `Config` instance ...
optimizer_config = fdl.Config(MomentumOptimizer, learning_rate=0.1)
# ... or overridden later via attribute syntax.
optimizer_config.learning_rate = 0.01
optimizer_config.momentum = 0.99

A Config instance can be “built” using Fiddle’s build() function, which calls the underlying callable object, passing in the configured parameters. For example:

optimizer_instance = fdl.build(optimizer_config)
assert isinstance(optimizer_instance, MomentumOptimizer)

In other words fdl.build(fdl.Config(MomentumOptimizer, learning_rate=0.1)) is effectively equivalent to MomentumOptimizer(learning_rate=0.1).

Nesting configuration#

A single Config instance may seem a bit like a functools.partial that you need to call fdl.build() on to invoke, with some syntax sugar for accessing/mutating parameters. However, Config instances become much more powerful when they are nested, i.e., passed as values to parameters in another Config instance. This is because, unlike invoking a functools.partial, Fiddle’s build() first traverses the parameters of Config instances, recursively building any nested Config instances it finds. For example:

class Trainer:
  def __init__(self, model, optimizer, num_steps):
    self.model = model
    self.optimizer = optimizer
    ...

trainer_config = fdl.Config(
    Trainer,
    model=Config(SomeModel, ...),
    optimizer=optimizer_config,  # Defined above.
    num_steps=10000,
)

trainer_instance = fdl.build(trainer_config)
assert isinstance(trainer_instance, Trainer)
assert isinstance(trainer_instance.optimizer, MomentumOptimizer)

Object sharing#

While recursively building nested subconfigurations, fdl.build() ensures that each Config instance is only built once — the value obtained when building a Config is stored in a memoization dictionary, and if the same instance is encountered again, the result from building it the first time is reused. In this way, Fiddle maintains a one-to-one correspondence between the “configuration graph” (graph of Config instances) and the resulting “object graph” returned from fdl.build().

This behavior makes object sharing very natural to represent with Fiddle. For example:

class DualEncoder:
  def __init__(self, query_encoder, item_encoder):
    self.query_encoder = query_encoder
    self.item_encoder = item_encoder

# Create an encoder config...
encoder_config = fdl.Config(SomeEncoder, ...)
# Share the same encoder config instance by reusing the config.
dual_encoder_config = fdl.Config(
    DualEncoder, query_encoder=encoder_config, item_encoder=encoder_config)

dual_encoder_instance = fdl.build(dual_encoder_config)
assert dual_encoder_instance.query_encoder is dual_encoder_instance.item_encoder

fdl.Partial#

In addition to fdl.Config, Fiddle provides one other core “buildable” type called fdl.Partial (both fdl.Config and fdl.Partial inherit from a Buildable base class). The fdl.Partial type is in many ways just like fdl.Config — it maintains a reference to a callable, and provides mutable access to the callable’s parameter values via attribute syntax. However, when built via fdl.build(), instead of invoking the underlying callable with the configured parameters, fdl.Partial instead creates a corresponding functools.partial object. In other words, fdl.build(fdl.Partial(MomentumOptimizer, learning_rate=0.1)) is effectively equivalent to functools.partial(MomentumOptimizer, learning_rate=0.1).

Using fdl.Partial can be a great option especially for long-running top-level functions. For example:

def train(model, num_steps):
  ...

train_partial = fdl.Partial(
    train,
    model=fdl.Config(SomeModel, ...),
    num_steps=10000
)
train_fn = fdl.build(train_partial)
train_fn()

Building train_fn via a fdl.Partial and then executing it separately avoids performing what is likely the majority of the program’s workload inside fdl.build(), and also avoids Fiddle appearing in any stack traces related to errors during the execution of train_fn.


Learn More#

Developing#