第6篇:分布式&对话服务部署——FastChat 部署 OpenAI 兼容接口
本文为「本地大模型部署系列」第6篇,完整系列持续更新中。
前言
如果你已经用 Ollama 跑通了单模型本地服务,或者用 llama.cpp 实现了轻量推理,那么必然会遇到下一个工程挑战:同时管理多个模型、对接已有 OpenAI 格式的项目、给团队提供统一 API 入口。
这篇文章解决的正是这三个问题。
FastChat 是由 UC Berkeley LMSys 团队开源的多模型服务框架,它的核心能力是:
- 把多个本地模型统一包装成 OpenAI 兼容接口(
/v1/chat/completions、/v1/models)
- 三组件架构支持横向扩展,一个 Controller 调度多个 Model Worker
- 任何使用
openai Python 库的项目,只改一行 base_url 即可切换到本地模型
适用场景:团队共用模型服务、多模型 A/B 测试、将个人项目从 OpenAI API 迁移到本地、构建私有化 LLM 中间层。
本篇学完你将掌握:
- FastChat 三组件架构原理与请求流转链路
- 从零启动完整的 FastChat 服务栈(含多模型)
- OpenAI Python SDK 直接对接本地 FastChat 服务
- 流式输出 + 多轮对话完整实现
- 高频踩坑与排查方案
一、核心原理极简讲解
1.1 FastChat 的定位
市面上本地部署框架各有侧重:
| 框架 |
核心定位 |
适合场景 |
| Ollama |
单模型极简一键部署 |
个人快速测试、日常使用 |
| llama.cpp |
极致轻量、CPU 推理 |
低配机器、边缘设备 |
| FastChat |
多模型服务编排 + OpenAI 兼容接口 |
团队服务、多模型管理、项目对接 |
| vLLM |
高吞吐生产级推理 |
高并发、企业生产环境 |
FastChat 的差异化在于:它不是一个推理引擎,而是一个模型服务编排层。它把调度、推理、接口这三件事彻底解耦,每一层都可以独立扩展替换。
1.2 三组件架构深度解析
FastChat 采用经典的三层服务架构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ┌─────────────────────────────────────────────────────────────┐ │ FastChat 服务架构 │ │ │ │ ┌─────────────┐ ┌─────────────────────────────────┐ │ │ │ Client │────▶│ OpenAI API Server │ │ │ │(curl/Python)│ │ (Port: 8000) │ │ │ └─────────────┘ │ /v1/models │ │ │ │ /v1/chat/completions │ │ │ └────────────┬────────────────────┘ │ │ │ HTTP 转发 │ │ ┌────────────▼────────────────────┐ │ │ │ Controller │ │ │ │ (Port: 21001) │ │ │ │ 路由 & 负载均衡 │ │ │ └──┬──────────────────┬───────────┘ │ │ │ │ │ │ ┌──────────▼────┐ ┌──────────▼────┐ │ │ │ Model Worker │ │ Model Worker │ ... │ │ │ (Port:21002) │ │ (Port:21003) │ │ │ │ Qwen2.5-7B │ │ Llama3-8B │ │ │ └───────────────┘ └───────────────┘ │ └─────────────────────────────────────────────────────────────┘
|
Controller(调度中心)
- 职责:维护所有已注册 Worker 的心跳列表,根据调度策略(轮询/最短队列)将请求路由到合适的 Worker
- 特点:无状态轻量进程,不参与任何模型计算,仅做服务发现和路由
- 关键配置:
--dispatch-method(lottery 随机轮询 / shortest_queue 最短队列优先)
Model Worker(模型推理进程)
- 职责:加载并持有一个具体的模型权重,执行实际的前向推理计算
- 特点:一个 Worker 进程 = 一个模型实例;多 GPU 可通过
--num-gpus 配置张量并行
- 启动后自动向 Controller 注册心跳,断线自动重连
OpenAI API Server(接口层)
- 职责:对外暴露标准 OpenAI REST API,将外部请求翻译成内部 FastChat 协议后转发给 Controller
- 特点:完全兼容 OpenAI API 规范,
/v1/chat/completions、/v1/models、/v1/embeddings 均支持
1.3 请求完整流转链路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Client 发送请求 │ ▼ OpenAI API Server(Port 8000) ├── 解析 OpenAI 格式请求 ├── 提取 model 字段(如 "qwen2.5-7b") └── 转发到 Controller │ ▼ Controller(Port 21001) ├── 查找持有该 model_name 的 Worker 列表 ├── 根据 dispatch_method 选择一个 Worker └── 返回该 Worker 地址给 API Server │ ▼ Model Worker(Port 21002) ├── 执行模型推理(流式 / 非流式) └── 返回生成结果 │ ▼ OpenAI API Server └── 封装为 OpenAI 格式响应返回给 Client
|
1.4 为什么这个架构适合多模型场景
- 松耦合:三个组件可独立重启,Worker 挂掉不影响 Controller 和其他 Worker
- 横向扩展:同一个模型可以启动多个 Worker 实例(不同 GPU 或不同机器),Controller 自动负载均衡
- 模型隔离:不同模型各自占用独立进程和显存,互不干扰
- 接口统一:无论后端挂了多少模型,对外始终是同一个 OpenAI 格式 API
二、环境 & 前置依赖
2.1 硬件要求
| 配置项 |
最低要求 |
推荐配置 |
| GPU 显存 |
8GB(单 7B 模型) |
24GB+(多模型并发) |
| 系统内存 |
16GB |
32GB+ |
| 操作系统 |
Linux(推荐)/ Windows(有额外注意事项) |
|
| CUDA 版本 |
11.8+ |
12.1+ |
多模型显存估算:几个 Worker 同时运行就需要叠加几块显存。7B FP16 约需 14GB,量化到 4-bit 约需 4-5GB。务必规划好显存分配再启动多个 Worker。
2.2 软件依赖
1 2 3 4 5 6 7 8
| Python 3.10+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
|
2.3 安装 FastChat
方式一:pip 直接安装(推荐,快速)
1
| pip install "fschat[model_worker,webui]"
|
注意:包名是 fschat,不是 fastchat,这是一个高频踩坑点。
方式二:源码安装(功能最全,可修改源码)
1 2 3
| git clone https://github.com/lm-sys/FastChat.git cd FastChat pip install -e ".[model_worker,webui]"
|
验证安装:
1
| python -m fastchat.serve.controller --help
|
出现参数帮助信息即表示安装成功。
三、Step-by-Step 实操流程
3.1 准备工作:目录与模型
本篇以 Qwen2.5-7B-Instruct 和 Llama-3.1-8B-Instruct 为例(模型下载参见本系列第2篇)。
假设模型存放路径:
1 2 3
| ~/models/ ├── Qwen2.5-7B-Instruct/ └── Llama-3.1-8B-Instruct/
|
3.2 启动 Controller
Controller 是整个服务栈的核心枢纽,必须第一个启动。
1 2 3 4 5 6 7 8 9 10 11 12
| python -m fastchat.serve.controller \ --host 0.0.0.0 \ --port 21001 \ --dispatch-method shortest_queue
nohup python -m fastchat.serve.controller \ --host 0.0.0.0 \ --port 21001 \ --dispatch-method shortest_queue \ > logs/controller.log 2>&1 &
|
确认启动成功:
1 2 3 4 5 6 7
|
curl http://localhost:21001/list_models
|
3.3 启动 Model Worker(以 Qwen2.5-7B 为例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| python -m fastchat.serve.model_worker \ --host 0.0.0.0 \ --port 21002 \ --worker-address http://localhost:21002 \ --controller-address http://localhost:21001 \ --model-path ~/models/Qwen2.5-7B-Instruct \ --model-names qwen2.5-7b,qwen2.5 \ --num-gpus 1
python -m fastchat.serve.model_worker \ --host 0.0.0.0 \ --port 21002 \ --worker-address http://localhost:21002 \ --controller-address http://localhost:21001 \ --model-path ~/models/Qwen2.5-7B-Instruct \ --model-names qwen2.5-7b \ --num-gpus 1 \ --load-4bit
|
日志确认:
1 2 3
| # Worker 启动后会打印注册信息: # INFO | model_worker.py | Register to controller # INFO | model_worker.py | Send heart beat. Models: ['qwen2.5-7b', 'qwen2.5'].
|
验证 Worker 已注册到 Controller:
1 2
| curl http://localhost:21001/list_models
|
3.4 启动 OpenAI API Server
1 2 3 4 5
| python -m fastchat.serve.openai_api_server \ --host 0.0.0.0 \ --port 8000 \ --controller-address http://localhost:21001 \ --api-keys sk-your-custom-key-optional
|
--api-keys 是可选的,设置后调用方需要在请求头中携带 Authorization: Bearer sk-your-custom-key-optional,适合内网鉴权。不设置则任何请求都可通过。
3.5 验证服务(curl 测试)
查询已加载模型列表:
1 2 3 4 5 6 7 8 9 10 11
| curl http://localhost:8000/v1/models \ -H "Content-Type: application/json"
|
发送对话请求:
1 2 3 4 5 6 7 8 9 10 11
| curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen2.5-7b", "messages": [ {"role": "system", "content": "你是一个专业的AI助手。"}, {"role": "user", "content": "用一句话解释什么是大模型。"} ], "temperature": 0.7, "max_tokens": 200 }'
|
3.6 多模型同时挂载
这是 FastChat 的核心能力。启动第二个 Worker,挂载不同模型,使用不同端口。
1 2 3 4 5 6 7 8 9
| python -m fastchat.serve.model_worker \ --host 0.0.0.0 \ --port 21003 \ --worker-address http://localhost:21003 \ --controller-address http://localhost:21001 \ --model-path ~/models/Llama-3.1-8B-Instruct \ --model-names llama3-8b,llama3 \ --num-gpus 1
|
端口分配规范(推荐约定):
| 组件 |
端口 |
说明 |
| Controller |
21001 |
固定,所有 Worker 注册目标 |
| Model Worker 1 |
21002 |
Qwen2.5-7B |
| Model Worker 2 |
21003 |
Llama3-8B |
| Model Worker 3 |
21004 |
其他模型 |
| OpenAI API Server |
8000 |
对外统一入口 |
两个 Worker 启动后,验证:
1 2
| curl http://localhost:8000/v1/models
|
切换模型只需修改请求中的 model 字段:
1 2 3 4
| curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "llama3-8b", "messages": [{"role": "user", "content": "Hello!"}]}'
|
3.7 Python 客户端调用
这是 FastChat 最大的工程价值:任何基于 openai 库的项目,改一行 base_url 即可切换到本地模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| from openai import OpenAI
client = OpenAI( base_url="http://localhost:8000/v1", api_key="sk-your-custom-key-optional" )
response = client.chat.completions.create( model="qwen2.5-7b", messages=[ {"role": "system", "content": "你是一个专业的AI助手。"}, {"role": "user", "content": "FastChat 和 vLLM 的区别是什么?"} ], temperature=0.7, max_tokens=500 )
print(response.choices[0].message.content)
|
3.8 流式输出实现
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
| from openai import OpenAI
client = OpenAI( base_url="http://localhost:8000/v1", api_key="none" )
def chat_stream(model: str, messages: list, **kwargs): """ 流式对话,实时打印生成内容 """ try: stream = client.chat.completions.create( model=model, messages=messages, stream=True, temperature=kwargs.get("temperature", 0.7), max_tokens=kwargs.get("max_tokens", 1024), top_p=kwargs.get("top_p", 0.9), ) full_response = "" for chunk in stream: if chunk.choices[0].delta.content is not None: content = chunk.choices[0].delta.content print(content, end="", flush=True) full_response += content print() return full_response except Exception as e: print(f"流式请求失败: {e}") raise
messages = [ {"role": "system", "content": "你是一个专业的Python工程师。"}, {"role": "user", "content": "写一个简单的装饰器示例,并解释原理。"} ]
response = chat_stream("qwen2.5-7b", messages)
|
3.9 多轮对话实现
多轮对话的核心是在客户端维护 messages 列表,每次请求都带上完整的历史上下文。
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
| from openai import OpenAI from typing import List, Dict, Optional
client = OpenAI( base_url="http://localhost:8000/v1", api_key="none" )
class ConversationManager: """ 多轮对话管理器 支持对话记忆、上下文截断、会话重置 """ def __init__( self, model: str = "qwen2.5-7b", system_prompt: str = "你是一个专业的AI助手。", max_history_turns: int = 10, ): self.model = model self.max_history_turns = max_history_turns self.messages: List[Dict] = [ {"role": "system", "content": system_prompt} ] def _truncate_history(self): """保留 system prompt + 最近 N 轮(每轮包含 user + assistant 两条)""" system_msg = self.messages[0] history = self.messages[1:] max_msgs = self.max_history_turns * 2 if len(history) > max_msgs: history = history[-max_msgs:] self.messages = [system_msg] + history def chat(self, user_input: str, stream: bool = True) -> str: """发送消息并获取回复""" self.messages.append({"role": "user", "content": user_input}) try: if stream: response_content = self._stream_response() else: response = client.chat.completions.create( model=self.model, messages=self.messages, temperature=0.7, max_tokens=1024 ) response_content = response.choices[0].message.content self.messages.append({ "role": "assistant", "content": response_content }) self._truncate_history() return response_content except Exception as e: self.messages.pop() raise RuntimeError(f"对话请求失败: {e}") def _stream_response(self) -> str: """流式响应内部实现""" stream = client.chat.completions.create( model=self.model, messages=self.messages, stream=True, temperature=0.7, max_tokens=1024 ) full_content = "" for chunk in stream: if chunk.choices[0].delta.content is not None: content = chunk.choices[0].delta.content print(content, end="", flush=True) full_content += content print() return full_content def reset(self, new_system_prompt: Optional[str] = None): """重置会话(保留 system prompt)""" system_content = new_system_prompt or self.messages[0]["content"] self.messages = [{"role": "system", "content": system_content}] print("会话已重置。") def show_history(self): """打印当前对话历史""" for msg in self.messages: role = msg["role"].upper() content = msg["content"][:100] + "..." if len(msg["content"]) > 100 else msg["content"] print(f"[{role}]: {content}")
def main(): conv = ConversationManager( model="qwen2.5-7b", system_prompt="你是一个专业的Python工程师,回答简洁实用。", max_history_turns=8 ) print("多轮对话启动,输入 'quit' 退出,'reset' 重置会话,'history' 查看历史\n") while True: user_input = input("你: ").strip() if user_input.lower() == "quit": break elif user_input.lower() == "reset": conv.reset() continue elif user_input.lower() == "history": conv.show_history() continue elif not user_input: continue print("AI: ", end="") try: conv.chat(user_input) except RuntimeError as e: print(f"错误: {e}") print("对话结束。")
if __name__ == "__main__": main()
|
3.10 使用 vLLM 作为 Worker 后端
FastChat 支持将 vLLM 作为 Worker 的推理后端,大幅提升高并发吞吐量:
1 2 3 4 5 6 7 8 9 10 11 12 13
| pip install vllm
python -m fastchat.serve.vllm_worker \ --host 0.0.0.0 \ --port 21002 \ --worker-address http://localhost:21002 \ --controller-address http://localhost:21001 \ --model-path ~/models/Qwen2.5-7B-Instruct \ --model-names qwen2.5-7b \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.85
|
vLLM Worker 与默认 Worker 对 Controller 和 API Server 完全透明,切换后调用方无需任何改动。vLLM 的 PagedAttention 技术在高并发场景下吞吐量比默认 Worker 高 5-10 倍(详见本系列第8篇)。
四、完整三组件启动命令模板(一键可用)
将以下内容保存为 start_fastchat.sh(Linux)或 start_fastchat.ps1(Windows):
Linux/macOS 启动脚本:
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
| #!/bin/bash
MODEL1_PATH="$HOME/models/Qwen2.5-7B-Instruct" MODEL1_NAME="qwen2.5-7b" MODEL2_PATH="$HOME/models/Llama-3.1-8B-Instruct" MODEL2_NAME="llama3-8b" LOG_DIR="./logs"
mkdir -p $LOG_DIR
echo "[1/4] 启动 Controller..." nohup python -m fastchat.serve.controller \ --host 0.0.0.0 \ --port 21001 \ --dispatch-method shortest_queue \ > $LOG_DIR/controller.log 2>&1 &
echo "等待 Controller 启动..." sleep 3
echo "[2/4] 启动 Model Worker 1($MODEL1_NAME)..." nohup python -m fastchat.serve.model_worker \ --host 0.0.0.0 \ --port 21002 \ --worker-address http://localhost:21002 \ --controller-address http://localhost:21001 \ --model-path $MODEL1_PATH \ --model-names $MODEL1_NAME \ --num-gpus 1 \ > $LOG_DIR/worker1.log 2>&1 &
echo "[3/4] 启动 Model Worker 2($MODEL2_NAME)..." nohup python -m fastchat.serve.model_worker \ --host 0.0.0.0 \ --port 21003 \ --worker-address http://localhost:21003 \ --controller-address http://localhost:21001 \ --model-path $MODEL2_PATH \ --model-names $MODEL2_NAME \ --num-gpus 1 \ > $LOG_DIR/worker2.log 2>&1 &
echo "等待模型加载(约 60 秒)..." sleep 60
echo "[4/4] 启动 OpenAI API Server..." nohup python -m fastchat.serve.openai_api_server \ --host 0.0.0.0 \ --port 8000 \ --controller-address http://localhost:21001 \ > $LOG_DIR/api_server.log 2>&1 &
echo "等待 API Server 启动..." sleep 5
echo "" echo "=== FastChat 服务启动完成 ===" echo "API 地址:http://localhost:8000" curl -s http://localhost:8000/v1/models | python3 -m json.tool
|
Windows PowerShell 启动脚本:
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
|
$MODEL1_PATH = "D:\models\Qwen2.5-7B-Instruct" $MODEL1_NAME = "qwen2.5-7b" $MODEL2_PATH = "D:\models\Llama-3.1-8B-Instruct" $MODEL2_NAME = "llama3-8b" $LOG_DIR = ".\logs"
New-Item -ItemType Directory -Force -Path $LOG_DIR | Out-Null
Write-Host "[1/4] 启动 Controller..." Start-Process python -ArgumentList "-m fastchat.serve.controller --host 0.0.0.0 --port 21001 --dispatch-method shortest_queue" -RedirectStandardOutput "$LOG_DIR\controller.log" -WindowStyle Hidden
Start-Sleep -Seconds 3
Write-Host "[2/4] 启动 Model Worker 1..." Start-Process python -ArgumentList "-m fastchat.serve.model_worker --host 0.0.0.0 --port 21002 --worker-address http://localhost:21002 --controller-address http://localhost:21001 --model-path `"$MODEL1_PATH`" --model-names $MODEL1_NAME --num-gpus 1" -RedirectStandardOutput "$LOG_DIR\worker1.log" -WindowStyle Hidden
Write-Host "[3/4] 启动 Model Worker 2..." Start-Process python -ArgumentList "-m fastchat.serve.model_worker --host 0.0.0.0 --port 21003 --worker-address http://localhost:21003 --controller-address http://localhost:21001 --model-path `"$MODEL2_PATH`" --model-names $MODEL2_NAME --num-gpus 1" -RedirectStandardOutput "$LOG_DIR\worker2.log" -WindowStyle Hidden
Write-Host "等待模型加载(约 60 秒)..." Start-Sleep -Seconds 60
Write-Host "[4/4] 启动 OpenAI API Server..." Start-Process python -ArgumentList "-m fastchat.serve.openai_api_server --host 0.0.0.0 --port 8000 --controller-address http://localhost:21001" -RedirectStandardOutput "$LOG_DIR\api_server.log" -WindowStyle Hidden
Start-Sleep -Seconds 5 Write-Host "=== FastChat 服务启动完成,API 地址:http://localhost:8000 ==="
|
五、关键参数详解
5.1 Controller 参数
| 参数 |
默认值 |
说明 |
--host |
localhost |
监听地址,对外服务需改为 0.0.0.0 |
--port |
21001 |
Controller 监听端口 |
--dispatch-method |
lottery |
调度策略:lottery(随机轮询)/ shortest_queue(最短队列,高并发推荐) |
5.2 Model Worker 参数
| 参数 |
默认值 |
说明 |
--model-path |
必填 |
模型本地路径或 HuggingFace 模型名 |
--model-names |
取目录名 |
对外暴露的模型名称,支持逗号分隔多个别名(如 qwen2.5-7b,qwen) |
--num-gpus |
1 |
使用 GPU 数量,多 GPU 启用张量并行 |
--max-gpu-memory |
自动 |
每块 GPU 最大显存(如 12GiB),用于多 Worker 显存分配 |
--load-8bit |
False |
开启 8-bit 量化加载(显存减半,速度略降) |
--load-4bit |
False |
开启 4-bit 量化加载(显存约减 75%,精度有损) |
--worker-address |
必填 |
Worker 自身可被 Controller 访问的地址 |
--controller-address |
http://localhost:21001 |
Controller 地址 |
--device |
cuda |
推理设备:cuda / cpu / mps(Apple Silicon) |
重要:--model-names 和 --model-path 的区别:--model-path 是磁盘路径,--model-names 是 API 请求中 model 字段使用的名称。两者完全独立,请求时用 --model-names 中定义的名称。
5.3 OpenAI API Server 参数
| 参数 |
默认值 |
说明 |
--host |
localhost |
监听地址 |
--port |
8000 |
API Server 端口 |
--controller-address |
http://localhost:21001 |
Controller 地址 |
--api-keys |
无(不鉴权) |
设置后请求头需携带 Authorization: Bearer <key> |
--allowed-origins |
* |
CORS 跨域配置,前端直连时可按需限制 |
5.4 推理参数透传(请求体参数)
| 参数 |
说明 |
推荐范围 |
temperature |
控制生成随机性,0 为确定性输出 |
0.1~1.0,代码生成建议 0.1~0.3 |
max_tokens |
最大生成 token 数 |
按任务需求,默认 512 |
top_p |
核采样概率阈值 |
0.85~0.95 |
top_k |
候选 token 数量限制 |
40~50 |
stop |
停止词列表 |
如 `[“< |
presence_penalty |
重复惩罚(存在性) |
-2.0~2.0 |
frequency_penalty |
重复惩罚(频率) |
-2.0~2.0 |
六、踩坑记录 & 问题解决
坑1:三组件启动顺序错误
现象:Worker 启动后报 Failed to register to controller,或 API Server 请求返回 No available workers。
原因:未按 Controller → Worker → API Server 的顺序启动。
解决:严格按顺序启动,Controller 完全就绪(能响应 /list_models)后再启动 Worker,Worker 日志显示心跳发送成功后再启动 API Server。
1 2 3
| curl http://localhost:21001/list_models
|
坑2:Worker 注册到 Controller 失败
现象:Worker 日志中循环出现 Send heart beat. Failed to connect to controller.
排查步骤:
1 2 3 4 5 6 7 8 9 10 11 12
| ps aux | grep controller Get-Process python
curl http://localhost:21001/list_models
telnet <controller-ip> 21001
|
坑3:多模型时显存分配冲突 (CUDA OOM)
现象:启动第二个 Worker 时报 CUDA out of memory。
解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| python -m fastchat.serve.model_worker \ --model-path ~/models/Qwen2.5-7B-Instruct \ --model-names qwen2.5-7b \ --num-gpus 1 \ --max-gpu-memory 10GiB
--load-4bit
CUDA_VISIBLE_DEVICES=0 python -m fastchat.serve.model_worker \ --model-path ~/models/Qwen2.5-7B \ --model-names qwen2.5-7b ...
CUDA_VISIBLE_DEVICES=1 python -m fastchat.serve.model_worker \ --model-path ~/models/Llama-3.1-8B \ --model-names llama3-8b ...
|
坑4:流式输出中断 / 超时
现象:流式请求在中途断开,或客户端长时间等待后超时。
解决:
1 2 3 4 5 6 7 8 9 10 11
| client = OpenAI( base_url="http://localhost:8000/v1", api_key="none", timeout=300.0 )
|
坑5:--model-names 的坑(最高频踩坑)
现象:API 请求报 The model 'xxx' does not exist,但 /v1/models 接口能看到模型。
原因:请求中的 model 字段和 Worker 的 --model-names 大小写或拼写不一致。
1 2 3 4 5 6 7 8
| --model-names Qwen2.5-7B-Instruct
"model": "qwen2.5-7b"
--model-names qwen2.5-7b,qwen2.5
|
建议:在 Worker 启动时通过 --model-names 明确指定简洁的模型别名,避免使用目录原始名称(往往含大写和版本号)。
坑6:Windows 下多进程启动注意事项
现象:Windows 上 Model Worker 启动时报 RuntimeError: CUDA error: invalid device ordinal 或多进程冲突。
原因:Windows 下 Python 多进程(multiprocessing)的 fork 行为与 Linux 不同。
解决:
另外,Windows 下建议优先使用 --device cpu 先验证服务流程是否正常,再切换到 GPU。
七、场景适配 & 优劣分析
7.1 FastChat 适合的场景
- 多模型服务管理:团队有多个本地模型需要对外提供服务,统一由 FastChat 管理,API 入口唯一
- OpenAI 接口替换:现有项目已使用 OpenAI SDK,迁移成本极低(只改
base_url)
- 团队内部模型服务:搭建内网私有 LLM 服务,多人共用,Controller 统一调度
- 多模型 A/B 测试:同时挂载多个版本模型,通过修改请求
model 字段切换
- 模型热切换:可以动态添加/移除 Worker 而不影响 API Server
7.2 FastChat 不适合的场景
- 极致高并发生产环境:默认 Worker 的并发性能不如 vLLM(PagedAttention 优势明显)
- 个人快速测试:三组件启动流程相对繁琐,不如 Ollama 一条命令简单
- 资源非常有限的机器:三个进程同时运行有一定系统开销
7.3 与主流框架的定位对比
| 维度 |
Ollama |
llama.cpp |
FastChat |
vLLM |
| 上手难度 |
⭐ 极简 |
⭐⭐ 需编译 |
⭐⭐⭐ 三组件 |
⭐⭐⭐ 中等 |
| 多模型支持 |
支持 |
手动管理 |
原生多模型 |
支持 |
| OpenAI 兼容 |
支持 |
部分 |
完全兼容 |
完全兼容 |
| 推理性能 |
中等 |
中等(CPU优) |
中等 |
最高(PagedAttention) |
| 适合并发 |
低并发 |
低并发 |
中并发 |
高并发 |
| 主要场景 |
个人测试 |
低配机器 |
多模型服务/团队 |
生产高并发 |
| 横向扩展 |
有限 |
手动 |
Worker 级扩展 |
单节点优化 |
工程决策树:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 需要本地大模型服务? │ ├── 个人使用、快速测试? │ └── → Ollama(最简单) │ ├── 低配机器 / 无 GPU? │ └── → llama.cpp(CPU 友好) │ ├── 多个模型需要统一管理 / 对接已有 OpenAI 项目? │ └── → FastChat(本篇方案) │ └── 高并发生产环境、性能优先? └── → vLLM(第8篇方案) 或 FastChat + vLLM Worker(两者结合)
|
八、本篇小结
核心知识点复盘
- FastChat 三组件架构:Controller(调度)+ Model Worker(推理)+ OpenAI API Server(接口),三层解耦,各司其职
- 请求链路:Client → API Server → Controller(选 Worker)→ Worker → 返回结果
- 多模型管理:一个 Controller 对应多个 Worker,每个 Worker 独立进程,不同端口
- OpenAI 兼容:只改
base_url,现有代码零改动迁移到本地
启动命令速查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| python -m fastchat.serve.controller --host 0.0.0.0 --port 21001 --dispatch-method shortest_queue
python -m fastchat.serve.model_worker \ --port 21002 --worker-address http://localhost:21002 \ --controller-address http://localhost:21001 \ --model-path /path/to/model --model-names my-model --num-gpus 1
python -m fastchat.serve.openai_api_server --host 0.0.0.0 --port 8000 \ --controller-address http://localhost:21001
curl http://localhost:8000/v1/models
|
架构要点速记
1 2 3 4 5
| 启动顺序:Controller → Worker(s) → API Server 端口约定:Controller:21001 | Worker:21002+ | API:8000 模型名称:由 --model-names 决定,与目录名无关 显存规划:多 Worker = 显存叠加,用 --max-gpu-memory 或 CUDA_VISIBLE_DEVICES 隔离 性能升级:将 model_worker 替换为 vllm_worker 无感提升
|
本文为「本地大模型部署系列」第6篇,完整系列持续更新中。