メインコンテンツへ移動
AIチュートリアル

SGLangでOpenJevをデプロイし、1トークン分類に利用する

Modal上にopenjev-sglangをデプロイし、Noul、Choice、Score評価を送信する方法、サービスの健全性を確認する方法、制限と認証を設定する方法、既存のSGLangバックエンドにAPIを接続する方法を解説します。

SGLangでOpenJevをデプロイし、1トークン分類に利用する

openjev-sglangとは?

openjev-sglangは、SGLang上でQwen3.6-35B-A3Bを使用してTypeSafe/Jev HTTP APIを実装するサーバーです。分類に関する質問を、長い応答を生成するのではなく、効率的な最初のトークンの確率読み出しに変換します。

デフォルトのデプロイでは、NVIDIA B200 GPUごとに1つのSGLangコンテナを実行します。SGLang 0.5.19は、Rustフロントエンド、radixキャッシュ、分割可能なprefill CUDAグラフを提供します。別のPythonプロセスが、FastAPI、uvloop、RustバックエンドのHugging Face tokenizer、およびlocalhost上のSGLangへの非同期接続プールを使用して、公開APIを処理します。

TypeSafeにはリクエストを送信しません。公開エンドポイントでは評価APIを公開し、SGLangの生成および管理用ルートはlocalhost上で非公開のままにします。

主な機能

  • 3種類の評価: yes/noの確率を求めるNoul、カテゴリ選択を行うChoice、順序付きレベルを扱うScore。
  • 1トークン推論: 各質問は、生成された思考連鎖を使わず、回答ラベルの対数確率から評価されます。
  • 共有プレフィックスキャッシュ: 共通のプロンプトプレフィックスを事前にウォームアップしてから、質問の分岐を並行して実行します。
  • 最大64問: ChoiceとScoreの質問では、デフォルトで2〜64個の回答をサポートします。
  • 構造化された状態: stateとinstructionsには、文字列、JSONオブジェクト、配列を指定できます。
  • 運用エンドポイント: ヘルスチェック、liveness、モデルカタログ、制限値、OpenAPIスキーマ、Swagger UI、インタラクティブなScalarリファレンスを利用できます。
  • Modalデプロイ: 付属のアプリケーションは、オートスケーリング、永続的なモデルキャッシュ、起動時の復旧、スケール・トゥ・ゼロに対応しています。
  • 設定可能な保護: リクエスト制限、同時実行制御、タイムアウト、任意のBearer認証を利用できます。

前提条件

uvをインストールし、付属のクラウドデプロイを使用する場合はModalアカウントを取得してください。初回のビルドでは大容量のSGLangイメージをインポートし、初回のGPU起動ではモデルの重みをダウンロードしてカーネルをコンパイルまたはキャプチャします。

CUDAの依存関係はSGLangコンテナ内に保持されます。ワークステーションでuv syncを実行すると、API、デプロイユーティリティ、テストのみがインストールされます。

プロジェクトをインストールする

リポジトリをクローンしてそのディレクトリに移動し、Python環境を同期します。

uv sync

現在のマシンでModalの認証が完了していない場合は、セットアップを実行します。

uv run modal setup

一時的なModalデプロイを実行する

エンドツーエンドテストを行うには、一時サーバーを起動し、付属の推論チェックを実行して、その後サーバーを停止します。

uv run modal run modal_app.py

このコマンドはスモークテストのレポートをsmoke-result.jsonとして保存します。レポートには、起動待ち時間、推論レイテンシ、利用可能なキャッシュ使用量の情報が含まれます。

安定したエンドポイントをデプロイする

アプリケーションを永続的なModalサービスとしてデプロイします。

uv run modal deploy modal_app.py

デプロイによって、https://YOUR-SERVER.us-west.modal.directのようなURLが表示されます。アクセスしやすいように環境変数へ保存します。

export OPENJEV_URL="https://YOUR-SERVER.us-west.modal.direct"

