Skip to content

Commit 22dea35

Browse files
committed
fix(serve): Update Nova Bedrock deployment notebook with working e2e flow
Simplify notebook to use existing completed training job with BedrockModelBuilder deploy flow. Fix Nova inference content format to use array of {text: ...} objects. Remove broken SFTTrainer cells that fail due to botocore service model mismatch.
1 parent e6ad60c commit 22dea35

File tree

1 file changed

+47
-149
lines changed

1 file changed

+47
-149
lines changed

sagemaker-serve/example_notebooks/bedrock_nova_deployment.ipynb

Lines changed: 47 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,26 @@
44
"cell_type": "markdown",
55
"metadata": {},
66
"source": [
7-
"# Deploy a Nova Model to Amazon Bedrock\n",
7+
"# Deploy a Fine-Tuned Nova Model to Amazon Bedrock\n",
88
"\n",
9-
"This notebook demonstrates how to fine-tune an Amazon Nova model using the SageMaker SDK\n",
10-
"and deploy it to Amazon Bedrock using `BedrockModelBuilder`.\n",
9+
"This notebook demonstrates how to deploy a fine-tuned Amazon Nova model to\n",
10+
"Amazon Bedrock using `BedrockModelBuilder`.\n",
1111
"\n",
1212
"The workflow:\n",
13-
"1. Fine-tune Nova Micro using `SFTTrainer`\n",
14-
"2. Create a `BedrockModelBuilder` from the completed training job\n",
13+
"1. Retrieve a completed Nova SFT training job\n",
14+
"2. Create a `BedrockModelBuilder` from the training job\n",
1515
"3. Deploy to Bedrock — the builder automatically:\n",
1616
" - Detects the model as Nova\n",
1717
" - Reads the checkpoint URI from the training job manifest\n",
18-
" - Calls `CreateCustomModel`\n",
19-
" - Polls until the model is Active\n",
20-
" - Calls `CreateCustomModelDeployment`\n",
21-
" - Polls until the deployment is Active\n",
22-
"4. Clean up resources\n",
18+
" - Calls `CreateCustomModel` and polls until Active\n",
19+
" - Calls `CreateCustomModelDeployment` and polls until Active\n",
20+
"4. Test inference\n",
21+
"5. Clean up resources\n",
2322
"\n",
2423
"**Prerequisites:**\n",
2524
"- AWS credentials with SageMaker and Bedrock access\n",
26-
"- `sagemaker-serve` and `sagemaker-train` packages installed\n",
25+
"- `sagemaker-serve` package installed\n",
26+
"- A completed Nova SFT training job\n",
2727
"- An IAM role with Bedrock and SageMaker permissions"
2828
]
2929
},
@@ -40,110 +40,24 @@
4040
"metadata": {},
4141
"outputs": [],
4242
"source": [
43-
"import os\n",
44-
"import json\n",
45-
"import time\n",
46-
"import random\n",
47-
"import boto3\n",
43+
"import os, json, time, random, boto3\n",
4844
"\n",
49-
"# Set your region — Nova fine-tuning is available in us-east-1\n",
5045
"REGION = \"us-east-1\"\n",
5146
"os.environ[\"AWS_DEFAULT_REGION\"] = REGION\n",
5247
"os.environ[\"SAGEMAKER_REGION\"] = REGION\n",
5348
"\n",
5449
"from sagemaker.core.helper.session_helper import get_execution_role\n",
55-
"\n",
5650
"role_arn = get_execution_role()\n",
57-
"account_id = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\n",
58-
"bucket = f\"sagemaker-{REGION}-{account_id}\"\n",
59-
"\n",
60-
"print(f\"Region: {REGION}\")\n",
61-
"print(f\"Account: {account_id}\")\n",
62-
"print(f\"Role: {role_arn}\")\n",
63-
"print(f\"Bucket: {bucket}\")"
64-
]
65-
},
66-
{
67-
"cell_type": "markdown",
68-
"metadata": {},
69-
"source": [
70-
"## Step 1: Prepare training data\n",
71-
"\n",
72-
"Upload a small JSONL dataset in the chat-messages format that Nova expects."
73-
]
74-
},
75-
{
76-
"cell_type": "code",
77-
"execution_count": null,
78-
"metadata": {},
79-
"outputs": [],
80-
"source": [
81-
"s3 = boto3.client(\"s3\", region_name=REGION)\n",
82-
"\n",
83-
"# Ensure the bucket exists\n",
84-
"try:\n",
85-
" s3.head_bucket(Bucket=bucket)\n",
86-
"except Exception:\n",
87-
" s3.create_bucket(\n",
88-
" Bucket=bucket,\n",
89-
" CreateBucketConfiguration={\"LocationConstraint\": REGION},\n",
90-
" )\n",
91-
" print(f\"Created bucket: {bucket}\")\n",
92-
"\n",
93-
"train_key = \"nova-example/train.jsonl\"\n",
94-
"train_uri = f\"s3://{bucket}/{train_key}\"\n",
95-
"\n",
96-
"rows = []\n",
97-
"for i in range(50):\n",
98-
" rows.append(json.dumps({\n",
99-
" \"messages\": [\n",
100-
" {\"role\": \"user\", \"content\": f\"What is {i+1} + {i+1}?\"},\n",
101-
" {\"role\": \"assistant\", \"content\": f\"The answer is {(i+1)*2}.\"}\n",
102-
" ]\n",
103-
" }))\n",
104-
"\n",
105-
"s3.put_object(Bucket=bucket, Key=train_key, Body=\"\\n\".join(rows).encode())\n",
106-
"print(f\"Uploaded {len(rows)} examples to {train_uri}\")"
107-
]
108-
},
109-
{
110-
"cell_type": "markdown",
111-
"metadata": {},
112-
"source": [
113-
"## Step 2: Create model package group\n",
114-
"\n",
115-
"SFTTrainer requires a model package group to register the fine-tuned model.\n",
116-
"We create one if it doesn't already exist."
117-
]
118-
},
119-
{
120-
"cell_type": "code",
121-
"execution_count": null,
122-
"metadata": {},
123-
"outputs": [],
124-
"source": [
125-
"sm = boto3.client(\"sagemaker\", region_name=REGION)\n",
126-
"\n",
127-
"MODEL_PACKAGE_GROUP = f\"nova-example-{account_id}\"\n",
128-
"\n",
129-
"try:\n",
130-
" sm.describe_model_package_group(ModelPackageGroupName=MODEL_PACKAGE_GROUP)\n",
131-
" print(f\"Model package group already exists: {MODEL_PACKAGE_GROUP}\")\n",
132-
"except sm.exceptions.ClientError:\n",
133-
" sm.create_model_package_group(\n",
134-
" ModelPackageGroupName=MODEL_PACKAGE_GROUP,\n",
135-
" ModelPackageGroupDescription=\"Nova fine-tuning example models\",\n",
136-
" )\n",
137-
" print(f\"Created model package group: {MODEL_PACKAGE_GROUP}\")"
51+
"print(f\"Role: {role_arn}\")"
13852
]
13953
},
14054
{
14155
"cell_type": "markdown",
14256
"metadata": {},
14357
"source": [
144-
"## Step 3: Fine-tune Nova Micro with SFTTrainer\n",
58+
"## Step 1: Retrieve the completed training job\n",
14559
"\n",
146-
"This launches a SageMaker training job. It typically takes 15-30 minutes to complete."
60+
"Use an existing completed Nova SFT training job. Replace the job name with your own."
14761
]
14862
},
14963
{
@@ -152,19 +66,9 @@
15266
"metadata": {},
15367
"outputs": [],
15468
"source": [
155-
"from sagemaker.train.sft_trainer import SFTTrainer\n",
156-
"\n",
157-
"trainer = SFTTrainer(\n",
158-
" model=\"nova-textgeneration-micro\",\n",
159-
" training_dataset=train_uri,\n",
160-
" accept_eula=True,\n",
161-
" model_package_group=MODEL_PACKAGE_GROUP,\n",
162-
")\n",
163-
"\n",
164-
"# Set wait=True to block until training completes\n",
165-
"trainer.train(wait=True)\n",
69+
"from sagemaker.core.resources import TrainingJob\n",
16670
"\n",
167-
"training_job = trainer._latest_training_job\n",
71+
"training_job = TrainingJob.get(training_job_name=\"nova-textgeneration-micro-sft-20251208154822\")\n",
16872
"print(f\"Training job: {training_job.training_job_name}\")\n",
16973
"print(f\"Status: {training_job.training_job_status}\")"
17074
]
@@ -173,7 +77,7 @@
17377
"cell_type": "markdown",
17478
"metadata": {},
17579
"source": [
176-
"## Step 4: Deploy to Bedrock with BedrockModelBuilder\n",
80+
"## Step 2: Deploy to Bedrock with BedrockModelBuilder\n",
17781
"\n",
17882
"The builder handles the full deployment flow:\n",
17983
"- Fetches the model package from the training job\n",
@@ -192,9 +96,8 @@
19296
"from sagemaker.serve.bedrock_model_builder import BedrockModelBuilder\n",
19397
"\n",
19498
"builder = BedrockModelBuilder(model=training_job)\n",
195-
"\n",
196-
"print(f\"Model package: {builder.model_package}\")\n",
197-
"print(f\"S3 artifacts: {builder.s3_model_artifacts}\")"
99+
"print(f\"Model package: {builder.model_package}\")\n",
100+
"print(f\"S3 artifacts: {builder.s3_model_artifacts}\")"
198101
]
199102
},
200103
{
@@ -204,7 +107,7 @@
204107
"outputs": [],
205108
"source": [
206109
"rand = random.randint(1000, 9999)\n",
207-
"custom_model_name = f\"nova-example-{rand}-{int(time.time())}\"\n",
110+
"custom_model_name = f\"nova-e2e-{rand}-{int(time.time())}\"\n",
208111
"deployment_name = f\"{custom_model_name}-dep\"\n",
209112
"\n",
210113
"print(f\"Deploying as: {custom_model_name}\")\n",
@@ -225,9 +128,10 @@
225128
"cell_type": "markdown",
226129
"metadata": {},
227130
"source": [
228-
"## Step 5: Test inference (optional)\n",
131+
"## Step 3: Test inference\n",
229132
"\n",
230-
"Once the deployment is Active, you can invoke it via the Bedrock Runtime API."
133+
"Once the deployment is Active, invoke it via the Bedrock Runtime API.\n",
134+
"Nova expects `content` as an array of objects with a `text` key."
231135
]
232136
},
233137
{
@@ -238,32 +142,23 @@
238142
"source": [
239143
"bedrock_runtime = boto3.client(\"bedrock-runtime\", region_name=REGION)\n",
240144
"\n",
241-
"# Get the model ARN from the deployment\n",
242-
"bedrock = boto3.client(\"bedrock\", region_name=REGION)\n",
243-
"dep_info = bedrock.get_custom_model_deployment(\n",
244-
" customModelDeploymentIdentifier=deployment_arn\n",
245-
")\n",
246-
"model_arn = dep_info.get(\"modelArn\")\n",
247-
"print(f\"Model ARN: {model_arn}\")\n",
248-
"\n",
249-
"# Invoke\n",
250-
"invoke_response = bedrock_runtime.invoke_model(\n",
145+
"resp = bedrock_runtime.invoke_model(\n",
251146
" modelId=deployment_arn,\n",
252147
" contentType=\"application/json\",\n",
253148
" body=json.dumps({\n",
254-
" \"messages\": [{\"role\": \"user\", \"content\": \"What is 7 + 7?\"}]\n",
149+
" \"messages\": [{\"role\": \"user\", \"content\": [{\"text\": \"What is 7 + 7?\"}]}]\n",
255150
" }),\n",
256151
")\n",
257152
"\n",
258-
"result = json.loads(invoke_response[\"body\"].read())\n",
259-
"print(f\"Response: {result}\")"
153+
"result = json.loads(resp[\"body\"].read())\n",
154+
"print(f\"Response: {json.dumps(result, indent=2)}\")"
260155
]
261156
},
262157
{
263158
"cell_type": "markdown",
264159
"metadata": {},
265160
"source": [
266-
"## Step 6: Cleanup\n",
161+
"## Step 4: Cleanup\n",
267162
"\n",
268163
"Delete the deployment and custom model to avoid ongoing charges."
269164
]
@@ -276,23 +171,26 @@
276171
"source": [
277172
"bedrock = boto3.client(\"bedrock\", region_name=REGION)\n",
278173
"\n",
174+
"dep_info = bedrock.get_custom_model_deployment(\n",
175+
" customModelDeploymentIdentifier=deployment_arn\n",
176+
")\n",
177+
"model_arn = dep_info.get(\"modelArn\")\n",
178+
"\n",
279179
"# Delete deployment first\n",
280-
"if deployment_arn:\n",
281-
" try:\n",
282-
" bedrock.delete_custom_model_deployment(\n",
283-
" customModelDeploymentIdentifier=deployment_arn\n",
284-
" )\n",
285-
" print(f\"Deleted deployment: {deployment_arn}\")\n",
286-
" except Exception as e:\n",
287-
" print(f\"Failed to delete deployment: {e}\")\n",
180+
"try:\n",
181+
" bedrock.delete_custom_model_deployment(\n",
182+
" customModelDeploymentIdentifier=deployment_arn\n",
183+
" )\n",
184+
" print(f\"Deleted deployment: {deployment_arn}\")\n",
185+
"except Exception as e:\n",
186+
" print(f\"Failed to delete deployment: {e}\")\n",
288187
"\n",
289188
"# Then delete the custom model\n",
290-
"if model_arn:\n",
291-
" try:\n",
292-
" bedrock.delete_custom_model(modelIdentifier=model_arn)\n",
293-
" print(f\"Deleted custom model: {model_arn}\")\n",
294-
" except Exception as e:\n",
295-
" print(f\"Failed to delete custom model: {e}\")"
189+
"try:\n",
190+
" bedrock.delete_custom_model(modelIdentifier=model_arn)\n",
191+
" print(f\"Deleted custom model: {model_arn}\")\n",
192+
"except Exception as e:\n",
193+
" print(f\"Failed to delete custom model: {e}\")"
296194
]
297195
}
298196
],
@@ -309,4 +207,4 @@
309207
},
310208
"nbformat": 4,
311209
"nbformat_minor": 4
312-
}
210+
}

0 commit comments

Comments
 (0)