AI Fundamentals

LLM Inference Thực Sự Hoạt Động Như Thế Nào? Từ Prompt Đến Prefill, Decode Và KV Cache

Một request tới LLM không chỉ đơn giản là chạy model rồi trả về text. Bài viết đi từ tokenization và Transformer đến prefill, autoregressive decode, KV cache và những bottleneck quyết định hiệu năng inference thực tế.

Minh họa cho bài LLM Inference Thực Sự Hoạt Động Như Thế Nào? Từ Prompt Đến Prefill, Decode Và KV Cache
Mục lục bài viết

Hãy tưởng tượng hai request cùng được gửi tới một LLM.

Request đầu tiên chỉ có một câu hỏi ngắn:

“Giải thích TCP handshake.”

Prompt chỉ vài chục token, nhưng người dùng yêu cầu một bài giải thích dài 1.500 token.

Request thứ hai thì ngược lại. Người dùng gửi một tài liệu dài hàng chục nghìn token rồi chỉ hỏi:

“Điều khoản thanh toán nằm ở đâu?”

Câu trả lời có thể chỉ cần vài chục token.

Nhìn từ phía API, cả hai request đều giống nhau: gửi prompt vào model và nhận text đi ra. Nhưng nhìn từ phía GPU, chúng là hai workload rất khác. Request thứ hai có thể mất đáng kể thời gian trước khi token đầu tiên xuất hiện, vì toàn bộ prompt dài phải được xử lý trước. Request đầu tiên lại có thể bắt đầu trả lời nhanh nhưng tiếp tục giữ GPU bận trong thời gian dài vì model phải sinh hàng nghìn token nối tiếp nhau.

Sự khác biệt này dẫn tới một mental model rất quan trọng:

LLM inference không phải một phép tính duy nhất. Nó là một pipeline, và phần quan trọng nhất của pipeline lại tách thành hai pha có đặc tính phần cứng rất khác nhau: prefill và decode.

Nếu không hiểu điều này, nhiều quyết định tối ưu inference dễ trở thành tối ưu sai chỗ. Bạn có thể mua GPU mạnh hơn nhưng TTFT vẫn cao. Bạn có thể giảm FLOPs của attention nhưng throughput không tăng như mong đợi. Bạn có thể có đủ VRAM để chứa model nhưng server vẫn chỉ chịu được rất ít request đồng thời vì KV cache đã chiếm gần hết phần bộ nhớ còn lại.

Bởi vậy, để hiểu tại sao các hệ thống như vLLM cần PagedAttention, tại sao continuous batching quan trọng, tại sao quantization đôi khi tăng tốc rất nhiều nhưng đôi khi không, hay tại sao một số hệ thống tách riêng prefill và decode thành các worker khác nhau, chúng ta phải quay lại câu hỏi cơ bản hơn:

Thực sự chuyện gì xảy ra từ lúc một prompt đi vào LLM cho tới lúc từng token xuất hiện trên màn hình?


1. Inference là lúc model sử dụng những gì nó đã học

Một LLM trải qua nhiều giai đoạn trong vòng đời của nó, nhưng ba khái niệm thường bị trộn lẫn là training, fine-tuning và inference.

Trong training, model thực hiện forward pass để dự đoán, tính loss, chạy backward pass để lấy gradient rồi dùng optimizer cập nhật trọng số. Fine-tuning vẫn là training về mặt cơ chế: một phần hoặc toàn bộ tham số tiếp tục được cập nhật, chỉ khác ở dữ liệu, mục tiêu và thường là quy mô.

Inference thì khác.

Khi inference bắt đầu, các trọng số đã học của model được giữ cố định. Request mới đi qua mạng neural để tạo prediction, nhưng không có vòng backward pass nhằm học từ request đó. Research ban đầu cũng đặt inference ở đúng vị trí này: model đã được huấn luyện, trọng số được cố định và request được xử lý bằng forward computation.

Có thể hình dung rất đơn giản:

Giai đoạn Model đang làm gì? Có cập nhật trọng số? Mục tiêu chính
Training Học quy luật từ dữ liệu Có Học model
Fine-tuning Điều chỉnh model cho mục tiêu cụ thể Có Thay đổi hành vi/năng lực
Inference Dùng model để tạo prediction Không Phục vụ request

Điểm quan trọng là inference không có nghĩa model “lấy câu trả lời đã lưu sẵn ra khỏi database”. Các trọng số chứa những pattern model đã học, nhưng output cụ thể vẫn phải được tính toán dựa trên prompt hiện tại.

Với decoder-only Transformer — kiến trúc phổ biến cho generative LLM — nhiệm vụ cốt lõi tại mỗi bước có thể viết thành:

P(xt+1|x1,x2,…,xt)

Nghĩa là:

Với tất cả token đã thấy cho đến hiện tại, token tiếp theo có xác suất là gì?

Model chọn hoặc sample một token, thêm token đó vào sequence rồi thực hiện lại quá trình để sinh token tiếp theo.

Đây chính là autoregressive generation.

