Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b6be787
add spec
voorhs May 23, 2026
d5d2d29
pull dev
voorhs May 25, 2026
dceb985
upd tech spec
voorhs Jun 5, 2026
94b4e12
add feasibility advisor: CLI script, package, tests; expand proposal
Samoed Jun 9, 2026
c8675b9
fix
Samoed Jun 15, 2026
ad2c3bb
Merge branch 'dev' into feat/feasibility-check
Samoed Jun 15, 2026
f927729
add more handling
Samoed Jun 15, 2026
82a7828
add more handling
Samoed Jun 15, 2026
bbb039e
fix typing & lint
Samoed Jun 16, 2026
4e1966c
Merge branch 'dev' into feat/feasibility-check
Samoed Jun 16, 2026
334783c
try to fix typing
Samoed Jun 16, 2026
4e4da91
roll back config changes
Samoed Jun 16, 2026
8bd0b01
move cli logic
Samoed Jun 16, 2026
b77d575
simplify logic
Samoed Jun 16, 2026
1f1778a
simplify logic
Samoed Jun 16, 2026
e0f1486
remove from init
Samoed Jun 16, 2026
6496b4e
revert pyproject.toml
Samoed Jun 16, 2026
bc3df74
update typing
Samoed Jun 16, 2026
7cb0f53
refactor
Samoed Jun 22, 2026
1841d08
commit missing files
Samoed Jun 24, 2026
b79d83e
Merge branch 'dev' into feat/feasibility-check
Samoed Jun 24, 2026
bfd5e0c
fix typing
Samoed Jun 24, 2026
319d88e
fix typing
Samoed Jun 24, 2026
908b28d
fix test
Samoed Jun 24, 2026
3c676ef
upd
Samoed Jul 6, 2026
570a135
add scripts
Samoed Jul 6, 2026
7eb73f8
upd w&b
Samoed Jul 6, 2026
953262b
improve w&b run name
Samoed Jul 6, 2026
ac8cc67
cap cpu
Samoed Jul 6, 2026
970a7f3
address follow-up review P0/P1/P2 items across advisor + calibrator
Samoed Aug 4, 2026
0dfcd6d
fix _StepTimingCallback missing on_train_begin (and every other HF hook)
Samoed Aug 7, 2026
b8f1b24
improve advisor
Samoed Aug 13, 2026
85848f2
improve calibrate
Samoed Aug 13, 2026
d84b4be
chore: move advisor calibration harness to experiments repo
voorhs Aug 17, 2026
3cb6ca5
fix: drop dangling reference to deleted proposal doc in advisor docst…
voorhs Aug 17, 2026
5444a38
refactor: promote _advisor to public autointent.advisor package
voorhs Aug 17, 2026
b0619a2
refactor: narrow advisor public surface to 15 names
voorhs Aug 17, 2026
751f0e1
fix: reduce_to_fit pruned by VRAM regardless of the binding constraint
voorhs Aug 17, 2026
9e12b0c
refactor: declare preset cost ranking explicitly
voorhs Aug 17, 2026
60d7b23
style: clear mechanical ruff and mypy findings in the advisor
voorhs Aug 17, 2026
14ef1cc
refactor: split _resource_phase and _apply_embedding_cache
voorhs Aug 17, 2026
285160b
feat: make the Pipeline.fit preflight gate opt-in
voorhs Aug 17, 2026
ff76750
docs: add the compute feasibility advisor page
voorhs Aug 17, 2026
070c8ed
Merge remote-tracking branch 'origin/dev' into feat/feasibility-check
voorhs Aug 17, 2026
aa7e9fd
fix: address final review findings
voorhs Aug 17, 2026
7a07530
docs: unwrap hard-wrapped lines in advisor.rst
voorhs Aug 18, 2026
f52d19c
style: apply ruff format to three files that had drifted
voorhs Aug 18, 2026
48109b8
fix: make the advisor read the split config, cv, and CPU count
voorhs Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,12 @@ vector_db*
/wandb
model_output/
my.py
.DS_store
.DS_store

# Local advisor validation artifacts + Superpowers process docs (never shipped)
res-adapt-ckeck/
banking77_*_repredicted.json
claude-issue-investigation.md
review-of-review.md
docs/superpowers/
.python-version
89 changes: 89 additions & 0 deletions docs/source/advisor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
Compute feasibility advisor
===========================

.. note::

**Experimental.** The advisor's estimates are heuristic and calibrated against a limited hardware sample. Treat them as guidance, not guarantees, and read :ref:`advisor-accuracy` before relying on a number. The Python surface may change in a minor release.

