#
تشريح عملية تدريب النموذج

لفهم تقنيات تحسين الأداء التي يمكن تطبيقها لتحسين كفاءة استخدام الذاكرة وسرعة تدريب النموذج، من المفيد التعرف على كيفية استخدام وحدة معالجة الرسوميات (GPU) أثناء التدريب، وكيف تختلف كثافة العمليات الحسابية باختلاف العملية التي يتم تنفيذها.

لنبدأ باستكشاف مثال توضيحي على استخدام وحدة GPU وتشغيل تدريب نموذج. وللتوضيح، سنحتاج إلى تثبيت بعض المكتبات:

    pip install transformers datasets accelerate nvidia-ml-py3

تتيح مكتبة `nvidia-ml-py3` إمكانية مراقبة استخدام الذاكرة في النماذج من داخل بايثون. قد تكون على دراية بأمر `nvidia-smi` في الجهاز - تسمح هذه المكتبة بالوصول إلى نفس المعلومات مباشرة في بايثون.

ثم، نقوم بإنشاء بعض البيانات الوهمية:معرّفات رموز عشوائية بين 100 و30000 وتصنيفات ثنائية للمصنف.

في المجموع، نحصل على 512 تسلسلًا، لكل منها طول 512، ونخزنها في `Dataset` بتنسيق PyTorch.

    >>> import numpy as np
    >>> from datasets import Dataset

    >>> seq_len, dataset_size = 512, 512
    >>> dummy_data = {
    ...     "input_ids": np.random.randint(100, 30000, (dataset_size, seq_len)),
    ...     "labels": np.random.randint(0, 1, (dataset_size)),
    ... }
    >>> ds = Dataset.from_dict(dummy_data)
    >>> ds.set_format("pt")

لطباعة إحصائيات موجزة لاستخدام وحدة GPU وتشغيل التدريب مع `Trainer`، نقوم بتعريف دالتين مساعدتين:

    >>> from pynvml import *

    >>> def print_gpu_utilization():
    ...     nvmlInit()
    ...     handle = nvmlDeviceGetHandleByIndex(0)
    ...     info = nvmlDeviceGetMemoryInfo(handle)
    ...     print(f"GPU memory occupied: {info.used//1024**2} MB.")

    >>> def print_summary(result):
    ...     print(f"Time: {result.metrics['train_runtime']:.2f}")
    ...     print(f"Samples/second: {result.metrics['train_samples_per_second']:.2f}")
    ...     print_gpu_utilization()

دعنا نتأكد من أننا نبدأ بذاكرة وحدة GPU خالية:

    >>> print_gpu_utilization()
    GPU memory occupied: 0 MB.

يبدو ذلك جيدًا: لم يتم شغل ذاكرة وحدة معالجة الرسومات كما نتوقع قبل تحميل أي نماذج. إذا لم يكن الأمر كذلك على جهازك، فتأكد من إيقاف جميع العمليات التي تستخدم ذاكرة وحدة GPU. ومع ذلك، لا يمكن للمستخدم استخدام كل ذاكرة وحدة GPU الفارغة. عندما يتم تحميل نموذج إلى وحدة GPU، يتم أيضًا تحميل النواة، والتي يمكن أن تستهلك 1-2 جيجابايت من الذاكرة. ولرؤية مقدار ذلك، نقوم بتحميل مصفوفة صغيرة إلى وحدة GPU والتي تؤدي إلى تحميل النواة أيضًا.

    >>> import torch

    >>> torch.ones((1, 1)).to("cuda")
    >>> print_gpu_utilization()
    GPU memory occupied: 1343 MB.

نلاحظ أن النواة وحدها تستهلك 1.3 جيجابايت من ذاكرة وحدة GPU. الآن دعنا نرى مقدار المساحة التي يستخدمها النموذج.

##
تحميل النموذج

أولاً، نقوم بتحميل نموذج `google-bert/bert-large-uncased`. نقوم بتحميل أوزان النموذج مباشرة إلى وحدة GPU حتى نتمكن من التحقق من مقدار المساحة التي تستخدمها الأوزان فقط.

    >>> from transformers import AutoModelForSequenceClassification

    >>> model = AutoModelForSequenceClassification.from_pretrained("google-bert/bert-large-uncased").to("cuda")
    >>> print_gpu_utilization()
    GPU memory occupied: 2631 MB.

