0

Chi Phí Token Tiếng Việt Thực Tế: Không Phải Đắt Gấp 4.5 Lần

Khi đưa LLM vào hệ thống production, mỗi token gửi đi và nhận về đều biến thành chi phí trên hóa đơn hàng tháng — và khoản chi phí đó không hề dừng lại dù prompt không còn thay đổi, điều mình nhận ra khi truy tìm lý do hóa đơn của một coding agent tăng nhanh hơn cả cuộc hội thoại.

Một đoạn prompt nhìn có vẻ ngắn nhưng qua tokenizer có thể phình to bất ngờ, đặc biệt là với tiếng Việt — điều mình từng gặp lần đầu khi benchmark các model BERT tiếng Việt. Khi xây dựng pipeline lắng nghe mạng xã hội xử lý hàng nghìn bình luận mỗi giờ, chi phí thực tế của mình từng đội lên nhanh chóng.

Tìm kiếm trên mạng, con số phổ biến nhất xuất hiện là: Tiếng Việt tốn gấp 4.54 lần số token so với tiếng Anh.

Con số này nghe rất giật gân, nhưng thực chất nó đo lường các tokenizer từ năm 2019 mà ngày nay chẳng còn ai dùng trong production.

Mình đã benchmark 13 tokenizer trên cùng một đoạn văn đối chiếu tiếng Anh - tiếng Việt. Kết quả: các tokenizer hiện đại chỉ tốn từ 1.05x đến 2.14x (PhoBERT thậm chí đạt 0.87x). Việc nâng cấp thế hệ tokenizer giúp giảm tới 37% chi phí mà không cần thay đổi prompt.

Nguồn Gốc Con Số "Thuế Token 4.54x"

Con số 4.54x bắt nguồn từ các bài viết đo trên GPT-2 và RoBERTa.

Những tokenizer này được huấn luyện thuần tiếng Anh và không có bảng mã dấu tiếng Việt. Khi gặp các từ như "được", "nghĩa", "những", chúng buộc phải xé nhỏ từng ký tự thành 4-5 mảnh token.

Đó là nhược điểm của công nghệ năm 2019, không phải bản chất của tiếng Việt.

Bảng Đo Lường Thực Tế Trên 13 Tokenizer

Tokenizer Thế hệ Tỷ lệ (Token VN / Token EN)
gpt2 Cũ, thuần tiếng Anh 4.03x
roberta-base Cũ, thuần tiếng Anh 3.98x
phobert-base* Chuyên biệt tiếng Việt 0.87x
gpt2-vietnamese Chuyên biệt tiếng Việt 1.05x
xlm-roberta-base Đa ngôn ngữ hiện đại 1.14x
mmBERT-base Đa ngôn ngữ hiện đại 1.24x
qwen2.5-7b Đa ngôn ngữ hiện đại 1.26x
qwen3-8b Đa ngôn ngữ hiện đại 1.26x
o200k_base (GPT-4o / GPT-4.1) Đa ngôn ngữ hiện đại 1.34x
deepseek-v3.2 Model suy luận đời mới 1.88x
cl100k_base (GPT-3.5 / GPT-4) Đa ngôn ngữ hiện đại 2.14x

* PhoBERT hoạt động tối ưu nhất khi tách từ trước; ở đây dùng văn bản thô nên tỷ lệ 0.87x là con số sàn.

Giảm 37% Chi Phí Miễn Phí Chỉ Nhờ Đổi Endpoint

Điểm đáng chú ý nhất là khoảng cách giữa hai thế hệ tokenizer của OpenAI:

  • cl100k_base (GPT-3.5 và GPT-4): 2.14x.
  • o200k_base (GPT-4o và GPT-4.1): 1.34x.

Chuyển từ GPT-4 sang GPT-4o giúp bạn tự động tiết kiệm ~37% lượng token tiếng Việt mà không cần sửa một dòng code logic nào. Bộ từ vựng mới của OpenAI bao phủ các âm tiết tiếng Việt đầy đủ hơn hẳn.

Model Đời Mới Chưa Chắc Đã Tiết Kiệm Token

