【GLM-4部署实战】GLM-4-9B-Chat模型本地部署实践指南

系列篇章💥

No.文章
1【GLM-4部署实战】GLM-4-9B-Chat模型本地部署实践指南
2【GLM-4部署实战】GLM-4-9B-Chat模型之对话机器人部署测试
3【GLM-4部署实战】GLM-4-9B-Chat模型之vLLM部署推理实践
4【GLM-4微调实战】GLM-4-9B-Chat模型之Lora微调实战


引言

在人工智能的浪潮中,深度学习模型的部署已成为技术研究和实践的热点。自然语言处理(NLP)领域,尤其是对话系统,正迅速成为智能应用的核心。GLM-4-9B-Chat模型以其卓越的语言理解与生成能力,为构建智能对话系统提供了坚实的基础。然而,模型的部署并非易事,它涉及到环境配置、依赖管理、代码编写等多个环节。本文将通过本地模型推理和OpenAI API风格服务发布测试,带你一步步完成GLM-4-9B-Chat模型的部署实践,让你对深度学习模型的部署有一个全面而深入的理解。

一、GLM-4-9B-Chat介绍

GLM-4-9B,作为智谱 AI 推出的GLM-4 系列的最新力作,以其开源之姿,引领着预训练模型的新潮流。在涵盖语义理解、数学计算、逻辑推理、编程代码以及广泛知识领域的多项数据集评估中,GLM-4-9B 及其经过人类偏好优化的衍生版本 GLM-4-9B-Chat,均以卓越的性能脱颖而出。

GLM-4-9B-Chat 不仅精通多轮对话,更以其网页浏览、代码执行、自定义工具调用(Function Call)和长文本推理能力(支持高达128K的上下文长度)等高级功能,展现出其非凡的智能。此外,本代模型在多语言处理上取得突破,支持包括日语、韩语、德语在内的26种语言,真正实现了跨文化交流的无缝对接。

为了满足更广泛的应用需求,还特别推出了支持1M上下文长度(约合200万中文字符)的增强版模型,进一步拓展了智能处理的边界,为复杂场景下的深度学习和智能决策提供了强有力的支持。

1、模型测评

在一系列经典任务的严格评测中,GLM-4-9B-Chat 模型以其非凡的表现力,显著超越了 Llama-3-8B-Instruct 和 ChatGLM3-6B 这两个强劲的对手。以下是测试结果,它们不仅彰显了 GLM-4-9B-Chat 在各个领域的卓越性能,也印证了其在人工智能领域的领先地位:

在这里插入图片描述

2、多语言能力

在涵盖六种不同语言的多语言数据集上,对 GLM-4-9B-Chat 与 Llama-3-8B-Instruct 进行了全面的测试。测试结果及其对应的数据集和选取的语言,如下表。
在这里插入图片描述

3、工具调用能力

在Berkeley Function Calling Leaderboard上,GLM-4-9B-Chat 模型的测试表现令人瞩目,与业界领先的 gpt-4-turbo 模型相比,两者实力不相上下,竞争异常激烈。以下是测试结果,它们展现了GLM-4-9B-Chat 在函数调用任务上的卓越实力和精准性能:
在这里插入图片描述

二、环境准备

首先,可以通过AutoDL平台轻松租用一台高性能的计算设备,该设备配备了24GB显存的顶级显卡,例如NVIDIA 4090,为您的研究和开发提供了强大的硬件支持。在挑选镜像时,我们建议您选择"PyTorch-2.1.0-3.10(ubuntu22.04)-12.1",这是一个专为Python 3.10环境优化的镜像,预装了PyTorch 2.1.0,确保了软件环境的稳定性和兼容性。
在这里插入图片描述

三、下载github源码

git clone https://github.com/THUDM/GLM-4.git

执行如下:
在这里插入图片描述

四、安装相关依赖

1.升级pip
确保pip工具是最新版本,以便顺利安装后续依赖。

python -m pip install --upgrade pip

升级完成如下:
在这里插入图片描述
2. 更换pypi源
为了加速库的安装,推荐使用清华大学的TUNA镜像站。

pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

设置如下:
在这里插入图片描述

3.安装依赖包
安装FastAPI、Uvicorn、Requests等必要的Python库。

