Source code for autogluon.tabular.models.tabdpt.tabdpt_model

from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar

from autogluon.core.constants import BINARY, MULTICLASS, REGRESSION
from autogluon.tabular.models.abstract.abstract_torch_model import AbstractTorchModel

if TYPE_CHECKING:
    import numpy as np
    import pandas as pd


# FIXME: Nick:
#  TODO: batch_size is linear to memory usage
#   512 default
#   should be less for very large datasets
#   128 batch_size on Bioresponse -> 12 GB VRAM
#       Train Data Rows:    2500
#       Train Data Columns: 1776
#       Problem Type:       binary
#  FIXME: Just set context_size = infinity, everything is way faster, memory usage is way lower, etc.
#   Train Data Rows:    100000
#   Train Data Columns: 10
#   binary
#   only takes 6.7 GB during inference with batch_size = 512
# FIXME: Make it work when loading on CPU?
# FIXME: Can we run 8 in parallel to speed up?
# TODO: clip_sigma == 1 is terrible, clip_sigma == 16 maybe very good? What about higher values?
#  clip_sigma >= 16 is roughly all equivalent
# FIXME: TabDPT stores self.X_test for no reason
# FIXME: TabDPT creates faiss_knn even if it is never used. Better if `context_size=None` means it is never created.
# TODO: unit test
# TODO: memory estimate
[docs] class TabDPTModel(AbstractTorchModel): gpu_strongly_recommended: bool = True # in-context inference is 12-63x slower on CPU ag_key = "TABDPT" ag_name = "TabDPT" seed_name = "seed" ag_priority = 50 _supported_problem_types = ["binary", "multiclass", "regression"] default_random_seed = 0 #: Hugging Face repo hosting every TabDPT checkpoint. _hf_repo_id: ClassVar[str] = "Layer6/TabDPT" #: Checkpoint filename in :attr:`_hf_repo_id` to pin via ``model_weight_path``, or None to #: use the installed ``tabdpt`` package's default weights. The tabdpt package can only load #: checkpoints of its own version (e.g. tabdpt 1.2 cannot load the v1.1 checkpoint), so pins #: must match the package the extra installs. _checkpoint_filename: ClassVar[str | None] = None #: Estimator constructor kwargs pinned for this version, mapped to the version's default value #: (resolved from the fit hyperparameters, falling back to the default). Empty -> the installed #: package's defaults. _constructor_defaults: ClassVar[dict[str, object]] = {} #: Predict-time hyperparameters accepted by this version, split by task (``temperature`` / #: ``permute_classes`` are classification-only). _predict_param_names: ClassVar[dict[str, tuple[str, ...]]] = { "classifier": ("context_size", "n_ensembles", "permute_classes", "temperature"), "regressor": ("context_size", "n_ensembles"), } _default_auxiliary_params_extra = { "max_rows": 100000, # TODO: Test >100k rows "max_features": 2500, # TODO: Test >2500 features # TabDPT decomposes a label into base-`max_num_classes` digits above the checkpoint's # output-head width and sums the per-digit log-probabilities # (`tabdpt.classifier._predict_large_cls`), so the head width bounds a forward pass, not # the usable class count. Verified to fit and predict a calibrated distribution at 11, 20, # 100 and 160 classes; the cost is ceil(log_head(n_classes)) passes per prediction. "max_classes": 160, } minimum_num_gpus = 0.5 _default_ag_args_ensemble_extra = { "refit_folds": True, # Sequential fold fitting is much faster for TabDPT: a child fit is near-instant # (in-context model), so parallel fold workers pay far more in per-worker CUDA # context + checkpoint loading than they save. Benchmarked on 1 GPU with an # 8-fold bag: sequential is 2-4x faster than any parallel fold split # (0.125/0.25/0.5 GPU per fold) at both 600 and 30k train rows. "fold_fitting_strategy": "sequential_local", } default_resources_physical_cores_only = True default_num_gpus = 1 def __init__(self, **kwargs): super().__init__(**kwargs) self._predict_hps = None self._use_flash_og = None def _fit( self, X: pd.DataFrame, y: pd.Series, num_cpus: int = 1, num_gpus: int = 0, **kwargs, ): device = self._resolve_fit_device(num_gpus=num_gpus) from tabdpt import TabDPTClassifier, TabDPTRegressor model_cls = TabDPTClassifier if self.problem_type in [BINARY, MULTICLASS] else TabDPTRegressor fit_params, self._predict_hps = self._get_tabdpt_params(num_gpus=num_gpus) X = self.preprocess(X, y=y) y = y.to_numpy() if self._checkpoint_filename is not None: fit_params["model_weight_path"] = self._download_checkpoint() self.model = model_cls( device=device, **fit_params, ) self.model.fit(X=X, y=y) @classmethod def _download_checkpoint(cls) -> str: """Resolve this version's checkpoint to a local path (from cache, else download). Tries the local cache first so prefetched / offline compute nodes skip the etag HEAD-request that ``hf_hub_download`` makes by default. """ from huggingface_hub import hf_hub_download from huggingface_hub.errors import LocalEntryNotFoundError try: return hf_hub_download( repo_id=cls._hf_repo_id, filename=cls._checkpoint_filename, local_files_only=True, ) except LocalEntryNotFoundError: return hf_hub_download(repo_id=cls._hf_repo_id, filename=cls._checkpoint_filename) def _get_tabdpt_params(self, num_gpus: float) -> tuple[dict, dict]: model_params = self._get_model_params() valid_predict_params = ( self.seed_name, "context_size", "permute_classes", "temperature", "n_ensembles", "batch_size", ) predict_params = {} for hp in valid_predict_params: if hp in model_params: predict_params[hp] = model_params.pop(hp) predict_params.setdefault(self.seed_name, self.default_random_seed) predict_params.setdefault("context_size", None) task = "classifier" if self.problem_type in [BINARY, MULTICLASS] else "regressor" supported_predict_params = (self.seed_name, *self._predict_param_names[task]) predict_params = {key: val for key, val in predict_params.items() if key in supported_predict_params} fit_params = model_params for param, default in self._constructor_defaults.items(): fit_params.setdefault(param, default) fit_params.setdefault("verbose", False) fit_params.setdefault("compile", False) if fit_params.get("use_flash", True): fit_params["use_flash"] = self._use_flash(num_gpus=num_gpus) return fit_params, predict_params @staticmethod def _use_flash(num_gpus: float) -> bool: """Detect if torch's native flash attention is available on the current machine.""" if num_gpus == 0: return False import torch if not torch.cuda.is_available(): return False if not torch.backends.cuda.is_flash_attention_available(): return False device = torch.device("cuda:0") capability = torch.cuda.get_device_capability(device) return capability != (7, 5) def _post_fit(self, **kwargs): super()._post_fit(**kwargs) self._use_flash_og = self.model.use_flash return self def get_device(self) -> str: return self.model.device def _set_device(self, device: str): self.model.to(device) if device == "cpu": self.model.use_flash = False self.model.model.use_flash = False else: self.model.use_flash = self._use_flash_og self.model.model.use_flash = self._use_flash_og def _predict_proba(self, X, **kwargs) -> np.ndarray: X = self.preprocess(X, **kwargs) if self.problem_type in [REGRESSION]: y_pred = self.model.predict(X, **self._predict_hps) return y_pred y_pred_proba = self.model.ensemble_predict_proba(X, **self._predict_hps) return self._convert_proba_to_unified_form(y_pred_proba) def _preprocess(self, X: pd.DataFrame, **kwargs) -> np.ndarray: """TabDPT requires a numpy array as input, with missing values left as NaN. TabDPT handles NaN itself and does more with it than impute: it appends a binary missing-indicator column per affected feature before mean-imputing (`tabdpt.estimator`), so the encoding's default -1 for missing would both hide the missingness and put an out-of-range value on the feature's numeric axis. """ X = super()._preprocess(X, **kwargs) X = self._label_encode_categoricals(X, preserve_missing=True) return X.to_numpy() def _more_tags(self) -> dict: return {"can_refit_full": True} # FIXME: This is copied from TabPFN, but TabDPT is not the same @classmethod def _estimate_memory_usage_static( cls, *, X: pd.DataFrame, hyperparameters: dict | None = None, **kwargs, ) -> int: """Heuristic memory estimate based on TabPFN's memory estimate logic in: https://github.com/PriorLabs/TabPFN/blob/57a2efd3ebdb3886245e4d097cefa73a5261a969/src/tabpfn/model/memory.py#L147. This is based on GPU memory usage, but hopefully with overheads it also approximates CPU memory usage. """ # TODO: update, this is not correct anymore, consider using internal TabPFN functions directly. features_per_group = 3 # Based on TabPFNv2 default (unused) n_layers = 12 # Based on TabPFNv2 default embedding_size = 192 # Based on TabPFNv2 default dtype_byte_size = 2 # Based on TabPFNv2 default model_mem = 14489108 # Based on TabPFNv2 default n_samples, n_features = X.shape[0], min(X.shape[1], 500) n_feature_groups = (n_features) / features_per_group + 1 # TODO: Unsure how to calculate this X_mem = n_samples * n_feature_groups * dtype_byte_size activation_mem = n_samples * n_feature_groups * embedding_size * n_layers * dtype_byte_size baseline_overhead_mem_est = 1e9 # 1 GB generic overhead # Add some buffer to each term + 1 GB overhead to be safe memory_estimate = model_mem + 4 * X_mem + 2 * activation_mem + baseline_overhead_mem_est # TabDPT memory estimation is very inaccurate because it is using TabPFN memory estimate. Double it to be safe. memory_estimate = memory_estimate * 2 # Note: This memory estimate is way off if `context_size` is not None return int(memory_estimate)