การฝังตัวที่รวดเร็วบน GPU

การค้นหน้าที่รวดเร็วและแม่นยำมีความสำคัญอย่างยิ่งต่อ Perplexity ทั้งหมด ตั้งแต่ Search และ Computer ไปจนถึงแพลตฟอร์ม API ของเรา เบื้องหลังการทำงานหนักจะดำเนินการโดยโมเดลการฝังตัวและการจัดอันดับ ซึ่งช่วยให้ระบบของเราidentify ผลลัพธ์ที่เกี่ยวข้องมากที่สุดสำหรับคำค้นหาที่กำหนด เราบรรลุสถานะของรัฐ

ผู้เขียนPerplexity Engineering

การค้นหน้าที่รวดเร็วและแม่นยำมีความสำคัญอย่างยิ่งต่อ Perplexity ทั้งหมด ตั้งแต่ Search และ Computer ไปจนถึงแพลตฟอร์ม API ของเรา เบื้องหลังการทำงานหนักจะดำเนินการโดยโมเดลการฝังตัวและการจัดอันดับ (Embedding and Ranking Models) ซึ่งช่วยให้ระบบของเราidentify ผลลัพธ์ที่เกี่ยวข้องมากที่สุดสำหรับคำค้นหาที่กำหนด เราบรรลุคุณภาพและความหน่วงระดับแนวหน้าด้วยการฝึกฝนและให้บริการโมเดลของเราเอง เช่น pplx-embed

บทความนี้นำเสนอภาพเบื้องหลังของโครงสร้างพื้นฐานการให้บริการของ Perplexity สำหรับโมเดลประเภทพิเศษนี้ เราอภิปรายถึงเทคนิคของเราในการตอบสนองความต้องการด้านการอนุมานของการค้นหาแบบ AI-native อย่างมีประสิทธิภาพ ช่วยให้สามารถสร้างต้นแบบและประเมินผลโมเดลได้อย่างรวดเร็ว พร้อมทั้งขับเคลื่อน ดัชนีการค้นหาขนาด exabyte ของเรา เทคนิคเหล่านี้ร่วมกันขยายขอบเขต Pareto ของคุณภาพและประสิทธิภาพการค้นหา ช่วยให้เราสามารถให้บริการตัวแทน (Agents) และผู้ใช้ด้วยผลลัพธ์ที่ดีที่สุดเท่าที่จะเป็นไปได้ด้วยต้นทุนและความหน่วงที่ต่ำที่สุด

การฝังตัวสำหรับการค้นหา (Embeddings for Search)

ในการตั้งค่าการค้นหาทั่วไป เอกสารที่ทำดัชนีแล้วจะถูกแมปไปยังสเปซเวกเตอร์ที่มีมิติสูงโดยใช้โมเดลการฝังตัวและจัดเก็บไว้ในฐานข้อมูลเวกเตอร์ ด้วยการฝังตัวคำค้นหาโดยใช้โมเดลเดียวกัน เอกสารที่คล้ายคลึงกันจึงสามารถค้นหาได้โดยการหาเวกเตอร์ที่ใกล้เคียงที่สุดกับเวกเตอร์ของคำค้นหานั้น สิ่งนี้นำไปสู่รูปแบบการจราจรสองรูปแบบที่แตกต่างกันสำหรับเอ็นจิ้นการอนุมานในการให้บริการ:

  • การฝังตัวแบบแบตช์ (Batch Embedding): เมื่อสร้าง ขยาย หรือทำดัชนีฐานข้อมูลใหม่ เอกสารจำนวนมากจะต้องถูกฝังตัวลงในสเปซเวกเตอร์ โดยเพิ่มปริมาณงานให้สูงสุดเพื่อลดต้นทุนให้น้อยที่สุด

หลังจากการค้นหาเวกเตอร์ เอกสารจำนวนมากจะต้องถูกให้คะแนน (Scored) โดยสร้างความสมดุลระหว่างปริมาณงานและความหน่วง

  • การฝังตัวแบบออนไลน์ (Online Embedding): เมื่อทำการสืบค้นฐานข้อมูล จะต้องฝังตัวคำค้นหาสั้นๆ เพื่อใช้ในการค้นหา (Lookups) ซึ่งช่วยลดความหน่วงให้น้อยที่สุด

