rss_feed

詳細検索

expand_more
~
0
10000
最小
最大
Qwen3.8-27BをvLLM-Radianceで高速推論――R9700 1枚でPP 11,000t/s超・2並列時TG 100t/s超を記録

Qwen3.8-27BをvLLM-Radianceで高速推論――R9700 1枚でPP 11,000t/s超・2並列時TG 100t/s超を記録

16GBメモリ環境向けにモデルを分割する

今回のマシンはホストメモリが16GBしかありません。一方、配布モデルは単一のmodel.safetensorsが約19.8GBあるため、そのまま読み込んで変換・ロードしようとするとメモリ不足になり、読み込みが終わらなくなってしまいます。そこで、モデルを約2GiBを目安に複数シャードへ分割して対処します。

まず、Radianceイメージ内で必要なPythonライブラリを利用できることを確認します。

docker compose pull vllm
docker compose run --rm \
  --no-deps \
  --entrypoint python3 \
  vllm \
  -c 'import safetensors, torch; print("safetensors and torch: OK")'

以降の分割処理もRadianceコンテナ内で実行します。

分割スクリプトを作成する

リポジトリ直下にsplit_local_model.pyを作成し、次の内容を保存します。

import json
import os
import sys

from safetensors import safe_open
from safetensors.torch import save_file


if len(sys.argv) != 2:
    raise SystemExit(f"Usage: {sys.argv[0]} MODEL_DIR")

model_dir = os.path.abspath(sys.argv[1])
src = os.path.join(model_dir, "model.safetensors")
index_path = os.path.join(model_dir, "model.safetensors.index.json")
shard_max = 2 * 1024**3

if not os.path.isfile(src):
    raise SystemExit(f"Source not found: {src}")

existing_shards = [
    name
    for name in os.listdir(model_dir)
    if name.startswith("model-") and name.endswith(".safetensors")
]
if existing_shards or os.path.exists(index_path):
    raise SystemExit(
        "Existing shards or index found. Move or remove them before splitting."
    )

with safe_open(src, framework="pt", device="cpu") as source:
    metadata = source.metadata()
    keys = list(source.keys())
    temporary_shards = []
    current = {}
    current_size = 0
    weight_map = {}

    def flush():
        global current, current_size
        if not current:
            return
        shard_number = len(temporary_shards) + 1
        temporary_name = f"model-{shard_number:05d}.safetensors"
        temporary_path = os.path.join(model_dir, temporary_name)
        save_file(current, temporary_path, metadata=metadata)
        temporary_shards.append(temporary_path)
        for key in current:
            weight_map[key] = temporary_name
        print(
            "wrote",
            temporary_name,
            f"{current_size / 1024**3:.2f} GiB",
            flush=True,
        )
        current = {}
        current_size = 0

    for key in keys:
        tensor = source.get_tensor(key)
        tensor_size = tensor.numel() * tensor.element_size()
        if current and current_size + tensor_size > shard_max:
            flush()
        current[key] = tensor
        current_size += tensor_size

    flush()

shard_count = len(temporary_shards)
renamed_weight_map = {}

for old_path in temporary_shards:
    old_name = os.path.basename(old_path)
    shard_number = int(
        old_name.removeprefix("model-").removesuffix(".safetensors")
    )
    new_name = f"model-{shard_number:05d}-of-{shard_count:05d}.safetensors"
    new_path = os.path.join(model_dir, new_name)
    os.rename(old_path, new_path)
    for key, mapped_name in weight_map.items():
        if mapped_name == old_name:
            renamed_weight_map[key] = new_name

total_size = sum(
    os.path.getsize(os.path.join(model_dir, name))
    for name in set(renamed_weight_map.values())
)
index = {
    "metadata": {"total_size": total_size},
    "weight_map": renamed_weight_map,
}

with open(index_path, "w", encoding="utf-8") as output:
    json.dump(index, output, ensure_ascii=False, indent=2)
    output.write("\n")

print(f"created {index_path}")
print("The original model.safetensors has NOT been removed yet.")

このスクリプトはテンソル単位でシャードを作成します。単一テンソルが2GiBを超える場合、そのテンソルを含むシャードも2GiBを超えますが、異常ではありません。

コンテナ内で分割を実行する

