📝

Qwen3.5 を Databricks のノートブックで推論してみた

に公開

はじめに

Qwen3.5 は Alibaba Cloud が公開している大規模言語モデル(LLM)シリーズです。0.8B / 8B / 27B などサイズ別のバリエーションが揃い、Apache 2.0 ライセンスで商用利用も可能という点で実用価値が高く、Qwen 公式のモデルカードでは 201 言語・方言へのサポート拡大が掲げられています。日本語の AI メディアでも ASCII.jp の記事「9B なのに 120B 超え!? Qwen3.5-9B がローカル AI の常識を変えた」(田口和裕氏、2026/3/20)などで、サイズに対する品質の高さや日本語を含む多言語対応が紹介されるなど、注目を集めているモデルです。

いずれにしても、「Databricks のクラスタ上でどこまで素直に動かせるか?」を確かめてみたくなり、HuggingFace の model card にあるサンプルコードを Databricks のノートブックで試してみました。

Databricks ML Runtime は pandas / numpy / PyTorch / flash_attn など多数のパッケージがプリインストールされた環境です。一方で、vLLM は最新の PyTorch と独自のバージョン要件を持つ専門の推論エンジンで、両者を組み合わせるときには バージョンを揃えるための軽い調整がいくつか必要になります。この記事では、0.8B をきっかけに 5 つの整合調整ポイントを押さえていった過程と、それらがそのまま 27B にも流用できたという経験を紹介します。

本記事で紹介するコードの全文は GitHub リポジトリ1-1_qwen35_08b_vllm_local.py(0.8B)と 2-1_qwen35_27b_vllm_local.py(27B)として公開しています。


環境

