ข้ามไปยังเนื้อหา
KoishiAI
EN
← สารบัญ

บทที่ 5 จาก 9 · AI 101 — คู่มือเริ่มต้นสำหรับคนที่จะเอา AI ไปใช้งานจริง

ดูยังไงว่า AI กินทรัพยากรเท่าไร

สูตรคำนวณ VRAM และ KV cache ที่โตตามจำนวนคนใช้พร้อมกัน คำสั่ง nvidia-smi ที่ใช้จริง ตัวเลขที่ต้องดูและวิธีตีความ พร้อมวิธี benchmark ที่ไม่หลอกตัวเอง

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

5.1 VRAM ประกอบด้วยอะไรบ้าง

VRAM ที่ใช้จริง = น้ำหนักโมเดล + KV cache + activation + overhead ของตัวรัน
ส่วนประกอบลักษณะ
น้ำหนักโมเดลคงที่ คำนวณล่วงหน้าได้ด้วยสูตรในบทที่ 2
KV cacheโตตามความยาว context และจำนวนคนที่ใช้พร้อมกัน — ตัวที่คนลืมบ่อยที่สุด
activationค่าระหว่างคำนวณ ขึ้นกับ batch size
overheadCUDA context และ buffer ของตัวรัน ราว 0.5–2 GB

สูตรคำนวณ KV cache

KV cache (ไบต์) = 2 × layers × kv_heads × head_dim × ctx_len × batch × bytes_per_value
ตัวแปรความหมาย
2เพราะเก็บทั้ง Key และ Value
layersจำนวนชั้นของโมเดล ดูได้ใน config.json
kv_headsจำนวน KV heads โมเดลใหม่ใช้ GQA ทำให้เลขนี้น้อยกว่า attention heads มาก
head_dimhidden_size ÷ num_attention_heads
bytes2 สำหรับ FP16, 1 สำหรับ FP8

ตัวอย่างจริงกับโมเดล 8B ทั่วไป ที่มี 36 ชั้น, 8 kv_heads, head_dim 128:

เงื่อนไขคำนวณได้หมายเหตุ
8K context, 1 คน, FP162 × 36 × 8 × 128 × 8192 × 1 × 21.2 GB
32K context, 1 คน, FP162 × 36 × 8 × 128 × 32768 × 1 × 24.8 GBใหญ่เกือบเท่าตัวโมเดลที่ Q4
32K context, 8 คนพร้อมกัน, FP164.8 × 838.6 GBจุดที่ระบบจริงพัง
32K context, 8 คนพร้อมกัน, FP838.6 ÷ 219.3 GBเหตุผลที่ควรเปิด --kv-cache-dtype fp8

บทเรียนที่แพงที่สุดของมือใหม่

ทดสอบคนเดียวบนเครื่องแล้วทุกอย่างดี พอเปิดให้คนใช้จริง 10 คนแล้วระบบล่ม เพราะ KV cache โตเป็นเส้นตรงตามจำนวนคนที่ใช้พร้อมกัน คูณกับความยาว context คำนวณ VRAM ที่จำนวนผู้ใช้สูงสุดที่คาดไว้เสมอ ไม่ใช่ที่หนึ่งคน

5.2 เครื่องมือดูการใช้ทรัพยากร

# ภาพรวมการ์ด อัปเดตทุก 1 วินาที
nvidia-smi -l 1

# ดูเฉพาะตัวเลขที่สนใจ ในรูปแบบที่เอาไปทำกราฟต่อได้
nvidia-smi --query-gpu=timestamp,name,memory.used,memory.total,\
utilization.gpu,utilization.memory,temperature.gpu,power.draw,clocks_throttle_reasons.active \
  --format=csv -l 2

# บันทึกลงไฟล์ระหว่างรัน benchmark
nvidia-smi --query-gpu=timestamp,memory.used,utilization.gpu,power.draw \
  --format=csv -l 1 > gpu_log.csv &

# หน้าจอแบบโต้ตอบ (ติดตั้งด้วย apt install nvtop)
nvtop

# ดูว่ากระบวนการไหนกิน VRAM อยู่
nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv

# CPU / RAM / ดิสก์
htop ; free -h ; df -h

วัดจากในโค้ด:

import torch, time

def report(tag: str):
    a = torch.cuda.memory_allocated() / 1024**3      # ที่ tensor ใช้จริง
    r = torch.cuda.memory_reserved() / 1024**3       # ที่ PyTorch จองไว้จากการ์ด
    p = torch.cuda.max_memory_allocated() / 1024**3
    print(f"{tag:24s} allocated={a:6.2f} GB reserved={r:6.2f} GB peak={p:6.2f} GB")
    torch.cuda.reset_peak_memory_stats()

report("ก่อนโหลด")

from transformers import AutoModelForCausalLM, AutoTokenizer

mid = "Qwen/Qwen3-8B"
tok = AutoTokenizer.from_pretrained(mid)
model = AutoModelForCausalLM.from_pretrained(
    mid, torch_dtype=torch.bfloat16, device_map="cuda",
    use_safetensors=True,          # บังคับรูปแบบปลอดภัย
)
report("หลังโหลดน้ำหนัก")

ids = tok("อธิบายการทำงานของ KV cache", return_tensors="pt").to("cuda")
t0 = time.perf_counter()
out = model.generate(**ids, max_new_tokens=256, do_sample=False)
dt = time.perf_counter() - t0
report("หลัง generate")

n = out.shape[-1] - ids["input_ids"].shape[-1]
print(f"สร้าง {n} tokens ใน {dt:.2f}s = {n/dt:.1f} tok/s")
print(torch.cuda.memory_summary(abbreviated=True))

