Embeddings מהירים ב-GPUs

מאמר זה מציג מבט מבפנים על תשתית ההגשה של Perplexity עבור מחלקה מיוחדת זו של מודלים.

מחבריםPerplexity Engineering

חיפוש מהיר ומדויק חיוני לכלל Perplexity, החל מ-Search ו-Computer ועד לפלטפורמת ה-API שלנו. מאחורי הקלעים, העבודה הקשה נעשית על ידי מודלי embeddings ודירוג, המסייעים למערכות שלנו לזהות את התוצאות הרלוונטיות ביותר עבור שאילתה נתונה. אנו משיגים איכות וזמן השהייה מתקדמים באמצעות אימון והגשה של מודלים משלנו, כגון pplx-embed.

מאמר זה מציג מבט מאחורי הקלעים על תשתית ההגשה של Perplexity עבור מחלקה מיוחדת זו של מודלים. אנו דנים בטכניקות שלנו לטיפול יעיל בצורכי ההיסק של חיפוש ילידי-בינה מלאכותית, המאפשרות יצירת אבטיפוס והערכה מהירות של מודלים, ובו בזמן מפעילות את אינדקס החיפוש בקנה מידה אקסה־בייט שלנו. יחד, טכניקות אלו מרחיבות את חזית פארטו של איכות החיפוש והיעילות, ומאפשרות לנו לשרת סוכנים ומשתמשים עם התוצאות הטובות ביותר האפשריות בעלות ובחביון הנמוכים ביותר.

Embeddings לחיפוש

בהגדרת חיפוש טיפוסית, מסמכים ממופתים ממופים למרחב וקטורי רב-ממדי באמצעות מודל embeddings ומאוחסנים במסד נתונים וקטורי. על ידי ביצוע embedding לשאילתה באמצעות אותו מודל, ניתן לאתר מסמכים דומים על ידי מציאת הווקטורים הקרובים ביותר לאלו של השאילתה. הדבר מוליד שני דפוסי תנועה שונים עבור מנוע הסקה לשרת:

  • Batch Embedding: בעת בנייה, הרחבה או אינדוקס מחדש של מסד הנתונים, יש לבצע embedding למסמכים בכמות גדולה לתוך מרחב הווקטורים, תוך מיקסום התפוקה כדי למזער עלויות.

בעקבות חיפוש וקטורי, יש לדרג אצוות גדולות של מסמכים, תוך איזון בין תפוקה לזמן השהייה.

  • Online Embedding: בעת שאילתת מסד הנתונים, יש לבצע embedding לשאילתה קצרה לצורך חיפושים, תוך מזעור זמן ההשהייה.

בנינו את תשתית ההסקה שלנו כך שתנצל כמה שיותר רכיבים משותפים על פני מקרי שימוש שונים. מכיוון שאנו משתמשים בדרך כלל במודלי Transformer קטנים כדי לייצר embeddings, אנו חולקים את עיקר המימוש עם קוד הסקת ה-LLM שלנו: embeddings של אצוות דומים ל-prefill מוגבל-חישוב, ואילו embeddings מקוונים, שלעתים רחוקות רצים על אסימונים ספורים, דומים חישובית ל-decode מוגבל-זיכרון. לפיכך אנו עושים שימוש חוזר ב-prefill ואופטימיזציה של prefill ו-decode kernels שלנו כדי להגיש מודלי embeddings. כתוצאה מכך, אנו יכולים להשיג תפוקת הסקת אצוות עצומה עם עבודה הנדסית נוספת מינימלית, תוך שמירה על זמן השהייה נמוך עבור עומסי עבודה של embeddings מקוונים.

Tulips, Roses וקצת Ivy

אנו חושפים הסקה דרך APIs תקניים, הן מבפנים והן מבחוץ דרך פלטפורמת ה-API שלנו. מאחורי הקלעים, שירותים רבים מעורבים בעיבוד בקשת embedding:

  • Ivy הוא שער HTTP ב-Rust ששירותי Perplexity קוראים לו.

הוא מטפל בעבודה מצד ה-CPU עבור בקשות כגון ניתוח JSON, טוקניזציה, יצירת תבניות קלט ופיצול אצוות, ותרגום בקשות לפרוטוקול gRPC מותאם אישית עבור שרתים במורד הזרם. הפרדה זו מאפשרת לנו להגדיר פרמטרים מסוימים סביב טוקניזציה ועיצוב קלט מבלי לגעת במופעי ההסקה הכבדים יותר.

  • Tulip הוא ממשק שרת ההסקה.

