Дата публикации
science

ИИ‑агент, который сначала читает, а потом пишет код: как +15% скорости для llama.cpp изменили правила игры

Что открыли

Исследователи проверили простую идею: если дать код‑агенту не только репозиторий проекта, но и доступ к статьям, форкам и другим бэкендам, он находит оптимизации, которые не видит, работая только по коду.

Они добавили фазу «исследования литературы» в цикл автономного код‑агента и направили его на CPU‑инференс в llama.cpp для модели TinyLlama 1.1B с квантованием Q4_0.

Результат за один прогон:

  • агент провёл 30+ экспериментов;
  • из них 5 оптимизаций попали в итоговый код;
  • 4 из 5 — это слияние (fusion) ядер и проходов по памяти;
  • итоговый выигрыш для flash attention:
    • на x86 (AWS c6i.2xlarge, AVX‑512): +15,1% к скорости генерации текста (tg), +1,2% к скорости обработки промпта (pp);
    • на ARM (AWS c7g.2xlarge, NEON): +5% к tg, +1,9% к pp.

Для базового пути без flash attention три более «мелкие» оптимизации дали:

  • +2,5% к скорости обработки промпта (pp);
  • +0,9% к скорости генерации текста (tg).

Все улучшения проверили на TinyLlama 1.1B Q4_0 с помощью llama-bench:

  • параметры: -p 512 -n 128 -t 8 -r 5;
  • метрики: токены в секунду для prompt processing (pp) и text generation (tg).

Общий бюджет эксперимента:

  • ~3 часа работы;
  • 4 облачных VM;
  • ~$20 на CPU‑инстансы;
  • ~$9 на API‑вызовы;
  • всего ~$29.

Главный вывод: код‑агент, который сначала читает статьи, форки и другие бэкенды, а уже потом пишет патчи, генерирует более глубокие гипотезы и находит реальные ускорения там, где «агент по коду» застревает в микровыигрышах на уровне шума.


Как исследовали

От Karpathy к pi‑autoresearch

Исходная идея цикла «автоисследования» пришла из работы Андрея Карпати:

  • агент получает train.py и метрику качества (например, val_bpb);
  • сам предлагает изменения гиперпараметров и кода;
  • запускает эксперименты;
  • отбрасывает проигравшие варианты.

В одном из предыдущих запусков на 16 GPU агент провёл ~910 экспериментов за 8 часов и снизил val_bpb на 2,87%.

Дальше появился pi-autoresearch — переиспользуемый цикл для любого проекта, где есть бенчмарк и тесты. Shopify CEO Тоби Лютке запустил его на Liquid — Ruby‑шаблонизаторе, через который проходит $292 млрд годового товарооборота Shopify.

Результат Liquid‑прогона:

  • ~120 экспериментов;
  • 93 коммита;
  • время parse+render сократилось на 53%;
  • количество аллокаций — на 61%;
  • 974 юнит‑теста прошли без регрессий.

Там агенту хватило контекста исходников: он увидел, что узкое место — StringScanner, и перебрал варианты внутри кода.

Где «агент по коду» ломается

С llama.cpp ситуация другая. Здесь узкое место не в одном классе, а в архитектуре вычислений и работе с памятью. Вопросы, которые нужно задать:

  • модель ограничена вычислениями или памятью?
  • нужно ли сливать несколько проходов по памяти в один?
  • что уже сделали в ik_llama.cpp, llamafile, CUDA/Metal‑бэкендах?

Когда агент смотрел только на код, он пошёл по очевидному пути — микродооптимизация SIMD в горячих циклах GGML‑матриц. Примеры первых попыток:

  • предвыборка (prefetch) в внутреннем цикле dot‑product для Q4_0: +0,8%;
  • разворачивание цикла в 2 раза с двумя аккумуляторами: +0,9%;
  • удаление временного буфера в mul_mat: −2,8% (регрессия);
  • вынос расчёта границ блоков: +0,6%.

Все результаты — на уровне шума. В постмортеме агент сам сформулировал причину:

«Микрооптимизации в вычислительном пути почти не помогают, потому что генерация текста упирается в пропускную способность памяти, а не в вычисления».