Transformer ban đầu được xây dựng quanh cơ chế attention thay cho recurrence, cho phép xử lý các vị trí của sequence song song trong những trường hợp toàn bộ sequence đã có sẵn. Nhưng khi sinh văn bản autoregressively, dependency “token sau phụ thuộc token trước” lại tạo ra một chuỗi decode tuần tự mà serving system phải xử lý.


2. Từ một chuỗi ký tự đến prediction của token tiếp theo

Giả sử application gửi:

Text
Why is the sky blue?

LLM không đưa trực tiếp chuỗi Unicode này vào Transformer.

Pipeline ở mức đơn giản hóa trông như sau:

Text
Raw text
   │
   ▼
Tokenizer
   │
   ▼
Token IDs
   │
   ▼
Embedding + Position Information
   │
   ▼
Transformer Blocks
   │
   ▼
Final Hidden State
   │
   ▼
LM Head
   │
   ▼
Logits over vocabulary
   │
   ▼
Sampling / Decoding
   │
   ▼
Next Token

Mỗi bước giải quyết một vấn đề khác nhau.

Tokenization: model trước tiên cần một alphabet riêng

Tokenizer biến text thành một sequence các token ID.

Ví dụ minh họa:

Text
"unhappiness"

→ ["un", "happi", "ness"]

→ [481, 19372, 2641]

Các ID ở đây chỉ mang tính minh họa; tokenizer thực tế của từng model sẽ tạo ra segmentation và ID khác nhau.

Nhiều tokenizer hiện đại sử dụng những biến thể của BPE hoặc byte-level BPE. Chẳng hạn, tiktoken của OpenAI là một BPE tokenizer có thể xử lý arbitrary text và ánh xạ text sang một sequence token có thể đảo ngược lại về dữ liệu ban đầu.

Điều này có một implication trực tiếp đối với inference:

Chi phí không đi theo số từ hay số ký tự mà application nhìn thấy. Nó đi theo token sequence mà model thực sự xử lý.

Hai đoạn văn có cùng số ký tự không nhất thiết tạo ra cùng số token. Code, JSON, một số ngôn ngữ hoặc những pattern ký tự bất thường có thể tokenize rất khác nhau.

Vì vậy khi thiết kế AI system, developer nên suy nghĩ bằng:

Text
input tokens
output tokens
context length

thay vì chỉ bằng:

Text
characters
words
pages

Embedding: token ID chưa phải là thứ Transformer có thể suy luận trên

Token ID 481 không mang một ý nghĩa toán học hữu ích nếu đứng riêng.

Model có một embedding matrix:

E∈ℝV×d

Trong đó:

  • (V) là vocabulary size;
  • (d) là hidden dimension.

Token ID đóng vai trò như index để lấy một vector:

xi=E[tokeni]

Nếu vocabulary có 100.000 token và hidden dimension là 4.096, embedding table về mặt khái niệm có shape:

Text
[100000, 4096]

Sau lookup, một sequence gồm 2.000 token trở thành một tensor đại diện cho 2.000 vector trong không gian hidden của model.

Nhưng ở đây xuất hiện một vấn đề khác.

Self-attention tự thân không biết rằng token ở vị trí 12 đứng trước token ở vị trí 13. Model vì vậy cần một cơ chế đưa position information vào phép tính.

Transformer nguyên bản sử dụng positional encoding. Nhiều họ LLM sau này sử dụng Rotary Position Embedding — RoPE — trong đó position được đưa vào query và key thông qua các phép rotation. RoPE cho phép attention score chứa thông tin về quan hệ vị trí tương đối giữa token.

🌱 Cách hiểu đơn giản

Embedding nói cho model biết:

“Token này đại diện cho loại thông tin gì?”

Position information bổ sung:

“Token này đang đứng ở đâu so với những token khác?”

Model cần cả hai.


3. Bên trong Transformer: attention không phải “tìm kiếm từ giống nhau”

Sau embedding, representation đi qua nhiều Transformer block.

Một block decoder điển hình có hai thành phần lớn:

Text
Self-Attention
      │
      ▼
Feed-Forward / MLP

cùng với residual connections, normalization và một số biến thể kiến trúc khác tùy model.

Ở mức bài này, phần quan trọng nhất cần hiểu là self-attention.

Query, Key và Value

Từ hidden state (X), model tạo ra ba representation:

Q=XWQ
K=XWK
V=XWV

Sau đó scaled dot-product attention có dạng:

Attention(Q,K,V)=softmax(QKTdk)V

Đây là công thức trung tâm được giới thiệu trong Transformer.

Nhưng công thức dễ khiến người mới nhìn attention như một phép toán bí hiểm. Mental model hữu ích hơn là:

Text
Query
= token hiện tại đang cần loại thông tin gì?

Key
= mỗi token khác đang cung cấp loại thông tin nào?

Q · K
= mức độ phù hợp giữa nhu cầu hiện tại và thông tin đó

Value
= thông tin thực sự được lấy vào representation

Ví dụ:

Text
The server rejected the request because it was invalid.

Để representation của "it" hữu ích, model cần khai thác context xung quanh. Một hoặc nhiều attention head có thể đặt trọng số cao vào những token liên quan.

