- PVSM.RU - https://www.pvsm.ru -

Как я учил планировщик класть широкие команды для Эльбруса, и почему LLM проиграла жадному алгоритму за 3 миллисекунды

Как я учил планировщик класть широкие команды для Эльбруса, и почему LLM проиграла жадному алгоритму за 3 миллисекунды - 1

Все началось с того что я решил, что смогу оптимизировать компилятор Эльбрус с помощью ИИ
Вдохновлялся Эльбрусом 2: машина, которая в то время считала ПРО и космические задачи, 10 процессоров, десятки мегафлопс, когда это было реально много. Симметричный мультипроцессор с общей памятью, тегированная архитектура, аппаратная поддержка языков высокого уровня. В общем к проекту это вдохновение никак не повлияло.

Потом был Эльбрус-3 — уже с явным параллелизмом инструкций, прообраз VLIW. Потом тяжелые 90-е, потом Эльбрус 2000 / E2K. Ну про историю эльбруса вы уже слышали не раз.
Поэтому вернемся к моему проекту.

1. Что это за проект

Как я учил планировщик класть широкие команды для Эльбруса, и почему LLM проиграла жадному алгоритму за 3 миллисекунды - 2

Прототип к исследовательскому проекту «Планировщик инструкций для VLIW‑архитектур». Главный тезис:

Планировщик широких команд принимает жадные необратимые решения, и на реалистичной модели машины у стандартной эвристики есть измеримый, доказуемый разрыв до оптимума — вот бюджет, за который имеет смысл бороться обученной моделью.

Проще говоря, NEX CLI x Elbrus берет граф и модель e2k‑v6 и показывает: сколько насчитал жадный, сколько мог бы идеальный, и на какой операции и каком канале разница.

2. Зачем вообще трогать планирование под Эльбрус

Как я учил планировщик класть широкие команды для Эльбруса, и почему LLM проиграла жадному алгоритму за 3 миллисекунды - 3

Эльбрус — это VLIW. Там нет того, что прячет ваши косяки на x86: нет развитого OoО.

  • Что куда можно:

    • Умножать можно только в каналах 0, 1, 3 и 4. Результат готов через 4 такта.

    • Делить можно только в канале 5. Результат готов через 11 тактов, и следующее деление можно начать только через 2 такта.

    • Загружать данные из памяти можно в каналах 0, 2, 3 и 5. Результат готов через 5 тактов.

    • Записывать в память можно только в каналах 2 и 5.

Ошибся на один такт с делителем — встал весь пакет. Положил запись не туда — заклинил ,5, который нужен и делителю, и загрузкам. Компилятор lcc — единственный, кто может это разрулить статически.

Отсюда идея проекта: не написать «новый компилятор», а сделать честный стенд, где видно разницу между тем, что делает жадная эвристика, и тем, что вообще возможно. И на этом стенде уже можно спорить, мерить и учить модели.

python3 -m vliw run slotclash
# жадный: 23 такта, оптимум: 22 такта. -1 такт = -4%.
# Вопрос: где жадный зевнул монопольный ,5?

Сценарии так и называются — ловушки: slotclash, divpressure, memstream, unroll4, wide_ilp. У каждого — lesson в одну строчку. Например, ⁣memstream: 8 троек LOAD-ADD-STORE, узкое место — запись, потому что записей 8, а каналов под них 2.

3. Цифры не из воздуха. Но я им сам не до конца верю

Числа в e2k-v6-measured сняты с настоящего lcc-1.29.16 кросс‑сборкой под e2k-v6 тремя независимыми приемами. Это написано в шапке репа vliw/core/model.py [1]:

1. Матрица портов — спрошена у ассемблера, а не угадана по листингу.
Для каждой пары (операция, канал) собирается крошечный .s. Если так нельзя аппаратно — ассемблер отвечает прямым отказом: 'muls' cannot be encoded in ALC2. Это ответ про кодировку широкой команды, про само железо, а не про то, что решил планировщик lcc. Автомат этого — tools/probe_matrix.py [2].

2. Латентность — цепочкой зависимых. x = x * b пять раз подряд. Следующая обязана ждать предыдущую, компилятор вынужден развести их ровно на latency. Разница номеров bundle читается напрямую. Так намеряны MUL 4, DIV 11, LOAD 5.