Optimizing a search space can take hours and needs more VRAM than a laptop GPU has. The advisor answers "will this fit, and how long will it take?" *before* anything is downloaded or trained.

Command line
------------

Two subcommands. ``inspect`` prices a specific preset or config; ``recommend`` detects your hardware and picks the heaviest bundled preset that still fits.

.. code-block:: bash

# What will transformers-light cost on this machine?
autointent-advisor inspect transformers-light

# ...against a real dataset rather than placeholder sizes
autointent-advisor inspect transformers-light --dataset banking77

# Which preset should I use?
autointent-advisor recommend --dataset banking77

# Machine-readable output
autointent-advisor inspect ./my-config.yaml --json

Without ``--dataset``, the advisor uses placeholder dataset sizes (``--n-samples``, ``--n-classes``, ``--avg-tokens``, ``--task``), so it is useful before you have assembled any data. ``--budget-vram-gb`` overrides hardware detection, and ``recommend`` also accepts ``--budget-time-h``.

Both subcommands exit non-zero when nothing is feasible, so they work as a CI gate.

Reading a report
----------------

Each finding carries a severity:

``ample``
Comfortably within budget.
``tight``
Fits, but with little headroom — expect swapping or thermal throttling.
``over``
Exceeds the budget. Any ``over`` finding makes the whole report infeasible.

The drivers table lists the modules that dominate the cost, so it shows *what* to change. ``low confidence`` on a report means Hub metadata was unavailable or incomplete for at least one model, and conservative large-model defaults were substituted — the numbers are much rougher when you see it.

Findings are not only about hardware. The advisor also prices your ``DataConfig``: it reports ``over`` when a class has too few samples for the stratified split to succeed, using the same minimum as :py:func:`~autointent.context.data_handler.check_split_readiness`, and when ``LogisticRegressionCV`` would not have ``cv`` samples per class *after* the train/validation split. Per-class counts are measured on the train split you supply, so the advisor discounts them by whatever ``validation_size``, ``n_folds``, and ``separation_ratio`` will take away.

From Python
-----------

.. code-block:: python

from autointent import Dataset
from autointent.advisor import dataset_stats, detect_hardware, estimate, recommend

report = estimate("transformers-light")
print(report.is_feasible, report.resource.vram_gb)

result = recommend(stats=dataset_stats(Dataset.from_json(path)))
print(result.chosen)

``reduce_to_fit`` goes further: it prunes the most expensive scoring module repeatedly until the search space fits, raising ``ReduceToFitError`` if nothing does.

Inside ``Pipeline.fit``
-----------------------

``Pipeline.fit`` accepts a ``preflight`` gate. It defaults to ``"off"``, so the advisor never runs unless you ask — it makes network calls to the Hugging Face Hub for model metadata, which does not belong on every fit by default.

.. code-block:: python

pipeline.fit(dataset, preflight="warn") # log findings, always continue
pipeline.fit(dataset, preflight="strict") # raise PreflightError if infeasible

``"strict"`` raises :class:`autointent.advisor.PreflightError` before allocating any VRAM, which is the useful mode in CI.

.. _advisor-accuracy:

How accurate is it?
-------------------

Validated end to end on one machine class (RTX 3060 Laptop, 6 GB VRAM / 16 GB RAM), where all four fitted presets matched their predicted verdict: both ``over`` predictions did run out of memory, and both feasible predictions did fit. Known limits:

