Snowflake Gateway による A/B テスト用トラフィック分割を試してみた

Snowflake Gateway による A/B テスト用トラフィック分割を試してみた

Snowflake の2026年8月アップデートで一般提供されたGateway Monitoring と A/B Testing について、ゲートウェイの作成からトラフィック分割までを実際に試してみましたので、その内容をまとめてみます。
2026.09.05

はじめに

2026年8月のアップデートで、リアルタイム推論サービスとしてデプロイされた ML モデルを複数のモデルバージョンにトラフィックを振り分けながら、品質を継続的に監視・比較できる Gateway Monitoring と A/B Testing が一般提供となりました。

https://docs.snowflake.com/en/release-notes/2026/other/2026-08-04-gateway-monitoring-ab-testing-ga

こちらについて、特にゲートウェイの作成からトラフィックの分割について試してみましたので、その内容を本記事でまとめてみます。

本機能の概要

本機能については以下に記載があります。

https://docs.snowflake.com/en/developer-guide/snowflake-ml/inference/gateway-monitor-and-ab-testing

Snowflake では Model Registry という機能に機械学習モデル(ML モデル)を登録することで、そのメタデータを管理することができます。

Model Registry に登録したモデルは、サービスとしてコンテナ(SPCS)で実行することが可能です。これにより、リアルタイム推論サービスとしてデプロイできます。

https://docs.snowflake.com/en/developer-guide/snowflake-ml/inference/real-time-inference-rest-api

今回のアップデートでは、以下の機能群によりデプロイ後のモデルの運用・監視が容易になります。

  • Snowflake Gateway:複数のモデルバージョン(サービス)に対してトラフィックを割合で分配できるエンドポイント(traffic split gateway)を提供する。A/B テストの土台となる仕組みとして利用できる
  • Gateway Monitoring:ゲートウェイ経由の推論ログを Auto Capture で自動収集し、ドリフト(予測分布のズレ)・性能指標(精度・RMSE など)・統計指標を定期的に集計できる
  • A/B Testing:Gateway によるトラフィック分割と Gateway Monitoring を組み合わせることで、既存モデル(baseline)と新モデル(challenger)を実際のユーザーリクエストで比較できる

また、現時点での主な制約は以下の通りです。

  • 単一出力の二値分類・回帰・多クラス分類モデルのみサポートされる
  • 監視対象にしたい推論サービスは、すべて Auto Capture を有効化しておく必要がある

Model Registry や SPCS へのモデルデプロイについては、以下の記事でも紹介されていますので、あわせてご参照ください。

https://dev.classmethod.jp/articles/try-snowparkml-model-registry-preview/

https://dev.classmethod.jp/articles/snowsight-model-registry-spcs-model-serving-snowsight/

試してみる

前提条件

モデルの学習自体は Snowflake の機能ではなく、通常の Python の機械学習ワークフロー(scikit-learn / XGBoost 等)で行い、学習済みモデルを snowflake.ml.registry.Registrylog_model() で Model Registry に登録する流れです。

ここでは以下の設定としています。

  • 実行環境:Snowflake Notebooks
  • モデル
    • ここでは簡単に scikit-learn のLinearRegression(単純な線形回帰)を用いました
    • また、モデル自体もデータからパラメータを推定せずに、パラメータ値(傾き・切片)を直接代入する形としています

モデルを作成して Model Registry に登録する

Workspace 上の Notebook でライブラリ・コンテキストを設定し、v1 モデル(ここでのベースモデル)を用意しました。

import numpy as np
from sklearn.linear_model import LinearRegression
from snowflake.ml.registry import Registry
from snowflake.snowpark.context import get_active_session

session = get_active_session()
session.use_database("YASUHARA_TEST_DB")
session.use_schema("PUBLIC")
reg = Registry(session=session)

# ベースモデル: y = 2x + 3
model_v1 = LinearRegression()
model_v1.coef_ = np.array([2.0])
model_v1.intercept_ = 3.0

# 入力データの見本
sample_input = np.array([[1.0], [2.0], [3.0]])

mv1 = reg.log_model(
    model_v1,
    model_name="my_model",
    version_name="v1",
    conda_dependencies=["scikit-learn"],
    metrics={"score": 1.0},
    sample_input_data=sample_input
)