3. Занятие порта — потоком независимых. 8–16 штук с готовыми операндами. Шаг между выдачами = темп устройства. Так намеряны DIV раз в 2 такта, MUL раз в такт.

А теперь детектив.

Обожаю несмешно шутить

Обожаю несмешно шутить

Первая версия модели была неверна. Был probe.c — 4 деления + 4 умножения. Все muls легли в ,0 подряд. Я прочитал это как «умножитель один, держит порт 8 тактов». А потом проверка ассемблером показала: умножение исполнимо на четырех каналах, а поток из 16 независимых умножений идет по одному в такт. Четырех операций просто не хватило, чтобы у жадного планировщика lcc появился повод их раскидать.

Мораль, которая теперь вбита в README: по выводу компилятора видно то, что компилятор захотел сделать, а не то, что железо может.

Все это прогонялось потом десятками ИИ‑агентов для перепроверки пар, каналов, сочетаний. Нашли ровно два запрета сочетаний в одной широкой команде: STORE,2 + LOAD,3 и STORE,5 + LOAD,0 — швы между кластерами ,0,1,2 и ,3,4,5. Сверялись с официальным «Руководством по эффективному программированию на платформе Эльбрус» 1.2 — но числа оттуда в модель НЕ подставляли: там Е4С/Е8С, а тулчейн целится в v6. Это сверка, а не источник.

И все равно — я не считаю цифры паспортом:

  • живого железа у меня не было, SSH Эльбруса — мне лень с ним возится.

  • из 19 классов latency измерена у 9. Остальное — ASSUMED=1, список открыт в ISA.md [3];

  • LOAD 5 против 3 (L1 hit, L2 11, L3 40, память ~100) из доки МЦСТ — расхождение поколений(Я перепроверял, и цифры сходились с документацией 24 года);

  • occupancy склеивает занятость устройства и слота. div8.c меряет устройство (раз в 2 такта), а в fma_kernel.s fdivd,5@t19 + aaurwd,5@t20 — слот свободен раньше. Модель пессимистична сознательно.

Поэтому давайте честно: это лучшая доступная аппроксимация, а не даташит.

И здесь — оффер, ради которого статья и пишется. Это не вам нужен мой проект. Это мне нужны вы.

Если у вас есть живой e2k-v6 — прогоните цепочки из examples/probes/ (mul_latency.c, div_latency.c, load_latency.c, store_burst.c...). Пришлите листинги lcc -O3 -fverbose-asm. Закроем 13 допущений, сверим LOAD 5 vs 3, померяем межкластер. Без вас эта таблица так и останется «замерами по поведению компилятора».

4. ИИ‑арка: как Qwen на 2 ГБ пытался стать оптимизатором

Как я учил планировщик класть широкие команды для Эльбруса, и почему LLM проиграла жадному алгоритму за 3 миллисекунды - 5

Изначальный план был наивный и красивый: научить LLM класть расписания напрямую. Граф на вход, строки id: такт=.. канал=.. на выход. Чем не планировщик?

База — Qwen2.5-3B-Instruct, QLoRA r16/alpha32, <2% обучаемых параметров. Адаптеры по ~120 МБ, три прогона, лежат открыто Shmonika/nex-vliw-lora — в git не влезли из‑за лимита 100 МБ. База в GGUF Q4_K_M — 2.1 ГБ, запускается на ноутбуке без GPU через llama.cpp.

Промпт — один и тот же код на обучение и на запуск (training/encode.py), иначе разъедется молча — уже проходили с EOS‑багом, когда модель без маркера конца не останавливалась и лила 24 лишних id:

vliw/core/     # чистый домен. Никаких print, никакого UI
vliw/cli.py    # Session, кэш, команды
vliw/ui/       # plain-рендер для терминала
vliw/tui/      # полноэкранный Textual
vliw/agent/    # разговорный агент поверх уже посчитанного
vliw/learned/  # LoRA-трек, опциональный
training/ tools/ examples/probes/ docs/

Датасет — 40 тысяч обученных данных: train_merged.jsonl — 39700 = dataset 20000 + extra 20000 - отсев. Пары граф -> оптимум через Oracle(budget=1.5с), только доказанные, размеры 6–14. Эвалы: eval 300, eval_wide 300 (4-24), eval_hard 300 (16-24, средний 20.3). Хотите — напишите мне, скину детали прогона, скрипты и промпты все открыты.

