唠唠闲话

随着模型训练和推理技术的成熟,我们如何在资源有限的情况下更有效地利用这些技术?近年来,各种方案的出现极大地降低了科研门槛。从 vLLM 项目的内存优化到端侧小模型和代理运行超大模型,这些方案为模型推理提供了多样化的路径。

本文旨在探讨模型推理的技术方案,提供一个全面的入门指南,包括开源和闭源服务。

为什么用 API 接口?

自 2023 年 3 月推出 ChatGPT API 服务以来,几乎所有大模型服务和项目都支持 OpenAI 接口,这已然成为行业标准。使用 OpenAI 风格的 API 进行大模型推理有以下好处:

  • 通用性:一套代码可适配调用各类大模型。
  • 广泛支持:商用模型服务和开源项目均支持这一接口。
  • 灵活性:API 化的大模型可在不使用时关闭服务,有效减少 GPU 资源占用。
  • 易于配置:开源工具能轻松将任意模型服务转换为 OpenAI 接口规范的服务。

当然,这种方法也有局限,例如部分特性(如 system prompt 和 Function call)可能与某些模型不兼容。不过,诸如 XinferenceLangchain Tools 等项目可以弥补这些不足。

关联项目

教程从模型的性能效率调用方式两个角度切入,涉及项目:

  1. 性能效率

    • 量化推理,零 Python 代码使用开源模型:Ollama
    • 内存优化,提高吞吐量:vLLM
  2. 调用方式

OneAPI 项目

OneAPI 是一个 API 管理和分发系统,支持几乎所有主流 API 服务。OneAPI 通过简单的配置允许使用一个 API 密钥调用不同的服务,实现服务的高效管理和分发。

讯飞/智谱/千问/Gemini/Claude,其模型调用方式各不相同,但借助 OneAPI 能统一转化为 OpenAI 格式。

官方提供了一键部署的 docker-compose 方案。部署完成后,访问 http://localhost:3000/ 并使用初始账号(用户名为 root,密码为 123456)进行登录,登录后根据提示修改密码。

20240323230852

类似的开源项目还有 AI GateWay 或者 LiteLLM等,各有优劣,但个人强推 OneAPI。

支持模型

下表列举了几个 OneAPI 支持的 API 服务,完整的模型列表参见 OneAPI 官方仓库。

所属公司 模型 文档地址 备注
面壁智能 智谱 glm 智谱 API 文档 新用户 1B Token 免费额度,1个月有效期
阿里云 通义千问 Qwen 千问 API 文档 新用户 1B Token 免费额度,半年有效期
百川智能 百川大模型 百川 API 文档 新用户 8元免费额度,3个月有效期
讯飞 SparkDesk 讯飞 API 文档 新用户 100K Token 免费额度,1年有效期
谷歌 Gemini/PaLM2 Google API 文档 提供免费版 API,存在使用限制
OpenAI GPT OpenAI API 文档 网页版可免登录使用,API 需国外银行卡
微软 Azure Azure API 文档 需注册 Azure 账号,支持国内 visa 信用卡或双币卡

添加渠道

集成各类 API 服务添加到 OneAPI 中:

20240415154947

以谷歌的 Gemini 为例:点击渠道 => 新建渠道

20240323231603

内容说明:

  • 渠道名称:自定义
  • 渠道 API 地址:官方 API 地址或者反向代理地址
  • 用户组:这个选项是给号商用的,可以不管
  • 模型:填入用户调用模型
  • 密钥:API 密钥
  • 模型映射关系:用户调用模型 => 实际调用模型。比如 { "dall-e-3": "cogview-3" }:当用户请求 dall-e-3 时,实际请求了 cogview-3 模型。

模型映射关系在一些场景很有用,比如:

  • LlamaIndex 中使用 OpenAI 对象,非 GPT 的模型名称会报错
  • lobe-hub 自带的绘图插件只支持调用 dall-e-3 模型

创建密钥

