Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 12 additions & 5 deletions .claude/skills/add-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ machinery — `get_device()` / `_set_device()` are abstract and the load path ca
(so a non-torch device string like `"gpu"` would crash it). If the model is **not** torch (JAX/Flax,
or any library that manages device placement itself at the process level, e.g. via
`CUDA_VISIBLE_DEVICES` / `jax.devices()`), inherit **`AbstractModel`** even though it runs on GPU, and
just add the GPU resource methods (`_get_default_resources`, `get_minimum_resources`,
`_get_default_ag_args_ensemble` with `sequential_local`, `_class_tags`, `_more_tags`) — do **not**
just add the GPU resource attributes (`default_num_gpus`, `minimum_num_gpus`,
`_default_ag_args_ensemble_extra` with `sequential_local`, plus `_more_tags`) — do **not**
implement `get_device`/`_set_device`. `tabstar/model.py` (a GPU foundation model on `AbstractModel`)
is the reference; `tabfm/model.py` is the JAX example.

Expand Down Expand Up @@ -110,10 +110,17 @@ __all__ = ["gen_{ModelKey}", "{ModelKey}_info", "{ModelKey}_method_metadata"]
The AutoGluon wrapper class. Use the template in `references/model_patterns.md` section "Model wrapper template". Key points:
- Start with `from __future__ import annotations`
- Inherit from `AbstractTorchModel` (torch-based models) or `AbstractModel` (CPU models **and non-torch GPU models** — see Step 2: JAX/Flax etc. use `AbstractModel`)
- Set `ag_key`, `ag_name`, `ag_priority = 65`, `seed_name = "random_state"`
- Implement `_fit()`, `_set_default_params()`, `supported_problem_types()`
- Set `ag_key`, `ag_name`, `ag_priority = 65`, `seed_name = "random_state"`, and
`_supported_problem_types = [...]`
- Implement `_fit()` and `_set_default_params()`
- **Declare config as class attributes, not override methods** (AutoGluon 1.6). Read
`references/model_patterns.md` → "Declare config as class attributes". Overriding
`supported_problem_types()` is the one AutoGluon actively rejects: `verify_model` raises, so the
model's smoke test fails. The others (`_get_default_resources`, `get_minimum_resources`,
`_get_default_ag_args_ensemble`, `_get_default_auxiliary_params`) still work but are the old
style. Never mutate `self.params` / `self.params_aux` after construction — it raises in 1.7.
- **Honor the `_fit` contract** (read `references/model_patterns.md` → "The `_fit` contract"). The most common review findings on new wrappers are: ignoring the provided `X_val`/`y_val` (and instead auto-splitting a second holdout), ignoring `time_limit`, hardcoding the thread count instead of wiring `num_cpus`, and label-encoding + `fillna(0)` categoricals when the library handles them natively. `models/realmlp/model.py` is the reference for all of these. (In-context-learning foundation models have no train loop / no eval set, so they legitimately ignore `time_limit` + `X_val` — see `sap_rpt_oss`/`tabstar`/`tabfm`.)
- For GPU models: also implement `_get_default_resources()`, `get_minimum_resources()`, `_get_default_ag_args_ensemble()` (with `fold_fitting_strategy: sequential_local` — **and `refit_folds: True` for foundation/pre-trained TFMs**; see the "Foundation models: set `refit_folds=True`" note in `references/model_patterns.md`. From-scratch NNs omit it), `_class_tags()` (with `can_estimate_memory_usage_static: False`), `_more_tags()` (with `can_refit_full: True`). **Only torch models** (`AbstractTorchModel`) additionally implement `get_device()` / `_set_device()`; non-torch GPU models on `AbstractModel` must NOT (they have no `.to(device)`).
- For GPU models: also set `default_resources_physical_cores_only = True`, `default_num_gpus = 1`, `minimum_num_gpus = 1`, and `_default_ag_args_ensemble_extra` (with `fold_fitting_strategy: sequential_local` — **and `refit_folds: True` for foundation/pre-trained TFMs**; see the "Foundation models: set `refit_folds=True`" note in `references/model_patterns.md`. From-scratch NNs omit it), plus `_more_tags()` (with `can_refit_full: True`). Do **not** declare a `can_estimate_memory_usage_static` tag: AutoGluon derives it from whether you implement `_estimate_memory_usage_static`. **Only torch models** (`AbstractTorchModel`) additionally implement `get_device()` / `_set_device()`; non-torch GPU models on `AbstractModel` must NOT (they have no `.to(device)`).
- Docstring must include: description, paper title, authors, codebase URL, license
- Keep optional third-party imports (the wrapped library itself) inside `_fit` / per-method scope so importing this module never requires the optional dep at top-level
- Decide the model's untimed **warm-up** (Step 3g) while you have the library docs in hand
Expand Down
171 changes: 92 additions & 79 deletions .claude/skills/add-model/references/model_patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ class {ClassName}Model(AbstractTorchModel):
ag_priority = 65
seed_name = "random_state"

