Large Language Model をローカルで動かす時代が到来するらしい。
Reddit には LocalLLaMA というサブレディットがあり、かなり盛り上がっている。 そこでは シリコンマックを使っているひとが結構いる。 Meta の 商用利用も可能な Llama 2 がオープンソースで提供された結果、 その派生プロジェクトがいろいろ存在している。 そのなかには、低スペックの シリコンマックでも LLM を実行できるものがある。 Pytorch もMetal 対応していて、 おそらくは、Pytorch 依存の LLM モデルについては、Linux + CUDA と同じ手順で動かせるのではないかと思う。
Pytorch を使う方法を 手元の M1 Macbook Air 8GB で試したが残念ながら作動しなかった。 たぶんメモリが足りないのだと思う。 十分メモリがあれば動くのではないかと思うが定かではない。
それでも Llama.cppと軽量化されたモデルを使うことで M1 Macbook Air 8GB でも作動させることができた。
このエントリーでは、Linux + CUDA で、 Llama 2 をベースに商用利用可能な日本語LLM ELYZA を試します。
Ubuntu 22.04 でざっと以下のような環境で試しています。
CUDA
$ python --version
Python 3.8.17
$ python -m venv ~/local/venv-elyza
$ source ~/local/venv-elyza/bin/activate
(venv-elyza) $
依存するライブラリを入れます。
(venv-elyza) $ pip install transformers torch accelerate
次のようなタスクをやってみます。
ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(国産)、植物油脂)、殻付き海老(インド)、いか(中国)
こんなスペック情報があったとして、ここから原産地情報だけを抜き出したい。
期待する結果は:
です。
コードの詳細は https://huggingface.co/elyza/ELYZA-japanese-Llama-2-7bをご覧ください。
まずプロンプトを生成するところまでコードを書きます。
main.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
DEFAULT_SYSTEM_PROMPT = "あなたは誠実で優秀な日本人の校正者です。"
#MODEL_NAME = "elyza/ELYZA-japanese-Llama-2-7b-instruct"
MODEL_NAME = 'elyza/ELYZA-japanese-Llama-2-7b-fast-instruct'
def to_prompt(text, tokenizer):
inst_open, inst_close = "[INST]", "[/INST]"
sys_open, sys_close = "<<SYS>>\n", "\n<</SYS>>\n\n"
return "{bos_token}{b_inst} {system}{prompt} {e_inst} ".format(
bos_token=tokenizer.bos_token,
b_inst=inst_open,
system=f"{sys_open}{DEFAULT_SYSTEM_PROMPT}{sys_close}",
prompt=text,
e_inst=inst_close)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
text = """
次の例文から原産地名を列挙して箇条書きにしてください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(国産)、植物油脂)、殻付き海老(インド)、いか(中国)
"""
prompt = to_prompt(text, tokenizer)
print(prompt)
モデルは fast なしと fast 付きのものがあります。 オフィシャルアナウンスページ https://note.com/elyza/n/na405acaca130 によれば、fast 付きのほうが性能が高いようです。詳しくはそちらをご覧ください。
プロンプトは以下のようにしました。
次の例文から原産地名を列挙して箇条書きにしてください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(国産)、植物油脂)、殻付き海老(インド)、いか(中国)
実行してみます。
(venv-elyza) $ python main.py
<s>[INST] <<SYS>>
あなたは誠実で優秀な日本人の校正者です。
<</SYS>>
次の例文から原産地名を列挙して箇条書きにしてください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(国産)、植物油脂)、殻付き海老(インド)、いか(中国)
[/INST]
まだ このLLMに投げる プロンプトを生成 しただけです。 INST タグや SYS タグで情報を入れるのが Llama のお作法とのこと。
上記コードに続けて、モデルをロードして結果を得るところまで記述します。
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
device_map="auto",
torch_dtype=torch.float16)
inputs = tokenizer(
prompt,
add_special_tokens=False,
return_tensors='pt')
with torch.no_grad():
output_ids = model.generate(
inputs['input_ids'].to(model.device),
max_new_tokens=100,
do_sample=True,
temperature=0.01,
top_p=0.9,
pad_token_id=tokenizer.pad_token_id,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id,
repetition_penalty=1.1)
output = tokenizer.decode(output_ids.tolist()[0])
print(output)
実行。
$ python main.py
<s> [INST] <<SYS>>
あなたは誠実で優秀な日本人の校正者です。
<</SYS>>
次の例文から原産地名を列挙して箇条書きにしてください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(国産)、植物油脂)、殻付き海老(インド)、いか(中国)
[/INST] 承知しました。以下が原産地名のリストになります。
- エクアドル
- インド
- 中国</s>
残念。「国産」は産地名として認識できなかったようです。
試しに「国産も産地名として扱ってください。」を text に追記してやってみたところ、 以下の結果がかえりました。
- エクアドル
- インド
- 中国
- 国産
国産が最後に列挙されていますが、どうなんでしょうか。 じゃがいも(国産) があるから「国産」を列挙してくれたのか? それとも、無条件に結果に含めればよいと解釈されたのか?
では、「国産」に代えて「カナダ」にして試してみます。 つまりプロンプトの text は次のようにしました。
次の例文から原産地名を列挙して箇条書きにしてください。
国産も産地名として扱ってください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(カナダ)、植物油脂)、殻付き海老(インド)、いか(中国)
それでは実行してみます。
$ python main.py
<s> [INST] <<SYS>>
あなたは誠実で優秀な日本人の校正者です。
<</SYS>>
次の例文から原産地名を列挙して箇条書きにしてください。
国産も産地名として扱ってください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(カナダ)、植物油脂)、殻付き海老(インド)、いか(中国)
[/INST] 承知しました。
- エクアドル
- カナダ
- 中国
- インド
- 日本の他
ただし、いかの産地については「中国」とありますが、これは誤りです。
正しい答えは「日本の他」となります。</s>
えっと、内容の誤りを指摘してきた。優秀な「校正者」なので、そうしたのであろうか? 「チェッカー」という前提にすれば、そこまで余分な指摘をしないで済ませてくれるのであろうか?
$ python main.py
<s> [INST] <<SYS>>
あなたは誠実で優秀な日本人のチェッカーです。
<</SYS>>
次の例文から原産地名を列挙して箇条書きにしてください。
国産も産地名として扱ってください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(カナダ)、植物油脂)、殻付き海老(インド)、いか(中国)
[/INST] 承知しました。
- エクアドル
- カナダ
- 中国
- インド
- 日本の他
ただし、いかの産地については「中国」と記載されていますが、現在では養殖されているため、正確には中国ではなく日本での生産となります。</s>
いや更に踏み込んで説明されてしまった。 この LLM にとっては、いかの産地は「中国」であるはずはない、ということらしい。 実際この指摘が正しいのかは(自分には)わからない。 正しくない理由「現在では養殖されているため」と言われると妙に説得力増すな。
「原産地名」と書いてしまったのがいけないのかもしれない。
「原産地名」に代えて「産地名」を使うことにしてみます。 また、出力形式を JSON とします。
次の例文に存在する産地名を見つけて JSON 形式で出力してください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(カナダ)、植物油脂)、殻付き海老(インド)、いか(中国)
実行します。
$ python main.py
<s> [INST] <<SYS>>
あなたは誠実で優秀な日本人のチェッカーです。
<</SYS>>
次の例文に存在する産地名を見つけて JSON 形式で出力してください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(カナダ)、植物油脂)、殻付き海老(インド)、いか(中国)
[/INST] {"産地":["エクアドル", "カナダ", "中国"]}</s>
今度は意図通りできました。 JSON で回答してといったことで、余分の補足情報が入らなくなりました。
結果の JSON だけを取りだす to_answer 関数を書きます。
import re
def to_answer(text):
p = re.compile('.*\\[/INST\\](.*)</s>$', re.MULTILINE | re.DOTALL)
m = p.search(text)
if m:
return m.group(1).strip()
else :
return text
これを使えば、以下のように結果が得られます。
$ python main.py
{"産地":["エクアドル", "カナダ", "中国"]}
キーを「産地」ではなく「産地名」にしたい。
プロンプトに JSON のキーは「産地名」としてください。 を追加。
次の例文に存在する産地名を見つけて JSON 形式で出力してください。
JSON のキーは「産地名」としてください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(カナダ)、植物油脂)、殻付き海老(インド)、いか(中国)
実行してみます。
{
"産地名": "エクアドル",
"産地名": "カナダ",
"産地名": "インド",
"産地名": "中国"
}
キーなのに重複してしまっている。難しい。 アレクサと同じで どういえば伝わるのか問題 があるな。
ここまでのコードを整理します。
main.py
import re
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
#DEFAULT_SYSTEM_PROMPT = "あなたは誠実で優秀な日本人の校正者です。"
DEFAULT_SYSTEM_PROMPT = "あなたは誠実で優秀な日本人のチェッカーです。"
#MODEL_NAME = "elyza/ELYZA-japanese-Llama-2-7b-instruct"
MODEL_NAME = 'elyza/ELYZA-japanese-Llama-2-7b-fast-instruct'
def to_answer(text):
p = re.compile('.*\\[/INST\\](.*)</s>$', re.MULTILINE | re.DOTALL)
m = p.search(text)
if m:
return m.group(1).strip()
else :
return text
def to_prompt(text, tokenizer):
inst_open, inst_close = "[INST]", "[/INST]"
sys_open, sys_close = "<<SYS>>\n", "\n<</SYS>>\n\n"
return "{bos_token}{b_inst} {system}{prompt} {e_inst} ".format(
bos_token=tokenizer.bos_token,
b_inst=inst_open,
system=f"{sys_open}{DEFAULT_SYSTEM_PROMPT}{sys_close}",
prompt=text,
e_inst=inst_close)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
text = """
次の例文に存在する産地名を見つけて JSON 形式で出力してください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(カナダ)、植物油脂)、殻付き海老(インド)、いか(中国)
"""
prompt = to_prompt(text, tokenizer)
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
device_map="auto",
torch_dtype=torch.float16)
inputs = tokenizer(
prompt,
add_special_tokens=False,
return_tensors='pt')
with torch.no_grad():
output_ids = model.generate(
inputs['input_ids'].to(model.device),
max_new_tokens=100,
do_sample=True,
temperature=0.01,
top_p=0.9,
pad_token_id=tokenizer.pad_token_id,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id,
repetition_penalty=1.1)
output = tokenizer.decode(output_ids.tolist()[0])
final_answer = to_answer(output)
print(final_answer)
なお、 回答のゆれをできる限りなくし、 毎回同じ回答が出ることを期待して オプションの temperature 値は 0.01 に設定しています。
それではこれを REST サーバにして、 単純に text を POST して回答を得られるようにします。
Bottle を入れます。
(venv-elyza) $ pip install bottle
main.py を修正します。
from bottle import Bottle, run, request
...省略...
app = Bottle()
@app.post('/query')
def query():
text = request.body.getvalue().decode('utf-8')
print(text)
prompt = to_prompt(text, tokenizer)
inputs = tokenizer(
prompt,
add_special_tokens=False,
return_tensors='pt')
with torch.no_grad():
output_ids = model.generate(
inputs['input_ids'].to(model.device),
max_new_tokens=100,
do_sample=True,
temperature=0.01,
top_p=0.9,
pad_token_id=tokenizer.pad_token_id,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id,
repetition_penalty=1.1)
output = tokenizer.decode(output_ids.tolist()[0])
return to_answer(output)
run(app, host='localhost', port=8000, debug=True)
サーバ側はこれで完成です。
(venv-elyza) $ python main.py
として起動しておきます。
クライアントは、curl を使います。
q.sh
#!/bin/bash
curl -X POST "http://localhost:8000/query" -d '次の例文に存在する産地名を見つけて JSON 形式で出力してください。例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(カナダ)、植物油脂)、殻付き海老(インド)、いか(中国)' -H "Content-Type: plain/text"
実行。
$ sh q.sh
{"産地":["エクアドル", "カナダ", "中国"]}
うまくいきました。
他の例も試してみます。 今度は q.sh ファイルに書かないで、直接コマンドラインにコマンドを入力します。
$ curl -X POST "http://localhost:8000/query" -d \
"次の例文から価格のみを抜き出して JSON にしてください。例文: この商品は【ネットストア限定、消費税込5,000円以上で配送料無料、対象商品】です" \
-H "Content-Type: plain/text"
{"price":5000}
うまくいきました。
このような LLM サーバを常時起動させておきたい。 しかし、NVIDIA GeForce RTX 3060 はファンがうるさく、常時起動はいやだ。 となると、シリコンマックじゃないのかこれは。
シリコンマック(M2)は 無印/Pro/Max のグレードがあるが、 メモリ帯域がそれぞれ 100GB/s, 200GB/s, 400GB/s になっているらしく、 これが LLM で推論させるときの処理時間の短縮に直結するらしい。
mac mini (M2 Pro) の 32GB モデルの場合 24万円のようなので、 LLMサーバとして使うとしたらこれかな。 GPU側に総メモリのうち 2/3 が割り振られたとしたら 21GB のGPUメモリということになる。20G程度の NVIDIA GPUとなると相当なお値段になるはず。 今検索したところによれば RTX 4090 24GB で 30万円を超える。
mac mini であれば(たぶん)静か、かつ設置スペースが少なくてすむ。 シリコン版の Pytorch の制約があると困るが、それの検証をするにも、 今の 8GB macbook air M1 ではどうにもならない。