Transformers documentation

LLM'lerin Hız ve Bellek Açısından Optimize Edilmesi

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

LLM'lerin Hız ve Bellek Açısından Optimize Edilmesi

GPT3/4, Falcon ve Llama gibi Büyük Dil Modelleri (LLM'ler), insan odaklı görevleri ele alma yeteneklerinde hızla ilerlemekte ve modern bilgi tabanlı sektörlerde vazgeçilmez araçlar olarak konumlanmaktadır. Ancak bu modelleri gerçek dünya görevlerinde dağıtmak hâlâ zorlu kalmaktadır:

  • Neredeyse insana yakın metin anlama ve üretme yetenekleri sergilemek için LLM'lerin şu anda milyarlarca parametreden oluşması gerekmektedir (bkz. Kaplan et al, Wei et. al). Bu durum, çıkarım için bellek gereksinimlerini dolayısıyla artırmaktadır.
  • Birçok gerçek dünya görevinde, LLM'lere kapsamlı bağlamsal bilgi verilmesi gerekir. Bu durum, modelin çıkarım sırasında çok uzun girdi dizilerini yönetebilme yeteneğini zorunlu kılar.

Bu zorlukların özü, özellikle geniş girdi dizileriyle uğraşırken LLM'lerin hesaplama ve bellek yeteneklerini artırmakla ilgilidir.

Bu kılavuzda, verimli LLM dağıtımı için etkili teknikleri inceleyeceğiz:

  1. Düşük Hassasiyet: Araştırmalar, azaltılmış sayısal hassasiyetle, yani 8-bit ve 4-bit ile çalışmanın, model performansında önemli bir düşüş olmadan hesaplama avantajları sağlayabildiğini göstermiştir.

  2. Flash Attention: Flash Attention, dikkat algoritmasının bir varyantıdır; yalnızca daha bellek verimli bir yaklaşım sunmakla kalmaz, optimize edilmiş GPU bellek kullanımı sayesinde artan bir verimlilik de sağlar.

  3. Mimari Yenilikler: LLM'lerin çıkarım sırasında her zaman aynı şekilde dağıtıldığı, yani uzun bir giriş bağlamıyla otoregresif metin üretimi yapıldığı göz önüne alındığında, daha verimli çıkarıma olanak tanıyan özel model mimarileri önerilmiştir. Bu kapsamda model mimarilerindeki en önemli gelişmeler Alibi, Rotary embeddings, Multi-Query Attention (MQA) ve Grouped-Query-Attention (GQA)'dir.

Bu kılavuz boyunca, otomatik regresif üretimi bir tensör perspektifinden analiz edeceğiz. Daha düşük hassasiyete geçmenin avantaj ve dezavantajlarını inceliyor, en güncel dikkat algoritmalarını kapsamlı bir şekilde ele alıyor ve geliştirilmiş LLM mimarilerini tartışıyoruz. Bunu yaparken, her bir özellik iyileştirmesini sergileyen pratik örnekler çalıştırıyoruz.

1. Daha Düşük Hassasiyet

LLM'lerin bellek gereksinimleri, LLM'leri ağırlık matrisleri ve vektörleri kümesi olarak, metin girdilerini ise vektör dizisi olarak görerek en iyi şekilde anlaşılabilir. Aşağıda, ağırlıklar tanımı, tüm model ağırlık matrisleri ve vektörlerini ifade etmek için kullanılacaktır.

Bu rehber yazılırken, LLM'ler en az birkaç milyar parametreden oluşuyordu. Her parametre, genellikle float32, bfloat16 veya float16 biçiminde saklanan 4.5689 gibi ondalık bir sayıdan oluşur. Bu sayede, LLM'i belleğe yüklemek için gereken bellek ihtiyacını kolayca hesaplayabiliriz:

X milyar parametreli bir modelin ağırlıklarını yükleme işlemi, float32 hassasiyetinde yaklaşık 4 * X GB VRAM gerektirir

Günümüzde modeller nadiren tam float32 hassasiyetinde eğitilir; genellikle bfloat16 hassasiyetinde, daha az sıklıkla ise float16 hassasiyetinde eğitilirler. Bu nedenle genel kural şu şekildedir:

X milyar parametreli bir modelin ağırlıklarını yüklemek, bfloat16/float16 hassasiyetinde yaklaşık 2 * X GB VRAM gerektirir

Daha kısa metin girdileri (1024 token'dan az) için çıkarım bellek gereksinimi, ağırlıkları yüklemek için gereken bellek gereksinimi tarafından büyük ölçüde belirlenir. Bu nedenle, şimdilik çıkarım bellek gereksiniminin, modelin GPU VRAM'e yüklenmesi için gereken bellek gereksinimine eşit olduğunu varsayalım.

bfloat16'da bir modelin yüklenmesi için yaklaşık ne kadar VRAM gerektiğine dair bazı örnekler vermek gerekirse:

  • GPT3 için 2 * 175 GB = 350 GB VRAM gereklidir
  • Bloom 352 GB VRAM gerektirir
  • Llama-2-70b 2 * 70 GB = 140 GB VRAM gerektirir
  • Falcon-40b için 2 * 40 GB = 80 GB VRAM gereklidir
  • MPT-30b için 2 * 30 GB = 60 GB VRAM gereklidir
  • bigcode/starcoder için 2 * 15.5 = 31 GB VRAM gereklidir

Bu doküman yazılırken, piyasadaki en büyük GPU çipi, 80GB VRAM sunan A100 & H100'dür. Daha önce listelenen modellerin çoğu, yalnızca yüklenmek için 80GB'tan fazlasına ihtiyaç duyar ve bu nedenle zorunlu olarak tensor parallelism ve/veya pipeline parallelism gerektirir.

🤗 Transformers, yapılandırma sınıflarında base_tp_plan içeren desteklenen modeller için tensor paralelliğini destekler. Tensor Parallelism hakkında daha fazla bilgiyi burada bulabilirsiniz. Ayrıca, modelleri tensor paralelliğine uygun bir şekilde yazmakla ilgileniyorsanız, text-generation-inference kütüphanesine göz atabilirsiniz.

Naive pipeline parallelism, kutudan çıktığı gibi desteklenir. Bunun için, burada açıklandığı üzere farklı katmanları kullanılabilir GPU'lara otomatik olarak yerleştiren device="auto" parametresiyle modeli yüklemeniz yeterlidir. Ancak, bu naive pipeline parallelism çok etkili olsa da GPU bekleme sorunlarını ele almaz. Bunun için, burada açıklandığı üzere daha gelişmiş bir pipeline parallelism gereklidir.

8 x 80GB A100 düğümüne erişiminiz varsa, BLOOM'u şu şekilde yükleyebilirsiniz

!pip install transformers accelerate bitsandbytes optimum
from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained("bigscience/bloom", device_map="auto", pad_token_id=0)

device_map="auto" kullanıldığında dikkat katmanları, tüm kullanılabilir GPU'lar arasında eşit olarak dağıtılır.

Bu kılavuzda, tek bir 40 GB A100 GPU cihaz çipi üzerinde çalıştırılabildiği için bigcode/octocoder modelini kullanacağız. Dikkat edin, bundan sonra uygulayacağımız tüm bellek ve hız optimizasyonları, model veya tensör paralelliği gerektiren modellere de aynı şekilde uygulanabilir.

Model bfloat16 hassasiyetinde yüklendiğinden, yukarıdaki kaba kurala göre bigcode/octocoder ile çıkarım (inference) çalıştırmak için gereken belleğin yaklaşık 31 GB VRAM olmasını bekleriz. Bir deneyelim.

Önce modeli ve tokenizer'ı yükler, ardından ikisini de Transformers'ın pipeline nesnesine iletiriz.

from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
import torch

model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", dtype=torch.bfloat16, device_map="auto", pad_token_id=0)
tokenizer = AutoTokenizer.from_pretrained("bigcode/octocoder")

pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
prompt = "Question: Please write a function in Python that transforms bytes to Giga bytes.\n\nAnswer:"

result = pipe(prompt, max_new_tokens=60)[0]["generated_text"][len(prompt):]
result

Çıktı:

Here is a Python function that transforms bytes to Giga bytes:\n\n```python\ndef bytes_to_giga_bytes(bytes):\n    return bytes / 1024 / 1024 / 1024\n```\n\nThis function takes a single

Harika, artık sonucu doğrudan baytları Gigabayt'a dönüştürmek için kullanabiliriz.

def bytes_to_giga_bytes(bytes):
  return bytes / 1024 / 1024 / 1024

Tepe hızlandırıcı bellek tahsisini ölçmek için torch.accelerator.memory.max_memory_allocated fonksiyonunu çağırıyoruz.

bytes_to_giga_bytes(torch.accelerator.max_memory_allocated())

Çıktı:

29.0260648727417

Tahmini hesabımıza oldukça yakın! Sayının tam olarak doğru olmadığını, bayttan kilobayta geçişte 1000 yerine 1024 ile çarpma gerektiğini görebiliyoruz. Bu nedenle, tahmini formül aynı zamanda “en fazla X GB” hesabı olarak da anlaşılabilir. Modeli tam float32 hassasiyetinde çalıştırmaya çalışmış olsaydık, tam 64 GB VRAM gerekecekti.

Neredeyse tüm modeller günümüzde bfloat16 ile eğitilmektedir; GPU'nuz bfloat16'ı destekliyorsa modeli tam float32 hassasiyetinde çalıştırmak için bir neden yoktur. Float32, modelin eğitildiği hassasiyetten daha iyi çıkarım sonuçları vermez.

Hub üzerinde model ağırlıklarının hangi formatta saklandığından emin değilseniz, her zaman checkpoint'in "dtype" ayarına bakabilirsiniz, örneğin burada. from_pretrained(..., dtype=...) ile yüklerken, modelin config dosyasında yazanla aynı hassasiyet türüne ayarlanması önerilir; ancak orijinal tür float32 ise çıkarım için hem float16 hem de bfloat16 kullanılabilir.

Tahsis edilmiş tüm belleği serbest bırakarak tepe tahsisli GPU belleğini doğru bir şekilde ölçebilmemiz için bir flush(...) fonksiyonu tanımlayalım.

del pipe
del model

import gc
import torch

def flush():
  gc.collect()
  torch.accelerator.empty_cache()
  torch.accelerator.reset_peak_memory_stats()

Bir sonraki deney için şimdi çağıralım.

flush()

Accelerate kütüphanesinden, XPU, MLU, NPU, MPS gibi çeşitli donanım arka uçlarını dikkate alan, donanım bağımsız bir yardımcı yöntem olan release_memory yöntemini de kullanabilirsiniz.

from accelerate.utils import release_memory
# ...

release_memory(model)

Peki, GPU'nuzda 32 GB VRAM yoksa ne olur? Model ağırlıklarının, performansta önemli bir kayba yol açmadan 8-bit veya 4-bit'e kuantize edilebildiği bulunmuştur (bkz. Dettmers et al.). Yakın tarihli GPTQ makalesinde gösterildiği gibi, model kabul edilebilir bir performans kaybıyla 3 veya 2 bite kadar kuantize edilebilir 🤯.

Çok fazla detaya girmeden, kuantizasyon şemaları, modelin çıkarım sonuçlarını mümkün olduğunca doğru tutmaya çalışırken ağırlıkların hassasiyetini azaltmayı hedefler (bkz. bfloat16’ye mümkün olduğunca yakın). Kuantizasyonun özellikle metin üretimi için iyi çalıştığını unutmayın; çünkü bizim için önemli olan yalnızca en olası bir sonraki tokenler kümesini seçmektir ve bir sonraki token logit dağılımının kesin değerleri pek önemli değildir. Önemli olan, bir sonraki token logit dağılımının kabaca aynı kalması ve böylece bir argmax veya topk işleminin aynı sonuçları vermesidir.

Çeşitli kuantizasyon teknikleri bulunmaktadır; bunları burada detaylı olarak ele almayacağız, ancak genel olarak tüm kuantizasyon teknikleri şu şekilde çalışır:

    1. Tüm ağırlıkları hedef hassasiyete göre kuantize edin
    1. Kantize ağırlıkları yükleyin ve girdi vektör dizisini bfloat16 hassasiyetinde iletin
    1. Ağırlıkları dinamik olarak bfloat16'ya dekuantize edin ve hesaplamayı girdi vektörleriyle bfloat16 hassasiyetinde gerçekleştirmek için kullanın

Kısacası, bu girdiler-ağırlık matrisi çarpımlarını ifade eder; burada XX girdiler olup, WW ağırlık matrisi ve YY çıktıdır: Y=X∗W Y = X * W

şu hale getirilir: Y=X∗dequantize(W) Y = X * \text{dequantize}(W)

her matris çarpımı için. Girdiler ağ grafiği boyunca ilerledikçe, tüm ağırlık matrisleri için sıralı olarak dekuantizasyon ve yeniden kuantizasyon gerçekleştirilir.

Dolayısıyla, kuantize ağırlıklar kullanıldığında çıkarım süresi genellikle azalmaz, aksine artar. Yeterince teori, hadi deneyelim! Ağırlıkları Transformers ile kuantize etmek için bitsandbytes kütüphanesinin yüklü olduğundan emin olmanız gerekir.

!pip install bitsandbytes

Ardından, from_pretrained fonksiyonuna load_in_8bit=True bayrağını ekleyerek modelleri 8-bit kuantizasyonla yükleyebiliriz.

model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", quantization_config=BitsAndBytesConfig(load_in_8bit=True), pad_token_id=0)

Şimdi, örneğimizi tekrar çalıştıralım ve bellek kullanımını ölçelim.

pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)

result = pipe(prompt, max_new_tokens=60)[0]["generated_text"][len(prompt):]
result

Çıktı:

Here is a Python function that transforms bytes to Giga bytes:\n\n```python\ndef bytes_to_giga_bytes(bytes):\n    return bytes / 1024 / 1024 / 1024\n```\n\nThis function takes a single

Harika, öncekiyle aynı sonucu alıyoruz, yani doğrulukta bir kayıp yok! Bu sefer ne kadar bellek kullanıldığına bakalım.

bytes_to_giga_bytes(torch.accelerator.max_memory_allocated())

Çıktı:

15.219234466552734

Çok daha az! Sadece 15 GB’ın biraz üzerine indik ve bu sayede bu modeli 4090 gibi tüketici GPU’larında çalıştırabiliriz. Bellek verimliliğinde çok güzel bir kazanç görüyoruz ve modelin çıktısında neredeyse hiçbir bozulma yok. Ancak çıkarım sırasında hafif bir yavaşlama da fark edebiliriz.

Modelleri siler ve belleği tekrar boşaltırız.

del model
del pipe
flush()

4-bit kuantizasyonun zirve GPU bellek tüketiminin ne olduğunu görelim. Modeli 4-bit’e kuantize etmek, öncekiyle aynı API ile yapılabilir - bu sefer load_in_8bit=True yerine load_in_4bit=True geçirilerek.

model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", quantization_config=BitsAndBytesConfig(load_in_4bit=True), pad_token_id=0)

pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)

