minigpt

A small MoonBit GPT training toolkit with tensor autodiff, tokenizer support, checkpoints, and a teaching CLI.

gpt
transformer
tensor
autodiff
training
moon add Milky2018/minigpt@0.1.1
Download zip
Author
Version
0.1.1
License
Apache-2.0
Last updated
last month
Downloads
15

Dependencies

README

#MiniGPT in MoonBit

这是一个用 MoonBit 写的极简自动补全程序。当前目标是尽量对齐 Andrej Karpathy nanoGPTshakespeare_char 配置:同一份 Tiny Shakespeare 语料、字符级 tokenizer、90/10 train/val 切分,以及同一组小型 GPT 超参数。命令行里训练时主要只改 --steps

当前模型是 GPT decoder-only transformer:token embedding + position embedding + 6 层 decoder block。每个 block 包含 pre-norm multi-head causal self-attention、MLP/GELU、dropout 和残差连接,最后用 tied token embedding 作为输出头。它不是语料检索,不会复制 prompt 后面的原文片段。

#数据

本仓库自带两份教学语料,方便从源码 checkout 后直接跑 demo。发布到 Mooncakes 的包会排除 data/docs/;作为依赖使用时,请准备自己的 UTF-8 文本文件,并通过 --data 传给训练命令。

源码仓库里的默认语料文件:

data/tiny_shakespeare.txt

这个文件来自 Karpathy char-rnn 仓库里的 Tiny Shakespeare 数据集,也是 nanoGPT data/shakespeare_char/prepare.py 下载的同一份文本。

字符级准备流程:

characters = 1,115,394 vocab size = 65 train tokens = 1,003,854 val tokens = 111,540 split = first 90% train, last 10% val

第二个教学目标是很小的英文菜谱语料:

data/recipe_demo.txt

这个文件以 habakan/moonbit-gpt-edge-demo 的 Cooklang-style recipe sentences 为种子扩展而来,来源和许可证记录在 data/RECIPE_DEMO_SOURCE.md。它使用 49 个英文词反复组合,word-level tokenizer 加上换行 token 后 vocab size 为 50,适合课堂里快速演示“模型如何从短语料中学会菜谱风格补全”,不适合作为模型质量 benchmark。

菜谱语料可以直接训练。默认 tokenizer 仍然是 char,用于对齐 nanoGPT;菜谱演示可以显式使用 --tokenizer word,让模型一次预测一个英文词;英文长文本更推荐尝试 --tokenizer bpe,它会从训练语料自动学习子词词表。

#训练

训练会从语料中构造 tokenizer(默认字符级),并按 nanoGPT 的 always_save_checkpoint = False 行为在 eval 时保存验证集 loss 创新低的 checkpoint。默认输出文件是 minigpt-model.bin

moon run --release cmd/main -- train

指定输出文件或训练迭代数:

moon run --release cmd/main -- train --out minigpt-model.bin --steps 5000

训练参数默认对齐 nanoGPT,也可以显式覆盖成小配置做 smoke/benchmark:

--data UTF-8 语料路径,默认 data/tiny_shakespeare.txt --tokenizer tokenizer 类型:char、word 或 bpe,默认 char --bpe-vocab-size BPE 目标词表大小,默认 512 --out checkpoint 输出路径,默认 minigpt-model.bin --steps 训练迭代数,默认 5000 --batch-size batch size,默认 64 --block-size 上下文长度,默认 256 --n-embd embedding width,默认 384 --n-head attention head count,默认 6 --n-layer transformer block count,默认 6 --learning-rate 学习率,默认 0.001 --eval-interval 每多少个 iter 做 eval,默认 250 --eval-iters 每次 eval 的 batch 数,默认 200 --log-interval 每多少个 iter 打印 train loss,默认 10 --always-save-checkpoint 每次 eval 后保存 checkpoint,默认 false

--steps 对应 nanoGPT 的 max_iters,循环结束条件是 iter_num > max_iters,因此 --steps 1 会执行 iter 0 和 iter 1 两次更新。iter 0 的 eval 不保存 checkpoint;只有 iter_num > 0 且 val loss 创新低时才会写入 --out