เราสร้างโครงสร้างพื้นฐานการอนุมานขึ้นมาเพื่อใช้ประโยชน์จากคอมโพเนนต์ส่วนกลางให้ได้มากที่สุดในหลากหลายกรณีการใช้งาน เนื่องจากโดยทั่วไปเราใช้โมเดล Transformer ขนาดเล็กเพื่อสร้างการฝังตัว (Embeddings) เราจึงใช้การทำงานส่วนใหญ่ร่วมกับโค้ดการอนุมาน LLM ของเรา: แบตช์การฝังตัวจะคล้ายกับการเติมล่วงหน้าแบบจำกัดการคำนวณ (Compute-bound Prefill) ในขณะที่การฝังตัวแบบออนไลน์ ซึ่งมักจะทำงานบนโทเค็นจำนวนน้อย จะคล้ายคลึงกับการถอดรหัสแบบจำกัดหน่วยความจำ (Memory-bound Decode) ในเชิงการคำนวณ ดังนั้นเราจึงนำ เคอร์เนลการเติมล่วงหน้าและการถอดรหัสที่ได้รับการปรับแต่ง ของเรากลับมาใช้ใหม่เพื่อให้บริการโมเดลการฝังตัว ส่งผลให้เราสามารถบรรลุปริมาณงานการอนุมานแบบแบตช์ขนาดใหญ่ด้วยงานทางวิศวกรรมเพิ่มเติมที่น้อยที่สุด ในขณะที่ยังคงรักษาความหน่วงต่ำสำหรับเวิร์กโหลดการฝังตัวแบบออนไลน์

Tulips, Roses และ Ivy บางส่วน

เราแสดงผลการอนุมานผ่าน API ที่เป็นมาตรฐาน ทั้งภายในและภายนอกผ่านแพลตฟอร์ม API ของเรา เบื้องหลัง มีบริการหลายรายการที่เกี่ยวข้องกับการประมวลผลคำขอการฝังตัว:

  • Ivy คือเกตเวย์ HTTP ของ Rust ที่บริการของ Perplexity เรียกใช้งาน

ทำหน้าที่จัดการงานฝั่ง CPU สำหรับคำขอต่างๆ เช่น การแยกวิเคราะห์ JSON, การ ทำโทเค็น (Tokenization), เทมเพลตอินพุต และการแบ่งแบตช์ พร้อมทั้งแปลงคำขอให้เป็นโปรโตคอล gRPC แบบกำหนดเองสำหรับเซิร์ฟเวอร์ปลายทาง การแยกส่วนนี้ช่วยให้เราสามารถกำหนดค่าพารามิเตอร์บางอย่างเกี่ยวกับการทำโทเค็นและการจัดรูปแบบอินพุตได้โดยไม่ต้องไปยุ่งกับอินสแตนซ์การอนุมานที่หนักกว่า

  • Tulip คืออินเทอร์เฟซของเซิร์ฟเวอร์การอนุมาน

เป็นเซิร์ฟเวอร์ gRPC ที่ใช้งานด้วย Rust, tokio และ `tonic` Tulip รับคำขอการอนุมาน gRPC พร้อมทั้งจัดการการจัดตารางเวลาและการจัดกลุ่มแบตช์ จากนั้นจะส่งแบตช์ไปยังเอ็นจิ้น ROSE และส่งคืนการตอบสนองที่เสร็จสมบูรณ์ไปยังไคลเอนต์

  • **ROSE** (Runtime-Optimized Serving Engine) ทำหน้าที่ประมวลผลการอนุมานโมเดล (Model Inference)

โดยหลักแล้วถูกกำหนดใน Python โดยมี เคอร์เนล, เลเยอร์ และคำนิยามสำหรับโมเดลที่หลากหลาย ROSE ทำหน้าที่นำทางส่งผ่าน (Forward Passes) ผ่านโมเดล พร้อมทั้งให้บริการการจัดการกราฟ CUDA ที่เชี่ยวชาญพิเศษสำหรับการฝังตัว (Embeddings) ซึ่งเชื่อมต่อกับ Tulip ผ่านฟังก์ชัน step() ซึ่งรับแบตช์และส่งคืนการอ้างอิงถึงการคำนวณที่ดำเนินการบนตัวเร่งความเร็ว (Accelerator)

