Skip to content

Commit 0789b80

Browse files
feat: conditional framework extras (#176)
* feat: make framework dependencies optional * fix: treat keras as core framework * fix: treat keras as core in retrain
1 parent 01fe5a5 commit 0789b80

12 files changed

Lines changed: 160 additions & 64 deletions

File tree

Dockerfile

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,20 @@ RUN pip install --no-cache-dir --upgrade pip && \
3333
pip install --no-cache-dir poetry && \
3434
poetry config virtualenvs.create false
3535

36-
# Install CPU-only PyTorch (no CUDA/NVIDIA packages - quickstart image includes torch)
37-
# Install before dependency copy so pyproject changes don't invalidate this layer.
38-
RUN pip install --no-cache-dir torch==2.7.1 --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple
39-
40-
# Install main dependencies + AWS support (no Spark provider yet)
36+
# Install large stable dependencies before poetry to maximize build cache reuse.
37+
# INSTALL_PYTORCH controls whether CPU-only PyTorch is installed.
38+
ARG INSTALL_PYTORCH="true"
39+
RUN if [ "$INSTALL_PYTORCH" = "true" ]; then \
40+
pip install --no-cache-dir torch==2.7.1 \
41+
--index-url https://download.pytorch.org/whl/cpu \
42+
--extra-index-url https://pypi.org/simple; \
43+
fi
44+
45+
# Install main dependencies + optional framework extras (no Spark provider yet)
46+
# XGBoost is core; extras cover optional frameworks.
47+
ARG POETRY_EXTRAS="aws catboost"
4148
COPY pyproject.toml poetry.lock /code/
42-
RUN poetry install --only=main --no-root --extras aws
49+
RUN poetry install --only=main --no-root --extras "${POETRY_EXTRAS}"
4350

4451
# Application code
4552
COPY plexe/ /code/plexe/

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,6 @@ test-full: build
250250
--enable-final-evaluation
251251
@echo "✅ Full test passed!"
252252

253-
254253
# ============================================
255254
# Example Datasets
256255
# ============================================
@@ -321,6 +320,7 @@ build:
321320
-f Dockerfile .
322321
@echo "✅ Build complete: plexe:py$(PYTHON_VERSION)"
323322

323+
324324
# Build Databricks variant
325325
.PHONY: build-databricks
326326
build-databricks:

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,18 @@ See [`plexe/integrations/base.py`](plexe/integrations/base.py) for the full inte
178178

179179
### 3.1. Installation Options
180180
```bash
181-
pip install plexe # Core (XGBoost, CatBoost, LightGBM, Keras, PyTorch, scikit-learn)
182-
pip install plexe[pyspark] # + Local PySpark execution
183-
pip install plexe[aws] # + S3 storage support (boto3)
181+
pip install plexe # Core (XGBoost, Keras, scikit-learn)
182+
```
183+
184+
You can add optional dependencies either by framework or by task grouping:
185+
- Framework extras: `catboost`, `lightgbm`, `pytorch`
186+
- Task extras: `tabular` (CatBoost + LightGBM), `vision` (PyTorch)
187+
- Platform extras: `pyspark`, `aws`
188+
189+
Examples:
190+
```bash
191+
pip install "plexe[tabular,pyspark]" # tabular stack + local PySpark
192+
pip install "plexe[pytorch,aws]" # explicit framework + S3 support
184193
```
185194

186195
Requires Python >= 3.10, < 3.13.

plexe/config.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,28 @@ class ModelType:
5050
PYTORCH = "pytorch"
5151

5252

53-
# Default model types (enabled by default, user can override via --allowed-model-types)
54-
def _is_module_available(module_name: str) -> bool:
55-
return importlib.util.find_spec(module_name) is not None
53+
def detect_installed_frameworks() -> list[str]:
54+
"""Detect which ML frameworks are installed and importable.
5655
56+
XGBoost and Keras are always available (core dependencies). Other frameworks are
57+
checked via importlib.util.find_spec() which is fast and has no side effects.
58+
"""
59+
available = [ModelType.XGBOOST, ModelType.KERAS] # Always available (core dependencies)
5760

58-
DEFAULT_MODEL_TYPES = [
59-
ModelType.XGBOOST,
60-
ModelType.CATBOOST,
61-
ModelType.LIGHTGBM,
62-
ModelType.KERAS,
63-
]
64-
if _is_module_available("torch"):
65-
DEFAULT_MODEL_TYPES.append(ModelType.PYTORCH)
61+
_OPTIONAL_FRAMEWORKS = [
62+
("catboost", ModelType.CATBOOST),
63+
("lightgbm", ModelType.LIGHTGBM),
64+
("torch", ModelType.PYTORCH),
65+
]
66+
for module_name, model_type in _OPTIONAL_FRAMEWORKS:
67+
if importlib.util.find_spec(module_name) is not None:
68+
available.append(model_type)
69+
70+
return available
71+
72+
73+
# Default model types (auto-detected from installed packages, user can override via --allowed-model-types)
74+
DEFAULT_MODEL_TYPES = detect_installed_frameworks()
6675

6776
# Task-compatible model types based on data layout
6877
# Maps DataLayout enum to compatible model types

plexe/helpers.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@
1717
if TYPE_CHECKING:
1818
from pyspark.sql import SparkSession
1919

20-
from plexe.config import ModelType, StandardMetric, DEFAULT_MODEL_TYPES, TASK_COMPATIBLE_MODELS
20+
from plexe.config import (
21+
DEFAULT_MODEL_TYPES,
22+
TASK_COMPATIBLE_MODELS,
23+
ModelType,
24+
StandardMetric,
25+
detect_installed_frameworks,
26+
)
2127
from plexe.models import DataLayout
2228

2329
logger = logging.getLogger(__name__)
@@ -47,6 +53,13 @@ def select_viable_model_types(data_layout: DataLayout, selected_frameworks: list
4753
selected = DEFAULT_MODEL_TYPES
4854
logger.info(f"Using default model types: {selected}")
4955
else:
56+
# Validate that user-selected frameworks are actually installed
57+
installed = detect_installed_frameworks()
58+
not_installed = [f for f in selected_frameworks if f not in installed]
59+
if not_installed:
60+
raise ValueError(
61+
f"Requested model types {not_installed} are not installed. " f"Installed frameworks: {installed}"
62+
)
5063
selected = selected_frameworks
5164
logger.info(f"User-selected model types: {selected}")
5265

plexe/retrain.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55
"""
66

77
import inspect
8+
import os
89
import json
910
import logging
1011
import shutil
1112
import tarfile
1213
from pathlib import Path
1314

1415
import joblib
15-
import keras
1616
import pandas as pd
17+
18+
# Ensure keras uses TensorFlow backend even when retrain is invoked directly.
19+
os.environ.setdefault("KERAS_BACKEND", "tensorflow")
20+
import keras
1721
import yaml
1822
from sklearn.model_selection import train_test_split
1923
from xgboost import XGBClassifier, XGBRegressor
@@ -209,7 +213,10 @@ def retrain_model(
209213
logger.info("Created new untrained XGBoost model from architecture")
210214

211215
elif model_type == "catboost":
212-
from catboost import CatBoostClassifier, CatBoostRegressor
216+
try:
217+
from catboost import CatBoostClassifier, CatBoostRegressor
218+
except ImportError:
219+
raise RetrainingError("CatBoost is required for retraining CatBoost models but is not installed")
213220

214221
# CatBoost uses native format — try classifier first, then regressor
215222
try:
@@ -229,7 +236,10 @@ def retrain_model(
229236
logger.info("Created new untrained CatBoost model from architecture")
230237

231238
elif model_type == "lightgbm":
232-
from lightgbm import LGBMClassifier, LGBMRanker
239+
try:
240+
from lightgbm import LGBMClassifier, LGBMRanker
241+
except ImportError:
242+
raise RetrainingError("LightGBM is required for retraining LightGBM models but is not installed")
233243

234244
trained_model = joblib.load(original_model_path)
235245
params = trained_model.get_params()

plexe/tools/submission.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,10 @@ def save_model(model: Any) -> str:
323323
logger.info(f"XGBoost model validated: {type(model).__name__}")
324324

325325
elif model_type == "catboost":
326-
from catboost import CatBoostClassifier, CatBoostRegressor
326+
try:
327+
from catboost import CatBoostClassifier, CatBoostRegressor
328+
except ImportError:
329+
raise ValueError("CatBoost is required for CatBoost models but is not installed")
327330

328331
if not isinstance(model, CatBoostClassifier | CatBoostRegressor):
329332
error_msg = f"Expected CatBoostClassifier or CatBoostRegressor, got {type(model)}"
@@ -333,7 +336,10 @@ def save_model(model: Any) -> str:
333336
logger.info(f"CatBoost model validated: {type(model).__name__}")
334337

335338
elif model_type == "lightgbm":
336-
from lightgbm import LGBMClassifier, LGBMRegressor, LGBMRanker
339+
try:
340+
from lightgbm import LGBMClassifier, LGBMRegressor, LGBMRanker
341+
except ImportError:
342+
raise ValueError("LightGBM is required for LightGBM models but is not installed")
337343

338344
if not isinstance(model, LGBMClassifier | LGBMRegressor | LGBMRanker):
339345
error_msg = f"Expected LGBMClassifier, LGBMRegressor, or LGBMRanker, got {type(model)}"

plexe/workflow.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,15 @@ def build_model(
482482
logger.info("Skipping Phase 6 (already completed)")
483483
final_package_dir = work_dir / "model"
484484

485+
summary_journal = journal or context.scratch.get("_search_journal")
486+
if summary_journal and summary_journal.nodes:
487+
total_attempts = len(summary_journal.nodes)
488+
logger.info(
489+
"Search summary: %s/%s solutions succeeded",
490+
summary_journal.successful_attempts,
491+
total_attempts,
492+
)
493+
485494
logger.info(f"Model building complete! Validation performance: {final_metrics}")
486495
logger.info(f"Final model package: {final_package_dir}")
487496

0 commit comments

Comments
 (0)