שימוש בדיוק מעורב אוטומטי באמצעות PyTorch

הקדמה

מודלים עמוקים יותר צריכים יותר כח מחשב ומשאבי זיכרון. האימוץ של טרaining מהר יותר של רשתות עצביות נובע מפיתוח טכניקות חדשות. במקום השימוש בFP32 (פועל-דיוק צבעים מערביים), אפשר להשתמש בFP16 (חצי-דיוק צבעים מערביים), וחוקרים גילו שהשימוש בשניהם יחד הוא בעל ברירת מחדל טובה יותר.

טכניקת דחיסות מעורבת מאפשרת את הדחיסות החצי דיוק בעוד שהיא משמרת על הדיוק הגבוה ברשתות הסינגליסים. המונח "טכניקת דחיסות מעורבת" מתייחסת לעובדה שהשיטה משתמשת בשני סוגי הדיוק הזהיר – הגבוה והחצי.

בסקירה הזו של האוטומטית עם דחיסות מעורבת (AMP) בעזרת PyTorch, אנחנו מראים איך הטכניקה פועלת, בעיצוב צעד אחר צעד בתהליך של השימוש בAMP, ואנחנו מדברים על יישומים מתקדמים יותר של טכניקות AMP עם תשתיות קוד למשתמשים על מנת לשלב אותם מאוחר יותר בקוד המשלהם.

דרישות

ידע בסיסי של PyTorch: מו familiarity עם PyTorch, כולל את התפיסות המרכזיות כמו טנסורים, מודלים, והמחזור האמיתי של האימון.

הבנה של בסיסים של למידה עמוקה: תפיסות כמו רשתות עצביות, היפודרםפרציפציה, והתפתחות.

ידע על האימון ברמת הדיגיטל המעורבת: ידע על היתרונות והמנועים של האימון ברמת הדיגיטל המעורבת, כולל שימוש מוגבל בזכרון וחישוב מהר יותר.

גישה למחשב מתאמה: GPU שתומך על רמת הדיגיטל המעורבת, כמו NVIDIA GPU עם מעבדות Tensor Cores (לדוגמא, ארכיטקטורות Volta, Turing, Ampere).

הגדרת הסביבה הפייתונית והערימת CUDA: סביבה פייתונית Python עם PyTorch מותקנת וCUDA מוגדר עבור האיספקציה של GPU.

סקירה של הרמת הדיגיטל המעורבת

כמו רוב הפרימטים ללמידת עמיקה, PyTorch בדרך כלל מתאמן על מידע בסיסי 32 בתאומות רוח (FP32). לעומת זאת, FP32 איננו תמיד נחוץ להצלחה. אפשר להשתמש במידע בסיסי 16 בתאומות רוח עבור כמה פעולות, בהן FP32 לוקח יותר זמן וזכרון.

לכן, מהנדסי NVIDIA פיתחו טכניקה שמאפשרת את האימון ברמת הדיגיטל המעורבת בFP32 עבור מספר קטן של פעולות בעוד רוב הרשת מתפקדת בFP16.

  • המערך מועבר להשתמש בסוגית 16 במקום שאפשרי.
  • שמירה על השיא של FP32 כדי לאסוף את העדכונים בשינויי המשקל בכל ההיבט.
  • השימוש בסקלינג האובייקט כדי לשמר על הערכים הקטנטנים של הגרעינים.

שימוש בדיוק מעט ב-PyTorch

עבור האימוץ המעורבב של האימון, פיטורץ מציעה מספר תכונות שכבוצות בתוכו.
הפרמטרים של מודל מועברים ל-FP16 בזמן שאתה מקיש עליהם את השיטה .half(), והמידע של טנסור מועבר ל-FP16 בזמן שאתה מקיש .half() עליו. מתמטיקה מהירה ב-FP16 תישמש לביצוע כל העיבודים על המודלים או הטנסורים האלה. ספקי מתמטיקה של ניוויזיה (cuBLAS ו-cuDNN) מותמכים טוב על פיטורץ. המידע מתוך ערך העמודה FP16 מטפל בעזרת תאי המטאסורים לביצוע GEMMs והתמציתים. על מנת להשתמש בתאי המטאסורים ב-cuBLAS, המימדים של GEMM ([M, K] x [K, N] -> [M, N]) צריכים להיות מספרים רבים ב-8.

