type
Post
status
Published
date
Jan 17, 2025
slug
summary
tags
category
icon
password
前言:
llama系列属于半开源产品,什么是半开源,也就是只开源了推理流程,没有开源训练流程
通过pytorch-llama项目入门大模型
github地址:https://github.com/hkproj/pytorch-llama
模型下载地址:https://huggingface.co/meta-llama/Llama-2-7b/tree/main
从中可以学到
1.大模型推理流程
2.旋转位置编码
3.kv cache,学习为什么需要kv cache,以及为什么cache kv,而不cache q
学习文章
等入门大模型需要学习的知识以及细节等
模型参数:
propmts长度为4
max_gen_len最大生成长度:64
top_p: 0.9
temperature: 0.6
推理流程
- 创建一个size为(4,64+max(tokenizer.encode(prompts))=115)的Tensor(tokens)用于存放模型生成的token_id,默认填充都是-1。tokens先将promtp填充进去。
- 创建一个长度114,下标从1开始,模型输入是(4,1),然后遍历
cur_iterator,一个一个的生成下一个token,模型每次输出(4,1,32000)size的logits。cur_iterator = tqdm(range(1, total_len), desc="Generating tokens") - 对probs进行降序排序,得到概率值降序列表和对应的下标列表。
- 将每个概率值与前面的所有概率和进行累加,得到
probs_sum 将 probs_sum - probs_sort > p的所有概率置为0- 将每个概率除以概率和,使得概率和还是1
- 从中随机选择一个下标
- 然后根据probs_ids得到对应的token。size为(4,1)
if temperature>0: (top_p参数是有用的)
对
logits 进行softmax,得到(4,32000)size的probs。top_p进行采样,采样逻辑如下:



理解一下top_p的作用,也就是将前n个概率相加,直到遇到再加上一个概率大于top_p,那么样本集就是这n个概率,top_p设置的越大,样本集中的token个数越多,反之越小。
else: (top_p参数无用)
直接从logits选择最大数值对应的下标
- 替代next token的时候需要判断一下,tokens这个位置上是原本的就有内容(prompt)的还是pad_id,如果是pad_id则进行替换
- 判断是否都到了eos_token,如果都到了,则结束循环
- 结束循环后,判断是不是有eos_id,如果有则将eos_id前面的内容打印出来
模型推理细节
输入输出
是一个token一个token的进行推理,模型输入是(4,1),模型输出是(4,1,32000),32000就是vocab_size。模型输入的时候额外传入一个start_pos,从1开始,生成第一个token传入的就是1,从2开始传入的就是2

- tokens经过nn.Embedding变成h,h的size是(4,1,4096)
- 从提前计算好的复数频率张量中,根据start_pos选取一行,第一次是1,那么就选择第一行,

- 核心:self_attention层
传入self_attention进行计算,三个入参,x(tokens),start_pos, freqs_complex
计算xq, size(4,1,4096)
计算xk, size(4,1,4096)
计算xv, size(4,1,4096)
分多头:
xq (4,1,32,128),32✖️128=4096
xk (4,1,32,128)
xv (4,1,32,128)
应用旋转位置编码:xq不应用
xk (4,1,32,128)
xv (4,1,32,128)
kv cache
self.cache_k和self.cache_v都是(4,1024,32,128),初始值都是0

self.cache_k[:batch_size, start_pos : start_pos + seq_len] = xkself.cache_v[:batch_size, start_pos : start_pos + seq_len] = xvself.cache_k[:4,1:2] = xk
self.cache_v[:4,1:2] = xv
从缓存中取值:取前start_pos+1对应的值,seq_len永远是1,因为是一个token一个token生成的

keys = self.cache_k[:batch_size, : start_pos + seq_len]values = self.cache_v[:batch_size, : start_pos + seq_len] 

注意力计算
xq 与 keys进行计算得到scores
scores与values计算得到output
kv cache总结
每次遍历的时候计算的xk,xv存入cache_k,cache_v列表中,然后将之前所有的计算好的取出来的到keys,values,然后参与后续的注意力计算。
花了两天理解了为什么模型推理的时候每次只喂一个token,我的理解是将所有的token都喂给模型,这样模型才能根据所有的上文语义信息预测下一个token啊,如果只给模型喂一个token,那么怎么得到next_token呢,我对这个产生了疑问。
如果模型的输入是(batch_size, seq_len=12, dim),那么模型的输出也是这个,相当于预测了12个token,前11个token都是重复计算的,然后只取最后一个token就是模型预测出来的下一个token的结果,这个是我能理解的方式,模型训练也是这么训练的。
如果模型的输入是(batch_size, seq_len=1, dim)那么模型的输出也是(batch_size, seq_len=1, dim),只喂给了模型一个token,模型能get到上文的语义信息吗。
那么这两种计算方式到底等不等价。答案是结合kv cache这两种方式是等价的。
通过scores*v得出来的注意力矩阵,得到模型输出
如果喂给模型所有上下文信息(batch_size, seq_len=12, dim),那么scores*v得出来的注意力矩阵也是这个,然后通过一个Linear层得到模型输出是(batch_size, seq_len=12, vocab_size),假设得到了next_token,那么下一次喂给模型的就是(batch_size, seq_len=13, dim),那么模型输出就是(batch_size, seq_len=13, vocab_size),那么这个矩阵的第十三行也就是next_token的结果,根据矩阵运算,可以看出来,第十三行的结果,只与第十二个token计算出来的q,以及前12个k,v有关,所有每次只喂给模型一个token就行,只要前面计算出来的kv都缓存下来,那么就不影响最终的结果。
这时候在看那篇知乎文章就理解的很通透了。
有问题,欢迎您在底部评论区留言,一起交流~