0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RTX 4090 × 4枚で Qwen3.6-27B ファインチューン「Fable-Fusion-711」を vLLM (Docker) で動かす — ハイブリッド構成の KV 設計まで

0
Posted at

はじめに

Qwen3.6-27B のコミュニティファインチューンとして、DavidAU 氏の Qwen3.6-27B-Fable-Fusion-711 が公開されています。ARC-C で 0.711 を出し、ベースの Qwen3.6-27B を 7 指標中 6 つで上回った(残り 1 つは同点)と主張しているモデルです。

本記事では、このモデルを RTX 4090 24GB × 4枚(WSL2 + Docker)で BF16 フル精度・画像入力ありで動かす ところまでを扱います。

  • ModelScope ミラーからダウンロードするとファイルが足りず、そのままでは起動しません(実際に止まりました)
  • Qwen3.6 は 64 層中 48 層が線形アテンションというハイブリッド構成で、KV キャッシュの考え方が通常の 27B と根本的に違います。ここを理解しないとパラメータを間違えます
  • 起動スクリプトはそのままコピーして使える形で置いてあります

前回の Muse Glimmer 30B の記事 と同じマシンでの検証なので、あわせて読むと差分が見えると思います。

このモデルは Heretic による de-censoring(拒否応答の除去)が施されたバリアントです。作者の計測では拒否率がベースの 99/100 に対して 4/100 とされています。業務利用する場合は、ガードレールをアプリケーション側で別途用意することを前提にしてください。本記事はあくまでデプロイ手順の技術検証です。

1. モデルの構成を読む

1.1 基本情報

config.json から実際の値を拾うと以下のとおりです。