cd GLM-4/basic_demo/
pip install -r requirements.txt 

安装完成如下:(pip install ‘pillow<10,>=6.2.0’)
在这里插入图片描述

requirements.txt 文件如下:

torch>=2.3.0
torchvision>=0.18.0
transformers>=4.42.4
huggingface-hub>=0.24.0
sentencepiece>=0.2.0
jinja2>=3.1.4
pydantic>=2.8.2
timm>=1.0.7
tiktoken>=0.7.0
accelerate>=0.32.1
sentence_transformers>=3.0.1
gradio>=4.38.1 # web demo
openai>=1.35.0 # openai demo
einops>=0.8.0
pillow>=10.4.0
sse-starlette>=2.1.2
bitsandbytes>=0.43.1 # INT4 Loading

# vllm>=0.5.2
# flash-attn>=2.5.9 # using with flash-attention 2
# PEFT model, not need if you don't use PEFT finetune model.
# peft>=0.11.1

五、下载模型文件

使用 modelscope 中的 snapshot_download 函数下载模型。第一个参数为模型名称,参数 cache_dir 用于指定模型的下载路径。

import torch
from modelscope import snapshot_download, AutoModel, AutoTokenizer
import os
model_dir = snapshot_download('ZhipuAI/glm-4-9b-chat', cache_dir='/root/autodl-tmp', revision='master')

在 /root/autodl-tmp 路径下新建 download-model.py 文件,并在其中输入以上内容:
在这里插入图片描述
运行 python /root/autodl-tmp/download-model.py 执行下载。需注意,模型大小约为 18 GB,下载模型大概需要 10 - 20 分钟,请耐心等待。
在这里插入图片描述

六、本地模型推理测试

1、加载模型

根据本地模型地址,加载分词器和语言模型

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

# 设定使用的设备为CUDA,如果CUDA不可用,将回退到CPU
device = "cuda"

# 指定预训练模型的路径
mode_name_or_path = '/root/autodl-tmp/ZhipuAI/glm-4-9b-chat'

# 从指定路径加载预训练的分词器
tokenizer = AutoTokenizer.from_pretrained(mode_name_or_path, trust_remote_code=True)


# 从指定路径加载预训练的模型
# torch_dtype指定数据类型,low_cpu_mem_usage优化CPU内存使用,trust_remote_code允许加载远程代码
model = AutoModelForCausalLM.from_pretrained(
    mode_name_or_path,
    torch_dtype=torch.bfloat16,
    low_cpu_mem_usage=True,
    trust_remote_code=True
).to(device).eval()  # 将模型设置为评估模式

加载成功如下:
在这里插入图片描述

2、定义消息

定义提示消息,对输入消息进行分词处理

# 定义用户输入的查询文本
query = "请介绍一下AI大模型"

# 使用分词器的apply_chat_template方法来准备输入数据
# 这个方法会根据聊天模板将用户输入格式化为模型可接受的格式
# add_generation_prompt添加生成提示,tokenize进行分词,return_tensors指定返回PyTorch张量
# return_dict指定返回字典格式,方便后续处理
inputs = tokenizer.apply_chat_template(
    [{"role": "user", "content": query}],
    add_generation_prompt=True,
    tokenize=True,
    return_tensors="pt",
    return_dict=True
)

# 将输入数据移动到指定的设备上
inputs = inputs.to(device)

3、文本生成

利用模型,根据输入的input,生成输出的响应output

# 定义生成文本时的参数
gen_kwargs = {
    "max_length": 2500,  # 设置生成文本的最大长度
    "do_sample": True,  # 是否从可能的下一个词中随机选择
    "top_k": 1  # 从概率最高的k个词中选择
}

# 使用torch.no_grad()上下文管理器来禁用梯度计算,这在推理时可以减少内存使用
with torch.no_grad():
    # 使用模型的generate方法生成文本
    outputs = model.generate(**inputs, **gen_kwargs)
    
    # 截取生成的文本,去除开头的提示部分
    outputs = outputs[:, inputs['input_ids'].shape[1]:]
    
    # 使用分词器的decode方法将生成的词ID解码回文本,并打印出来
    print(tokenizer.decode(outputs[0], skip_special_tokens=True))

输出如下:

