
Nemotron 9B-v2-Japanese を SageMaker 東京リージョンに VPC 閉域構成でデプロイしてみた
はじめに
こんにちは、クラスメソッド製造ビジネステクノロジー部の森茂です。
前回の記事では、NVIDIA 公式の日本語強化 LLM Nemotron 9B-v2-Japanese を DGX Spark で動かし、Tool Calling や RAG など色々なケースで試してみました。
DGX Spark でのローカル検証は手軽ですが、「本番環境でどう動かすか」となると話が変わってきます。個人情報や機密データを扱うケースでは、データが日本国内から出ない閉域構成が求められることも多いですよね。
この記事では、Nemotron 9B-v2-Japanese を Amazon SageMaker の東京リージョンにデプロイし、VPC 内の閉域構成で動作するところまでを試してみました。前回と同じベンチマーク(JCommonsenseQA)で比較しているので、ローカル環境とクラウド環境の違いも見えてくるかと思います。
SageMaker で動かす前に
推論エンジンは vLLM を使う
前回の記事では DGX Spark でも NGC コンテナ版 vLLM(v0.13.0)が動作することを確認しましたが、SageMaker では LMI コンテナ経由で vLLM をバックエンドとして利用します。
SageMaker の LMI(Large Model Inference)コンテナ経由で vLLM をバックエンドとして指定します。LMI コンテナは V17(2025 年 9 月)以降、vLLM の AsyncLLMEngine を統合した async モードがデフォルトになっています。今回は async モードで起動し、OPTION_ROLLING_BATCH=disable と OPTION_CHAT_TEMPLATE でチャットテンプレートを指定する構成にしています。
JumpStart / Marketplace の状況
2026 年 2 月時点で、SageMaker JumpStart に Nemotron 9B-v2-Japanese は登録されていません。英語版の Nemotron nano 9b v2 は AWS Marketplace に登録されていますが、日本語版は未登録です。自分で LMI コンテナを設定してデプロイします。
デプロイ構成
実線は推論リクエストの流れ、破線はデプロイ時のモデルロードを表しています。すべての通信が VPC 内で完結する構成です。
- モデルは HuggingFace Hub から事前に S3 にダウンロード。エンドポイントからインターネットへのアクセスは不要
- SageMaker Runtime への呼び出しは VPC エンドポイント経由。データが VPC の外に出ない
- 推論エンジンは LMI コンテナ内の vLLM。BF16 フル精度で A10G(24GB)に収まる
| 項目 | 設定 |
|---|---|
| インスタンス | ml.g5.2xlarge(1x A10G, 24GB VRAM) |
| リージョン | ap-northeast-1(東京) |
| コンテナ | SageMaker LMI V20(DJL 0.36.0 + vLLM) |
| 動作モード | async モード(OPTION_ROLLING_BATCH=disable) |
| モデル精度 | BF16(17.8GB) |
| max_model_len | 4096 |
インスタンスに ml.g5.2xlarge を選んだ理由は、BF16(17.8GB)+ vLLM オーバーヘッド(~1GB)+ KV キャッシュ(Attention 4 層分で ~2-3GB)の合計が約 21GB で、A10G の 24GB に収まるためです。Mamba-2 ハイブリッドアーキテクチャでは 56 層中 Attention が 4 層だけなので、KV キャッシュは全層 Attention の約 1/14 で済みます。
デプロイ手順
モデルを S3 に配置する
閉域構成では、エンドポイントから HuggingFace Hub にアクセスできません。事前にモデルをダウンロードして S3 にアップロードしておきます。
このステップだけがインターネット接続を必要とします。実行環境には AWS CLI(v2.32.0 以降)と HuggingFace CLI(hf)が必要です。hf は brew install huggingface-cli や uv があれば uvx hf でインストール不要で使えます。ディスク空き容量は 20GB 以上を確保してください(モデルサイズ約 18GB)。手元の PC でも EC2 でも、条件を満たしていればどこからでも実行できます。
AWS CLI の認証は aws login が手軽です。ブラウザが開いてマネジメントコンソールと同じ方法でサインインでき、一時的な認証情報が自動でローテーションされます。hf は公開モデルのダウンロードなのでログイン不要です。
# HuggingFace からモデルをダウンロード(約 18GB)
hf download nvidia/NVIDIA-Nemotron-Nano-9B-v2-Japanese \
--local-dir /tmp/nemotron-9b-japanese
# S3 にアップロード(バケット名は任意、後続の手順と合わせる)
aws s3 cp /tmp/nemotron-9b-japanese \
s3://<your-bucket>/models/nemotron-9b-v2-japanese/ \
--recursive
S3 のバケット名は任意ですが、SageMaker のデフォルトバケット(sagemaker-{region}-{account-id})を使うと後続の手順と揃えやすいです。sagemaker.Session().default_bucket() を呼ぶと自動作成されるので、バケットを事前に作っておく必要はありません。
Python SDK で実行する場合
AWS CLI の代わりに Python SDK でも同じことができます。sagemaker.Session().default_bucket() でバケットの自動作成と命名を任せられるので、バケット名を意識せずに済みます。
from huggingface_hub import snapshot_download
from sagemaker.s3 import S3Uploader
import sagemaker
sess = sagemaker.Session()
bucket = sess.default_bucket()
model_dir = snapshot_download(
"nvidia/NVIDIA-Nemotron-Nano-9B-v2-Japanese",
local_dir="/tmp/nemotron-9b-japanese"
)
s3_model_uri = S3Uploader.upload(
local_path=model_dir,
desired_s3_uri=f"s3://{bucket}/models/nemotron-9b-v2-japanese/"
)
print(f"Model uploaded to: {s3_model_uri}")
以降の手順は VPC 内で完結します。
VPC エンドポイントを作成する
SageMaker エンドポイントを閉域構成で動かすには、VPC 内にインターフェースエンドポイントを作成して、AWS サービスへの通信を VPC 内に閉じ込めます。
必要な VPC エンドポイントは 6 つ。
| エンドポイント | タイプ | 用途 |
|---|---|---|
com.amazonaws.ap-northeast-1.sagemaker.api |
Interface | SageMaker API |
com.amazonaws.ap-northeast-1.sagemaker.runtime |
Interface | 推論呼び出し |
com.amazonaws.ap-northeast-1.s3 |
Gateway | モデルアーティファクト取得 |
com.amazonaws.ap-northeast-1.ecr.dkr |
Interface | コンテナイメージ取得 |
com.amazonaws.ap-northeast-1.ecr.api |
Interface | ECR API |
com.amazonaws.ap-northeast-1.logs |
Interface | CloudWatch Logs(推奨) |
上の 5 つはデプロイに必須、CloudWatch Logs はデバッグ用に追加しておくと安心です。完全閉域の VPC ではコンテナログが CloudWatch に届かないため、デプロイ失敗時に原因を追えなくなります。S3 は Gateway 型(無料)、それ以外は Interface 型(ENI 課金あり)。
Interface 型のエンドポイントでは、セキュリティグループのインバウンドルールで HTTPS(443)を VPC CIDR から許可し、プライベート DNS を有効にしておきます。プライベート DNS を有効にすれば、SDK のコードを変更せずにそのまま閉域経由で通信できます。
SageMaker モデルを作成する
AWS コンソールの SageMaker から、モデルを作成します。LMI コンテナに vLLM バックエンドを指定し、Nemotron 固有の設定を環境変数で渡します。
環境変数のなかで見落としがちなのが 2 つあります。
OPTION_TRUST_REMOTE_CODE=true は Mamba-2 ハイブリッドアーキテクチャのカスタムモデルコードを読み込むために必須で、これがないとモデルのロード自体が失敗します。
OPTION_MAMBA_SSM_CACHE_DTYPE=float32 は Mamba-2 レイヤーの状態キャッシュの精度を指定するもので、省略すると推論結果の精度が劣化します。エラーにはならないため気づきにくく、忘れずに設定しておくのがおすすめです。
環境変数をまとめると以下のとおりです。
| 環境変数 | 値 | 説明 |
|---|---|---|
HF_MODEL_ID |
/opt/ml/model |
S3 からマウントされるパス |
OPTION_ROLLING_BATCH |
disable |
rolling batch を無効化(async モード) |
OPTION_DTYPE |
bf16 |
モデルの精度 |
OPTION_TRUST_REMOTE_CODE |
true |
Mamba2 カスタムコードの読み込み |
OPTION_MAMBA_SSM_CACHE_DTYPE |
float32 |
Mamba2 状態キャッシュの精度 |
OPTION_TENSOR_PARALLEL_DEGREE |
1 |
GPU 並列数(A10G 1 枚) |
OPTION_MAX_MODEL_LEN |
4096 |
最大コンテキスト長 |
OPTION_CHAT_TEMPLATE |
/opt/ml/model/chat_template.jinja |
チャットテンプレートファイル |
OPTION_ROLLING_BATCH=disable で LMI の rolling batch モードを無効化し、async モード(V17 以降のデフォルト)で起動しています。async モードは vLLM の AsyncLLMEngine をそのまま利用するため、rolling batch モードより性能と安定性が向上しています。OPTION_CHAT_TEMPLATE はモデルに同梱されているチャットテンプレートのパスで、推論リクエスト時にプロンプトへ自動適用されます。
Python SDK で作成する場合
コンソール操作の代わりに Python SDK でも同じ構成を作成できます。VPC 閉域構成では vpc_config でサブネットとセキュリティグループを指定します。
import sagemaker
import boto3
role = sagemaker.get_execution_role()
sess = sagemaker.Session()
region = "ap-northeast-1"
# LMI コンテナ URI(2026-02 時点の最新: LMI V20 / DJL 0.36.0)
container_uri = sagemaker.image_uris.retrieve(
framework="djl-lmi",
version="0.36.0",
region=region
)
env = {
"HF_MODEL_ID": "/opt/ml/model",
"OPTION_ROLLING_BATCH": "disable",
"OPTION_DTYPE": "bf16",
"OPTION_TRUST_REMOTE_CODE": "true",
"OPTION_MAMBA_SSM_CACHE_DTYPE": "float32",
"OPTION_TENSOR_PARALLEL_DEGREE": "1",
"OPTION_MAX_MODEL_LEN": "4096",
"OPTION_CHAT_TEMPLATE": "/opt/ml/model/chat_template.jinja",
}
# VPC 情報(デフォルト VPC を使う例)
ec2 = boto3.client("ec2", region_name=region)
vpcs = ec2.describe_vpcs(Filters=[{"Name": "isDefault", "Values": ["true"]}])
vpc_id = vpcs["Vpcs"][0]["VpcId"]
subnets = ec2.describe_subnets(Filters=[{"Name": "vpc-id", "Values": [vpc_id]}])
subnet_ids = [s["SubnetId"] for s in subnets["Subnets"]]
sgs = ec2.describe_security_groups(
Filters=[{"Name": "vpc-id", "Values": [vpc_id]}, {"Name": "group-name", "Values": ["default"]}]
)
sg_id = sgs["SecurityGroups"][0]["GroupId"]
model = sagemaker.Model(
image_uri=container_uri,
model_data=f"s3://{sess.default_bucket()}/models/nemotron-9b-v2-japanese/",
env=env,
role=role,
vpc_config={"Subnets": subnet_ids, "SecurityGroupIds": [sg_id]},
)
predictor = model.deploy(
instance_type="ml.g5.2xlarge",
initial_instance_count=1,
container_startup_health_check_timeout=900,
model_data_download_timeout=1800,
)
エンドポイントを作成する
モデルの作成が完了したら、エンドポイント設定を作成してエンドポイントをデプロイします。
タイムアウト設定を 2 点変更しておくのがおすすめです。container_startup_health_check_timeout は 900 秒(デフォルト: 300 秒)に延長してください。18GB のモデルロードと vLLM の初期化に時間がかかるため、デフォルトだとタイムアウトする可能性があります。model_data_download_timeout も同様に 1800 秒に延長しておくと安心です。
デプロイ完了の確認
エンドポイントのステータスが InService になれば、デプロイ完了です。自分の環境では S3 からのモデルダウンロードと vLLM の初期化を含めて約 11 分(677 秒)でした。
動作確認として、簡単なプロンプトを送ってみます。
import json
import boto3
sm_runtime = boto3.client("sagemaker-runtime", region_name="ap-northeast-1")
payload = {
"inputs": "東京タワーの高さは?",
"parameters": {
"max_new_tokens": 128,
"temperature": 0,
}
}
response = sm_runtime.invoke_endpoint(
EndpointName="nemotron-9b-v2-japanese",
ContentType="application/json",
Body=json.dumps(payload),
)
result = json.loads(response["Body"].read().decode("utf-8"))
print(result["generated_text"])
正常にレスポンスが返ってくれば、閉域構成でのデプロイは完了です。
ハマったポイント
デプロイから動作検証までの過程で、いくつかのハマりポイントがありました。中でもチャットテンプレートの特殊トークン問題は、ベンチマーク精度に直結する重要な発見でした。
チャットテンプレートの特殊トークン問題
今回の検証で一番大きな発見がこれです。
Nemotron 9B-v2-Japanese のチャットテンプレートでは、System / User / Assistant のロール区切りに特殊トークンを使います。HuggingFace Hub 上の README やブログ記事では <extra_id_0> <extra_id_1> <extra_id_2> という記法をよく見かけますが、実はこれらは正しい特殊トークンではありません。
モデルの tokenizer_config.json を確認すると、実際のロール区切りトークンは <SPECIAL_10>(ID: 10)、<SPECIAL_11>(ID: 11)、<SPECIAL_12>(ID: 12)です。<extra_id_0> という文字列をトークナイザーに渡すと、単一の特殊トークンとして認識されず < extra _id _ 0 > の 6 サブワードに分割されてしまいます。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(
"nvidia/NVIDIA-Nemotron-Nano-9B-v2-Japanese",
trust_remote_code=True
)
# 間違い: BOS を除いて 6 サブワードに分割される
tokenizer.encode("<extra_id_0>")
# → [1, 523, 15683, 1620, 290, 29900, 1572] (BOS + 6 subwords)
# 正解: 単一の特殊トークン
tokenizer.encode("<SPECIAL_10>")
# → [1, 10] (BOS + special token)
テンプレート全体で見ると、<extra_id_X> 方式ではロール区切りだけで 18 個余計なトークンがプロンプトに混入する計算です。モデルにとっては意味のないノイズなので、推論精度に影響します。
実際にベンチマークで比較したところ、<extra_id_X> 方式では正答率 83.3% だったのに対し、正しい <SPECIAL_X> トークンを使うと 84.8% に改善しました。1.5pp の差は小さく見えるかもしれませんが、特定のカテゴリ(正解が A の問題)では 66.2% → 75.0% と大きな改善が見られます。
対策としては、HuggingFace の tokenizer.apply_chat_template() を使ってローカルでテンプレートを適用し、レンダリング済みのテキストを inputs として送信するのが確実です。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(
"nvidia/NVIDIA-Nemotron-Nano-9B-v2-Japanese",
trust_remote_code=True
)
messages = [{"role": "user", "content": "東京タワーの高さは?"}]
formatted = tokenizer.apply_chat_template(
messages, tokenize=False,
add_generation_prompt=True,
enable_thinking=False,
)
# formatted には <SPECIAL_10>, <SPECIAL_11> 等が正しく含まれる
payload = {
"inputs": formatted,
"parameters": {"max_new_tokens": 128, "temperature": 0},
}
LMI の messages API の制限
LMI コンテナの async モードは OpenAI 互換の messages フォーマットもサポートしていますが、Nemotron 9B-v2-Japanese ではうまく動作しませんでした。すべてのリクエストで prompt_tokens: 8 と表示され、プロンプトの内容にかかわらず同じ応答が返ってきます。
チャットテンプレート内の <SPECIAL_X> トークンが、LMI 側のトークナイザー処理で正しく解釈されていない可能性が高いです。前述のとおり、ローカルでテンプレートを適用して inputs フォーマットで送信すれば問題なく動作します。
S3 モデルの指定方法
sagemaker-core v3 SDK(sagemaker.core.resources)を使う場合、S3ModelDataSource の設定に注意が必要です。s3_data_type は "S3Prefix" を指定し、compression_type は "None" を明示してください。compression_type を省略すると "Gzip" がデフォルトになり、非圧縮のモデルファイルを解凍しようとしてエラーになります。
ベンチマーク比較
JCommonsenseQA
前回の DGX Spark での検証と同一条件で計測しました。
| 項目 | 設定 |
|---|---|
| データセット | JCommonsenseQA v1.1 |
| 分割 | validation(1,119 問) |
| 評価方式 | 3-shot |
| temperature | 0 |
| シンキングモード | OFF |
| 環境 | vLLM バージョン | 正答率 | 正答数 | 平均レイテンシ |
|---|---|---|---|---|
| DGX Spark(NGC vLLM BF16) | v0.13.0 | 83.5% | 934/1,119 | 0.38 秒/問 |
| SageMaker(LMI vLLM BF16) | v0.15.1 | 84.8% | 949/1,119 | 0.30 秒/問 |
DGX Spark(NGC コンテナ)と SageMaker(LMI コンテナ)で、vLLM のバージョンも GPU も異なりますが、正答率の差は 1.3pp に収まっています。同じ推論エンジンであれば、ハードウェアが変わっても精度はほぼ一致するという結果です。
コスト比較
| 項目 | DGX Spark | SageMaker (ml.g5.2xlarge) |
|---|---|---|
| 初期コスト | $3,999(本体) | なし |
| ランニングコスト | 電気代のみ | $2.197/h |
| 月額目安(平日 8h 稼働) | ~ 数千円 | ~ $352 |
| 月額目安(24h 稼働) | ~ 数千円 | ~ $1,604 |
| ネットワーク | オフライン可 | VPC 閉域可 |
| スケーラビリティ | 1 台固定 | Auto Scaling 可 |
DGX Spark は一度購入すれば電気代だけで使い続けられるので、開発・実験フェーズでは圧倒的にコスパが良いです。一方で SageMaker は、使わないときにエンドポイントを削除すれば課金が止まるため、利用頻度が低い本番環境やバッチ処理的な使い方には向いています。
VPC エンドポイント(Interface 型)の費用も忘れずに考慮しておきたいところです。東京リージョンでは ENI 1 つあたり $0.014/h で、AZ 数 × エンドポイント数の分だけ積み上がります。エンドポイントを使わないときは VPC エンドポイントも削除しておくと節約になります。
クリーンアップ
検証が終わったら、エンドポイントを忘れずに削除します。ml.g5.2xlarge は $2.197/h なので、1 日放置すると約 $53 の課金。
削除する対象は 3 つ。エンドポイント → エンドポイント設定 → モデルの順に、依存関係の逆順で削除します。
import boto3
sm = boto3.client("sagemaker", region_name="ap-northeast-1")
# 1. エンドポイント削除
sm.delete_endpoint(EndpointName="nemotron-9b-v2-japanese")
# 2. エンドポイント設定削除
sm.delete_endpoint_config(EndpointConfigName="nemotron-9b-v2-japanese")
# 3. モデル削除
sm.delete_model(ModelName="nemotron-9b-v2-japanese")
VPC エンドポイントも不要であれば削除しておきましょう。VPC コンソールのエンドポイント一覧から、今回作成した 6 つを選択して削除できます。
まとめ
Nemotron 9B-v2-Japanese を SageMaker 東京リージョンに閉域デプロイすること自体は問題なくできました。VPC エンドポイント 6 つの設定さえ済ませれば、データが日本国内の VPC から出ない構成を実現できます。デプロイ時間は約 11 分、推論レイテンシも 0.30 秒/問と実用的な水準です。
今回の検証で得られた大きな収穫は、vLLM であればハードウェアが変わっても精度がほぼ一致するということです。DGX Spark(NGC vLLM、83.5%)と SageMaker(LMI vLLM、84.8%)で、バージョンも GPU も異なるにもかかわらず差は 1.3pp でした。同じ推論エンジンを使う限り、ローカルで確認した精度がクラウドでも再現できるのは、デプロイ先を選定するうえで安心材料になりますね。
次回は SageMaker 上でのドメイン特化ファインチューニングを試してみる予定です。







