上一章讲到 vLLM 调度器的逻辑,本章开始介绍如何对请求进行执行。
所有的逻辑要从 gpu_model_runner.py 的 execute_model 开始,按照 transformer 的结构,应该会进行以下的步骤:
embedding
注意力计算
Add & Norm
MLP
整层 Norm
下面我们结合代码进行分析:
根据 gpu_model_runner.py 中的代码逻辑,首先进行的是 execute_model 的三个主要的环节:
词嵌入: 词嵌入
调用模型进行 token 生成
对模型输出进行采样
接下来 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版本分析
评论