Чи варто брати більшу LoRA? Порівнюйте точність із ціною, а не з відчуттями
Невеликий стенд, який зберігає рецепти LoRA як дані й показує точність, час навчання, затримку та пам'ять, щоб вибір рангу й кількості ітерацій спирався на цифри.
Тека з тринадцятьма експериментами
Уявіть теку з тринадцятьма експериментами з донавчання. Там є перенесення стилю, витягування JSON, маршрутизація інструментів, налаштування вподобань для повідомлень комітів, дистиляція ланцюжка міркувань і мультимодальний прогін, що читає стовпчикові діаграми. Усе це LoRA поверх Llama на 1B, навчена локально на Mac з M-чипом через MLX. Робочий процес змінив експеримент номер дванадцять. Він не вчить модель нічого нового. Він лише як слід вимірює всі інші.
Питання, на яке відповідають інтуїцією
Візьмімо задачу класифікації. Це могла б бути маршрутизація тікетів підтримки. Тут же треба було розмічати pull request'и на GitHub категоріями conventional commits: bug, feat, docs, refactor, test, chore, perf і style. Виходить вісім класів з акуратно розділеними навчальною та тестовою вибірками.
Ви донавчаєте невелику модель, і вона працює. Далі постає питання, яке постає завжди: а чи не виявиться ранг 16 кращим за ранг 4?
Зазвичай на нього відповідають міркуванням. Ранг задає розмірність низькорангового оновлення, більший ранг — більша ємність, класів вісім і є мовні тонкощі, отже, мабуть, так. Ранг підвищують, точність зсувається на величину, яку легко списати на шум, і зміна лишається, бо час уже витрачено. Це не інженерія. Це здогадка з прикладеним конфігом.
Справжня проблема була в тому, що ніхто не питав, чи окупає себе більший адаптер, а для цього треба мати, на що ділити. Адаптер рангу 16 на 16 шарах має приблизно у вісім разів більше навчуваних параметрів, ніж ранг 4 на 8 шарах. Він довше навчається, довше зливається з вагами, і викочувати доводиться вже інший артефакт. Якщо він дає один пункт точності, угода погана. Якщо п'ятнадцять, рішення очевидне. Поки витрати ніхто не вимірював, зрозуміти, який із випадків ваш, було неможливо.
Рецепти як звичайні дані
Рішенням став стенд, побудований на нудній ідеї, і в цьому весь сенс: конфігурація навчання — це дані, а не код.
RECIPES = [
{"name": "base-zs", "train": None, "shots": 0},
{"name": "base-fs", "train": None, "shots": 4},
{"name": "lora-r4-l8", "train": {"lora_rank": 4, "num_layers": 8, "iters": 600}},
{"name": "lora-r16-l16", "train": {"lora_rank": 16, "num_layers": 16, "iters": 600}},
{"name": "lora-long", "train": {"lora_rank": 8, "num_layers": 8, "iters": 1500}},
]
П'ять елементів списку, і все. Для кожного прогін робить усе сам: навчає адаптер, вливає його в базові ваги, додає злиту модель в Ollama через згенерований Modelfile, оцінює її на відкладеній тестовій вибірці й записує результат.
Два рецепти взагалі обходяться без навчання. Так задумано, і саме цю ідею варто перейняти насамперед. base-zs — це вихідна модель на 1B у режимі zero-shot. base-fs — та сама модель із чотирма прикладами в системному промпті:
FS_EXAMPLES = (
"Title: fix crash when checkout receives empty payload\nBody: Guard added.\nLabel: bug\n\n"
"Title: add bulk delete to dashboard\nBody: New endpoint + UI.\nLabel: feat\n\n"
"Title: bump dependencies for billing\nBody: Routine maintenance.\nLabel: chore\n\n"
"Title: speed up search list query with index\nBody: p95 480ms -> 120ms.\nLabel: perf\n"
)
Few-shot майже безкоштовний: трохи довший промпт і хвилин десять на те, щоб написати приклади. Донавчання ж тягне за собою прогін навчання, злитий артефакт моделі, історію з деплоєм і зобов'язання перенавчати модель щоразу, коли змінюється набір міток. Не порівнявши з безкоштовним варіантом, не можна стверджувати, що дорогий був потрібен. У жодному з одинадцяти попередніх проєктів не було few-shot-бази, і заднім числом це виглядає незручно.
Чотири числа на кожен рецепт
Кожен рецепт отримує чотири виміри, і користь у тому, щоб бачити їх поруч:
- Точність, заради якої все це й робиться
- Час навчання — реальний час, заміряний через
perf_counterдовкола справжнього підпроцесу - Затримка інференсу — медіана по всій тестовій вибірці, а не середнє
- Пікова пам'ять, з якої видно, чи вміститься все це на вашій машині
Час вимірюється, а не оцінюється на око:
t0 = time.perf_counter()
subprocess.run(cmd, check=True)
train_secs = time.perf_counter() - t0
Медіана краща за середнє, бо один викид холодного старту на початку прогону здатен потягнути середнє до безглуздого значення, і ви витратите пів дня на пошуки проблеми в моделі, з якою все гаразд.
На M1 Pro повний прогін триває близько 45 хвилин, і в цьому головний виграш. Сорок п'ять хвилин без нагляду — це настільки дешево, що запустити прогін простіше, ніж сперечатися, і це змінює звички. Думки про гіперпараметри поступаються місцем таблиці.
Що прогін уміє розділити
Несподівана знахідка взагалі не стосувалася якогось одного числа. Рецепт lora-long (ранг 8, 8 шарів, 1500 ітерацій) потрібен, щоб розвести дві змінні, які постійно плутають. «Більша LoRA допомогла» зазвичай означає, що ранг підняли й навчання заразом тривало довше, бо конфіг змінили один раз і подивилися на один результат. Це різні важелі. Більший ранг додає ємності, а більше ітерацій означає далі сходитися в межах уже наявної ємності. Задачі може бракувати одного й бути вдосталь іншого, і лікування в цих випадках зовсім різне.
Зафіксуйте ранг 8 і підніміть ітерації з 600 до 1500 — ефект ізольовано. Ви платите вдвічі з половиною довшим навчанням за того самого розміру артефакту й тієї самої ціни інференсу. Якщо результат лишається близьким до прогону на 600 ітерацій, задача ніколи не впиралася в ємність, і ранг був хибним слідом. Якщо він різко зростає, модель весь цей час була недонавчена, а суперечка про ранг крутилася довкола не тієї ручки. Скільки не дивись на конфіг, відповіді на це питання там немає.
Короткий підсумковий абзац у звіті обирається за простими правилами: він називає рецепт із найкращою точністю та рецепт із найкращою точністю на секунду навчання. Два різні переможці — нормальний результат. Коли названо обох, рішення доводиться ухвалювати по-справжньому, а правило «перемагає найбільше число» тихо ухвалило б його за вас.
Обмеження, про яке варто сказати
Стенд працює з класифікацією, а це простий випадок: точність — це точний збіг із відомою міткою, тож оцінювання зводиться до порівняння рядків і нічого не коштує. Із задачами генерації так не вийде. У схожому порівнянні для проєкту з циклом самовдосконалення довелося використати LLM у ролі судді, залишивши перевірку на точний збіг лише для структурованих полів, і тоді надійність самого судді стала окремим питанням.
Структура при цьому зберігається. Рецепти лишаються даними, ціна вимірюється за тим самим годинником, що й якість, а в таблиці, як і раніше, чотири стовпці. Ускладнюється лише функція точності.
До чого все зводиться
Розмови про донавчання здебільшого крутяться довкола якості, хоча більшість реальних рішень стосується ціни. Ранг, шари й ітерації коштують по-різному. Щоб чесно їх порівняти, потрібен стенд, який запускається так швидко, що сперечатися замість цього ніхто не стане. 45-хвилинне завдання без нагляду дешевше за впевненого інженера й помиляється рідше, тож спершу побудуйте таблицю, а думку сформуйте потім.