Điều cần tránh là diễn giải quá literal rằng “head số 7 luôn xử lý đại từ” hay “mỗi attention head có đúng một nhiệm vụ ngữ pháp”. Một số head có thể hình thành những pattern dễ diễn giải, nhưng internal representation của LLM là distributed và phức tạp hơn việc gán mỗi head một chức năng cố định.

Causal attention: model không được nhìn vào tương lai

Generative decoder-only LLM sử dụng causal masking.

Khi tính representation ở vị trí (t), model chỉ được attend tới:

Text
1 ... t

chứ không được nhìn:

Text
t+1, t+2, ...

Nếu không có constraint này trong quá trình language modeling, model có thể “nhìn đáp án” là token tương lai thay vì học next-token prediction.

Causal structure này cũng là nền tảng của decode: khi đang tạo token mới, toàn bộ context hợp lệ chính là những token đã xuất hiện trước nó.


Feed-forward network: attention không phải toàn bộ Transformer

Sau attention, representation của mỗi token tiếp tục đi qua một MLP/FFN.

Khác với attention — nơi information được trộn giữa các token — FFN chủ yếu thực hiện transformation trên representation của từng position.

Một dạng khái niệm đơn giản là:

FFN(x)=W2σ(W1x)

Các model hiện đại có thể dùng những biến thể như gated MLP và SwiGLU, nhưng architectural idea vẫn giống nhau: projection lên không gian lớn hơn, áp dụng nonlinear transformation rồi project trở lại hidden dimension.

Sau nhiều Transformer blocks, hidden state cuối cùng được đưa qua language-model head để tạo một score cho từng token trong vocabulary.

Những score chưa chuẩn hóa này gọi là logits.


4. Logits không phải câu trả lời: model phải chọn một token

Giả sử vocabulary chỉ có năm token và model tạo:

Text
blue    8.4
red     5.1
green   3.7
fast    0.6
table  -1.3

Đây là logits, chưa phải probability.

Softmax biến chúng thành một distribution:

Text
blue   → 0.94
red    → 0.035
green  → 0.020
...

Sau đó decoding policy quyết định token nào thực sự được chọn.

Một số cơ chế quen thuộc gồm:

  • greedy decoding;
  • temperature;
  • top-k;
  • top-p/nucleus sampling.

Temperature thường được áp dụng bằng cách scale logits:

pi=softmax(ziT)

Temperature thấp làm distribution sắc hơn. Temperature cao làm nhiều candidate có cơ hội được sample hơn.

Greedy decoding đơn giản chọn candidate có probability cao nhất ở mỗi bước. Nó deterministic trong những điều kiện nhất định, nhưng không có nghĩa nó luôn là decoding strategy tốt nhất cho code, toán hoặc reasoning. Local choice tốt nhất ở mỗi token không đảm bảo sequence cuối cùng tốt nhất.

Sau khi token thắng cuộc được chọn:

Text
token_t

nó được append vào sequence:

Text
[prompt tokens]
+
[generated token 1]
+
...
+
[generated token t]

rồi model phải tạo:

Text
token_(t+1)

Đây là vòng lặp khiến generative inference khác đáng kể với nhiều inference workload truyền thống.

Text
Context
   │
   ▼
Predict next token
   │
   ▼
Select token
   │
   ▼
Append to context
   │
   └─────────────┐
                 │
                 ▼
          Predict again

Nếu model tạo 1.000 output tokens, về logic nó phải thực hiện khoảng 1.000 decode iterations nối tiếp nhau.

Các iteration có thể được tối ưu mạnh ở cấp GPU và serving engine, nhưng dependency giữa token (t) và token (t+1) không đơn giản biến mất.

Đây chính là lý do speculative decoding đáng quan tâm: nó tìm cách tận dụng một draft model rẻ hơn để đề xuất nhiều token rồi để target model verify chúng song song, nhưng vẫn bảo toàn distribution của target model khi thuật toán được triển khai đúng.


5. Insight quan trọng nhất: inference tách thành Prefill và Decode

Cho đến đây, ta đã mô tả inference như một vòng next-token prediction.

Nhưng nếu nhìn ở cấp GPU, request không chạy theo cùng một kiểu từ đầu đến cuối.

Nó chia thành:

Text
                     LLM INFERENCE

Prompt tokens
     │
     ▼
┌──────────────────────────────┐
│           PREFILL            │
│                              │
│ Process prompt tokens        │
│ in parallel                  │
│                              │
│ Build initial KV cache       │
└──────────────┬───────────────┘
               │
          First token
               │
               ▼
┌──────────────────────────────┐
│            DECODE            │
│                              │
│ Generate one new token       │
│ per autoregressive iteration │
│                              │
│ Reuse + extend KV cache      │
└──────────────┬───────────────┘
               │
               ▼
          Stream output

Hai pha dùng cùng model, cùng weights và cùng Transformer layers, nhưng shape của workload khác đáng kể.