זהו שרת gRPC המיושם ב-Rust, ב-tokio וב-tonic. מנוע Tulip מקבל בקשות הסקה ב-gRPC, ומטפל בתזמון ובקיבוץ (batching). לאחר מכן, הוא שולח את הקבוצות למנוע ROSE, ומחזיר תגובות שהושלמו ללקוחות.

  • ROSE (מנוע הגשה מותאם בזמן ריצה) מיישם הסקת מודל.

הוא מוגדר בעיקר ב-Python, ומספק kernels, שכבות והגדרות למגוון רחב של מודלים. ROSE מממשת את מעברי הforward דרך מודלים, תוך אספקת ניהול גרפי CUDA המותאם במיוחד עבור embeddings. הוא מקושר ל-Tulip באמצעות פונקציית step(), המקבלת אצווה ומחזירה הפניה לחישוב שהיא מבצעת על המאיץ.

ארכיטקטורת הגשת מודל עבור בקשות embedding, מ-Ivy אל שרתי Tulip שמפעילים את ROSE על GPUs.

שמים לב מעבר ל-Kernel

גם דגמים מבוססי Transformer וגם ארכיטקטופת ה-Hopper/Blackwell הבסיסיות הן טכנולוגיות בוגרות, כך שהטמעת הסקה בצד ה-GPU התכנסה ליישום אופטימלי ברובו על פני מנועי הסקה שונים. אף על פי כן, גילינו הזדמנויות נוספות לשיפור בזמני ריצה ובמערכות חיווט שחושפות את המודמים מקצה לקצה בפני לקוח. בפרט, מצאנו שביכולתנו לשפר זמני השהייה על ידי ניהול מדוקדק של גרפי CUDA ועל ידי בניית הפשטת LazyTensor למעקב אסינכרוני אחר תוצאת צד-GPU במנוע ה-Rust הניידי. מימשנו תכונות אלו ב-Tulip, כדי שתוכל להתממשק ביעילות עם מימושי המודל של ROSE.

Tulip

עיצבנו את Tulip כך שיהיה ממשק קל משקל ככל האפשר על פני הגשת המודל שלנו. היא מטפלת בבקשות נכנסות במשימות Tokio אסינכרוניות, תוך תחזוקת מאגר של בקשות שהיא עוקבת אחריהן ומתזמנת מהן אצוות לשליחה למאיץ. מנגנון התזמון ב-Tulip פשוט מאוד: בקשות מצטברות בזמן ש-Tulip שולחת עבודה או ממתינה לתוצאות. מתוך הבקשות המצטברות, רצפים נבחרים על בסיס כל הקודם זכה להרצה דרך המודל.

מנגנון התזמון הפשוט מונע על ידי תצפית על ביצועי המודל. עבור מודלי embeddings קטנים, באורכי הרצף שאנו מגישים עבורם, שמנו לב שהעלות הליניארית של שכבות צפופות גוברת על העלות הריבועית של קשב. לפיכך, זמן ההשהייה פרופורציונלי ברובו למספר האסימונים, ולא למספר הרצפים. כתוצאה מכך, ברגע שאצווה גדולה די הצורך כדי לרוות את ה-GPU, כלומר סביב 512 אסימונים במודל שמתחת למיליארד פרמטרים, דחיסת רצפים נוספים לתוכו אינה משפרת את היעילות.

כדי להתממשק ביעילות עם המודל, Tulip מסתמכת על גרפי CUDA ומעקב אחר תוצאות עצלן כדי לחפוף עבודת GPU ו-CPU ולנצל במלואו את המשאבים הזמינים.

ניהול גרפי CUDA

הרצת המעבר הקדמי של מודל כוללת גם עבודה בצד ה-CPU וגם עבודה בצד ה-GPU. ה-CPU אחראי על תזמון קבוצות והפעלת ליבות עם הפרמטרים המתאימים, בעוד שה-GPU מבצע את ליבות כפל המטריצות, תשומת הלב, הנורמליזציה או ההפעלה הרלוונטיות. עבור עומסי עבודה בעלי תפוקה גבוהה כגון אימון ואינדוקס מחדש, תקורות בצד ה-CPU זניחות משום שגודל הקבוצות והשהיה בצד ה-GPU גדולים שניהם. עם זאת, בגודל קבוצות קטן יותר, העבודה בצד ה-CPU יכולה לעלות על העבודה בצד ה-GPU.

