Cách xây dựng LLM từ con số 0: Bài học và hướng dẫn chi tiết
Xây dựng một mô hình ngôn ngữ tối giản từ đầu chỉ cần ít hơn 300 dòng mã Python. Quá trình này giúp bạn thấu hiểu cơ chế token hóa, sự chú ý (attention) và suy luận (inference), từ đó trở thành người tiêu thụ API giỏi hơn khi tích hợp các LLM sản xuất vào ứng dụng.

Cách xây dựng LLM từ con số 0: Bài học và hướng dẫn chi tiết
Việc xây dựng một mô hình ngôn ngữ tối thiểu từ con số không chỉ tốn ít hơn 300 dòng mã Python. Quá trình này giúp làm rõ chính xác cách thức hoạt động của token hóa, sự chú ý (attention) và suy luận (inference), qua đó biến bạn thành một người tiêu thụ API giỏi hơn rất nhiều khi tích hợp các LLM sản xuất vào ứng dụng của mình.
Giới thiệu
Hầu hết các nhà phát triển coi các mô hình ngôn ngữ lớn (LLM) như những "hộp đen". Bạn gửi văn bản đầu vào, các token (đơn vị mã hóa) xuất ra, và ở đâu đó ở giữa, phép màu xảy ra. Mô hình tư duy này hoạt động tốt cho đến khi bạn cần gỡ lỗi một tích hợp API bị hỏng, điều chỉnh các tham số lấy mẫu (sampling parameters) hoặc tìm hiểu lý do tại sao mô hình của bạn liên tục bị ảo giác (hallucinating) dữ liệu có cấu trúc.
GuppyLM, một dự án gần đây đã lọt lên trang chủ của HackerNews với 842 điểm, đã làm cho các chi tiết nội tại trở nên rõ ràng. Đây là một transformer với 8,7 triệu tham số, được viết từ đầu bằng Python. Nó có thể được huấn luyện trong vòng chưa đầy một giờ trên một GPU dành cho người tiêu dùng. Toàn bộ mã nguồn nằm gọn trong một tệp duy nhất. Mục tiêu không phải để cạnh tranh với GPT-4, mà là để giải mã những gì LLM thực sự làm.
Bài viết này sẽ mô tả cách xây dựng một LLM siêu nhỏ, chức năng của từng thành phần và những bài học về chi tiết nội bộ mà nó mang lại khi bạn làm việc chuyên nghiệp với các API AI.
💡 Nếu bạn đang kiểm thử các tích hợp API AI, các Kịch bản Kiểm thử (Test Scenarios) của Apidog cho phép bạn xác minh các phản hồi dạng luồng (streaming), đưa ra các khẳng định (assertions) về cấu trúc token và mô phỏng các trường hợp ngoại lệ mà không tốn tín dụng sản xuất. Chúng ta sẽ nói kỹ hơn về điều này sau.
Điều gì làm cho một mô hình ngôn ngữ trở nên "siêu nhỏ"?
Một LLM sản xuất như GPT-4 có hàng trăm tỷ tham số. Một LLM "siêu nhỏ" nằm trong phạm vi từ 1 triệu đến 25 triệu tham số. Ví dụ: GuppyLM (8,7 triệu), nanoGPT của Karpathy (124 triệu), MicroLM (1-2 triệu).
Những gì bạn có thể làm với LLM siêu nhỏ:
- Huấn luyện trên laptop hoặc Google Colab.
- Nằm hoàn toàn trong bộ nhớ CPU.
- Kiểm tra, sửa đổi và gỡ lỗi mô hình ở cấp độ trọng số (weight).
Hạn chế:
- Không xử lý được suy luận phức tạp.
- Không tạo ra văn bản dài mạch lạc một cách đáng tin cậy.
- Không có chiều sâu về mặt thực tế như các mô hình sản xuất.
Giá trị nằm ở sự hiểu biết kỹ thuật, không phải ở kết quả đầu ra.
Các thành phần cốt lõi: LLM thực sự hoạt động như thế nào
Trước khi triển khai, hãy hiểu bốn phần chính:
Tokenizer (Bộ token hóa)
Tokenizer chuyển đổi văn bản thành các ID số nguyên. Ví dụ, "Xin chào, thế giới!" có thể trở thành [15496, 11, 995, 0]. Mỗi số nguyên tương ứng với một từ phụ (subword) trong một từ vựng cố định.
Tại sao điều này quan trọng với API: Việc đếm token ảnh hưởng đến độ trễ và chi phí. Hiểu cách tokenizer chia nhỏ văn bản giúp bạn viết các câu lệnh (prompt) vừa vặn trong cửa sổ ngữ cảnh và tránh bị cắt bớt.
- GuppyLM: Token hóa theo ký tự.
- GPT-4 (sản xuất): Token hóa BPE (byte-pair encoding) với từ vựng 50K-100K.
Lớp Embedding (Nhúng)
Chuyển đổi ID token thành các vector dày đặc (ví dụ: 384 chiều). Các token tương tự sẽ nằm gần nhau trong không gian vector. Các embedding vị trí được cộng thêm để thông báo cho mạng về thứ tự.
Các khối Transformer
Mỗi khối chứa:
- Self-attention (Tự chú ý): Cho phép mỗi token "nhìn thấy" tất cả các token khác trong chuỗi để dự đoán token tiếp theo.
- GuppyLM: 6 đầu chú ý trong 6 lớp.
- Feed-forward network (Mạng truyền tiếp): MLP hai lớp, kích hoạt ReLU (đơn giản hơn SwiGLU của các mô hình gần đây).
Lớp đầu ra
Sau khối transformer cuối cùng, một lớp tuyến tính chiếu các vector về kích thước từ vựng. Áp dụng softmax để xác suất, chọn (hoặc lấy mẫu) token tiếp theo, và lặp lại.
Xây dựng LLM tối thiểu bằng Python
Dưới đây là một LLM chức năng dựa trên GuppyLM, sử dụng PyTorch tiêu chuẩn:
import torch
import torch.nn as nn
import torch.nn.functional as F
# Siêu tham số
VOCAB_SIZE = 256 # một slot cho mỗi ký tự ASCII
D_MODEL = 128 # chiều của embedding
N_HEADS = 4 # đầu chú ý
N_LAYERS = 3 # khối transformer
SEQ_LEN = 64 # cửa sổ ngữ cảnh
DROPOUT = 0.1
class SelfAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
self.n_heads = n_heads
self.head_dim = d_model // n_heads
self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
self.proj = nn.Linear(d_model, d_model, bias=False)
self.dropout = nn.Dropout(DROPOUT)
def forward(self, x):
B, T, C = x.shape
qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim)
q, k, v = qkv.unbind(dim=2)
q = q.transpose(1, 2)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
# Mặt nạ nhân quả: mỗi token chỉ "thấy" các token trước đó
scale = self.head_dim ** -0.5
attn = (q @ k.transpose(-2, -1)) * scale
mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool()
attn = attn.masked_fill(mask, float('-inf'))
attn = F.softmax(attn, dim=-1)
attn = self.dropout(attn)
out = (attn @ v).transpose(1, 2).reshape(B, T, C)
return self.proj(out)
class TransformerBlock(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
self.attn = SelfAttention(d_model, n_heads)
self.ff = nn.Sequential(
nn.Linear(d_model, 4 * d_model),
nn.ReLU(),
nn.Linear(4 * d_model, d_model),
nn.Dropout(DROPOUT),
)
self.ln1 = nn.LayerNorm(d_model)
self.ln2 = nn.LayerNorm(d_model)
def forward(self, x):
x = x + self.attn(self.ln1(x))
x = x + self.ff(self.ln2(x))
return x
class TinyLLM(nn.Module):
def __init__(self):
super().__init__()
self.embed = nn.Embedding(VOCAB_SIZE, D_MODEL)
self.pos_embed = nn.Embedding(SEQ_LEN, D_MODEL)
self.blocks = nn.ModuleList([
TransformerBlock(D_MODEL, N_HEADS) for _ in range(N_LAYERS)
])
self.ln_f = nn.LayerNorm(D_MODEL)
self.head = nn.Linear(D_MODEL, VOCAB_SIZE, bias=False)
def forward(self, idx):
B, T = idx.shape
tok_emb = self.embed(idx)
pos = torch.arange(T, device=idx.device)
pos_emb = self.pos_embed(pos)
x = tok_emb + pos_emb
for block in self.blocks:
x = block(x)
x = self.ln_f(x)
logits = self.head(x)
return logits
# Khởi tạo và đếm tham số
model = TinyLLM()
total_params = sum(p.numel() for p in model.parameters())
print(f"Model size: {total_params:,} parameters") # ~1.2M
Vòng lặp huấn luyện
import torch.optim as optim
def train(model, data, epochs=100, lr=3e-4):
optimizer = optim.AdamW(model.parameters(), lr=lr)
model.train()
for epoch in range(epochs):
# data: tensor ID token, shape [batch, seq_len+1]
x = data[:, :-1] # đầu vào: tất cả trừ cái cuối
y = data[:, 1:] # đích: tất cả trừ cái đầu
logits = model(x)
loss = F.cross_entropy(logits.reshape(-1, VOCAB_SIZE), y.reshape(-1))
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 0:
print(f"Epoch {epoch}, loss: {loss.item():.4f}")
Suy luận (tạo văn bản)
@torch.no_grad()
def generate(model, prompt_ids, max_new_tokens=50, temperature=1.0, top_k=10):
model.eval()
ids = torch.tensor([prompt_ids])
for _ in range(max_new_tokens):
idx_cond = ids[:, -SEQ_LEN:] # cắt theo cửa sổ ngữ cảnh
logits = model(idx_cond)
logits = logits[:, -1, :] / temperature # chỉ token cuối cùng
# lấy mẫu top-k
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = float('-inf')
probs = F.softmax(logits, dim=-1)
next_id = torch.multinomial(probs, num_samples=1)
ids = torch.cat([ids, next_id], dim=1)
return ids[0].tolist()
Điều này dạy chúng ta gì về hành vi của API AI
Bằng cách triển khai mô hình này, bạn sẽ hiểu thực tế cách tiêu thụ API AI hiệu quả hơn.
Nhiệt độ và lấy mẫu là cơ học, không phải phép màu
Nhiệt độ tác động lên logits trước softmax. Nhiệt độ cao = phân bố phẳng hơn = ngẫu nhiên hơn. Nhiệt độ thấp = phân bố "cứng" hơn = tính xác định cao hơn. Nếu API của bạn trả về kết quả không nhất quán với temperature=0.0, đó không phải lỗi: argmax thuần túy có thể được điều chỉnh bởi các API để tránh đầu ra suy biến.
Cửa sổ ngữ cảnh là giới hạn cứng
Dòng idx_cond = ids[:, -SEQ_LEN:] cho thấy mô hình loại bỏ các token cũ. Đừng tin rằng lịch sử hội thoại luôn được API ghi nhớ sau khi cửa sổ ngữ cảnh bị vượt quá.
Token dạng luồng (streaming) chỉ là các bước suy luận được lộ ra
Các API streaming chỉ lộ từng token được tạo ra trong vòng lặp suy luận. Nếu streaming bị gián đoạn, bạn cần khởi động lại – không thể tiếp tục từ giữa chừng.
Logits giải thích sự khó khăn của đầu ra có cấu trúc
Mô hình cần dự đoán đúng token tại mỗi bước để tạo ra, ví dụ, JSON hợp lệ. Các công cụ như Outlines và Guidance hạn chế phân bố logits để áp dụng ngữ pháp. Các API AI có "đầu ra có cấu trúc" thực hiện việc này nội bộ.
Cách kiểm thử tích hợp API AI với Apidog
Với sự hiểu biết về cách hoạt động của LLM, bạn có thể tạo các bài kiểm thử API tốt hơn. Các Kịch bản Kiểm thử của Apidog cho phép phối hợp các cuộc gọi API và đưa ra khẳng định về phản hồi của AI.
Ví dụ kiểm thử cho API chat dạng streaming:
- Tạo Kịch bản Kiểm thử trong Apidog với endpoint
/v1/chat/completionscủa bạn. - Định nghĩa các khẳng định về phản hồi:
response.choices[0].finish_reason == "stop"response.usage.total_tokens < 4096
- Thêm một bước gửi phản hồi dưới dạng ngữ cảnh để mô phỏng các cuộc hội thoại nhiều lượt (multi-turn).
- Sử dụng Smart Mock của Apidog để mô phỏng các phản hồi của AI và kiểm tra lỗi:
- Mô phỏng
finish_reason: "length",finish_reason: "content_filter"và timeout giữa streaming.
- Mô phỏng
Như vậy, bạn kiểm thử tích hợp AI mà không tốn tín dụng API cho mỗi lần chạy CI.
Kiểm thử khẳng định đếm token
{
"assertions": [
{
"field": "response.usage.completion_tokens",
"operator": "less_than",
"value": 512
},
{
"field": "response.choices[0].finish_reason",
"operator": "equals",
"value": "stop"
},
{
"field": "response.choices[0].message.content",
"operator": "not_empty"
}
]
}
Hãy chạy điều này trên nhiều mô hình (GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro) trong một Kịch bản Kiểm thử duy nhất để nắm bắt các khác biệt về lược đồ API trước khi đưa vào sản xuất.
Nâng cao: Lượng tử hóa và tối ưu hóa suy luận
Với LLM siêu nhỏ hoạt động tốt, hãy hiểu hai kỹ thuật thiết yếu cho môi trường sản xuất:
Lượng tử hóa (Quantization)
Các trọng số mô hình mặc định là float 32 bit. Lượng tử hóa giảm xuống INT8 hoặc INT4, tiết kiệm bộ nhớ (ít hơn 4-8 lần) với ít mất mát về độ chính xác.
# Ví dụ: lượng tử hóa động INT8 trong PyTorch
import torch.quantization
quantized_model = torch.quantization.quantize_dynamic(
model, {nn.Linear}, dtype=torch.qint8
)
Các API thường chạy các mô hình đã được lượng tử hóa. Sự khác biệt về chất lượng giữa các phiên bản mô hình thường liên quan đến lượng tử hóa.
Bộ nhớ đệm KV (KV Cache)
Trong vòng lặp suy luận ở trên, chúng tôi tính toán lại sự chú ý cho toàn bộ chuỗi ở mỗi token. Các hệ thống sản xuất lưu trữ bộ nhớ đệm các cặp khóa-giá trị (KV) của các token trước đó. Do đó, chỉ token mới cần tính toán thêm. Điều này giải thích tại sao token đầu tiên của streaming là chậm nhất.
LLM siêu nhỏ so với API sản xuất: Khi nào dùng cái nào
| Trường hợp sử dụng | LLM siêu nhỏ | API sản xuất |
|---|---|---|
| Học chi tiết nội tại mô hình | Tốt nhất | Quá mức |
| Tạo mẫu ứng dụng mới | Chất lượng chưa đủ | Tốt nhất |
| Dữ liệu riêng tư/cảm giác | Lựa chọn tốt | Tùy nhà cung cấp |
| Triển khai ngoại tuyến/edge | Khả thi | Không thể |
| Nhạy cảm về chi phí, lượng lớn | Có thể với sự đánh đổi | Đắt ở quy mô lớn |
| Tác vụ đòi hỏi suy luận | Không khả thi | Cần thiết |
Khuyến nghị thực tế: sử dụng API sản xuất cho ứng dụng của bạn, nhưng chạy một mô hình siêu nhỏ để hiểu những gì đang diễn ra "phần sau cánh gà". Hai cái này bổ sung cho nhau.
Kết luận
Xây dựng một LLM siêu nhỏ từ đầu mất một cuối tuần. Mục tiêu không phải là sản xuất, mà là hiểu cách mọi mô hình ngôn ngữ – từ GuppyLM đến GPT-4o – thực sự hoạt động. Kiến thức này hữu ích khi gỡ lỗi các tích hợp streaming, điều chỉnh tham số lấy mẫu hoặc tạo bài kiểm thử cho các API AI.
Dự án GuppyLM là một điểm khởi đầu tuyệt vời. Hãy clone, huấn luyện trên một tập dữ liệu văn bản và phân tích vòng lặp suy luận. Sau đó, quay lại các tích hợp API của bạn và nhìn mọi thứ dưới một góc độ khác.
Hãy thử các Kịch bản Kiểm thử của Apidog để mang lại sự nghiêm ngặt cho các bài kiểm thử API AI, giống như cách bạn kiểm tra bất kỳ backend nào khác.
Câu hỏi thường gặp (FAQ)
Một LLM "siêu nhỏ" cần bao nhiêu tham số để tạo văn bản mạch lạc? Khoảng 10M-50M với một tập dữ liệu khá sẽ tạo ra các câu mạch lạc tại chỗ. Dưới 1M, nó có xu hướng tạo ra các ký tự vô nghĩa. GuppyLM (8,7M) hoạt động tốt cho các cuộc hội thoại ngắn trong miền huấn luyện của nó (60 chủ đề).
Tôi có thể chạy LLM siêu nhỏ mà không cần GPU không? Có. Các mô hình dưới 100M chạy tốt trên CPU, dù chậm hơn. Mô hình trên (1,2M) tạo ra token trong vài mili-giây trên laptop.
Nên huấn luyện trên tập dữ liệu nào?
Mô hình cấp ký tự: văn bản từ Dự án Gutenberg, Wikipedia hoặc ngữ liệu đơn giản. GuppyLM sử dụng các cuộc hội thoại (60K mục) trên HuggingFace (arman-bd/guppylm-60k-generic). Đối với mã, hãy dùng The Stack hoặc CodeParrot.
Sự khác biệt giữa nhiệt độ và top-k là gì? Nhiệt độ điều chỉnh độ ngẫu nhiên chung. Top-k giới hạn các ứng viên ở k khả năng cao nhất trước khi áp dụng nhiệt độ. Kết hợp cả hai: trước hết top-k lọc, sau đó nhiệt độ định nghĩa phân bố.
Tại sao LLM của tôi lại lặp lại?
Sự lặp lại xảy ra khi mô hình gán xác suất cao cho các token đã tạo. Các API sản xuất sử dụng hình phạt lặp lại (repetition_penalty=1.1) để giảm thiểu điều này.
Mất bao lâu để huấn luyện một LLM siêu nhỏ? Mô hình trên đạt độ mạch lạc trong chưa đầy 2 giờ trên một GPU (RTX 3060). GuppyLM huấn luyện trên Colab trong thời gian tương tự. Các mô hình lớn hơn (100M+) yêu cầu multi-GPU và nhiều ngày huấn luyện.
Làm thế nào để biến LLM siêu nhỏ thành endpoint API thực sự? Xuất sang GGUF với script của llama.cpp và phục vụ qua llama-server. Như vậy, bạn sẽ có một endpoint tương thích OpenAI cục bộ. Apidog có thể kiểm tra trực tiếp trên endpoint này.
Các LLM sản xuất xử lý ngữ cảnh lớn hơn như thế nào? Các kỹ thuật như RoPE (Rotary Position Embedding) với mở rộng quy mô, sự chú ý cửa sổ trượt và RAG (retrieval augmented generation) mở rộng ngữ cảnh. Transformer cốt lõi không thay đổi; mẹo nằm ở cách mã hóa vị trí và áp dụng cửa sổ chú ý.