作成したスクリプトをRadianceコンテナ内で実行し、単一のmodel.safetensorsを複数のシャードへ分割します。このComposeでは、/modelsが読み取り専用でマウントされるため、この時だけ一時的に同じホストディレクトリを/models-rwとして(読み書き可能な状態で)追加マウントします。

docker compose run --rm \
  --no-deps \
  --entrypoint python3 \
  -v "$PWD/models:/models-rw:rw" \
  vllm \
  /work/split_local_model.py \
  /models-rw/Qwen3.8-27B-Quark-AWQ-MXFP4

分割結果を検証する

ls -lh "$MODEL_DIR"/model*.safetensors
ls -lh "$MODEL_DIR/model.safetensors.index.json"

インデックスと実ファイルの対応もコンテナ内で検証します。下記コマンドで検証が可能です。

docker compose run --rm \
  --no-deps \
  --entrypoint python3 \
  vllm \
  - /models/Qwen3.8-27B-Quark-AWQ-MXFP4 <<'PY'
import json
import os
import sys

model_dir = sys.argv[1]
index_path = os.path.join(model_dir, "model.safetensors.index.json")

with open(index_path, encoding="utf-8") as file:
    index = json.load(file)

shards = sorted(set(index["weight_map"].values()))
missing = [
    name
    for name in shards
    if not os.path.isfile(os.path.join(model_dir, name))
]

print("tensor entries:", len(index["weight_map"]))
print("shard count:", len(shards))
print("missing shards:", missing)

if missing:
    raise SystemExit("validation failed")
PY

期待する状態は次のとおりです。

  • model-00001-of-0000N.safetensors形式の分割ファイルが複数存在する
  • model.safetensors.index.jsonが存在する
  • 検証結果にmissing shards: []と表示される

起動して動作を確認する

まず、Composeの展開結果を確認します。

docker compose config | grep -A3 -B3 'Qwen3.8-27B-Quark-AWQ-MXFP4'

モデルパスが/models/Qwen3.8-27B-Quark-AWQ-MXFP4になっていればOKです。下記のコマンドで起動します。なお、初回起動時はカーネル9のコンパイルなどが走り、15~30分くらいかかりますので、気長に待ちましょう。ログも眺めておくと良いでしょう。

docker compose up -d
docker compose logs -f vllm

Application startup complete.が表示されれば、モデルのロードとAPIの起動は完了です。続いて、別のターミナルからヘルスチェックを実行します。

curl -fsS http://localhost:11434/health && echo "health check: OK"

health check: OKと表示されたら、最後にOpenAI互換APIから実際に推論できることを確認します。無事に推論できれば、vLLM-Radianceの導入は成功です!

分割前のmodel.safetensorsを削除する

分割版で起動と推論の両方に成功した後は、分割元としてダウンロードした約19.8GBのmodel.safetensorsは不要になるため削除します。先に削除すると、分割や検証に失敗した際にモデルを再ダウンロードすることになるため、このタイミングで削除することとしています。

rm -- "$MODEL_DIR/model.safetensors"

test ! -e "$MODEL_DIR/model.safetensors" \
  && echo 'single-file weight removed'

まとめ

Radeon AI PRO R9700を1枚、しかもUSB4接続のeGPUとして使用する構成でも、vLLM-RadianceによってQwen3.8-27Bの推論を大幅に高速化できました。
特に大きかったのはprefillの改善で、llama.cppで課題となっていた長いコンテキストの待ち時間が解消され、約5分だったのが実用的な時間に短縮されています。

それから、16GBメモリ環境では、デカすぎる単一safetensorsの扱いがモデル読み込みのネックになっていましたが、モデルをシャード分割することで無事回避できました。同様にメモリが少ない環境でも再現しやすい設定例になっていると思います。

最後に、MTPの設定時に注意点を一つ。"disable_padded_drafter_batch":trueを入れ忘れたところGPUがハングする事態になりましたので、もしパラメータを変更する場合はご注意ください。

おまけ:11,000t/s超出てるログの一部
おまけ:11,000t/s超出てるログの一部

参考リンク

脚注について
このページの脚注:1件 / 記事全体:9件ページ 1件 / 全体 9件
  1. ここでのカーネルは、GPU上で実行される演算プログラム。コンパイル結果はキャッシュされるため、2回目以降の起動は短縮される ↩︎

コメントを投稿する

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です