デフォルトのModalエンドポイントは意図的に認証なしで動作します。US Westのルーティングリージョンを使用し、設定されたUSリージョンでコンピュートをスケジュールできます。コンテナ数に明示的な上限はなく、5分間アイドル状態が続くとインスタンスはスケール・トゥ・ゼロになります。

スケール・トゥ・ゼロになったサーバーは、起動中にHTTP 503を返します。付属のスモークコマンドは、これらの起動時レスポンスを自動的に再試行します。

デプロイしたURLに対してスモークテストを実行します。

uv run openjev smoke "$OPENJEV_URL"

スモークテストでは、3種類すべての回答タイプ、64個の回答を持つ質問、基本的なセマンティックチェックを実行し、65個の回答が拒否されることを確認します。

最初の評価を送信する

主要なルートはPOST /v1/systemoneです。次のリクエストでは、Noul、Choice、Scoreの質問を使って顧客メッセージを分類します。

curl "$OPENJEV_URL/v1/systemone" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "jev-latest",
    "state": [
      {"role": "system", "content": "You are a support assistant."},
      {"role": "user", "content": "I was charged twice. Please refund the duplicate."}
    ],
    "questions": {
      "refund": {
        "type": "noul",
        "instructions": "Does the user request a refund?"
      },
      "department": {
        "type": "choice",
        "instructions": "Which department should handle this?",
        "criteria": {
          "billing": "Payments and refunds",
          "technical": "Software bugs"
        }
      },
      "urgency": {
        "type": "score",
        "instructions": "How urgent is the request?",
        "criteria": ["Routine", "Urgent", "Emergency"]
      }
    }
  }'

JSONをファイルに保存し、シェルのクォートなしで送信することもできます。

curl "$OPENJEV_URL/v1/systemone" \
  -H 'Content-Type: application/json' \
  --data-binary @examples/request.json

質問タイプを理解する

  • Noul: yesの正規化確率を返します。
  • Choice: 確率が最も高い選択肢と、完全な確率分布を返します。元の選択肢名はレスポンス内で保持されます。
  • Score: 各レベルのインデックスとその確率から算出した0始まりの期待レベルを、凡例とともに返します。

選択肢は、説明が続く文字ラベルとしてモデルに提示されます。応答キーは通常、モデルからは隠されます。選択肢の説明がnullの場合は、そのキーが意味を提供します。

状態と指示の指定

stateとinstructionsは、文字列、JSONオブジェクト、または配列を受け付けます。チャットメッセージのリスト、または正確に{"messages": [...]}を含むオブジェクトは、モデル固有のチャットテンプレートでレンダリングされます。

元のメッセージのロールとオブジェクトは保持され、各分類質問は別のユーザーメッセージとして追加されます。その他の構造化された値は、そのままユーザーメッセージにシリアライズされます。messagesとその他のフィールドを含むオブジェクトも保持されるため、メタデータが暗黙に破棄されることはありません。

チャット状態がサポートするのはテキストコンテンツのみです。画像、音声、動画のコンテンツはサポートされません。

サービスエンドポイントを確認する

  • POST /v1/systemoneは、Noul、Choice、Scoreの評価を実行します。
  • GET /v1/modelsは、TypeSafeおよびOpenAI形式のフィールドを含むモデルカタログを返します。
  • GET /v1/limitsは、受付制限を報告します。
  • GET /healthは、準備状態、SGLangのヘルス状態、起動時間を報告します。
  • GET /health/liveは、APIプロセスが稼働しているかを確認します。
  • GET /は、編集可能なリクエスト例を備えたScalar APIリファレンスを開きます。
  • GET /docsは、組み込みのSwagger UIを開きます。
  • GET /openapi.jsonは、生成されたOpenAPIスキーマを返します。

リクエストモデル名jev-latestは互換性エイリアスです。デフォルトの公開モデルIDはQwen/Qwen3.6-35B-A3Bで、NVIDIAのリポジトリは内部で重みのソースとして使用されます。

