ChatGLM大模型推理加速之Speculative Decoding

       

目录

一、推测解码speculative decoding

1、自回归解码

2、speculative decoding

3、细节理解

二、核心逻辑代码

1、算法流程代码

2、模型自回归代码

a、带缓存的模型自回归实现代码

b、优化版本带缓存的模型自回归实现代码

c、ChatGLM的past_key_values的回滚

三、效果实测

1、效果对比

2、解码日志展示

        大模型时代,模型的推理效率尤为重要,推理速度的快慢和模型生成的质量好坏对用户的体验影响很大。大模型生成速度慢,生成效果好;小模型推理速度快,但是推理质量稍差。当前大模型推理速度满不足不了业务实效性需求,小模型不能满足业务质量指标的情况下存不存在一种业务在实际落地的时候最优选择呢?google论文Fast Inference from Transformers via Speculative Decoding和deepmind论文Accelerating Large Language Model Decoding with Speculative Sampling给出了相同思路的解决方案,也就是这篇博客要谈到的东西Speculative Decoding,翻译为推测解码。简单的来说,推测解码一种联合小模型和大模型各自的优势的解码算法,用大模型来对小模型的生成结果进行评估和修正,以实现完美保留大模型的生成质量的同时提升推理速度的目标。

一、推测解码speculative decoding

1、自回归解码

首先看看自回归解码的流程,如图所示

1、把输入序列输入模型,把输出结果logits处理成概率分布,采样得到next token。

2、把新生成的next token和原来的输入序列拼接在一起,作为新的输入序列,然后重复第一步。直到结束——新生成的token是结尾标识符EOS或者达到最大限定长度。

注意其中1中得到next token可以采用greedy search、beam search、topK和topP sample 以及contrastive search等算法。

2、speculative decoding

推测解码本质也也是自回归解码,大体流程上和上述流程相似,只不过在推理过程才使用了一大一小的模型,对效率和质量做了权衡,当然也是以牺牲显存和两次模型训练为代价的。用google论文中的示意图来举例说明:

小模型先生成japan’s benchmark bond n,大模型对这些文本进行评估,发现bond生成不好,拒绝该生成,丢弃掉bond n 这部分生成,修正生成为 nikkei ;然后把japan’s benchmark nikkei 拼接起来由小模型继续生成,直到结束位置。

具体的流程是什么样的呢?看deepmind论文中的算法伪代码

根据上述算法伪代码,可以得出Speculative Sampling的具体流程:

1、设置小模型每次自回归解码的步数K以及最大解码输出长度T。

2、首先用小模型Mq做自回归生成,连续生成采样得到gama个tokens,并同时得到每个token的概率分布q_logits。

3、然后用前缀输入prefix和这个gama个tokens拼接在一起,输入大模型Mp执行forward,得到每个token的概率分布p_logits。

4、使用大小模型的logits做对比,如果大模型发现小模型的某个token不好,拒绝它,使用某种办法重新采样这个token;如果小模型生成的token都很好,大模型直接生成下一个token

5、然后重复2-4步骤,直到结束条件。

3、细节理解

算法中有几个细节需要注意

1、论文中并行的计算得到小模型生成的gama个tokens的大模型对应的logits

其实这里的理解不要太复杂了,就是等待小模型生成完了gama个tokens以后直接用大模型进行forward,这里就利用了矩阵计算,并行的得到了大模型对应的logits。不需要考虑的复杂,小模型生成一个token,1号大模型就计算一次;小模型再生成一个,2号大模型再计算该token的logits。这样极其浪费资源并且并没有一次forward那么快。

2、怎么来评价小模型生成的token好不好?

利用概率分布来评价,这里有个前提,大模型的质量比较好,生成的结果都是正确的。大模型下,该token概率为p,小模型下为q,如果p>q,说明大模型都认为这个位置该生成这个token,那么就接受这个token;反之则以1-p/q的概率拒绝这个token——大模型的概率越小就越要拒绝,越大就约不拒绝。

3、拒绝该token后,怎么生成新的token

论文中给出了一个方案,使用如上公式用大模型的logtis和小模型的logits进行计算得到一个新的概率分布——最后采样的结果就是大模型和小模型概率差异化越大,就越能被采样到。

以上就是原始论文中的核心思想和重要的地方,但是在代码实现的时候,还是有一些细节的,使用缓存进行加速推理、logits归一化的加速、采样的方案选择等等,对后续算法的效果和效率都有一定的影响。

二、核心逻辑代码

          首先声明该本篇博客中实现的算法代码借鉴了方佳瑞博士大模型推理妙招—投机采样(Speculative Decoding)的实现整体思路,局部细节进行了优化,并使用ChatGLM基座模型进行了效果实测,证实优化后生成质量和推理速度均有提升。

1、算法流程代码

