Source code for protomotions.agents.peft.sft_agent
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""SFT agent for PEFT adapters on a frozen discrete-token GPC prior."""
from pathlib import Path
from typing import Optional
from lightning.fabric import Fabric
from protomotions.agents.fine_tuning.pretrained_modules import PretrainedModulesMixin
from protomotions.agents.optimizer.factory import instantiate_optimizer
from protomotions.agents.supervised.agent import SupervisedAgent
from protomotions.agents.peft.prior_setup import DiscretePriorPEFTSetupMixin
# SFT uses SupervisedAgent directly, so it opts into the shared frozen-module
# lifecycle here. RLFT receives the same mixin through FineTuningAgent.
[docs]
class DiscretePriorPEFTSFTAgent(
DiscretePriorPEFTSetupMixin,
PretrainedModulesMixin,
SupervisedAgent,
):
"""Train a discrete-prior PEFT adapter with target-token supervision.
The model owns the expert labeling path through the frozen target encoder;
the generic supervised loop stores those labels and applies the configured
supervised loss during optimization.
"""
[docs]
def __init__(self, fabric: Fabric, env, config, root_dir: Optional[Path] = None):
if getattr(config.model, "critic", None) is not None:
raise ValueError("DiscretePriorPEFTSFTAgent does not use a critic.")
super().__init__(fabric, env, config, root_dir=root_dir)
@property
def has_critic(self):
return False
[docs]
def create_model(self):
# SupervisedAgent's external-expert slot is unused here. PEFT SFT gets
# labels from the frozen target encoder inside DiscretePriorPEFTSFTModel.
self.expert_model = None
return super().create_model()
def _should_build_target_encoder(self, mimic_target_poses_dim: int) -> bool:
if mimic_target_poses_dim <= 0:
raise ValueError(
"DiscretePriorPEFTSFTAgent requires environment observations to include "
"mimic_target_poses so the frozen target encoder can build "
"supervision labels."
)
return True
[docs]
def create_optimizers(self, model):
optimizer = instantiate_optimizer(
self.config.model.actor_optimizer,
model,
params=self._actor_optimizer_params(model),
)
self.training_model, self.supervised_optimizer = self._setup_model_optimizer(
model,
optimizer,
)
[docs]
def get_state_dict(self, state_dict):
state_dict = super().get_state_dict(state_dict)
# RLFT warm-start reads the actor optimizer state from SFT checkpoints.
state_dict["actor_optimizer"] = self.supervised_optimizer.state_dict()
return state_dict