見出し画像

WSL2でllama-cpp-pythonを試してみる

未来の私のために、備忘録です。

使用するPCはドスパラさんの「GALLERIA UL9C-R49」。スペックは
・CPU: Intel® Core™ i9-13900HX Processor
・Mem: 64 GB
・GPU: NVIDIA® GeForce RTX™ 4090 Laptop GPU(16GB)
・GPU: NVIDIA® GeForce RTX™ 4090 (24GB)
・OS: Ubuntu22.04 on WSL2(Windows 11)
です。


1. 準備

venv環境の構築

python -m venv llama.cpp
cd $_
source bin/activate

パッケージのインストール

cuBALSを有効にするために、

CMAKE_ARGS="-DLLAMA_CUBLAS=on" pip install llama-cpp-python

でインストールします。アップグレードするときは

CMAKE_ARGS="-DLLAMA_CUBLAS=on" pip install llama-cpp-python --upgrade --force-reinstall --no-cache-dir

です。

2. コードを作る

llama-cpp-python向けにいつものコードを修正します。修正ポイントを説明してから、コードのまとめをコピペします。

修正ポイントの解説

import sys
import argparse
from huggingface_hub import hf_hub_download
from llama_cpp import Llama
from transformers import AutoTokenizer
from typing import List, Dict
import time

# argv
parser = argparse.ArgumentParser()
parser.add_argument("--model-path", type=str, default=None)
parser.add_argument("--no-instruct", action='store_true')
parser.add_argument("--no-use-system-prompt", action='store_true')
parser.add_argument("--max-tokens", type=int, default=256)
parser.add_argument("--ggml-model-path", type=str, default=None)
parser.add_argument("--ggml-model-file", type=str, default=None)
parser.add_argument("--n-ctx", type=int, default=2048)
parser.add_argument("--n-threads", type=int, default=1)
parser.add_argument("--n-gpu-layers", type=int, default=-1)

args = parser.parse_args(sys.argv[1:])

## check and set args
model_id = args.model_path
if model_id == None:
    exit
if args.ggml_model_path == None:
    exit
if args.ggml_model_file == None:
    exit

is_instruct = not args.no_instruct
use_system_prompt = not args.no_use_system_prompt
max_new_tokens = args.max_tokens
n_ctx = args.n_ctx
n_threads = args.n_threads
n_gpu_layers = args.n_gpu_layers

量子化関連の引数を追加しています。
ggml-model-path: 量子化モデルのパス
ggml-model-file: 量子化モデルのパスにあるどのファイルを使用するか
n-ctx: 出力する最大トークン数
n-threads: 推論時に使用するCPUスレッド数。GPUにオフロードする前提なのでデフォルトは1にしています
n-gpu-layers: GPUにどんだけオフロードするか。デフォルト-1だと全部という意味です、はい

## Instantiate tokenizer from base model
tokenizer = AutoTokenizer.from_pretrained(
    model_id,
    trust_remote_code=True
)

チャットテンプレートを使用したいので、AutoTokenizerを使用しています。
指定するのは、量子化モデルではなく、ベースのモデル名です。
llama-cpp-pythonの llama_cpp/llama_chat_format.py 付近をきちんと読み込めばいいのでしょうが、時間も無いのでこれでお茶を濁しています。


補足。
Llamaクラスを初期化するときに chat_format を指定すれば良い。このformatは以下のいずれかから選択し、指定することになる。