def speculative_decode_generate(self, prompt, gamma, do_sample=False, debug=False, promotion=True):eos_token = self.tokenizer.eos_token_id# 小模型生成类初始化approx_model_kv_cached_genration = KVCachedGenration(self.approx_model, eos_token, self.temperature, self.top_k,self.top_p, do_sample)# 大模型生成类初始化target_model_kv_cached_genration = KVCachedGenration(self.target_model, eos_token, self.temperature, self.top_k,self.top_p, do_sample)inputs = self.tokenizer([prompt], return_tensors="pt", padding=True)prefix = inputs['input_ids'].to(self.device)seq_len = prefix.shape[1]T = seq_len + self.max_lenend = Tassert prefix.shape[0] == 1, "input batch size must be 1"eos_token_index = Nonewith torch.no_grad():while prefix.shape[1] < T:prefix_len = prefix.shape[1]# 得到小模型的生成结果x, index = approx_model_kv_cached_genration.generate_promotion(prefix, gamma, debug)#记录小模型生成的eos_token位置if index is not None:eos_token_index = prefix_len + index# 得到大模型的生成结果t_x, _ = target_model_kv_cached_genration.generate_promotion(x, 1, debug)n = prefix_len + gamma - 1# 大模型评价小模型效果for i in range(gamma):if self.random_seed:torch.manual_seed(self.random_seed)# r==1的时候表示只要大模型的logits比小模型的小,就拒绝生成r = torch.rand(1, device=self.device)else:r = torch.as_tensor(1.0, dtype=torch.float, device=self.device)j = x[:, prefix_len + i]# 拒接 以1-p/q 的概率拒绝 生成if r > (target_model_kv_cached_genration.prob_history[:, prefix_len + i - 1,j] / approx_model_kv_cached_genration.prob_history[:, prefix_len + i - 1, j]):n = prefix_len + i - 1break# 根据小模型采纳的token位置来回滚past_key_values缓存approx_model_kv_cached_genration.kv_rollback(n + 1)prefix = x[:, :n + 1]if eos_token_index is not None:# 如果小模型生成的eos不被接受,则继续生成if eos_token_index > n:eos_token_index = Noneelse:# 如果小模型生成的eos被接受,则停止break# 如果拒绝一些小模型生成的token,从大模型中采样if n < prefix_len + gamma - 1:if do_sample:t = sample(max_fn(target_model_kv_cached_genration.prob_history[:, n,:] - approx_model_kv_cached_genration.prob_history[:, n, :]))else:# t = torch.argmax(max_fn(target_model_kv_cached_genration.prob_history[:, n,#                         :] - approx_model_kv_cached_genration.prob_history[:, n, :]),#                  dim=-1).unsqueeze(0)# 直接取大模型logits最大的那个tokent = torch.argmax(target_model_kv_cached_genration.prob_history[:, n, :], dim=-1).unsqueeze(0)# 根据生成的token位置大模型回滚past_key_values缓存target_model_kv_cached_genration.kv_rollback(n + 1)else:if do_sample:t = sample(target_model_kv_cached_genration.prob_history[:, -1, :])else:t = torch.argmax(target_model_kv_cached_genration.prob_history[:, -1, :], dim=-1).unsqueeze(0)# 根据生成的token位置大模型回滚past_key_values缓存target_model_kv_cached_genration.kv_rollback(n + 2)prefix = torch.cat((prefix, t), dim=1)# 如果生成eos token 则停止生成if t == eos_token:eos_token_index = prefix.shape[1] - 1breakif eos_token_index:end = eos_token_index + 1input_ids = inputs['input_ids'].tolist()[0]assert len(input_ids) <= end, "eos_token选择位置错误"output_token_ids = prefix.tolist()[0][len(input_ids):end]#得到最终输出response = self.tokenizer.decode(output_token_ids)

代码中实现了上述推测解码的整个流程,并给出了退出解码过程的条件。也实现了不同采样token的方式以及不同拒绝小模型token的策略,对实际结果也是很有影响的。

2、模型自回归代码

这里其实也比较细节的,实现的好不好会影响模型最终生成的效率的,加不加缓存以及向量归一化都会极大影响模型推理速度。

a、带缓存的模型自回归实现代码
@torch.no_grad()
def generate(self, input_ids, gamma):eos_index = Nonex = input_idsfor index, _ in enumerate(range(gamma)):# past_key_values存在,非首次推理if self.past_key_values:kv_cached_len = self.past_key_values[0][0].shape[0]# 获取位置idsposition_ids = self.get_position_ids(x, x.device)position_ids = position_ids[..., kv_cached_len:]# ChatGLM一定要输入位置编码,一般推理的时候模型内部会自己配置位置编码的outputs = self.model(x[:, kv_cached_len:], position_ids=position_ids,past_key_values=self.past_key_values, use_cache=True)logits = outputs.logits# logits归一化for i in range(logits.shape[1]):logits[:, i, :] = norm_logits(logits[:, i, :], self.temperature, self.top_k, self.top_p)probs = logits[:, -1, :]self.past_key_values = outputs.past_key_valuesself.prob_history = torch.cat([self.prob_history, logits], dim=1)else:# past_key_values不存在,首次推理outputs = self.model(x)self.prob_history = outputs.logits# logits归一化for i in range(self.prob_history.shape[1]):self.prob_history[:, i, :] = norm_logits(self.prob_history[:, i, :], self.temperature, self.top_k,self.top_p)self.past_key_values = outputs.past_key_valuesprobs = self.prob_history[:, -1, :]# 采样得到next_tokif self.do_sample:next_tok = sample(probs)else:next_tok = torch.argmax(probs, dim=-1).unsqueeze(0)x = torch.cat((x, next_tok), dim=1)if next_tok == self.eos_token and eos_index is None:eos_index = indexreturn x, eos_index# copy from https://github.com/LeeSinLiang/microGPT/blob/ed40cf9780dbeb180adfe94c227d4aa97e69250e/gpt.py
def top_k_top_p_filter(logits: torch.Tensor, top_k: int = 0, top_p: float = 0.0):"""Args:logits (torch.Tensorpe_): 2D tensor with shape (batch, vocab)top_k (int, optional): top_k. Defaults to 0.top_p (float, optional): top_p. Defaults to 0.0.Returns:torch.Tensor: a renormalized logits"""if top_k > 0:filter = torch.topk(logits, min(top_k, logits.size(-1)))[0]logits[logits < filter[:, [-1]]] = float('-inf')if top_p > 0.0:sorted_logits, sorted_indices = torch.sort(logits, descending=True)cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)filter = cumulative_probs > top_pfilter[..., 1:] = filter[..., :-1].clone()filter[..., 0] = 0indices_to_remove = filter.scatter(1, sorted_indices, filter)logits[indices_to_remove] = float('-inf')return logitsdef norm_logits(logits: torch.Tensor, temperature: float, top_k: float, top_p: float) -> torch.Tensor:"""Args:logits (torch.Tensor): shape (1, vocab)temperature (float): temperaturetop_k (float): top_ktop_p (float): top_pReturns:torch.Tensor: next token with shape as (batch,  1)"""assert logits.dim() == 2logits = logits / temperaturelogits = top_k_top_p_filter(logits, top_k=top_k, top_p=top_p)probs = F.softmax(logits, dim=1)return probsdef sample(probs: torch.Tensor, num_samples: int = 1):idx_next = torch.multinomial(probs, num_samples=num_samples)if (idx_next.item() == 0):raise RuntimeErrorreturn idx_next