result = pipe(prompt, max_new_tokens=60)[0]["generated_text"][len(prompt):]
result

Çıktı:

Here is a Python function that transforms bytes to Giga bytes:\n\n```\ndef bytes_to_gigabytes(bytes):\n    return bytes / 1024 / 1024 / 1024\n```\n\nThis function takes a single argument

Daha öncekiyle neredeyse aynı çıktı metnini görüyoruz - yalnızca kod parçasından hemen önceki python eksik. Ne kadar bellek gerektiğine bakalım.

bytes_to_giga_bytes(torch.accelerator.max_memory_allocated())

Çıktı:

9.543574333190918

Sadece 9,5 GB! 15 milyar parametreden büyük bir model için bu gerçekten çok az.

Buradaki modelimizde doğrulukta çok az bir düşüş görsek de, 4-bit kuantizasyon pratikte çoğu zaman 8-bit kuantizasyona veya tam bfloat16 çıkarımına kıyasla farklı sonuçlara yol açabilir. Denemek kullanıcıya kalmıştır.

Ayrıca, 4-bit kuantizasyon için kullanılan daha agresif kuantizasyon yöntemi nedeniyle, çıkarımın burada 8-bit kuantizasyona kıyasla yine biraz daha yavaş olduğunu da not edelim. Bu durum quantize\text{quantize} ve dequantize\text{dequantize} işlemlerinin çıkarım sırasında daha uzun sürmesine yol açar.

