基于vLLM加速大模型推理服务

3/22/2024 vLLMLLaMA-Factory推理性能优化

# 1. 推理服务性能优化

# 1.1 推理服务的优化方向

随着LLM的不断发展和应用,如何提高模型推理性能成为了一个重要的研究方向。推理性能受到显存带宽而不是计算能力的限制,服务吞吐量受到推理batch_size的限制。针对推理性能和服务吞吐量的优化,可以从三个角度开展:推理引擎层、服务层优化以及量化技术。

  • 推理引擎层主要是针对计算性能进行优化,例如KernelFusion、KV-Cache、FlashAttention、TP+PP、PagedAttention等技术;

  • 服务层优化主要关注吞吐量的提升,包括Dynamic-Batching、Continous-Batching等技术。

    此外,还有针对特定场景的优化技术,例如流式、交互式及持续生成,以及长序列推理等。

  • 模型量化方面主要涉及Weight-Only、int8、int4以及KV-Cache量化等。

对于以上这些技术基础理论的介绍,详见这篇文章:大模型推理-2-推理引擎和服务性能优化 (opens new window)

# 1.2 基于vLLM加速大模型推理

# 1.2.1 vLLM项目简介

vLLM是一个大型语言模型推理加速工具,它通过优化内存管理、连续批处理、CUDA核心优化和分布式推理支持等技术手段,显著提高了大型语言模型的推理速度和效率。在官方实验中,vLLM 的吞吐量比 HF 高出 24 倍,比 TGI 高出 3.5 倍。

注:vLLM官方目前还不支持AutoGPTQ的量化模型,也不支持ChatGLM-1模型,也不支持LoRA微调后的adapters。

vLLM吞吐量极高

# 1.2.2 vLLM基本原理

vLLM是LLM推理和服务引擎,支持多种模型,具有极高的性能,PagedAttention是vLLM背后的核心技术。

作者发现大模型推理的性能瓶颈主要来自于内存。一是自回归过程中缓存的K和V张量非常大,在LLaMA-13B中,单个序列输入进来需要占用1.7GB内存。二是内存占用是动态的,取决于输入序列的长度。由于碎片化和过度预留,现有的系统浪费了60%-80%的内存。

PagedAttention灵感来自于操作系统中虚拟内存和分页的经典思想,它可以允许在非连续空间立存储连续的KV张量。具体来说,PagedAttention把每个序列的KV缓存进行了分块,每个块包含固定长度的token,而在计算attention时可以高效地找到并获取那些块。

KV缓存分片的过程

每个固定长度的块可以看成虚拟内存中的页,token可以看成字节,序列可以看成进程。那么通过一个块表就可以将连续的逻辑块映射到非连续的物理块,而物理块可以根据新生成的token按需分配。

通过PagedAttention生成序列的过程

所以序列在分块之后,只有最后一个块可能会浪费内存(实际中浪费的内存低于4%)。高效利用内存的好处很明显:系统可以在一个batch中同时输入更多的序列,提升GPU的利用率,显著地提升吞吐量。

PagedAttention的另外一个好处是高效内存共享。例如,在并行采样的时候,一个prompt需要生成多个输出序列。这种情况下,对于这个prompt的计算和内存可以在输出序列之间共享。

并行采样的例子

通过块表可以自然地实现内存共享。类似进程之间共享物理页,在PagedAttention中的不同序列通过将逻辑块映射到一样的物理块上可以实现共享块。为了确保安全共享,PagedAttention跟踪物理块的引用计数,并实现了Copy-on-Write机制。内存共享减少了55%内存使用量,大大降低了采样算法的内存开销,同时提升了高达2.2倍的吞吐量。

同一个输入采样生成多个输出的例子

# 1.2.3 vLLM基本用法

vLLM只是用来对prompts进行批处理,但它并不能独立使用,需要搭配 Dynamic-Batching 去使用,这里可以自己去实现,也可以使用开源实现,比如:triton-inference-server、mosecorg/mosec、ShannonAI/service-streamer等。

vLLM基本用法

# 2. 需求分析及测试环境

# 2.1 需求与技术选型

# 2.1.1 需求场景

[1] 对并行处理做了优化,能够在较快响应速度的前提下支持较多的并发量。

[2] 支持目前主流的大模型(如ChatGLM、Baichuan、Qwen、LLaMA),支持流式输出。

[3] 在显存溢出时让它使用内存,这样只是推理变慢而不是爆显存导致挂掉。

# 2.1.2 技术选型

技术选型:调研了很多开源项目,发现还是LLaMA-Factory最合适。它是一个主要用于大模型微调的项目,里面自带推理服务,在2024.3.07的版本在推理服务里支持了vLLM技术,可用来部署高效的推理服务。

