runner.py モデルを一度だけ読む
def main():
model, tokenizer, cfg = load()
main は始まるときに load() でモデル・・設定を一度読みます。下の クラスは main の中で定義されていて、外側の model・tokenizer・cfg をそのまま使います(クロージャー)。要求ごとにモデルを作り直しません。
したを実際の で動かすを作ります。runner.py をバックグラウンドで起動し、 で と /generate を呼んでみましょう。
はを一度読み、順に要求を処理します。 は状態、 /generateは文章を返します。
は自身です。はここで実行し、の住所欄とは区別します。と&はタブが生きている間だけ有効で、は不要です。
generate.py をで実行すると、実行するたびにチェックポイントを読んでを作り直します。質問が多いと、そのたびに同じ準備を繰り返します。Runner はモデルを一度だけ読んでに置き、要求が来るたびに generate だけを呼びます。
に分けると、役割も分かれます。
約束さえ守れば、クライアントは何にでもなれます。の XChat の代わりに、Web ページ・スマートフォンのアプリ・別のサーバーが同じ Runner を呼べます。した重みを読んで計算を担うこのようなを、 ともいいます。Runner はとても小さなランタイムで、実際には ・・ のようなランタイムが同じ仕事を速く行います。
は、クライアントが要求を送り、サーバーが応答を返す約束です。
curl で要求を自分で作れます。
Runner は2つのアドレスだけを開きます。
/generate の本文のフィールドです。
間違った要求には、サーバーは落ちずに 400 と {"error": "invalid request"} で答えます。本文が4,096を超える、JSON ではない、フィールドの形式が違う、prompt がない — これらはすべて 400 です。制限を設ける理由は、サーバーを守るためです。tokens を100万で送る要求1つが、サーバーを何時間も占有することがあります。公開サーバーなら、このような検査が必ず必要です。
Runner は 127.0.0.1 の8765番で要求を待ちます。
ここでの「このコンピューター」は、ブラウザーの中の仮想 です。だから curl と XChat は必ず右のターミナルの中で実行します。自分のコンピューターのブラウザーのアドレス欄に localhost:8765 と打つと、仮想 Linux ではなく自分のコンピューターを指すので接続できません。
同じポートを2つのプログラムが同時に使うことはできません。Runner がすでに起動しているのにもう一度起動すると、OSError: になります。
Runner は終わらずに要求を待ち続けるプログラムです。そのまま実行するとターミナルを占有して他のコマンドを打てないので、裏で(で)実行します。
実際のサーバーは、コンピューターが再起動しても、プログラムが落ちても、自動でもう一度起動する必要があります。Linux は systemd のサービス(Restart=always)で、 は launchd でこれを行います。この仮想 Linux の Runner は、このタブが開いている間だけ動きます。
XSLM の は、要求を一度に1つずつ処理します。生成が終わる前に来た要求は、前の要求が終わるまで待ちます。授業用には単純で安全です。
多くの人が使うサーバーは違うやり方をします。
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 を継承し、要求を1つ担当します。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 の中で1つずつ検査し、どれか1つでも違えば を起こします。
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 で答え、サーバーは動き続けます。エラーのたびにサーバーが落ちると、要求1つでサービス全体が止まってしまうからです。
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() は、要求を1つずつ受け取って Handler に渡すことを果てしなく繰り返します。kill か で止めます。
status okとtextを確認。空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 を一度だけ読んでメモリーに置きます。ファイルが変わっても気づきません。止めて起動し直してはじめて、新しい重みを使います。実際のサービスでは、新しいモデルを横に立ち上げてから要求を移すやり方で、止めずに入れ替えます。