- **Feasibility verdicts are the reliable part.** That is what the advisor was built and validated for.
- **VRAM is close but not a guaranteed ceiling.** One preset used 1.22× its prediction. Leave headroom rather than trusting the figure exactly.
- **Wall-time estimates are indicative only.** Measured error has run in both directions across formula revisions, once by more than an order of magnitude for cross-encoders. ``--budget-time-h`` inherits that uncertainty.
- **CPU parallelism is modelled, not measured.** The CPU coefficients are calibrated single-threaded; core count is then applied as a capped Amdahl speedup (higher for CatBoost, which uses every core by default, than for scikit-learn's L-BFGS, which only threads inside BLAS), divided across concurrent ``hpo_config.n_jobs`` trials. It is a correction for the fact that core count used to change nothing at all, not a validated speedup curve.
- **Preset ranking does not depend on time estimates.** ``recommend`` orders presets by a declared cost ranking, so unstable time figures cannot reorder its choice.
- **Only one hardware class has been validated end to end.** Treat other machines as unverified.
4 changes: 4 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ Reference
:doc:`🌐 Inference servers <server>`
Deploy a trained pipeline behind HTTP (FastAPI) or MCP (FastMCP): installation extras, environment variables, and how to run each server.

:doc:`🔍 Compute feasibility advisor <advisor>`
Estimate VRAM, RAM, disk, and wall-time for a search space before training. Run it from the CLI or gate ``Pipeline.fit`` on it.

:doc:`🔧 API Reference <autoapi/autointent/index>`
Complete technical documentation for all classes, methods, and functions. Essential reference for developers integrating AutoIntent into their applications.

Expand All @@ -84,4 +87,5 @@ Reference
user_guides
learn/index
server
advisor
autoapi/autointent/index
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies = [
"aiofiles (>=24.1.0,<25.0.0)",
"threadpoolctl (>=3.0.0,<4.0.0)",
"packaging (>=23.2)",
"psutil (>=5.9.0,<8.0.0)",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -121,6 +122,7 @@ typing = [
"joblib-stubs (>=1.4.2.5.20240918,<2.0.0)",
"pandas-stubs (>= 2.2.3.250527, <3.0.0)",
"types-aiofiles (>=24.1.0.20250606)",
"types-psutil>=7.2.2.20260518",
]
docs = [
"sphinx (>=8.1.3,<9.0.0)",
Expand All @@ -145,6 +147,7 @@ Documentation = "https://deeppavlov.github.io/AutoIntent/"
[project.scripts]
"basic-aug" = "autointent.generation.utterances._basic.cli:main"
"evolution-aug" = "autointent.generation.utterances._evolution.cli:main"
"autointent-advisor" = "autointent.advisor._cli:main"

[build-system]
requires = ["uv_build>=0.8.7,<0.9.0"]
Expand Down
4 changes: 2 additions & 2 deletions src/autointent/_pipeline/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from ._pipeline import Pipeline
from ._pipeline import Pipeline, PreflightMode

__all__ = ["Pipeline"]
__all__ = ["Pipeline", "PreflightMode"]
93 changes: 88 additions & 5 deletions src/autointent/_pipeline/_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal

import numpy as np
import yaml
Expand Down Expand Up @@ -34,10 +34,14 @@

if TYPE_CHECKING:
from autointent import Dataset
from autointent.advisor import Finding, PreflightReport
from autointent.custom_types import ListOfGenericLabels, SearchSpacePreset, SearchSpaceValidationMode
from autointent.modules.base import BaseDecision, BaseRegex, BaseScorer


PreflightMode = Literal["off", "warn", "strict"]


class Pipeline:
"""Pipeline optimizer class.

Expand Down Expand Up @@ -152,6 +156,45 @@ def from_optimization_config(cls, config: dict[str, Any] | Path | str | Optimiza
pipeline.set_config(optimization_config.hpo_config)
return pipeline

def _build_advisor_config(self) -> dict[str, Any]:
"""Reconstruct an ``OptimizationConfig``-shaped dict for the advisor."""
search_space = [
{"node_type": opt.node_type, "search_space": opt.modules_search_spaces}
for opt in self.nodes.values()
if isinstance(opt, NodeOptimizer)
]
return {
"search_space": search_space,
"data_config": self.data_config.model_dump(),
"logging_config": self.logging_config.model_dump(),
"embedder_config": self.embedder_config.model_dump(),
"cross_encoder_config": self.cross_encoder_config.model_dump(),
"transformer_config": self.transformer_config.model_dump(),
"hpo_config": self.hpo_config.model_dump(),
}

def _run_preflight(self, dataset: Dataset, *, refit_after: bool, mode: PreflightMode) -> PreflightReport:
"""Run the advisor against this pipeline's effective config + dataset.

Logs each finding at INFO/WARNING/ERROR (by severity). When ``mode`` is
``"strict"`` and any OVER finding is produced, raises ``PreflightError``.

Imported lazily: the advisor probes the HF Hub, so it must stay off the
``import autointent`` path.
"""
from autointent.advisor import PreflightError, Severity, dataset_stats, detect_hardware, run_preflight

config = self._build_advisor_config()
stats = dataset_stats(dataset)
hardware = detect_hardware()
report = run_preflight(config, stats, hardware, refit_after=refit_after)
_log_preflight_report(report, self._logger)
if mode == "strict":
over: list[Finding] = [f for f in report.findings if f.severity == Severity.OVER]
if over:
raise PreflightError(over)
return report

def _fit(self, context: Context) -> None:
"""Optimize the pipeline.

Expand Down Expand Up @@ -193,22 +236,42 @@ def fit(
dataset: Dataset,
refit_after: bool = False,
incompatible_search_space: SearchSpaceValidationMode = "filter",
preflight: PreflightMode = "off",
) -> Context:
"""Optimize the pipeline from dataset.

Args:
dataset: dataset for optimization.
refit_after: whether to refit on whole data after optimization. Valid only for hold-out validaiton.
sampler: sampler type to use.
incompatible_search_space: wow to handle data-incompatible modules occurring in search space.
incompatible_search_space: how to handle data-incompatible modules occurring in search space.
preflight: **experimental** gate that runs
:func:`autointent.advisor.run_preflight` over the pipeline's
effective config + dataset before any heavy work.
``"off"`` (default) skips it entirely. ``"warn"`` logs findings —
INFO for AMPLE, WARNING for TIGHT, ERROR for OVER — but never
raises; note it probes the HF Hub for model metadata, so it adds
network round-trips. ``"strict"`` additionally raises
:class:`autointent.advisor.PreflightError` when any finding has
severity OVER, so unfeasible runs abort before fit.

Raises:
RuntimeError: If pipeline is in inference mode.
PreflightError: If ``preflight="strict"`` and any OVER finding is produced.
"""
if self._is_inference():
msg = "Pipeline in inference mode cannot be fitted"
raise RuntimeError(msg)

# Filter the search space first: ``validate_modules`` drops modules this
# dataset cannot use (e.g. ``mlknn`` on multiclass, ``dnnc`` and its ~6.4 GB
# reranker on multilabel), and preflight must price what will actually run
# rather than what was requested. It takes ``dataset`` only, so it does not
# depend on the ``Context`` built below.
self.validate_modules(dataset, mode=incompatible_search_space)

if preflight != "off":
self._run_preflight(dataset, refit_after=refit_after, mode=preflight)

context = Context(self._seed)
context.set_dataset(dataset, self.data_config)
context.configure_logging(self.logging_config)
Expand All @@ -218,8 +281,6 @@ def fit(
context.configure_hpo(self.hpo_config)
context.configure_vector_index(self.vector_index_config)

self.validate_modules(dataset, mode=incompatible_search_space)

test_utterances = context.data_handler.test_utterances()
if test_utterances is None:
self._logger.warning(
Expand Down Expand Up @@ -472,3 +533,25 @@ def make_report(logs: dict[str, Any], nodes: list[NodeType]) -> str:
messages = [json.dumps(c, indent=4) for c in configs]
msg = "\n".join(messages)
return "resulting pipeline configuration is the following:\n" + msg


def _log_preflight_report(report: PreflightReport, logger: logging.Logger) -> None:
"""Log each preflight finding at the appropriate level."""
# Imported lazily for the same reason as in ``Pipeline._run_preflight``: the
# advisor probes the HF Hub, so it must stay off the ``import autointent``
# path. Do not hoist to module scope.
from autointent.advisor import Severity

level_for = {
Severity.AMPLE: logging.INFO,
Severity.TIGHT: logging.WARNING,
Severity.OVER: logging.ERROR,
}
header = (
f"Preflight ({report.preset_name or 'pipeline'}): verdict={'feasible' if report.is_feasible else 'INFEASIBLE'}"
)
logger.info(header)
for finding in report.findings:
logger.log(level_for[finding.severity], "[%s] %s", finding.phase, finding.message)
if report.low_confidence:
logger.info("Preflight: low-confidence (heuristic fallback in use)")
37 changes: 37 additions & 0 deletions src/autointent/advisor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Pre-flight compute feasibility advisor.

**Experimental.** This subpackage estimates VRAM, RAM, disk, and wall-time for a
search space before any training starts. Estimates are heuristic and calibrated
against a limited hardware sample, so treat them as guidance rather than
guarantees — see the accuracy caveats in the ``advisor`` page of the docs. The
public surface may change in a minor release.

Two ways in: the ``autointent-advisor`` console script, and the functions below.
``Pipeline.fit(preflight=...)`` wires the same machinery into a fit, opt-in.
"""

from __future__ import annotations

from ._errors import PreflightError
from ._hardware import HardwareProfile, detect_hardware
from ._report import DatasetStats, Finding, PreflightReport, RecommendationResult, ResourceEstimate, Severity
from ._runner import run_preflight
from ._workflows import ReduceToFitError, dataset_stats, estimate, recommend, reduce_to_fit

__all__ = [
"DatasetStats",
"Finding",
"HardwareProfile",
"PreflightError",
"PreflightReport",
"RecommendationResult",
"ReduceToFitError",
"ResourceEstimate",
"Severity",
"dataset_stats",
"detect_hardware",
"estimate",
"recommend",
"reduce_to_fit",
"run_preflight",
]
Loading