Introduction
In every AI project, we aim to build workflows that balance speed, cost, and performance. This typically involves:
- Designing effective pipelines or DAGs that optimize the flow of data and operations
- Selecting appropriate implementations for each step of our workflow
- Tuning hyperparameters to maximize our chosen metrics
In addition to optimizing our workflows, we face another critical requirement: managing configurations for multiple scenarios or “modes” in our codebase, mainly:
- Setting up development & production configurations, including connection strings, secrets, and file paths
- Building workflows for different “modes”, for example: conversational chatbot vs. single-query RAG, agentic vs. predefined prompt chains, etc.
- Creating specialized sub-models or sub-workflows for various contexts, like different countries or populations
Managing these different modes and scenarios while optimizing each for its specific context is exactly what Hypster is designed for.
Hypster is a Pythonic framework for building and instantiating configuration spaces using a straightforward, intuitive API. It supports swappable and hierarchical configurations, enabling end-to-end optimization for complex AI workflows.
If you find Hypster useful, please consider starring it on GitHub: github.com/gilad-rubin/hypster
Explore feature guides and examples in the docs: gilad-rubin.gitbook.io/hypster
Code Examples
In the following sections, I’ll cover the main features of the package and explain key concepts along the way.
For more detailed information, see the docs linked above.
Installation
pip install hypster
Basic Usage
Define your configuration space with a function and wrap it with @config
:
from hypster import HP, config
@config
def my_config(hp: HP):
env = hp.select(["prod", "dev"], default="dev")
llm_model = hp.select({
"haiku": "claude-3-haiku-20240307",
"sonnet": "claude-3-5-sonnet-20240620"
})
temperature = hp.number(0.5)
prompt = hp.text("What is your favorite food?")
The @config
wrapper turns the my_config
function into a Hypster object that contains the space of possible configurations — a “superposition” of configurations ⚛️
To instantiate this space into a concrete configuration, we use the values
API:
results = my_config(values={"llm_model": "haiku"})
# results:
# {
# "env": "dev",
# "llm_model": "claude-3-haiku-20240307",
# "temperature": 0.5,
# "prompt": "What is your favorite food?"
# }
Note how Hypster handles default values for env
, temperature
, and prompt
.
results = my_config(values={
"llm_model": "gpt-4o-mini",
"temperature": 0,
"prompt": "What are you, really?"
})
# results:
# {
# "env": "dev",
# "llm_model": "gpt-4o-mini",
# "temperature": 0,
# "prompt": "What are you, really?"
# }
In this example, we override the hp.select
options and add an option ("gpt-4o-mini"
) that wasn’t in the original set of options.
We can also decide on arbitrary values for the hp.number
and hp.text
.
Nested & Conditional Logic
One of the benefits of using a Pythonic interface for configuration management is the ease with which we can define conditional logic, loops, and almost everything that the language enables. In addition, we can define arbitrary classes in a straightforward manner.
These last two points are more difficult to achieve when using YAML-based configuration systems.
Let’s take a look at a toy example:
from hypster import HP, config
@config
def conditional_config(hp: HP):
# Note: imports should be stated inside the config function
from models import CNN, RNN, Transformer
model_type = hp.select(["CNN", "RNN", "Transformer"], default="CNN")
if model_type == "CNN":
num_layers = hp.select([3, 5, 7], default=5)
model = CNN(num_layers=num_layers)
elif model_type == "RNN":
cell_type = hp.select(["LSTM", "GRU"], default="LSTM")
model = RNN(cell_type=cell_type)
else:
num_heads = hp.select([4, 8, 16], default=8)
model = Transformer(num_heads=num_heads)
We can choose to return only the model
object using final_vars
:
results = conditional_config(
final_vars=["model"],
values={"model_type": "RNN", "cell_type": "GRU"}
)
# results
# {"model": RNN(cell_type="GRU")}
In this case, the function runs from start to finish but only returns the specified variables in final_vars
. If this field is left empty, it’ll return all variables defined in the function.
Automatic Naming
Hypster can automatically name different parameters using simple heuristics — variable names, dictionary keys, and function/class keyword arguments.
This saves repetitive code and allows for a smoother development experience:
from hypster import HP, config
@config
def naming_config(hp: HP):
# Note: class definitions and imports should be
# defined inside the config function
class Model:
def __init__(self, model_type, learning_rate):
self.model_type = model_type
self.learning_rate = learning_rate
def func(param):
return param
model = Model(
model_type=hp.select(["cnn", "rnn"]),
learning_rate=hp.number(0.01)
)
var = func(param=hp.select(["option1", "option2"]))
Hypster automatically names parameters using dot notation where applicable, for example: model.learning_rate
.
results = naming_config(
final_vars=["model"],
values={"model.model_type": "cnn", "var.param": "option1"}
)
# results
# {"model": Model(model_type="cnn", learning_rate=0.01)}
Automated naming can be explicitly overridden using hp.select(..., name="my_key")
if you prefer a different parameter name or if automatic naming doesn’t fit your case. For more information see the docs.
Nesting
A major use-case that Hypster intends to tackle is hierarchical optimization. This means that if you build a configuration space for one part of your project, you can nest it within a parent Hypster function and have access to both levels.
First, define your config function:
from hypster import HP, config
@config
def my_config(hp: HP):
env = hp.select(["prod", "dev"], default="dev")
llm_model = hp.select({
"haiku": "claude-3-haiku-20240307",
"sonnet": "claude-3-5-sonnet-20240620"
})
temperature = hp.number(0.5)
prompt = hp.text("What is your favorite food?")
import hypster
hypster.save(my_config, "my_config.py")
Using hypster.save()
, we export the function into a module and save it to disk.
Then, we can nest our object inside the parent config function using hp.nest
:
from hypster import HP, config
@config
def my_config_parent(hp: HP):
import hypster
config_hypster = hypster.load("my_config.py")
my_config = hp.nest(config_hypster)
var = hp.select(["a", "b", "c"], default="a")
Using the nesting API, we can easily access nested parameters inside our functions and get back a dictionary of the results:
results = my_config_parent(
values={"my_config.llm_model": "haiku", "var": "d"}
)
# results
# {
# "my_config": {
# "env": "dev",
# "llm_model": "claude-3-haiku-20240307",
# "temperature": 0.5,
# "prompt": "What is your favorite food?"
# },
# "var": "d"
# }
Notice that automatic naming with dot notation applies here as well: my_config.llm_model
.
Summary: Key Features and Current Status
- Simple and straightforward API: Writing and reading configuration spaces with Hypster is easy. The API draws inspiration from Optuna’s “define-by-run” and Streamlit’s API. Soon, Hypster will add integrations that leverage these packages to display configuration spaces and perform intelligent hyperparameter optimization.
- DRY (Don’t Repeat Yourself): Hypster’s design minimizes code duplication. Features like automatic parameter naming and convenient defaults in
final_vars
reduce repetitive code, prevent bugs, and make it easier to evolve configurations over time. - Pythonic: Hypster embraces Python’s expressiveness. You can implement conditional and nested logic, and instantiate classes directly within your configuration functions.
- UI-centric: Hypster shines when used with a responsive UI rather than a CLI or YAML files. This approach allows users to visualize available options and their relationships. Upcoming versions will let you interact with Jupyter and Streamlit UI-based configuration spaces.
- Early stage development: Hypster is still young. The API may evolve, and more testing is needed to ensure production reliability. Feedback on GitHub is welcome for improving the framework.
See It in Action
Hypster was built as part of a larger project that enables what I call “hyper-optimized AI workflows”. I’ve written about it here.
Hypster naturally integrates with the DAGWorks Hamilton framework for building and executing DAGs. To see a live demo of how these packages work together, watch the Hamilton meetup recording: YouTube — Hypster + Hamilton workflow demo.