項目 内容
ベースモデル Qwen/Qwen3.6-27B
制作 DavidAU(多段ファインチューン)/ Nightmedia(マージ・ベンチ)/ TeichAI(Polaris データセット)/ armand0e(Fable トレース)/ trohrbaugh(Heretic 処理)
ライセンス Apache 2.0
architectures Qwen3_5ForConditionalGeneration
model_type qwen3_5
総パラメータ数 約 27B(Vision encoder 含む)
層数 64
Hidden dimension 5120
FFN 中間次元 17408
Attention パターン linear_attention × 3 → full_attention × 1 の繰り返し(full_attention_interval: 4
Attention heads (Q / KV) 24 / 4
Head dimension 256
partial_rotary_factor 0.25
線形アテンション Gated DeltaNet、key heads 16 / value heads 48、head dim 128、conv kernel 4
MTP mtp_num_hidden_layers: 1
Vision encoder depth 27、hidden 1152、patch size 16、spatial merge 2
語彙サイズ 248,320
コンテキスト長 262,144(rope_type: default、YaRN 不要)
BF16 チェックポイント 実測 51.7 GiB

1.2 ベンチマーク(作者公開値)

Nightmedia 氏による mxfp8 での計測値です。

モデル arc/c arc/e boolq hswag obkqa piqa wino
Fable-Fusion-711 0.711 0.879 0.910 0.790 0.514 0.823 0.763
Qwen3.6-27B-Instruct(ベース) 0.647 0.803 0.910 0.773 0.450 0.806 0.742
Qwen3.6-35B-A3B-Instruct 0.581 0.757 0.892 0.751 0.428 0.803 0.688

de-censoring 側の数値も公開されています。

指標 本モデル ベース Qwen3.6-27B
KL divergence 0.0469 0(定義上)
Refusals 4 / 100 99 / 100

第三者検証では、公開重みが作者の主張する 4/100 を再現しなかったという報告もあります。ベンチマーク値は「作者の主張」として受け取り、自分のユースケースで確認するのが妥当です。

1.3 ここが重要:64層中48層が線形アテンション

このモデルを扱ううえで一番効いてくるのがこの構造です。

config.jsonlayer_types を見ると、64 層のうち full_attention はわずか 16 層、残り 48 層は linear_attention(Gated DeltaNet)です。これが KV キャッシュの見積もりを通常の Dense モデルと全く別物にします。

KV キャッシュ(full attention 層のみ、トークン長に比例)

4 KV heads × 256 head_dim × 2 (K/V) × 2 byte = 4 KB / 層 / token
4 KB × 16 層 = 64 KB / token

27B クラスとしては破格に安いです。128K コンテキストでもたった 8 GiB です。

線形アテンションの状態(シーケンス単位、トークン長に非依存)

48 value heads × 128 × 128 × 4 byte (fp32) = 3 MiB / 層 / シーケンス
3 MiB × 48 層 = 約 144 MiB / シーケンス

つまりこのモデルは、

  • コンテキストを伸ばすのは安い
  • 同時実行数を増やすのは高い

という、通常の Transformer とは逆の経済性を持っています。--max-num-seqs を安易に上げてはいけない、というのが実務上の結論です。

2. 検証環境

項目 内容
GPU NVIDIA GeForce RTX 4090 24GB × 4
GPU 接続 PCIe のみ(NVLink なし、P2P 非対応)
OS Windows + WSL2 (Ubuntu)
ドライバ 591.86 / CUDA 13.1
コンテナ Docker(nvidia runtime 有効)
イメージ vllm/vllm-openai:v0.21.0
vLLM 0.21.0(Qwen3.6 は 0.19.0 以上が必要)

vLLM が 0.19.0 未満だと Gated DeltaNet 系のレイヤーを解決できません。前回 Muse Glimmer 用に使った :muse-glimmer タグの専用イメージは流用できないので注意してください。

3. 事前準備

3.1 GPU を空ける

BF16 51.7 GiB を TP=4 で分散するので、4枚とも空いている必要があります。

nvidia-smi
docker ps

私の環境では MinerU と OCR のコンテナが GPU0 / GPU3 を掴んでいたので止めました。

docker update --restart=no <container-name>
docker stop <container-name>

3.2 モデルのダウンロード

ModelScope のミラーから取得します。

pip install -U modelscope
modelscope download \
  --model Zoupers/Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-MTP \
  --local_dir /root/HuggingFaceCache/FF711-27B

WSL2 では /mnt/c/...(9p 経由)ではなく /root/...(ext4)に置いてください。読み込み速度が段違いです。

4. ハマりどころ

4.1 ModelScope ミラーには設定ファイルが3つ足りない

これが最大の落とし穴でした。起動するといきなり落ちます。

OSError: Can't load image processor for '/models/FF711-27B'.
... make sure '/models/FF711-27B' is the correct path to a directory
containing a preprocessor_config.json file

config.json の architectures が Qwen3_5ForConditionalGeneration(マルチモーダル)なので、vLLM は image processor をロードしにいきます。ところが ModelScope ミラー側にファイルがありません。HF 本家と比較すると以下が欠落していました。

ファイル ModelScope (Zoupers) HF (DavidAU)
preprocessor_config.json
video_preprocessor_config.json
vocab.json

重み側は問題ありません。model.safetensors.index.json を確認すると vision 系テンソルが 333 個ちゃんと入っています。純粋に設定ファイルの同期漏れです。

対処:この 3 ファイルは HF 本家とベースの Qwen3.6-27B で sha256 まで完全に一致 します(実際に照合しました)。ファインチューンで前処理パラメータは変わらないので、ModelScope 上の公式ベースから持ってくれば十分です。

cd /root/HuggingFaceCache
modelscope download --model Qwen/Qwen3.6-27B \
  preprocessor_config.json video_preprocessor_config.json vocab.json \
  --local_dir ./_qwen36_cfg
cp ./_qwen36_cfg/{preprocessor_config.json,video_preprocessor_config.json,vocab.json} \
   ./FF711-27B/

4.2 mmproj-*.gguf は不要

GGUF リポジトリ側に mmproj-BF16.gguf などが置いてあり、モデルカードにも「画像を使うには mmproj が必要」と書かれています。これに引っ張られそうになりますが、llama.cpp 系専用の話です。

GGUF 量子化では vision encoder が別ファイルに切り出されるため LM Studio や KoboldCpp では別途必要になりますが、safetensors + vLLM の経路では vision encoder は本体の重みに含まれています。ダウンロード不要です。

4.3 Prefix caching と Mamba 層の組み合わせは experimental

起動ログに出ます。

Mamba cache mode is set to 'align' for Qwen3_5ForConditionalGeneration
by default when prefix caching is enabled
Warning: Prefix caching in Mamba cache 'align' mode is currently enabled.
Its support for Mamba layers is experimental.

64 層中 48 層が線形アテンションなので、この experimental の重みは軽くありません。マルチターンで出力が微妙におかしくなった場合は、まず --enable-prefix-caching を外して切り分けてください。

4.4 無視してよい警告

[transformers] `Qwen2VLImageProcessorFast` is deprecated.

preprocessor_config.jsonimage_processor_type が旧名のまま。互換動作するので放置で問題ありません。

Unknown vLLM environment variable detected: VLLM_WSL2_ENABLE_PIN_MEMORY

→ 0.21.0 では変数名が整理されたようですが、WSL2 では Using 'pin_memory=False' as WSL is detected に落ちて正常に起動します。

5. KV キャッシュの実測

まず素直に起動して、実際に何がどれだけ確保されるかを見ます。

docker logs ff711-vllm 2>&1 | grep -iE "Available KV cache|GPU KV cache size|Maximum concurrency"

max_model_len=65536 / max_num_seqs=8 / gpu_memory_utilization=0.88 での結果です。

Available KV cache memory: 6.44 GiB
GPU KV cache size: 391,759 tokens
Maximum concurrency for 65,536 tokens per request: 5.98x

検算してみます。

6.44 GiB × 4枚         = 25.76 GiB
25.76 GiB ÷ 64 KB/token = 約 42.2 万 token(理論値)
実際の表示               = 39.2 万 token
差分                     = 約 1.9 GiB

この差分 1.9 GiB が、1.3 節で計算した線形アテンションのシーケンス状態(144 MiB × 8 seq ≒ 1.15 GiB)と conv state です。設定から予測した値と実測がきれいに一致したので、モデルの挙動理解は正しいと判断できます。

nvidia-smi 側はこうなります。

GPU0: 21801MiB / 24564MiB   ← ディスプレイ出力分で他より約 600MiB 多い
GPU1: 21225MiB / 24564MiB
GPU2: 21202MiB / 24564MiB
GPU3: 21202MiB / 24564MiB

推論中の GPU 使用率は 62〜64%、消費電力は 190W / 450W 程度でした。使用率が出ているのに電力が上がらないのは、PCIe 経由の allreduce 待ちで止まっている時間が長いためです。4090 は P2P 非対応なので、この構成では避けられません。

コンテキスト長の決め方

プールは 39.2 万トークンで固定なので、コンテキストを伸ばすと同時実行数と直接トレードオフします。

--max-model-len 理論同時実行数
65,536 5.98x
131,072 2.99x
262,144 1.49x

Qwen 公式は「思考能力を保つため 128K 以上を維持することを推奨」としています。加えて画像入力を使う場合、画像だけで数万トークンを消費します(後述)。私は 131,072 を選びました。

6. 起動スクリプト

~/myvllm/ff711-vllm.sh として保存し chmod +x してください。

#!/usr/bin/env bash
set -euo pipefail

MODEL_DIR="${MODEL_DIR:-/root/HuggingFaceCache/FF711-27B}"
IMAGE="${IMAGE:-vllm/vllm-openai:v0.21.0}"
SERVED_MODEL_NAME="${SERVED_MODEL_NAME:-ff711}"
PORT="${PORT:-8000}"
API_KEY="${API_KEY:-sk-dummy}"

# Fixed 4-GPU setup. BF16 weights measure 51.7GiB (12.9GiB/card at TP=4).
GPU_DEVICES="${GPU_DEVICES:-3,2,1,0}"
TP_SIZE="${TP_SIZE:-4}"

# Only 16 of 64 layers are full_attention -> ~64KB/token.
# Measured pool at util 0.88: 6.44GiB/card = 391,759 tokens.
MAX_MODEL_LEN="${MAX_MODEL_LEN:-131072}"

# GPU0 also drives the display (~600MiB, fluctuates). 0.90 is the ceiling.
GPU_MEMORY_UTILIZATION="${GPU_MEMORY_UTILIZATION:-0.88}"

# The 48 linear-attention layers hold a per-sequence recurrent state
# (~144MiB/seq) that does NOT shrink with context.
# Long context is cheap here; concurrency is not.
MAX_NUM_SEQS="${MAX_NUM_SEQS:-8}"
MAX_NUM_BATCHED_TOKENS="${MAX_NUM_BATCHED_TOKENS:-8192}"

# Empty on the first run; pin it afterwards from the startup log.
KV_CACHE_MEMORY="${KV_CACHE_MEMORY:-}"

LIMIT_MM_PER_PROMPT="${LIMIT_MM_PER_PROMPT:-{\"image\":3}}"

# MTP tensors ARE in model.safetensors.index.json, but acceptance rate
# after a multi-stage merge is unverified. Off by default.
ENABLE_MTP="${ENABLE_MTP:-false}"
NUM_SPECULATIVE_TOKENS="${NUM_SPECULATIVE_TOKENS:-2}"

ENABLE_TOOL_CALL="${ENABLE_TOOL_CALL:-true}"
# "Prefix caching in Mamba cache 'align' mode is experimental".
# If multi-turn output goes subtly wrong, set this to false FIRST.
ENABLE_PREFIX_CACHING="${ENABLE_PREFIX_CACHING:-true}"

if [ ! -d "${MODEL_DIR}" ]; then
  echo "ERROR: model directory not found: ${MODEL_DIR}"
  exit 1
fi

# The ModelScope mirror ships none of these. Pull them from Qwen/Qwen3.6-27B.
MISSING=()
for f in preprocessor_config.json video_preprocessor_config.json vocab.json; do
  [ -f "${MODEL_DIR}/${f}" ] || MISSING+=("$f")
done
if [ ${#MISSING[@]} -gt 0 ]; then
  echo "ERROR: missing config files in ${MODEL_DIR}: ${MISSING[*]}"
  exit 1
fi

VLLM_ARGS=(
  --model /models/FF711-27B
  --served-model-name "${SERVED_MODEL_NAME}"
  --trust-remote-code
  --tensor-parallel-size "${TP_SIZE}"
  --max-model-len "${MAX_MODEL_LEN}"
  --gpu-memory-utilization "${GPU_MEMORY_UTILIZATION}"
  --max-num-seqs "${MAX_NUM_SEQS}"
  --max-num-batched-tokens "${MAX_NUM_BATCHED_TOKENS}"
  --limit-mm-per-prompt "${LIMIT_MM_PER_PROMPT}"
  --generation-config auto
  --api-key "${API_KEY}"
  --host 0.0.0.0
  --port "${PORT}"
)

[ -n "${KV_CACHE_MEMORY}" ] && VLLM_ARGS+=(--kv-cache-memory "${KV_CACHE_MEMORY}")

if [ "${ENABLE_PREFIX_CACHING}" = "true" ]; then
  VLLM_ARGS+=(--enable-prefix-caching)
fi

if [ "${ENABLE_MTP}" = "true" ]; then
  VLLM_ARGS+=(--speculative-config \
    "{\"method\":\"qwen3_next_mtp\",\"num_speculative_tokens\":${NUM_SPECULATIVE_TOKENS}}")
fi

# Qwen3.6 thinks by default. No /think soft switch - clients disable it
# per request via chat_template_kwargs. No --chat-template: the checkpoint
# ships chat_template.jinja itself.
if [ "${ENABLE_TOOL_CALL}" = "true" ]; then
  VLLM_ARGS+=(
    --enable-auto-tool-choice
    --reasoning-parser qwen3
    --tool-call-parser qwen3_coder
  )
fi

docker run --rm -it \
  --name ff711-vllm \
  --gpus "\"device=${GPU_DEVICES}\"" \
  --network host \
  --ipc=host \
  --shm-size 32g \
  --ulimit memlock=-1 \
  --ulimit stack=67108864 \
  -v "${MODEL_DIR}:/models/FF711-27B:ro" \
  -e CUDA_DEVICE_ORDER=PCI_BUS_ID \
  -e CUDA_VISIBLE_DEVICES=0,1,2,3 \
  -e PYTORCH_NVML_BASED_CUDA_CHECK=1 \
  -e VLLM_WSL2_ENABLE_PIN_MEMORY=1 \
  -e TRANSFORMERS_OFFLINE=1 \
  -e HF_DATASETS_OFFLINE=1 \
  -e HF_HUB_OFFLINE=1 \
  -e HF_HOME=/root/HuggingFaceCache \
  -e HUGGINGFACE_HUB_CACHE=/root/HuggingFaceCache \
  -e VLLM_WORKER_MULTIPROC_METHOD=spawn \
  -e NCCL_P2P_DISABLE=1 \
  -e NCCL_IB_DISABLE=1 \
  -e NCCL_SHM_DISABLE=0 \
  "${IMAGE}" \
  "${VLLM_ARGS[@]}"

各設定の意図

設定 理由
--tensor-parallel-size 4 BF16 51.7GiB を4枚に分散。PP は 24GB カードでは OOM(Muse Glimmer で検証済み)
--max-model-len 131072 公式が 128K 以上を推奨。画像 3 枚で数万トークン消費するため 64K では足りない
--gpu-memory-utilization 0.88 GPU0 の画面出力分。0.90 に上げても約 3 万トークンしか増えず、リスクに見合わない
--max-num-seqs 8 線形アテンションのシーケンス状態が高い。4 に下げても 0.6GiB しか戻らないので 8 で妥協
--reasoning-parser qwen3 Qwen3.6 は既定で <think> を出力する
--tool-call-parser qwen3_coder Qwen 公式の vLLM レシピと同じ
--generation-config auto 同梱の generation_config.json(temperature 1.0 / top_p 0.95 / top_k 20)を採用
--trust-remote-code マージ由来の設定差分を吸収するため保険で付与
NCCL_P2P_DISABLE=1 4090 はハードウェアとして P2P 非対応

KV プールを固定する

初回起動でログの値を確認したら、そこから 0.15 GiB ほど引いた値を固定します。

KV_CACHE_MEMORY=6764573184   # 6.3 GiB

GPU0 のディスプレイ使用量は Windows 側の操作で変動するので、実測値ぴったりを入れると「ある日ブラウザを開いたら起動しなくなる」ことがあります。固定すると profiling をスキップして起動も速くなります。

7. 動作確認

7.1 テキスト

curl -s localhost:8000/v1/chat/completions \
  -H "Authorization: Bearer sk-dummy" \
  -H "Content-Type: application/json" -d '{
  "model": "ff711",
  "messages": [{"role": "user", "content": "RAG について日本語で説明してください。"}],
  "temperature": 1.0, "top_p": 0.95, "top_k": 20, "max_tokens": 1024
}'

7.2 思考モードの切り替え

Qwen3.6 は既定で思考します。/think /no_think のソフトスイッチは廃止されているので、リクエスト単位で指定します。

{
  "chat_template_kwargs": {"enable_thinking": false}
}

逆に、エージェント用途で過去ターンの思考を保持したい場合は Qwen3.6 の新機能が使えます。

{
  "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}
}

推論の一貫性が上がるうえ、同じ思考を繰り返さなくなるためトークン消費が減り、KV キャッシュのヒット率も改善するとされています。

7.3 サンプリングパラメータ

Qwen 公式推奨は用途で分かれます。--generation-config auto でサーバ側に入るのは思考モードの値なので、instruct 側はクライアントから明示的に送る必要があります。

用途 temperature top_p top_k presence_penalty
思考モード・汎用 1.0 0.95 20 0.0
思考モード・精密なコーディング 0.6 0.95 20 0.0
instruct(思考なし) 0.7 0.80 20 1.5

7.4 画像入力

curl -s localhost:8000/v1/chat/completions \
  -H "Authorization: Bearer sk-dummy" \
  -H "Content-Type: application/json" -d '{
  "model": "ff711",
  "messages": [{"role": "user", "content": [
    {"type": "image_url", "image_url": {"url": "https://example.com/sample.png"}},
    {"type": "text", "text": "この画像の内容を日本語で説明してください。"}
  ]}],
  "temperature": 1.0, "top_p": 0.95, "max_tokens": 1024
}'

8. 画像トークンの上限は必ず見直す

画像入力を使うなら、ここが一番効くチューニングポイントです。

preprocessor_config.json の既定値はこうなっています。

{
    "size": {"longest_edge": 16777216, "shortest_edge": 65536},
    "patch_size": 16,
    "merge_size": 2
}

patch 16・merge 2×2 なので、1 画像あたりのトークン数は ピクセル数 ÷ 1024 です。

longest_edge 1画像あたり 3画像合計
16,777,216(既定) 16,384 tok 49,152 tok
4,194,304 4,096 tok 12,288 tok
2,097,152 2,048 tok 6,144 tok

既定の 16M ピクセルは超巨大画像や長尺動画のための余裕であって、スクリーンショットや文書 1 ページには過剰です。しかも vLLM は起動時の profiling で最悪ケース分の encoder budget を予約するため、使っていなくても KV キャッシュを削られます。

cd /root/HuggingFaceCache/FF711-27B
python3 - << 'EOF'
import json
p='preprocessor_config.json'
d=json.load(open(p))
d['size']['longest_edge']=4194304
json.dump(d,open(p,'w'),indent=4)
EOF

--mm-processor-kwargs で渡す方法もありますが、ビルドによって受理されるキーが違うので、設定ファイルを直接書き換えるほうが確実です。文書 OCR 用途なら 8,388,608 くらいまでに留めるのが無難でしょう。

9. MTP(Multi-Token Prediction)について

このモデル名の末尾にある「MTP」は、Qwen3.6 の Multi-Token Prediction ヘッドを保持していることを指します。vLLM では投機デコードとして有効化できます。

--speculative-config '{"method":"qwen3_next_mtp","num_speculative_tokens":2}'

model.safetensors.index.json を確認したところ、mtp.layers.0.* が 15 テンソル分きちんと登録されていました(model-mtp-restored.safetensors に格納)。重みのロード自体は通るはずです。

ただし本記事では既定で無効にしています。

  • 多段ファインチューン・マージを経た後の受理率が未検証である
  • 作者自身が「temperature は 1 以下、repetition_penalty は 1(無効)を維持すること。受理率が 50% を下回るなら通常版のほうが速い」と明記している
  • 受理率が低い場合、投機デコードはエラーを出さずに単に遅くなるため、気づきにくい

有効化して使う場合は、必ず受理率をログで確認してから本採用してください。

10. 最終的なパラメータ

パラメータ 決定理由
並列方式 TP=4 PP は 24GB カードでは OOM
--max-model-len 131072 公式推奨の 128K 以上。画像分の消費も考慮
--gpu-memory-utilization 0.88 GPU0 の画面出力分の余裕
--max-num-seqs 8 線形アテンションのシーケンス状態が高価
--max-num-batched-tokens 8192 encoder budget との連動。下げすぎない
--limit-mm-per-prompt {"image":3} 要件が3枚。予約は最悪ケース基準なので増やさない
preprocessor_config.jsonlongest_edge 4,194,304 既定の 16M は過剰。KV への影響が最も大きい
--speculative-config 未使用 受理率が未検証

11. まとめ

  • RTX 4090 24GB × 4枚(WSL2 + Docker)で Qwen3.6-27B ベースの Fable-Fusion-711 は BF16 フル精度・画像入力ありで動きます
  • ModelScope ミラーは preprocessor_config.json / video_preprocessor_config.json / vocab.json が欠落しています。ベースの Qwen3.6-27B から持ってくれば sha256 まで一致するので安全に補完できます
  • GGUF 側の mmproj-*.gguf は llama.cpp 専用。safetensors + vLLM では不要です
  • 64層中48層が線形アテンションという構造が全てを決めます。KV は 64KB/token と安く長文に強い一方、シーケンス状態が 144MiB/seq と高いので同時実行数は上げられません
  • 実測 KV プールは 6.44 GiB/枚 = 391,759 token。設定から計算した理論値と 1.9 GiB の差があり、それが線形アテンションの状態分と一致しました
  • 画像を使うなら longest_edge の見直しが最優先。既定のままだと 1 画像で 16,384 トークンを予約します
  • vLLM は 0.19.0 以上が必須。Muse Glimmer 用の専用イメージは流用できません

スループットの実測とベースの Qwen3.6-27B との日本語比較は、次回まとめようと思います。

参考リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?