続けて、challenger(v2)としてパラメータの値を変えたモデルも同様に登録します。

# challenger(v2):あえてズラしたパラメータのモデル
# y = 1.2x + 8
model_v2 = LinearRegression()
model_v2.coef_ = np.array([1.2])
model_v2.intercept_ = 8.0

mv2 = reg.log_model(
    model_v2,
    model_name="my_model",
    version_name="v2",
    conda_dependencies=["scikit-learn"],
    metrics={"score": 0.6},
    sample_input_data=sample_input
)

コンピュートプールを作成

リアルタイム推論でサービングするため、SPCS(Snowpark Container Services)上でモデルサービスを稼働させる Compute Pool を作成します。

CREATE COMPUTE POOL IF NOT EXISTS yasuhara_inference_pool
    MIN_NODES = 1
    MAX_NODES = 2
    INSTANCE_FAMILY = 'CPU_X64_XS'
    AUTO_RESUME = TRUE;

モデルを推論サービスとしてデプロイする

v1・v2 それぞれを推論サービスとしてデプロイします。
この際、Auto Captureを有効化します。監視対象にする推論サービスは Auto Capture の有効化が必要となります。注意点として、autocapture は一度作成したサービスに対して後から有効化することはできません。

mv1 = reg.get_model("MY_MODEL").version("V1")
mv1.create_service(
    service_name="MY_MODEL_V1_SERVICE",
    service_compute_pool="yasuhara_inference_pool",
    ingress_enabled=True,
    autocapture=True
)

mv2 = reg.get_model("MY_MODEL").version("V2")
mv2.create_service(
    service_name="MY_MODEL_V2_SERVICE",
    service_compute_pool="yasuhara_inference_pool",
    ingress_enabled=True,
    autocapture=True
)

https://docs.snowflake.com/en/developer-guide/snowflake-ml/inference/auto-capture-inference-logs#activate-auto-capture

https://docs.snowflake.com/en/developer-guide/snowpark-ml/reference/latest/api/model/snowflake.ml.model.ModelVersion

デプロイ後、Snowsight でもサービスの状態を確認できました。

2026-09-04_22h49_28

2026-09-04_22h49_38

推論方法ごとの Auto Capture の記録有無を確認する

ゲートウェイ作成前に、各サービス単体で推論を試してみます。

SQL と Python それぞれ以下のように推論できます。

-- SQLで推論
> SELECT YASUHARA_TEST_DB.PUBLIC.MY_MODEL_V1_SERVICE!predict(5.0);
+----------------------------------------------------------+             
| YASUHARA_TEST_DB.PUBLIC.MY_MODEL_V1_SERVICE!PREDICT(5.0) |             
|----------------------------------------------------------|             
| {                                                        |
|   "output_feature_0": 13                                 |
| }                                                        |
+----------------------------------------------------------+

Python の例:

import pandas as pd

mv1 = reg.get_model("MY_MODEL").version("V1")
input_df = pd.DataFrame({"input_feature_0": [5.0]})
result = mv1.run(input_df, function_name="predict", service_name="MY_MODEL_V1_SERVICE")
print(result)

上記で単発実行はできましたが、Auto Capture がログを記録するのはリアルタイム推論(REST API 経由)のリクエストのみでした。
そのため、ログを溜めたい場合は、REST API エンドポイント経由でリクエストを送る必要があります。

curl -X POST "https://<サービスの ingress_url>/predict" \
  -H "Content-Type: application/json" \
  -H 'Authorization: Snowflake Token="<PAT トークン>"' \
  -d '{"dataframe_split": {"index": [0], "columns": ["input_feature_0"], "data": [[5.0]]}}'

サービスのエンドポイントは、以下で確認できます。

SHOW ENDPOINTS IN SERVICE YASUHARA_TEST_DB.PUBLIC.MY_MODEL_V1_SERVICE;

なお、このエンドポイントに外部からアクセスする際は、当然認証が必要です。
ノートブック内から呼ぶ場合は、セッショントークンを使って以下のようにリクエストできます。

import requests

url = "http://my-model-v1-service.xxxxx.svc.spcs.internal:5000/predict"
headers = {
    "Content-Type": "application/json"
}
payload = {
    "dataframe_records": [{"input_feature_0": 5.0}]
}