AI大模型,即人工智能大型模型,是指那些具有海量参数和强大计算能力的深度学习模型。这些模型通常用于处理复杂的自然语言处理(NLP)、计算机视觉、语音识别等任务。以下是关于AI大模型的一些详细介绍:

1. **定义**- AI大模型是指具有数十亿甚至数千亿参数的深度学习模型。
   - 这些模型通常采用大规模数据集进行训练,以实现高水平的性能。

2. **应用领域**- **自然语言处理**:如机器翻译、文本摘要、问答系统等。
   - **计算机视觉**:如图像识别、目标检测、图像生成等。
   - **语音识别**:如语音转文字、语音合成等。
   - **推荐系统**:如个性化推荐、商品推荐等。

3. **特点**- **强大的计算能力**:AI大模型需要大量的计算资源进行训练和推理。
   - **海量参数**:这些模型通常具有数十亿甚至数千亿个参数,能够捕捉到数据中的复杂模式。
   - **自学习能力**:AI大模型能够从大量数据中自动学习,无需人工干预。
   - **泛化能力**:这些模型在训练数据之外的未知数据上也能表现出良好的性能。

4. **挑战**- **计算资源**:训练和推理AI大模型需要大量的计算资源,如GPU、TPU等。
   - **数据隐私**:AI大模型通常需要大量数据来训练,这可能会引发数据隐私问题。
   - **模型可解释性**:AI大模型的决策过程往往难以解释,这可能会影响其在实际应用中的可信度。

5. **代表性模型**- **Transformer**:一种基于自注意力机制的深度学习模型,广泛应用于NLP任务。
   - **BERT**(Bidirectional Encoder Representations from Transformers):一种基于Transformer的预训练语言模型,在NLP任务中取得了显著成果。
   - **GPT**(Generative Pre-trained Transformer):一种基于Transformer的生成式语言模型,能够生成高质量的文本。

总之,AI大模型是人工智能领域的一个重要研究方向,具有广泛的应用前景。随着技术的不断发展,AI大模型将在更多领域发挥重要作用。

七、OpenAI API服务测试

1、修改模型地址

cd GLM-4/basic_demo/
编辑openai_api_server.py文件,修改模型地址,更改为本地的模型地址目录
在这里插入图片描述

openai_api_server.py 完整代码如下;

import time
from asyncio.log import logger
import re
import uvicorn
import gc
import json
import torch
import random
import string

from vllm import SamplingParams, AsyncEngineArgs, AsyncLLMEngine
from fastapi import FastAPI, HTTPException, Response
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from typing import List, Literal, Optional, Union
from pydantic import BaseModel, Field
from transformers import AutoTokenizer, LogitsProcessor
from sse_starlette.sse import EventSourceResponse

EventSourceResponse.DEFAULT_PING_INTERVAL = 1000
import os

MODEL_PATH = os.environ.get('MODEL_PATH', '/root/autodl-tmp/ZhipuAI/glm-4-9b-chat')
MAX_MODEL_LENGTH = 8192


@asynccontextmanager
async def lifespan(app: FastAPI):
    yield
    if torch.cuda.is_available():
        torch.cuda.empty_cache()
        torch.cuda.ipc_collect()


app = FastAPI(lifespan=lifespan)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


def generate_id(prefix: str, k=29) -> str:
    suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=k))
    return f"{prefix}{suffix}"


class ModelCard(BaseModel):
    id: str = ""
    object: str = "model"
    created: int = Field(default_factory=lambda: int(time.time()))
    owned_by: str = "owner"
    root: Optional[str] = None
    parent: Optional[str] = None
    permission: Optional[list] = None


class ModelList(BaseModel):
    object: str = "list"
    data: List[ModelCard] = ["glm-4"]


class FunctionCall(BaseModel):
    name: Optional[str] = None
    arguments: Optional[str] = None


class ChoiceDeltaToolCallFunction(BaseModel):
    name: Optional[str] = None
    arguments: Optional[str] = None


class UsageInfo(BaseModel):
    prompt_tokens: int = 0
    total_tokens: int = 0
    completion_tokens: Optional[int] = 0


class ChatCompletionMessageToolCall(BaseModel):
    index: Optional[int] = 0
    id: Optional[str] = None
    function: FunctionCall
    type: Optional[Literal["function"]] = 'function'