הכנסת APEX

השימוש ב-APEXבמחשבות המעורבבות של האימון משמש להאץ את ההאימון באותו הזמן שמשמר על הדיוק והיציבות של האימון בדיוק אחד. אפקס יכול לבצע עיבודים ב-FP16 או FP32, ולהשתלט על המעבר העליון לפרמטרים באופן אוטומטי, ולהגביר את הפסדים באופן אוטומטי.

אפקס נוצר כדי להקל על החוקרים לכך שיכללו את האימוץ המערבי-דיוקי במודלים שלהם. AMP, האות הקצר לאוטומטית מערבי-דיוקי, הוא אחד מהתכונות של Apex, ההרחבה הקלה הזו של פייטורץ'. כמה שורות נוספות ברשת שלהם הם כל מה שצריך לקבל את התועלת של אימוץ מערבי-דיוקי עם AMP. Apex נשקף ב CVPR 2018, ושימו לב שהקהילה של פייטורץ' הראתה תמיכה חזקה באפקס מאז שהוא יצא.

על ידי עיצוב של המודל המופעל בצורה קטנה, AMP מובילה למצב שבו אתה לא צריך לדאוג על סוגים מעורבים בזמן יצירת או הרצת הסקripט שלך. ההנחות של AMP אולי לא תואמות טוב כמות במודלים שמשתמשים בפייטורץ' בדרכים לא רגילות, אך יש אחריות להתאים את ההנחות האלה כך שיתאימו.

AMP מעניקה את כל היתרונות של אימוץ מערבי-דיוקי בלי צורך בסקלציות אובדן או תרגולות סוגים להעבדה באופן ברור. האתר GitHub של Apex מכיל הוראות על תהליך ההתקנה, והמידע הרשמי של ה API שלו נמצא פה.

איך מופעלים AMP

AMP משתמש בפרדיגמה רשימת הלבנים/רשימת האבודים ברמה ההגיונית. הפעלות של טנסור בPyTorch מכילות פעלות של רשתות עצבים כמו torch.nn.functional.conv2d, פעלות מתמטיות פשוטות כמו torch.log, ושיטות טנסור כמו torch.Tensor. add__. יש שלושה קטגוריות עיקריות של פעלות ביקום זה:

  • רשימת הלבנים: פעלות שיכולות להרוויח מאיץ המדידה של FP16. יישומים טיפוסיים כוללים המצאת מלאי והתמצית.
  • רשימת האבודים: הקלטים צריכים להיות בFP32 עבור פעלות בהן 16 בתומות של דיוק אולי לא מספיק.
  • כל השאר (כל הפעלות הנשארות): פעלות שיכולות להפעיל בFP16, אך עליה המחיר של ההעברה FP32 -> FP16 על מנת לבצע אותן בFP16 אינה משנה מפני שהאיץ אינו משמעותי.

המשימה של AMP פשוטה מאוד, לפחות בתאוריה. AMP מקבל אם פעלת PyTorch ברשימת הלבנים, ברשימת האבודים, או אינה בשום אחד לפני שיחילו אותה. כל הטעמים צריכים להיות מועברים לFP16 אם הם ברשימת הלבנים או FP32 אם הם ברשימת האבודים. אם אינם בשום אחד, רק אתם צריכים לוודא שכל הטעמים הם אחד סוג. המדיניות הזו אינה כל-כך פשוטה ליישם במציאות כמו שנראית.

שימוש בAmp בשילוב עם מודל PyTorch