Что вышло по цифрам:

  • Скорость: baseline — миллисекунды, oracle на n=24 — 0.61с, 60 графов — 1.8с. Модель — 27–30с на граф, ~11 ток/с, на реале 178–395с, best-of-8 — 4 минуты на граф(после первого запроса скорость быстрее). Сервер держит 3.5 ГБ, без -c ограничения OOM‑киллер его убивал прямо посреди /learned.

  • Легкие выборки — пустые. Жадный уже 298/300 и 300/300 точно в оптимуме CP‑SAT. Выигрывать нечего. 477 секунд модели за тот же ответ, что эвристика дает мгновенно.

  • Прогон 0 без EOS — 26.7% валидных, прогон 1 с EOS — 97.7% на узком, 32.7% на широком. Все 11 провалов из 15 — STORE на чужом канале. В dataset не было ни одного STORE. Модель его никогда не видела.

  • Реал — 112-386 узлов против обучения на 4-24, словарь 37% UNKNOWN (предикаты, SIMD, AAU), 0/13 против случайного перебора за то же время.

То есть цифры сказали прямо: как бухгалтер модель медленная и хрупкая. Держать ее в ядре — значит платить 30 секунд за то, что алгоритм делает за 3 миллисекунды, и получать 17% законных на трудных графах.

Пришлось перейти на CAB (лучшее, что я случайно придумал).

5. CAB: забираем у модели бухгалтерию, оставляем чутье

Ладно, это очень смешно

Ладно, это очень смешно

CAB — “портфель законных расписаний из одного ответа модели”. Второй этап SEED стоит на нем, но суть — в первом.

Идея из docs/DIAG3.md: модель отлично чувствует порядок, но тонет в бухгалтерии — 315 незаконных каналов, 208 конфликтов, 26 неготовых операндов. При этом из 98 законных ответов 97 были точным оптимумом. Значит — забираем бухгалтерию целиком.

Что берем у модели:

берем

как

что выбрасываем

порядок

сортировка по ее (такт,канал)priority

такты

как defer_until «не раньше»

каналы

не берем вообще

самая частая ошибка

Бухгалтерию ведет list_schedule из core/baseline.py — он по построению не выдаст раньше предков и не посадит двоих на канал. Расписание законно механизмом, а не удачей.

Дорогая здесь только генерация. Поэтому из одного ответа строим 5 кандидатов без лишних генераций: модель, модель+каналы (починка Куном), порядок, порядок+такты, жадный. Плюс те же 5 со второй генерации по грамматике GBNF. Побеждает min (makespan), при равенстве — ответ самой модели, чтобы не приписывать алгоритму чужое. Подпись в UI честная: CAB: <кандидат>.

Предсказание, записанное ДО замера, на slotclash с убитыми в ,0 каналами:

  • сырой — 22, но незаконно

  • жадный — 23

  • порядок — 23

  • порядок+такты — 22 = оптимум

Порядка мало, нужно еще «придержать» операцию у монопольного ,5. defer_until это возвращает.

Замер на трудном эвале, где у жадного наконец есть зазор (0/300 в оптимуме, разрыв 1.03):

  • жадный — 1.03, 0/300

  • модель + CAB — 0.82, 59/300 точно в оптимуме (62/300 после замера STORE=2)

  • вклад: порядок 25, модель 16, порядок+такты 13, модель+каналы 7. Итого 61/300 строго лучше жадного — 20% трудных графов.

Без CAB на тех же данных — 17% законных. С CAB — 100% по построению. Рост законных ничего не доказывает, доказывают 5 тактов на 30 и 60+ графов, отыгранные у доказанного оптимума.

SEED-1 (подсказка верхней границей в оракул) при этом мертв замером, а не мнением: даже идеальный оптимум экономит 5% узлов, потому что поиск идет снизу вверх и тратит все на провальные пробы. Платить 30с генерации за 5% от 30мс — бессмысленно. Оставили в коде, пригодится если поиск пойдет сверху вниз.

6. Где ИИ сейчас: не бухгалтер, а мини‑агент

И тут важный дисклеймер.