LLaMA-Factory支持vLLM

关于显存溢出时让它用内存,可参考:ZeRO-Inference: Democratizing massive model inference (opens new window) 文章。

ZeRO-Inference

vLLM的issues里有个人在自己的Fork仓库里进行了实现,还没合并到主分支,详见:https://github.com/vllm-project/vllm/issues/3563 (opens new window)

Offload_Model_Weights_to_CPU

另外,调研时尝试过的其他开源项目如下所示,仅供参考:

# 2.2 服务器测试环境

实验环境:实体GPU服务器,NVIDIA RTX 4090 / 24GB,CentOS 7.9,Anaconda3-2019.03,CUDA 12.4

如果没有GPU服务器,可以租用AutoDL等平台的。服务器的租用及基础环节的安装这里就不赘述了,详见我的另一篇博客:常用深度学习平台的使用指南 (opens new window)

# 3. 部署高效推理服务

# 3.1 使用LLaMA-Factory部署推理服务

# 3.1.1 拉取项目并安装依赖

拉取项目并安装依赖,并准备模型文件(这里以 chatglm3-6b (opens new window) 为例)

$ git clone https://github.com/hiyouga/LLaMA-Factory.git
$ conda create -n llama_factory python=3.10
$ conda activate llama_factory
$ cd LLaMA-Factory
$ pip3 install -r requirements.txt
$ pip3 install vllm==0.3.3
1
2
3
4
5
6

# 3.1.2 启动支持vLLM的推理服务

启动支持vLLM的推理服务,官方支持多种方式,这里仅以 API Demo 为例:

$ CUDA_VISIBLE_DEVICES=0 python3 src/api_demo.py \
    --model_name_or_path /root/llm_models/THUDM/chatglm3-6b/ \
    --template default \
    --infer_backend vllm \
    --vllm_gpu_util 0.9
1
2
3
4
5

LLaMA-Factory推理服务

注:vllm_gpu_util 参数用于控制显存占用比例,默认值为0.9,详见 /LLaMA-Factory/src/llmtuner/hparams/model_args.py

