Source code for physicsnemo.experimental.peft.config

# 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.

"""LoRA configuration for ``physicsnemo.experimental.peft``.

LoRA (Low-Rank Adaptation) fine-tunes a frozen model by adding a small trainable
low-rank update ``B @ A`` beside selected linear layers. ``LoRAConfig`` declares
*which* layers to adapt and the adapter's capacity (``rank``) and strength
(``alpha``).
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Callable, Literal, Union

if TYPE_CHECKING:
    import torch
    import torch.nn as nn

# Accepted lora_A initialization spec: the named "default" strategy
# (kaiming_uniform, matching nn.Linear / the common PEFT default) or a callable
# that initializes the tensor in place. Extend the Literal as named strategies
# are added (e.g. SVD-based PiSSA/OLoRA).
LoRAInit = Union[Literal["default"], Callable[["torch.Tensor"], None]]


[docs] @dataclass class LoRAConfig: """Configuration for applying LoRA to a model. Exactly one of ``target_modules``, ``target_pattern`` or ``target_filter`` must be provided. They select layers by *fully-qualified* module name (e.g. ``blocks.3.Attn.qkv_project``), NOT bare leaf names — leaf names are not unique (the same short name can appear in many submodules). Parameters ---------- rank : int Low-rank dimension ``r``. Must be positive. alpha : float | None LoRA scaling numerator; ``scaling = alpha / rank``. ``None`` defaults ``alpha`` to ``rank`` (scaling 1.0). target_modules : list[str] | None Exact fully-qualified module names to wrap. target_pattern : str | None Regex (``re.search``) matched against fully-qualified module names. target_filter : Callable[[str, nn.Module], bool] | None Predicate ``(name, module) -> bool`` (most flexible selector). lora_dropout : float Dropout on the LoRA input path; ``0.0`` disables it. In ``[0.0, 1.0)``. extras_trainable : list[str] Additional fully-qualified module names to leave fully trainable (not low-rank), e.g. a final head or norm. wrap_mlp : bool Convenience flag to *also* adapt the transformer **feed-forward (FFN)** sub-block — the position-wise ``Linear -> activation -> Linear`` that follows attention in a transformer block (NOT arbitrary or standalone MLPs, and not the model as a whole). In PhysicsNeMo transformer blocks this is the ``ln_mlp1`` module: under Transformer Engine the fused ``te.LayerNormMLP``, otherwise a ``Sequential(LayerNorm, Mlp)``. Matched by the known feed-forward naming of those blocks, so it is a no-op on models without that structure. Additive to the selector above. init : {"default"} or callable Initialization for the ``lora_A`` factor (``lora_B`` is always zero, so the adapter is identity at init). ``"default"`` uses ``kaiming_uniform_(a=√5)`` — matching ``nn.Linear`` and the common PEFT default. Pass a callable ``(tensor) -> None`` to initialize ``lora_A`` in place with a custom scheme (e.g. ``lambda t: nn.init.normal_(t, std=0.01)`` for a Gaussian with a scale you control). Honored by wrappers built on ``_make_lora_params``; wrappers with their own parameterization (e.g. equivariant layers) initialize themselves and ignore this. """ rank: int = 16 alpha: float | None = None target_modules: list[str] | None = None target_pattern: str | None = None target_filter: Callable[[str, "nn.Module"], bool] | None = None lora_dropout: float = 0.0 extras_trainable: list[str] = field(default_factory=list) wrap_mlp: bool = False init: LoRAInit = "default" def __post_init__(self) -> None: selectors = { "target_modules": self.target_modules, "target_pattern": self.target_pattern, "target_filter": self.target_filter, } set_selectors = [k for k, v in selectors.items() if v is not None] if len(set_selectors) != 1: raise ValueError( "Exactly one of target_modules, target_pattern, target_filter " f"must be set, got {len(set_selectors)} ({set_selectors})." ) if self.target_modules is not None and len(self.target_modules) == 0: raise ValueError("target_modules is an empty list — nothing to select.") if self.target_pattern is not None and self.target_pattern == "": raise ValueError("target_pattern is an empty string — nothing to select.") if self.rank <= 0: raise ValueError(f"rank must be a positive integer, got {self.rank}.") if not (0.0 <= self.lora_dropout < 1.0): raise ValueError( f"lora_dropout must be in [0.0, 1.0), got {self.lora_dropout}." ) if not (callable(self.init) or self.init == "default"): raise ValueError( f"init={self.init!r} is not supported; use 'default' or a callable " "that initializes the lora_A tensor in place." ) @property def effective_alpha(self) -> float: """``alpha`` if set, else equal to ``rank`` (→ scaling 1.0).""" return float(self.alpha) if self.alpha is not None else float(self.rank) @property def scaling(self) -> float: """The LoRA scaling factor ``alpha / rank``.""" return self.effective_alpha / self.rank