正常的自回归流程,每次生成的时候都会传入past_key_values(这个时候模型输入就是x[:, kv_cached_len:])进行加速,同时输入position_ids保证结果正确。得到logits后进行归一化(为了后面能够对比大小模型的logits,大模型能够评价小模型,必须scale到同一个值域里面)、topK、topP以及softmax处理,然后采样得到下一个token。以上是没有优化过的版本,在归一化、topK、topP以及softmax处理过程中,大小模型首次推理以及大模型非首次推理都是多个token,代码里采用for循环的方式实现,耗时太多,可以矩阵计算并行化。

b、优化版本带缓存的模型自回归实现代码

并不是优化自回归这个算法本身,是优化自回归过程中大小模型首次推理以及大模型非首次推理logits归一化的实现。这里采用并行计算也就是矩阵计算。

def norm_logits_whole(logits: torch.Tensor, temperature: float, top_k: float, top_p: float) -> torch.Tensor:# 优化一版提速  矩阵计算代替原来的for循环assert logits.dim() == 3"""Args:logits (torch.Tensor): shape (1, vocab)temperature (float): temperaturetop_k (float): top_ktop_p (float): top_pReturns:torch.Tensor: next token with shape as (batch,  1)"""def top(scores: torch.FloatTensor, top_k: int = 0, top_p: float = 0.0):sorted_logits, sorted_indices = torch.sort(scores, descending=False)cumulative_probs = sorted_logits.softmax(dim=-1)cumulative_probs = cumulative_probs.cumsum(dim=-1)sorted_indices_to_remove = cumulative_probs <= (1 - top_p)sorted_indices_to_remove[..., -1:] = 0# 主要是这句代码优化indices_to_remove = sorted_indices_to_remove.scatter(2, sorted_indices, sorted_indices_to_remove)scores = scores.masked_fill(indices_to_remove, -float("Inf"))return scoresif top_k > 0:filter = torch.topk(logits, min(2, logits.size(-1)))[0]logits[logits < filter[..., [-1]]] = float('-inf')logits = logits / temperaturelogits = top(logits, top_k, top_p)probs = F.softmax(logits, dim=2)return probs@torch.no_grad()
def generate_promotion(self, input_ids, gamma, debug=False):eos_index = Nonex = input_idsfor index, _ in enumerate(range(gamma)):if self.past_key_values:kv_cached_len = self.past_key_values[0][0].shape[0]position_ids = self.get_position_ids(x, x.device)position_ids = position_ids[..., kv_cached_len:]# ChatGLM一定要输入位置编码,一般推理的时候模型内部会自己配置位置编码的outputs = self.model(x[:, kv_cached_len:], position_ids=position_ids,past_key_values=self.past_key_values, use_cache=True)logits = outputs.logits# 优化提速版本归一化logits = norm_logits_whole(logits, self.temperature, self.top_k, self.top_p)probs = logits[:, -1, :]self.past_key_values = outputs.past_key_valuesself.prob_history = torch.cat([self.prob_history, logits], dim=1)else:outputs = self.model(x)self.prob_history = outputs.logits# 优化提速版本归一化self.prob_history = norm_logits_whole(self.prob_history, self.temperature, self.top_k, self.top_p)self.past_key_values = outputs.past_key_valuesprobs = self.prob_history[:, -1, :]if self.do_sample:next_tok = sample(probs)else:next_tok = torch.argmax(probs, dim=-1).unsqueeze(0)x = torch.cat((x, next_tok), dim=1)if next_tok == self.eos_token and eos_index is None:eos_index = indexreturn x, eos_indexdef sample(probs: torch.Tensor, num_samples: int = 1):idx_next = torch.multinomial(probs, num_samples=num_samples)if (idx_next.item() == 0):raise RuntimeErrorreturn idx_next
c、ChatGLM的past_key_values的回滚
@torch.no_grad()
def kv_rollback(self, end_index):past_key_values_keeps = []for index, kv in enumerate(self.past_key_values):k, v = kvk = k[:end_index, :, :, :]v = v[:end_index, :, :, :]kv = (k, v)past_key_values_keeps.append(kv)self.past_key_values = past_key_values_keepsself.prob_history = self.prob_history[:, :end_index, :]

注意past_key_values不同的模型的维度不一样,llama k, v  (batch, num_head, seq_len, hidden_dim);ChatGLM k, v (seq, batch, head, hidden_dim)而Bloom  k (batch * head, hidden_dim, seq); v (batch * head, seq, hidden_dim),在实现的时候需要注意。

比较重要的代码笔记都介绍完毕了,没有写文件的依赖和一些不重要的逻辑,放一下我这边的一个python文件speculate_decoding.py的全部代码——实现了不同的采样方式、gama动态输入、是否debug等:

