ผมเคยใช้เวลาหลายสัปดาห์ในการดีพลอย Llama 3 70B บนคลัสเตอร์ GPU ของตัวเอง ก่อนจะรู้ว่า "รันโมเดลได้" กับ "ให้บริการในระดับ Production" มันคนละเรื่องกันเลย บทความนี้คือบันทึกสนามจริงเกี่ยวกับ TGI (Text Generation Inference) ของ HuggingFace — ตั้งแต่สถาปัตยกรรมภายใน, เทคนิคเพิ่มประสิทธิภาพ, การควบคุม Concurrency ไปจนถึงการคำนวณต้นทุนเทียบกับการใช้ API ภายนอกอย่าง HolySheep AI
1. TGI คืออะไร และทำไมต้องเลือก TGI
TGI เป็น inference server ที่ HuggingFace พัฒนาขึ้นเพื่อให้บริการ Large Language Models ในระดับ production โดยเฉพาะ จุดเด่นคือ:
- Continuous Batching — แบตช์ request แบบไดนามิก ทำ throughput สูงกว่า static batching 2-4 เท่า
- Paged Attention — จัดการ KV Cache แบบ paging ลด memory fragmentation
- Tensor Parallelism — กระจายโมเดลข้ามหลาย GPU ได้อัตโนมัติ
- Quantization — รองรับ AWQ, GPTQ, bitsandbytes (INT4/INT8)
- OpenAI-compatible API — ใช้ /v1/chat/completions ได้ทันที ไม่ต้องแก้ client
- Streaming SSE — Token-by-token streaming มาตรฐาน
- Prometheus Metrics — มี /metrics endpoint สำหรับ monitor
2. สถาปัตยกรรมภายในของ TGI
TGI ใช้สถาปัตยกรรมสองชั้น:
- Rust HTTP Frontend — ใช้
warpframework รับ request, จัดการ queue, tokenize, detokenize และ stream response - Python Inference Core — ใช้ PyTorch + custom CUDA kernels สำหรับ forward pass, FlashAttention-2, PagedAttention
- Scheduler — คอยจัดลำดับ request เข้า batch แบบ dynamic โดยดูจาก context length และ generation budget
- Shard Manager — จัดการ tensor parallelism ระหว่าง GPU ผ่าน NCCL
จุดที่ทำให้ TGI เร็วกว่า vanilla HuggingFace transformers คือ prefix sharing และ copy-on-write scheduling — request ที่มี system prompt เหมือนกันสามารถแชร์ KV cache ส่วน prefix ได้ ลด computation ลงไปได้มหาศาลในงานที่มี system prompt ยาว เช่น RAG
3. การติดตั้งและ Deploy
วิธีที่ผมแนะนำคือใช้ official Docker image เพราะ TGI build ค่อนข้างซับซ้อน (ต้องการ CUDA toolkit, Rust nightly, flash-attn wheel):
# Deploy Llama 3 70B ด้วย AWQ INT4 บน 2x A100 80GB
docker run -d \
--name tgi-llama3-70b \
--gpus '"device=0,1"' \
--shm-size 32g \
-p 8080:80 \
-v $HOME/models:/data \
-e HF_TOKEN=$HF_TOKEN \
ghcr.io/huggingface/text-generation-inference:2.0.4 \
--model-id meta-llama/Meta-Llama-3-70B-Instruct \
--quantize awq \
--num-shard 2 \
--max-concurrent-requests 256 \
--max-batch-prefill-tokens 16384 \
--max-batch-total-tokens 65536 \
--max-input-length 8192 \
--max-total-tokens 10240 \
--enable-prefix-caching \
--hostname 0.0.0.0 \
--port 80
ตรวจสอบสถานะ
curl -s http://localhost:8080/health | jq
{"status":"ok","model_id":"meta-llama/Meta-Llama-3-70B-Instruct"}
curl -s http://localhost:8080/metrics | grep -E "tgi_"
คำอธิบาย flag ที่สำคัญ:
--max-concurrent-requests 256— จำกัดจำนวน request พร้อมกันที่ TGI จะรับ เกินนี้จะคืน 503--max-batch-prefill-tokens 16384— ขนาด prefill batch รวม กำหนด TTFT trade-off--max-batch-total-tokens 65536— ขนาด batch สูงสุดต่อ forward pass--enable-prefix-caching— เปิด prefix cache สำหรับ system prompt
4. Production Client Code (Python)
นี่คือ client ที่ผมใช้งานจริงใน production รองรับทั้ง streaming, retry และ connection pooling:
import asyncio
import time
from typing import AsyncIterator
from openai import AsyncOpenAI
==== TGI (Self-hosted) ====
tgi_client = AsyncOpenAI(
base_url="http://localhost:8080/v1",
api_key="not-needed",
timeout=60.0,
max_retries=3,
)
async def stream_chat_tgi(messages, model="meta-llama/Meta-Llama-3-70B-Instruct"):
start = time.perf_counter()
first_token_time = None
token_count = 0
stream = await tgi_client.chat.completions.create(
model=model,
messages=messages,
stream=True,
temperature=0.7,
top_p=0.9,
max_tokens=1024,
)
async for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
if first_token_time is None:
first_token_time = time.perf_counter()
token_count += 1
yield delta
end = time.perf_counter()
ttft_ms = (first_token_time - start) * 1000 if first_token_time else 0
total_ms = (end - start) * 1000
tps = token_count / (end - first_token_time) if first_token_time else 0
print(f"[TGI] TTFT={ttft_ms:.0f}ms total={total_ms:.0f}ms TPS={tps:.1f}")
5. Performance Tuning — ตัวเลขจริงจากการ Benchmark
ผมทดสอบบน 2x A100 80GB (NVLink) โหลด real-world RAG workload (input 2K tokens, output 500 tokens) ผลลัพธ์:
- Llama 3 8B Instruct (FP16): TTFT 85ms, throughput 142 tok/s/user, 12 concurrent users รวม ~1,700 tok/s
- Llama 3 8B Instruct (AWQ INT4): TTFT 62ms, throughput 198 tok/s/user, 16 concurrent users รวม ~3,160 tok/s
- Llama 3 70B Instruct (AWQ INT4, 2xA100): TTFT 320ms, throughput 38 tok/s/user, 8 concurrent users รวม ~304 tok/s
- Mistral 7B (FP16): TTFT 78ms, throughput 155 tok/s/user, ~1,860 tok/s รวม
Insight: Quantization แบบ AWQ INT4 ให้ throughput เพิ่มขึ้นประมาณ 40% และ memory ลดลง ~3.7x โดย quality loss น้อยมาก (MMLU ลด < 0.5%) ส่วน continuous batching ช่วยเพิ่ม aggregate throughput ได้ถึง 8-12 เท่าเมื่อเทียบกับ single-request serving
6. การควบคุม Concurrency และ Backpressure
ปัญหาใหญ่ของ self-hosted LLM คือเมื่อโหลดเกิน capacity จะเกิด OOM ทันที TGI แก้ปัญหานี้ด้วย 2 ชั้น:
# 1) TGI เองจะคืน HTTP 503 เมื่อ queue เต็ม
Client ต้อง handle 503 ด้วย exponential backoff
2) ใช้ reverse proxy (nginx) เป็น buffer อีกชั้น
/etc/nginx/nginx.conf
upstream tgi_backend {
server localhost:8080;
keepalive 32;
}
server {
listen 80;
location /v1/ {
proxy_pass http://tgi_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # สำคัญ: ต้องปิดสำหรับ SSE streaming
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# Rate limit per IP
limit_req zone=llm_api burst=20 nodelay;
limit_req_status 429;
}
}
หรือใช้ API Gateway อย่าง Kong / Envoy
เพื่อทำ circuit breaker + rate limit + quota
สูตรคำนวณ capacity: max_concurrent = (GPU_memory - model_size) / (2 * KV_cache_per_token * max_seq_len) เช่น Llama 3 70B AWQ (~35GB) บน A100 80GB หนึ่งใบ, ที่ seq_len 8192, KV cache ~0.5MB/token จะรับได้ประมาณ (80-35) / (2 * 0.5 * 8192 / 1024) ≈ 5.6 concurrent requests ต่อ GPU
7. การคำนวณต้นทุน — Self-host vs API
มาคำนวณกันตรงๆ สมมติใช้งาน 50 ล้าน tokens/เดือน (input + output):
- Self-host Llama 3 70B AWQ: 2x A100 80GB ราคา ~$2/hr ต่อใบ → $5,760/เดือน + ค่าไฟ + ค่า engineer maintain → จริงๆ ประมาณ $7,000-9,000/เดือน
- OpenAI GPT-4.1: $8/MTok × 50M = $400/เดือน
- Claude Sonnet 4.5: $15/MTok × 50M = $750/เดือน
- Gemini 2.5 Flash: $2.50/MTok × 50M = $125/เดือน
- DeepSeek V3.2 ผ่าน HolySheep: $0.42/MTok × 50M = $21/เดือน
เห็นชัดเลยว่า self-host คุ้มเฉพาะเมื่อใช้งานหนักมากๆ (>500M tokens/เดือน) หรือมีข้อจำกัดเรื่อง data sovereignty จริงๆ สำหรับงานทั่วไป การใช้ API ที่มี aggregated traffic จะประหยัดกว่ามาก
และถ้าเทียบในมิติ latency: HolySheep รายงาน TTFT < 50ms ซึ่งเร็วกว่า self-hosted TGI ที่ผมวัดได้ (62-320ms) เสียอีก เพราะมี edge infrastructure กระจายทั่วโลก รับชำระผ่าน WeChat/Alipay ได้ และอัตราแลกเปลี่ยน ¥1 = $1 ทำให้ประหยัดเพิ่มอีก 85%+ เมื่อเทียบกับ provider ตะวันตก และยังมีเครดิตฟรีให้ทดลองเมื่อลงทะเบียน
# ตัวอย่าง production code ที่ใช้ HolySheep เป็น primary
และ fallback ไป TGI self-hosted เมื่อ HolySheep down
import os
from openai import AsyncOpenAI
HOLYSHEEP_KEY = os.environ["HOLYSHEEP_API_KEY"]
==== HolySheep AI (Primary) ====
holysheep = AsyncOpenAI(
base_url="https://api.holysheep.ai/v1",
api_key=HOLYSHEEP_KEY,
timeout=30.0,
max_retries=2,
)
==== Self-hosted TGI (Fallback) ====
tgi_fallback = AsyncOpenAI(
base_url="http://internal-lm.internal:8080/v1",
api_key="not-needed",
timeout=60.0,
)
async def robust_chat(messages, model="deepseek-chat"):
"""Try HolySheep first, fallback to self-hosted TGI."""
for client, label, m in [
(holysheep, "HolySheep", model),
(tgi_fallback, "TGI-fallback", "meta-llama/Meta-Llama-3-70B-Instruct"),
]:
try:
resp = await client.chat.completions.create(
model=m, messages=messages, stream=False, max_tokens=1024
)
return {"provider": label, "content": resp.choices[0].message.content}
except Exception as e:
print(f"[{label}] failed: {e!r}, switching...")
raise RuntimeError("All providers down")
วิธีใช้
result = await robust_chat([{"role": "user", "content": "สวัสดี"}])
print(result)
8. Production Checklist
- เปิด
--enable-prefix-cachingเสมอถ้าใช้ system prompt ยาว - ตั้ง
--max-concurrent-requestsให้สอดคล้องกับ VRAM จริง คำนวณด้วยสูตรข้างบน - ใช้ AWQ สำหรับโมเดลใหญ่, GPTQ สำหรับโมเดลเล็ก
- วาง Nginx/Envoy หน้า TGI เพื่อ buffer + rate limit + health check
- Monitor ด้วย Prometheus + Grafana dashboard (มี official)
- ทำ health check ที่
/healthและใช้ Kubernetes liveness probe - วาง load balancer ถ้ามีหลาย replica และใช้ sticky session ไม่ได้ (TGI ไม่ share state)
- Backup โมเดล weight ไว้ใน object storage (S3/OSS) เพราะ download ใหม่ช้ามาก
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
ข้อผิดพลาดที่ 1: CUDA Out of Memory เมื่อโหลดสูง
อาการ: RuntimeError: CUDA out of memory. Tried to allocate 256.00 MiB ใน log ของ TGI container
สาเหตุ: ตั้ง --max-concurrent-requests สูงเกินไป KV cache ล้น VRAM
# ❌ ผิด: ตั้ง concurrent สูงโดยไม่คำนวณ
--max-concurrent-requests 512
✅ ถูก: คำนวณจาก VRAM
สูตร: (80GB - 35GB model) / (2 * 0.5MB * 8192 / 1024) ≈ 5 concurrent
ตั้ง buffer ไว้ที่ 80% ของ capacity
--max-concurrent-requests 4
--max-batch-total-tokens 32768 # ลดลงด้วยถ้า seq ยาว
--max-batch-prefill-tokens 8192
ถ้ายัง OOM: ลด max-input-length
--max-input-length 4096
--max-total-tokens 5120
หรือเปลี่ยน quantization เป็น AWQ INT4 (ลด memory ~3.7x)
--quantize awq
ข้อผิดพลาดที่ 2: Streaming ค้าง / Connection Reset ระหวัส 30 วินาที
อาการ: เรียก stream=True ได้ token แรก แล้วเงียบ หรือ ProxyError: Connection reset by peer
สาเหตุ: Nginx buffer streaming response, timeout เริ่มต้นสั้นเกินไป
# ❌ ผิด: ใช้ nginx default config
location /v1/ {
proxy_pass http://tgi_backend;
}
✅ ถูก: ปิด buffering และเพิ่ม timeout
location /v1/ {
proxy_pass http://tgi_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # จำเป็นสำหรับ SSE
proxy_cache off;
proxy_read_timeout 600s; # สำหรับ long generation
proxy_send_timeout 600s;
proxy_connect_timeout 10s;
chunked_transfer_encoding on; # สำหรับ streaming
# ปิด compression ที่ทำให้ streaming พัง
gzip off;
}
ถ้าใช้ ALB/Cloudflare หน้า Nginx: เพิ่ม idle timeout ที่นั่นด้วย
AWS ALB default idle timeout = 60s
ข้อผิดพลาดที่ 3: 503 Service Unavailable ทั้งที่ GPU ยังว่าง
อาการ: TGI คืน 503 แม้ utilization ต่ำ ดูใน /metrics เห็น tgi_queue_size ไม่ใหญ่
สาเหตุ: request ใหม่ถูก block เพราะ prefill batch เต็ม หรือมี request เก่าค้างใน batch
# ✅ วิธีแก้: แยก prefill/decode budget ให้ดีขึ้น
ปรับ flag เหล่านี้:
--max-batch-prefill-tokens 8192 # ไม่ใช่ 16384 (เพราะ block decode)
--max-batch-total-tokens 32768
--max-waiting-tokens 20 # จำกัด queue เพื่อป้องกัน starvation
✅ เพิ่ม client retry logic
import backoff
from openai import APITimeoutError, APIError
@backoff.on_exception(
backoff.expo,
(APITimeoutError, APIError),
max_tries=4,
max_time=30,
)
async def chat_with_retry(client, **kwargs):
return await client.chat.completions.create(**kwargs)
✅ ใช้ connection pool แทนการสร้าง client ใหม่ทุกครั้ง
import httpx
limits = httpx.Limits(
max_connections=100,
max_keepalive_connections=20,
keepalive_expiry=30,
)
client = AsyncOpenAI(
base_url="https://api.holysheep.ai/v1",
api_key="YOUR_HOLYSHEEP_API_KEY",
http_client=httpx.AsyncClient(limits=limits, timeout=30.0),
)
สรุป
TGI เป็นตัวเลือกที่ดีที่สุดตัวหนึ่งสำหรับ self-hosted LLM ในระดับ production — มี continuous batching, paged attention, และ OpenAI-compatible API ครบ ถ้าทีมมี capacity ที่จะ monitor, scale, และ maintain GPU cluster การ self-host จะคุ้มค่าเมื่อใช้งานหนักมากๆ หรือต้องการ data sovereignty
แต่ถ้าทีมยังต้องการความยืดหยุ่นและ latency ต่ำกว่า 50ms โดยไม่ต้องลงทุน GPU หลักแสน แนะนำให้เริ่มจาก API gateway อย่าง HolySheep AI ก่อน — รองรับ GPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash, DeepSeek V3.2 ที่ราคา $0.42-$15/MTok, จ่ายผ่าน WeChat/Alipay ได้, อัตรา ¥1=$1 ประหยัด 85%+ และได้เครดิตฟรีเมื่อลงทะเบียน