llm-jp-4-33b-thinking-nvfp4
bf16で約62GiBあったモデルが 20.1GiB になり、RTX 5090 (32GB) 1枚で動作します。
RTX 5090・--max-model-len 16384 の実測で、KVキャッシュに 6.09GiB(24,944トークン分)を確保できました。
[!IMPORTANT]
このモデルはHarmony応答形式を使う thinking モデルです。vLLMで正しく動かすには
--trust-remote-code と
専用のreasoning parserが必要です。
詳細は「
vLLMでの使い方」を必ずお読みください。
量子化仕様
| 項目 | 内容 |
|---|
| 量子化形式 | NVFP4 (nvfp4-pack-quantized) |
| 重み | FP4 (E2M1) / group size 16 / tensor_group / スケールはFP8 (E4M3) |
| 活性化 | FP4 (E2M1) / group size 16 / 実行時に動的量子化、グローバルスケールはキャリブレーション済み |
| 量子化対象 | 全ての Linear 層 |
| 除外 | lm_head(bf16のまま) |
| アルゴリズム | QuantizationModifier(RTN + 活性化スケールのキャリブレーション) |
| llm-compressor | 0.13.0 |
| compressed-tensors | 0.18.0 |
embed_tokens と lm_head はbf16のまま残しています(それぞれ約2.0GiB)。
動作環境
NVFP4は Blackwell世代 (sm_120以降) のネイティブFP4演算を前提とします。
| |
|---|
| 検証GPU | NVIDIA GeForce RTX 5090 (32GB, sm_120) |
| NVIDIA driver | 580.159.03 / CUDA 13.0 |
| vLLM | 0.27.1 |
| カーネル | FlashInfer b12x (SM120向けネイティブNVFP4 GEMM) |
sm_100系 (B100/B200) でも動作します。sm_120未満のGPUではvLLMがMarlinの
重みのみ量子化 (W4A16) にフォールバックするため、動作はしますが本来の速度は出ません。
起動時のログに次の行が出ていれば、ネイティブFP4演算が使われています。
Using FlashInferCutlassNvFp4LinearKernel for NVFP4 GEMM
RTX 5090での実測値(--max-model-len 16384、単一リクエスト):
| 項目 | 実測 |
|---|
| モデルサイズ | 20.1 GiB |
| 重みロード時間 | 2.4秒 |
| KVキャッシュ | 6.09 GiB (24,944トークン) |
| 生成速度 | 約66 tok/s |
長いコンテキストの同時リクエストを増やしたい場合は --kv-cache-dtype fp8 を付けると
KVキャッシュの容量がおよそ倍になります。
vLLMでの使い方
1. reasoning parser を用意する
llm-jp-4はOpenAI Harmony形式で応答しますが、トークナイザがHarmony公式実装と異なるため、
専用のパーサーが必要です。本リポジトリの
vllm_plugin/ に
llm-jp-4-cookbook 由来のファイルを同梱しています。
vllm_plugin/
├── llmjp4_harmony.py # Harmonyトークン列のパーサー
├── llmjp4_reasoning_parser.py # vLLM用 reasoning parser (`llmjp4` として登録)
└── example_cli.py # パーサーを登録してから vllm CLI を起動するラッパー
2. サーバーを起動する
1huggingface-cli download Holy-fox/llm-jp-4-33b-thinking-nvfp4 \
2 --local-dir ./llm-jp-4-33b-thinking-nvfp4
3
4cd ./llm-jp-4-33b-thinking-nvfp4
5
6PYTHONPATH=vllm_plugin python3 vllm_plugin/example_cli.py serve . \
7 --served-model-name llm-jp-4-33b-thinking \
8 --reasoning-parser llmjp4 \
9 --trust-remote-code \
10 --enable-auto-tool-choice --tool-call-parser hermes \
11 --max-model-len 16384 \
12 --gpu-memory-utilization 0.92 \
13 --host 0.0.0.0 --port 8000
(
--enable-auto-tool-choice はOpen WebUI等への対応用です。理由は
「
Open WebUI などから使う場合」を参照してください。)
Dockerで動かす場合:
1docker run --rm --gpus all -p 8000:8000 --ipc=host --shm-size=16g \
2 -e PYTHONPATH=/model/vllm_plugin \
3 -v "$PWD:/model:ro" \
4 --entrypoint python3 \
5 vllm/vllm-openai:v0.27.1 \
6 /model/vllm_plugin/example_cli.py serve /model \
7 --served-model-name llm-jp-4-33b-thinking \
8 --reasoning-parser llmjp4 \
9 --trust-remote-code \
10 --enable-auto-tool-choice --tool-call-parser hermes \
11 --max-model-len 16384 \
12 --gpu-memory-utilization 0.92 \
13 --host 0.0.0.0 --port 8000
--reasoning-parser llmjp4 を付けずに素の vllm serve を使うと、
<|channel|>analysis などの制御トークンがそのまま応答本文に混ざります。
3. リクエストを送る
[!WARNING]
必ず stream: true を使ってください。
現在のvLLMのインターフェース上の制約で、非ストリーミングでは思考内容(reasoning)と
最終回答(content)を分離できません。これはcookbookにも記載されている既知の制限です。
1curl http://localhost:8000/v1/chat/completions \
2 -H "Content-Type: application/json" \
3 -d '{
4 "model": "llm-jp-4-33b-thinking",
5 "messages": [{"role": "user", "content": "二次方程式の解の公式を導出して下さい。"}],
6 "stream": true,
7 "max_tokens": 4096,
8 "temperature": 0.7,
9 "top_p": 0.9
10 }'
Python (OpenAIクライアント):
1from openai import OpenAI
2
3client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
4
5stream = client.chat.completions.create(
6 model="llm-jp-4-33b-thinking",
7 messages=[{"role": "user", "content": "日本の四季について簡潔に説明してください。"}],
8 stream=True,
9 max_tokens=4096,
10 temperature=0.7,
11 top_p=0.9,
12)
13
14reasoning, content = [], []
15for chunk in stream:
16 delta = chunk.choices[0].delta
17 if getattr(delta, "reasoning", None):
18 reasoning.append(delta.reasoning)
19 if delta.content:
20 content.append(delta.content)
21
22print("--- 思考過程 (analysis) ---")
23print("".join(reasoning))
24print("\n--- 最終回答 (final) ---")
25print("".join(content))
4. Open WebUI などから使う場合
Open WebUI で Function Calling を Native にしていると tool_choice: "auto" が送られ、
vLLM が次のエラーを返します。
"auto" tool choice requires --enable-auto-tool-choice and --tool-call-parser to be set
サーバー起動時に以下を追加すればエラーは解消し、通常のチャットは問題なく動作します
(思考チャンネルの分離にも影響しません)。
--enable-auto-tool-choice --tool-call-parser hermes
[!NOTE]
これはエラーを回避するための措置で、ツール呼び出し自体は機能しません。
llm-jp-4はHarmonyのcommentaryチャンネルでツールを呼びますが、
vLLM 0.27.1に同梱のツールパーサーはどれもこの形式を解釈できません
(gpt-oss用の openai パーサーは実体のないスタブです)。
このフラグを付けた状態でツールを渡すと、呼び出しが生のJSONテキストとして本文に出力されます。
ツールを実際に使いたい場合は、Open WebUI側の Function Calling を Default
(プロンプトベース)にしてください。
5. 推論の深さ (reasoning effort) を変える
チャットテンプレートは reasoning_effort を受け取ります(low / medium / high、既定は medium)。
OpenAI互換APIからは chat_template_kwargs で指定します。
1{
2 "model": "llm-jp-4-33b-thinking",
3 "messages": [{"role": "user", "content": "..."}],
4 "chat_template_kwargs": {"reasoning_effort": "high"},
5 "stream": true
6}
オフライン推論 (vLLMのPython API)
1from vllm import LLM, SamplingParams
2import sys
3sys.path.insert(0, "vllm_plugin")
4from llmjp4_harmony import HarmonyMessageParser
5
6llm = LLM(model=".", trust_remote_code=True, max_model_len=16384)
7tokenizer = llm.get_tokenizer()
8
9prompt = tokenizer.apply_chat_template(
10 [{"role": "user", "content": "日本語で自己紹介してください。"}],
11 tokenize=False,
12 add_generation_prompt=True,
13 reasoning_effort="medium",
14)
15
16out = llm.generate([prompt], SamplingParams(max_tokens=2048, temperature=0.7, top_p=0.9))
17token_ids = out[0].outputs[0].token_ids
18
19# NOTE: out[0].outputs[0].text は使わないでください(空白の扱いが崩れます)。
20# 必ずトークナイザ側の decode を通してください。
21parser = HarmonyMessageParser(tokenizer)
22for msg in parser.iter_messages(token_ids):
23 if msg.channel and msg.content:
24 channel = tokenizer.decode(msg.channel.token_ids)
25 text = tokenizer.decode(msg.content.token_ids)
26 print(f"[{channel}] {text}")
llm-jp-4を扱う上での注意点
--trust-remote-code は必須です。 本モデルは llmjp4_tokenizer.py(LlamaTokenizerFast の
サブクラス)を同梱しています。これはHarmonyの制御トークンとテキストトークンを分けてデコードし、
SentencePieceの空白まわりの既知の問題
(1,
2) を回避するためのものです。
- 生の出力テキストをそのまま使わないでください。 上記の理由から、必ず同梱トークナイザの
decode を通してください。
eos_token は <|endoftext|> ではなく <|return|> (id=2) です。
openai-harmony ライブラリでは本モデルをトークナイズできません(語彙が異なります)。
キャリブレーション
キャリブレーションには
llm-jp/llm-jp-4-33b-thinking-dpo-data
の
chosen(選択された)側 のみを使用し、
2,046サンプルを抽出しました。
思考過程(CoT)込みのHarmony形式で入力
各サンプルはモデル自身のチャットテンプレートで、学習時と同じ完全なHarmony形式に展開しています。
これにより、最終回答だけでなく analysisチャンネル(思考過程) の活性化統計も取得できます。
<|start|>system<|message|>...<|end|>
<|start|>user<|message|>...<|end|>
<|start|>assistant<|channel|>analysis<|message|>{chosen_analysis}<|end|>
<|start|>assistant<|channel|>final<|message|>{chosen_final}<|return|>
なお、マルチターン会話では過去ターンの思考過程は落としています(推論時のチャットテンプレートの
挙動に合わせるため)。対象ターンのCoTは常に保持しています。
サンプルの内訳
活性化統計が入力空間の一部に偏らないよう、3軸で層化して抽出しました。
| 軸 | 方針 | 結果 |
|---|
| 推論の深さ | reasoning_low / medium / high を均等に | 各33.3% |
| 言語 | かな・漢字の文字比率でサンプル毎に判定し 日本語:英語 = 6:4 | ja 60.0% / en 40.0% |
| データソース | 14サブセット全てを各バケット内でラウンドロビン | 全14種、各5.2〜10.5% |
14サブセットの内訳は daring_anteater / flan / jaster_v1.4.1 /
llmjp_extraction_wiki_ja_v0.1 / llmjp_extraction_wiki_ja_v0.2 / llmjp_magpie_sft_v1.0 /
logical_math_coding_wizard8x22b / multiturn_calm3 / nemotron_post_v2_stem /
nemotron_post_v3_chat / nemotron_post_v3_if / nemotron_post_v3_math /
random_to_fixed_multiturn_calm3 / synthetic_jp_en_coding です。
コードブロックを含むサンプルは全体の38.0%でした。
系列長の上限は3,072トークン、合計 5,622,914トークン を使用しています。
量子化の実行
33Bをbf16で読むと62GiB必要で32GBのGPUには載らないため、pipeline="sequential" を使い、
重みをCPU RAMに置いてデコーダ層を1層ずつGPUへ載せながら量子化しています。
1from llmcompressor import oneshot
2from llmcompressor.modifiers.quantization import QuantizationModifier
3
4recipe = QuantizationModifier(targets="Linear", scheme="NVFP4", ignore=["lm_head"])
5
6oneshot(
7 model=model, # CPU上にbf16でロード
8 dataset=ds,
9 recipe=recipe,
10 pipeline="sequential",
11 sequential_prefetch=True,
12 max_seq_length=3072,
13 num_calibration_samples=2046,
14)
実行時間はRTX 5090で約68秒/サブグラフ x 65サブグラフ = 約73分でした(VRAM使用量は約4GB)。
ライセンス
Apache License 2.0(元モデルおよびcookbookに従います)。
謝辞
- LLM-jp / 国立情報学研究所 — 元モデル
llm-jp-4-33b-thinking
- llm-jp/llm-jp-4-cookbook (Yusuke Oda) —
vllm_plugin/ 配下のreasoning parser
- vLLM Project — llm-compressor