# Примеры MLumina

Подробные разборы примеров кода: examples/ и ray\_wrapper/examples/ — что делает каждый скрипт, как запускать, какие есть известные проблемы.

# Examples Overview

# Обзор примеров

В репозитории MLumina есть два независимых дерева примеров:

- **`examples/`** — самостоятельные скрипты, демонстрирующие каждый пакет фреймворка (`xgboost`, `image_models`, `torch_models`, `interpolation_tables`, `components_wear`), плюс пара скриптов для проверки связи с Ray и планировщиком.
- **`src/mlumina/ray_wrapper/examples/`** — "поваренная книга" готовых пайплайнов Ray Train/Tune (линейная регрессия, SVM, полносвязная сеть, CNN, Vision Transformer), у каждого есть пара `train.py`/`tune.py` для прямого запуска и `schedule_*.py` для запуска через сервис-планировщик.

## Статус примеров

Корневой `run.sh` перечисляет варианты запуска — на момент написания этой документации активна (раскомментирована) только одна строка:

```shell
python3.12 ./examples/components_wear/example_objects_simulate_ray.py
```

Остальные примеры не покрыты CI, но **все скрипты из `examples/` были развёрнуты и прогнаны на локальном Docker-стеке и приведены в рабочее состояние**: `docker compose -f docker-compose-local.yml up -d --build`, затем каждый пример выполнен изнутри кластера (`docker exec`/`docker run` с примонтированным репозиторием). По ходу проверки нашлись и были исправлены баги в самих примерах, в модуле `components_wear`, в `Dockerfile`/`.dockerignore` и в конфигурации MLflow — подробности на страницах отдельных примеров (разделы «Исправлено»/«✅ Проверено»).

## Как запускать

Большинство примеров в `examples/` — обычные Python-скрипты, требующие установленного пакета (`pip install -e .`) и, для Ray-версий, поднятого кластера (`docker compose up -d --build` в корне репозитория, см. книгу «Документация MLumina» → страница Deployment). Скрипты в `ray_wrapper/examples/` дополнительно предполагают запуск изнутри контейнера `head`/`worker`/`scheduler` (пути вида `/home/ray/src/mlumina` в `runtime_env` — это путь внутри контейнера).

## Содержание книги

1. XGBoost Example — полный цикл: загрузка данных, обучение, тюнинг, локально и через Ray.
2. Image Models Example — обучение и инференс YOLO через `YoloImageModel`.
3. Torch Models Example — обучение простой сети и `auto_search`.
4. Interpolation Tables Examples — интерполяция таблиц и сравнение sklearn-моделей.
5. Components Wear Examples — симуляция износа графа компонентов.
6. Scheduler and Ray Basics — минимальные скрипты проверки связи с кластером и планировщиком.
7. Ray Wrapper: Linear Regression and SVM — простейшие шаблоны `ray.tune`.
8. Ray Wrapper: FCNN and CNN — полносвязные и свёрточные сети на MNIST/CIFAR-10.
9. Ray Wrapper: Vision Transformer — ViT на CIFAR-10/CIFAR-100.
10. Running Examples via Scheduler — как отправить любой из примеров в очередь планировщика вместо прямого запуска.

Модульная документация самих API-классов, которые используют эти примеры, — в книге «Документация MLumina».

# XGBoost Example

# XGBoost Example

Файл: `examples/xgboost/example.py`. Самый полный и надёжный пример в репозитории — единственный, где корневой README и код полностью совпадают.

> ✅ **Проверено на локальном Docker-стеке**: и `run_example()`, и `run_example_ray()` отрабатывают до конца, финальная модель логируется в MLflow и реально попадает в S3 на MinIO (подтверждено напрямую через `mc ls`).

Данные: `examples/xgboost/data/train_xgboost.csv` и `test_xgboost.csv` (оба файла присутствуют в репозитории — пример готов к запуску "из коробки"). Конфигурация берётся из `configs/settings.yaml` через `mlumina.core.MLuminaConfig`.

## Что делает скрипт

```python
if __name__ == "__main__":
    run_example()       # локальный цикл: обучение + тюнинг без Ray
    run_example_ray()    # распределённый цикл: обучение + тюнинг через Ray
```

### `run_example()` — локальный путь

1. `get_config()` — читает уже проинициализированный `MLuminaConfig` (инициализация происходит автоматически при импорте `mlumina.core.config`, см. страницу Getting Started в книге «Документация MLumina»).
2. `xgboost_load_dataset(train_path=config.data.train_file, eval_path=config.data.eval_file, ray_data=False)` — загружает `xgb.DMatrix` из CSV, путь к файлам берётся из `configs/settings.yaml` (`data.train_file`/`data.eval_file`).
3. `XGBoostModel(mlflow_uri=config.cluster.mlflow_uri).train(dtrain, deval, **config.training.xgboost.model_dump())` — обучает модель с фиксированными параметрами из секции `training.xgboost` конфига (`objective`, `tree_method`, `subsample`, `colsample_bytree`, `num_boost_round`, `error_func`).
4. `XGBoostTuningModel(...).run_optimization(dtrain, deval)` — подбирает гиперпараметры локальным Optuna (диапазоны — из секции `tune.xgboost` конфига).
5. Переобучает модель на лучших параметрах (`xgboost_tuning_model.train(...)`) — отдельно от автоматического переобучения, которое `run_optimization` уже делает сама внутри себя при включённом MLflow.