項目
GPU インスタンス g5.12xlarge(NVIDIA A10G 24GB × 4)
Databricks Runtime 17.3 LTS ML(16.4 LTS ML でも動作確認済み)
推論エンジン vLLM 0.18.1
モデル精度 bfloat16
Tensor Parallelism 自動検出(torch.cuda.device_count()

0.8B は A10G 1 枚でも十分動きますが、後で 27B(bf16 約 54GB)も動かすことを見据えて最初からマルチ GPU 構成にしました。g5.12xlarge 1 ノードに A10G × 4 が載っている AWS の GPU インスタンスです。

ランタイムは 17.3 LTS ML を選択しています。これは本記事執筆時点(2026 年 4 月)で 最新の LTS(Long Term Support)バージョンです。なお、1 世代前の 16.4 LTS ML でも同じコードで動作することを確認しています。

vLLM は 0.18.1 を使用しました。Qwen3.5 シリーズ自体は vLLM v0.17.1 で公式サポートが入り、その後 0.18.0 で tool calling が追加、0.18.1 ではその tool calling 周りのバグ修正が入った安定パッチという位置づけです。本検証時点ではこれが Qwen3.5 系を安定して動かす最新の推奨バージョンだったため、0.18.1 を採用しています。


Step 1: まずは 0.8B を試す

HuggingFace の Qwen3.5-0.8B の model card に載っている vLLM サンプルコードは、こんな感じです:

from vllm import LLM, SamplingParams
llm = LLM(model="Qwen/Qwen3.5-0.8B", tensor_parallel_size=4, dtype="bfloat16")
out = llm.generate(["What is 2+2?"], SamplingParams(max_tokens=64))
print(out[0].outputs[0].text)

これを Databricks のノートブックに貼って %pip install vllm すると、vLLM と ML Runtime のそれぞれが持つパッケージ要件を揃えるため、5 つの調整を踏むことになります。どれも一度整理してしまえば以降の別モデル検証でも流用できるので、順番に紹介します。

ポイント 1: numpy ABI ミスマッチ

ValueError: numpy.dtype size changed, may indicate binary incompatibility.
Expected 96 from C header, got 88 from PyObject

ABI(Application Binary Interface)とは、コンパイル済みバイナリ同士が連携するための約束事です。Python のライブラリには C 拡張(高速化のためにコンパイルされた部分)があり、numpy のデータ構造サイズが変わると崩れます。

vLLM をインストールすると numpy 2.x が入りますが、Databricks ML Runtime にプリインストールされている pandas や OpenCV の C 拡張は numpy 1.x の ABI に基づいてコンパイルされています。この衝突が上のエラーの正体です。

解決策: numpy を 1.x に戻す。

%pip install "numpy<2.0" --force-reinstall --no-deps

ポイント 2: OpenCV バイナリの衝突

vLLM が依存パッケージとしてインストールする opencv-python-headless は numpy 2.x 向けにコンパイルされているため、問題 1 で numpy を 1.x に戻すと、今度は OpenCV の方が動かなくなります。

解決策: vLLM がインストールした OpenCV を削除。

%pip uninstall opencv-python-headless opencv-python -y

削除すると、ML Runtime に元々含まれている numpy 1.x 互換の OpenCV が Python のモジュール検索で自動的に見つかるので、問題は解消します。

ポイント 3: flash_attn のシンボル不整合

ImportError: flash_attn_2_cuda.cpython-312-x86_64-linux-gnu.so:
  undefined symbol: _ZN3c104cuda29c10_cuda_check_implementationEiPKcS2_ib

ML Runtime にプリインストールされている flash_attn は、Runtime 付属の PyTorch 用にコンパイルされています。vLLM は PyTorch 2.10.0 を持ち込むため、flash_attn のバイナリが呼ぼうとする PyTorch の内部関数名(シンボル)が新 PyTorch には存在せず、undefined symbol エラーになります。

ポイントは、flash_attn が Databricks がランタイムの一部として配置しているシステムディレクトリ(/databricks/python/lib/...)にあるため、ユーザー側の pip uninstall の管理範囲外であるという点です。ここでは 2 段構えで整合を取ります:

① ディスク上のフォルダをリネーム(ワーカープロセス向け)

vLLM はマルチ GPU 推論のためにワーカープロセスを新規起動します。これらのプロセスはディスクを直接参照するため、フォルダ名を変えてしまえば flash_attn は見つかりません。

import subprocess
subprocess.run(["bash", "-c",
    'for dir in /databricks/python/lib/python3.12/site-packages; do '
    '[ -d "$dir/flash_attn" ] && '
    'mv "$dir/flash_attn" "$dir/flash_attn.disabled"; done'])

② Python にダミーモジュールを登録(メインプロセス向け)

メインプロセスでは、import flash_attn された時点で sys.modules にダミーが登録されていれば、ディスクを見にいきません。

import sys, types
sys.modules["flash_attn"] = types.ModuleType("flash_attn")
# サブモジュールも同様にダミー登録

vLLM は flash_attn が使えないと判断すると、自動的に FlashInfer という別のアテンションバックエンドにフォールバックします。実用的な推論品質への影響は確認した限りありません。

ポイント 4: cv2 インポートエラー

Qwen3.5 は画像・動画も扱えるマルチモーダルモデルです。そのため vLLM は起動時に cv2 を読み込もうとします。テキスト推論だけの用途でもこの import が走るため、問題 2 で OpenCV を削除した状態ではエラーになります。

解決策: flash_attn と同じくダミーモジュールを登録。

cv2_mock = types.ModuleType("cv2")
cv2_mock.__spec__ = importlib.machinery.ModuleSpec("cv2", None)
sys.modules["cv2"] = cv2_mock

ポイント 5: カーネルハートビートタイムアウト

from vllm import LLMLLM(...) は、Triton カーネルのコンパイルやモデル重みのダウンロードで数分間メインスレッドをブロックします。Databricks のカーネル監視機構は約 5 分無応答のプロセスを自動で終了するため、素直にロードするとタイムアウトします。

解決策: モデルロードをバックグラウンドスレッドで実行し、メインスレッドはハートビートに応答できるようにする。

import threading
def load_model():
    global llm
    from vllm import LLM
    llm = LLM(model="Qwen/Qwen3.5-0.8B", tensor_parallel_size=4, dtype="bfloat16")

loader = threading.Thread(target=load_model)
loader.start()
while loader.is_alive():
    loader.join(timeout=30)  # 30 秒ごとにメインスレッドへ制御を戻す
    print(f"Loading... elapsed")

0.8B の推論成功

この 5 つの調整の後、ようやく Qwen3.5-0.8B がノートブック上で動きました。

# 質問: "What is 2+2?"
# 回答: "2 + 2 equals **4**."

# 質問: "日本の首都はどこですか?"
# 回答: "日本の首都は**東京**です。"

軽い質問なら 1〜2 秒で返ってきます。


Step 2: 同じコードで 27B も動かす

0.8B で動いたなら、次は自然と「27B でも動くだろうか?」が気になります。結論から言うと、モデル名を差し替えるだけで動きました。

# 変更点はこれだけ
llm = LLM(model="Qwen/Qwen3.5-27B", tensor_parallel_size=4, dtype="bfloat16")

Tensor Parallelism を torch.cuda.device_count() で自動検出しているため、4 枚の A10G に重みが 1/4 ずつシャーディングされ、1 枚あたり ~13.5GB ほどの VRAM 使用で収まりました。g5.12xlarge の 24GB × 4 = 96GB VRAM 構成が、そのまま 27B bf16(約 54GB)を受け止めてくれます。

27B 固有の追加調整は不要でした。0.8B のために整理した 5 つの調整(numpy、OpenCV、flash_attn、cv2、ハートビート)がそのままスケールアップ時にも使い回せます。


パフォーマンス(A10G × 4、bfloat16)

同じ 7 種類のプロンプトを、chat_template_kwargs={"enable_thinking": True/False} で 2 モードずつ(合計 14 run/モデル)、max_tokens=4096、warmup 後の単発計測で取りました。サンプリングは enable_thinking=False の時 temperature=0.7, top_p=0.8=True の時はモデルカード推奨の temperature=1.0, top_p=0.95, top_k=20, min_p=0.0, presence_penalty=1.5

Note: Qwen3.5 は Qwen3 時代の /think / /no_think soft switch を公式サポートしていません。thinking モードの切り替えには chat_template_kwargs={"enable_thinking": True/False}llm.chat() に渡す必要があります。

全 14 run 平均のスループット

モデル 平均 tok/s レンジ
Qwen3.5-0.8B ~33.8 28.3〜35.0
Qwen3.5-27B ~13.2 11.5〜13.6

tok/s は 0.8B が 27B の約 2.5 倍。モデルサイズ相応の差が素直に出ています。どちらもサンプリングモードに関わらず tok/s は安定していました。

プロンプトごとの実測値

プロンプト 0.8B think=False 0.8B think=True 27B think=False 27B think=True
"What is the capital of France?" 41 tok / 1.21 s 135 tok / 3.99 s 60 tok / 4.50 s 141 tok / 10.56 s
"What is 2+2?" 8 tok / 0.28 s 394 tok / 11.47 s 9 tok / 0.79 s 156 tok / 11.78 s
"2+2は?" 39 tok / 1.16 s 1,288 tok / 37.57 s 11 tok / 0.94 s 435 tok / 32.40 s
"Explain TCP vs UDP" 776 tok / 22.38 s 886 tok / 26.06 s 856 tok / 63.31 s 2,449 tok / 181.92 s
"TCPとUDPの違い" 903 tok / 25.80 s 2,755 tok / 80.44 s 1,119 tok / 82.37 s 2,365 tok / 175.34 s
"123*456+789?" 164 tok / 4.72 s 1,739 tok / 51.24 s 241 tok / 17.67 s 227 tok / 16.76 s
(logic puzzle) 261 tok / 7.62 s 4,055 tok / 118.82 s 344 tok / 25.32 s 1,469 tok / 108.72 s

同じ質問、違う応答 — thinking モード有無で劇的に変わる

"What is the capital of France?" を両モデル × 両モードで投げたときの応答全文:

Qwen3.5-0.8B, enable_thinking=False(41 トークン):

The capital of France is **Paris**.

It is the largest city in France, the largest city in Europe, and the seat of the
French government, parliament, and the President of the Republic.

Qwen3.5-27B, enable_thinking=False(60 トークン):

The capital of France is **Paris**.

It is the country's most populous city and serves as its center for politics,
culture, finance, and commerce. Located in the north-central part of the country
on the Seine River, Paris has been the capital since the 12th century.

Qwen3.5-27B, enable_thinking=True(141 トークン、thinking プロセス付き):

Thinking Process:

1.  **Identify the core question:** The user is asking "What is the capital of France?"
2.  **Access knowledge base:** Retrieve information about France's capital city.
3.  **Verify fact:** Paris is the capital and most populous city of France.
4.  **Formulate answer:** State the answer clearly and concisely.
5.  **Review constraints:** The user wants a helpful assistant response. No specific
    format required, so a brief, accurate answer is ideal.
</think>

The capital of France is Paris.

検証のまとめ

  1. enable_thinking=False では両モデルとも簡潔な回答を返す(10〜60 トークン)。0.8B と 27B で応答の長さに大きな差はない。
  2. enable_thinking=True にすると thinking プロセスが展開される(100〜1,500 トークン超)。モデルは内部推論を書き出した後に </think> 区切りで最終回答を出す。
  3. tok/s の 2.5 倍差(0.8B: 34、27B: 13)は thinking モードに依存せず、モデルサイズ相応の生成速度差として安定している。
  4. ユーザ体感レイテンシは「tok/s × トークン数」で決まるため、thinking モードを有効にするかどうかが短い質問に対する体感速度に大きく影響する。

(おまけ)AI Runtime(サーバレス GPU)で 0.8B を動かしてみた

ここまではクラシックコンピュート(g5.12xlarge)の話でしたが、Databricks には AI Runtime(旧サーバレス GPU) という、クラスター管理なしで GPU ノートブックを実行できる環境もあります。せっかくなので 0.8B をこちらでも動かしてみました(2026 年 4 月時点で US リージョンのみ提供のため、日本リージョンからは使えない点は注意)。

必要な調整事項が 3 つに減った

同じ 0.8B + vLLM でも、AI Runtime の Default 環境で試したら、本編で紹介した調整事項が 5 つ → 3 つに減りました

調整事項 クラシック (ML Runtime) AI Runtime (Default)
numpy を 1.x にダウングレード 必要 不要(最初から 2.x)
flash_attn リネーム + モック 必要 不要(flash_attn が入っていない)
OpenCV アンインストール 必要 必要(別の理由、後述)
cv2 ダミーモジュール登録 必要 必要
ハートビート回避スレッド 必要 必要
hf_transfer の追加インストール 不要 必要(環境変数がデフォルト ON)

AI Runtime の Default 環境は最小構成で、flash_attn も numpy 1.x 用 C 拡張も入っていないため、本編で手間がかかった 2 つの調整(Issue 1 と Issue 3)が AI Runtime ではそもそも発生しません。ユースケースに応じて ML Runtime(豊富なプリインストール)か AI Runtime(最小構成)かを選べる、という位置づけです。

一方で、AI Runtime ならではの調整ポイント

AI Runtime でも OpenCV の削除は必要でしたが、理由が違います。クラシックでは「numpy 1.x と 2.x の ABI 不整合」でしたが、AI Runtime では vLLM が入れた OpenCV を import した瞬間に Python プロセスがそのまま強制終了してしまいます(専門的には SIGABRT と呼ばれる、OS レベルの「異常終了」シグナルで落ちます)。

原因は、OpenCV がもともとデスクトップアプリ向けに作られていて、画面描画まわりの共有ライブラリ(例えばウインドウを表示するためのライブラリなど)を内部で呼び出そうとする設計だからです。AI Runtime のコンテナはサーバ用途に最適化された最小構成で、そうした画面描画系のライブラリを入れていないため、OpenCV が「あるはず」と思って呼び出したものが見つからず、即終了してしまいます。

ここで opencv-python-headless というパッケージが何を提供しているかを整理しておきます。このパッケージは pip からインストールできる 1 つのパッケージですが、実は以下の 2 つを同梱しています:

  • C++ で書かれた OpenCV の本体(コンパイル済みの .so ライブラリファイル)
  • それを Python から呼び出すためのラッパーimport cv2 で読み込まれる Python モジュール)

