Обучение на арендованной GPU
Арендованная GPU превращает обучение в задачу с началом, концом и счётом. Работа та же, что и при обучении локально; меняется другое — как загрузить данные, как смотреть снаружи, как забрать веса и как выключить машину.
До того как что-то арендовать
Два решения потом обойдутся дороже, чем сейчас.
Сначала выложите датасет на CDN. Упакованный одним tar-архивом в dataset-репозиторий
на Hugging Face, он одинаково работает у любого провайдера, быстро отдаётся всем и
требует только HF_TOKEN в окружении задачи, если репозиторий приватный. Загрузка
датасета с домашнего канала или его скачивание на машину из медленного источника —
это оплаченное время GPU, потраченное на ожидание.
tar cf my-dataset.tar my-dataset/huggingface-cli upload my-org/my-dataset my-dataset.tar --repo-type datasetimport tarfile from huggingface_hub import hf_hub_download path = hf_hub_download( "my-org/my-dataset", "my-dataset.tar", repo_type="dataset")with tarfile.open(path) as archive: archive.extractall("/root/data")Потом подберите размер диска. Провайдеры, которые тарифицируют хранилище, считают по выделенному объёму, а не по использованному, и уменьшить диск после создания нельзя. Сложите подготовленные данные, чекпойнты и примерно 30 процентов запаса — и на этом остановитесь.
Установка на машине
pip install libreyolo # Добавляйте только те extras, которые нужны запуску. rfdetr — для обучения# RF-DETR, lora — для параметр-эффективного дообучения, onnx — для экспорта потом.pip install "libreyolo[rfdetr,lora]"import torch print(torch.__version__, torch.cuda.is_available())print(torch.cuda.get_device_name(0)) # Wheel, собранный под другую архитектуру, вернёт True, а потом упадёт# на первом же настоящем ядре, поэтому запустите одно.x = torch.rand(2000, 2000, device="cuda")print(float((x @ x).sum()))Если в образе ещё нет сборки PyTorch с CUDA, подходящей для карты, сначала поставьте
PyTorch, а потом LibreYOLO — тогда pip не подтянет собственную CPU-сборку torch. Второй
сниппет — не лишняя формальность: wheel, собранный под неподходящую архитектуру
GPU, сообщает torch.cuda.is_available() == True, а потом падает на первой же
настоящей операции с CUDA error: no kernel image is available for execution on the device. Одно матричное умножение ловит это раньше, чем вы потратите на настройку час.
Если провайдер даёт том, направьте HF_HOME в постоянное хранилище, чтобы скачанные
чекпойнты и датасеты сохранялись между запусками.
Запуск
Запускайте задачу отдельно от сессии. Интерактивная сессия умирает вместе с сетевым соединением и уносит обучение с собой.
nohup libreyolo train \ model=LibreYOLO9s.pt \ data=/root/data/my-dataset/data.yaml \ epochs=100 batch=-1 imgsz=640 \ project=/root/runs name=run1 \ > /root/train.log 2>&1 &from libreyolo import LibreYOLO if __name__ == "__main__": model = LibreYOLO("LibreYOLO9s.pt") model.train( data="/root/data/my-dataset/data.yaml", epochs=100, batch=64, # общий батч на все GPU device="0,1,2,3", project="/root/runs", name="run1", )Именно здесь batch=-1 особенно полезен, потому что карта обычно незнакомая — на ней
вы ещё не обучали. Он прогоняет модель в режиме обучения с настоящим обратным проходом
и берёт наибольшую степень двойки, которая влезает, — это быстрее, чем выяснять потолок
по ошибке нехватки памяти через двадцать минут после старта. См.
Гиперпараметры.
На машине с несколькими GPU device="0,1,2,3" сам поднимает по одному воркеру на
каждую GPU, а batch остаётся общим батчем на все. Защита __main__ обязательна,
потому что каждый воркер заново импортирует скрипт. Об этом и об остальном
распределённом поведении — Обучение на нескольких GPU.
Наблюдение снаружи
Каждый запуск пишет status.json в свою директорию запуска и атомарно перезаписывает
его каждую эпоху. Это дешёвое чтение: несколько сотен байт с состоянием, текущей
эпохой, ETA и последними метриками — без разбора лога.
cat /root/runs/run1/status.jsonimport json with open("/root/runs/run1/status.json") as handle: status = json.load(handle) print(status["state"], status["current_epoch"], status["eta_seconds"])print(status.get("metrics"))# На машине (по умолчанию слушает 127.0.0.1:8420):libreyolo monitor /root/runs/run1 --no-browser # Со своей машины, потом откройте http://localhost:8420 локально:# ssh -L 8420:localhost:8420 <user>@<host>Рядом лежит metrics.jsonl с полной историей по эпохам, а train.log — с выводом
консоли. libreyolo monitor поднимает браузерную панель по всем трём файлам,
используя только стандартную библиотеку, так что на машине не нужно ничего, кроме
самого LibreYOLO. Достучаться до неё можно через проброс порта по SSH.
Ничто из этого не трогает процесс обучения, поэтому так можно подключиться к живому запуску, снова открыть завершённый или разобрать упавший.
Забрать веса до того, как перестанете платить
Машина одноразовая. Отправляйте чекпойнты на промежуточных этапах, а не только в конце: иначе падение, вытеснение или закончившийся баланс унесут весь запуск.
huggingface-cli upload my-org/my-run \ /root/runs/run1/weights/best.pt best.ptweights/best.pt и weights/last.pt пишутся каждую эпоху и при каждом улучшении.
save_period=N добавляет сверху снимки weights/epoch_<N>.pt — именно это делает
отправку в середине запуска дешёвой. summary.json и results.csv, если семейство их
пишет, занимают мало места, и их тоже стоит забрать.
Аккуратный способ автоматизировать отправку — колбэк на on_train_epoch_end. См.
Логгеры экспериментов: размещённые в облаке бэкенды заодно
отдают метрики, вообще не трогая машину.
Перестать платить
Это та часть, которая при ошибке стоит настоящих денег, и правило зависит от модели провайдера.
На маркетплейсе, где вы арендуете голую машину, тарификация идёт по реальному времени, пока инстанс не уничтожен. Простаивающая GPU тарифицируется ровно так же, как загруженная, поэтому остановка самого процесса обучения ничего не экономит. У остановленного инстанса всё равно тарифицируется диск.
На serverless-платформе, где задача — это функция с декоратором, контейнер сворачивается до нуля, как только функция возвращает управление, поэтому забытая машина куда менее вероятна. Зависшая задача без таймаута всё равно тарифицируется, так что таймаут ставьте всегда.
Остановка вместо уничтожения — настоящий рычаг и настоящая ловушка. Замер на арендованной машине 8x RTX 4090 с диском на 250 ГБ, 2026-07-31: в работе — $3.4828 в час, остановленная — $0.0694 в час только за диск, уничтоженная — ноль. Это экономия 98 процентов при сохранении окружения, подготовленных данных и чекпойнтов.
Ставку для остановленной машины можно посчитать до аренды:
stopped $/hr = allocated_GB * storage_cost_per_GB_per_month / 730
= 250 * 0.20 / 730 = $0.0694/hrСравните её с ценой пересборки: снова арендовать, скачать образ, установить и заново подготовить данные. На той же машине пересборка заняла около 15 минут настройки плюс 43 ГБ входящего трафика — примерно $1.00 в сумме. На фоне $0.0694 в час возвращение в пределах примерно 14 часов говорит в пользу остановки, а перерыв подлиннее — в пользу уничтожения и пересборки из подготовленной копии.
Один риск делает остановку небезопасной для дефицитного железа: остановка освобождает GPU. Их никто не резервирует, поэтому запуститься снова получится, только если у хоста они всё ещё свободны. Диск в безопасности, GPU — нет.
Serverless, как функция
Если возиться с машиной не хочется, и Modal, и Beam запускают Python-функцию с
декоратором на GPU и сворачиваются до нуля, когда она возвращает управление. Ночной
набор тестов самого LibreYOLO запускается на Modal, а tools/ci/modal_nightly.py в
репозитории библиотеки — готовый рабочий пример, с которого можно копировать.
import modal
image = (
modal.Image.debian_slim(python_version="3.11")
.apt_install("git", "libgl1", "libglib2.0-0") # системные библиотеки OpenCV
.pip_install("libreyolo[rfdetr]")
)
app = modal.App("libreyolo-train")
cache = modal.Volume.from_name("libreyolo-cache", create_if_missing=True)
@app.function(gpu="A100", timeout=6 * 60 * 60, volumes={"/cache": cache})
def train():
import os
os.environ["HF_HOME"] = "/cache/hf" # кэшировать веса между запусками
from libreyolo import LibreYOLO
model = LibreYOLO("LibreYOLO9s.pt")
model.train(data="coco8.yaml", epochs=100, project="/cache/runs")
cache.commit() # сохранить том
@app.local_entrypoint()
def main():
train.remote()Запуск — modal run modal_train.py. Файловая система контейнера эфемерна, поэтому всё,
что стоит сохранить, кладётся в том или выгружается наружу. Задавайте timeout= явно:
это единственное, что стоит между зависшим запуском и бесконечным счётом.
У Beam та же форма: декоратор @function, Volume и вызов train.remote() из
__main__.
Подбор размера по стоимости задачи
$/час — не то число, которое стоит оптимизировать. Маленькая модель наполовину
простаивает на большой карте, поэтому более дешёвая и медленная GPU часто выходит
дешевле в пересчёте на эпоху. Прогоните на арендованной карте несколько шагов под
профилировщиком, прежде чем затевать длинный запуск: если вердикт — dataloader или
host / launch, более быстрая GPU не даст ничего, а больше воркеров или больший батч
дадут много. См.
Производительность обучения.
Смежные материалы
- Датасеты — про структуру, которая должна быть у подготовленного архива, и про команду doctor, которая ловит проблемы до того, как GPU начнёт тарифицироваться.
- Обучение на нескольких GPU — про машины с несколькими картами.