طي الأوزان، ومسارات CUDA، والخطأ البرمجي الذي جعل نموذجي يتحدث بالعكس — فيليب ماكرادولي

AAI Engineer
Computing/SoftwareInternet Technology

Transcript

00:00:00.
00:00:13مرحبًا جميعًا، شكراً لحضوركم و
00:00:18سأبدأ الحديث الآن. لذا
00:00:22يدور هذا الحديث حول ورقة بحثية
00:00:27أعددتها، وهي بسيطة جدًا.
00:00:33الطرح واضحة للغاية.
00:00:36إنها تتكون أساسًا من سطرين من
00:00:39الجبر اللذين يجعلان طبقة RMS norm
00:00:42في نماذج المحولات أقل تكلفة،
00:00:45وأسرع، وتحسنها بطريقة ما
00:00:48كطبقة ضمن بنية
00:00:51المحولات (transformers). بشكل مشابه لكيفية
00:00:54استخدام layer norm سابقاً كمعيار أسبوعي
00:00:57ثم تم استبدالها بـ RMS norm.
00:01:00هذا العمل يتماشى مع هذا النهج من التفكير.
00:01:04وقد حظيت بفرصة لقاء
00:01:09بعض الأشخاص من عالم المصادر المفتوحة
00:01:11شاركت في تأليف هذه الورقة
00:01:14جنبًا إلى جنب مع نيلز غراف الذي كان
00:01:17بمثابة المبتكر لهذه الفكرة.
00:01:21ومن هناك ينطلق هذا العمل.
00:01:23هذا المعروض مستند على arXiv.
00:01:26يمكنكم الاطلاع عليه، وقراءته، وتجربته.
00:01:29يوجد مستودع برمجي أيضاً.
00:01:31وبالنسبة للمفهوم، فلنقل إن الفكرة
00:01:36وطريقة التفكير يكون من الأسهل
00:01:38شرحها ربما عبر آلية flash attention.
00:01:40فبالطريقة نفسها التي تنتظر بها آلية flash attention
00:01:45حتى تكون هناك عملية ضرب
00:01:48وتسعى للحد من عمليات الاتصال هذه
00:01:52بين الذاكرة لتصبح العملية بأكملها
00:01:54أسرع.
00:01:55تعتبر هذه فكرة مشابهة في ذلك الاتجاه.
00:01:58وهي تقوم ببعض تحسينات
00:02:01تجعل عملية RMS norm أسرع بكثير
00:02:08وبالتالي تحسن نموذج المحول ككل.
00:02:15وهناك سؤال مطروح: حسناً، لماذا RMS norm؟
00:02:19بما أن تلك الطبقة لا تقوم بأي عمليات رياضية تقريبًا.
00:02:24وهذا صحيح.
00:02:26إن حصة الجزء الرياضي منها
00:02:29إذا نظرت إليها صغيرة جداً.
00:02:31ومع ذلك، فإن الوقت الفعلي أو زمنيًا،
00:02:34كما يقولون، كبير للغاية.
00:02:36فعلى سبيل المثال، في خطوة فك تشفير واحدة،
00:02:40أي عند القيام بالاستدلال،
00:02:43يمكن بدء تشغيل RMS norm نحو 33 مرة.
00:02:47بالطبع، يعتمد ذلك على النموذج وما إلى ذلك.
00:02:50في الورقة البحثية، لديكم النماذج المحددة
00:02:52وكيف تم اختبار ذلك.
00:02:54والسؤال هو كيف يمكن تحسين هذا
00:02:59وكيف يمكن تفادي وزن ضرب المصفوفات
00:03:03نوعاً ما.
00:03:06والسبب في بطء هذا
00:03:09هو أن وحدات معالجة الرسوميات ليست بطيئة أو سيئة في الرياضيات،
00:03:15بل سيئة في كل شيء آخر حول الحسابات الفعلية.
00:03:20وهذا يعني بدء العمل، العمل الفعلي.
00:03:24فمثلاً، بدء العملية كما يحدث
00:03:30في بعض التجارب 33 مرة،
00:03:32يستغرق وقتاً طويلاً.
00:03:35وعلى سبيل المثال، دمج كل عملية تطبيع (normalization)
00:03:40في عملية ضرب المصفوفات يمكن أن يساعد في تجنب ذلك.
00:03:44كذلك القيام بطي الأوزان (weight folding)
00:03:46يمكن أن يساعد في نقل البيانات بين الذاكرة
00:03:50وتلك عملية بطيئة أيضاً بالنسبة لوحدات معالجة الرسوميات.
00:03:54وأيضاً وقت الانتظار.
00:03:56فمثلاً، تأجيل عملية القسمة
00:04:00التي تتم في طبقة RMS norm
00:04:02هو أيضاً وسيلة لتجنب خطوة الانتظار هذه.
00:04:06لذا، ما تفعله هذه الورقة البحثية أساساً
00:04:09هو تحسين كل الجوانب الثلاثة هذه
00:04:13عبر القيام ببعض الحيل الجبرية
00:04:16في طريقة حساب RMS norm.
00:04:19هذا كل شيء.
00:04:21ومن الناحية الرياضية، هذه هي الحيل.
00:04:25إنها تتمحور بشكل أساسي حول أول طرحين.
00:04:29الأول هو التطبيع بدون أوزان.
00:04:31يمكنكم رؤية ذلك هنا.
00:04:33والتطبيع المؤجل.
00:04:35إذن هذا هو الثاني.
00:04:37والآن في البنيات الأكثر حداثة،
00:04:39هناك حالة يمكن أن تظهر فيها RMS مرتين.
00:04:44فعلى سبيل المثال، يحدث هذا في نموذج Gemma 4.
00:04:48لذا فإن إلغاء التطبيع الأولي يعمل أيضاً.
00:04:52وكل هذا تم إثباته جبرياً في الورقة البحثية.
00:04:58والطرح الأول هو هذا
00:05:00حيث ينطوي كسب وطيات الوزن
00:05:04في مصفوفة واحدة.
00:05:06يمكنكم رؤية W هنا بنجمة.
00:05:09ويتم حساب ذلك أوفلاين (مسبقاً)،
00:05:12بشكل مشابه لربما في flash attention
00:05:14حيث تقوم بحساب بعض الأشياء جانبيًا
00:05:16حتى لا تكون هناك اتصالات
00:05:18بين أجزاء الذاكرة طوال الوقت.
00:05:20إذن هذه خطوة تم إنجازها،
00:05:24وهي طي الأوزان.
00:05:26والخطوة الأخرى هي تأجيل قسمة السلمي (scalar divide)
00:05:32لضرب المصفوفات بحيث يمكن تنفيذها بالتوازي.
00:05:35في الحالة العادية، يتعين عليك الحساب مرة،
00:05:38ثم الانتظار والحساب مجدداً.
00:05:41في هذه الحالة، الفكرة هي تقسيم هذا
00:05:44بحيث يمكن تنفيذها بالتوازي.
00:05:48والطرح الثالث، وهو نوع من هذه النسخة،
00:05:51هو أن هناك نوعاً ما، إذا وجد اثنان،
00:05:56ونظراً لأن هذا غير متغير مع المقاييس،
00:05:58يمكن إسقاط أحدهما ويستمر بالعمل.
00:06:01وهذا ينطبق على النماذج الأحدث
00:06:04التي يمكن أن تمتلك هذه البنية والتنفيذ.
00:06:10لذا، من أجل تحويل هذا إلى واقع،
00:06:13خاصة هذا الطرح رقم اثنين.
00:06:16بالنسبة لهذا المفهوم مثلاً، الأمر سهل.
00:06:20هناك مستودع يسمى transformer tricks.
00:06:22يمكنك تطبيق هذا على أي نموذج وسيعمل.
00:06:25ولكن للقيام بذلك، يلزم بعض العمل على النواة (kernel).
00:06:29لذا فليس من السهل جداً القيام به.
00:06:31ومن أجل أن أقوم بذلك،
00:06:35كنت أطبق هذا وخرجت بهذه التجربة مرة.
00:06:42تبدو الفكرة جيدة بشكل عام حيث يبدو الأمر مثل:
00:06:46حسناً، النص التوجيهي هو “بنية المحول (transformer)
00:06:48حدثت ثورة في معالجة اللغات الطبيعية لأن”،
00:06:51ثم هناك نوع من المخرجات المتوقعة.
00:06:54لكن في المخرجات التي حصلت عليها،
00:06:56رأيت هذا التكرار وتأخراً بخطوة واحدة.
00:06:59كما ترون هنا، تظهر كلمة “لأن” مجدداً.
00:07:01وكان هناك شيء ما يحدث مع التدفقات في وحدة المعالجة
00:07:07وكنت أحاول معرفة ما الذي يحدث.
00:07:10وكنت أحصل على هذا التأخر بخطوة ونوعاً من المخرجات
00:07:15التي كانت من الماضي بطريقة ما.
00:07:18وأثناء تصحيح كل هذا،
00:07:21أدركت أنه في عملية بناء شيء من هذا القبيل.
00:07:26فكما شرحت في الطرح الثاني أو تأجيل هاتين العمليتين،
00:07:32في CUDA، يمكنك القيام بأمرين.
00:07:35يمكنك استخدام أنوية الموتر (tensor cores) التي تقوم بجزء من ضرب المصفوفات
00:07:39ويمكنك استخدام أنوية CUDA التي تشغل أموراً مثل العمليات العنصرية،
00:07:44والاختزالات، والجذور التربيعية، وما إلى ذلك.
00:07:47لذا كانت الفكرة هي القيام بذلك بالتوازي والحصول على الفائدة
00:07:52مما كنت أشرحه في الورقة البحثية لاختبار هذا المفهوم بالفعل.
00:07:58هكذا كان من المفترض أن يبدو الأمر.
00:08:00إذا قمت بتنفيذ الأمور بالتتابع،
00:08:03فإن هناك وقت انتظار خامل عندما تحسب وحدة المتجهات الـ RMS وتحجيم الأبعاد،
00:08:10ثم تليها عملية ضرب المصفوفات.
00:08:12لذا كانت الفكرة هي: حسناً، باستخدام flash norm، وهي التقنية المذكورة في الورقة البحثية،
00:08:16من المفترض إجراء العمليتين معاً بالتوازي.
00:08:19بحيث تحسب وحدة المصفوفة matmul بينما تحسب وحدة المتجهات RMS.
00:08:23وبهذه الطريقة توفر الوقت.
00:08:26ولكن لا يمكنك فعل ذلك ببساطة في بلغة بايثون.
00:08:28عليك الانتقال لمستوى أدنى قليلاً.
00:08:30وقد قمت بذلك باستخدام أكواد CUDA بهذه الطريقة.
00:08:35وبدا هذا بشكل عام جيداً في ذلك الوقت.
00:08:42لكنني أدركت لاحقاً أنني ارتكبت خطأ صغيراً.
00:08:47وهو أن عملية الدمج في النهاية، حيث يُفترض دمج التدفقين، كانت ضمنية في حالتي.
00:08:57وعندما اختبرت هذا، نجح اختبار الوحدة.
00:09:01وبدت الجودة متماثلة في اختبارات الحيرة (perplexity) وما إلى ذلك لأنها عملية توليد متشابهة.
00:09:08لكن مع التوليد الطويل، تمكنت من ملاحظة المشكلة.
00:09:11ولم تكن لدي أي فكرة عن سبب حدوث ذلك.
00:09:14وكان السبب أنه عند إجراء الدمج الضمني، لم يكن أحد التدفقات قد أنهى عمله بعد.
00:09:24فحدثت ظروف تنافس (race conditions) قرأت من الماضي من عملية ضرب مصفوفات لم تكتمل.
00:09:31لذا كانت الفكرة لإصلاح ذلك تدور حول جعل عملية الدمج صريحة واضحة.
00:09:40والانتظار حتى تنتهي إحدى العمليات لضمان عدم القراءة من البيانات القديمة عند الدمج.
00:09:48كان هذا هو الاستنتاج الرئيسي في استكشاف تدفقات CUDA.
00:09:54وهكذا قمت بتنفيذ الأمر.
00:09:57حيث كان الدمج ضمنياً.
00:10:00فقرأت عملية التحجيم اللاحق قيمة قديمة من المخزن المؤقت.
00:10:06وتم إصلاح هذا الكود بهذه الطريقة حيث يتعين تحديد نهاية عملية ضرب المصفوفات.
00:10:14ثم تحديد نهاية حساب الـ RMS.
00:10:17وبعد ذلك ننتظر التدفق الأول في عملية التحجيم اللاحق.
00:10:21ثم ننتظر التدفق الثاني.
00:10:24أصلح ذلك الخطأ وجعل فكرة الورقة تنجح، والنموذج ينطق للأمام بدلاً من الخلف.
00:10:33كان هذا المنظور الأكاديمي الرائع ربما.
00:10:38لكنني أردت أيضاً تجربة الأمور عملياً، أليس كذلك؟
00:10:40نشر هذا وتجربته، ومعرفة كيف يمكنني تشغيله في بيئة إنتاجية.
00:10:47يمكنكم أيضاً قراءة الورقة البحثية وملاحظة جميع الاختبارات.
00:10:50معظمها أُجري على نماذج Llama، ولكن هذا يعمل على معماريات أخرى أيضاً.
00:10:56لذا، ما يمكنك فعله لهذه الورقة البحثية المحددة هو، على سبيل المثال، طي الأوزان الذي شرحته، الاقتراح الأول، يمكنك القيام به باستخدام بعض البرمجيات في المستودع.
00:11:10الأمر سريع جداً، تقول فقط flashify وهو يقوم بذلك.
00:11:13ومع ذلك، بالنسبة للنقطة الثانية التي ذكرتها، تحتاج إلى القيام ببعض العمل على النواة (kernel) إذا أردت ذلك.
00:11:19كما شرحت في مثالي.
00:11:22وهذه بعض النتائج المبنية على نماذج Llama وهناك أنواع مختلفة من التفاصيل التي يمكنك الاطلاع عليها أيضاً.
00:11:29مثل ماذا يحدث إذا قمت فقط بالتطبيع المؤجل، وماذا يحدث إذا استخدمت نواة مدمجة بالكامل.
00:11:36لذا فهناك الكثير من التجارب للتعمق هنا واختبار جميع الاقتراحات.
00:11:41وكانت هذه نتائجنا في مستويات مختلفة، لنقل، من التدقيق والتفاصيل.
00:11:48ولكن حتى الأسلوب البسيط مثل طي الأوزان يظهر بعض التحسن.
00:11:54وهذا يعمل أيضاً مع الأدوات اليومية التي تستخدمها في النموذج.
00:11:59لذا ليس الأمر وكأن عليك إعادة اختراع العجلة أو البدء من الصفر.
00:12:05حيث يعمل مع torch compile لأنه يشبه نوعاً ما نقطة حفظ جديدة، وهذا كل شيء.
00:12:13وتطبيق Flash Attention يقدم حيلاً مماثلة عند طبقة مختلفة.
00:12:16كما أنه يعمل مع النماذج الكمية (quantized models).
00:12:18لذا فمن الرائع حقاً تطبيق هذا ويمكنك الحصول على نموذج يحتوي على طبقة التطبيع الجديدة والممتازة هذه.
00:12:27والمكان الذي يمكنك الحصول منه على هذه التفاصيل والبرمجيات لتشغيل هذا هو مستودع transformer tricks.
00:12:34فهو يحتوي على حيل جبرية مختلفة كما شرحت، بالإضافة إلى الورقة البحثية التي ذكرتها.
00:12:41وأيضاً هناك مستودع نماذج HuggingFace، حيث قمت بتطبيق هذا على بعض النماذج.
00:12:50ويمكنك الحصول على رابط HuggingFace لذلك النموذج واختباره.
00:12:54وما يمكنك فعله أيضاً مع نماذج HuggingFace هذه هو نشرها في بيئة الإنتاج.
00:13:00فعندما كنت أفكر في القيام بذلك، أدركت أنه حسناً، الآن وقد اكتمل الجانب العلمي وهناك رابط لنموذج HuggingFace،
00:13:11فإن محرك الاستنتاج الخاص بـ Superlink كان طريقة رائعة لنشر أي نموذج HuggingFace بالفعل.
00:13:19وقد قمنا بذلك في فعاليات الهثاكاثون حيث يقدم المشاركون نموذج HuggingFace مخصصاً أو نقطة حفظ قاموا بتعديلها ودوزنتها.
00:13:27ويمكنك الاختبار بنفسك، حتى لو كان لديك نسختك الخاصة من هذه الحيل الجبرية وتحب تحسين نموذج واختبار أفكارك البحثية،
00:13:37يمكنك تجربة ذلك بالفعل وتوفير نسخة منشورة من هذا النموذج على عنقود سحابي دون الحاجة للقلق بشأن الكود الوسيط لنشر النماذج.
00:13:48وهذا أمر رائع للغاية. والنقطة الأساسية هي أنه إذا كان العنقود بالكامل مفتوح المصدر واستنتاج النموذج مفتوح المصدر أيضاً،
00:13:58يمكنك حينها اختباره على هذه الأفكار البحثية التي قد تكون أكثر ابتكاراً، كأن ترغب في التلاعب بالنواة أو تطبيق flash norm وما شابه،
00:14:12حيث يكون من الصعب جداً القيام بذلك عبر نقطة نهاية مستأجرة لا تملك فيها عملية الاستنتاج.
00:14:18لذلك، فأنت بحاجة إلى شيء قابل للنقل والمرونة ليتيح لك القيام بهذا العمل،
00:14:23ولكنه أيضاً جاهز للإنتاج بدرجة كافية لتتمكن من اختبار الأشياء على نطاق واسع.
00:14:27ويمكنك، على سبيل المثال، استخدام Cy لدمج هذا مع نماذج أخرى.
00:14:33كما ترون في أعلى اليسار، يمكنك الحصول على هذه النماذج المحسنة مع نماذج أخرى لتنفيذ مهام وكيلة (agentic tasks) إذا أردت،
00:14:43وتطبيق حالة الاستخدام الأكبر تلك من البداية إلى النهاية.
00:14:46والطريقة التي يعمل بها Cy هي أن عنقود الإنتاج هذا يساعدك على نشر النماذج.
00:14:52لذا يمكنك إلقاء نظرة على مستودع Cy لمزيد من التفاصيل حول هذا.
00:14:57وهناك أيضاً آلية تنظيم طوابير أذكى تساعدك، خاصة إذا كنت تعمل مع نماذج أصغر،
00:15:03لأنني عند العمل على تقنية flash norm، عملت مع نماذج Llama أصغر وكذلك مع وكلاء أصغر من HuggingFace.
00:15:11لذا فإن توفير طريقة لنشر النماذج الأصغر التي يمكنها العمل أيضاً على نفس وحدة معالجة الرسومات (GPU) حتى لا تضطر إلى إنفاق أموالك على تكاليف GPUs،
00:15:24بل التبديل بين النماذج بمرونة، خاصة النماذج الأصغر، كان أمراً مفيداً للغاية.
00:15:30ويمكنك أيضاً التحكم في إعدادات النموذج عبر واجهة برمجة التطبيقات (API) بالإضافة إلى العنقود السحابي،
00:15:35وهو أمر مريح للغاية دون الحاجة إلى متخصص في البنية التحتية ليدعمك في بحثك مفتوح المصدر.
00:15:40لذا فهذا أمر ممتاز أيضاً.
00:15:43وأنت تمتلك سحابتك الخاصة، وهو أمر مفيد إذا كنت تريد أوزاناً مفتوحة ونماذج مفتوحة ومصادر مفتوحة.
00:15:50وهناك أيضاً كتالوج يملكه Cy لنماذج مختلفة، وليس فقط النماذج التي ذكرتها،
00:15:57ولكن يمكنك إلقاء نظرة.
00:15:59هناك أيضاً نماذج إعادة الترتيب والتضمين (re-ranking & embedding) إذا كنت تبني شيئاً في هذا المجال.
00:16:03وبهذا أصل نوعاً ما إلى نهاية قصة رحلتي البحثية حيث شاركت في تأليف هذه الورقة حول تقنية تحسن نموذج الـ Transformer،
00:16:15ولكنني وجدت أيضاً طريقة لنقل هذا إلى بيئة الإنتاج واختباره، والعمل بمرونة مع هذه النماذج مفتوحة المصدر.
00:16:24ولا تترددوا في التواصل معي عبر LinkedIn، إذا كانت لديكم أي أسئلة أو مساهمات.
00:16:30العديد من هذه الأشياء التي ذكرتها، بعضها عبارة عن طلبات سحب (PRs) على VLLM أو على Hugging Face.
00:16:36قد تجدونها في كل مكان.
00:16:38يمكنكم أيضاً الاطلاع على الورقة البحثية.
00:16:40هذا هو رابط arXiv المتاح لديكم هناك.
00:16:43ولديك أيضاً مستودع Cy وحسابي على LinkedIn.
00:16:47لذا، شكراً جزيلاً لكم على الحضور.
00:16:58يمكنكم طرح الأسئلة علي.
00:16:59سنكون هنا.
00:17:00بالقرب منكم.
00:17:13شكراً لكم