ביצוע על CPU ועל GPU במהלך מעבר קדמי ללא גרפי CUDA.

כדי למזער תקורות, במקום להשיק kernels עצמאיים, ניתן לבנות גרף CUDA כדי ללכוד את המטא-נתונים הנדרשים להשקת כל ה-kernels של מעבר forward בקריאה אחת לדרייבר ה-CUDA. הדבר מבטל את הצורך להריץ מחדש קוד Python ו-PyTorch יקר עבור התצורות שעבורן ניתן ללכוד גרפי CUDA.

בכל מודל, אנו עוקבים אחר נקודת פיתול, וקובעים את המספר המינימלי של אסימונים שבו הרצת GPU יקרה יותר מהפעלת kernel בצד ה-CPU. מכיוון שמודלי embeddings קטנים, אנו מבחינים שנקודת פיתול זו מגיעה באצוות של אלפי אסימונים ועשרות רצפים. מימוש קשב מסוימים מסתמכים על קלטים דינמיים מצד המארח כדי להגדיר הפעלות kernel, מה שמונע גרפי CUDA מלאים של מודל prefill/dense. ביצענו upstreamed לשינויים ב-kernels הרלוונטיים כדי לאפשר אותם במנוע ההסקה שלנו.

כדי לטפל בתקורות, אנו בונים גרפי CUDA של מודל שלם עבור כל מודלי ה-embeddings וחופפים עבודת CPU עם עבודת GPU. מכיוון שגרפי CUDA ממזערים את התקורות מצד ה-CPU, ברגע שגרף מושק, יש לנו זמן פנוי להתחיל ולהכניס לתור את הרצת האצווה הבאה בכל פעם שהיא זמינה. התוצאות של האצווה הממתינה נעות במעקב באמצעות LazyTensor, המאפשר למשימה אסינכרונית ב-Rust להיחסם עד להשלמת הרצת האצווה הקודמת. גרפי CUDA מסייעים להגשה בהשהייה נמוכה על ידי הבטחה שאיננו מעוכבים על ידי עלות השקות ה-kernel ומאפשרים תזמון משופר במקרה עתיר התפוקה כשהם משחררים את ה-CPU לעבודה על האצווה הבאה מוקדם יותר.

ביצוע על CPU ועל GPU במהלך מעבר קדמי עם גרפי CUDA.

יש ללכוד גרפי CUDA עבור כל תצורה נפרדת, שעבור embeddings פירושו גרף לכל שילוב של ספירת רצפים וספירת אסימונים. מכיוון שרשת זו רחבה, אנו מרפדים ספירות אסימונים לדליים שהם כפולות של 64 או 256. הדבר עדיין מביא לאלפי גרפים שעשויים לקחת מספר דקות ללכידה עבור מודל טיפוסי. עלות הלכידה נובעת משני מקורות: מעבר forward להוט שיש להריץ כדי לקמפל kernels ולהגדיר חוצצים עבור kernels שונים הזקוקים להם, ואחריו ריצת הלכידה המריצה מחדש קוד Python.

אנו ממזערים עלויות הפעלה על ידי לכידת גרפי CUDA בצורה עצלנה ככל שהמנוע מגיש. אנו עוקבים אחר כל תצורה ומוודאים שהיא עוברת ריצת חימום להוט לפני הפעלת לכידת הגרף ושחזורו בפגיעה השנייה. כל ההרצות הבאות של אותה תצורת גרף עוברות לאחר מכן שחזור גרף CUDA. לכידת גרפים עצלנה משפיעה על זמני ההשהייה p99 במהלך ההפעלה; עם זאת, היא בעלת ערך בפריסת מספר דקות של עבודה להוט על פני מספר שעות. זמני הפעלה מהירים יותר מאפשרים לנו להרחיב ולנהל טוב יותר פריסות embeddings.

טנסורים עצלנים

באמצעות CUDA, עבודת GPU היא אסינכרונית. מכיוון שהשקת kernel באופן אסינכרוני מכניסה אותו לתור ב-stream, קוד המארח חייב לסנכרן במפורש כדי לקרוא החוצה את הווקטורים המתקבלים. כדי לאפשר דרגה גבוהה יותר של מקביליות וליכולת להתחיל אצוות עתידיות תוך כדי המתנה לסיום הקודמת בהתקן, אנו מסתמכים על הפשטת LazyTensor כדי לעקוב אחר ערכים.

