上一章讲到 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
|
|
V
Qwen2Model
|
|
V
Qwen2DecoderLayer * 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 = None
else:
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.py
def 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、v
self.attn: 计算注意力分数
o_proj: 输出投影,通过将注意力矩阵与o矩阵相乘得到输出矩阵
复制代码
其中 attention 的计算是通过 flash_attn 实现的。
vllm/v1/attention/backends/flash_attn.py
FlashAttentionImpl.forward()
vllm/vllm_flash_attn/flash_attn_interface.py
flash_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版本分析
评论