Найти в Дзене
Один Rust не п...Rust

Контекстная информация ML

ML-система обладающая следующим функционалом: Это значит, что всю важную информацию о модели машинного обучения (например, её настройки, результаты обучения, промежуточные данные) можно упаковать в один файл — как фотографию состояния модели. Потом этот файл можно открыть, и модель продолжит работу с того же места, как будто ничего не происходило. Пример:
Представьте, что вы играете в компьютерную игру и сохраняете прогресс в один файл. Потом вы можете загрузить этот файл и продолжить игру с того же уровня, с теми же очками и предметами. Здесь то же самое, только вместо игры — модель машинного обучения. Простыми словами:
Модель может работать на разных "движках" — как машина, которая может ездить и на бензине, и на электричестве. Здесь речь идёт о том, что модель может использовать для вычислений как обычный процессор (CPU), так и специальные видеокарты (CUDA — это технология от NVIDIA для ускорения вычислений на видеокартах). Пример:
Если вы редактируете видео, то на слабом ноутбуке
Оглавление

GitHub - nicktretyakov/contextML
ML на RUST без заморочек

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 для структурированного логирования.

Простыми словами: Это как ведение дневника с метками времени и уровнями важности. Вместо хаотичных записей — аккуратные блоки с информацией: что, когда и почему произошло.

Проект состоит из четырёх файлов:

  1. main.rs — основной код.
  2. Cargo.toml — манифест с зависимостями.
  3. build.rs — скрипт сборки, который компилирует C-мок CUDA.
  4. 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

Последовательность действий:

  1. Настраивается tracing-subscriber (логи в консоль, уровень по переменной окружения или info по умолчанию).
  2. Создаётся RootContext:вставляются гиперпараметры (batch_size, learning_rate, max_epochs).
    создаётся и добавляется дочерний ModelContext с 3 признаками и оптимизатором Adam.
  3. Корневой контекст полностью сериализуется в context.bin.
  4. Файл читается обратно и десериализуется → получаем идентичный контекст.
  5. Запускается 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),
    собрать метрики и трассы,
    сохранить модель (если цикл дошёл до конца).