Ollama、vLLM、TGI与量化技术详解

前言

本地部署 LLM 可以保护数据隐私、降低成本、减少延迟。本文详细介绍多种本地部署方案,包括 Ollama、vLLM、TGI 等工具,以及模型量化技术。


部署方案对比

┌─────────────────────────────────────────────────────────────────┐
│                   本地部署方案选择                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  你的需求是什么?                                                │
│       │                                                         │
│       ├── 快速体验/开发 → Ollama                                │
│       │                                                         │
│       ├── 高并发生产 → vLLM                                     │
│       │                                                         │
│       ├── Hugging Face 生态 → TGI                              │
│       │                                                         │
│       ├── Apple Silicon → MLX                                  │
│       │                                                         │
│       └── 边缘设备 → llama.cpp                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
方案 易用性 性能 并发 量化支持 适用场景
Ollama ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐ 开发/个人
vLLM ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 生产环境
TGI ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ HF 生态
llama.cpp ⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ CPU/边缘
MLX ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ Mac

Ollama 部署

安装与基本使用

# macOS
brew install ollama

# Linux
curl -fsSL https://ollama.com/install.sh | sh

# 启动服务
ollama serve

# 拉取模型
ollama pull llama3.1:8b
ollama pull qwen2.5:7b
ollama pull codellama:13b

# 运行模型
ollama run llama3.1:8b

# 列出模型
ollama list

Python SDK

import ollama
from typing import Generator

class OllamaClient:
    """Ollama 客户端封装"""
    
    def __init__(self, host: str = "http://localhost:11434"):
        self.client = ollama.Client(host=host)
    
    def chat(
        self,
        model: str,
        messages: list,
        stream: bool = False
    ) -> str | Generator:
        """对话"""
        response = self.client.chat(
            model=model,
            messages=messages,
            stream=stream
        )
        
        if stream:
            return self._stream_response(response)
        return response["message"]["content"]
    
    def _stream_response(self, response) -> Generator:
        """流式响应"""
        for chunk in response:
            yield chunk["message"]["content"]
    
    def generate(
        self,
        model: str,
        prompt: str,
        system: str = None
    ) -> str:
        """生成"""
        response = self.client.generate(
            model=model,
            prompt=prompt,
            system=system
        )
        return response["response"]
    
    def embed(self, model: str, text: str) -> list:
        """获取嵌入向量"""
        response = self.client.embeddings(
            model=model,
            prompt=text
        )
        return response["embedding"]
    
    def list_models(self) -> list:
        """列出模型"""
        response = self.client.list()
        return [m["name"] for m in response["models"]]

# 使用
client = OllamaClient()

# 对话
messages = [
    {"role": "user", "content": "用Python写一个快速排序"}
]
response = client.chat("llama3.1:8b", messages)
print(response)

# 流式对话
for chunk in client.chat("llama3.1:8b", messages, stream=True):
    print(chunk, end="", flush=True)

# 嵌入
embedding = client.embed("nomic-embed-text", "Hello world")

自定义模型

# Modelfile
FROM llama3.1:8b

# 设置参数
PARAMETER temperature 0.7
PARAMETER num_ctx 4096
PARAMETER top_p 0.9

# 系统提示
SYSTEM """
你是一个专业的Python编程助手。
你会提供清晰、简洁、高效的代码解决方案。
"""

# 创建模型
ollama create python-assistant -f Modelfile

# 运行
ollama run python-assistant

REST API

import requests
import json

class OllamaAPI:
    """Ollama REST API"""
    
    def __init__(self, base_url: str = "http://localhost:11434"):
        self.base_url = base_url
    
    def generate(self, model: str, prompt: str) -> str:
        """生成"""
        response = requests.post(
            f"{self.base_url}/api/generate",
            json={"model": model, "prompt": prompt, "stream": False}
        )
        return response.json()["response"]
    
    def chat_stream(self, model: str, messages: list):
        """流式对话"""
        response = requests.post(
            f"{self.base_url}/api/chat",
            json={"model": model, "messages": messages, "stream": True},
            stream=True
        )
        
        for line in response.iter_lines():
            if line:
                data = json.loads(line)
                if "message" in data:
                    yield data["message"]["content"]
    
    def pull_model(self, model: str):
        """拉取模型"""
        response = requests.post(
            f"{self.base_url}/api/pull",
            json={"name": model},
            stream=True
        )
        
        for line in response.iter_lines():
            if line:
                print(json.loads(line))

vLLM 生产级部署

vLLM 是目前最先进的推理引擎,其核心优势在于 PagedAttentionContinuous Batching

