Source code for physicsnemo.experimental.peft.utils

# SPDX-FileCopyrightText: Copyright (c) 2023 - 2026 NVIDIA CORPORATION & AFFILIATES.
# SPDX-FileCopyrightText: All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""PEFT utilities: base fingerprinting, optimizer param splitting, reporting."""

from __future__ import annotations

import hashlib
import logging

import torch.nn as nn

from physicsnemo.experimental.peft.lora import is_lora_layer

logger = logging.getLogger("experimental.peft")


def compute_base_fingerprint(model: nn.Module) -> str:
    """Stable hash of module class names + state_dict keys/shapes.

    Used only internally to detect architecture (in)compatibility between an
    adapter and a base model — compared exact-match, not shown to users — so the
    full SHA-256 digest is returned (no truncation, no collision risk).

    MUST be computed on the pristine base, *before* ``apply_lora`` wraps
    anything: after wrapping, keys gain ``base_layer.`` prefixes and the
    original structure can no longer be recovered.
    """
    h = hashlib.sha256()
    for name, module in model.named_modules():
        h.update(f"{name}:{type(module).__name__}\n".encode())
    for key, tensor in model.state_dict().items():
        h.update(f"{key}:{tuple(tensor.shape)}\n".encode())
    return h.hexdigest()


[docs] def split_params_for_optimizer(model: nn.Module) -> dict[str, list]: """Split parameters into ``{'lora', 'extras', 'frozen'}``. Route ``lora + extras`` to AdamW — NOT to optimizers like Muon whose Newton-Schulz orthogonalization is degenerate on low-rank factors. ``frozen`` is returned for reporting only. """ lora: list = [] extras: list = [] frozen: list = [] lora_ids: set[int] = set() for module in model.modules(): if is_lora_layer(module): lora.extend([module.lora_A, module.lora_B]) lora_ids.update({id(module.lora_A), id(module.lora_B)}) for p in model.parameters(): if id(p) in lora_ids: continue if p.requires_grad: extras.append(p) else: frozen.append(p) return {"lora": lora, "extras": extras, "frozen": frozen}
[docs] def set_adapter_enabled(model: nn.Module, enabled: bool) -> None: """Enable/disable all LoRA deltas in ``model``. Lets you run a base-only forward (e.g. for an adapter-vs-base comparison) without merging or reloading. """ for module in model.modules(): if is_lora_layer(module): module.enabled = enabled