Các nghiên cứu về serving LLM cũng phân biệt rõ hai mục tiêu latency: Time to First Token (TTFT) gắn chặt với prompt processing/prefill, còn Time Per Output Token hoặc inter-token latency mô tả trải nghiệm trong decode.

Prefill: model xử lý prompt

Giả sử prompt có 4.000 token.

Ở prefill, cả sequence đã có sẵn.

Model có thể xử lý representation của nhiều token song song bằng các matrix operations lớn:

Text
[token 1]
[token 2]
[token 3]
...
[token 4000]

        ↓ parallel computation

Transformer layers

Vì workload có arithmetic intensity cao và tận dụng tốt matrix multiplication, prefill thường thiên về compute-bound hơn decode. Điều này có nghĩa tốc độ tính toán của accelerator trở thành yếu tố rất quan trọng. Các nghiên cứu characterization của LLM serving quan sát prefill sử dụng compute resources cao hơn, trong khi decode thể hiện pressure lớn hơn lên memory subsystem.

Với standard dense self-attention, attention matrix của một sequence dài (N) chứa quan hệ giữa các query và key positions, nên lượng attention computation tăng theo (O(N^2)).

Điều này rất quan trọng với long context.

Nếu tăng context từ:

Text
8K → 16K

thì phần dense attention computation không chỉ đơn giản tăng 2×; về asymptotic analysis, thành phần quadratic có thể tăng gần 4×.

Nhưng không phải toàn bộ Transformer đều có complexity (O(N^2)). MLP và một số projection scale khác với attention, vì vậy latency end-to-end không nhất thiết tăng đúng 4×.

FlashAttention giải quyết điều gì?

Một misconception thường gặp là:

FlashAttention biến attention từ (O(N^2)) thành linear attention.

Không phải.

FlashAttention là exact attention. Nó không thay đổi mathematical attention thành một approximation tuyến tính.

Thay vào đó, nó nhìn vấn đề ở góc độ hardware IO.

GPU có nhiều tầng memory. HBM có capacity lớn nhưng việc di chuyển dữ liệu tới/ra khỏi on-chip SRAM tốn thời gian. Naive attention có thể tạo và đọc/ghi nhiều intermediate data xuống HBM.

FlashAttention dùng tiling để xử lý các block Q/K/V theo cách giảm số lần đọc và ghi giữa HBM và SRAM. Paper gốc mô tả chính xác mục tiêu của thuật toán là làm attention IO-aware, không phải đơn giản giảm số phép nhân từ quadratic xuống linear.

🔬 Deep Dive — FLOPs không phải toàn bộ câu chuyện

Hai implementation có thể thực hiện cùng mathematical operation và gần tương đương về số FLOPs, nhưng runtime khác đáng kể.

Lý do là GPU không chỉ “tính”.

GPU phải:

Text
load data
↓
execute kernels
↓
write intermediate results
↓
load them again
↓
continue compute

Nếu phần lớn thời gian bị tiêu tốn cho data movement, giảm memory IO có thể mang lại speedup dù complexity toán học không đổi.

Đây là một principle sẽ xuất hiện lần nữa ở decode.


Decode: một workload rất khác

Sau prefill, model bắt đầu tạo output.

Giả sử đã có:

Text
Prompt: 4000 tokens
Output so far: 137 tokens

Để sinh token thứ 138, model chỉ có một query position mới cần xử lý.

Đây không còn là một matrix-matrix workload lớn giống prefill.

Ở batch nhỏ, lượng computation trên mỗi lần load model weights tương đối thấp. GPU phải đọc rất nhiều weights và KV-cache data để sản xuất một lượng output computation khá nhỏ.

Vì thế decode thường được mô tả là memory-bound, và đặc biệt trong nhiều batch nhỏ, memory-bandwidth pressure trở thành bottleneck quan trọng. Benchmark và system research hiện đại tiếp tục quan sát sự khác biệt này giữa prefill và decode.

Tuy nhiên “decode = bandwidth và không gì khác” cũng quá đơn giản.

Kernel launch overhead, batch size, context length, architecture, quantization kernel và phần cứng đều ảnh hưởng. Một nghiên cứu năm 2026 về batch-1 decode thậm chí cho thấy workload có thể memory-dominated nhưng không đạt gần peak HBM bandwidth trên GPU rất nhanh, vì các overhead khác bắt đầu nổi lên.

Mental model tốt hơn là:

Prefill thường có nhiều computation có thể song song hóa trên prompt; decode phải chạy iteration-by-iteration và thường có arithmetic intensity thấp hơn, khiến memory movement trở thành yếu tố cực kỳ quan trọng.

Prefill Decode
Input mỗi phase Toàn bộ prompt Token mới nhất
Parallelism theo sequence Cao Bị giới hạn bởi autoregressive dependency
Workload điển hình Matrix-heavy Memory-heavy ở nhiều serving regime
KV cache Tạo cache Đọc và mở rộng cache
UX metric nổi bật TTFT TPOT / ITL / tokens/s
Tối ưu thường gặp FlashAttention, chunked prefill batching, quantization, KV management, speculative decoding

Đây là điểm nối giữa machine learning và systems engineering.


