从固定小权重到CPU HTTP服务
上一节的GGUF命令适用于已有制品。本篇给出从空目录获取权重、校验、载入、启动到停止的首次入口。选择公开的 sshleifer/tiny-gpt2,仅用于运行链路:它是很小的测试模型,实际会重复词语,没有可靠问答能力。CPU足够,不把这个实验的延迟或质量推广到大模型。
引擎固定 Python3.12、torch2.8.0、transformers4.57.3。模型revision同时固定权重、config与tokenizer;该GPT2测试模型没有chat_template,所以输入采用普通续写prompt,不能套聊天角色模板。换instruction模型时必须先核对其tokenizer.chat_template及stop/eos约定。
首次完整实验
先创建环境并取得固定制品:
python3.12 -m venv .venv
.venv/bin/pip install torch==2.8.0 transformers==4.57.3 huggingface-hub==0.36.2将下面下载器保存为download.py并运行 .venv/bin/python download.py。首次需要访问公开模型仓库,随后离线运行。
from huggingface_hub import snapshot_download
from pathlib import Path
import hashlib
snapshot_download('sshleifer/tiny-gpt2',revision='5f91d94bd9cd7190a9f3216ff93cd1dd95f2c7be',local_dir='model',allow_patterns=['*.json','*.txt','pytorch_model.bin'])
actual=hashlib.sha256(Path('model/pytorch_model.bin').read_bytes()).hexdigest()
assert actual=='b706b24034032bdfe765ded5ab6403d201d295a995b790cb24c74becca5c04e6'
print('model verified')下面完整服务器保存为server.py,执行 MODEL_DIR=./model .venv/bin/python server.py。
import json, os, threading, socket, re
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.request import urlopen,Request
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
folder=os.environ['MODEL_DIR']
tok=AutoTokenizer.from_pretrained(folder,local_files_only=True)
model=AutoModelForCausalLM.from_pretrained(folder,local_files_only=True).eval()
torch.set_num_threads(1)
lock=threading.Lock()
class Handler(BaseHTTPRequestHandler):
def log_message(self,*args):pass
def respond(self,status,body):
self.close_connection=True
data=json.dumps(body).encode();self.send_response(status);self.send_header('Connection','close');self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(data)));self.end_headers();self.wfile.write(data)
def do_GET(self):self.respond(200 if self.path=='/health' else 404,{'ready':self.path=='/health'})
def do_POST(self):
if self.path!='/generate':return self.respond(404,{'error':'not found'})
lengths=self.headers.get_all('Content-Length',[])
if self.headers.get('Transfer-Encoding') is not None:
return self.respond(400,{'error':'transfer encoding unsupported'})
if len(lengths)!=1 or re.fullmatch(r'[0-9]+',lengths[0]) is None or len(lengths[0])>8:
return self.respond(400,{'error':'one decimal content length required'})
n=int(lengths[0])
if n<=0:return self.respond(400,{'error':'empty body'})
if n>8192:return self.respond(413,{'error':'body too large'})
self.connection.settimeout(3)
try:
raw=self.rfile.read(n)
if len(raw)!=n:raise ValueError('incomplete body')
obj=json.loads(raw);prompt=obj['prompt'];new=obj.get('max_new_tokens',8)
if not isinstance(prompt,str) or type(new) is not int or not 1<=new<=32:raise ValueError()
ids=tok(prompt,return_tensors='pt');count=ids['input_ids'].shape[1]
if not 1<=count or count+new>128:raise ValueError()
except (TimeoutError,socket.timeout):return self.respond(408,{'error':'body read timeout'})
except (ValueError,KeyError,TypeError):return self.respond(400,{'error':'invalid input or token budget'})
with lock,torch.inference_mode():out=model.generate(**ids,max_new_tokens=new,do_sample=False,pad_token_id=tok.eos_token_id)
self.respond(200,{'text':tok.decode(out[0,count:]),'input_tokens':count,'output_tokens':out.shape[1]-count})
server=ThreadingHTTPServer(('127.0.0.1',8087),Handler)
print('ready 127.0.0.1:8087',flush=True)
try:server.serve_forever()
except KeyboardInterrupt:pass
finally:server.server_close()在另一个终端请求 curl http://127.0.0.1:8087/health,就绪返回ready:true;请求生成:
curl -s http://127.0.0.1:8087/generate -H 'Content-Type: application/json' -d '{"prompt":"Refund policy:","max_new_tokens":8}'验证时实际输出是重复 factors 的8个token;这证明权重经过真实前向并从HTTP返回,不代表模型能回答退款。输入与输出预算总计最多128,超限返回400而非悄悄截断。请求必须恰有一个十进制Content-Length,缺失、负数、非数字、空体返回400,超过8192返回413;不支持Transfer-Encoding。读取等待超过3秒返回408,所有响应关闭连接,避免残留请求体影响下一请求。这个socket超时限制一次阻塞读取,不是严格总墙钟deadline;它也不限制线程数量或排队长度。线程锁避免本示例中模型同时生成;这是教学入口,不是高吞吐推理服务。只绑定127.0.0.1。结束时在服务终端按Ctrl-C,finally释放端口。
若MODEL_DIR不存在或文件校验不符,先修制品路径,不通过随机初始化掩盖缺失权重。若健康检查失败,读服务启动异常,区分端口占用、缺依赖、模型格式不匹配。更换到GGUF需要对应引擎及转换/量化元数据,不能把PyTorch权重改扩展名。批处理、KV缓存、分页注意力解决的是不同层次的吞吐/内存问题,应固定模型和输入再测并发、首token时间与总时延。
练习
发送200次重复hello作为prompt,确认400;将MODEL_DIR改为不存在目录,确认服务不报告ready。使用同一模型固定输入比较max_new_tokens为1和8,观察output_tokens。用原始HTTP分别发缺失、负数、非数字、超限Content-Length,以及分块传输标记,检查错误状态;不要为这些无效请求发送实际大体积数据。
完成后查看本章答案。
返回完整学习路线。