Что может локальный coding-agent на 16 GB VRAM

03 Окт 2026

Обычно в работе с кодом я использую frontier-модели. Для разработки у меня достаточно мощный ПК, но специально под локальный inference я его не собирал:

GPU: NVIDIA GeForce RTX 4070 Ti SUPER, 16 GB VRAM
RAM: 64 GB DDR5-6400
OS: Windows

Стало интересно, насколько далеко можно зайти на таком железе с современной локальной моделью. Не просто запустить чат и попросить написать функцию, а дойти до coding-agent, который читает настоящий проект, соблюдает правила репозитория и способен найти реальную проблему в коде.

Для эксперимента я взял Swift 1.5 Qwen3.8-27B Q8_0, запустил его через LM Studio и подключил к Cline внутри Cursor.

Получилось медленно. Иногда очень медленно. Но в первом настоящем read-only code review модель нашла lifecycle-баг с requestAnimationFrame, который потом подтвердился при отдельной проверке исходников. На разбор небольшого TypeScript-проекта она при этом потратила около двух часов.

Примерно эту границу мне и хотелось найти: что уже можно получить локально на обычной рабочей машине, а где до привычного опыта с frontier-моделями ещё далеко.

Почему Swift 1.5

Изначально я сформулировал для себя довольно узкую задачу: TypeScript, в перспективе WebGL/GLSL и работа с реальным проектом в агентном режиме. Скорость генерации была вторична, приоритет — качество кода.

Из нескольких Qwen3.8-27B-производных остановился на Swift 1.5. Модель ориентирована в том числе на coding, agentic и long-horizon задачи. В актуальной карточке модели авторы отдельно показывают результаты на LiveCodeBench и Terminal-Bench и называют coding/agentic одним из основных направлений post-training.

Для quant выбрал Q8_0.

Можно было взять вариант заметно меньше, тем более что видеопамяти всего 16 GB. Но задача была не выжать максимум tokens per second, а посмотреть, что даст более качественный quant, если позволить части модели жить в RAM.

Q8_0 сейчас занимает около 29 GB. В карточке модели он обозначен как вариант с максимальной fidelity, а для длинных agentic-задач авторы рекомендуют Q6_K или выше.

То есть в видеокарту модель целиком не помещается даже близко.

29 GB модели и 16 GB VRAM

LM Studio на llama.cpp умеет partial GPU offload: часть модели работает через GPU, оставшееся остаётся в системной памяти.

Сначала получил рабочую конфигурацию с контекстом 32K:

Context Length: 32768
GPU Offload:    22

LM Studio оценил использование GPU примерно в 12.61 GB. nvidia-smi показал:

13154 MiB / 16376 MiB

После этого увеличил контекст до 65536. С прежним offload оценка VRAM уже подбиралась к 14.5 GB, поэтому количество слоёв на GPU уменьшил до 19.

Финальный вариант:

Context Length: 65536
GPU Offload:    19

Оценка LM Studio:

GPU:   12.54 GB
Total: 42.87 GB

Фактически через nvidia-smi:

12882 MiB / 16376 MiB

Осталось около 3.4 GiB VRAM.

После этой настройки локальный запуск перестал выглядеть для меня как задача “влезет ли файл модели в видеокарту”. Не влезает. И не обязан.

В этой конфигурации видеокарта берёт часть работы на себя, остальные веса остаются в 64 GB RAM, а расход памяти дополнительно зависит от контекста и KV-cache.

Поэтому GPU Offload = 19 — не универсальная рекомендация, а просто рабочее значение на моей машине. Под другое железо нужно индивидуально подбирать offload и обязательно смотреть на фактический VRAM через nvidia-smi.

LM Studio и сейчас позволяет отдельно задавать context length и GPU offload, а также учитывать эти параметры при оценке памяти.

Первый простой тест модель провалила

После настройки хотелось проверить что-нибудь совсем небольшое.

Дал Swift задачу написать TypeScript-функцию для компиляции WebGL shader:

Напиши на TypeScript функцию

compileShader(
  gl: WebGL2RenderingContext,
  type: GLenum,
  source: string
): WebGLShader

Функция должна создать и скомпилировать shader. Если компиляция
завершилась ошибкой, получить текст через getShaderInfoLog(),
удалить shader и бросить Error.

Верни только код.

В ответе была строка:

throw new Error(Failed to create shader);

То есть 27B-модель выдала синтаксически невалидный TypeScript на задаче, которую сложно назвать серьёзным coding benchmark.

В этот момент в LM Studio был выставлен Temperature = 0.1.