על מנת להכיל Amp בשיript הקיים של PyTorch, עשה את הצעדים הבאים:

  • השתמש בספרית Apex כדי להיות מודל Amp.
  • התחיל את Amp כדי שיעשה את השינויים הנדרשים למודל, לאופטימיzer, ולפונקציות הפנימיות של PyTorch.
  • הערה את המקום בו מתרחשת החזרה החוצנית (.backward()) כך שAmp יכול להגביר את הפסד ולנקות את המצב הפנימי לאחר האיטירציה.

צעד 1

יש רק שורה אחת של קוד עבור הצעד הראשון:

from apex import amp

צעד 2

על מנת לשלב את הצעד הזה, שהוא רק שורה אחת באורך, חייבים להצעיד כבר את המודל המערכתי ואת האופטימיzer המשמשים להכשירה.

model, optimizer = amp.initialize(model, optimizer, opt_level="O1")

ההגדרות הנוספות מאפשרות לך להתאונן את הערכים והסוגים המבניים של Amp. הפעולה amp.initialize()מקבלת הרבה פרמטרים, בעיקר שלושה מהם:

  • models (torch.nn.Module או רשימה של torch.nn.Modules) – מודלים לשינוי/קסטור.
  • מוכנים (אופציonal, torch.optim.Optimizer או רשימת torch.optim.Optimizers) – מערכות העדפה לשינוי/קלט. הם הכרחיים להשתמש בהם בהתאמה, ונותרות ברצונו לביצוע הבחינה.
  • רמת העדפה (סרט מילים, אופציונלי, ברירת מחדל=“O1”) – רמת העדפה היוקרתית או מעורבת על-ידיים. הערכים המקבלים הם “O0”, “O1”, “O2” ו“O3”, והם מופיעים בפירוש במעלה. יש 4 רמות של עידוד:

O0 לאימוץ FP32: זוהי פעולה לא פעילה. אתה לא צריך לדאוג לגבי זה מפני שהמודל שמגיע לך צריך להיות FP32, וO0 עשוי לעזור להגיע לבסיס לדיוק.

O1 לאימוץ מעורב מיקסי (מומלץ בשימוש רגיל): לשינוי כל שיטה ושיטה Tensor ושיטה Torch להשתמש בשיטת קלט לבדיקה לבנה-שורשת. בFP16, עיבודים ברשימת הלבנה כמו ג' יממים וקונולוגיות מבצעים בעזרת Tensor Core. לדוגמה, Softmax הוא עיבוד ברשימת השיתוף הצבעוני שדורש FP32 דיוק. אם לא נאמר ברצונו אחרת, O1 גם משתמש בסקאלה דינמית של הפסד.

O2 לאימוץ "כמעט FP16": O2 מסייע את המשקלים של המודל לFP16, מתייחסים לשיטה הקדמה של המודל להעביר את המידע הקדמי לFP16, שומרים על הבצעים בFP32, שומרים על משקלי המודל בFP32 המ

O3 לאחסון FP16: O3 עשוי לא היה כה יציב כמו O1 וO2 במונחים של מדויק המיצול המעורב. לכן, עשוי להיות יעיל להגדיר מסלול בסיס למודל שלך, מול מה שהיעילות של O1 וO2 יכולה להיות בודקת.
התפקיד המונחה הנוסף keep_batchnorm_fp32=True ב O3 עשוי לעזור לך לקבע את "מהירות האור" אם המודל שלך משתמש בסינון מיצול, אפשר לאפשר סינון cudnn בצורה המעורבת.

O0 ו O3 אינם מעורבים במיצול מעורב, אך הם עוזרים להגדיר מסלול בסיס עבור דיוק ומהירות מסויימים. ישומה מעורבת מיצול מעורב מוגדרת כ O1 ו O2.
אתה יכול לנסות שניהם ולראות איזה שיפר את הביצועים והדיוק הכי גבוה עבור המודל הספציפי שלך.

שלב 3

וודא שאתה מזהה את המקום בה קורה העברה האחורה בקוד שלך.
יופיעות כמה שורות קוד שנראים כך:

loss = criterion(…)
loss.backward()
optimizer.step()

שלב 4

באמצעות המנהל התחום של Amp, אתה יכול להפעיל את הסקالה על ידי סיפור סידור אחורה פשוט:

