Transformer içəridən
Transformer içəridən
Bu kitabdakı hər şey bir arxitekturanın üzərində dayanır. Fine-tuning zamanı hansı weight-lərin dəyişdiyini, inference-in niyə yaddaşa ilişdiyini, context window-un niyə bahalı olduğunu və LoRA-nın niyə məhz attention matrix-lərinə qoşulduğunu başa düşmək üçün transformer-in içində data-nın necə axdığını bilmək lazımdır. Bu fəsildə bir token-in giriş embedding-indən növbəti token-in ehtimalına qədər keçdiyi yolu addım-addım izləyirik.
Problem: ardıcıllığı necə emal etməli
Mətn ardıcıllıqdır və mənası kontekstdən asılıdır. "Bank" sözü "çayın sahilində" ilə gəlirsə bir şey, "kredit" ilə gəlirsə başqa şey deməkdir. İkinci fəsildə gördük ki, hər token embedding vektoruna çevrilir. Lakin bu vektor statikdir, yəni "bank" hər cümlədə eyni vektorla başlayır. Bizə elə bir mexanizm lazımdır ki, hər token-in representation-ını ətrafındakı token-lərə görə yeniləsin.
Transformer-dən əvvəl bu iş RNN-lərlə görülürdü. RNN token-ləri bir-bir oxuyur və bütün keçmişi tək bir gizli vektorda sıxır. Bunun iki problemi var idi. Birincisi, uzaq məlumat yol boyu "unudulur". İkincisi, hesablama ardıcıldır, yəni 1000-ci token-i emal etmək üçün əvvəlki 999-u gözləmək lazımdır. GPU isə paralel iş üçün qurulub. 2017-ci ildə "Attention Is All You Need" məqaləsi recurrence-i tamamilə atdı: hər token birbaşa digər bütün token-lərə baxır və bütün mövqelər paralel hesablanır.
Attention: Q, K, V
Attention-u axtarış sistemi kimi düşünmək olar. Hər token üç rol oynayır:
- Query soruşur: "mən hansı məlumatı axtarıram?"
- Key elan edir: "məndə bu cür məlumat var."
- Value daşıyır: "uyğun gəlsəm, ötürəcəyim məzmun budur."
Üçü də eyni giriş vektorundan üç ayrı öyrənilən matrix-lə alınır. Giriş X-in forması [T, d_model] olsun (T token sayı, d_model representation ölçüsü):
Q = X · W_q # [T, d_k]
K = X · W_k # [T, d_k]
V = X · W_v # [T, d_v]
scores = Q · Kᵀ / sqrt(d_k) # [T, T]
weights = softmax(scores) # hər sətir cəmi 1
output = weights · V # [T, d_v]
scores[i][j] i-ci token-in query-sinin j-ci token-in key-inə nə qədər uyğun gəldiyini göstərir. Bu, ikinci fəsildəki dot product oxşarlığıdır. Softmax bu xalları ehtimal paylanmasına çevirir. Nəticədə hər token-in yeni representation-ı bütün value-ların çəkili ortalamasıdır.
sqrt(d_k)-ya bölmənin səbəbi praktikdir. Ölçü böyüdükcə dot product-ların variance-ı da böyüyür. Böyük xallar softmax-ı doyurur, yəni bir mövqe demək olar 1, qalanları 0 olur və gradient-lər yox olur. Bölmə xalları sabit miqyasda saxlayır.
Causal mask
Language model növbəti token-i proqnozlaşdırır. Training zamanı bütün ardıcıllıq birdən verilir, amma 5-ci mövqe 6-cı token-i görməməlidir, əks halda model sadəcə cavabı köçürər. Buna görə softmax-dan əvvəl diaqonaldan yuxarıdakı xallar -inf ilə əvəz olunur və onların weight-i sıfıra enir. Bu causal mask-dır. Onun sayəsində T uzunluqlu bir ardıcıllıq bir forward pass-da T ayrı training nümunəsi verir: hər mövqe öz növbəti token-ini proqnozlaşdırır.
Multi-head attention
Tək attention bir növ əlaqəni tutur. Dildə isə eyni anda çox əlaqə var: sintaktik bağlılıq, coreference, yaxınlıq, mövzu. Multi-head attention d_model-i h hissəyə bölür, məsələn d_model=4096 və h=32 olarsa, hər head 128 ölçülüdür. Hər head öz Q, K, V-si ilə paralel işləyir. Nəticələr birləşdirilir və çıxış matrix-i W_o ilə yenidən d_model-ə proyeksiya olunur. Interpretability tədqiqatları göstərir ki, ayrı-ayrı head-lər həqiqətən fərqli işlər görür: biri əvvəlki token-ə baxır, biri ardıcıllıqdakı nümunəni kopyalayır (induction head), biri isə müəyyən sintaktik əlaqəni izləyir.
Kompakt PyTorch implementasiyası:
import torch, torch.nn.functional as F
def causal_self_attention(x, W_qkv, W_o, n_heads):
B, T, D = x.shape
q, k, v = (x @ W_qkv).split(D, dim=-1) # hər biri [B, T, D]
# [B, T, D] -> [B, heads, T, head_dim]
q, k, v = (t.view(B, T, n_heads, D // n_heads).transpose(1, 2) for t in (q, k, v))
scores = q @ k.transpose(-2, -1) / (D // n_heads) ** 0.5 # [B, h, T, T]
mask = torch.triu(torch.ones(T, T, dtype=torch.bool), diagonal=1)
scores = scores.masked_fill(mask, float('-inf'))
out = F.softmax(scores, dim=-1) @ v # [B, h, T, hd]
return out.transpose(1, 2).reshape(B, T, D) @ W_o
Production kodunda bunun əvəzinə F.scaled_dot_product_attention(q, k, v, is_causal=True) çağırılır. Riyazi nəticə eynidir, amma yuxarıda göstərilən optimallaşdırılmış kernel-lərdən istifadə edir.
Mövqe məlumatı: absolute-dan RoPE-a
Attention-un özündə mövqe anlayışı yoxdur. Token-ləri qarışdırsanız, hər token eyni çəkili ortalamanı alar. "İt pişiyi qovdu" ilə "pişik iti qovdu" fərqlənməzdi. Mövqe məlumatını ayrıca vermək lazımdır.
İlk transformer sinusoidal absolute positional encoding istifadə edirdi: hər mövqe üçün sabit vektor embedding-ə əlavə olunurdu. GPT-2 bu vektorları öyrənirdi. Hər iki yanaşmanın zəif tərəfi odur ki, model training zamanı görmədiyi uzunluqlara yaxşı ümumiləşmir və vacib olan nisbi məsafə ("iki token geri") birbaşa təmsil olunmur.
Bu gün demək olar bütün open model-lər (Llama, Qwen, Mistral, DeepSeek) RoPE istifadə edir. RoPE embedding-ə heç nə əlavə etmir. Əvəzində query və key vektorlarını cüt-cüt ölçülər üzrə mövqeyə mütənasib bucaq qədər fırladır. İki fırlanmış vektorun dot product-ı yalnız onların bucaq fərqindən, yəni nisbi məsafədən asılı olur. Beləliklə nisbi mövqe attention xalının içinə təbii şəkildə daxil olur. RoPE-un praktik üstünlüyü context-i genişləndirmək imkanıdır. Fırlanma tezliklərini miqyaslandıran üsullar (position interpolation, NTK-aware scaling, YaRN) 8K-da train olunmuş model-i qısa əlavə training ilə 128K-ya çatdırmağa imkan verir.
Residual stream və normalization
Transformer block-u iki sub-layer-dən ibarətdir: attention və feed-forward. Hər biri girişi əvəz etmir, ona əlavə edir:
x = x + Attention(Norm(x))
x = x + FFN(Norm(x))
Bu residual connection sayəsində block-dan-block-a axan vektoru residual stream kimi təsəvvür etmək faydalıdır. Bu, ortaq bir "yaddaş lövhəsi"dir: hər layer oradan oxuyur və ona kiçik düzəliş yazır. Residual-lar gradient-lərin onlarla layer-dən keçib zəifləmədən geri qayıtmasına imkan verir, dərin network-ləri train etməyi mümkün edən də budur.
Normalization aktivasiyaların miqyasını sabit saxlayır. Müasir model-lər norm-u sub-layer-dən əvvəl qoyur (pre-norm), çünki bu, training-i daha stabil edir. Klassik LayerNorm vektordan ortalamanı çıxır və standart sapmaya bölür. RMSNorm ortalamanı çıxmağı atır və yalnız root mean square-ə bölür. Keyfiyyət demək olar eynidir, amma hesablama daha ucuzdur, ona görə Llama nəslindən bəri standart budur.
Feed-forward: SwiGLU
Attention token-lər arasında məlumat daşıyır. FFN isə hər token-in üzərində ayrıca işləyir: vektoru daha böyük ölçüyə genişləndirir, qeyri-xətti funksiyadan keçirir və geri sıxır. Parametrlərin təxminən üçdə ikisi buradadır və faktiki biliyin böyük hissəsinin burada saxlandığı güman olunur.
Klassik FFN W_2 · ReLU(W_1 · x) idi. Müasir model-lər SwiGLU istifadə edir:
FFN(x) = W_down · ( SiLU(W_gate · x) ⊙ (W_up · x) )
Burada bir proyeksiya "gate" rolunu oynayır və digərinin hansı hissəsinin keçəcəyini element-element idarə edir. Üç matrix olduğu üçün daxili ölçü adətən d_model-in 4 qatı yox, təxminən 8/3 qatı götürülür ki, parametr sayı dəyişməsin.
Decoder-only stack və logits
İndi tam modeli yığaq:
- Token ID-ləri embedding cədvəlindən keçir:
[T] → [T, d_model]. - N dəfə (kiçik model-lərdə 16–32, böyüklərdə 60–120) transformer block-u.
- Son RMSNorm.
- Unembedding matrix-i hər mövqeni lüğət ölçüsünə proyeksiya edir:
[T, d_model] → [T, vocab]. Bunlar logits-dir. - Softmax logits-i növbəti token üzərində ehtimal paylanmasına çevirir.
Training zamanı bu paylanma ilə real növbəti token arasında cross-entropy loss hesablanır, yəni birinci fəsildəki training loop-u. Generasiya zamanı yalnız son mövqenin paylanması lazımdır. Ondan token seçilir (sampling yeddinci fəsildə), ardıcıllığa əlavə olunur və proses təkrarlanır. Encoder-decoder (T5) və encoder-only (BERT) variantları da var. Lakin generative foundation model-lərin praktik olaraq hamısı decoder-only-dir, çünki bu forma miqyaslananda ən sadə və ən effektiv nəticə verir.
KV cache
Generasiyada hər yeni token üçün bütün ardıcıllığı yenidən emal etmək israfdır. Causal mask sayəsində keçmiş token-lərin key və value-ları gələcəkdən asılı deyil, yəni onlar dəyişmir. Ona görə onlar yadda saxlanılır və hər addımda yalnız yeni token-in Q, K, V-si hesablanır. Bu KV cache-dir.
Onun qiyməti yaddaşdır:
KV bytes = 2 (K və V) × layers × kv_heads × head_dim × seq_len × batch × bytes_per_value
32 layer, 32 head, head_dim 128, fp16 və 32K token olan model-də bir sorğu üçün təxminən 16 GB edir. Bu, çox vaxt model weight-lərinin özündən böyükdür. Uzun context və eyni anda çox istifadəçi xidmət etmək problemi əsasən KV cache problemidir. Yeddinci fəsildə buna qayıdacağıq.
Attention-un qiyməti və variantlar
scores matrix-i [T, T] formasındadır. Context iki qat artanda hesablama və (sadə implementasiyada) yaddaş dörd qat artır, yəni O(T²). 128K token-də bu ciddi xərcdir. Sahə buna bir neçə istiqamətdən cavab verdi:
- GQA (Grouped-Query Attention). Hər query head-in öz K/V head-i olmaq əvəzinə, bir neçə query head ortaq K/V paylaşır, məsələn 32 query head və 8 KV head. Keyfiyyət itkisi azdır, KV cache isə 4 qat kiçilir. Llama 3, Qwen və Mistral-da standartdır. Ekstremal halı bütün head-lərin tək K/V paylaşdığı MQA-dır. DeepSeek-in MLA-sı isə K/V-ni aşağı ölçülü latent vektora sıxır.
- FlashAttention. Bu, riyaziyyatı dəyişmir, yaddaşa müraciəti dəyişir. GPU-da darboğaz çox vaxt hesablama deyil, böyük HBM yaddaşı ilə kiçik, sürətli on-chip SRAM arasında data daşımaqdır. FlashAttention Q, K, V-ni bloklara bölür, hər bloku SRAM-da emal edir və softmax-ı "online" yığır. Bu yolla tam
[T, T]matrix-i heç vaxt yaddaşa yazılmır. Nəticə eyni, amma yaddaş xətti, sürət isə bir neçə qat yüksəkdir. - Sliding window və hybrid arxitekturalar. Bəzi layer-lər yalnız son N token-ə baxır. Bəzi model-lər isə attention layer-lərini xətti state-space layer-ləri (Mamba tipli) ilə növbələşdirir.
Mixture of Experts
Böyük model daha çox bilir, amma hər token üçün daha baha başa gəlir. MoE bu iki şeyi ayırır. FFN tək böyük blok yerinə çoxlu "expert"-ə (məsələn 64 və ya 256) bölünür. Kiçik bir router hər token üçün onlardan yalnız bir neçəsini (top-k, adətən 2–8) seçir. Model ümumilikdə yüzlərlə milyard parametrə malik ola bilər, amma hər token-də yalnız onların kiçik hissəsi aktivdir. Məsələn, DeepSeek-V3-ün təxminən 671B ümumi və 37B aktiv parametri var. Qiymət: bütün expert-lər yenə yaddaşda olmalıdır, training isə router-in expert-ləri bərabər yükləməsini təmin edən əlavə loss tələb edir. Fine-tuning zamanı MoE-nin özünəməxsus incəlikləri ikinci hissədə gələcək.
Tapşırıqlar
- Yuxarıdakı
causal_self_attentionfunksiyasını random weight-lərlə işə salın və nəticəniF.scaled_dot_product_attention(..., is_causal=True)ilə müqayisə edin (fərq 1e-5-dən kiçik olmalıdır). Sonra mask-ı silin və 3-cü mövqedəki çıxışın 5-ci token dəyişdikdə dəyişdiyini göstərin. - Açıq bir model-in konfiqurasiya faylını götürün (layer sayı, hidden size, attention və KV head-lərinin sayı, intermediate size, vocab). Parametr sayını əl ilə hesablayın və model kartındakı rəqəmlə müqayisə edin. Hansı hissə ən çox parametr tutur?
- Həmin model üçün fp16-da 8K, 32K və 128K context-də bir sorğunun KV cache ölçüsünü hesablayın. Model GQA istifadə etməsəydi (KV head = query head), 128K rəqəmi nə olardı?