Модель 606 MiB при ~49 токенах/с выкачивает ~30 ГБ/с из памяти — почти потолок DRAM для c6i.2xlarge. Когда CPU ждёт веса из DRAM, дополнительные SIMD‑трюки не спасают. Но из кода это неочевидно — нужны знания про roofline‑модель, типичные режимы batch‑size‑1 и характеристики железа.

Добавляем фазу ресёрча

Чтобы поднять качество гипотез, авторы добавили фазу исследования до первых правок кода:

  1. поиск по arXiv и другим источникам;
  2. анализ форков (ik_llama.cpp, llamafile и др.);
  3. сравнение CPU‑пути с CUDA/Metal/Vulkan/OpenCL‑бэкендами.

Базовый цикл autoresearch выглядит так:

  1. изменить код;
  2. запустить эксперимент;
  3. измерить метрику;
  4. принять или откатить.

pi-autoresearch обобщил это на любой проект с бенчмарком и тестами. В новой версии поверх этого добавили:

  • фазу ресёрча между «волнами» экспериментов;
  • параллельный запуск экспериментов через SkyPilot на нескольких VM.

Схема работы:

  • агент сам пишет autoresearch.sh (бенчмарк) и autoresearch.checks.sh (проверка корректности);
  • SkyPilot разворачивает по VM на эксперимент;
  • на каждой VM:
    • собирается проект;
    • запускается бенчмарк;
    • прогоняются проверки;
    • лог с метриками возвращается агенту;
  • агент читает sky logs, коммитит победителей, планирует новую волну.

Пример шаблона задачи SkyPilot (из experiment.yaml):

experiment.yaml : SkyPilot task template for one experiment
resources :
  cpus : 4
  memory : 8
  workdir : .
  envs :
    EXPERIMENT_ID : baseline
    EXPERIMENT_DESC : "baseline measurement"
    BUILD_CMD : "make -j$(nproc)"
    BENCH_TIMEOUT : 300
    CHECK_TIMEOUT : 300
setup : |
  cd ~/sky_workdir
  if [ -f setup_deps.sh ]; then
    bash setup_deps.sh
  else
    eval "${BUILD_CMD}"
  fi
run : |
  cd ~/sky_workdir
  # Build, benchmark, run checks, report METRIC lines
  eval "${BUILD_CMD}" 2>&1 | tail -30
  BENCH_OUTPUT=$(timeout "${BENCH_TIMEOUT}" bash autoresearch.sh 2>&1)
  echo "$BENCH_OUTPUT"
  # ... extract METRIC lines, run autoresearch.checks.sh ...
  echo "EXPERIMENT_STATUS: done"

Для оптимизации CPU‑кода GPU не нужны — при желании их можно явно запросить через --gpus.

Конкретный прогон на llama.cpp

Что именно сделали:

  • агент Claude Code получил репозиторий llama.cpp;
  • через SkyPilot ему выдали 4 VM на AWS;
  • цель: ускорить CPU‑инференс TinyLlama 1.1B Q4_0.

Аппаратные конфигурации:

  • x86: c6i.2xlarge (Intel Xeon Ice Lake, 8 vCPU, AVX‑512);
  • ARM: c7g.2xlarge (Graviton3, 8 vCPU, NEON).

Метрика:

  • llama-bench -p 512 -n 128 -t 8 -r 5;
  • измеряют pp (prompt processing) и tg (text generation) в токенах/с.

Сначала агент использовал 4 x86‑VM, чтобы снять базу и провести первые эксперименты. Потом добавил ARM‑VM, чтобы убедиться, что фьюжены работают и на NEON, и на AVX2/FMA, с безопасными скалярными запасными путями.

Что нашёл ресёрч

Агент запустил две параллельные ветки исследования:

  1. статьи и материалы про операторные фьюжены и flash attention;
  2. анализ форков и других бэкендов llama.cpp.

