تضمينات سريعة على وحدات معالجة الرسومات

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

المؤلفونPerplexity Engineering

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

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

تضمينات للبحث

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

  • تضمين الدفعات: عند بناء قاعدة البيانات أو توسيعها أو إعادة فهرستها، يجب تضمين المستندات الكبيرة في المساحة المتجهية، مما يزيد من الإنتاجية إلى أقصى حد لتقليل التكلفة.

بعد البحث عن المتجهات، يجب تقييم دفعات كبيرة من المستندات، مما يحقق التوازن بين الإنتاجية وزمن الاستجابة.

  • التضمين عبر الإنترنت: عند الاستعلام عن قاعدة البيانات، يجب تضمين استعلام قصير لعمليات البحث، مما يقلل من زمن الاستجابة.

لقد أنشأنا بنيتنا التحتية للاستدلال للاستفادة من أكبر عدد ممكن من المكونات المشتركة عبر حالات الاستخدام المختلفة. نظراً لأننا نستخدم عادةً نماذج Transformer الصغيرة لإنتاج التضمينات، فإننا نتشارك الجزء الأكبر من التنفيذ مع كود استدلال LLM الخاص بنا: تضمينات الدفعات تشبه مرحلة التعبئة المرتبطة بالحساب، بينما التضمينات عبر الإنترنت، التي تعمل غالباً على عدد قليل من الرموز، تشبه من الناحية الحسابية عملية فك التشفير المرتبطة بالذاكرة. وبالتالي فإننا نعيد استخدام نوى التعبئة وفك التشفير المحسّنة لتقديم نماذج التضمين. ونتيجة لذلك، يمكننا تحقيق إنتاجية هائلة للاستدلال المجمع مع الحد الأدنى من العمل الهندسي الإضافي، مع الحفاظ على زمن استجابة منخفض لأحمال عمل التضمين عبر الإنترنت.

التوليب والورد وبعض اللبلاب

نحن نعرض الاستدلال من خلال واجهات برمجة تطبيقات قياسية، داخلياً وخارجياً من خلال منصة واجهة برمجة التطبيقات الخاصة بنا. وراء الكواليس، تشارك خدمات متعددة في معالجة طلب التضمين:

  • Ivy هو بوابة HTTP بلغة Rust تستدعيها خدمات Perplexity.

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

  • Tulip هو واجهة خادم الاستدلال.

إنه خادم gRPC مُنفذ باستخدام Rust وtokio و`tonic`. يتلقى Tulip طلبات استدلال gRPC، ويتولى الجدولة والتجميع. ثم يرسل الدفعات إلى محرك ROSE، ويعيد الاستجابات المكتملة إلى العملاء.

تم تحديدها أساساً بلغة Python، حيث توفر نواة (kernels) والطبقات والتعريفات لمجموعة واسعة من النماذج. تنفذ ROSE عمليات التمرير الأمامي عبر النماذج، كما توفر إدارة رسومات CUDA المخصصة للتضمينات. وهي متصلة بـ Tulip عبر وظيفة step()، التي تأخذ دفعة وترجع مرجعاً للحساب الذي تنفذه على المسرّع.

بنية التدفق من طلب التضمينات عبر Ivy إلى خوادم Tulip المتماثلة

الاهتمام بما وراء النواة

تعتبر كل من النماذج المستندة إلى المحولات (Transformer) وهندسة Hopper/Blackwell الأساسية تقنيات ناضجة، لذا فقد تقارب استدلال التضمين على جانب وحدة المعالجة الرسومية (GPU) ليصبح تنفيذاً مثالياً إلى حد كبير عبر محركات الاستدلال المختلفة. ورغم ذلك، اكتشفنا فرصاً إضافية للتحسين في أوقات التشغيل والأنظمة التي تعرض النماذج من البداية إلى النهاية للعميل. على وجه الخصوص، وجدنا أنه يمكننا تحسين زمن الاستجابة من خلال الإدارة الدقيقة لرسومات CUDA وبناء تجريد LazyTensor لتتبع نتيجة جانب وحدة المعالجة الرسومية غير المتزامنة في محرك Rust الأصلي. لقد نفذنا هذه الميزات في Tulip، بحيث يمكنها التفاعل بفعالية مع تطبيقات نماذج ROSE.

Tulip

لقد صممنا Tulip لتكون واجهة خفيفة الوزن قدر الإمكان عبر تقديم النماذج الخاصة بنا. وهي تتعامل مع الطلبات الواردة في مهام Tokio غير المتزامنة، مع الحفاظ على مجموعة من الطلبات التي تتتبعها وجدولة الدفعات منها لإرسالها إلى المسرّع. آلية الجدولة في Tulip بسيطة للغاية: تتراكم الطلبات بينما تقوم Tulip بإرسال العمل أو انتظار النتائج. من بين الطلبات المتراكمة، يتم اختيار التسلسلات على أساس الأسبقية (من يأتي أولاً يُخدم أولاً) لتشغيلها عبر النموذج.

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