يمكننا أن نرى أن أوزان النموذج وحدها تستهلك 1.3 جيجابايت من ذاكرة وحدة GPU. يعتمد الرقم الدقيق على وحدة GPU المحددة التي تستخدمها. لاحظ أنه في وحدات GPU الأحدث، قد يستغرق النموذج في بعض الأحيان مساحة أكبر نظرًا لأن الأوزان يتم تحميلها بطريقة مُحسّنة تُسرّع من استخدام النموذج. الآن يمكننا أيضًا التحقق بسرعة مما إذا كنا نحصل على نفس النتيجة كما هو الحال مع `nvidia-smi` CLI:

    nvidia-smi

    Tue Jan 11 08:58:05 2022
    +-----------------------------------------------------------------------------+
    | NVIDIA-SMI 460.91.03    Driver Version: 460.91.03    CUDA Version: 11.2     |
    |-------------------------------+----------------------+----------------------+
    | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
    | Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
    |                               |                      |               MIG M. |
    |===============================+======================+======================|
    |   0  Tesla V100-SXM2...  On   | 00000000:00:04.0 Off |                    0 |
    | N/A   37C    P0    39W / 300W |   2631MiB / 16160MiB |      0%      Default |
    |                               |                      |                  N/A |
    +-------------------------------+----------------------+----------------------+

    +-----------------------------------------------------------------------------+
    | Processes:                                                                  |
    |  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
    |        ID   ID                                                   Usage      |
    |=============================================================================|
    |    0   N/A  N/A      3721      C   ...nvs/codeparrot/bin/python     2629MiB |
    +-----------------------------------------------------------------------------+

نحصل على نفس الرقم كما كان من قبل، ويمكنك أيضًا أن ترى أننا نستخدم GPU من طراز V100 مع 16 جيجابايت من الذاكرة. لذا الآن يمكننا بدء تدريب النموذج ورؤية كيف يتغير استخدام ذاكرة GPU. أولاً، نقوم بإعداد بعض معاملات التدريب القياسية:

    default_args = {
        "output_dir": "tmp"،
        "eval_strategy": "steps"،
        "num_train_epochs": 1،
        "log_level": "error"،
        "report_to": "none"،
    }

إذا كنت تخطط لتشغيل عدة تجارب، من أجل مسح الذاكرة بشكل صحيح بين التجارب، قم بإعادة تشغيل نواة Python بين التجارب.

##
استخدام الذاكرة في التدريب الأساسي

دعونا نستخدم `Trainer` وقم بتدريب النموذج دون استخدام أي تقنيات تحسين أداء GPU وحجم دفعة يبلغ 4:

    >>> from transformers import TrainingArguments، Trainer، logging

    >>> logging.set_verbosity_error()

    >>> training_args = TrainingArguments(per_device_train_batch_size=4، **default_args)
    >>> trainer = Trainer(model=model، args=training_args، train_dataset=ds)
    >>> result = trainer.train()
    >>> print_summary(result)

    الوقت: 57.82
    العينات / الثانية: 8.86
    ذاكرة GPU المشغولة: 14949 ميجابايت.

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

##
تشريح عمليات النموذج

تتضمن بنية المحولات 3 مجموعات رئيسية من العمليات مُجمعة أدناه حسب كثافة العمليات الحسابية.

1. عمليات ضرب المصفوفات

   تقوم الطبقات الخطية ومكونات الانتباه متعدد الرؤوس جميعها بعمليات ضرب المصفوفة بالمصفوفة على دفعات. هذه العمليات هي أكثر أجزاء تدريب المحولات كثافة من الناحية الحسابية.

2. عمليات التسوية الإحصائية

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

3. العمليات على مستوى العناصر

   هذه هي العمليات المتبقية: الانحيازات، والتسرب، ووظائف التنشيط، والوصلات المتبقية. هذه هي عمليات أقل كثافة من الناحية الحسابية.

يمكن أن تكون هذه المعرفة مفيدة لمعرفة عند تحليل اختناقات الأداء.

