69 điểm bởi GN⁺ 2026-02-17 | 1 bình luận | Chia sẻ qua WhatsApp
  • Dự án nghệ thuật do karpathy công bố. Triển khai toàn bộ thuật toán GPT trong một tệp duy nhất 200 dòng, không có phụ thuộc bên ngoài
  • Khác biệt với LLM production chỉ nằm ở quy mô và hiệu quả, còn cốt lõi là như nhau; hiểu được đoạn mã này đồng nghĩa với hiểu được bản chất thuật toán của GPT
  • Bao gồm dataset, tokenizer, engine autograd, kiến trúc Transformer tương tự GPT-2, optimizer Adam, cho tới vòng lặp huấn luyện và suy luận
  • kết tinh của 10 năm nỗ lực đơn giản hóa LLM qua các dự án như micrograd, makemore, nanogpt..., gói trọn bản chất của GPT trong hình thức tối giản đến mức không thể đơn giản hơn
  • Huấn luyện trên dữ liệu 32.000 cái tên để tạo ra các tên mới nghe hợp lý, đồng thời thực hiện mọi phép tính trực tiếp bằng autograd ở mức vô hướng
  • Quá trình huấn luyện gồm tính loss → backpropagation → cập nhật Adam, có thể chạy trong khoảng 1 phút

Tổng quan về microgpt

  • microgpt là một script Python 200 dòng triển khai đầy đủ quá trình huấn luyện và suy luận của mô hình GPT
    • Không cần thư viện ngoài nhưng vẫn bao gồm đầy đủ dataset, tokenizer, autograd, mô hình, optimizer, vòng lặp huấn luyện
  • Hợp nhất các dự án trước đó như micrograd, makemore, nanogpt thành một tệp duy nhất
  • Là cách triển khai chỉ giữ lại phần cốt lõi thuật toán, ở mức “không thể đơn giản hơn nữa”
  • Toàn bộ mã nguồn được cung cấp tại GitHub Gist, trang web, Google Colab

Cấu thành dataset

  • Nhiên liệu của mô hình ngôn ngữ lớn là luồng dữ liệu văn bản; trong production người ta dùng các trang web trên internet, còn microgpt dùng ví dụ đơn giản gồm 32.000 cái tên mỗi dòng một tên
  • Mỗi tên được xem như một "tài liệu", và mục tiêu của mô hình là học các mẫu thống kê trong dữ liệu để tạo ra các tài liệu mới tương tự
  • Sau khi huấn luyện xong, mô hình sẽ "ảo giác" (hallucinate) ra các tên mới nghe hợp lý như "kamon", "karai", "vialan"
  • Dưới góc nhìn của ChatGPT, hội thoại với người dùng cũng chỉ là một "tài liệu có hình dạng kỳ lạ"; khi khởi tạo tài liệu bằng prompt, phản hồi của mô hình chính là hoàn thiện tài liệu theo thống kê

Tokenizer

  • Vì mạng nơ-ron hoạt động trên số chứ không phải ký tự, cần có cách chuyển văn bản thành chuỗi ID token số nguyên rồi khôi phục lại
  • Các tokenizer production như tiktoken (dùng cho GPT-4) hoạt động theo các cụm ký tự để đạt hiệu quả cao, nhưng tokenizer đơn giản nhất là gán một số nguyên cho mỗi ký tự duy nhất trong dataset
  • Sắp xếp các chữ cái thường a-z rồi gán ID theo chỉ số cho từng ký tự; bản thân giá trị số nguyên không mang ý nghĩa gì, mỗi token chỉ là một ký hiệu rời rạc riêng biệt
  • Thêm token đặc biệt BOS (Beginning of Sequence) để báo hiệu "một tài liệu mới bắt đầu/kết thúc", nên "emma" sẽ được bọc thành [BOS, e, m, m, a, BOS]
  • Kích thước từ vựng cuối cùng là 27 (26 chữ cái thường + 1 BOS)

