اصطلاح GPU البرمجة في الصدأ: مكتبة الاتحاد الاقتصادي والنقدي


مقدمة


Emu هي لغة برمجة لرسومات الرسومات عالية المستوى يمكن تضمينها في الكود العادي بلغة برمجة نظام Rust .


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


تركيب


  1. تحتاج المكتبة التي تبحث عنها إلى تبعية OpenCL خارجية. تحتاج إلى تثبيت برنامج التشغيل المناسب لجهازك.
  2. Cargo.toml النص أدناه. سيؤدي ذلك إلى تنزيل أحدث الإصدارات المتاحة (إذا كنت بحاجة إلى تجميع محدد ، ثم بدلاً من * ضع الإصدار الذي تحتاجه):

     [dependencies] em = "*" //   Emu ocl = "*" //   OpenCL 

بناء الجملة


بناء جملة Emu بسيط للغاية ، لأن هذه اللغة مخصصة فقط لكتابة وظائف kernel التي يتم تجميعها في OpenCL .


أنواع البيانات


تحتوي لغة Emu على تسعة أنواع من البيانات تشبه تلك الموجودة في Rust. فيما يلي جدول بهذه الأنواع من البيانات:


اسموصف
f32رقم اثنين وثلاثين بت العائمة
i8رمز أو رقم ثمانية بت
i16وقعت ستة عشر بت عدد
i32وقعت رقم اثنين وثلاثين بت
i64وقعت أربعة وستون عدد بت
u8غير موقعة رقم ثمانية بت
u16غير موقعة رقم 16 بت
u32غير موقعة رقم اثنين وثلاثين
u64غير موقعة رقم أربعة وستون بت
boolالقيمة المنطقية
[TYPE]متجه يتكون من متغيرات من النوع TYPE

المتغيرات


يتم التصريح عن المتغيرات باستخدام الكلمة الرئيسية let ، والتي تقع خلف المعرّف والنقطتين ونوع البيانات وعلامة المساواة والقيمة المخصصة والفاصلة المنقوطة.


 let age: i32 = 54; let growth: f32 = 179.432; let married: bool = true; 

تحويل


يتم تحويل أنواع البيانات البدائية باستخدام العامل الثنائي as يلي ، بعد النوع المستهدف. ألاحظ أن النوع المستهدف يمكن أن يكون أيضًا وحدة قياس (انظر القسم التالي):


 let width: i16 = 324; let converted_width: i64 = width as i64; 

وحدات القياس


تسمح لك لغة Emu بمعالجة الأرقام كوحدات قياس ، والتي تم تصميمها لتبسيط الحسابات العلمية. في هذا المثال ، length تعريف length المتغير مبدئيًا بالمتر ، ولكن بعد ذلك يتم إضافة وحدات قياس أخرى إليه:


 let length: f32 = 3455.345; //  length += 7644.30405 as cm; //  length += 1687.3043 as mm; //  

الثوابت المعرفة مسبقا


يوجد لدى Emu مجموعة من الثوابت المحددة مسبقًا والتي يمكن استخدامها في الممارسة العملية. أدناه هو الجدول المقابل.


اسمقيمة
Y10 إلى قوة 24
Z10 إلى قوة 21
E10 إلى قوة 18
P10 إلى قوة 15
T10 إلى قوة 12
G10 إلى قوة 9
M10 إلى قوة 6
k10 إلى قوة 3
h10 إلى قوة 2
D10 إلى قوة 1
d10 إلى قوة -1
c10 إلى قوة -2
m10 إلى قوة -3
u10 إلى قوة -6
n10 إلى قوة -9
p10 إلى قوة -12
f10 إلى قوة -15
a10 إلى درجة -18
z10 إلى قوة -21
y10 إلى قوة -24

كما يتم تحديد الثوابت المقابلة للبيانات العلمية. يمكنك العثور على الجدول الذي يتكون من هذه الثوابت هنا .


البيانات الشرطية


تشبه عبارات Emu الشرطية العبارات المقابلة في Rust. تستخدم التعليمة البرمجية التالية بنيات شرطية:


 let number: i32 = 2634; let satisfied: bool = false; if (number > 0) && (number % 2 == 0) { satisfied = true; } 

للحلقات