### `run_example_ray()` — распределённый путь

То же самое, но:

- `ray.init(address=f"ray://{config.cluster.head_host}")` — подключение к Ray-кластеру по адресу из конфига (`cluster.head_host`, например `head:10001` внутри Docker-сети).
- `xgboost_load_dataset(..., ray_data=True)` (по умолчанию) — данные загружаются как `RayDMatrix`.
- `XGBoostModel.train_ray(...)` и `XGBoostTuningModel.run_optimization_ray(...)` — распределённое обучение/тюнинг через `xgboost_ray` и `ray.tune`.

## Как запустить

Внутри контейнера с доступом к Ray-кластеру (например, `docker compose exec head bash`, либо через сервис `scheduler`):

```shell
python examples/xgboost/example.py
```

Перед запуском убедитесь, что `configs/settings.yaml` указывает на существующие файлы (`data.train_file`/`data.eval_file` — тип `pydantic.FilePath`, при отсутствии файла на диске конфигурация не проинициализируется вовсе, упадёт при импорте `mlumina.core`).

## На что обратить внимание

- В `run_example()` `xgboost_tuning_model.train(...)` в конце вызывается на экземпляре, хотя `XGBoostModel.train` — `@staticmethod` (Python это допускает); работает корректно, просто нетипичный стиль вызова.
- При включённом `mlflow_uri` (он передаётся автоматически из `config.cluster.mlflow_uri`) `run_optimization`/`run_optimization_ray` теперь сами логируют финальную модель в MLflow — соответственно, ручное переобучение в конце `run_example()`/`run_example_ray()` частично дублирует эту работу (не ошибка, просто модель обучается дважды: один раз внутри тюнинга для лога в MLflow, второй раз — явно в самом скрипте).

## Исправленный баг: `XGBoostTuningModel.__init__() missing num_boost_round`

Изначально оба вызова `XGBoostTuningModel(...)` (в `run_example()` и `run_example_ray()`) падали с

```
TypeError: XGBoostTuningModel.__init__() missing 1 required positional argument: 'num_boost_round'
```

Причина: конструктор передавал параметры через `**config.tune.xgboost.model_dump()`, а `config.tune.xgboost` (`XGBoostTuneSettings`) не содержит поля `num_boost_round` — оно есть только в `config.training.xgboost` (`XGBoostTrainSettings`). Исправлено — в оба вызова добавлен `num_boost_round=config.training.xgboost.num_boost_round`.

## MLflow: версия клиента и сервера

Логирование модели в `run_optimization`/`run_optimization_ray` требует, чтобы версия пакета `mlflow` у клиента (в этом образе) и у `mlflow-server` совпадала — до фикса сервер был закреплён на `mlflow==2.21.1` (`mlflow/requirements.txt`), а клиент ставился без пина и получал `3.16.1`. MLflow 3.x использует новый API логирования моделей (`/api/2.0/mlflow/logged-models`), которого нет на сервере 2.x — `mlflow.xgboost.log_model(...)` падал с `404`. Сервер обновлён до `3.16.1`, версии синхронизированы.

# Image Models Example

# Image Models Example

Файлы: `examples/image_models/train.py` и `predict.py`. Демонстрируют `YoloImageModel` на подвыборке COCO (4 изображения, `examples/image_models/data/`).

> ✅ **Проверено на локальном Docker-стеке**: оба скрипта отрабатывают до конца (`train.py` дообучает все 3 модели, `predict.py` строит предсказания и сохраняет визуализации). Датасет и Docker-образ были сломаны — оба фикса описаны ниже.

## `train.py` — обучение всех pretrained-моделей в папке

```python
model_files = list(pretrained_dir.glob("*.pt"))   # examples/image_models/pretrained_models/*.pt

for model_path in model_files:
    base_model = YOLO(str(model_path))
    wrapper = YoloImageModel(base_model)
    wrapper.train(data=str(dataset_yaml), epochs=5, imgsz=640)
    wrapper.save(str(trained_dir / f"{model_path.stem}_trained.pt"))
```

В репозитории уже лежат три готовые pretrained-модели (`yolo11n.pt`, `yolov5nu.pt`, `yolov8n.pt` в `pretrained_models/`) — скрипт дообучает каждую на локальном мини-датасете (5 эпох) и сохраняет результат в `trained_models/`. Там же уже лежат готовые `*_trained.pt` — то есть скрипт был прогнан раньше, и его можно перезапускать повторно (перезапишет результат).

## `predict.py` — инференс всеми обученными моделями

```python
trained_models = list(trained_dir.glob("*.pt"))
for model_path in trained_models:
    model = YOLO(str(model_path))
    wrapper = YoloImageModel(model)
    results = wrapper.predict(str(image_path), save=False)
    results[0].plot(save=True, filename=str(out_img))
```