添加渠道后,点击令牌,新建令牌,创建密钥,复制密钥。密钥作用等同于 OpenAI 的 API。

20240323233424

渠道存在优先级,如果同一模型有多个渠道支持,OneAPI 会按权重使用不同渠道。但 root 用户可通过 -k 指定渠道,比如下边添加了两个渠道:

20240323233805

通过 sk-xxxxxx-1 指定 ChatAnywhere 渠道,通过 sk-xxxxxx-2 指定 bichat 渠道。

除此之外,你可以让每个模型只对应一个渠道,这样指定模型时,渠道也是确定的。

创建密钥后,走服务地址作为 API 代理地址,比如在本地启动服务,走 localhost

1
OPENAI_API_BASE=http://localhost:3000/v1

分配用户

OneAPI 支持多用户管理,root 和管理员能管理模型渠道,配置额度。普通用户则在注册后,通过添加兑换码导入额度。

20240323232808

创建密钥的过程与前边一致。

配置 Nginx

假设服务运行在 3000 端口,配置访问域名为 one-api.example.com

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
listen 80;
server_name one-api.example.com;
location /{
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# 流式
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
# 设置等待响应的时长
proxy_send_timeout 300;
# proxy_connect_timeout 300;
# proxy_read_timeout 300;
# send_timeout 300;
}
}

类似可配置 https 的访问。

反向代理

反向代理主要用于 OpenAI, Gemini, Claude 等限制国内 IP 访问服务。以 Gemini 为例,在支持访问的服务器上配置 Nginx:

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name your.domain.com;
location / {
proxy_pass https://generativelanguage.googleapis.com;
proxy_set_header Host generativelanguage.googleapis.com;
proxy_ssl_server_name on;
proxy_cache off;
proxy_buffering off;
proxy_connect_timeout 7200;
}
}

然后将代理链接 your.domain.com 并导入 OneAPI 的 Gemini 渠道。类似方法适用于对 Claude 和 OpenAI 等服务的转发。

模型代理站

如果硬件资源不足,可以用代理站作为补充。优点是省事,访问快,支持超大模型。缺点是数据经过一手,安全隐患增加一层,使用代理站更多出于实验方便。这里推荐几个站点:

代理站 服务地址 模型支持 备注
Chatanywhere chatanywhere/GPT_API_free OpenAI 系列 速度快,价格低
沃卡 wokaai.com OpenAI/Claude/Gemini/Midjourney 基于 OneAPI 项目
Chandler.ai ChandlerAi OpenAI/Claude/Grok 等 需国外信用卡
NeutrinoAPP neutrinoapp.com OpenAI/开源模型 需国外信用卡
monsterapi.ai monsterapi.ai 仅开源模型 支持模型微调,需国外信用卡
Predibase Predibase AI codellama/llama/gemma/mixtral 等 支持模型微调,需国外信用卡

后两个网站提供对开源模型的微调和推理服务,支持多种模型和微调方式,比如 MonsterAPI:

20240323223430

vllm

前边讲了闭源 API 转成 OpenAI 风格的方式,下边介绍开源模型如何以 OpenAI Style 启动,继而也能导入到 OneAPI 统一管理。这方面我们讲两个比较有代表性的项目。

vLLM 是去年6月推出的一个大模型推理加速框架,通过 PagedAttention 高效管理 attention 中缓存的张量,实现了比 HuggingFace Transformers 高 24 倍的吞吐量。vLLM 支持的开源模型包括 Llama,百川,千问等模型,也支持基于这些模型架构训练或微调得到的模型,比如 Lemur:

20240324172632

vLLM 提供了一个 OpenAI 风格的 API 服务,比如启动百川模型

1
2
3
4
5
6
7
8
# pip install vllm
python -m vllm.entrypoints.openai.api_server \
--model /sshfs/pretrains/baichuan-inc/Baichuan2-13B-Chat \
--trust-remote-code \
--tensor-parallel-size 2 \
--served-model-name baichuan \
--api-key sk-xxxx \
--port 8000