Tự động vi phân (Autograd)

  • Việc huấn luyện mạng nơ-ron cần gradient: với mỗi tham số, cần biết "nếu tăng nhẹ giá trị này thì loss tăng hay giảm, và bao nhiêu?"
  • Đồ thị tính toán có nhiều đầu vào (tham số mô hình và token đầu vào) nhưng hội tụ về một đầu ra vô hướng duy nhất là loss
  • Backpropagation bắt đầu từ đầu ra rồi lần ngược qua đồ thị, dựa vào quy tắc dây chuyền của vi tích phân để tính gradient của loss đối với mọi đầu vào
  • Được triển khai bằng lớp Value: mỗi Value bọc một giá trị vô hướng (.data) và theo dõi cách nó được tính ra
    • Khi thực hiện các phép toán như cộng, nhân..., một Value mới sẽ ghi nhớ các đầu vào (_children) và đạo hàm cục bộ (_local_grads) của phép toán đó
    • Ví dụ: __mul__ ghi lại ∂(a·b)/∂a=b, ∂(a·b)/∂b=a
  • Các khối phép toán được hỗ trợ: cộng, nhân, lũy thừa, log, exp, ReLU
  • Phương thức backward() sẽ duyệt đồ thị theo thứ tự topo ngược, áp dụng quy tắc dây chuyền ở từng bước
    • Bắt đầu từ nút loss với self.grad = 1 (∂L/∂L=1)
    • Nhân dồn các gradient cục bộ dọc theo đường đi để lan truyền ngược về các tham số
  • Cộng dồn bằng += (không phải gán): khi đồ thị phân nhánh, gradient từ từng nhánh sẽ chảy về độc lập rồi được cộng lại (hệ quả của quy tắc dây chuyền nhiều biến)
  • Về mặt thuật toán thì giống hệt .backward() của PyTorch, nhưng thay vì tensor thì hoạt động ở mức vô hướng nên đơn giản hơn nhiều, đổi lại hiệu quả thấp hơn

Khởi tạo tham số

  • Tham số là tri thức của mô hình, một tập lớn các số thực dấu phẩy động bắt đầu ngẫu nhiên rồi được tối ưu lặp đi lặp lại trong quá trình huấn luyện
  • Được khởi tạo bằng các giá trị ngẫu nhiên nhỏ từ phân phối Gaussian
  • Gồm các ma trận được đặt tên trong state_dict: bảng embedding, trọng số attention, trọng số MLP, phép chiếu đầu ra cuối cùng
  • Thiết lập hyperparameter:
    • n_embd = 16: số chiều embedding
    • n_head = 4: số đầu attention
    • n_layer = 1: số layer
    • block_size = 16: độ dài chuỗi tối đa
  • Theo chuẩn mô hình nhỏ là 4.192 tham số (GPT-2 có 1,6 tỷ, còn LLM hiện đại có hàng trăm tỷ)