快速走通完整训练和生成流程可以用小模型配置:

moon run --release cmd/main -- train \ --out /tmp/minigpt-small.bin \ --steps 3 \ --batch-size 4 \ --block-size 8 \ --n-embd 24 \ --n-head 4 \ --n-layer 2 \ --eval-interval 1 \ --eval-iters 1 \ --log-interval 1 \ --always-save-checkpoint true moon run --release cmd/main -- generate \ --model /tmp/minigpt-small.bin \ --prompt ROMEO: \ --max-new-tokens 8

菜谱语料的推荐训练示例:

moon run --release cmd/main -- train \ --data data/recipe_demo.txt \ --tokenizer word \ --out /tmp/minigpt-recipe.bin \ --steps 500 \ --batch-size 4 \ --block-size 8 \ --n-embd 32 \ --n-head 4 \ --n-layer 2 \ --learning-rate 0.01 \ --eval-interval 100 \ --eval-iters 4 \ --log-interval 100 \ --always-save-checkpoint true moon run --release cmd/main -- generate \ --model /tmp/minigpt-recipe.bin \ --prompt "heat" \ --max-new-tokens 4 \ --top-k 5 \ --temperature 1.0

这组菜谱参数是为了让小语料能学出有效短句,而不是追求 nanoGPT 对齐。实测 2 层、n_embd=32block_size=8steps=500 的 checkpoint 大约 630K;生成时 --top-k 5 --temperature 1.0 比贪心解码更自然,能减少固定坍缩到同一句的情况。菜谱每行通常只有 4 到 5 个 word token,所以只想补全一句时推荐 --max-new-tokens 45

自动训练 BPE tokenizer 的示例:

moon run --release cmd/main -- train \ --tokenizer bpe \ --bpe-vocab-size 512 \ --out /tmp/minigpt-bpe.bin \ --steps 20 \ --batch-size 4 \ --block-size 32 \ --n-embd 64 \ --n-head 4 \ --n-layer 2 \ --eval-interval 5 \ --eval-iters 2 \ --always-save-checkpoint true

作为库使用时,推荐从根包入口开始,不需要直接操作 tensor/nn/

///|
let rng = @random.Rand::new()

///|
let config = @minigpt.TrainingConfig(
batch_size=4,
block_size=8,
steps=20,
learning_rate=0.001,
eval_iters=1,
)

///|
let result = @minigpt.train_text(corpus_text, CharacterLevel, config, rng)

///|
let checkpoint = @minigpt.encode_checkpoint(result.model, result.tokenizer)

///|
let completion = @minigpt.generate_text(
result.model,
result.tokenizer,
"ROMEO:",
40,
rng,
SamplingConfig(top_k=50, temperature=0.8),
)

内置训练超参对齐 nanoGPT config/train_shakespeare_char.py

batch_size = 64 block_size = 256 n_layer = 6 n_head = 6 n_embd = 384 dropout = 0.2 learning_rate = 0.001 min_lr = 0.0001 warmup_iters = 100 eval_interval = 250 eval_iters = 200 log_interval = 10 weight_decay = 0.1 beta1 = 0.9 beta2 = 0.99 grad_clip = 1.0

#生成

生成只加载已经训练好的 checkpoint,不会重新训练:

moon run --release cmd/main -- generate --model minigpt-model.bin --prompt ROMEO:

如果 minigpt-model.bin 不存在,先运行上面的 train 命令;训练步数太少时可能不会触发 nanoGPT 的保存条件。

每生成一个 token,都会打印当前已经补全出的完整内容;生成长度由 --max-new-tokens 控制:

completion: ROMEO: ROMEO:T ROMEO:Th ROMEO:The

生成参数:

--model checkpoint 路径,默认 minigpt-model.bin --prompt 补全起始文本,默认 ROMEO: --max-new-tokens 生成 token 数,默认 80 --top-k 从模型 logits 最高的几个候选中采样,默认 20 --temperature 采样温度,默认 0.8