class ModelArguments:
    r"""
    Arguments pertaining to which model/config/tokenizer we are going to fine-tune or infer.
    """

    model_name_or_path: str = field(
        metadata={
            "help": "Path to the model weight or identifier from huggingface.co/models or modelscope.cn/models."
        },
    )
    adapter_name_or_path: Optional[str] = field(
        default=None,
        metadata={"help": "Path to the adapter weight or identifier from huggingface.co/models."},
    )
    cache_dir: Optional[str] = field(
        default=None,
        metadata={"help": "Where to store the pre-trained models downloaded from huggingface.co or modelscope.cn."},
    )
    use_fast_tokenizer: bool = field(
        default=True,
        metadata={"help": "Whether or not to use one of the fast tokenizer (backed by the tokenizers library)."},
    )
    resize_vocab: bool = field(
        default=False,
        metadata={"help": "Whether or not to resize the tokenizer vocab and the embedding layers."},
    )
    split_special_tokens: bool = field(
        default=False,
        metadata={"help": "Whether or not the special tokens should be split during the tokenization process."},
    )
    new_special_tokens: Optional[str] = field(
        default=None,
        metadata={"help": "Special tokens to be added into the tokenizer."},
    )
    model_revision: str = field(
        default="main",
        metadata={"help": "The specific model version to use (can be a branch name, tag name or commit id)."},
    )
    low_cpu_mem_usage: bool = field(
        default=True,
        metadata={"help": "Whether or not to use memory-efficient model loading."},
    )
    quantization_bit: Optional[int] = field(
        default=None,
        metadata={"help": "The number of bits to quantize the model using bitsandbytes."},
    )
    quantization_type: Literal["fp4", "nf4"] = field(
        default="nf4",
        metadata={"help": "Quantization data type to use in int4 training."},
    )
    double_quantization: bool = field(
        default=True,
        metadata={"help": "Whether or not to use double quantization in int4 training."},
    )
    quantization_device_map: Optional[Literal["auto"]] = field(
        default=None,
        metadata={"help": "Device map used to infer the 4-bit quantized model, needs bitsandbytes>=0.43.0."},
    )
    rope_scaling: Optional[Literal["linear", "dynamic"]] = field(
        default=None,
        metadata={"help": "Which scaling strategy should be adopted for the RoPE embeddings."},
    )
    flash_attn: Literal["off", "sdpa", "fa2", "auto"] = field(
        default="auto",
        metadata={"help": "Enable FlashAttention for faster training and inference."},
    )
    shift_attn: bool = field(
        default=False,
        metadata={"help": "Enable shift short attention (S^2-Attn) proposed by LongLoRA."},
    )
    mixture_of_depths: Optional[Literal["convert", "load"]] = field(
        default=None,
        metadata={"help": "Convert the model to mixture-of-depths (MoD) or load the MoD model."},
    )
    use_unsloth: bool = field(
        default=False,
        metadata={"help": "Whether or not to use unsloth's optimization for the LoRA training."},
    )
    visual_inputs: bool = field(
        default=False,
        metadata={"help": "Whethor or not to use multimodal LLM that accepts visual inputs."},
    )
    moe_aux_loss_coef: Optional[float] = field(
        default=None,
        metadata={"help": "Coefficient of the auxiliary router loss in mixture-of-experts model."},
    )
    disable_gradient_checkpointing: bool = field(
        default=False,
        metadata={"help": "Whether or not to disable gradient checkpointing."},
    )
    upcast_layernorm: bool = field(
        default=False,
        metadata={"help": "Whether or not to upcast the layernorm weights in fp32."},
    )
    upcast_lmhead_output: bool = field(
        default=False,
        metadata={"help": "Whether or not to upcast the output of lm_head in fp32."},
    )
    infer_backend: Literal["huggingface", "vllm"] = field(
        default="huggingface",
        metadata={"help": "Backend engine used at inference."},
    )
    vllm_maxlen: int = field(
        default=2048,
        metadata={"help": "Maximum input length of the vLLM engine."},
    )
    vllm_gpu_util: float = field(
        default=0.9,
        metadata={"help": "The fraction of GPU memory in (0,1) to be used for the vLLM engine."},
    )
    vllm_enforce_eager: bool = field(
        default=False,
        metadata={"help": "Whether or not to disable CUDA graph in the vLLM engine."},
    )
    offload_folder: str = field(
        default="offload",
        metadata={"help": "Path to offload model weights."},
    )
    use_cache: bool = field(
        default=True,
        metadata={"help": "Whether or not to use KV cache in generation."},
    )
    hf_hub_token: Optional[str] = field(
        default=None,
        metadata={"help": "Auth token to log in with Hugging Face Hub."},
    )
    ms_hub_token: Optional[str] = field(
        default=None,
        metadata={"help": "Auth token to log in with ModelScope Hub."},
    )
    export_dir: Optional[str] = field(
        default=None,
        metadata={"help": "Path to the directory to save the exported model."},
    )
    export_size: int = field(
        default=1,
        metadata={"help": "The file shard size (in GB) of the exported model."},
    )
    export_device: str = field(
        default="cpu",
        metadata={"help": "The device used in model export, use cuda to avoid addmm errors."},
    )
    export_quantization_bit: Optional[int] = field(
        default=None,
        metadata={"help": "The number of bits to quantize the exported model."},
    )
    export_quantization_dataset: Optional[str] = field(
        default=None,
        metadata={"help": "Path to the dataset or dataset name to use in quantizing the exported model."},
    )
    export_quantization_nsamples: int = field(
        default=128,
        metadata={"help": "The number of samples used for quantization."},
    )
    export_quantization_maxlen: int = field(
        default=1024,
        metadata={"help": "The maximum length of the model inputs used for quantization."},
    )
    export_legacy_format: bool = field(
        default=False,
        metadata={"help": "Whether or not to save the `.bin` files instead of `.safetensors`."},
    )
    export_hub_model_id: Optional[str] = field(
        default=None,
        metadata={"help": "The name of the repository if push the model to the Hugging Face hub."},
    )
    print_param_status: bool = field(
        default=False,
        metadata={"help": "For debugging purposes, print the status of the parameters in the model."},
    )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170

不同vllm_gpu_util参数设置的显存占用对比:

不同vllm_gpu_util参数设置的显存占用

# 3.2 对部署的推理服务进行测试

# 3.2.1 查看接口文档

使用Chrome浏览器打开此地址:http://<your_server_ip>:8000/docs,可以访问到接口文档。

LLaMA-Factory推理服务的接口文档

先请求一下 /v1/models 接口,发现模型名叫做 gpt-3.5-turbo。

LLaMA-Factory推理服务查看模型列表

然后在 /v1/chat/completions 接口里请求推理服务,注意把 model 和 content 改了。

LLaMA-Factory请求推理服务

# 3.2.2 测试流式输出

在接口文档那里,可以看到 curl 命令,把 stream 改成 true 即可变成流式输出。