del model
del pipe
flush()

Genel olarak, OctoCoder'ın 8-bit hassasiyette çalıştırılmasının gerekli GPU VRAM'i 32G GPU VRAM'den yalnızca 15GB'a düşürdüğünü ve modelin 4-bit hassasiyette çalıştırılmasının ise gerekli GPU VRAM'i 9GB'ın biraz üzerine indirdiğini gördük.

4-bit kuantizasyon, modelin çoğu kişi için oldukça erişilebilir olan RTX3090, V100 ve T4 gibi GPU'larda çalıştırılmasına olanak tanır.

Kantizasyon hakkında daha fazla bilgi almak ve modellerin 4-bit'ten bile daha az GPU VRAM belleği gerektirecek şekilde nasıl kantize edilebileceğini görmek için GPT-QModel uygulamasına göz atmanızı öneririz.

Sonuç olarak, model kuantizasyonunun bellek verimliliğini artırırken doğruluğu ve bazı durumlarda çıkarım süresini ödün olarak verdiğini hatırlamak önemlidir.

GPU belleği kullanım senaryonuz için bir kısıtlama değilse, kuantizasyona bakmak genellikle gerekmez. Ancak birçok GPU, kuantizasyon yöntemleri olmadan LLM'leri çalıştıramaz ve bu durumda 4-bit ve 8-bit kuantizasyon şemaları son derece faydalı araçlardır.

Daha ayrıntılı kullanım bilgisi için Transformers Quantization Docs belgelerine göz atmanızı şiddetle öneririz. Bir sonraki adımda, daha iyi algoritmalar ve geliştirilmiş bir model mimarisi kullanarak hesaplama ve bellek verimliliğini nasıl artırabileceğimize bakalım.

2. Flash Attention

Bugünün en yüksek performanslı LLM’leri, ileri beslemeli katmanlar, aktivasyon katmanları, katman normalizasyonu katmanları ve en önemlisi öz-dikkat katmanlarından oluşan daha az veya daha çok aynı temel mimariyi paylaşır.

Self-attention katmanları, modelin girdi token'ları arasındaki bağlamsal ilişkileri anlamasını sağladığı için Büyük Dil Modelleri'nin (LLM) merkezinde yer alır. Ancak self-attention katmanları için tepe GPU bellek tüketimi kare olarak hem hesaplama hem de bellek karmaşıklığı açısından girdi token sayısı (aynı zamanda dizi uzunluğu) olarak adlandırılır) ile birlikte artar ve bunu aşağıda NN . şu şekilde belirtiyoruz: daha kısa girdi dizileri (1000 girdi token'ına kadar) için bu durum pek dikkat çekici olmamakla birlikte, daha uzun girdi dizileri (yaklaşık 16000 girdi token'ı) için ciddi bir sorun haline gelir.

Let’s take a closer look. The formula to compute the output O\mathbf{O} of a self-attention layer for an input X\mathbf{X} of length NN is: O=Attn(X)=V×Softmax(QKT) with Q=WqX,V=WvX,K=WkX \textbf{O} = \text{Attn}(\mathbf{X}) = \mathbf{V} \times \text{Softmax}(\mathbf{QK}^T) \text{ with } \mathbf{Q} = \mathbf{W}_q \mathbf{X}, \mathbf{V} = \mathbf{W}_v \mathbf{X}, \mathbf{K} = \mathbf{W}_k \mathbf{X}