推論の仕組み

  1. APIは、スキーマ、回答数、本文サイズ、コンテキスト長、合計トークン予算を検証します。
  2. 思考を無効にしてモデル固有のチャットテンプレートを一度レンダリングし、共有プレフィックスと個別にトークン化された質問サフィックスを分離します。
  3. 共通プレフィックスをmax_new_tokens=1でSGLangに送信します。サンプリングされたトークンは破棄され、プレフィックスは機会に応じたradixキャッシュ再利用のために利用可能な状態で残ります。
  4. 質問ごとに1つのブランチを並行して送信します。各ブランチには、プレフィックス、質問サフィックス、アシスタントヘッダー、Answer:マーカーが含まれます。
  5. 有効な回答ラベルについてのみ対数確率を取得し、安定したソフトマックスで再正規化します。

N個の質問に対して、サーバーは1トークン呼び出しをN + 1回実行します。内訳は、キャッシュをウォームアップする呼び出しが1回、各質問の呼び出しがそれぞれ1回です。生成されたトークンは無視されるため、自己回帰的な継続や生成された思考の連鎖はありません。投機的デコーディングは有効になっていません。

ラベルはAからZまで続き、その後はAAやABのような検証済みの単一トークン組み合わせを使用します。起動時に、アクティブなトークナイザーですべての64個のラベルを確認します。これにより、10や64のような複数トークンの数字ラベルの使用を避けます。

使用量とキャッシュメトリクスを理解する

レスポンスヘッダーx-openjev-prefix-tokensは、要求された共通プレフィックスのサイズを報告します。SGLangがキャッシュ数を提供する場合、x-openjev-cached-tokensにはブランチのキャッシュヒット数の合計が含まれます。

SGLang 0.5.19のRustフロントエンドは、これらのキャッシュ数を公開しません。その構成ではヘッダーが存在せず、スモークレポートにはゼロではなくnullが記録されます。スケジューラーログには、実際のキャッシュヒットが引き続き表示される場合があります。Server-Timingヘッダーは、プロンプト準備、共有プリフィル、ブランチ推論を分けて示します。

usage.input_tokensは、ウォームアップ呼び出しとブランチ呼び出し全体で報告された完全なプロンプト数の合計で、キャッシュ済みトークンを含みます。usage.output_tokensはN + 1です。これらの数値は、固有の計算済みトークン数やTypeSafeの請求見積もりではなく、バックエンドの処理内容を表します。

制限と認証を設定する

デフォルトの受付制限には、64個の質問、ChoiceまたはScoreの質問1件あたり2~64個の回答、2 MiBのJSON本文、出力を含むブランチあたり32,768トークン、送信された入力トークンの合計262,144、同時評価数16、同時バックエンド呼び出し数64が含まれます。

無効なリクエストは推論前にHTTP 422を返します。サイズ制限を超える本文は413、過負荷はRetry-After付きの529、バックエンドのタイムアウトは504を返します。失敗またはキャンセルされた評価では、兄弟ブランチをキャンセルし、SGLangでの中断を試みます。

設定はOPENJEV_*環境変数で行えます。主な設定項目は次のとおりです。

  • OPENJEV_MODEL:デフォルトはnvidia/Qwen3.6-35B-A3B-NVFP4です。
  • OPENJEV_SERVED_MODEL_NAME:公開モデルIDを上書きし、SGLangに渡します。
  • OPENJEV_REVISION:固定されたチェックポイントのリビジョンを選択します。
  • OPENJEV_FRONTEND:デフォルトはrustです。pythonは明示的なフォールバックです。
  • OPENJEV_MAX_INPUT_TOKENS:デフォルトは32768です。
  • OPENJEV_MAX_TOTAL_INPUT_TOKENS:デフォルトは262144です。
  • OPENJEV_MAX_CONCURRENT_REQUESTS:デフォルトは16です。
  • OPENJEV_MAX_CONCURRENT_BRANCHES:デフォルトは64です。
  • OPENJEV_REQUEST_TIMEOUT:デフォルトは120秒です。
  • OPENJEV_TEMPERATURE:デフォルトは1.0で、ラベルの正規化に影響します。
  • OPENJEV_API_KEY:公開APIで任意のBearer認証を有効にします。
  • OPENJEV_BACKEND_API_KEY:SGLang用に別の任意のBearerキーを設定します。