class ChatMessage(BaseModel):
    # “function” 字段解释:
    # 使用较老的OpenAI API版本需要注意在这里添加 function 字段并在 process_messages函数中添加相应角色转换逻辑为 observation

    role: Literal["user", "assistant", "system", "tool"]
    content: Optional[str] = None
    function_call: Optional[ChoiceDeltaToolCallFunction] = None
    tool_calls: Optional[List[ChatCompletionMessageToolCall]] = None


class DeltaMessage(BaseModel):
    role: Optional[Literal["user", "assistant", "system"]] = None
    content: Optional[str] = None
    function_call: Optional[ChoiceDeltaToolCallFunction] = None
    tool_calls: Optional[List[ChatCompletionMessageToolCall]] = None


class ChatCompletionResponseChoice(BaseModel):
    index: int
    message: ChatMessage
    finish_reason: Literal["stop", "length", "tool_calls"]


class ChatCompletionResponseStreamChoice(BaseModel):
    delta: DeltaMessage
    finish_reason: Optional[Literal["stop", "length", "tool_calls"]]
    index: int


class ChatCompletionResponse(BaseModel):
    model: str
    id: Optional[str] = Field(default_factory=lambda: generate_id('chatcmpl-', 29))
    object: Literal["chat.completion", "chat.completion.chunk"]
    choices: List[Union[ChatCompletionResponseChoice, ChatCompletionResponseStreamChoice]]
    created: Optional[int] = Field(default_factory=lambda: int(time.time()))
    system_fingerprint: Optional[str] = Field(default_factory=lambda: generate_id('fp_', 9))
    usage: Optional[UsageInfo] = None


class ChatCompletionRequest(BaseModel):
    model: str
    messages: List[ChatMessage]
    temperature: Optional[float] = 0.8
    top_p: Optional[float] = 0.8
    max_tokens: Optional[int] = None
    stream: Optional[bool] = False
    tools: Optional[Union[dict, List[dict]]] = None
    tool_choice: Optional[Union[str, dict]] = None
    repetition_penalty: Optional[float] = 1.1


class InvalidScoreLogitsProcessor(LogitsProcessor):
    def __call__(
            self, input_ids: torch.LongTensor, scores: torch.FloatTensor
    ) -> torch.FloatTensor:
        if torch.isnan(scores).any() or torch.isinf(scores).any():
            scores.zero_()
            scores[..., 5] = 5e4
        return scores


def process_response(output: str, tools: dict | List[dict] = None, use_tool: bool = False) -> Union[str, dict]:
    lines = output.strip().split("\n")
    arguments_json = None
    special_tools = ["cogview", "simple_browser"]
    tools = {tool['function']['name'] for tool in tools} if tools else {}

    # 这是一个简单的工具比较函数,不能保证拦截所有非工具输出的结果,比如参数未对齐等特殊情况。
    ##TODO 如果你希望做更多判断,可以在这里进行逻辑完善。

    if len(lines) >= 2 and lines[1].startswith("{"):
        function_name = lines[0].strip()
        arguments = "\n".join(lines[1:]).strip()
        if function_name in tools or function_name in special_tools:
            try:
                arguments_json = json.loads(arguments)
                is_tool_call = True
            except json.JSONDecodeError:
                is_tool_call = function_name in special_tools

            if is_tool_call and use_tool:
                content = {
                    "name": function_name,
                    "arguments": json.dumps(arguments_json if isinstance(arguments_json, dict) else arguments,
                                            ensure_ascii=False)
                }
                if function_name == "simple_browser":
                    search_pattern = re.compile(r'search\("(.+?)"\s*,\s*recency_days\s*=\s*(\d+)\)')
                    match = search_pattern.match(arguments)
                    if match:
                        content["arguments"] = json.dumps({
                            "query": match.group(1),
                            "recency_days": int(match.group(2))
                        }, ensure_ascii=False)
                elif function_name == "cogview":
                    content["arguments"] = json.dumps({
                        "prompt": arguments
                    }, ensure_ascii=False)

                return content
    return output.strip()


