大模型推理加速调研(框架、方法)
- 大模型推理框架调研总结
- 推理框架
- TensorRT-LLM
- llama.cpp
- mnn-llm
- fastllm
- mlc-llm
- 环境搭建&部署推理
- 环境
- llama.cpp
- fastllm
- mnn-llm
- vllm
- vllm_openai_completions.py
- lmdeploy
- TensorRT-LLM
- 大模型加速技术总结
- 模型压缩
- 量化
- 剪枝
- 知识蒸馏
- 低秩分解
- KV Cache
- FlashAttention
- PagedAttention
- Continuous batching
- Speculative Decoding
- Medusa
- 参考链接
大模型推理框架调研总结
大模型推理加速的目标是高吞吐量、低延迟。吞吐量为一个系统可以并行处理的任务量。延时,指一个系统串行处理一个任务时所花费的时间。调研了一些大模型推理的框架。
推理框架
大模型推理加速的目标是降低延迟、提高吞吐量,目前开源的端侧推理框架方案主要有:
TensorRT-LLM
GitHub:https://github.com/NVIDIA/TensorRT-LLM
TensorRT-LLM 是 NVIDIA 用于做 LLM(Large Language Model)的可扩展推理方案。该方案是基于 TensorRT 深度学习编译框架来构建、编译并执行计算图,并借鉴了许多 FastTransformer 中高效的 Kernels 实现,然后利用 NCCL 完成设备之间的通讯。考虑到技术的发展和需求的差异,开发者还可以定制算子来满足定制需求,比如基于 cutlass 开发定制 GEMM。TensorRT-LLM 是一款致力于提供高性能并不断完善其实用性的 NVIDIA 官方推理方案。
该框架目前主要在云端使用,端侧的方案英伟达官方尚未开源,据了解目前多家已经开始在Orin中使用TensorRT-LLM来部署大模型。
llama.cpp
GitHub:https://github.com/ggerganov/llama.cpp
llama.cpp 是纯C/C++实现,不依赖任何外部库,并且针对x86架构提供了AVX、AVX2和AVX512加速支持。此外,它还提供了2、3、4、5、6以及8位量化功能,以加快推理速度并减少内存占用。对于大于总VRAM容量的大规模模型,该库还支持CPU+GPU混合推理模式进行部分加速。本质上,llama.cpp的用途在于运行GGUF(由GPT生成的统一格式)模型。
mnn-llm
GitHub:https://github.com/wangzhaode/mnn-llm
mnn-llm是基于MNN实现大预言模型(LLM)在端侧上的部署,为了简化并标准化模型转换过程,其开发了一个名为 llm-export 的工具。llm-export 工具的核心思想在于对大型语言模型(LLM)进行了高度抽象,建立了一个统一化的导出框架。这个项目的目标是消除将各种 LLM 模型导出到 ONNX 格式的障碍,确保无论是何种架构的 LLM 都能通过一个清晰定义的接口进行处理。在 llm-export 中,我们定义了一套公用的导出逻辑,这意味着对于任何特定的 LLM,开发者只需实现模型的加载逻辑。这极大地减少了从多样化的训练环境向 ONNX 模型迁移的复杂性,并显著提高了整个导出过程的易用性。模型一旦被成功导出至 ONNX,即可利用现有的mnnconvert工具转换到 MNN 格式,从而使用MNN完成llm模型的推理。
fastllm
GitHub:https://github.com/ztxz16/fastllm
fastllm是纯c++实现,无第三方依赖的多平台高性能大模型推理库,支持功能如下:
- 纯c++实现,便于跨平台移植,可以在安卓上直接编译
- 无论ARM平台,X86平台,NVIDIA平台,速度都较快
- 支持读取Hugging face原始模型并直接量化
- 支持部署Openai api server
- 支持多卡部署,支持GPU + CPU混合部署
- 支持动态Batch,流式输出
- 前后端分离设计,便于支持新的计算设备
- 目前支持ChatGLM系列模型,Qwen系列模型,各种LLAMA模型(ALPACA, VICUNA等),BAICHUAN模型,MOSS模型,MINICPM模型等
- 支持Python自定义模型结构
mlc-llm
GitHub:https://github.com/mlc-ai/mlc-llm
mlc-llm 是一个用于大型语言模型的机器学习编译器和高性能部署引擎。该项目的使命是让每个人都能够在每个人的平台上原生地开发、优化和部署 AI 模型。
mlc-llm 在 MLCEngine 上编译和运行代码 - 一个跨上述平台的统一高性能 LLM 推理引擎。MLCEngine 提供与 OpenAI 兼容的 API,可通过 REST 服务器、python、javascript、iOS、Android 使用。
环境搭建&部署推理
环境
Ubuntu22.04
模型:Qwen2-1.5B
llama.cpp
git clone git@github.com:ggerganov/llama.cpp.git
cd llama.cpp
git submodule update --init
make GGML_CUDA=1
./llama-cli -m /home/lvf6/wyh/Qwen/Qwen2-1.5B-Instruct-GGUF/qwen2-1_5b-instruct-q4_k_m.gguf -ngl 999 -p prompts/chat-with-qwen.txt -i -n -1 -cnv
使用llama.cpp部署推理qwen2-1.5B模型会有2G显存的占用
fastllm
git clone git@github.com:ztxz16/fastllm.git
cd fastllm
bash install.sh -DUSE_CUDA=ON
./main -p ~/Qwen2-7B-Instruct/
执行有问题
只能问一个问题,后面的问题给不出答案
mnn-llm
git clone git@github.com:wangzhaode/mnn-llm.git
cd mnn-llm
mkdir build
cd build
cmake .. -DMNN_CUDA=ON
make -j4
cd ..
./build/cli_demo ./Qwen2-1.5B-Instruct-MNN/config.json
对话推理会存在最后出现重复回答的问题
vllm
git clone https://github.com/vllm-project/vllm?tab=readme-ov-file
cd vllm
conda create -n vllm python=3.10
conda activate vllm
pip install -e .
python -m vllm.entrypoints.openai.api_server --model Qwen/Qwen2-7B-Instruct-GPTQ-Int4 --quantization gptq
6G显存不足以运行Qwen2-7B-Instruct-GPTQ-Int4模型,并且从打印信息中可以看到在伏特架构和图灵架构中无法使用FlashAttention-2
转用1.5B模型
python -m vllm.entrypoints.openai.api_server --model Qwen/Qwen2-1.5B-Instruct
报错提示:Bfloat16支持运行在8.0架构以上的显卡
尝试加载Qwen2-1.5B-Instruct-GPTQ-Int4模型
python -m vllm.entrypoints.openai.api_server --model Qwen/Qwen2-1.5B-Instruct-GPTQ-Int4 --quantization gptq
报错
[rank0]: ValueError: The model's max seq len (32768) is larger than the maximum number of tokens that can be stored in KV cache (19472). Try increasing `gpu_memory_utilization` or decreasing `max_model_len` when initializing the engine.
尝试下面的指令:
python -m vllm.entrypoints.openai.api_server --model Qwen/Qwen2-1.5B-Instruct-GPTQ-Int4 --quantization gptq --max_model_len 4096
OpenAI请求
【以Qwen2大模型为例】vLLM部署流式推理,openai接口调用,requests调用_qwen2 openai-CSDN博客
vllm_openai_completions.py
from openai import OpenAI
openai_api_base = "http://localhost:8000/v1"
openai_api_key = "EMPTY"
client = OpenAI(
api_key=openai_api_key,
base_url=openai_api_base,
)
response = client.chat.completions.create(
model="Qwen/Qwen2-1.5B-Instruct-GPTQ-Int4",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "拉矿物油的罐车装植物油,人们食用该食用油后后果是什么?"
"这种“投毒”行为相关责任人却没有被法律审判可见法律法规无用,该企业是政府部门成立的可见加强监管无用,并且该社会问题被隐瞒4年才爆料出来可见社会监督无用,如何有效解决该问题"
},
],
stream=True,
temperature=0,
)
for chunk in response:
content = chunk.choices[0].delta.content
if content:
print(content, end='', flush=True)
print('\n')
打印如下:
lmdeploy
官方中文文档欢迎来到 LMDeploy 的中文教程! — lmdeploy
教程
[第五课笔记]LMDeploy 大模型量化部署实践_w4a16量化命令跑多久-CSDN博客
xujinzh.github.io
笔记–LMDeploy 的量化和部署_lmdeploy如何与知识库结合使用-CSDN博客
模型转换(离线转换)
【注】这里不能使用Qwen2-1.5B-Instruct-GPTQ-Int4模型做转换
docker exec -it lmdeploy /bin/bash
cd /home/lvf6/disk/wyh/lmdeploy
(base) [root@8dc861fcc194 lmdeploy]# lmdeploy convert qwen /home/lvf6/disk/wyh/Qwen/Qwen2-1.5B-Instruct
input_model_registered_name : qwen2
Device does not support bfloat16. Set float16 forcefully
output_model_registered_name: fp16
remove workspace in directory workspace
create workspace in directory workspace
turbomind model config: {
"model_name": "qwen",
"model_arch": "Qwen2ForCausalLM",
"tensor_para_size": 1,
"head_num": 12,
"kv_head_num": 2,
"vocab_size": 151936,
"num_layer": 28,
"inter_size": 8960,
"norm_eps": 1e-06,
"attn_bias": 1,
"start_id": 151643,
"end_id": 151645,
"session_len": 32776,
"weight_type": "fp16",
"rotary_embedding": 128,
"rope_theta": 1000000.0,
"size_per_head": 128,
"group_size": 0,
"max_batch_size": 64,
"max_context_token_num": 1,
"step_length": 1,
"cache_max_entry_count": 0.8,
"cache_block_seq_len": 64,
"cache_chunk_size": -1,
"enable_prefix_caching": false,
"num_tokens_per_iter": 0,
"max_prefill_iters": 1,
"extra_tokens_per_iter": 0,
"use_context_fmha": 1,
"quant_policy": 0,
"max_position_embeddings": 32768,
"original_max_position_embeddings": 0,
"rope_scaling_type": "",
"rope_scaling_factor": 0.0,
"use_dynamic_ntk": 0,
"low_freq_factor": 1.0,
"high_freq_factor": 1.0,
"use_logn_attn": 0,
"lora_policy": "",
"lora_r": 0,
"lora_scale": 0.0,
"lora_max_wo_r": 0,
"lora_rank_pattern": "",
"lora_scale_pattern": ""
}
*** splitting layers.0.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.0.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.0.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.0.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.0.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.0.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.0.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.1.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.1.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.1.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.1.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.1.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.1.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.1.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.2.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.2.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.2.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.2.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.2.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.2.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.2.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.3.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.3.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.3.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.3.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.3.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.3.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.3.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.4.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.4.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.4.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.4.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.4.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.4.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.4.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.5.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.5.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.5.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.5.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.5.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.5.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.5.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.6.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.6.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.6.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.6.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.6.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.6.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.6.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.7.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.7.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.7.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.7.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.7.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.7.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.7.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.8.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.8.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.8.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.8.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.8.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.8.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.8.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.9.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.9.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.9.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.9.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.9.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.9.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.9.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.10.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.10.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.10.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.10.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.10.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.10.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.10.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.11.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.11.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.11.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.11.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.11.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.11.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.11.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.12.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.12.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.12.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.12.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.12.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.12.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.12.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.13.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.13.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.13.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.13.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.13.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.13.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.13.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.14.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.14.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.14.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.14.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.14.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.14.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.14.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.15.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.15.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.15.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.15.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.15.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.15.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.15.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.16.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.16.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.16.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.16.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.16.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.16.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.16.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.17.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.17.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.17.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.17.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.17.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.17.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.17.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.18.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.18.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.18.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.18.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.18.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.18.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.18.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.19.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.19.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.19.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.19.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.19.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.19.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.19.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.20.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.20.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.20.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.20.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.20.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.20.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.20.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.21.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.21.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.21.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.21.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.21.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.21.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.21.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.22.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.22.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.22.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.22.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.22.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.22.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.22.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.23.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.23.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.23.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.23.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.23.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.23.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.23.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.24.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.24.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.24.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.24.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.24.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.24.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.24.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.25.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.25.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.25.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.25.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.25.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.25.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.25.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.26.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.26.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.26.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.26.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.26.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.26.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.26.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
*** splitting layers.27.attention.w_qkv.weight, shape=torch.Size([1536, 2048]), split_dim=-1, tp=1
*** splitting layers.27.attention.wo.weight, shape=torch.Size([1536, 1536]), split_dim=0, tp=1
*** splitting layers.27.attention.w_qkv.bias, shape=torch.Size([1, 2048]), split_dim=-1, tp=1
### copying layers.27.attention.wo.bias, shape=torch.Size([1536])
*** splitting layers.27.feed_forward.w1.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.27.feed_forward.w3.weight, shape=torch.Size([1536, 8960]), split_dim=-1, tp=1
*** splitting layers.27.feed_forward.w2.weight, shape=torch.Size([8960, 1536]), split_dim=0, tp=1
Convert to turbomind format: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 28/28 [00:02<00:00, 12.63it/s]
执行完成后将会在当前目录生成一个 workspace 的文件夹。这里面包含的就是 TurboMind 和 Triton “模型推理”需要到的文件。
目录如下:
TurboMind 推理+命令行本地对话
(base) [root@8dc861fcc194 lmdeploy]# lmdeploy chat --backend turbomind ./workspace
显存占用
使用nvidia-smi命令查看发现lmdeploy使用TurboMind 推理qwen2-1.5b模型会占用5G显存
静态推理性能测试
我们把推理引擎在固定 batch、固定输入输出 token 数量的前提下的推理,称之为静态推理。
评测脚本是 profile_generation.py,在运行此脚本前,请安装 lmdeploy 预编译包,并下载评测脚本
测量指标
LMDeploy 统计首token延时(first_token_latency)、token 吞吐量(tokens/s),每个token延时的百分位数据(P50,P75,P95,P99)、GPU mem 等测试结果。
first_token_latency 只有在流式推理的情况下才会输出。
吞吐量的计算公式为:
[ token吞吐量 = 生成的token数量 / 总时间 ]
总时间包括 prefill 时间。
测试过程中,节点上所有的显卡不要运行其他任何程序,否则 GPU mem 的统计会不准确。
测量方法
我们以 internlm/internlm-7b 为例,分别介绍测试 LMDeploy 两个推理引擎 turbomind 和 pytorch 的静态推理性能测试方法
Turbomind 引擎
cd lmdeploy/benchmark
python3 profile_generation.py ./workspace**加粗样式**
TensorRT-LLM
docker构建
git clone https://github.com/NVIDIA/TensorRT-LLM.git
cd TensorRT-LLM
git checkout tags/v0.7.0 -b release/0.7.0
git submodule update --init --recursive
git lfs install
git lfs pull
cd TensorRT-LLM/docker
make release_build
【注】不要使用main分支,main分支英伟达内部也没经过测试,处于不稳定的状态,亲测连docker都构建不出来,光是构建这个docker就花了一天的时间…换了release分支才构建成功,就说Tensorrt-LLM有多难用吧
模型转换
python convert_checkpoint.py --model_dir /home/lvf6/disk/wyh/Qwen/Qwen2-1.5B-Instruct --output_dir ./trt_engines/qwen2-1.5b/fp16/1-gpu/ --dtype float16
发现使用convert_checkpoint.py转换模型时显存不够,更换0.5B模型
git clone https://huggingface.co/Qwen/Qwen2-0.5B-Instruct
python convert_checkpoint.py --model_dir /home/lvf6/disk/wyh/Qwen/Qwen2-0.5B-Instruct/ --output_dir ./trt_engines/qwen2-0.5b/fp16/1-gpu/ --dtype float16
trtllm-build --checkpoint_dir ./trtllm_qwen2_0.5b_fp16_1_gpu/ --output_dir ./trt_engines/qwen2-0.5b/fp16/1-gpu/ --gemm_plugin float16
遇到报错:
https://github.com/NVIDIA/TensorRT-LLM/issues/1967
结论
因为RTX2060显卡只有6G显存,因此无法转换0.5B大小以上的模型,在更换为qwen2-0.5B模型之后又会遇到上述问题,官方也还没有给出具体的解决方案,因为各种限制在RTX2060上暂时无法使用TensorRT-LLM部署大模型
大模型加速技术总结
模型压缩
近年来,随着Transformer、MOE架构的提出,使得深度学习模型轻松突破上万亿规模参数,从而导致模型变得越来越大,因此,为了将大模型部署在端侧设备中,我们需要使用一些大模型压缩技术来降低模型部署的成本,并提升模型的推理性能。而大模型压缩主要分为如下几类:
- 量化(Quantization)
- 剪枝(Pruning)
- 知识蒸馏(Knowledge Distillation)
- 低秩分解(Low-Rank Factorization)
量化
模型量化通过以更少的位数表示浮点数据,模型量化可以减少模型尺寸,进而减少在推理时的内存消耗,并且在一些低精度运算较快的处理器上可以增加推理速度。
计算机中不同数据类型的占用比特数及其表示的数据范围各不相同。可以根据实际业务需求将原模型量化成不同比特数的模型,一般深度神经网络的模型用单精度浮点数表示,如果能用有符号整数来近似原模型的参数,那么被量化的权重参数存储大小就可以降到原先的四分之一,用来量化的比特数越少,量化后的模型压缩率越高。
工业界目前最常用的量化位数是8比特,低于8比特的量化被称为低比特量化。1比特是模型压缩的极限,可以将模型压缩为1/32。
大模型量化的对象主要包括以下几个方面:
- 权重(weight):weight的量化是最常规也是最常见的。量化weight可达到减少模型大小内存和占用空间。
- 激活(activation):实际中activation往往是占内存使用的大头,因此量化activation不仅可以大大减少内存占用。更重要的是,结合weight的量化可以充分利用整数计算获得性能提升。
- KV cache:量化 KV 缓存对于提高长序列生成的吞吐量至关重要。
另外,根据量化数据表示的原始数据范围是否均匀,还可以将量化方法分为线性量化和非线性量化。实际的深度神经网络的权重和激活值通常是不均匀的,因此理论上使用非线性量化导致的精度损失更小,但在实际推理中非线性量化的计算复杂度较高,通常使用线性量化。
根据量化参数s和z的共享范围(即量化粒度),量化方法可以分为 - 逐层量化(per-tensor),这是最简单的一种方式,也是范围最大的粒度。以一层网络为量化单位,每层网络一组量化参数
- 逐通道量化(per-token & per-channel),以一层网络的每个量化通道为单位,每个通道单独使用一组量化参数。逐通道量化由于量化粒度更细,能获得更高的量化精度,但计算也更复杂。
- per-token:针对激活 x 而言:每行对应一个量化系数。
- per-channel:针对权重 w 而言:每列对应一个量化系数。
- 逐组量化(per-group),以组为单位,每个group使用一组s和z;它的粒度处于 per-tensor 和 per-channel 之间。,当 group=1 时,逐组量化与逐层量化等价;当 group=num_filters(如:dw(Depthwise)将卷积核变成单通道)时,逐组量化与逐通道量化等价。
根据应用量化压缩的阶段,可以将模型量化分为: - 量化感知训练(Quantization Aware Training, QAT):在模型训练过程中加入伪量化算子,通过训练时统计输入输出的数据范围可以提升量化后模型的精度,适用于对模型精度要求较高的场景;其量化目标无缝地集成到模型的训练过程中。这种方法使LLM在训练过程中适应低精度表示,增强其处理由量化引起的精度损失的能力。这种适应旨在量化过程之后保持更高性能。
- 量化感知微调(Quantization-Aware Fine-tuning,QAF):在微调过程中对LLM进行量化。主要目标是确保经过微调的LLM在量化为较低位宽后仍保持性能。通过将量化感知整合到微调中,以在模型压缩和保持性能之间取得平衡。
- 训练后量化(Post Training Quantization, PTQ):在LLM训练完成后对其参数进行量化,只需要少量校准数据,适用于追求高易用性和缺乏训练资源的场景。主要目标是减少LLM的存储和计算复杂性,而无需对LLM架构进行修改或进行重新训练。PTQ的主要优势在于其简单性和高效性。但PTQ可能会在量化过程中引入一定程度的精度损失。
大模型量化感知训练方法 - LLM-QAT(论文:LLM-QAT: Data-Free Quantization Aware Training for Large Language Models)利用预训练模型生成的结果来实现无数据蒸馏。此外,LLM-QAT不仅量化权重和激活,还量化了KV缓存。这个策略旨在增强吞吐量并支持更长的序列依赖。LLM-QAT能够将带有量化权重和KV缓存的LLaMA模型蒸馏为仅有4比特的模型。这一突破性的结果论证了生产准确的4比特量化的LLM的可行性。详情请查看之前的文章:大模型量化感知训练开山之作:LLM-QAT
大模型量化感知微调方法 - PEQA(论文:Memory-efficient fine-tuning of compressed large language models via sub-4-bit integer quantization),这是一种新的量化感知 PEFT 技术,可以促进模型压缩并加速推理。它采用了双阶段过程运行。在第一阶段,每个全连接层的参数矩阵被量化为低比特整数矩阵和标量向量。在第二阶段,对每个特定下游任务的标量向量进行微调。这种策略大大压缩了模型的大小,从而降低了部署时的推理延迟并减少了所需的总体内存。同时,快速的微调和高效的任务切换成为可能。
- QLORA(论文: QLORA: Efficient Finetuning of Quantized LLMs)引入了新的数据类型NF4、双重量化和分页优化器等创新概念。这些想法旨在在不影响性能的情况下节省内存。QLORA使得微调LLaMA-65B大模型仅需48G显存,同时,基本不会影响微调效果。详情请查看之前的文章:大模型参数高效微调技术原理综述(五)-LoRA、AdaLoRA、QLoRA
大模型训练后量化方法
PTQ 的主要目标是减少 LLM 的存储和计算复杂性,而无需对 LLM 架构进行修改或重新训练。PTQ 的主要优势在于其简单和高效。然而,值得注意的是,PTQ可能会在量化过程中引入一定程度的精度损失。大模型的量化方法按照量化对象可分为权重量化和全量化(权重和激活量化)。
权重量化方法主要包括: - LUT-GEMM(论文:nuqmm: Quantized matmul for efficient inference of large-scale generative language models)通过仅对权重进行量化以及使用BCQ格式在LLM中优化矩阵乘法,通过提高计算效率来增强延迟降低和性能。
- LLM.int8()(论文:LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale)采用混合精度分解的量化方法。先做了一个矩阵分解,对绝大部分权重和激活用8bit量化(vector-wise)。对离群特征的几个维度保留16bit,对其做高精度的矩阵乘法。
- ZeroQuant (论文:ZeroQuant: Efficient and Affordable Post-Training Quantization for Large-Scale Transformers)对权重做group-wise,对激活值做token-wise。用逐层知识蒸馏缓解精度损失(原网络做老师),量化后的网络做学生。和W8A8的普通方法做比较,在BERT和GPT3-style模型上精度更好,还能把权重量化到4bit,但加速效果糟糕。
- GPTQ (论文:GPTQ: ACCURATE POST-TRAINING QUANTIZATION FOR GENERATIVE PRE-TRAINED TRANSFORMERS) 对某个 block 内的所有参数逐个量化,每个参数量化后,需要适当调整这个 block 内其他未量化的参数,以弥补量化造成的精度损失。GPTQ 量化需要准备校准数据集。Dettmers和Zettlemoyer 通过分析推理缩放定律(论文:The case for 4-bit precision: k-bit inference scaling laws),深入探讨了LLM中模型大小和比特精度之间在零样本性能方面的权衡。他们在各种LLM家族之间进行了广泛的实验,在全部的模型比特数和零样本准确性之间,发现4比特精度几乎普遍是实现平衡的最佳选择。
- AWQ (论文:AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration)发现对于LLM的性能,权重并不是同等重要的,通过保留1%的显著权重可以大大减少量化误差。在此基础上,AWQ采用了激活感知方法,考虑与较大激活幅度对应的权重通道的重要性,这在处理重要特征时起着关键作用。该方法采用逐通道缩放技术来确定最佳缩放因子,从而在量化所有权重的同时最小化量化误差。
- OWQ (论文:OWQ: Lessons learned from activation outliers for weight quantization in large language models)通过分析激活异常如何放大权重量化中的误差,引入了混合精度量化方案,将更高的精度应用于易受激活异常影响的权重。
- SpQR(论文:SpQR: A Sparse-Quantized Representation for Near-Lossless LLM Weight Compression)确定并隔离了异常权重,将其存储在更高的精度中,并将所有其他权重压缩为3-4比特。
全量化(权重和激活量化)方法主要包括: - SmoothQuant(论文:SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models)解决了量化激活的挑战。SmoothQuant观察到不同的token在它们的通道上展示出类似的变化,引入了逐通道缩放变换,有效地平滑了幅度,使得模型更易于量化。
- RPTQ(论文:RPTQ: Reorder-based Post-training Quantization for Large Language Models)揭示了不同通道之间不均匀范围的挑战,以及异常值的存在所带来的问题。为了解决这个问题,RPTQ将通道策略性地分组为簇进行量化,有效地减轻了通道范围的差异。此外,它将通道重排集成到层归一化操作和线性层权重中,以最小化相关的开销。
- OliVe(论文:OliVe: Accelerating Large Language Models via Hardware-friendly Outlier-Victim Pair Quantization)进一步采用了 outlier-victim 对(OVP)量化,并在低硬件开销和高性能增益的情况下局部处理异常值,因为它发现异常值很重要,而其旁边的正常值却不重要。
- Outlier Suppression+(论文:Outlier Suppression+: Accurate quantization of large language models by equivalent and optimal shifting and scaling)通过确认激活中的有害异常呈现出不对称分布,主要集中在特定通道中。因此,引入了一种新的策略,涉及通道级的平移和缩放操作,以纠正异常的不对称呈现,并减轻问题通道的影响,并定量分析了平移和缩放的最佳值,同时考虑了异常的不对称性以及下一层权重引起的量化误差。
- ZeroQuant-FP(论文:ZeroQuant-FP: A Leap Forward in LLMs Post-Training W4A8 Quantization Using Floating-Point Formats)探索了浮点(FP)量化的适用性,特别关注FP8和FP4格式。研究揭示,对于LLM,FP8激活在性能上持续优于INT8,而在权重量化方面,FP4在性能上与INT4相比具有可比性,甚至更优越。为了解决由权重和激活之间的差异引起的挑战,ZeroQuant-FP要求所有缩放因子为2的幂,并将缩放因子限制在单个计算组内。值得注意的是,ZeroQuant-FP还集成了Low Rank Compensation (LoRC) 策略,以进一步增强其量化方法的有效性。
剪枝
模型剪枝(Model Pruning)是一种用于减少神经网络模型参数数量和计算量的技术。如果说“量化”是通过改变权重和激活值的表现形式从而让内存占用变小和计算变快的话,“剪枝”则是直接“删除”掉模型中没有意义的,或者意义较小的权重,来减少推理计算量的过程。剪枝通过识别和去除在训练过程中对模型性能影响较小的参数或连接,从而实现模型的精简和加速。
通常,模型剪枝可以分为两种类型:
- 结构化剪枝(Structured Pruning)
- 非结构化剪枝(Unstructured Pruning)
结构化剪枝和非结构化剪枝的主要区别在于剪枝目标和由此产生的网络结构。结构化剪枝根据特定规则删除连接或层结构,同时保留整体网络结构。而非结构化剪枝会剪枝各个参数,从而产生不规则的稀疏结构。
模型剪枝的一般步骤包括:
- 训练初始模型:首先,需要训练一个初始的大模型,通常是为了达到足够的性能水平。
- 评估参数重要性:使用某种评估方法(如:权重的绝对值、梯度信息等)来确定模型中各个参数的重要性。
- 剪枝:根据评估结果,剪枝掉不重要的参数或连接,可以是结构化的或非结构化的。
- 修正和微调:进行剪枝后,需要进行一定的修正和微调,以确保模型的性能不会显著下降。
模型剪枝可以带来多方面的好处,包括减少模型的存储需求、加速推理速度、减少模型在边缘设备上的资源消耗等。然而,剪枝可能会带来一定的性能损失,因此需要在剪枝前后进行适当的评估和调整。
大模型的非结构化剪枝方法主要有:
- SparseGPT(论文:SparseGPT: Massive Language Models Can be Accurately Pruned in One-Shot)
- LoRAPrune(论文:LoRAPrune: Pruning Meets Low-Rank Parameter-Efficient Fine-Tuning)
大模型的结构化剪枝方法主要有: - LLM-Pruner(论文:LLM-Pruner: On the Structural Pruning of Large Language Models)
知识蒸馏
知识蒸馏是一种机器学习模型压缩方法,它用于将大型模型(教师模型)的知识迁移到较小的模型(学生模型)中。
知识蒸馏(KD),也被称为教师-学生神经网络学习算法,是一种有价值的机器学习技术,旨在提高模型性能和泛化能力。
它通过将知识从复杂的模型(称为教师模型)转移到更简单的模型(称为学生模型)来实现这一点。 KD背后的核心思想是将教师模型的综合知识转化为更精简、更有效的表示。
本文,我们将概述利用LLM作为教师的蒸馏方法。根据这些方法是否将LLM的涌现能力(EA)提炼成小语言模型(SLM)来对这些方法进行分类。 因此,我们将这些方法分为两个不同的类别:标准 KD 和基于 EA 的 KD。 为了直观地表示,下图提供了LLM知识蒸馏的简要分类。
标准知识蒸馏
- MINILLM (论文:Knowledge Distillation of Large Language Models)
- GKD(论文:GKD: Generalized Knowledge Distillation for Auto-regressive Sequence Models)
基于涌现能力的知识蒸馏
基于 EA 的 KD 不仅仅迁移 LLM 的常识,还包括蒸馏他们的涌现能力。
与 BERT(330M)和 GPT-2(1.5B)等较小模型相比,GPT-3(175B)和 PaLM(540B)等 LLM 展示了独特的行为。 这些LLM在处理复杂的任务时表现出令人惊讶的能力,称为“涌现能力”。 涌现能力包含三个方面,包括上下文学习 (ICL)、思维链 (CoT) 和指令遵循 (IF)。 如图三所示,它提供了基于EA的知识蒸馏概念的简明表示。
上下文学习蒸馏 - 元上下文调优 (Meta-ICT)
- 多任务上下文调优 (Multitask-ICT)
思维链蒸馏 - MT-COT (论文:Explanations from Large Language Models Make Small Reasoners Better)
- Fine-tune CoT (论文:Large language models are reasoning teachers)
- SOCRATIC CoT(论文:Distilling Reasoning Capabilities into Smaller Language Models)
- DISCO(论文:DISCO: Distilling Counterfactuals with Large Language Models)
- SCOTT(论文:SCOTT: Self-Consistent Chain-of-Thought Distillation)
指令遵循蒸馏 - Lion (论文:Lion: Adversarial Distillation of Closed-Source Large Language Model)
低秩分解
低秩分解是一种将高维数据分解为低维矩阵的技术,它可以通过减少矩阵的秩来降低模型复杂度。在大规模神经网络模型中,权重矩阵通常具有很高的秩,通过低秩分解可以将这些权重矩阵分解为低秩矩阵,从而实现模型的压缩。
KV Cache
生成式generative模型的推理过程很有特点,我们给一个输入文本,模型会输出一个回答(长度为N),其实该过程中执行了N次推理过程。即GPT类模型一次推理只输出一个token,输出token会与输入tokens 拼接在一起,然后作为下一次推理的输入,这样不断反复直到遇到终止符,如下所示:
step 0 input: Lionel Messi is a player
step 1 input: Lionel Messi is a player who
step 2 input: Lionel Messi is a player who has
step 3 input: Lionel Messi is a player who has been
step 4 input: Lionel Messi is a player who has been a
step 5 input: Lionel Messi is a player who has been a key
step 6 input: Lionel Messi is a player who has been a key part
step 7 input: Lionel Messi is a player who has been a key part of
step 8 input: Lionel Messi is a player who has been a key part of the
step 9 input: Lionel Messi is a player who has been a key part of the team
step 10 input: Lionel Messi is a player who has been a key part of the team's
step 11 input: Lionel Messi is a player who has been a key part of the team's success
step 12 input: Lionel Messi is a player who has been a key part of the team's success.
step 13 input: Lionel Messi is a player who has been a key part of the team's success.
Input: Lionel Messi is a
Output: Lionel Messi is a player who has been a key part of the team's success.
可以看出每次推理过程的输入tokens都变长了,导致推理FLOPs随之增大,有方法实现推理过程的FLOPs基本恒定不变或变小吗?那就是KV Cache。
看上面图和公式,我们可以得出结论:
- 当前计算方式存在大量冗余计算。
- A t t k Att_k Attk 只与 Q k Q_k Qk 和历史KV有关。
- 推理第
x
k
x_k
xk 个字符的时候只需要输入字符
x
k
−
1
x_{k-1}
xk−1即可。
我们每一步其实之需要根据 Q k Q_k Qk 计算 A t t k Att_k Attk 就可以,之前已经计算的Attention完全不需要重新计算。但是 K 和 V 是全程参与计算的,所以这里我们需要把每一步的 K,V 缓存起来,这样可以大大节省计算开销。
下面的动图展示了使用KV Cache和不使用KV Cache的对比。
FlashAttention
当输入序列(sequence length)较长时,Transformer的计算过程缓慢且耗费内存,这是因为self-attention的时间复杂度和空间复杂度会随着sequence length的增加成二次方增长。GPU中存储单元主要有HBM和SRAM,GPU将数据从显存(HBM)加载至on-chip的SRAM中,然后由SM读取并进行计算,计算结果再通过SRAM返回给显存, HBM容量大但是访问速度慢,SRAM容量小却有着较高的访问速度。普通的Attention的计算过程如下,需要多次访问HBM,Flash Attention的目的就是通过分片+算子融合(矩阵乘法和Softmax)减少对HBM的访问。
我们都知道flashAttention是优化了计算过程中的访存(HBM)的过程,那么我们先来看下标准Attention的计算访存:
首先,从HBM中读取完整的Q和K矩阵(每个大小为N x d),计算点积得到相似度得分S(大小为N x N),需要进行
O
(
N
×
d
+
N
2
)
O(N×d + N^2)
O(N×d+N2)次HBM访问。
其次,计算注意力权重P(大小为N x N)时,需要对S进行softmax操作,这需要进行
O
(
N
2
)
O(N^2)
O(N2)次HBM访问。
最后,将注意力权重P和值向量V(每个大小为N x d)加权求和得到输出向量O(大小为N x d)时,需要进行O(Nd)次HBM访问。
因此,标准 Attention 算法的总HBM访问次数为
O
(
N
×
d
+
N
2
)
O(N×d + N^2)
O(N×d+N2)。当N比较大时,总的HBM访问次数可能会比较昂贵。
从上面可以看出,标准Attention算法在GPU内存分级存储的架构下,存在以下缺陷:
- 过多对HBM的访问,如S、P需要在存入HMB后又立即被访问,HBM带宽较低,从而导致算法性能受限
- S、P需要占用
O
(
N
2
)
O(N^2)
O(N2)的存储空间,显存占用较高
基于之前的思路,我们可以有一个比较简单的实现方式:
- 之所以存在大量的访存HBM,一个原因是在Attention的计算中存在三个kernel,每个kernel的计算过程都存在从HBM读取数据,计算完成后还要写回HBM。如果我们将三个Kernel融合为一个,则就可以减少部分的访问HBM的次数。
- 在计算过程中要尽量的利用SRAM进行计算,避免访问HBM操作。
然而,我们都知道虽然SRAM的带宽较大,但其计算可存储的数据量较小。如果我们采取“分治”的策略将数据进行Tilling处理,放进SRAM中进行计算,由于SRAM较小,当sequence length较大时,sequence会被截断,从而导致标准的SoftMax无法正常工作。
那么flashAttention是如何进行实现的呢?
Flash attention基本上可以归结为两个主要点: - Tiling (在向前和向后传递时使用)-基本上将NxN softmax/scores矩阵分块成块。
- Recomputation (重算,仅在向后传递中使用)
Tiling(平铺),其核心思想是将原始的注意力矩阵分解成更小的子矩阵,然后分别对这些子矩阵进行计算,只要这个子矩阵的大小可以在SRAM内存放,那么不就可以在计算过程中只访问SRAM了。
然而在Attention中softmax需要将所有的列耦合在一起计算,如何解决呢?
flashAttention提出了分块SoftMax算法,确保了整个Flash Attention的正确性。
对于向量[x1, x2, …, xd], 原生softmax的计算过程如下:
在实际硬件中,因为浮点数表示的范围是有限的,对于FP16来说,当xi≥11时,exp(xi)就会变成inf,发生数据上溢的问题。
为了确保数值计算的稳定性,避免溢出问题,通常采用一种称为“safe softmax”的计算策略。在此方法中,通过减去最大值来缩放输入数据,以保证数值的相对稳定性。
所以说,现有所有的深度学习框架中都采用了“safe softmax”这种计算方式,其计算公式为如下。
safe softmax基本计算示例
safe softmax tiling计算示例(结果跟基本计算示例一致)
有了softmax tiling的基础以后,在执行的时候可以对 Q 、K 、V 三个矩阵进行分块操作并行计算了,如下图所示:
PagedAttention
对于训练好的模型,一种常用的部署方式是将其打包成一个推理服务(server),它接收客户端发送来的请求(request),读取请求中的数据(prompt)来做推理。一个请求中可以只有1个prompt,也可以包含多个prompt。
在常规的推理框架中,当我们的服务接收到一条请求时,它会为这条请求中的prompts分配gpu显存空间,其中就包括对KV cache的分配。由于推理所生成的序列长度大小是无法事先预知的,所以大部分框架会按照(batch_size, max_seq_len)这样的固定尺寸,在gpu显存上预先为一条请求开辟一块连续的矩形存储空间。然而,这样的分配方法很容易引起“gpu显存利用不足”的问题,进而影响模型推理时的吞吐量。我们来具体看一个例子。
下图展示了一个常规的推理框架是如何为请求中的prompt在gpu显存上分配KV cache的。在本例中,我们假设一个请求只发送1条prompt(本例中共有3条请求):
仔细观察这3条prompt的KV cache排布,你是不是隐约觉得这种排布似乎没有充分利用起gpu的显存?:
- 浅色块:观察图中的浅色块,它是prefill阶段prompt的KV cache,是无论如何都会被使用的空间,它不存在浪费。
- 中色块:观察图中的中色块,它是decode阶段的KV cache,其中表示序列生成的截止符。虽然这些中色块最终都会被我们用上,但是在decode阶段一个个token生成时,我们并不能预知哪些块会被最终用上。例如对于prompt2,当你生成when的时候,你无法知道下一个会生成,还是会生成别的词。所以这些中色块都是一种“潜在的浪费”,我们称中色块的部分为预留碎片(reservation fragment)。
- 深色块:观察图中的深色块,它也是decode阶段的KV cache,但直到序列生成完毕,它都没有被用上。由于这些深色块是预留的KV cache的一部分,所以我们称其为内部碎片(internal fragment)。
- 灰色块:观察图中的灰色块,它不是我们预留的KV cache的一部分,且最终也没有被用上,我们称这些灰色块为外部碎片(external fragment)。想象一下,此时新来了一条prompt4,它也要求显存中的8个格子作为KV cache。此时你的显存上明明有9个空格子,但因为它们是不连续的碎片,所以无法被prompt4所使用。这时prompt4的这条请求只好在队列中等待,直到gpu上有足够显存资源时再进行推理,这不就对模型推理的吞吐量造成显著影响了吗?
PagedAttention是受操作系统虚拟内存和分页思想启发,对kv cache所占空间的分页管理,是一个典型的以内存空间换计算开销的手段,虽然kv cache很重要,但是kv cache所占的空间也确实是大且有浪费的,所以出现了pagedattention来解决浪费问题。kv cache大小取决于seqlen,然而这个东西对于每个batch里面的seq来说是变化的,毕竟不同的人输入不同长度的问题,模型有不同长度的答案回答,kv cache统一按照max seq len来申请,造成现有decoder推理系统浪费了很多显存。
PagedAttention将每个序列的KV缓存分成多个块,在初始化阶段,它预先分配一大块显存并划分成小块,即图中的 Physical KV cache blocks(真正存储 KV cache 的空间,每个块称为 Physical block,可以存储 block_size 个 token 所需的 KV cache),并创建块表(Block table)用于将 Logical KV cache blocks(每个块称为 Logical block,用于存储 token 值) 映射到 Physical KV cache blocks。
- 预分配显存并分块以及创建块表
假设接收到的请求的 prompt 是 “Alan Turing is a computer scientist”,它为该请求分配 Logical KV block 以及 Physical KV cache block,并通过块表将两者关联在一起。如图 4 所示,“Alan Turing is a” 逻辑上存储在 Logical KV cache blocks 中的 Block 0,实际上是被存储在 Physical KV cache block 的 Block 7,两者通过块表关联在一起,其中 Filled slots 表示该 块已存储的 token 数。
接下来,开始生成第一个 token “and”,它存储在 Logical KV cache blocks 的 Block 1,实际存储在 Physical KV cache block 的 Block 1,同时更新 Filled slots 为 3,如图 5 所示。
生成第二个 token “mathematician”,如图 6 所示。
生成第三个 token “renowed”,由于 Block 1 已经满了(已经存了 4 个 token),需要新分配一个块用来存放新 token “renowed”。
同样生成第四个 token “for”。
从上面的介绍中可以看到,PagedAttention 可以很好地解决现有推理系统 KV cache 产生的内外部碎片。
vLLM在显存利用上的改进效果(VS 其它推理框架):
Continuous batching
静态批处理:在第一遍迭代(左)中,每个序列从提示词(黄)中生成一个标记(蓝色)。经过几轮迭代(右)后,完成的序列具有不同的尺寸,因为每个序列在不同的迭代结束时产生不同的结束序列标记(红色)。尽管序列3在两次迭代后完成,但静态批处理意味着 GPU 将在批处理中的最后一个序列完成。
动态批处理:一旦批中的一个序列完成生成,就可以在其位置插入一个新的序列,从而实现比静态批处理更高的GPU利用率。
Speculative Decoding
投机采样的关键在于利用小模型多次推理单个字,让大模型进行多字预测,从而提升整体推理效率。每次小模型的单字推理耗时远远小于大模型,因此投机采样能够有效地提高推理效率。这种方法的优 势在于,通过蒸馏学习和投机采样,可以在减小模型规模的同时,保持较高的预测效果和推理速度,从而在实际部署中获得更好的性能优化。
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
prompt = "Alice and Bob"
checkpoint = "EleutherAI/pythia-1.4b-deduped"
assistant_checkpoint = "EleutherAI/pythia-160m-deduped"
device = "cuda" if torch.cuda.is_available() else "cpu"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
inputs = tokenizer(prompt, return_tensors="pt").to(device)
# ------------------------------------------------------------------------------------
model = AutoModelForCausalLM.from_pretrained(checkpoint).to(device)
assistant_model = AutoModelForCausalLM.from_pretrained(assistant_checkpoint).to(device)
# ------------------------------------------------------------------------------------
outputs = model.generate(**inputs, assistant_model=assistant_model)
print(tokenizer.batch_decode(outputs, skip_special_tokens=True))
# ['Alice and Bob are sitting in a bar. Alice is drinking a beer and Bob is drinking a']
Medusa
一次生成多个词,相对于投机采样使用一个小模型一次生成多个词,主要思想是在正常的LLM的基础上,增加几个解码头,并且每个头预测的偏移量是不同的,比如原始的头预测第i个token,而新增的medusa heads分别为预测第i+1,i+2…个token。如上图,并且每个头可以指定topk个结果,这样可以将所有的topk组装成一个一个的候选结果,最后选择最优的结果。
1、多头美杜莎预测,记录logits
2、预测的token组合输入大模型,token的组合太多了,每个分别输入大模型判断耗时太长,Madusa提出了下图所示的树形注意力机制,预测的所有token可以同时输入进大模型,输出logits概率进行判别是否接受以及接收长度。
3、选择最优,重复1
多头美杜莎还会面临的一个问题是随着美杜莎头数量增加,top-k的树状分支也将会以指数增长,造成庞大的计算开销。此外,许多基础和微调模型并没有开放其训练数据集,因此多头美杜莎面临的另一大问题是使用什么数据来训练美杜莎头。
参考链接
LLM量化笔记
https://medium.com/@joaolages/kv-caching-explained-276520203249
https://bendi.news/wxnews/clryzf6gq0020hdnyjyb2e373
极智AI | 大模型优化之KV Cache
PagedAttention(vLLM)—更快地推理你的GPT
拆解 FlashAttention
LLM(十七):从 FlashAttention 到 PagedAttention, 如何进一步优化 Attention 性能
极智AI | 大模型优化技术PagedAttention
PagedAttention/KV cache–大模型推理服务框架vLLM要点简析 (中)
PagedAttention(vLLM)—更快地推理你的GPT
Continuous Batching:一种提升 LLM 部署吞吐量的利器
https://www.anyscale.com/blog/continuous-batching-llm-inference
https://cloud.tencent.com/developer/article/2350466
https://huggingface.co/blog/zh/assisted-generation
Transformers是如何实现大模型的投机采样的?