$ find . -type f -name '*.py' | xargs grep register_chat_format
./llama_cpp/llama_chat_format.py:def register_chat_format(name: str):
./llama_cpp/llama_chat_format.py:@register_chat_format("llama-2")
./llama_cpp/llama_chat_format.py:@register_chat_format("alpaca")
./llama_cpp/llama_chat_format.py:@register_chat_format("qwen")
./llama_cpp/llama_chat_format.py:@register_chat_format("vicuna")
./llama_cpp/llama_chat_format.py:@register_chat_format("oasst_llama")
./llama_cpp/llama_chat_format.py:@register_chat_format("baichuan-2")
./llama_cpp/llama_chat_format.py:@register_chat_format("baichuan")
./llama_cpp/llama_chat_format.py:@register_chat_format("openbuddy")
./llama_cpp/llama_chat_format.py:@register_chat_format("redpajama-incite")
./llama_cpp/llama_chat_format.py:@register_chat_format("snoozy")
./llama_cpp/llama_chat_format.py:@register_chat_format("phind")
./llama_cpp/llama_chat_format.py:@register_chat_format("intel")
./llama_cpp/llama_chat_format.py:@register_chat_format("open-orca")
./llama_cpp/llama_chat_format.py:@register_chat_format("mistrallite")
./llama_cpp/llama_chat_format.py:@register_chat_format("zephyr")
./llama_cpp/llama_chat_format.py:@register_chat_format("pygmalion")
./llama_cpp/llama_chat_format.py:@register_chat_format("chatml")
./llama_cpp/llama_chat_format.py:@register_chat_format("mistral-instruct")
./llama_cpp/llama_chat_format.py:@register_chat_format("chatglm3")
./llama_cpp/llama_chat_format.py:@register_chat_format("openchat")
./llama_cpp/llama_chat_format.py:@register_chat_format("saiga")
$

これ以外のものを使用する場合は、自分でregister_chat_formatやら何やらで初期設定せにゃならんのだが、それはとても面倒なので今回はAutoTokenizerでお茶を濁している。
このchat_formatの機構が使用できれば、create_completionメソッドではなくcreate_chat_completionメソッドが使用できてエレガント!なのだが、仕方ない。

KARAKURI LMのように[ATTR] …[/ATTR] の指定が重要となってくる場合、それを使用しないと適切な結果が得られないのです。とはいっても、このregister_chat_formatにKARAKURI LM対応のchat_formatが追加されるまで「試してみる」を待つわけにもいかんし。


## Download the GGUF model
ggml_model_path = hf_hub_download(
    args.ggml_model_path,
    filename=args.ggml_model_file
)

## Instantiate model from downloaded file
model = Llama(
    model_path=ggml_model_path,
    n_ctx=n_ctx,
    n_threads=n_threads,
    n_gpu_layers=n_gpu_layers
)

指定した量子化モデルをHugging Faceからダウンロードし、インスタンス化します。

DEFAULT_SYSTEM_PROMPT = "あなたは誠実で優秀な日本人のアシスタントです。"

# generation params
# https://github.com/abetlen/llama-cpp-python/blob/main/llama_cpp/llama.py#L1268
generation_params = {
    #"do_sample": True,
    "temperature": 0.8,
    "top_p": 0.95,
    "top_k": 40,
    "max_tokens": max_new_tokens,
    "repeat_penalty": 1.1,
}

推論時に使用するパラメータを指定します。llama_cpp/llama.py#L1268 付近を見ると指定可能なパラメータとそのデフォルト値が列挙されているので、必要に応じて追加します。

def q(
    user_query: str,
    history: List[Dict[str, str]]=None
):
    start = time.process_time()
    # messages
    messages = ""
    if is_instruct:
        messages = []
        if use_system_prompt:
            messages = [
                {"role": "system", "content": DEFAULT_SYSTEM_PROMPT},
            ]
        user_messages = [
            {"role": "user", "content": user_query}
        ]
    else:
        user_messages = user_query
    if history:
        user_messages = history + user_messages
    messages += user_messages
    # generation prompts
    if is_instruct:
        prompt = tokenizer.apply_chat_template(
            conversation=messages,
            add_generation_prompt=True,
            tokenize=False
        )
    else:
        prompt = messages
    print("--- prompt")
    print(prompt)
    print("--- output")

ここまではこれまでと同じ。

    # 推論
    outputs = model.create_completion(
        prompt=prompt,
        #echo=True,
        #stream=True,
        **generation_params
    )
    #for output in outputs:
    #    print(output["choices"][0]["text"], end='')