@torch.inference_mode()
async def generate_stream_glm4(params):
    messages = params["messages"]
    tools = params["tools"]
    tool_choice = params["tool_choice"]
    temperature = float(params.get("temperature", 1.0))
    repetition_penalty = float(params.get("repetition_penalty", 1.0))
    top_p = float(params.get("top_p", 1.0))
    max_new_tokens = int(params.get("max_tokens", 8192))

    messages = process_messages(messages, tools=tools, tool_choice=tool_choice)
    inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
    params_dict = {
        "n": 1,
        "best_of": 1,
        "presence_penalty": 1.0,
        "frequency_penalty": 0.0,
        "temperature": temperature,
        "top_p": top_p,
        "top_k": -1,
        "repetition_penalty": repetition_penalty,
        "use_beam_search": False,
        "length_penalty": 1,
        "early_stopping": False,
        "stop_token_ids": [151329, 151336, 151338],
        "ignore_eos": False,
        "max_tokens": max_new_tokens,
        "logprobs": None,
        "prompt_logprobs": None,
        "skip_special_tokens": True,
    }
    sampling_params = SamplingParams(**params_dict)
    async for output in engine.generate(inputs=inputs, sampling_params=sampling_params, request_id=f"{time.time()}"):
        output_len = len(output.outputs[0].token_ids)
        input_len = len(output.prompt_token_ids)
        ret = {
            "text": output.outputs[0].text,
            "usage": {
                "prompt_tokens": input_len,
                "completion_tokens": output_len,
                "total_tokens": output_len + input_len
            },
            "finish_reason": output.outputs[0].finish_reason,
        }
        yield ret
    gc.collect()
    torch.cuda.empty_cache()


def process_messages(messages, tools=None, tool_choice="none"):
    _messages = messages
    processed_messages = []
    msg_has_sys = False

    def filter_tools(tool_choice, tools):
        function_name = tool_choice.get('function', {}).get('name', None)
        if not function_name:
            return []
        filtered_tools = [
            tool for tool in tools
            if tool.get('function', {}).get('name') == function_name
        ]
        return filtered_tools

    if tool_choice != "none":
        if isinstance(tool_choice, dict):
            tools = filter_tools(tool_choice, tools)
        if tools:
            processed_messages.append(
                {
                    "role": "system",
                    "content": None,
                    "tools": tools
                }
            )
            msg_has_sys = True

    if isinstance(tool_choice, dict) and tools:
        processed_messages.append(
            {
                "role": "assistant",
                "metadata": tool_choice["function"]["name"],
                "content": ""
            }
        )

    for m in _messages:
        role, content, func_call = m.role, m.content, m.function_call
        tool_calls = getattr(m, 'tool_calls', None)

        if role == "function":
            processed_messages.append(
                {
                    "role": "observation",
                    "content": content
                }
            )
        elif role == "tool":
            processed_messages.append(
                {
                    "role": "observation",
                    "content": content,
                    "function_call": True
                }
            )
        elif role == "assistant":
            if tool_calls:
                for tool_call in tool_calls:
                    processed_messages.append(
                        {
                            "role": "assistant",
                            "metadata": tool_call.function.name,
                            "content": tool_call.function.arguments
                        }
                    )
            else:
                for response in content.split("\n"):
                    if "\n" in response:
                        metadata, sub_content = response.split("\n", maxsplit=1)
                    else:
                        metadata, sub_content = "", response
                    processed_messages.append(
                        {
                            "role": role,
                            "metadata": metadata,
                            "content": sub_content.strip()
                        }
                    )
        else:
            if role == "system" and msg_has_sys:
                msg_has_sys = False
                continue
            processed_messages.append({"role": role, "content": content})

    if not tools or tool_choice == "none":
        for m in _messages:
            if m.role == 'system':
                processed_messages.insert(0, {"role": m.role, "content": m.content})
                break
    return processed_messages


@app.get("/health")
async def health() -> Response:
    """Health check."""
    return Response(status_code=200)


@app.get("/v1/models", response_model=ModelList)
async def list_models():
    model_card = ModelCard(id="glm-4")
    return ModelList(data=[model_card])