import torch
from torch.nn import functional as F
import timetorch.set_printoptions(precision=10)# copy from https://github.com/LeeSinLiang/microGPT/blob/ed40cf9780dbeb180adfe94c227d4aa97e69250e/gpt.py
def top_k_top_p_filter(logits: torch.Tensor, top_k: int = 0, top_p: float = 0.0):"""Args:logits (torch.Tensorpe_): 2D tensor with shape (batch, vocab)top_k (int, optional): top_k. Defaults to 0.top_p (float, optional): top_p. Defaults to 0.0.Returns:torch.Tensor: a renormalized logits"""if top_k > 0:filter = torch.topk(logits, min(top_k, logits.size(-1)))[0]logits[logits < filter[:, [-1]]] = float('-inf')if top_p > 0.0:sorted_logits, sorted_indices = torch.sort(logits, descending=True)cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)filter = cumulative_probs > top_pfilter[..., 1:] = filter[..., :-1].clone()filter[..., 0] = 0indices_to_remove = filter.scatter(1, sorted_indices, filter)logits[indices_to_remove] = float('-inf')return logitsdef norm_logits(logits: torch.Tensor, temperature: float, top_k: float, top_p: float) -> torch.Tensor:"""Args:logits (torch.Tensor): shape (1, vocab)temperature (float): temperaturetop_k (float): top_ktop_p (float): top_pReturns:torch.Tensor: next token with shape as (batch,  1)"""assert logits.dim() == 2logits = logits / temperaturelogits = top_k_top_p_filter(logits, top_k=top_k, top_p=top_p)probs = F.softmax(logits, dim=1)return probsdef sample(probs: torch.Tensor, num_samples: int = 1):idx_next = torch.multinomial(probs, num_samples=num_samples)if (idx_next.item() == 0):raise RuntimeErrorreturn idx_nextdef norm_logits_whole(logits: torch.Tensor, temperature: float, top_k: float, top_p: float) -> torch.Tensor:# 优化一版提速  矩阵计算代替原来的for循环assert logits.dim() == 3"""Args:logits (torch.Tensor): shape (1, vocab)temperature (float): temperaturetop_k (float): top_ktop_p (float): top_pReturns:torch.Tensor: next token with shape as (batch,  1)"""def top(scores: torch.FloatTensor, top_k: int = 0, top_p: float = 0.0):sorted_logits, sorted_indices = torch.sort(scores, descending=False)cumulative_probs = sorted_logits.softmax(dim=-1)cumulative_probs = cumulative_probs.cumsum(dim=-1)# Remove tokens with cumulative top_p above the threshold (token with 0 are kept)sorted_indices_to_remove = cumulative_probs <= (1 - top_p)# Keep at least min_tokens_to_keepsorted_indices_to_remove[..., -1:] = 0# indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)# 这个函数比较难理解、三维矩阵的scatter——就是把sorted_indices_to_remove 用 sorted_indices指定的index来还原# print('sorted_indices',sorted_indices)# print("sorted_indices_to_remove", sorted_indices_to_remove)indices_to_remove = sorted_indices_to_remove.scatter(2, sorted_indices, sorted_indices_to_remove)# print("indices_to_remove", indices_to_remove)scores = scores.masked_fill(indices_to_remove, -float("Inf"))return scoresif top_k > 0:filter = torch.topk(logits, min(2, logits.size(-1)))[0]logits[logits < filter[..., [-1]]] = float('-inf')logits = logits / temperaturelogits = top(logits, top_k, top_p)probs = F.softmax(logits, dim=2)return probsdef max_fn(x):"""norm(max (x, 0))"""x_max = torch.where(x > 0, x, torch.zeros_like(x))x_max_sum = torch.sum(x_max, dim=1, keepdim=True)return x_max / x_max_sumclass KVCachedGenration(object):def __init__(self, model, eos_token, temperature: float = 1.0, top_k: int = 0, top_p: float = 0, do_sample=False):self.model = modelself.eos_token = eos_tokenself.past_key_values = Noneself.prob_history = Noneself.do_sample = do_sampleself.temperature = temperatureself.top_k = top_kself.top_p = top_pdef get_position_ids(self, input_ids, device):batch_size, seq_length = input_ids.shapeposition_ids = torch.arange(seq_length, dtype=torch.long, device=device).unsqueeze(0).repeat(batch_size, 1)return position_ids@torch.no_grad()def generate(self, input_ids, gamma, debug=False):eos_index = Nonex = input_idsfor index, _ in enumerate(range(gamma)):if self.past_key_values:kv_cached_len = self.past_key_values[0][0].shape[0]position_ids = self.get_position_ids(x, x.device)position_ids = position_ids[..., kv_cached_len:]# ChatGLM一定要输入位置编码,一般推理的时候模型内部会自己配置位置编码的t1 = time.time()outputs = self.model(x[:, kv_cached_len:], position_ids=position_ids,past_key_values=self.past_key_values, use_cache=True)t2 = time.time()if debug:print(f"post inference time {round((t2 - t1) * 1000, 4)} ms, data shape {x[:, kv_cached_len:].shape}")logits = outputs.logitst1 = time.time()for i in range(logits.shape[1]):logits[:, i, :] = norm_logits(logits[:, i, :], self.temperature, self.top_k, self.top_p)t2 = time.time()if debug:print(f"norm_logits time {round((t2 - t1) * 1000, 4)} ms, data shape {x[:, kv_cached_len:].shape}")probs = logits[:, -1, :]self.past_key_values = outputs.past_key_valuesself.prob_history = torch.cat([self.prob_history, logits], dim=1)else:t1 = time.time()outputs = self.model(x)t2 = time.time()if debug:print(f"first inference time {round((t2 - t1) * 1000, 4)} ms, data shape {x.shape}")self.prob_history = outputs.logitst1 = time.time()for i in range(self.prob_history.shape[1]):self.prob_history[:, i, :] = norm_logits(self.prob_history[:, i, :], self.temperature, self.top_k,self.top_p)t2 = time.time()if debug:print(f"norm_logits time {round((t2 - t1) * 1000, 4)} ms, data shape {self.prob_history.shape}")self.past_key_values = outputs.past_key_valuesprobs = self.prob_history[:, -1, :]if self.do_sample:next_tok = sample(probs)else:next_tok = torch.argmax(probs, dim=-1).unsqueeze(0)x = torch.cat((x, next_tok), dim=1)if next_tok == self.eos_token and eos_index is None:eos_index = indexreturn x, eos_index@torch.no_grad()def generate_promotion(self, input_ids, gamma, debug=False):eos_index = Nonex = input_idsfor index, _ in enumerate(range(gamma)):if self.past_key_values:kv_cached_len = self.past_key_values[0][0].shape[0]position_ids = self.get_position_ids(x, x.device)position_ids = position_ids[..., kv_cached_len:]# ChatGLM一定要输入位置编码,一般推理的时候模型内部会自己配置位置编码的t1 = time.time()outputs = self.model(x[:, kv_cached_len:], position_ids=position_ids,past_key_values=self.past_key_values, use_cache=True)t2 = time.time()if debug:print(f"post inference time {round((t2 - t1) * 1000, 4)} ms, data shape {x[:, kv_cached_len:].shape}")logits = outputs.logits# 优化一版提速t1 = time.time()logits = norm_logits_whole(logits, self.temperature, self.top_k, self.top_p)t2 = time.time()if debug:print(f"norm_logits_whole time {round((t2 - t1) * 1000, 4)} ms, data shape {x[:, kv_cached_len:].shape}")probs = logits[:, -1, :]self.past_key_values = outputs.past_key_valuesself.prob_history = torch.cat([self.prob_history, logits], dim=1)else:t1 = time.time()outputs = self.model(x)t2 = time.time()if debug:print(f"first inference time {round((t2 - t1) * 1000, 4)} ms, data shape {x.shape}")self.prob_history = outputs.logits# 优化一版提速t1 = time.time()self.prob_history = norm_logits_whole(self.prob_history, self.temperature, self.top_k, self.top_p)t2 = time.time()if debug:print(f"norm_logits_whole time {round((t2 - t1) * 1000, 4)} ms, data shape {self.prob_history.shape}")self.past_key_values = outputs.past_key_valuesprobs = self.prob_history[:, -1, :]if self.do_sample:next_tok = sample(probs)else:next_tok = torch.argmax(probs, dim=-1).unsqueeze(0)x = torch.cat((x, next_tok), dim=1)if next_tok == self.eos_token and eos_index is None:eos_index = indexreturn x, eos_index@torch.no_grad()def kv_rollback(self, end_index):# kv cache 回滚到有效生成位置# ChatGLM k, v (seq, batch, head, hidden_dim)# for i in range(len(self.past_key_values)):#     self.past_key_values[i][0] = self.past_key_values[i][0][:, :, :end_index, :]#     self.past_key_values[i][1] = self.past_key_values[i][1][:, :, :end_index, :]# self.prob_history = self.prob_history[:, :end_index, :]# kv cache 回滚到有效生成位置# ChatGLM k, v (seq, batch, head, hidden_dim)past_key_values_keeps = []for index, kv in enumerate(self.past_key_values):k, v = kvk = k[:end_index, :, :, :]v = v[:end_index, :, :, :]kv = (k, v)past_key_values_keeps.append(kv)self.past_key_values = past_key_values_keepsself.prob_history = self.prob_history[:, :end_index, :]class SpeculateDecoding(object):def __init__(self, approx_model, target_model, tokenizer, temperature=1.0, top_k=0, top_p=0.85, max_len=100,random_seed=None):self.approx_model = approx_modelself.target_model = target_modelself.tokenizer = tokenizerself.device = approx_model.deviceself.random_seed = random_seedself.temperature = temperatureself.top_k = top_kself.top_p = top_pself.max_len = max_lendef speculative_decode_generate(self, prompt, gamma, do_sample=False, debug=False, promotion=True):eos_token = self.tokenizer.eos_token_idt1 = time.time()approx_model_kv_cached_genration = KVCachedGenration(self.approx_model, eos_token, self.temperature, self.top_k,self.top_p, do_sample)target_model_kv_cached_genration = KVCachedGenration(self.target_model, eos_token, self.temperature, self.top_k,self.top_p, do_sample)t2 = time.time()if debug:print(f'cached_genration init {round((t2 - t1) * 1000, 4)} ms')t1 = time.time()inputs = self.tokenizer([prompt], return_tensors="pt", padding=True)t2 = time.time()if debug:print(f'token2ids {round((t2 - t1) * 1000, 4)} ms')t1 = time.time()prefix = inputs['input_ids'].to(self.device)t2 = time.time()if debug:print(f'tensor to gpu {round((t2 - t1) * 1000, 4)} ms')seq_len = prefix.shape[1]reject_count = 0T = seq_len + self.max_lenend = Tassert prefix.shape[0] == 1, "input batch size must be 1"eos_token_index = Nonestart_t = time.time()with torch.no_grad():while prefix.shape[1] < T:prefix_len = prefix.shape[1]if promotion:x, index = approx_model_kv_cached_genration.generate_promotion(prefix, gamma, debug)else:x, index = approx_model_kv_cached_genration.generate(prefix, gamma, debug)if debug:print("小模型生成的token:", [self.tokenizer.decode(x[:, prefix_len:].tolist()[0])], "index: ",x[:, prefix_len:].tolist()[0], "eos index:", index)if index is not None:eos_token_index = prefix_len + indexif promotion:t_x, _ = target_model_kv_cached_genration.generate_promotion(x, 1, debug)else:t_x, _ = target_model_kv_cached_genration.generate(x, 1, debug)n = prefix_len + gamma - 1for i in range(gamma):if self.random_seed:torch.manual_seed(self.random_seed)r = torch.rand(1, device=self.device)else:r = torch.as_tensor(1.0, dtype=torch.float, device=self.device)j = x[:, prefix_len + i]# 拒接 以1-p/q 的概率拒绝 生成if r > (target_model_kv_cached_genration.prob_history[:, prefix_len + i - 1,j] / approx_model_kv_cached_genration.prob_history[:, prefix_len + i - 1, j]):n = prefix_len + i - 1reject_count += 1if debug:print("被拒绝的token:", [self.tokenizer.decode(j)], "index: ", j, "被拒绝的位置:", i)print("target_model",target_model_kv_cached_genration.prob_history[:, prefix_len + i - 1, j])print("approx_model",approx_model_kv_cached_genration.prob_history[:, prefix_len + i - 1, j])breakapprox_model_kv_cached_genration.kv_rollback(n + 1)prefix = x[:, :n + 1]if debug:print("eos_token_index:", eos_token_index)print("n:", n)# # 如果小模型生成的eos不被接受,则继续生成# if eos_token_index and eos_token_index > n:#     eos_token_index = None## # 如果小模型生成的eos被接受,则停止# if eos_token_index and eos_token_index <= n:#     breakif eos_token_index is not None:# 如果小模型生成的eos不被接受,则继续生成if eos_token_index > n:eos_token_index = Noneelse:# 如果小模型生成的eos被接受,则停止break# 如果拒绝一些小模型生成的token,从大模型中采样if n < prefix_len + gamma - 1:if do_sample:t = sample(max_fn(target_model_kv_cached_genration.prob_history[:, n,:] - approx_model_kv_cached_genration.prob_history[:, n, :]))else:# t = torch.argmax(max_fn(target_model_kv_cached_genration.prob_history[:, n,#                         :] - approx_model_kv_cached_genration.prob_history[:, n, :]),#                  dim=-1).unsqueeze(0)# 直接取大模型logits最大的那个tokent = torch.argmax(target_model_kv_cached_genration.prob_history[:, n, :], dim=-1).unsqueeze(0)if debug:print("拒绝后大模型重新生成的token:", [self.tokenizer.decode(t.tolist()[0])], " index: ", t.tolist()[0])target_model_kv_cached_genration.kv_rollback(n + 1)else:if do_sample:t = sample(target_model_kv_cached_genration.prob_history[:, -1, :])else:t = torch.argmax(target_model_kv_cached_genration.prob_history[:, -1, :], dim=-1).unsqueeze(0)target_model_kv_cached_genration.kv_rollback(n + 2)if debug:print("接受后大模型生成的next token:", [self.tokenizer.decode(t.tolist()[0])], " index: ", t.tolist()[0])prefix = torch.cat((prefix, t), dim=1)if t == eos_token:eos_token_index = prefix.shape[1] - 1breakend_time = time.time()if debug:print(f"while loop time {round((end_time - start_t) * 1000, 4)} ms")if eos_token_index:end = eos_token_index + 1t1 = time.time()input_ids = inputs['input_ids'].tolist()[0]assert len(input_ids) <= end, "eos_token选择位置错误"output_token_ids = prefix.tolist()[0][len(input_ids):end]response = self.tokenizer.decode(output_token_ids)response = self.target_model.process_response(response)t2 = time.time()if debug:print(f"post process {round((t2 - t1) * 1000, 4)} ms")return response, reject_count