Берёт одно фиксированное изображение (`data/images/000000000009.jpg`), прогоняет через каждую модель из `trained_models/` и сохраняет визуализацию в `predictions/` (`{model}_prediction.jpg`) — готовые примеры результатов уже есть в репозитории.

## Исправлено: абсолютные пути в датасете + недостающая системная библиотека

Раньше `examples/image_models/data/dataset.yaml` содержал абсолютные пути конкретной машины разработчика:

```yaml
train: /home/niashin/mephi/mlumina/examples/image_models/data/images
val: /home/niashin/mephi/mlumina/examples/image_models/data/images
```

Заменил на `train: images` / `val: images` без ключа `path:`. Важный нюанс, из-за которого первая попытка (`path: .`) не сработала: ultralytics резолвит `path:` **относительно текущей рабочей директории процесса** (если такой путь физически существует — а `.` существует всегда), а не относительно самого yaml-файла. Если `path:` в датасете вовсе не задан, ultralytics использует явный fallback — директорию самого yaml-файла, независимо от CWD запускающего процесса. Поэтому правильное решение — **не указывать `path:`**, а не указывать его как `.`.

Отдельно от датасета в самом Docker-образе не хватало системных библиотек `libgl1`/`libglib2.0-0`, из-за чего `import cv2` (тянется через `ultralytics`) падал с `ImportError: libGL.so.1: cannot open shared object file`. Добавлено в `Dockerfile`.

## Как запустить

```shell
python examples/image_models/train.py     # дообучение — работает из любой рабочей директории
python examples/image_models/predict.py    # инференс уже обученными моделями
```

Оба скрипта используют `YoloImageModel` в однопроцессном режиме (`train`/`predict`, не `train_ray`/`predict_ray`) — Ray не требуется и не инициализируется.

# Torch Models Example

# Torch Models Example

Файлы: `examples/torch_models/train.py` и `predict.py`.

> ✅ **Проверено на локальном Docker-стеке**: оба скрипта отрабатывают до конца. Недостающие данные (см. ниже) добавлены в репозиторий.

## Исправлено: недостающий файл данных

Оба скрипта читают `examples/torch_models/data/data.csv`:

```python
df = pd.read_csv(data_dir / "data.csv", sep=";")
X = df.drop("class", axis=1).values
y = df["class"].values
```

Папки `examples/torch_models/data/` в репозитории не было — файл данных отсутствовал, запуск падал с `FileNotFoundError`. Добавлен `examples/torch_models/data/data.csv` — синтетический датасет из 180 строк, 3 класса (по 60 строк каждый), 4 числовых признака (`f1`…`f4`, гауссовы "блобы" вокруг разных центров) и колонка `class` (0/1/2), разделитель `;`. Уже обученные веса (`models/model.pt`, `models/model_automl.pt`) в репозитории тоже есть и теперь соответствуют этим данным — оба файла перезаписываются при каждом запуске `train.py`.

## `train.py`

```python
class SimpleNet(nn.Module):
    def __init__(self, input_dim, num_classes):
        super().__init__()
        self.fc1 = nn.Linear(input_dim, 512)
        self.dropout1 = nn.Dropout(0.3)
        self.fc2 = nn.Linear(512, 256)
        self.dropout2 = nn.Dropout(0.3)
        self.fc3 = nn.Linear(256, num_classes)
    # forward: fc1 -> ReLU -> Dropout -> fc2 -> ReLU -> Dropout -> fc3

wrapper = TorchModelWrapper(model)
wrapper.learn(X_train=X, y_train=y, epochs=20, batch_size=4, lr=1e-3)
wrapper.save(models_dir / "model.pt")

wrapper_aml = TorchModelWrapper.auto_search(X, y)
wrapper_aml.save(models_dir / "model_automl.pt")
```

Сначала обучает вручную заданную трёхслойную сеть (512→256→classes, dropout 0.3), сохраняет как `model.pt`. Затем отдельно прогоняет `TorchModelWrapper.auto_search(X, y)` — полный перебор 54 архитектур (см. страницу Torch Models в книге «Документация MLumina») — и сохраняет лучшую как `model_automl.pt`. Обратите внимание: `batch_size=4` в ручном обучении — очень маленький батч, подходит только для небольших демонстрационных датасетов.

## `predict.py`

Пересоздаёт **точно такую же** архитектуру `SimpleNet` (руками, не через сериализацию структуры — `TorchModelWrapper.save` сохраняет только `state_dict`, не саму архитектуру), загружает веса `model.pt` и делает предсказания:

```python
model = SimpleNet(input_dim, num_classes)
state = torch.load(models_dir / "model.pt", map_location="cpu")
model.load_state_dict(state)

wrapper = TorchModelWrapper(model, device="cpu")
probs = wrapper.predict(X, return_probs=True)
labels = wrapper.predict(X, return_probs=False)
```

Важно: `input_dim`/`num_classes` в `predict.py` вычисляются заново из CSV — если структура данных при инференсе отличается от той, на которой обучалась модель (другое число признаков/классов), `load_state_dict` упадёт из-за несовпадения размерностей слоёв. Здесь нет сохранения/восстановления архитектуры "из коробки" — это ограничение `TorchModelWrapper.save`, а не только примера.