للتفاعل بفعالية مع النموذج، تعتمد Tulip على رسومات CUDA وتتبع النتائج الكسولة لتداخل عمل وحدة المعالجة الرسومية ووحدة المعالجة المركزية والاستفادة الكاملة من الموارد المتاحة.

إدارة رسومات CUDA

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

التمرير الأمامي الحريص: استدعاءات المضيف المتداخلة مع نوى الجهاز

لتقليل النفقات العامة، بدلاً من إطلاق نوى مستقلة، يمكن بناء رسم بياني لـ CUDA لالتقاط البيانات الوصفية المطلوبة لإطلاق جميع نوى التمرير الأمامي باستدعاء واحد لبرنامج تشغيل CUDA. يلغي هذا الحاجة إلى إعادة تشغيل أكواد Python وPyTorch الباهظة للتكوينات التي يمكن التقاط رسومات CUDA لها.

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

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

مرور أمامي لرسومات Cudagraph

يجب التقاط رسومات CUDA لكل تكوين مميز، وهو ما يعني بالنسبة للتضمينات رسماً بيانياً لكل تركيبة من عدد التسلسلات وعدد الرموز. نظراً لأن هذه الشبكة واسعة، فإننا نقوم بحشو أعداد الرموز لتصل إلى حاويات تكون مضاعفات لـ 64 أو 256. لا يزال هذا يترجم إلى آلاف الرسومات البيانية التي قد تستغرق عدة دقائق لالتقاطها لنموذج نموذجي. تأتي تكلفة التقاط الصور من مصدرين: تمرير أمامي حريص يجب تنفيذه لتجميع النوى وإعداد المخازن المؤقتة للنوى المختلفة التي تحتاج إليها، يليه تشغيل التقاط يعيد تنفيذ كود Python.

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

الموثقات الكسولة (Lazy Tensors)

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

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

تنفيذ LazyTensor

نحن نستفيد من LazyTensor في محرك مُشفّر ROSE الخاص بنا لتداخل عمل وحدة المعالجة الرسومية (GPU) ووحدة المعالجة المركزية (CPU). بدلاً من تشغيل كل استدعاء step() لرسمة CUDA والانتظار حتى تنتهي، يُرجع استدعاء step() كائنت LazyTensor لتتبع نتيجته غير المتزامنة. ويساعدنا ذلك، إلى جانب رسومات CUDA، على تحقيق أزمنة استجابة منخفضة وإنتاجية أفضل.

خط زمني يوضح تحضير وحدة المعالجة المركزية والمزامنة التي تتداخل مع دفعات وحدة المعالجة الرسومية المتتالية

ROSE

لقد قمنا بتكييف محرك ROSE الخاص بنا، والذي بنيناه في الأصل لتقديم نماذج LLM، للتعامل أيضاً مع تنفيذ نماذج التضمين. لتقليل الجهد اللازم لدعم نماذج التضمين، تعيد ROSE استخدام التعليمات البرمجية بقوة بين نماذج LLM والتضمينات. على سبيل المثال، يمر تقديم pplx-embed وفك تشفير نموذج Qwen3.5 LLM عبر نفس النوى. تتيح لنا هذه المشاركة تقديم نموذج تضمين تم ضبطه دقة في الأصل من نموذج LLM للنماذج الأولية والتقييم واستدلال الإنتاج بسهولة.

بالنسبة للطبقات الكثيفة، يكون استدلال التضمين وLLM متطابقين نظراً لأنه يتم معالجة متجهات الرموز بشكل مستقل. في طبقات الانتباه، يتم التعامل مع الاختلافات عن طريق إضافة دعم للمدخلات المتقطعة، إلى جانب إعدادات التعبئة وفك التشفير المُقسّمة المطلوبة بواسطة نماذج LLM. عند تقديم نموذج تضمين، فإننا لا نُنشئ ذاكرة التخزين المؤقت KV ونوجه إلى تنويعات نوى الانتباه التي تدعم التنسيق المتقطع لتجنب الحشو. كما يتم مشاركة روتينات التحويل والمعايرة الداعمة مع نماذج LLM.

Ivy

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

...ولكن النوى لا تزال مهمة

تدعم ROSE مجموعة متنوعة من خلفيات الانتباه. قد تكون النوى المختلفة مناسبة لأحجام مشكلات محددة. بمرور الوقت، قمنا بدمج نوى FlashInfer 2 وFlashInfer 3 وFlashAttention 4 لتنفيذ الانتباه المتقطع.