resp = requests.post(url, json=payload, headers=headers)
print(resp.status_code)
print(resp.json())

上記で URL として指定している内部 DNS 名・ポートは、以下で取得できます。

  • 内部 DNS 名
DESCRIBE SERVICE YASUHARA_TEST_DB.PUBLIC.MY_MODEL_V1_SERVICE                        
  ->> SELECT "dns_name" FROM $1;                                                  
+--------------------------------------------+                                                                                         
| dns_name                                   |                                                                                         
|--------------------------------------------|                                                                                         
| my-model-v1-service.xxxx.svc.spcs.internal |                                                                                         
+--------------------------------------------+          

https://docs.snowflake.com/ja/developer-guide/snowpark-container-services/working-with-services#service-dns-name

  • ポート
SHOW ENDPOINTS IN SERVICE YASUHARA_TEST_DB.PUBLIC.MY_MODEL_V1_SERVICE;

REST API 経由でリクエスト後、Auto Capture で自動収集された推論ログを INFERENCE_TABLE テーブル関数で取得できます。

-- Auto Capture が自動収集した推論ログを確認
SELECT * FROM TABLE(INFERENCE_TABLE('MY_MODEL'));
+-------------------------+-----------------+-------------------------+-------+----------+---------------------------------------------------------------+-----------------------------+------------------+-------------+--------+------------------------------------------------------------------------------+-------+-----------+                       
| TIMESTAMP               | START_TIMESTAMP | OBSERVED_TIMESTAMP      | TRACE | RESOURCE | RESOURCE_ATTRIBUTES                                           | SCOPE                       | SCOPE_ATTRIBUTES | RECORD_TYPE | RECORD | RECORD_ATTRIBUTES                                                            | VALUE | EXEMPLARS |
|-------------------------+-----------------+-------------------------+-------+----------+---------------------------------------------------------------+-----------------------------+------------------+-------------+--------+------------------------------------------------------------------------------+-------+-----------|
| 2026-09-04 09:30:14.915 | NULL            | 2026-09-04 09:30:14.915 | NULL  | NULL     | {                                                             | {                           | NULL             | LOG         | NULL   | {                                                                            | null  | NULL      |
|                         |                 |                         |       |          |   "snow.account.name": "xxxxx",                               |   "name": "inference-table" |                  |             |        |   "snow.model_serving.function.name": "predict",                             |       |           |
|                         |                 |                         |       |          |   "snow.compute_pool.id": 72,                                 | }                           |                  |             |        |   "snow.model_serving.hop_ids": [                                            |       |           |
|                         |                 |                         |       |          |   "snow.compute_pool.name": "YASUHARA_INFERENCE_POOL",        |                             |                  |             |        |     4                                                                        |       |           |
|                         |                 |                         |       |          |   "snow.compute_pool.node.id": "10.16.69.57",                 |                             |                  |             |        |   ],                                                                         |       |           |
|                         |                 |                         |       |          |   "snow.compute_pool.node.instance_family": "CPU_X64_XS",     |                             |                  |             |        |   "snow.model_serving.last_hop_id": 4,                                       |       |           |
|                         |                 |                         |       |          |   "snow.database.id": 294,                                    |                             |                  |             |        |   "snow.model_serving.request.data.input_feature_0": 5,                      |       |           |
|                         |                 |                         |       |          |   "snow.database.name": "YASUHARA_TEST_DB",                   |                             |                  |             |        |   "snow.model_serving.request.extra_columns.request_id": "5",                |       |           |
|                         |                 |                         |       |          |   "snow.executable.engine": "SnowparkContainers",             |                             |                  |             |        |   "snow.model_serving.request.timestamp": "2026-09-04T09:30:14.910084066Z",  |       |           |
|                         |                 |                         |       |          |   "snow.executable.type": "MODEL_INFERENCE_INTERNAL_SERVICE", |                             |                  |             |        |   "snow.model_serving.response.code": 200,                                   |       |           |
|                         |                 |                         |       |          |   "snow.model.version.id": 270,                               |                             |                  |             |        |   "snow.model_serving.response.data.output_feature_0": 13,                   |       |           |
|                         |                 |                         |       |          |   "snow.model.version.name": "V1",                            |                             |                  |             |        |   "snow.model_serving.response.timestamp": "2026-09-04T09:30:14.915649429Z", |       |           |
|                         |                 |                         |       |          |   "snow.query.id": "01c6d742-0104-948d-0000-00b40c93c39e",    |                             |                  |             |        |   "snow.model_serving.truncation_policy": "NONE"                             |       |           |
|                         |                 |                         |       |          |   "snow.schema.id": 1142,                                     |                             |                  |             |        | }                                                                            |       |           |
|                         |                 |                         |       |          |   "snow.schema.name": "PUBLIC",                               |                             |                  |             |        |                                                                              |       |           |
|                         |                 |                         |       |          |   "snow.service.container.name": "proxy",                     |                             |                  |             |        |                                                                              |       |           |
|                         |                 |                         |       |          |   "snow.service.id": 301,                                     |                             |                  |             |        |                                                                              |       |           |
|                         |                 |                         |       |          |   "snow.service.instance": "0",                               |                             |                  |             |        |                                                                              |       |           |
|                         |                 |                         |       |          |   "snow.service.name": "MY_MODEL_V1_SERVICE",                 |                             |                  |             |        |                                                                              |       |           |
|                         |                 |                         |       |          |   "snow.service.type": "Service"                              |                             |                  |             |        |                                                                              |       |           |
|                         |                 |                         |       |          | }                                                             |                             |                  |             |        |                                                                              |       |           |
+-------------------------+-----------------+-------------------------+-------+----------+---------------------------------------------------------------+-----------------------------+------------------+-------------+--------+------------------------------------------------------------------------------+-------+-----------+