Kiến trúc

  • Kiến trúc mô hình là một hàm không trạng thái: nhận token, vị trí, tham số, khóa/giá trị đã được lưu trong bộ nhớ đệm của các vị trí trước đó, rồi trả về logit (điểm số) cho token tiếp theo
  • Theo GPT-2 nhưng được đơn giản hóa đôi chút: dùng RMSNorm (thay cho LayerNorm), không có bias, dùng ReLU (thay cho GeLU)
  • Các hàm trợ giúp

    • linear: phép nhân ma trận-véc tơ tính một tích vô hướng cho mỗi hàng của ma trận trọng số, là phép biến đổi tuyến tính đã học — thành phần cơ bản của mạng nơ-ron
    • softmax: chuyển các điểm số thô (logit) thành phân phối xác suất, sao cho mọi giá trị nằm trong khoảng [0,1] và tổng bằng 1; để ổn định số học thì trừ giá trị lớn nhất trước
    • rmsnorm: chuẩn hóa lại véc tơ để có căn bậc hai của trung bình bình phương đơn vị, giúp ngăn kích hoạt phình ra hoặc co lại khi đi qua mạng, từ đó ổn định quá trình huấn luyện
  • Cấu trúc mô hình

    • Embedding: ID token và ID vị trí lần lượt tra cứu các hàng trong bảng embedding (wte, wpe); hai véc tơ được cộng lại để mã hóa đồng thời token là và nó đang ở đâu trong chuỗi
      • Các LLM hiện đại thường bỏ qua embedding vị trí và dùng các kỹ thuật định vị tương đối như RoPE
    • Khối attention: chiếu token hiện tại thành ba véc tơ Q (query), K (key), V (value)
      • Query: “Tôi đang tìm gì?”, Key: “Tôi chứa gì?”, Value: “Nếu được chọn thì tôi cung cấp gì?”
      • Ví dụ: khi dự đoán ký tự tiếp theo trong “emma” tại chữ “m” thứ hai, mô hình có thể học một query như “nguyên âm gần đây nhất là gì?”, và chữ “e” phía trước sẽ khớp tốt với query này nên nhận trọng số attention cao
      • Key và value được thêm vào KV cache để có thể tham chiếu các vị trí trước đó
      • Mỗi attention head tính tích vô hướng giữa query và toàn bộ key đã lưu trong cache (được scale theo √d_head), dùng softmax để lấy trọng số attention, rồi tính tổng có trọng số của các value đã lưu trong cache
      • Đầu ra của tất cả các head được nối lại rồi chiếu qua attn_wo
      • Khối attention là nơi duy nhất token ở vị trí t có thể “nhìn” các token trước đó từ 0..t-1; attention là cơ chế giao tiếp giữa các token
    • Khối MLP: mạng feedforward 2 tầng: mở rộng lên gấp 4 lần chiều embedding → áp dụng ReLU → thu gọn trở lại
      • Đây là nơi diễn ra phần lớn quá trình “suy nghĩ” theo từng vị trí
      • Khác với attention, đây là phép tính cục bộ hoàn toàn tại thời điểm t
      • Transformer xen kẽ giữa giao tiếp (attention) và tính toán (MLP)
    • Kết nối dư: cả khối attention và MLP đều cộng đầu ra của chúng trở lại đầu vào
      • Giúp gradient đi trực tiếp xuyên qua mạng, từ đó cho phép huấn luyện các mô hình sâu
    • Đầu ra: trạng thái ẩn cuối cùng được chiếu qua lm_head sang kích thước từ vựng để tạo ra một logit cho mỗi token (ở đây là 27 số); logit càng cao = token đó càng có khả năng xuất hiện tiếp theo
    • Điểm đặc biệt của KV cache: dù việc dùng KV cache trong lúc huấn luyện là hiếm, microgpt xử lý mỗi lần chỉ một token nên nó được xây dựng tường minh; các key và value đã lưu là những nút Value đang hoạt động trong đồ thị tính toán nên vẫn tham gia backpropagation

Vòng lặp huấn luyện

  • Vòng lặp huấn luyện lặp đi lặp lại: (1) chọn tài liệu → (2) chạy truyền xuôi mô hình trên các token → (3) tính loss → (4) lấy gradient bằng backpropagation → (5) cập nhật tham số
  • Token hóa

    • Ở mỗi bước huấn luyện, chọn một tài liệu và bọc BOS ở hai đầu: “emma” → [BOS, e, m, m, a, BOS]
    • Mục tiêu của mô hình là dự đoán từng token kế tiếp dựa trên các token trước đó
  • Truyền xuôi và loss

    • Các token được đưa vào mô hình từng cái một, đồng thời xây dựng KV cache
    • Ở mỗi vị trí, mô hình xuất ra 27 logit, rồi được chuyển thành xác suất bằng softmax
    • Loss tại mỗi vị trí là log xác suất âm của token đúng tiếp theo: −log p(target), được gọi là cross-entropy loss
    • Loss đo mức độ mô hình ngạc nhiên trước thứ thực sự xảy ra: nếu gán xác suất 1.0 thì loss bằng 0, còn nếu xác suất gần 0 thì loss tiến tới +∞
    • Lấy trung bình loss của mọi vị trí trong toàn bộ tài liệu để thu được một scalar loss duy nhất
  • Truyền ngược

    • Chỉ với một lần gọi loss.backward(), backpropagation được chạy trên toàn bộ đồ thị tính toán
    • Sau đó, .grad của từng tham số cho biết cần thay đổi như thế nào để giảm loss
  • Bộ tối ưu Adam

    • Thay vì gradient descent đơn giản (p.data -= lr * p.grad), mô hình dùng Adam
    • Với mỗi tham số, Adam duy trì hai trung bình động:
      • m: trung bình của các gradient gần đây (momentum)
      • v: trung bình của bình phương gradient gần đây (điều chỉnh learning rate theo từng tham số)
    • m_hat, v_hat là các phiên bản hiệu chỉnh bias của m và v vốn được khởi tạo từ 0
    • Learning rate giảm tuyến tính trong quá trình huấn luyện
    • Sau khi cập nhật, .grad được đặt lại về 0
  • Kết quả huấn luyện

    • Trong 1.000 bước, loss giảm từ khoảng 3.3 (đoán ngẫu nhiên giữa 27 token: −log(1/27)≈3.3) xuống khoảng 2.37
    • Càng thấp càng tốt, và mức thấp nhất là 0 (dự đoán hoàn hảo), nên vẫn còn dư địa cải thiện; tuy vậy, rõ ràng mô hình đang học các mẫu thống kê của tên

