به عنوان یک تجربه ای ارزنده که در مسیر یادگیری کودا بدست آوردم برخود لازم دانستم تا آن را با شما به اشتراک بگذارم در ابتدای نصب و کار با پلتفرم کودا برای این که با مشکلی موجه نشوید حتما سخت افزار کارت گرافیکتان را در سایت NVIDIA بررسی نمایید که آیا پلتفرم CUDA را پشتیبانی می کند یا خیر؟چون کارت گرافیک نصب شده روی سیستم من NVIDIA GEFORCE 210 بود و اطمینان خاطر داشتم که کودا را پشتیبانی می کند ،حتی در حین نصب کودا هم یک خطای سطحی داد و کار نصب را تکمیل نمود (شکل 1)
شکل1
اما در زمان اجرای کد (از همان کدهای sample کودا )با خطایی که در شکل 2 آمده مواجه شدم که این خطا مربوط به انتقال داده از CPU به GPU بود و نمی توانست کرنل کودا را اجرا نماید.
بعد از چند روز جستجوی وسیع در سایتهای مختلف متوجه این موضوع شدم که NVIDIA GEFORCE 210 از اولین سری کارتهای گرافیک NVIDIA بوده که کودا را پشتیبانی می کرده است و با نسخه های قدیمی سازگاری دارد ،سپس با بررسی لیست سخت افزار های کارت گرافیک از سایت NVIDIA و تعویض کارت گرافیک با GEFORCE GTX 1050 مشکل حل شد.شما می توانید با مراجعه به آدرس زیر سخت افزار کارت گرافیک خود را بررسی نمایید.
https://developer.nvidia.com/cuda-gpus
و لازم بذکر است بهترین محیط برای برنامه نویسی کودا Visual Studio 2013 با پلتفرم کودا 7.5 و یا 8 است.
و امّا ترتیب نصب ملزومات کودا :
1- نصب درایور کارت گرافیک
2- نصب ویژوال استودیو2013
3- نصب مازول ++C
4- نصب NSIGHT
5- نصب پلتفرم CUDA
شکل4:GEFORCE 210
که شما می توانید با مراجعه به لینک http://cuda-tabatabaei.blogsky.com/category/Programming بصورت دقیقتر مراحل را مشاهده نمایید.
در ابتدا بهتر است اطمینان حاصل کنید که کارت گرافیک کامپییوترتان مبتنی بر کودا ست به یکی از روش زیر می توانید اقدام کنید
1-کیلدهای ترکیبی win+R به منظور اجرای پنجره Run همزمان فشار دهید کلمه dxdiag را تایپ کنید پنجره Directx properties باز میشود . تب ِDisplay را اتنخاب کنید در این قسمت به شما مشخصات کارت گرافیک تان را نشان میدهد .
شکل 4
2- پنجره Control Panel را باز کنید ((Start - Control Panel برروی سیستم دو بار کلیک نمایید در پنجره properties برروی تب HaedWard سپس Device Manager کلیک کنید . لیستproperties Directx را باز کنید در نتیجه نام و مدل کارت گرافیکتان را خواهید یافت .
(شکل 5)
نیازمندی های سیستم در کودا
برروی استفاده از کودا بروی سیستم تان مورد زیر بایستی نصب شده باشد :
1-یک GPU مبتنی بر کودا
2-یک ورژن از ویندوز ساپورت شده (از Windows Seven به بعد )
3- یک ورژن از Visual Studio ساپورت شده از VS2010به بعد
4-ابزار nvidia cuda که در سایت cuda – downloads موجود می باشد .
می توانید برنامه cuda را با Proxy از سایت NVida.com ویا برخی از نسخه ها را از softgozer تهیه کنید که حجم ان با برنامه Nsight در حدود 1.02گیگابایت می باشد .جهت نصب با توجه به نسخه دانلود شده cuda برروی بستر OS دلخواه که در اینجا Version 8.0.4.4 برWindows 10 میباشد مراحل زیر را انجام می دهیم : لازم به ذکر است که قبل از نصب Cudaمی بایست برروی سیستم Visual Studio نصب شده باشد که برنامه Cuda به عنوان یکی از Feature ها روی ان بار گذاری شود . پس از دریافت از سایت softgozer فایل Cuda -8.0.4.4 Win10.exe را اجرا می نماییم که در ابتدا در مسیر جهت Extract برنامه CUDA سوال می کند.
شکل 6
پس ار ان شروع به Extract میکند
شکل 7
پس از اتمام شدن قسمت Extract بصورت AutoRun برنامه Setup Cuda اجرا می شود و شروع به چک کردن سیستم در رابطه با OS .وکارت گرافیک می کند البته دقت داشته باشید شاید بعضی مواقع Cuda بروی سیستم ناسازگار نصب شود اما هنگام اجرای برنامه به مشکل برخورد می نمایید .
شکل 8
وپس از چک کردن سیستم چنانچه به مشکل برخورد نکنید پنجره ذیل ظاهر می گردد
شکل 9
که با انتخاب گزینه Accept و زدن دو عدد next شروع به نصب Package cuda می کند
شکل 10
پس از اتمام نصب Cuda برنامه Visual Studio را اجرا نمایید و با انتخاب گزینه New Project سمت چپ منو nvidia و زیر منوی Runtime 8.0 Cuda ایجاد شده است که با انتخاب ان و تعیین نمودن نام پروژه و Solution Name گزینه ok را اجرا می نماییم
شکل 11
پس از انتخاب گزینه ok شروع به ساخت پروژه می نماید و یک Sample جمع دو برابر 5 عنصری را ایجاد می نماید که میتوان از ان جهت اموزش Cuda استفاده خیلی مفید نمود.
شکل 12
دستورات مهم مدیریت حافظه در CUDA
اصطلاحات در کارتهای گرافیکی
Shaders ها برنامه های کوچکی هستند که میتوانند روی GPU اجرا شوند . این برنامه ها روی بلاک های GPU پخش می شوند و به صورت موازی اجرا می شوند . و در دو نوع Vertex و Pixcel وجود دارد . کارت های گرافیکی از واحدهای سایه زنی Vertex Shader و Pixel shader استفاده می کنند .
سایه زن راس : Vertex Shader : در پردازندهای گرافیکی واحدی وجود دارد که وظیفه ی ساخت پیکره بندی و اسکلت اجسام را برعهده دارد . این واحد سایه زن راس نام دارد . هر چه تعداد این واحدها بیشتر باشد . پردازنده یگرافیکی قدرتمندتر عمل میکند . در کارت های گرافیک جدید . سایه زن جای خود را به پردازندهای جریانی واگذار کرده است که به انها SM یا Streaming Multiprocessor گفته میشود . این برنامه بیشتر در autocad,corel استفاده می شود .
سایه زن پیکسل PIXCEL SHADER : : پردازندهای گرافیکی برای ان که بتواند به اجسام ساخته شده توسط سایه زن روح ببخشد و ان ها را در دنیای واقعی نزدیکتر کنند به واحدی به اسم سایه زن پیکسل نیاز دارند . در حقیقت سایه زن پیکسل رنگ و میزان نور هر یک از پیکسل های اجسام ساخته شده توسط سایه زن راس را تعیین میکند . این سایه زن موجب ایجاد اثرات . سایه ها .روشنایی .ماتی ودیگر پدیده های یک تصویر می شود .به دلیل نقش مهمی که این سایه زن در یک پردازنده گرافیکی دارد. عموما تعداد انها نسبت به تعداد سایه زن راس بیشتر است . به عنوان مثال کارت گرافیک GeForce 7800GTX دارای 48 عدد سایه زن پیکسل و هشت پردازنده ی راس می باشد.
پردازنده ی جریانی : Stream Multiprocessor : پردازنده های گرافیکی مبتنی بر Directx نسخه ی 10 شامل تغییرات عمده ای در معماری شدند .در این پردازنده های گرافیکی سایه زن و پیکسل حذف شدند و پردازنده جریانی جایگزین انها شدند . هر پردازنده جریانی تقریبا مشابه با یک پردازنده ی بسیار کوچک است و به تنهایی وظایف مربوط به سایه زن های راس و پیکسل را انجام می دهد .پردازنده های گرافیکی شرکت NVIDIA از سری 8000 به بعد و شرکت ATI از 2000 به بعد دارای این پردازنده های جریانی هستند . به عنوان مثلا پردازنده ی گرافیکی شرکت انویدیا با نام GEFORCE GTX 285 دارای 240 پردازنده ی جریانی هست که هر یک به تنهایی می تواند وظایف سایه زنی راس و پیکسل را انجام دهد.)شکل شماره 10(
8- برنامه نویس ناهمگن
برنامه نویسی ناهمگن گونه ای از برنامه نویسی است که در ان قست های مختلف کد تولید شده . برروی گونه های مختلفی از پردازنده اجرا میشود . به عنوان مثال می توان کدی را تصور کرد که برخی از قسمت های ان بر روی CPU و برخی از اجزای دیگر ان بر روی پردازنده های کارت گرافیکی اجرا می شوند . این مفهوم می تواند کمک شایانی به بهره بری بیشتر از توان محاسباتی رایانه ها کند. چرا که می توان از همه ی ظرفیت ها پردازشی یک سامانه به طور همزمان استفاده کرد .بدیهی است که برای استفاده حداکثری از این ظرفیت باید چار چوب فراهم شودکه توسعه چنین کدهایی را تسهیل نماید . معماری CUDA چار چوبی مناسب برای توسعه ی برنامه ی ناهمگن است . این معماری بستری را فراهم می کند که به کمک ان می توان کدهایی ایجاد کرد که قطعه های مختلف ان توانایی اجرا شدن بر روی پردازنده های کارت های گرافیکی و یا پردازنده های اصلی رایانه را دارند . در این معماری دو سکوی اجرایی با نام های host platform device platform درنظر گرفته شده است . سکوی host شامل یک یا چند پردازنده ی معمولی یا چند هسته ای د رکارت های گرافیکی NVIDIA است و قطعه ی کد مربوط به ان در نخ های اجرایی بسیار سبک و به صورت موازی اجرا می شوند شکل 11 این دو سکوی اجرایی را نشان می دهد
کدی که بر روی سکوی HOST اجرا می شوند یک کد عادی است و با زبان های معمول برنامه نویسی مانند C نوشته و کامپایل می شوند .اما کد دستوری device به علت ساختار اجرایی متفاوتش APIها و نیز دستورات سطح بالای خاصی دارد بنابراین باید با کامپایلری مناسب و متفاوت از کامپایلرهای عادی کامپایل شود . انم این کامپایلر nvcc است که به همراه تمامی ان چیزی که برای توسعه کد های device مورد نیاز است در یک بسته به نام CUDA Toolkit گرد اوری شده است
شکل13:اجرای برنامه ها توسط پردازنده های مختلف در قالب یک برنامه
9- محاسبات ناهمگن در پردازنده گرافیکی
در یک دسته بندی کلی . محاسباتی را که در یک رایانه انجام می شود می توان به دو دسته موازی (همزمانی ) و سریال (همگامی ) تقسیم کرد . منظور از محاسبات موازی محاسباتی است که از یکدیگر مستقل بوده و امکان انجام انها به صورت همزمان وجود دارد . محاسبات سری بر خلاف محاسبات موازی به یکدیگر وابسته بوده و باید به ترتیب و یکی پس از دیگری انجام شوند . پردازنده مرکزی به لحاظ دارا بودن برخی از خصوصیات فنی از نظیر حافظه کش و انشعابهای زیاد توانایی بالایی برای انجام محاسبات سری را دارد . برعکس پردازنده گرافیکی به دلیل توانایی هایی بالایی که در انجام محاسبات شناور دارد برای محاسبات موازی بسیار ممکن است و می تواند انها را بسیار سریعتر انجام دهد . بتابراین به نظر میرسد که برای رسیدن به بالاترین راندمان باید از پردازنده مرکزی در محاسبات سری واز پردازنده گرافیکی در محاسبات موازی استفاد کرد . محاسبات ناهمگن همین انتخاب درست پردازنده برای هر نوع از محاسبات و عملکردها می باشد .مدل محاسبات پردازنده گرافیکی استفاده از یک CPUو یک GPU به همراه یکدیگر در یک مد پردازشی ناهمگن است . قسمت ترتیبی کاربرد روی CPU وقسمت سنگین محاسبات روی GPU اجرا می شود . از دیدگاه کاربر کاربد ها سریعتر اجرا می شوند زیرا ازکارایی بالای GPU و موازی سازی در افزایش کارایی استفاده شده است .CPU و GPU فضا های حافظه های مجزایی دارند و هیچ دسترسی مستقیمی به حافظه GPU وجود ندارد. برای اینکه کاربردها بتوانند از حافظه GPU استفاده کنند باید داده ها ابتدا در حافظه GPU ذخیره شوند . کد میزبان (CPU) انتقال داده به حافظه دستگاه (GPU ) را مدیریت می کند و سپس محاسبات موازی روی GPU انجام میشود . کد میزبان مسئول تخصیص و باز پس گیری حافظه از GPU است یکی از مدهای عملیاتی GPU یه صورت شکل زیر است .( شکل شماره 14)
1-داده از حافظه اصلی CPU به حافظه GPU کپی میشود
2- CPU دستور العمل های پردازشی را برای GPU ارسال می دهند
3- هسته های GPU دستور العمل هایی را به صورت موازی انجام می دهند .
4- نتیجه از حافظه GPU به حافظه اصلی کپی می شود .
10- معماری داخلی هسته کودایی یک کارت گرافیک
GPU های Fermi حداکثر دارای 512 هسته کودایی هستند که هر هسته کودایی در هر کلاک یک دستور العمل اعشاری با دقت تکی یا یک دستور العمل صحیح را به ازای هر thread اجرا می کند . فرکانس کاری فرمی حدود 1.5 گیگا هرتز می باشد . یعنی می توان 1.5 میلیارد عملیات ممیز شناور را در هر ثانیه انجام داد . این 512 هسته کودایی به 16 گروه تقسیم می شوند که به هر گروه یک(Streaming Processor) SM گفته میشود .بنابراین هر SM دارای 32 هسته کودا خواهد بود که به ان پردازنده کودا نیز گفته میشود . در شکل 15 SM ها در صورت مستطیل های سبز رنگ نمایش داده شده اند و هر کدام شامل 32 هسته هستند . داخل هر بسته کودا یک ALU صحیح و یک ALU واحد ممیز شناور قرار دارد هر کدام از این 16 عد SM یک کش L2 با ظرفیت 768 کیلو بایت احاطه می کند. هر SM دادای بخش های کش . رجیستر Warp Dispatch می باشد که دو بخش انتهایی وظیفه اماده شدن برای پردازش بعدی را به عهده دارد.
شکل شماره 15:ساختار داخلی هسته کودایی کارت گرافیک شرکت NVIDIA با معماری فرمی
داخل هر SM دارای 4 ستون است که شامل 32 هسته کودا،16واحد لود و ذخیره (LD /SD) 4 واحد توابع خاص SFU می شود واحد لود و ذخیره ادرس های مبدا و مقصد را برای 16 Thread در هر کلاک محاسبه می کند . واحد SFU انواع خاصی از دستور العمل ها را مثل کسینوس و جذر را محاسبه می کند . هر SUF نیز می تواند یک دستور العمل را در هر کلاک اجرا کند.حداکثر 32Thread موازی در هر SM زمان بندی می شوند تا اجرا شوند که به این کار WARP گویند. این زمان بندی در واحدی به همین نام یعنی WARP انجام می شود .هرSM دارای 2 واحد زمانبندیWARPو دو واحدDispatch است و این پیکربندی می تواند دو Warp را همزمان اجرا کند GPU های فرمی داری 6 کنترلر حافظه 64 بیتی هستند که در شکل 15 به صورت DRAM نشان داده شده است .این کنترلرها، دادها را به هسته های CUDA تحویل می دهند
شکل شماره 16:معماری داخلی یک SM
11- نحوه انجام محاسبات در GPU
عمدتا محاسبات مربوط به کاربردهای گرافیکی شامل محاسبات ریاضی ماتریسی است زیرا در هر لحظه نیاز به پردازش ماتریس پیکسل های تصویر داریم . بدیهی است که با افزایش میزان تفکیک پذیری تصاویر حجم این ماتریس ها بزرگ می شود و اگر بخواهیم کل محاسبات ماتریسی خود را به تنهایی و توسط پردازنده های اصلی سیستم انجام دهیم امکان ناتوانی پردازنده درپاسخ به نیازهای برنامه های گرافیکی وجود خواهد داشت . لذا برای غلبه براین مشکل کارت های گرافیکی پای به عرصه وجود گذاشتند . هرکارت گرافیکی حاوی تعداد نسبتا زیادی ریز پردازنده است که به صورت موازی کار می کنند و هر کدام از ان قسمتی از ماتریس تصویر را پردازش می کند . به عبارت بهتر در پردازش یک تصویر خاص کل ماتریس ان تصویر به تعدادی زیر ماتریس کوچکتر تقسیم شده و محاسبات انها به صورت موازی ودر نخ های پردازشی بسیار سبک صورت میگیرد . مثلا به جای اینکه محاسبات مربوط به یک ماتریس 400*400 را به پردازنده ی اصلی واگذار کنیم میتوان این ماتریس را به 1600 زیر ماتریس کوچک 10*10 تقسیم کرد و هر کدام از این ماتریس های کوچک را در یک نخ اجرایی سبک و برروی ریز پردازندهای کارت گرافیکی به صورت موازی اجرا کرد .
پردازنده های گرافیکی، پردازنده های چند هسته ی بسیار موازی هستند و در گروه کامپیوترهای موازی یک دستور العمل چند داده SIMD قرار دارند . در پردازنده های گرافیکی یک عملیات یکسان به طور همزمان روی چند داده اجرا می شود . درحال حاضر میان افزار کودا . پرکاربردترین ابزار برنامه نویسی بر پایه پردازنده گرافیکی است . کودا یک مدل برنامه نویس موازی ++C/C است واز مدل برنامه نویسی سریال – موازی ناهمگن پیروی میکند . کودا به CPU های چند هسته ای نیز به خوبی نگاشت میشود . کودا مجموعه های از کرنل ها و یا توابع کامپایلر nvcc کتابخانه های پیشتیبان و درایورهای سخت افزاری را ارائه میدهد .فناوری کودا سرعت عملکرد پردازش گرافیکی را از طریق تحت کنترل در اوردن قدرت GPU امکانپذیر میکند. این فناوری اجازه میدهد صدها تراشه گرافیکی مجزای انویدیا در کنار هم به پردازش موازی اطلاعات بپردازد . مدل برنامه نویسی کودا . کاملا روی موازی سازی داده تمرکز دارد . کودا با ارائه انتزاع های برنامه نویسی سبک وزن و مناسب . به برنامه نویسان اجازه میدهد تا کرنلها را با استفاده از نخ ها اجراء کنند .در زمان اجراء این نخ ها . به مجموعه ای از بلوک ها که شامل دهها نخ هستند و به طور همروند با هم کرده و منابع را به اشتراک میگذارند .بسط داده می شوند هر چند کودا به دنبال انتزاعی کردن جزئیات سخت افزار است اما اگاهی از معماری سخت افزار مورد استفاده هنوز هم برای برنامه نویسانی که کارایی برایشان اهمیت زیادی دارد مهم است . برای ایجاد پیاده سازیهای کار امد روی پردازنده گرافیکی به درک کامل معماری مورد استفاده . مدل نخ و مدل حافظه نیاز است .
12- کرنل ، نخ و روند اجرایی انها در GPU
کرنل . یک تابع مانند توابع نرمال زبان C است که روی ماشین میزبان یا همان CPU فراخوانی شده و روی ماشین دستگاه یا همان GPU اجرا می شود . در فراخوانی یک کرنل . نخ های زیادی اجرا شده و امکان محاسبات موازی را فراهم می نمایند . نخها در GPU به صورت همزمان یک دستور یکسان را روی داده های متفاوت اجراء می کنند . یک کرنل در کودا توسط ارایه ای از نخ ها اجرا میشود . به تعداد نخ های تعیین شده در کرنل از تابع کپی گرفته شده و به هر نخ یک کپی از تابع داده میشود تا بتواند دستور را روی داده ی خود اجرا نماید . در طول اجرای یک کرنل .همه نخ ها کد یکسانی را اجرا می کنند .هر نخ . دارای شناسه (ID) است که از ان برای محاسبه ادرسهای حافظه و تصمیمات کنترلی استفاده می شود . می توان گفت که نخ ، اجرایی از یک کرنل با یک شناسه یا شاخص خاص است هر نخ ، از شاخص خود برای دسترسی به عناصری که در یک مجموعه هستند . استفاده می کتد .مهمترین ویژگی های نخ های کودا عبارتند از :
1- نخ های بی اندازه سبک وزن هستند
2- سربار ایجاد انها بسیار کم است
3- از سوئیچ کردن فوری پشتیبانی می کنند.
روند اجرای نخ در سخت افزار پردازنده گرافیکی طی مراحل زیر انجام می شود :
1- فراخونی کرنل با دستوری مشابه با KernelFunction////dimGrid. dimBlock //// انجام میشود . سپس بلوک های نخ به صورت سریالی بین همهSM هایی که بطور بالقوه دارای بیش از یک بلوک نخ در هر SM هسنتد، توزیع می شوند .
2- بلوک های نخ متلق به هر SM به صورت تارهایی warp از نخ هاپس از فراخوانی کرنل توسط سخت افزار اغاز به کار میکند SM تارهایی که اماده اجرا روی چند پردازنده هایی جریانی هستند را زمان بندی و اجرا می کند .
3- محاسبات موازی توسط نخ ها در GPU انجام می شود
4- کرنل پس از تکمیل محاسبات موازی به اتمام رسیده و تمام منابع ازاد میشود
سازمان دهی نخ های کودا در بلاک و گرید
بلوک یا بلاک .گروهی از نخ ها است (شکل 17) هماهنگی نخها با استفاده از تابع syncthreads انجام می شوند این تابع باعث توقف یک نخ در یک نقطه خاص در کرنل می شود تا زماینکه نخ های دیگر درون بلوک به همان نقطه برسند . تار warp یک گروه متشکل از 32 نخ است که به صورت SIMD توسط SM اجرا میشوند . تارها واحدهای زمان بندی در یک SM هستند . هر بلوک نخ به تارهای 32 نخی مستقیم می شود و نخ های درون تارها روی پردازندهای اسکالر اجراء می دشوند . برای مثال اگر 3 بلوک به یک SM اختصاص داده شود و هر بلوک دارای 256 نخ باشد . تعداد تارها در هر بلوک برابر 8 (32/256) خواهد بود . این موضوع نشان میدهد که برای 3 بلوک 24 تار داریم . در هر لحظه تنهایی یکی از 24 تار برای واکشی و اجرای دستور العمل می شوند . زمانبندی تارها به صورت زیر انجام میگردد.
1-سخت افزار SM زمابندی تار با سربار صفر را پیاده سازی میکند
2-تارهایی که عملوندهای دستور العمل بعدی را دراختیار دارند دارای اولویت برای اجرا هستند .
3-تارهای واجد شرایط براساس یک سیاست زمانبندی برای اجرا انتخاب میشود
4-همه ی نخ های درون یک تار در صورت انتخاب شدن .دستور العمل یکسانی را اجرا می کنند.
گروهی از بلوکها یک گرید را شکل می دهند . در کودا . کرنل یک گرید از بلوکهای نخ را اغاز می کند . این بلوک های نخ روی SM ها اجرا می شوند . نخ های درون هر بلوک روی پردازندهای اسکالر اجرا شده و میتوانند از طریق حافظه مشترک با هم همکاری کنند. نخ های درون بلوک های مختلف نمی تواند با هم همکاری داشته باشند .
شکل شماره 17:سلسله مراتب دو بعدی از بلوکها و نخها در پردازش یک تصویر 48*32 پیکسلی با استفاده از یک نخ در هر پیکسل
شکل 17و18 رابطه میان مولفه های مختلف کودا را نشان می دهد . می توان دید که معماری گرافیکی برای موازی سازی است . GPU پردازشگری است که به دلیل استفاده از معماری موازی و استفاده از تعداد هسته های زیاد امکان پردازش کاراتری را نسبت به CPU فراهم می کند ودر عین حال هزینه پایینی دارد . همچنین کودا .امکان استفاده از این معماری را به کاربر می دهد .
شکل 19: سلسه مراتب سازمان دهی نخ های اجرایی در کودا
برای دسترسی به یک نخ خاص متغیرهای درونی ویژه ای وجود دارد که در زبانهای برنامه نویسی حامی CUDA تعبیه شده اند . همچنین متغیرهای درونی ویزه ای نیز برای پی بردن به ابعاد تور و بلوک های ایجاد شده در نظر گرفته شده است . این متغییرها نیز عمدتا سه بعدی هستند .
در معماری CUDA حافظه ی کارت گرافیکی به خاطر افزایش کارایی دارای سلسه مراتب به شرح زیر است
1* st place : Register file
2*nd place : Shared Momory
3*rd place :Constant Memory
4*th : Texture Memory
*Tie for last place : Local Memory and Global Momory
Register ها : به ازای هر نخ یک register وجود دارد که طول عمر داده های درون ان نیز به اندازه ی عمر نخ خواهد بود
شکل 21:حافظه Register و نخ
Shared memory به ازای هر بلوک از نخ ها یک قسمت حافظه ی مشترک وجود دارد که موجبات همکاری نخ های آان بلوک را فراهم می سازد . عمر داده های درون ان به اندازه عمر بلوک متناظر ان است و در ضمن زمان دسترسی به ان نیز کم است و لذا داده هایی که در ان قرار میگیرد در زمان سریعتری قالب دسترس اند
Local momory به ازای هر نخ در قسمت DRAM کارت گرافیکی یک قسمت حافظه ی محلی وجود دارد که عمر داده های درون ان به اندازه ی عمر نخ است
Global (device) memory این حافظه یک حافظه عمومی است که هم توسط CPU و هم توسط تمامی نخ ها قابل دسترسی است . طول عمر داده ها درون ان هم از زمان Allocation تا زمان De allocation است .ویژگی های این حافظه زمان تاخیر زیاد . ظرفیت بالا و عدم cache شدن است
شکل24:حافظه Device
Host(CPU) memory این حافظه به صورت مستقیم در دسترس نخ های device نیست . بنابراین برای اینکه device بتواند پردازشی روی ان ها انجام دهد باید داده ها درون ان ابتدا از حافظه های device انتقال یابد .
به ازای هر نخ یک حافظه local memory ویک رجیستر وجو دارد و در سطح بعد . برای بلوک حافظه یک Shared momory ودر نهایت به ازای هر device یک حافظه ی global داریم . برای افزایش کارایی برنامه های CUDA سلسه مراتبی شامل تور. بلوک .و نخ در نظر گرفته شده است . حال زمان ان رسیده است تا بدانیم که اجرای این واحدها بر روی سخت افزار ها به چه صورت است . در شکل 25 با یک نمای ساده از ارتباطات سخت افزاری CUDA اورده شده است .
در شکل 25 Global memory حافظه عمومی device است و SMEM خلاصه شده ی عبارت Shared memory است . هر بلوک از تور بر روی یک multiprocessor اجرا می شود " البته این امکان وجود دارد که چند بلوک روی یک multiprocessor اجرا شوند که این امر وابسته به منابع موجود در هر multiprocessor ونیز به ابعاد بلوک ها ست و درضمن اجرای یک نخ از یک multiprocessor وبه یک multiprocessor دیگر انتقال نخواهد یافت .
شکل26:سیستم معماری در موازی سازی CUDA
شکل 28 : حافظه اشتراکی در کودا
نخ های هر بلوک بسیار سبک اند و نیز به سادگی از حالت اجرا به حالت انتظار می روند و بلعکس . در واقع ایده ی اصلی برای این کاردر معماری CUDA این است که برای تعویض حالت اجرای یک نخ نیازی به ذخیره سازی اطلاعات PCB نیست . PCB بلوکی است که سیستم عامل اطلاعات هر پردازش رادر ان ذخیره می کند تا وضیعت اجرایی Process حفظ شود .پس از انکه مجددا نوبت اجرا به این پردازش رسید . اطلاعات PCB ان وضعیت رجیسترها و ..را بازیابی کنند . هر multiprocessor به تعداد حداکثر نخ های که میتواند اجرا کند register دارد که یک محدودیت سخت افزاری است . مثلا فرض کنید که یک multiprocessor حداکثر توانایی اجرای 256 نخ را داشته باشد . دراینصورت 256 رجیستر نیز خواهد داشت . درهر لحظه یکی از این نخ ها اجرا می شود و سپس اجرای ان رها شده ونوبت به دیگری می رسد . منتها برای تعویض اجرا نیازی به ذخیره سازی حالت نخ در حال اجرا نیست . زیرا تعداد کافی رجیستر در اختیار داریم .پس باخیال راحت اجرای ان را رها کرده و به سراغ نخ بعدی می رویم . نکته دیگری که دررابطه وجود دارد این است که تعداد بلوک هایی که می توانند به صورت همزمان روی یک multiprocessor اجرا شوند. بستگی به میزان منابع multiprocessor و نیز ابعاد بلوک دارد . قسمت shared memory در شکل 28 حاظه ی مربوط بین نخ های یک بلوک است که برای همکاری این نخ ها تعبیه شده است Global memory نیز حافظه ای از DRAM مربوط به device است که پل ارتباطی host و device به شمار میرود . مدیریت اجرا و شناسه دهی به نخ هایی که multiprocessorها اجرا میکنند با خود انهاست . واحدهای زمانبندی در multiprocessor ها wrap نام دارند . هر wrap شامل تعداد نخ است . درهر زمان واحد زمانبند یک wrap را به حالت اجرا می برد . multiprocessor روی تمامی نخ هایwrapمذکور دستور واحدی را اجرا می کند . به عنوان مثال اگر فرض کنیم که مثال قبلی هر 32 wrap نخ را در خود جای میدهد . انگاه هر بلوک ما 4=32/128wrap خواهد داشت و multiprocessor در کل 2/4 یعنی 8 wrap خواهد داشت. با گذر زمان هر بار . multiprocessor یک wrap را برداشته و دستور مناسب را درمورد ان اجرا کرده و به سراغ wrap بعدی می رود .
13-همکاری نخ ها
برای افزایش کار امدی کد در CUDA . این امکان فراهم شده است که نخ ها بتوانند با یکدیگر همکاری داشته باشند . این همکاری می تواند در زمینه ی دسترسی به حافظه باشد که باعث کاهش پهنای باند مصرف شده در برنامه می شود و یا میتواند در زمینه ی اشتراک نتایج بدست امده باشد که در این حالت سربار محاسبه ی مجدد برای هر نخ کاهش می یابد .لازم به ذکر است که همکاری بین تمام نخها قابل پیاده سازی نیست . زیرا اگر تعداد نخ های برنامه زیاد باشد نه تنها کارایی را افزایش نمدهد بلکه سبب کاهش ان نیز خواهد شد . بنابراین همکاری نخ ها فقط در یک بلوک صورت میگیرد . در حقیقت دسته بندی نخ ها در قالب بلوک های مجزا در یک تور به همین منظور صورت گرفته است . در هر بلوک یک حافظه shared memory وجود دارد که بین نخ های ان بلوک مشترک است و نخ های ان بلوک می توانند از داده های درون ان استفاده کرده یا نتایج محاسبات خود را در ان قراردهند . علاوه بر همکاری از طریق حافظه ی مشترک نخ های درون یک بلوک می توانند اجرایشان رانیز با هم synchronize کنند.
تـــذکر : نخ های یک بلوک نمیتواند با نخ های بلوک های دیگر همکاری داشته باشند . مدیریت حافظه هر کدام از device ها و host حافظه مربوط به خود را دارند . مدیریت حافظه device در کد host صورت می گیرد . این مدیریت شامل گرفتن و پس دادن حافظه . کپی کردن به device یا برداشتن اطلاعات از ان است .
Memory Allocation and Release
به کمک توابع زیر میتوان حافظه ای به اندازه مورد نیاز در قسمت device از global memory گرفت یا ان را پس داد.
. cuda Malloc(void**Pointer . size _ t nbytes )
. cuda Memset (void**Pointer .int value . size _ t nbytes )
. cudaFree (void**Pointer)
توابع cudaMalloc و cudaFree به ترتیب حافظه ی مورد نیاز را می گیرند و پس می دهند cudaMemset هم داده ی مورد نظر را در حافظه ی گرفته شده قرار می دهد . به کمک تابع زیر می توان فضایی از حافظه host یا device را در فضایی از حافظه ی host یا device قرار دهد .
CudaMemcpy ( void *dest . void * source . size _t bytes . enum cudaMemcpyKind direction )
enum cudaMemcpyKind میتواند یکی از مقادیر زیر را بگیرد .
· Cuda Memcpy Host Device
· Cuda Memcpy Device To Host
· Cuda Memcpy Device Device
· Host synchronization
· همه اجراهای kernel به صورت غیر همگام صورت میگیرد . بدین معنا که بلافاصله پس ا زاجرا و پیش از اینکه اجرای همه نخ ها به پایان برسد به CPU باز می گردد . برای همگام کردن نخ های د رحال اجرا از تابع cudaThread synchronize استفاده می شود . همچنین لازم به ذکر است که تابع CudaMemcpy به صورت همگام اجرا می شود . بدین معنا کپی اغاز نمی شود تا همه ی فراخوانی های قبلی CUDA کامل شود و نیز کنترل به CUP باز نمی گردد تا اینکه کل عملیات کپی کردن صورت گیرد .
یک پردازنده برداری ، یا آرایه پرداز ،رایانه خاصی است با چند پردازنده برای پردازش موازی تعداد زیادی از آرایه ها
آرایه پرداز در واقع یک واحد پردازش مرکزی (CPU) می باشد که یک مجموعه از دستورالعملهایی را اجرا می کند که روی آرایه تک بعدی از داده که همان بردار است ، عمل می کند. این درست در مقابل پردازنده های عددی قرار می گیرد که دستورالعملهای آن ، صرفا روی یک تکه داده عمل می کند ،اکثر پردازنده ها ،پردازنده های عددی می باشند. پردازنده های برداری در دهه 1970 مورد بحث واقع شدند و پایه و اساس اکثر ابررایانه های دهه 1980 تا 1990 را تشکیل دادند پیشرفتهای پردازنده های عددی ،مخصوصا ریزپردازنده ها ،باعث کاهش بکار گیری پردازنده های برداری سنتی در ابررایانه ها و همچنین کاهش استفاده از تکنیکهای پردازش برداری در پردازنده ها در اوایل دهه 1990 شد.امروزه اکثر پردازنده ها از یک معماری استفاده می کنند که در آن دستورالعملهایی برای پردازش برداری روی چندین تکه داده ، را بطور برجسته نشان می دهد.این دستورالعملها را SMID (تک دستور،چند داده)می شناسند.از مثالهای مشهود اینگونه دستورالعملها می توان به AltiVec,MMX,SSE اشاره نمود.نمونه هایی از استفاده از تکنیک پردازش برداری را می توان در کنسولهای بازی و شتاب دهنده های گرافیکی نیز پیدا کرد.در سال 2000 شرکتهای آی بی ام و سونی و توشیبا با همکاری هم موفق به تولید یک پردازنده سلولی (نوعی ریزپردازنده)شدند ،که این پردازنده سلولی شامل یک پردازنده عددی و هشت پردازنده برداری بود که در پلی استیشن 3 مورد استفاده قرار گرفت.
در برخی از طراحی های پردازنده ها ،دستورالعملهای چندگانه روی چندین تکه داده عمل می کنند که آنها را به عنوان MMID (چند دستوره،چند داده)می شناسیم.اینگونه طراحی ها مختص کاربردهای ویژه هستند و بطور عادی برای استفاده های متداول کاربرد ندارند
پردازش
برداری اولین بار در اوایل دهه ۱۹۶۰ توسط وستینگهاوس، در پروژه Solomon مورد توجه
قرار گرفت و توسعه یافت. هدف این پروژه افزایش چشمگیر سرعت محاسبات ریاضی، با
استفاده همزمان از تعداد زیادی عملیات ساده ریاضی، زیر نظر یک پردازنده بود. به
این صورت که پردازنده در هر کلاک یک سیگنال مشترکی را برای تمامی واحدهای محاسبه و منطق (ALU)ها
میفرستاد.
در حالی که هر یک از این واحدهای محاسبه ورودیهای مجزایی داشتند، یک کار مشترک
روی آن ورودیها انجام میدادند. این روش به ماشینهای Solomon این قابلیت را میداد
که یک الگوریتم را روی انبوهی از دادهها (که از آرایهها تغذیه میشدند) اجرا
نماید.
در
سال ۱۹۶۲، وستینگهاوس پروژه را لغو کرد ولی دوباره در دانشگاه ایلینوی تحت عنوان ILLIAC IV از سر گرفته
شد. در طراحی ابتدایی آن، این ماشین به ۲۵۶ واحد محاسبه و منطق مجهز بود، درحالی
که وقتی در سال ۱۹۷۲ عرضه شد تنها ۶۴ واحد محاسبه و منطق داشت و فقط میتوانست ۱۰۰
تا ۱۵۰ میلیون فلاپس کار کند.
برای کار با اطلاعات فشرده و حجیم، مانند دینامیک محاسباتی سیالات، همین ماشین
معیوب (به دلیل اینکه نتوانستند تعداد ۲۵۶ واحد محاسبه و منطق ایجاد کنند)
سریعترین ماشین دنیا بود.
تکنیک
پردازش برداری تقریبا در تمام طراحیهای پردازندههای مدرن وجود دارند، اگرچه
معمولا با نام SIMD میشناسند.
در اینگونه پردازندهها، واحد پردازش برداری در کنار واحد پردازنده عددی کار میکند
و برنامههایی با آن بخش کار میکنند که واقعا میدانند که این واحد حضور دارد.
بیشتر پردازندهها دستورالعملی دارند که بیان میکند «A را با B جمع کن و درون C بریز». مقادیر A، B و C (حداقل در تئوری) به ندرت میتواند داخل دستورالعمل باشند (چه به صورت صریح، چه رجیستر). در واقع داده به ندرت به طور خام فرستاده میشود، بلکه به صورت اشارهای به آدرس حافظه که داده داخل آن است، میباشد. رمزگشایی[۲] این نشانی و دریافت داده از حافظه[۳]، خود زمان بر است. با افزایش سرعت پردازندهها، این تاخیر حافظه تبدیل به مانعی بزرگ برای عملکرد سریع پردازنده محسوب میشود.
برای کاهش زمان تاخیر، اغلب پردازندههای امروزی از فن خط لوله[۴] استفاده میکنند. در این تکنیک دستورالعملها از چندین بخش عبور میکند تا نوبتش برای اجرا فرارسد. اولین بخش، آدرس را خوانده و کدگشایی میکند، بخش بعد مقادیر آدرسها را از حافظه میگیرد و بعدی کار محاسبه و اجرا را انجام میدهد. در تکنیک خط لوله، رمز کار در این است که شروع کدگشایی دستورالعمل بعدی، باید حتی قبل از خروج دستورالعمل قبلی از پردازنده صورت گیرد؛ در نتیجه واحد کدگشایی آدرس همواره مشغول به کار میباشد. هر دستورالعمل برای اجرای کامل به همان زمان قبلی (بدون خط لوله) نیاز دارد. زمانی که آن را تاخیر مینامیم. ولی پردازنده با خط لوله میتواند دستهای از دستورالعملها را خیلی سریعتر انجام دهد.
پردازنده برداری یک قدم فراتر بر میدارد و بجای ایجاد خط لوله برای دستورالعملها، دادهها را نیز خط لوله میکند. دستورالعملهایی هستند که بجای اینکه بگویند «A را با B جمع کن»، میگوید «تمامی اعداد از اینجا تا آنجا را با تمامی اعداد از اینجا تا آنجا جمع کن». و بجای اینکه دستورالعملها را پشت سر هم رمزگشایی و دادههای مربوط به آنها را از حافظه دریافت کند، یک دستورالعمل را از حافظه میخواند و با فرض اینکه میداند که آدرس بعدی یکی بیشتر از آدرس فعلی است، دستورالعمل بعدی را رمزگشایی میکند. این عمل صرفه جویی چشمگیری در زمان رمزگشایی میکند. برای نشان دادن اینکه چه تفاوتی میکند، فرض کنید میخواهیم دو آرایه ۱۰ تایی از اعداد را با هم جمع کنیم. در حالت عادی نیاز به یک حلقه داریم که هر بار یک زوج از این دو آرایه را انتخاب میکند و سپس آنها را با هم جمع میکند
execute this loop 10 times
read the next instruction and decode it
fetch this number
fetch that number
add them
put the result here
end loop
در
حالی که در پردازش برداری اینگونه خواهد بود:
read instruction and decode it
fetch these 10 numbers
fetch those 10 numbers
add them
put the results here
در
پردازش برداری، اول اینکه فقط دو انتقال آدرس از پردازنده به حافظه داریم، همچنین
در این حالت بجای اینکه ۱۰ مرتبه یک دستورالعمل را کدگشایی کند تنها یک بار این
کار را انجام میدهد. همچنین کد مورد استفاده در پردازش برداری کوتاهتر است که
این خود سبب کاهش حافظه مورد نیاز برای دستورالعملهای آن میباشد.
بررسی
وابستگی بین این اعداد لازم نیست چون دستورالعمل برداری، چندین عمل غیر وابسته را
معین میکند. این خود منطق کنترل را ساده میکند. مسئله زمانی جالب تر میشود که
بتوان چند عمل را روی چند داده انجام داد. کد زیر را در نظر بگیرید که در آن میخواهیم
دو گروه عدد را با هم جمع کنیم و سپس با گروه سوم ضرب کنیم. در این کد عمل دریافت
دستورالعمل فقط یک بار انجام میشود (بر خلاف حالت عادی که ۲*۱۰=۲۰) و این دو عمل
فقط در یک دستورالعمل انجام میپذیرد:
read instruction and decode it
fetch these 10 numbers
fetch those 10 numbers
fetch another 10 numbers
add and multiply them
put the results here
عملیات
ریاضی بالا خیلی سریعتر انجام خواهند شد چرا که دیگر تاخیر در دریافت و کدگشایی
دستورالعمل بعدی را نداریم (فقط یک دستورالعمل داریم).
باید توجه داشت که تمام مسائل را نمیتوان با استفاده از این روش بهبود داد. پیاده سازی این دستورالعملها در پردازنده خود پیچیدگی زیادی را بر هسته پردازنده تحمیل میکند. این پیچیدگیها معمولا سبب میشوند که دستورالعملهای دیگر دیرتر اجرا شوند. به عنوان مثال زمانی که بخواهیم فقط دو عدد تنها را با هم جمع کنیم. همچنین دستورالعملهای پیچیده سبب کندی قسمت رمزگشایی و پیچیدگی بیشتر آن خواهد شد، که این خود باعث کندی اجرای دستورات عادی میشود.
در حقیقت، پردازش برداری برای انجام عملیات روی انبوهی از دادهها بهترین کارایی را دارند. برای همین است که این پردازندهها اصولا در ابر رایانهها استفاده میشوند. این ابر رایانهها عموما برای پیشبینی وضعیت هوا و آزمایشگاههای فیزیک استفاده میشوند که انبوهی از داده را به چالش میگیرد.
کد زیر مثال حقیقی معماری x86 با دستورالعملهای برداری میباشد. در اینجا از دو آرایه اعداد اعشاری استفاده میکند.
//SSE simd function for vectorized multiplication of 2 arrays with single-precision floatingpoint numbers
//1st param pointer on source/destination array, 2nd param 2. source array, 3rd param number of floats per array
void mul_asm(float* out, float* in, unsigned int leng)
{ unsigned int count, rest;
//compute if array is big enough for vector operation
rest = (leng*4)%16;
count = (leng*4)-rest;
// vectorized part; 4 floats per loop iteration
if (count>0){
__asm __volatile__ (".intel_syntax noprefix\n\t"
"loop: \n\t"
"movups xmm0, [ebx+ecx] ;loads 4 floats in first register (xmm0)\n\t"
"movups xmm1, [eax+ecx] ;loads 4 floats in second register (xmm1)\n\t"
"mulps xmm0,xmm1 ;multiplies both vector registers\n\t"
"movups [eax+ecx],xmm0 ;write back the result to memory\n\t"
"sub ecx,16 ;increase address pointer by 4 floats\n\t"
"jnz loop \n\t"
".att_syntax prefix \n\t"
: : "a" (out), "b" (in), "c"(count), "d"(rest): "xmm0","xmm1");
}
// scalar part; 1 float per loop iteration
if (rest!=0)
{
__asm __volatile__ (".intel_syntax noprefix\n\t"
"add eax,ecx \n\t"
"add ebx,ecx \n\t"
"rest: \n\t"
"movss xmm0, [ebx+edx] ;load 1 float in first register (xmm0)\n\t"
"movss xmm1, [eax+edx] ;load 1 float in second register (xmm1)\n\t"
"mulss xmm0,xmm1 ;multiplies both scalar parts of registers\n\t"
"movss [eax+edx],xmm0 ;write back the result\n\t"
"sub edx,4 \n\t"
"jnz rest \n\t"
".att_syntax prefix \n\t"
: : "a" (out), "b" (in), "c"(count), "d"(rest): "xmm0","xmm1");
}
return;
}