สถาปัตยกรรมการให้บริการตั้งแต่คำขอฝังตัวผ่าน Ivy ไปยังเซิร์ฟเวอร์ Tulip ที่จำลองแบบ (Serving architecture from an embeddings request through Ivy to replicated Tulip servers)

ให้ความสนใจมากกว่าแค่ตัวเคอร์เนล (Paying Attention Beyond the Kernel)

ทั้งโมเดลที่อิงตาม Transformer และสถาปัตยกรรม Hopper/Blackwell เบื้องล่างต่างก็เป็นเทคโนโลยีที่โตเต็มที่แล้ว ดังนั้นการฝังตัวการอนุมาน (Embedding Inference) ฝั่ง GPU จึงหลอมรวมไปสู่การนำไปใช้งานที่เหมาะสมที่สุดในเอ็นจิ้นการอนุมานต่างๆ ถึงกระนั้น เราก็ได้ค้นพบโอกาสเพิ่มเติมในการปรับปรุงรันไทม์และฮาร์เนสที่แสดงผลโมเดลแบบ end-to-end ให้กับไคลเอนต์ โดยเฉพาะอย่างยิ่ง เราพบว่าเราสามารถปรับปรุงความหน่วง (Latencies) ได้ด้วยการจัดการกราฟ CUDA อย่างระมัดระวัง และด้วยการสร้างนามธรรม LazyTensor เพื่อติดตามผลลัพธ์ฝั่ง GPU แบบอะซิงโครนัสในเอ็นจิ้น Rust ดั้งเดิม เราได้นำฟีเจอร์เหล่านี้ไปใช้ใน Tulip เพื่อให้สามารถเชื่อมต่อกับอิมปิลิเมนต์ชันโมเดลของ ROSE ได้อย่างมีประสิทธิภาพ

Tulip

เราออกแบบ Tulip ให้เป็นอินเทอร์เฟซที่มีน้ำหนักเบาที่สุดเท่าที่จะเป็นไปได้เหนือการให้บริการโมเดลของเรา โดยจะจัดการคำขาที่เข้ามาในงานอะซิงโครนัสของ Tokio, รักษาพูลของคำขอที่ติดตามและจัดกำหนดการแบตช์เพื่อส่งไปยังตัวเร่งความเร็ว กลไกการกำหนดเวลาใน Tulip นั้นเรียบง่ายมาก: คำขอจะสะสมในขณะที่ Tulip กำลังส่งงานหรือรอผลลัพธ์ จากคำขอที่สะสมเหล่านี้ ลำดับจะถูกเลือกตามลำดับก่อนหลัง (First-come, First-served) เพื่อรันผ่านโมเดล

กลไกการกำหนดเวลาอย่างง่ายนี้มีแรงจูงใจมาจากการสังเกตประสิทธิภาพของโมเดล สำหรับโมเดลการฝังตัวขนาดเล็ก ที่ความยาวลำดับที่เราให้บริการ เราสังเกตเห็นว่าต้นทุนเชิงเส้นของเลเยอร์หนาแน่นมีมากกว่าต้นทุนกำลังสองของแอตเทนชัน ดังนั้น ความหน่วงจึงเป็นสัดส่วนกับจำนวนโทเค็นเป็นส่วนใหญ่ ไม่ใช่จำนวนลำดับ ด้วยเหตุนี้ เมื่อแบตช์มีขนาดใหญ่พอที่จะทำให้ GPU อิ่มตัว ซึ่งอยู่ที่ประมาณ 512 โทเค็นบนโมเดลที่มีพารามิเตอร์ต่ำกว่าหนึ่งพันล้าน การอัดลำดับเข้าไปในแบตช์มากขึ้นจึงไม่ได้ช่วยปรับปรุงประสิทธิภาพแต่อย่างใด

เพื่อให้สามารถเชื่อมต่อกับโมเดลได้อย่างมีประสิทธิภาพ Tulip จึงอาศัยกราฟ CUDA และการติดตามผลลัพธ์แบบลาซีเพื่อทำให้งานของ GPU และ CPU ซ้อนทับกัน และใช้งานทรัพยากรที่มีอยู่ได้อย่างเต็มที่

การจัดการกราฟ CUDA

การรันการส่งผ่านไปข้างหน้า (Forward Pass) ของโมเดลเกี่ยวข้องกับการทำงานทั้งฝั่ง CPU และฝั่ง GPU CPU มีหน้าที่กำหนดตารางเวลาแบตช์และเรียกใช้เคอร์เนลด้วยพารามิเตอร์ที่เหมาะสม ในขณะที่ GPU จะดำเนินการคูณเมทริกซ์, แอตเทนชัน (Attention), นอร์ม (Norm) หรือเคอร์เนลการเปิดใช้งาน (Activation Kernels) ที่เกี่ยวข้อง สำหรับเวิร์กโหลดที่มีปริมาณงานสูง เช่น การฝึกฝนและการทำดัชนีซ้ำ โอเวอร์เฮดฝั่ง CPU จะมีน้อยมากเนื่องจากขนาดแบตช์และความหน่วงฝั่ง GPU มีขนาดใหญ่ อย่างไรก็ตาม สำหรับขนาดแบตช์ที่เล็กกว่า การทำงานฝั่ง CPU อาจมีมากกว่าการทำงานฝั่ง GPU

การส่งผ่านไปข้างหน้าแบบกระตือรือร้น: การเรียกโฮสต์สลับกับเคอร์เนลอุปกรณ์ (Eager forward pass: host invocations interleaved with device kernels)

เพื่อบรรเทาโอเวอร์เฮด แทนที่จะเปิดใช้เคอร์เนลที่เป็นอิสระ กราฟ CUDA สามารถสร้างขึ้นเพื่อบันทึกเมตาดาต้าที่จำเป็นในการเปิดใช้งานเคอร์เนลทั้งหมดของการส่งผ่านไปข้างหน้าด้วยการเรียกเพียงครั้งเดียวไปยังไดรเวอร์ CUDA ซึ่งช่วยลดความจำเป็นในการรันโค้ด Python และ PyTorch ที่มีราคาแพงซ้ำสำหรับการกำหนดค่าที่สามารถบันทึกกราฟ CUDA ได้

ในแต่ละโมเดล เราจะติดตามจุดเปลี่ยน (Inflection Point) โดยกำหนดจำนวนโทเค็นขั้นต่ำที่การทำงานของ GPU มีราคาแพงกว่าการเรียกใช้เคอร์เนลฝั่ง CPU เนื่องจากโมเดลการฝังตัวมีขนาดเล็ก เราจึงสังเกตเห็นว่าจุดเปลี่ยนนี้เกิดขึ้นที่แบตช์ขนาดหลายพันโทเค็นและหลายสิบลำดับ อิมปิลิเมนต์ชัน Attention บางอย่างอาศัยอินพุตฝั่งโฮสต์แบบไดนามิกเพื่อกำหนดค่าการเรียกใช้เคอร์เนล ซึ่งขัดขวางกราฟ CUDA แบบเติมล่วงหน้า/หนาแน่นสำหรับโมเดลเต็มรูปแบบ เราได้ upstream การเปลี่ยนแปลงไปยังเคอร์เนลที่เกี่ยวข้องเพื่อให้สามารถใช้งานได้ในเอ็นจิ้นการอนุมานของเรา

เพื่อแก้ไขปัญหาโอเวอร์เฮด เราสร้างกราฟ CUDA สำหรับโมเดลทั้งหมดสำหรับโมเดลการฝังตัวทุกรุ่น และทำให้งานของ CPU ทับซ้อนกับงานของ GPU เนื่องจากกราฟ CUDA ช่วยลดโอเวอร์เฮดฝั่ง CPU เมื่อเปิดใช้งานกราฟแล้ว เราจึงมีเวลาว่างในการเริ่มต้นและเข้าคิวการดำเนินการของแบตช์ถัดไปเมื่อใดก็ตามที่พร้อมใช้งาน ผลลัพธ์ของแบตช์ที่รอดำเนินการจะถูกติดตามด้วย LazyTensor ซึ่งช่วยให้งานแบบอะซิงโครนัสใน Rust สามารถบล็อกจนกว่าแบตช์ก่อนหน้าจะดำเนินการเสร็จสิ้น กราฟ CUDA ช่วยในการให้บริการที่มีความหน่วงต่ำโดยรับประกันว่าเราจะไม่ถูกขัดขวางด้วยต้นทุนของการเรียกใช้เคอร์เนล และอำนวยความสะดวกในการจัดตารางเวลาที่ดีขึ้นในกรณีที่มีปริมาณงานสูง เนื่องจากการปลดปล่อย CPU ให้ทำงานในแบตช์ถัดไปได้เร็วยิ่งขึ้น

การส่งผ่านไปข้างหน้าของกราฟ Cuda (Cudagraph forward pass)

กราฟ CUDA จะต้องถูกบันทึกไว้สำหรับการกำหนดค่าแต่ละแบบที่แตกต่างกัน ซึ่งสำหรับการฝังตัวหมายถึงกราฟหนึ่งชุดต่อการรวมกันของจำนวนลำดับและจำนวนโทเค็น เนื่องจากตารางนี้กว้างขวางมาก เราจึงทำการแพดจำนวนโทเค็นไปยังบั๊กเก็ตที่เป็นพหุกูณของ 64 หรือ 256 แต่วิธีนี้ก็ยังคงส่งผลให้มีกราฟหลายพันรายการซึ่งอาจใช้เวลาหลายนาทีในการบันทึกสำหรับโมเดลทั่วไป ต้นทุนของการบันทึกมาจากสองแหล่ง: การส่งผ่านไปข้างหน้าแบบกระตือรือร้น (Eager Forward Pass) ที่ต้องดำเนินการเพื่อคอมไพล์เคอร์เนลและตั้งค่าบัฟเฟอร์สำหรับเคอร์เนลต่างๆ ที่ต้องการ ตามด้วยการรันการบันทึกซึ่งรันโค้ด Python ซ้ำ

เราบรรเทาต้นทุนการเริ่มต้นระบบด้วยการบันทึกกราฟ CUDA แบบลาซีในขณะที่เอ็นจิ้นให้บริการ เราติดตามการกำหนดค่าแต่ละรายการและมั่นใจว่าผ่านการรันวอร์มอัพแบบกระตือรือร้นก่อนที่จะทริกเกอร์การบันทึกและการเล่นซ้ำของกราฟในการเรียกใช้ครั้งที่สอง การดำเนินการทั้งหมดที่ตามมาของกำหนดค่ากราฟเดียวกันจะผ่านการเล่นซ้ำกราฟ CUDA การบันทึกกราฟแบบลาซีส่งผลกระทบต่อความหน่วง p99 ในระหว่างการเริ่มต้นระบบ อย่างไรก็ตาม มีประโยชน์ในการกระจายงานแบบกระตือรือร้นหลายนาทีให้กระจายออกไปเป็นหลายชั่วโมง เวลาเริ่มต้นที่เร็วยขึ้นช่วยให้เราสามารถปรับขนาดและจัดการการปรับใช้การฝังตัวได้ดียิ่งขึ้น

เทนเซอร์แบบลาซี (Lazy Tensors)

ผ่านทาง CUDA การทำงานของ GPU จะเป็นแบบอะซิงโครนัส เนื่องจากการเปิดใช้งานเคอร์เนลแบบอะซิงโครนัสจะนำไปต่อคิวบนสตรีม โค้ดโฮสต์จึงต้องทำการซิงโครไนซ์อย่างชัดเจนเพื่ออ่านเวกเตอร์ผลลัพธ์ออกมา เพื่ออำนวยความสะดวกในระดับความขนานที่สูงขึ้นและสามารถเริ่มแบตช์ในอนาคตได้ในขณะที่รอให้แบตช์ก่อนหน้าทำงานบนอุปกรณ์เสร็จสิ้น เราจึงอาศัยนามธรรม LazyTensor ในการติดตามค่าต่างๆ