$ curl -X 'POST' \
  'http://<your_server_ip>:8000/v1/chat/completions' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "model": "gpt-3.5-turbo",
  "messages": [
    {
      "role": "user",
      "content": "解释一下量子计算"
    }
  ],
  "tools": [],
  "do_sample": true,
  "temperature": 0,
  "top_p": 0,
  "n": 1,
  "max_tokens": 0,
  "stream": true
}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

LLaMA-Factory流式输出

# 3.2.3 进行压力测试

这里不使用专业的压力测试工具了,直接写个 Python 脚本进行压力测试。

# -*- coding: utf-8 -*-

import threading
import requests
import json


def send_post_request(url, payload):
    """
    向指定的URL发送POST请求。
    """
    headers = {
        "accept": "application/json",
        "Content-Type": "application/json"
    }

    updated_payload = {
        "model": "gpt-3.5-turbo",
        "messages": [
            {
                "role": "user",
                "content": payload["prompt"]
            }
        ],
        "tools": [
            {
                "type": "function",
                "function": {
                    "name": "string",
                    "description": "string",
                    "parameters": {}
                }
            }
        ],
        "temperature": 0,
        "top_p": 0,
        "n": 1,
        "max_tokens": 0,
        "stream": False
    }

    response = requests.post(url, headers=headers, data=json.dumps(updated_payload))
    try:
        response_json = response.json()
        print(response_json)
    except ValueError:
        print("Response could not be decoded as JSON:", response.text)


def threaded_requests(url, payload, num_threads, total_requests):
    """
    创建并启动多线程以达到指定的请求总量。
    """
    rounds = (total_requests + num_threads - 1) // num_threads  # 计算需要的轮数
    for _ in range(rounds):
        threads = []
        for _ in range(num_threads):
            if total_requests <= 0:
                break  # 如果已经达到请求总量,停止创建新线程
            thread = threading.Thread(target=send_post_request, args=(url, payload))
            thread.start()
            threads.append(thread)
            total_requests -= 1

        for thread in threads:
            thread.join()


if __name__ == '__main__':
    api_url = 'http://your_server_ip:8000/v1/chat/completions'
    payload = {
        "prompt": "解释一下量子计算"
    }
    num_threads = 50       # 线程数
    total_requests = 100   # 总请求数

    threaded_requests(api_url, payload, num_threads, total_requests)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

可以看到,哪怕只是在一张单卡4090显卡上,大模型的响应速度也是非常迅速的。

对LLaMA-Factory部署的服务进行压力测试

# 4. 参考资料

[1] vLLM是一个大型语言模型推理加速工具 from Github (opens new window)

[2] vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention from vLLM官方文档 (opens new window)

[3] vLLM:给大模型提提速,支持高并发吞吐量提高24倍,同时推理速度最少提高 8 倍 from CSDN (opens new window)

[4] vLLM Feature:Offload Model Weights to CPU from Github issues (opens new window)

[5] 如何让vLLM适配一个新模型 from 知乎 (opens new window)

[6] How continuous batching enables 23x throughput in LLM inference while reducing p50 latency from anyscale (opens new window)

[7] 如何解决LLM大语言模型的并发问题 from 知乎 (opens new window)

[8] 大模型的N种高效部署方法:以LLama2为例 from 美熙科技说 (opens new window)

[9] LightLLM:纯Python超轻量高性能LLM推理框架 from AI文摘 (opens new window)

[10] 大模型推理百倍加速之KV cache篇 from 知乎 (opens new window)

[11] 大模型推理-2-推理引擎和服务性能优化 from 知乎 (opens new window)

[12] 在 Triton 中部署 vLLM 模型 from Github (opens new window)

[13] VLLM推理加速与部署 from Github (opens new window)

[14] Triton Inference Server教程2 from CSDN (opens new window)

[15] 使用本地模型替代 OpenAI:多模型并发推理框架 from 知乎 (opens new window)

[16] 怎么在我们项目中使用vLLM推理 from Github issues (opens new window)

[17] LLaMA-Factory统一 100 多个 LLM 的高效微调 from Github (opens new window)

[18] ZeRO-Inference: Democratizing massive model inference from Deepspeed官方文档 (opens new window)

[19] 图解大模型计算加速系列:vLLM源码解析1,整体架构 from AINLP (opens new window)

[20] 量化模型能否用vllm部署 from Github issues (opens new window)

[21] Would it be possible to support LoRA fine-tuned models from Github issues (opens new window)

[22] Support LoRA adapter from Github issues (opens new window)

[23] 大模型部署综述 from 吃果冻不吐果冻皮 (opens new window)

[24] LLM后端推理引擎性能大比拼 from 吃果冻不吐果冻皮 (opens new window)

Last Updated: 6/14/2024, 4:28:35 PM