# Interpolation Tables Examples

# Interpolation Tables Examples

Файлы: `examples/interpolation_tables/interpolation_table.py`, `models_evaluation.py`, `models_evaluation_ray.py`.

> ✅ **Проверено на локальном Docker-стеке**: все три скрипта отрабатывают до конца. Недостающие данные и ошибка выбора метрики (см. ниже) исправлены.

## Исправлено: недостающий файл данных

Каждый скрипт читает `examples/interpolation_tables/data/table_example.csv`:

```python
csv_path = os.path.join(os.path.join(os.path.dirname(__file__), "data"), "table_example.csv")
df = pd.read_csv(csv_path, sep=";")
```

Папки `examples/interpolation_tables/data/` в репозитории не было. Добавлен `examples/interpolation_tables/data/table_example.csv` — регулярная сетка 5×4 (20 строк): `x` ∈ {1000, 3000, 5000, 7000, 9000}, `y` ∈ {200, 600, 1000, 1400} (охватывает точку запроса `(4723, 937)` из `interpolation_table.py` и даёт минимум 4 точки на ось, нужные `RegularGridInterpolator` с `method="cubic"`), `z` — гладкая функция от `x`/`y`, плюс колонка `target` (для `models_evaluation*.py`, где `X = df[["x","y","z"]]`, `y = df["target"]`) с небольшим шумом.

## `interpolation_table.py`

```python
interp = TableInterpolator(df, x_col="x", y_col="y", z_col="z")
value = interp.get(4723, 937)
Xg, Yg, Zg = interp.get_grid(step_x=50, step_y=50)
interp.plot_surface(step_x=50, step_y=50)
```

Прямое повторение примера из README/документации модуля — единственный кусок кода в проекте, который дословно совпадает с `make_docs/source/introduction/what_is_mlumina.rst`.

## `models_evaluation.py` — сравнение моделей без Ray

Последовательно демонстрирует все функции модуля `interpolation_tables.models_evaluation`, на train/test-разбиении первых 15 / оставшихся строк CSV:

1. `evaluate_model(LinearRegression(), X_train, y_train, X_test, y_test)` — обучение + метрики одной модели.
2. `grid_search_model(DecisionTreeRegressor, {"max_depth": (2,5,1), "max_features": (1,3,1)}, ..., optimize_metric="mse")` — перебор сетки для одного класса модели.
3. `compare_multiple_models([...], ..., optimize_metric="mse")` — сравнение 4 моделей (`SVR`, `DecisionTreeRegressor`, `LinearRegression`, `XGBRegressor`), каждая со своей сеткой гиперпараметров; замеряется время выполнения.
4. `hyperopt_search_model(DecisionTreeRegressor, {"max_depth": (2,10,1), "min_samples_split": (2,20,1)}, ..., max_evals=40, optimize_metric="mse")` — TPE-поиск вместо полного перебора.

## `models_evaluation_ray.py` — распределённая версия

То же самое, но `compare_multiple_models_ray([...], ..., optimize_metric="mse")` — каждая из 4 моделей оптимизируется в отдельной Ray-задаче.

## Исправлено: неверная метрика оптимизации

Раньше пример использовал `optimize_metric="r2"`. Но `grid_search_model`/`compare_multiple_models_ray` внутри всегда выбирают результат с **наименьшим** значением метрики (`if score < best_score`) — это корректно для `mse`/`rmse`/`mae` (меньше — лучше), но **не корректно для `r2`** (для R² лучше — *больше*, в идеале ближе к 1; с `"r2"` выбиралась бы модель с наименьшим/наиболее отрицательным R², т.е. фактически худшая по этой метрике). Заменено на `optimize_metric="mse"` — с ним обе версии (без Ray и с Ray) стабильно выбирают одну и ту же (действительно лучшую) модель `LinearRegression`.

Ни `grid_search_model`, ни `compare_multiple_models`/`compare_multiple_models_ray` по-прежнему не поддерживают направление оптимизации (`mode="min"/"max"`) — используйте только `mse`/`rmse`/`mae`-подобные метрики в `optimize_metric`, не `r2`.

## Как запустить

```shell
python examples/interpolation_tables/interpolation_table.py
python examples/interpolation_tables/models_evaluation.py
python examples/interpolation_tables/models_evaluation_ray.py   # требует ray.init() внутри скрипта — работает и локально (без кластера)
```

# Components Wear Examples

# Components Wear Examples

Файлы: `examples/components_wear/example.py`, `example_objects_simulate.py`, `example_objects_simulate_ray.py`. Данные: `examples/components_wear/data/graph.json` (граф из двух узлов A/B, по 4 компонента в каждом, с одной зависимостью B→A) и `data/object.json` (конкретный объект: компонент `A2` с износом 22000 и `B1` с износом 63000).

`example_objects_simulate_ray.py` — **единственный пример во всём репозитории, явно включённый в `run.sh`** (все остальные строки там закомментированы).

> ✅ **Проверено на локальном Docker-стеке**: все три скрипта отрабатывают до конца — включая `example.py`, который раньше падал (см. ниже).

## `example.py`

