Seri · The Quiet Machine · Bölüm 13
RTX 3060 Üzerinde Bir Kodlama Asistanını QLoRA ile Fine-Tune Etmek
Box’ta zaten bir RAG ve memory katmanına bağlı yerel bir kodlama modeli çalışıyordu: retrieval, modeli sürekli değişen gerçeklere bağlı tutuyor; uzun bir system prompt da neredeyse hiç değişmeyen formatlama ve ton kurallarını dayatıyor. İyi çalışıyor. Ama cevabını baştan varsaymak yerine test etmeye değer bir soru da doğurdu: bir kural neredeyse hiç değişmiyorsa, neden modelin ağırlıklarında değil de her konuşmada yeniden göndermek zorunda kaldığım bir prompt’ta duruyor? Bunu box’ın RTX 3060’ında QLoRA ile test ettim.
Fine-tuning’i retrieval yerine kullanmanın asıl gerekçesi bu. RAG, gerçeklerin sahibi. Fine-tuning’in ise davranışın sahibi olması gerekiyor: commit message formatı, ton, aylarca sabit kalan review içgüdüleri. Bunu ağırlıklara bir kez işlersen, system prompt ya küçülür ya da büsbütün ortadan kalkar.
Deneyi çalıştırdım. Üç deneme, her biri kendine özgü bir kök sebeple başarısız oldu; dördüncüsü tuttu, ardından gelen eval de ikiye bölünmüş döndü: bir eksende gerçek bir kazanç, başka bir eksende gerçek bir gerileme. Başka hiçbir şey kıpırdamadı. Yine de yayına aldım — tek bir belirli iş için çağırdığım opsiyonel bir model olarak.
Güvenebileceğim bir dataset kurmak
Fine-tuning, instruction/output çiftleri istiyor; elimde de diskte hazır duran üç kaynak vardı: kişisel repolarımda yıllara yayılan kendi commit geçmişim, kendi kod stilim için tek yazılı doğru kaynak olarak tuttuğum bir conventions dokümanı ve elle seçtiğim bir avuç kod örneği. Diff’ler “bu değişiklik için commit mesajı yaz”a dönüştü, conventions dokümanı soru-cevap çiftleri oldu, dedup da embedding benzerliği üzerinden çalışıp sette zaten var olan bir şeye fazla yakın düşen her şeyi eledi.
O dataset’in erken bir build’i, daha GPU’ya hiç uğramadan bozuldu: kazara, kişisel bir training set’te hiçbir işi olmayan bir projeyi de içine çekmişti. Review sırasında yakaladım, tümüyle temizledim ve training başlamadan dataset’i sıfırdan temiz kurdum. O ilk build’den hiçbir şey kullanılmadı.
Eğittiğim set: 419 örnek; 369’u training, 50’si eval için ayrıldı. Elle seçilmiş, “lessons” adında bütün bir planlı kategoriyi de düşürdüm; çünkü dışarıda bıraktığım projeyle, güvenli biçimde ayıklanamayacak kadar iç içe geçmişti. Bir kategoriyi kaybetmek, riske atmaktan iyidir.
12GB’lık bir kart için ölçülü QLoRA
Base model: Qwen2.5-Coder-7B-Instruct, training için 4-bit yüklendi. QLoRA rank 16; tutucu bir alpha ve dropout ile, yalnızca attention’ı değil her linear layer’ı hedefleyerek — style transfer’in tutması için attention head’lerinden fazlası gerektiği varsayımıyla. Batch size’ı gradient accumulation ile 1’e çektim, optimizer state’i GPU’nun dışında tutmak için paged_adamw_8bit kullandım, gradient checkpointing’i açtım ve bf16 compute ile çalıştırdım.
Plan Unsloth’tu. Box’ın sistem Python’u 3.14’tü; bu sürümün hiç ML wheel’i yok. Unsloth’un güncel sürümü de bu karttaki CUDA yığınının kaldırabileceğinden daha yeni bir torch istiyordu. uv ile izole bir Python 3.12 kurdum ve onun yerine sabitlenmiş bir yığına döndüm: düz PEFT, bitsandbytes, Transformers ve TRL — bu GPU’da bir arada çalışsınlar diye özellikle seçilmiş sürümler. Training ortamının kendisini de network share yerine box’ın yerel diskine kurdum; veriyi SMB üzerinden çekmek, yerelde tutmaktan yavaştı.
İşe yaramadan önceki üç başarısızlık
- Context-length OOM. İlk deneme sequence length 2048 ile çalıştı; training’in kendisi için gayet yeterliydi, ama ilk epoch’un sonunda training-içi bir eval geçişi başlatmaya kalkıştığı anda VRAM’i tüketti. Çözüm: sequence length’i 1024’e düşürmek, eval’i de training bittikten sonra ayrı bir geçişe almak — böylece koşu ortasında optimizer state ile VRAM’i paylaşmak zorunda kalmıyor.
- Bir CUDA driver mayını. Biraz alan geri kazanmak için
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True‘yu ayarladım — tam da bu tür VRAM baskısında normalde güvenli sayılan bir flag. Ama bu box’ın WSL2 CUDA driver’ı torch 2.4 ile eşleşince, daha ilk backward pass’teCUDA device not readyfırlattı. GPU’yu, training script’inin dışında çıplak bir matmul-ve-backward döngüsüyle tek başına sınadım; tıkır tıkır geçti, demek ki sorun kart değildi. Sorun flag’deydi. Flag’i çıkarınca koşu düzeldi. - Merge ortasında bir WSL bellek çökmesi. Training bitti; sonra LoRA adaptörünü tam bir fp16 checkpoint’e geri merge etmek (bunu CPU’da yaptım, çünkü merge edilmiş ağırlıklar 12GB VRAM’e sığmıyor) WSL2’nin varsayılan 16GB bellek sınırını aştı ve bütün VM’i kilitledi. Windows tarafından çektiğim
wsl --shutdown, Docker ve box’taki her şey dahil hepsini geri getirdi; hiçbiri zarar görmeden. Sonrasında.wslconfig‘teki sınırları yükselttim: bellek 26GB’a, swap tavanı 12GB’a — bir daha olmasın diye.
Sonunda işe yarayan koşu
Sequence length 1024’te 138 adım, yaklaşık 59 dakika wall-clock, final train loss 0.888. Adaptörü merge ettim, GGUF’a çevirdim, box’ın ana kodlama modelinin zaten çalıştığı quant seviyesiyle aynı olan Q4_K_M’ye quantize ettim ve kabaca 4.7GB’lık bir model dosyasına vardım. Bilerek hiçbir system prompt eklemeden, mevcut modelin yanına, Ollama’ya deploy ettim. Ardından gelen eval’in bütün amacı da onun bir prompt’a ihtiyaç duyup duymadığını görmekti.
Eval bölünmüş döndü
Aynı prompt’lar üzerinde üç şeyi karşılaştırdım: prompt’suz haliyle hiç dokunulmamış base model, prompt’suz haliyle fine-tune edilmiş model ve referans noktası olarak mevcut system-prompt’lu modelim. Loss için kullandığım 50 örneklik held-out setin üstüne, dört kategoride yirmi üç eşleştirilmiş karşılaştırma.
- Commit mesajları: kazandı. Fine-tune edilmiş model, prompt’suz haliyle, gerçek convention’ıma uyan, doğru formatlanmış commit mesajları üretti. Training verisinin en yoğun olduğu kategori buydu; belli de oldu.
- Convention soruları: iyileşti. Daha az halüsinasyonlu cevap; yazılı conventions dokümanımın dediğine uyan daha çok cevap.
- Bilinen gotcha’lar için code review: geriledi. Fine-tune edilmiş model burada, hem base hem de system-prompt’lu modelden gözle görülür biçimde daha kısa ve daha küçümseyici çıktı. Commit mesajları 419 training örneğinin 376’sını oluşturuyordu — model o register’ı biraz fazla iyi öğrendi ve farklı bir register isteyen bir göreve de taşıdı.
- Bunların hiçbiriyle ilgisi olmayan genel kodlama soruları: etkilenmedi. Beş held-out prompt üzerinde base modele karşı cevaplar sabit kaldı, hatta biraz iyileşti bile. Fine-tune her ne değiştirdiyse, modelin genel yeteneğine dokunmadı.
Baştaki karar kuralı basitti: fine-tune edilmiş model, hiçbir prompt olmadan, system-prompt’lu modelin bir prompt’la yaptığına açıkça yaklaşamıyorsa, deney net bir kazanç sayılmayacaktı. Dört cepheden ikisinde o çıtaya yaklaştı, üçüncüsünde geriledi. Bu, hangi yönden bakılırsa bakılsın net bir sonuç değil.
Şimdi ne çalıştırıyorum
RAG varsayılan olarak kalıyor. Gerçekleri o hallediyor; bu pilottan sonra da davranış işinin büyük kısmını hâlâ o, system prompt üzerinden görüyor — çünkü o prompt, fine-tune’un tam anlamıyla yerini alamadığı bir şeyi hâlâ yapıyor.
Fine-tune edilmiş model canlı, ama yalnızca bir yan model olarak: onu, prompt’suz daha iyi olduğu commit mesajları için açıkça çağırıyorum; geri kalan her şeyi — özellikle metni formatlamaktan öte bir gotcha üzerine muhakeme yürütmesi gereken her şeyi — hâlâ tam system prompt’uyla koşan modele bırakıyorum. Commit ağırlığı azaltılmış, yeniden dengelenmiş bir dataset: sıradaki deney belli. Ama henüz onu çalıştırmaya karar vermiş değilim.