三、效果实测

这一节我们开始效果实测,看看具体到实际业务落地效率和准确率指标,gama参数(小模型一次生成多少个token)和logits归一化优化的影响等。

测试环境Linux系统、显卡4090显卡、基于ChatGLM6B进行了业务微调得到大模型、花大力气蒸馏了6B得到1.5B小模型。

业务测试数据1088条,样例如下:

从坐席和客户的对话中提取出空调品牌、空调样式、是否5匹、故障类型、姓名、服务时间、联系方式和地址等等几个重要的信息。注意由于我们业务场景需要准确性,不需要模型发散输出多样性,我们解码的时候采用greedy search 解码策略,也就是生成token的时候采用torch.argmax(logits)来获取下一个token。

1、效果对比

tiny模型和big模型耗时以及准确率如下:

小模型
general.chat.tiny: 100%|████████████████████| 1088/1088 [09:03<00:00,  2.00it/s]
tiny cost total time is: 543.863 S, each time is: 499.87412222168024 ms
total 8704
correct 8539
items all acc 0.9810431985294118
{'姓名': 0.9898897058823529, '服务时间': 0.9476102941176471, '联系方式': 0.9908088235294118, '地址': 0.9448529411764706, '空调品牌': 0.9963235294117647, '空调样式': 0.9926470588235294, '是否5匹': 0.9972426470588235, '故障类型': 0.9889705882352942}
glm tiny acc: 0.859375
大模型
general.chat.common: 100%|█████████| 1088/1088 [31:12<00:00,  0.58it/s]
common cost total time is: 1871.7942 S, each time is: 1720.3990845264123 ms
total 8704
correct 8564
items all acc 0.9839154411764706
{'姓名': 0.9880514705882353, '服务时间': 0.9595588235294118, '联系方式': 0.9871323529411765, '地址': 0.9604779411764706, '空调品牌': 0.9926470588235294, '空调样式': 0.9954044117647058, '是否5匹': 0.9954044117647058, '故障类型': 0.9926470588235294}
glm common acc: 0.8759191176470589

可以看到我们蒸馏后的小模型效果也很好,和大模型差距不大,只有1.65个百分点的差距,耗时小模型每次推理500ms左右,大模型每次1720ms,小模型的速度大概是大模型的3.5倍。

看speculate sampling算法

logits归一化未优化  gama==7

r = troch.rand(1)  大模型argmax生成下一个token  以1-p(x)/q(x)的概率拒绝小模型的token  gama==7
general.chat.speculatedecode: 100%|█████████| 1088/1088 [15:14<00:00,  1.19it/s]
speculate cost total time is: 914.1663 S, each time is: 840.2263719369383 ms
total 8704
correct 8576
items all acc 0.9852941176470589
{'姓名': 0.9880514705882353, '服务时间': 0.9604779411764706, '联系方式': 0.9898897058823529, '地址': 0.9641544117647058, '空调品牌': 0.9944852941176471, '空调样式': 0.9944852941176471, '是否5匹': 0.9972426470588235, '故障类型': 0.9935661764705882}
glm speculate acc: 0.8869485294117647

logits归一化优化   gama==7

r = troch.rand(1)  大模型argmax生成下一个token  以1-p(x)/q(x)的概率拒绝小模型的token  gama==7 优化版本
general.chat.speculatedecode: 100%|█████████| 1088/1088 [12:56<00:00,  1.40it/s]
speculate promotion cost total time is: 776.8863 S, each time is: 714.0498757362366 ms
total 8704
correct 8577
items all acc 0.9854090073529411
{'姓名': 0.9880514705882353, '服务时间': 0.9613970588235294, '联系方式': 0.9898897058823529, '地址': 0.9641544117647058, '空调品牌': 0.9944852941176471, '空调样式': 0.9944852941176471, '是否5匹': 0.9972426470588235, '故障类型': 0.9935661764705882}
glm speculate promotion acc: 0.8878676470588235