1. 核心技术解析

  • PagedAttention:借鉴了操作系统的虚拟内存管理,将 KV Cache 分页存储在非连续的内存空间中,解决了显存碎片化问题,显存利用率提升至 90% 以上。
  • Continuous Batching:不同于传统的静态 Batching(需等待所有请求完成),vLLM 可以在每个 Token 生成步动态插入新请求,极大提升了吞吐量。

2. 高级部署配置

# 启动支持多 LoRA 的 vLLM 服务
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3-8b \
    --enable-lora \
    --lora-modules sql-lora=/path/to/sql-adapter chat-lora=/path/to/chat-adapter \
    --max-loras 4 \
    --tensor-parallel-size 2 \ # 使用 2 张显卡进行张量并行
    --gpu-memory-utilization 0.95 \
    --max-model-len 8192

3. 投机采样 (Speculative Decoding)

使用一个小模型(如 Llama-68M)来预测 Token,大模型仅负责验证,可提升 2x-3x 的推理速度。

python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3-70b \
    --speculative-model ibm-fms/llama-light-125m \
    --num-speculative-tokens 5 \
    --tensor-parallel-size 4

4. 性能监控 (Prometheus)

vLLM 默认在 :8000/metrics 暴露监控指标。

# 关键指标说明
# vllm:num_requests_running: 当前运行中的请求数
# vllm:iteration_tokens_total: 每秒生成的 Token 总数
# vllm:gpu_cache_usage_perc: KV Cache 显存占用率

Docker 部署

# docker-compose.yml
version: '3.8'

services:
  vllm:
    image: vllm/vllm-openai:latest
    runtime: nvidia
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
    ports:
      - "8000:8000"
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    command: >
      --model meta-llama/Llama-2-7b-chat-hf
      --dtype float16
      --max-model-len 4096
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

Text Generation Inference (TGI)

启动服务

# Docker 启动
docker run --gpus all -p 8080:80 \
    -v $PWD/data:/data \
    ghcr.io/huggingface/text-generation-inference:latest \
    --model-id meta-llama/Llama-2-7b-chat-hf \
    --quantize bitsandbytes-nf4 \
    --max-input-length 4096 \
    --max-total-tokens 8192

Python 客户端

from huggingface_hub import InferenceClient
from text_generation import Client

class TGIClient:
    """TGI 客户端"""
    
    def __init__(self, url: str = "http://localhost:8080"):
        self.client = Client(url)
        self.hf_client = InferenceClient(url)
    
    def generate(
        self,
        prompt: str,
        max_new_tokens: int = 512,
        temperature: float = 0.7
    ) -> str:
        """生成"""
        response = self.client.generate(
            prompt,
            max_new_tokens=max_new_tokens,
            temperature=temperature,
            top_p=0.95,
            do_sample=True
        )
        return response.generated_text
    
    def stream(self, prompt: str, max_new_tokens: int = 512):
        """流式生成"""
        for token in self.client.generate_stream(
            prompt,
            max_new_tokens=max_new_tokens
        ):
            yield token.token.text
    
    def chat(self, messages: list) -> str:
        """对话"""
        response = self.hf_client.chat_completion(
            messages,
            max_tokens=512
        )
        return response.choices[0].message.content

# 使用
tgi_client = TGIClient()
response = tgi_client.generate("写一个Python函数计算斐波那契数列")

for token in tgi_client.stream("解释什么是机器学习"):
    print(token, end="", flush=True)

llama.cpp 部署

编译与运行

# 克隆
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp

# 编译(CPU)
make

# 编译(CUDA)
make LLAMA_CUDA=1

# 编译(Metal - macOS)
make LLAMA_METAL=1

# 下载/转换模型
python convert.py /path/to/model --outtype f16

# 量化
./quantize models/llama-7b/ggml-model-f16.gguf \
    models/llama-7b/ggml-model-q4_k_m.gguf q4_k_m

# 运行
./main -m models/llama-7b/ggml-model-q4_k_m.gguf \
    -p "Hello" -n 128

# 启动服务器
./server -m models/llama-7b/ggml-model-q4_k_m.gguf \
    --host 0.0.0.0 --port 8080

Python 绑定

from llama_cpp import Llama

