Skip to content

jkvc/reqm

Repository files navigation

reqm

Ridiculously Easy Quant Manager

Directory-based config management and object factory built on Hydra.

PyPI CI

Install

pip install reqm

The problem

Hydra is excellent for config-driven instantiation. But using it as a general object factory requires ceremony:

# You have to do all of this just to instantiate one object
with hydra.initialize(config_path="conf"):
    cfg = hydra.compose(config_name="my_model")
    model = hydra.utils.instantiate(cfg.model)

This ceremony means Hydra stays in the lab. It's awkward in a notebook, verbose in a service, and doesn't belong in production call sites.

reqm gives you Hydra's power — config-driven instantiation, composable overrides, recursive object graphs — with none of the ceremony:

from reqm import QuantManager
import my_configs

QM = QuantManager(my_configs)
model = QM.build("summarizer_prod")

Same call in a notebook, a FastAPI endpoint, a test, or a batch job.


Core concept: the Quant

A Quant is the unit reqm builds and manages. It is:

  • Callable — invoked directly with its inputs
  • Config-driven — constructor arguments defined in YAML, no hardcoding
  • Auditable — implements dummy_inputs(), example inputs the factory uses to verify it actually runs
from reqm import Quant
from reqm.overrides_ext import override

class Summarizer(Quant):
    def __init__(self, model_name: str, max_tokens: int):
        self.model = load_model(model_name)
        self.max_tokens = max_tokens

    @override
    def dummy_inputs(self) -> list[dict]:
        return [{"text": "The quick brown fox jumps over the lazy dog."}]

    @override
    def __call__(self, text: str) -> str:
        return self.model.summarize(text, max_tokens=self.max_tokens)

The dummy_inputs contract is what separates a Quant from a plain ABC. reqm can call each Quant with its own dummy inputs at build time — if it fails, it fails early and loudly, not silently in production.


Config modules and QuantManager

A config module is any importable Python package containing YAML files:

my_configs/
├── __init__.py
├── summarizer_prod.yaml
├── summarizer_fast.yaml
└── serving/
    └── prod.yaml

Each YAML config declares what to build:

# @package _global_
_target_: myproject.models.Summarizer
model_name: gpt-4o
max_tokens: 512

QuantManager takes the config module and gives you a uniform API. Construct it once in the __init__.py next to your configs directory, then import QM everywhere:

# myproject/__init__.py (next to configs/)
import myproject.configs as configs
from reqm import QuantManager

QM = QuantManager(configs)
# Any call site — notebook, script, service, test
from myproject import QM

QM.list_configs()          # ["serving/prod", "summarizer_fast", "summarizer_prod"]
QM.validate()              # check all configs have # @package _global_
cfg = QM.get_config("summarizer_prod")   # resolved OmegaConf DictConfig
obj = QM.build("summarizer_prod")        # instantiated object

Configs can compose other configs via Hydra defaults lists:

# @package _global_
defaults:
  - /base_model@child
  - _self_
_target_: myproject.models.Ensemble
weight: 0.6

The uniform call site

The core value proposition: ONE script, swap the config name, get different experimental results. No code changes, no if/else chains, no factory functions.

Construct QM once in the __init__.py right next to your configs directory:

# myproject/__init__.py (lives next to configs/)
import myproject.configs as configs
from reqm import QuantManager

QM = QuantManager(configs)

Then every script just imports it:

import sys
from myproject import QM

model = QM.build(sys.argv[1])       # <-- only this string changes
result = model(text="Hello world")
python evaluate.py summarizer_prod
python evaluate.py summarizer_fast
python evaluate.py summarizer_experiment_v3

Runnable example

The repo includes a complete example project at examples/estimators/ that demonstrates Quant subclasses, non-Quant configurable dependencies (Filters), Hydra config composition, and multiple scripts sharing a single QM instance defined in examples/estimators/__init__.py:

# Evaluate a single estimator config
uv run python -m examples.estimators.scripts.evaluate mean_simple

# Inspect the fully resolved config YAML
uv run python -m examples.estimators.scripts.inspect_config ensemble/mean_median

# Compare multiple configs side by side
uv run python -m examples.estimators.scripts.compare mean_simple mean_outlier median_simple

# Validate all configs
uv run python -m examples.estimators.scripts.validate_configs

# Sweep all configs and rank by performance
uv run python -m examples.estimators.scripts.sweep

PyTorch integration

reqm does not depend on PyTorch, but its primary use case is config-driven model experimentation with nn.Module. The repo includes a TorchQuant bridge class that resolves the __call__ vs forward() conflict — subclass it instead of plain Quant when your model is an nn.Module:

Don't subclass TorchQuant directly for every model — create a domain base class that locks the forward signature for your use case:

from myproject.torch_quant import TorchQuant  # copy from examples/

# Domain base — locks forward(self, x: Tensor) -> Tensor for all regressors
class Regressor(TorchQuant):
    in_features: int

    @override
    @abc.abstractmethod
    def forward(self, x: torch.Tensor) -> torch.Tensor: ...

    @override
    def dummy_inputs(self) -> list[dict]:
        return [{"x": torch.randn(4, self.in_features)}]


# Concrete model — must match the Regressor signature
class LinearRegressor(Regressor):
    def __init__(self, in_features: int):
        super().__init__()
        self.in_features = in_features
        self.linear = nn.Linear(in_features, 1)

    @override
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.linear(x)

TorchQuant.forward uses @allow_any_override so domain bases can narrow freely. Once the domain base locks the signature (without @allow_any_override), all concrete models must match — enforced at class definition time, not runtime.

See docs/torch_integration.md for the full explanation, and examples/torch_models/ for a runnable example:

uv run python -m examples.torch_models.scripts.evaluate linear_simple
uv run python -m examples.torch_models.scripts.evaluate mlp_small
uv run python -m examples.torch_models.scripts.audit

Why not just Hydra?

Hydra is framework-first. It expects to own your program's entry point. reqm is library-first — it has no opinion about your application structure and works wherever Python runs.

Hydra reqm
Object instantiation yes yes
Config composition yes yes (via Hydra)
Auditability (dummy_inputs) no yes
Works in notebooks limited yes
CLI ceremony required yes no

Publishing

To release a new version to PyPI:

  1. Bump the version in pyproject.toml
  2. Build and publish:
uv build
uv publish --token pypi-YOUR_TOKEN

Name

reqm is also a nod to Rue Esquermoise, one of the oldest streets in Lille, France, dating to the 13th century. Its etymology traces to the Flemish eskelm — "frontier." A fitting name for a library that sits at the frontier between research and production.


Status

Core API implemented: Quant, QuantManager, config composition, and override support all work. See examples/estimators/ for a complete working project.

About

Ridiculously Easy Quant Manager — config-based aliased object factory with enforced interfaces

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages