SSD Nodes Learn Hosting plans →
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-08-28

ทำไม LLM ที่โฮสต์เองถึงทำงานช้าเมื่อมีผู้ใช้หลายคน

พบสาเหตุที่ LLM ของคุณค้างเมื่อมีผู้ใช้เกิน 5 คน พร้อมวิธีแก้ปัญหาผ่านการตั้งค่า num_parallel และการจัดการ KV Cache เพื่อเพิ่มประสิทธิภาพการประมวลผลคำขอพร้อมกันให้ดียิ่งขึ้น

เหตุใด LLM ที่โฮสต์เองจึงทำงานช้าลงเมื่อมีผู้ใช้เพิ่มขึ้น

LLM ที่โฮสต์เองจะเกิดอาการค้างเมื่อมีผู้ใช้พร้อมกัน 5 คน เนื่องจากเซิร์ฟเวอร์ยังคงสร้างคำตอบได้ทีละหนึ่งรายการเท่านั้น ส่วนอีก 4 คนที่เหลือจะต้องรออยู่ในคิว เอกสารของ Ollama ระบุไว้อย่างชัดเจนเกี่ยวกับค่าเริ่มต้นว่า OLLAMA_NUM_PARALLEL คือ "จำนวนคำขอสูงสุดที่แต่ละโมเดลจะประมวลผลพร้อมกัน โดยมีค่าเริ่มต้นเป็น 1" ระบบไม่ได้เสียหายแต่อย่างใด เพียงแต่ผู้ใช้ 4 ใน 5 คนของคุณกำลังรอคิวอยู่เท่านั้น

การแก้ไขปัญหานี้มักไม่ใช่การขยายขนาดเครื่องเซิร์ฟเวอร์ แต่เป็นการใช้ serving engine ที่สามารถผลักดันคำขอจำนวนมากผ่านโมเดลในการประมวลผลรอบเดียว (forward pass) พร้อมกับต้องมีหน่วยความจำสำรองเพียงพอที่จะเก็บข้อมูลการสนทนาของทุกคนไว้ในระหว่างการประมวลผล ทั้งสองส่วนนี้มีความสำคัญเท่าเทียมกัน และส่วนที่สองคือสิ่งที่กำหนดขีดจำกัดสูงสุดของระบบคุณอย่างแท้จริง

สองขั้นตอนที่ทุกคำขอต้องผ่าน

ขั้นตอน Prefill จะอ่าน prompt ทั้งหมดในคราวเดียวและสร้าง attention cache สำหรับ prompt นั้น โทเค็นทุกตัวใน prompt จะผ่านโมเดลไปพร้อมกัน ดังนั้น Prefill จึงเป็นการคูณเมทริกซ์ขนาดใหญ่หนึ่งครั้ง ซึ่งถูกจำกัดด้วยความเร็วในการคำนวณทางคณิตศาสตร์ (arithmetic throughput) จากนั้นขั้นตอน Decode จะเขียนคำตอบออกมาทีละหนึ่งโทเค็น แต่ละโทเค็นจำเป็นต้องอ่านค่าน้ำหนัก (weights) ทั้งหมดของโมเดลออกจากหน่วยความจำอีกครั้ง ในขณะที่การคำนวณทางคณิตศาสตร์สำหรับโทเค็นเดียวนั้นมีขนาดเล็กมาก ขั้นตอน Decode จึงถูกจำกัดด้วยแบนด์วิดท์ของหน่วยความจำ (memory bandwidth)

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

ตัวเลขสองค่าที่อธิบายประสบการณ์ของผู้ใช้คือ TTFT (time to first token) ซึ่งประกอบด้วยเวลารอในคิวบวกกับเวลาทำ Prefill และ ITL (inter-token latency) ซึ่งคือช่วงเวลาระหว่างโทเค็นที่สตรีมออกมา โดยถูกกำหนดโดยขั้นตอน Decode เซิร์ฟเวอร์ที่ทำงานช้ามักเกิดจากปัญหาในขั้นตอนใดขั้นตอนหนึ่ง และวิธีการแก้ไขก็ไม่เหมือนกัน การระบุให้ชัดเจนว่าคุณกำลังเผชิญกับปัญหาในขั้นตอนใดก่อนที่จะเปลี่ยนการตั้งค่าใดๆ เป็นสิ่งที่ควรทำ และ การวัดเวลาของ Prefill และ Decode แยกกัน คือวิธีที่คุณจะค้นพบคำตอบนั้น

Static batching ทำให้ทุกคนต้องรอการตอบกลับที่ช้าที่สุด

Static batching เป็นรูปแบบพื้นฐานที่สุด ซึ่งเกิดขึ้นเมื่อคุณจัดกลุ่มคำขอด้วยตนเองในโค้ดแอปพลิเคชัน โดยที่ engine จะรวบรวมคำขอจำนวน N รายการมาประมวลผลพร้อมกัน และจะกักทุก slot ไว้จนกว่าการสร้างข้อความที่ใช้เวลานานที่สุดในกลุ่มจะเสร็จสิ้น

ผู้ใช้รายหนึ่งที่ขอสรุปความยาว 1,200 token จะทำให้คำตอบแบบบรรทัดเดียวอีกสี่รายการถูกกักไว้ใน batch เพราะ batch จะไม่ปล่อย slot ใดๆ จนกว่าสมาชิกที่ช้าที่สุดจะทำงานเสร็จ

ผลที่ตามมามีสองประการ ประการแรก ลำดับที่เสร็จสิ้นแล้วยังคงครอบครอง slot โดยไม่ได้ประมวลผลสิ่งที่มีประโยชน์ ส่งผลให้ throughput โดยรวมลดลงเมื่อความยาวของผลลัพธ์มีความแตกต่างกัน ซึ่งความยาวของผลลัพธ์ในแชทมักจะแตกต่างกันมาก ประการที่สอง คำขอที่เข้ามาหลังจาก batch ถูกสร้างขึ้นแล้วหนึ่งจังหวะ จะต้องรอให้ batch ทั้งหมดทำงานเสร็จสิ้นก่อนจึงจะเริ่มขั้นตอน prefill ได้ ซึ่งหมายความว่าค่า TTFT ของคำขอนั้นจะถูกกำหนดโดยความยาวของงานที่ผู้อื่นส่งเข้ามา

Continuous batching ยอมรับและยุติคำขอในทุกโทเค็น

Continuous batching จะจัดตารางการทำงานในระดับขั้นตอนการถอดรหัส (decoding step) ทีละขั้นตอน หลังจากจบแต่ละขั้นตอน ตัวจัดตารางจะคัดลำดับที่เพิ่งสร้าง stop token ออกไป แล้วจึงนำคำขอที่รออยู่ในคิวเข้ามาแทนที่ในช่องว่างที่ว่างลง คำตอบที่สิ้นสุดที่ขั้นตอนที่ 40 จะทำให้ช่องว่างนั้นว่างลงที่ขั้นตอนที่ 40 ไม่ใช่รอจนจบทั้ง batch

เรื่องนี้ไม่ใช่เรื่องแปลกใหม่ llama-server ระบุเอกสาร -cb, --cont-batching ไว้ว่า "เลือกว่าจะเปิดใช้งาน continuous batching (หรือที่เรียกว่า dynamic batching) หรือไม่ (ค่าเริ่มต้น: เปิดใช้งาน)" และ vLLM ก็ถูกสร้างขึ้นโดยใช้แนวคิดนี้เป็นหลัก Ollama เองก็ให้บริการคำขอแบบขนานเช่นกัน ค่าเริ่มต้นเพียงแค่จำกัดจำนวนไว้ที่ 1 เท่านั้น ซึ่งเป็นเหตุผลว่าทำไมหลายคนจึงสรุปว่าฮาร์ดแวร์ของตนไม่สามารถรองรับการทำงานพร้อมกันได้ ทั้งที่จริงแล้วเป็นเพราะการตั้งค่าที่กำหนดไว้เช่นนั้น

ผลลัพธ์ของ continuous batching ที่เผยแพร่มักวัดจาก datacenter card ซึ่งมีทั้งพลังประมวลผลเหลือเฟือและหน่วยความจำหลายสิบกิกะไบต์สำหรับ cache รูปแบบของผลลัพธ์เหล่านั้นสามารถนำมาปรับใช้กับเครื่องของคุณได้ แต่ขนาดของมันนั้นไม่สามารถเทียบกันได้ และส่วนของหน่วยความจำด้านล่างนี้คือเหตุผลว่าทำไม

การทำ Prefill แย่งทรัพยากรประมวลผลกับขั้นตอน Decode

เมื่อมีคำขอใหม่เข้ามาในขณะที่กำลังสตรีมคำตอบอยู่ 4 รายการ ระบบจะต้องทำ prefill สำหรับ prompt นั้นก่อน ซึ่งขั้นตอน prefill ใช้ทรัพยากรประมวลผลสูง หากตัวจัดตารางเวลา (scheduler) ให้ขั้นตอน prefill นี้ทำงานแยกเป็นเอกเทศ ผู้ใช้งานทั้ง 4 รายที่กำลังรับข้อมูลสตรีมอยู่จะไม่ได้รับ token ใดๆ ในช่วงเวลานั้น สำหรับ prompt ที่มีความยาว การหยุดชะงักนี้จะสังเกตเห็นได้ชัดเจนในทุกหน้าต่างที่เปิดอยู่ นี่คืออาการกระตุกที่ผู้ใช้มักกล่าวถึงเมื่อบอกว่าเซิร์ฟเวอร์มีอาการสะดุดทุกครั้งที่มีคนอื่นกดส่งคำขอ

Chunked prefill จะแบ่ง prompt ที่ยาวออกเป็นส่วนย่อยๆ และแทรกแต่ละส่วนเข้าไปในขั้นตอนเดียวกับการทำ decode ที่กำลังทำงานอยู่ คู่มือการปรับแต่งของ vLLM ระบุถึงข้อแลกเปลี่ยนนี้ไว้อย่างชัดเจนว่า การกำหนด chunk budget ที่เล็กลงจะ "ช่วยให้ค่า ITL ดีขึ้นเนื่องจากมีการทำ prefill น้อยลงซึ่งช่วยลดการหน่วงขั้นตอน decode" ในขณะที่ค่าที่สูงกว่าจะ "ช่วยให้ค่า time to first token (TTFT) ดีขึ้นเนื่องจากสามารถประมวลผล token ในขั้นตอน prefill ได้มากขึ้นต่อหนึ่ง batch" คุณกำลังเลือกว่าจะปกป้องประสบการณ์ของผู้ใช้งานกลุ่มใด ระหว่างคนที่กำลังรอให้คำตอบเริ่มแสดงผล หรือคนที่กำลังเฝ้าดูข้อความที่กำลังสตรีมอยู่

ความยาวของ prompt เป็นตัวตัดสินว่าปัญหานี้จะส่งผลกระทบมากน้อยเพียงใด prompt ขนาด 6,000 token ที่มีคำตอบ 200 token จะเท่ากับงาน prefill 6,000 token เทียบกับขั้นตอน decode 200 ขั้น การแชทแบบ Retrieval-augmented และ system prompt ที่ยาวล้วนผลักดันให้คุณเข้าสู่สภาวะนี้ ดังนั้น prefill จึงไม่ใช่แค่ค่าความคลาดเคลื่อนเล็กน้อยอีกต่อไป แต่กลายเป็นสิ่งที่ผู้ใช้งานต้องรอคอย Prefix caching จะช่วยได้เมื่อส่วนที่ยาวนั้นมีการใช้งานซ้ำ: vLLM มีฟีเจอร์ --enable-prefix-caching ซึ่งจะนำ cache กลับมาใช้ใหม่สำหรับส่วน prefix ของ prompt ที่ใช้ร่วมกัน แทนที่จะต้องคำนวณใหม่สำหรับทุกคำขอ

หน่วยความจำที่หมดก่อนใครคือ KV cache

ทุกโทเค็นในทุกบทสนทนาที่กำลังดำเนินอยู่จะทิ้ง key vector และ value vector ไว้ในทุกเลเยอร์ของโมเดล สิ่งนี้เรียกว่า KV cache (key/value cache) ซึ่งช่วยให้ขั้นตอนการถอดรหัส (decode) ไม่ต้องคำนวณ prompt ทั้งหมดใหม่สำหรับทุกโทเค็นที่สร้างขึ้น ขนาดของ cache ต่อโทเค็นถูกกำหนดไว้ตายตัวตามโครงสร้างของโมเดล คือ 2 (หนึ่ง key, หนึ่ง value) คูณด้วยจำนวนเลเยอร์ คูณด้วยจำนวน key/value heads คูณด้วยมิติของ head และคูณด้วยจำนวนไบต์ต่อค่าหนึ่งค่า คุณสามารถอ่านค่าเหล่านี้ได้จาก config.json ของโมเดล

เมื่อคำนวณได้หนึ่งครั้ง เพดานของหน่วยความจำก็จะไม่ใช่เรื่องลึกลับอีกต่อไป โมเดลขนาด 8B ทั่วไปที่มี 36 เลเยอร์, 8 key/value heads และมิติของ head เท่ากับ 128 โดยเก็บ cache ในรูปแบบ 16-bit จะใช้หน่วยความจำ 2 36 8 128 2 ไบต์ต่อโทเค็น ซึ่งเท่ากับ 147,456 ไบต์ หรือประมาณ 144 KiB ดังนั้นบทสนทนาที่มีความยาว 8,192 โทเค็นจึงต้องการ cache ประมาณ 1.2 GB หากมี 5 บทสนทนาพร้อมกันก็จะใช้ประมาณ 6 GB นอกเหนือจากขนาดของ weights และนี่คือคำตอบที่แท้จริงว่ารองรับผู้ใช้งานได้กี่คน

การทำงานพร้อมกัน (concurrency) จะเพิ่มภาระของบริบท (context) และเครื่องมือต่างๆ ก็ระบุเรื่องนี้ไว้อย่างชัดเจน ใน FAQ ของ Ollama ระบุว่า: "การประมวลผลคำขอแบบขนานสำหรับโมเดลที่กำหนดจะส่งผลให้ขนาดของบริบทเพิ่มขึ้นตามจำนวนคำขอแบบขนาน ตัวอย่างเช่น บริบทขนาด 2K ที่มี 4 คำขอแบบขนานจะส่งผลให้เกิดบริบทขนาด 8K และการจัดสรรหน่วยความจำเพิ่มเติม" RAM ที่ต้องใช้จะแปรผันตาม OLLAMA_NUM_PARALLEL คูณด้วย OLLAMA_CONTEXT_LENGTH ใน llama-server บริบทที่คุณกำหนดด้วย -c จะถูกแบ่งใช้ใน -np slots ดังนั้นการเพิ่มจำนวน slot เพียงอย่างเดียวจะทำให้ความจุของแต่ละคำขอลดลง ควรตรวจสอบขนาดบริบทต่อ slot จาก log ตอนเริ่มต้นแทนการคาดเดา

vLLM ใช้วิธีการจองหน่วยความจำล่วงหน้าแทน โดย --gpu-memory-utilization (ค่าเริ่มต้น 0.92) คือ "สัดส่วนของหน่วยความจำ GPU ที่จะใช้สำหรับตัวประมวลผลโมเดล" พื้นที่ที่เหลือหลังจากหักส่วนของ weights จะกลายเป็น paged KV pool และเมื่อ pool ดังกล่าวไม่เพียงพอ ตัวจัดตารางเวลา (scheduler) จะทำการยกเลิกคำขอ (evict) แทนที่จะปล่อยให้ระบบล้มเหลว:

WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.

ใน engine รุ่น V1 ของ vLLM โหมดการแย่งชิงทรัพยากร (preemption mode) เริ่มต้นคือ RECOMPUTE ดังนั้นคำขอที่ถูกยกเลิกจะทิ้ง cache ของตนเองและทำการ prefill ใหม่เมื่อได้รับอนุญาตให้กลับเข้ามาทำงานอีกครั้ง งานส่วนนี้จึงถูกทำซ้ำสองรอบ เอกสารประกอบเตือนว่า "การแย่งชิงทรัพยากรและการคำนวณใหม่สามารถส่งผลเสียต่อ latency โดยรวม" และบรรทัด log นี้คือคำอธิบายที่ดีที่สุดว่าเหตุใดผู้ใช้งานที่โชคร้ายคนหนึ่งจึงต้องรอนานกว่าคนอื่นมาก ในขณะที่ค่าเฉลี่ยของคุณยังดูปกติ ให้ตั้งค่า disable_log_stats=False เพื่อบันทึกจำนวนสะสม หรืออ่านตัวนับการแย่งชิงทรัพยากรจาก Prometheus metrics ที่ vLLM เปิดให้เข้าถึงได้

การเปลี่ยนแปลงที่จำนวนผู้ใช้พร้อมกัน 2, 5 และ 20 ราย

ผู้ใช้ 2 ราย: แทบไม่ส่งผลกระทบต่อ GPU ที่มี cache เหลือเฟือ เพราะสตรีมการถอดรหัส (decode stream) ที่สองจะทำงานไปพร้อมกับสตรีมแรกโดยใช้เวลาเพิ่มขึ้นเพียงเล็กน้อย แต่สำหรับ VPS ที่ใช้ CPU เพียงอย่างเดียวและมี RAM 4 ถึง 8 GB นั้นไม่ใช่เรื่องฟรี ทั้งสองสตรีมต้องแชร์ vCPU และแบนด์วิดท์ RAM ชุดเดียวกัน ทำให้ผู้ใช้แต่ละรายได้รับจำนวน token ต่อวินาทีลดลงประมาณครึ่งหนึ่ง และความต้องการ cache จะเพิ่มขึ้นเป็นสองเท่าภายใต้งบประมาณทรัพยากรที่จำกัดกว่ามาก

ผู้ใช้ 5 ราย: นี่คือจุดที่ค่าเริ่มต้นไม่เพียงพออีกต่อไป และปัญหาจะเริ่มจากการเป็นปัญหาคิว (queue problem) หากตั้งค่า OLLAMA_NUM_PARALLEL ไว้ที่ 1 ผู้ใช้สี่คนจะต้องรอคิวจากคนที่กำลังขอคำตอบที่ยาว และแต่ละคนจะได้รับความเร็วปกติทันทีที่ถึงคิวของตนเอง หากเพิ่มจำนวนการประมวลผลแบบขนาน (parallel count) ปัญหาจะเปลี่ยนรูปแบบไป: การมี 5 ช่องทางที่บริบท (context) ช่องละ 8K หมายถึงต้องหาพื้นที่สำหรับ cache ขนาด 40K token หากไม่พอใน VRAM เอนจินจะย้ายเลเยอร์ไปไว้ใน RAM ของระบบ และหากไม่พอใน RAM อีก เครื่องจะทำการ swap และจำนวน token ต่อวินาทีจะลดลงอย่างรวดเร็ว

ผู้ใช้ 20 ราย: โดยปกติแล้วมนุษย์ 20 คนใน UI แชทไม่ได้ส่งคำขอพร้อมกันทั้ง 20 ราย และนี่คือสิ่งที่สำคัญที่สุดที่ต้องเข้าใจก่อนตัดสินใจซื้อฮาร์ดแวร์ คนหนึ่งคนจะอ่านคำตอบและใช้เวลาคิด 20 ถึง 60 วินาทีก่อนจะถามคำถามถัดไป ดังนั้นเซสชันส่วนใหญ่จึงอยู่ในสถานะว่าง (idle) แต่สำหรับเอเจนต์ 20 ตัว หรือการสรุปเอกสาร 20 งานนั้นถือเป็น 20 สตรีมที่ทำงานจริงโดยไม่มีช่วงเวลาว่างเลย ซึ่งต้องใช้เครื่องที่มีสเปกต่างออกไป นักพัฒนาหนึ่งคนที่ ชี้เป้าเอเจนต์เขียนโค้ดไปยังเซิร์ฟเวอร์ Ollama ของตนเอง จะมีลักษณะการใช้งานใกล้เคียงกับกรณีหลังมากกว่ากรณีแรก เพราะเอเจนต์จะส่งคำขออย่างต่อเนื่องตราบเท่าที่งานยังดำเนินอยู่ และไม่มีช่วงหยุดพักอ่านเหมือนที่มนุษย์ทำ

ผู้ใช้งานของคุณมีการใช้งานพร้อมกัน หรือเพียงแค่ล็อกอินค้างไว้เท่านั้น?

คำนวณจำนวนคำขอที่กำลังประมวลผล (requests in flight) ก่อนที่จะกำหนดขนาดของระบบ การคำนวณเป็นเรื่องพื้นฐาน: จำนวนคำขอที่กำลังประมวลผล เท่ากับ จำนวนผู้ใช้งาน คูณด้วยจำนวนวินาทีที่ใช้สร้างข้อความต่อหนึ่งรอบ หารด้วยจำนวนวินาทีระหว่างรอบ

  1. วัดความเร็วในการประมวลผลแบบ single-stream ของคุณเองก่อน โดยต้องรวมทั้งขั้นตอน prefill และ decode อย่าหยิบตัวเลขจากฮาร์ดแวร์ของผู้อื่นมาใช้: วัดค่า tokens per second บนเครื่องของคุณเอง แล้วใช้ค่าที่ได้จริง
  2. ประเมิน duty cycle ตัวอย่างเช่น ผู้ใช้งานแชท 20 คน ใช้เวลาสร้างข้อความ 12 วินาทีต่อรอบ และมีหนึ่งรอบทุกๆ 90 วินาที จะได้ 20 * 12 / 90 ซึ่งเท่ากับประมาณ 2.7 คำขอที่กำลังประมวลผลพร้อมกัน
  3. กำหนดจำนวน slot ให้สูงกว่าค่าที่คำนวณได้เล็กน้อย จากนั้นตรวจสอบกับหน่วยความจำ: จำนวน slot คูณด้วย context ต่อคำขอ ต้องมีขนาดไม่เกินจำนวน cache tokens ที่คุณมีจริง
  4. รักษาความยาวของคิวให้สั้น เพื่อให้กรณีที่เกิด overflow ระบบจะแจ้งเตือนความล้มเหลวได้อย่างรวดเร็วและชัดเจน

จำนวน cache tokens ที่ใช้งานได้ คือหน่วยความจำที่เหลือหลังจากโหลด weights แล้ว หารด้วยต้นทุนต่อ token จากหัวข้อก่อนหน้า การ์ดจอขนาด 24 GB ที่รันโมเดล 8B แบบ 16-bit จะใช้หน่วยความจำประมาณ 16 GB สำหรับ weights และเหลือ cache ที่ใช้งานได้ประมาณ 6 GB ที่ระดับการใช้งานปกติ ซึ่งเพียงพอสำหรับบทสนทนาขนาด 8K จำนวน 5 รายการ หากต้องการเพิ่มจำนวน ให้ลดขนาด context ต่อคำขอ หรือจัดเก็บ cache ในรูปแบบ 8-bit (llama-server ใช้ --cache-type-k q8_0) ทั้งสองวิธีช่วยเพิ่มความสามารถในการรองรับการใช้งานพร้อมกันโดยแลกกับบางสิ่ง ซึ่งควรศึกษาความคุ้มค่าของการแลกเปลี่ยนนี้อย่างละเอียดก่อนตัดสินใจลงทุนในฮาร์ดแวร์: จุดคุ้มทุนระหว่าง GPU VPS กับการใช้ API tokens

เมื่อค่าเริ่มต้นของ Ollama ไม่เพียงพออีกต่อไป

ให้เพิ่มจำนวนการประมวลผลแบบขนาน (parallel count) ผ่าน service unit เนื่องจากคำสั่ง export ใน shell จะไม่มีผลกับ daemon ที่จัดการโดย systemd

sudo systemctl edit ollama.service
[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"
sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama ps

systemctl show ควรแสดงตัวแปรทั้ง 3 รายการที่คุณเพิ่งตั้งค่าไป หากไม่แสดงผล แสดงว่าการบันทึก drop-in ไม่สำเร็จ และสิ่งอื่นที่คุณทำหลังจากนี้จะไม่มีผลใดๆ จากนั้น ollama ps จะแสดงรายการโมเดลที่โหลดอยู่พร้อมขนาดที่มากกว่าน้ำหนักของโมเดลเพียงอย่างเดียว เนื่องจาก 4 slots ที่ 8,192 tokens จะจองพื้นที่ cache เพิ่มอีก 32,768 tokens ไว้ข้างๆ กัน หากคอลัมน์ PROCESSOR แสดงว่าโมเดลบางส่วนทำงานบน CPU ในขณะที่คุณคาดหวังให้ทำงานบน GPU ทั้งหมด นั่นหมายความว่าคุณกำหนดขนาด cache ไว้เกินกว่าที่การ์ดจอจะเหลือพื้นที่ให้ ให้ลดตัวเลขตัวใดตัวหนึ่งลง การลด context มักจะเป็นทางเลือกที่ปลอดภัยกว่า แต่หน้าต่าง (window) ที่เล็กเกินไปจะตัดทอน prompt ที่ยาวโดยไม่แจ้งเตือน แทนที่จะแสดงข้อผิดพลาด ดังนั้นจึงควร กำหนดขนาด num_ctx อย่างตั้งใจ แทนการลดค่าลงเรื่อยๆ จนกว่าโมเดลจะใส่ได้พอดี

ค่าเริ่มต้นของคิว (queue) เป็นสิ่งที่ควรพิจารณาอีกครั้ง Ollama รองรับคิวได้สูงสุด OLLAMA_MAX_QUEUE คำขอ และ "ค่าเริ่มต้นคือ 512" หากเกินกว่านั้น ระบบจะตอบกลับ "ด้วยข้อผิดพลาด 503 ซึ่งระบุว่าเซิร์ฟเวอร์ทำงานหนักเกินไป" การตั้งคิวไว้ที่ 512 บนเครื่องที่รองรับการประมวลผลได้ครั้งละ 4 คำขอ เป็นสัญญาที่คุณไม่สามารถทำตามได้จริง เพราะไคลเอนต์ที่อยู่ในลำดับที่ 300 จะหมดเวลา (timeout) ไปนานก่อนที่จะถึงคิวของตนเอง การตั้งคิวให้สั้นจะทำให้เกิดข้อผิดพลาดที่แอปพลิเคชันของคุณสามารถลองใหม่หรือรายงานผลได้ ซึ่งดีกว่าการปล่อยให้หน้าจอค้างโดยไม่มีการตอบสนอง

ทดสอบการใช้งานจริงโดยส่งคำขอ 2 รายการพร้อมกันจาก 2 terminal แล้วสังเกตผลลัพธ์ หากรายการที่สองไม่แสดงผลจนกว่ารายการแรกจะเสร็จสิ้น แสดงว่าการตั้งค่าแบบขนานไม่มีผลใช้งานจริง

เมื่อเอนจินสำหรับให้บริการจริงเริ่มคุ้มค่ากับการลงทุน

vLLM จะแสดงประสิทธิภาพที่คุ้มค่ากับการตั้งค่าเพิ่มเติมเมื่อคุณมี GPU ที่มีทรัพยากรเหลือเฟือและมีคำขอที่กำลังประมวลผลอยู่จริงมากกว่า 4 รายการ ตัวจัดตารางเวลาของ vLLM ทำงานในระดับ token และใช้การจัดการ cache แบบแบ่งหน้า (paged) เพื่อให้สามารถนำส่วนที่ว่างกลับมาใช้ใหม่ได้ อีกทั้งยังเปลี่ยน VRAM ที่เหลืออยู่ให้กลายเป็นความสามารถในการประมวลผลพร้อมกันแทนที่จะปล่อยให้ว่างเปล่า ณ เดือนสิงหาคม 2026 การติดตั้งและเริ่มใช้งานตามเอกสารประกอบมีเพียงสองคำสั่งดังนี้:

uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instruct
curl http://localhost:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "Qwen/Qwen2.5-1.5B-Instruct",
        "messages": [{"role": "user", "content": "Say hello."}]
    }'

หากได้รับคำตอบที่มีอาร์เรย์ choices แสดงว่าเซิร์ฟเวอร์ทำงานแล้วและโมเดลถูกโหลดเรียบร้อย ภายใต้ภาระงานหนัก ค่าปรับแต่งสองตัวที่มีความสำคัญคือ --max-num-seqs ซึ่งคือ "จำนวนลำดับสูงสุดที่จะประมวลผลในการวนซ้ำหนึ่งครั้ง" และ --max-num-batched-tokens ซึ่งคือ "จำนวน token สูงสุดที่สามารถประมวลผลในการวนซ้ำหนึ่งครั้ง" ค่าแรกจะเป็นตัวจำกัดจำนวนการประมวลผลพร้อมกัน ส่วนค่าที่สองคือขีดจำกัดของ chunked prefill ที่ได้อธิบายไปก่อนหน้านี้

หากมีคำขอที่กำลังประมวลผลอยู่น้อยกว่าสี่รายการ หรือบนเครื่องที่ไม่มี GPU ที่รองรับ vLLM จะเพิ่มความซับซ้อนโดยให้ผลลัพธ์กลับมาเพียงเล็กน้อยเท่านั้น vLLM ต้องการการ์ดระดับ CUDA และจะจองหน่วยความจำส่วนใหญ่ไว้ตั้งแต่เริ่มทำงาน ซึ่งเป็นการแลกเปลี่ยนที่ไม่คุ้มค่าสำหรับ VPS ขนาด 4 ถึง 8 GB ในกรณีนั้น คำตอบคือการใช้โมเดลที่เล็กลงพร้อม context ที่สั้นกว่าและคิวที่คุณควบคุมเอง ความแตกต่างระหว่าง Ollama และ vLLM ในฐานะเอนจินสำหรับให้บริการ ได้ครอบคลุมการตัดสินใจเลือกไว้อย่างครบถ้วน และ การรัน Qwen 3 8B บน VPS แสดงให้เห็นว่าโมเดลขนาดกลางต้องการทรัพยากรเท่าใดก่อนที่คุณจะเพิ่มผู้ใช้งานแม้แต่คนเดียว

ข้อแลกเปลี่ยนที่ความเชื่อทั่วไปไม่ได้บอกไว้

Continuous batching ช่วยเพิ่ม throughput รวม และมักจะช่วยปรับปรุง median latency ให้ดีขึ้นด้วย เนื่องจากคำขอที่อยู่ในคิวจะเริ่มประมวลผลได้เร็วขึ้น อย่างไรก็ตาม tail latency จะได้รับผลกระทบในทางตรงกันข้าม ซึ่งเป็นประเด็นที่แทบไม่มีการกล่าวถึง

ทุก sequence ที่เพิ่มเข้ามาในหนึ่ง step จะเพิ่มภาระงานเล็กน้อย ส่งผลให้ ITL ของทุกคนสูงขึ้นเมื่อ batch เริ่มเต็ม การ prefill ของคำขอใหม่จะเข้ามาแย่งทรัพยากรใน step ที่ผู้ใช้งานแบบ streaming ควรจะได้รับ หากเกิดสภาวะ cache pressure ตัว scheduler จะทำการ preempt ซึ่งส่งผลให้คำขอที่กำลังสร้างอยู่ครึ่งๆ กลางๆ ต้องกลับไปเริ่ม prefill ใหม่ตั้งแต่ต้น

UI ของแชทจะแสดงผลที่ค่า tail ไม่ใช่ค่าเฉลี่ย การที่ stream หยุดชะงักไปสองวินาทีระหว่างประโยคจะทำให้ผู้ใช้รู้สึกว่าระบบพัง แม้ว่าเวลาโดยรวมจนเสร็จสิ้นจะอยู่ในเกณฑ์ดีก็ตาม ให้วัดค่า p95 TTFT และ p95 ITL ภายใต้โหลดที่คุณคาดการณ์ไว้ และให้มองว่าค่า mean tokens per second เป็นเพียงตัวเลขบอกความจุของระบบ ไม่ใช่ตัวบ่งชี้ประสบการณ์การใช้งานจริง

แนวทางปฏิบัติที่เหมาะสมจึงสรุปได้ดังนี้ ให้จำกัด concurrency ไว้ต่ำกว่าขีดจำกัดของหน่วยความจำเล็กน้อย เพื่อให้ engine ไม่จำเป็นต้องทำการ preempt คิวที่สั้นและคาดการณ์ได้ดีกว่า batch ที่ลึกจนทำให้เกิดการ thrashing เพราะผู้ใช้ที่รอสี่วินาทีแล้วได้รับข้อมูลแบบลื่นไหลย่อมพึงพอใจมากกว่าผู้ใช้ที่เริ่มได้ทันทีแต่ต้องหยุดชะงักถึงสองครั้ง

สิ่งที่ควรตรวจสอบเมื่อระบบทำงานช้า

ผู้ใช้แต่ละรายใช้งานได้ปกติ แต่ต้องรอคิวนาน นี่คือปัญหาเรื่องคิว ไม่ใช่ปัญหาความเร็ว ให้ตรวจสอบการตั้งค่า parallel เป็นอันดับแรก โมเดลกำลังให้บริการอย่างถูกต้อง โดยประมวลผลทีละคำขอ

ได้รับ HTTP 503 จาก Ollama คิวเต็มแล้ว อาจเป็นไปได้ว่าเครื่องทำงานเต็มขีดความสามารถจริงๆ หรือมีการตั้งค่า OLLAMA_MAX_QUEUE ไว้ต่ำโดยเจตนาเพื่อลดภาระงาน ซึ่งเป็นพฤติกรรมที่ควรจะเป็น

จำนวน Token ต่อวินาทีลดลงอย่างมากเมื่อมีภาระงานบนเครื่องที่ใช้ CPU ให้รันคำสั่ง vmstat 1 ในขณะที่เกิดปัญหา หากคอลัมน์ si และ so มีค่าที่ไม่ใช่ศูนย์ แสดงว่าเครื่องกำลังทำ swapping ซึ่งหมายความว่าน้ำหนักของโมเดล (weights) กำลังถูกอ่านจากดิสก์ในทุกๆ token ที่สร้างขึ้น ไม่มีการตั้งค่าใดที่จะแก้ไขปัญหานี้ได้ ให้ลดขนาดโมเดลหรือลดจำนวน slot ลง

ผู้ใช้หนึ่งในสิบรายต้องรอนานกว่ารายอื่นมาก ให้ค้นหาคำว่า preempted ใน log ของ vLLM สาเหตุส่วนใหญ่มักเกิดจากการทำ preemption และการคำนวณซ้ำ ซึ่งหมายความว่า cache ถูกใช้งานเกินขีดความสามารถสำหรับความยาวของบริบท (context length) ที่คุณอนุญาต

ค่า TTFT แย่แม้ในขณะที่เซิร์ฟเวอร์ว่างงาน นี่คือปัญหาเรื่อง prefill ไม่ใช่ concurrency การใช้ prompt ที่ยาวต้องใช้เวลาจริงก่อนที่ token แรกจะปรากฏ ดังนั้นให้พิจารณาขนาดของ prompt และการทำ prefix caching ก่อนที่จะตรวจสอบฮาร์ดแวร์ หากการรอนานเกิดขึ้นเฉพาะกับผู้ใช้คนแรกหลังจากช่วงที่ไม่มีการใช้งาน และผู้ใช้คนถัดไปใช้งานได้ปกติ นั่นไม่ใช่ปัญหา prefill แต่เป็นเพราะ Ollama ยกเลิกการโหลดโมเดลและอ่านน้ำหนักจากดิสก์ใหม่อีกครั้ง ซึ่งควรแก้ไขโดย การคงโมเดลไว้ในหน่วยความจำระหว่างคำขอ