X=(x1,...xN)\mathbf{X} = (\mathbf{x}_1, ... \mathbf{x}_{N}) dikkat katmanına girdi dizisi olarak tanımlanır. Projeksiyonlar Q\mathbf{Q} ve K\mathbf{K} her biri NN vektörden oluşur ve bu da QKT\mathbf{QK}^T boyutunun N2N^2 .

LLM'ler genellikle birden fazla dikkat başlığına sahiptir, bu nedenle birden fazla öz-dikkat hesabını paralel olarak gerçekleştirir. LLM'in 40 dikkat başlığına sahip olduğunu ve bfloat16 hassasiyetinde çalıştığını varsayarsak, depolamak için gereken bellek miktarını hesaplayabiliriz QKT\mathbf{QK^T} matrisleri için 40∗2∗N240 * 2 * N^2 bayt. N=1000N=1000 için yalnızca yaklaşık 50 MB VRAM gerekir, ancak N=16000N=16000 için 19 GB VRAM gerekecektir ve N=100,000N=100,000 için yalnızca QKT\mathbf{QK}^T matrislerini depolamak için neredeyse 1 TB gerekecektir.

Kısaca, varsayılan öz-dikkat algoritması, büyük giriş bağlamları için hızla kabul edilemez düzeyde bellek maliyetli hale gelir.

LLM'ler metin anlama ve üretim konularında ilerledikçe, giderek daha karmaşık görevlerde kullanılmaya başlanmaktadır. Modeller bir zamanlar birkaç cümlenin çevirisini veya özetlemesini yaparken, artık tüm sayfaları işleyebilmekte ve bu da kapsamlı girdi uzunluklarını işleme yeteneğini gerektirmektedir.

Büyük giriş uzunlukları için fahişe bellek gereksinimlerinden nasıl kurtulabiliriz? QKT\mathbf{QK}^T matrisinden kurtulan yeni bir kendine-dikkat (self-attention) mekanizması hesaplama yöntemine ihtiyacımız var. Tri Dao ve ark. tam da böyle yeni bir algoritma geliştirdi ve ona Flash Attention.

Kısaca, Flash Attention V×Softmax(QKT)\mathbf{V} \times \text{Softmax}(\mathbf{QK}^T) hesaplamasını ayrı bir parça olarak ele alır ve bunun yerine, birden fazla softmax hesaplama adımında gezinerek çıktının daha küçük parçalarını hesaplar: Oi←sija∗Oi+sijb∗Vj×Softmax(QKi,jT) for multiple i,j iterations \textbf{O}_i \leftarrow s^a_{ij} * \textbf{O}_i + s^b_{ij} * \mathbf{V}_{j} \times \text{Softmax}(\mathbf{QK}^T_{i,j}) \text{ for multiple } i, j \text{ iterations}

ile sijas^a_{ij} ve sijbs^b_{ij} her ii ve jj .

Lütfen dikkat edin ki Flash Attention'ın tamamı biraz daha karmaşıktır ve bu kılavuzun kapsamı dışında olduğu için burada büyük ölçüde basitleştirilmiştir. Daha fazla ayrıntı için okuyucular, iyi yazılmış Flash Attention makalesine göz atmaya davet edilir.

Buradaki ana çıkarım şudur:

softmax normalizasyon istatistiklerini takip ederek ve bazı akıllı matematiksel işlemler kullanarak Flash Attention, varsayılan self-attention katmanına kıyasla sayısal olarak aynı çıkışları, yalnızca NN .

Formüle bakıldığında, daha fazla hesaplama yapılması gerektiği için Flash Attention'ın varsayılan öz-dikkat formülüne kıyasla çok daha yavaş olması gerektiği sezgisel olarak söylenebilir. Gerçekten de Flash Attention, softmax normalizasyon istatistiklerinin sürekli olarak yeniden hesaplanması gerektiğinden, normal dikkate kıyasla daha fazla FLOP gerektirir (ilgiliyseniz daha fazla ayrıntı için makaleye bakın)

Ancak Flash Attention, GPU'nun daha yavaş, yüksek bant genişlikli belleğinin (VRAM) taleplerini önemli ölçüde azaltma yeteneğinden kaynaklanan bir avantajla, varsayılan dikkate kıyasla çıkarımda çok daha hızlıdır; bunun yerine daha hızlı çip üstü belleğe (SRAM) odaklanır.

Temel olarak Flash Attention, tüm ara yazma ve okuma işlemlerinin, çıkış vektörünü hesaplamak için daha yavaş VRAM belleğine erişmek yerine hızlı çip üzerindeki SRAM belleği kullanılarak yapılmasını sağlar O\mathbf{O} .

Uygulamada, Flash Attention kullanılabilirse kullanmamanın şu anda hiçbir geçerli nedeni yoktur. Algoritma matematiksel olarak aynı çıktıları verir ve hem daha hızlı hem de daha bellek verimlidir.

3. Mimari Yenilikler

Şimdiye kadar hesaplama ve bellek verimliliğini artırmak için şunlara baktık:

  • Ağırlıkları daha düşük bir hassasiyet biçimine dönüştürme
  • Kendine-dikkat algoritmasının daha az bellek ve hesaplama gerektiren bir sürümüyle değiştirilmesi

Şimdi, uzun metin girdileri gerektiren görevler için bir LLM'in mimarisini nasıl en etkili ve verimli şekilde değiştirebileceğimize bakalım, örneğin:

  • Bilgi kurtarma destekli soru yanıtlama,
  • Özetleme,
  • Sohbet

chat'in yalnızca LLM'in uzun metin girdilerini işlemesini değil, aynı zamanda kullanıcı ile asistan arasındaki (ChatGPT gibi) ileri-geri diyaloğu verimli bir şekilde yönetebilmesini de gerektirdiğini unutmayın.

Eğitim tamamlandıktan sonra temel LLM mimarisini değiştirmek zordur; bu nedenle, LLM’nin görevleri hakkında önceden değerlendirmeler yapmak ve model mimarisini buna göre optimize etmek önemlidir. Model mimarisinin, büyük giriş dizileri için hızla bellek ve/veya performans darboğazına dönüşen iki önemli bileşeni vardır.

  • Konumsal gömme
  • Anahtar-değer önbelleği

Her bileşeni daha ayrıntılı inceleyelim

3.1 LLM'lerde konumsal gömme iyileştirmeleri

Self-attention, her token'i diğer token'lara ilişkilendirir. Örneğin, Softmax(QKT)\text{Softmax}(\mathbf{QK}^T) metin giriş dizisinin ““Merhaba”, “Ben”, “seni”, “seviyorum” matrisi aşağıdaki gibi görünebilir:

