ChatGLM本地部署API化:显存、流式输出与并发实战

发布时间:2026/10/25 4:04:16
ChatGLM本地部署API化:显存、流式输出与并发实战 本地把模型跑起来的那一刻通常是最爽的python demo.py一敲终端里蹦出第一句回复感觉这事儿成了。但接下来往往是另一回事产品同事要接、前端要联调、测试要压并发、业务方要按接口计费口径对数——你不可能让每个人都装一遍 CUDA、装一遍 PyTorch、再背一遍模型路径。这时候真正要做的事情就浮出水面了对本地部署的 ChatGLM 模型进行 API 调用也就是把那个躺在显卡里的模型包装成一个标准的、能在内网被任意语言调用的 HTTP 服务。这篇内容我围绕这条主线讲透从为什么值得包一层、显存账怎么算、服务怎么起、流式怎么接、并发怎么扛到那些只有真跑过才会遇到的坑。不管你是刚把模型跑通想往前一步还是已经在写服务但被 OOM 和断流折磨过下面这些应该都能直接抄。1. 包一层 API 之前先想清楚要解决什么问题1.1 直连模型脚本的三个真实痛点大部分人的起点是一个demo.py加载 tokenizer、加载 model、写个 while 循环读 input、调model.chat()打印结果。这个形态在验证阶段非常合适但只要往生产靠近一步立刻会撞上三堵墙。第一堵墙是进程模型不对。模型加载一次要几十秒甚至几分钟占掉十几 GB 显存而你的业务请求是零零散散来的。如果每个请求都重新加载模型等于每次都在做冷启动如果用一个长驻进程读 stdin那这个进程和业务系统之间就没有任何标准协议只能靠管道硬怼。第二堵墙是并发语义缺失。单进程单请求第二个人点一下按钮只能排队等第一个人把 500 个 token 吐完。第三堵墙是客户端绑定。前端是 JS、数据侧是 Java、脚本侧是 Python你不能要求所有调用方都装 transformers。所以包 API 这件事本质上是把模型推理从一个脚本变成一个服务服务持有模型常驻显存对外暴露 HTTP 接口请求过来排队进显存结果按协议返回。它解决的不是模型能不能跑而是模型能不能被别人用。1.2 三种调用形态的取舍直调、WebUI、自建 API同样是对外提供能力常见有三条路选错了后面返工成本很高。我把它们的差异摊开说。形态典型做法上手成本可控性适用阶段脚本直调transformers model.chat()极低高但没法共享单人验证、离线批处理现成 WebUI官方或社区的对话界面低中业务逻辑难插入内部演示、非技术人员试用自建 API 服务FastAPI/Flask 包一层走 HTTP中最高可加鉴权、日志、限流业务集成、多端复用OpenAI 兼容层在自建服务上加/v1/chat/completions中高且生态工具直接复用想低成本迁移已有调用方我的建议很直接如果你只是自己玩脚本直调足够只要有一个外部调用方就上自建 API如果调用方已经在用 OpenAI 风格的 SDK那就在自建 API 上再叠一层兼容协议。兼容层这一步的性价比极高因为大量现成的客户端库、评测脚本、Agent 框架默认都按/v1/chat/completions说话你把这层壳补上等于零成本接入整个生态。代价是要处理一些字段语义的细微差别下面会专门讲。1.3 选型背后的两个关键决定第一个决定是框架用 FastAPI 还是 Flask。我倾向 FastAPI原因不是它更时髦而是三件具体的事一是原生async流式输出SSE写起来自然不用手搓生成器挂载二是自带 Pydantic 校验请求体字段类型错了直接 422省掉一堆手写 if三是自动生成接口文档联调时前端自己去看能省掉你一半的沟通时间。Flask 当然也能做同步模型下差异不大但流式那块会别扭一些。第二个决定是推理后端用原生 transformers 还是更快的方案。原生 transformers 的好处是稳、依赖少、跟模型仓库的示例代码一致出了问题好查坏处是吞吐一般尤其是没做批处理的时候。如果你只是内部工具、QPS 个位数原生完全够用别过早优化。真到了需要扛并发再考虑换 vLLM 之类的推理引擎但那意味着量化格式、模型结构支持、接口形态都要重新对一遍属于另一条工程路线。2. 动手之前先把显存和依赖这两笔账算清楚2.1 依赖清单与那些版本不对就报错的坑先给一份能跑通的最小依赖集合我按重要性排# 基础推理 pip install torch --index-url https://download.pytorch.org/whl/cu121 pip install transformers accelerate sentencepiece cpm_kernels # 服务层 pip install fastapi uvicorn pydantic sse-starlette # 客户端测试 pip install requests openai这里有几个坑值得单独拎出来说。transformers的版本要跟模型对得上太老的版本不认识新的模型结构症状是加载时报一堆KeyError或者干脆把权重当随机初始化输出全是乱码太新的版本有时候又会改动默认的attn_implementation在你那张老卡上跑出莫名其妙的报错。我的做法是先在干净环境里按模型仓库 README 装一遍、跑通demo.py把这个版本组合记下来再往服务环境里搬。accelerate不是可选项device_mapauto靠它工作不装的话多卡和 CPU offload 都不会生效。还有一个容易忽略的torch的 CUDA 版本要和驱动匹配。nvidia-smi右上角显示的 CUDA Version 是驱动支持的上限装超过它的运行时会直接CUDA error: no kernel image is available。这个报错看着像硬件坏了其实只是版本错配。注意不要在同一个环境里同时装 CPU 版和 GPU 版 torchpip 会保留后装的那个但残留的库文件会导致一些诡异的undefined symbol错误。发现不对就干净重装。2.2 显存怎么估权重 KV Cache 中间激活显存估算这件事网上一堆6B 大概 13G的说法但它没说清是在什么上下文长度、什么批大小下。我把账拆成三块你自己代入参数算比背结论有用。第一块是权重公式很朴素参数量 × 每参数字节数。fp16 是 2 字节int8 是 1 字节int4 是 0.5 字节左右实际有量化元数据开销略高于理论值。第二块是 KV Cache这是很多人算漏的部分也是长上下文下爆显存的主因。估算式KV Cache ≈ 2 × 层数 × KV头数 × 头维度 × 序列长度 × 批大小 × 字节数以 6B 级别、28 层、GQA 2 组、头维度 128 的模型为例单 token 的 KV 占用约 2×28×2×128×2 ≈ 28KB跑到 8K 上下文、batch 4就是 28KB × 8192 × 4 ≈ 900MB。看着不大但如果你的模型没做 GQA每层头数等于 KV 头数这个数字会翻十几倍8K 上下文单条就能吃掉好几 GB。第三块是中间激活和框架开销这块最没法定量我的经验值是留 1.52GB 的余量长上下文时留 3GB。合起来给一张常见档位的参考表单卡、8K 上下文、batch 1模型规模精度权重大致占用加上 KV 与开销落地的卡6Bfp161213GB1416GB24GB 卡舒适6Bint86.57.5GB911GB16GB 卡可跑6Bint445GB68GB12GB 卡勉强9Bfp161819GB2124GB24GB 卡吃紧9Bint4910GB1214GB16GB 卡可跑这张表只能当起点用。真正上线前一定要做一件事用你的真实最大上下文和最大并发去压一遍看nvidia-smi的峰值而不是看空闲时的占用。空闲占用好看不代表扛得住KV Cache 是随请求长出来的。2.3 模型目录结构和加载参数怎么定模型下载下来一般是一个目录里面有config.json、tokenizer.model或tokenizer.json、若干.safetensors或.bin分片。加载时我习惯这样写import torch from transformers import AutoTokenizer, AutoModel MODEL_PATH /data/models/chatglm-local tokenizer AutoTokenizer.from_pretrained( MODEL_PATH, trust_remote_codeTrue, ) model AutoModel.from_pretrained( MODEL_PATH, trust_remote_codeTrue, torch_dtypetorch.float16, device_mapauto, low_cpu_mem_usageTrue, ).eval()几个参数值得解释。trust_remote_codeTrue是因为这类模型仓库里带了自定义的建模文件不开这个开关加载器会拒绝执行。安全上要留意这个开关等于允许执行仓库里的 Python 代码所以模型来源必须是可信渠道别随手从网上拖一个来路不明的权重包。torch_dtype显式指定成 fp16避免某些环境下默认按 fp32 加载显存直接翻倍。low_cpu_mem_usageTrue让加载过程分片进行能把加载时的内存峰值压低不少机器内存紧张时是救命参数。.eval()别忘虽然推理时影响不大但 dropout 没关会带来输出不稳定。3. 从零搭一个能用的 API 服务3.1 最小可用版本一个文件跑起来先上一个能跑的最小版本把它当作骨架后面所有扩展都往这个骨架上长。# server.py import time import uuid from typing import List, Optional import torch from fastapi import FastAPI from pydantic import BaseModel, Field from transformers import AutoTokenizer, AutoModel MODEL_PATH /data/models/chatglm-local app FastAPI(titlelocal-llm-api) tokenizer AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_codeTrue) model AutoModel.from_pretrained( MODEL_PATH, trust_remote_codeTrue, torch_dtypetorch.float16, device_mapauto, ).eval() class Message(BaseModel): role: str content: str class ChatRequest(BaseModel): messages: List[Message] temperature: float Field(0.8, ge0.0, le1.5) top_p: float Field(0.8, gt0.0, le1.0) max_new_tokens: int Field(512, ge1, le4096) app.post(/v1/chat) def chat(req: ChatRequest): history [] # 最后一条作为当前提问前面的作为多轮历史 for m in req.messages[:-1]: history.append((m.content, )) # 简化处理见 3.2 query req.messages[-1].content t0 time.time() response, new_history model.chat( tokenizer, query, historyhistory, temperaturereq.temperature, top_preq.top_p, max_lengthreq.max_new_tokens, ) return { id: fchatcmpl-{uuid.uuid4().hex[:12]}, content: response, latency_ms: int((time.time() - t0) * 1000), } if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)这里有个关键点必须说清楚uvicorn.run(app, ...)里workers千万别设成大于 1。多个 worker 就是多个进程每个进程都会独立加载一份模型显存直接乘倍数第二份直接 OOM。想提高并发只能靠服务内部排队、批处理或者上多卡绝不是加 worker。启动python server.py # 或者用命令行形式方便挂后台 uvicorn server:app --host 0.0.0.0 --port 8000 --timeout-keep-alive 75--timeout-keep-alive这个参数在流式场景下很重要默认 5 秒长时间不吐字的推理会被中间层掐断症状是客户端收到半截回复就断了。3.2 多轮对话的拼接history 到底怎么传这是新手最容易做错的地方。模型的chat()接口吃的是history列表元素是(用户问, 模型答)的元组它不会自动帮你从 messages 数组里还原。如果你按 OpenAI 的 messages 格式一把塞进去最常见的结果是模型只看到最后一句话前面几轮全丢回复牛头不对马嘴。正确做法是把 messages 按对话顺序配对def messages_to_history(messages: List[Message]): history [] pending_user None for m in messages: if m.role user: if pending_user is not None: # 上一轮用户话没被回答补一个空回答避免错位 history.append((pending_user, )) pending_user m.content elif m.role assistant and pending_user is not None: history.append((pending_user, m.content)) pending_user None return history, pending_user or 如果你用的是带apply_chat_template的模型版本走另一条路更稳inputs tokenizer.apply_chat_template( [{role: m.role, content: m.content} for m in messages], add_generation_promptTrue, return_tensorspt, ).to(model.device) outputs model.generate( inputs, max_new_tokensreq.max_new_tokens, temperaturereq.temperature, top_preq.top_p, do_samplereq.temperature 0, repetition_penalty1.05, ) reply tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokensTrue)注意outputs[0][inputs.shape[1]:]这个切片不加的话你会把输入的 prompt 原样带回来前端一看输出开头是自己发的问题很尴尬。另外do_sample要跟 temperature 联动temperature 为 0 时应该关采样走贪心否则某些版本会报错或者输出退化。历史该由谁维护有一个架构上的选择。我倾向让服务端无状态客户端每次把完整 messages 传上来服务端只负责转换成 history 跑一次推理。这样服务可以水平扩展虽然模型实例不能扩但路由层可以也不会因为服务重启把用户的上下文弄丢。代价是每次请求都重复计算历史部分的 KV长对话下延迟会明显上涨。真要做性能优化再引入 prefix caching 或者会话级 KV 缓存那是后话别一开始就把架构做复杂。3.3 流式输出SSE 怎么接才不踩坑非流式接口在生成 500 字时用户盯着转圈要等十几秒体验很差。流式输出是刚需而其中最省事的方案是 SSEServer-Sent Events本质就是一个长连接的text/event-stream响应一行行往下推。from fastapi.responses import StreamingResponse import json def sse_pack(data: dict) - str: return fdata: {json.dumps(data, ensure_asciiFalse)}\n\n app.post(/v1/chat/stream) def chat_stream(req: ChatRequest): history, query messages_to_history(req.messages) def gen(): yield sse_pack({event: start}) for chunk, _ in model.stream_chat( tokenizer, query, historyhistory, temperaturereq.temperature, top_preq.top_p, max_lengthreq.max_new_tokens, ): yield sse_pack({event: delta, text: chunk}) yield sse_pack({event: done}) yield data: [DONE]\n\n return StreamingResponse(gen(), media_typetext/event-stream)stream_chat返回的是(累积文本, 本次新增)的迭代器它每次给的是到目前为止的完整文本不是增量片段。所以前端要展示增量得自己算差集或者你在服务端就把它切成增量再推。我一般选后者服务端算 diff 更省前端心智prev for full_text, _ in model.stream_chat(...): delta full_text[len(prev):] prev full_text if delta: yield sse_pack({event: delta, text: delta})流式有三个具体的坑。第一是中间层缓冲某些网关会攒够一定字节才转发导致你这边一秒一个字对方那边十秒一坨。解决思路是在响应头里加X-Accel-Buffering: no以及确认media_type里带了charsetutf-8。第二是客户端超时requests 默认没有读超时但很多 HTTP 客户端有默认 30 秒长回答会被砍。第三是断连处理用户中途关页面服务端还在傻算必须有机制感知到客户端断开并终止生成否则显存一直被占着。提示SSE 是单向的客户端没法中途发消息给服务端。想要停止生成按钮得另开一个POST /v1/chat/cancel接口用请求 id 去标记取消状态生成循环里检查这个标记。3.4 加一层 OpenAI 兼容接口的真实价值到这一步你的服务能用了但调用方还是要按你的自定义格式写代码。如果叠一层/v1/chat/completions情况就不一样了现成的 SDK、评测脚本、Agent 框架、甚至一些低代码平台改个base_url就能指过来。核心是把字段对齐。请求侧主要映射这几项messages直接透传temperature、top_p、max_tokens注意 OpenAI 叫max_tokens很多本地实现叫max_new_tokens别混、stream。响应侧结构要贴近{ id: chatcmpl-abc123, object: chat.completion, created: 1710000000, model: local-chatglm, choices: [ { index: 0, message: {role: assistant, content: ...}, finish_reason: stop } ], usage: {prompt_tokens: 32, completion_tokens: 128, total_tokens: 160} }流式的话每个 chunk 长这样最后一个是data: [DONE]{id:chatcmpl-abc123,object:chat.completion.chunk,choices:[{index:0,delta:{content:你},finish_reason:null}]}usage字段很多人懒得填但只要有计费或者统计需求就绕不开。token 数不需要精确到跟模型内部完全一致用 tokenizer 数一遍即可prompt_tokens len(tokenizer.encode(prompt_text)) completion_tokens len(tokenizer.encode(reply_text))有一点要提前跟调用方说清楚本地模型的finish_reason只有stop遇到结束符和length被 max_tokens 截断两种而 OpenAI 还有tool_calls、content_filter等。如果你的兼容层不返回这些依赖工具调用的框架会直接跑不通别以为改个 URL 就万事大吉。4. 客户端怎么调几种姿势和对应注意点4.1 requests 裸调最笨但最可控联调阶段我最喜欢用这个因为能看清每一个字节。import requests resp requests.post( http://127.0.0.1:8000/v1/chat, json{ messages: [{role: user, content: 用三句话说清什么是向量检索}], temperature: 0.7, max_new_tokens: 256, }, timeout(5, 120), # 连接 5s读 120s ) resp.raise_for_status() print(resp.json()[content])timeout一定要写成元组只写一个数字会被当成同时作用于连接和读取长文本生成必然超时。这个细节坑过不少人症状是本地测都好好的接了业务就报 ReadTimeout。流式版本with requests.post(url, jsonpayload, streamTrue, timeout(5, 300)) as r: for line in r.iter_lines(decode_unicodeTrue): if not line or not line.startswith(data: ): continue data line[6:] if data [DONE]: break print(data)iter_lines而不是iter_content因为 SSE 是按行分帧的按字节块读会拿到半行解析直接崩。4.2 用 openai SDK 指向本地如果你的兼容层做完了客户端代码几乎不用改from openai import OpenAI client OpenAI(base_urlhttp://127.0.0.1:8000/v1, api_keynot-needed) stream client.chat.completions.create( modellocal-chatglm, messages[{role: user, content: 帮我写一个正则匹配 11 位手机号}], streamTrue, temperature0.3, ) for chunk in stream: delta chunk.choices[0].delta.content if delta: print(delta, end, flushTrue)api_key随便填个非空字符串即可SDK 会校验非空但本地服务通常不验。如果你在服务端加了简易鉴权那就填真实 token。model字段本地服务可以不校验但建议保留透传将来多模型路由时能直接用上。4.3 前端接入跨域和首字延迟前端直连本地服务第一个拦路虎是 CORS。加中间件就好from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[http://localhost:5173, http://内网域名], allow_methods[*], allow_headers[*], )别图省事写allow_origins[*]还带allow_credentialsTrue浏览器会直接拒绝这个组合。第二个是首字延迟。用户点击发送到看见第一个字这段时间包括网络往返、prompt 编码、prefill 计算。6B 模型在消费级卡上1K prompt 的 prefill 大概几百毫秒到一秒多加上排队体感可能两三秒。改善手段有两个一是先把 loading 状态立刻显示出来让用户知道系统收到了二是服务端在开始生成前先推一个空的心跳事件防止中间的网关认为连接空闲而断开。5. 并发、吞吐和稳定性这部分决定它能不能真上线5.1 单卡并发为什么会排队以及排队该怎么办一张卡上的模型同一时刻只能算一个 batch。你开 10 个线程同时打请求它们不是并行推理而是在抢同一把锁实际是串行执行总耗时约等于 10 次单次耗时之和。这不是代码写得不好是物理限制。所以第一个认知要纠正加线程不会提升吞吐只会提升排队体验。真正能提吞吐的手段只有三个批处理把多个请求拼成一个 batch 一次前向、更快的推理引擎更好的 kernel 和调度、更大的卡或更多卡。批处理是最容易拿到的收益vLLM 这类引擎之所以快很大一部分来自 continuous batching。如果暂时不换引擎那至少要把排队做对请求进来先入队由一个工作线程消费队列队列长度设上限超了直接返回 429 而不是无限堆积。否则高峰期所有请求都在等最后一个超时全烂掉用户感知就是服务挂了。import asyncio semaphore asyncio.Semaphore(2) # 最多同时 2 个在算 app.post(/v1/chat) async def chat(req: ChatRequest): async with semaphore: return await asyncio.to_thread(run_inference, req)用asyncio.to_thread把同步推理扔到线程池避免阻塞事件循环——这一步不做的话一个长请求会把整个服务卡住连健康检查都探不通。信号量控制并发数数值取多少要看显存余量宁可小一点让排队在客户端可见也别让 OOM 把整服务带走。5.2 参数怎么配一份能直接用的默认值参数没有绝对最优但有明显不合理的组合。给一份我常用的起点参数默认取值范围调整逻辑temperature0.701.0抽取类任务 0.10.3创意类 0.81.0top_p0.80.11.0与 temperature 二选一调别同时拉满max_new_tokens512644096按业务最大回复长度定留给显存的余量要够repetition_penalty1.051.01.2出现复读时小步上调超过 1.2 容易语句破碎上下文上限8K视模型而定超了要截断历史从最早的轮次开始丢关于temperature和top_p同时调的问题多说一句这俩都在做从概率分布里怎么选词一起拧容易把分布改得面目全非。我的习惯是固定top_p0.8只动temperature这样行为可预期。max_length和max_new_tokens这两个参数在不同版本的接口里含义不一样有的版本max_length指总长度输入输出有的指新生成长度。上线前一定要用固定输入实测一次看返回被截在多少字别照抄别人的配置。5.3 超时、限流和降级别让一个慢请求拖垮全部一套能扛住真实流量的服务至少要有三层保护。第一层是单请求超时。推理线程里加个时间检查超过 N 秒强制停。但注意model.generate是阻塞的没法从外面打断可行的做法是用StoppingCriteriafrom transformers import StoppingCriteria, StoppingCriteriaList import time class TimeLimit(StoppingCriteria): def __init__(self, seconds): self.deadline time.time() seconds def __call__(self, input_ids, scores, **kwargs): return time.time() self.deadline stopping StoppingCriteriaList([TimeLimit(60)])第二层是入口限流。按 IP 或按调用方做令牌桶超出直接 429 带Retry-After。这一层不一定要写进服务里放在前面的网关更合适。第三层是降级。当队列积压超过阈值把max_new_tokens临时压到 128或者直接返回当前繁忙请稍后再试。听起来粗暴但比让所有请求都超时好得多。5.4 加日志和指标不然后面全是玄学我强烈建议从第一版就把这几项记下来请求 id、输入 token 数、输出 token 数、排队耗时、首字耗时、总耗时、是否被截断。有了这些用户说今天特别慢你能立刻定位是排队长了还是单次推理慢了。import logging, time logger logging.getLogger(llm-api) t_start time.time() first_token_at None ... logger.info({ req_id: req_id, prompt_tokens: prompt_tokens, completion_tokens: completion_tokens, queue_ms: int((t_infer - t_start) * 1000), ttft_ms: int((first_token_at - t_infer) * 1000) if first_token_at else None, total_ms: int((time.time() - t_start) * 1000), finish_reason: finish_reason, })ttfttime to first token这个指标特别有用因为它跟用户感知最贴近。总耗时 8 秒但 ttft 只有 300 毫秒用户是能忍的ttft 就 5 秒他会以为卡死了。6. 踩过的坑和排查速查表6.1 显存相关OOM 的七种面孔CUDA out of memory是最常见的报错但原因差别很大得对症。现象可能原因处理启动就 OOM权重精度不对按 fp32 加载了显式指定torch_dtype启动就 OOM端口上还有旧进程没退nvidia-smi查 PIDkill 掉跑一会儿 OOMKV Cache 随请求累积降max_new_tokens、限制并发长文本时 OOM上下文超预期加历史截断逻辑多 worker 时 OOM每个 worker 加载一份模型workers 设为 1报错但不是 OOM显存碎片重启服务或设PYTORCH_CUDA_ALLOC_CONF偶发 OOM峰值并发超过预估上信号量限流有个特别隐蔽的情况服务刚起、显存占用很漂亮跑了半小时突然 OOM。这八成是历史记录没做长度控制某个用户的对话越聊越长某一轮直接把上下文顶到上限。解法是在拼接 history 时按 token 数反向截断保留最近 N 轮def trim_history(history, tokenizer, max_tokens6000): total 0 kept [] for q, a in reversed(history): cost len(tokenizer.encode(q)) len(tokenizer.encode(a)) if total cost max_tokens: break total cost kept.append((q, a)) return list(reversed(kept))6.2 输出质量异常复读、截断、乱码、不听话一直在重复同一句话通常是repetition_penalty没开或者 temperature 太低。小步往上调 penalty1.05 → 1.1别一次拉到 1.3那会让句子变得支离破碎。也检查一下是不是历史里塞了重复内容模型被带偏了。回答到一半就断先看finish_reason。如果是length就是max_new_tokens不够往上调同时留意显存。如果返回里根本没有结束标记那可能是流式连接被中间层掐了去看 5 秒 keep-alive 那个参数。输出全是乱码或者特殊符号八成是 tokenizer 和模型不匹配或者权重没加载成功随机初始化。回退到最小脚本测一次如果最小脚本也乱那就是模型文件本身有问题检查是不是下载不完整、分片缺了。模型完全不按格式回答比如你要求输出 JSON 它偏要加一段解释。这类指令遵循能力跟模型规模和微调程度强相关本地小模型本来就弱。可行的补救是 few-shot在 messages 里塞一两个正确示例命中率会明显提高。我实测下来给两个示例比写一段你必须输出 JSON的强指令更有效。6.3 服务层面连不上、502、乱序本地 curl 通容器里不通先确认服务监听的是0.0.0.0而不是127.0.0.1。再确认端口映射对上了curl http://127.0.0.1:8000/docs能打开说明服务活着。502 或连接被重置常见于响应时间超过网关超时。查一下反向代理的proxy_read_timeout默认值通常 60 秒长回答会被砍断。流式输出顺序乱了或者内容重复检查你的 SSE 分帧是否符合规范——每帧必须以\n\n结尾少一个换行解析器就会把两帧粘一起。另外别在生成器里用print调试混进响应体会直接污染数据流。中文变成\u4f60\u597d这种转义json.dumps少了ensure_asciiFalse加上就好。注意调试阶段建议把每个请求的原始请求体和前 200 字响应落盘出问题时能快速复现。但涉及用户内容要按合规要求处理别把这些日志随手丢在公开可读的目录里。6.4 一份可以直接照抄的启动检查清单上线前我会照这个清单过一遍能省掉大部分低级故障显存峰值实测过用的是真实最大上下文和最大并发不是随便发两句话。workers是 1确认没有第二个进程偷偷占着卡。max_new_tokens和上下文上限有硬性约束不是敞开的。历史截断逻辑存在且按 token 数算不按轮数算。入口有并发控制超限返回 429 而不是排队到死。流式的 keep-alive 和代理超时都调过长回答能完整吐完。日志里有 ttft、总耗时、finish_reason 三个字段。客户端断开时服务端能感知并终止生成显存能释放。/health探针只检查进程存活不触发模型推理否则会被探针打满。兼容层的字段映射测过尤其max_tokens和max_new_tokens。我自己在这个项目上最实在的一条体会是别急着上复杂的推理引擎先把一个进程、一把锁、一个队列这套最朴素的模型跑稳。很多团队一上来就折腾高性能方案结果连用户点了停止按钮显存没释放这种小问题都没解决性能再高也是白搭。等你把队列长度、ttft、显存峰值这几个数摸清楚了再决定要不要换引擎、要不要加卡判断会踏实得多。另外一个小技巧本地调试时把temperature设成 0 走贪心输出稳定可复现定位问题时不会因为这次刚好生成了别的词而怀疑人生。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询