```python
graph = ComponentsGraph(graph_test)
component_object = ComponentsObject(component_object_test)

print(component_object())
print(component_object.get_probabilities(graph))
print(graph.simulate_objects_bn())

component_object.update_wear(graph, 10000)
print(component_object())
print(component_object.get_probabilities(graph))

result = component_object.predict_fix(graph, target_wear=20_000, step=1_000)
for row in result[1]:
    print(row)
```

### Исправлено: два независимых бага

1. **Неверный импорт.** Скрипт импортировал `from src.mlumina.components_wear import ...` — такого пакета `src` в установленном окружении нет (`mlumina` ставится как обычный пакет, без префикса `src`), скрипт падал сразу на старте с `ModuleNotFoundError: No module named 'src'`. Исправлено на `from mlumina.components_wear import ...`, как во всех остальных примерах.
2. **Опечатка в самом фреймворке.** После фикса импорта скрипт всё равно падал на строке `component_object.update_wear(graph, 10000)`: `ComponentsObject.update_wear()` и `.predict_fix()` вызывали несуществующий метод `self._get_wear_dict()`, тогда как в классе был определён `_get__dict()` (опечатка, двойное подчёркивание). Метод переименован в `_get_wear_dict` — сейчас `update_wear`/`predict_fix` работают как задокументировано.

Теперь весь скрипт, включая `update_wear`/`predict_fix`, отрабатывает от начала до конца.

## `example_objects_simulate.py` — рабочий пример, без Ray

```python
graph = ComponentsGraph(graph_test)
brakes = graph.simulate_objects(n_objects=10000, total_wearout=10000000, step=1000, seed=42, fix_coeff=1.0)

print(f"Broken parts: {len(brakes)}")
print(brakes.head())
```

Симулирует 10 000 независимых объектов на графе из `graph.json`, каждый "проезжает" 10 000 000 условных единиц износа с шагом 1000. Возвращает `pandas.DataFrame` со всеми зафиксированными отказами (колонки `failed_node`, `wear`, `all_parts`). Объект, загруженный из `object.json` (`component_object_test`), в этом скрипте создаётся, но не используется — вся работа идёт через `graph.simulate_objects(...)`, который сам генерирует случайные объекты внутри себя (см. страницу Components Wear в «Документация MLumina»: `simulate_objects` не принимает готовый `ComponentsObject`, а строит собственные случайные объекты на каждой итерации).

При таком объёме (10 000 объектов × 10 000 шагов) выполнение занимает заметное время на одном процессе — на локальном тесте (1 CPU в контейнере) это было около 6.5 минут, результат — 11 944 зафиксированных отказа. Отсюда мотивация для следующего примера.

## `example_objects_simulate_ray.py` — тот же сценарий через Ray

```python
ray.init("ray://localhost:10001")
...
brakes = graph.simulate_objects_ray(n_objects=10000, total_wearout=10000000, step=1000, seed=42, fix_coeff=1.0)
```

Идентичные параметры симуляции, но каждый из 10 000 объектов считается в отдельной Ray-задаче (`@ray.remote`) — распараллеливается на все воркеры кластера. Подключается напрямую к Ray Client Server (`ray://localhost:10001` — порт `HEADNODEPORT` из `.env`, при запуске не из Docker-сети нужно, чтобы этот порт был проброшен на хост, что и происходит в `docker-compose.yml`). На локальном тесте отработал за ~5 минут, 12 030 отказов; в середине прогона несколько Ray-воркеров были убиты out-of-memory killer'ом (конкурировал за память с параллельно запущенным тяжёлым тестом на том же урезанном локальном стенде) — Ray автоматически повторил упавшие задачи, и скрипт всё равно успешно завершился без вмешательства.

## Как запустить

```shell
# без Ray (медленнее, но не требует кластера)
python examples/components_wear/example_objects_simulate.py

# с Ray (нужен поднятый кластер, порт HEADNODEPORT=10001 должен быть доступен)
python examples/components_wear/example_objects_simulate_ray.py

# example.py — теперь тоже полностью рабочий, включая update_wear/predict_fix
python examples/components_wear/example.py
```

# Scheduler and Ray Basics

# Scheduler and Ray Basics

Два минимальных скрипта для проверки, что инфраструктура (Ray-кластер, планировщик) вообще работает — без них имеет смысл начинать отладку любого другого примера, если что-то не запускается.

## `examples/ray_example.py` — проверка связи с Ray-кластером

```python
ray.init(address="ray://localhost:10001")

print(f"{len(ray.nodes())} nodes, {ray.cluster_resources()['CPU']} CPU total")

@ray.remote
def f():
    time.sleep(0.001)
    return socket.gethostbyname(socket.gethostname())

object_ids = [f.remote() for _ in range(10000)]
ip_addresses = ray.get(object_ids)

for ip_address, num_tasks in Counter(ip_addresses).items():
    print(f"{num_tasks} tasks on {ip_address}")
```

Подключается к Ray Client Server по адресу `ray://localhost:10001` (порт `HEADNODEPORT`), печатает число узлов и суммарные CPU-ресурсы кластера, затем запускает 10 000 тривиальных удалённых задач и показывает, сколько из них выполнилось на каждом IP-адресе — наглядно демонстрирует, что worker'ы реально участвуют в распределении нагрузки, а не всё выполняется на одном узле.