Her kelime jetonu, diğer tüm kelime jetonlarına dikkat etme olasılık kütlesine sahip olur ve bu nedenle diğer tüm kelime jetonlarıyla ilişkilendirilir. Örneğin “love” kelimesi, “Hello” kelimesine %5, “I” kelimesine %30 ve kendisine %65 oranında dikkat eder.

Konum gömme (position embedding) içermeyen, öz-dikkate (self-attention) dayalı bir LLM, metin girdilerinin birbirine göre konumlarını anlamakta büyük zorluklar yaşardı. Bunun nedeni, QKT\mathbf{QK}^T ifadesinin, her kelime belirtecini (token), birbirine göre konumsal uzaklıklarından bağımsız olarak, O(1)O(1) hesaplamada diğer her kelime belirteciyle ilişkilendirmesidir. Dolayısıyla, konum gömme içermeyen bir LLM'de her belirtecin diğer tüm belirteçlere olan uzaklığı aynı görünür; e.g. örneğin “"Hello I love you" ile “"You love I hello" arasındaki farkı ayırt etmek çok zor olurdu.

LLM'in cümle sırasını anlayabilmesi için ek bir ipucu gerekir ve bu genellikle konumsal kodlamalar (veya konumsal gömme olarak da adlandırılır) biçiminde uygulanır. Konumsal kodlamalar, her token'ın konumunu, LLM'in cümle sırasını daha iyi anlayabilmesi için kullanabileceği sayısal bir sunuma kodlar.

Makalenin yazarları Attention Is All You Need sinüsoidal konumsal gömme (positional embedding) yöntemini tanıttı: P=p1,…,pN\mathbf{P} = \mathbf{p}_1, \ldots, \mathbf{p}_N . burada her bir vektör pi\mathbf{p}_i konumunun sinüsoidal bir fonksiyonu olarak hesaplanır: ii . Konumsal kodlamalar, ardından girdi dizisi vektörlerine doğrudan eklenir: X^=x^1,…,x^N\mathbf{\hat{X}} = \mathbf{\hat{x}}_1, \ldots, \mathbf{\hat{x}}_N = x1+p1,…,xN+pN\mathbf{x}_1 + \mathbf{p}_1, \ldots, \mathbf{x}_N + \mathbf{p}_N bu sayede modele cümle sırasını daha iyi öğrenmesi için ipucu verilir.

Sabit konum gömme yerine, diğerleri (örneğin Devlin ve ark.) eğitim sırasında öğrenilen konum gömme P\mathbf{P} içeren öğrenilmiş konum kodlamaları kullandı.

Cümle sırasını LLM'lere kodlamak için kullanılan baskın yöntemler sinüsoidal ve öğrenilmiş konum gömmelemeleriydi, ancak bu konum kodlamalarıyla ilgili birkaç sorun tespit edildi:

  1. Sinüsoidal ve öğrenilen konum gömmeleleri (position embeddings) her ikisi de mutlak konum gömmeleleridir (absolute positional embeddings), i.e. yani her konut kimliği için benzersiz bir gömme kodlar: 0,…,N0, \ldots, N . Gösterildiği üzere Huang ve ark. ve Su ve ark., mutlak konum gömmeleleri, uzun metin girdileri için LLM performansını düşürür. Uzun metin girdileri için, modelin girdi token'larının birbirine göre mutlak konumları yerine göreli konumsal mesafelerini öğrenmesi avantajlıdır.
  2. Öğrenmiş konum gömme (learned position embeddings) kullanıldığında, LLM'in sabit bir girdi uzunluğu olan $N$ üzerinde eğitilmesi gerekir; bu da modelin, üzerinde eğitildiği uzunluktan daha uzun girdi uzunluklarına genelleştirilmesini (extrapolate) zorlaştırır.

Son zamanlarda, yukarıda bahsedilen sorunlarla başa çıkabilen göreli konumsal gömme yöntemleri daha popüler hale gelmiştir; bunların en dikkat çekicileri şunlardır:

Her ikisi de RoPE ve ALiBi kelime token'larının birbirleriyle ilişkilendirildiği yer olan self-attention algoritmasında cümle sırasının LLM'e doğrudan işaret edilmesinin en iyi yöntem olduğunu savunur. Daha spesifik olarak, cümle sırası QKT\mathbf{QK}^T hesaplamasını değiştirerek işaret edilmelidir.

Çok fazla detaya girmeden, RoPE konum bilgisinin sorgu-anahtar çiftlerine kodlanabileceğini belirtir, e.g. qi\mathbf{q}_i ve xj\mathbf{x}_j her vektörü bir açıyla döndürerek θ∗i\theta * i ve θ∗j\theta * j sırasıyla i,ji, j her vektörün cümle konumunu tanımlayan: q^iTx^j=qiTRθ,i−jxj. \mathbf{\hat{q}}_i^T \mathbf{\hat{x}}_j = \mathbf{{q}}_i^T \mathbf{R}_{\theta, i -j} \mathbf{{x}}_j.

Rθ,i−j\mathbf{R}_{\theta, i - j} bu sayede bir döndürme matrisini temsil eder. θ\theta öğrenme sırasında öğrenilmez, bunun yerine eğitim sırasında maksimum girdi dizisi uzunluğuna bağlı olarak önceden tanımlanmış bir değere ayarlanır. değil öğrenme sırasında öğrenilmez, bunun yerine eğitim sırasında maksimum girdi dizisi uzunluğuna bağlı olarak önceden tanımlanmış bir değere ayarlanır.

Bu sayede, qi\mathbf{q}_i ve qj\mathbf{q}_j arasındaki olasılık puanı yalnızca i≠ji \ne j olduğunda etkilenir ve tamamen göreli mesafeye i−ji - j bağlıdır, vektörlerin ii ve jj .

RoPE, günümüzün en önemli LLM'lerinden birkaçında kullanılmaktadır:

Alternatif olarak, ALiBi çok daha basit bir göreli konum kodlama şeması önerir. Girdi token'larının birbirine göreli uzaklığı, önceden tanımlanmış bir değerle ölçeklendirilmiş negatif bir tamsayı olarak m her sorgu-anahtar girdisine eklenir QKT\mathbf{QK}^T matrisine, softmax hesabından hemen önce.

ALiBi makalesinde gösterildiği üzere, bu basit göreli konumsal kodlama, modelin çok uzun metin giriş dizilerinde bile yüksek performansını korumasını sağlar.

ALiBi, şu anki en önemli LLM'lerin birçoğunda kullanılmaktadır, örneğin:

