https://vk.com/rustmlclub
Цель проекта
ML-система обладающая следующим функционалом:
- всё состояние (гиперпараметры, веса модели, метрики, подмодели) можно сохранить/восстановить одним файлом,
Это значит, что всю важную информацию о модели машинного обучения (например, её настройки, результаты обучения, промежуточные данные) можно упаковать в один файл — как фотографию состояния модели. Потом этот файл можно открыть, и модель продолжит работу с того же места, как будто ничего не происходило.
Пример:
Представьте, что вы играете в компьютерную игру и сохраняете прогресс в один файл. Потом вы можете загрузить этот файл и продолжить игру с того же уровня, с теми же очками и предметами. Здесь то же самое, только вместо игры — модель машинного обучения.
- поддерживаются разные бэкенды оптимизации (CPU, CUDA),
Простыми словами:
Модель может работать на разных "движках" — как машина, которая может ездить и на бензине, и на электричестве. Здесь речь идёт о том, что модель может использовать для вычислений как обычный процессор (CPU), так и специальные видеокарты (CUDA — это технология от NVIDIA для ускорения вычислений на видеокартах).
Пример:
Если вы редактируете видео, то на слабом ноутбуке это будет делать процессор (CPU), а на мощном компьютере с видеокартой — CUDA, и всё будет работать быстрее.
- есть глобальный мониторинг и детальное логирование,
Простыми словами:
Модель не просто работает, но и постоянно отслеживает, что происходит, и записывает все важные события — как чёрный ящик в самолёте. Это помогает понять, почему модель ведёт себя так или иначе, и быстро найти ошибки.
Пример:
Представьте, что у вас умный дом: он не только включает свет, но и ведёт журнал, когда и почему это происходило (например, "включил свет в 19:00, потому что стало темно").
- ошибки FFI надёжно обрабатываются.
Простыми словами:
FFI (Foreign Function Interface) — это способ, которым программы на разных языках общаются между собой. Здесь говорится, что если при таком общении что-то идёт не так (например, одна программа передала другой неверные данные), то система не упадёт, а аккуратно обработает ошибку — как опытный переводчик, который не растеряется, если ему скажут что-то непонятное.
Пример:
Представьте, что вы заказываете еду на иностранном сайте через переводчика. Если вы ошиблись в заказе, переводчик не бросит вас, а поможет исправить ошибку.
Зачем это нужно?:
Иерархические контексты:
RootContext и ModelContext реализуют ContextProvider
Это как матрешки: у вас есть большой контекст (RootContext), внутри которого могут быть более мелкие контексты (например, ModelContext). Каждый контекст может предоставлять данные или настройки для работы программы.
Автоматическое делегирование запросов к родительскому контексту
Если дочерний контекст не знает, как ответить на запрос, он автоматически передаёт его "родителю". Например, если у вас есть контекст для модели машинного обучения, и он не знает, где взять данные для обучения, он спрашивает у RootContext.
Динамическая типизация через dyn Any
Rust — строго типизированный язык, но иногда нужно работать с объектами, тип которых заранее неизвестен. dyn Any позволяет проверять тип объекта во время выполнения программы и при необходимости приводить его к нужному типу.
Асинхронные пайплайны:
TrainingPipeline с собственным контекстом
Представьте конвейер на заводе: данные поступают, обрабатываются, передаются дальше. TrainingPipeline — это такой конвейер для обучения модели, у которого есть свой набор настроек и данных (контекст).
Потоковая обработка данных через DataStream
Данные приходят не сразу, а потоком (как вода из крана). DataStream позволяет обрабатывать их по мере поступления, не дожидаясь, пока всё загрузится.
Генераторы на основе Future
Генераторы — это функции, которые могут "замораживаться" и потом продолжать работу. В Rust они часто реализуются через Future — это обещание, что результат будет получен позже (например, после загрузки данных из сети).
Динамическая диспетчеризация:
Трейт-объекты ContextProvider
Трейт — это как контракт: если структура реализует трейт ContextProvider, она обязуется предоставлять контекст. Трейт-объект позволяет работать с разными структурами через общий интерфейс.
Динамическое приведение типов через downcast_ref
Если у вас есть объект с неизвестным типом, но вы знаете, что он реализует определённый трейт, можно попробовать привести его к конкретному типу с помощью downcast_ref.
Thread-local трейсинг:
Контекст трассировки в TLS
TLS (Thread-Local Storage) — это как карман у каждого потока, где он хранит свои данные. Контекст трассировки позволяет отслеживать, что происходит в каждом потоке отдельно.
Иерархия span-ов для распределенного трейсинга
Span — это как метка времени: вы начинаете отслеживать выполнение кода в одном месте, а потом можете добавлять вложенные метки. Это помогает понять, сколько времени занимает каждая часть программы, особенно в распределённых системах.
Небезопасные операции:
Интеграция с CUDA через FFI
CUDA — это технология для работы с видеокартами. FFI (Foreign Function Interface) позволяет Rust вызывать функции из других языков (например, C), чтобы работать с CUDA напрямую.
Работа с сырыми указателями
В Rust обычно безопасно работают с памятью, но иногда нужно использовать "сырые" указатели — это как работать с памятью напрямую, без проверок. Это опасно, но иногда необходимо для производительности.
Кастомный контекст для GPU
Можно создать специальный контекст, который будет управлять работой с видеокартой, чтобы ускорить вычисления.
Продвинутая работа с памятью:
Кастомные аллокаторы
Аллокатор — это как менеджер памяти: он выделяет и освобождает память. Кастомный аллокатор позволяет оптимизировать этот процесс под конкретные задачи.
Generic-контексты с параметрами аллокатора
Можно создать универсальный контекст, который будет работать с любым аллокатором — это делает код более гибким.
Явное управление памятью
В Rust память обычно управляется автоматически, но иногда нужно вручную контролировать, когда и как память выделяется и освобождается.
Гибридная архитектура:
Комбинация синхронного и асинхронного кода
Синхронный код выполняется сразу, а асинхронный — когда будет готов результат. Гибридная архитектура позволяет совмещать оба подхода для оптимальной производительности.
Передача контекстов между потоками
Контексты можно передавать из одного потока в другой, чтобы разные части программы могли обмениваться данными.
Атомарные счетчики для генерации ID
Атомарные операции позволяют безопасно увеличивать счётчик (например, для генерации уникальных ID) даже если к нему обращаются сразу несколько потоков.
Экспериментальные фичи Rust:
Generic-аллокаторы
Это возможность создавать универсальные аллокаторы, которые работают с любыми типами данных.
Генераторы (нестабильная фича)
Генераторы позволяют "приостанавливать" выполнение функции и возвращаться к ней позже. Это удобно для работы с потоками данных.
Явное управление паникой
Можно настроить, как программа будет вести себя при ошибках, чтобы избежать неожиданных завершений.
Что реализовано в коде?:
Система управления состоянием (контекстами) для простого ML-воркфлоу. Код построен вокруг идеи иерархического контекста, который может хранить параметры, подконтексты и быть полностью сериализуемым/десериализуемым.
Простыми словами: Это как коробка с ящичками, где в каждом ящичке лежат нужные для работы данные. Каждый ящичек (контекст) может хранить параметры (например, настройки модели), а также другие, более мелкие ящички (подконтексты). Всё это можно легко сохранить на диск (сериализовать) или загрузить обратно (десериализовать).
Пример:
Представьте, что вы готовите блюдо по рецепту. У вас есть большая папка (главный контекст) с рецептом, в которой лежат:
- список ингредиентов (параметры),
- инструкции по приготовлению (подконтексты),
- заметки о том, что можно заменить (дополнительные настройки).
Всю папку можно скопировать (сериализовать) и отправить другу, а он сможет воспроизвести рецепт (десериализовать).
В ML-воркфлоу присутствует:
- простая линейная регрессия как пример модели,
Простыми словами: Это самая простая математическая модель, которая ищет зависимость между двумя величинами. Например, как цена на квартиру зависит от её площади.
Пример:
Допустим, у вас есть данные:
- Площадь квартиры (м²): 30, 50, 70
- Цена (млн руб.): 3, 5, 7
Линейная регрессия найдёт формулу: Цена = Площадь × 0.1
Теперь можно предсказать цену для квартиры 60 м²: 60 × 0.1 = 6 млн руб.
- поддержка разных оптимизаторов (SGD, Adam, CUDA),
Простыми словами: Оптимизаторы — это алгоритмы, которые помогают модели быстрее и точнее находить лучшие параметры (например, в формуле линейной регрессии). SGD и Adam — это разные стратегии обучения, а CUDA — это использование видеокарты для ускорения вычислений.
Примеры:
- SGD (Стохастический градиентный спуск): Как спускаться с горы маленькими шагами, чтобы найти самую низкую точку.
- Adam: Более умный способ спускаться — он учитывает, как быстро и в каком направлении лучше двигаться.
- CUDA: Видеокарта помогает делать вычисления намного быстрее, как если бы у вас было 100 помощников вместо одного.
- безопасная обёртка над FFI-вызовами «CUDA» (на деле — мок, который всегда возвращает ошибку),
Простыми словами: FFI — это способ вызывать код на другом языке (например, C) из Rust. Здесь сделан безопасный интерфейс для работы с CUDA (видеокартами), но на самом деле это "мок" — заглушка, которая всегда возвращает ошибку (для тестирования или если CUDA недоступна).
Пример:
Представьте, что у вас есть пульт от телевизора, но телевизор ещё не подключён. Пульт (обёртка) есть, но при нажатии кнопок ничего не происходит (мок).
- глобальная система мониторинга и трассировки,
Простыми словами: Это как чёрный ящик в самолёте — система записывает всё, что происходит во время работы программы: какие ошибки были, сколько времени заняли операции, какие данные передавались.
Пример:
Вы запускаете программу, и она пишет в лог:
- "Начал обучение модели в 10:00"
- "Ошибка: не хватило памяти в 10:05"
- "Закончил обучение в 10:15"
Это помогает понять, что пошло не так.
- продуманная обработка ошибок,
Простыми словами: Программа не падает при ошибках, а аккуратно сообщает, что случилось, и пытается продолжить работу или предложить решение.
Пример:
Если вы пытаетесь открыть файл, которого нет, программа не закрывается, а пишет: "Файл не найден. Проверьте путь или создайте новый файл."
- использование tracing для структурированного логирования.
Простыми словами: Это как ведение дневника с метками времени и уровнями важности. Вместо хаотичных записей — аккуратные блоки с информацией: что, когда и почему произошло.
Проект состоит из четырёх файлов:
- main.rs — основной код.
- Cargo.toml — манифест с зависимостями.
- build.rs — скрипт сборки, который компилирует C-мок CUDA.
- cuda_mock.c — C-реализация заглушек CUDA, имитирующая ошибки.
Разберём всё по частям.
1. Зависимости и сборка (Cargo.toml, build.rs, cuda_mock.c)
- В Cargo.toml перечислены ключевые crates:anyhow и thiserror — удобная и производная обработка ошибок.
serde + bincode — сериализация/десериализация в бинарный формат.
tracing + tracing-subscriber — мощное инструментированное логирование.
lazy_static — глобальный мониторинг.
libc — для FFI.
другие вспомогательные (ndarray, rand и т.д., хотя в текущем коде не используются). - build.rs компилирует файл src/cuda_mock.c в статическую библиотеку cuda_mock и линkuje её к Rust-проекту.
- cuda_mock.c — чисто демонстрационные заглушки:cuda_init() → возвращает -1 (ошибка).
cuda_create_context() → возвращает NULL.
cuda_optimizer_step() → возвращает 0 (успех, но до него дело не доходит из-за предыдущих ошибок).
cuda_destroy_context() — просто освобождает память.
Это сделано специально, чтобы показать, как Rust-обёртка корректно обрабатывает ошибки CUDA.
2. Обработка ошибок (ContextError, CudaError)
Определены два кастомных типа ошибок с помощью thiserror:
- CudaError — ошибки инициализации и работы с CUDA.
- ContextError — ошибки внутри контекстной системы (ключ не найден, несоответствие типов, ошибки сериализации/IO, CUDA).
Все ошибки в проекте в итоге преобразуются в anyhow::Error в main.
3. Безопасная FFI-обёртка над CUDA (mod cuda_safe)
Модуль предоставляет RAII-обёртку SafeCudaContext:
- В конструкторе вызывает cuda_init() и cuda_create_context().
- Поскольку мок всегда возвращает ошибку, конструктор всегда вернёт Err(CudaError::DriverError(-1)) или Err(CudaError::InvalidContext).
- Метод step() вызывает cuda_optimizer_step.
- В Drop вызывается cuda_destroy_context.
Благодаря этому ресурсы корректно освобождаются даже при ошибке, а все FFI-вызовы безопасно обёрнуты в Result.
4. Трейт сериализации контекстов (SerializableContext)
trait SerializableContext: Send + Sync {
fn serialize(&self) -> Result<Vec<u8>, ContextError>;
fn deserialize(data: &[u8]) -> Result<Box<dyn SerializableContext>, ContextError>
where
Self: Sized;
}
Это центральный трейт проекта. Все контексты должны уметь:
- полностью сериализовать себя в байты (bincode),
- воссоздать себя из байт.
5. Иерархический корневой контекст (RootContext)
RootContext — контейнер верхнего уровня:
- data: HashMap<String, Vec<u8>> — простые значения (параметры гиперпараметров, скаляры и т.п.), хранятся уже сериализованными.
- children: HashMap<String, Box<dyn SerializableContext>> — подконтексты (например, модель).
Методы:
- insert<T: Serialize + ...> — сериализует значение и кладёт в data.
- get<T: Deserialize> — десериализует значение по ключу.
- add_child / get_child — работа с дочерними контекстами.
Реализация SerializableContext для RootContext:
- При сериализации рекурсивно сериализует все дочерние контексты.
- При десериализации рекурсивно воссоздаёт их (в текущей реализации предполагается, что все дети тоже RootContext, но это упрощение).
6. Контекст модели (ModelContext)
Это конкретный подконтекст, представляющий простую линейную регрессию:
weights: Vec<f32>, bias: f32
optimizer: Optimizer (SGD | Adam | Cuda)
learning_rate: f32
metrics: TrainingMetrics
Методы:
- new() — инициализирует веса нулями.
- predict() — скалярное произведение + bias.
- train_step() — один шаг обучения:вычисляет предсказание,
ошибку и MSE-лосс,
вручную обновляет веса и bias (простой SGD),
сохраняет лосс в историю. - update_with_cuda() — альтернативное обновление весов через FFI-вызов (в примере используется только для демонстрации).
Реализация SerializableContext:
- Сериализует пару (SerializedContext заглушка, сама ModelContext).
- Десериализует обратно.
7. Глобальная система мониторинга
lazy_static! {
static ref MONITOR: Arc<Mutex<MonitoringSystem>> = ...
}
MonitoringSystem хранит:
- метрики (имя → вектор значений),
- трассы (временные метки + сообщения),
- время старта.
Функции-хелперы record_metric и add_trace позволяют записывать данные из любого места.
В конце main выводится отчёт: средние значения метрик и хронология трасс.
8. Функция main
Последовательность действий:
- Настраивается tracing-subscriber (логи в консоль, уровень по переменной окружения или info по умолчанию).
- Создаётся RootContext:вставляются гиперпараметры (batch_size, learning_rate, max_epochs).
создаётся и добавляется дочерний ModelContext с 3 признаками и оптимизатором Adam. - Корневой контекст полностью сериализуется в context.bin.
- Файл читается обратно и десериализуется → получаем идентичный контекст.
- Запускается train_model (передаётся десериализованный контекст как &dyn SerializableContext, хотя в текущей реализации он не используется внутри функции).
Внутри train_model:
- Создаётся новый SafeCudaContext → падает с ошибкой, но ошибка перехватывается через .context(...) и превращается в anyhow::Error.
- Добавляется трасса "CUDA context initialized" (выполнится до падения? Нет — после неудачного создания трасса не добавляется).
- Создаётся отдельная модель ModelContext с оптимизатором Cuda.
- 100 эпох:обычный train_step (ручное обновление весов),
каждые 10 эпох — попытка update_with_cuda (в реальном коде упадёт, но в текущем градиенты заглушка). - Модель сериализуется в trained_model.bin.
- Выводится отчёт мониторинга.
9. Что на самом деле происходит при запуске
Из-за моков CUDA:
- SafeCudaContext::new() всегда возвращает ошибку.
- превратит CudaError в anyhow::Error с сообщением "Failed to initialize CUDA context".В train_model строкаRustlet cuda_ctx = cuda_safe::SafeCudaContext::new().context("Failed to initialize CUDA context")?;
- Программа завершится с ошибкой и напечатает стек через anyhow.
- Тем не менее, перед падением успеет:сериализовать/десериализовать контекст,
выполнить часть тренировочного цикла (обычные шаги SGD),
собрать метрики и трассы,
сохранить модель (если цикл дошёл до конца).