Не падаем духом и не списываем модель в утиль. Настроил отдельный preset:

Temperature:       1.0
Top K:             20
Top P:             0.95
Min P:             0
Repeat Penalty:    1.0
Presence Penalty:  0
Thinking:          ON
Reasoning Budget:  Unrestricted

После этого тот же тест прошёл нормально.

Оказалось, что эти значения практически совпадают с рекомендованным sampling самой Swift 1.5:

temperature:        1.0
top_p:              0.95
top_k:              20
min_p:              0
presence_penalty:   0
repetition_penalty: 1

С локальной моделью мало скачать правильные веса. Настройки инференса тоже заметно влияют на то, с какой моделью в итоге работаешь.

Около шести токенов в секунду

После настройки скорость была примерно такой:

~6 tok/s

В одном из замеров:

5.92 tok/s
654 tokens
thinking ~1:35
MTP draft acceptance ~63.6%

В другом MTP acceptance был около 65%.

Быстрой такую работу не назовёшь. После облачных frontier-моделей разница хорошо чувствуется.

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

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

От локальной модели к агенту

Для первого запуска использовал LM Studio ещё и потому, что через него легко получить локальный API.

Сервер поднялся на:

http://127.0.0.1:1234

Проверка:

Invoke-RestMethod http://127.0.0.1:1234/v1/models

показала модель:

swift-1.5-qwen3.8-27b

После этого подключил Cline в Cursor:

API Provider:   LM Studio
Model:          swift-1.5-qwen3.8-27b
Context Window: 65536

Получилась такая цепочка:

Cursor
  -> Cline
    -> LM Studio
      -> Swift 1.5 Qwen3.8-27B Q8_0

Cline сейчас напрямую поддерживает LM Studio как локальный provider, со стандартным сервером на localhost:1234.

Сам API никаких сложностей не доставил, кроме одной мелочи с PowerShell: первый POST испортил кириллицу, и модель получила ????.

Пришлось явно кодировать JSON body в UTF-8:

$body = @{
    model = "swift-1.5-qwen3.8-27b"
    messages = @(
        @{
            role = "user"
            content = "Ответь одной строкой: API работает"
        }
    )
    temperature = 1.0
    top_p = 0.95
} | ConvertTo-Json -Depth 10

$utf8Body = [System.Text.Encoding]::UTF8.GetBytes($body)

$response = Invoke-RestMethod `
    -Uri "http://127.0.0.1:1234/v1/chat/completions" `
    -Method Post `
    -ContentType "application/json; charset=utf-8" `
    -Body $utf8Body

$response.choices[0].message.content

После этого получил:

API работает

Сначала только читать

Подключение агента прошло легко. Сложнее оказалось решить, сколько ему сразу разрешить.

Изначально Auto-approve включал:

Read
Edit
Web Fetch
MCP

Для начала оставил ему только:

Read

Edit, Terminal и остальные действия — вручную. Cline работает в Plan mode.

Сначала хотелось понять, насколько предсказуемо локальная модель работает с настоящим репозиторием, и только потом разрешать ей что-либо менять.

Под рукой не оказалось WebGL-проекта, и я решил поэкспериментировать над чем-то попроще. С прошлого Нового года у меня остался пакет snowfall-canvas, реализующий Canvas 2D “новогодний снегопад” для веб-приложений.

Swift нормально это определил и не начал придумывать shaders или GLSL. В обзорном проходе он довольно точно восстановил структуру: typed arrays, LUT для fastSin, batching отрисовки, DPR logic, ResizeObserver, visibilitychange, adaptive performance и ограничение количества частиц.

Был и менее приятный момент. Для обхода репозитория сначала агент предложил:

Get-ChildItem d:\tests\snowfall-canvas -Recurse -File |
Where-Object {
    $_.FullName -notmatch 'node_modules|\.git|dist|build|out|coverage|\.next'
}

Проблема в том, что фильтрация здесь происходит уже после -Recurse. Тяжёлые каталоги всё равно могут физически обходиться.

Команду отклонил.

После Reject Swift перестроил работу и перешёл к более точечным reads/search.

AGENTS.md вместо надежды на хороший промпт

После первого прохода добавил в проект AGENTS.md.

Главные правила были достаточно простыми:

- без широкого recursive traversal корня;
- не обходить node_modules, .git и другие тяжёлые каталоги;
- предпочитать targeted reads/search;
- repo-wide traversal только после разрешения;
- для shell использовать PowerShell 7;
- искать root cause, а не латать симптом;
- не добавлять абстракции и зависимости без необходимости.