Her ikisi de RoPE ve ALiBi konum kodlamaları, eğitim sırasında görülmemiş giriş uzunluklarına ekstrapole edilebilir; ancak ekstrapolasyonun ALiBi için RoPE. ALiBi için alt üçgenli konum matrisinin değerleri, giriş dizisinin uzunluğuna uydurmak üzere basitçe artırılır. RoPE, aynı θ\theta eğitim sırasında kullanılan c.f Press ve ark.. Ancak topluluk, $\theta$ değerini uyarlayarak RoPE konum gömme işlemlerinin ekstrapole edilmiş metin giriş dizileri için iyi çalışmasını sağlayan birkaç etkili yöntemi keşfetti (bkz. burası).

RoPE ve ALiBi, eğitim sırasında öğrenilmeyen, bunun yerine aşağıdaki sezgilere dayanan göreli konumsal gömmedir:

  • Metin girdileriyle ilgili konumsal ipuçları doğrudan QKT\mathbf{QK}^T matrisine, öz-dikkat katmanının
  • LLM, sabit bir göreli mesafe konumsal kodlamayı öğrenmeye teşvik edilmelidir.
  • Girdi token'ları birbirinden ne kadar uzaksa, sorgu-değer olasılıklarının da o kadar düşük olma olasılığı artar. Hem RoPE hem de ALiBi, birbirinden uzak token'ların sorgu-anahtar olasılığını düşürür. RoPE, sorgu-anahtar vektörleri arasındaki açıyı artırarak vektörel çarpımlarını azaltmak suretiyle bu olasılığı düşürür. ALiBi ise vektörel çarpıma büyük negatif sayılar ekleyerek düşürür.

Sonuç olarak, büyük metin girdilerini işlemeyi gerektiren görevlerde dağıtılmak üzere tasarlanan LLM'ler, RoPE ve ALiBi gibi göreli konumsal gömme (positional embedding) yöntemleriyle eğitildiğinde daha iyi performans gösterir. Ayrıca, RoPE ve ALiBi ile eğitilmiş bir LLM yalnızca örneğin N1=2048N_1 = 2048 gibi sabit bir uzunlukta eğitim almış olsa bile, konumsal gömme dışa vurum (extrapolation) yoluyla $N_1$'den çok daha büyük metin girdileriyle, örneğin $N_2 = 8192 > N_1$ gibi, pratikte kullanılabilir.

3.2 Anahtar-değer önbelleği

LLM'lerle otomatik regresif metin üretimi, bir giriş dizisini iteratif olarak işleyip bir sonraki token'ı örneklemeyi, bu token'ı giriş dizisine eklemeyi ve LLM, üretimin bittiğini gösteren bir token üretecek kadar bu işlemi sürdürmeyi içerir.

Otomatik regresif üretimin nasıl çalıştığına dair daha görsel bir açıklama için Transformer’s Generate Text Tutorial sayfasına göz atın.

Otomatik regresif (auto-regressive) çalışmanın pratikte nasıl işlediğini göstermek için hızlı bir kod parçası çalıştıralım. En olası bir sonraki token'ı torch.argmax ile alacağız.

input_ids = tokenizer(prompt, return_tensors="pt")["input_ids"].to(model.device)

for _ in range(5):
  next_logits = model(input_ids)["logits"][:, -1:]
  next_token_id = torch.argmax(next_logits,dim=-1)

  input_ids = torch.cat([input_ids, next_token_id], dim=-1)
  print("shape of input_ids", input_ids.shape)

generated_text = tokenizer.batch_decode(input_ids[:, -5:])
generated_text

Çıktı:

shape of input_ids torch.Size([1, 21])
shape of input_ids torch.Size([1, 22])
shape of input_ids torch.Size([1, 23])
shape of input_ids torch.Size([1, 24])
shape of input_ids torch.Size([1, 25])
[' Here is a Python function']

Gördüğümüz gibi, her seferinde metin girdi token'larını az önce örneklenen token ile artırıyoruz.

Çok az istisna dışında, LLM'ler neden dil modelleme hedefi kullanılarak eğitilir ve bu nedenle dikkat puanı matrisinin üst üçgenini maskeleme işlemine tabi tutar - bu yüzden yukarıdaki iki şemada dikkat puanları boş bırakılmıştır (bkz olasılık 0'dır). Neden dil modelleme hakkında kısa bir özet için Illustrated Self Attention blog'una başvurabilirsiniz.

Sonuç olarak, tokenlar asla sonraki tokenlara bağlı değildir; daha spesifik olarak qi\mathbf{q}_i vektörü, $j > i$ ise hiçbir anahtar veya değer vektörüyle kj,vj\mathbf{k}_j, \mathbf{v}_j ilişkilendirilmez. Bunun yerine qi\mathbf{q}_i yalnızca önceki anahtar-değer vektörlerine $\mathbf{k}{m < i}, \mathbf{v}{m < i} \text{ , for } m \in {0, \ldots i - 1}$ odaklanır. Gereksiz hesaplamaları azaltmak için, her katmanın tüm önceki zaman adımlarındaki anahtar-değer vektörleri önbelleğe alınabilir.

Aşağıda, LLM'ye her ileri geçişte anahtar-değer önbelleğini alıp ileterek bundan yararlanmasını söyleyeceğiz. Transformers'ta, use_cache bayrağını forward çağrısına geçirerek anahtar-değer önbelleğini alabilir ve ardından mevcut token ile birlikte iletebiliriz.

past_key_values = None # past_key_values is the key-value cache
generated_tokens = []
next_token_id = tokenizer(prompt, return_tensors="pt")["input_ids"].to(model.device)

for _ in range(5):
  next_logits, past_key_values = model(next_token_id, past_key_values=past_key_values, use_cache=True).to_tuple()
  next_logits = next_logits[:, -1:]
  next_token_id = torch.argmax(next_logits, dim=-1)

  print("shape of input_ids", next_token_id.shape)
  print("length of key-value cache", past_key_values.get_seq_length())  # past_key_values are of shape [num_layers, 0 for k, 1 for v, batch_size, length, hidden_dim]
  generated_tokens.append(next_token_id.item())

generated_text = tokenizer.batch_decode(generated_tokens)
generated_text

Çıktı:

shape of input_ids torch.Size([1, 1])
length of key-value cache 20
shape of input_ids torch.Size([1, 1])
length of key-value cache 21
shape of input_ids torch.Size([1, 1])
length of key-value cache 22
shape of input_ids torch.Size([1, 1])
length of key-value cache 23
shape of input_ids torch.Size([1, 1])
length of key-value cache 24
[' Here', ' is', ' a', ' Python', ' function']

Görüldüğü üzere, anahtar-değer önbelleği kullanıldığında metin girdi token'ları uzunluk olarak artmaz, tek bir girdi vektörü olarak kalır. Öte yandan, anahtar-değer önbelleğinin uzunluğu her çözümleme adımında bir artırılır.