Modalランチャーは、ローカル環境の OPENJEV_PROFILE、OPENJEV_FRONTEND、OPENJEV_SERVED_MODEL_NAME を自動的に転送します。その他のリモート設定は image.env(...) に追加するか、認証情報にはModal Secretを使用してください。

既存のSGLangバックエンドを使用してローカルで実行する

すでに実行中のバックエンドを使用するには、APIをそのローカルURLに向けます。

uv run openjev serve --connect http://127.0.0.1:30000

バックエンドでは、一致するモデルとトークナイザーのリビジョンを使用する必要があります。また、選択トークンの対数確率、radixキャッシュ、および十分なコンテキスト長をサポートしている必要があります。

SGLang 0.5.19が別のPython環境にインストールされているB200ホストまたはコンテナでは、そのインタープリターを明示的に指定します。

uv run openjev serve --sglang-python /path/to/sglang/bin/python

開発と検証

付属のコマンドを使用して、オフラインテスト、トークナイザー統合チェック、リンティング、スキーマ生成を実行します。

uv run pytest
uv run pytest -m integration
uv run ruff check .
uv run openjev schema

通常のテストでは、ユニット動作とAPI動作をオフラインで検証します。統合テストでは実際のトークナイザーと小規模なHugging Faceダウンロードを使用しますが、GPUは必要ありません。スキーマ生成にもGPUやモデルのダウンロードは必要ありません。

高度なデプロイのヒント

GPUコンテナをウォーム状態に保つ

Modalは5分間アイドル状態が続くと、サービスをゼロまでスケールダウンします。アイドル時のGPU使用量を抑えることよりも、一貫して低い起動レイテンシーを優先する場合は、modal_app.py で min_containers=1 を設定してください。

初回起動時の初期化を見込む

GPUの初回起動では、重みのダウンロードとカーネルのコンパイルまたはキャプチャが行われます。モデルの重み、SGLangのチューニングデータ、Tritonのコンパイルデータは、openjev-huggingface Modal Volumeに永続化されます。以降の起動ではこれらのファイルが再利用されますが、CUDAグラフのキャプチャは起動時に引き続き実行されます。

キャッシュの再利用は状況次第と考える

Radixの再利用は、リクエストごとに固定されたKVセッションではありません。ハイブリッドQwenモデルの再帰状態、キャッシュページの境界、キャッシュの逼迫、同時トラフィックによって、ヒット率が低下する可能性があります。バックエンドでは --mamba-radix-cache-strategy extra_buffer を使用します。

信頼度を慎重に解釈する

OpenJevでは、信頼度を 1 - H(probabilities) / log(number_of_options) と定義し、0から1の範囲にクランプします。一様な確率では信頼度は0になり、確率質量が1点に集中している場合は1になります。

これらの確率は指定された選択肢を条件としており、プロンプトの文言やラベルの順序に左右されることがあります。正しさを較正した推定値ではありません。

バックエンド障害に備える

SGLangが予期せず終了すると、APIも終了します。その後Modalランチャーがコンテナを終了させるため、停止した推論バックエンドに接続された公開HTTPプロセスを残さず、Modalがコンテナを置き換えられます。通常のシャットダウンでは、両方のウォッチャーが正常に無効化されます。

まとめ

openjev-sglangは、効率的な1トークン確率読み出しにより、Qwen3.6でJev互換の分類を実行するための用途を絞った手段を提供します。Modal設定ではスケーラブルなB200デプロイを実現でき、ローカルコマンドではテストや既存のSGLangサービスへの接続に対応します。スモークテストから始め、制限値と認証設定を検証し、トラフィックの増加に合わせてタイミングとキャッシュヘッダーを監視してください。