Туда же попал принцип “Lazy senior”: сначала понять задачу, проверить YAGNI, поискать существующее решение, стандартную возможность платформы или уже установленную зависимость, и только потом писать новый код.

Cline увидел файл как workspace rules. Отдельно попросил его без tools перечислить действующие правила — основные ограничения он восстановил корректно. Поддержка AGENTS.md сейчас есть и в официальной документации Cline.

Дополнительно я использовал .clineignore для тяжёлых каталогов. В текущей документации Cline он уже помечен как deprecate soon. Ограничения на tools и ручное подтверждение действий для этого важнее.

После обзорного анализа захотелось дать задачу, результат которой будет гораздо более жёсткой проверкой.

Найди настоящий баг или скажи, что его нет

Swift получил read-only code review того же snowfall-canvas.

Условия специально сформулировал так, чтобы модель не могла отделаться стандартным набором “улучшений”:

- ничего не редактировать;
- не запускать команды;
- не предлагать косметический рефакторинг;
- не предлагать новые абстракции;
- искать реальный defect / edge case / баг;
- соблюдать AGENTS.md;
- если убедительного дефекта нет — так и сказать.

Если проблема найдётся, требовалось указать:

  1. где она находится;
  2. сценарий воспроизведения;
  3. root cause;
  4. минимальный фикс;
  5. одну проверку, которую стоит добавить.

После этого Swift думал примерно два часа.

Для небольшого TypeScript-проекта это чрезмерно. Unrestricted reasoning, который на этапе настройки казался естественным выбором ради максимального качества, оказался вполне реальным ограничением практической работы.

Зато найденная проблема оказалась не косметикой.

requestAnimationFrame, который пережил destroy()

В resize logic был отдельный requestAnimationFrame.

Упрощённо:

requestAnimationFrame(() => {
    this.resizeQueued = false;
    this.resizeToContainer();
});

Проблема в том, что id этого rAF нигде не сохранялся.

destroy() при этом нормально завершал остальной lifecycle:

  • отменял основной render-loop rAF;
  • отключал ResizeObserver;
  • снимал listeners.

Но про pending resize-rAF он ничего не знал.

Swift восстановил такой сценарий:

const snow = new SnowfallCanvas({ canvas, container });
snow.init();

Затем:

  1. контейнер меняет размер;
  2. ResizeObserver или requestResize() вызывает resize logic;
  3. queueResize() ставит requestAnimationFrame;
  4. до следующего кадра вызывается snow.destroy();
  5. объект уже закончил lifecycle;
  6. pending callback всё равно срабатывает;
  7. вызывается resizeToContainer();
  8. затем applyResize().

В итоге старый экземпляр после destroy() снова меняет:

canvas.width
canvas.height
ctx.setTransform(...)

Это уже не просто внутренний флаг, который остался в неправильном состоянии.

Canvas принадлежит коду, использующему библиотеку. После destroy() разумно ожидать, что старый экземпляр его больше не тронет.

Особенно неприятен сценарий с SPA или component unmount: компонент уничтожается, canvas сразу переиспользует другой renderer, а через кадр старый SnowfallCanvas приходит со своим отложенным callback и меняет backing buffer.

Минимальный фикс

Swift предложил сохранить отдельный handle:

private resizeRafId: number | null = null;

При планировании:

private queueResize = (): void => {
    if (this.resizeQueued) return;

    this.resizeQueued = true;

    this.resizeRafId = requestAnimationFrame(() => {
        this.resizeRafId = null;
        this.resizeQueued = false;
        this.resizeToContainer();
    });
};

А в destroy():

if (this.resizeRafId !== null) {
    cancelAnimationFrame(this.resizeRafId);
    this.resizeRafId = null;
    this.resizeQueued = false;
}

То есть без scheduler, нового helper layer или очередной абстракции. Потеряли lifecycle handle — сохраняем его и отменяем при завершении lifecycle.

Для regression test модель предложила тот же сценарий в минимальном виде:

requestResize()
-> rAF scheduled
-> destroy()
-> cancelAnimationFrame(same id)

Этого достаточно, чтобы проверить сам root cause без симуляции Vue или сложного DOM lifecycle.

После ответа я отдельно сверил найденную цепочку с исходниками.

Она подтвердилась:

queueResize()
-> requestAnimationFrame()
-> id не сохраняется

destroy()
-> отменяет только основной rafId
-> resize-rAF остаётся pending

pending callback
-> resizeToContainer()
-> applyResize()
-> canvas.width / canvas.height / setTransform()