入力プロンプトと推論結果は自力でトークン化せずに、呼び出し先に委せています。
stream=Trueとするとストリームになるのですが、トークン数をうまく取得できない(output["usage"]が出力されない)ので、お茶を濁しています。

参考までに、create_completionメソッドの戻り値はこんな感じです。

{
    'id': 'cmpl-aff63777-ae81-40ff-8cca-fad2bcd55b79',
    'object': 'text_completion',
    'created': 1707034843,
    'model': '/path/to/karakuri-lm-70b-chat-v0.1-q3_K_M.gguf',
    'choices': [
        {
            'text': ' ドラえもんは、日本の漫画家藤子・F・不二雄が(略)',
            'index': 0,
            'logprobs': None,
            'finish_reason': 'stop'
        }
    ],
    'usage': {
        'prompt_tokens': 91,
        'completion_tokens': 179,
        'total_tokens': 270
    }
}

さて、コードの続きです。

    output = outputs["choices"][0]["text"]
    print(output)
    if is_instruct:
        user_messages.append(
            {"role": "assistant", "content": output}
        )
    else:
        user_messages += output
    end = time.process_time()
    ##
    input_tokens = outputs["usage"]["prompt_tokens"]
    output_tokens = outputs["usage"]["completion_tokens"]
    total_time = end - start
    tps = output_tokens / total_time
    print(f"prompt tokens = {input_tokens:.7g}")
    print(f"output tokens = {output_tokens:.7g} ({tps:f} [tps])")
    print(f"   total time = {total_time:f} [s]")
    return user_messages

ouputs変数の usageに含まれているトークン数を使用して、秒あたりトークン数を算出しています。
なお、llama.cppがstderrに出力している秒あたりトークン数と異なる値となっているのですが、それは計測の開始タイミングが異なるためです。
(この処理では、q関数の先頭からの開始時間としています。なので、llama.cppの推論結果に対する値と比較すると小さくなっています。)

コードのまとめ

import sys
import argparse
from huggingface_hub import hf_hub_download
from llama_cpp import Llama
from transformers import AutoTokenizer
from typing import List, Dict
import time

# argv
parser = argparse.ArgumentParser()
parser.add_argument("--model-path", type=str, default=None)
parser.add_argument("--ggml-model-path", type=str, default=None)
parser.add_argument("--ggml-model-file", type=str, default=None)
parser.add_argument("--no-instruct", action='store_true')
parser.add_argument("--no-use-system-prompt", action='store_true')
parser.add_argument("--max-tokens", type=int, default=256)
parser.add_argument("--n-ctx", type=int, default=2048)
parser.add_argument("--n-threads", type=int, default=1)
parser.add_argument("--n-gpu-layers", type=int, default=-1)

args = parser.parse_args(sys.argv[1:])

## check and set args
model_id = args.model_path
if model_id == None:
    exit
if args.ggml_model_path == None:
    exit
if args.ggml_model_file == None:
    exit

is_instruct = not args.no_instruct
use_system_prompt = not args.no_use_system_prompt
max_new_tokens = args.max_tokens
n_ctx = args.n_ctx
n_threads = args.n_threads
n_gpu_layers = args.n_gpu_layers

## Instantiate tokenizer from base model
tokenizer = AutoTokenizer.from_pretrained(
    model_id,
    trust_remote_code=True
)

## Download the GGUF model
ggml_model_path = hf_hub_download(
    args.ggml_model_path,
    filename=args.ggml_model_file
)

## Instantiate model from downloaded file
model = Llama(
    model_path=ggml_model_path,
    n_ctx=n_ctx,
    n_threads=n_threads,
    n_gpu_layers=n_gpu_layers
)

DEFAULT_SYSTEM_PROMPT = "あなたは誠実で優秀な日本人のアシスタントです。"