@app.post("/v1/chat/completions", response_model=ChatCompletionResponse)
async def create_chat_completion(request: ChatCompletionRequest):
    if len(request.messages) < 1 or request.messages[-1].role == "assistant":
        raise HTTPException(status_code=400, detail="Invalid request")

    gen_params = dict(
        messages=request.messages,
        temperature=request.temperature,
        top_p=request.top_p,
        max_tokens=request.max_tokens or 1024,
        echo=False,
        stream=request.stream,
        repetition_penalty=request.repetition_penalty,
        tools=request.tools,
        tool_choice=request.tool_choice,
    )
    logger.debug(f"==== request ====\n{gen_params}")

    if request.stream:
        predict_stream_generator = predict_stream(request.model, gen_params)
        output = await anext(predict_stream_generator)
        if output:
            return EventSourceResponse(predict_stream_generator, media_type="text/event-stream")
        logger.debug(f"First result output:\n{output}")

        function_call = None
        if output and request.tools:
            try:
                function_call = process_response(output, request.tools, use_tool=True)
            except:
                logger.warning("Failed to parse tool call")

        if isinstance(function_call, dict):
            function_call = ChoiceDeltaToolCallFunction(**function_call)
            generate = parse_output_text(request.model, output, function_call=function_call)
            return EventSourceResponse(generate, media_type="text/event-stream")
        else:
            return EventSourceResponse(predict_stream_generator, media_type="text/event-stream")
    response = ""
    async for response in generate_stream_glm4(gen_params):
        pass

    if response["text"].startswith("\n"):
        response["text"] = response["text"][1:]
    response["text"] = response["text"].strip()

    usage = UsageInfo()

    function_call, finish_reason = None, "stop"
    tool_calls = None
    if request.tools:
        try:
            function_call = process_response(response["text"], request.tools, use_tool=True)
        except Exception as e:
            logger.warning(f"Failed to parse tool call: {e}")
    if isinstance(function_call, dict):
        finish_reason = "tool_calls"
        function_call_response = ChoiceDeltaToolCallFunction(**function_call)
        function_call_instance = FunctionCall(
            name=function_call_response.name,
            arguments=function_call_response.arguments
        )
        tool_calls = [
            ChatCompletionMessageToolCall(
                id=generate_id('call_', 24),
                function=function_call_instance,
                type="function")]

    message = ChatMessage(
        role="assistant",
        content=None if tool_calls else response["text"],
        function_call=None,
        tool_calls=tool_calls,
    )

    logger.debug(f"==== message ====\n{message}")

    choice_data = ChatCompletionResponseChoice(
        index=0,
        message=message,
        finish_reason=finish_reason,
    )
    task_usage = UsageInfo.model_validate(response["usage"])
    for usage_key, usage_value in task_usage.model_dump().items():
        setattr(usage, usage_key, getattr(usage, usage_key) + usage_value)

    return ChatCompletionResponse(
        model=request.model,
        choices=[choice_data],
        object="chat.completion",
        usage=usage
    )