loss = criterion(…)
with amp.scale_loss(loss, optimizer) as scaled_loss:
    scaled_loss.backward()
optimizer.step()

זה כל זה. עכשיו אתה יכול להרצית מחדש את הסקripט שלך עם האמצעות המעורבת באימוץ מיצול מעורב.

תפיסת הקריאות לפונקציות

פיטורכ (PyTorch) מחסור באובייקט של מודל סטטי או גרף כדי להתמקד עליו ולהכניס את ההספדים המצוcursor. בגלל שהוא חלק מאוד גמיש ודינמי, דרך "התיקוף הקרקעי" של הפעלות הנדרשות, אמפ (Amp) יכול להפעיל ולהספיד פרמטרים דינמית.

לדוגמה, ניתן להשתמש בקוד הנפוץ לראשונה כדי לוודא שהאגודלים לשימוש בשיטה torch.nn.functional.linear תועדים תמיד בפורמט 16בית:

orig_linear = torch.nn.functional.linear
def wrapped_linear(*args):
 casted_args = []
  for arg in args:
    if torch.is_tensor(arg) and torch.is_floating_point(arg):
      casted_args.append(torch.cast(arg, torch.float16))
    else:
      casted_args.append(arg)
  return orig_linear(*casted_args)
torch.nn.functional.linear = wrapped_linear

למרות שאמפ עשוי להוסיף הדרכים הטווח הקטנות לגמרי על מנת לעזור לקוד להיות יותר עמיד, קריאת Amp.init() ממש גורמת לתיקוף הקרקעי להיווצר בכל הפעלות הרלוונטיות בפיטורכ כך שהאגודלים יהיו מוספדים בצורה נכונה בזמן ההתנהגות.

מינימזציית הספידים

כל משקל נותן רק ספיד FP32 -> FP16 פעם אחת בכל התהליך מפני שאמפ שומר על אסף פרמטרים ספיד פנימי ומשתמש בהם בהתאמה לצורך. בכל התהליך, המנהל הסינון להגברה מדגיש לאמפ באיזה זמן לנקות את האסף.

הספידים עצמים והגדלת הגרפים בעזרת פיטורכ

"אימוץ הערכת הדיוק מעורבת אוטומטי" מתייחסת לשילוב של torch.cuda.amp.autocast ו torch.cuda.amp.GradScaler. בשימוש ב torch.cuda.amp.autocast, ניתן להגדיר אוטוקאסטים רק עבור אזורים מסויימים. האוטוקאסט מבחר במדידה הדיוק המתאימה עבור הערכים על הGPU במטרה לאופיין את היעילות בעוד שמשמר על הדיוק.

האינスטנציות torch.cuda.amp.GradScaler עוזרות לביצוע את השלבים של הקצב הגראדיאלי. הקצב הגראדיאלי מפחית את התדירות התחתונה, שעוזר לרשתות עם שיפורים בגרדיאל 16 להגיע להתאמה טובה יותר.

הנה קטע קוד שמראה איך להשתמש באוטוקאסט() על מנת לקבל את הדיוק המעורב המוטול בPyTorch:

# יוצר מודל ואופטימיזר בדיוק ברגיל
model = Net().cuda()
optimizer = optim.SGD(model.parameters(), ...)

# יוצר אינסטנץ torch.cuda.amp.GradScaler פעם אחת בהתחלה של האימון.
scaler = GradScaler()

for epoch in epochs:
    for input, target in data:
        optimizer.zero_grad()

        # מבצע את המסלול הקדמי עם אוטוקאסטים.
        with autocast(device_type='cuda', dtype=torch.float16):
            output = model(input)
            loss = loss_fn(output, target)

        # הפעולות האחוריות מבצעות באותו סוג הדיוק שהאוטוקאסט בחר עבור הפעולות הקדמיות התואמות.
        scaler.scale(loss).backward()

        # scaler.step() קודם כל מפעיל את השיפורים המקצועיים של האופטימיזר על הפרמטרים המיועדים.
   
        scaler.step(optimizer)

        # עודף את הגודל לשיבוט הבא.
        scaler.update()