logits归一化优化   gama==14

r = troch.rand(1) 大模型argmax生成下一个token 以1-p(x)/q(x)的概率拒绝小模型的token gama==14 优化版本
general.chat.speculatedecode: 100%|█████████| 1088/1088 [11:41<00:00, 1.55it/s]
speculate promotion cost total time is: 700.5773 S, each time is: 643.9129872357144 ms
total 8704
correct 8574
items all acc 0.9850643382352942
{'姓名': 0.9880514705882353, '服务时间': 0.9604779411764706, '联系方式': 0.9889705882352942, '地址': 0.9632352941176471, '空调品牌': 0.9944852941176471, '空调样式': 0.9954044117647058, '是否5匹': 0.9972426470588235, '故障类型': 0.9926470588235294}
glm speculate promotion gama 14 acc: 0.8851102941176471

speculate sampling算法确实有效,相对大模型而言确实能提升推理速度并完美保持大模型的生成质量。基线效果是提速1.05倍、优化logits归一化后提速1.41倍、gama=14时提速1.67倍,同时算法在测试集上的准确率均超过单一大模型准确率,有偶然性,这个和测试数据以及大模型和小模型的效果强相关,当然不仅仅是准确率,推理速度也是和上述因素强相关的。

另一方面,gama增加的同时只要小模型效果还行,那被拒绝的次数不太多的情况下,一次推理过程,大模型的forward次数就减少了,那么提速是理所当然的了。

2、解码日志展示

最后我们看一下解码过程中debug的日志,同一个输入

gama=7  logits归一化未优化时

while loop time 795.3556 ms

可以看到大模型forward耗时是小模型forward耗时的3-5倍之间;norm_logits的时间在首次forward后,seq_len为352,耗时80ms左右,小模型和大模型都要执行一次;后面大模型forward每次过8个token,norm_logits为1.75ms左右。

gama=7  logits归一化优化后

while loop time 691.3598 ms

每次inference大小模型forward基本没有变化,变化的是norm_logits为优化后的norm_logits_whole,首次forward后,seq_len为352,耗时0.9ms左右,这块儿优化耗时巨大;后面大模型forward每次过8个token,norm_logits_whole为0.25ms左右也有不小的优化。总体上来看也有100ms左右的优化。

gama=14  logits归一化优化后

while loop time 542.4328 ms

当gama=14和gama=7对比大模型forward一次15个token为25ms左右,8个token也为25ms左右,耗时差不多,这个和GPU的矩阵并行计算有关;但是整个解码算法过程中大模型forward次数为4,gama=7时大模型forward次数为8,而小模型耗时确实基本不变,因此整体上优化耗时为4*25ms=100ms左右,再加上其他的一些随机耗时差不多能到100-150ms左右。

以上是一个具体测试用例的分析,Speculative Decoding算法的收益和大小两个模型的效果差异有明显关系,相差不是太大,才会有加速的效果;同时小模型的效果又不能比大模型差太小,不然直接使用小模型就好了。相对而言应用场景比较受限,资源消耗也是需要两次训练资源,推理的时候需要大小模型占用的显存。就本次效果测试来说,ChatGLM6B大模型推理占用显存12.8G;小模型4.5G;Speculative Decoding大小模型联合则占用17.5G左右。当然这个Speculative Decoding算法很优秀,实现比较简单,确实比较取巧的实现了大模型推理的加速效果,同时生成质量完美保留。

最后的最后,对推理速度和效果都卡的比较死的业务场景对这个算法有一定的需求。

参考文章

大模型推理妙招—投机采样(Speculative Decoding)

Fast Inference from Transformers via Speculative Decoding

Accelerating Large Language Model Decoding with Speculative Sampling

LLMSpeculativeSampling代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/222149.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

EM的理论基础

1 EM定义​ 电迁移(Electro-Migration)是指在外加电场下,电子和金属原子之间的动量转移导致材料的运动。这种动量传递导致金属原子(比如Cu原子)从其原始位置移位,如图7-1。这种效应随着导线中电流密度的增加而增加,并且在更高的温度下,动量传递变得更加严重。因此,在先…

[WMCTF2020]Make PHP Great Again require_once 特性

php源码分析 require_once 绕过不能重复包含文件的限制-安全客 - 安全资讯平台 这里是特性 我们首先来解释一下 <?php highlight_file(__FILE__); require_once flag.php; if(isset($_GET[file])) {require_once $_GET[file]; }这个是我们的源代码 PHP包含的格式是将 已…

考验的是技术!如何绕过微软设计的安装Windows 11的硬件要求

这篇文章解释了绕过微软设计的硬件要求的三种方法,允许你在几乎任何电脑上安装Windows 11。 注意:绕过Windows 11要求所涉及的一些过程需要更深入的技术知识。请不要编辑计算机的注册表,除非你对此乐此不疲,因为它可能会损坏你的设备。 绕过Windows 11要求 虽然绕过Wind…

AUTO.js连接电脑时,握手失败

使用模拟器上的autox.js连接vscode。ipv4正确&#xff0c;但总是握手失败。 检查了一下vscode安装的插件&#xff0c;最开始安装的是这个&#xff1a; 将之前安装的插件禁用。 更换这个插件&#xff1a; 然后启动服务后就可以连接成功了。

Nginx服务器配置SSL证书

本文将全面介绍如何在Nginx或Tengine服务器配置SSL证书&#xff0c;具体包括下载和上传证书文件&#xff0c;在Nginx上配置证书文件、证书链和证书密钥等参数&#xff0c;以及安装证书后结果的验证。成功配置SSL证书后&#xff0c;您将能够通过HTTPS加密通道安全访问Nginx服务器…

成分党品牌

