# reqm — Ridiculously Easy Quant Manager

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

## The one-line pitch

`QuantManager(config_module).build("config_name")` — same call in a notebook,
a FastAPI endpoint, a test, or a batch job. No Hydra ceremony, no call site
changes when you swap implementations.

## Installation

```bash
pip install reqm
```

## Minimal working example

```python
# 1. Define your Quant — subclass and implement __call__ + dummy_inputs
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)


# 2. Write a config file (my_configs/summarizer_prod.yaml)
#    # @package _global_
#    _target_: myproject.models.Summarizer
#    model_name: gpt-4o
#    max_tokens: 512


# 3. Build it — this line never changes, no matter what config you point to
import my_configs
from reqm import QuantManager

QM = QuantManager(my_configs)
model = QM.build("summarizer_prod")
result = model(text="Explain quantum entanglement simply.")
```

## Key concepts

### Quant
The unit reqm builds and manages. Subclass `reqm.Quant` and implement:
- `__call__(self, **kwargs)` — the inference/computation (narrow the signature)
- `dummy_inputs(self) -> list[dict]` — example inputs for build-time sanity check

`dummy_inputs` is what makes a Quant **auditable**: reqm can call it at build
time to verify the Quant actually runs. If it fails, it fails immediately
with a clear error, not silently in production.

### QuantManager
Directory-based config manager. Takes an importable Python module containing
YAML config files and provides a uniform API. Construct it once in the
`__init__.py` next to your configs directory, then import `QM` everywhere:

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

QM = QuantManager(configs)
```

```python
# Any call site
from myproject import QM

QM.list_configs()                      # ["model_a", "serving/prod"]
QM.validate()                          # check all configs have @package _global_
cfg = QM.get_config("model_a")         # resolved OmegaConf DictConfig
raw = QM.get_raw_config("model_a")     # resolved YAML string
obj = QM.build("model_a")              # instantiated object
```

All methods accept optional `config_overrides` (dict) and `param_overrides`
(Hydra CLI-style strings) for runtime customization.

### Config files
Standard Hydra YAML. The `_target_` key points to your class. All other keys
become constructor arguments. Every config must start with `# @package _global_`.

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

Configs can compose other configs via Hydra defaults lists:

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

## The uniform call site pattern

The core value proposition: ONE script, swap the config name, get different
experimental results. No code changes needed. `QM` is constructed once in
your package `__init__.py` and imported by every script:

```python
import sys
from myproject import QM

model = QM.build(sys.argv[1])       # <-- only this string changes
result = model(text="Hello world")
```

```bash
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 a single `QM` instance shared across scripts:

```bash
uv run python -m examples.estimators.scripts.evaluate mean_simple
uv run python -m examples.estimators.scripts.evaluate mean_outlier
uv run python -m examples.estimators.scripts.evaluate median_simple
uv run python -m examples.estimators.scripts.evaluate trimmed_mean
uv run python -m examples.estimators.scripts.evaluate ensemble/mean_median
```

## Public API

```python
from reqm import Quant, QuantManager, ConfigValidationError

# Define your Quant
class MyQuant(Quant): ...

# Construct once in __init__.py next to configs/
QM = QuantManager(config_module)
QM.list_configs() -> list[str]
QM.validate(config_name=None) -> None
QM.get_config(config_name, *, config_overrides=None, param_overrides=None) -> DictConfig
QM.get_raw_config(config_name, *, config_overrides=None, param_overrides=None) -> str
QM.build(config_name, *, config_overrides=None, param_overrides=None) -> object
```

## PyTorch integration (TorchQuant)

reqm does not depend on PyTorch, but its primary use case is config-driven
model experimentation with `nn.Module`. The problem: `nn.Module.__call__`
runs hooks and calls `forward()` — if you override `__call__` (as plain
Quant requires), you bypass PyTorch's machinery.

The solution is `TorchQuant`, a bridge class. **Copy it into your project**
(full source below — this is the canonical reference):

```python
"""TorchQuant — bridge between nn.Module and Quant.

Copy this file into your project. reqm does not depend on PyTorch,
so this class is not part of the reqm package.
"""

