Skip to content

Commit 2a030ca

Browse files
authored
Wrap nn.Linear LoRA path with LoRALinear (to_wrap/adapter) (#5310)
Signed-off-by: Hollow Man <hollowman@opensuse.org>
1 parent 328ffab commit 2a030ca

10 files changed

Lines changed: 432 additions & 193 deletions

File tree

‎src/megatron/bridge/models/conversion/peft_bridge.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
persistent_buffers,
4040
)
4141
from megatron.bridge.peft.canonical_lora import ModuleDict
42+
from megatron.bridge.peft.lora_layers import LinearAdapter
4243
from megatron.bridge.peft.lora_merge import LoRAMerge
4344
from megatron.bridge.peft.utils import (
4445
get_adapter_attributes_from_linear,
@@ -595,6 +596,12 @@ def _megatron_global_adapters_info_all_pp_ranks(
595596
input_is_parallel = adapter.input_is_parallel
596597
base_linear_is_parallel = True
597598
requires_expert_splits = adapter.linear_in.weight.ndim > 2
599+
elif isinstance(adapter, LinearAdapter):
600+
# Adapter wrapping a plain nn.Linear: no parallelism layout
601+
# and the wrapped module has no Megatron ``config`` to derive one from.
602+
input_is_parallel = False
603+
base_linear_is_parallel = False
604+
requires_expert_splits = False
598605
else:
599606
attrs = get_adapter_attributes_from_linear(to_wrap)
600607
input_is_parallel = attrs.input_is_parallel

‎src/megatron/bridge/peft/adapter_wrapper.py‎

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import torch
1818
import torch.nn as nn
19+
from megatron.core.utils import make_sharded_tensor_for_checkpoint
1920

2021
from megatron.bridge.peft.utils import ParallelLinearAdapter
2122

@@ -24,6 +25,33 @@
2425
from megatron.core.dist_checkpointing.mapping import ShardedStateDict
2526

2627

28+
def _plain_module_sharded_state_dict(
29+
module: nn.Module,
30+
prefix: str,
31+
sharded_offsets: Tuple[Tuple[int, int, int], ...] = (),
32+
) -> "ShardedStateDict":
33+
"""Build a sharded state dict for a module that has no ``sharded_state_dict``.
34+
35+
Used as a fallback for plain ``nn.Linear`` / delta-only ``LinearAdapter`` leaves on
36+
the simple (non-parallel) path: each parameter is wrapped as a replicated
37+
:class:`ShardedTensor` so distributed checkpointing preserves the trained weights
38+
instead of silently dropping them.
39+
40+
Args:
41+
module: The plain module whose parameters to shard.
42+
prefix: Key prefix applied to every parameter.
43+
sharded_offsets: Offsets prepended to each sharded tensor.
44+
45+
Returns:
46+
A sharded state dict mapping ``f"{prefix}{param_name}"`` to ShardedTensor.
47+
"""
48+
sharded_state_dict: Dict[str, Any] = {}
49+
for name, param in module.state_dict(prefix="", keep_vars=True).items():
50+
key = f"{prefix}{name}"
51+
sharded_state_dict[key] = make_sharded_tensor_for_checkpoint(param, key, prepend_offsets=sharded_offsets)
52+
return sharded_state_dict
53+
54+
2755
def _compute_mamba_dim_info(wrapped_module: nn.Module) -> Dict[str, int]:
2856
"""Compute Mamba dimension information from a wrapped module's config.
2957
@@ -101,6 +129,7 @@ def __init__(self, to_wrap: nn.Module, adapter: nn.Module) -> None:
101129
self.to_wrap = to_wrap
102130
self.adapter = adapter
103131
self._adapter_enabled = True
132+
self._base_returns_tuple = True
104133

105134
def enable_adapter_layers(self) -> None:
106135
"""Enable the adapter layers, allowing them to contribute to the forward pass output."""
@@ -140,13 +169,24 @@ def base_linear_forward(
140169
4. both: (out, bias, ln_out)
141170
"""
142171
linear_output = self.to_wrap(x, *args, **kwargs)
143-
assert isinstance(linear_output, tuple), (
144-
f"{self.to_wrap} should return a tuple but instead returns {linear_output}"
145-
)
146172

147173
bias = None
148174
layernorm_output = x
149175

176+
# Plain nn.Linear (and similar modules) return a bare tensor rather than the
177+
# Megatron-style tuple. Handle that case without asserting, so the wrapper stays
178+
# a drop-in for nn.Linear in simple (non-parallel) models. Record the return
179+
# shape so ``forward`` implementations can mirror it (tensor vs. tuple).
180+
if isinstance(linear_output, torch.Tensor):
181+
self._base_returns_tuple = False
182+
bias = getattr(self.to_wrap, "bias", None)
183+
return linear_output, bias, layernorm_output
184+
185+
assert isinstance(linear_output, tuple), (
186+
f"{self.to_wrap} should return a tensor or a tuple but instead returns {type(linear_output).__name__}"
187+
)
188+
self._base_returns_tuple = True
189+
150190
if len(linear_output) == 2:
151191
linear_output, bias = linear_output
152192
if isinstance(linear_output, tuple) and len(linear_output) == 2:
@@ -213,10 +253,24 @@ def sharded_state_dict(
213253
adapter_sharded_state_dict_kwargs["mamba_dim_info"] = _compute_mamba_dim_info(self.to_wrap)
214254

215255
sharded_state_dict = {}
216-
sharded_state_dict.update(self.to_wrap.sharded_state_dict(prefix, sharded_offsets, metadata))
217-
sharded_state_dict.update(
218-
self.adapter.sharded_state_dict(
219-
f"{prefix}adapter.", sharded_offsets, metadata, **adapter_sharded_state_dict_kwargs
256+
# The wrapped module may be a plain nn.Linear (simple, non-parallel path) that
257+
# does not implement distributed sharding. Fall back to wrapping each parameter
258+
# as a replicated ShardedTensor so the trained weights are preserved.
259+
if hasattr(self.to_wrap, "sharded_state_dict"):
260+
sharded_state_dict.update(self.to_wrap.sharded_state_dict(prefix, sharded_offsets, metadata))
261+
else:
262+
sharded_state_dict.update(_plain_module_sharded_state_dict(self.to_wrap, prefix, sharded_offsets))
263+
# Likewise, a delta-only LinearAdapter has no sharded layout; only parallel adapters
264+
# carry distributed-sharding information. Fall back to replicated ShardedTensors for
265+
# the plain-adapter case so the trained LoRA matrices are not dropped.
266+
if hasattr(self.adapter, "sharded_state_dict"):
267+
sharded_state_dict.update(
268+
self.adapter.sharded_state_dict(
269+
f"{prefix}adapter.", sharded_offsets, metadata, **adapter_sharded_state_dict_kwargs
270+
)
271+
)
272+
else:
273+
sharded_state_dict.update(
274+
_plain_module_sharded_state_dict(self.adapter, f"{prefix}adapter.", sharded_offsets)
220275
)
221-
)
222276
return sharded_state_dict

‎src/megatron/bridge/peft/canonical_lora.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,15 +319,16 @@ def transform(self, m: nn.Module, name: Optional[str] = None, prefix: Optional[s
319319
"""
320320

321321
# Skip already transformed modules
322-
if isinstance(m, (LinearAdapter, LoRALinear, LoRALinearSplitQKV, LoRALinearSplitFC1UpGate, LoRATopKRouter)):
322+
if isinstance(m, (LoRALinear, LoRALinearSplitQKV, LoRALinearSplitFC1UpGate, LoRATopKRouter)):
323323
return m
324324

325325
if (ans := self.match(m, name, prefix)) is not None:
326326
(match, full_name) = ans
327327
if isinstance(m, nn.Linear) and not is_modelopt_linear(m):
328-
return LinearAdapter(
328+
adapter = LinearAdapter(
329329
m, dim=self.dim, alpha=self.alpha, dropout=self.dropout, lora_A_init_method=self.lora_A_init_method
330330
)
331+
return LoRALinear(m, adapter)
331332

332333
is_expert = is_expert_linear(full_name)
333334
attrs = get_adapter_attributes_from_linear(m, is_expert=is_expert)

‎src/megatron/bridge/peft/lora.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,21 +127,22 @@ def transform(self, module: nn.Module, name: Optional[str] = None, prefix: Optio
127127
nn.Module: The modified module with LoRA applied, or the original module if not a target.
128128
"""
129129
# Skip already transformed modules
130-
adapter_types = (LinearAdapter, LoRALinear, LoRATopKRouter)
130+
adapter_types = (LoRALinear, LoRATopKRouter)
131131
if isinstance(module, adapter_types):
132132
return module
133133

134134
if (ans := self.match(module, name, prefix)) is not None:
135135
_, full_name = ans
136136
if isinstance(module, nn.Linear) and not is_modelopt_linear(module):
137-
return LinearAdapter(
137+
adapter = LinearAdapter(
138138
module,
139139
dim=self.dim,
140140
alpha=self.alpha,
141141
dropout=self.dropout,
142142
lora_A_init_method=self.lora_A_init_method,
143143
lora_dtype=self.lora_dtype,
144144
)
145+
return LoRALinear(module, adapter)
145146

146147
is_expert = is_expert_linear(full_name)
147148
attrs = get_adapter_attributes_from_linear(

‎src/megatron/bridge/peft/lora_layers.py‎

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
import math
16-
from typing import Any, Literal, Optional, Tuple
16+
from typing import Any, Literal, Optional
1717

1818
import torch
1919
import torch.nn as nn
@@ -62,7 +62,7 @@ def bias(self) -> torch.Tensor | None:
6262
"""Return the wrapped linear bias."""
6363
return getattr(self.to_wrap, "bias", None)
6464

65-
def forward(self, x: torch.Tensor, *args: Any, **kwargs: Any) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
65+
def forward(self, x: torch.Tensor, *args: Any, **kwargs: Any):
6666
"""Forward pass that combines the wrapped module output with the adapter output.
6767
6868
Args:
@@ -71,18 +71,21 @@ def forward(self, x: torch.Tensor, *args: Any, **kwargs: Any) -> Tuple[torch.Ten
7171
**kwargs: Additional keyword arguments for the wrapped module.
7272
7373
Returns:
74-
A tuple containing:
75-
- Combined output (linear_output + adapter_output) if adapter is enabled,
76-
otherwise just the linear_output
77-
- Bias term (if present, otherwise None)
74+
When the wrapped module returns a Megatron-style tuple, a
75+
``(combined_output, bias)`` tuple; when it returns a bare tensor
76+
(e.g. a plain ``nn.Linear``), a bare tensor so the wrapper stays a
77+
drop-in replacement in simple (non-parallel) models.
7878
"""
7979
# pylint: disable=C0115,C0116
8080
linear_output, bias, layernorm_output = self.base_linear_forward(x, *args, **kwargs)
8181
if not self._adapter_enabled:
82-
return linear_output, bias
82+
return linear_output if not self._base_returns_tuple else (linear_output, bias)
8383
adapter_output = self.adapter_forward(self.adapter, layernorm_output.contiguous(), *args, **kwargs)
8484
adapter_output = adapter_output.reshape(linear_output.shape)
85-
return linear_output + adapter_output, bias
85+
combined = linear_output + adapter_output
86+
if not self._base_returns_tuple:
87+
return combined
88+
return combined, bias
8689

8790

8891
class LoRATopKRouter(AdapterWrapper):
@@ -343,11 +346,18 @@ def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, None]:
343346
return out, None
344347

345348

346-
class LinearAdapter(nn.Linear):
347-
"""Linear with LoRA that preserves the base weight and bias checkpoint keys.
349+
class LinearAdapter(nn.Module):
350+
"""Delta-only LoRA adapter for a plain ``nn.Linear``, mirroring :class:`ParallelLinearAdapter`'s role.
351+
352+
This adapter holds *only* the low-rank LoRA delta (``linear_in`` -> ``linear_out``) and
353+
produces just the scaled adaptation term. It is intended to be wrapped together with the
354+
original linear by :class:`LoRALinear` (the ``to_wrap`` / ``adapter`` pattern), so that
355+
base weights and adapter weights live in distinct submodules and adapter state is
356+
checkpointed under the ``adapter.`` prefix.
348357
349358
Args:
350-
orig_linear: The linear module to augment.
359+
orig_linear: The linear module to augment (only its shape/dtype/device are used; its
360+
weights are *not* copied).
351361
dim: LoRA's dimension (in_features -> dim -> out_features).
352362
alpha: LoRA's scaling alpha.
353363
dropout: Dropout probability (default: 0.0).
@@ -366,47 +376,31 @@ def __init__(
366376
lora_A_init_method: Literal["xavier", "uniform"] = "xavier",
367377
lora_dtype: Optional[torch.dtype] = None,
368378
) -> None:
369-
"""Initialize LinearAdapter by copying from original Linear and adding LoRA components.
379+
"""Initialize the LoRA delta weights from the original Linear's shape and dtype.
370380
371381
Args:
372-
orig_linear: The original Linear module to adapt.
382+
orig_linear: The original Linear module to adapt (weights are not copied).
373383
dim: LoRA rank dimension.
374384
alpha: LoRA scaling factor.
375385
dropout: Dropout probability.
376386
dropout_position: When to apply dropout ('pre' or 'post' LoRA computation).
377387
lora_A_init_method: Initialization method for LoRA matrix A.
378388
lora_dtype: Data type for LoRA weights.
379389
"""
390+
super().__init__()
380391
assert isinstance(orig_linear, nn.Linear)
381-
super(LinearAdapter, self).__init__(
382-
in_features=orig_linear.in_features,
383-
out_features=orig_linear.out_features,
384-
bias=orig_linear.bias is not None,
385-
device=orig_linear.weight.device,
386-
dtype=orig_linear.weight.dtype,
387-
)
388-
# copy weights
389-
self.weight.data.copy_(orig_linear.weight.data)
390-
if orig_linear.bias is not None:
391-
self.bias.data.copy_(orig_linear.bias.data)
392-
# initialize the adapter
392+
self.in_features = orig_linear.in_features
393+
self.out_features = orig_linear.out_features
393394
self._init_adapter(
394395
dim=dim,
395396
alpha=alpha,
396397
dropout=dropout,
397398
dropout_position=dropout_position,
398399
lora_A_init_method=lora_A_init_method,
399400
lora_dtype=lora_dtype,
401+
device=orig_linear.weight.device,
402+
base_dtype=orig_linear.weight.dtype,
400403
)
401-
self._adapter_enabled = True
402-
403-
def enable_adapter_layers(self) -> None:
404-
"""Enable the adapter layers, allowing them to contribute to the forward pass output."""
405-
self._adapter_enabled = True
406-
407-
def disable_adapter_layers(self) -> None:
408-
"""Disable the adapter layers, making the forward pass return only the base module output."""
409-
self._adapter_enabled = False
410404

411405
@torch.no_grad
412406
def _init_adapter(
@@ -417,8 +411,10 @@ def _init_adapter(
417411
dropout_position: Literal["pre", "post"] = "pre",
418412
lora_A_init_method: Literal["xavier", "uniform"] = "xavier",
419413
lora_dtype: Optional[torch.dtype] = None,
414+
device: Optional[torch.device] = None,
415+
base_dtype: Optional[torch.dtype] = None,
420416
) -> None:
421-
"""Initialize the LoRA weights and freeze the base parameters.
417+
"""Initialize the LoRA delta weights.
422418
423419
Args:
424420
dim: LoRA's dimension (in_features -> dim -> out_features).
@@ -427,20 +423,16 @@ def _init_adapter(
427423
dropout_position: Where to apply dropout relative to LoRA (choices: ['pre', 'post'], default='pre').
428424
lora_A_init_method: Initialization method for lora_A (choices: ['xavier', 'uniform']).
429425
lora_dtype: Adapter weight dtype. Defaults to the base weight dtype.
426+
device: Device for the LoRA weights.
427+
base_dtype: Base weight dtype, used when ``lora_dtype`` is not provided.
430428
"""
431429
self.dim = dim
432430
self.alpha = alpha
433431
self.scale = alpha / dim
434432

435-
# Freeze original weights
436-
device = self.weight.device
437-
self.weight.requires_grad = False
438-
if self.bias is not None:
439-
self.bias.requires_grad = False
440-
441433
in_features = self.in_features
442434
out_features = self.out_features
443-
dtype = lora_dtype or self.weight.dtype
435+
dtype = lora_dtype or base_dtype
444436

445437
self.linear_in = nn.Linear(in_features, dim, bias=False, dtype=dtype, device=device)
446438
self.linear_out = nn.Linear(dim, out_features, bias=False, dtype=dtype, device=device)
@@ -457,24 +449,20 @@ def _init_adapter(
457449
self.dropout_position = dropout_position
458450

459451
def forward(self, x: torch.Tensor) -> torch.Tensor:
460-
"""Forward pass combining Linear output with LoRA adaptation.
452+
"""Compute the scaled LoRA delta only (no base-weight term).
461453
462454
Args:
463455
x: Input tensor.
464456
465457
Returns:
466-
Combined output from original linear layer and LoRA adaptation.
458+
The scaled low-rank adaptation ``scale * linear_out(linear_in(x))`` with dropout
459+
applied per ``dropout_position``.
467460
"""
468461
# pylint: disable=C0115,C0116
469-
res = torch.nn.functional.linear(x, self.weight, self.bias)
470-
471-
if not self._adapter_enabled:
472-
return res
473-
474462
if self.dropout_position == "pre":
475463
x = self.dropout(x)
476464
lora_res = self.linear_out(self.linear_in(x))
477465
lora_res = lora_res * self.scale
478466
if self.dropout_position == "post":
479467
lora_res = self.dropout(lora_res)
480-
return res + lora_res
468+
return lora_res

0 commit comments

Comments
 (0)