Anahtar-değer önbelleğinin kullanılması, QKT\mathbf{QK}^T ifadesini temelde qcKT\mathbf{q}_c\mathbf{K}^T şeklinde indirger; burada qc\mathbf{q}_c geçerli girdi token'ının sorgu projeksiyonudur ve her zaman yalnızca tek bir vektördür.

Anahtar-değer önbelleğini kullanmanın iki avantajı vardır:

  • tam olarak hesaplanmasına kıyasla daha az hesaplama yapıldığı için hesaplama verimliliğinde önemli bir artış QKT\mathbf{QK}^T
  • Gerekli maksimum bellek, üretilen token sayısı ile karesel olarak artmaz, yalnızca doğrusal olarak artar.

Key-value önbelleğinden her zaman yararlanılmalıdır; bu, aynı sonuçları verir ve daha uzun giriş dizileri için önemli bir hızlanma sağlar. Transformers, metin hattı veya generate metodu kullanıldığında key-value önbelleğini varsayılan olarak etkinleştirir. Önbelleklere adanmış kapsamlı bir rehberimiz burada bulunmaktadır.

Anahtar-değer önbellekleri kullanmanızı önermemize rağmen, bunları kullandığınızda LLM çıktınızın hafifçe farklı olabileceğini unutmayın. Bu, matris çarpma çekirdeklerinin (kernels) kendilerine özgü bir özelliktir — bunun hakkında daha fazla bilgiyi burada okuyabilirsiniz.

3.2.1 Çok turlu sohbet

Anahtar-değer önbelleği, otomatik-geri dönüşlü çözümleme için birden fazla geçiş gerektiren sohbet gibi uygulamalarda özellikle faydalıdır. Bir örneğe bakalım.

User: How many people live in France?
Assistant: Roughly 75 million people live in France
User: And how many are in Germany?
Assistant: Germany has ca. 81 million inhabitants

Bu sohbette, LLM otoregresif çözmeyi iki kez çalıştırır:

  1. İlk seferde anahtar-değer önbelleği boş olup girdi istemi "User: How many people live in France?" şeklindedir ve model, her çözümleme adımında anahtar-değer önbelleğini büyütürken "Roughly 75 million people live in France" metnini otoregresif olarak üretir.
  2. Girdi isteminin ikinci kez "User: How many people live in France? \n Assistant: Roughly 75 million people live in France \n User: And how many in Germany?" olduğu durumda, önbellek sayesinde ilk iki cümle için tüm anahtar-değer vektörleri zaten hesaplanmıştır. Bu nedenle girdi isteği yalnızca "User: And how many in Germany?" ile oluşur. Kısaltılmış girdi isteği işlenirken, hesaplanan anahtar-değer vektörleri birinci çözümleme (decoding) aşamasının anahtar-değer önbelleğine eklenir. İkinci Asistan yanıtı olan "Germany has ca. 81 million inhabitants", ardından "User: How many people live in France? \n Assistant: Roughly 75 million people live in France \n User: And how many are in Germany?" metninin kodlanmış anahtar-değer vektörlerinden oluşan anahtar-değer önbelleği kullanılarak otoregresif olarak üretilir.

Burada iki noktaya dikkat edilmelidir:

  1. Sohbet ortamında dağıtılan LLM'ler için tüm bağlamı korumak, LLM'in konuşmanın önceki tüm bağlamını anlaması açısından hayati önem taşır. Örneğin, yukarıdaki örnekte LLM, "And how many are in Germany" diye sorduğunda kullanıcının nüfustan bahsettiğini anlamalıdır.
  2. Anahtar-değer önbelleği, sohbet için son derece faydalıdır; çünkü sohbet geçmişini baştan yeniden kodlamak zorunda kalmak yerine (örneğin bir kodlayıcı-çözücü mimarisi kullanıldığında olduğu gibi) kodlanmış sohbet geçmişini sürekli olarak büyütmemizi sağlar.

transformers içinde, generate çağrısı, varsayılan use_cache=True parametresine ek olarak return_dict_in_generate=True geçirildiğinde past_key_values değerini döndürür. Bunun henüz pipeline arayüzü üzerinden kullanılamadığını unutmayın.

# Generation as usual
prompt = system_prompt + "Question: Please write a function in Python that transforms bytes to Giga bytes.\n\nAnswer: Here"
model_inputs = tokenizer(prompt, return_tensors='pt')
generation_output = model.generate(**model_inputs, max_new_tokens=60, return_dict_in_generate=True)
decoded_output = tokenizer.batch_decode(generation_output.sequences)[0]

# Piping the returned `past_key_values` to speed up the next conversation round
prompt = decoded_output + "\nQuestion: How can I modify the function above to return Mega bytes instead?\n\nAnswer: Here"
model_inputs = tokenizer(prompt, return_tensors='pt')
generation_output = model.generate(
  **model_inputs,
  past_key_values=generation_output.past_key_values,
  max_new_tokens=60,
  return_dict_in_generate=True
)
tokenizer.batch_decode(generation_output.sequences)[0][len(prompt):]

Çıktı:

 is a modified version of the function that returns Mega bytes instead.

def bytes_to_megabytes(bytes):
   return bytes / 1024 / 1024

Answer: The function takes a number of bytes as input and returns the number of

Harika, dikkat katmanı için aynı anahtar ve değerlerin yeniden hesaplanmasına ek zaman harcanmıyor! Ancak bir pürüz var. QKT\mathbf{QK}^T matrisi için gereken tepe bellek önemli ölçüde azaltılsa da, uzun giriş dizileri veya çok turlu sohbetlerde anahtar-değer önbelleğini bellekte tutmak çok yüksek bellek maliyetine yol açabilir. Anahtar-değer önbelleğinin, tüm önceki giriş vektörleri için xi, for i∈{1,…,c−1}\mathbf{x}_i \text{, for } i \in \{1, \ldots, c - 1\} tüm öz-dikkat katmanları ve tüm dikkat başlıkları için anahtar-değer vektörlerini depolaması gerektiğini unutmayın.

Daha önce kullandığımız bigcode/octocoder LLM'i için anahtar-değer önbelleğinde saklanması gereken float değer sayısını hesaplayalım. Float değer sayısı, dizi uzunluğunun iki katı ile dikkat başlarının sayısının, dikkat başı boyutunun ve katman sayısının çarpımına eşittir. Bu hesabı, varsayımsal bir 16000 giriş dizisi uzunluğu için LLM'imizde yaparsak:

config = model.config
2 * 16_000 * config.n_layer * config.n_head * config.n_embd // config.n_head

Çıktı:

7864320000