DeepSeek-V3.2 là một model suy luận rất mạnh, nhưng tokenizer tiếng Việt của nó ở mức 1.88x, cao hơn nhiều so với mmBERT (1.24x) hay Qwen3 (1.26x). Năng lực tư duy và hiệu quả mã hóa token là hai câu chuyện hoàn toàn khác nhau.

Các tokenizer chuyên biệt như PhoBERT (0.87x) và gpt2-vietnamese (1.05x) thậm chí còn tốn ít token hơn tiếng Anh. Rõ ràng, việc tiếng Việt tốn nhiều token phụ thuộc vào tokenizer bạn chọn, không phải do ngôn ngữ — bài học về thiết kế bộ từ vựng y hệt những gì mình rút ra khi fine-tune model nhận diện ngôn ngữ.

Thí Nghiệm

Đây là toàn bộ quy trình chạy thực tế, từng bước một. Script đầy đủ nằm ở phần phụ lục cuối bài.

1. Viết đoạn văn tiếng Anh/tiếng Việt song song riêng cho benchmark này — không lấy từ nguồn ngoài, để số token phản ánh đúng nội dung tương đương thay vì hai câu chỉ tình cờ khác độ dài:

EN_TEXT = (
    "Running a language model in production is nothing like running it in a "
    "notebook. The moment real traffic hits your endpoint, every token you "
    "send and receive turns into a line item on your bill. ..."
)

VI_TEXT = (
    "Chạy một mô hình ngôn ngữ trong môi trường production hoàn toàn khác với "
    "chạy trong notebook. Ngay khi traffic thật đổ vào endpoint, mỗi token "
    "bạn gửi đi và nhận về đều trở thành một dòng trong hóa đơn. ..."
)

2. Liệt kê từng tokenizer cần benchmark, gắn nhãn thế hệ và loader. Đây là 3 trong số 13:

TOKENIZER_SPECS = [
    {"name": "gpt2", "family": "legacy-english-only", "loader": "hf", "id": "gpt2"},
    {"name": "phobert-base", "family": "vietnamese-specific", "loader": "hf", "id": "vinai/phobert-base"},
    {"name": "o200k_base (GPT-4o / GPT-4.1)", "family": "modern-multilingual", "loader": "tiktoken", "id": "o200k_base"},
]

3. In từng kết quả ngay khi tính xong, và ghi nhận thất bại trung thực thay vì áng chừng:

status_word = entry["status"]
detail = (
    f"ratio={entry.get('ratio_vi_over_en')}"
    if status_word == "ok"
    else f"error={entry.get('error')}"
)
print(f"[{status_word:7s}] {spec['name']:32s} {detail}", flush=True)

Đây là lý do llama-3-8bgemma-2-9b-it hiện thành "Gated (no token)" trong bảng ở trên thay vì một tỷ lệ bịa ra.

4. Ghi toàn bộ payload — cả hai đoạn văn, số từ/ký tự, và kết quả từng tokenizer — ra file cho bài viết:

payload = {
    "en_text": EN_TEXT, "vi_text": VI_TEXT,
    "en_word_count": len(EN_TEXT.split()), "vi_word_count": len(VI_TEXT.split()),
    "en_char_count": len(EN_TEXT), "vi_char_count": len(VI_TEXT),
    "results": results,
}

Tự chạy lại: uv run python tokenizer_cost_benchmark.py.

Bài Học Cho Kỹ Sư

  • Đừng tin vào con số 4.54x cũ kỹ trên các bài blog tổng hợp.
  • Với các API thương mại, hãy ưu tiên các model dùng tokenizer thế hệ mới (như o200k_base của GPT-4o) để tiết kiệm 37% chi phí.
  • Nếu tự host model nhỏ làm RAG hoặc phân loại, các model như PhoBERT hoặc Qwen3 cho hiệu quả token tiếng Việt vượt trội.

