מה זה kernel ב-CUDA - הפונקציה שרצה אלף פעמים בו זמנית¶
אם קראתם קוד CUDA כלשהו ונתקלתם במילה kernel, ומיד חשבתם על ליבת מערכת הפעלה, יש כאן בלבול שכדאי לפזר מיד. kernel ב-CUDA הוא מושג לגמרי שונה, והוא בעצם אחד הרעיונות הכי חשובים להבין כדי לכתוב קוד GPU בכלל.
אז מה זה בעצם¶
kernel הוא פונקציה שאתם כותבים בקוד CUDA שמיועדת לרוץ על ה-GPU, לא על ה-CPU. אבל ההבדל האמיתי לא ב"איפה" היא רצה, אלא ב"כמה פעמים" היא רצה. כשאתם קוראים לפונקציה רגילה ב-C, היא רצה פעם אחת. כשאתם משגרים kernel ב-CUDA, אתם בעצם אומרים לחומרה - תריצי את הפונקציה הזאת אלפי פעמים, במקביל, כל עותק שלה על נתונים קצת שונים.
בקוד זה נראה כמעט כמו קריאה רגילה לפונקציה, עם תוספת קטנה של תחביר מיוחד - סוגריים משולשים שמגדירים כמה עותקים אתם רוצים להריץ. מבחינת המתכנת, אתם כותבים פעם אחת מה קורה ל"thread בודד אחד", וה-GPU דואג להריץ את זה על כל ה-threads שביקשתם, בו זמנית ככל שהחומרה מאפשרת.
דוגמה שממחישה את זה¶
הדוגמה הכי קלאסית היא חיבור שני וקטורים גדולים. בקוד CPU רגיל הייתם כותבים לולאה שעוברת איבר איבר ומחברת אותם, אחד אחרי השני. ב-kernel של CUDA אתם כותבים פונקציה שמתארת מה קורה לאיבר בודד אחד - "קח את האינדקס שלך, חבר את שני האיברים המתאימים, שמור את התוצאה". ואז אתם משגרים את ה-kernel הזה עם מספר threads השווה לאורך הוקטור. אם הוקטור מכיל מיליון איברים, אתם מבקשים מיליון threads, וה-GPU מריץ כמה שהוא יכול בו זמנית, על פני אלפי ליבות חומרה, עד שהעבודה כולה מסתיימת.
זה ההבדל המהותי - במקום "עבור על הרשימה", אתם אומרים "כל איבר ברשימה, תדע לטפל בעצמך".
grid, block ו-thread¶
כדי לארגן את אותם אלפי threads בצורה הגיונית, CUDA משתמשת בהיררכיה. threads מאורגנים בתוך block, וכל ה-blocks יחד מרכיבים grid. זה נשמע מסובך אבל זה בעצם רק דרך לחלק את העבודה לקבוצות שניתן לנהל - כל block מקבל פיסת עבודה משלו, וה-GPU מפזר את ה-blocks על פני ה-SM השונים בחומרה, בדיוק כמו שתיארנו כשדיברנו על ארכיטקטורת GPU.
כל thread בתוך kernel יודע "מי הוא" דרך אינדקסים מובנים שהחומרה מספקת - איזה block הוא נמצא בו, ואיפה בתוך ה-block. השילוב של המידע הזה נותן לכל thread את המספר הסידורי הייחודי שלו, וכך הוא יודע בדיוק על איזה חלק מהנתונים לעבוד.
מה שחשוב להבין על ריצה במקביל¶
הנקודה שמבלבלת הכי הרבה מתחילים היא לחשוב על kernel כמו לולאה. זה לא לולאה. כל ה-threads לא רצים "אחד אחרי השני" - הם רצים ממש בו זמנית, על פני ליבות חומרה שונות, ואין שום סדר מובטח ביניהם אלא אם אתם דואגים לזה בעצמכם. זו בדיוק הסיבה שקוד GPU דורש דרך חשיבה שונה - אתם לא מתכננים רצף פעולות, אתם מתכננים איך אלפי עותקים זהים של אותה משימה יתנהגו בו זמנית מבלי לדרוך אחד על השני.
למה זה כל כך חשוב ללמוד נכון¶
כתיבת kernel נכון היא לא רק עניין של תחביר. איך אתם מחלקים את העבודה ל-blocks, איך threads ניגשים לזיכרון, וכמה משאבים כל thread צורך - כל אלה משפיעים ישירות על הביצועים, לפעמים בפער של פי עשר. זו הסיבה שברגע שיודעים לכתוב kernel בסיסי, השלב הבא הוא ללמוד לכתוב kernel יעיל, וזה כבר עולם שלם בפני עצמו.
בדיוק ככה בנוי קורס תכנות GPU - מתחילים מכתיבת kernel ראשון פשוט, וממשיכים דרך ניהול זיכרון, אופטימיזציית גישות, ועד לפרויקטים אמיתיים כמו כפל מטריצות עם tiling.
לסיכום¶
kernel הוא הפונקציה שמבצעת את העבודה בפועל על ה-GPU, אבל בניגוד לפונקציה רגילה, היא נועדה לרוץ אלפי פעמים במקביל, כל עותק על חלק אחר מהנתונים. ברגע שהרעיון הזה מתיישב, כל שאר עולם CUDA - blocks, grids, זיכרון משותף - מתחיל להרגיש הרבה יותר טבעי.
בואו תתחילו לכתוב את ה-kernel הראשון שלכם עם קהילה שתומכת בדרך.