6. KV Cache: lý do model không tính lại toàn bộ quá khứ ở mỗi token

Giả sử prompt và output hiện tại gồm:

Text
A B C D

Model muốn sinh token:

Text
E

Trong self-attention, token mới cần attend tới những token trước.

Một implementation cực kỳ ngây thơ có thể chạy lại toàn bộ sequence:

Text
A B C D

Sau khi sinh E, lại chạy:

Text
A B C D E

Sau đó:

Text
A B C D E F

Rất nhiều Key và Value representation của các token cũ sẽ bị tính lại dù chúng không thay đổi.

KV cache loại bỏ phần lặp thừa đó.

Trong mỗi attention layer, model lưu Key và Value của các token đã xử lý:

Text
Past tokens
    │
    ▼
┌──────────────────────┐
│       KV CACHE       │
│                      │
│ K1 V1                │
│ K2 V2                │
│ K3 V3                │
│ ...                  │
│ Kt Vt                │
└──────────────────────┘
          ▲
          │
      new K, V

Khi token mới xuất hiện, model chỉ cần tạo Q/K/V mới tương ứng với position đó rồi attention query mới có thể đọc các K/V đã cache.

Pseudo-code đơn giản:

Python
kv_cache = prefill(prompt_tokens)

token = first_output_token

while token != EOS:
    q, new_k, new_v = transformer_step(token)

    kv_cache.append(new_k, new_v)

    hidden = attention(
        query=q,
        keys=kv_cache.keys,
        values=kv_cache.values,
    )

    token = sample(lm_head(hidden))

Code thật phức tạp hơn rất nhiều, nhưng mental model này giải thích tại sao KV cache tồn tại.

KV cache đổi compute lấy memory

KV caching giảm lượng recomputation, nhưng cái giá là memory.

Một approximation hữu ích cho dung lượng KV cache của một request là:

KV bytes≈2×L×S×Hkv×Dh×B

Trong đó:

  • (2): Key + Value;
  • (L): số Transformer layers;
  • (S): số token đã cache;
  • (H_{kv}): số KV heads;
  • (D_h): head dimension;
  • (B): bytes mỗi phần tử.

Ví dụ giả định:

Text
Layers        = 32
KV heads      = 8
Head dim      = 128
Sequence      = 8192 tokens
KV dtype      = BF16 = 2 bytes

Ta có xấp xỉ:

2×32×8192×8×128×2≈2 GiB

cho một sequence.

Con số này chỉ là ví dụ architectural, nhưng nó cho thấy vì sao serving system phải coi KV cache như một first-class resource.

Nếu model dùng Multi-Query Attention hoặc Grouped-Query Attention, số KV heads có thể nhỏ hơn số query heads, từ đó giảm đáng kể KV-cache footprint.

💡 Mental model

Model weights giống một bộ dữ liệu lớn gần như cố định mà tất cả request phải dùng.

KV cache giống working memory riêng của từng request.

Model có thể vừa VRAM nhưng server vẫn OOM khi concurrency tăng, vì:

Text
weights
+
KV cache request #1
+
KV cache request #2
+
...
+
KV cache request #N
+
activations / runtime workspace

mới là memory footprint thực tế.


7. Tại sao serving engine cần nhiều thứ hơn model.generate()

Một model chạy được trên một GPU không đồng nghĩa với việc nó trở thành một production inference service tốt.

Production server phải đồng thời giải quyết:

Text
request scheduling
batch formation
KV-cache allocation
memory fragmentation
streaming
cancellation
variable sequence length
latency SLO
throughput

Từ đây, nhiều kỹ thuật inference hiện đại trở nên dễ hiểu hơn vì mỗi kỹ thuật đang tấn công một bottleneck cụ thể.


Continuous batching: batch ở cấp iteration, không phải cả request

Static batching có một vấn đề rõ ràng.

Giả sử ba request cùng bắt đầu:

Text
A → cần 20 output tokens
B → cần 200 output tokens
C → cần 2000 output tokens

Nếu batch được coi như một unit cố định cho đến khi tất cả request hoàn thành, resource dành cho A có thể bị giữ vô ích sau khi A kết thúc.

Các serving system như Orca chuyển scheduling xuống cấp iteration.

Sau mỗi decode iteration:

Text
Iteration 101:
[A] [B] [C]

A finishes

Iteration 102:
[D] [B] [C]

Request mới có thể được đưa vào slot vừa trống mà không phải chờ toàn bộ batch hoàn thành.

Orca gọi hướng tiếp cận này là iteration-level scheduling; ý tưởng sau đó trở thành nền tảng của continuous/in-flight batching trong nhiều inference engine.

Điểm quan trọng không đơn giản là:

“Batching làm GPU nhanh hơn.”

Mà là:

Batching giúp amortize việc đọc weights và tận dụng parallelism của accelerator trên nhiều request. Continuous batching cố giữ lợi ích này dù request có độ dài khác nhau và kết thúc ở những thời điểm khác nhau.