ИИ неэффективен только в моем сетапе. У меня нет мощностей гонять модели на сотни гигабайт — тысячегиговые монстры явно умнее моего Qwen на 2.5 ГБ. Суть не в том, что «ИИ бесполезен», а в том, что я откинул его на задний план.

Он в проекте есть. Он просто занимается фоновым:

  • поставляет порядок/такты для CAB,

  • помогает алгоритму как эвристика,

  • пересказывает код и панели человеческим языком поверх уже посчитанных ядром чисел,

  • перебирает детали в /doctor, /explain, /agent.

По сути я собрал агентного мини‑ИИ для Эльбруса на той же Qwen: один llama-server на unix‑socket, база — для чата, LoRA — для плана, офлайн‑фолбэк с детерминированным ответом, если весов нет. Ядро считает, модель формулирует… Живая заливка решетки токенами, trace для разбора, --raw/--pure для аудита.

И он ждет большое железо.

Если у вас или у меня появится ПК/сервер помощнее, больше ресурсов — туда можно встроить модель поумнее, которая, возможно, справится лучше всяких алгоритмов. Будет ли это прорывом — я не знаю.
Но проект это позволяет: реализуете Scheduler(kind="learned") через те же encode_prompt/decode_completion, кладете адаптер в training/checkpoints/, GGUF в vendor/models/ — и compare/explain/bench/CAB подхватят без правок ядра. Сделаете модель бухгалтером — отлично. Не выйдет — откатитесь к помощнику одной строкой флага /learned --cab.

Пропорция смешивания трудных графов (DIV 20% против единиц процентов в реальном lcc), словарь реала, обрезка контекста 4096 — все это отдельные решения, которые еще предстоит принять. Лучше принимать их вместе.

Финал:

[Проект оупенсорс] модульный монолит, а не комбайн

Я специально не делал микросервисы и плагины ради плагинов. Внутри — модульный монолит:

vliw/core/     # чистый домен. Никаких print, никакого UI
vliw/cli.py    # Session, кэш, команды
vliw/ui/       # plain-рендер для терминала
vliw/tui/      # полноэкранный Textual
vliw/agent/    # разговорный агент поверх уже посчитанного
vliw/learned/  # LoRA-трек, опциональный
training/ tools/ examples/probes/ docs/

Ядро не знает про интерфейс. Оба рендера читают один Session.results():

# vliw/cli.py:197
def results(self):
    # (scenario, profile, width) -> (baseline, oracle, metrics)

Хочешь свой планировщик — реализуешь один протокол:

# vliw/core/api.py — ЭТО ТОЧКА ПОДСТАНОВКИ МОДЕЛИ
class Scheduler(Protocol):
    name: str; kind: str; short: str
    def schedule(self, dag: DAG, model: MachineModel) -> SchedulingResult: ...

kind = "heuristic" — жадный, "exact" — оракул, "learned" — модель. Весь остальной код — валидация, метрики, /compare, /explain, визуализация — не меняется.

Хочешь свою машину — не правишь планировщики:

MachineModel(name="my-e2k", ops={...}, ports=[...])
PROFILES["my"] = my  # или get_profile("e2k-v6-measured").with_width(3)

pyproject.toml честный: dependencies=[]. Ядро работает на голом Python 3.10. tui=[rich,textual], learned=[torch,transformers,peft] — только extras. Команды стандартные: run, load, compare, doctor, bounds, path, analyze, all, scenarios, sweep, selfcheck.

Это важно для всего, что ниже: проект позволяет пересобрать себя под вашу модель. Слот под «бухгалтера» готов.

Исходный код прототипа: https://github.com/smworklair/NEX‑CLI‑x-Elbrus
[4]Веса LoRA: https://huggingface.co/Shmonika/nex‑vliw‑lora [5]

Автор: Shmon

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ii/457988

Ссылки в тексте:

[1] model.py: http://model.py

[2] matrix.py: http://matrix.py

[3] ISA.md: http://ISA.md

[4] https://github.com/smworklair/NEX‑CLI‑x-Elbrus
: https://github.com/smworklair/NEX-CLI-x-Elbrus%EF%BF%BC

[5] https://huggingface.co/Shmonika/nex‑vliw‑lora: https://huggingface.co/Shmonika/nex-vliw-lora

[6] Источник: https://habr.com/ru/articles/1082094/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1082094