现在越来越多的成分党品牌崛起&#xff0c;主打成分护肤&#xff0c;不添加任何成分&#xff0c;其中有Timeless、The Ordinary、John Jeff、欧邦琪&#xff08;Obagi&#xff09;、修丽可&#xff08;SkinCeuticals&#xff09;、HFP 。 参考文章&#xff1a;成分党都会良心推…

试以单链表为存储结构实现简单选择排序的算法

简单选择排序&#xff0c;就是每趟把剩余元素最小或者最大的选出来排到前面 这道题值得推敲的是&#xff0c;p作为一个链表结点也是可以作为for循环的初始条件和判断条件的&#xff0c;至于查找到最小值之后&#xff0c;可以把两者的数值进行一个交换&#xff0c;就不用删结点…

怎么抠图换背景?这三个方法让你轻松抠图

怎么抠图换背景&#xff1f;抠图是每个独立站商家每天必不可少的工作&#xff0c;简单一张图用PS进行抠图还好&#xff0c;但如何多张图&#xff0c;用PS就效率非常低&#xff0c;且需要专业的PS技能才能上手实现精准抠图的目的&#xff0c;那么怎么快速抠图换背景呢&#xff0…

risc-v system instruction

ECALL ecall 指令以前叫做 scall&#xff0c;用于执行环境的变更&#xff0c;它会根据当前所处模式触发不同的执行环境切换异常, 用来执行需要更高权限才能执行的功能; 简单来说&#xff0c;ecall 指令将权限提升到内核模式并将程序跳转到指定的地址。操作系统内核和应用程序…

检测车牌的SIFT特征并匹配

# 代码5-14 检测车牌的SIFT特征并匹配 import cv2img1 cv2.imread(../data/plate.jpg) img2 cv2.imread(../data/car.jpg)sift cv2.SIFT_create() # 利用sift.detectAndCompute()函数找到特征点&#xff0c;计算描述符&#xff1b; kp1, des1 sift.detectAndCompute(img1, …

web279(s2-001)

目前java小白一个&#xff0c;主要是学学别人的思路 进入题目&#xff0c;登录框一个 抓包也没发现什么东西 网上说是struts2框架 Struts2是用Java语言编写的一个基于MVC设计模式的Web应用框架 判断是不是基于struts2的一些方法&#xff1a; 1.通过页面回显的错误消息来判断…

6.5.编解码器信息的收集

那在上节课中呢&#xff1f;我向你介绍了add track相关的内容&#xff0c;那今天呢&#xff1f;我们来看看编解码器信息的收集。那在这里呢&#xff0c;我们需要问几个重要的问题&#xff0c;那首先呢&#xff0c;就是我们上节课通过&#xff0c;可以让web rtc知道我们都要传输…

孩子都能学会的FPGA:第三十一课——用FPGA实现SPI主机发送数据

&#xff08;原创声明&#xff1a;该文是作者的原创&#xff0c;面向对象是FPGA入门者&#xff0c;后续会有进阶的高级教程。宗旨是让每个想做FPGA的人轻松入门&#xff0c;作者不光让大家知其然&#xff0c;还要让大家知其所以然&#xff01;每个工程作者都搭建了全自动化的仿…

安装spaCy及语言包下载安装

文章目录 1. spaCy的安装1.1 安装spaCy包方式1 : 通过pip / conda命令安装方式2 : 通过离线导入 1.2 安装语言模型方式1 : 通过pip / conda命令安装方式2 : 通过离线导入 2. 常见问题a. 版本问题 3. 参考文档 关注公众号&#xff1a;『AI学习星球』 回复&#xff1a;遥感图像语…

使用podman管理容器

本章主要介绍使用 podman 管理容器。 了解什么是容器&#xff0c;容器和镜像的关系安装和配置podman拉取和删除镜像给镜像打标签导出和导入镜像创建和删除镜像数据卷的使用管理容器的命令使用普通用户管理容器 对于初学者来说&#xff0c;不太容易理解什么是容器&#xff0c;…

论文阅读《Parameterized Cost Volume for Stereo Matching》

论文地址&#xff1a;https://openaccess.thecvf.com/content/ICCV2023/papers/Zeng_Parameterized_Cost_Volume_for_Stereo_Matching_ICCV_2023_paper.pdf 源码地址&#xff1a;https://github.com/jiaxiZeng/Parameterized-Cost-Volume-for-Stereo-Matching 概述 现有的立体匹…

【C语言】操作符详解(四):结构成员访问操作符

结构成员访问操作符 结构体 ⭐C语言已经提供了内置类型&#xff0c;如: char、short、int、long、float、double等&#xff0c;但是只有这些内置类型还是不够的&#xff0c;假设我想描述学生&#xff0c;描述一本书&#xff0c;这时单一的内置类型是不行的。描述一个学生需要名…

MATLAB 最小二乘空间直线拟合 (37)

MATLAB 最小二乘空间直线拟合 (37) 一、算法介绍二、算法实现1.代码一、算法介绍 对于空间中的这样一组点:大致呈直线分布,散乱分布在直线左右, 我们可采用最小二乘方法拟合直线,使用下面的代码可以得到图中的结果。(其中图片中的点解释和具体的实现代码如下所示) C++…

云计算:Vmware 安装 FusionCompute

目录 一、理论 1.FusionCompute 二、实验 1.Vmware 安装 FusionCompute&#xff08;CNA&#xff09; 2.Vmware 安装 FusionCompute&#xff08;VRM&#xff09; 三、问题 1. VRM-WEB登录失败 2.Windows cmd中无法ping通虚拟机 一、理论 1.FusionCompute &#xff08;…

孩子还是有一颗网安梦——Bandit通关教程:Level 13 → Level 14

&#x1f575;️‍♂️ 专栏《解密游戏-Bandit》 &#x1f310; 游戏官网&#xff1a; Bandit游戏 &#x1f3ae; 游戏简介&#xff1a; Bandit游戏专为网络安全初学者设计&#xff0c;通过一系列级别挑战玩家&#xff0c;从Level0开始&#xff0c;逐步学习基础命令行和安全概念…