Merge pull request #272 from karpathy/feature/customtokenizer
Big Change: Custom Tokenizer training: add the ability to train custom tokenizers instead of using the pretrained Llama 2 tokenizer. This is useful in custom, narrow-domain LLMs because smaller vocab sizes make much smaller, faster, and potentially more capable models. For example, in tinystories a vocab size 4096 custom tokenizer compresses the input text sequences about as well as the Llama 2 tokenizer with vocab size 32000. The result is also "safer" because a badly trained model can't accidentally e.g. output some random chinese character and rapidly go "off the rails" in subsequent tokens.
This commit is contained in:
@@ -142,6 +142,49 @@ Which gives the same results. More detailed testing will be done in `test_all.py
|
||||
$ pytest
|
||||
```
|
||||
|
||||
## custom tokenizers
|
||||
|
||||
In everything above, we've assumed the custom Lllama 2 tokenizer with 32,000 tokens. However, in many boutique LLMs, using vocabulary this big might be an overkill. If you have a small application you have in mind, you might be much better off training your own tokenizers. This can make everything nicer - with smaller vocabs your model has fewer parameters (because the token embedding table is a lot smaller), the inference is faster (because there are fewer tokens to predict), and your average sequence length per example could also get smaller (because the compression is a lot more efficient on your data). So let's see how we train a custom tokenizer.
|
||||
|
||||
By default, to pretokenize the tinystories dataset we had to run, in order:
|
||||
|
||||
```
|
||||
python tinystories.py download
|
||||
python tinystories.py pretokenize
|
||||
```
|
||||
|
||||
The `pretokenize` stage here loads the Llama 2 tokenizer (vocab size 32,000) and uses it to convert the downloaded text into integers, and saves that to file. We now change this as follows, to train an example 4096-token tokenizer:
|
||||
|
||||
```
|
||||
python tinystories.py download
|
||||
python tinystories.py train_vocab --vocab_size=4096
|
||||
python tinystories.py pretokenize --vocab_size=4096
|
||||
```
|
||||
|
||||
The `train_vocab` stage will call the `train_vocab.sh` script, which calls the `sentencepiece` library to train the tokenizer, storing it in a new file `data/tok4096.model`. I tried to reproduce as well as I could the settings that (I think) Meta used to train their vocabulary. This uses the Byte Pair Encoding algorithm that starts out with raw utf8 byte sequences of the text data and then iteratively merges the most common consecutive pairs of tokens to form the vocabulary. Inspect the `tinystories.py` file - the custom tokenizers are stored in a special directory structure indexed by the vocab size.
|
||||
|
||||
A quick note of interest is that vocab size of 4096 trained specifically on tinystories creates integer sequences with about the same sequence length per example as the default Llama 2 tokenizer of 32000 tokens! This means that our custom, tailored tokenizer is a lot better adapted to our specific text, and can compress it very effectively. So our trained models are smaller and faster.
|
||||
|
||||
Now that we have pretokenized the dataset with our custom tokenizer, we can train the model. The training script `train.py` doesn't care about the exact tokens, it only cares about the vocabulary size so it can correctly initialize the model. So when training your model, make sure to pass in
|
||||
|
||||
```
|
||||
python train.py --vocab_source=custom --vocab_size=4096
|
||||
```
|
||||
|
||||
(The defaults are `llama2` and `32000` respectively, which indicates the default Llama 2 tokenizer). This trains the model. Finally we are ready to run inference with our `run.c` script. For that we need two things. Number one, we have to export our tokenizer in the `.bin` format, do that with:
|
||||
|
||||
```
|
||||
python tokenizer.py --tokenizer-model=data/tok4096.model
|
||||
```
|
||||
|
||||
This writes the tokenizer to `data/tok4096.bin`. Now we can run inference, pointing it to this tokenizer using the `-z` flag:
|
||||
|
||||
```
|
||||
./run out/model.bin -z data/tok4096.bin
|
||||
```
|
||||
|
||||
This should print the samples. If you leave out the `-z` flag, it will use the default Llama 2 tokenizer, which would generate a good sequence of integers, but they would get translated using a different vocabulary to text, so it would look like gibberish.
|
||||
|
||||
## performance
|
||||
|
||||
There are many ways to potentially speed up this code depending on your system. Have a look at the [Makefile](Makefile), which contains a lot of notes. The `make run` command currently uses the `-O3` optimization by default, i.e.:
|
||||
@@ -249,12 +292,12 @@ If your candidate PRs have elements of these it doesn't mean they won't get merg
|
||||
|
||||
## unsorted todos
|
||||
|
||||
- revive tests; train a tiny Llama test model (committed to repo) and use it as reference in unit tests
|
||||
- make it easier to add a new dataset with not too much pain
|
||||
- add multiquery support into run.c
|
||||
- add custom bpe training code and the ability to train a smaller vocabulary (32K is to much)
|
||||
- should calculate freq_cis online in the script run.c instead of loading them
|
||||
- int4/8 quantization
|
||||
- export the model in a more sensible output format with a proper header, etc.
|
||||
- train a tiny Llama test model (committed to repo) and use it as reference in unit tests
|
||||
- support Llama 2 7B Chat models and tune run.c to Chat UI/UX
|
||||
- llama2.cu investigate and merge
|
||||
- (LoRA) finetuning and export of Llama 2 models
|
||||
|
||||
@@ -11,12 +11,13 @@ from torch import nn
|
||||
|
||||
@dataclass
|
||||
class ModelArgs:
|
||||
# default hyperparameters for the Llama 7B model
|
||||
dim: int = 4096
|
||||
n_layers: int = 32
|
||||
n_heads: int = 32
|
||||
n_kv_heads: Optional[int] = None
|
||||
vocab_size: int = -1 # defined later by tokenizer
|
||||
multiple_of: int = 256 # make SwiGLU hidden layer size multiple of large power of 2
|
||||
vocab_size: int = 32000
|
||||
multiple_of: int = 256 # MLP hidden layer size will be multiple of
|
||||
norm_eps: float = 1e-5
|
||||
max_seq_len: int = 2048
|
||||
dropout: float = 0.0
|
||||
|
||||
@@ -508,6 +508,7 @@ void error_usage() {
|
||||
fprintf(stderr, " -s <int> random seed, default time(NULL)\n");
|
||||
fprintf(stderr, " -n <int> number of steps to run for, default 256. 0 = max_seq_len\n");
|
||||
fprintf(stderr, " -i <string> input prompt\n");
|
||||
fprintf(stderr, " -z <string> optional path to custom tokenizer\n");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
@@ -515,6 +516,7 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
// default inits
|
||||
char *checkpoint = NULL; // e.g. out/model.bin
|
||||
char *tokenizer = "tokenizer.bin";
|
||||
float temperature = 1.0f; // 0.0 = greedy deterministic. 1.0 = original. don't set higher
|
||||
float topp = 1.0f; // top-p in nucleus sampling. 1.0 = off. 0.9 works well, but slower
|
||||
rng_seed = 0; // seed rng with time by default
|
||||
@@ -534,6 +536,7 @@ int main(int argc, char *argv[]) {
|
||||
else if (argv[i][1] == 's') { rng_seed = atoi(argv[i + 1]); }
|
||||
else if (argv[i][1] == 'n') { steps = atoi(argv[i + 1]); }
|
||||
else if (argv[i][1] == 'i') { prompt = argv[i + 1]; }
|
||||
else if (argv[i][1] == 'z') { tokenizer = argv[i + 1]; }
|
||||
else { error_usage(); }
|
||||
}
|
||||
if(rng_seed == 0) { rng_seed = (unsigned int)time(NULL);}
|
||||
@@ -567,13 +570,13 @@ int main(int argc, char *argv[]) {
|
||||
// right now we cannot run for more than config.seq_len steps
|
||||
if (steps <= 0 || steps > config.seq_len) { steps = config.seq_len; }
|
||||
|
||||
// read in the tokenizer.bin file
|
||||
// read in the tokenizer .bin file
|
||||
char** vocab = (char**)malloc(config.vocab_size * sizeof(char*));
|
||||
float* vocab_scores = (float*)malloc(config.vocab_size * sizeof(float));
|
||||
unsigned int max_token_length;
|
||||
{
|
||||
FILE *file = fopen("tokenizer.bin", "rb");
|
||||
if (!file) { fprintf(stderr, "couldn't load tokenizer.bin\n"); return 1; }
|
||||
FILE *file = fopen(tokenizer, "rb");
|
||||
if (!file) { fprintf(stderr, "couldn't load %s\n", tokenizer); return 1; }
|
||||
if (fread(&max_token_length, sizeof(int), 1, file) != 1) { fprintf(stderr, "failed read\n"); return 1; }
|
||||
int len;
|
||||
for (int i = 0; i < config.vocab_size; i++) {
|
||||
|
||||
@@ -9,6 +9,8 @@ import tiktoken
|
||||
from model import ModelArgs, Transformer
|
||||
from tokenizer import Tokenizer
|
||||
|
||||
from tinystories import get_tokenizer_model_path
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
out_dir = 'out' # ignored if init_from is not 'resume'
|
||||
start = "" # or "<|endoftext|>" or etc. Can also specify a file, use as: "FILE:prompt.txt"
|
||||
@@ -51,7 +53,9 @@ if compile:
|
||||
model = torch.compile(model) # requires PyTorch 2.0 (optional)
|
||||
|
||||
# load the tokenizer
|
||||
enc = Tokenizer()
|
||||
assert checkpoint["config"]["dataset"] == "tinystories" # TODO: generalize
|
||||
tokenizer_model = get_tokenizer_model_path(vocab_size=gptconf.vocab_size)
|
||||
enc = Tokenizer(tokenizer_model=tokenizer_model)
|
||||
|
||||
# encode the beginning of the prompt
|
||||
if start.startswith('FILE:'):
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
"""
|
||||
Download, preprocess and serve the TinyShakespeare dataset as a DataLoader.
|
||||
|
||||
Follows the same interface as the TinyStories dataset.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from tqdm import tqdm
|
||||
|
||||
from tokenizer import Tokenizer
|
||||
|
||||
DATA_CACHE_DIR = "data"
|
||||
|
||||
def download_file(url: str, fname: str, chunk_size=1024):
|
||||
"""Helper function to download a file from a given url"""
|
||||
resp = requests.get(url, stream=True)
|
||||
total = int(resp.headers.get("content-length", 0))
|
||||
with open(fname, "wb") as file, tqdm(
|
||||
desc=fname,
|
||||
total=total,
|
||||
unit="iB",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
) as bar:
|
||||
for data in resp.iter_content(chunk_size=chunk_size):
|
||||
size = file.write(data)
|
||||
bar.update(size)
|
||||
|
||||
|
||||
def download():
|
||||
"""Downloads the dataset to disk."""
|
||||
os.makedirs(DATA_CACHE_DIR, exist_ok=True)
|
||||
|
||||
# download the TinyShakespeare dataset, unless it's already downloaded
|
||||
data_url = "https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt"
|
||||
data_filename = os.path.join(DATA_CACHE_DIR, "tinyshakespeare.txt")
|
||||
if not os.path.exists(data_filename):
|
||||
print(f"Downloading {data_url} to {data_filename}...")
|
||||
download_file(data_url, data_filename)
|
||||
else:
|
||||
print(f"{data_filename} already exists, skipping download...")
|
||||
|
||||
print("Download done.")
|
||||
|
||||
def pretokenize():
|
||||
enc = Tokenizer()
|
||||
|
||||
data_file = os.path.join(DATA_CACHE_DIR, "tinyshakespeare.txt")
|
||||
|
||||
all_tokens = []
|
||||
with open(data_file, "r") as f:
|
||||
for line in f:
|
||||
text = line.strip()
|
||||
tokens = enc.encode(text, bos=True, eos=False)
|
||||
all_tokens.extend(tokens)
|
||||
all_tokens = np.array(all_tokens, dtype=np.uint16)
|
||||
print(f"Total tokens: {len(all_tokens)}")
|
||||
with open(data_file.replace(".txt", ".bin"), "wb") as f:
|
||||
f.write(all_tokens.tobytes())
|
||||
print(f"Saved {data_file.replace('.txt', '.bin')}")
|
||||
print("Done.")
|
||||
|
||||
|
||||
class PretokDataset(torch.utils.data.IterableDataset):
|
||||
"""Loads pretokenized examples from disk and yields them as PyTorch tensors."""
|
||||
|
||||
def __init__(self, split, max_seq_len):
|
||||
super().__init__()
|
||||
self.split = split
|
||||
self.max_seq_len = max_seq_len
|
||||
|
||||
def __iter__(self):
|
||||
# get worker info within a DataLoader
|
||||
worker_info = torch.utils.data.get_worker_info()
|
||||
worker_id = worker_info.id if worker_info else 0
|
||||
# get DDP rank info
|
||||
rank = dist.get_rank() if dist.is_initialized() else 0
|
||||
# combine the worker_id and worker_rank to create a unique seed for rng
|
||||
seed = 42 + worker_id + 1337 * rank
|
||||
rng = random.Random(seed)
|
||||
print(f"Created a PretokDataset with rng seed {seed}")
|
||||
data_file = os.path.join(DATA_CACHE_DIR, "tinyshakespeare.bin")
|
||||
m_all = np.memmap(data_file, dtype=np.uint16, mode="r")
|
||||
|
||||
# split out 10% of the data for validation
|
||||
split_ix = int(len(m_all) * 0.9)
|
||||
if self.split == "train":
|
||||
m = m_all[:split_ix]
|
||||
else:
|
||||
m = m_all[split_ix:]
|
||||
|
||||
num_batches = len(m) // self.max_seq_len
|
||||
num_batches -= 1 # drop the last partial batch
|
||||
assert num_batches > 0, "this split is way too small? investigate."
|
||||
|
||||
while True:
|
||||
ixs = list(range(num_batches))
|
||||
rng.shuffle(ixs)
|
||||
for ix in ixs:
|
||||
start = ix * self.max_seq_len
|
||||
end = start + self.max_seq_len + 1
|
||||
# calling .astype will copy the data into a new numpy array, now in RAM
|
||||
chunk = torch.from_numpy((m[start:end]).astype(np.int64))
|
||||
x = chunk[:-1]
|
||||
y = chunk[1:]
|
||||
yield x, y
|
||||
|
||||
|
||||
class ShakespeareTask:
|
||||
|
||||
@staticmethod
|
||||
def iter_batches(split, batch_size, max_seq_len, device, num_workers=0):
|
||||
ds = PretokDataset(split, max_seq_len)
|
||||
dl = torch.utils.data.DataLoader(
|
||||
ds, batch_size=batch_size, pin_memory=True, num_workers=num_workers
|
||||
)
|
||||
for x, y in dl:
|
||||
x = x.to(device, non_blocking=True)
|
||||
y = y.to(device, non_blocking=True)
|
||||
yield x, y
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("stage", type=str, choices=["download", "train_tokenizer", "pretokenize"])
|
||||
args = parser.parse_args()
|
||||
|
||||
# depending on the stage call the appropriate function
|
||||
fun = {
|
||||
"download": download,
|
||||
"pretokenize": pretokenize,
|
||||
}
|
||||
fun[args.stage]()
|
||||
+126
-20
@@ -9,6 +9,7 @@ import os
|
||||
import random
|
||||
from typing import List
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from functools import partial
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
@@ -37,7 +38,7 @@ def download_file(url: str, fname: str, chunk_size=1024):
|
||||
|
||||
|
||||
def download():
|
||||
"""Downloads the dataset to disk."""
|
||||
"""Downloads the TinyStories dataset to DATA_CACHE_DIR"""
|
||||
os.makedirs(DATA_CACHE_DIR, exist_ok=True)
|
||||
|
||||
# download the TinyStories dataset, unless it's already downloaded
|
||||
@@ -66,10 +67,61 @@ def download():
|
||||
print(f"Number of shards: {len(shard_filenames)}")
|
||||
print(f"Example story:\n{data[0]}")
|
||||
|
||||
def train_vocab(vocab_size):
|
||||
"""
|
||||
Trains a custom sentencepiece tokenizer on the TinyStories dataset.
|
||||
The custom tokenizer files will be saved in DATA_CACHE_DIR/tok{N} directories,
|
||||
where N is the vocab size. This is also where the pretok .bin files will go.
|
||||
"""
|
||||
assert vocab_size > 0, "Vocab size must be positive"
|
||||
|
||||
def process_shard(args):
|
||||
# output file prefix path for sentencepiece
|
||||
prefix = os.path.join(DATA_CACHE_DIR, f"tok{vocab_size}")
|
||||
|
||||
# how many shards we'll use for vocab training, kept low for efficiency
|
||||
num_shards = 10
|
||||
|
||||
# 1) export a large chunk of text as a single text file tiny.txt
|
||||
tiny_file = os.path.join(DATA_CACHE_DIR, "tiny.txt")
|
||||
data_dir = os.path.join(DATA_CACHE_DIR, "TinyStories_all_data")
|
||||
shard_filenames = sorted(glob.glob(os.path.join(data_dir, "*.json")))
|
||||
|
||||
print(f"Writing temporary file {tiny_file} with {num_shards} shards...")
|
||||
with open(tiny_file, "w") as of:
|
||||
for shard in tqdm(shard_filenames[:num_shards]):
|
||||
with open(shard, "r") as f:
|
||||
data = json.load(f)
|
||||
for example in data:
|
||||
text = example["story"]
|
||||
text = text.strip()
|
||||
of.write(text + "\n")
|
||||
print(f"Size is: {os.path.getsize(tiny_file) / 1024 / 1024:.2f} MB")
|
||||
|
||||
# 2) run the train_vocab.sh script that trains the sentencepiece model
|
||||
print("Will now train the vocab with:")
|
||||
cmd = f"bash train_vocab.sh {tiny_file} {prefix} {vocab_size}"
|
||||
print(cmd)
|
||||
print("OK? [y/N] ")
|
||||
dec = input()
|
||||
if dec.lower() != "y":
|
||||
print("Exiting...")
|
||||
return
|
||||
os.system(cmd)
|
||||
|
||||
# 3) optional cleanup, ask the user if they'd like to delete tiny.txt
|
||||
dec = input(f"Delete the temporary file {tiny_file}? [y/N] ")
|
||||
if dec.lower() == "y":
|
||||
os.remove(tiny_file)
|
||||
print(f"Deleted {tiny_file}")
|
||||
|
||||
print(f"Trained tokenizer is in {prefix}.model")
|
||||
print("Done.")
|
||||
|
||||
|
||||
def process_shard(args, vocab_size):
|
||||
shard_id, shard = args
|
||||
enc = Tokenizer()
|
||||
tokenizer_model = get_tokenizer_model_path()
|
||||
enc = Tokenizer(tokenizer_model)
|
||||
with open(shard, "r") as f:
|
||||
data = json.load(f)
|
||||
all_tokens = []
|
||||
@@ -80,31 +132,49 @@ def process_shard(args):
|
||||
all_tokens.extend(tokens)
|
||||
# convert to uint16 nparray
|
||||
all_tokens = np.array(all_tokens, dtype=np.uint16)
|
||||
# write to disk
|
||||
tokenized_filename = shard.replace(".json", ".bin")
|
||||
# calculate the output filename
|
||||
if vocab_size == 0:
|
||||
# if we're using Llama 2, just save the tokenized file in the same dir
|
||||
tokenized_filename = shard.replace(".json", ".bin")
|
||||
else:
|
||||
# save .bin files into a new tok{N} directory
|
||||
bin_dir = os.path.join(DATA_CACHE_DIR, f"tok{vocab_size}")
|
||||
shard_basename = os.path.basename(shard)
|
||||
bin_basename = shard_basename.replace(".json", ".bin")
|
||||
tokenized_filename = os.path.join(bin_dir, bin_basename)
|
||||
# write the bytes
|
||||
with open(tokenized_filename, "wb") as f:
|
||||
f.write(all_tokens.tobytes())
|
||||
print(f"Saved {tokenized_filename}")
|
||||
# calculate the average sequence length (they are separated by BOS=1)
|
||||
avg_seq_len = all_tokens.size / ((all_tokens == 1).sum())
|
||||
print(f"Saved {tokenized_filename}, average seqlen: {avg_seq_len:.2f}")
|
||||
|
||||
|
||||
def pretokenize():
|
||||
def pretokenize(vocab_size):
|
||||
# iterate the shards and tokenize all of them one by one
|
||||
data_dir = os.path.join(DATA_CACHE_DIR, "TinyStories_all_data")
|
||||
shard_filenames = sorted(glob.glob(os.path.join(data_dir, "*.json")))
|
||||
if vocab_size > 0:
|
||||
# .bin files will be saved into tok{N} directory, create it once here
|
||||
bin_dir = os.path.join(DATA_CACHE_DIR, f"tok{vocab_size}")
|
||||
os.makedirs(bin_dir, exist_ok=True)
|
||||
|
||||
# process all the shards in a process pool
|
||||
fun = partial(process_shard, vocab_size=vocab_size)
|
||||
with ProcessPoolExecutor() as executor:
|
||||
executor.map(process_shard, enumerate(shard_filenames))
|
||||
executor.map(fun, enumerate(shard_filenames))
|
||||
print("Done.")
|
||||
|
||||
|
||||
class PretokDataset(torch.utils.data.IterableDataset):
|
||||
"""Loads pretokenized examples from disk and yields them as PyTorch tensors."""
|
||||
|
||||
def __init__(self, split, max_seq_len):
|
||||
def __init__(self, split, max_seq_len, vocab_size, vocab_source):
|
||||
super().__init__()
|
||||
self.split = split
|
||||
self.max_seq_len = max_seq_len
|
||||
self.vocab_size = vocab_size
|
||||
self.vocab_source = vocab_source
|
||||
|
||||
def __iter__(self):
|
||||
# get worker info within a DataLoader
|
||||
@@ -116,8 +186,14 @@ class PretokDataset(torch.utils.data.IterableDataset):
|
||||
seed = 42 + worker_id + 1337 * rank
|
||||
rng = random.Random(seed)
|
||||
print(f"Created a PretokDataset with rng seed {seed}")
|
||||
data_dir = os.path.join(DATA_CACHE_DIR, "TinyStories_all_data")
|
||||
shard_filenames = sorted(glob.glob(os.path.join(data_dir, "*.bin")))
|
||||
if self.vocab_source == "llama2":
|
||||
# the .bin files are right along the .json files
|
||||
bin_dir = os.path.join(DATA_CACHE_DIR, "TinyStories_all_data")
|
||||
shard_filenames = sorted(glob.glob(os.path.join(bin_dir, "*.bin")))
|
||||
elif self.vocab_source == "custom":
|
||||
# the .bin files are in tok{N} directory
|
||||
bin_dir = os.path.join(DATA_CACHE_DIR, f"tok{self.vocab_size}")
|
||||
shard_filenames = sorted(glob.glob(os.path.join(bin_dir, "*.bin")))
|
||||
# train/test split. let's use only shard 0 for test split, rest train
|
||||
shard_filenames = shard_filenames[1:] if self.split == "train" else shard_filenames[:1]
|
||||
while True:
|
||||
@@ -139,12 +215,25 @@ class PretokDataset(torch.utils.data.IterableDataset):
|
||||
y = chunk[1:]
|
||||
yield x, y
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# public interface functions
|
||||
|
||||
def get_tokenizer_model_path(vocab_size):
|
||||
"""
|
||||
Returns path to the sentencepiece tokenizer model for a given vocab size
|
||||
vocab_size = 0 designates the default Llama 2 tokenizer, in that case
|
||||
None is returned.
|
||||
"""
|
||||
if vocab_size == 0:
|
||||
return None
|
||||
else:
|
||||
return os.path.join(DATA_CACHE_DIR, f"tok{vocab_size}.model")
|
||||
|
||||
class Task:
|
||||
|
||||
@staticmethod
|
||||
def iter_batches(split, batch_size, max_seq_len, device, num_workers=0):
|
||||
ds = PretokDataset(split, max_seq_len)
|
||||
def iter_batches(batch_size, device, num_workers=0, **dataset_kwargs):
|
||||
ds = PretokDataset(**dataset_kwargs)
|
||||
dl = torch.utils.data.DataLoader(
|
||||
ds, batch_size=batch_size, pin_memory=True, num_workers=num_workers
|
||||
)
|
||||
@@ -153,16 +242,33 @@ class Task:
|
||||
y = y.to(device, non_blocking=True)
|
||||
yield x, y
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# CLI for constructing the dataset
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
These stages are designed to be run in order.
|
||||
|
||||
To tokenize data with the Llama 2 tokenizer:
|
||||
python tinystories.py download
|
||||
python tinystories.py pretokenize
|
||||
|
||||
To tokenize data with a custom tokenizer we train ourselves with sentencepiece, e.g.:
|
||||
python tinystories.py download
|
||||
python tinystories.py train_vocab --vocab_size=2048
|
||||
python tinystories.py pretokenize --vocab_size=2048
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("stage", type=str, choices=["download", "train_tokenizer", "pretokenize"])
|
||||
parser.add_argument("stage", type=str, choices=["download", "pretokenize", "train_vocab"])
|
||||
parser.add_argument("--vocab_size", type=int, default=0, help="pretokenization vocab size. 0 = use Llama 2 tokenizer.")
|
||||
args = parser.parse_args()
|
||||
|
||||
# depending on the stage call the appropriate function
|
||||
fun = {
|
||||
"download": download,
|
||||
"pretokenize": pretokenize,
|
||||
}
|
||||
fun[args.stage]()
|
||||
|
||||
if args.stage == "download":
|
||||
download()
|
||||
elif args.stage == "train_vocab":
|
||||
train_vocab(vocab_size=args.vocab_size)
|
||||
elif args.stage == "pretokenize":
|
||||
pretokenize(vocab_size=args.vocab_size)
|
||||
else:
|
||||
raise ValueError(f"Unknown stage {args.stage}")
|
||||
|
||||
+13
-8
@@ -4,20 +4,19 @@
|
||||
|
||||
import os
|
||||
import struct
|
||||
from logging import getLogger
|
||||
import argparse
|
||||
from typing import List
|
||||
|
||||
from sentencepiece import SentencePieceProcessor
|
||||
|
||||
TOKENIZER_MODEL = "tokenizer.model" # the llama sentencepiece tokenizer model
|
||||
TOKENIZER_BIN = "tokenizer.bin" # binary version of the tokenizer for inference in C
|
||||
|
||||
class Tokenizer:
|
||||
def __init__(self):
|
||||
model_path = TOKENIZER_MODEL
|
||||
def __init__(self, tokenizer_model=None):
|
||||
model_path = tokenizer_model if tokenizer_model else TOKENIZER_MODEL
|
||||
assert os.path.isfile(model_path), model_path
|
||||
self.sp_model = SentencePieceProcessor(model_file=model_path)
|
||||
#print(f"Loaded SentencePiece model from {model_path}")
|
||||
self.model_path = model_path
|
||||
|
||||
# BOS / EOS token IDs
|
||||
self.n_words: int = self.sp_model.vocab_size()
|
||||
@@ -59,17 +58,23 @@ class Tokenizer:
|
||||
|
||||
tokens.append(b)
|
||||
scores.append(s)
|
||||
|
||||
|
||||
# record the max token length
|
||||
max_token_length = max(len(t) for t in tokens)
|
||||
|
||||
# write to a binary file
|
||||
with open(TOKENIZER_BIN, 'wb') as f:
|
||||
# the tokenizer.bin file is the same as .model file, but .bin
|
||||
tokenizer_bin = self.model_path.replace('.model', '.bin')
|
||||
with open(tokenizer_bin, 'wb') as f:
|
||||
f.write(struct.pack("I", max_token_length))
|
||||
for bytes, score in zip(tokens, scores):
|
||||
f.write(struct.pack("fI", score, len(bytes)))
|
||||
f.write(bytes)
|
||||
|
||||
if __name__ == "__main__":
|
||||
t = Tokenizer()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-t", "--tokenizer-model", type=str, help="optional path to custom tokenizer ")
|
||||
args = parser.parse_args()
|
||||
|
||||
t = Tokenizer(args.tokenizer_model)
|
||||
t.export()
|
||||
|
||||
@@ -29,7 +29,6 @@ from torch.distributed import destroy_process_group, init_process_group
|
||||
from torch.nn.parallel import DistributedDataParallel as DDP
|
||||
|
||||
from tinystories import Task
|
||||
from tinyshakespeare import ShakespeareTask
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# I/O
|
||||
@@ -47,7 +46,8 @@ wandb_run_name = "run" + datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
|
||||
# data
|
||||
batch_size = 128 # if gradient_accumulation_steps > 1, this is the micro-batch size
|
||||
max_seq_len = 256
|
||||
dataset = "tinystories" # tinystories|tinyshakespeare
|
||||
vocab_source = "llama2" # llama2|custom; use Lllama 2 vocab from Meta, or custom trained
|
||||
vocab_size = 32000 # the Llama 2 tokenizer has 32K tokens
|
||||
# model
|
||||
dim = 288
|
||||
n_layers = 6
|
||||
@@ -83,6 +83,10 @@ config = {k: globals()[k] for k in config_keys} # will be useful for logging
|
||||
lr_decay_iters = max_iters # should be ~= max_iters per Chinchilla
|
||||
min_lr = 0.0 # minimum learning rate, should be ~= learning_rate/10 per Chinchilla
|
||||
|
||||
# validating checks
|
||||
assert vocab_source in ["llama2", "custom"]
|
||||
assert vocab_source == "custom" or vocab_size == 32000, "The vocab from Meta has 32K tokens"
|
||||
|
||||
# various inits, derived attributes, I/O setup
|
||||
ddp = int(os.environ.get("RANK", -1)) != -1 # is this a ddp run?
|
||||
if ddp:
|
||||
@@ -123,11 +127,12 @@ ctx = (
|
||||
)
|
||||
|
||||
# task-specific setup
|
||||
task = {'tinystories': Task, 'tinyshakespeare': ShakespeareTask}[dataset]
|
||||
iter_batches = partial(
|
||||
task.iter_batches,
|
||||
Task.iter_batches,
|
||||
batch_size=batch_size,
|
||||
max_seq_len=max_seq_len,
|
||||
vocab_size=vocab_size,
|
||||
vocab_source=vocab_source,
|
||||
device=device,
|
||||
num_workers=0,
|
||||
)
|
||||
@@ -142,7 +147,7 @@ model_args = dict(
|
||||
n_layers=n_layers,
|
||||
n_heads=n_heads,
|
||||
n_kv_heads=n_heads,
|
||||
vocab_size=32000,
|
||||
vocab_size=vocab_size,
|
||||
multiple_of=multiple_of,
|
||||
max_seq_len=max_seq_len,
|
||||
dropout=dropout,
|
||||
@@ -206,7 +211,7 @@ def estimate_loss():
|
||||
out = {}
|
||||
model.eval()
|
||||
for split in ["train", "val"]:
|
||||
batch_iter = iter_batches(split)
|
||||
batch_iter = iter_batches(split=split)
|
||||
losses = torch.zeros(eval_iters) # keep on CPU
|
||||
for k in range(eval_iters):
|
||||
X, Y = next(batch_iter)
|
||||
@@ -238,7 +243,7 @@ if wandb_log and master_process:
|
||||
wandb.init(project=wandb_project, name=wandb_run_name, config=config)
|
||||
|
||||
# training loop
|
||||
train_batch_iter = iter_batches("train")
|
||||
train_batch_iter = iter_batches(split="train")
|
||||
X, Y = next(train_batch_iter) # fetch the very first batch
|
||||
t0 = time.time()
|
||||
local_iter_num = 0 # number of iterations in the lifetime of this process
|
||||
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Trains a sentencepiece tokenizer model on a bunch of given data, my best
|
||||
# effort attempt to replicate how Meta trained their Llama 2 tokenizer.
|
||||
|
||||
# usage: $ train_vocab.sh <input> <model_prefix> <vocab_size>
|
||||
# example:
|
||||
# ./train_vocab.sh tiny.txt tokenizer_tiny 1024
|
||||
# requirements:
|
||||
# install https://github.com/google/sentencepiece
|
||||
|
||||
# check if the correct number of arguments are provided
|
||||
if [ $# -ne 3 ]; then
|
||||
echo "Usage: $0 <input> <model_prefix> <vocab_size>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# assign command-line arguments to variables
|
||||
input=$1
|
||||
model_prefix=$2
|
||||
vocab_size=$3
|
||||
|
||||
# check if input file exists
|
||||
if [ ! -f "$input" ]; then
|
||||
echo "Usage: $0 <input> <model_prefix> <vocab_size>"
|
||||
echo "input '$input' not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if vocab_size is a positive integer
|
||||
if ! [[ "$vocab_size" =~ ^[0-9]+$ ]] || [ "$vocab_size" -lt 1 ]; then
|
||||
echo "Usage: $0 <input> <model_prefix> <vocab_size>"
|
||||
echo "vocab_size size must be a positive integer."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Print the processed inputs
|
||||
echo "Input: $input"
|
||||
echo "Model Prefix: $model_prefix"
|
||||
echo "Vocabulary Size: $vocab_size"
|
||||
|
||||
# train a sentencepiece tokenizer model
|
||||
# Llama 2 config can be printed as follows:
|
||||
|
||||
# import sentencepiece.sentencepiece_model_pb2
|
||||
# mp = sentencepiece.sentencepiece_model_pb2.ModelProto()
|
||||
# mp.ParseFromString(open("tokenizer.model", "rb").read())
|
||||
# print(mp.trainer_spec)
|
||||
# print(mp.normalizer_spec)
|
||||
|
||||
# this gives:
|
||||
|
||||
# trainer_spec {
|
||||
# input: "/large_experiments/theorem/datasets/MERGED/all.test1.merged"
|
||||
# model_prefix: "spm_model_32k_200M_charcov099995_allowWSO__v2"
|
||||
# model_type: BPE
|
||||
# vocab_size: 32000
|
||||
# self_test_sample_size: 0
|
||||
# input_format: "text"
|
||||
# character_coverage: 0.9999499917030334
|
||||
# input_sentence_size: 200000000
|
||||
# seed_sentencepiece_size: 1000000
|
||||
# shrinking_factor: 0.75
|
||||
# num_threads: 80
|
||||
# num_sub_iterations: 2
|
||||
# max_sentence_length: 4192
|
||||
# shuffle_input_sentence: true
|
||||
# max_sentencepiece_length: 16
|
||||
# split_by_unicode_script: true
|
||||
# split_by_whitespace: true
|
||||
# split_by_number: true
|
||||
# treat_whitespace_as_suffix: false
|
||||
# split_digits: true
|
||||
# allow_whitespace_only_pieces: true
|
||||
# vocabulary_output_piece_score: true
|
||||
# hard_vocab_limit: true
|
||||
# use_all_vocab: false
|
||||
# byte_fallback: true
|
||||
# required_chars: ""
|
||||
# unk_id: 0
|
||||
# bos_id: 1
|
||||
# eos_id: 2
|
||||
# pad_id: -1
|
||||
# unk_surface: " \342\201\207 "
|
||||
# unk_piece: "<unk>"
|
||||
# bos_piece: "<s>"
|
||||
# eos_piece: "</s>"
|
||||
# pad_piece: "<pad>"
|
||||
# train_extremely_large_corpus: false
|
||||
# enable_differential_privacy: false
|
||||
# differential_privacy_noise_level: 0.0
|
||||
# differential_privacy_clipping_threshold: 0
|
||||
# }
|
||||
# normalizer_spec {
|
||||
# name: "identity"
|
||||
# precompiled_charsmap: ""
|
||||
# add_dummy_prefix: true
|
||||
# remove_extra_whitespaces: false
|
||||
# normalization_rule_tsv: ""
|
||||
# }
|
||||
|
||||
# let's now use spm_train to train this exact model
|
||||
# options docs: https://github.com/google/sentencepiece/blob/master/doc/options.md
|
||||
|
||||
# we'll depart on a few settings:
|
||||
# character_coverage -> 1.0
|
||||
|
||||
# other important notes:
|
||||
# --split-digits = true, per the paper
|
||||
# --allow_whitespace_only_pieces is true, default in spm is false
|
||||
# --byte_fallback is true, default in spm is false
|
||||
# --normalization_rule_name is identity, default in spm is nmt_nfkc
|
||||
|
||||
spm_train --input="$input" \
|
||||
--model_prefix="$model_prefix" \
|
||||
--model_type=bpe \
|
||||
--vocab_size="$vocab_size" \
|
||||
--self_test_sample_size=0 \
|
||||
--input_format="text" \
|
||||
--character_coverage=1.0 \
|
||||
--num_threads="$(nproc)" \
|
||||
--split_digits=true \
|
||||
--allow_whitespace_only_pieces=true \
|
||||
--byte_fallback=true \
|
||||
--unk_surface=" \342\201\207 " \
|
||||
--normalization_rule_name=identity \
|
||||
Reference in New Issue
Block a user