/* Defines the GPT-2 Tokenizer. Only supports decoding, i.e.: tokens (integers) -> strings This is all we need for unconditional generation. If we wanted to later prompt the model, we'd have to add decoding. Which could be tricky in C because of the regex involved, to look into later. */ #include #include #include // our own utilities // defines fopenCheck, freadCheck, fcloseCheck, fseekCheck, mallocCheck #include "utils.h" // ---------------------------------------------------------------------------- typedef struct { uint32_t vocab_size; char **token_table; int init_ok; int eot_token; // <|endoftext|> token id } Tokenizer; void safe_printf(const char *piece) { // the tokens are raw bytes, and we we only want to print the printable ones // many bytes can be various control codes, backspace, etc. if (piece == NULL) { return; } if (piece[0] == '\0') { return; } // handle individual byte tokens // every token is asserted to be at least one byte so doing piece[1] is ok if (piece[1] == '\0') { unsigned char byte_val = piece[0]; if (!(isprint(byte_val) || isspace(byte_val))) { return; // weird byte, don't print it } } printf("%s", piece); } void tokenizer_init(Tokenizer *tokenizer, const char *filename) { FILE *file = fopen(filename, "rb"); if (file == NULL) { // try to be more helpful as we just added this feature, erase later printf("---\n"); printf("WARNING: Failed to open the tokenizer file %s\n", filename); printf("The Tokenizer is a new feature added April 14 2024.\n"); printf("Re-run `python train_gpt2.py` to write it\n"); printf("---\n"); tokenizer->init_ok = 0; return; } // read in the header uint32_t header[256]; freadCheck(header, sizeof(uint32_t), 256, file); assert(header[0] == 20240328); int version = header[1]; tokenizer->vocab_size = header[2]; if (version == 1) { // version 1 didn't include the EOT token id // so we assume it is 50256, the EOT in GPT-2 assert(tokenizer->vocab_size == 50257); // let's be defensive here tokenizer->eot_token = 50256; } else if (version == 2) { tokenizer->eot_token = header[3]; } else { fprintf(stderr, "Tokenizer model file %s has bad version: %d\n", filename, version); exit(EXIT_FAILURE); } // read in all the tokens unsigned char length; tokenizer->token_table = (char **)mallocCheck(tokenizer->vocab_size * sizeof(char *)); for (uint32_t i = 0; i < tokenizer->vocab_size; i++) { freadCheck(&length, sizeof(unsigned char), 1, file); assert(length > 0); // every token should be at least one character char *token_bytes = (char *)mallocCheck(length + 1); freadCheck(token_bytes, sizeof(char), length, file); token_bytes[length] = '\0'; // Add null terminator for printing tokenizer->token_table[i] = token_bytes; } // cleanups fcloseCheck(file); tokenizer->init_ok = 1; } const char *tokenizer_decode(Tokenizer *tokenizer, uint32_t token_id) { if (tokenizer->init_ok == 0) { return NULL; } if (token_id < tokenizer->vocab_size) { return tokenizer->token_table[token_id]; } else { printf("invalid token id %d!\n", token_id); return NULL; } } void tokenizer_free(Tokenizer *tokenizer) { if (tokenizer->init_ok) { for (uint32_t i = 0; i < tokenizer->vocab_size; i++) { free(tokenizer->token_table[i]); } free(tokenizer->token_table); } }