أداء نواة الانتباه حسب شكل النموذج وحجم المشكلة

بشكل عام، نلاحظ أن FlashAttention 4 أسرع. ومع ذلك، يتفوق عليه FlashInfer 3 في نماذج Qwen عند طول التسلسل الكبير جداً. نظراً لأن الأداء والضبط قد يختلفان باختلاف عدد أبعاد رؤوس الانتباه، فإننا نحافظ على دعم التكوينات المتعددة ونتخذ قراراً لكل حالة على حدة عند التقديم.

المعايير

نقوم بالمقارنة مع vLLM v0.22.0، وتشغيل الاستدلال بدقة BF16 على أوزان النموذج الفعلية والمدخلات المستمدة من مجموعات بيانات التقييم. سُبقت جميع عمليات التوقيت بعمليات إحماء تحققت من أن التباين في التشابه الجيبي يقع ضمن 0.1%.

التضمينات منخفضة زمن الاستجابة (p50 / p90 / p99 / الحد الأقصى للملي ثانية)

نحن نبلغ عن أوقات التشغيل لحجم دفعة الطلبات المرمزة مسبقاً 1، والطلبات المتسلسلة بالكامل، وأطوال تسلسل تبلغ 128، و512، و4096 رمزاً.

نتائج معيار التضمينات منخفضة زمن الاستجابة

التسجيل منخفض زمن الاستجابة (p50 / p90 / p99 / الحد الأقصى للملي ثانية)

أحجام دفعات الطلبات المُرمزة مسبقاً هي 5 و25 و50، وطول التسلسل 512 رمزاً.

نتائج معيار التسجيل منخفض زمن الاستجابة

التضمينات عالية الإنتاجية (تضمين/ثانية)

حجم دفعة الطلبات 100، وأربع عمليات متزامنة ترسل الطلبات، وأطوال تسلسل تبلغ 512، و1024، و4096 رمزاً.

نتائج معيار التضمينات عالية الإنتاجية

التضمينات عالية التزامن (p50 / p90 / p99 / الحد الأقصى للملي ثانية)

طول التسلسل 512، وحجم الدفعة 1، ولكننا نرسل 1 و2 و4 و8 و16 طلباً متزامناً. يتضمن هذا المعيار أيضاً تكاليف الترميز من خلال Ivy، إلى جانب النفقات العامة للشبكة بين Ivy وTulip.

نتائج معيار التضمينات عالية التزامن

الخلاصة والعمل المستقبلي

تتيح لنا البنية التحتية للتقديم المكونة من Ivy وTulip وROSE تقديم تضمينات لشركة Perplexity بزمن استجابة أقل وإنتاجية أفضل، مما ينتج عنه بحث أكثر دقة بتكلفة منخفضة مقارنة بالحلول الجاهزة.

من خلال التركيز على نماذج محددة وتولي زمام الأمور للمكدس بأكمله، نحصل على الحرية اللازمة لتحقيق توازن فعال بين الأداء والمرونة، ومزج أساسيات Rust القابلة لإعادة الاستخدام وعالية الأداء جنباً إلى جنب مع كود النماذج الأكثر عمومية بلغة Python. تعمل العديد من محركات الاستدلال مفتوحة المصدر، مثل vLLM وSGLang وTokenSpeed، على دمج لغات مثل Rust وC++ في مكدسها. لقد استثمرنا في لغة Rust على مدار العامين الماضيين وحصدنا ثماراً عظيمة من حيث الأداء وقابلية الصيانة. ومن خلال مشاركة معظم تنفيذ التضمين مع مكدس تقديم LLM الخاص بنا، فإننا نحقق مكاسب في الإنتاجية أيضاً، دون الحاجة إلى بذل جهود هندسية كبيرة لصيانة نماذج التضمين.

مع تطور النماذج، سنستمر في تحسين كل طبقة من مكدسنا لتقليل أزمنة الاستجابة المرتبطة بوحدة المعالجة المركزية وتلك المرتبطة بوحدة المعالجة الرسومية على حد سواء. تتيح لنا بروتوكولاتنا المخصصة القائمة على gRPC داخل Ivy وTulip تعديل الاتصال لتقليل زمن استجابة الشبكة، بينما توفر ROSE أساساً لتحسين الإنتاجية الحسابية. بالإضافة إلى ذلك، مع نمو دعم لغة Python ذات الخيوط الحرة في جميع أنحاء النظام البيئي، سنكون قادرين على تحسين قابلية التشغيل البيني بين Python وRust بشكل أكبر تقليل النفقات العامة.

المراجع