async def predict_stream(model_id, gen_params):
    output = ""
    is_function_call = False
    has_send_first_chunk = False
    created_time = int(time.time())
    function_name = None
    response_id = generate_id('chatcmpl-', 29)
    system_fingerprint = generate_id('fp_', 9)
    tools = {tool['function']['name'] for tool in gen_params['tools']} if gen_params['tools'] else {}
    delta_text = ""
    async for new_response in generate_stream_glm4(gen_params):
        decoded_unicode = new_response["text"]
        delta_text += decoded_unicode[len(output):]
        output = decoded_unicode
        lines = output.strip().split("\n")

        # 检查是否为工具
        # 这是一个简单的工具比较函数,不能保证拦截所有非工具输出的结果,比如参数未对齐等特殊情况。
        ##TODO 如果你希望做更多处理,可以在这里进行逻辑完善。

        if not is_function_call and len(lines) >= 2:
            first_line = lines[0].strip()
            if first_line in tools:
                is_function_call = True
                function_name = first_line
                delta_text = lines[1]

        # 工具调用返回
        if is_function_call:
            if not has_send_first_chunk:
                function_call = {"name": function_name, "arguments": ""}
                tool_call = ChatCompletionMessageToolCall(
                    index=0,
                    id=generate_id('call_', 24),
                    function=FunctionCall(**function_call),
                    type="function"
                )
                message = DeltaMessage(
                    content=None,
                    role="assistant",
                    function_call=None,
                    tool_calls=[tool_call]
                )
                choice_data = ChatCompletionResponseStreamChoice(
                    index=0,
                    delta=message,
                    finish_reason=None
                )
                chunk = ChatCompletionResponse(
                    model=model_id,
                    id=response_id,
                    choices=[choice_data],
                    created=created_time,
                    system_fingerprint=system_fingerprint,
                    object="chat.completion.chunk"
                )
                yield ""
                yield chunk.model_dump_json(exclude_unset=True)
                has_send_first_chunk = True

            function_call = {"name": None, "arguments": delta_text}
            delta_text = ""
            tool_call = ChatCompletionMessageToolCall(
                index=0,
                id=None,
                function=FunctionCall(**function_call),
                type="function"
            )
            message = DeltaMessage(
                content=None,
                role=None,
                function_call=None,
                tool_calls=[tool_call]
            )
            choice_data = ChatCompletionResponseStreamChoice(
                index=0,
                delta=message,
                finish_reason=None
            )
            chunk = ChatCompletionResponse(
                model=model_id,
                id=response_id,
                choices=[choice_data],
                created=created_time,
                system_fingerprint=system_fingerprint,
                object="chat.completion.chunk"
            )
            yield chunk.model_dump_json(exclude_unset=True)

        # 用户请求了 Function Call 但是框架还没确定是否为Function Call
        elif (gen_params["tools"] and gen_params["tool_choice"] != "none") or is_function_call:
            continue

        # 常规返回
        else:
            finish_reason = new_response.get("finish_reason", None)
            if not has_send_first_chunk:
                message = DeltaMessage(
                    content="",
                    role="assistant",
                    function_call=None,
                )
                choice_data = ChatCompletionResponseStreamChoice(
                    index=0,
                    delta=message,
                    finish_reason=finish_reason
                )
                chunk = ChatCompletionResponse(
                    model=model_id,
                    id=response_id,
                    choices=[choice_data],
                    created=created_time,
                    system_fingerprint=system_fingerprint,
                    object="chat.completion.chunk"
                )
                yield chunk.model_dump_json(exclude_unset=True)
                has_send_first_chunk = True

            message = DeltaMessage(
                content=delta_text,
                role="assistant",
                function_call=None,
            )
            delta_text = ""
            choice_data = ChatCompletionResponseStreamChoice(
                index=0,
                delta=message,
                finish_reason=finish_reason
            )
            chunk = ChatCompletionResponse(
                model=model_id,
                id=response_id,
                choices=[choice_data],
                created=created_time,
                system_fingerprint=system_fingerprint,
                object="chat.completion.chunk"
            )
            yield chunk.model_dump_json(exclude_unset=True)

    # 工具调用需要额外返回一个字段以对齐 OpenAI 接口
    if is_function_call:
        yield ChatCompletionResponse(
            model=model_id,
            id=response_id,
            system_fingerprint=system_fingerprint,
            choices=[
                ChatCompletionResponseStreamChoice(
                    index=0,
                    delta=DeltaMessage(
                        content=None,
                        role=None,
                        function_call=None,
                    ),
                    finish_reason="tool_calls"
                )],
            created=created_time,
            object="chat.completion.chunk",
            usage=None
        ).model_dump_json(exclude_unset=True)
    elif delta_text != "":
        message = DeltaMessage(
            content="",
            role="assistant",
            function_call=None,
        )
        choice_data = ChatCompletionResponseStreamChoice(
            index=0,
            delta=message,
            finish_reason=None
        )
        chunk = ChatCompletionResponse(
            model=model_id,
            id=response_id,
            choices=[choice_data],
            created=created_time,
            system_fingerprint=system_fingerprint,
            object="chat.completion.chunk"
        )
        yield chunk.model_dump_json(exclude_unset=True)
    
        finish_reason = 'stop'
        message = DeltaMessage(
            content=delta_text,
            role="assistant",
            function_call=None,
        )
        delta_text = ""
        choice_data = ChatCompletionResponseStreamChoice(
            index=0,
            delta=message,
            finish_reason=finish_reason
        )
        chunk = ChatCompletionResponse(
            model=model_id,
            id=response_id,
            choices=[choice_data],
            created=created_time,
            system_fingerprint=system_fingerprint,
            object="chat.completion.chunk"
        )
        yield chunk.model_dump_json(exclude_unset=True)
        yield '[DONE]'
    else:
        yield '[DONE]'