يتم تعريف رأس حلقة For كـ for NUM in START..END ، حيث NUM هو متغير يأخذ قيمًا من النطاق [START; END) [START; END) خلال وحدة.


 let sum: u64 = 0; for i in 0..215 { sum += i; } 

بينما الحلقات


يتم تعريف عنوان حلقة في while (CONDITION) ، حيث CONDITION هو الشرط للحلقة للمتابعة إلى التكرار التالي. يشبه هذا الرمز المثال السابق:


 let sum: u64 = 0; let idx: i32 = 0; while (idx < 215) { sum += idx; idx += 1; } 

حلقات لا نهاية لها


لا تحتوي الحلقات اللانهائية على شرط خروج صريح ويتم تعريفها بواسطة الكلمة الأساسية loop . ومع ذلك ، يمكن متابعتها أو مقاطعتها بواسطة break ومتابعة العبارات (مثل النوعين الآخرين من الحلقات).


 let collapsed: u64 = 1; let idx: i32 = 0; loop { if idx % 2 == 0 { continue; } sum *= idx; if idx == 12 { break; } } 

العودة من الوظيفة


كما هو الحال في جميع لغات البرمجة الأخرى ، فإن return هي ناتج الوظيفة الحالية. يمكنه أيضًا إرجاع قيمة معينة إذا كان توقيع الوظيفة (انظر الأقسام التالية) يسمح بذلك.


 let result: i32 = 23446; return result; 

المشغلين الآخرين


  • عوامل التشغيل المتاحة: = ، += ، -= ، *= ، /= ، %= ، &= ، ^= ، <<= ، >>= ؛
  • عامل الفهرس هو [IDX] ؛
  • استدعاء المشغل - (ARGS) ؛
  • العوامل الأحادية: * للتراجع ،! لعكس البيانات المنطقية ، - لإنكار الأرقام ؛
  • العوامل الثنائية: + ، - ، * ، / ، % ، && ، || & ، | ، ^ ، >> ، << ، > ، < ، >= ، <= ، == ، == != .

وظائف


هناك ثلاثة أجزاء من الوظائف في Emu: المعرف والمعلمات ونص الوظيفة ، وتتألف من سلسلة من التعليمات القابلة للتنفيذ. النظر في وظيفة إضافة رقمين:


 add(left f32, right f32) f32 { return left + right; } 

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


مساحات العنوان


يتوافق كل معلمة للدالة مع مساحة عنوان محددة . بشكل افتراضي ، تتوافق جميع المعلمات مع مساحة __private__ .


تشير إضافة البادئات global_ و local_ إلى معرّف المعلمة بوضوح إلى مساحة العنوان الخاصة بها.


تنصح الوثائق باستخدام البادئة global_ لجميع المتجهات وليس بادئة أي شيء آخر.


وظائف مدمجة


يوفر Emu مجموعة صغيرة من الوظائف المدمجة (مأخوذة من OpenCL) التي تتيح لك إدارة بيانات GPU:


  • get_work_dim() - إرجاع عدد الأبعاد ؛
  • get_global_size() - إرجاع عدد العناصر العالمية لبعد معين ؛
  • get_global_id() - إرجاع المعرف الفريد للعنصر الخاص بالبعد المحدد ؛
  • get_global_size() - إرجاع عدد العناصر العالمية لبعد معين ؛
  • get_local_id() - إرجاع معرف فريد لعنصر محلي داخل مجموعة عمل محددة لبعد معين ؛
  • get_num_groups() - إرجاع عدد مجموعات العمل لبعد معين ؛
  • get_group_id() - إرجاع معرف فريد لمجموعة العمل.

في رمز التطبيق ، ستجد في أغلب الأحيان التعبير get_global_id(0) ، والذي يُرجع الفهرس الحالي لعنصر المتجه المرتبط get_global_id(0) إلى وظيفة kernel.


تنفيذ التعليمات البرمجية


النظر في بناء جملة لاستدعاء وظائف Emu من رمز الصدأ العادية. على سبيل المثال ، سوف نستخدم دالة تضاعف جميع عناصر المتجه برقم معين:


 use em::emu; emu! { multiply(global_vector [f32], scalar f32) { global_vector[get_global_id(0)] *= scalar; } } 