Yaklaşık 8 milyar float değeri! 8 milyar float değeri float16 hassasiyetinde saklanmak için yaklaşık 15 GB RAM gerektirir, bu da model ağırlıklarının kendisinin yaklaşık yarısı kadardır! Araştırmacılar, anahtar-değer önbelleğinin saklanmasının bellek maliyetini önemli ölçüde azaltan iki yöntem önermiştir; bu yöntemler alt bölümlerde ele alınmaktadır.

3.2.2 Çoklu Sorgu Dikkati (MQA)

Multi-Query-Attention, Noam Shazeer’in Fast Transformer Decoding: One Write-Head is All You Need makalesinde önerilmiştir. Başlığın da belirttiği gibi Noam, n_head adet anahtar-değer projeksiyon ağırlığı yerine, tüm dikkat başlıkları arasında paylaşılan tek bir anahtar-değer projeksiyon ağırlığı çiftinin kullanılmasının, model performansını önemli ölçüde düşürmeden mümkün olduğunu keşfetmiştir.

Tek bir head-value projeksiyon ağırlık çifti kullanarak, anahtar-değer vektörleri ki,vi\mathbf{k}_i, \mathbf{v}_i tüm dikkat başlarında aynı olmak zorundadır; bu da önbellekte n_head adet yerine yalnızca 1 anahtar-değer projeksiyon çifti saklamamız gerektiği anlamına gelir.

Çoğu LLM 20 ile 100 dikkat başlığı arasında kullanıldığından, MQA anahtar-değer önbelleğinin bellek tüketimini önemli ölçüde azaltır. Bu defterde kullanılan LLM için, 16000 giriş dizisi uzunluğunda gerekli bellek tüketimini 15 GB'dan 400 MB'ın altına düşürebiliriz.

Bellek tasarrufunun yanı sıra, MQA aşağıda açıklandığı gibi hesaplama verimliliğinin de artmasını sağlar. Otoregresif çözümlendirmede, büyük anahtar-değer vektörleri, mevcut anahtar-değer vektör çiftiyle birleştirilip ardından qcKT\mathbf{q}_c\mathbf{K}^T hesaplamasına beslenmek üzere her adımda yeniden yüklenmelidir. Otoregresif çözümlendirmede, sürekli yeniden yükleme için gereken bellek bant genişliği ciddi bir zaman darboğazına dönüşebilir. Anahtar-değer vektörlerinin boyutunu azaltarak daha az belleğe erişim gerekir, bu da bellek bant genişliği darboğazını azaltır. Daha fazla ayrıntı için Noam'ın makalesine.

Burada anlaşılması gereken önemli nokta, anahtar-değer dikkat başlarının sayısını 1'e indirmenin yalnızca bir anahtar-değer önbelleği kullanıldığında mantıklı olmasıdır. Anahtar-değer önbelleği olmadan tek bir ileri geçişte modelin tepe bellek tüketimi değişmez; çünkü her dikkat başının benzersiz bir sorgu vektörü olduğundan, her dikkat başının farklı bir QKT\mathbf{QK}^T matrisi vardır.

MQA, topluluk tarafından yaygın olarak benimsenmiş olup şu anda en popüler LLM'lerin çoğu tarafından kullanılmaktadır:

Ayrıca, bu defterde kullanılan kontrol noktası - bigcode/octocoder - MQA kullanmaktadır.

3.2.3 Gruplu Sorgu Dikkati (GQA)

Google'dan Ainslie ve arkadının önerdiği Grouped-Query-Attention, MQA kullanımının, standart çoklu anahtar-değer başlığı projeksiyonlarına kıyasla sıklıkla kalite düşüşüne yol açabildiğini tespit etmiştir. Makale, sorgu başlığı projeksiyon ağırlıklarının sayısını daha az radikal bir şekilde azaltarak daha fazla model performansının korunabileceğini savunmaktadır. Tek bir anahtar-değer projeksiyon ağırlığı kullanmak yerine, n < n_head anahtar-değer projeksiyon ağırlığı kullanılmalıdır. n değeri, n_head'ten önemli ölçüde daha küçük bir değer olarak, örneğin 2, 4 veya 8 seçildiğinde, MQA'dan elde edilen neredeyse tüm bellek ve hız kazanımları korunurken daha az model kapasitesi feda edilir ve bu da muhtemelen daha az performans kaybına yol açar.

Bununla birlikte, GQA'nın yazarları, mevcut model kontrol noktalarının, orijinal ön eğitim hesaplama gücünün yalnızca %5'i kadar bir maliyetle GQA mimarisine sahip olacak şekilde uptrained (üst eğitimden geçirilebilir) olduğunu buldu. Orijinal ön eğitim hesaplama gücünün %5'i hâlâ büyük bir miktar olsa da, GQA uptraining (üst eğitimi) mevcut kontrol noktalarının daha uzun giriş dizileri için faydalı kalmasını sağlar.

GQA yalnızca yakın zamanda önerildiğinden, bu not defterinin yazıldığı sırada benimsenmesi daha sınırlıydı. GQA'nın en dikkat çekici uygulaması Llama-v2'dir.

Sonuç olarak, LLM otomatik-geri dönüşlü (auto-regressive) çözümleme ile dağıtılıyorsa ve sohbet gibi durumlarda olduğu gibi büyük giriş dizilerini işlemek zorundaysa, GQA veya MQA'dan yararlanılması şiddetle önerilir.

Sonuç

Araştırma topluluğu, giderek daha büyük LLM'ler için çıkarım süresini hızlandıran yeni ve pratik yöntemler üretmeye devam etmektedir. Bunun bir örneği, “kolay token”ların daha küçük ve hızlı dil modelleri tarafından üretilip yalnızca “zor token”ların LLM'in kendisi tarafından üretilmesini sağlayan speculative decoding gibi umut verici bir araştırma yönüdür. Konuya daha derinlemesine girmek bu notebook'un kapsamı dışında kalmakla birlikte, bu güzel blog yazısında hakkında daha fazla bilgi edinilebilir.

GPT3/4, Llama-2-70b, Claude, PaLM gibi devasa LLM'lerin Hugging Face Chat veya ChatGPT gibi sohbet arayüzlerinde bu kadar hızlı çalışabilmesinin büyük ölçüde nedeni, yukarıda bahsedilen hassasiyet, algoritma ve mimari iyileştirmeleridir. İlerleyen süreçte GPU, TPU gibi hızlandırıcılar giderek daha da hızlanacak ve daha fazla bellek imkânı sunacaktır; ancak yine de en iyi performansı elde etmek için her zaman mevcut en iyi algoritmaların ve mimarilerin kullanıldığından emin olunmalıdır 🤗

Update on GitHub