Trade-off là scheduler trở nên phức tạp hơn. Batch lớn hơn có thể tăng throughput nhưng cạnh tranh memory, ảnh hưởng latency, hoặc khiến prefill workload mới làm gián đoạn decode đang chạy.

Đây là lý do production serving thường phải tối ưu goodput dưới latency SLO, chứ không chỉ tối đa một con số tokens/s.


PagedAttention: áp dụng tư duy virtual memory vào KV cache

KV cache có một đặc tính khó chịu:

Serving engine không biết trước mỗi request sẽ dài bao nhiêu.

Một request có thể kết thúc sau 40 output tokens.

Request khác có thể chạy thêm 4.000 token.

Nếu runtime phải reserve một vùng VRAM contiguous rất lớn cho từng request ngay từ đầu, memory dễ bị:

  • over-reserved;
  • fragmented;
  • khó chia sẻ;
  • khó sử dụng hiệu quả cho dynamic batching.

PagedAttention của vLLM lấy cảm hứng trực tiếp từ paging của virtual memory.

Thay vì coi KV cache của request là một vùng contiguous khổng lồ:

Text
Request A

[============================]

nó chia cache thành những block:

Text
Logical KV:

[A1][A2][A3][A4]

Physical VRAM:

[B7][A1][C2][A4][A2][D8][A3]

Một mapping layer biết logical block nào nằm ở physical block nào.

Nhờ vậy physical memory không cần contiguous, và block có thể được cấp phát khi sequence phát triển.

Paper PagedAttention báo cáo near-zero waste trong KV-cache management và trong workload đánh giá của tác giả, vLLM đạt throughput cao hơn đáng kể so với các serving system baseline cùng thời điểm.

🏗️ Production Note

PagedAttention không làm Transformer “thông minh hơn”.

Nó cũng không thay đổi model weights.

Nó là một memory-management optimization.

Đây là ví dụ điển hình cho việc một breakthrough trong AI serving có thể xuất phát từ một idea rất quen thuộc của operating systems hơn là từ một neural-network architecture mới.


Prefix caching: đừng prefill lại thứ đã giống hệt nhau

Giả sử 10.000 request đều bắt đầu bằng cùng system prompt dài 8.000 token:

Text
SYSTEM:
You are an enterprise assistant...
[8000 tokens of instructions]

USER:
...

Nếu mỗi request thực hiện prefill lại toàn bộ prefix giống nhau, hệ thống đang lặp lại một lượng computation đáng kể.

Automatic Prefix Caching của vLLM cho phép cache các KV blocks từ prefix đã xử lý và reuse chúng khi request khác chia sẻ cùng prefix.

Kỹ thuật này đặc biệt hữu ích với:

  • long system prompts;
  • document prefixes được query nhiều lần;
  • multi-turn conversations có prefix lớn;
  • agent workloads có instruction block lặp lại.

Nhưng prefix caching không phải magic cache cho mọi thứ.

Hai prompt chỉ gần giống nhau về semantic nhưng khác token sequence thì không tự động dùng chung exact KV prefix.

Và nó chủ yếu loại bỏ repeated prefill work. Nó không làm phần generation dài sau đó biến mất.


Quantization: giảm lượng data phải di chuyển

Nếu decode thường phải vận chuyển lượng lớn model weights qua memory hierarchy, một câu hỏi tự nhiên xuất hiện:

Nếu weights nhỏ hơn thì sao?

Đó là một trong những động lực lớn của quantization.

Một 7B model nếu weights ở BF16 cần về mặt lý thuyết:

7×109×2≈14GB

chỉ cho weights.

Nếu dùng 4-bit representation, theoretical raw weight payload giảm xuống khoảng:

7×109×0.5≈3.5GB

Nhưng production footprint thực tế còn có scales, metadata, KV cache, runtime buffers và những thành phần khác.

Quan trọng hơn:

Weight size giảm 4× không đảm bảo latency giảm 4×.

Speedup phụ thuộc:

  • GPU;
  • kernel implementation;
  • batch size;
  • context length;
  • quantization format;
  • dequantization overhead;
  • phần nào của workload đang bottleneck.

AWQ là một ví dụ về weight-only low-bit quantization được thiết kế để bảo vệ các weight channels quan trọng dựa trên activation statistics và đi kèm hardware-aware inference implementation.

Vì thế cách đánh giá đúng không phải:

Text
INT4 = chắc chắn nhanh hơn BF16 4×

mà là:

Text
quality
+
memory footprint
+
measured latency
+
throughput
+
hardware/kernel compatibility

Speculative decoding: giảm số lần target model phải đi từng bước nhỏ

Autoregressive dependency tạo ra một constraint:

Text
target model:
token 1
  ↓
token 2
  ↓
token 3
  ↓
token 4

Speculative decoding dùng một model hoặc mechanism rẻ hơn để draft nhiều candidate:

Text
Draft model:

A → B → C → D

Target model sau đó verify nhiều draft token trong một pass có nhiều parallelism hơn.

Nếu candidate được chấp nhận, ta có thể tiến nhiều token trong một target-model step thay vì chỉ một token.

