فاینتیونینگ کامل (Full fine-tuning) یک مدل زبانی 7B نیازمند دستکم 56GB حافظهی گرافیکی است؛ هزینهای سنگین که در عمل مسیر آموزش مدلهای سفارشی را برای بسیاری از پروژهها مسدود میکند. متدهای LoRA و QLoRA با فریز کردن وزنهای پایه و تزریق ماتریسهای کمرتبه، این معادله را به کلی تغییر دادهاند و امکان فاینتیونینگ باکیفیت را در حافظهی کمتر از 6GB فراهم میسازند.
تهیهی کارتهای گرافیک حرفهای یا اجارهی GPUهای ابری خارجی به دلیل چالش پرداختهای ارزی برای کاربران داخل ایران بسیار دشوار و پرهزینه است. در چنین شرایطی، بهرهگیری از QLoRA امکان آموزش مدلهای مدرن متنباز مانند Llama 3 را روی کارتهای گرافیک مصرفی ردهمعمولی (Consumer GPU) و سیستمهای شخصی کاملاً شدنی میکند.
در این درس ابتدا منطق ریاضی LoRA و معماری کوانتیزاسیون NF4 در QLoRA را بررسی میکنیم، سپس آن را بدون وابستگی به فریمورکهای واسط در PyTorch کدنویسی کرده و در نهایت نحوهی ادغام (merge) و استقرار تطبیقدهندهها را مرور خواهیم کرد.
فاینتیونینگ کامل یک مدل 7B به 56GB حافظهی گرافیکی (VRAM) نیاز دارد. شما چنین حافظهای در اختیار ندارید؛ بیشتر شرکتها هم ندارند. اما LoRA امکان فاینتیون همین مدل را با آموزش کمتر از 1% از پارامترها و تنها با 6GB فراهم میکند. این رویکرد به معنای فدا کردن کیفیت نیست؛ عملکرد آن در بیشتر تسکها با فاینتیونینگ کامل برابری میکند. تمام اکوسیستم متنبازِ فاینتیونینگ بر پاشنهی همین یک ترفند میچرخد.
مرتبط: در فاز 10 حلقههای SFT و DPO را از صفر پیادهسازی کردیم. این درس آن مفاهیم را به جعبهابزارهای مدرن تنظیم کارآمد پارامتر (Parameter-Efficient Fine-Tuning یا PEFT) در سال 2026 از جمله PEFT، TRL، Unsloth، Axolotl و LLaMA-Factory متصل میکند.
اهداف یادگیری
- پیادهسازی LoRA با تزریق ماتریسهای تطبیقدهندهی کمرتبه (A و B) به لایههای اتنشن یک مدل از پیش آموزشدیده
- محاسبهی صرفهجویی در پارامترها با LoRA در مقایسه با فاینتیونینگ کامل: آموزش پارامتر با رتبهی و ابعاد به جای پارامتر
- فاینتیونینگ مدل با QLoRA (مدل پایهی کوانتیزهشدهی 4-بیتی + تطبیقدهندههای LoRA) برای اجرا روی حافظهی کارتهای گرافیک مصرفی (consumer GPU)
- مرج کردن (merge) وزنهای LoRA با مدل پایه برای استقرار و مقایسهی سرعت استنتاج در حالت با و بدون تطبیقدهنده
صورت مسئله
فرض کنید یک مدل پایه مانند Llama 3 8B دارید و میخواهید تیکتهای پشتیبانی مشتریان را با لحن اختصاصی شرکت شما پاسخ دهد. راهکار اصلی SFT است، اما SFT با چالش هزینه روبهرو است.
در فاینتیونینگ کامل، تکتک پارامترهای مدل بهروزرسانی میشوند. مدل Llama 3 8B حدود 8 میلیارد پارامتر دارد. در فرمت fp16، هر پارامتر 2 بایت فضا اشغال میکند؛ یعنی فقط برای بارگذاری وزنها به 16GB حافظه نیاز دارید. حین آموزش، گرادیانها (16GB)، متغیرهای وضعیت بهینهساز برای Adam (شامل 32GB برای تکانه + واریانس) و اکتیویشنها نیز به این حجم اضافه میشوند. مجموع این مقادیر برای یک مدل 8B به حدود 56GB حافظهی گرافیکی میرسد.
یک کارت A100 با 80GB حافظه بهسختی میتواند این بار را تحمل کند. هزینهی اجارهی دو کارت A100 در ارائهدهندگان ابری ساعتی 3 تا 4 دلار است. آموزش مدل روی 50,000 نمونه برای 3 دوره (epoch)، بین 6 تا 10 ساعت زمان میبرد؛ یعنی 30 تا 40 دلار برای هر آزمایش. اگر برای تنظیم ابرپارامترها 10 آزمایش اجرا کنید، پیش از استقرار نهایی 400 دلار هزینه روی دستتان میماند.
اگر این مقیاس را روی Llama 3 70B ببرید، ارقام غیرمنطقی میشوند: 140GB صرفاً برای بارگذاری وزنها. در این حالت به یک کلاستر کامل نیاز دارید و هر آزمایش بیش از 100 دلار تمام میشود.
علاوه بر این، یک چالش عمیقتر هم در میان است: فاینتیونینگ کامل تمام وزنهای مدل را دستکاری میکند. آموزش مدل روی دادههای پشتیبانی ممکن است تواناییهای عمومی آن را تخریب کند؛ پدیدهای موسوم به فراموشی فاجعهبار (Catastrophic Forgetting). در این وضعیت، مدل در تسک مدنظر شما تقویت میشود، اما عملکردش در سایر وظایف افت میکند.
شما به روشی نیاز دارید که پارامترهای کمتری را آموزش دهد، حافظهی کمتری مصرف کند و دانش قبلی مدل را از بین نبرد.
مفاهیم پایه
روش LoRA: تطبیق کمرتبه
ادوارد هو (Edward Hu) و همکارانش در مایکروسافت در ژوئن 2021 مقالهی LoRA را منتشر کردند. بینش کلیدی این مقاله این بود: تغییرات وزنها حین فاینتیونینگ ذاتاً رتبهی پایینی (low intrinsic rank) دارند. برای تغییر رفتار مدل نیازی به بهروزرسانی تمام 16.7 میلیون پارامتر یک ماتریس 4096x4096 ندارید؛ دادههای مفید این بهروزرسانی را میتوان در ماتریسی با رتبهی 16 یا 32 ثبت کرد.
روابط ریاضی این ایده بسیار شفاف است. یک لایهی خطی استاندارد چنین محاسباتی انجام میدهد:
که در آن یک ماتریس است. برای تصویر کردن (projection) اتنشن در ابعاد 4096x4096، این ماتریس شامل 16,777,216 پارامتر است.
روش LoRA وزن اصلی را ثابت (freeze) نگه میدارد و یک تجزیهی کمرتبه به آن اضافه میکند:
که در آن ماتریسی از مرتبهی و ماتریسی از مرتبهی است. رتبهی بسیار کوچکتر از انتخاب میشود؛ معمولاً 8، 16 یا 32.
برای روی یک لایهی 4096x4096:
- پارامترهای لایهی اصلی: 4096 x 4096 = 16,777,216
- پارامترهای افزوده در LoRA: (4096 x 16) + (16 x 4096) = 65,536 + 65,536 = 131,072
- نسبت کاهش: 131,072 / 16,777,216 = 0.78%
این یعنی شما تنها 0.78% از پارامترها را آموزش میدهید و در عین حال 95 تا 100 درصد کیفیت فاینتیونینگ کامل را حفظ میکنید.
ماتریس A با توزیع نرمال تصادفی (گوسی) و ماتریس B با مقادیر صفر مقداردهی اولیه میشوند. در نتیجه حاصلضرب BA در ابتدای کار دقیقاً صفر است؛ مدل کار خود را با همان رفتار دستنخوردهی اولیه شروع میکند و بهمرور تطبیق لازم را فرا میگیرد.
ضریب مقیاس: آلفا
روش LoRA یک ضریب مقیاس به نام معرفی میکند تا میزان اثرگذاری بهروزرسانی کمرتبه بر خروجی مدل کنترل شود:
در صورت برقراری ، ضریب مقیاس برابر با 1x خواهد بود. وقتی باشد (حالت پیشفرض و متداول)، این ضریب 2x میشود. این ابرپارامتر این امکان را فراهم میکند که نرخ یادگیری مسیر LoRA را فارغ از نرخ یادگیری پایهی شبکه تنظیم کنید.
راهنمای تجربی:
- مقدار یک قرارداد رایج میان توسعهدهندگان است (مقالهی اصلی در اغلب ارزیابیها از استفاده کرد).
- حالت مقیاس 1x ایجاد میکند؛ محافظهکارانه اما پایدار.
- مقادیر بزرگتر گامهای بهروزرسانی درشتتری در هر مرحله رقم میزنند که میتواند سرعت همگرایی را بالا ببرد یا به ناپایداری منجر شود.
لایههای هدف برای اعمال LoRA
یک ترنسفورمر از لایههای خطی متعددی تشکیل شده است، اما لزومی ندارد LoRA را به تمام آنها تزریق کنید. در مقالهی مرجع، ترکیبهای متفاوتی بررسی شده است:
| لایههای هدف | پارامترهای قابل آموزش (مدل 7B) | کیفیت |
|---|---|---|
| فقط q_proj | 4.7M | خوب |
| q_proj + v_proj | 9.4M | بهتر |
| q_proj + k_proj + v_proj + o_proj | 18.9M | بهترین گزینه برای اتنشن |
| تمام لایههای خطی (اتنشن + MLP) | 37.7M | بهبود جزئی با دو برابر پارامتر |
نقطهی بهینهی کاربردی در اغلب سناریوها، ترکیب q_proj + v_proj است. این لایهها وظیفهی تصویر کردن query و value را در مکانیسم خوداتنشن (Self-Attention) بر عهده دارند و تعیین میکنند مدل بر چه چیزهایی متمرکز شود و چه اطلاعاتی را بیرون بکشد. افزودن لایههای MLP برای وظایف دشواری مانند تولید کد اثر مثبتی دارد، اما در تسکهای سادهتر صرفاً تعداد پارامترها را دو برابر میکند بدون آنکه خروجی رشد چشمگیری نشان دهد.
انتخاب رتبه (Rank)
رتبهی قدرت بیان و ظرفیت تطبیقدهنده را مشخص میکند:
| رتبه | پارامترهای قابل آموزش (به ازای هر لایه) | بهترین کاربرد |
|---|---|---|
| 4 | 32,768 | دستهبندی ساده، تحلیل احساسات |
| 8 | 65,536 | پرسشوپاسخ تکدامنهای، خلاصهسازی |
| 16 | 131,072 | تسکهای چنددامنهای، دنبال کردن دستورالعملها |
| 32 | 262,144 | استدلال پیچیده، تولید کد |
| 64 | 524,288 | بازده نزولی در بیشتر تسکها |
| 128 | 1,048,576 | بهندرت توجیهپذیر است |
تیم هو نشان داد مقدار در تسکهای سرراست بخش عمدهی عملکرد مطلوب را تامین میکند. در عمل، مقادیر و پراستفادهترین گزینهها هستند. انتخاب رتبههای فراتر از بهندرت ارتقای محسوسی در کیفیت نشان میدهد و مزیت اصلی LoRA در کاهش مصرف حافظه را هدر میدهد.
فناوری QLoRA: کوانتیزاسیون 4-بیتی + LoRA
تیم دتمرز (Tim Dettmers) و همکارانش در دانشگاه واشنگتن در مه 2023 روش QLoRA را معرفی کردند. ایدهی پایه این بود: مدل پایهی فریز شده به دقت 4 بیت کوانتیزه شود و سپس تطبیقدهندههای LoRA با فرمت fp16 روی آن سوار شوند.
این تغییر، موازنهی مصرف حافظه را به کلی دگرگون کرد:
| روش | حافظهی وزنها (7B) | حافظهی آموزش (7B) | GPU مورد نیاز |
|---|---|---|---|
| فاینتیون کامل (fp16) | 14GB | حدود 56GB | 1x A100 80GB |
| LoRA (مدل پایه fp16) | 14GB | حدود 18GB | 1x A100 40GB |
| QLoRA (مدل پایه 4-بیتی) | 3.5GB | حدود 6GB | 1x RTX 3090 24GB |
نوآوری فنی QLoRA متکی بر سه ستون اصلی است:
فرمت NF4 (Normal Float 4-bit): نوع دادهای جدید که اختصاصاً برای وزنهای شبکههای عصبی مهندسی شده است. توزیع وزنها در شبکههای عصبی معمولاً شبیه به توزیع نرمال است. فرمت NF4 تعداد 16 سطح کوانتیزاسیون خود را دقیقاً روی صدکهای یک توزیع نرمال استاندارد قرار میدهد. این ساختار از نظر نظریهی اطلاعات برای دادههای نرمال بهینهترین حالت ممکن است و اتلاف دادهی بسیار کمتری در مقایسه با کوانتیزاسیون یکنواخت INT4 یا Float4 استاندارد ثبت میکند.
کوانتیزاسیون دوگانه (Double Quantization): مقادیر مقیاسبندی مورد نیاز برای کوانتیزاسیون، خود مقداری از حافظه را اشغال میکنند. هر بلوک 64تایی از وزنها یک فاکتور مقیاس fp32 (معادل 4 بایت) لازم دارد. در یک مدل 7B این مقادیر حدود 0.4GB فضا میگیرند. کوانتیزاسیون دوگانه این فاکتورهای مقیاس را به فرمت fp8 کوانتیزه کرده و بار حافظهی آنها را به 0.1GB تقلیل میدهد؛ بهینهسازی کوچکی که در مقیاسهای بزرگ ارزش پیدا میکند.
بهینهسازهای صفحهبندیشده (Paged Optimizers): در پردازش توالیهای طولانی، متغیرهای وضعیت بهینهساز (تکانه و واریانس در Adam) ممکن است از سقف VRAM عبور کنند. این مکانیزم با بهرهگیری از معماری حافظهی یکپارچهی NVIDIA، وضعیتهای بهینهساز را در صورت سرریز شدن VRAM به طور خودکار به رم سیستم منتقل کرده و هنگام نیاز فراخوانی میکند. این رویکرد مانع از خطای پایان حافظه (OOM) میشود، هرچند نرخ پردازش (throughput) را اندکی کاهش میدهد.
ارزیابی کیفیت
آیا تقلیل پارامترها یا کوانتیزه کردن لایههای پایه، خروجی را با افت کیفیت مواجه میکند؟ نتایج آزمایشهای چند پژوهش مختلف را ببینیم:
| روش | MMLU (5-shot) | MT-Bench | HumanEval |
|---|---|---|---|
| فاینتیون کامل (Llama 2 7B) | 48.3 | 6.72 | 14.6 |
| LoRA r=16 | 47.9 | 6.68 | 14.0 |
| QLoRA r=16 (NF4) | 47.5 | 6.61 | 13.4 |
| QLoRA r=64 (NF4) | 48.1 | 6.70 | 14.2 |
عملکرد LoRA در در اغلب ارزیابیها اختلافی کمتر از 1% با فاینتیونینگ کامل دارد. افت عملکرد QLoRA با رتبهی نیز تنها در حد چند دهم درصد است. از سویی دیگر، QLoRA در با مصرف 90% حافظهی کمتر، عملکردی معادل فاینتیونینگ کامل رقم میزند.
هزینهها در دنیای واقعی
فاینتیونینگ Llama 3 8B روی 50,000 نمونه (در 3 دوره):
| روش | کارت گرافیک | زمان | هزینه تقریبی |
|---|---|---|---|
| فاینتیون کامل | 2x A100 80GB | 8 ساعت | حدود 32 دلار |
| LoRA r=16 | 1x A100 40GB | 4 ساعت | حدود 8 دلار |
| QLoRA r=16 | 1x RTX 4090 24GB | 6 ساعت | حدود 5 دلار |
| QLoRA r=16 (Unsloth) | 1x RTX 4090 24GB | 2.5 ساعت | حدود 2 دلار |
| QLoRA r=16 | 1x T4 16GB | 12 ساعت | حدود 4 دلار |
اجرای QLoRA روی یک پردازندهی گرافیکی معمولی کمتر از هزینهی یک ناهار تمام میشود. همین صرفهی اقتصادی موجب رشد انفجاری جامعهی مدلهای متنباز در سال 2023 شد و پای QLoRA را به عنوان گزینهی پیشفرض به تمام ابزارهای آموزشی سال 2026 باز کرد.
استک مدرن PEFT در سال 2026
| فریمورک | ماهیت | زمان استفاده |
|---|---|---|
| Hugging Face PEFT | کتابخانهی مرجع روشهای LoRA/QLoRA/DoRA/IA3 | زمانی که نیازمند کنترل دقیق بر اجزا هستید و حلقهی آموزش شما با transformers.Trainer هماهنگ است. |
| TRL | ابزار آموزش مبتنی بر بازخورد هاگینگ فیس (شامل SFT، DPO، GRPO، PPO، ORPO) | زمانی که پس از SFT قصد اجرای DPO یا GRPO را دارید؛ این ابزار روی PEFT بنا شده است. |
| Unsloth | بازنویسی گامهای رو به جلو و گامهای رو به عقب (backward pass) با کرنلهای Triton | زمانی که به 2 تا 5 برابر سرعت بیشتر و نصف مصرف VRAM بدون افت دقت نیاز دارید؛ ویژهی خانوادهی Llama، Mistral و Qwen. |
| Axolotl | لایهی رابط با کانفیگهای YAML بر پایهی PEFT + TRL + DeepSpeed + Unsloth | زمانی که به چرخههای آموزش تکرارپذیر، یکپارچه و قابل ردیابی با گیت نیاز دارید. |
| LLaMA-Factory | واسط کاربری متنی، گرافیکی و وب بر پایهی PEFT و TRL | زمانی که به دنبال فاینتیونینگ بدون کد برای بیش از 100 خانوادهی مختلف مدلها هستید. |
| torchtune | دستورالعملهای بومی پایتورچ بدون وابستگی به transformers |
زمانی که حداقل وابستگی جانبی را میخواهید و ساختار سازمانی شما بر PyTorch متمرکز است. |
قاعدهی کلی: برای پژوهشها یا آزمایشهای تکمرحلهای سراغ PEFT بروید. برای پایپلاینهای باثبات تولیدی، از ترکیب Axolotl با کرنلهای Unsloth بهره بگیرید. جهت ساخت سریع پروتوتایپها نیز LLaMA-Factory بهترین گزینه است.
مرج کردن وزنهای تطبیقدهنده
پس از خاتمهی آموزش، با دو بخش سروکار دارید: مدل پایهی فریز شده و فایل کمحجم تطبیقدهندهی LoRA (معمولاً بین 10 تا 100 مگابایت). از این مرحله به بعد دو مسیر دارید:
-
جدا نگه داشتن بخشها: مدل پایه را فراخوانی کنید و وزنهای تطبیقدهنده را روی آن بارگذاری نمایید. با جابهجا کردن تطبیقدهندهها میتوانید وظایف مختلف را پوشش دهید؛ به این ترتیب چند نسخهی فاینتیونشده را با اتکا به یک مدل واحد سرو میکنید.
-
مرج دائمی وزنها: مقدار را محاسبه کرده و فایل نهایی را به شکل یک مدل استاندارد جدید ذخیره کنید. حجم این مدل تفاوتی با مدل اولیه ندارد؛ نه تاخیری در زمان استنتاج اضافه میشود و نه نیازی به مدیریت ماژولهای جانبی خواهید داشت.
اگر قصد دارید همزمان به تسکهای گوناگون خدمترسانی کنید (مثلاً یک تطبیقدهنده برای تیکت پشتیبانی، یکی برای کدنویسی و یکی برای ترجمه)، فایلها را جدا نگه دارید. اما برای استقرار یک مدل واحد و تخصصی، آنها را merge کنید.
رویکردهای تکمیلی جهت ترکیب همزمان چند تطبیقدهنده:
- روش TIES-Merging (یاکاف و همکاران، 2023): پارامترهای کماثر را غربال میکند، تداخل در علامت ضرایب را حل مینماید و سپس فرایند ادغام را پیش میبرد تا اصطکاک میان تطبیقدهندهها به حداقل برسد.
- روش DARE (یو و همکاران، 2023): درصدی از پارامترهای تطبیقدهندهها را به طور تصادفی دور میریزد و بقیه را بازتنظیم میکند. این شیوه برای ادغام چند مهارت به شکلی شگفتآور عمل میکند.
- حساب تطبیقدهندهها (Task arithmetic): وزنهای ماژولها را با هم جمع یا کم کنید. جمع کردن مستقیم یک تطبیقدهندهی «کدنویسی» با یک تطبیقدهندهی «ریاضی» معمولاً مدلی تولید میکند که در هر دو حوزه مسلط است.
چه زمانهایی نباید فاینتیون کنیم؟
فاینتیونینگ گزینهی سوم است، نه نخستین اقدام.
گام اول: مهندسی پرامپت (Prompt engineering). پرامپت سیستمی بهتری تدوین کنید، چند نمونه مثال (few-shot) اضافه کنید و از روش زنجیرهی تفکر (chain-of-thought) بهره بگیرید. این فرایند بدون هزینه است و تنها چند دقیقه زمان میبرد. اگر پرامپت کار شما را تا 80% راه میاندازد، احتمالاً به فاینتیونینگ نیاز ندارید.
گام دوم: بازیابی پیشرفته (RAG). اگر نیازمند تزریق دادههای اختصاصی (مستندات سازمانی، پایگاه دانش یا کاتالوگ محصولات) هستید، روشهای بازیابی هم ارزانترند و هم نگهداری سادهتری نسبت به تغییر وزنهای مدل دارند. درس 06 را ببینید.
گام سوم: فاینتیونینگ. تنها زمانی سراغ فاینتیونینگ بروید که قصد دارید لحن، ساختار بیانی یا فرمت خروجی خاصی به مدل بدهید که با پرامپت تضمین نمیشود؛ یا زمانی که خروجیهای ساختاریافتهی کاملاً پایدار مدنظر دارید؛ وقتی قصد دارید یک مدل بزرگ را در مدلی کوچکتر تقطیر (distill) کنید؛ یا هنگامی که تاخیر سیستم حیاتی است و نمیتوانید توکنهای اضافی برای مثالهای پرامپت صرف کنید.
پیادهسازی گامبهگام
ما LoRA را از صفر و در بستر PyTorch پیاده میکنیم؛ بدون کتابخانههای واسط و فارغ از پیچیدگیهای پنهان. لایهی LoRA را طراحی میکنیم، به معماری مدل تزریق میکنیم، آن را آموزش میدهیم و در نهایت وزنها را merge میکنیم.
گام 1: لایهی LoRA
import torch
import torch.nn as nn
import math
class LoRALayer(nn.Module):
def __init__(self, in_features, out_features, rank=8, alpha=16):
super().__init__()
self.rank = rank
self.alpha = alpha
self.scaling = alpha / rank
self.A = nn.Parameter(torch.randn(in_features, rank) * (1 / math.sqrt(rank)))
self.B = nn.Parameter(torch.zeros(rank, out_features))
def forward(self, x):
return (x @ self.A @ self.B) * self.scaling
ماتریس A با مقادیر تصادفی مقیاسبندیشده و B با صفر مقداردهی اولیه میشوند. حاصلضرب BA در آغاز کار دقیقاً صفر است؛ بنابراین مدل آموزش را با حفظ ساختار عملکردی اولیهاش شروع میکند.
گام 2: لایهی خطی پوششیافته با LoRA
class LinearWithLoRA(nn.Module):
def __init__(self, linear, rank=8, alpha=16):
super().__init__()
self.linear = linear
self.lora = LoRALayer(
linear.in_features, linear.out_features, rank, alpha
)
for param in self.linear.parameters():
param.requires_grad = False
def forward(self, x):
return self.linear(x) + self.lora(x)
لایهی خطی اصلی کاملاً فریز میشود. فقط ماتریسهای LoRA (یعنی A و B) اجازهی آموزش دارند.
گام 3: تزریق LoRA به داخل ساختار مدل
def inject_lora(model, target_modules, rank=8, alpha=16):
for param in model.parameters():
param.requires_grad = False
lora_layers = {}
for name, module in model.named_modules():
if isinstance(module, nn.Linear):
if any(t in name for t in target_modules):
parent_name = ".".join(name.split(".")[:-1])
child_name = name.split(".")[-1]
parent = dict(model.named_modules())[parent_name]
lora_linear = LinearWithLoRA(module, rank, alpha)
setattr(parent, child_name, lora_linear)
lora_layers[name] = lora_linear
return lora_layers
ابتدا کل پارامترهای مدل را فریز میکنیم. سپس درخت ماژولهای مدل را بررسی کرده، لایههای خطی هدف را یافته و نسخهی مجهز به LoRA را جایگزین آنها میکنیم. در این ساختار، تنها ماتریسهای A و B در LoRA پارامترهای آموزشپذیر کل سیستم خواهند بود.
گام 4: شمارش پارامترها
def count_parameters(model):
total = sum(p.numel() for p in model.parameters())
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
frozen = total - trainable
return {
"total": total,
"trainable": trainable,
"frozen": frozen,
"trainable_pct": 100 * trainable / total if total > 0 else 0
}
گام 5: مرج کردن وزنها
def merge_lora_weights(model):
for name, module in model.named_modules():
if isinstance(module, LinearWithLoRA):
with torch.no_grad():
merged = (
module.lora.A @ module.lora.B
) * module.lora.scaling
module.linear.weight.data += merged.T
parent_name = ".".join(name.split(".")[:-1])
child_name = name.split(".")[-1]
if parent_name:
parent = dict(model.named_modules())[parent_name]
else:
parent = model
setattr(parent, child_name, module.linear)
پس از اجرای تابع merge، لایههای واسط LoRA حذف میشوند. حجم مدل به ابعاد اولیهی خود بازمیگردد و تطبیقهای یادگرفتهشده در بطن وزنها جای میگیرند، بدون آنکه تاخیری در زمان استنتاج اضافه شود.
گام 6: شبیهسازی کوانتیزاسیون QLoRA
def quantize_to_nf4(tensor, block_size=64):
blocks = tensor.reshape(-1, block_size)
scales = blocks.abs().max(dim=1, keepdim=True).values / 7.0
scales = torch.clamp(scales, min=1e-8)
quantized = torch.round(blocks / scales).clamp(-8, 7).to(torch.int8)
return quantized, scales
def dequantize_from_nf4(quantized, scales, original_shape):
dequantized = quantized.float() * scales
return dequantized.reshape(original_shape)
این بخش، کوانتیزاسیون 4-بیتی را با مپ کردن وزنها به 16 مقدار مجزا در بلوکهای 64تایی شبیهسازی میکند. نسخهی عملیاتی QLoRA از کتابخانهی bitsandbytes برای پیادهسازی واقعی فرمت NF4 روی سختافزار GPU بهره میبرد.
گام 7: چرخهی آموزش
def train_lora(model, data, epochs=5, lr=1e-3, batch_size=4):
optimizer = torch.optim.AdamW(
[p for p in model.parameters() if p.requires_grad], lr=lr
)
criterion = nn.MSELoss()
losses = []
for epoch in range(epochs):
epoch_loss = 0.0
n_batches = 0
indices = torch.randperm(len(data["inputs"]))
for i in range(0, len(indices), batch_size):
batch_idx = indices[i:i + batch_size]
x = data["inputs"][batch_idx]
y = data["targets"][batch_idx]
output = model(x)
loss = criterion(output, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch_loss += loss.item()
n_batches += 1
avg_loss = epoch_loss / n_batches
losses.append(avg_loss)
return losses
گام 8: تست نهایی دمو
def demo():
torch.manual_seed(42)
d_model = 256
n_classes = 10
model = nn.Sequential(
nn.Linear(d_model, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, n_classes),
)
n_samples = 500
x = torch.randn(n_samples, d_model)
y = torch.randint(0, n_classes, (n_samples,))
y_onehot = torch.zeros(n_samples, n_classes).scatter_(1, y.unsqueeze(1), 1.0)
data = {"inputs": x, "targets": y_onehot}
params_before = count_parameters(model)
lora_layers = inject_lora(
model, target_modules=["0", "2"], rank=8, alpha=16
)
params_after = count_parameters(model)
losses = train_lora(model, data, epochs=20, lr=1e-3)
merge_lora_weights(model)
params_merged = count_parameters(model)
return {
"params_before": params_before,
"params_after": params_after,
"params_merged": params_merged,
"losses": losses,
}
در این دمو یک مدل کوچک میسازیم، LoRA را به دو لایهی آن تزریق میکنیم، آموزش میدهیم و در پایان وزنها را merge میکنیم. تعداد پارامترهای آموزشپذیر در طول فاینتیونینگ تا حدود 1% کاهش مییابد و پس از تکمیل عملیات merge، معماری مدل مجدداً به ساختار پایدار اولیهی خود بازمیگردد.
راهنمای عملیاتی
در اکوسیستم Hugging Face، پیادهسازی LoRA روی یک مدل واقعی تنها نیازمند حدود 20 خط کد است:
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "v_proj"],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
جهت اجرای QLoRA، کافی است پیکربندی کوانتیزاسیون با bitsandbytes را اضافه کنید:
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B",
quantization_config=bnb_config,
device_map="auto",
)
model = get_peft_model(model, lora_config)
تمام شد. چرخهی آموزش و پایپلاین دادهها بدون هیچ دستکاری باقی میمانند. مدل پایه در قالب 4 بیتی مقیم حافظه میشود، لایههای LoRA با دقت fp16 آموزش میبینند و تمام پردازش روی یک کارت 6GB پیش میرود.
برای فرآیند آموزش با ابزار Trainer در Hugging Face:
from transformers import TrainingArguments, Trainer
from datasets import load_dataset
dataset = load_dataset("tatsu-lab/alpaca", split="train[:5000]")
training_args = TrainingArguments(
output_dir="./lora-llama",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
fp16=True,
logging_steps=10,
save_strategy="epoch",
optim="paged_adamw_8bit",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
)
trainer.train()
model.save_pretrained("./lora-adapter")
حجم تطبیقدهندهی خروجی نهایتاً بین 10 تا 100 مگابایت خواهد بود و فایل مدل پایه دستنخورده باقی میماند. شما میتوانید این ماژولها را بدون الزام به جابهجایی کل مدل، روی Hub هاگینگ فیس منتشر کنید.
تحویل پروژهها
این درس موارد زیر را در اختیارتان قرار میدهد:
- فایل
outputs/prompt-lora-advisor.md-- پرامپتی تخصصی جهت تعیین رتبهی LoRA، ماژولهای هدف و ابرپارامترهای متناسب با تسک شما. - فایل
outputs/skill-fine-tuning-guide.md-- مهارتی برای هدایت ایجنتها براساس درخت تصمیم (decision tree) مرتبط با چگونگی و زمان فاینتیونینگ.
تمرینها
-
بررسی اثر تغییر رتبه (Rank ablation study): کد دمو را به ترتیب با مقادیر رتبهی 2، 4، 8، 16، 32 و 64 اجرا کنید. نمودار تابع زیان (loss) نهایی را برحسب رتبهها رسم کنید. نقطهی بازده نزولی را که در آن دو برابر کردن رتبه تاثیری در کاهش نصف تابع زیان ندارد، مشخص کنید. در یک تسک دستهبندی با بردار ورودی 256تایی، این نقطه معمولاً در محدودهی r=8 تا 16 دیده میشود.
-
مقایسهی ماژولهای هدف: تابع inject_lora را دستکاری کنید تا فقط لایهی "0"، فقط لایهی "2"، فقط لایهی "4" و در نهایت هر سه لایه هدف قرار گیرند. هر سناریو را 20 دوره آموزش دهید. سرعت همگرایی و کمینهی تابع زیان نهایی را بسنجید. این تمرین تفاوتهای عملیاتی بین تزریق به q_proj، v_proj یا کلیهی لایههای خطی را نشان میدهد.
-
ارزیابی خطای کوانتیزاسیون: ماتریس وزنهای مدل آموزشدیده را قبل و بعد از عبور از توابع quantize_to_nf4 و dequantize_from_nf4 ذخیره کنید. میانگین مربعات خطا (Mean Squared Error)، بیشینهی خطای قدرمطلق و همبستگی وزنهای اولیه و بازسازیشده را به دست آورید. این آزمایش را با مقادیر مختلف block_size از جمله 32، 64، 128 و 256 تکرار کنید.
-
میزبانی همزمان چند تطبیقدهنده: دو تطبیقدهندهی LoRA روی دو بخش مختلف از مجموعهداده (نمونههای با ایندکس زوج در برابر فرد) آموزش دهید. هر دو را ذخیره کنید. مدل پایه را تنها یک بار در حافظه بارگذاری کرده، سپس تطبیقدهندهها را روی آن جابهجا کنید و مطمئن شوید با ورودی مشابه، خروجیهای متمایزی تولید میکنند. این مکانیزم مبنای سرویسدهی به چند مدل فاینتیونشده روی یک مدل پایه در پروداکشن است.
-
مقایسهی استنتاج قبل و بعد از ادغام: خروجی مدل LoRA را پیش و پس از اجرای تابع merge_lora_weights روی 100 ورودی یکسان مقایسه کنید. بررسی کنید که خروجیها (با در نظر گرفتن تلورانس محاسبات ممیز شناور تا 1e-5) کاملاً یکسان باشند. سپس سرعت استنتاج را بسنجید؛ مدل ادغامشده به دلیل جایگزینی دو ضرب ماتریسی با یک ضرب واحد، باید سریعتر عمل کند.
واژهنامه و اصطلاحات
| واژه | در زبان محاورهای | تعریف دقیق فنی |
|---|---|---|
| LoRA | «فاینتیونینگ ارزان و سریع» | تطبیق کمرتبه (Low-Rank Adaptation): فریز کردن وزنهای مدل پایه و آموزش دو ماتریس کوچک A و B که حاصلضرب آنها تقریب بهروزرسانی کامل وزنها است. |
| QLoRA | «فاینتیون روی لپتاپ» | روش کوانتیزهشدهی LoRA: بارگذاری مدل پایه با دقت 4-بیتی NF4 و آموزش تطبیقدهندههای LoRA با فرمت fp16 روی آن، که اجرای فاینتیونینگ مدلهای 7B را در 6GB حافظهی گرافیکی ممکن میکند. |
| رتبه (Rank یا r) | «ظرفیت یادگیری مدل» | بعد داخلی میان دو ماتریس A و B؛ نقطهی تعادل میان قدرت بیان و تعداد پارامترهای آموزشپذیر. |
| آلفا (Alpha) | «نرخ یادگیری در LoRA» | ضریب مقیاس اعمالشده بر خروجی بخش LoRA؛ مقدار شدت اثرگذاری مقادیر تطبیقیافته را بر خروجی نهایی تنظیم میکند. |
| فرمت NF4 | «کوانتیزاسیون 4-بیتی» | نوع دادهی 4-بیتی Normal Float که سطوح کوانتیزاسیون آن بر پایهی صدکهای توزیع نرمال چیده شده و برای ذخیرهسازی وزنهای شبکه عصبی بهینهترین ساختار است. |
| تطبیقدهنده (Adapter) | «بخش کوچک آموزشدیده» | ماتریسهای A و B در LoRA که در قالب فایلی مجزا (بین 10 تا 100 مگابایت) ذخیره میشوند و روی هر نسخهای از مدل پایه قابل بارگذاری هستند. |
| ماژولهای هدف (Target modules) | «لایههای میزبان LoRA» | لایههای خطی مشخصشده در معماری مدل (مانند q_proj و v_proj) که ماتریسهای LoRA به آنها تزریق میشوند. |
| مرج کردن (Merging) | «جوش دادن وزنها» | محاسبهی و جایگزینی آن به جای وزنهای اولیه برای حذف کامل تاخیر ناشی از تطبیقدهنده در فاز استنتاج. |
| بهینهسازهای صفحهبندیشده (Paged optimizers) | «فرار از خطای حافظه (OOM)» | انتقال متغیرهای بهینهساز (مانند تکانه و واریانس در Adam) به رم سیستم در شرایط سرریز شدن حافظهی گرافیکی. |
| فراموشی فاجعهبار (Catastrophic forgetting) | «خراب شدن مهارتهای اولیهی مدل» | تخریب قابلیتهای یادگرفتهشدهی قبلی به دلیل تغییر مستقیم تمام وزنها حین یادگیری یک تسک جدید. |
مراجع و مطالعهی بیشتر
- مقالهی هو و همکاران، "LoRA: Low-Rank Adaptation of Large Language Models" (سال 2021) -- مقالهی بنیادین ارائهدهندهی تجزیهی کمرتبه که روی مدل GPT-3 175B حتی با رتبهی 4 نتایج موفقی ثبت کرد.
- مقالهی دتمرز و همکاران، "QLoRA: Efficient Finetuning of Quantized Language Models" (سال 2023) -- ارائهدهندهی ساختار NF4، کوانتیزاسیون دوگانه و بهینهسازهای صفحهبندیشده که فاینتیونینگ مدلهای 65B را روی یک کارت 48GB محقق کرد.
- مستندات کتابخانهی PEFT (در huggingface.co/docs/peft) -- کتابخانهی مرجع روشهای LoRA، QLoRA و سایر شیوههای کارآمد پارامتر در اکوسیستم Hugging Face.
- پژوهش یاداف و همکاران، "TIES-Merging: Resolving Interference When Merging Models" (سال 2023) -- ارائهی راهکارهای ترکیب چندین تطبیقدهندهی LoRA بدون افت عملکرد.
- مقالهی رافایلوف و همکاران، "Direct Preference Optimization: Your Language Model is Secretly a Reward Model" (کنفرانس NeurIPS 2023) -- استخراج فرمولبندی الگوریتم DPO؛ فاز تنظیم ترجیحات پس از SFT بدون نیاز به مدل پاداش.
- مستندات پکیج TRL -- راهنمای کامل
SFTTrainer،DPOTrainer،KTOTrainerو ساختار تعامل آنها با PEFT، bitsandbytes و Unsloth. - مستندات فریمورک Unsloth -- بهرهگیری از کرنلهای بهینهشده برای دو برابر کردن نرخ پردازش در فاینتیونینگ و کاهش نصف مصرف حافظه؛ لایهی شتابدهندهی زیرین در پکیج TRL.
- مستندات فریمورک Axolotl -- چارچوب مبتنی بر فایلهای کانفیگ YAML برای فاینتیونینگ توزیعشده با SFT، DPO و QLoRA؛ ساختاری پایدار جهت جایگزینی اسکریپتهای دستی.