# generation params
# https://github.com/abetlen/llama-cpp-python/blob/main/llama_cpp/llama.py#L1268
generation_params = {
    #"do_sample": True,
    "temperature": 0.8,
    "top_p": 0.95,
    "top_k": 40,
    "max_tokens": max_new_tokens,
    "repeat_penalty": 1.1,
}


def q(
    user_query: str,
    history: List[Dict[str, str]]=None
):
    start = time.process_time()
    # messages
    messages = ""
    if is_instruct:
        messages = []
        if use_system_prompt:
            messages = [
                {"role": "system", "content": DEFAULT_SYSTEM_PROMPT},
            ]
        user_messages = [
            {"role": "user", "content": user_query}
        ]
    else:
        user_messages = user_query
    if history:
        user_messages = history + user_messages
    messages += user_messages
    # generation prompts
    if is_instruct:
        prompt = tokenizer.apply_chat_template(
            conversation=messages,
            add_generation_prompt=True,
            tokenize=False
        )
    else:
        prompt = messages
    print("--- prompt")
    print(prompt)
    print("--- output")
    # 推論
    outputs = model.create_completion(
        prompt=prompt,
        #echo=True,
        #stream=True,
        **generation_params
    )
    #for output in outputs:
    #    print(output["choices"][0]["text"], end='')
    output = outputs["choices"][0]["text"]
    print(output)
    if is_instruct:
        user_messages.append(
            {"role": "assistant", "content": output}
        )
    else:
        user_messages += output
    end = time.process_time()
    ##
    input_tokens = outputs["usage"]["prompt_tokens"]
    output_tokens = outputs["usage"]["completion_tokens"]
    total_time = end - start
    tps = output_tokens / total_time
    print(f"prompt tokens = {input_tokens:.7g}")
    print(f"output tokens = {output_tokens:.7g} ({tps:f} [tps])")
    print(f"   total time = {total_time:f} [s]")
    return user_messages

print('history = ""')
print('history = q("ドラえもんとはなにか")')
print('history = q("続きを教えてください", history)')

3. 実行してみる

使用するモデル

KARAKURI MLの量子化モデルです。

チャットテンプレートは各モデルが提供する tokenizer_config.json を使用したいため、ベースとなるモデルのパスも指定します。今回ですとKARAKURI LMです。

実行する

前節のコードをファイルに保存して実行します。
ここでは query4llama-cpp.py という名前で保存しています。以下の引数で指定します。

python -i query4llama-cpp.py \
	--model-path karakuri-ai/karakuri-lm-70b-chat-v0.1 \
	--ggml-model-path mmnga/karakuri-lm-70b-chat-v0.1-gguf \
	--ggml-model-file karakuri-lm-70b-chat-v0.1-q3_K_M.gguf

以下のようにstderrをリダイレクトすると、llama.cppの出力が表示されなくなります。

python -i query4llama-cpp.py \
	--model-path karakuri-ai/karakuri-lm-70b-chat-v0.1 \
	--ggml-model-path mmnga/karakuri-lm-70b-chat-v0.1-gguf \
	--ggml-model-file karakuri-lm-70b-chat-v0.1-q3_K_M.gguf 2> /dev/null

プロンプト入力までに出力されるログはこんな感じ。

ggml_init_cublas: GGML_CUDA_FORCE_MMQ:   no
ggml_init_cublas: CUDA_USE_TENSOR_CORES: yes
ggml_init_cublas: found 2 CUDA devices:
  Device 0: NVIDIA GeForce RTX 4090 Laptop GPU, compute capability 8.9, VMM: yes
  Device 1: NVIDIA GeForce RTX 4090, compute capability 8.9, VMM: yes
