TRL documentation
1M Token Ötesinde Eğitim
1M Token Ötesinde Eğitim
Bir ajan, saatlerce bir görev üzerinde çalışabilir: dosyaları okur, komutları çalıştırır ve oturumun her turunu ileri taşır. Biriken bu oturum yüz binlerce tokena ulaşır; bu nedenle sınır modelleri artık milyon token'lık bağlam pencerelerini öne çıkarıyor.
Bir modelin bu kadar uzun bir bağlamda iyi olması, onu bu uzunluktaki diziler üzerinde eğitmeyi gerektirir ve zorluk tam da burada başlar: bir milyon token'lık bir dizi tek bir GPU'ya sığmaz, sekiz GPU'ya da yardım olmadan sığmaz.
Bu kılavuzdaki örnek, tek bir 8 GPU'lu düğümde, her adımda tam olarak bir adet bu türden dizgi üzerinde eğitim yapar.
Çalıştırın
8 H100 (veya daha iyisi) olan bir düğüm ve transformers>=5.17.0 gerekir: örnek, bu sürümde eklenen gradient checkpointing'in offload özelliğini kullanır.
accelerate launch \
--config_file examples/sft_qwen3_8b_1m_context/context_parallel_8gpu.yaml \
examples/sft_qwen3_8b_1m_context/sft_qwen3_8b_1m_context.pyBetik, PG-19 kitaplarını kullanarak Qwen3-8B modelini ince ayar yapar; kitaplar, her biri yaklaşık bir milyon token uzunluğuna ulaşana kadar uç uca birleştirilir. Yaklaşık on dakikalık yükleme ve tokenizasyon işleminden sonra ilk adım tamamlanır:
{'loss': 4.311, 'grad_norm': 29.25, 'num_tokens': 1049000.0, 'epoch': 0.25}
8%|▊ | 1/12 [06:20<1:09:41, 380.10s/it]Okunmaya değer üç sayı:
num_tokens1.049e6'dir. Bu, kısa dizilerden oluşan bir parti değil, tek bir dizidir. Bir milyonluk tüm aralık boyunca her token, kendinden önceki her token'a dikkat eder.- Kayıp 4,3 ile başlar, bu normal bir başlangıç kaybıdır. Uzun bağlam için ince ayarları hatalı yapılmış bir çalıştırma ise 10 civarında başlar ve bunun nedenini daha sonra ele alacağız.
- Adım başına 380 sn. Bir adım bir diziyi temsil ettiğinden, bu her dizi için altı dakikadan biraz fazlasına denk gelir.
Basitçe, bu dizinin GPU başına 288 GB bellek gerektirdiği söylenebilir. Bu rehberin geri kalanı, bu değerin 56'ya nasıl düşürüldüğünün hikâyesidir.
Az önce ne oldu
Aşağıdaki her şey, zaten bildiğimiz bir kurulumdan yola çıkarak inşa edilir: tek bir GPU, dizide birkaç bin token. Diziler büyüdükçe sorunlar başlar ve bu sorunlar sabit bir sırayla ortaya çıkar.
Kayıp, bozulacak ilk şeydir
Qwen3-4B'yi tek bir 80 GB kart üzerinde çalıştırın ve diziyi büyütün. İlk olarak bellek yetersizliği yaşayan şey model değildir. Kayıptır. Bellek profilini kontrol edin.
Zirve, kaybın hesaplandığı ileri ve geri geçişler arasındadır. Zirvenin neden orada olduğunu anlamak için, çözümlemenin son aşamalarının ne yaptığını ve kaybın bunlardan nasıl oluşturulduğunu incelememiz gerekir.
Çözücü (decoder) son katmanı, şekli, olan gizli bir durum (hidden state) üretir; burada dizinin uzunluğunu, gizli boyutu ifade eder. Bu gizli durum, ardından dil modelleme başlığı (language modeling head) ağırlıklarıyla çarpılarak şekli, olan bir logits matrisi oluşturur; burada kelime dağarcığı (vocabulary) boyutudur. Bu matris, dizi uzun olduğu ve kelime dağarcığı büyük olduğu için (yüz binin üzerinde giriş) çok büyüktür. Kayıp (loss), bu matris ve hedef etiketler kullanılarak çapraz entropi (cross entropy) fonksiyonuyla hesaplanır ve bu matrisin bellekte somutlaştırılması (materialize edilmesi) tam olarak tepe noktasını oluşturur.
Tüm logits matrisini oluşturmadan kaybı hesaplayabilir miyiz? Evet, parçalar halinde ilerleyebiliriz.
Devasa logits matrisi: parçalara ayırın
Tüm dizinin logitlerini tek seferde hesaplamak yerine, her seferinde birkaç yüz satır alırız, bu satırlar için kaybı hesaplar ve parçaları toplarız. Logit matrisinin tamamı asla aynı anda bellekte tutulmaz. Satırlar, temiz bir şekilde bölünebilen boyuttur: kayıp, satırlar üzerinden bir toplamdır ve içindeki softmax, tek bir satırın sözlüğü boyunca çalışır; bu nedenle satırların bir parçası, kendi kaybı için gereken her şeyi taşır.
Tek bir büyük çarpma işlemi, birkaç daha küçük çarpma işlemine dönüşür ve bellek tasarrufu o kadar büyük olur ki kayıp bir daha asla zirveye ulaşmaz. TRL, her çarpma işleminin GPU'yu iyi kullanacak kadar büyük kalmasını sağlamak için parça başına 256 satır kullanır.
Yeni profil şu şekilde görünüyor.
Zirve kayboldu.
TRL'de bunu nasıl etkinleştiririm? Yapılacak bir şey yok: parçalı kayıp (chunked loss) varsayılandır. Düz kayba (plain loss) geçmek isterseniz, SFTConfig içinde loss_type="nll" değerini ayarlayın.
Bu, diziyi ne kadar ileriye götürebilmemizi sağlıyor? Bellek açısından, bu tek değişiklik bile bizi 32k token'dan 160k token'a taşıyor.
Model 100.000 konumunu hiç görmedi: konumları YaRN ile yeniden ölçekleyin
Şu anda belleğe 160k token sığdırabiliyoruz. Peki model bunları gerçekten okuyabiliyor mu?
Qwen3-4B, 40.960 token uzunluğundaki diziler üzerinde eğitildi. Yapılandırma dosyası bunu belirtiyor:
>>> from transformers import AutoConfig
>>> AutoConfig.from_pretrained("Qwen/Qwen3-4B").max_position_embeddings
40960O noktadan sonraki her konum, modelin hiç karşılaşmadığı bir konumdur. Hiçbir şey çökmez, ancak çalışma, modelin yerleştiremediği tokenlar üzerinde eğitim yapar. 160k tokenlik bir dizideki her token için kayıp aşağıdadır:
Model 40.960'da çökmez. Bir süre daha devam eder ve ancak 70k civarından sonra konuyu kaybetmeye başlar. Oradan itibaren istikrarlı bir şekilde yükselir ve aynı metnin unigram entropisinin (6.45) üzerine çıkar: bağlamı tamamen yok sayıp yalnızca token frekanslarından tahmin yapan bir modelden daha kötü.
Her token kendi konumuyla birlikte gelir ve model, 0 ile 40.960 arasındaki konumların ne anlama geldiğini öğrenmiştir. 100.000. konum hakkında soru sorduğunuzda, modelin dayanak alacağı hiçbir şey yoktur.
Çıkış yolu, modele bilmediği bir konum asla vermemektir. Ölçeklendirme yapıyoruz: 100.000. konum 25.000. konum oluyor ve dizideki her konum, modelin bildiği aralığın içine geri dönüyor. Tokenlar değişmiyor, yalnızca her birine verilen konum değişiyor. YaRN tam olarak bunu yapıyor ve katsayı, ne kadar ileri gittiğimizin basit bir ölçüsüdür: 40.960 eğitim, 163.840 istenen, yani 4.
TRL'de bunu nasıl etkinleştirilir? Model yüklendiğinde RoPE yapılandırmasını iletin:
training_args = SFTConfig(
...,
model_init_kwargs={
"rope_parameters": {
"rope_type": "yarn",
"rope_theta": 1_000_000, # has to match what the model ships with
"factor": 4.0,
"original_max_position_embeddings": 40960,
},
},
)Aynı ölçüm, yeniden ölçeklendirilmiş çalıştırma üstte:
Yeniden ölçeklendirilmiş çalışma, 160k'ya kadar tamamen düz seyrediyor. Son 20k token'da ortalama kaybı 2.8 iken, yeniden ölçeklendirme olmadan bu değer 7.3'tür.
Aktivasyonlar geriye kalan kısımdır: bunları dışa aktarın
Kayıp (loss) parçalara ayrılmış ve konumlar yeniden ölçeklendirilmişken, tek bir GPU bizi 160k token'a kadar götürüyor. Ardından yine doluyor. Bu sefer kartı ne dolduruyor?
Model ve optimizatörü, dizi uzunluğundan bağımsız olarak sabit 22 GB yer kaplar. Bunun üzerinde, her kırmızı bant bir katmanın kaydedilmiş aktivasyonunu temsil eder ve her biri 0,47 GB’tır. Bunlar, ileri geçiş yazdıkça üst üste yığılır, orada bekler ve geri geçiş onları tükettikçe tek tek kaldırılır. Üstteki gri alan, milisaniyeler içinde yeniden hesaplanıp atılan her şeyi ifade eder.
TRL bunları sizin için zaten azaltır ve varsayılan olarak açıktır. Gradient checkpointing, her katmana girenleri saklar ve katmanın kendi içinde hesapladığı her şeyi (attention skorları, geniş MLP ara katmanı, normlar) atar. Backward pass bunlara ihtiyaç duyduğunda, onları geri almak için katmanı ikinci kez çalıştırır. Kalan, katman başına bir saklanan tensördür; her biri sequence x hidden boyutundadır ve uzun sequence uzunluklarında bu, kart üzerindeki en büyük şey olmaya devam eder.
İşe yarayan gözlem, bu tensörlerin ileri geçiş sırasında hiç gerekmemesidir. Yazılırlar, dokunulmaz bırakılırlar ve çok daha sonra okunurlar. Dolayısıyla bu arada GPU üzerinde durmak zorunda değillerdir: geriye doğru geçiş onlara ulaştığında geri getirilmek üzere CPU belleğine taşınabilirler.
training_args = SFTConfig(..., gradient_checkpointing_kwargs={"offload": True})
Aynı adım, aynı uzunluk: 38 bant üst üste yığılmak yerine, aynı anda yalnızca 4 bant bellekte kalır ve tepe değer 59,9 GB'dan 48,8 GB'a düşer. Bu da dizgi uzunluğunu artırır:
48k’nın altında ikisi aynıdır, çünkü bu uzunlukta taşınacak neredeyse hiçbir şey yoktur. Bu eşiğin ötesinde offload edilen çalıştırma daha düşük kalır ve daha önce 160k’da duran dizi, aynı kart üzerinde artık 256k’a ulaşır.
Bu yöntem ücretsiz değildir. Kaydedilen her aktivasyon, host'a geçer ve geri döner; bu trafiğin, yanındaki hesaplama işlemleri arasında sığması gerekir.
Tek GPU yeterli değil: diziyi birçok GPU arasında bölün
Parçalı kayıp, aktarılmış aktivasyonlar: artık tek bir kart 256k token üzerinde eğitim yapabiliyor. Bir milyona nasıl ulaşılır? Engel olanı görmek için, geriye yayılım sırasında (yukarıdaki profildeki gri tepe noktaları) tek bir katmanın içine bakın.
Gradient checkpointing, ileri geçişin neredeyse hiçbir şeyi saklamadığı anlamına gelir: her katman girdisini saklar ve geri kalanı atar. Katman, geriye doğru geçiş ona ulaştığında yeniden hesaplanır ve her şeyin aynı anda var olduğu an tam olarak budur. MLP'de, modelin tek bir satırında,
down_proj(act_fn(gate_proj(x)) * up_proj(x))
dört tensör tam ara genişlikte oluşturulur ve standart uygulama, geri yayılım bunların üzerinden geçene kadar hepsini bellekte tutar. Bir milyon token’da, MLP’nin her token’ı 2560’tan 9728’e genişlettiği bir modelde:
gate_proj(x) 19 GB = 1,048,576 x 9728 x 2 bytes
act_fn(gate_proj(x)) 19 GB
up_proj(x) 19 GB
act_fn(gate_proj(x)) * up_proj(x) 19 GB
total 76 GB76 GB, tek bir katman içinde, 80 GB kapasiteli ve model ile optimizatöre zaten 30 GB ayırmış bir kart üzerinde. Bu noktada cevap, diziyi daha akıllıca depolamak değil. Cevap, her GPU'ya bundan daha azını vermek.
Her GPU'ya dizinin bir dilimini verin. Dört GPU ile her GPU, 1.048.576 yerine 262.144 token tutar. Dört tensör de buna göre küçülür: 76 GB yerine 19 GB, bu da modelin 30 GB'ı ile birlikte karta sığar.
Açık itiraz dikkat mekanizmasıdır. Her token, kendinden önceki her tokena dikkat etmelidir ve hiçbir GPU önceki tokenlara sahip değildir. Dolayısıyla GPU’lar eksik olanları birbirleriyle değiştirir ve ortaya çıkan sonuç, tek bir devasa GPU’nun hesaplayacağıyla birebir aynıdır.
Bu değiş tokuşu çalıştırmak için iki yol vardır: bağlam paralelliği (CP) ve Ulysses dizi paralelliği (SP).
| CP | SP | |
|---|---|---|
| FSDP2 | ✅ | ❌ |
| DeepSpeed | ❌ | ✅ |
| Ayar | cp_size | sp_size |
| Ne bölünür | tokenlar | dikkat başlıkları |
| Ne kadar ölçeklenebilir | herhangi bir sayıda GPU | KV başlarının sayısına kadar, burada 8 |
| Dikkat | SDPA, yalnızca nedensel | SDPA veya FlashAttention |
| Gereksinimler | accelerate 1.11 | accelerate 1.12, DeepSpeed 0.18.1 |
FSDP2'de CP kullanın. Her GPU kendi dilimini tutar ve diğer dilimler sırayla ona gelir, böylece her token sonunda önceki her token'ı görür.
parallelism_config:
parallelism_config_cp_size: 4DeepSpeed'de SP kullanın. Dilimlerin taşınması yerine, dikkat (attention) işleminden hemen önce batch yeniden karıştırılır; böylece her GPU tüm token'ları barındırır ancak dikkat başlarının (attention heads) yalnızca dörtte birine sahip olur, ardından işlem sonrası yeniden karıştırılır.
parallelism_config:
parallelism_config_sp_size: 4Bugün bu iki ayar çakışmıyor: cp_size FSDP2 gerektirirken sp_size yalnızca DeepSpeed altında çalışır. Arka ucu seçin, yöntem kendiliğinden belirlenir. Bu bölümün geri kalanı, bu kılavuzun başındaki örneğin kullandığı FSDP2 yolunu izler: GPU'ları ayrı toplularda bağımsız işçiler değil, tek bir diziyi paylaşan tek bir gruptur.
Açıldığında iki şey değişir:
- Diziler
cp_size * 2'nin katına doldurulmalıdır, bu nedenle dört GPU için SFTConfig'depad_to_multiple_of=8kullanılmalıdır. - Causal-SDPA gereksinimi, belgelerin birbirini okumasını engellemek için blok-köşegenli bir maskeye dayanan paketlemeyi (packing) dışlar ve TRL, her ikisini birden istediğinizde hata verir. Ayrıca, katmanları kayan pencere (sliding-window) veya parçalı (chunked) dikkat kullanan modelleri de dışlar; Accelerate bu modelleri reddeder: OpenAI GPT-OSS, Gemma 3 ve 4, Qwen3.5 ve sonrası. Qwen3 ve Qwen3-MoE boyunca tam dikkat (full attention) kullandığı için, burada kullanılan modeller bunlardır.
Dilimlerin etrafta dolaştırılması, tahmin ettiğinizden daha az maliyetlidir. Tek bir düğümde 131k token ile bir adım, iki GPU'da 34,6 sn, dört GPU'da 17,8 sn ve sekiz GPU'da 9,5 sn sürer. Grubun her iki katına çıkması adım süresini neredeyse yarıya indirir ve iki GPU'dan sekiz GPU'ya geçiş, olası 4x hızlanmanın 3,7x'ini geri kazanır.
Ve bu, dört kaldıracın sonuncusudur:
Bir kart 256k'nın hemen ötesinde duruyor. Dördü birlikte tüm bir milyona ulaşıyor!, 63.6 GB'da rahat bir pay bırakarak. Bu rehberin açılışında yer alan dizi sığıyor.
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:Truedeğerini ayarlayın. Yukarıdaki çalıştırma bir milyon token için 80 GB’lık bir kartta 63,6 GB gerektiriyor ve bu ayar olmadan yine de başarısız oluyor: istediği tensörler büyük ve ardışık olduğundan, ayıracın (allocator) elindeki bloklar arasında onlara yer bulması mümkün olmuyor.
Daha fazla okuma
- Bağlam paralelliği,
cp_sizeve bunun oluşturduğu cihaz ızgarası için accelerate kılavuzu. - Ulysses ve ring attention, iki değişimin nasıl farklılaştığı ve her birinin iletişim maliyetinin ne olduğu hakkında.
- RoPE bölümünde kullanılan konum yeniden ölçeklendirmesi olan YaRN.