Source code for protomotions.agents.peft.utils.model_state

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Model-state loading rules for discrete-prior PEFT checkpoints.

The agent should not need to know why a missing key is acceptable. This module
owns those low-level checkpoint rules and exposes one readable entry point:
load a PEFT model state if it is adapter-only or a compatible full training
checkpoint.
"""

from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass

from torch import nn

from protomotions.agents.peft.utils.adapter_state import (
    REFERENCE_STATE_PREFIXES,
    STUDENT_TO_REFERENCE_PREFIX,
    is_adapter_checkpoint_key,
    refuse_reference_without_conditioning,
    strip_actor_prefix,
)
from protomotions.agents.utils.checkpoint import (
    prepare_model_state_dict, expected_checkpoint_state, validate_checkpoint_state,
)
from protomotions.agents.utils.normalization import (
    materialize_lazy_running_stats_from_state_dict,
)


REFERENCE_FULL_CHECKPOINT_PREFIXES = tuple(
    "_actor." + prefix for prefix in REFERENCE_STATE_PREFIXES
)


[docs] def has_reference_state(model_state: dict) -> bool: """Return True when a PEFT checkpoint carries any reference-policy state.""" return any( strip_actor_prefix(key).startswith(REFERENCE_STATE_PREFIXES) for key in model_state )
[docs] @dataclass(frozen=True) class PEFTModelStateLoad: """What ``load_compatible_peft_model_state`` found in a checkpoint. Attributes: adapter_only: True for an adapter checkpoint without frozen prior weights. reference_loaded: True when the checkpoint restored the reference policy. """ adapter_only: bool reference_loaded: bool
[docs] def load_compatible_peft_model_state( module: nn.Module, model_state: dict, ) -> PEFTModelStateLoad: """Load adapter-only state or a compatible full PEFT training checkpoint. Either kind restores the reference policy when the checkpoint stores one. A reference saved before it kept its own conditioning network is refused (``scripts/migrate_peft_reference_conditioning.py`` writes a migrated file). Resume and inference use the restored reference; a new RLFT run replaces it at training start. """ if is_adapter_only_model_state(model_state): result = module._actor.load_adapter_state_dict(model_state, strict=True) return PEFTModelStateLoad( adapter_only=True, reference_loaded=result["reference_loaded"], ) model_state = prepare_model_state_dict(module, model_state) refuse_reference_without_conditioning(model_state) reference_state_present = has_reference_state(model_state) expected = expected_checkpoint_state(module, model_state) optional_prefixes = optional_full_checkpoint_state_prefixes(module) if reference_state_present: # The reference has the student's architecture. Derive its expected # keys/shapes without allocating or changing a live reference module. for student_prefix, reference_prefix in STUDENT_TO_REFERENCE_PREFIX.items(): source, target = "_actor." + student_prefix, "_actor." + reference_prefix expected.update({target + key[len(source):]: value for key, value in list(expected.items()) if key.startswith(source)}) optional_prefixes = tuple( prefix for prefix in optional_prefixes if prefix not in REFERENCE_FULL_CHECKPOINT_PREFIXES ) validate_checkpoint_state(expected, model_state, optional_prefixes=optional_prefixes) if reference_state_present: module._actor.ensure_reference_modules() materialize_lazy_running_stats_from_state_dict(module, model_state) module.load_state_dict(model_state, strict=False) if reference_state_present: module._actor.mark_reference_loaded() return PEFTModelStateLoad( adapter_only=False, reference_loaded=reference_state_present, )
[docs] def optional_full_checkpoint_state_prefixes(module: nn.Module) -> tuple[str, ...]: """Return state_dict prefixes that are optional for full PEFT checkpoints. Each module owns the optional state it introduces by defining ``optional_full_checkpoint_state_prefixes()`` with local state_dict prefixes. This helper qualifies those local prefixes by walking ``named_modules()``, keeping checkpoint compatibility rules close to the modules that create the state instead of centralizing private path strings. """ prefixes: list[str] = [] for module_name, submodule in module.named_modules(): hook = getattr(submodule, "optional_full_checkpoint_state_prefixes", None) if not callable(hook): continue base = f"{module_name}." if module_name else "" for prefix in _as_prefix_tuple(hook()): prefixes.append(f"{base}{prefix}") return tuple(dict.fromkeys(prefixes))
def _as_prefix_tuple(prefixes: str | Iterable[str]) -> tuple[str, ...]: result = (prefixes,) if isinstance(prefixes, str) else tuple(prefixes) if not all(isinstance(prefix, str) for prefix in result): raise TypeError("optional checkpoint state prefixes must be strings.") return result
[docs] def is_adapter_only_model_state(model_state: dict) -> bool: """Return True for an adapter checkpoint: adapter/task state and reference state only.""" return bool(model_state) and all(is_adapter_checkpoint_key(key) for key in model_state)