
Finetuning
- 121 installs
- 850 repo stars
- Updated August 3, 2026
- awslabs/agent-plugins
Finetuning is a Claude skill that generates code to fine-tune a base model using Amazon SageMaker serverless training jobs, supporting SFT, DPO, RLVR, and RLAIF trainers.
About
Finetuning is a skill that generates code to fine-tune a base model using Amazon SageMaker serverless training jobs. A developer uses it once a technique and base model are selected and a dataset is uploaded, to produce training code for SFT, DPO, RLVR, or RLAIF. It handles RLVR reward functions, RLAIF custom prompts, and EULA review, following strict template-based code generation.
- Generates code to fine-tune a base model via SageMaker serverless training jobs
- Supports SFT, DPO, RLVR, and RLAIF trainers, including RLVR Lambda reward functions
- Enforces EULA review and strict minimal-code generation from templates
Finetuning by the numbers
- 121 all-time installs (skills.sh)
- Ranked #765 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
finetuning capabilities & compatibility
Requires an AWS/SageMaker account; training jobs incur AWS compute costs
- Capabilities
- finetuning setup · dataset evaluation · dataset transformation
- Works with
- aws
- Use cases
- data analysis · orchestration
- Pricing
- Bring your own API key
What finetuning says it does
Generates code that fine-tunes a base model using SageMaker serverless training jobs.
Supports SFT, DPO, RLVR, and RLAIF trainers, including RLVR Lambda reward function and RLAIF custom prompt creation.
npx skills add https://github.com/awslabs/agent-plugins --skill finetuningAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 121 |
|---|---|
| repo stars | ★ 850 |
| Last updated | August 3, 2026 |
| Repository | awslabs/agent-plugins ↗ |
What it does
Generate SageMaker training-job code to fine-tune a selected base model with SFT, DPO, RLVR, or RLAIF.
Who is it for?
Generating SageMaker training-job code to fine-tune a selected base model
Skip if: Selecting a model or technique (use finetuning-setup) or non-SageMaker training
When should I use this skill?
A fine-tuning technique, base model, and dataset are ready and you need to generate the SageMaker training code.
What you get
Template-faithful SageMaker training code that fine-tunes the chosen base model with the selected technique.
- SageMaker fine-tuning code (notebook or script)
- RLVR reward function
- EULA acceptance flow
By the numbers
- supports 4 trainers (SFT, DPO, RLVR, RLAIF)
- 5 code templates (sft, dpo, rlvr, rlaif builtin, rlaif custom)
Files
Prerequisites
Before starting this workflow, verify:
1. A use_case_spec.md file exists
- If missing: Activate the
use-case-specificationskill first, then resume - DON'T EVER offer to create a use case spec without activating the use-case-specification skill.
2. A fine-tuning technique (SFT, DPO, RLVR, RLAIF, or CPT/RFT (for Nova)) and base model have already been selected
- If missing: Activate the
model-selectionand/orfinetuning-techniqueskills to collect what's missing, then resume - Don't make recommendations on the spot. You MUST activate the appropriate skill.
3. A base model name available on SageMakerHub has been identified
- If missing: Activate the
model-selectionskill to get it - Important: Only use the model name that
model-selectionretrieves, as it may differ from other commonly used names for the same model
4. The SDK environment has been verified (SDK version, region, execution role)
- If not done: Activate the
sdk-getting-startedskill first, then resume
5. A training dataset uploaded to a bucket in the environment's default region.
- If not met: Help the user upload the dataset to the correct S3
---
Critical Rules
Code Generation Rules
- ✅ Use EXACTLY the imports shown in each code template
- ❌ Do NOT add additional imports even if they seem helpful
- ❌ Do NOT create variables before they're needed in that section
- 📋 Copy the code structure precisely - no improvisation
- 🎯 Follow the minimal code principle strictly
- ✅ When writing code, make sure the indentation and f strings are correct
User Communication Rules
- ❌ NEVER offer to move on to a downstream skill while training is in progress (logically impossible)
- ❌ NEVER set ACCEPT_EULA to True without explicit user confirmation in the conversation
- ✅ Always mention both the number AND title of sections you reference
- ✅ If user asks how to run (notebook): If
run_cellis available, offer to run it. Otherwise, tell them to run cells one by one (mention ipykernel requirement). - ✅ If user asks how to run (script): Tell them to run with
python3 <script>.py
---
Workflow
1. Code Generation Setup
1.1 Directory Setup
1. Identify project directory from conversation context
- If unclear (multiple relevant directories exist) → Ask user which folder to use
- If no project directory exists → activate the directory-management skill to set one up
⏸ Wait for user.
1.2 Select Code Template
Read references/code_output_guide.md for output format rules, then read the code template matching the finetuning strategy:
- SFT →
code_templates/sft.py - DPO →
code_templates/dpo.py - RLVR →
code_templates/rlvr.py - RLAIF with built-in rewards →
code_templates/rlaif_builtin.py - RLAIF with custom prompt →
code_templates/rlaif_custom_prompt.py
The template is a Python file where each # Cell N: Label comment marks the start of a new section. Split on these markers — everything between one marker and the next becomes one unit of output.
1.3 Generate Code
1. Write the code from the template following the rules in code_output_guide.md 2. Use same order, dependencies, and imports as the template 3. DO NOT improvise or add extra code 4. If the model is NOT a Meta/Llama model (model ID does NOT start with meta-):
- Omit the
ACCEPT_EULA = Falseline from the config cell - Omit the
accept_eula=ACCEPT_EULA,line from the trainer call
5. If the model is from the Nova family, omit any code containing max_epochs or lr_warmup_steps_ratio from the Configure Trainer section and the Hyperparameter Overrides section
1.4 Auto-Generate Configuration Values
In the 'Setup & Credentials' cell, populate:
1. BASE_MODEL
- Use the exact SageMakerHub model name from context
2. MODEL_PACKAGE_GROUP_NAME
- Generate from use case (read
use_case_spec.mdif needed) - Format rules:
- Lowercase, alphanumeric with hyphens only
- 1-63 characters
- Pattern:
[a-zA-Z0-9](-*[a-zA-Z0-9]){0,62} - Example: "Customer Support Chatbot" →
customer-support-chatbot-v1
3. Save notebook
2. RLVR Reward Function (for RLVR only, skip this section if technique is SFT or DPO)
2.1 Check Reward Function Status
- Ask if user has a reward function already, or would like help creating one.
- If user says they have one → Ask for the SageMaker Hub Evaluator ARN. Only proceed to Section 2.3 once the user provides a valid Evaluator ARN. If they don't have it registered as a SageMaker Hub Evaluator, continue to 2.2.
- If user says they do not have one → Continue to 2.2
2.2 Generate Reward Function From Template
1. Follow workflow in references/rlvr_reward_function.md section "Helping Users Create Custom Reward Functions"
2.3 Set CUSTOM_REWARD_FUNCTION value
1. Set the value for CUSTOM_REWARD_FUNCTION in the Notebook with the ARN of the reward function (either given directly by the user, or from the function generation code as evaluator.arn).
3. RLAIF (for RLAIF only, skip this section if technique is not RLAIF)
Read references/rlaif_guide.md and follow its instructions.
4. EULA review and acceptance
1. Look up the official license link for the selected base model from references/eula_links.md 2. Display the license to the user following the phrasing in references/eula_links.md. For OSS models: "This model is licensed under {License}. Please review the license terms here: {URL}." For Nova models: "This model is subject to the AWS Service Terms: {URL}." 3. Check if the selected base model is a Meta/Llama model (model ID starts with meta-)
- If Meta/Llama: Tell the user they must read and agree to the EULA before using this model. Ask: "Do you accept the license terms? (yes/no)". If the user confirms, set
ACCEPT_EULA = Trueand uncommentaccept_eula=ACCEPT_EULAin the generated notebook. If the user declines, leaveACCEPT_EULA = Falseand warn that training will fail without acceptance. - If non-Meta: Inform the user of the license for their awareness. No code-level action needed — the
ACCEPT_EULAvariable andaccept_eulaparameter should already be omitted from the notebook (see Step 1.3).
5. Post-Generation
After generating the code, offer to run it. Training can take hours depending on your dataset and model.
Notebook mode: If run_cell is available, offer to run the cells. Otherwise tell the user to run cells themselves.
Script mode: Present the user with options:
"Would you like me to:
>
1. Leave it to you — run with python scripts/[script_name]2. Run it and wait until it's done
3. Start it but don't wait — we can check status later"
- Option 1: Done. Wait for user to come back.
- Option 2: Execute the script as-is.
trainer.train(wait=True)blocks until complete. Report final status. - Option 3: Change
wait=Truetowait=Falsein the script, execute, report the training job name.
Checking status:
describe-training-job --training-job-name NAME→TrainingJobStatus,FailureReason,SecondaryStatusTransitions- For model package ARN after completion:
list-model-packages --model-package-group-name GROUP_NAME --sort-by CreationTime --sort-order Descending --max-results 1
Showing results after completion:
- Use
scripts/mlflow_reference.pyas the pattern to query MLflow metrics - Present loss by epoch as a text table (total_loss, val_eval_total_loss for SFT; rewards/margins for DPO; critic/rewards/mean for RLVR)
CRITICAL:
- DON'T suggest moving to next steps before training completes
- DON'T elaborate on the next steps unless the user specifically asks you about them.
6. Continuous Customization
If the user wants to finetune a model they had already customized, follow the instructions in references/continuous_customization.md
---
References
rlvr_reward_function.md- Lambda reward function creation guide (RLVR only)templates/rlvr_reward_function_source_template.py- Lambda reward function source template for open-weights models (RLVR only)templates/nova_rlvr_reward_function_source_template.py- Lambda reward function source template for Nova 2.0 Lite (RLVR only)code_templates/sft.py- Complete notebook template for Supervised Fine-Tuning (OSS path)code_templates/dpo.py- Complete notebook template for Direct Preference Optimization (OSS path)code_templates/rlvr.py- Complete notebook template for Reinforcement Learning from Verifiable Rewards (OSS path)references/continuous_customization.md- Instructions on fine-tuning an already fine-tuned model.rlaif_guide.md- instructions on RLAIF finetuning optionsrlaif_builtin.py- Code template for RLAIF with built-in judge promptrlaif_custom_prompt.py- Code template for RLAIF with custom judge prompt
# DPO (Direct Preference Optimization) Template
# Cell 0 [markdown]: Fine-Tuning
# Cell 1: Install Dependencies
%pip install --upgrade 'sagemaker>=3.7.1,<4.0' boto3 -q # NOTEBOOK_ONLY
# Cell 2: Setup & Credentials
import boto3
import json
from pathlib import Path
from botocore.exceptions import ClientError
from sagemaker.ai_registry.dataset import DataSet
from sagemaker.core.resources import ModelPackageGroup
from sagemaker.core.helper.session_helper import Session, get_execution_role
from sagemaker.core import Attribution, set_attribution
set_attribution(Attribution.SAGEMAKER_AGENT_PLUGIN)
# Setup
sm_client = boto3.Session().client("sagemaker")
sagemaker_session = Session(sagemaker_client=sm_client)
bucket = sagemaker_session.default_bucket()
# Configuration - USER please fill in these fields with your information:
BASE_MODEL = "" # e.g., "meta-textgeneration-llama-3-8b"
TRAINING_DATA_S3 = "" # S3 path
S3_OUTPUT_PATH = f"s3://{bucket}/finetuning-output/"
ROLE_ARN = get_execution_role() # You can change this to a specific role.
ACCEPT_EULA = False # Set to True to accept the base model's End-User License Agreement
MODEL_PACKAGE_GROUP_NAME = "" # Auto-generated based on use case
# Cell 3: Create Dataset and Model Package Group
# Create Model Package Group
try:
model_package_group = ModelPackageGroup.create(
model_package_group_name=MODEL_PACKAGE_GROUP_NAME,
model_package_group_description="",
)
print(f"Created new model package group named {MODEL_PACKAGE_GROUP_NAME}")
except ClientError as e:
if e.response['Error']['Code'] in ('ResourceInUse', 'ValidationException'):
model_package_group = ModelPackageGroup.get(model_package_group_name=MODEL_PACKAGE_GROUP_NAME)
print(f"There is already a model package group with the name {MODEL_PACKAGE_GROUP_NAME}.\nIf you want to save your finetuned model under a different name, change the value of MODEL_PACKAGE_GROUP_NAME in the previous cell.")
else:
raise
# Create Dataset
# Register dataset in SageMaker AI Registry. This creates a versioned dataset that can be referenced by ARN
dataset = DataSet.create(
name=MODEL_PACKAGE_GROUP_NAME,
source=TRAINING_DATA_S3,
wait=True
)
TRAINING_DATASET_ARN = dataset.arn
print(f"Here is your model package group ARN: {model_package_group.model_package_group_arn}\n")
print(f"Here is your training dataset ARN: {dataset.arn}")
# Cell 4: Configure Trainer
from sagemaker.train.dpo_trainer import DPOTrainer
from sagemaker.train.common import TrainingType
trainer = DPOTrainer(
model=BASE_MODEL,
training_type=TrainingType.LORA,
model_package_group=model_package_group,
training_dataset=TRAINING_DATASET_ARN,
s3_output_path=S3_OUTPUT_PATH,
sagemaker_session=sagemaker_session,
#accept_eula=ACCEPT_EULA, # Uncomment for Meta models
role=ROLE_ARN
)
print("Here are the recommended hyperparameters for the current training job:")
print(f"Batch size: {trainer.hyperparameters.global_batch_size}")
print(f"Learning rate: {trainer.hyperparameters.learning_rate}")
print(f"Adam Beta: {trainer.hyperparameters.adam_beta}")
# Remove the following two print statements for Nova models (Nova models don't use max_epochs or lr_warmup_steps_ratio)
print(f"Number of epochs: {trainer.hyperparameters.max_epochs}")
print(f"Learning rate warmup steps ratio: {trainer.hyperparameters.lr_warmup_steps_ratio}")
# Cell 5: Hyperparameter Overrides
# To change a hyperparameter, uncomment its corresponding line, and set the value you want.
# Note: If the value you choose is not supported for your model, you will get an error indicating the allowed range.
# Uncomment the following line to change the learning rate
# trainer.hyperparameters.learning_rate = 0.0002
# Uncomment the following line to change the batch size
# trainer.hyperparameters.global_batch_size = 16
# Uncomment the following line to change the number of epochs (unavailable for Nova models)
# trainer.hyperparameters.max_epochs = 5
# Uncomment the following line to change the learning rate warmup steps ratio (unavailable for Nova models)
# trainer.hyperparameters.lr_warmup_steps_ratio = 0.05
# Uncomment the following line to change Adam Beta
# trainer.hyperparameters.adam_beta = 0.01
# Cell 6: Start Training
# Start training
training_job = trainer.train(wait=True)
print(f"Training Job Name: {training_job.training_job_name}")
print(f"Training Status: {training_job.training_job_status}")
# Save manifest
manifest_dir = Path("[PROJECT_DIR]") / "manifests"
manifest_dir.mkdir(parents=True, exist_ok=True)
manifest_path = manifest_dir / f"training-{training_job.training_job_name}.json"
manifest_path.write_text(json.dumps({
"training_job_name": training_job.training_job_name,
"model_package_group_name": MODEL_PACKAGE_GROUP_NAME,
}, indent=2))
print(f"Manifest saved: {manifest_path}")
# Cell 7: Plot and Display Metrics # NOTEBOOK_ONLY_SECTION
import matplotlib.pyplot as plt
import mlflow
from mlflow.tracking import MlflowClient
run_id = training_job.mlflow_details.mlflow_run_id
mlflow.set_tracking_uri(training_job.mlflow_config.mlflow_resource_arn)
client = MlflowClient()
metrics = ["loss_per_batch", "rewards/chosen", "rewards/rejected", "rewards/margins", "acc_per_batch"]
fig, axes = plt.subplots(1, len(metrics), figsize=(4 * len(metrics), 3))
for idx, metric in enumerate(metrics):
history = client.get_metric_history(run_id, metric)
axes[idx].plot([h.step for h in history], [h.value for h in history], linewidth=2, marker='o', markersize=4)
axes[idx].set_xlabel('Step')
axes[idx].set_ylabel(metric.split('/')[-1])
axes[idx].set_title(metric, fontweight='bold')
axes[idx].grid(True, alpha=0.3)
plt.suptitle(f'Training Metrics: {training_job.training_job_name}', fontweight='bold')
plt.tight_layout()
plt.show()
# RLAIF (Reinforcement Learning from AI Feedback) Template — Builtin Reward Prompt
# Cell 0 [markdown]: Fine-Tuning
# Cell 1: Install Dependencies
%pip install --upgrade 'sagemaker>=3.7.1,<4.0' boto3 -q # NOTEBOOK_ONLY
# Cell 2: Setup & Credentials
import boto3
import json
from pathlib import Path
from botocore.exceptions import ClientError
from sagemaker.ai_registry.dataset import DataSet
from sagemaker.core.resources import ModelPackageGroup
from sagemaker.core.helper.session_helper import Session, get_execution_role
from sagemaker.core import Attribution, set_attribution
from sagemaker.train.rlaif_trainer import RLAIFTrainer
from sagemaker.train.common import TrainingType
set_attribution(Attribution.SAGEMAKER_AGENT_PLUGIN)
# Setup
sm_client = boto3.Session().client("sagemaker")
sagemaker_session = Session(sagemaker_client=sm_client)
bucket = sagemaker_session.default_bucket()
# Configuration - USER please fill in these fields with your information:
BASE_MODEL = "" # Sagemaker Hub model id
TRAINING_DATA_S3 = "" # S3 path
S3_OUTPUT_PATH = f"s3://{bucket}/finetuning-output/"
ROLE_ARN = get_execution_role() # You can change this to a specific role
ACCEPT_EULA = False # Set to True to accept the base model's End-User License Agreement (OSS models only)
MODEL_PACKAGE_GROUP_NAME = "" # Auto-generated based on use case
# Reward model — the Bedrock LLM used as judge
# Available models and regions: see references/rlaif_guide.md
REWARD_MODEL_ID = ""
# Builtin reward prompt value — choose one that matches your use case:
# "Builtin.Summarize", "Builtin.Faithfulness", "Builtin.ChainOfThought", "Builtin.Evaluation"
REWARD_PROMPT_VALUE = ""
# Cell 3: Create Dataset and Model Package Group
# Create Model Package Group
try:
model_package_group = ModelPackageGroup.create(
model_package_group_name=MODEL_PACKAGE_GROUP_NAME,
model_package_group_description="",
)
print(f"Created new model package group named {MODEL_PACKAGE_GROUP_NAME}")
except ClientError as e:
if e.response['Error']['Code'] in ('ResourceInUse', 'ValidationException'):
model_package_group = ModelPackageGroup.get(model_package_group_name=MODEL_PACKAGE_GROUP_NAME)
print(f"There is already a model package group with the name {MODEL_PACKAGE_GROUP_NAME}.\nIf you want to save your finetuned model under a different name, change the value of MODEL_PACKAGE_GROUP_NAME in the previous cell.")
else:
raise
# Create Dataset
dataset = DataSet.create(
name=MODEL_PACKAGE_GROUP_NAME,
source=TRAINING_DATA_S3,
wait=True
)
TRAINING_DATASET_ARN = dataset.arn
print(f"Here is your model package group ARN: {model_package_group.model_package_group_arn}\n")
print(f"Here is your training dataset ARN: {dataset.arn}")
# Cell 4: Configure Trainer
trainer = RLAIFTrainer(
model=BASE_MODEL,
model_package_group=model_package_group,
reward_model_id=REWARD_MODEL_ID,
reward_prompt=REWARD_PROMPT_VALUE,
training_dataset=TRAINING_DATASET_ARN,
s3_output_path=S3_OUTPUT_PATH,
sagemaker_session=sagemaker_session,
#accept_eula=ACCEPT_EULA, # Uncomment for Meta models
role=ROLE_ARN,
)
print("Here are the recommended hyperparameters for the current training job:")
print(f"Batch size: {trainer.hyperparameters.global_batch_size}")
print(f"Learning rate: {trainer.hyperparameters.learning_rate}")
print(f"Epochs: {trainer.hyperparameters.max_epochs}")
# Cell 5: Hyperparameter Overrides
# To change a hyperparameter, uncomment its corresponding line, and set the value you want.
# Note: If the value you choose is not supported for your model, you will get an error indicating the allowed range.
# Uncomment the following line to change the learning rate
# trainer.hyperparameters.learning_rate = 0.0002
# Uncomment the following line to change the batch size
# trainer.hyperparameters.global_batch_size = 16
# Uncomment the following line to change the number of epochs
# trainer.hyperparameters.max_epochs = 5
# Cell 6: Start Training
training_job = trainer.train(wait=True)
print(f"Training Job Name: {training_job.training_job_name}")
print(f"Training Status: {training_job.training_job_status}")
# Save manifest
manifest_dir = Path("[PROJECT_DIR]") / "manifests"
manifest_dir.mkdir(parents=True, exist_ok=True)
manifest_path = manifest_dir / f"training-{training_job.training_job_name}.json"
manifest_path.write_text(json.dumps({
"training_job_name": training_job.training_job_name,
"model_package_group_name": MODEL_PACKAGE_GROUP_NAME,
}, indent=2))
print(f"Manifest saved: {manifest_path}")
# Cell 7: Plot and Display Metrics # NOTEBOOK_ONLY_SECTION
import matplotlib.pyplot as plt
import mlflow
from mlflow.tracking import MlflowClient
run_id = training_job.mlflow_details.mlflow_run_id
mlflow.set_tracking_uri(training_job.mlflow_config.mlflow_resource_arn)
client = MlflowClient()
metrics = [
"critic/rewards/mean",
"response_length/mean",
"actor/entropy_loss",
"actor/grad_norm",
"critic/advantages/mean",
]
fig, axes = plt.subplots(1, len(metrics), figsize=(4 * len(metrics), 3))
for idx, metric in enumerate(metrics):
history = client.get_metric_history(run_id, metric)
if history:
axes[idx].plot([h.step for h in history], [h.value for h in history], linewidth=2, marker='o', markersize=4)
axes[idx].set_xlabel('Step')
axes[idx].set_ylabel(metric.split('/')[-1])
axes[idx].set_title(metric, fontweight='bold')
axes[idx].grid(True, alpha=0.3)
plt.suptitle(f'Training Metrics: {training_job.training_job_name}', fontweight='bold')
plt.tight_layout()
plt.show()
# RLAIF (Reinforcement Learning from AI Feedback) Template — Custom Reward Prompt
# Cell 0 [markdown]: Fine-Tuning
# Cell 1: Install Dependencies
%pip install --upgrade 'sagemaker>=3.7.1,<4.0' boto3 -q # NOTEBOOK_ONLY
# Cell 2: Setup & Credentials
import boto3
import json
from pathlib import Path
from botocore.exceptions import ClientError
from sagemaker.ai_registry.dataset import DataSet
from sagemaker.ai_registry.evaluator import Evaluator
from sagemaker.ai_registry.air_constants import REWARD_PROMPT
from sagemaker.core.resources import ModelPackageGroup
from sagemaker.core.helper.session_helper import Session, get_execution_role
from sagemaker.core import Attribution, set_attribution
from sagemaker.train.rlaif_trainer import RLAIFTrainer
from sagemaker.train.common import TrainingType
set_attribution(Attribution.SAGEMAKER_AGENT_PLUGIN)
# Setup
sm_client = boto3.Session().client("sagemaker")
sagemaker_session = Session(sagemaker_client=sm_client)
bucket = sagemaker_session.default_bucket()
# Configuration - USER please fill in these fields with your information:
BASE_MODEL = "" # Sagemaker Hub model id
TRAINING_DATA_S3 = "" # S3 path
S3_OUTPUT_PATH = f"s3://{bucket}/finetuning-output/"
ROLE_ARN = get_execution_role() # You can change this to a specific role
ACCEPT_EULA = False # Set to True to accept the base model's End-User License Agreement (OSS models only)
MODEL_PACKAGE_GROUP_NAME = "" # Auto-generated based on use case
# Reward model — the Bedrock LLM used as judge
# Available models and regions: see references/rlaif_guide.md
REWARD_MODEL_ID = ""
# Cell 3: Register Custom Reward Prompt
# Insert path to the custom Jinja prompt file (usually ../scripts/custom_reward_prompt.jinja)
CUSTOM_PROMPT_PATH = ""
reward_prompt_evaluator = Evaluator.create(
name="[GENERATE A NAME FOR THE EVALUATOR HERE]", # lowercase alphanumeric + hyphens, max 20 chars
type=REWARD_PROMPT,
source=CUSTOM_PROMPT_PATH,
sagemaker_session=sagemaker_session,
wait=True
)
REWARD_PROMPT_ARN = reward_prompt_evaluator.arn
print(f"Reward Prompt Evaluator ARN: {REWARD_PROMPT_ARN}")
# Cell 4: Create Dataset and Model Package Group
# Create Model Package Group
try:
model_package_group = ModelPackageGroup.create(
model_package_group_name=MODEL_PACKAGE_GROUP_NAME,
model_package_group_description="",
)
print(f"Created new model package group named {MODEL_PACKAGE_GROUP_NAME}")
except ClientError as e:
if e.response['Error']['Code'] in ('ResourceInUse', 'ValidationException'):
model_package_group = ModelPackageGroup.get(model_package_group_name=MODEL_PACKAGE_GROUP_NAME)
print(f"There is already a model package group with the name {MODEL_PACKAGE_GROUP_NAME}.\nIf you want to save your finetuned model under a different name, change the value of MODEL_PACKAGE_GROUP_NAME in the previous cell.")
else:
raise
# Create Dataset
dataset = DataSet.create(
name=MODEL_PACKAGE_GROUP_NAME,
source=TRAINING_DATA_S3,
wait=True
)
TRAINING_DATASET_ARN = dataset.arn
print(f"Here is your model package group ARN: {model_package_group.model_package_group_arn}\n")
print(f"Here is your training dataset ARN: {dataset.arn}")
# Cell 5: Configure Trainer
trainer = RLAIFTrainer(
model=BASE_MODEL,
model_package_group=model_package_group,
reward_model_id=REWARD_MODEL_ID,
reward_prompt=REWARD_PROMPT_ARN, # ARN of the registered custom prompt evaluator
training_dataset=TRAINING_DATASET_ARN,
s3_output_path=S3_OUTPUT_PATH,
sagemaker_session=sagemaker_session,
#accept_eula=ACCEPT_EULA, # Uncomment for Meta models
role=ROLE_ARN,
)
print("Here are the recommended hyperparameters for the current training job:")
print(f"Batch size: {trainer.hyperparameters.global_batch_size}")
print(f"Learning rate: {trainer.hyperparameters.learning_rate}")
print(f"Epochs: {trainer.hyperparameters.max_epochs}")
# Cell 6: Hyperparameter Overrides
# To change a hyperparameter, uncomment its corresponding line, and set the value you want.
# Note: If the value you choose is not supported for your model, you will get an error indicating the allowed range.
# Uncomment the following line to change the learning rate
# trainer.hyperparameters.learning_rate = 0.0002
# Uncomment the following line to change the batch size
# trainer.hyperparameters.global_batch_size = 16
# Uncomment the following line to change the number of epochs
# trainer.hyperparameters.max_epochs = 5
# Cell 7: Start Training
training_job = trainer.train(wait=True)
print(f"Training Job Name: {training_job.training_job_name}")
print(f"Training Status: {training_job.training_job_status}")
# Save manifest
manifest_dir = Path("[PROJECT_DIR]") / "manifests"
manifest_dir.mkdir(parents=True, exist_ok=True)
manifest_path = manifest_dir / f"training-{training_job.training_job_name}.json"
manifest_path.write_text(json.dumps({
"training_job_name": training_job.training_job_name,
"model_package_group_name": MODEL_PACKAGE_GROUP_NAME,
}, indent=2))
print(f"Manifest saved: {manifest_path}")
# Cell 8: Plot and Display Metrics # NOTEBOOK_ONLY_SECTION
import matplotlib.pyplot as plt
import mlflow
from mlflow.tracking import MlflowClient
run_id = training_job.mlflow_details.mlflow_run_id
mlflow.set_tracking_uri(training_job.mlflow_config.mlflow_resource_arn)
client = MlflowClient()
metrics = [
"critic/rewards/mean",
"response_length/mean",
"actor/entropy_loss",
"actor/grad_norm",
"critic/advantages/mean",
]
fig, axes = plt.subplots(1, len(metrics), figsize=(4 * len(metrics), 3))
for idx, metric in enumerate(metrics):
history = client.get_metric_history(run_id, metric)
if history:
axes[idx].plot([h.step for h in history], [h.value for h in history], linewidth=2, marker='o', markersize=4)
axes[idx].set_xlabel('Step')
axes[idx].set_ylabel(metric.split('/')[-1])
axes[idx].set_title(metric, fontweight='bold')
axes[idx].grid(True, alpha=0.3)
plt.suptitle(f'Training Metrics: {training_job.training_job_name}', fontweight='bold')
plt.tight_layout()
plt.show()
# RLVR (Reinforcement Learning from Verifiable Rewards) Template
# Cell 0 [markdown]: Fine-Tuning
# Cell 1: Install Dependencies
%pip install --upgrade 'sagemaker>=3.7.1,<4.0' boto3 -q # NOTEBOOK_ONLY
# Cell 2: Setup & Credentials
import boto3
import json
from pathlib import Path
from botocore.exceptions import ClientError
from sagemaker.ai_registry.dataset import DataSet
from sagemaker.core.resources import ModelPackageGroup
from sagemaker.core.helper.session_helper import Session, get_execution_role
from sagemaker.core import Attribution, set_attribution
set_attribution(Attribution.SAGEMAKER_AGENT_PLUGIN)
# Setup
sm_client = boto3.Session().client("sagemaker")
sagemaker_session = Session(sagemaker_client=sm_client)
bucket = sagemaker_session.default_bucket()
# Configuration - USER please fill in these fields with your information:
BASE_MODEL = "" # e.g., "meta-textgeneration-llama-3-8b"
TRAINING_DATA_S3 = "" # S3 path
S3_OUTPUT_PATH = f"s3://{bucket}/finetuning-output/"
ROLE_ARN = get_execution_role() # You can change this to a specific role.
ACCEPT_EULA = False # Set to True to accept the base model's End-User License Agreement
MODEL_PACKAGE_GROUP_NAME = "" # Auto-generated based on use case
# Cell 3: Register Reward Function
from sagemaker.ai_registry.evaluator import Evaluator
reward_function_path = "" # Insert path to the local reward function (usually ../scripts/lambda_function.py)
evaluator = Evaluator.create(
name="[GENERATE A NAME FOR THE EVALUATOR HERE]",
type="RewardFunction",
source=reward_function_path,
)
CUSTOM_REWARD_FUNCTION = evaluator.arn
print(f"Reward Function ARN: {CUSTOM_REWARD_FUNCTION}")
# Cell 4: Create Dataset and Model Package Group
# Create Model Package Group
try:
model_package_group = ModelPackageGroup.create(
model_package_group_name=MODEL_PACKAGE_GROUP_NAME,
model_package_group_description="",
)
print(f"Created new model package group named {MODEL_PACKAGE_GROUP_NAME}")
except ClientError as e:
if e.response['Error']['Code'] in ('ResourceInUse', 'ValidationException'):
model_package_group = ModelPackageGroup.get(model_package_group_name=MODEL_PACKAGE_GROUP_NAME)
print(f"There is already a model package group with the name {MODEL_PACKAGE_GROUP_NAME}.\nIf you want to save your finetuned model under a different name, change the value of MODEL_PACKAGE_GROUP_NAME in the previous cell.")
else:
raise
# Create Dataset
# Register dataset in SageMaker AI Registry. This creates a versioned dataset that can be referenced by ARN
dataset = DataSet.create(
name=MODEL_PACKAGE_GROUP_NAME,
source=TRAINING_DATA_S3,
wait=True
)
TRAINING_DATASET_ARN = dataset.arn
print(f"Here is your model package group ARN: {model_package_group.model_package_group_arn}\n")
print(f"Here is your training dataset ARN: {dataset.arn}")
# Cell 5: Configure Trainer
from sagemaker.train.rlvr_trainer import RLVRTrainer
from sagemaker.train.common import TrainingType
trainer = RLVRTrainer(
model=BASE_MODEL,
model_package_group=model_package_group,
training_dataset=TRAINING_DATASET_ARN,
s3_output_path=S3_OUTPUT_PATH,
sagemaker_session=sagemaker_session,
#accept_eula=ACCEPT_EULA, # Uncomment for Meta models
role=ROLE_ARN,
custom_reward_function=CUSTOM_REWARD_FUNCTION
)
print("Here are the recommended hyperparameters for the current training job:")
print(f"Batch size: {trainer.hyperparameters.global_batch_size}")
print(f"Learning rate: {trainer.hyperparameters.learning_rate}")
# Delete the following print statement for Nova models (Nova models don't use max_epochs)
print(f"Number of epochs: {trainer.hyperparameters.max_epochs}")
# Cell 6: Hyperparameter Overrides
# To change a hyperparameter, uncomment its corresponding line, and set the value you want.
# Note: If the value you choose is not supported for your model, you will get an error indicating the allowed range.
# Uncomment the following line to change the learning rate
# trainer.hyperparameters.learning_rate = 0.0002
# Uncomment the following line to change the batch size
# trainer.hyperparameters.global_batch_size = 16
# Uncomment the following line to change the number of epochs (unavailable for Nova models)
# trainer.hyperparameters.max_epochs = 5
# Cell 7: Start Training
# Start training
training_job = trainer.train(wait=True)
print(f"Training Job Name: {training_job.training_job_name}")
print(f"Training Status: {training_job.training_job_status}")
# Save manifest
manifest_dir = Path("[PROJECT_DIR]") / "manifests"
manifest_dir.mkdir(parents=True, exist_ok=True)
manifest_path = manifest_dir / f"training-{training_job.training_job_name}.json"
manifest_path.write_text(json.dumps({
"training_job_name": training_job.training_job_name,
"model_package_group_name": MODEL_PACKAGE_GROUP_NAME,
}, indent=2))
print(f"Manifest saved: {manifest_path}")
# Cell 8: Plot and Display Metrics # NOTEBOOK_ONLY_SECTION
import matplotlib.pyplot as plt
import mlflow
from mlflow.tracking import MlflowClient
run_id = training_job.mlflow_details.mlflow_run_id
mlflow.set_tracking_uri(training_job.mlflow_config.mlflow_resource_arn)
client = MlflowClient()
# Core RL metrics - adjust val-core metric names based on your data source and reward function
metrics = [
"critic/rewards/mean",
"response_length/mean",
"actor/entropy_loss",
"actor/grad_norm",
"critic/advantages/mean",
]
# Note: Validation reward metrics follow the pattern: val-core/{data_source}/reward(/acc)/mean@{k}
# Add your specific val-core metrics to the list above, e.g.:
# "val-core/my_dataset/reward/mean@1"
# ResponseQuality: Verl allows printing to a file. Check training job output for details.
fig, axes = plt.subplots(1, len(metrics), figsize=(4 * len(metrics), 3))
for idx, metric in enumerate(metrics):
history = client.get_metric_history(run_id, metric)
if history:
axes[idx].plot([h.step for h in history], [h.value for h in history], linewidth=2, marker='o', markersize=4)
axes[idx].set_xlabel('Step')
axes[idx].set_ylabel(metric.split('/')[-1])
axes[idx].set_title(metric, fontweight='bold')
axes[idx].grid(True, alpha=0.3)
plt.suptitle(f'Training Metrics: {training_job.training_job_name}', fontweight='bold')
plt.tight_layout()
plt.show()
# SFT (Supervised Fine-Tuning) Template
# Cell 0 [markdown]: Fine-Tuning
# Cell 1: Install Dependencies
%pip install --upgrade 'sagemaker>=3.7.1,<4.0' boto3 -q # NOTEBOOK_ONLY
# Cell 2: Setup & Credentials
import boto3
import json
from pathlib import Path
from botocore.exceptions import ClientError
from sagemaker.ai_registry.dataset import DataSet
from sagemaker.core.resources import ModelPackageGroup
from sagemaker.core.helper.session_helper import Session, get_execution_role
from sagemaker.core import Attribution, set_attribution
set_attribution(Attribution.SAGEMAKER_AGENT_PLUGIN)
# Setup
sm_client = boto3.Session().client("sagemaker")
sagemaker_session = Session(sagemaker_client=sm_client)
bucket = sagemaker_session.default_bucket()
# Configuration - USER please fill in these fields with your information:
BASE_MODEL = "" # e.g., "meta-textgeneration-llama-3-8b"
TRAINING_DATA_S3 = "" # S3 path
S3_OUTPUT_PATH = f"s3://{bucket}/finetuning-output/"
ROLE_ARN = get_execution_role() # You can change this to a specific role.
ACCEPT_EULA = False # Set to True to accept the base model's End-User License Agreement
MODEL_PACKAGE_GROUP_NAME = "" # Auto-generated based on use case
# Cell 3: Create Dataset and Model Package Group
# Create Model Package Group
try:
model_package_group = ModelPackageGroup.create(
model_package_group_name=MODEL_PACKAGE_GROUP_NAME,
model_package_group_description="",
)
print(f"Created new model package group named {MODEL_PACKAGE_GROUP_NAME}")
except ClientError as e:
if e.response['Error']['Code'] in ('ResourceInUse', 'ValidationException'):
model_package_group = ModelPackageGroup.get(model_package_group_name=MODEL_PACKAGE_GROUP_NAME)
print(f"There is already a model package group with the name {MODEL_PACKAGE_GROUP_NAME}. If you want to save your finetuned model under a different name, change the value of MODEL_PACKAGE_GROUP_NAME in the previous cell.")
else:
raise
# Create Dataset
# Register dataset in SageMaker AI Registry. This creates a versioned dataset that can be referenced by ARN
dataset = DataSet.create(
name=MODEL_PACKAGE_GROUP_NAME,
source=TRAINING_DATA_S3,
wait=True
)
TRAINING_DATASET_ARN = dataset.arn
print(f"Here is your model package group ARN: {model_package_group.model_package_group_arn}\n")
print(f"Here is your training dataset ARN: {dataset.arn}")
# Cell 4: Configure Trainer
from sagemaker.train.sft_trainer import SFTTrainer
from sagemaker.train.common import TrainingType
trainer = SFTTrainer(
model=BASE_MODEL,
training_type=TrainingType.LORA,
model_package_group=model_package_group,
training_dataset=TRAINING_DATASET_ARN,
s3_output_path=S3_OUTPUT_PATH,
sagemaker_session=sagemaker_session,
#accept_eula=ACCEPT_EULA, # Uncomment for Meta models
role=ROLE_ARN
)
print("Here are the recommended hyperparameters for the current training job:")
print(f"Batch size: {trainer.hyperparameters.global_batch_size}")
print(f"Learning rate: {trainer.hyperparameters.learning_rate}")
# Remove the following two print statements for Nova models (Nova models don't use max_epochs or lr_warmup_steps_ratio)
print(f"Number of epochs: {trainer.hyperparameters.max_epochs}")
print(f"Learning rate warmup steps ratio: {trainer.hyperparameters.lr_warmup_steps_ratio}")
# Cell 5: Hyperparameter Overrides
# To change a hyperparameter, uncomment its corresponding line, and set the value you want.
# Note: If the value you choose is not supported for your model, you will get an error indicating the allowed range.
# Uncomment the following line to change the learning rate
# trainer.hyperparameters.learning_rate = 0.0002
# Uncomment the following line to change the batch size
# trainer.hyperparameters.global_batch_size = 16
# Uncomment the following line to change the number of epochs (unavailable for Nova models)
# trainer.hyperparameters.max_epochs = 5
# Uncomment the following line to change the learning rate warmup steps ratio (unavailable for Nova models)
# trainer.hyperparameters.lr_warmup_steps_ratio = 0.05
# Cell 6: Start Training
# Start training
training_job = trainer.train(wait=True)
print(f"Training Job Name: {training_job.training_job_name}")
print(f"Training Status: {training_job.training_job_status}")
# Save manifest
manifest_dir = Path("[PROJECT_DIR]") / "manifests"
manifest_dir.mkdir(parents=True, exist_ok=True)
manifest_path = manifest_dir / f"training-{training_job.training_job_name}.json"
manifest_path.write_text(json.dumps({
"training_job_name": training_job.training_job_name,
"model_package_group_name": MODEL_PACKAGE_GROUP_NAME,
}, indent=2))
print(f"Manifest saved: {manifest_path}")
# Cell 7: Plot and Display Metrics # NOTEBOOK_ONLY_SECTION
import matplotlib.pyplot as plt
import mlflow
from mlflow.tracking import MlflowClient
run_id = training_job.mlflow_details.mlflow_run_id
mlflow.set_tracking_uri(training_job.mlflow_config.mlflow_resource_arn)
client = MlflowClient()
fig, axes = plt.subplots(1, 2, figsize=(12, 3))
for idx, metric in enumerate(["total_loss", "val_eval_total_loss"]):
history = client.get_metric_history(run_id, metric)
axes[idx].plot([h.step for h in history], [h.value for h in history], linewidth=2, marker='o', markersize=4)
axes[idx].set_xlabel('Step')
axes[idx].set_ylabel('Loss')
axes[idx].set_title(metric, fontweight='bold')
axes[idx].grid(True, alpha=0.3)
plt.suptitle(f'Training Metrics: {training_job.training_job_name}', fontweight='bold')
plt.tight_layout()
plt.show()
Code Output Guide
Mode Selection
Ask the user once before generating code: "Would you like me to generate a Jupyter notebook or a Python script?"
If the output format has already been decided in the conversation context, keep consistent — do not re-ask.
Shared Rules (Both Modes)
- Use EXACTLY the imports shown in each code template — do not add extras
- Replace
[PLACEHOLDER]values with user-specific configuration - Include
set_attribution(Attribution.SAGEMAKER_AGENT_PLUGIN)in the setup cell/section
Reading Code Templates
Templates use # Cell N: Label markers to delimit sections. # NOTEBOOK_ONLY skips a line in script mode; # NOTEBOOK_ONLY_SECTION on a # Cell N: line skips the entire section.
Notebook Mode
Write a .ipynb file in <project-dir>/notebooks/.
Naming and appending:
- Notebook path:
<project-dir>/notebooks/<project-name>.ipynb - If the notebook already exists → ask: _"Would you like me to append cells to the existing notebook, or create a new one?"_
- If it doesn't exist → create it
- When appending, use the template's
# Cell 0 [markdown]:cell as the section divider before the new cells
Formatting:
- Use your file write tool to create the complete notebook JSON, OR use notebook MCP tools (
create_notebook,add_cell) if available - Do NOT use bash commands, shell scripts, or
echo/catpiping - 2-space JSON indentation
- Each source line is a separate string ending with
\n(except the last) - Escape quotes:
\" - No trailing commas
Structure:
- Wrap cells in
{"cells": [...], "metadata": {...}, "nbformat": 4, "nbformat_minor": 4} - Code cells:
cell_type,execution_count: null,metadata: {},outputs: [],source: [...] - Markdown cells:
cell_type: "markdown", noexecution_countoroutputs # Cell 0 [markdown]:becomes a markdown cell; all others become code cells
Execution:
- If notebook execution tools are available (e.g.,
run_cellMCP), offer to run cells for the user. If not available, tell the user to run cells themselves. - Do NOT use bash commands or inline scripts to execute notebook cells.
Script Mode
Write a numbered .py file in <project-dir>/scripts/.
Naming:
- Format:
NN_<descriptive_name>.py(e.g.,01_sft_finetuning.py) — use the next available number in<project-dir>/scripts/
Formatting:
- Plain Python file, standard text
- Use
# %%cell markers to preserve logical sections (IDE-compatible) - Include a docstring at the top describing what the script does
# Cell 0 [markdown]:→ a comment block or docstring
Dependencies:
- Install any required pip packages directly (e.g.,
pip install sagemaker>=3.7.1) before writing or running the script. Do not embed install commands in the script itself.
Execution:
- Run the script using standard Python execution (
python3 <script>.py).
Resumption After Interruption
If the conversation was interrupted while a job was running (e.g., context compaction, user stopped and restarted, connection drop), do NOT re-run the script. Instead, check for an existing job by name or ARN from the conversation context or PLAN.md, and monitor its status rather than launching a duplicate.
Continuous Customization (Multi-Round Fine-Tuning)
Adds a subsequent fine-tuning round on top of an already-customized model. Uses the Model Package ARN from a previous training job as the base model instead of a SageMakerHub model name.
---
Prerequisites
| Requirement | How to obtain |
|---|---|
| Training data S3 path for current round | Collect from the conversation context, ask user if not available |
| Confirmation that data is in correct format for current finetuning strategy | From dataset-evaluation skill or user is sure it's correct |
| Fine-tuning technique | If not available from context, ask which technique for this round: SFT, DPO, RLVR, or RLAIF |
| Previous model package group name | From the prior output, or help the user find it (Section A instructions) |
| Previous training job name | Ask user or get from user's account (Section B instructions) |
| Reward function for RLVR as a Lambda Evaluator | Ask user if they have one, otherwise follow rlvr_reward_function.md to create one |
| Reward prompt as a RewardPrompt Evaluator and reward model id for RLAIF | Ask user if they have one, otherwise follow rlaif_guide.md |
---
Output Placement
The output format (notebook or script) should already be established from the conversation context — do not re-ask if it has already been decided. If necessary, the output format guide is in references/code_output_guide.md.
- Notebook mode: If the user has an existing notebook from the previous round, append these cells under a new markdown header describing the round, e.g.,
## DPO Fine-Tuning (Round 2). If no prior notebook exists, create a new one with a name reflecting the use case and techniques, e.g.,news-app-sft-to-dpo.ipynb. - Script mode: Write a new numbered
.pyfile in<project-dir>/scripts/, e.g.,02_dpo_finetuning_round2.py. Use# %%cell markers to separate logical sections.
---
Section A: Setup & Credentials
<!-- markdownlint-disable MD001 -->
Re-establishes session variables. Required to ensure all variables are defined.
Agent Instructions
1. Set NEW_TRAINING_DATA_S3 to the user's dataset S3 path for the current round. 2. Set PREVIOUS_MODEL_PACKAGE_GROUP_NAME:
- If the prior output is available: Copy the model package group name from it.
- If not: Use the AWS CLI MCP tool
list-model-package-groupswith these flags:
--query 'ModelPackageGroupSummaryList[].{Name:ModelPackageGroupName,Status:ModelPackageGroupStatus,Created:CreationTime}'
--output tableOptionally add --name-contains <keyword> to filter by name.
- If you can't identify the name from the list: Ask the user.
Code
import boto3
from sagemaker.ai_registry.dataset import DataSet
from sagemaker.core.resources import ModelPackageGroup
from sagemaker.core.helper.session_helper import Session, get_execution_role
# Setup
sm_client = boto3.Session().client("sagemaker")
sagemaker_session = Session(sagemaker_client=sm_client)
bucket = sagemaker_session.default_bucket()
S3_OUTPUT_PATH = f"s3://{bucket}/finetuning-output/"
ROLE_ARN = get_execution_role()
# Configuration
NEW_TRAINING_DATA_S3 = "" # S3 path to the dataset for this round
PREVIOUS_MODEL_PACKAGE_GROUP_NAME = "" # Model package group name from the previous round---
Section B: Retrieve Previous Model Package ARN
Looks up the model package ARN from the previous training job.
Agent Instructions
1. Ask the user if they have the training job name from the previous fine-tuning round or need help finding it. 2. If the user provides the name → Set it as previous_training_job_name in the code. 3. If the user needs help → Use the AWS CLI MCP tool list-training-jobs with these flags:
--status-equals Completed
--query 'TrainingJobSummaries[].{Name:TrainingJobName,Status:TrainingJobStatus,Created:CreationTime}'
--output tablePresent the results and let the user pick the correct job. 4. If the user is unsure and wants to fill it later → Leave the placeholder <previous_training_job_name> and tell them to replace it before running.
Code
from sagemaker.core.resources import TrainingJob
previous_training_job_name = "<previous_training_job_name>" # USER: paste your previous training job name here
job = TrainingJob.get(training_job_name=previous_training_job_name)
previous_model_package_arn = job.output_model_package_arn
print(f"Previous Model Package ARN: {previous_model_package_arn}")Troubleshooting: If this cell throwsValidationException: Requested resource not found, the job name is wrong or the output is connected to a different AWS region than where the job ran. Verify the region withboto3.Session().region_name.
---
Section C: Register New Dataset
Registers the current round's training data as a versioned DataSet.
Agent Instructions
- Set
nameto something descriptive of the use case and round, e.g.,"customer-support-chatbot-dpo-round2".
Code
from sagemaker.ai_registry.dataset import DataSet
dataset = DataSet.create(
name="<dataset_name>",
source=NEW_TRAINING_DATA_S3,
wait=True
)
new_dataset_arn = dataset.arn
print(f"New Training Dataset ARN: {new_dataset_arn}")---
Section D: Add Evaluators
If necessary, use this section to register the custom RLVR reward function or custom RLAIF reward prompt as an evaluator on SageMaker AI Hub Registry.
from sagemaker.ai_registry.evaluator import Evaluator
from sagemaker.ai_registry.air_constants import REWARD_FUNCTION, REWARD_PROMPT
evaluator = Evaluator.create(
name="",
type=REWARD_FUNCTION, # Use REWARD_FUNCTION for RLVR, REWARD_PROMPT for RLAIF
source="", # Path to reward function or prompt file
sagemaker_session=sagemaker_session,
wait=True
)
print(f"Evaluator ARN: {evaluator.arn}")---
Section E: Configure and Start Training
Runs the next fine-tuning round. The key difference from the first round: model receives previous_model_package_arn instead of a base model name.
Agent Instructions
Choose the trainer class matching the user's technique for this round and pass additional inputs if needed:
| Technique | Import | Additional Trainer inputs |
|---|---|---|
| SFT | from sagemaker.train.sft_trainer import SFTTrainer | |
| DPO | from sagemaker.train.dpo_trainer import DPOTrainer | |
| RLVR | from sagemaker.train.rlvr_trainer import RLVRTrainer | custom_reward_function |
| RLAIF | from sagemaker.train.rlaif_trainer import RLAIFTrainer | reward_prompt, reward_model_id, built_in_metrics |
Code (SFT example — swap trainer class for DPO/RLVR/RLAIF)
from sagemaker.train.sft_trainer import SFTTrainer
from sagemaker.train.common import TrainingType
step2_trainer = SFTTrainer(
model=previous_model_package_arn,
training_type=TrainingType.LORA,
model_package_group=PREVIOUS_MODEL_PACKAGE_GROUP_NAME,
training_dataset=new_dataset_arn,
s3_output_path=S3_OUTPUT_PATH,
sagemaker_session=sagemaker_session,
role=ROLE_ARN,
)
step2_job = step2_trainer.train(wait=True)
print(f"Training Job Name for current round: {step2_job.training_job_name}")
print(f"Training Status: {step2_job.training_job_status}")---
Rules
- ✅ Reuse the same
PREVIOUS_MODEL_PACKAGE_GROUP_NAMEfrom the first round so all model versions stay grouped together - ❌ Do NOT pass
accept_eula— it only applies to the initial base model download - ❌ Do NOT re-create the
ModelPackageGroup— it already exists from the first round
Model License Information
| SageMaker Hub Model ID | Model Name | License URL(s) |
|---|---|---|
huggingface-reasoning-qwen3-32b | Qwen3-32B | https://huggingface.co/Qwen/Qwen3-32B/blob/main/LICENSE |
huggingface-reasoning-qwen3-14b | Qwen3-14B | https://huggingface.co/Qwen/Qwen3-14B/blob/main/LICENSE |
huggingface-reasoning-qwen3-8b | Qwen3-8B | https://huggingface.co/Qwen/Qwen3-8B/blob/main/LICENSE |
huggingface-reasoning-qwen3-4b | Qwen3-4B | https://huggingface.co/Qwen/Qwen3-4B/blob/main/LICENSE |
huggingface-reasoning-qwen3-1-7b | Qwen3-1.7B | https://huggingface.co/Qwen/Qwen3-1.7B/blob/main/LICENSE |
huggingface-reasoning-qwen3-06b | Qwen3-0.6B | https://huggingface.co/Qwen/Qwen3-0.6B/blob/main/LICENSE |
huggingface-llm-qwen2-5-72b-instruct | Qwen2.5-72B-Instruct | https://huggingface.co/Qwen/Qwen2.5-72B-Instruct/blob/main/LICENSE |
huggingface-llm-qwen2-5-32b-instruct | Qwen2.5-32B-Instruct | https://huggingface.co/Qwen/Qwen2.5-32B-Instruct/blob/main/LICENSE |
huggingface-llm-qwen2-5-14b-instruct | Qwen2.5-14B-Instruct | https://huggingface.co/Qwen/Qwen2.5-14B-Instruct/blob/main/LICENSE |
huggingface-llm-qwen2-5-7b-instruct | Qwen2.5-7B-Instruct | https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/blob/main/LICENSE |
deepseek-llm-r1-distill-llama-70b | DeepSeek-R1-Distill-Llama-70B | https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-70B/blob/main/LICENSE |
deepseek-llm-r1-distill-qwen-32b | DeepSeek-R1-Distill-Qwen-32B | https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B/blob/main/LICENSE |
deepseek-llm-r1-distill-qwen-14b | DeepSeek-R1-Distill-Qwen-14B | https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-14B/blob/main/LICENSE |
deepseek-llm-r1-distill-llama-8b | DeepSeek-R1-Distill-Llama-8B | https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-8B/blob/main/LICENSE |
deepseek-llm-r1-distill-qwen-7b | DeepSeek-R1-Distill-Qwen-7B | https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B/blob/main/LICENSE |
deepseek-llm-r1-distill-qwen-1-5b | DeepSeek-R1-Distill-Qwen-1.5B | https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B/blob/main/LICENSE |
openai-reasoning-gpt-oss-120b | GPT-OSS-120B | https://huggingface.co/openai/gpt-oss-120b/blob/main/LICENSE<br>https://huggingface.co/openai/gpt-oss-120b/blob/main/USAGE_POLICY |
openai-reasoning-gpt-oss-20b | GPT-OSS-20B | https://huggingface.co/openai/gpt-oss-20b/blob/main/LICENSE<br>https://huggingface.co/openai/gpt-oss-20b/blob/main/USAGE_POLICY |
meta-textgeneration-llama-3-3-70b-instruct | Llama 3.3 70B Instruct | https://huggingface.co/meta-llama/Llama-3.3-70B-Instruct/blob/main/LICENSE |
meta-textgeneration-llama-3-2-3b-instruct | Llama 3.2 3B Instruct | https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct/blob/main/LICENSE.txt |
meta-textgeneration-llama-3-2-1b-instruct | Llama 3.2 1B Instruct | https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/blob/main/LICENSE.txt |
meta-textgeneration-llama-3-1-8b-instruct | Llama 3.1 8B Instruct | https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct/blob/main/LICENSE |
nova-textgeneration-pro | Amazon Nova Pro | https://aws.amazon.com/service-terms/ |
nova-textgeneration-micro | Amazon Nova Micro | https://aws.amazon.com/service-terms/ |
nova-textgeneration-lite | Amazon Nova Lite | https://aws.amazon.com/service-terms/ |
nova-textgeneration-lite-v2 | Amazon Nova Lite v2 | https://aws.amazon.com/service-terms/ |
huggingface-reasoning-nvidia-nemotron-3-nano-30b-a3b-bf16 | Nemotron 3 Nano 30B | https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16/blob/main/LICENSE |
huggingface-vlm-qwen3-6-27b | Qwen3.6-27B | https://huggingface.co/Qwen/Qwen3-VL-27B/blob/main/LICENSE |
huggingface-vlm-qwen3-5-27b | Qwen3.5-27B | https://huggingface.co/Qwen/Qwen3-VL-27B/blob/main/LICENSE |
huggingface-vlm-qwen3-5-9b | Qwen3.5-9B | https://huggingface.co/Qwen/Qwen3-VL-9B/blob/main/LICENSE |
huggingface-vlm-qwen3-5-4b | Qwen3.5-4B | https://huggingface.co/Qwen/Qwen3-VL-4B/blob/main/LICENSE |
huggingface-vlm-gemma-4-31b-it | Gemma 4 31B | https://huggingface.co/google/gemma-4-31b-it/blob/main/LICENSE |
meta-vlm-llama-4-scout-17b-16e-instruct | Llama 4 Scout 17B | https://huggingface.co/meta-llama/Llama-4-Scout-17B-16E-Instruct/blob/main/LICENSE |
RLAIF Fine-Tuning Guide
RLAIF (Reinforcement Learning from AI Feedback) uses a Bedrock LLM as a judge to score model outputs during training. No human-labeled preference pairs are needed — the judge evaluates responses in real time.
How RLAIF Differs from RLVR
- RLVR: reward comes from a Lambda function (verifiable, deterministic)
- RLAIF: reward comes from a Bedrock LLM judge (flexible, open-ended)
- Best for: summarization, helpfulness, instruction-following, open-ended quality
Reward Model Options
The reward_model_id sets the Bedrock LLM used as judge. To get the current list of available models, run:
venv/bin/python3 -c "from sagemaker.train.constants import _ALLOWED_REWARD_MODEL_IDS; import json; print(json.dumps(_ALLOWED_REWARD_MODEL_IDS, indent=2))"Present the output to the user as a numbered list showing each model name and its available regions, then ask them to pick one.
---
Option 1: Builtin Reward Prompt
The simplest path. Choose one of the four builtin prompts — the SDK maps it to the corresponding Jinja template in the Hub recipe.
Pass the builtin name directly as the reward_prompt parameter:
"Builtin.Summarize"— evaluates summarization quality"Builtin.Faithfulness"— evaluates factual consistency with source"Builtin.ChainOfThought"— evaluates step-by-step reasoning quality"Builtin.Evaluation"— general response quality evaluation
When to use: When one of the four builtin prompts matches the use case well enough. Ask the user which one fits, or suggest based on the task.
Under the hood: reward_prompt="Builtin.Summarize" sets the hyperparameter judge_prompt_template to the matching template. No Evaluator.create() call needed.
See code_templates/rlaif_builtin.py for the full code template.
---
Option 2: Custom Reward Prompt
When the builtin prompts don't fit the use case, register a custom Jinja prompt file as a RewardPrompt evaluator. Suitable for: domain-specific quality, structured output validation, or multi-criteria scoring.
Key difference from RLVR:
- RLVR uses
Evaluator.create(type=REWARD_FUNCTION)→ deploys a Lambda function - RLAIF uses
Evaluator.create(type=REWARD_PROMPT)→ uploads a text/Jinja file to S3
The Bedrock judge receives the prompt and evaluates the model output. No Lambda is involved.
Steps
1. Write the prompt file — create a .jinja file with a suitable name in the project's scripts directory. The prompt should instruct the judge how to evaluate the model's response. It can reference {{ prompt }} and {{ response }} template variables. To help user write the prompt - think:
- What should the judge look for in a good response?
- What should it penalize?
- Should it return a score, a label, or a ranking?
<!-- markdownlint-disable MD029 -->
2. Register the prompt as an evaluator:
from sagemaker.ai_registry.evaluator import Evaluator
from sagemaker.ai_registry.air_constants import REWARD_PROMPT
reward_prompt_evaluator = Evaluator.create(
name="[GENERATE A NAME HERE]", # lowercase alphanumeric + hyphens, max 20 chars
type=REWARD_PROMPT,
source="path/to/custom_reward_prompt.jinja", # local file path or S3 URI
sagemaker_session=sagemaker_session,
wait=True
)
REWARD_PROMPT_ARN = reward_prompt_evaluator.arn
print(f"Evaluator ARN: {REWARD_PROMPT_ARN}")3. Pass the ARN as `reward_prompt` to RLAIFTrainer (instead of the builtin string).
See code_templates/rlaif_custom_prompt.py for the full code template.
---
Notes
mlflow_experiment_nameandmlflow_run_nameare optional but recommended for tracking.- For continued fine-tuning from a previously trained model, pass a
ModelPackageobject asmodelinstead of a base model string. Seecontinuous_customization.md.
RLVR Lambda Reward Function Guide
What Is a Lambda Reward Function?
For RLVR (Reinforcement Learning from Verifiable Rewards) training, a Lambda reward function is an AWS Lambda that evaluates model outputs during training and returns numerical reward scores. SageMaker invokes this Lambda within the training loop to provide learning signals that guide model optimization.
---
Helping Users Create Custom Reward Functions
Tell user: I will now review your use case and data, as well as my own resources, to propose a reward function that you can use to train your model. I will do my best to match it to your needs, but I strongly suggest that you review it carefully before starting the training.
Step 1: Analyze the Use Case
Reward functions are specific to the use case and dataset. Consider the task and data format to understand what constitutes a good output and how to measure it.
1. Review these materials:
use_case_spec.md— problem description and success criteria- Conversation context — the user's goals
- 20 rows of training data — structure and content of the expected responses
2. Answer these questions internally (involve the user if you need clarification):
- Given the analysis in (1), what makes a good response? A bad response? A partially correct response?
- Which aspects of the response can be verified programmatically?
- Are there specific constraints or formats the output must follow?
- How would the base model's initial responses during early training likely look?
---
Step 2: Analyze the Structure of the Response
- Which parts of the response contain the content you want to verify programmatically?
- How are those parts delimited? How can they be parsed?
- How rigid should the extraction patterns be, given the 20 rows of data reviewed?
- Are there special formats to account for (fractions, LaTeX, Unicode, Markdown, etc.)? How do they affect the extraction logic?
- Does the base model include a thinking block in its output?
- Does the use case require changing the model's behavior within the thinking block, or only in the final response?
- If warranted, how can the response format/schema be verified programmatically?
- If there is a ground truth in the data:
- Does the model's response need to match it exactly?
- Does a partial match count? If so, how?
- How can you deterministically decide whether the response is close enough to the ground truth?
---
Step 3: Plan the Verification Logic
- Write a function that extracts the verifiable parts identified in Steps 1 and 2 from the response.
- Identify the most suitable and performant tools for checking format or schema (e.g., which Python libraries?).
- If you need to validate generated code, write a function that executes it and returns a pass/fail/test result with a corresponding reward score.
- Are there keywords to check for? Which ones, and how many need to be present?
- What is the appropriate similarity function for comparing the response to the ground truth?
- If the response contains a block of text where the choice of words can vary slightly and still be correct, how can you verify that it is similar enough to the ground truth?
- Share the plan with the user and get confirmation before proceeding.
Step 4: Add Anti-Gaming Checks
Add at least two mechanisms to detect and penalize gaming behavior. Common gaming patterns include:
- Padding — inserting filler characters to inflate response length
- Skipping steps — jumping to a final answer without showing required reasoning
- Repetition — filling length requirements with repeated whitespace or words
- Dummy content — using placeholder text instead of genuine answers
- Echo attack — repeating the prompt or question back as the answer
- Nonsense — producing incoherent or irrelevant text
---
Step 5: Design the Aggregation Method
If the use case allows, think of rewards as a pyramid where each layer depends on the one beneath it. No credit is given for higher layers until lower ones are fully satisfied.
- Layer 1 (Foundation) — Structure
- Is the output formatted correctly and machine-parsable?
- Example: If JSON is expected, is it valid JSON? Are all required fields filled?
- Layer 2 (Core) — Semantics
- Is the output factually correct and does it deliver real value?
- Example: Can generated code pass unit tests? Is the math answer correct?
- Layer 3 (Polish) — Behavior
- Does the output meet operational and safety requirements?
- Example: Is the response concise? Free from toxic content? Complete?
- Aggregation
- What is the most suitable weighted distribution across these layers and their sub-components?
- Ensure each component function returns a spread of scores even for low-quality responses. If a component returns 0 for 90%+ of plausible early-training outputs, it will flatten the reward signal and stall learning.
- Briefly share the reasoning with the user and get confirmation.
---
Step 6: Write the Function as a Python Script
1. Create a file called lambda_function.py in the project's scripts directory. 2. Read the directory-management skill to determine the correct directory for storing scripts. 3. Consult the reward function templates for structural reference:
- Nova 2.0 Lite →
templates/nova_rlvr_reward_function_source_template.py - All other models →
templates/rlvr_reward_function_source_template.py
Critical rules:
- The
lambda_handlerfunction must be copied tolambda_function.pyexactly as given in the template. Do not change its signature or internal logic. - The chat template used in the example reward functions is correct. Use it to extract the assistant's response. Then apply the parsing logic from Step 2 to extract the parts of the response you want to score.
- Do not copy anything beyond the
lambda_handlerand the assistant-response extraction. The rest of the template is an example that will not work out of the box. You must customize the reward logic based on the use case and data, as described in Steps 1–5. Copying the template's reward logic without customization will likely produce flat rewards, wasting the user's time and compute budget.
Code writing principles:
1. Provide a learning gradient: Return diverse scores across [-1.0, 1.0], with partial credit for partial answers where appropriate — not just {-1, 0, 1}. 2. Verify correctly: Use actual parsing tools (json.loads, ast.parse, etc.), not string matching. 3. Include all necessary imports: Add every required import statement at the top of the file. 4. Execute fast: Complete in <100 ms with no API calls or blocking operations. 5. Be deterministic: Same input → same output, always. 6. Be bounded: The final score must always fall within [-1.0, 1.0]. Add return min(1.0, max(-1.0, score)) at the end. 7. Comment thoroughly: Include detailed comments explaining the reward logic.
Step 7: Test Locally
Test the reward function by executing it against crafted sample data:
1. Build test input. Infer the expected Lambda event and response format from the lambda_handler function in the appropriate source template. Choose one prompt from the training data reviewed in Step 1. Construct four test events that mimic what SageMaker sends to the Lambda:
- An excellent response — use the response from the data.
- A partially correct response — generate one that gets some things right but misses others.
- A bad response — generate one that is clearly wrong or off-topic, but without gaming.
- A gaming response — generate one that tries to get rewards by gaming.
2. Explain what you are doing and show the user the four responses that you want to test.
3. Write the batch to a temp file (e.g., /tmp/test_reward_input.json).
4. Run the function Invoke lambda_function.py via the shell:
python3 -c "
import sys, json
sys.path.insert(0, '<project-dir>/scripts')
from lambda_function import lambda_handler
with open('/tmp/test_reward_input.json') as f:
event = json.load(f)
result = lambda_handler(event, None)
print(json.dumps(result, indent=2))
"5. Verify the output. Check that:
- All scores fall within [-1.0, 1.0].
- The excellent response scores highest, the bad response scores lowest.
- No errors or exceptions occurred.
6. Show the user the test inputs, expected score ordering, and actual scores.
7. If the test fails or scores don't match expectations, fix the function and re-run until it passes. Inform the user about what you are doing.
Step 8: Check In with the User
- Share the path to the reward function with the user.
- Remind user that this is only a suggestion, and emphasize the need to review the reward function before launching the training. It is up to them to decide if they want to use it, edit it, or choose not to use it.
- Let the user know that the source templates are also available for them under finetuning/templates, if they want to compare your function to them or customize them on their own.
Step 9: Register the Reward Function in the Finetuning Output
After the reward function is written and tested, generate the registration code that corresponds to Cell 3 in code_templates/rlvr.py. Add the registration code to the finetuning output as Cell 3, following the format already chosen for this session (notebook or script).
Set reward_function_path to the path where lambda_function.py was saved in Step 6.
from sagemaker.ai_registry.evaluator import Evaluator
# Insert path to lambda_function.py from Step 6 here:
reward_function_path = ""
evaluator = Evaluator.create(
name="[GENERATE A NAME FOR THE EVALUATOR HERE]",
type="RewardFunction",
source=reward_function_path,
)
CUSTOM_REWARD_FUNCTION = evaluator.arn
print(f"Reward Function ARN: {CUSTOM_REWARD_FUNCTION}")Generate an appropriate name for the Evaluator based on the use case and current context.
- Format: lowercase, alphanumeric with hyphens only, 1–20 characters
- Pattern:
[a-zA-Z0-9](-*[a-zA-Z0-9]){0,20}
# scripts/mlflow_reference.py
# Reference for querying MLflow metrics from a training job.
# The agent reads this to understand the pattern, then writes
# its own code adapted to what the user needs.
import os
os.environ['AWS_DEFAULT_REGION'] = '[REGION]'
from sagemaker.core.resources import TrainingJob
import mlflow
from mlflow.tracking import MlflowClient
# Connect to MLflow via the training job
tj = TrainingJob.get(training_job_name='[TRAINING_JOB_NAME]')
mlflow.set_tracking_uri(tj.mlflow_config.mlflow_resource_arn)
client = MlflowClient()
run_id = tj.mlflow_details.mlflow_run_id
# List available metrics
run = client.get_run(run_id)
print(run.data.metrics.keys())
# Get full history for a metric
history = client.get_metric_history(run_id, '[METRIC_NAME]')
for h in history:
print(f"step={h.step}, value={h.value:.4f}")
"""
Provide your custom reward function code below. Learn about the available libraries and templates that you can use
at: https://docs.aws.amazon.com/sagemaker/latest/dg/customize-model.html.
- You must add your evaluation logic in the reward_function() function
- Do not remove the lambda_handler() function or modify its schema as it is required to create the reward function
"""
import json # For JSON parsing - adjust imports based on your use case
import re # For pattern matching and validation
from typing import Dict, Any, List, Optional, Union # For type hints
# Add any other imports your use case requires
# ========================================================================================
# NOTE: INITIAL SUGGESTION ONLY - MUST BE CUSTOMIZED
#
# YOU MUST:
# 1. Review and update each section per YOUR use case
# 2. Customize the logic for YOUR SPECIFIC requirements
# 3. Replace example values (field names, thresholds, etc.) with your actual values
# 4. Test thoroughly before using
#
# DO NOT use this code as-is. It will not work until you uncomment and customize it.
# =========================================================================================
# =========================================================================================
# SECTION 1: Helper function — content normalization
# =========================================================================================
# Nova messages use content as a string, a list of {"type":"text","text":"..."} chunks,
# or a dict with a "text" key. This helper normalizes all forms to a plain string.
def content_to_text(content: Any) -> str:
"""
Normalize Nova message content to a plain string.
Args:
content: String, list of text chunks, or dict with "text" key
Returns:
Plain text string
"""
if content is None:
return ""
if isinstance(content, str):
return content
if isinstance(content, list):
parts: List[str] = []
for item in content:
if isinstance(item, str):
parts.append(item)
elif isinstance(item, dict) and "text" in item:
parts.append(item["text"])
else:
parts.append(str(item))
return "".join(parts)
if isinstance(content, dict) and "text" in content:
return content["text"]
return str(content)
# =========================================================================================
# SECTION 2: Helper function — ground truth extraction
# =========================================================================================
# Nova reference_answer can be a dict with flexible keys (answer, label, sentiment, etc.),
# a JSON string, or a plain string.
def coerce_ground_truth(ground_truth: Union[str, Dict[str, Any], Any]) -> Optional[str]:
"""
Extract the ground-truth answer as a string from reference_answer.
Args:
ground_truth: Dict, JSON string, or plain string
Returns:
Ground truth string, or None if not found
"""
if ground_truth is None:
return None
if isinstance(ground_truth, str):
s = ground_truth.strip()
if not s:
return None
if s.startswith("{") or s.startswith("["):
try:
ground_truth = json.loads(s)
except Exception:
return s
else:
return s
if isinstance(ground_truth, dict):
for key in ("ground_truth", "answer", "label", "sentiment", "polarity", "target"):
if key in ground_truth and ground_truth[key] is not None:
return str(ground_truth[key])
if len(ground_truth) == 1:
only_val = next(iter(ground_truth.values()))
if only_val is not None:
return str(only_val)
return None
return str(ground_truth)
# =========================================================================================
# SECTION 3: Helper function — number extraction
# =========================================================================================
# TODO: UPDATE or REMOVE the helper function as per YOUR use case
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
def extract_number(text: str) -> Optional[float]:
"""
Extract numerical answer from text.
Looks for numbers after answer keywords, or returns the last number found.
Args:
text: Text containing a numerical answer
Returns:
Extracted number as float, or None if no number found
"""
if not text:
return None
# Try to find numbers after common answer keywords
answer_patterns = [
r'(?:equals|is|answer is|result is|=)\s*(-?\d+\.?\d*)',
r'(?:answer|result|solution):\s*(-?\d+\.?\d*)',
]
for pattern in answer_patterns:
match = re.search(pattern, text, re.IGNORECASE)
if match:
try:
return float(match.group(1))
except ValueError:
pass
# Fallback: find all numbers and return the last one (likely the answer)
pattern = r'-?\d+\.?\d*'
matches = re.findall(pattern, text)
if matches:
try:
return float(matches[-1])
except ValueError:
return None
return None
# =========================================================================================
# SECTION 4: Helper function — reasoning quality
# =========================================================================================
# TODO: UPDATE or REMOVE the helper function as per YOUR use case
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
def compute_reasoning_quality(response: str) -> float:
"""
Compute reasoning quality score based on response characteristics.
This is a simple heuristic - customize based on your needs.
Args:
response: The model's response text
Returns:
Quality score between 0.0 and 1.0
"""
if not response:
return 0.0
score = 0.0
# Check for reasoning indicators (customize these for your use case)
reasoning_indicators = [
'because', 'therefore', 'thus', 'since', 'so',
'first', 'second', 'then', 'finally',
'step', 'calculate', 'compute', 'equals'
]
response_lower = response.lower()
# Award points for reasoning indicators (max 0.55)
indicator_count = sum(1 for indicator in reasoning_indicators if indicator in response_lower)
score += min(indicator_count * 0.11, 0.55)
# Award points for response length (indicates detailed reasoning, max 0.25)
if len(response) > 30:
score += 0.05
if len(response) > 60:
score += 0.1
if len(response) > 120:
score += 0.1
# Award points for structured response (max 0.2)
if '\n' in response or '.' in response:
score += 0.2
return min(score, 1.0)
# =========================================================================================
# SECTION 5: Helper function — answer extraction
# =========================================================================================
# TODO: UPDATE or REMOVE the helper function as per YOUR use case
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
def extract_answer(response: str) -> Optional[str]:
"""
Extract the answer from a Nova model response.
Looks for <|begin_of_solution|>...<|end_of_solution|> blocks and \\boxed{} patterns.
Args:
response: The model's response text
Returns:
Extracted answer string, or None if not found
"""
if not response:
return None
# Try solution block first
solution_match = re.search(
r"<\|begin_of_solution\|>(.*?)<\|end_of_solution\|>",
response,
re.DOTALL,
)
if solution_match:
boxed = re.findall(r"\\boxed\{([^}]+)\}", solution_match.group(1))
if boxed:
return boxed[-1].strip()
# Fallback: boxed anywhere
boxed = re.findall(r"\\boxed\{([^}]+)\}", response)
if boxed:
return boxed[-1].strip()
return None
# =========================================================================================
# SECTION 6: Sample reward function
# =========================================================================================
# TODO: UPDATE or REMOVE the reward function as per YOUR use case
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
def reward_function(sample: Dict[str, Any], index: int) -> Dict[str, Any]:
"""
Args:
sample: Dictionary containing messages and reference_answer
index: Sample index in batch
Returns:
Dictionary with reward scores and metrics
"""
# ========================================================================
# SECTION 7: Parse input
# ========================================================================
# TODO: UPDATE logic to parse the input as per YOUR use case
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
messages = sample.get('messages', [])
ground_truth = sample.get('reference_answer', {})
# Get the assistant's response (last message with role assistant or nova_assistant)
response = ""
for msg in messages:
role = msg.get('role', '')
if role in ('assistant', 'nova_assistant'):
response = content_to_text(msg.get('content', ''))
# Extract numerical answers
predicted = extract_number(response)
expected_str = coerce_ground_truth(ground_truth)
expected = extract_number(expected_str) if expected_str else None
# Compute metrics
exact_match = 0.0
answer_present = 0.0
reasoning_quality = compute_reasoning_quality(response)
if predicted is not None and expected is not None:
exact_match = 1.0 if abs(predicted - expected) < 1e-6 else 0.0
answer_present = 1.0
# ========================================================================
# SECTION 8: Compute reward scores
# ========================================================================
# TODO: UPDATE logic to compute aggregate score
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
aggregate_reward = 0.7 * exact_match + 0.3 * reasoning_quality
# ========================================================================
# SECTION 9: Form the metrics list
# ========================================================================
# TODO: UPDATE logic to compute metrics list
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
metrics = [
{
'name': 'exact_match',
'value': float(exact_match),
'type': 'Reward'
},
{
'name': 'answer_present',
'value': float(answer_present),
'type': 'Metric'
},
{
'name': 'reasoning_quality',
'value': float(reasoning_quality),
'type': 'Metric'
}
]
# ========================================================================
# SECTION 10: Return output
# ========================================================================
# TODO: UPDATE the return statement to return YOUR output
# UPDATE the key before creating the evaluator
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
return {
'id': str(sample.get('id', f'sample-{index:03d}')),
'aggregate_reward_score': float(aggregate_reward),
'metrics_list': metrics
}
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""
AWS Lambda Handler for reward function.
SageMaker Nova RLVR invokes this with a bare list of samples and expects
a bare list of {id, aggregate_reward_score, ...} dicts in return.
"""
# Event is a bare list of samples
batch = event if isinstance(event, list) else []
results = []
for i, sample in enumerate(batch):
try:
result = reward_function(sample, i)
results.append(result)
except Exception as e:
results.append({
'id': str(sample.get('id', f'sample-{i:03d}') if isinstance(sample, dict) else f'sample-{i:03d}'),
'aggregate_reward_score': 0.0,
'metrics_list': []
})
return results
"""
Provide your custom reward function code below. Learn about the available libraries and templates that you can use
at: https://docs.aws.amazon.com/sagemaker/latest/dg/customize-model.html.
- You must add your evaluation logic in the reward_function() function
- Do not remove the lambda_handler() function or modify its schema as it is required to create the reward function
"""
import json # For JSON parsing - adjust imports based on your use case
import re # For pattern matching and validation
from typing import Dict, Any, List, Optional # For type hints
# Add any other imports your use case requires
# ========================================================================================
# NOTE: INITIAL SUGGESTION ONLY - MUST BE CUSTOMIZED
#
# YOU MUST:
# 1. Review and update each section per YOUR use case
# 2. Customize the logic for YOUR SPECIFIC requirements
# 3. Replace example values (field names, thresholds, etc.) with your actual values
# 4. Test thoroughly before using
#
# DO NOT use this code as-is. It will not work until you uncomment and customize it.
# =========================================================================================
# =========================================================================================
# SECTION 1: Helper function 1
# =========================================================================================
# TODO: UPDATE or REMOVE the helper function as per YOUR use case
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
def extract_number(text: str) -> Optional[float]:
"""
Extract numerical answer from text.
Looks for numbers after answer keywords, or returns the last number found.
Args:
text: Text containing a numerical answer
Returns:
Extracted number as float, or None if no number found
"""
if not text:
return None
# Try to find numbers after common answer keywords
answer_patterns = [
r'(?:equals|is|answer is|result is|=)\s*(-?\d+\.?\d*)',
r'(?:answer|result|solution):\s*(-?\d+\.?\d*)',
]
for pattern in answer_patterns:
match = re.search(pattern, text, re.IGNORECASE)
if match:
try:
return float(match.group(1))
except ValueError:
pass
# Fallback: find all numbers and return the last one (likely the answer)
pattern = r'-?\d+\.?\d*'
matches = re.findall(pattern, text)
if matches:
try:
return float(matches[-1]) # Return last number instead of first
except ValueError:
return None
return None
# =========================================================================================
# SECTION 2: Helper function 2
# =========================================================================================
# TODO: UPDATE or REMOVE the helper function as per YOUR use case
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
def compute_reasoning_quality(response: str) -> float:
"""
Compute reasoning quality score based on response characteristics.
This is a simple heuristic - customize based on your needs.
Args:
response: The model's response text
Returns:
Quality score between 0.0 and 1.0
"""
if not response:
return 0.0
score = 0.0
# Check for reasoning indicators (customize these for your use case)
reasoning_indicators = [
'because', 'therefore', 'thus', 'since', 'so',
'first', 'second', 'then', 'finally',
'step', 'calculate', 'compute', 'equals'
]
response_lower = response.lower()
# Award points for reasoning indicators (max 0.55)
indicator_count = sum(1 for indicator in reasoning_indicators if indicator in response_lower)
score += min(indicator_count * 0.11, 0.55)
# Award points for response length (indicates detailed reasoning, max 0.25)
if len(response) > 30:
score += 0.05
if len(response) > 60:
score += 0.1
if len(response) > 120:
score += 0.1
# Award points for structured response (max 0.2)
if '\n' in response or '.' in response:
score += 0.2
return min(score, 1.0)
# =========================================================================================
# SECTION 3: Sample reward function
# =========================================================================================
# TODO: UPDATE or REMOVE the reward function as per YOUR use case
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
def reward_function(sample: Dict[str, Any], index: int) -> Dict[str, Any]:
"""
Args:
sample: Dictionary containing messages and reference_answer
index: Sample index in batch
Returns:
Dictionary with reward scores and metrics
"""
# ========================================================================
# SECTION 4: Parse input
# ========================================================================
# TODO: UPDATE logic to parse the input as per YOUR use case
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
# Extract the response and reference
messages = sample.get('messages', sample.get('prompt', []))
reference_answer = sample.get('reference_answer', {}).get('text', '') or sample.get('reward_model', {}).get('ground_truth', '')
# Get the question and assistant's response
question = ""
response = ""
for msg in messages:
if msg.get('role') == 'user':
question = msg.get('content', '')
elif msg.get('role') == 'assistant':
response = msg.get('content', '')
# Extract numerical answers
predicted = extract_number(response)
expected = extract_number(reference_answer)
# Compute metrics
exact_match = 0.0
answer_present = 0.0
reasoning_quality = compute_reasoning_quality(response)
if predicted is not None and expected is not None:
exact_match = 1.0 if abs(predicted - expected) < 1e-6 else 0.0
answer_present = 1.0
# ========================================================================
# SECTION 5: Compute reward scores
# ========================================================================
# TODO: UPDATE logic to compute aggregate score
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
# Aggregate reward computation
aggregate_reward = 0.7 * exact_match + 0.3 * reasoning_quality
# ========================================================================
# SECTION 6: Form the metrics list
# ========================================================================
# TODO: UPDATE logic to compute metrics list
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
metrics = [
{
'name': 'exact_match',
'value': float(exact_match),
'type': 'Reward'
},
{
'name': 'answer_present',
'value': float(answer_present),
'type': 'Metric'
},
{
'name': 'reasoning_quality',
'value': float(reasoning_quality),
'type': 'Metric'
}
]
# ========================================================================
# SECTION 7: Return output
# ========================================================================
# TODO: UPDATE the return statement to return YOUR outout
# UPDATE the key before creating the evaluator
# Note the below lines of code are examples and will not work for your use case
# You MUST update them to match YOUR use case
return {
'id': str(sample.get('my_key', f'sample-{index:03d}')), # Use formatted index as fallback
'aggregate_reward_score': float(aggregate_reward),
'metrics_list': metrics
}
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""
AWS Lambda Handler for reward function
"""
try:
# Extract batch from event
batch = event.get('input', event) if isinstance(event, dict) else event
if 'batch' in event:
batch = event.get('batch', [])
elif 'body' in event:
body = json.loads(event.get('body', '{}'))
batch = body.get('batch', [])
if not batch:
return {"error":"Missing or empty batch"}
# Process each sample
results = []
for i, sample in enumerate(batch):
try:
result = reward_function(sample, i)
results.append(result)
except Exception as e:
return {"error": str(e)}
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps(results)
}
except Exception as e:
return {
'statusCode': 400,
'body': json.dumps({"error": str(e)})
}
Related skills
FAQ
Which trainers are supported?
SFT, DPO, RLVR, and RLAIF, including RLVR Lambda reward functions and RLAIF custom prompt creation.
What is required before running it?
A use_case_spec.md, a selected technique and base model, a verified SageMaker SDK environment, and a training dataset uploaded to the region's S3 bucket.