Suy luận

  • Sau khi huấn luyện xong, có thể lấy mẫu tên mới từ mô hình: cố định tham số rồi chạy truyền xuôi theo vòng lặp, dùng mỗi token vừa sinh làm đầu vào cho bước kế tiếp
  • Quy trình lấy mẫu

    • Mỗi mẫu bắt đầu bằng token BOS (“bắt đầu tên mới”)
    • Mô hình tạo ra 27 logit → chuyển thành xác suất → lấy mẫu ngẫu nhiên một token theo các xác suất đó
    • Token đó được hồi tiếp làm đầu vào kế tiếp; lặp lại cho đến khi mô hình lại sinh ra BOS (“kết thúc”) hoặc đạt độ dài chuỗi tối đa
  • Temperature

    • Trước softmax, các logit được chia cho temperature
    • Temperature 1.0: lấy mẫu trực tiếp từ phân phối mà mô hình đã học
    • Temperature thấp (ví dụ 0.5): làm phân phối sắc hơn, khiến mô hình có xu hướng bảo thủ hơn và thường chọn các phương án hàng đầu
    • Temperature gần 0: luôn chọn token có xác suất cao nhất (greedy decoding)
    • Temperature cao: làm phân phối phẳng hơn, tạo ra đầu ra đa dạng hơn nhưng kém nhất quán hơn

Cách chạy

  • Chỉ cần Python (không cần pip install, không có dependency): python train.py
  • Trên MacBook mất khoảng 1 phút
  • In loss ở mỗi bước: từ ~3.3 (ngẫu nhiên) giảm xuống ~2.37
  • Sau khi huấn luyện xong, sinh ra các tên mới do mô hình tưởng tượng như “kamon”, “ann”, “karai”
  • Cũng có thể chạy trong notebook Google Colab, và hỏi Gemini
  • Có thể thử tập dữ liệu khác, tăng num_steps để huấn luyện lâu hơn, hoặc tăng kích thước mô hình để có kết quả tốt hơn

Các bước tiến triển của mã

Tệp Nội dung bổ sung
train0.py Bảng đếm bigram — không có mạng nơ-ron, không có gradient
train1.py MLP + gradient thủ công (số học & phân tích) + SGD
train2.py Autograd (lớp Value) — thay thế gradient thủ công
train3.py Embedding vị trí + attention một head + rmsnorm + residual
train4.py Multi-head attention + vòng lặp layer — kiến trúc GPT hoàn chỉnh
train5.py Bộ tối ưu Adam — đây là train.py
  • Có thể xem mọi phiên bản và diff giữa từng bước trong Revisions của Gist build_microgpt.py

