写点什么

解析 vLLM 架构及源码系列:模型执行过程

作者:Jason黄
解析 vLLM 架构及源码系列:模型执行过程

上一章讲到 vLLM 调度器的逻辑,本章开始介绍如何对请求进行执行。


所有的逻辑要从 gpu_model_runner.py 的 execute_model 开始,按照 transformer 的结构,应该会进行以下的步骤:


  1. embedding

  2. 注意力计算

  3. Add & Norm

  4. MLP

  5. 整层 Norm


下面我们结合代码进行分析:


根据 gpu_model_runner.py 中的代码逻辑,首先进行的是 execute_model 的三个主要的环节:


  1. 词嵌入: 词嵌入

  2. 调用模型进行 token 生成

  3. 对模型输出进行采样


接下来 self.model 会调用到 qwen2.py 中,通过以下 5 个关键的模型实现进行推理,这 5 个模型实现,每一个 model.py 都会有:


第1层:XXXForCausalLM     🎯 总指挥官 (负责:调度所有工作)第2层:XXXModel          🏗️ 建筑工人 (负责:组装所有层)第3层:XXXDecoderLayer   🔄 流水线工人 (负责:重复执行相同工作)第4层:XXXAttention + MLP 👁️🧠 专业工人 (负责:注意力计算 + 数学运算)第5层:XXX基础算子        ⚙️ 工具箱 (负责:基础运算)
复制代码


例如:




实际从代码上来说,调用链就像上图所绘制的那样:


Qwen2ForCausalLM     |     |     VQwen2Model     |     |     VQwen2DecoderLayer * N ----> Qwen2Attention----->RMSNorm----->Qwen2MLP     |     |     V  Norm
复制代码


这里有一点会引起源码阅读的困难,就是调用链看起来都是把类名当做方法名在调用,例如:


def forward(    self,    input_ids: torch.Tensor,    positions: torch.Tensor,    intermediate_tensors: Optional[IntermediateTensors] = None,    inputs_embeds: Optional[torch.Tensor] = None,) -> Union[torch.Tensor, IntermediateTensors]:    # self.model实际上就是Qwen2Model,是一个class    hidden_states = self.model(input_ids, positions, intermediate_tensors,                               inputs_embeds)    return hidden_states
复制代码


为什么 class 可以当做方法调用呢,这是因为 Qwen2Model 实现了 nn.Module 基类,这个基类是 torch 中提供的,用于在做个 module 之间做嵌套,允许将其他 Module 实例作为属性赋值给当前模块,形成树状结构。例如,一个模型可以包含多个层(如卷积层、池化层等),这些层本身也是 Module 的子类。


实际来说他的效果就是,当“调用” 到 class 的时候实际是调用 class 对应的 forward 方法。

Embedding 实现

embedding 这里的实现,个人觉得是比较奇怪的,从上面的图中可以看到,在 gpu_model_runner 和 Qwen2Model 中都有对 get_input_embeddings 的调用:


# gpu_model_runner.py# 看起来这里仅处理多模态场景下,将vision embeddings和text embeddings统一放到一个embeddings中if self.supports_mm_inputs and get_pp_group().is_first_rank:    # NOTE(woosuk): To unify token ids and soft tokens (vision    # embeddings), we always use embeddings (rather than token ids)    # as input to the multimodal model, even when the input is text.    inputs_embeds_scheduled = self.model.get_input_embeddings(        input_ids=self.input_ids[:num_scheduled_tokens],        multimodal_embeddings=mm_embeds or None,    )
复制代码


# Qwen2Model# 事实上Qwen2Model中也会执行get_input_embeddings方法,将Input_ids中的text特征转换成词嵌入向量if get_pp_group().is_first_rank:    if inputs_embeds is not None:        hidden_states = inputs_embeds    else:        hidden_states = self.get_input_embeddings(input_ids)    residual = Noneelse:    assert intermediate_tensors is not None    hidden_states = intermediate_tensors["hidden_states"]    residual = intermediate_tensors["residual"]
复制代码


从代码看的出,实际来说并不需要在每一个 model 中都实现词嵌入的处理,只需要在 gpu_model_runner 中统一处理就可以了,因为实际调用的都是 Qwen2Model 提供的嵌入方法,只是调用位置分散到了两个代码逻辑中。

Attn 实现

注意力实现的代码如下:


attention 计算代码:


# vllm/model_executor/models/qwen2.pydef forward(    self,    positions: torch.Tensor,    hidden_states: torch.Tensor,) -> torch.Tensor:        qkv, _ = self.qkv_proj(hidden_states)    q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)    q, k = self.rotary_emb(positions, q, k)    attn_output = self.attn(q, k, v)    output, _ = self.o_proj(attn_output)    return output
复制代码


以上代码看起来就比较熟悉了:


qkv_proj: 将qkv一起做一次投影映射,然后再通过split方法得到通过输入序列计算的q、k、vself.attn: 计算注意力分数o_proj: 输出投影,通过将注意力矩阵与o矩阵相乘得到输出矩阵
复制代码


其中 attention 的计算是通过 flash_attn 实现的。


vllm/v1/attention/backends/flash_attn.pyFlashAttentionImpl.forward()
vllm/vllm_flash_attn/flash_attn_interface.pyflash_attn_varlen_func()
复制代码


值得注意的是,attention 部分的代码是没办法调试的,下面这个错误本身跟调试无关,但是其中体现的调用链可以看出 qwen2.py 的那部分代码已经被 torch 代码接管了。


