اولین – و من – برنامه احمقانه open cl شما 😇 – دیگر – 24 آوریل 2023
سلام . اول از همه، ویدیوی زیر را تماشا کنید: خوب، اکنون، این اولین برنامه openCL من است، بنابراین ممکن است مشکلاتی در اصطلاحات و غیره وجود داشته باشد، اما هدف این است که ساده ترین مثال ممکن را داشته باشیم، نه تنها به این دلیل که مفید است، بلکه به این دلیل است که
سلام .
اول از همه، ویدیوی زیر را تماشا کنید:
خوب، اکنون، این اولین برنامه openCL من است، بنابراین ممکن است مشکلاتی در اصطلاحات و غیره وجود داشته باشد، اما هدف این است که ساده ترین مثال ممکن را داشته باشیم، نه تنها به این دلیل که مفید است، بلکه به این دلیل است که فعلاً این تنها کاری است که می توانم انجام دهم. 😇
بیا بریم
همانطور که در ویدیو دیدید، ما باید یک زمینه با یک دستگاه ایجاد کنیم و به آن زمینه، تابع (هسته) خود را تغذیه می کنیم.
اگر می دانید C خوش شانس هستید، این را در ذهن ندارم.
بنابراین، بیایید با اصول اولیه شروع کنیم، پایه را تنظیم کنیم و زمانی که کار کرد، محاسبات را نیز انجام دهیم.
توجه داشته باشید، برخی از آموزشها ممکن است به شما این حس را بدهد که میتوانید چندین دستگاه را به یک زمینه اختصاص دهید، اما در واقع یک دستگاه در هر زمینه است.
من فکر می کنم شما می توانید چندین زمینه همزمان داشته باشید، (یکی در هر دستگاه) -> همانطور که من استنباط کردم، آزمایش نشده، آشنا نیست.
اولین کاری که انجام میدهیم این است که یک زمینه ایجاد میکنیم، متوجه میشویم که اگر ایجاد موفقیتآمیز باشد، گزارش نام دستگاهی را که اختصاص داده شده است دریافت میکند:
#property copyright "Lorentzos Roussos" #property link "https://www.mql5.com/en/users/lorio" #property version "1.00" #include <OpenCLOpenCL.mqh> bool busy=false,loaded=false; int ctx=INVALID_HANDLE; int OnInit() { ctx=INVALID_HANDLE; busy=false; loaded=false; EventSetMillisecondTimer(44); return(INIT_SUCCEEDED); } void OnTimer(){ if(!busy){ busy=true; if(!loaded){ EventKillTimer(); ctx=CLContextCreate(CL_USE_GPU_DOUBLE_ONLY); if(ctx!=INVALID_HANDLE){ Print("CL.Context Created"); } } busy=false; } } void OnDeinit(const int reason) { if(ctx!=INVALID_HANDLE){CLContextFree(ctx);} } void OnTick() { }
این ساختار کدی است که به طور کلی اجرا خواهیم کرد، در deinit اگر معتبر بود، زمینه را آزاد می کنیم.
چیزهای ساده تا اینجا.
همچنین توجه داشته باشید که 2 صفحه مستند برای OpenCL وجود دارد، یکی در کتابخانه استاندارد و دیگری برای پشتیبانی از دستورات بومی
من فکر می کنم کتابخانه استاندارد قدیمی است، مطمئن نیستم، روشن نیست، به هر حال روشن نیست، نه اینکه چیزی در این سایت وجود دارد، اما بیایید با بومی کار کنیم و اگر چیزی کم است، می دانیم کجا باید جستجو کنیم.
بنابراین، توابع بومی دارای این دستور به نام ProgramCreate هستند که یک کد منبع رشته ای را دریافت می کند که با “OpenCL C” (زبان openCl) نوشته شده است.
اما دستور KernelCreate دسته یک برنامه و نام هسته را دریافت میکند، به این معنی که همه کد منبع برنامههایمان را در رشتهای که در ایجاد برنامه ارسال میکنیم میریزیم و سپس هستهها را اعلام میکنیم.
برای این تست ما فقط یک “عملکرد” (هسته) خواهیم داشت، بنابراین فعلاً مهم نیست.
بنابراین، یک تابع ساده در OpenCL C، kay، و من C را نمیدانم، پس چطور میشود که یک آرایه و یک عدد بفرستیم و “کرنل” مقادیر موجود در آرایه را در آن عدد ضرب کند.
اما همانطور که در ویدئو تاکید شد بهترین راه برای کاهش عملکرد محاسبات موازی این است که آنها را به صورت خطی اجرا نکنیم. آن را بهعنوان حلقهای با تکرارکننده i در نظر بگیرید که در واقع با متغیر تکرار i به ایندکس دسترسی ندارد، بلکه با «پول» دیگری از شاخصها که برای همه «واحدهای محاسباتی» در دسترس است، دسترسی پیدا میکند.
بیایید متفاوت در مورد آن فکر کنیم تا منطقی باشد. اگر میخواهید به تنهایی عملیاتهای موازی را در mql5 ایجاد کنید، به یک گزارش مشترک از آنچه “وظایف” هنوز در دسترس هستند نیاز دارید و هنگامی که یکی از نمودارهای شما یک کار را تمام کرد، سپس یکی از نمودارهای شما را از آن مجموعه مشترک ناتمام انتخاب میکنند. وظایف
بنابراین، وقتی با get_global_id(0) (0) اجرا میشود، «تابع» میتواند بداند کدام شاخص در استخر دارد.
__kernel void biscuit(__global double *array, double by, int total_items){ int idx=get_global_id(0); if(idx>total_items){return;} array[idx]*=by; }
خوب، پس سوال دیگری که مطرح می شود این است که get_global_id از 0 شروع می شود یا 1؟ ، مثال در برنامه ایجاد اسناد نشان دهنده آن از 1 است. ما فکر می کنیم، یک راه این است که آرایه را در شاخص ضرب کنیم، بله اجازه دهید این کار را انجام دهیم.
بنابراین، آیا باید تایپ کنم؟…همم
بنابراین 3 سوال که نیاز به پاسخ دارد:
- آیا get_global_id(0) (و get_local_id(0) در این مورد) از 1 شروع می شود؟
- اگر مجموعه وظایف “باقیمانده” برابر با تعداد وظایفی است که ایجاد می کنیم، اگر شاخص بالاتر از کل وظایف باشد، چرا باید از آن خارج شوم؟
آیا دویدن بیش از حد غیر ممکن نیست؟ - آیا باید int را تایپ کنم تا آرایه را ضرب کنم؟
سپس تابع را به این تغییر می دهیم و متوجه می شویم:
__kernel void biscuit(__global double *array){ int idx=get_global_id(0); array[idx]*=idx; }
و بیایید برنامه را با این ایجاد کنیم، در اینجا انتظار 3 خطا از کامپایلر openCL را داریم.
باشه میگه برنامه ایجاد شد! عالی
string biscuit_source_code="__kernel void biscuit(__global double *array){rn" "int idx=get_global_id(0);rn" "array[idx]*=idx;}rn"; string build_log=""; program_handle=CLProgramCreate(ctx,biscuit_source_code,build_log); if(program_handle!=INVALID_HANDLE){ Print("Program created!"); }else{ Alert("Errorsn"+build_log); }
سپس حافظه ای را که فکر می کنم ایجاد می کنم
buffer_handle=CLBufferCreate(ctx,1000,CL_MEM_READ_WRITE);
من از تعداد زیادی دستگیره استفاده می کنم، من handle handler (و unloader) را می توان در اینجا ایجاد کرد، اما این یک آزمایش است.
سپس هسته، ما دسته برنامه را به اینجا می فرستیم، بنابراین نام هسته باید همان نامی باشد که در کد منبع ارسال شده است.
اسناد عبارتند از “نام هسته ای که اجرا از آن شروع می شود” بنابراین هیچ “عملکرد فرعی” نیازی به “هسته” ندارد؟ آن سوال 4 من حدس می زنم.
بسیار خوب، تا کنون هیچ خطایی وجود ندارد، احتمالاً در هنگام اجرا ظاهر می شوند.
این چیزی است که من تا الان دارم
#property copyright "Lorentzos Roussos" #property link "https://www.mql5.com/en/users/lorio" #property version "1.00" bool busy=false,loaded=false; int ctx=INVALID_HANDLE; int program_handle,kernel_handle,buffer_handle; int OnInit() { ctx=INVALID_HANDLE; program_handle=INVALID_HANDLE; kernel_handle=INVALID_HANDLE; buffer_handle=INVALID_HANDLE; busy=false; loaded=false; EventSetMillisecondTimer(44); return(INIT_SUCCEEDED); } void OnTimer(){ if(!busy){ busy=true; if(!loaded){ EventKillTimer(); ResetLastError(); ctx=CLContextCreate(CL_USE_GPU_DOUBLE_ONLY); if(ctx!=INVALID_HANDLE){ ResetLastError(); Print("CL.Context Created"); string biscuit_source_code="__kernel void biscuit(__global double *array){rn" "int idx=get_global_id(0);rn" "array[idx]*=idx;}rn"; string build_log=""; program_handle=CLProgramCreate(ctx,biscuit_source_code,build_log); if(program_handle!=INVALID_HANDLE){ ResetLastError(); Print("Program created!"); buffer_handle=CLBufferCreate(ctx,1000,CL_MEM_READ_WRITE); if(buffer_handle!=INVALID_HANDLE){ ResetLastError(); Print("buffer created"); kernel_handle=CLKernelCreate(program_handle,"biscuit"); if(kernel_handle!=INVALID_HANDLE){ ResetLastError(); Print("Kernel created"); }else{Print("Cannot create kernel #"+IntegerToString(GetLastError()));} }else{Print("Cannot create buffer #"+IntegerToString(GetLastError()));} }else{Alert("Errors #"+IntegerToString(GetLastError())+"n"+build_log);} }else{Print("Cannot create CL.context #"+IntegerToString(GetLastError()));} } busy=false; } } void OnDeinit(const int reason) { if(kernel_handle!=INVALID_HANDLE){CLKernelFree(kernel_handle);} if(buffer_handle!=INVALID_HANDLE){CLBufferFree(buffer_handle);} if(program_handle!=INVALID_HANDLE){CLProgramFree(program_handle);} if(ctx!=INVALID_HANDLE){CLContextFree(ctx);} } void OnTick() { }
اکنون، من باید آرگومان های هسته را اعلام کنم
در اینجا 3 نوع وجود دارد:
- CLSetKernelArg
- CLSetKernelArgMem
- CLSetKernelArgMemLocal
بنابراین، اولین مورد -من فرض میکنم- برای عبور دادن ثابتها است، مثلاً اگر یک مضرب را ارسال کنیم، با این خواهد بود.
دومی برای حافظه جهانی و سومی برای حافظه محلی است، حافظه محلی یک آرگومان در اندازه دریافت می کند و نه یک دسته بافر، بنابراین حافظه را در دستگاه به صورت محلی در CU اختصاص می دهد.
سوال پنجم این است که حافظه ثابت کجاست یا به صورت داخلی مدیریت می شود؟ شاید
بنابراین در اینجا من یک آرایه جهانی دارم بنابراین از CLSetKernelArgMem برای آرگومان اول استفاده خواهم کرد.
باشه
if(CLSetKernelArgMem(kernel_handle,0,buffer_handle)){ ResetLastError(); Print("Memory arg assigned to kernel"); }else{Print("Cannot assign memory arg#"+IntegerToString(GetLastError()));}
حالا چی؟ من باید حافظه را پر کنم، آرایه را پایین می فرستم، این مفید است.
اما صبر کن من آرایه ای ندارم، لعنتی. ما همزمان در حال آزمایش ایندکس هستیم (get_global_id(0)) پس بیایید یک آرایه ساختگی با مقدار 1.0 برای همه عناصر ایجاد کنیم.
باشه اولین ارور رو اینجا زدم بالاخره 😂 میگه خطای 5110
Print("Memory arg assigned to kernel"); double arr[]; ArrayResize(arr,1000,0); ArrayFill(arr,0,1000,1.0); uint filled=CLBufferWrite(buffer_handle,arr,0,0,1000); if(filled==1000){ Print("Filled "+IntegerToString(filled)+"items in buffer"); }else{Print("Cannot fill buffer #"+IntegerToString(GetLastError()));}
چه خطایی است که ببینیم ” ERR_OPENCL_WRONG_BUFFER_SIZE” اندازه بافر اشتباه است، اما چرا؟
بسیار خوب , اندازه بافر هنگام ایجاد بافر به بایت ها اشاره دارد نه آیتم ها ! خوب است بدانم، برای عادلانه بودن در اسناد آمده است. بد من
حالا که چی ؟ اجرا کردن ؟
بله، بسیار خوب، بنابراین به طور پیش فرض ناهمزمان است، من حدس میزنم، این سوال 6 است، بنابراین، اجرا را فراخوانی میکنم سپس دوباره تایمر را تنظیم میکنم و وضعیت اجرای هسته را جویا میشوم.
بیایید با نوع اجرای پیشفرض برویم، من یک پرچم مسدودکننده (مانند ویدیوها) نمیبینم، بنابراین باید به طور پیشفرض ناهمگام باشد.
ایناهاش :
void OnTimer(){ if(!busy){ busy=true; if(!loaded){ EventKillTimer(); ResetLastError(); ctx=CLContextCreate(CL_USE_GPU_DOUBLE_ONLY); if(ctx!=INVALID_HANDLE){ ResetLastError(); Print("CL.Context Created"); string biscuit_source_code="__kernel void biscuit(__global double *array){rn" "int idx=get_global_id(0);rn" "array[idx]*=idx;}rn"; string build_log=""; program_handle=CLProgramCreate(ctx,biscuit_source_code,build_log); if(program_handle!=INVALID_HANDLE){ ResetLastError(); Print("Program created!"); buffer_handle=CLBufferCreate(ctx,1000*8,CL_MEM_READ_WRITE); if(buffer_handle!=INVALID_HANDLE){ ResetLastError(); Print("buffer created"); kernel_handle=CLKernelCreate(program_handle,"biscuit"); if(kernel_handle!=INVALID_HANDLE){ ResetLastError(); Print("Kernel created"); if(CLSetKernelArgMem(kernel_handle,0,buffer_handle)){ ResetLastError(); Print("Memory arg assigned to kernel"); double arr[]; ArrayResize(arr,1000,0); ArrayFill(arr,0,1000,1.0); uint filled=CLBufferWrite(buffer_handle,arr,0,0,1000); if(filled==1000){ ResetLastError(); Print("Filled "+IntegerToString(filled)+"items in buffer"); if(CLExecute(kernel_handle)){ Print("Executing"); EventSetMillisecondTimer(44); loaded=true; }else{Print("Cannot execute kernel #"+IntegerToString(GetLastError()));} }else{Print("Cannot fill buffer #"+IntegerToString(GetLastError()));} }else{Print("Cannot assign memory arg#"+IntegerToString(GetLastError()));} }else{Print("Cannot create kernel #"+IntegerToString(GetLastError()));} }else{Print("Cannot create buffer #"+IntegerToString(GetLastError()));} }else{Alert("Errors #"+IntegerToString(GetLastError())+"n"+build_log);} }else{Print("Cannot create CL.context #"+IntegerToString(GetLastError()));} } else if(loaded){ ENUM_OPENCL_EXECUTION_STATUS status=(ENUM_OPENCL_EXECUTION_STATUS)CLExecutionStatus(kernel_handle); Comment("Kernel("+IntegerToString(kernel_handle)+" Status("+EnumToString(status)+")"); } busy=false; } }
این -بدیهی است- خیلی سریع تمام شد، اما چیزی که ما می خواهیم این است که نگاهی به آرایه بیندازیم.
بنابراین اگر کامل شد، بخوانید، چاپ کنید و به سرعت بروید (خروج) 🤓
if(status==CL_COMPLETE){ double get[]; ArrayResize(get,1000,0); ArrayFill(get,0,1000,0.0); ResetLastError(); if(CLBufferRead(buffer_handle,get,0,0,1000)){ string msg=""; for(int i=0;i<10;i++){ msg+=DoubleToString(get[i],2)+"n"; } Alert(msg); }else{Print("Cannot read buffer #"+IntegerToString(GetLastError()));} Print("Exit"); ExpertRemove(); }
aaand این چیزی است که ما دریافت کردیم، اولین عنصر 0 است، به این معنی که get_global_id(0) از 0 شروع می شود؟ اما بقیه 1.00 هستند
حالا باید بفهمم که آیا باید قبل از ضرب تایپ کنم یا نه، اما بیایید سریع خط ضرب را به این تغییر دهیم، من یک قوز دارم
array[idx]=array[idx]*idx;
نه، بنابراین من یک بافر دوم، یک int ایجاد می کنم و آن را با مقادیر شاخص پر می کنیم تا به انتهای این برسیم.
پس چه کنیم:
- رشته کد منبع را تغییر دهید
- بافر ایجاد کنید
- بافر arg را اضافه کنید، فقط این بار بنویسید (من فرض می کنم این enum ها از طرف دستگاه ها هستند نه مال ما)
- بافر int جدید را بخوانید و اگر 0,0,0,0,0,0 را دیدیم وحشت می کنیم
همچنین توجه داشته باشید که هیچ نشانه ای مبنی بر خطا دریافت نکردیم و محدودیت آرایه را بررسی نمی کنیم، یعنی چیزی را متوجه نشدم، به طور گسترده و فوری مشخص نیست یا از نظر نحوه کار گروه های کاری مهم نیست. به دستگاه منتقل می شود. ناشناخته های بسیاری.
به هر حال، من از این ساختار متنفرم، بنابراین با اضافه کردن بافر دوم، حتی زشت تر به نظر می رسد، اما این یک آزمایش است.
این کد به روز شده است:
void OnTimer(){ if(!busy){ busy=true; if(!loaded){ EventKillTimer(); ResetLastError(); ctx=CLContextCreate(CL_USE_GPU_DOUBLE_ONLY); if(ctx!=INVALID_HANDLE){ ResetLastError(); Print("CL.Context Created"); string biscuit_source_code="__kernel void biscuit(__global double *array,__global int *idx_array){rn" "int idx=get_global_id(0);rn" "idx_array[idx]=idx;rn" "array[idx]=array[idx]*idx;}rn"; string build_log=""; program_handle=CLProgramCreate(ctx,biscuit_source_code,build_log); if(program_handle!=INVALID_HANDLE){ ResetLastError(); Print("Program created!"); buffer_handle=CLBufferCreate(ctx,1000*8,CL_MEM_READ_WRITE); buffer_handle2=CLBufferCreate(ctx,1000*4,CL_MEM_WRITE_ONLY); if(buffer_handle!=INVALID_HANDLE&&buffer_handle2!=INVALID_HANDLE){ ResetLastError(); Print("buffer created"); kernel_handle=CLKernelCreate(program_handle,"biscuit"); if(kernel_handle!=INVALID_HANDLE){ ResetLastError(); Print("Kernel created"); if(CLSetKernelArgMem(kernel_handle,0,buffer_handle)&&CLSetKernelArgMem(kernel_handle,1,buffer_handle2)){ ResetLastError(); Print("Memory arg assigned to kernel"); double arr[]; ArrayResize(arr,1000,0); ArrayFill(arr,0,1000,1.0); uint filled=CLBufferWrite(buffer_handle,arr,0,0,1000); if(filled==1000){ ResetLastError(); Print("Filled "+IntegerToString(filled)+"items in buffer"); if(CLExecute(kernel_handle)){ Print("Executing"); EventSetMillisecondTimer(44); loaded=true; }else{Print("Cannot execute kernel #"+IntegerToString(GetLastError()));} }else{Print("Cannot fill buffer #"+IntegerToString(GetLastError()));} }else{Print("Cannot assign memory arg#"+IntegerToString(GetLastError()));} }else{Print("Cannot create kernel #"+IntegerToString(GetLastError()));} }else{Print("Cannot create buffer #"+IntegerToString(GetLastError()));} }else{Alert("Errors #"+IntegerToString(GetLastError())+"n"+build_log);} }else{Print("Cannot create CL.context #"+IntegerToString(GetLastError()));} } else if(loaded){ ENUM_OPENCL_EXECUTION_STATUS status=(ENUM_OPENCL_EXECUTION_STATUS)CLExecutionStatus(kernel_handle); Comment("Kernel("+IntegerToString(kernel_handle)+" Status("+EnumToString(status)+")"); if(status==CL_COMPLETE){ double get[]; ArrayResize(get,1000,0); ArrayFill(get,0,1000,0.0); int get_idx[]; ArrayResize(get_idx,1000,0); ArrayFill(get_idx,0,1000,-1); ResetLastError(); if(CLBufferRead(buffer_handle,get,0,0,1000)&&CLBufferRead(buffer_handle2,get_idx,0,0,1000)){ string msg=""; for(int i=0;i<10;i++){ msg+=DoubleToString(get[i],2)+"(idx:"+IntegerToString(get_idx[i])+")n"; } Alert(msg); }else{Print("Cannot read buffer #"+IntegerToString(GetLastError()));} Print("Exit"); ExpertRemove(); } } busy=false; } }
و من روی تمام مقادیر شاخص 0 می گیرم … هوم . که این سوال پیش می آید که اولین مقدار آرایه ضرب می شود و بقیه ضرب می شوند؟
پس آیا فقط اولی را اجرا می کند؟
بسیار خوب، بیایید بررسی حد را به سرعت اضافه کنیم.
نه نه
خوب اگر CLExecute فقط یک بار اجرا شود چه؟
هوم، بسیار خوب، پس اگر من یک شمارنده از خودم بسازم و به پمپ کردن آن ادامه دهم تا تمام شود، چه میشود که 44 ثانیه طول میکشد (44 میلیثانیه*1000)، بنابراین موارد را به 100 کاهش میدهم.
اما این موازی چگونه است؟ wtf . این مشکل مربوط به اسناد mql5 است، شخصی که کد را میفهمد، اسناد را مینویسد و حوصلهاش سر میرود یا شخصی که کد مینویسد و اسناد متفاوت است. در لحظه اوج شما چیزی را کاملاً درک می کنید که باید به طور گسترده برای ما دهقانان توضیح داده شود. چرا؟ زیرا اکوسیستم شما را سریعتر رشد می دهد! تصور کنید اگر 10000 کدنویس اسناد مربوط به این را بخوانند، اگر درصد بزرگتری آن را در زمان کمتری درک کنند، آنها زودتر چیزهای بیشتری ایجاد خواهند کرد. چیزهای بیشتر باعث جذب فعالیت بیشتر و غیره خواهد شد.
به هر حال بیایید ببینیم چه چیزی به اشتراک گذاشته اند، من آرایه هایی را به جای اعداد صحیح برای اندازه ها در آنجا می بینم. باشه
بله، بسیار خوب، بنابراین باید مطالعه اضافی و تعامل با توپ کریستالی وجود داشته باشد تا سعی کنید و ارتباط برقرار کنید. global_work_offset[] و جهانی_کار_اندازه[] و اندازه_کار_محلی[] آرایهها را به ویدیوی آموزشی بالا میآورم، اما آرایه افست را با یک عنصر (یک بعدی) روی 0 و آرایه اندازه کار با یک عنصر (بعد) را روی 1000 تنظیم کردم و کار کرد.
بنابراین، get_global_id(0) از 0 شروع می شود، بنابراین اسناد آنها دارای خطای کوچکی هستند، مگر اینکه چیز دیگری را از دست بدهم – که در هیچ کجا مستند نشده است-
این کد است
مال من و شما برای اولین بار cl باز شد، من آن را به عنوان بیش از 64k پیوست می کنم
و در اینجا قسمت دوم ویدیوی بالا با جزئیات بیشتر در OpenCL C است
سوالات باقی مانده:
- اگر مجموعه وظایف “باقیمانده” برابر با تعداد وظایفی است که ایجاد می کنیم، اگر شاخص بالاتر از کل وظایف باشد، چرا باید از آن خارج شوم؟
آیا دویدن بیش از حد غیر ممکن نیست؟ - آیا باید int را تایپ کنم تا آرایه را ضرب کنم؟
- توابع حافظه ثابت کجاست؟
- آیا توابع فرعی باید به عنوان هسته ایجاد شوند؟
ویدیو دوم.
من این را همانطور که فکر می کردم تایپ کردم، بنابراین، امیدوارم مفید باشد.
آموزش مجازی مدیریت عالی حرفه ای کسب و کار Post DBA + مدرک معتبر قابل ترجمه رسمی با مهر دادگستری و وزارت امور خارجه | آموزش مجازی مدیریت عالی و حرفه ای کسب و کار DBA + مدرک معتبر قابل ترجمه رسمی با مهر دادگستری و وزارت امور خارجه | آموزش مجازی مدیریت کسب و کار MBA + مدرک معتبر قابل ترجمه رسمی با مهر دادگستری و وزارت امور خارجه |
مدیریت حرفه ای کافی شاپ | حقوقدان خبره | سرآشپز حرفه ای |
آموزش مجازی تعمیرات موبایل | آموزش مجازی ICDL مهارت های رایانه کار درجه یک و دو | آموزش مجازی کارشناس معاملات املاک_ مشاور املاک |
- نظرات ارسال شده توسط شما، پس از تایید توسط مدیران سایت منتشر خواهد شد.
- نظراتی که حاوی تهمت یا افترا باشد منتشر نخواهد شد.
- نظراتی که به غیر از زبان فارسی یا غیر مرتبط با خبر باشد منتشر نخواهد شد.
ارسال نظر شما
مجموع نظرات : 0 در انتظار بررسی : 0 انتشار یافته : ۰