FAQ

ทำไม LLM ที่ผมโฮสต์เองถึงทำงานช้าลงเมื่อมีคนที่สองเข้ามาใช้งาน?

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

GPU ขนาดเล็กหนึ่งตัวสามารถรองรับผู้ใช้พร้อมกันได้กี่คน?

ให้คำนวณจากหน่วยความจำ ไม่ใช่จำนวนผู้ใช้ เริ่มจากขนาดของ Weights ตามด้วย KV cache ซึ่งมีค่าเท่ากับ 2 คูณจำนวนเลเยอร์ คูณจำนวน key/value heads คูณขนาดของ head และคูณจำนวนไบต์ ต่อหนึ่งโทเค็น ต่อหนึ่งการสนทนาที่กำลังทำงานอยู่ โมเดล 8B ทั่วไปที่มี 36 เลเยอร์, 8 key/value heads และขนาด head 128 จะใช้หน่วยความจำประมาณ 144 KiB ต่อโทเค็นในรูปแบบ 16-bit ดังนั้นการสนทนาที่ความยาว 8,192 โทเค็นจะใช้พื้นที่ประมาณ 1.2 GB การ์ดจอขนาด 24 GB ที่โหลดโมเดลนี้ในรูปแบบ 16-bit จะเหลือพื้นที่สำหรับ cache ประมาณ 6 GB ซึ่งรองรับการสนทนาเต็ม context ได้ประมาณ 5 รายการ หรือมากกว่านั้นหากคุณลดความยาวของ context ลง

Continuous batching ทำให้การตอบกลับของผู้ใช้แต่ละคนช้าลงหรือไม่?

โดยปกติค่ามัธยฐานของ latency จะดีขึ้น เพราะคำขอไม่ต้องรอให้ทั้ง batch ทำงานเสร็จสิ้น แต่ค่า tail latency จะแย่ลง เนื่องจากแต่ละลำดับที่เพิ่มเข้ามาจะเพิ่มภาระงานในทุกขั้นตอนการถอดรหัส (decoding step) การ prefill ของคำขอใหม่จะแย่งเวลาในขั้นตอนการสตรีมของผู้ใช้เดิม และคำขอที่ถูกขัดจังหวะ (preempted) จะต้องทำการ prefill ใหม่ถึงสองครั้ง ให้วัดค่า p95 inter-token latency แทนค่าเฉลี่ย เพราะหน้าต่างแชทจะทำให้เห็นช่วงหยุดชะงักได้ชัดเจนในแบบที่ค่าเฉลี่ยซ่อนไว้

ผมควรเพิ่ม OLLAMA_NUM_PARALLEL หรือย้ายไปใช้ vLLM?

ให้เพิ่มจำนวน parallel ก่อน เพราะทำได้ฟรีและใช้เพียงไฟล์ drop-in ไฟล์เดียว อีกทั้งยังช่วยแก้ปัญหาทั่วไปที่ผู้ใช้ 4 คนต้องรอคิวต่อจากคำตอบยาวๆ ของคนเดียวได้ หน่วยความจำคือข้อจำกัด: การเพิ่มคำขอแบบขนานจะคูณจำนวน context ที่คุณต้องเก็บไว้ ดังนั้นให้คอยสังเกตว่ามีเลเยอร์ใดถูกผลักไปใช้ CPU หรือไม่ ให้ย้ายไปใช้ vLLM เมื่อคุณมี GPU ที่มี VRAM เหลือเฟือและมีคำขอที่ทำงานอยู่จริงมากกว่า 4 รายการขึ้นไป เนื่องจากนั่นคือจุดที่ paged cache และการจัดตารางเวลาแบบ per-token จะให้ผลลัพธ์คุ้มค่ามากกว่าต้นทุนที่เสียไป

การเพิ่มจำนวน CPU cores จะช่วยแก้ปัญหาเซิร์ฟเวอร์ LLM ช้าได้หรือไม่?

ไม่ได้ผลสำหรับส่วนที่ผู้ใช้สังเกตเห็นได้มากที่สุด การถอดรหัส (decode) จะอ่านโมเดลทั้งหมดจากหน่วยความจำสำหรับทุกโทเค็น ดังนั้นจึงถูกจำกัดด้วยแบนด์วิดท์ของ RAM และคอร์ที่เพิ่มขึ้นจะไม่ช่วยอะไรเมื่อแบนด์วิดท์เต็มแล้ว ส่วนการ prefill จะทำงานได้ดีขึ้นตามจำนวนคอร์ ดังนั้นการเพิ่มคอร์จะช่วยลดเวลาในการแสดงโทเค็นแรก (time to first token) สำหรับ prompt ที่ยาว บน VPS ขนาด 4 ถึง 8 GB ข้อจำกัดหลักมักจะเป็นความจุของหน่วยความจำ วิธีแก้ที่มีประสิทธิภาพคือการใช้โมเดลที่เล็กลงหรือลดความยาวของ context แทนการเพิ่ม vCPU