LazyTensor จะติดตามบัฟเฟอร์โฮสต์ในหน่วยความจำแบบล็อกหน้า (Page-locked Memory) และการดำเนินการ cudaMemcpyAsync ผ่านอีเวนต์ที่คัดลอกข้อมูลจากอุปกรณ์ โดยจะเริ่มทำงานหลังจากเปิดตัวการส่งผ่านไปข้างหน้าบนสตรีมเดียวกัน เนื่องจากการดำเนินการคัดลอกต้องรอให้เคอร์เนลก่อนหน้าทั้งหมดบนสตรีมดำเนินการเสร็จสิ้น อีเวนต์ที่เกี่ยวข้องจึงติดตามทั้งการเสร็จสิ้นของการส่งผ่านไปข้างหน้าและความพร้อมใช้งานของผลลัพธ์บน CPU

การทำงานแบบ LazyTensor

เราใช้ประโยชน์จาก LazyTensor ในเอ็นจิ้นตัวเข้ารหัส ROSE เพื่อให้การทำงานของ GPU และ CPU ซ้อนทับกันได้ แทนที่การเรียกใช้งาน step() แต่ละครั้งจะรันกราฟ CUDA และรอจนกว่าจะเสร็จสิ้น ฟังก์ชัน step() จะส่งคืน LazyTensor เพื่อติดตามผลลัพธ์แบบอะซิงโครนัส เมื่อทำงานร่วมกับกราฟ CUDA วิธีนี้ช่วยให้เราบรรลุความหน่วงต่ำและแบนด์วิดท์ที่ดีขึ้น

ไทม์ไลน์แสดงการเตรียมการและการซิงโครไนซ์ของ CPU ที่ทับซ้อนกับแบตช์ GPU ที่ตามมา (Timeline showing CPU preparation and synchronization overlapping successive GPU batches)

ROSE

เราได้ปรับแต่งเอ็นจิ้น ROSE ซึ่งเดิมสร้างขึ้นสำหรับการให้บริการ LLM เพื่อจัดการกับการทำงานของโมเดลการฝังตัวด้วย เพื่อลดความพยายามที่จำเป็นในการรองรับโมเดลการฝังตัว ROSE จึงนำโค้ดกลับมาใช้ใหม่อย่างจริงจังระหว่าง LLM และการฝังตัว ตัวอย่างเช่น การให้บริการ pplx-embed และการถอดรหัส LLM Qwen3.5 ล้วนผ่านเคอร์เนลเดียวกัน การแชร์นี้ช่วยให้เราสามารถให้บริการโมเดลการฝังตัวที่เดิมได้รับการ fine-tune จาก LLM ได้อย่างง่ายดายสำหรับการสร้างต้นแบบ, การประเมินผล และการอนุมานในการผลิต

สำหรับเลเยอร์แบบหนาแน่น (Dense Layers) การอนุมานการฝังตัวและ LLM จะเหมือนกันเนื่องจากเวกเตอร์โทเค็นได้รับการประเมินผลอย่างอิสระ ในเลเยอร์แอตเทนชัน ความแตกต่างจะถูกจัดการโดยการเพิ่มการรองรับอินพุตแบบเรกเก็ด (Ragged Inputs) ควบคู่ไปกับการตั้งค่าการเติมล่วงหน้าและการถอดรหัสแบบแบ่งหน้า (Paged Prefill and Decode Setups) ที่ LLM ต้องการ เมื่อให้บริการโมเดลการฝังตัว เราจะไม่สร้างอินสแตนซ์ KV cache แต่จะส่งต่อไปยังรูปแบบต่างๆ ของเคอร์เนลแอตเทนชันที่รองรับรูปแบบเรกเก็ดเพื่อหลีกเลี่ยงการแพดดิง (Padding) รูทีนการแปลงและการสอบเทียบที่สนับสนุนก็จะถูกใช้ร่วมกับ LLM ด้วย

Ivy