Key Takeaway

تؤدي تحسينات الجبر وتأجيل العمليات وطي الأوزان إلى جعل طبقة RMS norm أسرع بكثير في نماذج المحولات وتجنب ظروف التنافس في أنوية CUDA.

Highlights

  • تتكون الورقة البحثية من سطرين من الجبر يجعلات طبقة RMS norm في نماذج المحولات أقل تكلفة وأسرع وأكثر تحسينًا.

  • يمكن بدء تشغيل طبقة RMS norm نحو 33 مرة في خطوة فك تشفير واحدة أثناء الاستدلال.

  • يتضمن الحل طي الأوزان في مصفوفة واحدة وتأجيل قسمة السلمي لضرب المصفوفات لتنفيذها بالتوازي.

  • يؤدي الدمج الضمني في أنوية CUDA إلى ظروف تنافس وقراءة قيم قديمة من المخزن المؤقت.

  • يتم إصلاح خطأ التدفقات عبر تحديد نهاية عملية ضرب المصفوفات ونهاية حساب RMS ثم انتظار التدفقات.

Timeline

مقدمة حول تحسين طبقة RMS norm

  • تتألف الفكرة من سطرين جبريين لتقليل تكلفة طبقة RMS norm في نماذج المحولات.
  • تم استيحاء الفكرة من آلية flash attention لتقليل اتصالات الذاكرة.
  • تتكرر طبقة RMS norm نحو 33 مرة في خطوة فك تشفير واحدة أثناء الاستدلال.