אם למשמעות סימטרית אחת באופרציה יש קלטים float16, אז המשמעות החזרה לאופרציה הזו ייצרת גראדיאטים float16, וfloat16 אולי לא יכול לביטא גראדיאטים עם מגה קטנה.

העדכון של הפרמטרים הקשורים יאבדו אם הערכים האלה יוזרקו לאפס ("תת עליפה").

סקאלצ 'יינג של הגראדיאטים היא טכניקה שמשתמשת במפלק סקלצ' ייןג כדי להרביע את האובדן של המשמעות ואחר כך לבצע משמעות חזרה על האובדן המסותם כדי למנוע תת עליפה. נחוצה גם להגביר את הגראדיאטים שמזריקים לאחור דרך הרשת על ידי אותו מפלק. כתוצאה מכך, הערכים של הגראדיאטים יש מגה גדולה יותר, שמנע אותם מלהיות זורקים לאפס.

לפני עדכון הפרמטרים, הגראדיאטים של כל פרמטר צריך להושטע כך שהמפלק הזה לא יתערב בקצב הלמידה. גם autocast וGradScaler יכולים להשתמש בנידונים בגלל שהם מודולארים.

עבודה עם גראדיאטים לא מוסקלים

ציפיית גראדיאטים

אנחנו יכולים להתאים את כל השינויים בשימוש בשיטה Scaler.scale(Loss).backward() השימוש ב .grad תכונות של הפרמטרים בין backward() ל scaler.step(optimizer) חייבות להיות לא מוסכמות לפני שינוין או בדיקה. אם רוצים למגבל את הגדלה הגלובלית (ראו torch.nn.utils.clip_grad_norm_()) או הגודלה המירבית (ראו torch.nn.utils.clip_grad_value_()) של קבוצת השינויים שלך לפחות או אותה מספר מסויים (קרוב לוודאי רמה מיועדת של המפסד), אתה יכול להשתמש בטכניקה הנקראת "קיצונית השינויים."

קיצונית השינויים ללא הפרידה מהמוסך תיבעת בגדלה הגלובלית/הגודלה המירבית של השינויים מוסכמים, שינייר את הקיצוניה המבוקשת (שהיתה אמורה להיות קיצונית לשינויים ללא המוסך). השינויים שבתוך האופטימיzer מופרדים בעזרת scaler.unscale (optimizer).
אתה יכול להפריד את השינויים של פרמטרים אחרים שנתנו קודם לאופטימיzer אחר (כמו אופטימיzer1) בעזרת scaler.unscale (optimizer1). אנחנו יכולים להדגים את התפיסה הזו על-ידי הוספת שתי שורות קוד:

# מפריד את השינויים של פרמטרים של optimizer במקום
        scaler.unscale_(optimizer)
# בגלל שהשינויים של פרמטרים של optimizer נפרדים, מקצועים כרגע: 
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)

עבודה עם הגבידות מוסקלים

אסטרבלציה של הגבידות

אסטרבלציית הגבידות מבוססת על היגיון פשוט בלתי סביר. במקום לעדכן את הפרמטרים של המודל, היא מחזיקה ומסכמת את הגבידות בין הטורים המתאחדים על מנת לחשב את הפסד והגבידות.

אחרי מספר טורים מסויים, הפרמטרים מעדכנים לפי הגבידות המצטברת. הנה פיסת קוד על איך להשתמש באסטרבלציית הגבידות בעזרת pytorch:

scaler = GradScaler()

