နားလည်ထားရမယ့် အချက်
LLM တစ်ခု run တာဆိုတာ layer တစ်ခုစီအတွက်၊ generate လုပ်တဲ့ token တစ်ခုစီအတွက် matrix ကြီးတွေကို — model ရဲ့ weight တွေကို လက်ရှိ activation တွေနဲ့ — ထပ်ခါထပ်ခါ မြှောက်ခြင်းပါပဲ။ ဒါက GPU တွေအတွက် အတိအကျ ဖန်တီးထားတဲ့ workload အမျိုးအစားပါ။
Matrix multiply တစ်ခုက independent ဖြစ်တဲ့ multiply-add operation သန်းချီပြီး ကွဲထွက်သွားပြီး၊ GPU မှာ core သေးသေးလေးထောင်ချီရှိပြီး တစ်ပြိုင်နက်တည်း run နိုင်ပါတယ်၊ CPU ကတော့ sequential logic အတွက် optimize လုပ်ထားတဲ့ core အနည်းငယ်ပဲ ရှိပါတယ်။ ဒီ parallelism ကွာဟချက်ကြောင့် model တစ်ခုတည်းအတွက် GPU inference က ပိုပြီး သိသိသာသာ မြန်ပါတယ် — 'ထူးချွန်မှု' ဒါမှမဟုတ် precision ကွာခြားလို့ မဟုတ်ပါ၊ arithmetic ကတော့ ဘယ်လိုပဲဖြစ်ဖြစ် တူညီပါတယ်။
Model တစ်ခုလုံး VRAM ထဲ မဝင်တဲ့အခါ layer offloading က ခွဲပေးပါတယ် — transformer layer တချို့ GPU ပေါ်မှာ run ပြီး ကျန်တာတွေက CPU ပေါ်မှာ run ပါတယ်၊ activation တွေက PCIe bus ကို ဖြတ်ပြီး နှစ်ခုကြား ရွှေ့ပါတယ်။
ဒါက GPU memory ထက်ကြီးတဲ့ model တွေကို run စေနိုင်ပေမယ့်၊ CPU ပေါ်ကျန်ခဲ့တဲ့ layer တစ်ခုစီ (PCIe ဖြတ်တဲ့ trip တစ်ခုစီ) က latency ထပ်ဖြည့်ပေးပါတယ်၊ ဒါကြောင့် offloading ဟာ graceful-degradation tool တစ်ခုပါ၊ အခမဲ့ ရလာတာ မဟုတ်ပါ။
Phase နှစ်ခုက အလုပ်လုပ်ပုံ ကွဲပြားပြီး သီးခြားနာမည် ပေးထိုက်ပါတယ် — Prefill နှင့် Decode။
| Phase | ဘာလုပ်လဲ |
|---|---|
| Prefill | Input prompt တစ်ခုလုံးကို parallel run ပြီး output token ပထမဆုံးကို ထုတ်ပေးပါတယ် — ၎င်းရဲ့ speed က time-to-first-token (TTFT) အဖြစ် ပေါ်လာပါတယ်။ |
| Decode | Token တစ်ခုစီကို sequential run ပြီး generate လုပ်ပါတယ်၊ token အသစ်တစ်ခုစီက အရင် token ပေါ်မူတည်နေလို့ပါ — ၎င်းရဲ့ speed က tokens/sec အဖြစ် ပေါ်လာပါတယ်။ |
KV cache ရှိရတဲ့ အကြောင်းအရင်းက decode ကို မြန်စေဖို့ပါ — အရင် token တစ်ခုစီရဲ့ key/value vector တွေကို step တိုင်း ပြန်တွက်စရာမလိုအောင် သိမ်းထားပါတယ်။ ဒါပေမဲ့ ဒီ cache က context length တိုးလာတာနဲ့အမျှ linearly ကြီးလာပါတယ်၊ conversation ရှည်လာတာနဲ့အမျှ VRAM ပိုသုံးလာပြီး နောက်ဆုံးမှာ model weight တွေထက်တောင် memory bottleneck အမှန်ဖြစ်လာနိုင်ပါတယ်။
- KV Cache
- Generation တစ်ခုလုံးက token အရင်တွေရဲ့ key/value vector တွေကို သိမ်းထားတာပါ၊ decoding step တိုင်း ပြန်မတွက်ရအောင်ပါ — decode မြန်ရတဲ့ အဓိကအကြောင်းရင်းလည်းဖြစ်သလို၊ context ရှည်ရင် memory အများကြီးသုံးရတဲ့ အဓိကအကြောင်းရင်းလည်းဖြစ်ပါတယ်။
- TTFT (Time To First Token)
- Prompt ပို့ပြီးကတည်းက generate လုပ်ထားတဲ့ token ပထမဆုံးကို ပြန်ရအထိ ကြာချိန်ပါ — generation မစခင် prompt တစ်ခုလုံးကို process လုပ်ရတဲ့ prefill phase က အဓိက သက်ရောက်ပါတယ်။
- Tokens/sec
- Token ပထမဆုံး ထွက်ပြီးနောက် decode ကာလအတွင်း steady-state generation speed ပါ — model က token နောက်ထပ်တစ်ခုစီကို ဘယ်လောက်မြန်မြန် ထုတ်ပေးလဲဆိုတာကို တိုင်းတာပါတယ်။
MODEL SPLIT ACROSS GPU/CPU + GROWING KV CACHE
---------------------------------------------
MODEL SPLIT ACROSS GPU/CPU + GROWING KV CACHE
-----------------------------------------------
GPU (fast, limited VRAM) CPU (slower, more RAM)
+---------------------+ +---------------------+
| layer 1 ... layer 24| | layer 25 ... layer 32|
+---------------------+ +---------------------+
| |
+------ activations -------+
(crosses PCIe bus)
KV CACHE (grows as tokens are generated)
[t1][t2][t3][t4][t5] ... -> cache size keeps growing
more context tokens = more VRAM used by the cache aloneလက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်
llama.cpp, Ollama, LM Studio စတဲ့ tool တွေမှာ CPU/GPU tradeoff ကို control လုပ်တဲ့ setting နှစ်ခုက — GPU ကို layer ဘယ်နှစ်ခု offload လုပ်မလဲဆိုတာနဲ့ ခွင့်ပြုမယ့် context length ပါ။
VRAM ထဲ ဝင်သလောက် layer အများဆုံး offload လုပ်ပါ၊ KV cache နဲ့ activation တွေအတွက် နေရာလွတ်ချန်ထားပါ — VRAM သုံးစွဲမှုက weight ချည်းမဟုတ်ပါဘူး။ Out-of-memory error တွေ့ရင် context လျှော့မယ့်အစား offload layer အရေအတွက် လျှော့ပါ၊ context က prefill time နဲ့ KV cache size နှစ်ခုစလုံးကို တိုက်ရိုက်သက်ရောက်ပါတယ်။
Benchmark လုပ်တဲ့အခါ TTFT နဲ့ tokens/sec ကို သီးခြား number အနေနဲ့ ကြည့်ပါ။ Prompt ရှည်လာရင် TTFT မြင့်လာပါတယ် (prefill work ပိုများလို့) ဒါပေမဲ့ steady-state tokens/sec ကတော့ မပြောင်းချင်ပါဘူး။ Conversation ရှည်လာလို့ KV cache ကြီးလာရင် memory bandwidth က bottleneck ဖြစ်လာပြီး model ကိုယ်တိုင် မပြောင်းလဲသေးလည်း decode ကို နှေးစေနိုင်ပါတယ်။
Quantization (precision နိမ့်တဲ့ weight) က phase နှစ်ခုစလုံးကို မြန်စေပြီး memory လျှော့ချပေးပါတယ်၊ output quality အနည်းငယ် ဆုံးရှုံးမှုနဲ့ — layer count ဒါမှမဟုတ် context limit မထိခင် ပထမဆုံး ဆွဲသုံးသင့်တဲ့ lever ပါ။
ခန့်မှန်းချေသာ ဖြစ်သည်၊ တိုင်းတာချက် မဟုတ်ပါ
Code ဥပမာရဲ့ KV-cache number တွေက 7B-class layer/head shape ယေဘုယျတစ်ခုကို သုံးထားတဲ့ formula-based approximation ပါ၊ real measurement မဟုတ်ပါ — အမှန်တကယ် သုံးစွဲမှုက architecture အတိအကျပေါ်မူတည်ပါတယ် (ဥပမာ grouped-query attention က KV head အနည်းငယ်ပဲ သုံးလို့ ဒီ number တွေ သိသိသာသာ လျော့ကျစေပါတယ်)၊ ဒါကြောင့် output ကို order-of-magnitude guide အဖြစ်ပဲ သဘောထားပါ၊ spec အနေနဲ့ မဟုတ်ပါ။
အတူတူ စမ်းရေးကြည့်မယ်
def kv_cache_mb(num_layers, num_heads, head_dim, context_len, bytes_per_val=2):
# KV cache holds one Key vector and one Value vector per token per layer.
# bytes = layers * 2 (K and V) * heads * head_dim * context_len * bytes_per_val
total_bytes = num_layers * 2 * num_heads * head_dim * context_len * bytes_per_val
return total_bytes / (1024 * 1024)
# Rough shape resembling a 7B-class dense transformer (32 layers, 32 heads, 128 head_dim)
LAYERS, HEADS, HEAD_DIM = 32, 32, 128
for ctx in [512, 2048, 8192, 32768]:
mb = kv_cache_mb(LAYERS, HEADS, HEAD_DIM, ctx)
print(f"context={ctx:>6} tokens -> approx KV cache = {mb:.1f} MB (fp16)")32 layers, 32 heads, 128 head-dim ပုံစံ (ခန့်မှန်းခြေ 7B-class dense model တစ်ခုနှင့်တူသည်) ကို fp16 ဖြင့် run လိုက်ရာ ရလဒ်များမှာ -
context= 512 tokens -> approx KV cache = 256.0 MB (fp16)
context= 2048 tokens -> approx KV cache = 1024.0 MB (fp16)
context= 8192 tokens -> approx KV cache = 4096.0 MB (fp16)
context= 32768 tokens -> approx KV cache = 16384.0 MB (fp16)
ဤသည်မှာ formula-based approximation တစ်ခုသာဖြစ်ပြီး real model တစ်ခုမှ measurement မဟုတ်ပါ — အမှန်တကယ်ဂဏန်းများသည် architecture အတိအကျ (ဥပမာ grouped-query attention သည် query head များထက် KV head အများကြီးနည်းစွာ အသုံးပြုသဖြင့် ဤဂဏန်းများကို သိသိသာသာ လျော့ကျစေသည်) နှင့် inference engine ၏ memory layout ပေါ်တွင် မူတည်ပါသည်။၅ မိနစ် စမ်းကြည့်
kv_cache_mb function ကို num_heads နှင့် သီးခြားဖြစ်တဲ့ num_kv_heads parameter တစ်ခု လက်ခံအောင် ပြင်ဆင်ပါ၊ ဒါက grouped-query attention (query head အများကြီးအတွက် KV head အနည်းငယ်ကိုပဲ share လုပ်တဲ့ပုံစံ) ကို model လုပ်ဖို့ပါ။ context=8192 အတွက် num_kv_heads=32 အစား num_kv_heads=8 နဲ့ estimate ကို ပြန် run ပြီး original ရလဒ်နဲ့ နှိုင်းယှဉ်ကြည့်ပါ — ဒါက Llama 3 လို real model တွေမှာ GQA က ပေးတဲ့ memory saving ခန့်မှန်းချေနီးပါးပါ။
သတိလေးတစ်ချက်
Response ပထမဆုံးက နှေးတာကို model တစ်ခုလုံး နှေးတယ်လို့ ယူဆခြင်း — prompt ရှည်လို့ TTFT မြင့်တာကို ဆိုလိုတာဖြစ်နိုင်ပြီး decode စတာနဲ့ tokens/sec ကတော့ ကောင်းနေနိုင်ပါတယ်။
'အင်တော်ဆားပါဒ်' ဆိုပြီး context length ကို အများဆုံးထားခြင်း — မသုံးသေးလည်း ခွင့်ပြုထားတဲ့ context token တိုင်းနဲ့ KV cache က ကြီးလာပါတယ်၊ model weight အစား real memory bottleneck တိတ်တဆိတ် ဖြစ်လာနိုင်ပါတယ်။
Hugging Face — LLM Inference Optimization — Local AI / Local LLM