前言
最近在尝试预训练大模型,想将我们的模型适配到VLLM框架上,所以也对VLLM框架的代码进行了阅读总结,下面给出一点经验.
将Transformers库中Modeling转换为VLLM模型
如VLLM官方文档所言,大部分Model的适配需要参考Transformer库中的modeling文件,大部分结构是类似的,但是需要额外注意以下几点:
- Transformers库会使用模型对应的Config类来反序列化JSON文件,但是VLLM会直接将JSON配置反序列化成一个字典,以固定的Key来获取配置值.所以如果你要支持的模型并不是以标准的Huggingface约定的风格来规定config.json,你可能要在如
get_head_size()
等函数中打上若干补丁. 👇🏻
def get_head_size(self) -> int:
# TODO remove hard code
if hasattr(self.hf_text_config, "model_type"
) and self.hf_text_config.model_type == 'deepseek_v2':
# FlashAttention supports only head_size 32, 64, 128, 256,
# we need to pad head_size 192 to 256
return 256
- VLLM只关注推理,因此在适配模型时只需要关注两件事情: Forward和Load Weights. 大部分Modeling函数中定义的Class是可以很好的复用的,但是你可能要修改一部分函数签名. 每个Model的组成部分都需要继承
nn.Module
并记得调用父类构造函数.一个经典的例子如下:
class MyModelForCausalLM(nn.Module):
def __init__(
self,
config,
cache_config: Optional[CacheConfig] = None,
quant_config: Optional[QuantizationConfig] = None,
):
super().__init__()
self.config = config
self.quant_config = quant_config
self.model = MyModel(config, cache_config, quant_config)
if self.config.tie_word_embeddings:
self.lm_head = self.model.decoder.embed_tokens
else:
self.lm_head = ParallelLMHead(config.vocab_size,
config.word_embed_proj_dim)
self.logits_processor = LogitsProcessor(config.vocab_size)
self.sampler = Sampler()
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
kv_caches: List[torch.Tensor],
attn_metadata: AttentionMetadata,
intermediate_tensors: Optional[IntermediateTensors] = None,
) -> torch.Tensor:
hidden_states = self.model(input_ids, positions, kv_caches,
attn_metadata)
return hidden_states
- 替换Transformer Blocks中的模块.VLLM始终是为多卡并行而设计的,而且基于算子融合的理念把大部分常用算子进行了整合.一些常用的替代方案是:
LMHead => ParallelLMHead
Embedding => VocabParallelEmbedding
RMSNorm => vllm.model_executor.layers.layernorm.RMSNorm
ROPE => get_rope()
对于激活函数,VLLM提供了Silu+MatrixMul和GELU+MatrixMul两种融合算子(用CUDA实现),另外也用CUDA写了单个的激活函数算子.如果你的模型用到了比较奇怪的自定义激活函数,那记得写好Forward函数之后加上@torch.jit.script
来优化下.
VLLM中的各种Linear层
在适配模型的时候,需要将Modelling代码中的Linear更换为各种可并行的Linear.VLLM提供了各种Linear,下面分析下他们的实现.
LinearBase和LinearMethod
LinearBase是一个nn.Module
,它通过quant_conf
参数来决定是否调用量化相关的矩阵乘算子.当不需要量化时,则调用 UnquantizedLinearMethod
来执行计算. LinearMethod类实际上是Torch Fx函数式的封装,UnquantizedLinearMethod会调用F.linear来进行矩阵乘计算.之所以使用函数式来进行实际计算,是因为当涉及到分布式并行时,Weights和Input Tensor通常是被切分的向量,自行维护Weights的加载和切分可能更有效率.
一个LinearMethod
类包含create_weights
(依据Linear层的Input/Output Size和Parameter Type等参数创建Weight向量并注册到相关的LinearBase Module中,并设置相关的Weights Loader.
ReplicatedLinear
朴实无华的线性层,每个分布式节点执行相同的操作,存在大量重复计算.
ColumnParallelLinear
如图所示,对于常见的 形如 Y=AX+b类型的矩阵乘法,ColumnParallelLinear将权重A按照第二维(如果LinearMethod涉及量化或者Lora合并,维度数目可能不同)进行切分,每个卡只执行一部分形如 $A_1X+b$的操作.但是注意,在执行完成之后,每个卡上的$Y_i$的Shape和内容均只有原Y的一部分,并不会自动Gather.这是考虑到TransformerBlock中的并行操作不需要完整的Tensor,如MLP层等.
实现 : 先根据PP或TP的WorldSize得出分片i的数目并将output_size切分为output_size_per_partition来设置Weights Tensor和输出. 同样因为切分了权重向量,因此重写Weights Loader,计算当前卡对应的Weights Tensor的子向量Offeset.
适用: 一般适用于MLP的前半个Linear.
MergedColumnParallelLinear
和上面提到的ColumnParallelLinear的计算流程是完全相同的,但是它用于将多个Linear融合在一起进行运算(比如MLP中Gate和Up两个Linear可以进行融合).因此它在Weights加载过程中不仅需要分加载每一行权重,更要把权重在Output这个维度进行concat,来保证算子融合的正确性.此外在Weights的加载过程中加入了对用于量化的PackedParameter的支持.
适用: 一般适用于MLP的Gate-Up 融合.
QKVParallelLinear
见名知意,是将三个Linear融合在一起,而且针对于QKV这三个Linear进行特化,输出是将qkv拼接在一起的Tensor.相比于MergedColumnParallelLinear的区别是shared的offset可以根据QKV HeadNum以及HeadSize来计算得出.