Skip to content

从固定小权重到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约定。

首次完整实验 ​

先创建环境并取得固定制品:

bash
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。首次需要访问公开模型仓库,随后离线运行。

python
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。

python
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;请求生成:

bash
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,以及分块传输标记,检查错误状态;不要为这些无效请求发送实际大体积数据。

完成后查看本章答案。

返回完整学习路线。

用心记录每一步成长