Fəsil 03 · 8 dəq oxu

Transformer içəridən

Transformer içəridən

Bu kitabdakı hər şey bir arxitekturanın üzərində dayanır. zamanı hansı dəyişdiyini, niyə yaddaşa ilişdiyini, niyə bahalı olduğunu və LoRA-nın niyə məhz matrix-lərinə qoşulduğunu başa düşmək üçün içində data-nın necə axdığını bilmək lazımdır. Bu fəsildə bir giriş növbəti 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 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 representation-ını ətrafındakı görə yeniləsin.

əvvəl bu iş RNN-lərlə görülürdü. RNN 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 emal etmək üçün əvvəlki 999-u gözləmək lazımdır. GPU isə paralel iş üçün qurulub. 2017-ci ildə " Is All You Need" məqaləsi recurrence-i tamamilə atdı: hər birbaşa digər bütün baxır və bütün mövqelər paralel hesablanır.

Attention: Q, K, V

axtarış sistemi kimi düşünmək olar. Hər üç 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 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 query-sinin j-ci 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 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

növbəti proqnozlaşdırır. Training zamanı bütün ardıcıllıq birdən verilir, amma 5-ci mövqe 6-cı 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 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 proqnozlaşdırır.

Multi-head attention

Tək 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 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 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

özündə mövqe anlayışı yoxdur. qarışdırsanız, hər 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 sinusoidal absolute positional encoding istifadə edirdi: hər mövqe üçün sabit vektor ə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 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 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 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

block-u iki sub-layer-dən ibarətdir: 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

arasında məlumat daşıyır. FFN isə hər ü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:

  1. ID-ləri cədvəlindən keçir: [T] → [T, d_model].
  2. N dəfə (kiçik model-lərdə 16–32, böyüklərdə 60–120) block-u.
  3. Son RMSNorm.
  4. Unembedding matrix-i hər mövqeni lüğət ölçüsünə proyeksiya edir: [T, d_model] → [T, vocab]. Bunlar logits-dir.
  5. Softmax logits-i növbəti üzərində ehtimal paylanmasına çevirir.

Training zamanı bu paylanma ilə real növbəti arasında loss hesablanır, yəni birinci fəsildəki training loop-u. Generasiya zamanı yalnız son mövqenin paylanması lazımdır. Ondan seçilir ( 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 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 üçün bütün ardıcıllığı yenidən emal etmək israfdır. Causal mask sayəsində keçmiş 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 Q, K, V-si hesablanır. Bu KV -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 olan model-də bir sorğu üçün təxminən 16 GB edir. Bu, çox vaxt model özündən böyükdür. Uzun context və eyni anda çox istifadəçi xidmət etmək problemi əsasən KV 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 bu ciddi xərcdir. Sahə buna bir neçə istiqamətdən cavab verdi:

  • GQA (Grouped-Query ). 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 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.
  • və hybrid arxitekturalar. Bəzi layer-lər yalnız son N baxır. Bəzi model-lər isə 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 üçü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 üçü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 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. zamanı MoE-nin özünəməxsus incəlikləri ikinci hissədə gələcək.

Tapşırıqlar

  1. Yuxarıdakı causal_self_attention funksiyasını random işə salın və nəticəni F.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 dəyişdikdə dəyişdiyini göstərin.
  2. Açıq bir model-in konfiqurasiya faylını götürün (layer sayı, hidden size, 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?
  3. Həmin model üçün fp16-da 8K, 32K və 128K context-də bir sorğunun KV ölçüsünü hesablayın. Model GQA istifadə etməsəydi (KV head = query head), 128K rəqəmi nə olardı?