Запуск: `python examples/ray_example.py` (с хоста, где проброшен порт 10001 — по умолчанию так и есть в `docker-compose.yml`).

## `examples/scheduler/test.py` — тестовая задача для планировщика

```python
import time
for i in range(30):
    print(i)
    time.sleep(1)
```

Ничего специфичного для mlumina — просто скрипт на ~30 секунд, который печатает числа. Предназначен не для прямого запуска, а как `entrypoint` для сервиса scheduler (`mlumina.scheduler`, порт 8264, см. книгу «Документация MLumina»): удобная минимальная нагрузка, чтобы проверить весь путь постановки задачи в очередь, её запуска планировщиком и завершения, не тратя время на реальное обучение модели.

Пример отправки этой задачи в планировщик (аналогично `ray_wrapper/examples/*/schedule_*.py`, см. следующие страницы книги):

```python
import requests

requests.post(
    "http://127.0.0.1:8264/add_task",
    json={
        "name": "scheduler_smoke_test",
        "entrypoint": "python examples/scheduler/test.py",
        "runtime_env": {"working_dir": "/home/ray/src/mlumina"},
        "priority": 1,
        "cpu": 1,
        "gpu": 0,
        "memory": 1,
        "timeout": 60,
    },
    timeout=60,
)
```

После постановки в очередь прогресс можно отслеживать через `GET /get_running_tasks_list` или веб-дашборд планировщика (`http://<host>:8264/`).

# Ray Wrapper: Linear Regression and SVM

# Ray Wrapper: Linear Regression and SVM

Самые простые примеры из `ray_wrapper/examples/` — хорошая отправная точка, чтобы понять паттерн `ray.tune.Tuner`, используемый во всех остальных моделях пакета `mlumina.ray_wrapper`. У обеих моделей есть только `tune.py` (готовой функции `train_*` без подбора гиперпараметров для них не предусмотрено).

## Linear Regression — `ray_wrapper/examples/lin_reg/tune.py`

```python
ray.init(address="auto")

search_space = {
    "fit_intercept": tune.choice([True, False]),
    "positive": tune.choice([True, False]),
    "test_size": 0.2,
    "random_state": 42,
}

tune_lin_reg(search_space=search_space, num_samples=4, resources={"CPU": 1}, max_concurrent_trials=4)
```

Внутри (`ray_wrapper/models/lin_reg/lin_reg.py`) каждый trial строит sklearn-датасет `diabetes` (`build_sklearn_ray_datasets`, регрессия), обучает `sklearn.linear_model.LinearRegression` с параметрами из `search_space`, репортит `rmse`/`r2`. Отбор — `ASHAScheduler(metric="rmse", mode="min")`. `test_size`/`random_state` передаются как обычные (не варьируемые) значения конфига — `tune.choice`/`tune.loguniform` применяются только к `fit_intercept`/`positive`.

## SVM — `ray_wrapper/examples/svm/tune.py`

```python
ray.init(address="auto")

search_space = {
    "kernel": tune.choice(["rbf", "linear", "poly"]),
    "C": tune.loguniform(1e-2, 1e2),
    "gamma": tune.choice(["scale", "auto"]),
    "degree": tune.choice([2, 3, 4]),
    "class_weight": tune.choice([None, "balanced"]),
    "test_size": 0.2,
    "random_state": 42,
}

tune_svm(search_space=search_space, num_samples=8, resources={"CPU": 1}, max_concurrent_trials=4)
```

В отличие от lin_reg, это **классификация**: внутри (`ray_wrapper/models/svm/svm.py`) каждый trial строит датасет `breast_cancer` (бинарная классификация), обучает `Pipeline(StandardScaler → SVC(kernel, C, gamma, degree, class_weight))`, репортит `accuracy`. Отбор — `ASHAScheduler(metric="accuracy", mode="max")`.

## Как запустить

Оба скрипта вызывают `ray.init(address="auto")` — рассчитаны на запуск **изнутри** кластера (в контейнере `head`/`worker`, где переменная окружения `RAY_ADDRESS`/сокет автообнаружения кластера уже настроены), либо нужно подключаться явно через `ray://<head>:10001`, поправив скрипт:

```shell
# из контейнера head/worker
python ray_wrapper/examples/lin_reg/tune.py
python ray_wrapper/examples/svm/tune.py
```

Результаты (`tune.Tuner.fit()` → `ResultGrid`) сохраняются локально в `/tmp/ray_results/tune_lin_reg_<uuid>` / `tune_svm_<uuid>` на том узле, где физически исполнялся driver-процесс trial'а — при работе на многих worker'ах это может быть не тот узел, с которого был запущен скрипт.

# Ray Wrapper: FCNN and CNN

# Ray Wrapper: FCNN and CNN

Полносвязная сеть (MNIST) и свёрточная сеть (CIFAR-10), обе — через `ray.train` (распределённое обучение) и `ray.tune` (подбор гиперпараметров поверх распределённого обучения).