Sự khác biệt so với LLM production

  • microgpt bao gồm bản chất thuật toán hoàn chỉnh của việc huấn luyện và chạy GPT; điểm khác biệt so với các LLM production như ChatGPT không làm thay đổi thuật toán cốt lõi mà nằm ở các yếu tố giúp nó vận hành ở quy mô lớn
  • Dữ liệu

    • Thay vì 32K tên ngắn, mô hình được huấn luyện bằng hàng nghìn tỷ token văn bản Internet (trang web, sách, mã nguồn, v.v.)
    • Khử trùng lặp dữ liệu, lọc chất lượng, và phối trộn cẩn thận giữa các miền
  • Tokenizer

    • Thay vì từng ký tự đơn lẻ, sử dụng tokenizer dưới dạng subword như BPE (Byte Pair Encoding)
    • Gộp các chuỗi ký tự thường xuyên xuất hiện cùng nhau thành một token duy nhất; các từ phổ biến như "the" là một token duy nhất, còn từ hiếm thì bị tách thành các mảnh
    • Từ vựng khoảng ~100K token, nhìn thấy nhiều nội dung hơn ở mỗi vị trí nên hiệu quả hơn rất nhiều
  • Autograd

    • Thay vì đối tượng Value dạng scalar trong Python thuần, sử dụng tensor (mảng số đa chiều cỡ lớn), chạy trên GPU/TPU có thể thực hiện hàng tỷ phép toán dấu chấm động mỗi giây
    • PyTorch xử lý autograd cho tensor, còn các CUDA kernel như FlashAttention hợp nhất nhiều phép toán
    • Toán học vẫn như cũ, nhưng rất nhiều scalar được xử lý song song
  • Kiến trúc

    • microgpt: 4.192 tham số; mô hình cỡ GPT-4: hàng trăm tỷ
    • Nhìn chung vẫn là mạng Transformer rất giống nhau, nhưng rộng hơn nhiều (chiều embedding 10.000+) và sâu hơn nhiều (100+ lớp)
    • Có thêm các loại khối lego và thay đổi thứ tự:
      • RoPE (embedding vị trí xoay) — thay cho embedding vị trí được học
      • GQA (grouped-query attention) — giảm kích thước KV cache
      • Kích hoạt tuyến tính có cổng — thay cho ReLU
      • Lớp MoE (mixture of experts)
    • Cấu trúc cốt lõi với attention (giao tiếp) và MLP (tính toán) luân phiên trên residual stream vẫn được giữ nguyên rất rõ ràng
  • Huấn luyện

    • Thay vì một tài liệu mỗi bước, sử dụng batch lớn (hàng triệu token mỗi bước), tích lũy gradient, mixed precision (float16/bfloat16), và tinh chỉnh hyperparameter cẩn thận
    • Việc huấn luyện các mô hình frontier chạy trên hàng nghìn GPU trong nhiều tháng
  • Tối ưu hóa

    • microgpt: Adam + giảm learning rate tuyến tính đơn giản
    • Ở quy mô lớn, tối ưu hóa là một lĩnh vực riêng: độ chính xác giảm (bfloat16, fp8), huấn luyện trên các cụm GPU quy mô lớn
    • Cần tinh chỉnh kỹ các thiết lập optimizer (learning rate, weight decay, tham số beta, lịch warmup/decay); giá trị đúng phụ thuộc vào kích thước mô hình, kích thước batch và cấu trúc dataset
    • Scaling laws (ví dụ: Chinchilla) hướng dẫn cách phân bổ ngân sách tính toán cố định giữa kích thước mô hình và số token huấn luyện
    • Nếu làm sai các chi tiết này ở quy mô lớn có thể lãng phí hàng triệu USD tiền tính toán; các nhóm thường thực hiện rất nhiều thí nghiệm nhỏ trước khi chạy huấn luyện toàn phần
  • Hậu huấn luyện (Post-training)

    • Mô hình cơ sở sinh ra từ quá trình huấn luyện (mô hình "pretrained") là trình hoàn tất tài liệu, không phải chatbot
    • Quá trình biến nó thành ChatGPT gồm hai bước:
      • SFT (fine-tuning có giám sát): thay tài liệu bằng các hội thoại được tuyển chọn và tiếp tục huấn luyện, không có thay đổi gì về mặt thuật toán
      • RL (học tăng cường): mô hình tạo phản hồi → được chấm điểm (bởi con người, mô hình "giám khảo", hoặc thuật toán) → học từ phản hồi đó
    • Về bản chất, mô hình vẫn học trên tài liệu, nhưng giờ tài liệu được cấu thành từ các token do chính mô hình tạo ra
  • Suy luận

    • Để phục vụ mô hình cho hàng triệu người dùng cần cả một stack kỹ thuật riêng: batching request, quản lý và paging KV cache (vLLM, v.v.), speculative decoding để tăng tốc, quantization để giảm bộ nhớ (chạy bằng int8/int4), và phân tán mô hình lên nhiều GPU
    • Về cơ bản vẫn là dự đoán token tiếp theo của chuỗi, nhưng có rất nhiều công sức kỹ thuật để làm nó nhanh hơn

