怎么构造Token表
construct token
nn.Embedding,根据字符对应的序号取,是可训练的
Language Models are Unsupervised Multitask Learners
Token是大模型的原子,一切都以标记为单位,与标记有关,不要忽略它!!
非英语语言表现较差,英语比非英语多得多训练数据,得到更长的优质token
gpt2写python不利,太多缩进,gpt2把空格都当成了token
同样的文本,gpt4的token数量比gpt2少一半,这是因为其token数量是gpt2的两倍,但这并不是好事,这意味着embedding表会非常大
但有个最佳点,所有词汇表中的token数量恰到好处,适当密集且高效
gpt4对python的空格处理已经相当搞笑,多个空格为一个token,这样能看到前面更多的token
unicode是一个包含150000个不同代码点的词汇表,它不一定我是我们想用的稳定表示,三种编码方式UTF-8,16,32,变长编码
如果使用utf8,那转向字节对编码算法,允许我们将这些字节序列压缩到一个可变的量,找到最频繁的token对,用一个新token代替,并将其加入词汇表
压缩多少次,这是一个超参数
这是大模型训练的预处理阶段,更多的同一个语言,有利于压缩token的数量
担心词与标点符号被认为一个token,比如dog.,dog!,dog?这是我们不希望看到的,他们不应该合并
特别的token(endoftext用于分隔文档)
后期需要用到special token,用于分隔/特定顺序。比如gpt4中的FIM_MIDDLE(FIM表示填充,fill in middle)
specialToken有助于我们微调大模型以达到特定的目的
另一个常用的方法
sentencepiece
全面,在训练和推理方面都很高效。llama使用。
stp在unicode代码点上直接运行BPE,然后有选项
character_coverage去为出现很少的很稀有的代码点(遇到了训练时候没见过的),将它们映射到UNKnown,如果开了byte_fallback就变为utf-8
句子开头额外出现一个粗体下划线,这么做优点是对于tiktoken,同一个单词,他在句子开头出现和中间出现完全不同,在开头加个下划线,再把空格变下划线,那分词器会将他们理解为同一个词
为什么词汇表大小不能无限增长?
- 计算成本增加,参数增加
- 部分token出现得很少,那么关于它那部分的embedding训练不足
- 压缩得太厉害,一个token可能被展开成为成千上万个,transformer没有时间实际恰当地处理这些信息
经验10000-100000
Learning to Compress Prompts with Gist Tokens
如果要新增token,那么要保持整个模型冻结,只训练新标记机器嵌入表示。将很长的提示符压缩为几个新的要点标记,测试时候只需要丢弃旧的token,用新token重排表示prompt即可
gpt2 problem
在算数时候将一串连续数字划分为1,2,3,4长度的数字是随机的
其中标记要么是您完成下一个标记的第一个字符,要么就是很长的token,被拆分成一段段
有特定的触发词,模型查看包含它的词就会变得混乱。怎么做到的?词汇表和训练数据集不一致,训练数据集缺乏该词汇
值得去理解tokenization
完整代码
import regex as re
tokens = 'Unicode! 🅤🅝🅘🅒🅞🅓🅔‽ 🇺🇳🇮🇨🇴🇩🇪! 😄 The very name strikes fear and awe into the hearts of programmers worldwide. We all know we ought to “support Unicode” in our software (whatever that means—like using wchar_t for all the strings, right?). But Unicode can be abstruse, and diving into the thousand-page Unicode Standard plus its dozens of supplementary annexes, reports, and notes can be more than a little intimidating. I don’t blame programmers for still finding the whole thing mysterious, even 30 years after Unicode’s inception.'.encode('utf-8')
tokens = map(int ,tokens)
tokens = list(tokens)
def get_stats(ids):
counts = {}
for pair in zip(ids, ids[1:]):
counts[pair] = counts.get(pair, 0) + 1
return counts
stats = get_stats(tokens)
# print(stats)
sorted(((v, k) for (k, v) in stats.items()), reverse=True)
top_pair = max(stats, key=stats.get)
print(top_pair)
def merge(ids, pair, idx):
newids = []
i = 0
while i < len(ids):
if i < len(ids) - 1 and ids[i] == pair[0] and ids[i+1] == pair[1]:
newids.append(idx)
i += 2
else:
newids.append(ids[i])
i += 1
return newids
tokens2 = merge(tokens, top_pair, 256)
print(f'original: {len(tokens)} tokens, new: {len(tokens2)} tokens')
# 转换多少次,这是一个超参数,需要在实际应用中进行调整
vocab_size = 276
num_merges = vocab_size - 256
ids = list(tokens)
merges = {}
for i in range(num_merges):
stats = get_stats(tokens)
pair = max(stats, key=stats.get)
idx = 256 + i
# print(f'merging {pair} into a new token {idx}')
ids = merge(ids, pair, idx)
merges[pair] = idx
vocab = {idx : bytes([idx]) for idx in range(256)}
for (p0, p1), idx in merges.items():
vocab[idx] = vocab[p0] + vocab[p1]
def decode(ids):
tokens = b"".join(vocab[idx] for idx in ids)
text = tokens.decode('utf-8', errors="replace")
return text
def encode(text):
tokens = list(text.encode("utf-8"))
while len(tokens) >= 2:
stats = get_stats(tokens)
pair = min(stats, key=lambda p: merges.get(p, float('inf'))) # min merge priority
if pair not in merges:
break
idx = merges[pair]
tokens = merge(tokens, pair, idx)
return tokens
print(decode(encode("hello world")))
gpt2pat = re.compile(r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""")
print(re.findall(gpt2pat, 'hello"s you sun are seek '))
魔改transformer
大体结构
wte是输出嵌入,但它实际上是token嵌入
wpe是位置嵌入
这两条信息将被添加,然后进入transformer
h表示隐藏, 表示所有灰色块
lmhead是末尾的线性部分
注意力是规约,MLP是映射
激活函数改进
relu->gelu,主要区别是0点附近变成连续
llama3使用swiglu
多头注意力机制
原始版本就是单纯地连接输出向量
Andrej将QKV合为一体进行拆分,提高pytorch地处理速度
查询和键值相互作用引起注意力,自回归掩码,确保标记仅关注以前的标记,softmax标准化注意力
class GPTConfig:
block_size: int = 1024 # max sequence length
vocab_size: int = 50257 # number of tokens: 50,000 BPE merges + 256 bytes tokens + 1 <|endof
n_layer: int = 12 # number of layers
n_head: int = 12 # number of heads
n_embd: int = 768 # embedding dimension
参数设置
B个序列,最大长度不超过T
标记数量不能超过最长块
事实上,类似dropout,BN层在模型评估和训练模式下有比较大的差别
预测
while x.size(1) < max_length:
# forward the model to get the logits
with torch.no_grad():
logits = model(x) # (B, T, vocab_size)
# take the logits at the last position
logits = logits[:, -1, :] # (B, vocab_size)
# get the probabilities
probs = F.softmax(logits, dim=-1)
# do top-k sampling of 50 (huggingface pipeline default)
# topk_probs here becomes (5, 50), topk_indices is (5, 50)
topk_probs, topk_indices = torch.topk(probs, 50, dim=-1)
# select a token from the top-k probabilities
ix = torch.multinomial(topk_probs, 1) # (B, 1)
# gather the corresponding indices
xcol = torch.gather(topk_indices, -1, ix) # (B, 1)
# append to the sequence
x = torch.cat((x, xcol), dim=1)
# print the generated text
for i in range(num_return_sequences):
tokens = x[i, :max_length].tolist()
decoded = enc.decode(tokens)
print(">", decoded)
token
GPT2压缩率大致为3比1,因此1000个字符大约是300个标记
额外加载多一个标记,原buf作为输入,偏移一位作为标签(懂吧),预测下一位
计算损失使用交叉熵函数即可
怎么判断初始化是不是好的?如果是好的,那概率分布大致是分散的,也就是每个概率都相等
adamW修正误差,是adam的改进版本,保留了m和v,两个缓冲区
训练
损失是单个元素的张量,item是获取元素然后送回到CPU内存
对于模型来说直接调用todevice就能移动到GPU上,但是对于普通张量返回的是一个指针,需要重新赋值
在早期调试阶段运行的大多数优化中,学习率设为3e-4是一个不错的默认值
如果出现了不在词汇表中的,获取所有从未出现的logit的偏差并将它们驱动到负无穷大来获得不错的收益
如果你有两个语义相似的token,你会期望它们在transformer的输出中获得相同的概率,所以相似的token应该具有相似的嵌入向量
所以wte张量基本被使用了两次,在transformer的底部和顶部
同时不用训练这么多参数,训练会更有效率(共享)
模块有默认初始化函数
自定义初始化函数用self.apply + 初始化函数
gpt2觉得残差流会累积误差,残差网络的每个块会贡献方差,然后累加
int8用于推理,而不是训练,因为int8具有均匀的间距
batch_size,还有一些通道数取2的幂,因为cuda是按2的幂分配SM的
如果迫不得已,请增加到能被64整除
梯度norm:使得各层的梯度上限不超过1.0,这样可以使得模型对于比较大的梯度不敏感
weight_decay(分类讨论):我们可以设置维度大于等于2的进行正则化,一维就没必要了。
numel()是统计张量中元素的总数,adamw中有fusion选项提供,内核融合
用micro step拼凑起big step,累加梯度
warmup
torch.compile和自定义验证竟然冲突??
contiguous保证向量在内存中连续存储,便于view展开
模型编译的问题在于它确实使我们的代码更快,但破坏了评估代码和采样代码(在作者的代码中)
每一定步数保存检查点,模型的状态字典,还可以保存adam的状态字典,因为有超参数b和w
GPU
采用TF32,32位张量拥有19位(1符号,8阶码,10浮动),对于普通FP32来说截断了13位
支持输入FP32,输出FP32,但是内部计算过程中会被截断成TF32以更快地执行操作
启动TF32:
torch.set_float32_matmul_precision(‘high’)
使用autocast上下文管理器:
PyTorch 提供的一个上下文管理器,用于在支持的硬件上自动执行混合精度训练
with torch.autocast(device_type=‘cuda’, dtype=torch.bfloat16):
output
loss
loss.backward()
torch.compile:
用于将 PyTorch 模型编译为更高效的表示形式,以便在特定硬件上进行更快的执行,
编译会分析模型,确切知道模型收益于什么,不会像python一样一层一层地走,而是总体优化,编译整个模型成为一个没有py解释器参与的对象
举个例子,当你完成一层计算的时候,python并不知道接下来就要用到其中的数据,所以将其写回GPU内存,然后再取出来,时间成本在内存带宽上严重浪费。而经过编译后的模型就比较聪明了,他会保留数据到cache之中
GPU SRAM: 19 TB/s (20 MB)
SRAM GPU HBM: 1.5 TB/s (40 GB)
HBM Main Memory DRAM: 12.8 GB/s (>1TB) (CPU DRAM)
内核融合:将多个独立的内核操作(卷积,激活函数,归一化等)融合成一个单一的内核操作,从而减少内存访问次数,降低计算开销提高数据局部性
分布式数据并行
# run the training loop
from torch.distributed import init_process_group, destroy_process_group
# set up DDP (distributed data parallel).
# torchrun command sets the env variables RANK, LOCAL_RANK, and WORLD_SIZE
ddp = int(os.environ.get('RANK', -1)) != -1 # is this a ddp run?
if ddp:
# use of DDP atm demands CUDA, we set the device appropriately according to rank
assert torch.cuda.is_available(), "for now i think we need CUDA for DDP"
init_process_group(backend='nccl')
ddp_rank = int(os.environ['RANK'])
ddp_local_rank = int(os.environ['LOCAL_RANK'])
ddp_world_size = int(os.environ['WORLD_SIZE'])
device = f'cuda:{ddp_local_rank}'
torch.cuda.set_device(device)
master_process = ddp_rank == 0 # this process will do logging, checkpointing etc.
else:
# vanilla, non-DDP run
ddp_rank = 0
ddp_local_rank = 0
ddp_world_size = 1
master_process = True
# attempt to autodetect device
device = "cpu"
if torch.cuda.is_available():
device = "cuda"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
device = "mps"
print(f"using device: {device}")
使用ddp, torchrun配合,有点像并行程序设计
DDP在反向传播时候,对梯度同步并广播求平均,同步到每个GPU
向后同步当微小步是最后时候才设置为True打开
for step in range(max_steps):
t0 = time.time()
optimizer.zero_grad()
loss_accum = 0.0
for micro_step in range(grad_accum_steps):
x, y = train_loader.next_batch()
x, y = x.to(device), y.to(device)
with torch.autocast(device_type=device, dtype=torch.bfloat16):
logits, loss = model(x, y)
# we have to scale the loss to account for gradient accumulation,
# because the gradients just add on each successive backward().
# addition of gradients corresponds to a SUM in the objective, but
# instead of a SUM we want MEAN. Scale the loss here so it comes out right
loss = loss / grad_accum_steps
loss_accum += loss.detach()
if ddp:
model.require_backward_grad_sync = (micro_step == grad_accum_steps - 1)
loss.backward()
if ddp:
dist.all_reduce(loss_accum, op=dist.ReduceOp.AVG)
norm = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
# determine and set the learning rate for this iteration
lr = get_lr(step)
for param_group in optimizer.param_groups:
param_group['lr'] = lr
optimizer.step()
torch.cuda.synchronize() # wait for the GPU to finish work
数据集
Reddit,社交网络
FineWeb~
在GPT2时代,对原始输入数据内容不太重视,
但现在,对重复数据删除,过滤,质量过滤等方面的良好实践进行了更多的审查
优质token很重要!!
数据加载时候可以考虑随机打乱,每个新时期的每个分片中排列文档
可以在文档如何相互跟随方面引入一些随机性
测试集
HelloSwaq,来判断哪个是句子的自然延续
微调
此时我们得到的只是预训练的大模型,如果想要对话还需要进行微调
时候可以考虑随机打乱,每个新时期的每个分片中排列文档
可以在文档如何相互跟随方面引入一些随机性
测试集
HelloSwaq,来判断哪个是句子的自然延续
微调
此时我们得到的只是预训练的大模型,如果想要对话还需要进行微调
加入对话的训练集,有一个用户,助手之类的结构
完整代码
import math
import time
import torch
import torch.nn as nn
from torch.nn import functional as F
class CausalSelfAttention(nn.Module):
def __init__(self, config):
assert config.n_embd % config.n_head == 0
self.c_attn = nn.Linear(config.n_embd, config.n_embd * 3)
self.n_head = config.n_head
self.c_proj = nn.Linear(config.n_embd, config.n_embd)
self.n_embd = config.n_embd
self.register_buffer("bias", torch.tril(torch.ones(config.block_size, config.block_size))
.view(1, 1, config.block_size, config.block_size))
self.c_proj.NANOGPT_SCALE_INIT = 1 # 不会与之前东西冲突
def forward(self, x):
B, T, C = x.size()
qkv = self.c_attn(x)
q, k, v = qkv.split(C, dim=2)
q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
# att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
# att = att.masked_fill(self.bias[:, :, :T, :T] == 0, float('-inf'))
# att = F.softmax(att, dim=-1)
# y = att @ v
# flashAttention
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
y = y.transpose(1, 2).contiguous().view(B, T, C)
y = self.c_proj(y)
return y
class MLP(nn.Module):
def __init__(self, config):
super().__init__()
self.c_fc = nn.Linear(config.n_embd, config.n_embd * 4)
self.gelu = nn.GELU(approximate='tanh')
self.c_proj = nn.Linear(config.n_embd * 4, config.n_embd)
self.c_proj.NANOGPT_SCALE_INIT = 1 # 不会与之前东西冲突
def forward(self, x):
x = self.gelu(self.c_fc(x))
x = self.c_proj(x)
return x
class Block(nn.Module):
def __init__(self, config):
super().__init__()
self.ln1 = nn.LayerNorm(config.n_embd)
self.attn = CausalSelfAttention(config)
self.ln2 = nn.LayerNorm(config.n_embd)
self.mlp = MLP(config)
def forward(self, x):
x = x + self.attn(self.ln1(x))
x = x + self.mlp(self.ln2(x))
return x
class GPTConfig:
block_size: int = 1024
vocab_size: int = 50257
n_layer: int = 12
n_head: int = 12
n_embd: int = 768
class GPT(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
self.transformer = nn.ModuleDict(dict(
wte = nn.Embedding(config.vocab_size, config.n_embd),
wpe = nn.Embedding(config.block_size, config.n_embd),
h = nn.ModuleList([Block(config) for _ in range(config.n_layer)]),
ln_f = nn.LayerNorm(config.n_embd)
))
self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
# 共享权重
self.transformer['wte'].weight = self.lm_head.weight
# 初始化权重
self.apply(self._init_weights)
def _init_weights(self, module):
if isinstance(module, nn.Linear):
std = 0.02
if hasattr(module, 'NANOGPT_SCALE_INIT'):
std *= (2 * self.config.n_layer) ** -0.5
torch.nn.init.normal_(module.weight, mean=0.0, std=std)
if module.bias is not None:
torch.nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
if module.padding_idx is not None:
torch.nn.init.zeros_(module.weight[module.padding_idx])
def forward(self, idx, targets=None):
b, t = idx.size()
assert t <= self.config.block_size, 'Cannot forward, model block size is exhausted.'
h = self.transformer['wte'](idx) + self.transformer['wpe'](torch.arange(t, device=idx.device))
for block in self.transformer['h']:
h = block(h)
h = self.transformer['ln_f'](h)
lm_logits = self.lm_head(h)
if targets is None:
return lm_logits
loss = F.cross_entropy(lm_logits.view(-1, lm_logits.size(-1)), targets.view(-1))
return loss
model = GPT(GPTConfig())
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, betas=(0.9, 0.95), eps=1e-8)
for i in range(50):
t0 = time.time()
torch.cuda.synchronize()
optimizer.zero_grad()
with torch.autocast(device_type='cpu', dtype=torch.bfloat16):
logits, loss = model(input_ids, labels)
loss.backward()
norm = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
# 设置学习率
lr = get_lr()