for epoch in epochs:
    for i, (input, target) in enumerate(data):
        with autocast():
            output = model(input)
            loss = loss_fn(output, target)
            # הונרמליזציית הפסד 
            loss = loss / iters_to_accumulate

        # מצטברת גבידות מוסקלים.
        scaler.scale(loss).backward()
          # עדכון משקלים
        if (i + 1) % iters_to_accumulate == 0:
            # אולי להסיר את המוניטין פה אם רצות 
            scaler.step(optimizer)
            scaler.update()
            optimizer.zero_grad()
  • האסטרבלציית הגבידות מוסיפה גבידות לתוך גודל הטור המתאים של batch_per_iter * iters_to_accumulate.
    הקנה המידה צריך להיות מוכוון לטור היעיל; זה מתכוון לבדיקה של גבידות inf/NaN, להימנעה מהצעד אם מוצאים אף אחד מהגבידות האלה, ולעדכון הקנה המידה לרזור הטור היעיל.
    חשוב גם לשמור על גבידות מוסקלות ופיקוד קנה המידה סטנדרטי בזמן שהגבידות עבור טור היעיל מסויים מוספים.

אם הדרגות לא מוסכמות (או שמספר הערך שלהם משתנה) לפני שליטתם, העברה החזרה הבאה תוסיף דרגות מוסוכמות לדרגות לא מוסכמות (או לדרגות שמוסכמות על ידי מספר אחר) אחרי מה זה יהיה בלתי אפשרי להשתמש בדרגות הלא מוסכמות המצטברות. עליך ליישם את השלב הבא.

  • ניתן להפוך את הדרגות לא מוסכמות על ידי שימוש ב unscale קצת לפני השלב, אחרי שכל הדרגות המוסכמות עבור השלב הבא נצטברו.
    כדי לוודא שמס 'מבנה יעיל מושלם, פשוט להתקשר update בסוף כל ההיפציות בהן קודם לכן התקשרתם step
  • enumerate(data) פונקציית זה מאפשרת לנו להשאר על מדידה של מס 'מבנה בזמן ההיפציה דרך הנתונים.
  • חלק את האובדן המתרחש על פי iters_to_accumulate(loss / iters_to_accumulate). זה מפחית את התרומה של כל מיני-מבנה שאנחנו מעבדים על ידי הגדלת את האובדן. אם אתה ממצה את האובדן בתוך כל מבנה, החלקה כבר נכונה ואין צורך בהגדלה נוספת. השלב הזה אולי לא נחוץ לגמרי בהתאם לאופן בו אתה מחשב את האובדן.
  • בעוד אנחנו משתמשים ב scaler.scale(loss).backward(), פיטורכי מצטבר את הגראדיאנטים המונחצים ושומר עליהם עד שאנחנו מקבלים optimizer.zero_grad().

דין גראדיאנטי

בזמן שאנחנו מיישמים דין גראדיאנטי, torch.autograd.grad() משמש לבנות את הגראדיאנטים, הם מושבבים ביחד כדי ליצור את ערכי הדין, ואחר כך מווספים לפסד. דין ה-L2 ללא הגבלה או אוטוקסטים מוצג בדוגמה הבאה.

for epoch in epochs:
    for input, target in data:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)

        # יוצר גראדיאנטים
        grad_prams = torch.autograd.grad(outputs=loss,
                                          inputs=model.parameters(),
                                          create_graph=True)

        # מחליט את המונחה של הדין ומוסף אותה לפסד
        grad_norm = 0
        for grad in grad_prams:
            grad_norm += grad.pow(2).sum()
        grad_norm = grad_norm.sqrt()
        loss = loss + grad_norm

        loss.backward()

        # אתה יכול לקלוט את הגראדיאנטים כאן

        optimizer.step()

עכשיו, הטנסורים שנתתים ל torch.autograd.grad() צריכים להיות מונחצים כדי ליישם דין הגראדיאנטי. נחוצה להפעיל את ההתחמקות לפני שילוב הגראדיאנטים כדי לקבל את ערכי הדין. בגלל שערך הדין הזה חלק מהמהלך הקדמי, הוא צריך להתרחב בתוך ההקשר של האוטוקסטים.
עבור אותו דין ה-L2, כך הוא נראה:

scaler = GradScaler()