async def parse_output_text(model_id: str, value: str, function_call: ChoiceDeltaToolCallFunction = None):
    delta = DeltaMessage(role="assistant", content=value)
    if function_call is not None:
        delta.function_call = function_call

    choice_data = ChatCompletionResponseStreamChoice(
        index=0,
        delta=delta,
        finish_reason=None
    )
    chunk = ChatCompletionResponse(
        model=model_id,
        choices=[choice_data],
        object="chat.completion.chunk"
    )
    yield "{}".format(chunk.model_dump_json(exclude_unset=True))
    yield '[DONE]'


if __name__ == "__main__":
    tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
    engine_args = AsyncEngineArgs(
        model=MODEL_PATH,
        tokenizer=MODEL_PATH,
        # 如果你有多张显卡,可以在这里设置成你的显卡数量
        tensor_parallel_size=1,
        dtype="bfloat16",
        trust_remote_code=True,
        # 占用显存的比例,请根据你的显卡显存大小设置合适的值,例如,如果你的显卡有80G,您只想使用24G,请按照24/80=0.3设置
        gpu_memory_utilization=0.9,
        enforce_eager=True,
        worker_use_ray=False,
        engine_use_ray=False,
        disable_log_requests=True,
        max_model_len=MAX_MODEL_LENGTH,
    )
    engine = AsyncLLMEngine.from_engine_args(engine_args)
    uvicorn.run(app, host='0.0.0.0', port=8000, workers=1)

2、发布API服务

python openai_api_server.py 

启动成功如下:(端口8000)
在这里插入图片描述

3、调用API服务

也可以使用python中的requests库进行调用,如下所示:

from openai import OpenAI

base_url = "http://127.0.0.1:8000/v1/"
client = OpenAI(api_key="EMPTY", base_url=base_url)

messages = [
    {"role": "system","content": "你是一个AI智能助手!"},
    { "role": "user","content": "你是谁"}
]
response = client.chat.completions.create(
    model="glm-4",
    messages=messages,
    stream=False,
    max_tokens=256,
    temperature=0.4,
    presence_penalty=1.2,
    top_p=0.8,
)
response

调用结果如下图所示:

ChatCompletion(id='chatcmpl-AJ56zgVTbBQb47Seewbva8Ye3mmP6', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='我是一个名为 ChatGLM 的人工智能助手,是基于清华大学 KEG 实验室和智谱 AI 公司于2024共同训练的语言模型开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。', role='assistant', function_call=None, tool_calls=None))], created=1722405795, model='glm-4', object='chat.completion', service_tier=None, system_fingerprint='fp_hawbL1lgv', usage=CompletionUsage(completion_tokens=45, prompt_tokens=25, total_tokens=70))

结语

通过本文的实践,我们成功地在本地部署了GLM-4-9B-Chat模型,并提供了一个高效的OpenAI风格的API服务。这个过程不仅展示了如何从零开始搭建一个模型部署环境,还涉及到了代码编写、服务启动和API调用等多个关键步骤。希望读者能够通过本教程,掌握深度学习模型部署的核心技术,并能够灵活应用到自己的项目中。

在这里插入图片描述

🎯🔖更多专栏系列文章:AI大模型提示工程完全指南AI大模型探索之路(零基础入门)AI大模型预训练微调进阶AI大模型开源精选实践AI大模型RAG应用探索实践🔥🔥🔥 其他专栏可以查看博客主页📑

😎 作者介绍:我是寻道AI小兵,资深程序老猿,从业10年+、互联网系统架构师,目前专注于AIGC的探索。
📖 技术交流:欢迎关注【小兵的AI视界】公众号或扫描下方👇二维码,加入技术交流群,开启编程探索之旅。
💘精心准备📚500本编程经典书籍、💎AI专业教程,以及高效AI工具。等你加入,与我们一同成长,共铸辉煌未来。
如果文章内容对您有所触动,别忘了点赞、⭐关注,收藏!加入我,让我们携手同行AI的探索之旅,一起开启智能时代的大门!

  • 129
    点赞
  • 125
    收藏
    觉得还不错? 一键收藏
  • 70
    评论
评论 70
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值