FAQ

  • Mô hình có "hiểu" điều gì không?

    • Đây là câu hỏi mang tính triết học, nhưng xét về mặt cơ học thì không có phép màu nào xảy ra
    • Mô hình là một hàm toán học lớn ánh xạ token đầu vào thành phân phối xác suất cho token tiếp theo
    • Trong quá trình huấn luyện, các tham số được điều chỉnh để khiến token tiếp theo đúng có xác suất cao hơn
    • Điều đó có cấu thành "hiểu" hay không là tùy mỗi người, nhưng cơ chế thì đã nằm trọn trong 200 dòng ở trên
  • Tại sao nó hoạt động?

    • Mô hình có hàng nghìn tham số có thể điều chỉnh, và optimizer di chuyển chúng từng chút một ở mỗi bước để giảm loss
    • Qua nhiều bước, các tham số ổn định tại những giá trị nắm bắt được tính quy luật thống kê của dữ liệu
    • Với tên gọi chẳng hạn: nhiều tên bắt đầu bằng phụ âm, "qu" có xu hướng đi cùng nhau, ba phụ âm liên tiếp là hiếm, v.v.
    • Mô hình học phân phối xác suất phản ánh các điều này, chứ không phải các quy tắc tường minh
  • Nó liên quan gì đến ChatGPT?

    • ChatGPT mở rộng cùng vòng lặp cốt lõi này (dự đoán token tiếp theo, lấy mẫu, lặp lại) lên quy mô khổng lồ và bổ sung hậu huấn luyện để biến nó thành tương tác hội thoại
    • Khi trò chuyện, system prompt, tin nhắn người dùng và câu trả lời đều chỉ là các token trong một chuỗi
    • Mô hình hoàn tất tài liệu mỗi lần một token theo đúng cách microgpt hoàn tất tên gọi
  • "Hallucination" là gì?

    • Mô hình tạo token bằng cách lấy mẫu từ một phân phối xác suất
    • không có khái niệm chân lý, chỉ biết chuỗi nào có vẻ hợp lý về mặt thống kê dựa trên dữ liệu huấn luyện
    • Việc microgpt "hallucinate" ra một cái tên như "karia" cũng là cùng hiện tượng với việc ChatGPT tự tin nói ra một sự thật sai
    • Cả hai đều là những phần hoàn tất nghe có vẻ hợp lý nhưng không có thật
  • Tại sao nó chậm vậy?

    • microgpt xử lý một scalar mỗi lần trong Python thuần, nên một bước huấn luyện mất vài giây
    • Trên GPU, cùng phép toán đó xử lý song song hàng triệu scalar, nên chạy nhanh hơn nhiều bậc độ lớn
  • Có thể khiến nó tạo ra tên hay hơn không?

    • Có thể: huấn luyện lâu hơn (num_steps lớn hơn), tăng kích thước mô hình (n_embd, n_layer, n_head), hoặc dùng dataset lớn hơn
    • Đây cũng chính là các cần gạt điều chỉnh quan trọng ở quy mô lớn
  • Nếu thay dataset thì sao?

    • Mô hình sẽ học bất kỳ mẫu hình nào có trong dữ liệu
    • Nếu thay bằng tên thành phố, tên Pokémon, từ tiếng Anh hoặc các file thơ ngắn, nó sẽ học cách tạo ra những thứ đó thay thế
    • Phần còn lại của mã không cần thay đổi

1 bình luận

 
mhj5730 2026-02-19

Cảm ơn bài viết hay.