runner.py 只读一次模型
def main():
model, tokenizer, cfg = load()
main 启动时用 load() 读一次模型、和设置。下面的 类定义在 main 内部,直接使用外面的 model、tokenizer、cfg(闭包)。不会每次请求都重建模型。
通过真正的 提供好的。写好 runner.py 并在后台启动,再用 调用 和 /generate。
只加载一次,顺序处理请求。 返回状态, /generate返回生成文本。
指自身。必须在此运行,不是在地址栏访问。与&只在此标签存活时运行,刷新后需重启,不需。
用运行 generate.py,每次运行都要读取检查点、重建模型。问题多时,每次都重复同样的准备。Runner 只读一次模型放在里,每次请求来时只 generate。
拆成,角色也分开了。
只要遵守约定,客户端可以是任何东西。网页、手机应用、其他服务器都可以代替终端 XChat 调用同一个 Runner。这种读取好的来负责计算的也叫 。Runner 是很小的运行时,实际中 、、 这样的运行时能快速完成同样的工作。
是客户端发送请求、服务器返回响应的约定。
可以用 curl 直接构造请求。
Runner 只开放两个地址。
/generate 正文的字段。
对错误的请求,服务器不会崩溃,而是用 400 和 {"error": "invalid request"} 回答。正文超过 4,096 、不是 JSON、字段格式错误、没有 prompt,全部是 400。设限制是为了保护服务器。一个把 tokens 设为 100 万的请求,就能把服务器占住几个小时。公开服务器必须做这样的检查。
Runner 在 127.0.0.1 的 8765 号等待请求。
这里的"这台计算机"是浏览器中的虚拟 。所以 curl 和 XChat 必须在右边的终端里运行。在自己电脑的浏览器地址栏输入 localhost:8765,指向的是自己的电脑而不是虚拟 Linux,连接不上。
两个程序不能同时使用同一个端口。Runner 已经启动又再启动,会出现 OSError: 。
Runner 是不会结束、一直等待请求的程序。直接运行会占住终端,无法输入其他命令,所以在后面(后台)运行。
真实服务器在计算机重启、程序崩溃时也要自动重新启动。Linux 用 systemd 服务(Restart=always), 用 launchd 做这件事。这个虚拟 Linux 中的 Runner 只在这个标签页打开时运行。
XSLM 的 一次处理一个请求。生成结束前进来的请求,要等前一个请求结束。对教学来说简单又安全。
多人使用的服务器做法不同。
FastAPI 会自动生成请求格式检查(pydantic)和 /docs 的 API 文档(Swagger)。XSLM 在 runner.py 中手写同样的检查。
要把 Runner 做成真实服务,还要加上这些。
编写runner.py并启动,立即记录PID,检查日志、health和生成结果。
cd ~/xslm
micro runner.py
nohup python3 -u runner.py > runner.log 2>&1 &
echo $! > runner.pid
cat runner.log
curl -s http://127.0.0.1:8765/health
curl -s http://127.0.0.1:8765/generate -H 'Content-Type: application/json' -d '{"prompt":"Q: hi\nA: ","tokens":8,"temperature":0.8}'
在终端中练习
共享一个模型,检查输入并返回 JSON,仅监听来宾回环地址。
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from generate import load, generate
def main():
model, tokenizer, cfg = load()
class Handler(BaseHTTPRequestHandler):
def reply(self, status, body):
data = json.dumps(body, ensure_ascii=False).encode('utf-8')
self.send_response(status)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
if self.path == '/health':
self.reply(200, {'status': 'ok', 'model': 'XSLM'})
else:
self.reply(404, {'error': 'not found'})
def do_POST(self):
if self.path != '/generate':
return self.reply(404, {'error': 'not found'})
try:
length = int(self.headers.get('Content-Length', '0'))
if not 0 < length <= 4096:
raise ValueError('request size')
data = json.loads(self.rfile.read(length))
prompt = data['prompt']
tokens = data.get('tokens', cfg['tokens'])
temperature = data.get('temperature', 0.8)
top_k = data.get('top_k', 8)
seed = data.get('seed', cfg['seed'])
if not isinstance(prompt, str) or not 0 < len(prompt) <= 512:
raise ValueError('prompt')
if type(tokens) is not int or not 1 <= tokens <= cfg['max_tokens']:
raise ValueError('tokens')
if type(top_k) is not int or top_k < 0 or type(seed) is not int:
raise ValueError('top_k/seed')
if type(temperature) not in (int, float):
raise ValueError('temperature')
result = generate(model, tokenizer, prompt, tokens, temperature, top_k, seed)
self.reply(200, {'text': result, 'model': 'XSLM'})
except (ValueError, KeyError, TypeError):
self.reply(400, {'error': 'invalid request'})
address = ('127.0.0.1', cfg['port'])
print('XSLM Runner:', address, flush=True)
HTTPServer(address, Handler).serve_forever()
if __name__ == '__main__':
main()
def main():
model, tokenizer, cfg = load()
main 启动时用 load() 读一次模型、和设置。下面的 类定义在 main 内部,直接使用外面的 model、tokenizer、cfg(闭包)。不会每次请求都重建模型。
class Handler(BaseHTTPRequestHandler):
def reply(self, status, body):
data = json.dumps(body, ensure_ascii=False).encode('utf-8')
self.send_response(status)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
Handler 继承 BaseHTTPRequestHandler,负责一个请求。reply 把转成 JSON,再变成 UTF-8 字节(因为 ensure_ascii=False,中文也原样保留),按 状态码 → 头部(Content-Type、Content-Length) → 空行(end_headers) → 正文 的顺序发送。Content-Length 不是字符数而是字节数,所以用编码后 的长度。
def do_GET(self):
if self.path == '/health':
self.reply(200, {'status': 'ok', 'model': 'XSLM'})
else:
self.reply(404, {'error': 'not found'})
收到 GET 请求时调用 do_GET。self.path 是 /health 就发送 200 和状态,否则发送 404。状态检查地址不做模型计算,直接回答,所以用来轻量地确认服务器是否在运行。
def do_POST(self):
if self.path != '/generate':
return self.reply(404, {'error': 'not found'})
try:
length = int(self.headers.get('Content-Length', '0'))
if not 0 < length <= 4096:
raise ValueError('request size')
data = json.loads(self.rfile.read(length))
prompt = data['prompt']
tokens = data.get('tokens', cfg['tokens'])
temperature = data.get('temperature', 0.8)
top_k = data.get('top_k', 8)
seed = data.get('seed', cfg['seed'])
if not isinstance(prompt, str) or not 0 < len(prompt) <= 512:
raise ValueError('prompt')
if type(tokens) is not int or not 1 <= tokens <= cfg['max_tokens']:
raise ValueError('tokens')
if type(top_k) is not int or top_k < 0 or type(seed) is not int:
raise ValueError('top_k/seed')
if type(temperature) not in (int, float):
raise ValueError('temperature')
路径不是 /generate 就是 404。然后在 try 中逐项检查,只要有一项不对就抛出 。
result = generate(model, tokenizer, prompt, tokens, temperature, top_k, seed)
self.reply(200, {'text': result, 'model': 'XSLM'})
except (ValueError, KeyError, TypeError):
self.reply(400, {'error': 'invalid request'})
通过检查就调用 generate,用 200 和 {"text": …} 回答。ValueError、KeyError、TypeError 都是错误请求,所以用 400 回答,服务器继续运行。如果每次出错服务器都崩溃,一个请求就能让整个服务停下。
address = ('127.0.0.1', cfg['port'])
print('XSLM Runner:', address, flush=True)
HTTPServer(address, Handler).serve_forever()
地址是 127.0.0.1 和设置中的端口。用 =True 立即输出启动信息,可以在 runner.log 中知道是否已启动。HTTPServer(…).serve_forever() 无休止地重复逐个接收请求并交给 Handler。用 kill 或 停止。
应返回status ok及含text的JSON。空JSON应返回400而非崩溃。
时检查日志和检查点。端口占用时先停旧PID。重新训练后重启Runner。
Runner 在浏览器中的虚拟 Linux 里运行,只在那个 Linux 的 127.0.0.1 上等待。地址栏的 localhost 是你真正的电脑,那里没有 Runner。请在右边的终端运行 curl。
8765 号端口已经被另一个 Runner 占用了。可以直接用已启动的 Runner,或者用 kill "$(cat runner.pid)" 关掉后重新启动。没有 PID 文件时,用 ps 找到 python3 runner.py 的编号再 kill。
Runner 只在启动时读一次 checkpoints/model.json 放在内存里。文件变了它也不知道。要关掉再启动才会用新权重。真实服务会在旁边启动新模型,再把请求转过去,不停机地切换。