for epoch in epochs:
    for input, target in data:
        optimizer.zero_grad()
        with autocast():
            output = model(input)
            loss = loss_fn(output, target)

        # מבצע סקאלינג אובדן עבור החזרה של autograd.grad, התוצאה #scaled_grad_prams
        scaled_grad_prams = torch.autograd.grad(outputs=scaler.scale(loss),
                                                 inputs=model.parameters(),
                                                 create_graph=True)

        # יוצרת grad_prams לפני החישוק בעונש (grad_prams חייבת להיות #לא מוסקלת). 
        # בגלל שאף אופטימיזר לא מבעל את scaled_grad_prams, משמשים חלון חישוק במקום אחד על ידי scaler.unscale_:
        inv_scaled = 1./scaler.get_scale()
        grad_prams = [p * inv_scaled for p in scaled_grad_prams]

        # מחולקים ומוסיפים את המשוואה לעונש. 
        with autocast():
            grad_norm = 0
            for grad in grad_prams:
                grad_norm += grad.pow(2).sum()
            grad_norm = grad_norm.sqrt()
            loss = loss + grad_norm

        # יש להגביר את הגיוס לקריאה האחורית.
        # אובדנים על הענף מוסוולים בצורה מתאימה.
        scaler.scale(loss).backward()

        # ניתן להפעיל unscale_ כאן 

        # השלבים step() ו update() מתרחשים כרגע.
        scaler.step(optimizer)
        scaler.update()

עבודה עם מודלים רבים, הפסדים ואופטימיזרים

Scaler.scale צריך להוצע על כל ארע אבדן ברשת שלך אם יש לך הרבה מהם.
אם יש לך הרבה אופטימיZרים ברשת שלך, אתה יכול לבצע scaler.unscale על כל אחד מהם, ואתה צריך להוצע scaler.step על כל אחד מהם. אך את scaler.update צריך להשתמש רק פעם אחת, אחרי הצעדים של כל האופטימיZרים המשמשים בהתבסס על השיבץ הזה:

scaler = torch.cuda.amp.GradScaler()

for epoch in epochs:
    for input, target in data:
        optimizer1.zero_grad()
        optimizer2.zero_grad()
        with autocast():
            output1 = model1(input)
            output2 = model2(input)
            loss1 = loss_fn(2 * output1 + 3 * output2, target)
            loss2 = loss_fn(3 * output1 - 5 * output2, target)

       #למרות שהשימור בגרף אינו קשור לAMP, הוא נמצא בדוגמה הזו מפני ששני הקריאות backward() משתפים את אותן אזורים בגרף. 
        scaler.scale(loss1).backward(retain_graph=True)
        scaler.scale(loss2).backward()

        #אם אתה רוצה להסתכל או לשנות את הגרדיאנטים של הפרמטרים שלהם, אתה יכול לSpecify איזה אופטימיZרים מקבלים הפרדה ברורה. .
        scaler.unscale_(optimizer1)

        scaler.step(optimizer1)
        scaler.step(optimizer2)

        scaler.update()

כל אופטימיZר מביט בגרדיאנטים שלו עבור infs/NaNs ועושה דיעבדה בודדת אם לעשות או להיעלם את הצעד. חלק מהאופטימיZרים עשויים להיעלם את הצעד, בעוד אחרים אולי לא. הדלילה מתרחשת רק פעם אחת לכל מספר מאות התבססים; לכן היא לא תשפיע על ההתמדה. עבור מודלים עם מספר אופטימיZרים, תוכל לדווח על הבעיה אם תראה שההתמדה גרועה אחרי הוספת ההפרדה בגרף לגרדיאנטים.

עבודה עם מספר גפיים מרובים

אחד הבעיות המשמעותיות במודלים עיצוב עמוקים הם שהם נעשים יותר גדולים מאשר להאמן על גפיים יחיד. יכול לקחת יותר מדי זמן לאמן מודל על גפיים יחיד, ועל ידי אמן מודלים במספר גפיים ניתן לגדל אותם בקצב הגבוה ביותר שהם יכולים. חוקר ידוע ספציפי הצליח להקטין את מהלך האמנות של ImageNet משבועיים ל-18 דקות או לאמן את הטרנספורמר-XL הרחב ביותר והמתקדם ביותר במהלך שבועיים במקום ארבע שנים.