Датасеты (`MNIST`/`CIFAR-10`) скачиваются автоматически через `torchvision` в `data_config["data_root"]` (`/tmp/mlumina_data/mnist` и `/tmp/mlumina_data/cifar10` в примерах) — при первом запуске узлам кластера нужен доступ в интернет; при повторных запусках данные переиспользуются из этой директории на каждом узле.

## FCNN — `ray_wrapper/examples/fcnn/train.py`

```python
train_config = {"hidden1": 256, "hidden2": 128, "dropout": 0.2, "lr": 1e-3, "epochs": 1, "batch_size_per_worker": 32}
scale_config = {"num_workers": 2, "use_gpu": False}
data_config = {"data_root": "/tmp/mlumina_data/mnist"}

train_fcnn(train_config=train_config, scale_config=scale_config, data_config=data_config)
```

`scale_config` управляет количеством Ray Train воркеров и GPU; `train_config` — гиперпараметрами архитектуры/обучения. Один запуск = одна конфигурация, без перебора.

### `ray_wrapper/examples/fcnn/tune.py`

```python
search_space = {
    "hidden1": tune.choice([128, 256, 512]),
    "hidden2": tune.choice([64, 128, 256]),
    "dropout": tune.choice([0.1, 0.2, 0.3]),
    "lr": tune.loguniform(1e-4, 1e-2),
    "epochs": 1,
    "batch_size_per_worker": tune.choice([16, 32, 64]),
}

tune_fcnn(
    search_space=search_space, num_samples=1,
    scale_config={"num_workers": 2, "use_gpu": False},
    data_config={"data_root": "/tmp/mlumina_data/mnist"},
    driver_resources={"CPU": 1}, max_concurrent_trials=2,
    search_algorithm="hyperopt",
)
```

`search_algorithm="hyperopt"` — использует `mlumina.ray_wrapper.build_search_algorithm` (см. книгу «Документация MLumina», страница Ray Wrapper) вместо дефолтного семплера Ray Tune. `num_samples=1` в примере — по сути демонстрация механики, а не реальный подбор (для реального тюнинга это значение обычно увеличивают на порядки).

## CNN (CIFAR-10) — `ray_wrapper/examples/cnn/train.py`

```python
scale_config = {"num_workers": 2, "use_gpu": False}
train_config = {"c1": 32, "c2": 64, "c3": 128, "lr": 1e-3, "epochs": 3, "batch_size_per_worker": 16}
data_config = {"data_root": "/tmp/mlumina_data/cifar10"}

train_cnn(
    scale_config=scale_config, train_config=train_config, data_config=data_config,
    mlflow_tracking_uri="http://mlflow-server:5001",
    mlflow_experiment_name="mlumina-cnn-cifar10-train",
    mlflow_run_name="cnn_cifar10_train_example",
)
```

В отличие от FCNN, CNN-пример сразу подключает MLflow-логирование (метрики, не модель — см. страницу Deployment/XGBoost в «Документация MLumina» про то, какие пути в фреймворке реально сохраняют артефакты в MLflow/S3, а какие только метрики). `c1`/`c2`/`c3` — число каналов в трёх свёрточных блоках.

`ray_wrapper/examples/cnn/tune.py` устроен аналогично `fcnn/tune.py` — варьирует `c1`/`c2`/`c3`/`lr`/`batch_size_per_worker` через `tune.choice`/`tune.loguniform`.

## Как запустить

Изнутри кластера (`head`/`worker`), с рабочей директорией `src/mlumina`:

```shell
python ray_wrapper/examples/fcnn/train.py
python ray_wrapper/examples/fcnn/tune.py
python ray_wrapper/examples/cnn/train.py
python ray_wrapper/examples/cnn/tune.py
```

Дольше всего в первый раз — скачивание датасета; сама тренировка при `epochs=1`/`3` и `num_workers=2` рассчитана на быстрый прогон-демонстрацию, а не на получение хорошей модели.

# Ray Wrapper: Vision Transformer

# Ray Wrapper: Vision Transformer

Vision Transformer (ViT) на CIFAR-10 и CIFAR-100 — самые тяжёлые примеры пакета `ray_wrapper`, обучаются "с нуля" (без предобученных весов).

## CIFAR-10 — `ray_wrapper/examples/vit/cifar_10/train.py`

```python
scale_config = {"num_workers": 2, "use_gpu": False}
train_config = {
    "num_layers": 6, "num_heads": 4, "hidden_dim": 192, "mlp_dim": 384,
    "lr": 1e-3, "epochs": 1, "batch_size_per_worker": 16,
}
data_config = {"data_root": "/tmp/mlumina_data/cifar10"}

train_vit_cifar10(scale_config=scale_config, train_config=train_config, data_config=data_config)
```

Компактная конфигурация трансформера: 6 слоёв, 4 головы внимания, скрытая размерность 192, MLP-размерность 384 — заметно меньше "классического" ViT-Base (12 слоёв, 768/3072), рассчитана на демонстрацию на CPU/малом кластере, а не на конкурентоспособное качество.

`ray_wrapper/examples/vit/cifar_10/tune.py` подбирает гиперпараметры поверх той же архитектуры аналогично примерам FCNN/CNN (см. предыдущую страницу).