ה-LazyTensor עוקב אחר חוצץ מארח בזיכרון נעול-עמוד ופעולת cudaMemcpyAsync באמצעות אירוע המעתיק נתונים מההתקן. הוא מופעל לאחר השקת מעבר ה-forward באותו stream. מכיוון שפעולת ההעתקה חייבת להמתין לכל ה-kernels הקודמים ב-stream שיבוצעו, האירוע המשויך עוקב הן אחר השלמת מעבר ה-forward והן אחר זמינות התוצאה ב-CPU.

ביצוע LazyTensor, מהפעלת גרף CUDA ועד להעתקה אסינכרונית ולסנכרון.

אנו מנצלים LazyTensors במנוע המקודד ROSE שלנו כדי לחפוף עבודת GPU ו-CPU. במקום שכל קריאת step() תריץ את גרף ה-CUDA ותמתין לסיומו, step() מחזירה LazyTensor כדי לעקוב אסינכרונית אחר התוצאה שלה. בשילוב עם גרפי CUDA, זה מסייע לנו להשיג זמני השהייה נמוכים ותפוקה טובה יותר.

הכנת CPU וסנכרון החופפים ל-batches עוקבים על ה-GPU.

ROSE

התאמנו את מנוע ROSE שלנו, שבנינו במקור עבור הגשה של LLMs, כך שיטפל גם בביצוע של מודלי embeddings. כדי למזער את המאמץ הנדרש לתמיכה במודלי embeddings, ROSE עושה שימוש חוזר נרחב בקוד בין LLMs ל-embeddings. למשל, ההגשה של pplx-embed ופענוח של Qwen3.5 LLM עוברים כולם דרך אותן ליבות. שיתוף זה מאפשר לנו להגיש בקלות מודל embeddings שבמקור עבר כוונון עדין מתוך LLM לצורכי יצירת אבטיפוס, הערכה והיסק בייצור.

עבור שכבות צפופות, היסק של embeddings ושל LLM הוא זהה, מאחר שווקטורי הטוקנים מעובדים באופן בלתי תלוי. בשכבות תשומת לב, ההבדלים מטופלים באמצעות הוספת תמיכה בקלטים ragged, לצד הגדרות ה-prefill וה-decode המדורגות הנדרשות על ידי LLMs. כאשר מגישים מודל embeddings, איננו מאתחלים מטמון KV ומעבירים לגרסאות של ליבות תשומת לב התומכות בפורמט ragged כדי להימנע מריפוד. גם שגרות ההמרה והכיול התומכות משותפות עם ה-LLMs.

Ivy

Ivy, שכבת פרוקסי ה-HTTP של ההסקה שלנו, ממלאת גם היא תפקיד חשוב בביצועים. מכיוון שמטעני בקשות משתנים בייצור, ניתוב בקשות בודדות להעתקים בודדים עלול לגרום לחוסר איזון בעומסים. Ivy מפצלת בקשות אצווה גדולות למקטעים ומאזנת את העומס ביניהם בין ההעתקים, מה שמשפר את הניצול ומחליק את זמני ההשהייה. העבודה האחרונה שלנו על טוקניזציה של unigram פנימית, הפרוסה במלואה ב Ivy, משפרת באופן דרסטי את זמני ההשהייה בהשוואה לטוקניזרים מדף.

...אבל ה-Kernels עדיין חשובים

ROSE תומך במגוון קצות־קצה של תשומת לב. ליבות שונות עשויות להתאים לגדלי בעיה ספציפיים. עם הזמן שילבנו את ליבות FlashInfer 2, FlashInfer 3 ו-FlashAttention 4 כדי לממש תשומת לב ragged.

ביצועי kernel של attention לאורך תצורות מודל וגדלי בעיה שונים.

באופן כללי, אנו רואים ש-FlashAttention 4 מהיר יותר. עם זאת, FlashInfer 3 עולה עליו בביצועיו במודלים מבוססי Qwen באורכי רצף ארוכים מאוד. מאחר שהביצועים והטיונינג יכולים להשתנות בהתאם למספרן ולממדן של ראשי תשומת הלב, אנו תומכים בכמה תצורות ומקבלים החלטה לכל מקרה לגופו בעת ההגשה.

מבחנים השוואתיים

אנו משווים מול vLLM v0.22.0, ומריצים הסקה בדיוק BF16 על משקלי מודל אמיתיים וקְלָטִים שמקורם במערכי נתונים של הערכה. כל הרצות התזמון קדמו להרצות חימום אשר וידאו שהסטייה בדמיון קוסינוס נמצאת בטווח של 0.1%.

