If you have ever watched an LLM generate a long response and wondered why it seems to crawl after the initial prompt flash, you are observing a hardware bottleneck in real time. Most developers blame their Python code, the framework, or the sheer size of the model. They profile functions, swap optimizers, and shave milliseconds off preprocessing. None of that fixes the real problem. The speed limit is not in your software. It is in the silicon.
Every large language model inference job depends on two physical traits of the GPU sitting in your server: how fast it can crunch numbers, and how fast it can move those numbers into position to be crunched.
Math Is Cheap. Moving Data Is Not
GPU marketing loves to talk about compute. Trillions of floating-point operations per second. The numbers are staggering. But compute is only half the story. The other half is memory bandwidth, the rate at which data travels from high-bandwidth memory into the compute cores where the actual arithmetic happens.
An LLM cannot run faster than the weaker of these two links. Imagine a commercial kitchen with twenty master chefs. The ovens are hot, the knives are sharp, and every cook is ready. But the produce delivery arrives by bicycle, one basket at a time. The kitchen stalls. Adding more chefs will not fix it. Buying faster ovens will not fix it. The bottleneck is the road.
In modern datacenter GPUs, the arithmetic units are so powerful that they often finish their calculations and then sit idle, burning cycles while they wait for weights and activations to stream through memory. This imbalance is not a bug in your code. It is the physical reality of how chips are built. Memory bandwidth has not kept pace with raw compute, and LLMs are particularly cruel to this imbalance because their forward passes require touching every single parameter for every single output token.
Why Prompts Feel Fast and Generation Feels Slow
LLM inference splits into two distinct phases, and they stress the hardware in completely different ways.
Prefill happens when your prompt first hits the model. All tokens arrive together. The GPU can process them in parallel using large matrix-matrix multiplications. Thousands of arithmetic units fire at once, and the workload stays dense. This phase is compute-bound. That sudden burst of speed you see at the start? That is the GPU doing exactly what it was built to do.
Decode is where things turn painful. When the model generates the next token, it does so one token at a time. This stage relies on matrix-vector operations, which use only a tiny fraction of the GPU's parallel capacity. Worse, every new token forces the GPU to reload the entire model weights from memory. The arithmetic units want work. Instead, they wait. Decode is memory-bound. The GPU is effectively acting as an expensive traffic controller, shuttling parameters back and forth across the memory bus while the math engines cool off. This is why a hundred-word response can take ten seconds even though the initial prompt analysis felt instant.
The KV cache makes this even more interesting. During decode, the model stores key and value tensors for every previous token so it does not recompute attention from scratch. That cache grows with sequence length. It also lives in memory. So now the GPU is not just reloading weights; it is reading and writing an ever-expanding cache on every single forward pass. The compute cores are barely breaking a sweat while the memory bus sweats for both of them.
Fighting the Memory Wall
Engineers have developed a small arsenal of techniques to reduce how much data must move, or at least to share the cost of moving it.
Batching is the most straightforward. If one user’s request forces a full weight load from memory, then processing eight or sixteen requests at once lets the GPU amortize that load across all of them. The weights are read once and reused for every sequence in the batch. In production, sophisticated scheduling systems group requests dynamically, sometimes called continuous or in-flight batching, so that the GPU rarely pauses. It is the difference between a bus and sixteen separate cars on the same route.
Quantization เข้าจัดการกับปัญหาแบนด์วิดท์โดยตรง โดยปกติแล้ว Model weights จะถูกเก็บในรูปแบบทศนิยม sixteen-bit floating-point การบีบอัดพวกมันลงเหลือเพียง eight-bit หรือแม้แต่ four-bit integers จะช่วยลดปริมาณข้อมูลที่ต้องเดินทางผ่านบัสลงได้ครึ่งหนึ่งหรือมากกว่านั้น แม้โมเดลยังคงต้องการความแม่นยำที่เพียงพอเพื่อสร้างผลลัพธ์ที่สอดคล้องกัน แต่เทคนิค post-training quantization สมัยใหม่สามารถลดขนาดหน่วยความจำ (memory footprint) ของโมเดลลงได้อย่างมหาศาลโดยไม่ทำลายคุณภาพ ข้อมูลที่เคลื่อนย้ายน้อยลงหมายถึงเวลาที่ต้องรอที่ memory controller ก็น้อยลงด้วย
FlashAttention ปรับโครงสร้างกลไก attention เพื่อเก็บผลลัพธ์ระหว่างทางไว้ภายในหน่วยความจำบนชิป (on-chip memory) ที่มีความเร็วสูงของ GPU กลไก attention แบบมาตรฐานต้องเขียน attention matrices ขนาดใหญ่ลงในหน่วยความจำภายนอกที่ช้า แล้วจึงอ่านพวกมันกลับมา FlashAttention จะแบ่งการคำนวณออกเป็น tiles ขนาดเล็กที่สามารถบรรจุใน SRAM ได้ โดยทำการคำนวณ softmax และการปรับสเกล (scaling) บนชิป และจะเขียนเฉพาะผลลัพธ์สุดท้ายกลับไปยังหน่วยความจำแบนด์วิดท์สูง (high-bandwidth memory) เท่านั้น มันเป็นการแลกการคำนวณที่เพิ่มขึ้นเล็กน้อยกับการลดจำนวนรอบการรับส่งข้อมูลกับหน่วยความจำหลัก ซึ่งมักจะเป็นการตัดสินใจที่คุ้มค่าเสมอ
PagedAttention แก้ปัญหาการสิ้นเปลืองหน่วยความจำในอีกรูปแบบหนึ่ง ในระหว่างขั้นตอนการ decode ค่า KV cache จะเติบโตขึ้นอย่างไม่คาดคิด ระบบแบบดั้งเดิมจะจัดสรรหน่วยความจำแบบเป็นบล็อกต่อเนื่อง (contiguous chunks) ที่มีขนาดตายตัวสำหรับแต่ละ sequence ซึ่งทำให้เกิดช่องว่างขนาดใหญ่เมื่อบาง sequence สิ้นสุดลงก่อนกำหนด ในขณะที่บาง sequence ขยายตัวออกไป PagedAttention นำแนวคิดเรื่องหน่วยความจำเสมือน (virtual memory) จากระบบปฏิบัติการมาใช้ โดยจะเก็บข้อมูล KV cache ในบล็อกขนาดคงที่ซึ่งสามารถจัดสรรแบบไม่ต่อเนื่องกันได้ และเชื่อมโยงผ่านตารางการอ้างอิงทางอ้อม (indirection table) วิธีนี้ช่วยป้องกันไม่ให้หน่วยความจำถูกปล่อยทิ้งไว้เฉยๆ ในบัฟเฟอร์ที่ถูกจองไว้แต่ใช้งานเพียงครึ่งเดียว และช่วยให้รองรับ batch sizes ที่ใหญ่ขึ้น ซึ่งจะช่วยเพิ่ม throughput โดยรวมด้วยการทำให้ memory bus ทำงานกับข้อมูลที่มีประโยชน์แทนที่จะเสียไปกับภาระงานจากการเกิด fragmentation overhead
เปลี่ยนมุมมองคำถาม
เมื่อเกิด latency spike ทีมงานจำนวนมากมักจะถามว่าควรเปลี่ยนไปใช้โมเดลที่เล็กลงหรือเขียน inference server ใหม่ดีหรือไม่ คำถามเหล่านั้นสำคัญก็จริง แต่เป็นเรื่องรอง คำถามแรกที่ควรตั้งคือเรื่องของตัวฮาร์ดแวร์เอง GPU ของคุณกำลังยุ่งอยู่กับการคำนวณจริงๆ หรือว่ามันกำลังขาดแคลนข้อมูล?
ลองดูตัวชี้วัดการใช้งาน (utilization metrics) ของคุณ วิเคราะห์การอิ่มตัวของแบนด์วิดท์หน่วยความจำ (memory bandwidth saturation) ควบคู่ไปกับอัตราการใช้งานหน่วยประมวลผล GPU (GPU compute occupancy) หากคุณพบว่ามีการแย่งชิงทรัพยากรหน่วยความจำสูง (high memory contention) และมีความเข้มข้นของการคำนวณทางคณิตศาสตร์ต่ำ (low arithmetic intensity) ในระหว่างการ decode นั่นหมายความว่าคุณไม่ได้มีปัญหาที่สถาปัตยกรรมโมเดล แต่คุณมีปัญหาทางฟิสิกส์ ทางออกจะไม่มาจากการเขียน Python ให้สะอาดขึ้น แต่มันจะมาจากการทำ batching ที่ดุดันขึ้น การทำ quantization ที่น้ำหนักเพื่อรีดข้อมูลผ่านท่อให้เร็วขึ้น การปรับโครงสร้าง attention ให้ทำงานบนชิป และการจัดการ KV cache เพื่อให้สามารถรองรับ batch ที่ใหญ่ขึ้นได้โดยที่หน่วยความจำไม่เต็ม
เมื่อคุณมองการทำ inference ผ่านเลนส์นี้ การทำ optimization จะกลายเป็นเรื่องเชิงกลไก คุณจะเลิกวิ่งไล่ตามตำนานที่ว่าความฉลาดของโมเดลทำให้ทุกอย่างช้าลง และเริ่มตัดสินใจทางวิศวกรรมโดยอิงจากสิ่งที่ฮาร์ดแวร์สามารถทำได้จริง นั่นคือจุดเปลี่ยนที่แยกความแตกต่างระหว่างระบบที่ใช้งานจริงแล้วขยายตัวได้ (scale) กับระบบที่แค่ทำงานได้เพียงอย่างเดียว