## CIFAR-100 — `ray_wrapper/examples/vit/cifar_100/train.py`

Идентичная конфигурация модели (`num_layers=6`, `num_heads=4`, `hidden_dim=192`, `mlp_dim=384`), но `data_config={"data_root": "/tmp/mlumina_data/cifar100"}` и, соответственно, `train_vit_cifar100` — 100 классов вместо 10. Учитывая ту же компактную архитектуру, что и для CIFAR-10, стоит ожидать заметно более низкое качество на CIFAR-100 (задача сложнее, ёмкость модели та же) — это нормально для демонстрационного примера, не повод считать код сломанным.

## Как запустить

Изнутри кластера (`head`/`worker`):

```shell
python ray_wrapper/examples/vit/cifar_10/train.py
python ray_wrapper/examples/vit/cifar_10/tune.py
python ray_wrapper/examples/vit/cifar_100/train.py
python ray_wrapper/examples/vit/cifar_100/tune.py
```

Как и в FCNN/CNN-примерах, при `use_gpu: False` обучение на CPU при `epochs=1` — это в первую очередь проверка того, что пайплайн работает end-to-end, а не полноценное обучение модели.

# Running Examples via Scheduler

# Running Examples via Scheduler

Для каждой модели в `ray_wrapper/examples/` рядом с `train.py`/`tune.py` лежит `schedule_*.py` — тот же пример, но запускаемый не напрямую, а через постановку задачи в очередь сервиса scheduler (порт 8264, подробнее — книга «Документация MLumina», страница Scheduler).

## Общий паттерн

```python
import requests

SCHEDULER_URL = "http://127.0.0.1:8264/add_task"

task_json = {
    "name": "fcnn",
    "entrypoint": "python ray_wrapper/examples/fcnn/tune.py",
    "runtime_env": {
        "working_dir": "/home/ray/src/mlumina",
        "pip": ["ray", "torch", "tqdm"],
    },
    "priority": 0,
    "cpu": 0,
    "gpu": 0,
    "memory": 1,
    "timeout": 0,
}

response = requests.post(SCHEDULER_URL, json=task_json, timeout=60)
print(response.status_code)
print(response.json())
```

Существующие файлы: `lin_reg/schedule_lin_reg_tune.py`, `svm/schedule_svm_tune.py`, `fcnn/schedule_fcnn_train.py`, `fcnn/schedule_fcnn_tune.py`, `cnn/schedule_cnn_train.py`, `cnn/schedule_cnn_tune.py`, `vit/cifar_10/schedule_cifar10_train.py`, `vit/cifar_10/schedule_cifar10_tune.py`, `vit/cifar_100/schedule_cifar100_train.py`, `vit/cifar_100/schedule_cifar100_tune.py` — по одному на каждую пару train/tune, кроме lin_reg и svm (у них есть только tune).

## На что обратить внимание

- **`working_dir` — путь внутри контейнера**, а не на машине, откуда отправляется HTTP-запрос (`/home/ray/src/mlumina` — это то, что смонтировано в контейнер `scheduler` в `docker-compose.yml`). Отправлять такие запросы имеет смысл из среды, где этот путь существует и виден Ray-кластеру — то есть из контейнеров стека, а не с произвольного ноутбука разработчика.
- **Поле `"name": "fcnn"` захардкожено одинаково во всех `schedule_*.py`**, включая `schedule_lin_reg_tune.py`, `schedule_svm_tune.py`, `schedule_cifar10_train.py` и т.д. — это copy-paste-артефакт, не ошибка запуска: имя используется только для генерации `Task.id` (`mlumina-{name}-priority-...-{uuid}`) и отображения в очереди/дашборде, само задание при этом запускает правильный `entrypoint`. Тем не менее в дашборде планировщика все такие задачи будут выглядеть как "fcnn" независимо от реальной модели — при желании стоит поправить `name` под конкретную задачу перед отправкой.
- **`"cpu": 0, "gpu": 0, "memory": 1`** — это ресурсы, которые *планировщик* резервирует для допуска задачи в очередь (проверка `_check_resources_by_allocation` против `ray.cluster_resources()`), а не то, что реально ограничивает Ray-задачу при выполнении — фактическое потребление ресурсов определяется тем, что делает сам `entrypoint`-скрипт (`scale_config`, `resources=` и т.п. внутри train/tune-функций). `Scheduler._add_task_to_queue` отклоняет задачу только если `cpu == 0 and gpu == 0 and memory == 0` одновременно — здесь `memory=1` этого не допускает, так что все такие задания успешно проходят допуск.
- **`"timeout": 0`** — без ограничения по времени выполнения (планировщик не станет автоматически отменять задачу по таймауту).

## Как отправить задачу

Изнутри контейнера, у которого есть доступ к scheduler по сети (например, `head`/`worker`, либо curl/python с той же Docker-сети):

```shell
python ray_wrapper/examples/fcnn/schedule_fcnn_tune.py
```

Прогресс — через `GET http://<host>:8264/get_task_queue`, `GET /get_running_tasks_list` или веб-дашборд по адресу `http://<host>:8264/`.