Ключевые находки:

  • ik_llama.cpp использует row‑interleaved репэкинг квантованных весов — это даёт 2,9× ускорение prompt processing. В llama.cpp это уже есть в виде формата Q4_0_8x8, и агент подтвердил, что он включён в бенчмарке.
  • статья Blockbuster предлагает сливать весь FFN‑блок (RMSNorm + gate matmul + up matmul + SwiGLU + down matmul) в один кэш‑резидентный проход. Агент попытался реализовать идею, но упёрся в ограничение: веса в формате Q4_0_8x8, а ggml_concat не умеет работать с репакнутыми квантованными тензорами. Для корректной реализации нужны изменения на уровне загрузчика модели, а не только графа.
  • агент проверил, используется ли AVX‑512 на c6i.2xlarge. Оказалось, да: флаг -march=native включает нужные макросы компилятора, даже если GGML_AVX512=OFF в CMake (эта переменная влияет только на сборку под MSVC).
  • PR #19139 в llama.cpp сливает веса gate+up для MoE‑моделей и даёт +12% к prompt processing, но для плотных моделей такой же трюк ещё не сделан.

Самой полезной оказалась не arXiv‑поисковая часть, а разбор форков и других бэкендов. Например, именно сравнение CPU‑пути с CUDA/Metal привело к идее фьюжена RMS_NORM + MUL на CPU.

После провала первой волны агент явно сменил стратегию:

«Нужно переключиться на оптимизации, которые уменьшают трафик памяти или улучшают паттерны доступа».

Матмулы занимают ~95% времени инференса. Остаётся ~5% на «мелочь» — softmax, RMSNorm, квантование. Но именно там операции достаточно маленькие, чтобы быть вычислительно ограниченными, а не памятью. Слияние проходов по памяти в этих местах даёт реальный выигрыш.


Что это меняет на практике

Пять оптимизаций, которые реально поехали

Из 30+ экспериментов в основной ветке осталось 5. Каждый отвечает за свой кусок «нематмульного» оверхеда.

1. Фьюжен softmax

Исходный код делал три прохода по данным:

  1. копировал буфер;
  2. масштабировал;
  3. добавлял маску.

До:

// Before: 3 passes
memcpy ( wp , sp , nc * sizeof ( float )); // pass 1: copy
ggml_vec_scale_f32 ( nc , wp , scale ); // pass 2: scale
ggml_vec_add_f32 ( nc , wp , wp , mp_f32 ); // pass 3: add mask

После фьюжена — один проход:

// After: 1 pass
for ( int i = 0 ; i < nc ; i ++ ) {
    wp [ i ] = sp [ i ] * scale + mp_f32 [ i ];
}

2. Фьюжен RMSNorm

Похожий приём для RMS‑нормализации. Было два шага:

  1. memcpy(y, x);
  2. ggml_vec_scale_f32(y, scale).

Теперь это один проход по массиву: y[i] = x[i] * scale.

3. Адаптивная параллелизация from_float

Функция from_float квантует активации в формат, который дальше идёт в dot‑product. Раньше стратегия распараллеливания была одинаковой для всех случаев.

Теперь агент разделил режимы:

  • при большом числе строк (prompt processing) — параллелизация по строкам;
  • при малом числе строк (text generation) — по элементам.

Чистое A/B‑сравнение на одной VM (без flash attention, чтобы изолировать эффект трёх первых изменений):

| | pp (токенов/с) | tg (токенов/с) | |-------------|---------------------|---------------------| | Базовый | 210.65 ± 0.64 | 48.90 ± 0.50 | | Оптимизированный | 215.97 ± 1.52 | 49.33 ± 0.37 | | Изменение | +2,5% | +0,9% |

Как и ожидалось, генерация почти не изменилась — она упирается в память и не затрагивает матмулы. Prompt processing, который ближе к compute‑bound, получил +2,5% за счёт меньшего числа проходов по данным.

4. Графовый фьюжен RMS_NORM + MUL на CPU

Эта оптимизация появилась напрямую из ресёрча по другим бэкендам. Агент заметил:

«Фьюжен RMS norm + MUL есть в CUDA, но его нет в CPU‑бэкенде».