checkpoint 会保存训练时使用的 tokenizer。字符级 checkpoint 只能编码训练语料词表里的字符;word-level checkpoint 只能编码训练语料词表里的词;BPE checkpoint 会保存 vocabulary 和 merge rules,可以编码由训练字符集组成的新词。默认 Shakespeare 语料不包含中文字符,所以中文 prompt 会被拒绝。

#Checkpoint 大小

当前 checkpoint 使用紧凑二进制格式。默认 GPT 配置大致为:

tokenizer = char vocab size = 65 model kind = gpt-transformer n_embd = 384 n_head = 6 n_layer = 6 block size = 256 model parameters ~= 10.6M Doubles training checkpoint ~= 250MB

#项目结构

tokenizer/ char/word tokenizer and train/val split tensor/ Tensor 和自动微分基础 nn/ 神经网络基础算子 optim/ AdamW 优化器 model.mbt MiniGPT 模型 train.mbt nanoGPT-style 训练循环 generate.mbt 采样生成 checkpoint.mbt checkpoint 编解码 cmd/main/ CLI 入口 docs/ 教学架构图 data/ Tiny Shakespeare 语料

Mooncakes 发布包保留根包、tokenizer/tensor/nn/optim/cmd/main/、测试、README 和 LICENSE;data/docs/、agent 协作文档和空工具目录只属于源码仓库,不会进入发布包。

#验证

moon check --warn-list +73 --target native moon test --target native

#
ArchitectureConfig

pub struct ArchitectureConfig {
// private fields
}

#
ArchitectureConfig::ArchitectureConfig

fn ArchitectureConfig::ArchitectureConfig(n_embd? : Int, n_head? : Int, n_layer? : Int) -> ArchitectureConfig

#
ArchitectureConfig::n_embd

fn ArchitectureConfig::n_embd(self : ArchitectureConfig) -> Int

#
ArchitectureConfig::n_head

fn ArchitectureConfig::n_head(self : ArchitectureConfig) -> Int

#
ArchitectureConfig::n_layer

fn ArchitectureConfig::n_layer(self : ArchitectureConfig) -> Int

#
EvalEvent

pub struct EvalEvent {
iter_num : Int
train_loss : Double
val_loss : Double
}

#
LogEvent

pub struct LogEvent {
iter_num : Int
loss : Double
learning_rate : Double
}

#
MiniGPT

pub struct MiniGPT {
// private fields
}

#
MiniGPT::MiniGPT

#
MiniGPT::block_size

fn MiniGPT::block_size(self : MiniGPT) -> Int

#
MiniGPT::kind_name

fn MiniGPT::kind_name(_self : MiniGPT) -> String

#
MiniGPT::n_embd

fn MiniGPT::n_embd(self : MiniGPT) -> Int

#
MiniGPT::n_head

fn MiniGPT::n_head(self : MiniGPT) -> Int

#
MiniGPT::n_layer

fn MiniGPT::n_layer(self : MiniGPT) -> Int

#
MiniGPT::vocab_size

fn MiniGPT::vocab_size(self : MiniGPT) -> Int

#
ModelConfig

pub struct ModelConfig {
// private fields
}

#
ModelConfig::ModelConfig

fn ModelConfig::ModelConfig(vocab_size : Int, n_embd? : Int, n_head? : Int, n_layer? : Int, block_size? : Int) -> ModelConfig

#
ModelConfig::block_size

fn ModelConfig::block_size(self : ModelConfig) -> Int

#
ModelConfig::from_architecture

fn ModelConfig::from_architecture(vocab_size : Int, architecture : ArchitectureConfig, block_size? : Int) -> ModelConfig

#
ModelConfig::n_embd

fn ModelConfig::n_embd(self : ModelConfig) -> Int

#
ModelConfig::n_head

fn ModelConfig::n_head(self : ModelConfig) -> Int

#
ModelConfig::n_layer

fn ModelConfig::n_layer(self : ModelConfig) -> Int

#
ModelConfig::vocab_size

fn ModelConfig::vocab_size(self : ModelConfig) -> Int

#
SamplingConfig

pub struct SamplingConfig {
// private fields
}

#
SamplingConfig::SamplingConfig

fn SamplingConfig::SamplingConfig(top_k? : Int, temperature? : Double, excluded_id? : Int?, allowed_ids? : Array[Int]?) -> SamplingConfig

#
SamplingConfig::allowed_ids

fn SamplingConfig::allowed_ids(self : SamplingConfig) -> Array[Int]?

#
SamplingConfig::excluded_id

fn SamplingConfig::excluded_id(self : SamplingConfig) -> Int?

#
SamplingConfig::temperature

fn SamplingConfig::temperature(self : SamplingConfig) -> Double

#
SamplingConfig::top_k

fn SamplingConfig::top_k(self : SamplingConfig) -> Int

#
TrainingCallbacks

pub struct TrainingCallbacks {
// private fields
}

#
TrainingCallbacks::TrainingCallbacks

fn TrainingCallbacks::TrainingCallbacks(on_eval? : (EvalEvent) -> Unit, on_log? : (LogEvent) -> Unit, on_checkpoint? : (TrainingState) -> Unit) -> TrainingCallbacks

#
TrainingCheckpoint

pub struct TrainingCheckpoint {
// private fields
}

#
TrainingCheckpoint::TrainingCheckpoint

fn TrainingCheckpoint::TrainingCheckpoint(model : MiniGPT, tokenizer :
Tokenizer
, optimizer :
AdamW
, iter_num : Int, best_val_loss : Double, config : TrainingConfig) -> TrainingCheckpoint

#
TrainingCheckpoint::best_val_loss

fn TrainingCheckpoint::best_val_loss(self : TrainingCheckpoint) -> Double

#
TrainingCheckpoint::from_state

#
TrainingCheckpoint::iter_num

fn TrainingCheckpoint::iter_num(self : TrainingCheckpoint) -> Int

#
TrainingConfig

pub struct TrainingConfig {
// private fields
}

#
TrainingConfig::TrainingConfig

fn TrainingConfig::TrainingConfig(batch_size~ : Int, block_size~ : Int, steps~ : Int, learning_rate~ : Double, min_lr? : Double, warmup_iters? : Int, eval_interval? : Int, eval_iters? : Int, log_interval? : Int, weight_decay? : Double, beta1? : Double, beta2? : Double, grad_clip? : Double, always_save_checkpoint? : Bool) -> TrainingConfig

#
TrainingConfig::always_save_checkpoint

fn TrainingConfig::always_save_checkpoint(self : TrainingConfig) -> Bool

#
TrainingConfig::batch_size

fn TrainingConfig::batch_size(self : TrainingConfig) -> Int

#
TrainingConfig::beta1

fn TrainingConfig::beta1(self : TrainingConfig) -> Double

#
TrainingConfig::beta2

fn TrainingConfig::beta2(self : TrainingConfig) -> Double

#
TrainingConfig::block_size

fn TrainingConfig::block_size(self : TrainingConfig) -> Int

#
TrainingConfig::eval_interval

fn TrainingConfig::eval_interval(self : TrainingConfig) -> Int

#
TrainingConfig::eval_iters

fn TrainingConfig::eval_iters(self : TrainingConfig) -> Int

#
TrainingConfig::grad_clip

fn TrainingConfig::grad_clip(self : TrainingConfig) -> Double

#
TrainingConfig::learning_rate

fn TrainingConfig::learning_rate(self : TrainingConfig) -> Double

#
TrainingConfig::learning_rate_at

fn TrainingConfig::learning_rate_at(self : TrainingConfig, iter : Int) -> Double

#
TrainingConfig::log_interval

fn TrainingConfig::log_interval(self : TrainingConfig) -> Int

#
TrainingConfig::min_lr

fn TrainingConfig::min_lr(self : TrainingConfig) -> Double

#
TrainingConfig::recommended

fn TrainingConfig::recommended() -> TrainingConfig

#
TrainingConfig::steps

fn TrainingConfig::steps(self : TrainingConfig) -> Int

#
TrainingConfig::warmup_iters

fn TrainingConfig::warmup_iters(self : TrainingConfig) -> Int

#
TrainingConfig::weight_decay

fn TrainingConfig::weight_decay(self : TrainingConfig) -> Double

#
TrainingResult

pub struct TrainingResult {
model : MiniGPT
tokenizer :
Tokenizer

stats : TrainingStats
}

#
TrainingState

pub struct TrainingState {
// private fields
}

#
TrainingState::best_val_loss

fn TrainingState::best_val_loss(self : TrainingState) -> Double

#
TrainingState::config

#
TrainingState::iter_num

fn TrainingState::iter_num(self : TrainingState) -> Int

#
TrainingState::model

fn TrainingState::model(self : TrainingState) -> MiniGPT

#
TrainingState::optimizer

#
TrainingStats

pub struct TrainingStats {
losses : Array[Double]
initial_eval_loss : Double
final_eval_loss : Double
best_val_loss : Double
saved_checkpoints : Int
}

#
BATCH_SIZE

let BATCH_SIZE : Int

#
BETA1

let BETA1 : Double

#
BETA2

let BETA2 : Double

#
BLOCK_SIZE

let BLOCK_SIZE : Int

#
DROPOUT

let DROPOUT : Double

#
EVAL_INTERVAL

let EVAL_INTERVAL : Int

#
EVAL_ITERS

let EVAL_ITERS : Int

#
GRAD_CLIP

let GRAD_CLIP : Double

#
LEARNING_RATE

let LEARNING_RATE : Double

#
LOG_INTERVAL

let LOG_INTERVAL : Int

#
MIN_LR

let MIN_LR : Double

#
MLP_MULTIPLIER

let MLP_MULTIPLIER : Int

#
N_EMBD

let N_EMBD : Int

#
N_HEAD

let N_HEAD : Int

#
N_LAYER

let N_LAYER : Int

#
TRAINING_STEPS

let TRAINING_STEPS : Int

#
WARMUP_ITERS

let WARMUP_ITERS : Int

#
WEIGHT_DECAY

let WEIGHT_DECAY : Double

#
decode_checkpoint

fn decode_checkpoint(bytes : Bytes) -> (MiniGPT,
Tokenizer
)

#
encode_checkpoint

fn encode_checkpoint(model : MiniGPT, tokenizer :
Tokenizer
) -> Bytes

#
encode_training_checkpoint

fn encode_training_checkpoint(checkpoint : TrainingCheckpoint) -> Bytes

#
generate_next_token_id

fn generate_next_token_id(model : MiniGPT, context_ids : Array[Int], rng :
Rand
, config : SamplingConfig) -> Int

#
generate_text

fn generate_text(model : MiniGPT, tokenizer :
Tokenizer
, prompt : String, max_new_tokens : Int, rng :
Rand
, config : SamplingConfig) -> String raise
TokenizerError

#
generate_token_ids

fn generate_token_ids(model : MiniGPT, prompt_ids : Array[Int], max_new_tokens : Int, rng :
Rand
, config : SamplingConfig) -> Array[Int]

#
train_text_with_architecture

fn train_text_with_architecture(text : String, tokenizer_config :
TokenizerConfig
, config : TrainingConfig, architecture : ArchitectureConfig, rng :
Rand
) -> TrainingResult

#
train_token_ids

fn train_token_ids(model : MiniGPT, train_ids : Array[Int], val_ids : Array[Int], config : TrainingConfig, rng :
Rand
) -> TrainingStats

#
train_token_ids_with_callbacks

fn train_token_ids_with_callbacks(model : MiniGPT, train_ids : Array[Int], val_ids : Array[Int], config : TrainingConfig, rng :
Rand
, callbacks : TrainingCallbacks) -> TrainingStats