MkItYs

MkItYs > AI・交渉・物語の自動生成 > 

images

チャットのエンジン/サーバ/クライアントを作る:Python (Transformers, aiohttp), JavaScript

images

チャットのエンジンと、そのサーバ〜クライアントを作ってみます。

関連


サーバ証明書を取得〜更新する(ワイルドカード証明書、ネームサーバ経由):Let's Encrypt

検証


クラウド:GCP
コンテナ:Docker
ゲストOS:Ubuntu 22.04
ウェブブラウザ:Chrome
画像生成アプリ:ComfyUI

概要


背景

いまはそれなりの言語モデルを、ローカル環境でも動かせるようになってきました。

まず量子化による劣化が、そこまで酷くならないことが分かってきました。さらに、マージツール(mergekit)によるさまざまなマージ手法(Chat Vector, MoE, ...)が試され、一定の成果を上げています。とくに進化計算を組み合わせたツール(mergekit-evolve)によるマージは、比較的小さな資源で、大きな効果を得られることで話題になりました。[※1]

対応

ここでは、チャットのエンジンと、そのサーバ〜クライアントの、かんたんな実装を試みます。

構成

言語モデルの一プロジェクト”LocalNovelLLM-project”から、適度なサイズでそれなりの言語モデルが出ていますーーここでは、このモデルを使います。LLaMA 系なので、インストラクションの形式は [INST] [/INST] になります:

https://huggingface.co/Local-Novel-LLM-project
https://huggingface.co/Local-Novel-LLM-project/Vecteus-v1

※1
Sakana AI 社は、錬金術ともいわれていたマージの技法に、進化計算を適用して成果を上げましたーーただ、そのソースコードは開示されていないので、ゴダード氏が、オープンソース版を開発〜公開しています:
https://sakana.ai/evolutionary-model-merge-jp/
https://blog.arcee.ai/tutorial-tutorial-how-to-get-started-with-evolutionary-model-merging/

設置


環境は、音声認識エンジン向けに作ったコンテナ/ライブラリを流用します:

音声認識のエンジン/サーバ/クライアントを作る:Python (Transformers, aiohttp), Javascript (MediaRecorder)

実装



バックエンド:エンジン
appllm/bin/llmprc.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

class LlmPrc:

  def __init__(self, nummax, nmodel):
    self.nummax = nummax
    cnfbnb = BitsAndBytesConfig(
      load_in_4bit=True,
      bnb_4bit_quant_type="nf4",
      bnb_4bit_use_double_quant=True,
      bnb_4bit_compute_dtype=torch.bfloat16)
    self.omodel = AutoModelForCausalLM.from_pretrained(
      nmodel,
      device_map="auto",
      quantization_config=cnfbnb)
    self.tokenz = AutoTokenizer.from_pretrained(
      nmodel)

  def setsys(self, prmsys):
    self.prmsys = prmsys
    self.prompt = ""
    self.lstusr = []
    self.lstres = []

  def addusr(self, prmusr, lstusr):
    lstusr.append(prmusr)
    if len(lstusr) > self.nummax:
      lstusr.pop(0)
    return lstusr

  def addres(self, outres, lstres):
    lstres.append(outres)
    if len(lstres) > self.nummax - 1:
      lstres.pop(0)
    return lstres

  def genprm(self, lstusr, lstres):
    prompt = ""
    i = 0
    while i < len(lstusr):
      if i == 0:
        if i == len(lstusr) - 1:
          prompt = "<s>" + "[INST]" + "<<SYS>>" + self.prmsys + "<</SYS>>" + lstusr[i] + "[/INST]"
        else:
          prompt = "<s>" + "[INST]" + "<<SYS>>" + self.prmsys + "<</SYS>>" + lstusr[i] + "[/INST]" + lstres[i]
      else:
        if i == len(lstusr) - 1:
          prompt = prompt + "</s>" + "<s>" + "[INST]" + lstusr[i] + "[/INST]"
        else:
          prompt = prompt + "</s>" + "<s>" + "[INST]" + lstusr[i] + "[/INST]" +  lstres[i]
      i = i + 1
    return prompt

  def genres(self, numout, numtmp, prmusr):
    self.addusr(prmusr, self.lstusr)
    self.prompt = self.genprm(self.lstusr, self.lstres)

    inputs = self.tokenz([self.prompt], return_tensors="pt").to("cuda")
    lstgen = self.omodel.generate(
      **inputs,
      max_new_tokens=numout,
      temperature=numtmp,
      do_sample=True,
      pad_token_id=self.tokenz.eos_token_id)
    output = self.tokenz.decode(lstgen[0]) # 推定結果の全体
    outres = self.tokenz.decode(lstgen[0][len(inputs["input_ids"][0]):]) # 推定結果の追加部分(応答)

    outres = outres.replace(" ", "")
    outres = outres.replace("「", "")
    outres = outres.replace("」", "")
    outres = outres.replace("</s>", "")

    #print("prompt[" + self.prompt + "]") # DBG
    #print("output[" + output + "]") # DBG
    #print("outres[" + outres + "]") # DBG

    self.addres(outres, self.lstres)

    return outres

バックエンド:サーバ
appllm/bin/llmsrv.py
import json
from aiohttp import web
from llmprc import LlmPrc

async def res001(datreq):
  global prmold
  dicreq = await datreq.json()
  txtreq = dicreq["txtreq"]
  numout = dicreq["numout"]
  numtmp = dicreq["numtmp"]
  prmsys = dicreq["prmsys"]
  if prmold != prmsys:
    llmprc.setsys(prmsys)
    prmold = prmsys
  txtres = llmprc.genres(numout, numtmp, txtreq)
  return web.Response(
    text=json.dumps({"txtres": txtres}),
    content_type="application/json",
  )

prttgt = 8080
adr001 = "/"
nummax = 4
nmodel = "Local-Novel-LLM-project/Vecteus-v1"

prmold = ""
llmprc = LlmPrc(nummax, nmodel)
appweb = web.Application()
appweb.add_routes([web.post(adr001, res001)])
web.run_app(appweb, port=prttgt)

フロントエンド:クライアント
appllm/bin/llmclt.py
import sys
import asyncio
import aiohttp

async def req001(dicreq):
  async with aiohttp.ClientSession() as sssweb:
    async with sssweb.post(adr001, json=dicreq) as datres:
      dicres = await datres.json()
      return dicres

numout = int(sys.argv[1])
numtmp = float(sys.argv[2])
prmsys = sys.argv[3]
txtreq = sys.argv[4]
adr001 = "http://localhost:8080/"

dicreq = {"txtreq": txtreq, "numout": numout, "numtmp": numtmp, "prmsys": prmsys}
dicres = asyncio.run(req001(dicreq))
print(dicres["txtres"])

利用


コンテナ上で、サーバを起動し〜クライアントを実行します:

# サーバ
$ python /system/vol104/bin/llmsrv.py &

# クライアント
$ python /system/vol104/bin/llmclt.py 34 0.75 'あなたは優れた助言者です' 'おはよう'