Điểm đặc biệt của speculative decoding chuẩn là mục tiêu không phải “dùng model nhỏ và chấp nhận output kém hơn”. Với verification/rejection sampling phù hợp, thuật toán có thể giữ nguyên output distribution của target model. Paper gốc báo cáo 2–3× acceleration trong các thí nghiệm của họ, nhưng mức tăng thực tế phụ thuộc draft acceptance rate và serving setup.

Nó hiệu quả nhất khi draft model thường đoán gần với target.

Nếu target liên tục reject draft:

Text
draft work
+
verification work
+
ít token được accept

thì lợi ích giảm mạnh.


8. Một production inference architecture trông như thế nào?

Đến đây chúng ta có thể ráp các mảnh lại.

Một serving stack đơn giản hơn nhiều hệ thống thực tế nhưng đủ để hình dung có thể là:

Text
                      ┌───────────────┐
User / Application ──▶│  API Gateway  │
                      └───────┬───────┘
                              │
                              ▼
                      ┌───────────────┐
                      │   Tokenizer   │
                      └───────┬───────┘
                              │
                              ▼
                   ┌─────────────────────┐
                   │ Request Scheduler   │
                   │ continuous batching │
                   └──────────┬──────────┘
                              │
                 ┌────────────┴────────────┐
                 │                         │
                 ▼                         ▼
        ┌────────────────┐        ┌────────────────┐
        │    Prefill     │        │     Decode     │
        │                │        │                │
        │ build KV cache │───────▶│ reuse KV cache │
        └────────────────┘        └───────┬────────┘
                                         │
                                         ▼
                                ┌─────────────────┐
                                │ Token Streaming │
                                └────────┬────────┘
                                         │
                                         ▼
                                       User

Trong một deployment nhỏ, prefill và decode có thể chạy trên cùng GPU và cùng worker.

Điều này đơn giản, tránh việc phải chuyển KV cache qua network và có thể sử dụng hardware hiệu quả ở nhiều workload.

Nhưng ở quy mô lớn, hai pha có resource profile khác nhau đến mức việc đặt chúng trên cùng worker có thể gây interference. Một long prefill request có thể chiếm GPU và làm chậm decode iterations của những user đang streaming output.

DistServe nghiên cứu hướng prefill-decode disaggregation: chạy hai phase trên những GPU khác nhau để có thể scale và optimize chúng độc lập theo TTFT và TPOT requirements.

Ví dụ:

Text
Requests
   │
   ▼
┌────────────────────┐
│   Prefill Pool     │
│ compute-oriented   │
└─────────┬──────────┘
          │
       KV cache
          │
          ▼
┌────────────────────┐
│    Decode Pool     │
│ latency-oriented   │
└─────────┬──────────┘
          │
          ▼
        Stream

Nhưng disaggregation không miễn phí.

KV cache phải được chuyển giữa workers, model replicas có thể làm tăng resource consumption và scheduler phải hiểu network topology.

Các nghiên cứu sau đó cũng cho thấy không tồn tại một kết luận đơn giản rằng “disaggregated luôn tốt hơn aggregated”. Hiệu quả phụ thuộc TTFT/TPOT SLO, hardware và workload; các kiến trúc hybrid cũng đang được nghiên cứu.

Đây là nuance quan trọng đối với senior engineer:

Production inference architecture nên bắt đầu từ workload và SLO, không phải từ danh sách optimization đang phổ biến.


9. Năm hiểu nhầm khiến developer tối ưu inference sai hướng

“Model vừa VRAM nghĩa là server đủ memory”

Không.

Model weights chỉ là một phần.

Production memory còn bao gồm:

Text
weights
+
KV cache
+
activations
+
CUDA/runtime workspace
+
temporary buffers

Với concurrency cao hoặc context dài, KV cache có thể trở thành thành phần quyết định capacity. Đây chính là vấn đề memory-management mà PagedAttention được thiết kế để giải quyết.


“Prompt và output token đều tốn chi phí giống nhau”

Không hẳn từ góc độ hardware.

Chúng đều đi qua model, nhưng prompt token chủ yếu được xử lý trong prefill, nơi có parallelism lớn hơn; output token được tạo autoregressively trong decode.

Vì vậy:

Text
100K input + 20 output

và:

Text
20 input + 100K output

là hai workload cực kỳ khác, dù tổng token giống nhau.

Prefill/decode serving research tồn tại chính vì hai phase có resource characteristics và latency objectives khác nhau.


“KV cache là cache câu trả lời”

Không.

KV cache không lưu final answer hay semantic answer để request khác tự động lấy lại.

Nó lưu Key/Value states cần thiết để attention có thể reuse computation của prefix đã xử lý.

Đây là một optimization bên trong inference execution.


“FlashAttention giải quyết quadratic attention”

Nó giải quyết một phần rất quan trọng của IO inefficiency và memory footprint, nhưng standard FlashAttention vẫn thực hiện exact attention.

Asymptotic computation của dense attention không tự nhiên biến thành (O(N)).

Điểm mạnh của FlashAttention là tổ chức computation theo hardware memory hierarchy hiệu quả hơn thông qua tiling và giảm HBM traffic.