llama_model_loader: loaded meta data with 23 key-value pairs and 723 tensors from /path/to/karakuri-lm-70b-chat-v0.1-q3_K_M.gguf (version GGUF V3 (latest))
llama_model_loader: Dumping metadata keys/values. Note: KV overrides do not apply in this output.
llama_model_loader: - kv   0:                       general.architecture str              = llama
llama_model_loader: - kv   1:                               general.name str              = LLaMA v2
llama_model_loader: - kv   2:                       llama.context_length u32              = 4096
llama_model_loader: - kv   3:                     llama.embedding_length u32              = 8192
llama_model_loader: - kv   4:                          llama.block_count u32              = 80
llama_model_loader: - kv   5:                  llama.feed_forward_length u32              = 28672
llama_model_loader: - kv   6:                 llama.rope.dimension_count u32              = 128
llama_model_loader: - kv   7:                 llama.attention.head_count u32              = 64
llama_model_loader: - kv   8:              llama.attention.head_count_kv u32              = 8
llama_model_loader: - kv   9:     llama.attention.layer_norm_rms_epsilon f32              = 0.000010
llama_model_loader: - kv  10:                       llama.rope.freq_base f32              = 10000.000000
llama_model_loader: - kv  11:                          general.file_type u32              = 12
llama_model_loader: - kv  12:                       tokenizer.ggml.model str              = llama
llama_model_loader: - kv  13:                      tokenizer.ggml.tokens arr[str,45416]   = ["<unk>", "<s>", "</s>", "<0x00>", "<...
llama_model_loader: - kv  14:                      tokenizer.ggml.scores arr[f32,45416]   = [-1000.000000, -1000.000000, -1000.00...
llama_model_loader: - kv  15:                  tokenizer.ggml.token_type arr[i32,45416]   = [3, 3, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, ...
llama_model_loader: - kv  16:                tokenizer.ggml.bos_token_id u32              = 1
llama_model_loader: - kv  17:                tokenizer.ggml.eos_token_id u32              = 2
llama_model_loader: - kv  18:            tokenizer.ggml.unknown_token_id u32              = 0
llama_model_loader: - kv  19:               tokenizer.ggml.add_bos_token bool             = true
llama_model_loader: - kv  20:               tokenizer.ggml.add_eos_token bool             = false
llama_model_loader: - kv  21:                    tokenizer.chat_template str              = {% if messages[0]['role'] == 'system'...
llama_model_loader: - kv  22:               general.quantization_version u32              = 2
llama_model_loader: - type  f32:  161 tensors
llama_model_loader: - type q3_K:  321 tensors
llama_model_loader: - type q4_K:  155 tensors
llama_model_loader: - type q5_K:   85 tensors
llama_model_loader: - type q6_K:    1 tensors
llm_load_vocab: special tokens definition check successful ( 259/45416 ).
llm_load_print_meta: format           = GGUF V3 (latest)
llm_load_print_meta: arch             = llama
llm_load_print_meta: vocab type       = SPM
llm_load_print_meta: n_vocab          = 45416
llm_load_print_meta: n_merges         = 0
llm_load_print_meta: n_ctx_train      = 4096
llm_load_print_meta: n_embd           = 8192
llm_load_print_meta: n_head           = 64
llm_load_print_meta: n_head_kv        = 8
llm_load_print_meta: n_layer          = 80
llm_load_print_meta: n_rot            = 128
llm_load_print_meta: n_embd_head_k    = 128
llm_load_print_meta: n_embd_head_v    = 128
llm_load_print_meta: n_gqa            = 8
llm_load_print_meta: n_embd_k_gqa     = 1024
llm_load_print_meta: n_embd_v_gqa     = 1024
llm_load_print_meta: f_norm_eps       = 0.0e+00
llm_load_print_meta: f_norm_rms_eps   = 1.0e-05
llm_load_print_meta: f_clamp_kqv      = 0.0e+00
llm_load_print_meta: f_max_alibi_bias = 0.0e+00
llm_load_print_meta: n_ff             = 28672
llm_load_print_meta: n_expert         = 0
llm_load_print_meta: n_expert_used    = 0
llm_load_print_meta: rope scaling     = linear
llm_load_print_meta: freq_base_train  = 10000.0
llm_load_print_meta: freq_scale_train = 1
llm_load_print_meta: n_yarn_orig_ctx  = 4096
llm_load_print_meta: rope_finetuned   = unknown
llm_load_print_meta: model type       = 70B
llm_load_print_meta: model ftype      = Q3_K - Medium
llm_load_print_meta: model params     = 69.20 B
llm_load_print_meta: model size       = 31.12 GiB (3.86 BPW)
llm_load_print_meta: general.name     = LLaMA v2
llm_load_print_meta: BOS token        = 1 '<s>'
llm_load_print_meta: EOS token        = 2 '</s>'
llm_load_print_meta: UNK token        = 0 '<unk>'
llm_load_print_meta: LF token         = 13 '<0x0A>'
llm_load_tensors: ggml ctx size =    0.83 MiB
llm_load_tensors: offloading 80 repeating layers to GPU
llm_load_tensors: offloading non-repeating layers to GPU
llm_load_tensors: offloaded 81/81 layers to GPU
llm_load_tensors:        CPU buffer size =   152.46 MiB
llm_load_tensors:      CUDA0 buffer size = 13043.00 MiB
llm_load_tensors:      CUDA1 buffer size = 18668.09 MiB
....................................................................................................
llama_new_context_with_model: n_ctx      = 2048
llama_new_context_with_model: freq_base  = 10000.0
llama_new_context_with_model: freq_scale = 1
llama_kv_cache_init:      CUDA0 KV buffer size =   264.00 MiB
llama_kv_cache_init:      CUDA1 KV buffer size =   376.00 MiB
llama_new_context_with_model: KV self size  =  640.00 MiB, K (f16):  320.00 MiB, V (f16):  320.00 MiB
llama_new_context_with_model:  CUDA_Host input buffer size   =    20.01 MiB
llama_new_context_with_model:      CUDA0 compute buffer size =   338.80 MiB
llama_new_context_with_model:      CUDA1 compute buffer size =   338.80 MiB
llama_new_context_with_model:  CUDA_Host compute buffer size =    17.60 MiB
llama_new_context_with_model: graph splits (measure): 5
AVX = 1 | AVX_VNNI = 1 | AVX2 = 1 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 1 | NEON = 0 | ARM_FMA = 0 | F16C = 1 | FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 1 | SSSE3 = 1 | VSX = 0 |
Model metadata: {'tokenizer.chat_template': "{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true and not '<<SYS>>' in messages[0]['content'] %}{% set loop_messages = messages %}{% set system_message = 'You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.\\n\\nIf a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don\\'t know the answer to a question, please don\\'t share false information.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if loop.index0 == 0 and system_message != false %}{% set content = '<<SYS>>\\n' + system_message + '\\n<</SYS>>\\n\\n' + message['content'] %}{% else %}{% set content = message['content'] %}{% endif %}{% if message['role'] == 'user' %}{{ bos_token + '[INST] ' + content.strip() + ' [ATTR] helpfulness: 4 correctness: 4 coherence: 4 complexity: 4 verbosity: 4 quality: 4 toxicity: 0 humor: 0 creativity: 0 [/ATTR] [/INST]' }}{% elif message['role'] == 'system' %}{{ '<<SYS>>\\n' + content.strip() + '\\n<</SYS>>\\n\\n' }}{% elif message['role'] == 'assistant' %}{{ ' '  + content.strip() + ' ' + eos_token }}{% endif %}{% endfor %}", 'tokenizer.ggml.add_eos_token': 'false', 'tokenizer.ggml.unknown_token_id': '0', 'tokenizer.ggml.eos_token_id': '2', 'general.architecture': 'llama', 'llama.rope.freq_base': '10000.000000', 'llama.context_length': '4096', 'general.name': 'LLaMA v2', 'tokenizer.ggml.add_bos_token': 'true', 'llama.embedding_length': '8192', 'llama.feed_forward_length': '28672', 'llama.attention.layer_norm_rms_epsilon': '0.000010', 'llama.rope.dimension_count': '128', 'tokenizer.ggml.bos_token_id': '1', 'llama.attention.head_count': '64', 'llama.block_count': '80', 'llama.attention.head_count_kv': '8', 'general.quantization_version': '2', 'tokenizer.ggml.model': 'llama', 'general.file_type': '12'}
Using chat template: {% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true and not '<<SYS>>' in messages[0]['content'] %}{% set loop_messages = messages %}{% set system_message = 'You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.\n\nIf a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don\'t know the answer to a question, please don\'t share false information.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if loop.index0 == 0 and system_message != false %}{% set content = '<<SYS>>\n' + system_message + '\n<</SYS>>\n\n' + message['content'] %}{% else %}{% set content = message['content'] %}{% endif %}{% if message['role'] == 'user' %}{{ bos_token + '[INST] ' + content.strip() + ' [ATTR] helpfulness: 4 correctness: 4 coherence: 4 complexity: 4 verbosity: 4 quality: 4 toxicity: 0 humor: 0 creativity: 0 [/ATTR] [/INST]' }}{% elif message['role'] == 'system' %}{{ '<<SYS>>\n' + content.strip() + '\n<</SYS>>\n\n' }}{% elif message['role'] == 'assistant' %}{{ ' '  + content.strip() + ' ' + eos_token }}{% endif %}{% endfor %}
Using chat eos_token:
Using chat bos_token:

llama.cppが出力しているログにあるModel metadataを見ると、chat_templateらしきものがあり、たしかにggufファイルをgrepするとファイル内に含まれているのですね。なるほど。

$ strings karakuri-lm-70b-chat-v0.1-q3_K_M.gguf | grep toxicity
{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true and not '<<SYS>>' in messages[0]['content'] %}{% set loop_messages = messages %}{% set system_message = 'You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.\n\nIf a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don\'t know the answer to a question, please don\'t share false information.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if loop.index0 == 0 and system_message != false %}{% set content = '<<SYS>>\n' + system_message + '\n<</SYS>>\n\n' + message['content'] %}{% else %}{% set content = message['content'] %}{% endif %}{% if message['role'] == 'user' %}{{ bos_token + '[INST] ' + content.strip() + ' [ATTR] helpfulness: 4 correctness: 4 coherence: 4 complexity: 4 verbosity: 4 quality: 4 toxicity: 0 humor: 0 creativity: 0 [/ATTR] [/INST]' }}{% elif message['role'] == 'system' %}{{ '<<SYS>>\n' + content.strip() + '\n<</SYS>>\n\n' }}{% elif message['role'] == 'assistant' %}{{ ' '  + content.strip() + ' ' + eos_token }}{% endif %}{% endfor %}

なので、以下のように参照できます。

>>> model.metadata["tokenizer.chat_template"]
"{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true and not '<<SYS>>' in messages[0]['content'] %}{% set loop_messages = messages %}{% set system_message = 'You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.\\n\\nIf a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don\\'t know the answer to a question, please don\\'t share false information.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if loop.index0 == 0 and system_message != false %}{% set content = '<<SYS>>\\n' + system_message + '\\n<</SYS>>\\n\\n' + message['content'] %}{% else %}{% set content = message['content'] %}{% endif %}{% if message['role'] == 'user' %}{{ bos_token + '[INST] ' + content.strip() + ' [ATTR] helpfulness: 4 correctness: 4 coherence: 4 complexity: 4 verbosity: 4 quality: 4 toxicity: 0 humor: 0 creativity: 0 [/ATTR] [/INST]' }}{% elif message['role'] == 'system' %}{{ '<<SYS>>\\n' + content.strip() + '\\n<</SYS>>\\n\\n' }}{% elif message['role'] == 'assistant' %}{{ ' '  + content.strip() + ' ' + eos_token }}{% endif %}{% endfor %}"
>>>

横道にそれました。

聞いてみる

>>> history = q("ドラえもんとはなにか")

とすると、

--- prompt
<s>[INST] <<SYS>>
あなたは誠実で優秀な日本人のアシスタントです。
<</SYS>>

ドラえもんとはなにか [ATTR] helpfulness: 4 correctness: 4 coherence: 4 complexity: 4 verbosity: 4 quality: 4 toxicity: 0 humor: 0 creativity: 0 [/ATTR] [/INST]
--- output

うまくテンプレート展開できています。

llama_print_timings:        load time =    1153.55 ms
llama_print_timings:      sample time =      31.95 ms /   151 runs   (    0.21 ms per token,  4725.84 tokens per second)
llama_print_timings: prompt eval time =    1151.12 ms /    91 tokens (   12.65 ms per token,    79.05 tokens per second)
llama_print_timings:        eval time =   12836.94 ms /   150 runs   (   85.58 ms per token,    11.69 tokens per second)
llama_print_timings:       total time =   14338.29 ms /   241 tokens

これはllama.cppの標準エラーへの出力内容。そして推論結果はこちら。

ドラえもんは、日本の漫画家藤子・F・不二雄(Fujiko F Fujio)によって描かれた漫画作品で、『月刊コロコロコミック』にて1970年1月号から2000年4月号にかけて連載されました。また、アニメ版は『ドラえもん』としてテレビ朝日系列で放送され、1973年4月1日から現在も放送されています。

物語は、未来の国から来たネコ型ロボットのドラえもんと小学生の野比のび太が繰り広げる日常を描いたギャグ・コメディ・SF漫画です。

この作品は、子供から大人まで幅広く親しまれ、日本の漫画・アニメの歴史を代表する作品の一つとなっています。また、日本だけでなく、世界にもファンが多く、多くの人に愛され続けています。
prompt tokens = 91
output tokens = 150 (10.683511 [tps])
total time = 14.040328 [s]

mmnga/karakuri-lm-70b-chat-v0.1-gguf/karakuri-lm-70b-chat-v0.1-q3_K_M.ggufより

良い感じ。



追記 - 2024/2/5

さらに追記 - 2024/2/7

chat_templateの件、カラクリさまにメールで問い合わせたところ、「都度必要である」旨のご回答をいただきました。

  • 学習プロセスにおいてそのように設定されているため。ユーザーの質問とアシスタントの回答のペアを複数ターンにわたって用意し、それぞれのペアに対して属性ラベルを事後的に付与している

ありがとうございます。
ですので、以下は戯れ言となるため、取り消し線にしております。


KARAKURI LMの tokenizer_config.json にあるchat_templateの定義ですと、
"role" == "user"の都度、[ATTR] ... [/ATTR] がプロンプトに追記され、プロンプトが長くなります。

条件式{% if loop.last %}を追加して[ATTR]の出力を制御するようにchat_templateを書き換えて試してみました(太字箇所)。以下がその定義。

"chat_template": "{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% elif false == true and not '<<SYS>>' in messages[0]['content'] %}{% set loop_messages = messages %}{% set system_message = 'You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.\\n\\nIf a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don\\'t know the answer to a question, please don\\'t share false information.' %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if loop.index0 == 0 and system_message != false %}{% set content = '<<SYS>>\\n' + system_message + '\\n<</SYS>>\\n\\n' + message['content'] %}{% else %}{% set content = message['content'] %}{% endif %}{% if message['role'] == 'user' %}{{ bos_token + '[INST] ' + content.strip() }}{% if loop.last %}{{ ' [ATTR] helpfulness: 4 correctness: 4 coherence: 4 complexity: 4 verbosity: 4 quality: 4 toxicity: 0 humor: 0 creativity: 0 [/ATTR]' }}{% endif %}{{ ' [/INST]' }}{% elif message['role'] == 'system' %}{{ '<<SYS>>\\n' + content.strip() + '\\n<</SYS>>\\n\\n' }}{% elif message['role'] == 'assistant' %}{{ ' ' + content.strip() + ' ' + eos_token }}{% endif %}{% endfor %}",

最後の1つだけに出力するように変更しても推論結果は(何十回か試したけれど)ブレませんでした。ブレないなら短い方が良いですよね。

この記事が気に入ったらサポートをしてみませんか?