5.3 ตัวเลขที่ต้องดูจริง และความหมายของมัน

ตัวเลขดูจากไหนตีความอย่างไร
VRAM usednvidia-smiถ้าเกือบเต็มเกิน 95% เสี่ยง OOM เมื่อ context ยาวขึ้น ให้ลด context หรือ quantize KV cache
GPU utilizationnvidia-smiสูงไม่ได้แปลว่าดี ตอน decode ตัวเลขนี้สูงได้ทั้งที่การ์ดรออ่านหน่วยความจำอยู่
TTFTวัดเองเวลาถึง token แรก สะท้อนความเร็วการอ่าน prompt ผู้ใช้รู้สึกกับตัวเลขนี้มากที่สุด
tok/s (decode)วัดเองความเร็วพิมพ์คำตอบ ถูกจำกัดด้วย bandwidth เป็นหลัก
Throughput รวมเมตริกของตัวรันtok/s ของทุกคนรวมกัน ตัวเลขที่สำคัญเมื่อให้บริการหลายคน
KV cache usagevLLM /metricsถ้าเกิน 90% บ่อย แปลว่าจะเริ่มปฏิเสธหรือคิวยาว ต้องลด context หรือเพิ่มการ์ด
Power drawnvidia-smiต่ำกว่า TDP มากตอนโหลดเต็ม แปลว่าติดคอขวดที่หน่วยความจำ ไม่ใช่ที่การคำนวณ
Throttle reasonsnvidia-smiถ้าขึ้น SwThermalSlowdown แปลว่าร้อนเกิน แก้ที่การระบายความร้อน ไม่ใช่ที่ซอฟต์แวร์

5.4 วิธี benchmark ที่ไม่หลอกตัวเอง

ข้อผิดพลาดที่ทำให้ตัวเลขไม่จริง:

  • วัดรอบแรกที่รวมเวลาโหลดโมเดลและคอมไพล์ kernel
  • วัดครั้งเดียวแล้วสรุป
  • ใช้ prompt สั้น 10 token แล้วบอกว่าเร็ว ทั้งที่งานจริง prompt ยาวกว่านั้นมาก
  • วัดที่ผู้ใช้คนเดียวแล้วคูณจำนวนคนเอาเอง
  • ไม่แยก TTFT กับ decode ทั้งที่สองอย่างนี้ติดคอขวดคนละที่

วิธีที่ถูก:

  1. warm-up 3 รอบทิ้ง แล้วค่อยเริ่มวัด
  2. ใช้ prompt ที่ยาวใกล้เคียงงานจริง เช่น 1,000–4,000 token
  3. วัดอย่างน้อย 10 รอบ รายงานค่ามัธยฐานและ p95 ไม่ใช่ค่าเฉลี่ย
  4. ไล่จำนวนผู้ใช้พร้อมกัน 1, 2, 4, 8, 16 แล้วดูว่าจุดไหนที่ latency พุ่ง
  5. บันทึก VRAM สูงสุดตลอดการทดสอบ ไม่ใช่ค่าตอนเริ่ม
  6. ทดสอบซ้ำหลังเครื่องร้อนแล้ว 20 นาที เพราะตัวเลขมักตกจากตอนเย็น

สคริปต์วัด TTFT และ decode แยกกัน พร้อมไล่จำนวนผู้ใช้:

import asyncio, statistics, time, httpx

BASE = "http://localhost:8000/v1/chat/completions"
PROMPT = "สรุปประเด็นสำคัญของข้อความต่อไปนี้เป็นข้อ ๆ:\n" + ("ตัวอย่างเนื้อหา " * 400)

async def one(client) -> tuple[float, float, int]:
    t0, first, n = time.perf_counter(), None, 0
    async with client.stream("POST", BASE, json={
        "model": "Qwen/Qwen3-8B",
        "messages": [{"role": "user", "content": PROMPT}],
        "max_tokens": 256, "temperature": 0, "stream": True,
    }, timeout=180) as r:
        async for line in r.aiter_lines():
            if line.startswith("data: ") and "[DONE]" not in line:
                if first is None:
                    first = time.perf_counter() - t0       # TTFT
                n += 1
    total = time.perf_counter() - t0
    decode = n / (total - first) if first and total > first else 0
    return first, decode, n

async def sweep():
    async with httpx.AsyncClient() as c:
        await asyncio.gather(*[one(c) for _ in range(3)])   # warm-up ทิ้ง
        for users in (1, 2, 4, 8, 16):
            res = await asyncio.gather(*[one(c) for _ in range(users)])
            ttft = [r[0] for r in res]
            decode = [r[1] for r in res]
            print(f"users={users:2d} "
                  f"TTFT med={statistics.median(ttft):.2f}s "
                  f"p95={sorted(ttft)[int(len(ttft)*0.95)-1]:.2f}s "
                  f"decode med={statistics.median(decode):.1f} tok/s "
                  f"รวม={sum(decode):.0f} tok/s")

asyncio.run(sweep())

สิ่งที่ผลลัพธ์จะบอกคุณ

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

สรุปบทนี้

VRAM ไม่ได้มีแค่ตัวโมเดล KV cache โตตามความยาว context คูณจำนวนคนพร้อมกัน และนั่นคือจุดที่ระบบพังเมื่อเปิดใช้จริง วัดด้วย prompt ที่ยาวเหมือนงานจริง แยก TTFT กับ decode รายงานมัธยฐานและ p95 แล้วไล่จำนวนผู้ใช้จนเห็นเพดาน

บทถัดไปว่าด้วยการเลือกโมเดลให้ตรงกับงาน โดยเริ่มจากข้อจำกัดของเรา ไม่ใช่จาก leaderboard