Технологии
«CPU готовит, GPU генерирует, CPU проверяет»: Hanno Labs собрала конвейер синтетических данных на hfdask поверх Hugging Face Jobs
15 сентября 2026 года Hanno Labs опубликовала в блоге Hugging Face разбор своей библиотеки hfdask, которая разворачивает временный кластер Dask прямо на Hugging Face Jobs по одному YAML-описанию. На примере генерации пар «вопрос — ответ» авторы собирают конвейер CPU → GPU → CPU: CPU-воркеры готовят исходные документы, GPU-воркер генерирует примеры локальным движком vLLM, а CPU-воркеры проверяют и записывают принятые записи в бакет Hugging Face.

Синтетика как распределённая нагрузка
В Hanno Labs синтетические данные стали одним из способов направить модель к нужному поведению — особенно когда нужные примеры трудно или дорого собрать вручную. Но генерация таких данных — сама по себе распределённая работа: языковым моделям нужны GPU, а всё вокруг них — чтение исходных материалов, фильтрация, сборка промптов, проверка выводов и запись датасетов — упирается в CPU.
Значительная часть исходных данных уже лежит в датасетах и бакетах Hugging Face, поэтому Hugging Face Jobs оказались естественным местом для запуска: вычисления доступны по требованию и находятся рядом с данными. Dask авторам уже нравился как инструмент для такой работы — не хватало чистого способа превратить набор Hugging Face Jobs в кластер Dask, и для этого они построили hfdask.
Что делает hfdask
hfdask превращает YAML-описание кластера во временный кластер Dask на Hugging Face Jobs: сначала запускается один coordinator Job, который держит планировщик и выполняет скрипт, затем — worker Jobs, добавляющие CPU- и GPU-воркеры. Машины соединяются через аутентифицированную шифрованную сеть Iroh, сервисы Dask слушают loopback, а не выставляют публичный планировщик, и на каждой машине поднимается по одному однопоточному воркеру на каждое полное CPU-ядро.
Видимые NVIDIA GPU отдаются первым воркер-процессам эксклюзивно, по одному GPU на процесс, и такие воркеры объявляют штатный ресурс GPU в Dask. Самой нагрузке обёртка не нужна: программа получает обычный клиент через distributed.get_client(), строит обычный граф задач и пользуется штатными механизмами Dask для размещения по воркерам и ресурсам; hfdask владеет окружающим жизненным циклом — доставкой исходников, установкой зафиксированных зависимостей, отправкой Job, сборкой кластера, очисткой и метаданными восстановления.
Библиотека открыта под лицензией Apache-2.0 и опубликована на PyPI как hfdask 0.1.1 (15 сентября 2026 года), но пока носит статус pre-release: авторы предупреждают, что API и конфигурация могут измениться до первого стабильного релиза. Для установки требуется Python 3.11 или новее.
Почему стадии разводят по железу
В разборе перечислены пять стадий конвейера и подходящее каждой железо: чтение, семплирование и нормализация документов — CPU; построение промптов и батчей — CPU; генерация примеров языковой моделью — GPU; разбор и проверка структурированного вывода — CPU; дедупликация и запись Parquet — CPU.
Гонять все стадии внутри одного GPU-процесса — значит тратить дорогое ускорительное время на CPU-работу; разнести их по скриптам — значит завести ещё одну систему оркестрации и увести каждую промежуточную границу в хранилище. Dask позволяет держать всё вычисление в одном графе: зависимости между стадиями остаются явными, CPU-работа идёт параллельно, а задачи генерации запрашивают GPU-воркеры через resources={"GPU": 1}. Тот же шаблон подходит и для синтетических инструкций, обоснований для классификации, аугментации документов, дистилляции, построения пар предпочтений и офлайн-оценки.
Шесть компонентов конвейера
Исходный датасет — test-сплит fancyzhx/ag_news с новостными текстами и одной из четырёх меток, смонтированный в каждый Job на зафиксированной ревизии в каталог /dataset: pandas читает колонки text и label, берёт сбалансированную выборку по 32 строки на категорию и нарезает её на партиции по 16 строк, сохраняя идентификатор строки и метку как провенанс. Модель генерации — Qwen/Qwen3-0.6B из примера в репозитории, тоже на фиксированной ревизии: она достаточно мала для одного Job l4x1, и авторы отдельно оговаривают, что это не рекомендация для реальной генерации синтетики.
Контракт с моделью — ровно один JSON-объект на исходную статью со строковыми полями question, answer и supporting_quote, где цитата обязана быть точной непрерывной выдержкой из статьи, а опираться на факты вне статьи запрещено. Ответ разбирается в строгую Pydantic-модель с extra="forbid": если модель добавит неизвестное поле, пропустит поле или вернёт другую форму, запись попадает в отбракованные, а не «тихо чинится» до вида валидного примера. Оставшиеся компоненты — конфигурация кластера, валидация и граф Dask, описывающий всё вычисление CPU → GPU → CPU.
Модель поднимается один раз на воркер
Загружать модель на каждую задачу было бы слишком дорого, поэтому движок vLLM создаётся один раз при старте GPU-воркера через Dask WorkerPlugin: CPU-воркеры инициализацию пропускают, а каждый GPU-воркер держит движок для последующих задач генерации. В примере он поднимается с tensor_parallel_size=1, max_model_len=2048, max_num_seqs=16, gpu_memory_utilization=0.5, enforce_eager=True и фиксированным seed, а выборка задана как temperature=0.2, top_p=0.9, max_tokens=256.
Валидация: строгая схема и честный брак
Удачный разбор ещё не делает пример хорошими данными, поэтому базовый валидатор требует валидного JSON, точного соответствия схеме Pydantic, непустых текстовых полей, границ длины (вопрос до 240 символов, ответ до 1000) и буквального присутствия supporting_quote в исходной статье, а дубликаты нормализованных вопросов удаляются перед записью. Отбракованные записи авторы считают ценной уликой: сырой вывод генерации и причина отказа сохраняются, чтобы улучшать промпт и политику валидации, а не гадать, почему изменился выход.
Отдельно оговорены границы метода: это минимальный контракт, а не финальная планка качества, поэтому перед обучением стоит посмотреть статистически значимую выборку, а более требовательные пайплайны добавляют судейство моделью, кросс-проверку другой моделью, фильтрацию персональных данных и проверки на загрязнение отдельными стадиями Dask. На выходе появляются accepted.parquet, rejected.parquet и summary.json со счётчиками attempted, accepted, rejected, duplicates и долей валидных записей validation_yield.
Кластер из одного YAML
Проект собирается как Git-репозиторий с uv: рабочие библиотеки (dask, distributed, pandas, pyarrow, pydantic и vllm==0.29.0 для Linux x86_64) ставятся обычными зависимостями, а hfdask — в deployment-группу, потому что это инфраструктура запуска. В YAML описаны namespace, координатор на cpu-basic с worker: true и один worker Job l4x1 с группой deploy, монтированиями модели, датасета и выходного бакета, таймаутом 30m и network.public_relays: true.
Координатор держит скрипт и планировщик: при worker: true одно полное ядро он резервирует под планировщик, а на остальных поднимает CPU-воркеров; Job l4x1 даёт один воркер с GPU и CPU-воркеров на оставшихся полных ядрах. Образ vllm/vllm-openai:v0.29.0 зафиксирован по дайджесту, но hfdask запускает собственный бутстрап, а не точку входа образа, поэтому окружение и образ остаются на одной версии vLLM. Флаг network.public_relays: true — явное согласие на публичное обнаружение и relay-фолбэк: данные в пути остаются зашифрованными, но сервисы обнаружения и релея могут видеть метаданные соединений.
Что происходит после команды запуска
Запуск — две команды: uv run --group deploy hf auth login и uv run --group deploy hfdask run --cluster cluster.yaml generate_dataset.py. Дальше hfdask проверяет конфигурацию локально, снимает снимок подходящих файлов из текущего рабочего дерева Git, загружает архив исходников под уникальным префиксом в приватный бакет jobs-artifacts и отправляет coordinator- и worker-Jobs.
Каждый Job проверяет и распаковывает исходники, затем выполняет uv sync --locked --no-dev --group deploy; машины находят фиксированных соседей по кластеру и поднимают шифрованные туннели Iroh, координатор запускает планировщик, ждёт ожидаемых воркеров и выполняет скрипт, а нагрузка пишет принятые и отбракованные строки в смонтированный бакет. Когда скрипт завершается, hfdask отменяет все известные Job и проверяет, что каждый достиг терминального состояния. Отправляющей машине не нужны Linux-only зависимости вроде vLLM, а изолированный вызов через uvx не заменяет deployment-группу: он даёт CLI, но не кладёт удалённый раннер в lockfile проекта.
Что hfdask берёт на себя, а что остаётся нагрузке
Авторы подчёркивают границу: hfdask владеет механикой развёртывания, а не семантикой синтетических данных. На его стороне — отправка HF Job, жизненный цикл координатора и воркеров, зафиксированный бутстрап окружения, доставка исходников с учётом Git, монтирования репозиториев и бакетов Hub, обнаружение CPU- и GPU-воркеров, аутентифицированный шифрованный транспорт, проверка очистки и манифесты восстановления. На стороне нагрузки остаются дизайн промптов, качество генерации, выходная схема, политика валидации, семантика дедупликации, стратегия чекпойнтов, лицензирование и провенанс датасета, а также пороги по стоимости и качеству.
Как это масштабировать
Граф при росте прогона не меняется — меняются настройки. Рекомендации авторов: кормить GPU (партиций должно быть больше, чем GPU-воркеров, а их размер должен позволять vLLM собирать батч); добавлять машины, а не распараллеливать модель, потому что workers.count добавляет независимые GPU-воркеры со своей репликой; держать CPU-стадии на CPU-воркерах; измерять не сырую пропускную способность, а число принятых примеров на доллар.
Длинные прогоны стоит писать детерминированными независимо восстанавливаемыми шардами и пропускать уже зафиксированные при ретрае, поскольку диски Job и состояние планировщика эфемерны, а optimize_graph=False нужен, чтобы слияние задач в Dask не схлопнуло по-разному аннотированные CPU- и GPU-стадии.
Вывод
hfdask появился потому, что нужная инфраструктура была рядом: в Hugging Face Jobs уже были CPU, GPU, модели, датасеты и хранилище, у Dask — модель программирования для разнородной работы, а не хватало надёжного жизненного цикла кластера между ними. Синтетические данные делают эту комбинацию наглядной: подготовка исходников — CPU, пакетная генерация моделью — GPU, валидация, дедупликация и надёжная запись — снова CPU, и все стадии остаются в одном обычном графе Dask, который разворачивается на временных Hugging Face Jobs по одному YAML-описанию. Установить библиотеку можно одной командой — uv add --group deploy hfdask.