https://docs.snowflake.com/en/developer-guide/snowflake-ml/inference/auto-capture-inference-logs#query-inference-data

Gateway を作成してトラフィックを分割する

ベースラインと challenger(v2 のサービス)にトラフィックを振り分けるゲートウェイを作成します。ゲートウェイ自体もスキーマ配下に作成されるオブジェクトです。

CREATE OR REPLACE GATEWAY my_gateway
  FROM SPECIFICATION $$
spec:
  type: traffic_split
  split_type: custom
  targets:
  - type: endpoint
    value: YASUHARA_TEST_DB.PUBLIC.MY_MODEL_V1_SERVICE!inference
    weight: 70
  - type: endpoint
    value: YASUHARA_TEST_DB.PUBLIC.MY_MODEL_V2_SERVICE!inference
    weight: 30
$$;

https://docs.snowflake.com/en/sql-reference/sql/create-gateway

ポイントは以下の通りです。

  • type:traffic_splitを指定します。カスタムの重み付けでトラフィックを分割したい場合は、split_type: customも合わせて指定します
  • targets.value:!inference の部分は、サービス仕様で定義された実際のエンドポイント名を指定する必要がありました。これは SHOW ENDPOINTS IN SERVICE <サービス名>;name 列で確認できます
  • weight:各ターゲットへの振り分け比率を整数で指定します。合計値が100になるよう指定します。今回は7030としているため、ベースライン(v1)に7割、challenger(v2)に3割のトラフィックが振り分けられる想定です

作成後、ゲートウェイの URL は以下のingress_url列で確認できます。

DESC GATEWAY my_gateway;

Snowsight 上からも確認できます。

2026-09-04_23h14_33

ゲートウェイの詳細:

2026-09-04_23h14_43

モデルモニターを作成する

Auto Capture を有効化した推論サービスに対して、モデルモニターを作成します。これにより、各モデルのドリフトや性能指標を継続的に集計し、A/B テストの結果を定量的に比較できるようになります。

今回は Snowsight UI からモデルモニターを作成しました。対象のゲートウェイの「Monitoring」タブから作成可能です。

作成時は、作成先のデータベース・スキーマ、対象のモデルを指定します。

2026-09-05_14h38_03

オプションで Ground Truth に関する設定を追加できます。正解データが入った Ground Truth テーブルと、そのテーブル上で推論リクエストとの突合に使う ID カラム、正解値が入ったスコアカラムをそれぞれ指定します。

2026-09-05_14h38_46

実行された SQL は以下のようになっていました。

CREATE MODEL MONITOR YASUHARA_TEST_DB.PUBLIC."yasuhara_monitor"
  WITH GATEWAY = YASUHARA_TEST_DB.PUBLIC.MY_GATEWAY
  MODEL = YASUHARA_TEST_DB.PUBLIC.MY_MODEL
  FUNCTION = 'PREDICT'
  WAREHOUSE = X_SMALL_WH
  REFRESH_INTERVAL = '1 minutes'
  AGGREGATION_WINDOW = '1 hours'
  GROUND_TRUTH = YASUHARA_TEST_DB.PUBLIC.MY_MODEL_GROUND_TRUTH
  ID_COLUMNS = ('REQUEST_ID')
  ACTUAL_SCORE_COLUMNS = ('ACTUAL_VALUE');

https://docs.snowflake.com/en/sql-reference/sql/create-model-monitor

ここで指定している GROUND_TRUTH = YASUHARA_TEST_DB.PUBLIC.MY_MODEL_GROUND_TRUTH は、あらかじめ用意しておく必要がある Ground Truth(正解データ)テーブルです。作成時のポイントは後述しますが、ここで使用した SQL を先に記載しておきます。

CREATE OR REPLACE TABLE yasuhara_test_db.public.my_model_ground_truth (
    request_id STRING,
    actual_value FLOAT
);

INSERT INTO yasuhara_test_db.public.my_model_ground_truth (request_id, actual_value)
VALUES
    ('1', 5.0),
    ('2', 7.0),
    ('5', 13.0),
    ('10', 23.0);

Gateway 経由でリクエストを送信し、トラフィック分割を確認する

ここから、実際にゲートウェイ経由で複数回リクエストを送り、指定した割合(70:30)で振り分けられているかを確認します。

リクエストの形式自体は、各サービスへ直接送っていたときと同じdataframe_split形式です。異なるのは送信先で、個々のサービスではなくゲートウェイの URL(DESC GATEWAYで確認したingress_url/predict)を指定します。ゲートウェイがリクエストを受け取り、内部で v1・v2 いずれかのサービスへ振り分けます。以下ではx1, 2, 5, 10を3周分(計12回)送信しています。

GATEWAY_URL="https://xxxxx-xxxxx-xxxxx.snowflakecomputing.app/predict"
PAT="<token>"

for x in 1 2 5 10 1 2 5 10 1 2 5 10; do
  resp=$(curl -s -X POST "$GATEWAY_URL" \
    -H "Content-Type: application/json" \
    -H "Authorization: Snowflake Token=\"$PAT\"" \
    -d "{\"dataframe_split\": {\"index\": [0], \"columns\": [\"input_feature_0\"], \"data\": [[$x]]}}")
  echo "x=$x -> $resp"
done

https://docs.snowflake.com/en/developer-guide/snowflake-ml/inference/stable-endpoints-api-reference

出力は以下の通りでした。

x=1 -> {"data":[[0,{"output_feature_0":5}]]}
x=2 -> {"data":[[0,{"output_feature_0":10.4}]]}
x=5 -> {"data":[[0,{"output_feature_0":13}]]}
x=10 -> {"data":[[0,{"output_feature_0":23}]]}
x=1 -> {"data":[[0,{"output_feature_0":9.2}]]}
x=2 -> {"data":[[0,{"output_feature_0":7}]]}
x=5 -> {"data":[[0,{"output_feature_0":13}]]}
x=10 -> {"data":[[0,{"output_feature_0":23}]]}
x=1 -> {"data":[[0,{"output_feature_0":9.2}]]}
x=2 -> {"data":[[0,{"output_feature_0":7}]]}
x=5 -> {"data":[[0,{"output_feature_0":13}]]}
x=10 -> {"data":[[0,{"output_feature_0":23}]]}

v1(y=2x+3)と v2(y=1.2x+8)は同じ x に対して異なる値を返すため、応答値からどちらのモデルが処理したかを判定できます。

x v1 の想定出力 v2 の想定出力
1.0 5 9.2
2.0 7 10.4
5.0 13 14
10.0 23 20

12回のリクエストのうち、v1 が9回・v2 が3回という結果になりました。サンプル数が少ないためブレはありますが、意図した70:30に近い比率で振り分けられていることが確認できました。

Snowsight 上でも、対象のゲートウェイの「Metrics」タブから同様の結果を確認できます。