参数说明(Ref:官方文档 - OpenAI Compatible Server):

  • --model:模型路径,本地路径需加上 --trust-remote-code 参数
  • --tensor-parallel-size:模型使用的 GPU 数目
  • --served-model-name:用户访问的模型名称
  • --api-key:API 密钥,可不填,允许任意密钥访问
  • --port:服务端口

vLLM 还支持 推理量化加载 Lora 参数分布式推理等。

类似 vllm 的项目还有 LightLLMFasterTransformer等。

Ollama

GGUF 格式以及 Ollama 更深入的介绍,另外再开帖子讨论

相比 vllm 或者直接使用 huggingface 的模型推理 Pipeline。ollama 极大降低了模型使用门槛:

  • 零 Python 代码以 OpenAI 风格启动模型
  • 支持在普通电脑上运行量化模型,低显存占用,支持 CPU 推理。

从 32 位量化到 4bit,会一定程度降低模型的表现,一种取舍,涉及模型压缩技术。

部署及使用

Docker 部署:https://ollama.com/blog/ollama-is-now-available-as-an-official-docker-image
API 文档:https://github.com/ollama/ollama/blob/main/docs/api.md

推荐使用 docker-compose 部署一键启动服务,方便修改端口,GPU 配置以及模型下载路径等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: '3.8'
services:
ollama:
image: ollama/ollama
container_name: ollama
ports:
- "11434:11434"
volumes: # 模型路径
- /sshfs/ollama:/root/.ollama
environment:
- CUDA_VISIBLE_DEVICES=6,7
# - OLLAMA_HOST="http://localhost:11434"
# - OLLAMA_KEEP_ALIVE="5m"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
restart: unless-stopped

http://localhost:11434 为 API 服务地址。

创建别名:

1
alias ollama='docker exec -it ollama ollama'

基本操作:

1
2
3
4
# 拉取模型,比如千问
ollama pull qwen:0.5b
# 查看拉取的模型列表
ollama list

GGUF 格式

GGUF: GPT-Generated Unified Format
GGUF:GPT 生成的统一格式

文档:https://github.com/ggerganov/ggml/blob/master/docs/gguf.md
模型量化方式:Which Quantization Method Is Best for You?: GGUF, GPTQ, or AWQ

GGUF 由llama.cpp团队介绍。它是一种为大型语言模型设计的量化方法。它允许用户在 CPU 上运行LLMs,同时通过提供速度改进将某些层卸载到 GPU。

GGUF 对于在 CPU 或 Apple 设备上运行模型的用户特别有用。在 GGUF 上下文中,量化涉及缩小模型权重(通常存储为 16 位浮点数)以节省计算资源。

GGUF 是一种更有效、更灵活的存储和用于LLMs推理的方式。它专为快速加载和保存模型而量身定制,采用用户友好的方法来处理模型文件。

开发者:Georgi Gerganov

20240413222810

文档:https://github.com/ggerganov/ggml/blob/master/docs/gguf.md

HuggingFace 大佬 Tom Jobbins 对大量的主流模型进行了量化。

量化模型的仓库地址:https://huggingface.co/TheBloke

模型列表

支持列表:https://ollama.com/library

Embedding:Embedding models

codellama 为例:

20240412215028

主要的关键字:模型参数量,模型类型,量化级别,量化方式。

模型类型 说明
instruct Fine-tuned to generate helpful and safe answers in natural language
python A specialized variation of Code Llama further fine-tuned on 100B tokens of Python code
code Base model for code completion

内存占用方面,千问 Qwen-72b-1.5 模型,4-bit 量化,推理显存 40G 左右,两张 3090 可用:

20240412220741

加载本地模型

文档:https://github.com/ollama/ollama/blob/main/docs/import.md

启动 hf 模型

  • safetensor 格式
  • GGUF 格式

缺陷

  1. 切换模型问题
  2. 首次加载问题
  3. 并发问题(特性已更新):https://github.com/ollama/ollama/pull/3418