Стандартный CPU‑путь делал:

  1. RMSNorm: читаем x, считаем норму, пишем y = x * scale;
  2. MUL: читаем y, читаем weights, пишем y = y * weights.

CUDA и Metal уже давно делают это в одном проходе: y = x * scale * weights.

Агент добавил в CPU‑исполнитель графа распознавание паттерна «RMS_NORM, за которым сразу идёт MUL от его вывода» и вызов нового фьюжен‑ядра. В AVX2‑ветке оно выглядит так:

// Fused RMS norm + multiply (AVX2 path)
__m256 vscale = _mm256_set1_ps ( scale );
for (; i + 7 < ne ; i += 8 ) {
    __m256 vx = _mm256_loadu_ps ( x + i );
    __m256 vw = _mm256_loadu_ps ( w + i );
    _mm256_storeu_ps ( y + i ,
        _mm256_mul_ps ( _mm256_mul_ps ( vx , vw ), vscale ));
}

Первая версия была скалярной и не дала выигрыша: компилятор уже хорошо векторизовал цепочку memcpy + ggml_vec_scale_f32 + binary_op<op_mul>. После переписывания на явные AVX2/NEON‑интринсики эффект стал заметен в сочетании с другими фьюженами и снизил разброс времени за счёт более предсказуемых паттернов доступа к памяти.

Важно: изначальная реализация графового фьюжена имела баг — не проверяла, что вывод RMSNorm не используется где‑то ещё в графе. Агент сам же поймал эту ошибку на «код‑ревью» и переключился на готовую инфраструктуру ggml_can_fuse(), которая уже есть во всех GPU‑бэкендах.

5. Фьюжен KQ в flash attention

В пути tiled flash attention над плиткой QK выполнялись три независимых прохода:

  1. масштабирование (scale);
  2. добавление маски;
  3. поиск максимума.

До фьюжена:

// Before: 3 passes over KQ tile
ggml_vec_scale_f32 ( M , kq , scale ); // pass 1
ggml_vec_add_f32 ( M , kq , kq , mask_row ); // pass 2
ggml_vec_max_f32 ( M , & max , kq ); // pass 3

После — один проход с AVX2 FMA:

// After: 1 AVX2 FMA pass
__m256 vscale = _mm256_set1_ps ( scale );
__m256 vmax = _mm256_set1_ps ( - INFINITY );
for ( int i = 0 ; i < M ; i += 8 ) {
    __m256 v = _mm256_fmadd_ps (
        _mm256_loadu_ps ( & kq [ i ]),
        vscale ,
        _mm256_loadu_ps ( & mask_row [ i ]));
    _mm256_storeu_ps ( & kq [ i ], v );
    vmax = _mm256_max_ps ( vmax , v );
}

Критично: эти фьюжены работают внутри уже существующего пути flash attention (-fa 1) в llama.cpp. Агент не «придумал» flash attention, он оптимизировал его реализацию. Поэтому для честного сравнения нужно включать -fa 1 и в базе, и в оптимизированной сборке.

Итоговые цифры по flash attention

Чистое A/B‑сравнение (5 повторов, обе сборки с flash attention):

x86, Intel Xeon (c6i.2xlarge, AVX‑512)

| Конфигурация | pp512 (т/с) | tg128 (т/с) | |--------------------|----------------------|----------------------| | База + FA | 241.24 ± 2.24 | 41.37 ± 19.24 | | Оптимизировано + FA| 244.22 ± 1.78 | 47.62 ± 0.59 | | Изменение | +1,2% | +15,1% |

ARM, Graviton3 (c7g.2xlarge, NEON)

| Конфигурация | pp512 (т/с) | tg128 (т/с) | |--------------------|----------------------|----------------------| | База + FA | 292.99 ± 2.47 | 94.07 ± 19.87 | | Оптимизировано + FA| 298.56 ± 4.28 | 98.77 ± 2.59 | | Изменение | +1,9% | +5% |

Ускорение генерации заметно больше, чем prompt processing: во время генерации внимание занимает большую долю общего времени. Ещё один эффект — резкое падение разброса значений tg на x86: с ±19 т/с до ±0,59 т/с. Фьюжены убирают лишние записи в память и делают поведение кэша более стабильным.

