大多數人挑模型的方式是看 Trending 第一名,然後
from_pretrained下去。 正確答案是:先確定 VRAM 上限與授權條款,再從那個交集裡挑排行最高的。 大多數人把pipeline包進 Flask 就當作上線了。 正確答案是:那個架構在第 5 個並發請求就會開始排隊,而你需要的是 continuous batching。
上一篇我們把環境架好、跑出第一個結果。這一篇處理三件實際工作:挑對模型、把它變成別人能用的服務、把自己訓練的成果推回 Hub。
一、選模型:從兩百萬個 repo 中挑對那一個
1.1 四道篩選器,順序不能顛倒
2,000,000+ 個模型
│
▼
┌──────────────────────────────────────────────┐
│ 篩選 1:任務類型(pipeline_tag) │ → 剩約 5%
│ text-generation / embeddings / ASR / … │
└──────────────┬───────────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ 篩選 2:VRAM 是否放得下(硬限制) │ → 剩約 1%
│ 參數量 × dtype bytes × 1.2 ≤ 你的 VRAM │
└──────────────┬───────────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ 篩選 3:授權是否允許你的用途(法務限制) │ → 剩約 0.6%
│ Apache-2.0 / MIT / Llama Community / … │
└──────────────┬───────────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ 篩選 4:目標語言與領域的實測表現 │ → 剩 3–5 個候選
│ 用你自己的 50 題測試集跑,不要只看榜單 │
└──────────────┬───────────────────────────────┘
▼
最終選擇
順序很重要。 很多人從第 4 步開始(「我要最強的」),花兩天調通一個 70B 模型,最後才發現公司規定資料不能出境、而那個模型的授權禁止商用。硬限制先過,偏好後談。
1.2 中文場景的實務建議(2026 年中)
| 用途 | 推薦起點 | 參數量 | 授權 | 備註 |
|---|---|---|---|---|
| 中文對話 / 通用 | Qwen/Qwen2.5-7B-Instruct | 7B | Apache-2.0 | 中文能力強、授權寬鬆,預設首選 |
| 邊緣裝置 / 低延遲 | Qwen/Qwen2.5-1.5B-Instruct | 1.5B | Apache-2.0 | int4 後約 1.2GB |
| 繁中在地化 | MediaTek-Research/Breeze-7B-Instruct-v1_0 | 7B | Apache-2.0 | 台灣用語與繁體最佳化 |
| 中文 Embedding | BAAI/bge-m3 | 568M | MIT | 多語、支援長文與混合檢索 |
| 中文 Reranker | BAAI/bge-reranker-v2-m3 | 568M | Apache-2.0 | RAG 精排必備 |
| 語音辨識 | openai/whisper-large-v3-turbo | 809M | MIT | 中文 WER 明顯優於 base |
模型迭代很快,這張表的價值不在具體型號,而在**「先看授權、再看規模、最後看能力」的挑選邏輯**。
1.3 用程式碼做篩選
1from huggingface_hub import list_models
2
3MY_VRAM_GB = 24
4OK_LICENSES = {"apache-2.0", "mit", "bsd-3-clause"}
5
6def rough_params_b(model_id: str) -> float | None:
7 """從 repo 名稱粗估參數量,例如 Qwen2.5-7B-Instruct → 7.0"""
8 import re
9 m = re.search(r"(\d+(?:\.\d+)?)\s*[bB](?![a-zA-Z])", model_id)
10 return float(m.group(1)) if m else None
11
12candidates = []
13for m in list_models(task="text-generation", sort="downloads",
14 direction=-1, limit=200, full=True):
15 lic = (m.card_data or {}).get("license") if m.card_data else None
16 size = rough_params_b(m.id)
17 if lic not in OK_LICENSES or size is None:
18 continue
19 vram_bf16 = size * 2 * 1.2
20 vram_int4 = size * 0.5 * 1.2
21 if vram_int4 <= MY_VRAM_GB:
22 candidates.append((m.id, size, lic, vram_bf16, vram_int4))
23
24print(f"{'模型':<45} {'B':>6} {'授權':<14} {'bf16':>7} {'int4':>7}")
25for cid, size, lic, b16, i4 in candidates[:12]:
26 fits = "✓" if b16 <= MY_VRAM_GB else "需量化"
27 print(f"{cid:<45} {size:>6.1f} {lic:<14} {b16:>6.1f}G {i4:>6.1f}G {fits}")
但最終決策一定要靠你自己的測試集。 準備 50 題涵蓋你實際場景的問題,跑過 3–5 個候選模型,人工評分。這半天的投資,可以避免上線後才發現「榜單第一在我的領域裡表現最差」。
二、載入模型的四個抽象層
抽象程度高 抽象程度低
控制力低 控制力高
◀────────────────────────────────────────────────────────────▶
┌──────────┐ ┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ pipeline │ │ AutoModel │ │ 量化載入 │ │ 推論伺服器 │
│ │ │ + generate │ │ bnb/AWQ/GPTQ │ │ vLLM / TGI │
├──────────┤ ├────────────┤ ├──────────────┤ ├──────────────┤
│ 原型 │ │ 自訂邏輯 │ │ VRAM 不足 │ │ 生產服務 │
│ 離線批次 │ │ 拿 hidden │ │ 成本壓縮 │ │ 高並發 │
└──────────┘ └────────────┘ └──────────────┘ └──────────────┘
第一篇已經講過前兩層。這裡處理後兩層——它們決定你的成本與可服務規模。
2.1 第三層:量化載入
量化就是用更少的位元表示同一個權重,以少量精度換大量記憶體。
同一個 Qwen2.5-7B 在不同精度下:
精度 VRAM 相對品質 推論速度 適用
──────────────────────────────────────────────────────────────
bf16 16.8 GB 100%(基準) 1.0× 24GB 以上顯卡
int8 8.8 GB 99.3% 0.7×(較慢) 12–16GB
nf4 5.0 GB 97.5% 0.9× 8–12GB
AWQ int4 4.6 GB 98.5% 1.6×(較快) 8–12GB,生產推薦
注意 int8 反而變慢——bitsandbytes 的 int8 需要在運算時反量化,額外開銷抵銷了頻寬節省。想要「又小又快」,應該用 AWQ 或 GPTQ 這類預先量化的模型。
bitsandbytes(動態量化,任何模型都能用):
1import torch
2from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
3
4bnb_config = BitsAndBytesConfig(
5 load_in_4bit=True,
6 bnb_4bit_quant_type="nf4", # nf4 品質優於 fp4
7 bnb_4bit_compute_dtype=torch.bfloat16, # 計算時反量化成 bf16
8 bnb_4bit_use_double_quant=True, # 二次量化,再省約 0.4GB
9)
10
11model_id = "Qwen/Qwen2.5-7B-Instruct"
12tok = AutoTokenizer.from_pretrained(model_id)
13model = AutoModelForCausalLM.from_pretrained(
14 model_id,
15 quantization_config=bnb_config,
16 device_map="auto",
17)
18print(f"VRAM 佔用:{model.get_memory_footprint() / 1e9:.2f} GB")
AWQ(預先量化,速度最佳):
1from transformers import AutoModelForCausalLM, AutoTokenizer
2
3# 直接載入社群已量化好的 repo,不需要 quantization_config
4model_id = "Qwen/Qwen2.5-7B-Instruct-AWQ"
5tok = AutoTokenizer.from_pretrained(model_id)
6model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto")
選擇原則: 開發與微調時用 bitsandbytes(彈性高、任何模型都能量化);生產部署找官方或社群的 AWQ / GPTQ 版本(速度快 1.5–2×)。兩者不要混用——用 bnb 微調出來的 LoRA,部署時應該合併回 bf16 底模再重新量化。
2.2 第四層:推論伺服器
為什麼 pipeline 不能當服務? 關鍵在批次策略:
靜態批次(transformers pipeline)
─────────────────────────────────
請求 A ████████████████████ (生成 200 token)
請求 B ████░░░░░░░░░░░░░░░░ (只需 40 token,但要等 A 結束)
請求 C ░░░░░░░░░░░░░░░░░░░░ (完全等待,直到整批完成)
└─── GPU 有 60% 時間在算 padding ───┘
連續批次(vLLM / TGI)
─────────────────────────────────
請求 A ████████████████████
請求 B ████↵ 完成即離開,槽位立刻給 D
請求 C ████████████↵
請求 D ██████████████████
└─── GPU 使用率 > 85% ───┘
實測差異(7B 模型、A10G、50 並發):
pipeline + Flask : 8 req/s,P99 12,000 ms
vLLM : 62 req/s,P99 1,400 ms
vLLM 提供 OpenAI 相容 API,這是目前最省事的生產選擇:
1pip install vllm
2
3python -m vllm.entrypoints.openai.api_server \
4 --model Qwen/Qwen2.5-7B-Instruct-AWQ \
5 --served-model-name my-chat \
6 --max-model-len 8192 \
7 --gpu-memory-utilization 0.90 \
8 --port 8000
1# 用 OpenAI SDK 呼叫自己的 vLLM,換模型只要改 base_url
2from openai import OpenAI
3
4client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
5
6resp = client.chat.completions.create(
7 model="my-chat",
8 messages=[{"role": "user", "content": "用三句話說明 continuous batching"}],
9 temperature=0.7,
10 max_tokens=256,
11)
12print(resp.choices[0].message.content)
TGI(Text Generation Inference)是 Hugging Face 官方的方案,Docker 一行啟動:
1docker run --gpus all --shm-size 1g -p 8080:80 \
2 -v $HF_HOME/hub:/data \
3 -e HF_TOKEN=$HF_TOKEN \
4 ghcr.io/huggingface/text-generation-inference:latest \
5 --model-id Qwen/Qwen2.5-7B-Instruct \
6 --max-input-tokens 4096 \
7 --max-total-tokens 8192 \
8 --quantize awq
--shm-size 1g不能省。少了它,多 GPU 的 NCCL 通訊會在啟動時卡死,而錯誤訊息完全不會提到共享記憶體。
三、三個演進階段:從腳本到服務
╔══════════════════════════════════════════════════════════════════╗
║ Phase 1:本機腳本 + Gradio(0–100 使用者) ║
╚══════════════════════════════════════════════════════════════════╝
┌──────────────┐ ┌───────────────────────────┐
│ 瀏覽器 │───────▶│ Gradio(單一 process) │
│ │◀───────│ 內含 transformers 模型 │
└──────────────┘ └───────────────────────────┘
部署:demo.launch() 或推到 HF Spaces
成本:$0(Space CPU 免費層)/ $0.60 per hr(T4 Space)
並發:1–3(超過就排隊)
能做:Demo、內部工具、使用者訪談
不能做:SLA、認證、多租戶
╔══════════════════════════════════════════════════════════════════╗
║ Phase 2:API + 專用推論伺服器(100–10K 使用者) ║
╚══════════════════════════════════════════════════════════════════╝
┌────────┐ ┌──────────────────┐ ┌──────────────────────┐
│ Web/App│──▶│ FastAPI │──▶│ vLLM (A10G × 1) │
└────────┘ │ · 認證 / 限流 │ │ continuous batching │
│ · Prompt 組裝 │ │ OpenAI 相容 API │
│ · 業務邏輯 │ └──────────────────────┘
└────────┬─────────┘
▼
┌──────────────────┐
│ Redis 快取 │ ← 相同 prompt 直接回,省 30–50% 呼叫
└──────────────────┘
新增:業務層與推論層分離、快取、串流回應、健康檢查
成本:~$750/月(A10G)+ $30(Redis)
並發:40–80(7B AWQ)
解決:延遲穩定、可水平擴充業務層、模型可獨立升級
未解決:GPU 閒置時仍在燒錢、單點故障
╔══════════════════════════════════════════════════════════════════╗
║ Phase 3:多模型 + 自動擴縮(10K+ 使用者) ║
╚══════════════════════════════════════════════════════════════════╝
┌────────┐ ┌───────────┐ ┌────────────────────────────────┐
│ API GW │─▶│ Router │─▶│ K8s GPU node pool │
│ 限流 │ │ · 依難度 │ │ ┌──────────┐ ┌──────────┐ │
│ 計量 │ │ · 依成本 │ │ │ 7B AWQ │ │ 1.5B │ │
└────────┘ └─────┬─────┘ │ │ ×3 (HPA) │ │ ×2 分類 │ │
│ │ └──────────┘ └──────────┘ │
│ └────────────────┬───────────────┘
▼ ▼
┌────────────────────┐ ┌──────────────────────────┐
│ 語意快取(向量比對) │ │ 私有 Model Registry │
│ 命中率 25–40% │ │ 掃描 + 授權審核 + mirror │
└────────────────────┘ └──────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ 可觀測性:TTFT / tokens-per-sec / 成本/請求 │
└────────────────────────────────────────────┘
新增:模型路由(簡單問題丟 1.5B,複雜的丟 7B)、語意快取、HPA、成本歸因
成本:~$3,500/月,但單位 token 成本比 Phase 2 低約 55%
解決:尖峰擴充、成本最佳化、多團隊隔離
代價:Kubernetes + GPU 排程的維運複雜度
四、用 Gradio 把模型變成 App
Gradio 是 Hugging Face 官方的 UI 框架,它的價值在於「模型函式 → 網頁」只需要包一層。
4.1 最小可用的串流聊天介面
1# app.py
2import torch
3import gradio as gr
4from threading import Thread
5from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
6
7MODEL_ID = "Qwen/Qwen2.5-1.5B-Instruct"
8
9tok = AutoTokenizer.from_pretrained(MODEL_ID)
10model = AutoModelForCausalLM.from_pretrained(
11 MODEL_ID,
12 torch_dtype=torch.bfloat16,
13 device_map="auto",
14)
15model.eval()
16
17
18def chat(message, history, system_prompt, temperature, max_tokens):
19 messages = [{"role": "system", "content": system_prompt}]
20 for turn in history: # Gradio 5 的 messages 格式
21 messages.append(turn)
22 messages.append({"role": "user", "content": message})
23
24 prompt = tok.apply_chat_template(messages, tokenize=False,
25 add_generation_prompt=True)
26 inputs = tok(prompt, return_tensors="pt").to(model.device)
27
28 streamer = TextIteratorStreamer(tok, skip_prompt=True, skip_special_tokens=True)
29 kwargs = dict(
30 **inputs,
31 streamer=streamer,
32 max_new_tokens=int(max_tokens),
33 temperature=float(temperature),
34 top_p=0.9,
35 do_sample=temperature > 0,
36 )
37 # 在背景執行緒生成,主執行緒逐塊 yield 給前端
38 Thread(target=model.generate, kwargs=kwargs).start()
39
40 partial = ""
41 for chunk in streamer:
42 partial += chunk
43 yield partial
44
45
46demo = gr.ChatInterface(
47 fn=chat,
48 type="messages",
49 title="🤗 我的第一個 LLM 助理",
50 description="以 Qwen2.5-1.5B-Instruct 驅動,支援串流回應。",
51 additional_inputs=[
52 gr.Textbox("你是一位友善且精準的中文技術助理。", label="System Prompt"),
53 gr.Slider(0.0, 1.5, value=0.7, step=0.1, label="Temperature"),
54 gr.Slider(64, 2048, value=512, step=64, label="Max Tokens"),
55 ],
56 examples=[
57 ["用三句話解釋什麼是 LoRA"],
58 ["幫我寫一個 Python 的 LRU cache"],
59 ],
60)
61
62if __name__ == "__main__":
63 demo.launch(server_name="0.0.0.0", server_port=7860)
三個讓體驗差很多的細節:
- 串流是必須的,不是加分項。 生成 500 token 大約要 12 秒。沒有串流時使用者盯著空白畫面 12 秒;有串流時 0.4 秒就看到第一個字。技術上一樣慢,感知上差 30 倍。
Thread+TextIteratorStreamer是標準組合。model.generate是阻塞呼叫,必須丟到背景執行緒,主執行緒才能持續 yield。type="messages"是 Gradio 5 的格式,與 OpenAI 的訊息結構一致,換後端時不用改資料轉換。
4.2 部署到 Spaces
Space 本質上就是一個 Hub repo,裡面有 app.py 和 requirements.txt:
1# deploy_space.py
2from huggingface_hub import HfApi
3
4api = HfApi()
5REPO = "your-username/my-llm-chat"
6
7api.create_repo(
8 repo_id=REPO,
9 repo_type="space",
10 space_sdk="gradio",
11 private=False,
12 exist_ok=True,
13)
14
15api.upload_folder(
16 folder_path="./my_space", # 內含 app.py, requirements.txt, README.md
17 repo_id=REPO,
18 repo_type="space",
19 commit_message="初次部署",
20)
21print(f"https://huggingface.co/spaces/{REPO}")
my_space/
├── app.py 上面那份 Gradio 程式
├── requirements.txt transformers / torch / accelerate / gradio
└── README.md 開頭的 YAML 決定 Space 設定
README.md 的 front matter 是 Space 的組態檔:
1---
2title: My LLM Chat
3emoji: 🤗
4colorFrom: blue
5colorTo: purple
6sdk: gradio
7sdk_version: "5.9.1"
8app_file: app.py
9pinned: false
10license: apache-2.0
11suggested_hardware: t4-small # 免費 CPU 跑不動 1.5B 以上
12---
Spaces 成本注意: 免費層是 2 vCPU + 16GB RAM,沒有 GPU。1.5B 模型在 CPU 上每個 token 約 200ms,一段回答要等一分鐘。要 GPU 就要升級到付費硬體(T4 small 約 $0.40/hr,A10G small 約 $1.05/hr),而且Space 只要有人開著就在計費。務必在 Settings 裡開啟 sleep(閒置後自動休眠)。
4.3 生產服務:FastAPI 前面、vLLM 後面
Gradio 適合 Demo,正式產品需要把業務邏輯與推論分開:
1# server.py
2import os
3import time
4from fastapi import FastAPI, HTTPException, Depends, Header
5from fastapi.responses import StreamingResponse
6from pydantic import BaseModel, Field
7from openai import AsyncOpenAI
8
9app = FastAPI(title="LLM Gateway")
10
11llm = AsyncOpenAI(
12 base_url=os.getenv("VLLM_URL", "http://vllm:8000/v1"),
13 api_key="not-needed",
14)
15API_KEYS = set(os.getenv("API_KEYS", "").split(","))
16
17SYSTEM = "你是一位客服助理,只根據提供的資訊回答,不確定時請說不知道。"
18
19
20class ChatRequest(BaseModel):
21 message: str = Field(..., max_length=4000)
22 stream: bool = False
23 temperature: float = Field(0.7, ge=0.0, le=2.0)
24
25
26def auth(x_api_key: str = Header(...)):
27 if x_api_key not in API_KEYS:
28 raise HTTPException(401, "invalid api key")
29 return x_api_key
30
31
32@app.post("/chat")
33async def chat(req: ChatRequest, _=Depends(auth)):
34 messages = [
35 {"role": "system", "content": SYSTEM},
36 {"role": "user", "content": req.message},
37 ]
38
39 if req.stream:
40 async def gen():
41 s = await llm.chat.completions.create(
42 model="my-chat", messages=messages,
43 temperature=req.temperature, max_tokens=512, stream=True,
44 )
45 async for chunk in s:
46 delta = chunk.choices[0].delta.content
47 if delta:
48 yield f"data: {delta}\n\n"
49 yield "data: [DONE]\n\n"
50 return StreamingResponse(gen(), media_type="text/event-stream")
51
52 t0 = time.perf_counter()
53 resp = await llm.chat.completions.create(
54 model="my-chat", messages=messages,
55 temperature=req.temperature, max_tokens=512,
56 )
57 return {
58 "reply": resp.choices[0].message.content,
59 "usage": resp.usage.model_dump(),
60 "latency_ms": round((time.perf_counter() - t0) * 1000),
61 }
62
63
64@app.get("/healthz")
65async def healthz():
66 try:
67 await llm.models.list()
68 return {"status": "ok"}
69 except Exception as e:
70 raise HTTPException(503, f"llm unreachable: {e}")
這個分層帶來三個好處: 業務層可以無 GPU 水平擴充(便宜)、模型可以獨立升級或替換、/healthz 讓 K8s 能正確判斷就緒狀態。
五、推送自己的模型到 Hub
5.1 三種推送方式
方式 適合 指令 / API
─────────────────────────────────────────────────────────────────────
push_to_hub() 訓練完直接推 model.push_to_hub(id)
HfApi.upload_folder() 已有本機資料夾、要細部控制 api.upload_folder(...)
hf CLI CI/CD、大檔案、shell 腳本 hf upload <repo> <path>
5.2 訓練後直接推送(最常用)
1from huggingface_hub import create_repo
2
3REPO = "your-username/qwen2.5-1.5b-support-zh"
4
5create_repo(REPO, repo_type="model", private=True, exist_ok=True)
6
7model.push_to_hub(REPO, private=True, commit_message="v1: 客服語料微調")
8tokenizer.push_to_hub(REPO)
一定要一起推 tokenizer。 只推 model 的 repo 是壞掉的 repo——別人(包括三個月後的你)
from_pretrained會直接失敗。這是 Hub 上最常見的 repo 缺陷。
5.3 完整控制:upload_folder
1from huggingface_hub import HfApi, create_repo
2
3api = HfApi()
4REPO = "your-username/qwen2.5-1.5b-support-zh"
5create_repo(REPO, repo_type="model", private=True, exist_ok=True)
6
7api.upload_folder(
8 folder_path="./output/final",
9 repo_id=REPO,
10 repo_type="model",
11 commit_message="v1.2: 加入 2026Q2 語料,格式遵從率 89% → 94%",
12 ignore_patterns=[
13 "checkpoint-*", # 中間檢查點,通常有數十 GB
14 "*.log", "runs/", # TensorBoard 紀錄
15 "optimizer.pt", # 優化器狀態,只有續訓才需要
16 ],
17)
18
19# 打上版本標籤,之後可以用 revision="v1.2" 精確載入
20api.create_tag(REPO, tag="v1.2", repo_type="model")
ignore_patterns不加會出事。Trainer預設會在輸出目錄留下每個 checkpoint,一個 7B 模型訓練 3 epoch 可能產生 90GB 的中間檔。不小心全推上去,除了浪費頻寬,之後每個人 clone 你的 repo 都會拉這 90GB。
5.4 CLI 推送(適合 CI)
1# 建立 repo
2hf repo create your-username/my-model --type model --private
3
4# 上傳整個資料夾
5hf upload your-username/my-model ./output/final . \
6 --commit-message "v1.2 release" \
7 --exclude "checkpoint-*" "*.log"
8
9# 上傳單一檔案
10hf upload your-username/my-model ./adapter_model.safetensors
大檔案(> 5GB)建議開啟 Xet / 分塊上傳加速:
1pip install "huggingface_hub[hf_xet]"
2export HF_HUB_ENABLE_HF_TRANSFER=1 # 高速下載/上傳後端
5.5 Model Card:不是文件,是介面
README.md 開頭的 YAML 是機器可讀的中繼資料,它決定你的模型在 Hub 上能不能被搜尋到、能不能被 pipeline 自動識別:
1---
2license: apache-2.0
3base_model: Qwen/Qwen2.5-1.5B-Instruct
4library_name: transformers
5pipeline_tag: text-generation
6language:
7 - zh
8 - en
9tags:
10 - customer-support
11 - lora
12 - traditional-chinese
13datasets:
14 - your-username/support-tickets-zh
15metrics:
16 - accuracy
17model-index:
18 - name: qwen2.5-1.5b-support-zh
19 results:
20 - task:
21 type: text-generation
22 dataset:
23 name: internal-support-eval
24 type: custom
25 metrics:
26 - type: accuracy
27 value: 0.912
28---
29
30# Qwen2.5-1.5B Support (繁中客服)
31
32以 `Qwen/Qwen2.5-1.5B-Instruct` 為底,用 12,400 筆去識別化的繁中客服對話
33做 LoRA 微調(r=16, α=32),目標是穩定輸出結構化的工單分類與回覆草稿。
34
35## 用途與限制
36
37**適用:** 電商情境的訂單、退換貨、物流查詢分類與初步回覆草稿。
38**不適用:** 醫療、法律、金融建議;不得作為最終決策依據。
39**已知失效模式:** 對於同時包含三個以上訴求的長工單,分類準確率下降至 71%。
40
41## 快速開始
42
43```python
44from transformers import pipeline
45
46pipe = pipeline("text-generation",
47 model="your-username/qwen2.5-1.5b-support-zh",
48 torch_dtype="bfloat16", device_map="auto")
49print(pipe([{"role": "user", "content": "我上週三下的單到現在還沒出貨"}],
50 max_new_tokens=256)[0]["generated_text"][-1]["content"])
51```
52
53## 評估結果
54
55| 指標 | 底模 | 本模型 |
56|------|------|--------|
57| 工單分類準確率 | 68.4% | 91.2% |
58| JSON 格式合規率 | 41.0% | 98.6% |
59| 平均回覆長度 | 312 字 | 148 字 |
60
61評估集:2,000 筆人工標注工單,與訓練集無重疊(以工單 ID 切分)。
62
63## 訓練細節
64
65- LoRA r=16, alpha=32, dropout=0.05,target: q/k/v/o/gate/up/down_proj
66- 3 epoch, lr=2e-4, cosine schedule, warmup 3%
67- 有效 batch size 32(per_device 4 × grad_accum 8)
68- 硬體:1× A10G 24GB,訓練時間 4.2 小時
69
70## 偏誤與風險
71
72訓練語料來自單一電商平台 2024–2026 的工單,對於該平台以外的商品類別、
73或非台灣地區的物流用語,表現會顯著下降。回覆草稿必須經人工確認後才可發送。
一份好的 Model Card 必須回答四個問題: 這模型能做什麼、不能做什麼、怎麼評估出來的、什麼情況會壞掉。第二和第四點最常被省略,也最重要——它們是下游使用者判斷「能不能用在我的場景」的唯一依據。
5.6 私有 repo 與組織治理
1from huggingface_hub import HfApi
2
3api = HfApi()
4
5# 建立組織 repo(權限跟著組織成員設定走)
6api.create_repo("my-org/internal-classifier", private=True, exist_ok=True)
7
8# 從私有變公開(發布時)
9api.update_repo_settings("my-org/internal-classifier", private=False)
10
11# 檢查誰改了什麼
12for c in api.list_repo_commits("my-org/internal-classifier")[:5]:
13 print(f"{c.created_at:%Y-%m-%d} {c.authors[0]:<16} {c.title}")
企業環境的三條規則:
- 內部模型一律 private + 組織 repo,不要放在個人帳號下——人員異動時 repo 會跟著走。
- CI 只給 fine-grained write token,並限定到單一 repo。
- 公開發布前檢查 repo 歷史。 Git 會保留所有 commit,如果你曾經不小心推過含真實客戶資料的 dataset,把它 delete 掉是不夠的——歷史裡還在。這種情況要開新 repo 重推。
六、為什麼選 X 不選 Y
6.1 Gradio vs FastAPI
選擇 選 Gradio 的理由 選 FastAPI 的理由
──────────────────────────────────────────────────────────────────────
Gradio 30 行就有完整 UI + 串流 需要自訂前端與認證
一鍵推到 Spaces 要接既有系統的 API 契約
非工程師能直接試用 要精細控制限流與計量
──────────────────────────────────────────────────────────────────────
翻轉條件:需要 SLA、多租戶、或 UI 要嵌進既有產品時 → FastAPI。
實務上兩者常並存:Gradio 給內部驗證,FastAPI 給正式流量
6.2 vLLM vs TGI
選擇 選 vLLM 的理由 選 TGI 的理由
──────────────────────────────────────────────────────────────────────
vLLM 吞吐通常較高(PagedAttention) Docker 開箱即用,參數少
OpenAI 相容 API,遷移成本近乎零 與 HF 生態整合最緊密
社群大、新模型支援最快 內建 Prometheus metrics
在 HF Inference Endpoints 上原生支援
──────────────────────────────────────────────────────────────────────
翻轉條件:已經在用 HF Inference Endpoints → TGI(就是它在跑)。
要自架且吞吐是首要指標 → vLLM。
兩者差距在 2026 年已經不大,選團隊熟悉的那個
6.3 bitsandbytes vs AWQ / GPTQ
選擇 選 bitsandbytes 的理由 選 AWQ/GPTQ 的理由
──────────────────────────────────────────────────────────────────────
bitsandbytes 任何模型都能即時量化 推論快 1.5–2×
是 QLoRA 微調的必要條件 品質損失更小(有校準)
不需要預先跑量化流程 vLLM/TGI 原生支援
──────────────────────────────────────────────────────────────────────
翻轉條件:訓練/微調 → bitsandbytes(QLoRA 必須)。
生產推論 → AWQ/GPTQ。
自己的微調模型要上線 → 先合併 LoRA 回 bf16,再用 AutoAWQ 量化一次
6.4 push_to_hub vs 自建 S3 / MinIO
選擇 選 Hub 的理由 選自建的理由
──────────────────────────────────────────────────────────────────────
HF Hub 免費 CDN,全球下載快 資料完全不能離開內網
Git 版本控制與 diff 已有 MLflow / S3 的既有流程
Model Card 生態與可搜尋性 需要自訂稽核與保存政策
與所有 HF 工具無縫整合 超大規模下的頻寬成本
──────────────────────────────────────────────────────────────────────
翻轉條件:法遵要求資料不出境 → 自建(可用 huggingface_hub 的 endpoint 參數
指向自架的 Hub-compatible 服務,程式碼幾乎不用改)
6.5 pipeline 批次 vs 推論伺服器
選擇 選 pipeline 批次的理由 選推論伺服器的理由
──────────────────────────────────────────────────────────────────────
pipeline 批次 離線工作負載(一次跑 10 萬筆) 線上請求、要求低延遲
無需常駐服務,跑完即釋放 GPU 並發 > 5
程式碼簡單,好排錯 需要 P99 保證
──────────────────────────────────────────────────────────────────────
翻轉條件:離線 ETL / 資料標注 / 批次嵌入 → pipeline 就夠,
而且成本更低(用完關機)。只要有「使用者在等」→ 推論伺服器
七、系統效應:三種架構的實測對比
情境:7B 模型、繁中客服問答、平均輸入 300 token / 輸出 200 token。
| 指標 | Gradio + transformers | FastAPI + vLLM (A10G) | + 語意快取 + 路由 |
|---|---|---|---|
| 並發能力 | 2–3 | 55 | 55(快取命中不佔 GPU) |
| P50 延遲 | 4,200 ms | 980 ms | 340 ms |
| P99 延遲 | 18,000 ms | 2,100 ms | 2,200 ms |
| 首 token 時間(TTFT) | 3,800 ms | 210 ms | 45 ms(命中時) |
| GPU 使用率 | 22% | 87% | 71% |
| 每百萬 token 成本 | $18.4 | $2.1 | $1.3 |
| 上線所需工時 | 0.5 天 | 4 天 | 12 天 |
這張表要傳達的重點是:從 Gradio 換到 vLLM,成本降低 8.7 倍、延遲降低 4 倍,代價是 3.5 天工時。 這個投資報酬率在有真實流量後幾乎必然划算;但在還沒有使用者之前做,就只是把 3.5 天花在還不確定有沒有人要的產品上。
八、常見錯誤與排查
| 症狀 | 原因 | 解法 |
|---|---|---|
| Space 一直顯示 “Building” | requirements.txt 裝不起來(多半是 torch 版本) | 在 requirements 明確指定 torch==2.x.x,看 Space 的 Build logs |
| vLLM 啟動時 OOM | --gpu-memory-utilization 預設 0.9,加上其他程序爆掉 | 降到 0.85,或關掉佔用 GPU 的其他行程 |
| 推上去的模型別人載不了 | 只推了 model,沒推 tokenizer | tokenizer.push_to_hub(REPO) |
| repo 大小異常(80GB+) | checkpoint 被一起推上去 | ignore_patterns=["checkpoint-*"],已推的需重建 repo |
| 串流卡住不動 | model.generate 在主執行緒阻塞 | 用 Thread + TextIteratorStreamer |
| Docker TGI 多卡啟動卡死 | 少了 --shm-size | 加 --shm-size 1g |
九、動手驗收:把一個模型從挑選到上線
Day 1 用第一節的四道篩選器,選出 3 個候選模型
準備 50 題自己場景的測試集,跑過三個模型並人工評分
↓
Day 2 選定模型,用 AWQ 版本啟 vLLM,量測 P50/P99 與吞吐
寫 Gradio 介面,推到 private Space 給團隊試用
↓
Day 3 收集回饋,寫 FastAPI gateway(認證、限流、串流)
加上 /healthz 與基本 metrics
↓
Day 4 如果有微調需求 → 進入第三篇
如果現成模型已達標 → 直接寫 Model Card 記錄選型依據並上線
十、系列導航
本文是「Hugging Face 實戰」系列的第 2 篇。
← 上一篇: Hugging Face 實戰(一):它到底是什麼,以及如何開始
→ 下一篇: Hugging Face 實戰(三):微調實戰 — Datasets、Trainer 與 LoRA/QLoRA
系列索引:
- 入門與生態系
- 用模型、跑 App、推送自己的模型 ← 目前
- 微調(Fine-tuning)
- 後訓練(Post-training)
- 端到端實戰:打造完整 LLM 應用