تتناول هذه الورقة البحثية تحسين طبقة RMS norm المشابهة لتطور استخدام layer norm سابقاً. تبدو الحصة الرياضية للطبقة صغيرة، لكن الوقت الفعلي لتشغيلها المتكرر أثناء الاستدلال يعد كبيراً للغاية بسبب بطء وحدات معالجة الرسوميات في إدارة عمليات الاتصال وبدء العمل.

الأسس الجبرية وتأجيل العمليات

  • يتم دمج عملية التطبيع في ضرب المصفوفات لتجنب هدر الوقت.
  • يساهم طي الأوزان في تقليل عمليات نقل البيانات البطيئة في الذاكرة.
  • يتم تأجيل قسمة السلمي لتنفيذ العمليات بشكل بالتوازي.

تعتمد الورقة على ثلاثة جوانب للتحسين تشمل التطبيع بدون أوزان، والتطبيع المؤجل، وإلغاء التطبيع الأولي في النماذج الحديثة مثل Gemma. يتم حساب طي الأوزان مسبقاً أوفلاين وتجميعها في مصفوفة واحدة لتقليل الاتصالات المستمرة بأجزاء الذاكرة.

خطأ التدفقات في CUDA ومعالجته

  • يؤدي الدمج الضمني للتدفقات إلى قراءة بيانات قديمة من المخزن المؤقت.
  • تحدث ظروف تنافس عندما لا ينتهي أحد التدفقات من عمله.
  • يتطلب الإصلاح تحديد نهاية ضرب المصفوفات ونهاية حساب RMS بوضوح.

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

التطبيق العملي ونشر النماذج

  • يتوفر طي الأوزان في مستودع transformer tricks لتنفيذ سريع وسهل.
  • تعمل هذه الحيل مع نماذج Llama والنماذج الكمية وتدعم torch compile.
  • يسمح عنقود الإنتاج Cy بنشر النماذج المحسنة واختبارها بمرونة.

تتيح المستودعات المتاحة تطبيق طي الأوزان وحيل الجبر بسهولة عبر أوامر مثل flashify دون الحاجة لبدء العمل من الصفر. كما يسهل نشر النماذج المخصصة واختبارها في بيئة الإنتاج باستخدام أدوات مفتوحة المصدر دون الاعتماد على نقاط نهاية مستأجرة مقيدة.

Community Posts

No posts yet. Be the first to write about this video!

Write about this video