Как я учил планировщик класть широкие команды для Эльбруса, и почему LLM проиграла жадному алгоритму за 3 миллисекунды. cli.. cli. Open source.. cli. Open source. python.. cli. Open source. python. ИИ.. cli. Open source. python. ИИ. Компиляторы.. cli. Open source. python. ИИ. Компиляторы. Процессоры.. cli. Open source. python. ИИ. Компиляторы. Процессоры. процессоры эльбрус.. cli. Open source. python. ИИ. Компиляторы. Процессоры. процессоры эльбрус. эльбрус.. cli. Open source. python. ИИ. Компиляторы. Процессоры. процессоры эльбрус. эльбрус. энтузиасты.
Как я учил планировщик класть широкие команды для Эльбруса, и почему 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. Матрица портов — спрошена у ассемблера, а не угадана по листингу.
Для каждой пары (операция, канал) собирается крошечный .s. Если так нельзя аппаратно — ассемблер отвечает прямым отказом: 'muls' cannot be encoded in ALC2. Это ответ про кодировку широкой команды, про само железо, а не про то, что решил планировщик lcc. Автомат этого — tools/probe_matrix.py.

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;

  • 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
Веса LoRA: https://huggingface.co/Shmonika/nex‑vliw‑lora

Автор: Shmon

Источник