Исследователи подчёркивают: тесты шли на shared‑тенанси EC2, где «шумные соседи» могут давать до 30% разброса между прогонами. Они многократно перемеряли базу и ориентируются на направление эффекта, а не на точность до десятых долей процента.

Что не взлетело

25+ экспериментов не попали в итоговый diff. Несколько показательных примеров:

  • SIMD‑softmax с отложенным горизонтальным суммированием: идея — копить суммы в __m256 и один раз схлопывать в конце. Выигрыш 0%: компилятор и так векторизует скалярный цикл достаточно хорошо.
  • Тюнинг размеров тайлов в flash attention: варианты Q=32/KV=128, Q=128/KV=32, Q=32/KV=32. Базовый 64×64 оказался уже оптимальным.
  • Фьюжен gate+up через ggml_concat: попытка склеить матрицы gate и up на этапе построения графа, чтобы уменьшить число загрузок активаций. Крэш: ggml_concat не поддерживает репакнутые квантованные тензоры Q4_0_8x8. Нужно менять загрузчик модели, а не граф.
  • Prefetch для V во время вычисления softmax по QK: 0% — железный префетчер и так хорошо работает с последовательным доступом.
  • Удаление «лишних» загрузок в sgemm llamafile: функция загрузки Q4_0 делала denibble + subtract 8, и внутренний цикл вызывал её три раза для одного блока. Агент закешировал результат. Эффект 0% — компилятор уже провёл common subexpression elimination.

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

Ошибки и шум

Были и чисто инженерные проблемы:

  • В autoresearch.sh нашли баг в парсинге JSON‑вывода llama-bench: скрипт фильтровал по несуществующему полю и репортил 14 т/с вместо 52 т/с для генерации. Несколько экспериментов успели «опираться» на неправильную базу.
  • На shared‑VM разброс доходил до 30% между запусками. Одна из VM постоянно показывала высокий шум. Пришлось:
    • перезапускать особенно шумные инстансы;
    • использовать стандартное отклонение как сигнал качества;
    • доверять только результатам со stddev < 2% от среднего.

Что это значит для вас

Если вы запускаете LLM на CPU

  • llama.cpp — один из главных инструментов для локального запуска LLM на CPU. Ускорение на +15% tg для flash attention на x86 и +5% на ARM — это либо больше токенов в секунду на тех же машинах, либо меньше машин при той же нагрузке.
  • Фьюжены, которые сделал агент, не завязаны на TinyLlama как таковую. Они оптимизируют общие участки пути инференса: softmax, RMSNorm, quantization, flash attention. Любая модель в llama.cpp, использующая эти же коды путей и flash attention, потенциально выигрывает.
  • ARM‑результаты показывают, что оптимизации не заточены только под AVX‑512. На Graviton3 с NEON прирост тоже есть.

Пока авторы не отправили pull request в основной репозиторий, но diff уже готов. Если вы собираете llama.cpp из исходников, можно интегрировать эти патчи и протестировать на своём железе.

Если вы разрабатываете под open‑source‑стек

Главная практическая идея — менять входной контекст для код‑агента.

Когда агент видит только кодовую базу, он склонен к гипотезам вида «сделать этот цикл быстрее». Это работает там, где всё узкое место — внутри одного файла или модуля (пример Liquid с StringScanner).

Когда производительность зависит от архитектуры вычислений, памяти и решений из других проектов, нужно:

  • явно давать агенту задачи «прочитать статьи и форки»;
  • указывать, какие бэкенды или проекты считать «эталонными конкурентами»;
  • заставлять его сравнивать паттерны: «есть ли этот фьюжен в CUDA/Metal, но нет в CPU?».

В этом запуске именно такой подход привёл к оптимизациям #4 и #5, которые не лежали на поверхности исходников CPU‑пути.

Если вы строите свои код‑агенты

Сравнение двух режимов:

