Что может локальный coding-agent на 16 GB VRAM
03 Окт 2026
Обычно в работе с кодом я использую frontier-модели. Для разработки у меня достаточно мощный ПК, но специально под запуск локальных моделей я его не собирал:
GPU: NVIDIA GeForce RTX 4070 Ti SUPER, 16 GB VRAM
RAM: 64 GB DDR5-6400
OS: Windows
Стало интересно, насколько далеко можно зайти на таком железе с современной локальной моделью. Не просто запустить чат и попросить написать функцию, а поработать с агентом, который читает настоящий проект, соблюдает правила репозитория и способен найти реальную проблему в коде.
Для эксперимента я взял Swift 1.5 Qwen3.8-27B Q8_0, запустил его через LM Studio и подключил к Cline внутри Cursor.
Получилось медленно. Иногда очень медленно. Но при разборе проекта модель нашла баг: после destroy() отложенный requestAnimationFrame всё ещё мог изменить canvas. Я отдельно сверил эту цепочку с исходниками — она подтвердилась. На разбор небольшого TypeScript-проекта ушло около двух часов.
Примерно эту границу мне и хотелось найти: что уже можно получить локально на обычной рабочей машине, а где до привычного опыта с frontier-моделями ещё далеко.
Почему Swift 1.5
Изначально я сформулировал для себя довольно узкую задачу: TypeScript, в перспективе WebGL/GLSL и работа с реальным проектом в агентном режиме. Скорость генерации была вторична, приоритет — качество кода.
Из нескольких вариантов на основе Qwen3.8-27B остановился на Swift 1.5. В актуальной карточке модели авторы делают акцент на программировании и работе в агентном режиме, показывают результаты на LiveCodeBench и Terminal-Bench.
Взял вариант Q8_0.
Можно было взять вариант поменьше, тем более что видеопамяти всего 16 GB. Но задача была не выжать максимум токенов в секунду, а посмотреть, что даст Q8_0, если позволить части модели жить в RAM.
Q8_0 сейчас занимает около 29 GB. В карточке модели он обозначен как вариант с максимальной fidelity. Для длинных задач в агентном режиме авторы рекомендуют Q6_K или выше.
То есть в видеокарту модель целиком не помещается даже близко.
29 GB модели и 16 GB VRAM
LM Studio на llama.cpp позволяет перенести часть модели на GPU, а остальное оставить в системной памяти.
Сначала получил рабочую конфигурацию с контекстом 32K:
Context Length: 32768
GPU Offload: 22
LM Studio оценил использование GPU примерно в 12.61 GB. nvidia-smi показал:
13154 MiB / 16376 MiB
После этого увеличил контекст до 65536. С прежней настройкой GPU Offload оценка расхода видеопамяти уже подбиралась к 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 — не универсальная рекомендация, а просто рабочее значение на моей машине. На другом железе нужно заново подобрать, сколько слоёв перенести на GPU, и обязательно проверить расход видеопамяти через nvidia-smi.
Сейчас LM Studio позволяет отдельно задавать размер контекста и число слоёв на GPU, а также учитывать эти параметры при оценке памяти.
Первый простой тест модель провалила
После настройки хотелось проверить что-нибудь совсем небольшое.
Дал Swift задачу написать TypeScript-функцию для компиляции шейдера WebGL:
Напиши на TypeScript функцию
compileShader(
gl: WebGL2RenderingContext,
type: GLenum,
source: string
): WebGLShader
Функция должна создать и скомпилировать shader. Если компиляция
завершилась ошибкой, получить текст через getShaderInfoLog(),
удалить shader и бросить Error.
Верни только код.
В ответе была строка:
throw new Error(Failed to create shader);
То есть 27B-модель выдала код с синтаксической ошибкой на задаче, которую сложно назвать серьёзным испытанием.
В этот момент в LM Studio был выставлен Temperature = 0.1.
Не падаем духом и не списываем модель в утиль. Настроил отдельный пресет:
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
После этого тот же тест прошёл нормально.
Оказалось, что эти значения практически совпадают с рекомендованными настройками генерации для 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. Стандартный адрес сервера — localhost:1234.
Сам API никаких сложностей не доставил, кроме одной мелочи с PowerShell: первый POST испортил кириллицу, и модель получила ????.
Пришлось явно кодировать тело запроса в 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 нормально это определил и не начал придумывать шейдеры или GLSL. В обзорном проходе он довольно точно восстановил структуру: типизированные массивы, таблицу значений для fastSin, пакетную отрисовку, работу с DPR, ResizeObserver, visibilitychange, адаптивную настройку производительности и ограничение количества частиц.
Был и менее приятный момент. Для обхода репозитория агент сначала предложил:
Get-ChildItem d:\tests\snowfall-canvas -Recurse -File |
Where-Object {
$_.FullName -notmatch 'node_modules|\.git|dist|build|out|coverage|\.next'
}
Проблема в том, что фильтрация здесь происходит уже после -Recurse. Тяжёлые каталоги всё равно могут физически обходиться.
Команду отклонил.
После Reject Swift перестроил работу и перешёл к более точечному чтению и поиску.
AGENTS.md вместо надежды на хороший промпт
После первого прохода добавил в проект AGENTS.md.
Главные правила были достаточно простыми:
- без широкого recursive traversal корня;
- не обходить node_modules, .git и другие тяжёлые каталоги;
- предпочитать targeted reads/search;
- repo-wide traversal только после разрешения;
- для shell использовать PowerShell 7;
- искать root cause, а не латать симптом;
- не добавлять абстракции и зависимости без необходимости.
Туда же попал принцип “Lazy senior”: сначала понять задачу, проверить YAGNI, поискать существующее решение, стандартную возможность платформы или уже установленную зависимость, и только потом писать новый код.
Cline распознал файл как правила проекта. Отдельно попросил его без обращения к инструментам перечислить действующие правила — основные ограничения он восстановил корректно. Поддержка AGENTS.md сейчас есть и в официальной документации Cline.
Дополнительно я использовал .clineignore для тяжёлых каталогов. В текущей документации Cline он уже помечен как deprecate soon. Ограничения на инструменты и ручное подтверждение действий для этого важнее.
После обзорного анализа захотелось дать задачу, результат которой будет гораздо более жёсткой проверкой.
Найди настоящий баг или скажи, что его нет
Swift получил задачу проверить код того же snowfall-canvas без правок.
Условия специально сформулировал так, чтобы модель не могла отделаться стандартным набором “улучшений”:
- ничего не редактировать;
- не запускать команды;
- не предлагать косметический рефакторинг;
- не предлагать новые абстракции;
- искать реальный defect / edge case / баг;
- соблюдать AGENTS.md;
- если убедительного дефекта нет — так и сказать.
Если проблема найдётся, требовалось указать:
- где она находится;
- сценарий воспроизведения;
- причину ошибки;
- минимальный фикс;
- одну проверку, которую стоит добавить.
После этого Swift думал примерно два часа.
Для небольшого TypeScript-проекта это чрезмерно. При настройке Unrestricted казался естественным выбором ради максимального качества, но на практике ждать пришлось слишком долго.
Зато найденная проблема оказалась не косметикой.
requestAnimationFrame, который пережил destroy()
В обработке изменения размера был отдельный requestAnimationFrame.
Упрощённо:
requestAnimationFrame(() => {
this.resizeQueued = false;
this.resizeToContainer();
});
Проблема в том, что id этого rAF нигде не сохранялся.
destroy() при этом нормально завершал остальную работу экземпляра:
- отменял rAF основного цикла отрисовки;
- отключал
ResizeObserver; - снимал обработчики событий.
Но про отложенный rAF для изменения размера он ничего не знал.
Swift восстановил такой сценарий:
const snow = new SnowfallCanvas({ canvas, container });
snow.init();
Затем:
- контейнер меняет размер;
ResizeObserverилиrequestResize()запускает обработку изменения размера;queueResize()ставитrequestAnimationFrame;- до следующего кадра вызывается
snow.destroy(); - экземпляр уже завершил работу;
- отложенный вызов всё равно срабатывает;
- вызывается
resizeToContainer(); - затем
applyResize().
В итоге старый экземпляр после destroy() снова меняет:
canvas.width
canvas.height
ctx.setTransform(...)
Это уже не просто внутренний флаг, который остался в неправильном состоянии.
Canvas принадлежит коду, использующему библиотеку. После destroy() разумно ожидать, что старый экземпляр его больше не тронет.
Особенно неприятно, если после уничтожения компонента canvas сразу использует другой код отрисовки. Через кадр старый SnowfallCanvas приходит со своим отложенным вызовом и меняет буфер холста.
Минимальный фикс
Swift предложил отдельно сохранять id отложенного вызова:
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;
}
То есть без отдельного планировщика и новых абстракций. Сохраняем id отложенного requestAnimationFrame и отменяем вызов в destroy().
Для теста на этот баг модель предложила тот же сценарий в минимальном виде:
requestResize()
-> rAF scheduled
-> destroy()
-> cancelAnimationFrame(same id)
Этого достаточно, чтобы проверить причину ошибки без симуляции Vue или сложных сценариев взаимодействия с DOM.
После ответа я отдельно сверил найденную цепочку с исходниками.
Она подтвердилась:
queueResize()
-> requestAnimationFrame()
-> id не сохраняется
destroy()
-> отменяет только основной rafId
-> resize-rAF остаётся pending
pending callback
-> resizeToContainer()
-> applyResize()
-> canvas.width / canvas.height / setTransform()
После проверки эксперимент для меня стал заметно интереснее простого запуска локальной LLM.
Общий обзор проекта может звучать убедительно и без полезных находок. Здесь модель нашла конкретный баг, который подтвердился при проверке исходников, и предложила небольшой фикс, устраняющий его причину.
Заодно она заметила ещё один подозрительный случай в адаптивной настройке производительности: механизм снижения нагрузки потенциально способен увеличить количество частиц. Но не стала смешивать его с основной находкой и оставила как дополнительное замечание, требующее отдельной проверки.
Хорошее ревью ещё не означает автономного агента
Два часа на разбор — первое очевидное ограничение.
Для такого проекта это слишком дорого по времени, даже если электричество и API-токены в данном случае не считаются.
Причём у Swift 1.5 есть несколько уровней reasoning effort. Авторы модели отдельно тестируют xhigh, medium и low, так что максимальный режим вовсе не обязательно использовать для каждого ревью.
Это один из следующих экспериментов: снизить reasoning effort и посмотреть, насколько быстрее модель работает и что происходит с качеством поиска багов.
Есть и менее заметная проблема — локальной модели всё равно нельзя автоматически верить в отчёте о собственных действиях.
Условие задачи было:
не запускай команды
Swift позже написал:
никаких команд изменения состояния не запускал
Формулировки похожи, но смысл уже чуть-чуть другой.
Если по журналу вызовов видно, что агент действительно не обращался к терминалу, никакого инцидента нет. Но такая подмена показывает, как модель может истолковать ограничение по-своему и затем уверенно описать собственное поведение.
То же относится к заявлениям вроде “посимвольно сравнил” или к проверкам dist/: если это существенно для вывода, лучше смотреть журнал вызовов инструментов, чем принимать отчёт модели за доказательство.
Поэтому режим только для чтения и ручное подтверждение Edit/Terminal в начале оказались вполне оправданными.
Не потому, что Swift постоянно пытался сделать что-то опасное. Большая часть поведения как раз выглядела разумно. Просто цена ошибки гораздо ниже, когда сначала понимаешь, как конкретная модель пользуется инструментами и соблюдает ограничения.
Конфигурация для повторения
Железо
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.
Настройки генерации
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 — параметр этого эксперимента, а не моя рекомендация. После двухчасового разбора как раз его хочется скорректировать первым.
Локальный API
http://127.0.0.1:1234
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 документация рассматривает как вариант для более крупных моделей и большего контекста.
В моём случае связка:
Cursor
-> Cline
-> LM Studio
-> Swift 1.5 Qwen3.8-27B Q8_0
дошла от простой проверки API до разбора реального TypeScript-проекта без правок и нашла баг, который подтвердился по исходникам.
Но называть её после этого полноценной заменой frontier-агента для работы с кодом, мягко говоря, я бы не стал.
Дальше планирую проверить, как Swift сама внесёт этот фикс, какой сделает diff, напишет ли нормальный тест на этот баг и сможет ли пройти цикл:
inspect
-> edit
-> test
-> fix
Не проверены: изменения в нескольких файлах, настоящий WebGL/GLSL, большой репозиторий, длинная сессия с агентом.
Баг найден и проверен. Можно включить Act mode, оставить Edit и Terminal на ручном подтверждении и смотреть, сможет ли та же локальная 27B-модель сама аккуратно довести исправление до работающего теста.