После проверки эксперимент для меня стал заметно интереснее простого запуска локальной LLM.

Архитектурный summary можно сделать убедительным почти на любом проекте. Здесь модель нашла конкретный delayed-callback race, дошла от потерянного handle до внешней мутации после destroy() и предложила небольшой фикс, который соответствует причине проблемы.

Заодно она заметила ещё один подозрительный edge case в adaptive performance, где механизм снижения нагрузки потенциально способен увеличить количество частиц. Но не стала смешивать его с основной находкой и оставила как secondary observation, требующий отдельной проверки.

Хороший review всё ещё не означает автономного агента

Два часа reasoning — первое очевидное ограничение.

Для такого проекта это слишком дорого по времени, даже если электричество и API-токены в данном случае не считаются.

Причём у Swift 1.5 есть несколько reasoning effort. Авторы модели отдельно тестируют xhigh, medium и low, так что максимальный режим вовсе не обязательно использовать для каждого review.

Это один из следующих экспериментов: уменьшить effort и посмотреть, насколько быстрее модель работает и что происходит с качеством поиска багов.

Есть и менее заметная проблема — локальной модели всё равно нельзя автоматически верить в отчёте о собственных действиях.

Условие задачи было:

не запускай команды

Swift позже написал:

никаких команд изменения состояния не запускал

Формулировки похожи, но смысл уже чуть-чуть другой.

Если tool log показывает, что terminal calls действительно не было, никакого инцидента нет. Но такая подмена показывает, как модель может слегка переинтерпретировать ограничение и затем уверенно описать собственное поведение.

То же относится к заявлениям вроде “посимвольно сравнил” или к проверкам dist/: если это существенно для вывода, лучше смотреть реальный tool log, а не принимать self-report модели как доказательство.

Поэтому read-only и ручное подтверждение Edit/Terminal в начале оказались вполне оправданными.

Не потому, что Swift постоянно пытался сделать что-то опасное. Большая часть поведения как раз выглядела разумно. Просто цена ошибки гораздо ниже, когда сначала понимаешь, как конкретная модель обращается с tools и ограничениями.

Конфигурация для повторения

Железо

NVIDIA GeForce RTX 4070 Ti SUPER, 16 GB VRAM
64 GB DDR5-6400
Windows

Модель

ukisai/Swift-1.5-Qwen3.8-27B-GGUF
Q8_0

LM Studio

Context Length:             65536
GPU Offload:                19
Flash Attention:            ON
Offload KV Cache to GPU:    ON
K/V Cache Quantization:     OFF
Max Concurrent Predictions: 1
MTP:                        ON
mmap:                       ON
Keep Model in Memory:       ON
RoPE:                       Auto

На другой машине GPU Offload нужно подобрать заново. Смысл настройки — оставить нормальный запас по VRAM и проверить результат через nvidia-smi.

Sampling

Temperature:       1.0
Top K:             20
Top P:             0.95
Min P:             0
Repeat Penalty:    1.0
Presence Penalty:  0
Thinking:          ON
Reasoning Budget:  Unrestricted

Unrestricted — параметр этого эксперимента, а не моя рекомендация. После двухчасового review как раз его хочется скорректировать первым.

Local API

http://127.0.0.1:1234

Model ID:

swift-1.5-qwen3.8-27b

Cline

API Provider:   LM Studio
Model:          swift-1.5-qwen3.8-27b
Context Window: 65536
Mode:           Plan
Auto-approve:   Read only

Для локальных моделей Cline сейчас рекомендует держать задачи сфокусированными и начинать новую задачу, когда контекст становится слишком большим. Машины с 64 GB+ RAM документация относит к диапазону для более крупных моделей и больших context windows.

В моём случае эта связка:

Cursor
  -> Cline
    -> LM Studio
      -> Swift 1.5 Qwen3.8-27B Q8_0

дошла от простого API smoke test до read-only code review реального TypeScript-проекта и нашла подтверждённый lifecycle-баг.

Но называть её после этого полноценной заменой frontier coding-agent, мягко говоря, я бы не стал.

Дальше планирую проверить, как Swift сама внесёт этот фикс, какой сделает diff, напишет ли нормальный regression test и сможет ли пройти цикл:

inspect
-> edit
-> test
-> fix

Не проверены: multi-file изменения, настоящий WebGL/GLSL, большой репозиторий, длинная agent-сессия.

Баг найден и проверен. Можно включить Act mode, оставить Edit и Terminal на ручном подтверждении и смотреть, сможет ли та же локальная 27B-модель сама аккуратно довести исправление до работающего теста.