# تحسين نماذج اللغة الكبيرة من حيث السرعة والذاكرة

تتقدم نماذج اللغة الكبيرة (LLMs) مثل GPT3/4 و[Falcon](https://huggingface.co/tiiuae/falcon-40b) و[Llama](https://huggingface.co/meta-llama/Llama-2-70b-hf) بسرعة في قدرتها على التعامل مع المهام المرتبطة بالبشر، مما يجعلها أدوات أساسية في الصناعات المعاصرة القائمة على المعرفة. ومع ذلك، لا يزال نشر هذه النماذج في مهام العالم الحقيقي تحدياً:

- لتحقيق قدرات فهم ونصّ توليد تقترب من القدرات البشرية، تتطلب النماذج اللغوية الكبيرة حالياً أن تتألف من مليارات المعاملات (انظر [Kaplan et al](https://huggingface.co/papers/2001.08361)، [Wei et. al](https://huggingface.co/papers/2206.07682)). وهذا يؤدي بالتالي إلى تضخيم متطلبات الذاكرة للاستدلال.
- في العديد من المهام الواقعية، يجب تزويد نماذج اللغة الكبيرة بمعلومات سياقية شاملة. وهذا يستلزم قدرة النموذج على إدارة تسلسلات إدخال طويلة جداً أثناء الاستدلال.

تكمن جوهر هذه التحديات في تعزيز القدرات الحسابية والذاكرة الخاصة بنماذج اللغة الكبيرة، لا سيما عند التعامل مع تسلسلات مدخلات واسعة النطاق.

في هذا الدليل، سنستعرض التقنيات الفعّالة لنشر نماذج اللغة الكبيرة (LLM) بكفاءة:

1. **دقة أقل:** أظهرت الأبحاث أن العمل بدقة رقمية مخفّضة، أي [8-bit و4-bit](./main_classes/quantization)، يمكن أن يحقق مزايا حسابية دون انخفاض ملحوظ في أداء النموذج.

2. **Flash Attention:** يُعدّ Flash Attention تنويعًا من خوارزمية الانتباه لا يوفر نهجًا أكثر كفاءة في استخدام الذاكرة فحسب، بل يحقق أيضًا كفاءة أعلى بفضل الاستخدام المُحسَّن لذاكرة وحدة معالجة الرسوميات (GPU).

3. **ابتكارات معمارية:** نظرًا إلى أن نماذج اللغة الكبيرة تُنشر دائمًا بالطريقة نفسها أثناء الاستدلال، أي توليد النص التلقائي مع سياق إدخال طويل، فقد طُرحت معماريات نماذج متخصصة تتيح استدلالًا أكثر كفاءة. وأهم التطورات في معماريات النماذج في هذا الصدد هي [Alibi](https://huggingface.co/papers/2108.12409) و[Rotary embeddings](https://huggingface.co/papers/2104.09864) و[Multi-Query Attention (MQA)](https://huggingface.co/papers/1911.02150) و[Grouped-Query-Attention (GQA)](https://huggingface.co/papers/2305.13245).

على مدار هذا الدليل، سنقدّم تحليلاً لتوليد النصوص التلقائي (auto-regressive) من منظور المصفوفات (tensors). سنتعمق في إيجابيات وسلبيات اعتماد دقة أقل، ونقدّم استكشافاً شاملاً لأحدث خوارزميات الانتباه (attention)، ونناقش معماريات نماذج اللغة الكبيرة (LLM) المحسّنة. وفي أثناء ذلك، سننفّذ أمثلة عملية تُبرز كل تحسين من تحسينات الميزات.

## 1. دقة أقل

يمكن فهم متطلبات الذاكرة الخاصة بنماذج اللغة الكبيرة (LLMs) بشكل أفضل من خلال النظر إلى النموذج على أنه مجموعة من مصفوفات الأوزان والمتجهات، والنصوص المدخلة على أنها تسلسل من المتجهات. في ما يلي، سيُستخدم المصطلح *weights* للإشارة إلى جميع مصفوفات الأوزان والمتجهات الخاصة بالنموذج.

عند كتابة هذا الدليل، تتألف نماذج اللغة الكبيرة (LLMs) من بضع مليارات من المعاملات على الأقل. ويتكون كل معامل من رقم عشري، مثل `4.5689`، والذي يُخزَّن عادةً إما بتنسيق [float32](https://en.wikipedia.org/wiki/Single-precision_floating-point_format) أو [bfloat16](https://en.wikipedia.org/wiki/Bfloat16_floating-point_format) أو [float16](https://en.wikipedia.org/wiki/Half-precision_floating-point_format). وهذا يتيح لنا حساب المتطلبات الذاكرة اللازمة لتحميل نموذج اللغة الكبيرة في الذاكرة بسهولة:

> *تحميل أوزان نموذج يحتوي على X مليار معامل يتطلب تقريباً 4 \* X جيجابايت من ذاكرة الفيديو (VRAM) بدقة float32*

ومع ذلك، نادراً ما تُدرَّب النماذج اليوم بدقة float32 الكاملة، بل عادةً بدقة bfloat16 أو، بشكل أقل شيوعاً، بدقة float16. لذلك تصبح القاعدة العملية:

> *تحميل أوزان نموذج يحتوي على X مليار معامل يتطلب تقريباً 2 \* X GB من ذاكرة VRAM بدقة bfloat16/float16*

بالنسبة إلى مدخلات نصية أقصر (أقل من 1024 رمزًا)، فإن المتطلبات الذاكرة للاستدلال تتأثر بشكل كبير بالمتطلبات الذاكرة لتحميل الأوزان. لذلك، لنفترض حاليًا أن المتطلبات الذاكرة للاستدلال تساوي المتطلبات الذاكرة لتحميل النموذج في ذاكرة GPU VRAM.

لإعطاء بعض الأمثلة على مقدار ذاكرة الفيديو (VRAM) التي يستهلكها تحميل نموذج تقريباً باستخدام bfloat16:

- يتطلب **GPT3** ذاكرة فيديو (VRAM) بحجم 2 \* 175 غيغابايت = **350 غيغابايت**
- يتطلب [**Bloom**](https://huggingface.co/bigscience/bloom) ذاكرة فيديو (VRAM) بحجم 2 \* 176 جيجابايت = **352 جيجابايت**
- يتطلب [**Llama-2-70b**](https://huggingface.co/meta-llama/Llama-2-70b-hf) ذاكرة فيديو (VRAM) بحجم 2 \* 70 غيغابايت = **140 غيغابايت**
- يتطلب [**Falcon-40b**](https://huggingface.co/tiiuae/falcon-40b) ذاكرة فيديو (VRAM) بحجم 2 \* 40 جيجابايت = **80 جيجابايت**
- يتطلب [**MPT-30b**](https://huggingface.co/mosaicml/mpt-30b) ذاكرة فيديو (VRAM) بحجم 2 \* 30 غيغابايت = **60 غيغابايت**
- يتطلب [**bigcode/starcoder**](https://huggingface.co/bigcode/starcoder) ذاكرة فيديو (VRAM) بحجم 2 \* 15.5 = **31 GB**

حتى تاريخ كتابة هذا المستند، أكبر شريحة GPU في السوق هي A100 & H100 التي توفر 80GB من ذاكرة VRAM. تتطلب معظم النماذج المذكورة سابقاً أكثر من 80GB فقط لتحميلها، وبالتالي فهي تتطلب حتماً [التوازي على مستوى الموترات](./perf_train_gpu_many#tensor-parallelism) و/أو [التوازي على مستوى خط الأنابيب](./perf_train_gpu_many#pipeline-parallelism).

🤗 Transformers now supports tensor parallelism for supported models having `base_tp_plan` in their respective config classes. Learn more about Tensor Parallelism [here](./perf_train_gpu_many#tensor-parallelism). Furthermore, if you're interested in writing models in a tensor-parallelism-friendly way, feel free to have a look at [the text-generation-inference library](https://github.com/huggingface/text-generation-inference/tree/main/server/text_generation_server/models/custom_modeling).

يُتاح توازي خط الأنابيب البسيط (Naive pipeline parallelism) بشكل افتراضي. ولتحقيق ذلك، ما عليك سوى تحميل النموذج باستخدام `device="auto"`، حيث سيُوزّع الطبقات المختلفة تلقائيًا على وحدات معالجة الرسوميات المتاحة، كما هو موضح [هنا](https://huggingface.co/docs/accelerate/v0.22.0/en/concept_guides/big_model_inference). ومع ذلك، على الرغم من فاعليته الكبيرة، فإن هذا التوازي البسيط لا يعالج مشكلة خمول وحدات معالجة الرسوميات. وللتغلب على ذلك، يلزم استخدام توازي خط أنابيب متقدم، كما هو موضح [هنا](./perf_train_gpu_many#pipeline-parallelism).

إذا كان لديك وصول إلى عقدة 8 x 80GB A100، يمكنك تحميل BLOOM على النحو التالي

```bash
!pip install transformers accelerate bitsandbytes optimum
```

```python
from transformers import AutoModelForCausalLM

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

باستخدام `device_map="auto"`، يتم توزيع طبقات الانتباه بالتساوي على جميع وحدات معالجة الرسوميات المتاحة.

في هذا الدليل، سنستخدم [bigcode/octocoder](https://huggingface.co/bigcode/octocoder) لأنه يمكن تشغيله على شريحة جهاز GPU من نوع A100 بسعة 40 غيغابايت واحدة. لاحظ أن جميع تحسينات الذاكرة والسرعة التي سنطبقها لاحقًا تنطبق بنفس القدر على النماذج التي تتطلب توازياً على مستوى النموذج أو على مستوى الموترات.

بما أن النموذج محمّل بدقة bfloat16، فوفقًا لقاعدة الإبهام المذكورة أعلاه، نتوقع أن تكون متطلبات الذاكرة لتشغيل الاستدلال مع `bigcode/octocoder` حوالي 31 جيجابايت من ذاكرة VRAM. لنجرّب ذلك.

نبدأ أولاً بتحميل النموذج ومُقسِّم النص، ثم نمرر كلاهما إلى كائن [pipeline](./main_classes/pipelines) في مكتبة Transformers.

```python
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)
```

```python
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
```

**المخرجات**:

```text
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
```

رائع، يمكننا الآن استخدام النتيجة مباشرةً لتحويل البايتات إلى غيغابايت.

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

لنقم باستدعاء [`torch.accelerator.memory.max_memory_allocated`](https://docs.pytorch.org/docs/main/generated/torch.accelerator.memory.max_memory_allocated.html) لقياس ذروة تخصيص ذاكرة المعالج التسارعي.

```python
bytes_to_giga_bytes(torch.accelerator.max_memory_allocated())
```

**المخرجات**:

```text
29.0260648727417
```

قريبٌ بما يكفي من حسابنا التقريبي! يمكننا ملاحظة أن الرقم ليس دقيقًا تمامًا، إذ يتطلب الانتقال من البايتات إلى الكيلوبايتات ضربًا في 1024 وليس 1000. وبالتالي، يمكن فهم صيغة الحساب التقريبي أيضًا على أنها حساب "بحد أقصى X GB". لاحظ أنه لو حاولنا تشغيل النموذج بدقة float32 الكاملة، لكانت هناك حاجة إلى 64 GB من ذاكرة VRAM.

> تُدرَّب جميع النماذج تقريبًا اليوم بدقة bfloat16، ولا يوجد سبب لتشغيل النموذج بدقة float32 الكاملة إذا كان [وحدة معالجة الرسوميات لديك تدعم bfloat16](https://discuss.pytorch.org/t/bfloat16-native-support/117155/5). لن تمنحك دقة float32 نتائج استدلال أفضل من الدقة التي تم تدريب النموذج بها.

إذا كنت غير متأكد من الصيغة التي تُخزَّن بها أوزان النموذج على Hub، يمكنك دائمًا الاطلاع على إعدادات نقطة التفتيش (checkpoint) تحت `"dtype"`، *على سبيل المثال* [هنا](https://huggingface.co/meta-llama/Llama-2-7b-hf/blob/6fdf2e60f86ff2481f2241aaee459f85b5b0bbb9/config.json#L21). يُوصى بإعداد النموذج على نفس نوع الدقة المذكور في الإعدادات عند التحميل باستخدام `from_pretrained(..., dtype=...)`، باستثناء الحالة التي يكون فيها النوع الأصلي float32، حيث يمكن استخدام كل من `float16` أو `bfloat16` للاستدلال.

لنعرّف دالة `flush(...)` لتحرير جميع الذاكرة المخصصة، حتى نتمكن من قياس ذروة الذاكرة المخصصة للوحدة الرسومية بدقة.

```python
del pipe
del model

import gc
import torch

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

لندعُها الآن للتجربة التالية.

```python
flush()
```

من مكتبة Accelerate، يمكنك أيضاً استخدام طريقة أداة مستقلة عن الجهاز تُسمى [release_memory](https://github.com/huggingface/accelerate/blob/29be4788629b772a3b722076e433b5b3b5c85da3/src/accelerate/utils/memory.py#L63)، والتي تأخذ في الاعتبار مختلف واجهات الأجهزة الخلفية مثل XPU وMLU وNPU وMPS وغيرها.

```python
from accelerate.utils import release_memory
# ...

release_memory(model)
```

لكن ماذا لو لم يتوفر لديك 32 غيغابايت من ذاكرة VRAM؟ لقد تبيّن أنه يمكن تكميم أوزان النموذج إلى 8 بت أو 4 بت دون فقدان كبير في الأداء (انظر [Dettmers et al.](https://huggingface.co/papers/2208.07339)). كما يمكن تكميم النموذج إلى 3 أو 2 بت مع فقدان مقبول في الأداء، كما هو موضح في ورقة [GPTQ](https://huggingface.co/papers/2210.17323) الحديثة 🤯.

بدون الخوض في تفاصيل كثيرة، تهدف مخططات التكميم إلى تقليل دقة الأوزان مع السعي إلى الحفاظ على دقة نتائج الاستدلال للنموذج قدر الإمكان (*أي* بقربها من bfloat16). تجدر الإشارة إلى أن التكميم يعمل بشكل خاص في توليد النصوص، حيث لا يهمنا سوى اختيار *مجموعة الرموز التالية الأكثر احتمالاً* ولا نهتم فعلياً بالقيم الدقيقة لتوزيع *logit* للرمز التالي. كل ما يهم هو أن يبقى توزيع *logit* للرمز التالي متشابهاً تقريباً، بحيث تعطي عملية `argmax` أو `topk` نفس النتائج.

توجد تقنيات مختلفة للتكميم، لن نتناولها بالتفصيل هنا، ولكن بشكل عام، تعمل جميع تقنيات التكميم على النحو التالي:

- 1. قم بتكميم جميع الأوزان إلى الدقة المستهدفة
- 2. حمّل الأوزان المُكمّاة، ومرّر تسلسل مدخلات المتجهات بدقة bfloat16
- 3. فكّ التكميم الديناميكي للأوزان إلى bfloat16 لأداء الحساب مع متجهات الإدخال بدقة bfloat16

باختصار، يعني هذا أن عمليات ضرب *مصفوفة المدخلات بالأوزان*، حيث $X$ هي *المدخلات*، و$W$ هي مصفوفة الأوزان، و$Y$ هي المخرجات:

$$ Y = X * W $$

تُغيَّر إلى

$$ Y = X * \text{dequantize}(W) $$

لكل عملية ضرب مصفوفات. يتم تنفيذ إلغاء التكميم وإعادة التكميم بشكل تسلسلي لجميع مصفوفات الأوزان أثناء مرور المدخلات عبر شبكة الرسم البياني.

لذلك، غالبًا ما **لا** ينخفض زمن الاستدلال عند استخدام الأوزان المُكمّاة، بل يزداد. يكفي من النظرية، فلنجرّب! ولكي تُكمّم الأوزان باستخدام Transformers، يجب التأكد من تثبيت مكتبة [`bitsandbytes`](https://github.com/bitsandbytes-foundation/bitsandbytes).

```bash
!pip install bitsandbytes
```

يمكننا بعد ذلك تحميل النماذج بتكميم 8 بت عن طريق إضافة علم `load_in_8bit=True` إلى `from_pretrained` ببساطة.

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

الآن، لنشغّل مثالنا مرة أخرى ونقيس استهلاك الذاكرة.

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

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

**المخرجات**:

```text
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
```

رائع، لقد حصلنا على نفس النتيجة السابقة، لذا لا يوجد أي خسارة في الدقة! دعونا ننظر إلى مقدار الذاكرة المستخدمة هذه المرة.

```python
bytes_to_giga_bytes(torch.accelerator.max_memory_allocated())
```

**المخرجات**:

```text
15.219234466552734
```

أقل بكثير! انخفضنا إلى ما يزيد قليلاً عن 15 غيغابايت، وبالتالي يمكننا تشغيل هذا النموذج على وحدات معالجة رسومية مخصصة للمستهلكين مثل 4090. نشهد مكسبًا ممتازًا في كفاءة الذاكرة، مع عدم وجود تدهور يُذكر في مخرجات النموذج. ومع ذلك، يمكننا أيضًا ملاحظة تباطؤ طفيف أثناء الاستدلال.

نحذف النماذج ثم نُمسح الذاكرة مرة أخرى.

```python
del model
del pipe
```

```python
flush()
```

لننظر إلى مقدار ذروة استهلاك ذاكرة وحدة معالجة الرسوميات الذي توفره التكميم بدقة 4 بت. يمكن تكميم النموذج إلى 4 بت باستخدام نفس واجهة برمجة التطبيقات السابقة - هذه المرة عن طريق تمرير `load_in_4bit=True` بدلاً من `load_in_8bit=True`.

```python
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
```

**المخرجات**:

```text
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
```

نرى تقريبًا نفس نص المخرجات كما كان من قبل - حيث ينقص فقط `python` مباشرة قبل مقتطف الشيفرة. دعونا نرى مقدار الذاكرة المطلوبة.

```python
bytes_to_giga_bytes(torch.accelerator.max_memory_allocated())
```

**المخرجات**:

```text
9.543574333190918
```

فقط 9.5 غيغابايت! هذا ليس كثيرًا حقًا لنموذج يتجاوز 15 مليار معامل.

على الرغم من أننا نشهد تدهوراً ضئيلاً جداً في دقة نموذجنا هنا، إلا أن التكميم بدقة 4 بت قد يؤدي في الممارسة العملية غالباً إلى نتائج مختلفة مقارنةً بالتكميم بدقة 8 بت أو الاستدلال الكامل بدقة `bfloat16`. ويتوقف الأمر على المستخدم لتجربته.

يُشار أيضاً إلى أن الاستدلال هنا كان أبطأ قليلاً مرة أخرى مقارنةً بتكميم 8 بت، وذلك بسبب استخدام طريقة تكميم أكثر عدوانية لتكميم 4 بت، مما يؤدي إلى استهلاك وقت أطول لعمليتي $\text{quantize}$ و$\text{dequantize}$ أثناء الاستدلال.

```python
del model
del pipe
```

```python
flush()
```

بشكل عام، لاحظنا أن تشغيل OctoCoder بدقة 8 بت قلّل الذاكرة المخصصة للرسوميات (VRAM) المطلوبة على وحدة معالجة الرسوميات (GPU) من 32 غيغابايت إلى 15 غيغابايت فقط، وأن تشغيل النموذج بدقة 4 بت قلّل الذاكرة المطلوبة بشكل أكبر إلى ما يزيد قليلاً عن 9 غيغابايت.

تتيح عملية التكميم بدقة 4 بت تشغيل النموذج على وحدات معالجة رسومية مثل RTX3090 وV100 وT4، وهي وحدات في متناول معظم المستخدمين.

لمزيد من المعلومات حول التكميم وللاطلاع على كيفية تكميم النماذج بحيث تتطلب ذاكرة GPU VRAM أقل من 4 بت، ننصح بالاطلاع على تطبيق [`GPT-QModel`](./quantization/gptq).

> ختاماً، من المهم التذكير بأن تكميم النموذج يوازن بين تحسين كفاءة الذاكرة والدقة، وفي بعض الحالات زمن الاستدلال.

إذا لم يكن ذاكرة وحدة معالجة الرسوميات (GPU) قيدًا في حالتك، فغالبًا لا حاجة للبحث في التكميم. ومع ذلك، لا تستطيع العديد من وحدات معالجة الرسوميات تشغيل نماذج اللغة الكبيرة (LLMs) دون استخدام طرق التكميم، وفي هذه الحالة، تُعد مخططات التكميم بدقة 4 بت و8 بت أدوات مفيدة للغاية.

لمزيد من المعلومات التفصيلية حول الاستخدام، نوصي بشدة بالاطلاع على [وثائق تكميم Transformers](./quantization/overview). والآن، دعونا ننظر في كيفية تحسين الكفاءة الحسابية وكفاءة الذاكرة من خلال استخدام خوارزميات أفضل ومعمارية نموذج محسّنة.

## 2. Flash Attention

تشارك نماذج اللغة الكبيرة (LLMs) الأعلى أداءً اليوم، إلى حدٍّ كبير، في البنية الأساسية نفسها التي تتكوّن من طبقات تقدّم أمامي (feed-forward) وطبقات تفعيل (activation) وطبقات تطبيع الطبقة (layer normalization) والأهم من ذلك كله طبقات الانتباه الذاتي (self-attention).

تُعدّ طبقات الانتباه الذاتي (Self-attention) جوهرية في نماذج اللغة الكبيرة (LLMs) إذ تُمكّن النموذج من فهم العلاقات السياقية بين الرموز المدخلة. ومع ذلك، فإن ذروة استهلاك ذاكرة وحدة معالجة الرسوميات (GPU) لطبقات الانتباه الذاتي تنمو *تربيعياً* من حيث التعقيد الحسابي وتعقيد الذاكرة مع عدد الرموز المدخلة (المعروف أيضاً باسم *طول التسلسل*) والذي نرمّز له فيما يلي بـ $N$ . ورغم أن هذا الأمر لا يُلاحظ بشكل كبير في التسلسلات المدخلة الأقصر (حتى 1000 رمز مدخل)، فإنه يصبح مشكلة جسيمة في التسلسلات المدخلة الأطول (عند حوالي 16000 رمز مدخل).

لنلقِ نظرةً أقرب. الصيغةُ المستخدمةُ لحساب المخرَج $\mathbf{O}$ لطبقة الانتباه الذاتي لإدخال $\mathbf{X}$ بطول $N$ هي:

$$ \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} $$

$\mathbf{X} = (\mathbf{x}_1, ... \mathbf{x}_{N})$ هو بالتالي تسلسل الإدخال لطبقة الانتباه. ستتكون الإسقاطات $\mathbf{Q}$ و $\mathbf{K}$ كلٌّ منهما من $N$ متجه، مما يؤدي إلى أن يكون حجم $\mathbf{QK}^T$ هو $N^2$ .

عادةً ما تحتوي نماذج اللغة الكبيرة على عدة رؤوس انتباه، مما يؤدي إلى تنفيذ عمليات الانتباه الذاتي المتعددة بالتوازي. بافتراض أن النموذج يحتوي على 40 رأس انتباه ويعمل بدقة bfloat16، يمكننا حساب المتطلبات الذاكرة لتخزين مصفوفات $\mathbf{QK^T}$ لتكون $40 * 2 * N^2$ بايت. فعند $N=1000$، لا نحتاج سوى حوالي 50 ميغابايت من ذاكرة الفيديو، لكن عند $N=16000$ سنحتاج إلى 19 غيغابايت من ذاكرة الفيديو، وعند $N=100,000$ سنحتاج إلى ما يقرب من 1 تيرابايت فقط لتخزين مصفوفات $\mathbf{QK}^T$.

باختصار، يصبح خوارزمية الانتباه الذاتي الافتراضية مكلفة للغاية من حيث الذاكرة بسرعة عند التعامل مع سياقات إدخال كبيرة.

مع تحسّن نماذج اللغة الكبيرة في فهم النصوص وتوليدها، تُطبَّق على مهام متزايدة التعقيد. فبينما كانت النماذج في السابق تتعامل مع ترجمة أو تلخيص عدد قليل من الجمل، فإنها اليوم تتولى إدارة صفحات كاملة، مما يتطلب القدرة على معالجة مدخلات طويلة الامتداد.

كيف يمكننا التخلّص من متطلبات الذاكرة الباهظة عند التعامل مع مدخلات طويلة؟ نحتاج إلى طريقة جديدة لحساب آلية الانتباه الذاتي تتخلّص من مصفوفة $\mathbf{QK}^T$. طوّر [Tri Dao et al.](https://huggingface.co/papers/2205.14135) خوارزمية جديدة تمامًا لهذا الغرض وسماها **Flash Attention**.

بإيجاز، تقسم تقنية Flash Attention عملية حساب $\mathbf{V} \times \text{Softmax}(\mathbf{QK}^T)$ إلى أجزاء، وتقوم بدلاً من ذلك بحساب مقاطع أصغر من المخرجات من خلال التكرار على عدة خطوات لحساب softmax:

$$ \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} $$

حيث تكون $s^a_{ij}$ و $s^b_{ij}$ إحصاءات تطبيع softmax معينة يجب إعادة حسابها لكل من $i$ و $j$ .

يرجى ملاحظة أن تقنية Flash Attention بأكملها أكثر تعقيداً بعض الشيء، وقد تم تبسيطها إلى حد كبير هنا، إذ إن الغوص في التفاصيل الدقيقة يقع خارج نطاق هذا الدليل. ويُشجَّع القارئ على الاطلاع على [ورقة Flash Attention](https://huggingface.co/papers/2205.14135) المكتوبة بإتقان للحصول على مزيد من التفاصيل.

الخلاصة الرئيسية هنا هي:

> من خلال تتبع إحصاءات تطبيع softmax واستخدام بعض الرياضيات الذكية، يمنح Flash Attention مخرجات **متطابقة عددياً** مقارنة بطبقة الانتباه الذاتي الافتراضية، مع تكلفة ذاكرة لا تزيد إلا بشكل خطي مع $N$ .

عند النظر إلى الصيغة، قد يقول المرء حدسيًا إن Flash Attention يجب أن يكون أبطأ بكثير مقارنةً بصيغة الانتباه الذاتي الافتراضية، نظرًا إلى الحاجة إلى إجراء المزيد من العمليات الحسابية. وفي الواقع، يتطلب Flash Attention عددًا أكبر من عمليات FLOPs مقارنةً بالانتباه العادي، إذ يجب إعادة حساب إحصاءات تطبيع softmax باستمرار (انظر [الورقة البحثية](https://huggingface.co/papers/2205.14135) لمزيد من التفاصيل إذا كنت مهتمًا).

> ومع ذلك، يكون Flash Attention أسرع بكثير في الاستدلال مقارنةً بآلية الانتباه الافتراضية، وذلك بفضل قدرته على تقليل الطلب بشكل كبير على ذاكرة GPU ذات النطاق الترددي العالي والأبطأ (VRAM)، والتركيز بدلاً من ذلك على ذاكرة الشريحة الأسرع (SRAM).

في جوهر الأمر، يضمن Flash Attention أن تتم جميع عمليات الكتابة والقراءة الوسيطة باستخدام ذاكرة SRAM السريعة *على الشريحة* بدلاً من الحاجة إلى الوصول إلى ذاكرة VRAM الأبطأ لحساب متجه الإخراج $\mathbf{O}$ .

في الممارسة العملية، لا يوجد حالياً أي سبب يدعو إلى **عدم** استخدام Flash Attention إذا كان متاحاً. فالخوارزمية تعطي مخرجات متطابقة رياضياً، وهي أسرع وأكثر كفاءة في استخدام الذاكرة.

## 3. الابتكارات المعمارية

حتى الآن، نظرنا في تحسين الكفاءة الحسابية وكفاءة الذاكرة من خلال:

- تحويل الأوزان إلى صيغة دقة أدنى
- استبدال خوارزمية الانتباه الذاتي بنسخة أكثر كفاءة في استخدام الذاكرة والحوسبة

لننظر الآن في كيفية تغيير بنية نموذج اللغة الكبير (LLM) ليكون الأكثر فعالية وكفاءة للمهام التي تتطلب مدخلات نصية طويلة، *على سبيل المثال*:

- الإجابة على الأسئلة المعززة بالاسترجاع،
- تلخيص،
- الدردشة

لاحظ أن *chat* لا يتطلب من نموذج اللغة الكبير (LLM) التعامل مع مدخلات نصية طويلة فحسب، بل يستلزم أيضاً أن يكون النموذج قادراً على التعامل بكفاءة مع الحوار المتبادل بين المستخدم والمساعد (مثل ChatGPT).

بمجرد تدريب النموذج، يصبح من الصعب تغيير بنية نموذج اللغة الأساسي، لذا من المهم مراعاة مهام نموذج اللغة مسبقًا وتحسين بنية النموذج وفقًا لذلك. هناك مكونان مهمان في بنية النموذج يصبحان بسرعة عنقَي زجاجة للذاكرة و/أو الأداء عند التعامل مع تسلسلات إدخال كبيرة.

- تضمينات المواقع
- ذاكرة التخزين المؤقت للمفتاح والقيمة

لننظر في كل مكوّن بالتفصيل

### 3.1 تحسين التضمينات الموضعية في نماذج اللغة الكبيرة

تضع آلية الانتباه الذاتي كل رمز في علاقة مع رموز أخرى. على سبيل المثال، قد تبدو مصفوفة $\text{Softmax}(\mathbf{QK}^T)$ لسلسلة النص المدخلة *"Hello", "I", "love", "you"* على النحو التالي:

![](/blog/assets/163_optimize_llm/self_attn_tokens.png)

يُمنح كل رمز كلمة كتلة احتمالية تحدد مدى انتباهه لجميع رموز الكلمات الأخرى، وبالتالي يُوضع في علاقة مع جميع رموز الكلمات الأخرى. على سبيل المثال، ينتبه رمز كلمة *"love"* إلى رمز كلمة *"Hello"* بنسبة 5%، وإلى *"I"* بنسبة 30%، وإلى نفسه بنسبة 65%.

ستواجه نموذج اللغة الكبير (LLM) القائم على الانتباه الذاتي (self-attention) صعوبات كبيرة في فهم مواضع مدخلات النص فيما بينها إذا كان يفتقر إلى تضمينات المواقع (position embeddings). والسبب في ذلك أن درجة الاحتمال المحسوبة بواسطة $\mathbf{QK}^T$ تربط كل رمز كلمة (word token) بكل رمز كلمة آخر في $O(1)$ من العمليات الحسابية بغض النظر عن المسافة الموضعية النسبية بينهما. وبالتالي، في نموذج اللغة الكبير الذي يفتقر إلى تضمينات المواقع، يبدو أن كل رمز على مسافة متساوية من جميع الرموز الأخرى، *على سبيل المثال*، سيكون التمييز بين *"Hello I love you"* و *"You love I hello"* تحدياً بالغ الصعوبة.

لكي يفهم نموذج اللغة الكبير ترتيب الجمل، تكون هناك حاجة إلى *إشارة* إضافية، وتُطبَّق عادةً على شكل *ترميز موضعي* (أو ما يُعرف أيضاً بـ*تضمين موضعي*). يقوم الترميز الموضعي بتحويل موضع كل رمز إلى تمثيل رقمي يمكن لنموذج اللغة الكبير الاستفادة منه لفهم ترتيب الجمل بشكل أفضل.

قدّم مؤلفو ورقة [*Attention Is All You Need*](https://huggingface.co/papers/1706.03762) تضمينات موضعية جيبية $\mathbf{P} = \mathbf{p}_1, \ldots, \mathbf{p}_N$ ، حيث يُحسب كل متجه $\mathbf{p}_i$ كدالة جيبية لموضعه $i$ . ثم تُضاف الترميزات الموضعية ببساطة إلى متجهات تسلسل الإدخال $\mathbf{\hat{X}} = \mathbf{\hat{x}}_1, \ldots, \mathbf{\hat{x}}_N$ = $\mathbf{x}_1 + \mathbf{p}_1, \ldots, \mathbf{x}_N + \mathbf{p}_N$ ، مما يُشجّع النموذج على تعلّم ترتيب الجمل بشكل أفضل.

بدلاً من استخدام ترميزات موضعية ثابتة، استخدم آخرون (مثل [Devlin et al.](https://huggingface.co/papers/1810.04805)) ترميزات موضعية متعلمة، حيث تُتعلَّم الترميزات الموضعية $\mathbf{P}$ أثناء التدريب.

كانت ترميزات المواقع الجيبية والتعلمية هي الأساليب السائدة لتشفير ترتيب الجمل في نماذج اللغة الكبيرة، لكن تم العثور على بعض المشكلات المتعلقة بهذه الترميزات الموضعية:

  1. تُعدّ تضمينات المواقع الجيبية والتضمينات المُتعلَّمة كلاهما تضمينات مواقع مطلقة، *أي* أنها ترمّز تضمينًا فريدًا لكل معرّف موقع: $0, \ldots, N$ . وكما أوضح [Huang et al.](https://huggingface.co/papers/2009.13658) و[Su et al.](https://huggingface.co/papers/2104.09864)، فإن التضمينات المطلقة للمواقع تؤدي إلى أداء ضعيف لنماذج اللغة الكبيرة عند التعامل مع مدخلات نصية طويلة. وفي حال المدخلات النصية الطويلة، يكون من المفيد أن يتعلم النموذج المسافة المكانية النسبية بين الرموز المدخلة بدلاً من موقعها المطلق.
  2. عند استخدام ترميزات المواقع المتعلمة، يجب تدريب نموذج اللغة الكبير على طول إدخال ثابت $N$، مما يجعل من الصعب الاستقراء إلى طول إدخال أطول من ذلك الذي تم تدريبه عليه.

أصبحت التضمينات الموضعية النسبية، القادرة على معالجة المشكلات المذكورة أعلاه، أكثر شيوعاً في الآونة الأخيرة، وأبرزها:

- [تضمين الموضع الدوّاري (RoPE)](https://huggingface.co/papers/2104.09864)
- [ALiBi](https://huggingface.co/papers/2108.12409)

يؤكد كلٌّ من *RoPE* و*ALiBi* على أن الطريقة الأمثل لإيصال ترتيب الجمل إلى نموذج اللغة الكبير هي الإشارة إليه مباشرةً داخل خوارزمية الانتباه الذاتي، إذ إن هذا هو المكان الذي تُوضع فيه رموز الكلمات في علاقة ببعضها البعض. وبشكل أكثر تحديداً، يجب أن يُشار إلى ترتيب الجمل من خلال تعديل عملية حساب $\mathbf{QK}^T$.

بدون الخوض في تفاصيل كثيرة، يلاحظ *RoPE* أنه يمكن ترميز المعلومات الموضعية في أزواج الاستعلام والمفتاح، *على سبيل المثال* $\mathbf{q}_i$ و $\mathbf{x}_j$، وذلك بتدوير كل متجه بزاوية $\theta * i$ و $\theta * j$ على التوالي، حيث تصف $i, j$ الموضع الجملي لكل متجه:

$$ \mathbf{\hat{q}}_i^T \mathbf{\hat{x}}_j = \mathbf{{q}}_i^T \mathbf{R}_{\theta, i -j} \mathbf{{x}}_j. $$

يمثل $\mathbf{R}_{\theta, i - j}$ بالتالي مصفوفة دوران. $\theta$ *ليس* يتعلم أثناء التدريب، بل يُضبط بدلاً من ذلك على قيمة محددة مسبقًا تعتمد على أقصى طول لسلسلة الإدخال أثناء التدريب.

> وبذلك، لا يتأثر درجة الاحتمال بين $\mathbf{q}_i$ و $\mathbf{q}_j$ إلا إذا كان $i \ne j$، ويعتمد حصرياً على المسافة النسبية $i - j$ بغض النظر عن المواقع المحددة $i$ و $j$ لكل متجه.

يُستخدم *RoPE* في عدد من أهم نماذج اللغة الكبيرة (LLMs) اليوم، مثل:

- [**Falcon**](https://huggingface.co/tiiuae/falcon-40b)
- [**Llama**](https://huggingface.co/papers/2302.13971)
- [**PaLM**](https://huggingface.co/papers/2204.02311)

كبديل، يقترح *ALiBi* مخططاً أبسط بكثير لتشفير المواقع النسبية. تُضاف المسافة النسبية التي تفصل بين رموز الإدخال إلى بعضها البعض كعدد صحيح سالب مُقاس بمقدار محدد مسبقاً `m` إلى كل عنصر من عناصر مصفوفة الاستعلام-المفتاح $\mathbf{QK}^T$ مباشرة قبل حساب softmax.

![](/blog/assets/163_optimize_llm/alibi.png)

كما هو موضح في ورقة [ALiBi](https://huggingface.co/papers/2108.12409)، يتيح هذا الترميز الموضعي النسبي البسيط للنموذج الحفاظ على أداء عالٍ حتى عند استخدام تسلسلات إدخال نصية طويلة جداً.

يُستخدم *ALiBi* في عدد من أهم نماذج اللغة الكبيرة (LLMs) في الوقت الحالي، مثل:

- [**MPT**](https://huggingface.co/mosaicml/mpt-30b)
- [**BLOOM**](https://huggingface.co/bigscience/bloom)

يمكن لكلٍّ من ترميزات المواقع *RoPE* و*ALiBi* الاستقراء إلى أطوال مدخلات لم تُرَ أثناء التدريب، في حين تبيّن أن الاستقراء يعمل بشكل أفضل بكثير دون أي إعداد مسبق في حالة *ALiBi* مقارنةً بـ*RoPE*. في حالة ALiBi، يكفي ببساطة زيادة قيم المصفوفة المثلثية السفلية للمواقع لتطابق طول تسلسل الإدخال. أما بالنسبة إلى *RoPE*، فإن الإبقاء على نفس قيمة $\theta$ المستخدمة أثناء التدريب يؤدي إلى نتائج ضعيفة عند تمرير مدخلات نصية أطول بكثير من تلك التي رُصدت أثناء التدريب، *انظر* [Press et al.](https://huggingface.co/papers/2108.12409). ومع ذلك، وجد المجتمع تقنيات فعالة قليلة تكيف قيمة $\theta$، مما يتيح لتضمينات المواقع *RoPE* أن تعمل بشكل جيد مع تسلسلات مدخلات نصية مستقراة (انظر [هنا](https://github.com/huggingface/transformers/pull/24653)).

> يعتمد كلٌّ من RoPE و ALiBi على تمثيلات موضعية نسبية *غير* متعلَّمة أثناء التدريب، بل تستند بدلاً من ذلك إلى الحدسيّات التالية:

- يجب تقديم إشارات موضعية حول مدخلات النص مباشرةً إلى مصفوفة $\mathbf{QK}^T$ في طبقة الانتباه الذاتي.
- يجب تحفيز نموذج اللغة الكبير على تعلم ترميز موضعي مسافة *نسبية* ثابتة.
- كلما ابتعدت رموز الإدخال النصية عن بعضها البعض، انخفضت احتمالية الاستعلام-القيمة الخاصة بها. وكلا من RoPE وALiBi يخفضان احتمالية الاستعلام-المفتاح للرموز البعيدة عن بعضها البعض. يخفض RoPE ذلك عن طريق تقليل حاصل ضرب المتجهات من خلال زيادة الزاوية بين متجهات الاستعلام-المفتاح. أما ALiBi فيخفض ذلك عن طريق إضافة أرقام سالبة كبيرة إلى حاصل ضرب المتجهات.

ختاماً، تُدرَّب نماذج اللغة الكبيرة (LLMs) المخصصة للنشر في مهام تتطلب معالجة مدخلات نصية كبيرة بشكل أفضل باستخدام التضمينات الموضعية النسبية، مثل RoPE وALiBi. كما يُلاحظ أنه حتى لو كان نموذج لغة كبيرة مزوّد بـ RoPE وALiBi قد تم تدريبه على طول ثابت فقط، مثل $N_1 = 2048$، فإنه لا يزال قابلاً للاستخدام عملياً مع مدخلات نصية أكبر بكثير من $N_1$، مثل $N_2 = 8192 > N_1$، وذلك من خلال استقراء التضمينات الموضعية.

### 3.2 ذاكرة التخزين المؤقت للمفتاح والقيمة

يعمل توليد النص التلقائي (Auto-regressive) باستخدام نماذج اللغة الكبيرة (LLMs) من خلال إدخال تسلسل مدخلات بشكل متكرر، ثم أخذ عينة من الرمز (token) التالي، وإلحاقه بتسلسل المدخلات، والمضي قدمًا في هذه العملية حتى تنتج نموذج اللغة الكبيرة رمزًا يشير إلى اكتمال عملية التوليد.

يرجى الاطلاع على [دليل توليد النصوص في Transformers](./llm_tutorial#default-generate) للحصول على شرح بصري أكثر تفصيلاً لكيفية عمل التوليد الذاتي.

لن نفِذ مقتطفًا برمجيًا سريعًا لإظهار كيفية عمل التوليد التلقائي (auto-regressive) عمليًا. سنأخذ ببساطة الرمز (token) التالي الأكثر احتمالًا عبر `torch.argmax`.

```python
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
```

**المخرجات**:

```text
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']
```

كما نلاحظ، في كل مرة نزيد رموز مدخلات النص بالرمز الذي تم عينته للتو.

باستثناء حالات قليلة جداً، تُدرَّب نماذج اللغة الكبيرة باستخدام [هدف نمذجة اللغة السببية](./tasks/language_modeling#causal-language-modeling)، ولذلك تُقنَّع مصفوفة المثلث العلوي لدرجات الانتباه - وهذا هو السبب في أن درجات الانتباه تُترك فارغة في المخططين أعلاه (*أي* احتمالية تساوي صفراً). لمراجعة سريعة حول نمذجة اللغة السببية، يمكنك الرجوع إلى [*مدونة الانتباه الذاتي الموضّح*](https://jalammar.github.io/illustrated-gpt2/#part-2-illustrated-self-attention).

وعليه، لا تعتمد الرموز *أبداً* على الرموز اللاحقة، وبشكل أكثر تحديداً، لا يُوضع متجه $\mathbf{q}_i$ في علاقة مع أي متجهات مفتاح أو قيمة $\mathbf{k}_j, \mathbf{v}_j$ إذا كان $j > i$ . بدلاً من ذلك، يهتم $\mathbf{q}_i$ فقط بمتجهات المفتاح-القيمة السابقة $\mathbf{k}_{m < i}, \mathbf{v}_{m < i} \text{ , for } m \in \{0, \ldots i - 1\}$. ولتقليل الحسابات غير الضرورية، يمكن تخزين متجهات المفتاح-القيمة لكل طبقة لجميع الخطوات الزمنية السابقة في ذاكرة مؤقتة.

فيما يلي، سنطلب من نموذج اللغة الكبير (LLM) الاستفادة من ذاكرة المفتاح-القيمة (key-value cache) من خلال استرجاعها وتحويلها في كل تمريرة أمامية. في مكتبة Transformers، يمكننا استرجاع ذاكرة المفتاح-القيمة عن طريق تمرير علم `use_cache` إلى استدعاء `forward`، ثم تمريرها مع الرمز الحالي.

```python
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
```

**المخرجات**:

```text
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']
```

كما يمكن ملاحظة، عند استخدام ذاكرة التخزين المؤقت للمفتاح والقيمة، لا يتم *زيادة* طول رموز النص المُدخل، بل تبقى متجه إدخال واحد. في المقابل، يتم زيادة طول ذاكرة التخزين المؤقت للمفتاح والقيمة بمقدار واحد في كل خطوة من خطوات فك الترميز.

> يعني استخدام ذاكرة التخزين المؤقت للمفتاح والقيمة أن $\mathbf{QK}^T$ تقلّص في الأساس إلى $\mathbf{q}_c\mathbf{K}^T$ حيث يكون $\mathbf{q}_c$ إسقاط الاستعلام للرمز المُدخل حاليًا وهو *دائمًا* متجه واحد فقط.

يتمتع استخدام ذاكرة التخزين المؤقت للمفتاح والقيمة بمزاياَ اثنتين:

- زيادة كبيرة في الكفاءة الحسابية، نظراً لأن عدد العمليات الحسابية المنفذة أقل مقارنةً بحساب المصفوفة الكاملة $\mathbf{QK}^T$. يؤدي ذلك إلى زيادة في سرعة الاستدلال
- لا يتزايد الحد الأقصى للذاكرة المطلوبة بشكل تربيعي مع عدد الرموز المولّدة، بل يتزايد بشكل خطي فقط.

> يجب *دائمًا* استخدام ذاكرة التخزين المؤقت للمفاتيح والقيم، إذ يؤدي ذلك إلى نتائج مطابقة وتسريع كبير للترتيبات الإدخالية الأطول. تكون ذاكرة التخزين المؤقت للمفاتيح والقيم مفعّلة افتراضيًا في Transformers عند استخدام خط أنابيب النص أو [طريقة `generate`](./main_classes/text_generation). لدينا دليل كامل مخصص لعمليات التخزين المؤقت [هنا](./kv_cache).

لاحظ أنه، على الرغم من توصيتنا باستخدام ذاكرات التخزين المؤقت للمفتاح والقيمة، فقد يكون ناتج نموذج اللغة الخاص بك مختلفاً قليلاً عند استخدامها. هذه خاصية مرتبطة بنوى ضرب المصفوفات نفسها -- يمكنك قراءة المزيد حول ذلك [هنا](https://github.com/huggingface/transformers/issues/25420#issuecomment-1775317535).

#### 3.2.1 محادثة متعددة الجولات

يُعدّ ذاكرة التخزين المؤقت للمفتاح والقيمة مفيدة بشكل خاص للتطبيقات مثل الدردشة التي تتطلب عدة جولات من فك الترميز التلقائي التتابعي. دعونا ننظر إلى مثال.

```text
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
```

في هذا الدردشة، يقوم نموذج اللغة الكبير بتشغيل فك الترميز التلقائي التكراري مرتين:

  1. في المرة الأولى، يكون مخزن المفاتيح والقيم فارغاً، ويكون مُوجِّه الإدخال `"User: How many people live in France?"`، ويولّد النموذج النص `"Roughly 75 million people live in France"` بشكل تلقائي متتابع مع زيادة حجم مخزن المفاتيح والقيم في كل خطوة فكّ تشفير.
  2. في المرة الثانية، يكون مُوجِّه الإدخال `"User: How many people live in France? \n Assistant: Roughly 75 million people live in France \n User: And how many in Germany?"`. بفضل ذاكرة التخزين المؤقت، تكون جميع متجهات المفتاح-القيمة للجملتين الأوليين محسوبة بالفعل. لذلك يتكوّن مُوجِّه الإدخال فقط من `"User: And how many in Germany?"`. أثناء معالجة مُوجِّه الإدخال المختصر، تُدمج متجهات المفتاح-القيمة المحسوبة له مع ذاكرة تخزين المفتاح-القيمة الخاصة بفك الترميز الأولي. ثم يتم توليد إجابة المساعد الثانية `"Germany has ca. 81 million inhabitants"` بشكل تلقائي متدرج، مع ذاكرة تخزين مفتاح-قيمة تتألف من متجهات المفتاح-القيمة المشفّرة لـ `"User: How many people live in France? \n Assistant: Roughly 75 million people live in France \n User: And how many are in Germany?"`.

يجب الإشارة إلى أمرين هنا:

  1. يُعدّ الاحتفاظ بكل السياق أمراً بالغ الأهمية بالنسبة لنماذج اللغة الكبيرة (LLMs) المُنفَّذة في المحادثات، حتى تتمكن النماذج من فهم كامل السياق السابق للمحادثة. فعلى سبيل المثال، في المثال أعلاه، يحتاج النموذج إلى فهم أن المستخدم يشير إلى عدد السكان عندما يسأل `"And how many are in Germany"`.
  2. يُعدّ ذاكرة التخزين المؤقت للمفتاح والقيمة مفيدة للغاية في المحادثة، إذ تتيح لنا توسيع سجل المحادثة المُرمَّز باستمرار بدلاً من الحاجة إلى إعادة ترميز سجل المحادثة من الصفر (كما هو الحال عند استخدام بنية مُرمِّز-مُفكِّك، على سبيل المثال).

في `transformers`، ستُرجع استدعاء `generate` قيمة `past_key_values` عند تمرير `return_dict_in_generate=True`، بالإضافة إلى الإعداد الافتراضي `use_cache=True`. يُرجى ملاحظة أن هذه الميزة غير متاحة بعد عبر واجهة `pipeline`.

```python
# 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):]
```

**المخرجات**:

```text
 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
```

رائع، فلا يُستغرق وقت إضافي لإعادة حساب نفس المفاتيح والقيم لطبقة الانتباه! ومع ذلك، هناك نقطة واحدة يجب الانتباه إليها. فبينما تقلّص الذاكرة الذروية المطلوبة لمصفوفة $\mathbf{QK}^T$ بشكل كبير، فإن الاحتفاظ بذاكرة المفاتيح والقيم في الذاكرة يمكن أن يصبح مكلفاً جداً من حيث استهلاك الذاكرة في حال وجود تسلسلات إدخال طويلة أو محادثات متعددة الأدوار. تذكّر أن ذاكرة المفاتيح والقيم تحتاج إلى تخزين متجهات المفاتيح والقيم لجميع متجهات الإدخال السابقة $\mathbf{x}_i \text{, for } i \in \{1, \ldots, c - 1\}$ لجميع طبقات الانتباه الذاتي ولكل رؤوس الانتباه.

لنحسب عدد قيم الفاصلة العائمة التي يجب تخزينها في ذاكرة التخزين المؤقت للمفتاح-القيمة (key-value cache) لنموذج اللغة الكبير `bigcode/octocoder` الذي استخدمناه سابقاً. يبلغ عدد قيم الفاصلة العائمة ضعف طول التسلسل مضروباً في عدد رؤوس الانتباه (attention heads) مضروباً في بُعد رأس الانتباه (attention head dimension) مضروباً في عدد الطبقات. عند حساب ذلك لنموذج اللغة الكبير الخاص بنا مع افتراض طول تسلسل مدخلات يبلغ 16000، نحصل على:

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

**المخرجات**:

```text
7864320000
```

حوالي 8 مليارات من قيم الفاصلة العائمة! يتطلب تخزين 8 مليارات من قيم الفاصلة العائمة بدقة `float16` حوالي 15 غيغابايت من ذاكرة الوصول العشوائي، أي ما يعادل تقريبًا نصف حجم أوزان النموذج نفسها! وقد اقترح الباحثون طريقتين تتيحان تقليل التكلفة الذاكرة لتخزين ذاكرة المؤشرات والقيم بشكل كبير، وسيتم استكشافهما في الأقسام الفرعية التالية.

#### 3.2.2 الانتباه متعدد الاستعلامات (MQA)

قُدِّم [Multi-Query-Attention](https://huggingface.co/papers/1911.02150) في ورقة Noam Shazeer المعنونة *Fast Transformer Decoding: One Write-Head is All You Need*. وكما يشير العنوان، اكتشف Noam أنه بدلاً من استخدام أوزان إسقاطات المفتاح والقيمة لـ `n_head`، يمكن استخدام زوج واحد من أوزان إسقاط الرأس والقيمة يُشارَك عبر جميع رؤوس الانتباه دون أن يتدهور أداء النموذج بشكل كبير.

> باستخدام زوج واحد من أوزان الإسقاط للرأس والقيمة، يجب أن تكون متجهات المفتاح والقيمة $\mathbf{k}_i, \mathbf{v}_i$ متطابقة عبر جميع رؤوس الانتباه، مما يعني بدورها أنه لا نحتاج إلى تخزين سوى زوج واحد من إسقاطات المفتاح والقيمة في ذاكرة التخزين المؤقت بدلاً من `n_head` أزواج.

بما أن معظم نماذج LLM تستخدم ما بين 20 و100 رأس انتباه، فإن MQA يقلل بشكل كبير من استهلاك ذاكرة ذاكرة التخزين المؤقت للمفتاح والقيمة. وبالنسبة لنموذج LLM المستخدم في هذا الدفتر، كان من الممكن بالتالي تقليل استهلاك الذاكرة المطلوب من 15 جيجابايت إلى أقل من 400 ميجابايت عند طول تسلسل الإدخال 16000.

بالإضافة إلى توفير الذاكرة، يؤدي MQA أيضاً إلى تحسين الكفاءة الحسابية كما هو موضح فيما يلي. في فك الترميز التلقائي التكراري (auto-regressive decoding)، يجب إعادة تحميل متجهات المفتاح-القيمة الكبيرة، وربطها بزوج متجهات المفتاح-القيمة الحالية، ثم إدخالها في عملية حساب $\mathbf{q}_c\mathbf{K}^T$ في كل خطوة. في فك الترميز التلقائي التكراري، يمكن أن يصبح عرض نطاق الذاكرة المطلوب لإعادة التحميل المستمرة عنق زجاجة زمنية جسيماً. من خلال تقليل حجم متجهات المفتاح-القيمة، تقل الحاجة إلى الوصول إلى الذاكرة، مما يقلل من عنق زجاجة عرض نطاق الذاكرة. لمزيد من التفاصيل، يرجى الاطلاع على [ورقة Noam](https://huggingface.co/papers/1911.02150).

الجزء المهم الذي يجب فهمه هنا هو أن تقليل عدد رؤوس الانتباه المفتاح-القيمة إلى 1 لا معنى له إلا إذا تم استخدام ذاكرة تخزين مؤقت للمفتاح-القيمة. يبقى استهلاك الذروة من الذاكرة للنموذج لتمريرة أمامية واحدة بدون ذاكرة تخزين مؤقت للمفتاح-القيمة دون تغيير، لأن كل رأس انتباه لا يزال لديه متجه استعلام فريد، وبالتالي فإن كل رأس انتباه لا يزال لديه مصفوفة $\mathbf{QK}^T$ مختلفة.

لقد حظي MQA بانتشار واسع في المجتمع، وهو الآن مستخدم في العديد من أشهر نماذج اللغة الكبيرة (LLMs):

- [**Falcon**](https://huggingface.co/tiiuae/falcon-40b)
- [**PaLM**](https://huggingface.co/papers/2204.02311)
- [**MPT**](https://huggingface.co/mosaicml/mpt-30b)
- [**BLOOM**](https://huggingface.co/bigscience/bloom)

كما أن نقطة التفتيش المستخدمة في هذا الدفتر - `bigcode/octocoder` - تستخدم تقنية MQA.

#### 3.2.3 الانتباه الاستعلامي المجمّع (GQA)

وجدت دراسة [Grouped-Query-Attention](https://huggingface.co/papers/2305.13245) التي اقترحها Ainslie وآخرون من Google أن استخدام MQA قد يؤدي في كثير من الأحيان إلى تدهور الجودة مقارنةً باستخدام إسقاطات رؤوس المفاتيح والقيم المتعددة التقليدية. وتجادل الورقة بأن أداء النموذج يمكن الحفاظ عليه بشكل أكبر من خلال تقليل عدد أوزان إسقاط رؤوس الاستعلام بشكل أقل جذرية. وبدلاً من استخدام وزن إسقاط مفاتيح وقيم واحد فقط، ينبغي استخدام `n < n_head` من أوزان إسقاط المفاتيح والقيم. وبخيار قيمة `n` أصغر بكثير من `n_head`، مثل 2 أو 4 أو 8، يمكن الحفاظ على معظم مكاسب الذاكرة والسرعة من MQA مع التضحية بقدر أقل من سعة النموذج، وبالتالي بأداء أقل على الأرجح.

علاوة على ذلك، وجد مؤلفو GQA أنه يمكن *إعادة تدريب* نقاط التفتيش الموجودة للنماذج على معمارية GQA باستخدام ما لا يقل عن 5% فقط من حسابات التدريب المسبق الأصلية. ورغم أن 5% من حسابات التدريب المسبق الأصلية قد لا تزال كمية هائلة، فإن *إعادة التدريب* على GQA يتيح الاستفادة من نقاط التفتيش الموجودة لفترة أطول مع تسلسلات الإدخال الأطول.

لم يُقترح GQA إلا مؤخراً، ولهذا السبب فإن انتشاره أقل وقت كتابة هذا الدفتر. التطبيق الأكثر شهرة لـ GQA هو [Llama-v2](https://huggingface.co/meta-llama/Llama-2-70b-hf).

> كخلاصة، يُوصى بشدة بالاستفادة من GQA أو MQA إذا كان نموذج LLM مُفعَّلاً بفك الترميز التلقائي التتابعي (auto-regressive decoding) وكان مطلوباً منه التعامل مع تسلسلات إدخال كبيرة، كما هو الحال على سبيل المثال في تطبيقات الدردشة.

## الخلاصة

تتوصل مجتمع البحث باستمرار إلى طرق جديدة وفعّالة لتسريع زمن الاستدلال في نماذج اللغة الكبيرة (LLMs) المتزايدة الحجم. ومن الأمثلة على ذلك اتجاه بحثي واعد هو [التفكيك التخميني](https://huggingface.co/papers/2211.17192)، حيث يتم توليد "الرموز السهلة" بواسطة نماذج لغة أصغر وأسرع، بينما تولّد نموذج اللغة الكبيرة (LLM) نفسه "الرموز الصعبة" فقط. إن الغوص في التفاصيل يتجاوز نطاق هذا الدفتر، لكن يمكن الاطلاع على المزيد في [هذا المنشور الرائع](https://huggingface.co/blog/assisted-generation).

السبب في أن النماذج اللغوية الضخمة مثل GPT3/4 وLlama-2-70b وClaude وPaLM تعمل بسرعة كبيرة في واجهات الدردشة مثل [Hugging Face Chat](https://huggingface.co/chat/) أو ChatGPT هو إلى حد كبير بفضل التحسينات المذكورة أعلاه في الدقة والخوارزميات والبنية. وفي المستقبل، ستصبح المعجلات مثل GPUs وTPUs وغيرها أسرع وتتيح ذاكرة أكبر، لكن يجب دائماً التأكد من استخدام أفضل الخوارزميات والبنى المتاحة للحصول على أفضل قيمة مقابل التكلفة 🤗