class LlamaCppClient:
    """llama.cpp Python 客户端"""
    
    def __init__(
        self,
        model_path: str,
        n_ctx: int = 4096,
        n_gpu_layers: int = -1  # -1 = 全部上 GPU
    ):
        self.llm = Llama(
            model_path=model_path,
            n_ctx=n_ctx,
            n_gpu_layers=n_gpu_layers,
            chat_format="llama-2",
            verbose=False
        )
    
    def generate(
        self,
        prompt: str,
        max_tokens: int = 512,
        temperature: float = 0.7
    ) -> str:
        """生成"""
        output = self.llm(
            prompt,
            max_tokens=max_tokens,
            temperature=temperature,
            stop=["</s>", "\n\n"]
        )
        return output["choices"][0]["text"]
    
    def chat(self, messages: list) -> str:
        """对话"""
        response = self.llm.create_chat_completion(
            messages,
            max_tokens=512
        )
        return response["choices"][0]["message"]["content"]
    
    def stream(self, prompt: str):
        """流式生成"""
        for chunk in self.llm(
            prompt,
            max_tokens=512,
            stream=True
        ):
            yield chunk["choices"][0]["text"]

# 使用
client = LlamaCppClient(
    "models/llama-7b-q4_k_m.gguf",
    n_gpu_layers=35
)

response = client.chat([
    {"role": "user", "content": "你好"}
])

模型量化

量化方法对比

量化 精度损失 大小 速度 推荐场景
FP16 1x 基准 高精度要求
INT8 很小 0.5x 平衡方案
INT4 0.25x 更快 资源受限
GPTQ 0.25x GPU 推理
AWQ 很小 0.25x 准确性优先
GGUF 可变 可变 CPU/通用

GPTQ 量化

from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

def quantize_gptq(
    model_name: str,
    output_dir: str,
    bits: int = 4
):
    """GPTQ 量化"""
    # 加载模型
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    # 量化配置
    quantize_config = BaseQuantizeConfig(
        bits=bits,
        group_size=128,
        desc_act=True
    )
    
    # 准备校准数据
    calibration_data = [
        "这是一段用于校准的文本...",
        "另一段校准文本..."
    ]
    
    # 加载并量化
    model = AutoGPTQForCausalLM.from_pretrained(
        model_name,
        quantize_config
    )
    
    model.quantize(calibration_data)
    model.save_quantized(output_dir)
    tokenizer.save_pretrained(output_dir)

# 使用量化模型
model = AutoGPTQForCausalLM.from_quantized(
    "model-gptq",
    device="cuda:0"
)

AWQ 量化

from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

def quantize_awq(
    model_name: str,
    output_dir: str
):
    """AWQ 量化"""
    # 加载
    model = AutoAWQForCausalLM.from_pretrained(model_name)
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    # 量化配置
    quant_config = {
        "zero_point": True,
        "q_group_size": 128,
        "w_bit": 4
    }
    
    # 量化
    model.quantize(tokenizer, quant_config=quant_config)
    
    # 保存
    model.save_quantized(output_dir)
    tokenizer.save_pretrained(output_dir)

# 使用
model = AutoAWQForCausalLM.from_quantized(
    "model-awq",
    fuse_layers=True
)

统一 API 服务

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional, List

app = FastAPI()

class ChatRequest(BaseModel):
    model: str
    messages: List[dict]
    temperature: Optional[float] = 0.7
    max_tokens: Optional[int] = 512
    stream: Optional[bool] = False

class LocalLLMRouter:
    """本地 LLM 路由器"""
    
    def __init__(self):
        self.backends = {}
    
    def register_backend(self, name: str, client):
        """注册后端"""
        self.backends[name] = client
    
    def chat(self, request: ChatRequest) -> str:
        """路由请求"""
        if request.model not in self.backends:
            raise ValueError(f"Model {request.model} not found")
        
        client = self.backends[request.model]
        return client.chat(request.messages)

router = LocalLLMRouter()

# 注册不同后端
router.register_backend("llama3", OllamaClient())
router.register_backend("codellama", VLLMServer("codellama/CodeLlama-7b"))

@app.post("/v1/chat/completions")
async def chat(request: ChatRequest):
    """OpenAI 兼容接口"""
    try:
        response = router.chat(request)
        return {
            "choices": [{
                "message": {"role": "assistant", "content": response}
            }]
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

最佳实践

场景 推荐方案 配置建议
个人开发 Ollama 默认配置
高并发 vLLM 连续批处理
Mac 本地 MLX/Ollama Metal 加速
CPU 部署 llama.cpp Q4_K_M 量化
生产环境 vLLM + K8s 多副本

参考资源

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:《 LLM应用开发——本地模型部署 》

本文链接:http://localhost:3015/ai/%E6%9C%AC%E5%9C%B0%E6%A8%A1%E5%9E%8B%E9%83%A8%E7%BD%B2.html

本文最后一次更新为 天前,文章中的某些内容可能已过时!