2026-09-05_15h03_36

再度リクエストを送ると、値が更新されていることが確認できました。

2026-09-05_15h16_02

モニタリング結果の確認

Monitoring タブでは、各モデルについて Auto Capture で記録された推論リクエスト・レスポンスの分布等を確認できます。

2026-09-05_15h29_51

本来はここで RMSE などの指標も確認できるはずですが、今回は Ground Truth との突合を必要とする性能指標がうまく取得できませんでした。

指標はMODEL_MONITOR_DRIFT_METRIC / MODEL_MONITOR_PERFORMANCE_METRICを使ってクエリから取得することも可能です。

https://docs.snowflake.com/en/developer-guide/snowflake-ml/inference/auto-capture-inference-logs#query-inference-data

Ground Truth との突合

検証時はうまくいきませんでしたが、性能指標(RMSE 等)を計算するには Ground Truth(正解データ)との ID による突合が行われます。
ここでは、以下のようなテーブルを正解として用意しました。

CREATE OR REPLACE TABLE yasuhara_test_db.public.my_model_ground_truth (
    request_id STRING,
    actual_value FLOAT
);

INSERT INTO yasuhara_test_db.public.my_model_ground_truth (request_id, actual_value)
VALUES
    ('1', 5.0),
    ('2', 7.0),
    ('5', 13.0),
    ('10', 23.0);

各列は以下の通りです。

  • request_id:推論リクエストとの突合に使う ID 列。STRING 型で用意する必要がある
  • actual_value:モデルの予測値と比較される正解値

推論リクエスト時にextra_columnsとしてrequest_idを送ることで、その値が Ground Truth テーブルのrequest_id列と突合され、一致した行のactual_valueが正解値として使われます。

{
    "dataframe_split": {
        "index": [0],
        "columns": ["input_feature_0", "request_id"],
        "data": [[5.0, "5"]]
    },
    "extra_columns": ["request_id"]
}

上記がリクエストの形式ですが、extra_columnsdataframe_split(またはdataframe_records)と並列のトップレベルフィールドとして指定します。

先のSELECT * FROM TABLE(INFERENCE_TABLE('MY_MODEL'));RECORD_ATTRIBUTES列を確認すると、"snow.model_serving.request.extra_columns.request_id": "5" として正しく記録されていましたが、今回は指標の自動算出まではできませんでした。

さいごに

Gateway Monitoring と A/B Testing を試してみました。トラフィック分割による A/B テストの仕組みとして、同じ入力に対して異なるモデルの応答が返ってくることが確認できました。振り分けが行われるゲートウェイの構築も CREATE 文のみで設定まで完了する点は容易と思います。

性能指標(RMSE 等)の算出に必要な Ground Truth との突合は、最終的な指標反映までは確認できませんでしたので、また試してみたいと思います。

同じような検証をされる方の参考になれば幸いです。

参考

https://www.snowflake.com/en/developers/guides/getting-started-with-ml-observability-in-snowflake/


Snowflake World Tour Tokyo 2026に参加しませんか?

Snowflakeの国内最大級イベント「Snowflake World Tour Tokyo」が2026年9月10日(木)・11日(金)にグランドプリンスホテル新高輪にて開催されます。
最新のAI・データ活用事例やライブデモを体感できる無料イベントです。

Snowflake World Tour Tokyoイベントに参加する


Snowflake Community Awards ファイナリストに選出されました

DevelopersIO で Snowflake 記事を執筆している かわばた が、Snowflake Community Awards「RISING COMMUNITY LEADER OF THE YEAR」部門・APJ枠のファイナリストに選ばれました。
最終選考の30%はコミュニティ投票です。記事がお役に立っていたようでしたら、9月15日(火)までにぜひ一票お願いします。フォームの「(4 of 6) RISING COMMUNITY LEADER OF THE YEAR」で Tomohiro Kawabata | Classmethod, Japan を選択、2分ほどで完了します。

投票フォームを開く


Snowflakeの導入支援はクラスメソッドに!

クラスメソッドでは Snowflake の導入を支援しております。
製品の詳細や支援の内容についてお気軽にお問い合わせください。

Snowflakeの詳細を見る

この記事をシェアする

関連記事