pip install opencv-python-headless するとこの両方が同時にインストールされ、pip uninstall するとどちらも消えます。つまり「Python 側のラッパーだけ」「C++ 側の本体だけ」のように片側だけ残す、というインストール方式は想定されていません。

そして先ほどクラッシュを起こしていたのは、同梱されている本体 .so ファイルのほう(画面描画ライブラリを内部で呼ぼうとしていたのはこっち)。なので pip uninstall opencv-python-headless opencv-python -y を実行して、本体もラッパーもまとめて取り除きます

%pip uninstall opencv-python-headless opencv-python -y

ただし、これだけだと今度は vLLM が起動時に実行する import cv2 が「そんなモジュールは無い」と ImportError で失敗してしまいます。そこで、中身が空の cv2 モジュールを Python 側で用意しておき、sys.modules に登録するという回避策を採ります:

import sys, types, importlib.machinery
cv2_mock = types.ModuleType("cv2")
cv2_mock.__spec__ = importlib.machinery.ModuleSpec("cv2", None)
sys.modules["cv2"] = cv2_mock

Python は import cv2 を実行するとき、まず sys.modules に同名のモジュールが登録されていないかチェックします。登録があればディスクを探しに行かずにそれを使うので、空っぽのダミーがあれば import 自体は成功します。