数据量大,当前仍需要 vllm 等项目。

flask

最后,如果需要对 API 进行灵活定制,可以用 flask 或 FastAPI 手写服务。举个例子,以 OpenAI 风格启动 HuggingFace 的 Embedding 模型:

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
from flask import Flask, request, jsonify
from sentence_transformers import SentenceTransformer
app = Flask(__name__)
# 加载模型
model_path = '/sshfs/pretrains/moka-ai/m3e-large'
model = SentenceTransformer(model_path)

@app.route('/v1/embeddings', methods=['POST'])
def get_embeddings():
data = request.get_json()
if not all((data, 'input' in data, 'model' in data)):
return jsonify({"error": "Missing required fields in request body"}), 400
input_text = data['input']
# 将单个输入转换为列表
if isinstance(input_text, str):
input_text = [input_text]
# 调用模型获取 embeddings
embeddings = model.encode(input_text)
# 构造响应
embeddings_response = []
for index, embedding in enumerate(embeddings):
embedding_dict = {
"object": "embedding",
"embedding": embedding.tolist(), # 将 numpy 数组转换为列表
"index": index
}
embeddings_response.append(embedding_dict)
# 可选: Token 计数
response = {
"object": "list",
"data": embeddings_response,
"model": data['model'],
"usage": {
"prompt_tokens": sum(len(text.split()) for text in input_text),
"total_tokens": sum(len(text.split()) for text in input_text)
}
}
return jsonify(response)

if __name__ == '__main__':
app.run(debug=True, port=5000, host='0.0.0.0')

测试请求:

1
2
3
4
curl -H "Content-Type: application/json" \
-X POST \
-d '{"input": "The food was delicious and the waiter...", "model": "text-embedding-ada-002"}' \
http://localhost:5000/v1/embeddings

除了常规策略,还有基于 ChatGPT 网页端进行逆向白嫖的,比如 FreeGPT35GPT4Freecoze-discord 等,当前暂不讨论。

ChatTool

用 API 跑数据可以直接用官方库 openai,也可以用 ChatTool,基于 API 的封装,支持多轮对话,异步处理数据等。

先进行配置:

1
2
export OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export OPENAI_API_BASE="https://one-api.example.com/v1"

或者在代码中设置:

1
2
3
import chattool
chattool.api_key = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
chattool.api_base = "https://one-api.example.com/v1"

举两个例子。

  1. 将数字翻译为罗马数字,串行处理数据,缓存文件为 chat.jsonl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def data2chat(msg):
    chat = Chat()
    chat.system("你是一个熟练的数字翻译家。")
    chat.user(f"请将该数字翻译为罗马数字:{msg}")
    chat.model = "gpt-3.5-turbo" # 默认模型
    # 注意: 在函数内执行 getresponse
    chat.getresponse(max_tries=3)
    return chat
    checkpoint = "chat.jsonl" # 缓存文件的名称
    msgs = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
    chats = process_chats(msgs, data2chat, checkpoint)
    # chats = load_chats(checkpoint)
  2. 用不同语言打印 hello world,并行处理数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from chattool import async_chat_completion, load_chats, Chat
    langs = ["python", "java", "Julia", "C++"]
    def data2chat(msg):
    chat = Chat()
    chat.user("请用语言 %s 打印 hello world" % msg)
    # 注意:不需要 getresponse 而交给异步处理
    return chat
    async_chat_completion(langs, chkpoint="async_chat.jsonl", nproc=2, data2chat=data2chat)
    chats = load_chats("async_chat.jsonl")

    在 Jupyter Notebook 中运行,需要使用 await 关键字和 wait=True 参数:

    1
    await async_chat_completion(langs, chkpoint="async_chat.jsonl", nproc=2, data2chat=data2chat, wait=True)

以上,将模型整合到 API 网关,统一服务接口供代码调用。这样,我们就能轻松地集成各类大模型,也省去配置环境的麻烦。