# --- AutoGluon 1.6 declarative config: attributes, not method overrides ---
_supported_problem_types = ["binary", "multiclass", "regression"]
# GPU models only: count physical cores, take one CUDA GPU, and require a whole GPU
# per fit. Drop all three for a CPU model (0 is the inherited default).
default_resources_physical_cores_only = True
default_num_gpus = 1
minimum_num_gpus = 1
# sequential_local avoids crashes when weights are not pre-downloaded and folds fit in
# parallel. Foundation / pre-trained models ALSO set refit_folds (see the note below);
# from-scratch NNs (TabM, RealMLP) omit it.
_default_ag_args_ensemble_extra = {
"fold_fitting_strategy": "sequential_local",
"refit_folds": True,
}

def _fit(
self,
X: pd.DataFrame,
Expand Down Expand Up @@ -114,62 +129,68 @@ class {ClassName}Model(AbstractTorchModel):
for param, val in default_params.items():
self._set_default_param_value(param, val)

@classmethod
def supported_problem_types(cls) -> list[str] | None:
return ["binary", "multiclass", "regression"]

def get_device(self) -> str:
return self.model.device

def _set_device(self, device: str):
self.model.to(device)

def _get_default_resources(self) -> tuple[int, int]:
num_cpus = ResourceManager.get_cpu_count(only_physical_cores=True)
num_gpus = min(1, ResourceManager.get_gpu_count_torch(cuda_only=True))
return num_cpus, num_gpus

def get_minimum_resources(self, is_gpu_available: bool = False) -> dict[str, int | float]:
return {
"num_cpus": 1,
"num_gpus": 1 if is_gpu_available else 0,
}

@classmethod
def _get_default_ag_args_ensemble(cls, **kwargs) -> dict:
"""Set fold_fitting_strategy to sequential_local to avoid crashes
if model weights aren't pre-downloaded when fitting in parallel.

Foundation / pre-trained (in-context-learning) models ALSO set ``refit_folds=True``
here — see the note just below. From-scratch NNs (TabM, RealMLP) omit it.
"""
default_ag_args_ensemble = super()._get_default_ag_args_ensemble(**kwargs)
default_ag_args_ensemble.update(
{
"fold_fitting_strategy": "sequential_local",
# Foundation models only — drop this line for from-scratch NNs.
"refit_folds": True,
},
)
return default_ag_args_ensemble

@classmethod
def _class_tags(cls) -> dict:
# TODO: implement memory estimation and set to True
return {"can_estimate_memory_usage_static": False}

def _more_tags(self) -> dict:
return {"can_refit_full": True}
```

> **Foundation models: set `refit_folds=True` in `_get_default_ag_args_ensemble`.** Every
Note what is *not* in that body. Problem types, resources, minimum resources and ensemble args
are declared as class attributes at the top of the class (see the next section), and the
memory-estimate capability is derived rather than declared.

> **Foundation models: set `refit_folds=True` in `_default_ag_args_ensemble_extra`.** Every
> pre-trained / in-context-learning wrapper (TabPFN, TabICL, LimiX, TabDPT, SAP-RPT-OSS,
> OrionMSP, TabSwift, ...) sets `refit_folds=True` *alongside*
> `fold_fitting_strategy: "sequential_local"`. A TFM has no train loop, so after bagging it
> refits a single model on all the data — much faster to score, at parity with the bagged
> ensemble. **Do not ship a TFM wrapper with only `sequential_local`** (a recurring miss).
> From-scratch NNs (TabM, RealMLP) intentionally omit it and set `can_refit_full=False`.

### Declare config as class attributes (AutoGluon 1.6)

AutoGluon 1.6 replaced a set of override methods with class attributes. Declare the attribute;
do not override the method. Only `_supported_problem_types` is enforced (AutoGluon's
`FitHelper.verify_model` raises on the old override, so `pytest -m models -k <Model>` fails),
but the whole table is the current convention and a new wrapper should follow all of it.

| Do not override | Declare instead |
|---|---|
| `supported_problem_types()` | `_supported_problem_types = [...]` |
| `_get_default_auxiliary_params()` | `_default_auxiliary_params_extra = {...}` |
| `_get_default_ag_args_ensemble()` | `_default_ag_args_ensemble_extra = {...}` |
| `_get_default_resources()` | `default_resources_physical_cores_only` + `default_num_gpus` |
| `get_minimum_resources()` | `minimum_num_gpus` (+ `gpu_required` if the model cannot run on CPU) |

The two `_extra` dicts are merged base-most class first, so a subclass wins over its parent.
That covers the common `super()` + `.update({...})` shape; keep the method only when the body
genuinely needs the parent's resolved value (for example
`refit_folds=parent.pop("refit_folds", True)`) or branches on state. Overriding still works at
runtime for every row except the first, so an inherited wrapper you have not converted is not
broken, just old.

`_default_auxiliary_params_extra` gains a typo guard the override never had: `verify_model`
checks every declared key against the known auxiliary params and fails on an unknown one. A
misspelled key in an overridden `_get_default_auxiliary_params` is silently ignored instead.

**Memory estimation is derived, not declared.** Do not write
`_class_tags() -> {"can_estimate_memory_usage_static": ...}`; AutoGluon reads whether the class
implements `_estimate_memory_usage_static`. And do not write an `_estimate_memory_usage` that
just forwards to the static estimate — that is the base-class default. So the whole memory story
for a new model is: implement `_estimate_memory_usage_static` (and it is on), or don't (and it is
off). Keep `_class_tags` only for other tags, e.g. TabM's `reset_torch_threads`.

**Never mutate `self.params` or `self.params_aux` after construction.** They are resolved
configuration; mutation warns in AutoGluon 1.6 and raises in 1.7. If `_fit` computes a value that
a later call needs, store it on the instance and override the getter. The
`TabPFNv26Model._max_batch_size_resolved` + `_get_max_batch_size()` pair in
`models/tabpfnv2_5/model.py` is the in-repo example; AutoGluon's own pattern references are
`AbstractModel.temperature_scalar` and `AbstractModel._get_max_batch_size`.

### Choosing `AbstractTorchModel` vs `AbstractModel`

`AbstractTorchModel` exists **only** to provide torch device management — `get_device()` /
Expand Down Expand Up @@ -200,31 +221,21 @@ class {ClassName}Model(AbstractModel):
ag_priority = 65
seed_name = "random_state"

_supported_problem_types = ["binary", "multiclass", "regression"]
default_resources_physical_cores_only = True
default_num_gpus = 1
minimum_num_gpus = 1
# refit_folds=True for foundation models (see note above); drop it for from-scratch NNs.
_default_ag_args_ensemble_extra = {
"fold_fitting_strategy": "sequential_local",
"refit_folds": True,
}

def _fit(self, X, y, num_cpus=1, num_gpus=0, **kwargs):
# Validate GPU availability against the actual backend (e.g. jax), not torch.
# Load the (pre-trained) model, build the sklearn-style wrapper, fit.
...

@classmethod
def supported_problem_types(cls): return ["binary", "multiclass", "regression"]

def _get_default_resources(self):
num_cpus = ResourceManager.get_cpu_count(only_physical_cores=True)
num_gpus = min(1, ResourceManager.get_gpu_count_torch(cuda_only=True))
return num_cpus, num_gpus

def get_minimum_resources(self, is_gpu_available=False):
return {"num_cpus": 1, "num_gpus": 1 if is_gpu_available else 0}

@classmethod
def _get_default_ag_args_ensemble(cls, **kwargs):
d = super()._get_default_ag_args_ensemble(**kwargs)
# refit_folds=True for foundation models (see note above); drop it for from-scratch NNs.
d.update({"fold_fitting_strategy": "sequential_local", "refit_folds": True})
return d

@classmethod
def _class_tags(cls): return {"can_estimate_memory_usage_static": False}
def _more_tags(self): return {"can_refit_full": True}
# NOTE: no get_device / _set_device — those are AbstractTorchModel-only.
```
Expand All @@ -241,6 +252,10 @@ class {ClassName}Model(AbstractModel):
ag_name = "TA-{ModelName}"
ag_priority = 65
seed_name = "random_state"
_supported_problem_types = ["binary", "multiclass", "regression"]
# CPU model: no GPU attributes. Set this only if the library benchmarks better on
# physical cores (most GBDTs and NNs do); leave it off to count logical cores.
default_resources_physical_cores_only = True

def _fit(
self,
Expand All @@ -267,14 +282,6 @@ class {ClassName}Model(AbstractModel):
for param, val in default_params.items():
self._set_default_param_value(param, val)

@classmethod
def supported_problem_types(cls) -> list[str] | None:
return ["binary", "multiclass", "regression"]

@classmethod
def _class_tags(cls) -> dict:
return {"can_estimate_memory_usage_static": False}

def _more_tags(self) -> dict:
return {"can_refit_full": True}
```
Expand Down Expand Up @@ -437,13 +444,19 @@ Decision order:

## Memory estimation — implement it for CPU models that fan out across folds

`can_estimate_memory_usage_static: False` with a `# TODO` is fine to *ship*, but for CPU models a
real estimate is what lets the scheduler safely fit cross-validation folds in parallel — a big
usability win that reviewers will ask for. When you can estimate peak memory from
`(n_rows, n_features, n_classes, …)`, implement `_estimate_memory_usage` / a static
`_estimate_memory_usage_static` and flip the tag to `True`. Reference:
Shipping without an estimate is fine (leave `_estimate_memory_usage_static` unimplemented and add
a `# TODO`), but for CPU models a real estimate is what lets the scheduler safely fit
cross-validation folds in parallel — a big usability win that reviewers will ask for. When you can
estimate peak memory from `(n_rows, n_features, n_classes, …)`, implement the classmethod
`_estimate_memory_usage_static`. That single method is the whole opt-in: AutoGluon 1.6 derives
`can_estimate_memory_usage_static` from its presence and the base `_estimate_memory_usage` already
forwards to it, so there is no tag to flip and no instance wrapper to write. Reference:
`autogluon/tabular/src/autogluon/tabular/models/ebm/ebm_model.py` (`_estimate_memory_usage_static`).

GPU models have a parallel hook, `_estimate_gpu_memory_usage_static`, which enables VRAM safety
checks the same way. Without it AutoGluon budgets parallel folds against node RAM, which is why
benchmark runs pass `fake_memory_for_estimates` (see the `benchmark-model` skill).

---

## hpo.py template
Expand Down Expand Up @@ -712,11 +725,11 @@ if self.fixed_random_state is not None:

### max_rows / max_features limits
```python
def _get_default_auxiliary_params(self) -> dict:
default_auxiliary_params = super()._get_default_auxiliary_params()
default_auxiliary_params.update({
"max_rows": 100_000,
"max_features": 2000,
})
return default_auxiliary_params
_default_auxiliary_params_extra = {
"max_rows": 100_000,
"max_features": 2000,
}
```
AutoGluon 1.6 also offers `min_features` / `min_cells` / `max_cells`, and reports a constraint
miss as a skip rather than a failure. Keys are validated, so a typo fails `verify_model`
instead of being silently ignored.
10 changes: 5 additions & 5 deletions .claude/skills/benchmark-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ Given `MODEL`, read the model's contribution under `packages/tabarena/src/tabare
| Derived value | Where to read it | Drives |
|---|---|---|
| **compute** (`"cpu"`/`"gpu"`) | `info.py` → `ModelDescriptor(compute=...)` / `MethodMetadata.compute` | `resources={"num_gpus": 1}` + `name="gpu"` for GPU; drop the override + `name="cpu"` for CPU |
| **problem types** | `model.py` → `supported_problem_types()` (a classmethod returning a subset of `["binary","multiclass","regression"]`, or `None`/**absent** = all types) | the eval `subsets`: all-types → `[[], ["binary"], ["multiclass"], ["regression"]]` (`[]` = the full set / overall leaderboard); regression-only (e.g. Nori) → `[["regression"]]` and scope setup with `task_subset=TaskSubset(subset="regression")` |
| **problem types** | `model.py` → the `_supported_problem_types` class attribute (a subset of `["binary","multiclass","regression"]`; **absent** = all types). Read it via `model_cls.supported_problem_types()` | the eval `subsets`: all-types → `[[], ["binary"], ["multiclass"], ["regression"]]` (`[]` = the full set / overall leaderboard); regression-only (e.g. Nori) → `[["regression"]]` and scope setup with `task_subset=TaskSubset(subset="regression")` |
| **HPO search space** | `info.py` → `search_space` (a `gen_<key>` generator); empty/absent ⇒ no HPO | default `NUM_CONFIGS` (foundation models with no real search space → `0`) |
| **pip extra** | `info.py` → `ModelInfo(pip_extra=...)` | the "install into the run venv" reminder in the docstring + Step 4 |
| **weights prefetch** | `info.py` → `ModelInfo(prefetch_weights=...)` (not `None` ⇒ foundation model) | a docstring note that the checkpoint is fetched from HF by the registry before the fits (no per-script action) |
| **static memory estimate** | `model.py` → `_estimate_memory_usage_static` / `can_estimate_memory_usage_static` | whether `fake_memory_for_estimates` can actually cap fold-parallelism (Step 1a caveat) |
| **static memory estimate** | `model.py` → whether the class implements `_estimate_memory_usage_static` (AutoGluon 1.6 derives `can_estimate_memory_usage_static` from its presence) | whether `fake_memory_for_estimates` can actually cap fold-parallelism (Step 1a caveat) |

Prefer **reading these files** over importing the model (no optional deps needed). If the venv already has the model installed, you may confirm quickly with:
`<PYTHON_PATH> -c "from tabarena.models.utils import get_model_info_from_name as g; i=g('<MODEL>'); print(i.method_metadata.compute, i.pip_extra, i.prefetch_weights, i.model_cls.supported_problem_types())"`
Expand All @@ -63,9 +63,9 @@ it only makes the budget more conservative, which is safe on the VRAM<RAM nodes
40/80/96 GB nodes). **If the partition's VRAM cannot be determined from context, ask the user —
do not guess.**
- **CPU models: never set it** (their estimate must be compared against real RAM).
- **Caveat** — the cap works *through the model's estimate*: if
`can_estimate_memory_usage_static=False` (e.g. TabFM, TabSwift), AutoGluon falls back to a small
data-size estimate and fold-parallelism stays at 8 regardless. Still set the value, but tell the
- **Caveat** — the cap works *through the model's estimate*: a model that does not implement
`_estimate_memory_usage_static` (e.g. TabFM, TabSwift) has no static estimate, so AutoGluon falls
back to a small data-size estimate and fold-parallelism stays at 8 regardless. Still set the value, but tell the
maintainer to sanity-check per-fold VRAM × 8 (or pin `num_folds_parallel` via
`ag_args_ensemble`) before launching.

Expand Down
Loading
Loading