| | GPU Autoresearch (Karpathy) | Literature‑Guided Autoresearch | |----------------------|-----------------------------|---------------------------------| | Цель | Обучение нейросети | Любой OSS‑проект | | Железо | GPU‑кластеры (H100/H200) | CPU‑VM (дешевле) | | Стратегия поиска | Агент думает по коду | Агент читает статьи + профилирует | | Экспериментов | ~910 за 8 часов | 30+ за ~3 часа | | Время на эксперимент | ~5 минут (обучение) | ~5 минут (сборка + бенчмарк) | | Стоимость | ~$300 (GPU) | ~$20 (CPU) + ~$9 (API) |

CPU‑оптимизации дороже по времени на один эксперимент (CMake‑сборка + бенчмарк), но дешевле в долларах. Агент проводит меньше экспериментов, зато тратит часть времени между волнами на чтение статей и кодов конкурентов.

Если вы хотите повторить этот подход в своём проекте, вам нужны:

  • репозиторий с чётким бенчмарком и тестами;
  • код‑агент уровня Claude Code или GPT‑4/5 с доступом к интернету (или хотя бы к локальной базе статей и форков);
  • оркестратор вроде SkyPilot, чтобы параллелить эксперименты по VM;
  • явное требование к агенту: сначала ресёрч, потом патчи.

Как запустить свой цикл автоисследований

Ниже — упрощённая схема, основанная на описанном подходе. Конкретные скрипты у авторов завязаны на их инфраструктуру, но логика та же.

1. Напишите бенчмарк и проверки

Файл autoresearch.sh должен:

  • собирать проект (если нужно);
  • запускать бенчмарк (например, llama-bench или ваш тест);
  • печатать метрики в удобном для парсинга формате (JSON или строки с METRIC:).

Файл autoresearch.checks.sh:

  • запускает юнит‑тесты и sanity‑чек;
  • возвращает ненулевой код выхода при регрессиях.

2. Настройте шаблон задачи для SkyPilot

Пример уже был выше, но ещё раз ключевые поля:

resources :
  cpus : 4
  memory : 8
  workdir : .
  envs :
    EXPERIMENT_ID : baseline
    EXPERIMENT_DESC : "baseline measurement"
    BUILD_CMD : "make -j$(nproc)"
    BENCH_TIMEOUT : 300
    CHECK_TIMEOUT : 300
setup : |
  cd ~/sky_workdir
  if [ -f setup_deps.sh ]; then
    bash setup_deps.sh
  else
    eval "${BUILD_CMD}"
  fi
run : |
  cd ~/sky_workdir
  eval "${BUILD_CMD}" 2>&1 | tail -30
  BENCH_OUTPUT=$(timeout "${BENCH_TIMEOUT}" bash autoresearch.sh 2>&1)
  echo "$BENCH_OUTPUT"
  # ... extract METRIC lines, run autoresearch.checks.sh ...
  echo "EXPERIMENT_STATUS: done"

3. Дайте агенту права на ресёрч

В промпте к код‑агенту явно сформулируйте задачи:

  • прочитать статьи по вашей теме (операторные фьюжены, квантование, attention и т.д.);
  • изучить популярные форки вашего проекта;
  • сравнить разные бэкенды (CPU vs CUDA vs Metal и т.п.);
  • выписать список гипотез до первых изменений кода.

После каждой волны экспериментов просите его:

  • анализировать профилирование и результаты;
  • корректировать гипотезы;
  • отбрасывать направления, которые упёрлись в шум.

4. Следите за шумом и багами

  • измеряйте стандартное отклонение метрик;
  • перезапускайте особенно шумные VM;
  • регулярно проверяйте корректность скриптов парсинга (как показал пример с 14 т/с вместо 52 т/с).

Исследование показывает простой, но важный сдвиг: для сложных задач производительности недостаточно дать ИИ‑агенту один только код. Нужны статьи, форки, чужие бэкенды и доменные знания. Тогда он начинает задавать те же вопросы, что и сильный системный инженер — и приносит ускорения, которые реально чувствуются на продакшн‑нагрузке.


Читайте также

ИИ‑агент, который сначала читает, а потом пишет код: как +15% скорости для llama.cpp изменили правила игры — VogueTech | VogueTech