В этом проекте создаётся виртуальный диск, который использует машинное обучение, чтобы предсказывать, какие данные понадобятся пользователю в следующий момент. Это позволяет ускорить работу с диском, снижая задержки при чтении. Данный код — виртуальное блочное устройство на базе ublk (userspace block driver в Linux) с использованием машинного обучения (LSTM-модель из библиотеки Burn). Устройство работает как loop-device (перенаправляет I/O на файл backing.img размером 100 GiB), но с ML-предсказанием следующего блока для снижения latency при чтении.
ublk (userspace block driver в Linux) - Это технология, которая позволяет создавать виртуальные "диски" (блочные устройства) прямо из пользовательского пространства Linux (то есть без изменения ядра системы). Обычно драйверы работают внутри ядра, но ublk позволяет делать это снаружи, что упрощает разработку и тестирование.
Пример:
Представьте, что у вас есть программа, которая эмулирует флешку. С ublk вы можете заставить систему думать, что это реальная флешка, не переписывая ядро Linux.
Виртуальное блочное устройство - Это "виртуальный диск", который ведёт себя как настоящий жёсткий диск или SSD, но на самом деле хранит данные в памяти или в файле на другом диске.
Пример:
Когда вы создаёте виртуальную машину, она использует виртуальный диск — это и есть блочное устройство, которое на самом деле — просто файл на вашем компьютере.
Loop-device - Это механизм в Linux, который позволяет использовать обычный файл как блочное устройство (например, как диск). Вы можете "примонтировать" файл и работать с ним, как с настоящим диском.
Пример:
Если у вас есть файл backing.img размером 100 ГБ, вы можете сделать его "диском" и записывать/читать данные, как будто это реальный жёсткий диск.
ML-предсказание (машинное обучение, LSTM-модель) - Здесь используется модель машинного обучения (LSTM — тип нейронной сети), которая пытается предсказать, какой блок данных будет запрошен следующим. Это позволяет заранее подгружать нужные данные и снижать задержки при чтении.
Пример:
Представьте, что вы смотрите фильм онлайн. Если сервис заранее знает, какой кусок видео вам понадобится следующим, он может начать его загружать заранее — так видео не будет "тормозить".
backing.img (файл размером 100 GiB) - Это файл, который используется как "хранилище" для виртуального диска. Все данные, которые записываются на виртуальное устройство, на самом деле сохраняются в этот файл.
Пример:
Как если бы вы создали виртуальный диск для игры — все сохранения и файлы игры на самом деле лежат в одном большом файле на вашем компьютере.
Снижение latency (задержки) при чтении - Благодаря предсказанию следующего блока, система может заранее подготовить данные, которые скорее всего понадобятся. Это уменьшает время ожидания (latency) при чтении данных с диска.
Пример:
Если вы знаете, что после первой страницы книги всегда читают вторую, вы можете заранее открыть вторую страницу — так читателю не придётся ждать.
Общая архитектура
- Параллельность: Каждая очередь (nr_queues, до ~64) — отдельный OS-поток с привязкой к CPU core (affinity) для максимальной производительности. Представьте, что у вас есть большая фабрика, где каждый конвейер (очередь) работает независимо от других. У каждого конвейера есть свой рабочий (поток операционной системы), который закреплён за определённым станком (ядром процессора). Так все конвейеры могут работать одновременно, не мешая друг другу, и фабрика работает максимально быстро.
Пример:
Если у вас 4 ядра процессора, то можно запустить 4 отдельных потока, каждый на своём ядре. Так задачи выполняются параллельно, а не по очереди.
- Per-queue независимость: Каждая очередь имеет свой Prefetcher (своя модель LSTM, история, буфер) — нет contention. У каждого конвейера (очереди) есть свой персональный помощник (Prefetcher), который запоминает, какие детали (данные) нужны дальше, и заранее их готовит. У каждого помощника своя память (модель LSTM) и свои записи (история, буфер). Они не спорят друг с другом за ресурсы, поэтому всё работает быстро и без задержек.
Пример:
Если у вас два кассира в магазине, и у каждого свой калькулятор и блокнот для записей, они не будут мешать друг другу и смогут быстро обслуживать клиентов.
- Prefetching: При READ модель предсказывает следующий блок, асинхронно загружает его. Если следующий запрос попадает в него — HIT (immediate complete, низкая latency). Когда вы читаете книгу, помощник (модель) пытается угадать, какую страницу вы откроете следующей, и заранее её приносит. Если угадал — вы сразу получаете страницу, не дожидаясь, пока её найдут. Это называется "попадание" (HIT), и всё происходит очень быстро.
Пример:
Вы смотрите сериал на Netflix. Пока вы смотрите одну серию, следующая уже загружается в фоновом режиме. Когда вы нажимаете "Далее", она сразу начинает играть — без задержки.
- Обучение: Онлайн (при каждом READ обновляется история, модель обучается на последовательностях). Помощник (модель) учится на ваших действиях в реальном времени. Каждый раз, когда вы читаете данные, он запоминает, что вы делали до этого, и старается лучше предсказывать, что вам понадобится дальше.
Пример:
Если вы всегда после просмотра новостей открываете почту, то со временем помощник начнёт загружать почту заранее, как только вы откроете новости.
- Метрики: Prometheus на :9090 (hits, misses, reads/writes per queue). Есть специальная программа (Prometheus), которая следит за тем, как часто помощник угадывает правильно (hits), как часто ошибается (misses), сколько раз вы читаете или записываете данные в каждой очереди. Это помогает понять, насколько хорошо работает система.
Пример:
В магазине ведётся статистика: сколько клиентов быстро обслужили (hits), сколько пришлось ждать (misses), сколько покупок сделали за день (reads/writes).
1. main.rs — Точка входа
- Инициализирует логгер и Prometheus-сервер (на фоне).
- Логгер — это как дневник программы. Туда записываются все важные события: что произошло, когда, с какими ошибками. Например, если программа запустилась, она запишет: "Запуск в 12:00".
- Prometheus-сервер — это система мониторинга. Она собирает данные о работе программы (например, сколько запросов в секунду обрабатывается, сколько памяти используется) и позволяет их визуализировать. Представьте, что у вас есть датчики в машине, которые показывают скорость, температуру двигателя и т.д. — Prometheus делает что-то похожее для программ.
- Создаёт sparse-файл backing.img (100 GiB).
- Sparse-файл — это файл, который занимает мало места на диске, но "притворяется" большим. Например, вы создаёте файл размером 100 ГБ, но на самом деле он занимает только 1 ГБ, потому что внутри него много "пустот". Это как коробка, в которой лежит одна маленькая вещь, но коробка огромная.
- backing.img — это виртуальный диск, на котором будут храниться данные. Например, как если бы вы создали виртуальный жёсткий диск для виртуальной машины.
- Определяет число очередей: nr_queues = num_cores * 2 (hyper-threading), лимит 64, глубина 256.
- num_cores — количество ядер процессора. Например, если у вас 8-ядерный процессор, то num_cores = 8.
- Hyper-threading — технология, которая позволяет каждому ядру процессора выполнять две задачи одновременно. Поэтому число очередей рассчитывается как количество ядер, умноженное на 2.
- Очереди — это как кассы в супермаркете: чем больше касс, тем быстрее обслуживаются покупатели. Здесь "очереди" нужны для обработки запросов к диску.
- Лимит 64 — максимальное количество очередей, даже если ядер больше.
- Глубина 256 — сколько запросов может одновременно находиться в одной очереди. Например, если глубина 256, то в каждой очереди может ждать обработки до 256 запросов.
- Создаёт контроллер ublk (UblkCtrlBuilder).
- ublk — это технология для создания виртуальных блочных устройств (например, виртуальных дисков) в Linux.
- Контроллер — это как "мозг", который управляет работой виртуального диска. Он отвечает за обработку запросов на чтение/запись данных.
- UblkCtrlBuilder — это инструмент, который создаёт и настраивает этот "мозг".
- Добавляет устройство (add_dev). Это как подключить новый жёсткий диск к компьютеру. Программа говорит операционной системе: "Вот тебе новое устройство, работай с ним!"
- Спавнит потоки для каждой очереди (spawn_queue_thread из queue.rs), с affinity к core.
- Потоки — это как работники, каждый из которых выполняет свою задачу. Здесь для каждой очереди создаётся отдельный поток.
- Affinity к core — это привязка потока к конкретному ядру процессора. Например, если у вас 8 ядер, то каждый поток будет "привязан" к своему ядру, чтобы не мешать другим и работать быстрее.
- Запускает устройство (start_dev — блокирует до готовности всех очередей). Программа запускает виртуальное устройство и ждёт, пока все очереди не будут готовы к работе. Это как запустить конвейер на заводе и дождаться, пока все рабочие места не будут укомплектованы.
- Graceful shutdown по Ctrl+C. Когда вы нажимаете Ctrl+C, программа не просто закрывается, а сначала завершает все операции, сохраняет данные, закрывает файлы и только потом выключается. Это как если бы вы не просто выдернули вилку из розетки, а сначала выключили все приборы, а потом отключили электричество.
- Join потоков + удаление устройства.
- Join потоков — программа дожидается, пока все потоки завершат свою работу.
- Удаление устройства — после завершения работы виртуальное устройство удаляется из системы, как если бы вы отключили внешний жёсткий диск.
2. model.rs — Модель предсказания (LSTM)
- Trait PredictionModel: Абстракция (train/predict) — для лёгкой замены модели. Это как чертёж или шаблон, по которому можно создать любую модель для предсказаний. Он описывает, что модель должна уметь: обучаться (train) и предсказывать (predict), но не говорит, как именно это делать. Благодаря этому можно легко поменять одну модель на другую, не ломая остальной код.
Пример:
Представьте, что у вас есть универсальный пульт для телевизора. Не важно, какой телевизор — Sony или Samsung — главное, чтобы пульт умел включать, выключать и переключать каналы. Так и здесь: не важно, какая модель внутри — главное, чтобы она умела обучаться и предсказывать.
- PrefetchModel: Внутренняя LSTM (2 слоя, hidden 64, dropout 0.1). Это конкретная модель, которая построена на основе LSTM (Long Short-Term Memory) — типа нейронной сети, хорошо подходящей для работы с последовательностями (например, временными рядами, текстами).
- 2 слоя — как два этажа в доме: данные проходят через оба, чтобы лучше обработаться.
- hidden 64 — количество "нейронов" на каждом слое, которые запоминают важные детали.
- dropout 0.1 — случайное "выключение" 10% нейронов при обучении, чтобы модель не запоминала слишком специфические детали и лучше обобщала.
Пример:
Представьте, что вы учитесь предсказывать погоду. Вы смотрите на температуру за последние дни (последовательность) и пытаетесь угадать, какая будет завтра. LSTM поможет учесть, как температура менялась со временем, а не просто смотреть на последний день.
- LstmPredictionModel: Реализация.Хранит модель (Param), оптимизатор (Adam), hidden state (Mutex). Это уже готовая, работающая модель на основе LSTM.
- Param — параметры модели (веса, которые она учит).
- Adam — метод, который помогает модели обучаться быстрее и точнее.
- hidden state (Mutex) — внутренняя память модели, которая сохраняется между предсказаниями (чтобы не забывать контекст). Mutex — это механизм, который не даёт разным частям программы одновременно портить эту память.
Пример:
Представьте, что вы читаете книгу и делаете пометки на полях. Hidden state — это ваши пометки, которые помогают не забыть, о чём была речь в предыдущей главе. Mutex — это как блокнот, который не даёт двум людям одновременно писать в него, чтобы не было путаницы.
train: MSE + MAE + L2 loss на последовательности (SEQ_LEN=19) → backward + step. train — процесс обучения модели.
- MSE (Mean Squared Error) — ошибка, которая штрафует модель за большие промахи.
- MAE (Mean Absolute Error) — ошибка, которая штрафует модель за любые промахи, независимо от их размера.
- L2 loss — штраф за слишком большие веса модели (чтобы она не переобучалась).
- SEQ_LEN=19 — модель смотрит на 19 последних значений, чтобы сделать предсказание.
- backward + step — модель считает, как ей изменить свои параметры, чтобы ошибка стала меньше, и делает шаг в этом направлении.
Пример:
Представьте, что вы учитесь стрелять из лука. MSE — это штраф за промах на большое расстояние, MAE — за любой промах, а L2 — за слишком сильный натяг тетивы. SEQ_LEN=19 — вы смотрите на 19 последних выстрелов, чтобы понять, как улучшить следующий.
predict: Stateful forward (сохраняет hidden) → возвращает normalized offset (0.0-1.0).Stateful forward — модель делает предсказание, сохраняя свою внутреннюю память (hidden state), чтобы следующее предсказание было точнее.
- normalized offset (0.0-1.0) — результат предсказания в виде числа от 0 до 1 (например, 0.3 — это 30% от возможного диапазона).
Пример:
Представьте, что вы предсказываете, насколько поднимется температура завтра. Модель говорит: "Температура поднимется на 30% от максимального возможного роста за день".
3. prefetcher.rs — Логика prefetching
- Struct Prefetcher: Per-queue.Модель (Box<dyn PredictionModel> — DIP).
История (Mutex<Vec<f32>> normalized offsets).
prefetch_buf (Vec<u8> размером max_io_buf_bytes, обычно 1 MiB).
prefetch_sector + valid (Atomic).
qid для метрик.
Это структура данных, которая отвечает за предсказание и предварительную загрузку (prefetch) данных с диска в память.
Ключевые поля:
- Модель (Box<dyn PredictionModel> — DIP): Здесь используется модель машинного обучения (например, LSTM), которая предсказывает, какие данные понадобятся в будущем.
- История (Mutex<Vec<f32>> normalized offsets): Хранит историю обращений к данным (например, какие сектора диска читались).
- prefetch_buf: Буфер, куда загружаются данные заранее.
- prefetch_sector + valid: Указывает, какие данные уже предзагружены и готовы к использованию.
- qid для метрик: Идентификатор очереди для сбора статистики (например, сколько раз предсказание сработало успешно).
Пример:
Если вы часто открываете фотографии в папке, prefetcher замечает это и заранее подгружает следующие фотографии, пока вы смотрите текущую.
- new: Создаёт LSTM-модель. LSTM (Long Short-Term Memory) — это тип нейронной сети, который хорошо подходит для работы с последовательностями данных (например, история обращений к диску).
Пример:
Представьте, что вы смотрите сериал. LSTM запоминает, какие серии вы смотрели, и предсказывает, какую серию вы откроете следующей.
- on_prefetch_complete: Устанавливает valid при успешном prefetch. on_prefetch_complete: Когда данные успешно предзагружены, система отмечает их как готовые к использованию.
- invalidate_prefetch: Если данные на диске изменились (например, после записи), предзагруженные данные становятся неактуальными и их нужно сбросить.
Пример:
Вы заказали еду на завтра, но меню изменилось — нужно отменить старый заказ и сделать новый.
- invalidate_prefetch: При WRITE (данные изменились).
- handle_read: Основная логика. Добавляет текущий offset в историю.
Если достаточно данных — train (предыдущая последовательность → текущий как target).
Предсказывает следующий normalized offset.
Запускает async prefetch read (fire-and-forget SQE с PREFETCH_UDATA).
Проверяет HIT: если запрос == prefetched → копирует из буфера, инкремент метрики HIT, immediate complete.
Возвращает bool (HIT или MISS).
Добавляет текущий offset (позицию на диске) в историю. - Если достаточно данных, обучает модель (предыдущие обращения → текущее как цель).
- Предсказывает следующий offset.
- Запускает асинхронную предзагрузку данных.
- Проверяет, совпадает ли запрос с предзагруженными данными (HIT). Если да — использует их, если нет (MISS) — загружает обычно.
Пример:
Вы читаете книгу. Система замечает, что вы читаете по 10 страниц в день, и заранее открывает следующие 10 страниц. Если вы действительно их открываете — это HIT, если нет — MISS.
- HIT: Запрос совпал с предзагруженными данными — экономия времени.
- MISS: Запрос не совпал — данные грузятся обычно.
Пример:
Вы заранее положили в рюкзак зонт, и вдруг пошёл дождь — это HIT. Если дождя не было — это MISS.
4. queue.rs — Обработка очередей I/O
- spawn_queue_thread: Создаёт поток.Устанавливает affinity.
Создаёт Prefetcher и буферы I/O.
Замыкание handler: маршрутизирует I/O.
Создаёт UblkQueue, регистрирует backing_fd, submit fetch commands.
Запускает цикл wait_and_handle_io (асинхронная обработка completions). - handler (в замыкании):
Если tgt_io (target I/O completion):Если PREFETCH_UDATA — on_prefetch_complete.
Иначе — complete driver IO.
Новый driver IO:READ: инкремент метрики, handle_read → если HIT return, иначе async read.
WRITE: инкремент метрики, async write, invalidate_prefetch.
Другие — complete 0.
5. utils.rs — Утилиты
- set_cpu_affinity: Привязывает поток к core (sched_setaffinity).
- get_core_list: Простой список cores (0..num_cores).
6. metrics.rs — Метрики Prometheus
- lazy_static counters: prefetch_hit/miss, io_read/write (per queue label).
- start_prometheus_server: Tokio-спавн Hyper-сервера на :9090/metrics.
- Инкременты в prefetcher и handler.
Поток работы на примере READ
- Driver → fetch completion → handler видит новый READ.
- prefetcher.handle_read:Обновляет историю.
Train на предыдущей seq.
Predict → submit prefetch read.
Если HIT → copy из prefetch_buf, complete_io, метрика HIT.
MISS → submit обычный read. - Когда prefetch/target read complete → handler → on_prefetch_complete или complete_io.
Для sequential workload модель быстро учится предсказывать +1 блок → высокий HIT rate.
Проект эффективен на multi-core (линейное масштабирование), с низким overhead ML. Запуск: sudo cargo run --release → /dev/ublkbN, метрики :9090.