هذا الملخص مُشتق من نقل البيانات هو كل ما تحتاجه: دراسة حالة حول تحسين المحولات 2020

##
تشريح ذاكرة النموذج

لقد رأينا أن تدريب النموذج يستخدم ذاكرة أكثر بكثير من مجرد وضع النموذج على GPU. ويرجع ذلك إلى هناك العديد من المكونات أثناء التدريب التي تستخدم ذاكرة GPU. المكونات الموجودة في ذاكرة GPU هي التالية:

1. أوزان النموذج
2. الدول المُحسّن
3. المُتدرجات
4. تنشيطات المسار الأمامي المحفوظة لحساب المُتدرجات
5. المخازن المؤقتة
6. ذاكرة محددة الوظائف

يتطلب نموذج نموذجي مدرب بدقة مختلطة 18 بايت للمُحسّن AdamW كل معلمة نموذج بالإضافة إلى ذاكرة التنشيط. للاستدلال لا توجد حالات مُحسّن و مُتدرجات، لذلك يمكننا طرح تلك. وهكذا ننتهي مع 6 بايت لكل معلمة نموذج للدقة المختلطة الاستدلال، بالإضافة إلى ذاكرة التنشيط.

دعنا نلقي نظرة على التفاصيل.

أوزان النموذج:

- 4 بايت * عدد المعلمات للتدريب على دقة fp32
- 6 بايت * عدد المعلمات لتدريب الدقة المختلطة (يحافظ على نموذج في fp32 وآخر بدقة fp16 في الذاكرة)

حالات المُحسّن:

- 8 بايت * عدد المعلمات للمُحسّن AdamW العادي (يحافظ على حالتين)
- 2 بايت * عدد المعلمات لمُحسّنات 8 بت AdamW مثل bitsandbytes
- 4 بايت * عدد المعلمات لمُحسّنات مثل SGD مع الزخم momentum (يحافظ على حالة واحدة فقط)

المُتدرجات

- 4 بايت * عدد المعلمات للتدريب بدقة fp32 أو بدقة مختلطة (المُتدرجات تكون دائمًا بدقة fp32)

تنشيطات المسار الأمامي

- يعتمد الحجم على العديد من العوامل، وأهمها طول التسلسل وحجم المخفية وحجم الدُفعة.

هناك المدخلات والمخرجات لذي يتم تمريرها وإرجاعها بواسطة وظائف المسار الأمامي والمسار الخلفي وتنشيطات المسار الأمامي المحفوظة لحساب المُتدرجات.

الذاكرة المؤقتة

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

ذاكرة محددة الوظائف

ثم، قد يكون لبرنامجك احتياجات خاصة بالذاكرة. على سبيل المثال، عند إنشاء نص باستخدام البحث الشعاعي، يحتاج البرنامج إلى الاحتفاظ بنسخ متعددة من المدخلات والمخرجات.

سرعة تنفيذ `forward` مقابل `backward`

بالنسبة للالتفافات والطبقات الخطية، هناك ضِعف عدد العمليات 2x flops في المسار الخلفى مقارنة بالمسار الأمامي، والتي يُترجم عمومًا إلى ~2x أبطأ (أحيانًا أكثر، لأن الأحجام في المسار الخلفى تميل إلى أن تكون أكثر صعوبة). عادةً ما تكون عمليات التنشيط محدودة بعرض النطاق الترددي، ومن المعتاد أن يتعين على التنشيط قراءة المزيد من البيانات في المسار الخلفى أكثر من المسار الأمامى. (على سبيل المثال، قراءة التنشيط المسار الأمامى مرة واحدة، وتكتب مرة واحدة، وبينما تقرأ عملية التنشيط الخلفي مرتين، gradOutput وإخراج الأمام، وتكتب مرة واحدة، gradInput).

كما ترى، هناك بضعة أماكن يمكننا فيها توفير ذاكرة GPU أو تسريع العمليات. الآن بعد أن فهمت ما يؤثر على استخدام GPU وسرعة الحساب، راجع صفحة وثائق أساليب وأدوات التدريب الفعال على GPU واحد لمعرفة المزيد حول تقنيات تحسين الأداء.

< > Update on GitHub