なぜこれで動くかというと、今回やりたいのはテキスト推論だけで、vLLM は起動時に import cv2 を呼ぶものの、テキスト推論経路では cv2 の関数を実際に一度も呼び出さないためです。したがって中身が空でも実害なく推論が進みます。画像入力まで使いたい場合は、別の画像処理ライブラリを用意するか、AI Runtime の画面描画ライブラリ不足を別途解決する必要があります。

さらに、AI Runtime の Default 環境では環境変数 HF_HUB_ENABLE_HF_TRANSFER=1 がデフォルトで有効になっています。これは Hugging Face の高速ダウンロードライブラリ hf_transfer を使う合図ですが、hf_transfer パッケージ自体はプリインストールされていないので、モデルダウンロード時に ImportError で落ちます。

%pip install hf_transfer

1 行追加するだけですが、これを忘れると ImportError の原因が一見分かりにくいので、あらかじめ知っておくと良いポイントです。

結論

  • 0.8B は AI Runtime の A10G × 1 で問題なく動きました
  • 27B はこの構成では VRAM 不足で動かない(24GB では 54GB の bf16 重みが載らない)ため、AI Runtime 上での検証は 0.8B に限定しています
  • クラスター管理が不要という点はとても快適です。現時点では US リージョンでの提供なので、日本リージョン内で運用したいケースでは当面クラシックコンピュートを使うことになりそうです。今後のリージョン拡大に期待したいところです
  • コードは GitHub の 1-4_qwen35_08b_vllm_serverless_gpu.py に置いてあります

まとめと次回予告

vLLM と Databricks ML Runtime のバージョン整合を 5 ポイント押さえることで、Qwen3.5 をノートブック上で安定して動かせるようになりました。しかも、同じコード・同じ環境で 0.8B から 27B までスムーズにスケールアップできる — 1 度整えたパターンを再利用できるのは、成熟した ML プラットフォームとして Databricks を使うメリットのひとつです。

次回は、このローカル推論を Model Serving 上の REST API エンドポイントとして公開します。ぜひお楽しみに。

コードの全文: GitHub — hiouchiy/qwen35-model-serving-on-databricks

  • 1-1_qwen35_08b_vllm_local.py(0.8B ローカル推論)
  • 2-1_qwen35_27b_vllm_local.py(27B ローカル推論)

Discussion