ที่ 3DN เราใช้ local inference บน local workstation GPU (RTX 3090) สำหรับ family-site machine translation และ hybrid AI engineering ร่างงาน Bulk EN→NL และ EN→ZH ผ่าน Tower บน Ollama ส่วน Thai เป็น weak link เราจึงใช้ Typhoon สำหรับ TH วันนี้ ขั้นตอนถัดไปไม่ใช่ “buy more API” แต่เป็น deliberate LoRA experiment บน infrastructure เดียวกัน ภายใต้หลังคาของเรา เพื่อ digital sovereignty และลด long-run token cost
โพสต์นี้คือ public plan: สิ่งที่เรา train, เหตุผลที่เลือก base model นั้น, วิธีรักษา production safe, และตำแหน่งงานของ Unbabel Snippets ด้านล่างคือ shape ของ lab — ไม่ใช่ paste-and-forget cookbook
ทำไมต้องใช้ LoRA เลย?
Full fine-tunes ของ multi-billion-parameter translation models มีค่าใช้จ่ายสูงและง่ายต่อการ overfit LoRA (low-rank adapter) train thin set of matrices บน frozen base สำหรับ desk ที่มี Tower weights อยู่แล้ว นั่นคือ learning curve ที่เหมาะสม: measurable EN→TH gains, Hugging Face adapter ที่เราสามารถ publish ได้ และ merge แบบ optional ในอนาคตหากต้องการ Ollama pin เฉพาะ
เราไม่ได้ train GGUF blob ที่ Ollama ให้บริการทุกวัน การ train ใช้ Hugging Face / PEFT-style weights; การ serving ยังคงอยู่บน Tower-Plus Q4 path ปัจจุบันจนกว่า eval จะบอกเป็นอย่างอื่น
# Mental model: freeze the base, train thin adapters
# ΔW ≈ B @ A with rank r << d_model
# Only A, B (and maybe biases) get gradients.
from peft import LoraConfig, get_peft_model, TaskType
lora = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"], # start small; expand if underfit
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM,
)
# model = get_peft_model(base_model, lora)
# model.print_trainable_parameters() # expect ~0.1–1% trainable
Base model: TowerInstruct-7B v0.2
Community guidance (และการอ่านของเราเอง) ชี้ให้เริ่มจาก TowerInstruct ไม่ใช่ generic chat LLM ตระกูลนี้สร้างมาเพื่อ translation-shaped prompts — pattern เดียวกับที่เราใช้บน desk อยู่แล้ว
เรากำลังติดตั้ง Unbabel/TowerInstruct-7B-v0.2 เป็น train base แรก:
- 7B เหมาะกับ QLoRA บน desktop 3090 และมีพื้นที่สำหรับ iterate
- v0.2 ดีกว่า v0.1 ในแง่ของ instruct/MT behaviour ที่สะอาดขึ้น
- 13B หรือ Tower-Plus-9B หลังจาก recipe 7B ใช้งานได้ — มี capacity มากกว่า, loops ช้าลง
เราใช้ QLoRA บน 4-bit quantized model ที่ Hugging Face เพื่อประหยัด memory และค่าใช้จ่ายในการ train
from peft import LoraConfig
from transformers import BitsAndParameters
lora = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM,
)
model = get_peft_model(base_model, lora)
# model = model.to_4bit(
# load_in_4bit=True,
# bnb_4bit_use_double_quant=True,
# bnb_4bit_quant_type="nf4",
# bnb_4bit_compute_dtype=torch.float16
# )
Training setup
เราใช้ accelerate ร่วมกับ deepspeed เพื่อจัดการ distributed training และ memory management
from accelerate import init_empty_weights, get_scheduler
from deepspeed import zero
from transformers import AdamW
# Initialize model with empty weights
with init_empty_weights():
model = get_peft_model(base_model, lora)
# Setup optimizer and scheduler
optimizer = AdamW(model.parameters(), lr=2e-5)
scheduler = get_scheduler("linear", optimizer, num_warmup_steps=1000, num_training_steps=10000)
# Zero Redundancy Optimizer (ZeRO) for memory efficiency
zero.init_zero_3(model, stage=3)
Training loop
เราใช้ train.py เป็น entrypoint สำหรับ training loop
from train import train
if __name__ == "__main__":
train()
Training script
สคริปต์ train ที่ใช้ใน train.py:
import torch
from accelerate import init_empty_weights, get_scheduler
from transformers import AdamW
# Initialize model with empty weights
with init_empty_weights():
model = get_peft_model(base_model, lora)
# Setup optimizer and scheduler
optimizer = AdamW(model.parameters(), lr=2e-5)
scheduler = get_scheduler("linear", optimizer, num_warmup_steps=1000, num_training_steps=10000)
# Zero Redundancy Optimizer (ZeRO) for memory efficiency
zero.init_zero_3(model, stage=3)
# Training loop
for epoch in range(1):
model.train()
for batch in train_dataloader:
inputs = {k: v.to(device) for k, v in batch.items()}
outputs = model(**inputs)
loss = outputs.loss
loss.backward()
optimizer.step()
scheduler.step()
optimizer.zero_grad()
Training data
เราใช้ train.jsonl เป็น training data ที่มีรูปแบบ JSONL (JSON Lines)
{"src": "English: What is the capital of France?", "tgt": "ไทย: เมืองหลวงของฝรั่งเศสคืออะไร?"}
{"src": "English: Who wrote 'Pride and Prejudice'?", "tgt": "ไทย: ใครเป็นผู้เขียน 'Pride and Prejudice'?"}
Training metrics
เราติดตาม training metrics ด้วย tensorboard และ wandb
from tensorboard import program
from wandb import init as wandb_init
# TensorBoard
tb = program.TensorBoard()
tb.configure(host="0.0.0.0", port=6006, logdir="./logs")
tb.start()
# Weights & Biases
wandb_init(project="3dn-translation", entity="3dn")
Training logs
เราบันทึก training logs ลงใน ./logs โดยใช้ deepspeed และ accelerate
from deepspeed import zero
from accelerate import get_scheduler
# Zero Redundancy Optimizer (ZeRO) for memory efficiency
zero.init_zero_3(model, stage=3)
# Setup optimizer and scheduler
optimizer = AdamW(model.parameters(), lr=2e-5)
scheduler = get_scheduler("linear", optimizer, num_warmup_steps=1000, num_training_steps=10000)
# Training loop
for epoch in range(1):
model.train()
for batch in train_dataloader:
inputs = {k: v.to(device) for k, v in batch.items()}
outputs = model(**inputs)
loss = outputs.loss
loss.backward()
optimizer.step()
scheduler.step()
optimizer.zero_grad()
# Save logs
torch.save(model.state_dict(), "./logs/model.pth")
Training evaluation
เราประเมินผล training ด้วย eval.py และใช้ eval.jsonl เป็น evaluation data
from eval import evaluate
import torch
# Load model and tokenizer
model = get_peft_model(base_model, lora)
model.load_state_dict(torch.load("./logs/model.pth"))
# Evaluate
evaluate(model, "./eval.jsonl")
Unbabel’s Tower line lives at unbabel.com. Public weights: Hugging Face Unbabel/TowerInstruct-7B-v0.2.
# Lab layout on a local workstation (HF weights, not Ollama GGUF)
# …/tower-lora/
# modelsTowerInstruct-7B-v0.2 # safetensors shards
# datasetsen-th # jsonl pairs
# adaptersen-th-r16 # PEFT output
# out
# Stage download on a build host, then LAN copy (multi-GB, offline train later):
python -c "from huggingface_hub import snapshot_download;
snapshot_download('Unbabel/TowerInstruct-7B-v0.2',
local_dir='/var/tmp/hf-models/TowerInstruct-7B-v0.2')"
รูปร่างข้อมูล (โปรแกรมเมอร์สนใจ schema)
หนึ่งแถว หนึ่งงาน จับคู่ prompt style ที่เราใช้ใน production MT เพื่อให้ train ≈ serve:
{
"instruction": "แปลข้อความภาษาอังกฤษต่อไปนี้เป็นภาษาไทย",
"input": "Every voter deserves a market.",
"output": "ผู้มีสิทธิเลือกตั้งทุกคนสมควรได้รับตลาด"
}
# Tiny loader sketch — holdout is sacred
import json
from pathlib import Path
def load_jsonl(path: Path):
rows = []
for line in path.read_text(encoding="utf-8").splitlines():
if line.strip():
rows.append(json.loads(line))
return rows
def split_holdout(rows, frac=0.1, seed=7):
import random
rng = random.Random(seed)
idx = list(range(len(rows)))
rng.shuffle(idx)
n = max(1, int(len(rows) * frac))
hold = {idx[i] for i in range(n)}
train = [rows[i] for i in idx if i not in hold]
test = [rows[i] for i in idx if i in hold]
return train, test
# แนะนำ: เคปี EN ของเรา + Typhoon draft + light human gold edit
# หลีกเลี่ยง: unedited bulk MT soup, HTML debris, mixed dual-language lines
วิธีที่เราจะดำเนินงาน
- ติดตั้ง HF base บน local workstation — full safetensors tree ภายใต้
…/tower-lora/models(download staged บน build host, แล้ว LAN copy) - ข้อมูลก่อน rank — คู่ EN→TH สะอาดไม่กี่ร้อยถึงสองพันคู่; hold out ~10% ตลอดไป
- Train QLoRA — rank 8–16, short context, หนึ่งงานหนักบน 3090 ครั้งละหนึ่ง (unload competing Ollama models)
- Evaluate อย่างซื่อสัตย์ — base Tower vs LoRA (และต่อมา Typhoon); chrF/BLEU เป็นแนวทาง, มนุษย์สำหรับชื่อและ product terms ตาราง holdout แรกอยู่ด้านบน
- เผยแพร่เมื่อชนะ — Hugging Face adapter card; จากนั้นจึง merge→GGUF / optional
local workstation-tower-thpin
# QLoRA load sketch (4-bit base + LoRA) — 3090-friendly
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch
bnb = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
base_id = r"…/tower-lora/modelsTowerInstruct-7B-v0.2"
tok = AutoTokenizer.from_pretrained(base_id, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
base_id,
quantization_config=bnb,
device_map="auto",
torch_dtype=torch.bfloat16,
)
# then get_peft_model(model, lora_config) and Trainer / custom loop
# Production MT prompt shape we already use (train should rhyme)
def tower_prompt(en: str, lang_name: str = "Thai") -> str:
return (
f"Translate the following English source text to {lang_name}.n"
f"English: {en.strip()}n"
f"{lang_name}:n"
)
# Desk rule until LoRA wins holdout:
# NL/ZH -> local workstation-tower (Tower-Plus)
# TH -> local Typhoon-translate
# judgment / SEO -> frontier Grok (not bulk MT)
# Ship gate: never trust vibes
def better_than_baseline(scores: dict) -> bool:
# scores = {"tower": chrF, "lora": chrF, "typhoon": chrF}
return scores["lora"] > scores["tower"] + 0.5 # margin TBD on real holdout
# if not better_than_baseline(...): keep Typhoon in prod; LoRA stays lab
นโยบาย Production MT ยังคงเป็น dual-track จนกว่าการประเมินจะบอกเป็นอย่างอื่น: Tower สำหรับ NL/ZH, Typhoon สำหรับ TH, frontier Grok สำหรับ judgment — ไม่ใช่สำหรับ bulk translation
ความสำเร็จมีลักษณะอย่างไร
- สคริปต์การ train และ schema ของ dataset ที่สามารถทำซ้ำได้บน local workstation
- adapter ที่ชนะ TowerInstruct แบบ stock บน Thai holdout ของเรา โดยไม่ทำลาย pairs ที่เราจะเพิ่มในภายหลัง
- Hugging Face card ที่ผู้อื่น (และเราในอนาคต) สามารถโหลดได้ด้วย PEFT บน base เดียวกัน
- ไม่มี romance: หาก Typhoon ยังชนะอยู่ TH production ก็ยังคงใช้ Typhoon
# หลังการ train — สิ่งที่เราคาดหวังบน disk
# adapters/en-th-r16/
# adapter_config.json
# adapter_model.safetensors
# README.md # base id, pair, r, data size, license, eval table
Compute และ ops
นี่คือ managed hosting และ lab compute บน metal ที่เราควบคุม — ปรัชญาเดียวกันกับ hybrid coding agents และ local Flux thumbs การใช้งานระยะยาวยังคงได้รับประโยชน์จาก prompt cache และ session continuity ที่รอบคอบบน director model; งาน LoRA เองเป็น GPU batch ที่จำกัด ไม่ใช่ API ที่เปิดตลอดเวลา
ในส่วนของ family money spine ความซื่อตรงเดียวกันนี้ยังคงใช้บังคับ: virtual credits และ DutchBud / fintech rails จะทำงานได้ก็ต่อเมื่อ state สอดคล้องกับ claims เท่านั้น ตัว translation adapter จะ “ships” ก็ต่อเมื่อ eval สอดคล้องกับ card
# One heavy job on the 3090 — do not co-host train + Flux + Ollama-14B
# (ops checklist, not poetry)
# 1) stop competing GPU consumers
# 2) train QLoRA
# 3) write adapter
# 4) free VRAM, restore Ollama desk pins
Status — first lab numbers (smoke, not production)
ตัว QLoRA adapter ตัวแรกอยู่บน disk แล้ว Train set: 110 curated EN→TH pairs; holdout: 20 unseen lines. Recipe: TowerInstruct-7B-v0.2 base, rank r=16 on q/k/v/o, ChatML prompts, 3 epochs, train loss ≈ 1.77 (~3.5 minutes after weight load on a single desktop 3090).
Holdout automatic scores (sacrebleu corpus BLEU / chrF, greedy decode, EOS stop):
| Model | BLEU | chrF |
|---|---|---|
| Base TowerInstruct-7B-v0.2 (4-bit) | 2.06 | 14.54 |
| Base + EN→TH LoRA r16 | 2.88 | 16.22 |
| Delta | +0.82 | +1.68 |
อ่านนี้อย่างเย็นชา: ตัว adapter ชนะ stock Tower ใน holdout เล็กๆ นี้ด้วยคะแนนที่ต่ำกว่าเล็กน้อย แต่คะแนนสัมบูรณ์ยังคงต่ำ — base แย่ใน Thai บน desk ของเรา และ 110 แถวเป็น pipeline proof ไม่ใช่ production corpus เรา ไม่ได้ ปิด family-site Thai MT บน Typhoon จากตัวเลขเหล่านี้ ประตู ship จากแผนยังคงเปิด: LoRA จะแทนที่ Typhoon ก็ต่อเมื่อ holdout + human review ยืนยันจาก gold set ที่ใหญ่กว่า
สิ่งที่เราพิสูจน์ได้: QLoRA train → PEFT adapter save → base-vs-adapter eval ด้วย BLEU/chrF บน workstation stack เดียวกัน, offline หลังจาก HF weight tree อยู่ local ขั้นตอน lab ถัดไปคือ EN→TH gold เพิ่มเติม (และ Typhoon-draft + edit อย่างระมัดระวัง) ไม่ใช่คำกล่าวอ้างที่โรแมนติก
อ่านเพิ่มเติม: Unbabel · TowerInstruct-7B-v0.2 on Hugging Face
ใส่ความเห็น