لترجمة هذه الوظيفة إلى كود OpenCL ، تحتاج إلى وضع توقيعها في build! الماكرو build! على النحو التالي:


 use em::build; //    build! {...} extern crate ocl; use ocl::{flags, Platform, Device, Context, Queue, Program, Buffer, Kernel}; build! { multiply [f32] f32 } 

تأتي الإجراءات الإضافية لاستدعاء وظائف Emu التي كتبتها من كود Rust. لا يمكن أن يكون أسهل:


 fn main() { let vector = vec![0.4445, 433.245, 87.539503, 2.0]; let result = multiply(vector, 2.0).unwrap(); dbg!(result); } 

مثال التطبيق


يأخذ هذا البرنامج عدديًا باعتباره الوسيطة الأولى ، حيث من الضروري ضرب الوسائط التالية. ستتم طباعة الموجه الناتج إلى وحدة التحكم:


 use em::{build, emu}; //    build! {...} extern crate ocl; use ocl::{flags, Buffer, Context, Device, Kernel, Platform, Program, Queue}; emu! { multiply(global_vector [f32], scalar f32) { global_vector[get_global_id(0)] *= scalar; } } build! { multiply [f32] f32 } fn main() { //     : let args = std::env::args().collect::<Vec<String>>(); if args.len() < 3 { panic!(": cargo run -- <SCALAR> <NUMBERS>..."); } //      : let scalar = args[1].parse::<f32>().unwrap(); //      : let vector = args[2..] .into_iter() .map(|string| string.parse::<f32>().unwrap()) .collect(); //    : let result = multiply(vector, scalar).unwrap(); dbg!(result); } 

يمكنك تنفيذ هذا الكود باستخدام أمر cargo run -- 3 2.1 3.6 6.2 . النتيجة الناتجة تلبي التوقعات:


 [src/main.rs:33] result = [ 6.2999997, 10.799999, 18.599998, ] 

رابط OpenCL


كما ذكرنا سابقًا ، Emu هو مجرد تجريد على OpenCL ، وبالتالي لديه القدرة على التفاعل مع صندوق ocl. الكود أدناه مأخوذ من مثال في المستودع الرسمي :


 use em::emu; //  "ocl"        Rust: extern crate ocl; use ocl::{flags, Platform, Device, Context, Queue, Program, Buffer, Kernel}; //  Emu    (OpenCL)   //     "EMU: &'static str": emu! { //     : multiply(global_buffer [f32], coeff f32) { global_buffer[get_global_id(0)] *= coeff; } } fn multiply(global_buffer: Vec<f32>, coeff: f32) -> ocl::Result<Vec<f32>> { //        , //  , ,   : let platform = Platform::default(); let device = Device::first(platform)?; let context = Context::builder() .platform(platform) .devices(device.clone()) .build()?; let program = Program::builder() .devices(device) .src(EMU) .build(&context)?; let queue = Queue::new(&context, device, None)?; let dims = global_buffer.len(); //    : let buffer = Buffer::<f32>::builder() .queue(queue.clone()) .flags(flags::MEM_READ_WRITE) .len(dims) .copy_host_slice(&global_buffer) .build()?; //       , //    : let kernel = Kernel::builder() .program(&program) .name("multiply") .queue(queue.clone()) .global_work_size(dims) .arg(&buffer) .arg(&coeff) .build()?; //    (    //   : unsafe { kernel.cmd() .queue(&queue) .global_work_offset(kernel.default_global_work_offset()) .global_work_size([dims, 0, 0]) .local_work_size(kernel.default_local_work_size()) .enq()?; } //  ,         // "dims": let mut vector = vec![0.0f32; dims]; buffer.cmd() .queue(&queue) .offset(0) .read(&mut vector) .enq()?; Ok(vector) } fn main() { let initial_data = vec![3.7, 4.5, 9.0, 1.2, 8.9]; //   ,   Emu,  //  "initial_data": let final_data = multiply(initial_data, 3.0).unwrap(); println!("{:?}", final_data); } 

اكتمال


أتمنى أن تستمتعوا بالمقال. يمكنك الحصول على إجابة سريعة على أسئلتك في الدردشة باللغة الروسية في Rust ( إصدار للمبتدئين ).


Source: https://habr.com/ru/post/ar454678/


All Articles