错误代码是我在 attn_output = self.attn(q, k, v) 中尝试加入一个 print 的调试代码,结果发现这部分代码会被 torch 编译的时候拒绝。


这个错误的核心是 torch._dynamo,它是 PyTorch 2.0 引入的一个新特性,用于对模型进行即时编译(JIT Compilation)来提升性能。当 PyTorch Dynamo 尝试分析和优化您的代码时,它会追踪每个操作。


您看到的错误 Failed to trace builtin operator 就是说 Dynamo 无法理解或处理 Python 内置的 print 函数。因为 print 并不是一个与 PyTorch 计算图相关的操作,它只是一个输出信息的函数。在编译和优化的过程中,Dynamo 无法有效地“追踪”或优化它,因此报错并停止了编译。


(EngineCore_0 pid=3514727)   File "/data/huangjch/vllm-main/vllm/v1/worker/gpu_worker.py", line 244, in determine_available_memory(EngineCore_0 pid=3514727)     self.model_runner.profile_run()(EngineCore_0 pid=3514727)   File "/data/huangjch/vllm-main/vllm/v1/worker/gpu_model_runner.py", line 2529, in profile_run(EngineCore_0 pid=3514727)     = self._dummy_run(self.max_num_tokens, is_profile=True)(EngineCore_0 pid=3514727)   File "/opt/miniconda3/envs/jason/lib/python3.10/site-packages/torch/utils/_contextlib.py", line 116, in decorate_context(EngineCore_0 pid=3514727)     return func(*args, **kwargs)(EngineCore_0 pid=3514727)   File "/data/huangjch/vllm-main/vllm/v1/worker/gpu_model_runner.py", line 2308, in _dummy_run(EngineCore_0 pid=3514727)     outputs = self.model((EngineCore_0 pid=3514727)   File "/opt/miniconda3/envs/jason/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1751, in _wrapped_call_impl(EngineCore_0 pid=3514727)     return self._call_impl(*args, **kwargs)(EngineCore_0 pid=3514727)   File "/opt/miniconda3/envs/jason/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1762, in _call_impl(EngineCore_0 pid=3514727)     return forward_call(*args, **kwargs)(EngineCore_0 pid=3514727)   File "/data/huangjch/vllm-main/vllm/model_executor/models/qwen2.py", line 496, in forward(EngineCore_0 pid=3514727)     hidden_states = self.model(input_ids, positions, intermediate_tensors,(EngineCore_0 pid=3514727)   File "/data/huangjch/vllm-main/vllm/compilation/decorators.py", line 272, in __call__(EngineCore_0 pid=3514727)     output = self.compiled_callable(*args, **kwargs)(EngineCore_0 pid=3514727)   File "/opt/miniconda3/envs/jason/lib/python3.10/site-packages/torch/_dynamo/eval_frame.py", line 659, in _fn(EngineCore_0 pid=3514727)     raise e.with_traceback(None) from None(EngineCore_0 pid=3514727) torch._dynamo.exc.Unsupported: Failed to trace builtin operator(EngineCore_0 pid=3514727)   Explanation: Dynamo does not know how to trace builtin operator `print` with argument types ['str'] (has_kwargs False)(EngineCore_0 pid=3514727)   Hint: Avoid calling builtin `print` with argument types ['str']. Consider using an equivalent alternative function/method to `print`.(EngineCore_0 pid=3514727)   Hint: If you are attempting to call a logging function (e.g. `print`), you can try adding it to `torch._dynamo.config.reorderable_logging_functions`.(EngineCore_0 pid=3514727)   Hint: Please report an issue to PyTorch.(EngineCore_0 pid=3514727) (EngineCore_0 pid=3514727)   Developer debug context: builtin print [<class 'torch._dynamo.variables.constant.ConstantVariable'>] False
复制代码

MLP 实现


class Qwen2MLP(nn.Module):
def __init__( self, hidden_size: int, intermediate_size: int, hidden_act: str, quant_config: Optional[QuantizationConfig] = None, prefix: str = "", ) -> None: super().__init__() self.gate_up_proj = MergedColumnParallelLinear( hidden_size, [intermediate_size] * 2, bias=False, quant_config=quant_config, prefix=f"{prefix}.gate_up_proj", ) self.down_proj = RowParallelLinear( intermediate_size, hidden_size, bias=False, quant_config=quant_config, prefix=f"{prefix}.down_proj", ) if hidden_act != "silu": raise ValueError(f"Unsupported activation: {hidden_act}. " "Only silu is supported for now.") self.act_fn = SiluAndMul()
def forward(self, x): # 向上投影,将hidden_size扩充到intermediate_size gate_up, _ = self.gate_up_proj(x) # 执行激活函数,这里使用的是SiluAndMul x = self.act_fn(gate_up) # 向下投影,将intermediate_size降维到hidden_size x, _ = self.down_proj(x) return x
复制代码


以上就是从调度的请求到执行的过程分析了,再往下看的话,后面会再分析下 flash_attn 的实现,这里会讲到如何进行那些注意力计算,如何优化性能。


系列文章:

解析 vLLM 架构及源码系列 - 整体架构

解析 vLLM 架构及源码系列 - API Server

解析 vLLM 架构及源码系列:KVCache初始化之V1版本分析

发布于: 刚刚阅读数: 4
用户头像

Jason黄

关注

还未添加个人签名 2018-10-18 加入

云原生、AI Infra领域从业者

评论

发布
暂无评论
解析 vLLM 架构及源码系列:模型执行过程_vLLM源码_Jason黄_InfoQ写作社区