Tài Liệu Tham Khảo

  • Better Language Models and Their Implications (GPT-2) — Bài công bố GPT-2 gốc của OpenAI, tokenizer mà huyền thoại "4.54x" trong bài này bắt nguồn từ đó.
  • PhoBERT — Tokenizer/model chuyên biệt tiếng Việt của VinAI, mốc 0.87x trong bài viết.
  • tiktoken — Thư viện BPE của OpenAI hiện thực cl100k_base và o200k_base, hai tokenizer được so sánh trong phần "tiết kiệm 37% miễn phí".
  • Qwen3-8B — Một trong các tokenizer đa ngôn ngữ hiện đại được benchmark.
  • "The Tokenization Tax" — Nghiên cứu thực tế độc lập (Techcombank) mà bài viết trích dẫn để đối chiếu.

Hệ thống của bạn đang tốn tỷ lệ bao nhiêu token cho tiếng Việt? Hãy cùng chia sẻ kinh nghiệm nhé.

👉 Theo dõi mình tại: LinkedIn | GitHub

Phụ Lục: Toàn Bộ Script

Toàn bộ file có thể chạy được:

#!/usr/bin/env python3
"""Measure real token-count inflation for Vietnamese vs English across tokenizer
generations (legacy English-only BPE, Vietnamese-specific, modern multilingual).

No fabricated numbers: every tokenizer that fails to load (gated repo, network
error, missing files) is recorded as skipped with the real error, not estimated.
"""
import json
import sys
import time
from pathlib import Path

OUT_DIR = Path("content/2026-09-01/tokenizer-cost/scratch")

# Matched-content EN/VI pair, first-person voice, written for this benchmark
# (not lifted from an external source) so token counts reflect genuinely
# parallel content rather than independently-varying sentence structure.
EN_TEXT = (
    "Running a language model in production is nothing like running it in a "
    "notebook. The moment real traffic hits your endpoint, every token you "
    "send and receive turns into a line item on your bill. A prompt that "
    "looked short on screen can balloon once the tokenizer gets its hands on "
    "it, especially if your users aren't writing in English. I learned this "
    "the hard way while building a social listening pipeline that ingested "
    "thousands of Vietnamese comments an hour. The dashboard cost numbers "
    "didn't match my mental model at all, and it took a real benchmark, not "
    "a blog post, to figure out why."
)

VI_TEXT = (
    "Chạy một mô hình ngôn ngữ trong môi trường production hoàn toàn khác với "
    "chạy trong notebook. Ngay khi traffic thật đổ vào endpoint, mỗi token "
    "bạn gửi đi và nhận về đều trở thành một dòng trong hóa đơn. Một prompt "
    "nhìn có vẻ ngắn trên màn hình có thể phình to đáng kể sau khi qua tay "
    "tokenizer, nhất là khi người dùng của bạn không viết bằng tiếng Anh. "
    "Mình nhận ra điều này một cách khá đau đớn khi xây dựng pipeline social "
    "listening xử lý hàng nghìn bình luận tiếng Việt mỗi giờ. Con số chi phí "
    "trên dashboard không khớp với mô hình mình nghĩ trong đầu chút nào, và "
    "phải chạy benchmark thật sự, chứ không phải đọc blog, mới tìm ra lý do."
)