Ivy ซึ่งเป็นเลเยอร์พร็อกซี HTTP สำหรับการอนุมานของเรา ก็มีบทบาทสำคัญในด้านประสิทธิภาพเช่นกัน เนื่องจากเพย์โหลดของคำขอมีความแตกต่างกันในการผลิต การส่งเส้นทางคำขอแต่ละรายการไปยังเรพ리카แต่ละตัวอาจทำให้เกิดความไม่สมดุลของภาระงาน (Load Imbalance) Ivy จะแบ่งคำขอแบตช์ขนาดใหญ่ออกเป็นส่วนย่อยๆ และปรับสมดุลภาระงานระหว่างเรพ리카 ซึ่งช่วยปรับปรุงการใช้งานและทำให้ความหน่วงราบรื่นขึ้น งานล่าสุดของเราเกี่ยวกับการ ทำโทเค็น Unigram ภายในองค์กร ซึ่งนำมาใช้เต็มรูปแบบใน Ivy ช่วยปรับปรุงความหน่วงได้อย่างมากเมื่อเทียบกับตัวทำโทเค็นที่มีอยู่ทั่วไป (Off-the-shelf Tokenizers)

...แต่เคอร์เนลก็ยังคงมีความสำคัญ

ROSE รองรับแบ็ก엔ด์แอตเทนชันที่หลากหลาย เคอร์เนลที่แตกต่างกันอาจเหมาะกับขนาดปัญหาที่เฉพาะเจาะจง เมื่อเวลาผ่านไป เราได้ผสานรวมเคอร์เนล FlashInfer 2, FlashInfer 3 และ FlashAttention 4 เพื่อใช้งานเรกเก็ดแอตเทนชัน (Ragged Attention)

ประสิทธิภาพของเคอร์เนลแอตเทนชันตามรูปร่างของโมเดลและขนาดปัญหา (Attention kernel performance by model shape and problem size)

โดยทั่วไป เราสังเกตเห็นว่า FlashAttention 4 ทำงานได้เร็วกว่า อย่างไรก็ตาม FlashInfer 3 มีประสิทธิภาพเหนือกว่าบนโมเดลที่อิงตาม Qwen เมื่อใช้ความยาวลำดับ (Sequence Lengths) ที่ยาวมากๆ เนื่องจากประสิทธิภาพและการปรับแต่งอาจแตกต่างกันไปตามจำนวนและมิติของ Attention Heads เราจึงยังคงรองรับการกำหนดค่าที่หลากหลายและตัดสินใจเป็นกรณีๆ ไปเมื่อให้บริการ

การเปรียบเทียบมาตรฐาน (Benchmarks)

เราทำการเปรียบเทียบกับ vLLM v0.22.0 โดยรันการอนุมานด้วยความแม่นยำ BF16 บนน้ำหนักโมเดลจริงและอินพุตที่ได้จากชุดข้อมูลการประเมิน การรันการจับเวลาทั้งหมดนำหน้าด้วยการรันวอร์มอัพ (Warmup Runs) ซึ่งตรวจสอบว่าความเบี่ยงเบนในความคล้ายคลึงโค사인 (Cosine Similarity) อยู่ภายใน 0.1%

การฝังตัวความหน่วงต่ำ (p50 / p90 / p99 / สูงสุด ms)

เรา گزارش รันไทม์สำหรับขนาดแบตช์คำขอที่ทำโทเค็นล่วงหน้า 1 รายการ, คำขอแบบต่อเนื่องอย่างสมบูรณ์, ความยาวลำดับที่ 128, 512 และ 4096 โทเค็น

ผลการเปรียบเทียบมาตรฐานการฝังตัวความหน่วงต่ำ (Low-latency embeddings benchmark results)

การให้คะแนนความหน่วงต่ำ (p50 / p90 / p99 / สูงสุด ms)

ขนาดแบตช์คำคนที่ทำโทเค็นล่วงหน้าแล้ว 5, 25 และ 50, ความยาวลำดับ 512 โทเค็น

ผลการเปรียบเทียบมาตรฐานการให้คะแนนความหน่วงต่ำ (Low-latency scoring benchmark results)

การฝังตัวปริมาณงานสูง (emb/s)

ขนาดแบตช์คำขอ (Request Batch Size) 100, มีกระบวนการทำงานพร้อมกันสี่กระบวนการที่ส่งคำขอ, และมีความยาวลำดับที่ 512, 1024 และ 4096 โทเค็น

ผลการเปรียบเทียบมาตรฐานการฝังตัวปริมาณงานสูง (High-throughput embeddings benchmark results)

การฝังตัวที่มีความพร้อมกันสูง (p50 / p90 / p99 / สูงสุด ms)

ความยาวลำดับ 512, ขนาดแบตช์ 1 แต่เราส่งคำขอพร้อมกัน 1, 2, 4, 8 และ 16 คำขอ การเปรียบเทียบมาตรฐานนี้ยังรวมถึงต้นทุนการทำโทเค็นผ่าน Ivy ควบคู่ไปกับโอเวอร์เฮดของเครือข่ายระหว่าง Ivy และ Tulip ด้วย

ผลการเปรียบเทียบมาตรฐานการฝังตัวความพร้อมกันสูง (High-concurrency embeddings benchmark results)

บทสรุปและงานในอนาคต

โครงสร้างพื้นฐานการให้บริการที่ประกอบด้วย Ivy, Tulip และ ROSE ช่วยให้เราสามารถให้บริการการฝังตัวสำหรับ Perplexity ด้วยความหน่วงที่ต่ำลงและปริมาณงานที่ดีขึ้น ส่งผลให้การค้นหามีความแม่นยำมากขึ้นในต้นทุนที่ลดลงเมื่อเทียบกับโซลูชันสำเร็จรูป

ด้วยการมุ่งเน้นไปที่โมเดลเฉพาะและรับผิดชอบสเตกทั้งหมด เราจึงได้รับอิสระที่จำเป็นในการสร้างสมดุลที่มีประสิทธิภาพระหว่างประสิทธิภาพและความยืดหยุ่น โดยผสมผสานพริมิทิฟ Rust ที่นำกลับมาใช้ใหม่ได้สูงและมีประสิทธิภาพเข้ากับโค้ดการสร้างแบบจำลอง Python ทั่วไป เอ็นจิ้นการอนุมานโอเพนซอร์สจำนวนมาก เช่น vLLM, SGLang และ TokenSpeed กำลังผสานรวมภาษาอย่าง Rust และ C++ เข้ากับสเตกของตน เราได้ลงทุนใน Rust ตลอดสองปีที่ผ่านมาและได้รับผลตอบแทนที่คุ้มค่าในแง่ของประสิทธิภาพและความสามารถในการบำรุงรักษา ด้วยการใช้การทำงานของการฝังตัวส่วนใหญ่ร่วมกับสเตกการให้บริการ LLM ของเรา เราจึงได้รับปริมาณงาน (Throughput) ที่เพิ่มขึ้นเช่นกัน โดยไม่ต้องใช้ความพยายามทางวิศวกรรมอย่างมากในการบำรุงรักษาโมเดลการฝังตัว

ในขณะที่โมเดลพัฒนาขึ้น เราจะยังคงปรับปรุงแต่ละเลเยอร์ของสเตกของเราอย่างต่อเนื่องเพื่อลดความหน่วงทั้งที่ถูกจำกัดด้วย CPU และจำกัดด้วย GPU โปรโตคอลที่ใช้ gRPC แบบกำหนดเองของเราภายใน Ivy and Tulip ช่วยให้เราสามารถปรับแต่งการสื่อสารเพื่อลดความหน่วงของเครือข่าย ในขณะที่ ROSE เป็นพื้นฐานในการปรับปรุงปริมาณงานในการคำนวณ นอกจากนี้ เนื่องจากการรองรับ Python แบบ free-threaded เติบโตขึ้นทั่วทั้งระบบนิเวศ เราจึงจะสามารถปรับปรุงความสามารถในการทำงานร่วมกันระหว่าง Python-Rust ให้ดียิ่งขึ้นเพื่อลดโอเวอร์เฮด

ข้อมูลอ้างอิง