from __future__ import annotations

import abc

import torch.nn as nn

from reqm import Quant
from reqm.overrides_ext import allow_any_override, override


class TorchQuant(nn.Module, Quant):
    """Base class for Quants that are also PyTorch modules.

    Subclasses override forward() (not __call__) to define computation,
    just like any nn.Module. The dummy_inputs() contract from Quant still
    applies — implement it so reqm can verify the model runs at build time.

    Why this class exists:
        nn.Module.__call__ runs forward/backward hooks, autograd profiling,
        then calls self.forward(). Overriding __call__ directly (as plain
        Quant requires) bypasses all of that. TorchQuant resolves this by:

        1. Providing a concrete __call__ that delegates to nn.Module.__call__
           (preserving hooks)
        2. Making forward() the abstract method with @allow_any_override so
           subclasses can narrow its signature freely
    """

    @override
    def __call__(self, **kwargs: object) -> object:
        """Delegate to nn.Module.__call__, preserving hooks and autograd."""
        return nn.Module.__call__(self, **kwargs)

    @override
    @abc.abstractmethod
    @allow_any_override
    def forward(self, **kwargs: object) -> object:
        """Define the model's computation. Subclasses override this."""
        ...
```

Usage — subclass TorchQuant, override `forward()` (not `__call__`):

```python
class MyModel(TorchQuant):
    def __init__(self, hidden_dim: int):
        super().__init__()
        self.linear = nn.Linear(hidden_dim, 1)

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

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

Everything works: hooks fire, gradients flow, `state_dict()` works,
`dummy_inputs()` audits the model at build time.

### Two-level signature narrowing (domain base classes)

`TorchQuant.forward` has `@allow_any_override` — any direct subclass can
narrow the signature freely. But you should NOT have every concrete model
subclass TorchQuant directly. Instead, create a **domain base class** that
narrows `forward` and locks it down for all models in that domain:

```python
class Regressor(TorchQuant):
    """All regressors: forward(self, x: Tensor) -> Tensor."""
    in_features: int

    @override
    @abc.abstractmethod
    def forward(self, x: torch.Tensor) -> torch.Tensor: ...
    # No @allow_any_override — signature is now locked

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

Now concrete models MUST match the locked signature:

```python
class LinearRegressor(Regressor):      # OK
    @override
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.linear(x)

class BadRegressor(Regressor):          # REJECTED at class definition time
    @override
    def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
        return a + b
```

This is the pattern:
1. **TorchQuant** — `forward` is open (`@allow_any_override`)
2. **Domain base** (e.g. Regressor) — narrows `forward`, locks it (no `@allow_any_override`)
3. **Concrete model** — must match the domain base's signature exactly

Without step 2, any model could define `forward` with any signature, and
your uniform evaluation scripts would break at runtime instead of at class
definition time.

For the full working example project (models, configs, scripts), see
https://github.com/jkvc/reqm/tree/main/examples/torch_models

## Why not just Hydra?

Hydra is framework-first — it expects to own your program's entry point.
reqm is library-first: no `@hydra.main`, no `hydra.initialize()`, no context
managers. reqm handles all Hydra plumbing internally. You get config-driven
instantiation and composition without the ceremony.

## Common mistakes

**Don't** call `hydra.initialize()` yourself — reqm handles it.

**Don't** skip `dummy_inputs` — it's not optional. reqm uses it to verify
your Quant actually runs before you're in production.

**Don't** hardcode constructor args in Python — put them in the YAML config.
That's the whole point.

**Don't** forget `# @package _global_` — every YAML config needs this header.
Call `QM.validate()` in your tests to catch missing headers early.