DataParallel ו DistributedDataParallel

אינטגרציית הגדילה ברמה הזו לא מפתיעה את איכות התוצאות, ופיטורך מציע את השילוב הטוב ביותר בין קלות השימוש לבין השליטה. nn.DataParallel ו nn.parallel.DistributedDataParallel הם שתי תכונות בפיטורך עבור התפשטת האימון על מנת הגדילה במוליכות מסויימות במחשב הגלובלי. אתה יכול להשתמש בקרובים הקלים האלה ובשינויים כדי לאמן את הרשת על מספר מגפים.

DataParallel בתהליך אחד

במכונה אחת, DataParallel עוזר להתפשט את האימון על המגפים הרבים.
בואו נסתכל קרוב יותר על איך DataParallel באמת עובד במעשה.
כשמשתמשים בDataParallel לאמן רשת עצבים, מתרחשים השלבים הבאים:

מקור:

  • המיני-באץ מוחלק על GPU:0.
  • מפצל ומשתפץ המיני-באץ לכל הGPUים הזמינים.
  • המודל מעתיק להם.
  • ההעברה הקדמית מתרחשת על כל הGPUים.
  • מחשבת האובדן ביחס לתוצאות הרשת על GPU:0, וחזרה של האובדן לכל הGPUים השונים. על כל הGPUים צריך לחשב את הגרדיאנטים.
  • סכם העקומות על הGPU:0 ויישם את האופטימיzer כדי לעדכן את המודל.

על דבר, הדאגות שנדברו כאן מיועדות רק לautocast. התנהגות GradScaler נשארת בלתי שינויה. לא משנה האם torch.nn.DataParallel יוצר תהליכים לכל מכשיר כדי לבצע את ההדרכה הקדמית. מצב הautocast מודבק בכל אחד מהם, והדבר הבא יעבד:

model = Model_m()
p_model = nn.DataParallel(model)

# מציב את הautocast בשרת העיקרי
with autocast():
    # יהיה autocast ב p_model. 
    output = p_model(input)
    # loss_fn גם יהיה autocast
    loss = loss_fn(output)

DistributedDataParallel, מכשיר גפולוגי אחד לתהליך

מדריך ההגדרות לtorch.nn.parallel.DistributedDataParallel מעצה לשימוש בגPU אחת לתהליך במהירות הטובה המושלמת. במצב זה, אף על פי שDistributedDataParallel לא משתחררת threads פנימית; לכן שימוש בautocast וGradScaler אינו מושפע.

DistributedDataParallel, מספר גPU לתהליך

כאן, torch.nn.parallel.DistributedDataParallel עשויה לייצר thread צדדי כדי לבצע את העברה הקדמית על כל מכשיר, כמו torch.nn.DataParallel. התיקון אותו: יש ליישם autocast בתוך שימושי הקדמה של המודל שלך כדי לוודא שזה מופעל בthreads הצדדיים.

סיכוי

במאמר זה, אנחנו הבאים את:

  • היצע של Apex.
  • הבחנו באיך Amps פועלים.
  • הבחנו באיך לבצע קידום גרדיאנט, קישור גרדיאנט, אספקלציה גרדיאנטית ודיעבד הפנימי.
  • הבחנו באיך אנחנו יכולים לעבוד עם מודלים מרובים, אובדנים ומתקנים.
  • הבחנו באיך אנחנו יכולים לבצע DataParallel בתהליך אחד כשאנחנו עובדים עם מספר גPU.

הפניות

https://developer.nvidia.com/blog/apex-pytorch-easy-mixed-precision-training/
https://nvidia.github.io/apex/amp.html
https://discuss.pytorch.org/t/accumulating-gradients/30020
https://towardsdatascience.com/how-to-scale-training-on-multiple-gpus-dae1041f49d2

Source:
https://www.digitalocean.com/community/tutorials/automatic-mixed-precision-using-pytorch