“Tokens per second càng cao thì serving càng tốt”

Không nhất thiết.

Giả sử hệ thống A:

Text
TTFT = 8 seconds
generation = 100 tokens/s

và hệ thống B:

Text
TTFT = 500 ms
generation = 60 tokens/s

Với interactive chatbot, user có thể thích B hơn.

Ngược lại, offline batch processing có thể ưu tiên aggregate throughput.

Production serving thường quan tâm đồng thời:

Text
TTFT
TPOT / ITL
throughput
goodput
concurrency
GPU utilization
memory usage
cost / token

DistServe và nhiều serving research hiện đại tối ưu system dựa trên latency constraints cho cả prefill lẫn decode chứ không chỉ một throughput number duy nhất.


10. Mental model để nhìn bất kỳ inference optimization nào

Sau khi hiểu pipeline, ta có thể phân loại hầu hết optimization bằng một câu hỏi đơn giản:

Nó đang loại bỏ bottleneck nào?

Technique Bottleneck chính nó nhắm tới Mental model
FlashAttention Attention memory IO Di chuyển intermediate data ít hơn
Quantization Weight memory/capacity/traffic Mỗi parameter nhỏ hơn
Continuous batching GPU utilization Nhiều request chia sẻ một iteration
PagedAttention KV fragmentation/allocation KV cache được quản lý theo block
Prefix caching Repeated prefill Reuse KV của prefix giống nhau
Speculative decoding Serial decode latency Draft nhiều token, target verify
Prefill/decode disaggregation Phase interference Scale hai workload riêng

Bảng này quan trọng hơn việc thuộc tên từng framework.

Framework sẽ thay đổi.

GPU sẽ thay đổi.

Kernel sẽ thay đổi.

Nhưng những câu hỏi nền tảng vẫn còn:

Text
Compute đang xảy ra ở đâu?

Data phải di chuyển bao nhiêu?

Phần memory nào tăng theo context?

Phần nào tăng theo concurrency?

Request đang mắc ở prefill hay decode?

Mục tiêu là latency hay throughput?

Optimization đang giảm compute,
memory traffic,
memory capacity,
hay scheduling waste?

Khi trả lời được những câu hỏi đó, các công nghệ inference bớt giống một danh sách “mẹo tối ưu GPU” và bắt đầu trở thành một hệ thống có logic.


Kết luận

Quay lại hai request ban đầu.

Một prompt khổng lồ nhưng câu trả lời ngắn có thể chịu phần lớn latency ở prefill: tokenizer biến text thành token, Transformer xử lý prompt, attention xây context và KV cache được hình thành trước khi token đầu tiên có thể xuất hiện.

Một prompt ngắn nhưng câu trả lời dài nhanh chóng chuyển trọng tâm sang decode: model phải sinh token autoregressively, liên tục đọc weights và KV cache rồi mở rộng cache thêm từng bước.

Bây giờ chúng ta cũng thấy vì sao những optimization tưởng như không liên quan thực ra nằm trên cùng một bản đồ.

FlashAttention tổ chức attention IO tốt hơn.

Continuous batching làm GPU phục vụ nhiều sequence hiệu quả hơn.

PagedAttention quản trị KV cache giống một hệ thống virtual memory.

Prefix caching tránh lặp lại prefill của cùng một prefix.

Quantization giảm memory footprint và lượng data của weights.

Speculative decoding tìm cách tiến nhiều token hơn qua bottleneck autoregressive.

Và ở quy mô lớn, thậm chí prefill và decode có thể được tách thành những resource pools riêng vì hai phase có resource profile khác nhau.

Mental model quan trọng nhất vì thế không phải:

“Inference là lúc model trả lời.”

Mà là:

Inference là một pipeline thực thi Transformer trong đó prompt được xử lý thành state ở prefill, state đó được giữ lại trong KV cache, rồi decode sử dụng state ấy để tạo từng token tiếp theo trong một vòng autoregressive. Performance của hệ thống cuối cùng phụ thuộc không chỉ vào model, mà vào cách compute, memory và scheduling được tổ chức quanh hai pha này.

Một khi nhìn inference theo cách đó, việc tối ưu LLM serving không còn bắt đầu bằng câu hỏi:

“Framework nào nhanh nhất?”

Nó bắt đầu bằng câu hỏi tốt hơn:

“Workload của tôi đang bị giới hạn ở đâu?”

Nguồn tham khảo

  • Attention Is All You Need — Vaswani et al., 2017.
  • RoFormer: Enhanced Transformer with Rotary Position Embedding — Su et al.
  • OpenAI tiktoken — byte-pair encoding tokenizer implementation.
  • Orca: A Distributed Serving System for Transformer-Based Generative Models — OSDI 2022.
  • Efficient Memory Management for Large Language Model Serving with PagedAttention — Kwon et al., 2023.
  • FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness — Dao et al., 2022.
  • Fast Inference from Transformers via Speculative Decoding — Leviathan et al.
  • AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration — Lin et al.
  • DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving — Zhong et al.
Chia sẻXLinkedIn