TOKENIZER_SPECS = [
    {"name": "gpt2", "family": "legacy-english-only", "loader": "hf", "id": "gpt2"},
    {"name": "roberta-base", "family": "legacy-english-only", "loader": "hf", "id": "FacebookAI/roberta-base"},
    {
        "name": "phobert-base",
        "family": "vietnamese-specific",
        "loader": "hf",
        "id": "vinai/phobert-base",
        "note": "PhoBERT's BPE expects word-segmented Vietnamese input (e.g. via VnCoreNLP); "
                "raw unsegmented text was used here, so this number is a lower bound on "
                "PhoBERT's real efficiency, not its best case.",
    },
    {"name": "gpt2-vietnamese", "family": "vietnamese-specific", "loader": "hf", "id": "NlpHUST/gpt2-vietnamese"},
    {"name": "xlm-roberta-base", "family": "modern-multilingual", "loader": "hf", "id": "FacebookAI/xlm-roberta-base"},
    {
        "name": "mmBERT-base",
        "family": "modern-multilingual",
        "loader": "hf",
        "id": "jhu-clsp/mmBERT-base",
        "note": "ModernBERT-architecture encoder trained on 3T tokens across 1,833 languages "
                "(JHU CLSP, ICML 2026) — first model to meaningfully beat XLM-R on multilingual "
                "benchmarks; encoder, not a causal LM, but the tokenizer comparison is still valid.",
    },
    {"name": "qwen2.5-7b", "family": "modern-multilingual", "loader": "hf", "id": "Qwen/Qwen2.5-7B"},
    {"name": "qwen3-8b", "family": "modern-multilingual", "loader": "hf", "id": "Qwen/Qwen3-8B"},
    {"name": "deepseek-v3.2", "family": "modern-multilingual", "loader": "hf", "id": "deepseek-ai/DeepSeek-V3.2"},
    {"name": "llama-3-8b", "family": "modern-multilingual", "loader": "hf", "id": "meta-llama/Meta-Llama-3-8B"},
    {"name": "gemma-2-9b-it", "family": "modern-multilingual", "loader": "hf", "id": "google/gemma-2-9b-it"},
    {"name": "cl100k_base (GPT-3.5 / GPT-4)", "family": "modern-multilingual", "loader": "tiktoken", "id": "cl100k_base"},
    {"name": "o200k_base (GPT-4o / GPT-4.1)", "family": "modern-multilingual", "loader": "tiktoken", "id": "o200k_base"},
]


def run():
    results = []
    for spec in TOKENIZER_SPECS:
        entry = {"name": spec["name"], "family": spec["family"], "id": spec["id"]}
        t0 = time.time()
        try:
            if spec["loader"] == "hf":
                from transformers import AutoTokenizer, PreTrainedTokenizerFast
                try:
                    tok = AutoTokenizer.from_pretrained(spec["id"])
                except Exception:
                    # Some very new model configs (e.g. deepseek_v32) aren't fully
                    # recognized by this transformers version's AutoConfig yet.
                    # The tokenizer files themselves are still standard, so load
                    # them directly, bypassing model-aware AutoConfig entirely.
                    tok = PreTrainedTokenizerFast.from_pretrained(spec["id"])
                    entry["loaded_via"] = "PreTrainedTokenizerFast fallback (AutoTokenizer failed on model config)"
                en_ids = tok.encode(EN_TEXT)
                vi_ids = tok.encode(VI_TEXT)
            else:
                import tiktoken
                enc = tiktoken.get_encoding(spec["id"])
                en_ids = enc.encode(EN_TEXT)
                vi_ids = enc.encode(VI_TEXT)
            entry["en_tokens"] = len(en_ids)
            entry["vi_tokens"] = len(vi_ids)
            entry["ratio_vi_over_en"] = round(len(vi_ids) / len(en_ids), 3)
            entry["status"] = "ok"
            if "note" in spec:
                entry["note"] = spec["note"]
        except Exception as e:  # noqa: BLE001 - deliberately broad, this is a survey script
            entry["status"] = "skipped"
            entry["error"] = str(e).splitlines()[0][:300]
        entry["load_and_encode_seconds"] = round(time.time() - t0, 2)
        status_word = entry["status"]
        detail = (
            f"ratio={entry.get('ratio_vi_over_en')}"
            if status_word == "ok"
            else f"error={entry.get('error')}"
        )
        print(f"[{status_word:7s}] {spec['name']:32s} {detail}", flush=True)
        results.append(entry)

    payload = {
        "en_text": EN_TEXT,
        "vi_text": VI_TEXT,
        "en_word_count": len(EN_TEXT.split()),
        "vi_word_count": len(VI_TEXT.split()),
        "en_char_count": len(EN_TEXT),
        "vi_char_count": len(VI_TEXT),
        "results": results,
    }
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    out_path = OUT_DIR / "benchmark_results.json"
    out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2))
    print(f"\nWrote {out_path}", flush=True)
    return payload


if __name__ == "__main__":
    run()

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí