前面我们读取出了 gpt2 的参数张量配置,现在我们接着看 main 函数
char *tiny_stories_train = "data/TinyStories_train.bin";
char *tiny_stories_val = "data/TinyStories_val.bin";
char *tiny_shakespeare_train = "data/tiny_shakespeare_train.bin";
char *tiny_shakespeare_val = "data/tiny_shakespeare_val.bin";
char *train_tokens = access(tiny_shakespeare_train, F_OK) != -1 ? tiny_shakespeare_train : tiny_stories_train;
char *val_tokens = access(tiny_shakespeare_val, F_OK) != -1 ? tiny_shakespeare_val : tiny_stories_val;
这个其实没什么好说的,有「小小莎士比亚」就用,没有就用「小故事」。 我们「小小莎士比亚」已经准备好了,直接略过。
DataLoader
int B = 4;
int T = 64;
DataLoader train_loader;
dataloader_init(&train_loader, train_tokens, B, T);
printf("train dataset num_batches: %d\n", train_loader.num_batches);
DataLoader val_loader;
dataloader_init(&val_loader, val_tokens, B, T);
printf("val dataset num_batches: %d\n", val_loader.num_batches);
int val_num_batches = 10;
B
可能是 micro batch length,T
可能是 sequence length。
看上去是用上面的 train_tokens
和 val_tokens
来初始化两个 DataLoader
,
所以我们要看一下初始化时做了什么:
void dataloader_init(DataLoader *loader, char *filename, int B, int T)
{
loader->B = B;
loader->T = T;
// open the input file for reading
loader->tokens_file = fopen(filename, "rb");
if (loader->tokens_file == NULL)
{
printf("Error opening tokens file\n");
exit(1);
}
// determine the file size
fseek(loader->tokens_file, 0, SEEK_END);
loader->file_size = ftell(loader->tokens_file);
fseek(loader->tokens_file, 0, SEEK_SET);
if (loader->file_size < (B * T + 1) * sizeof(int))
{
printf("Error: file size is too small for the batch size and sequence length\n");
exit(1);
}
loader->current_position = 0; // start at the beginning
// allocate space for B*T + 1 integers to store the inputs and targets
loader->batch = (int *)malloc((B * T + 1) * sizeof(int));
loader->inputs = loader->batch;
loader->targets = loader->batch + 1; // targets are shifted by one
loader->num_batches = loader->file_size / (B * T * sizeof(int));
}
主要的内容是读取出文件大小,并为 batch
申请空间,这个写出来也不难:
impl DataLoader {
fn new(tokens_filename: &str, micro_batch_len: usize, sequence_len: usize) -> Self {
let tokens_file_content = fs::read(tokens_filename).unwrap();
let tokens_file_size = tokens_file_content.len();
let batch_token_size = micro_batch_len * sequence_len * 4;
Self {
tokens_file_content,
tokens_file_size,
micro_batch_len,
sequence_len,
batch_content: vec![0; batch_token_size + 4],
inputs_index: 0,
targets_index: 4,
current_position: 0,
num_batches: tokens_file_size / batch_token_size,
}
}
}
主要差别有一点,因为 llm.c 中保存的地址是 int*
,
所以 rust 的 target 需要偏移 4 个 byte。
然后就是训练的大循环了:
for (int step = 0; step <= 40; step++) {
// once in a while estimate the validation loss
if (step % 10 == 0) {
...
}
// once in a while do model inference to print generated text
if (step > 0 && step % 20 == 0) {
...
}
// do a training step
...
}
熟悉编程的朋友会注意到,这个循环会跑 41 次训练,每 10 次会估算一下验证损失, 每 20 次会印出生成文字。所以看上去这个循环多跑了一次训练, 不过最后一次训练后的结果并没有用到。
再接下来,就没有什么知识点了:
// free
dataloader_free(&train_loader);
dataloader_free(&val_loader);
gpt2_free(&model);
return 0;
看上去就是把前面申请的内容释放掉,这个我们可能完全不用担心。
好了,目前看上去我们只有关键循环没有去做了,老实说这系列我已经有些想弃坑了, 一来由于中间插入了一个系列,导致中间隔了差不多要半年的时间, 这半年原来的 llm.c 还在不断进化,我这个已经有点旧了,如果更新的话, 前面两篇估计是要重写,那其实也相当于弃坑了。二来, 这半年已经涌出了不少其他语言版本的 fork。 对 C 不熟的朋友也可以去看看有没有自己熟悉一点的语言。 所以我这个系列可能只对我自己还有些价值了。