Embeddings בהשהייה נמוכה (p50 / p90 / p99 / מקסימום אלפיות השנייה)

אנו מדווחים על זמני ריצה עבור גודל אצווה של בקשות מטווקנות-מראש 1, בקשות רצופות לחלוטין, אורכי רצף של 128, 512 ו-4096 אסימונים.

מדדי זמן השהיה של embedding עם רצפים שעברו pre-tokenization ובקשות עוקבות באורכי רצף של 128, 512 ו-4096 טוקנים, עם גודל batch של בקשות 1.

דירוג בהשהייה נמוכה (p50 / p90 / p99 / מקסימום אלפיות השנייה)

גדלי אצווה של בקשות טוקונו-מראש 5, 25 ו-50, אורך רצף של 512 אסימונים.

מדדי זמן השהיה של scoring עם רצפים שעברו pre-tokenization ובקשות עוקבות באורך רצף 512, עם גדלי batch של בקשות 5, 25 ו-50.

Embeddings בתפוקה גבוהה (emb/s)

גודל אצווה של בקשות 100, ארבעה תהליכים מקבילים השולחים בקשות, אורכי רצף של 512, 1024 ו-4096 אסימונים.

תפוקת embedding באורכי רצף של 512, 1024 ו-4096 טוקנים, עם גודל batch של בקשות 100 וארבעה תהליכים מקבילים.

Embeddings עתירי מקביליות (p50 / p90 / p99 / מקסימום אלפיות השנייה)

אורך רצף 512, גודל אצווה 1, אך אנו שולחים בקשות מקבילות של 1, 2, 4, 8 ו-16. מבחן השוואתי זה כולל גם עלויות טוקניזציה דרך Ivy, לצד תקורה רשתית בין Ivy ל-Tulip.

זמן השהיה של embedding לאורך מספרי בקשות מקבילות, כולל tokenization של Ivy ותקורת הרשת בין Ivy ל-Tulip.

סיכום ועבודה עתידית

תשתית ההגשה המורכבת מ-Ivy, Tulip ו-ROSE מאפשרת לנו להגיש embeddings עבור Perplexity בהשהייה נמוכה יותר ובתפוקה טובה יותר, וכתוצאה מכך חיפוש מדויק יותר בעלות מופחתת בהשוואה לפתרונות מדף.

על ידי התמקדות במודלים ספציפיים ולקיחת בעלות על כל המחסנית, אנו מקבלים את החופש הדרוש כדי להשיג איזון יעיל בין ביצועים לגמישות, תוך ערבוב פרימיטיבי Rust הניתנים לשימוש חוזר ולביצועים גבוהים לצד קוד דוגמנות Python גנרי יותר. מנועי הסקה רבים בקוד פתוח, כגון vLLM, SGLang ו-TokenSpeed, משלבים שפות כמו Rust ו-C++ במחסנית שלהם. השקענו ב-Rust במהלך השנתיים האחרונות וקצרנו פירות עצומים הן בביצועים והן ביכולת התחזוקה. על ידי שיתוף רוב מימוש ה-embeddings עם מחחסנית הגשת ה-LLM שלנו, אנו מפיקים רווחים גם בתפוקה, מבלי לדרוש השקעת מאמץ הנדסי משמעותי בתחזוקת מודלי embeddings.

ככל שמודלים מתפתחים, נמשיך לשפר כל שכבה במחסנית שלנו כדי לצמצם זמני השהייה המוגבלים הן ב-CPU והן ב-GPU. פרוטוקולי ה-gRPC המותאמים אישית שלנו בתוך Ivy ו-Tulip מאפשרים לנו לכוונן תקשורת כדי לצמצם זמני השהייה ברשת, בעוד ש-ROSE מספקת בסיס לשיפור התפוקה החישובית. בנוסף, ככל שהתמיכה ב-Python משוחרר-שרשורים (free-threaded) גדלה בכל האקולוגיה, נוכל לשפר עוד יותר את יכולת הפעולה ההדדית בין Python ל-Rust כדי לצמצם תקורות.

מקורות

ציטוט

BibTeX
@online{perplexity2026FastEmbeddingsOnGpus,
  author = {{Perplexity} and Nandor Licker and Sebastian Sepulveda and Phi Hoang and Jerry Ma},
  title = {{Fast embeddings on GPUs}},
  date = {2026-09-04},
  url = {https://www.perplexity.ai/hub/blog/fast-embeddings-on-gpus}
}