[LoRA] Laguna: per-layer LoRA hidden-dim resolution for packed attention (#30298)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Filip
2026-08-04 14:48:05 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent a9c3b55435
commit 19d3f86895
3 changed files with 296 additions and 96 deletions
+27 -19
View File
@@ -124,6 +124,27 @@ def get_hidden_dim(
Please implement the function in the model class if it is not. Please implement the function in the model class if it is not.
You can reference this function in llama.py. You can reference this function in llama.py.
""" """
return get_default_hidden_dim(
module_name, config, layer_idx, lora_added_vocab_size
)
def get_default_hidden_dim(
module_name: str,
config: AutoConfig,
layer_idx: int,
lora_added_vocab_size: int = 0,
) -> Tuple[int]:
"""
Config-driven LoRA input/output dims for a module, assuming uniform
attention geometry across layers.
This is the fallback used when a model does not define ``get_hidden_dim``.
Models with per-layer geometry (e.g. Laguna's per-layer attention head
counts) should define ``get_hidden_dim`` on the model class, override the
layer-dependent modules there, and delegate the rest back to this helper
rather than re-deriving every branch.
"""
head_dim = getattr( head_dim = getattr(
config, "head_dim", config.hidden_size // config.num_attention_heads config, "head_dim", config.hidden_size // config.num_attention_heads
) )
@@ -141,11 +162,7 @@ def get_hidden_dim(
inter = config.intermediate_size inter = config.intermediate_size
first_k = getattr(config, "first_k_dense_replace", None) first_k = getattr(config, "first_k_dense_replace", None)
moe_freq = getattr(config, "moe_layer_freq", 1) moe_freq = getattr(config, "moe_layer_freq", 1)
if ( if first_k is not None and layer_idx >= first_k and layer_idx % moe_freq == 0:
first_k is not None
and layer_idx >= first_k
and layer_idx % moe_freq == 0
):
moe_inter = getattr(config, "moe_intermediate_size", None) moe_inter = getattr(config, "moe_intermediate_size", None)
n_shared = getattr(config, "n_shared_experts", None) n_shared = getattr(config, "n_shared_experts", None)
if moe_inter is not None and n_shared is not None: if moe_inter is not None and n_shared is not None:
@@ -155,11 +172,7 @@ def get_hidden_dim(
inter = config.intermediate_size inter = config.intermediate_size
first_k = getattr(config, "first_k_dense_replace", None) first_k = getattr(config, "first_k_dense_replace", None)
moe_freq = getattr(config, "moe_layer_freq", 1) moe_freq = getattr(config, "moe_layer_freq", 1)
if ( if first_k is not None and layer_idx >= first_k and layer_idx % moe_freq == 0:
first_k is not None
and layer_idx >= first_k
and layer_idx % moe_freq == 0
):
moe_inter = getattr(config, "moe_intermediate_size", None) moe_inter = getattr(config, "moe_intermediate_size", None)
n_shared = getattr(config, "n_shared_experts", None) n_shared = getattr(config, "n_shared_experts", None)
if moe_inter is not None and n_shared is not None: if moe_inter is not None and n_shared is not None:
@@ -182,8 +195,7 @@ def get_hidden_dim(
elif module_name == "kv_b_proj": elif module_name == "kv_b_proj":
return ( return (
config.kv_lora_rank, config.kv_lora_rank,
config.num_attention_heads config.num_attention_heads * (config.qk_nope_head_dim + config.v_head_dim),
* (config.qk_nope_head_dim + config.v_head_dim),
) )
elif module_name in DSA_INDEXER_LORA_NAMES: elif module_name in DSA_INDEXER_LORA_NAMES:
from sglang.srt.configs.model_config import ( from sglang.srt.configs.model_config import (
@@ -202,14 +214,12 @@ def get_hidden_dim(
return config.hidden_size, get_dsa_index_n_heads(config) return config.hidden_size, get_dsa_index_n_heads(config)
elif module_name == "gate_up_proj_moe": elif module_name == "gate_up_proj_moe":
moe_inter = ( moe_inter = (
getattr(config, "moe_intermediate_size", None) getattr(config, "moe_intermediate_size", None) or config.intermediate_size
or config.intermediate_size
) )
return config.hidden_size, moe_inter * 2 return config.hidden_size, moe_inter * 2
elif module_name == "down_proj_moe": elif module_name == "down_proj_moe":
moe_inter = ( moe_inter = (
getattr(config, "moe_intermediate_size", None) getattr(config, "moe_intermediate_size", None) or config.intermediate_size
or config.intermediate_size
) )
return moe_inter, config.hidden_size return moe_inter, config.hidden_size
elif module_name == "embed_tokens": elif module_name == "embed_tokens":
@@ -221,9 +231,7 @@ def get_hidden_dim(
# if contain extra tokens will be added; otherwise is 0. # if contain extra tokens will be added; otherwise is 0.
return config.hidden_size, config.vocab_size + lora_added_vocab_size return config.hidden_size, config.vocab_size + lora_added_vocab_size
else: else:
raise NotImplementedError( raise NotImplementedError("get_hidden_dim not implemented for " + module_name)
"get_hidden_dim not implemented for " + module_name
)
def get_normalized_target_modules( def get_normalized_target_modules(
+29
View File
@@ -50,6 +50,7 @@ from sglang.srt.layers.vocab_parallel_embedding import (
ParallelLMHead, ParallelLMHead,
VocabParallelEmbedding, VocabParallelEmbedding,
) )
from sglang.srt.lora.utils import get_default_hidden_dim
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
from sglang.srt.model_loader.weight_utils import default_weight_loader from sglang.srt.model_loader.weight_utils import default_weight_loader
from sglang.srt.models.utils import apply_qk_norm from sglang.srt.models.utils import apply_qk_norm
@@ -566,6 +567,31 @@ class LagunaModel(nn.Module):
def get_input_embeddings(self) -> nn.Embedding: def get_input_embeddings(self) -> nn.Embedding:
return self.embed_tokens return self.embed_tokens
def get_hidden_dim(self, module_name: str, layer_idx: int) -> Tuple[int, int]:
"""LoRA input/output dims for a module, honoring Laguna's per-layer
attention widths.
Laguna sizes each layer's attention from
``num_attention_heads_per_layer[layer_idx]`` (see ``LagunaAttention``),
so ``config.num_attention_heads`` — a single global value — is wrong for
any layer with a different head count. The generic
:func:`get_default_hidden_dim` fallback would use that global value and
mis-size the ``qkv_proj`` / ``o_proj`` LoRA buffers, crashing at
generation with ``sgemm_lora_a.py: assert x.shape[-1] == K``. We
override just those two attention projections and delegate every other
module (MLP, MoE, embed, lm_head, ...) to the shared helper.
"""
config = self.config
# No fallback; Laguna's head_dim is non-standard.
head_dim = config.head_dim
num_heads = config.num_attention_heads_per_layer[layer_idx]
num_kv_heads = config.num_key_value_heads
if module_name == "qkv_proj":
return config.hidden_size, head_dim * (num_heads + num_kv_heads * 2)
elif module_name == "o_proj":
return head_dim * num_heads, config.hidden_size
return get_default_hidden_dim(module_name, config, layer_idx)
def forward( def forward(
self, self,
input_ids: torch.Tensor, input_ids: torch.Tensor,
@@ -696,6 +722,9 @@ class LagunaForCausalLM(nn.Module):
def get_input_embeddings(self) -> nn.Embedding: def get_input_embeddings(self) -> nn.Embedding:
return self.model.embed_tokens return self.model.embed_tokens
def get_hidden_dim(self, module_name: str, layer_idx: int) -> Tuple[int, int]:
return self.model.get_hidden_dim(module_name, layer_idx)
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]): def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
stacked_params_mapping = [ stacked_params_mapping = [
("qkv_proj", "q_proj", "q"), ("qkv_proj", "q_proj", "q"),
@@ -0,0 +1,163 @@
"""Unit tests for Laguna's per-layer LoRA hidden-dim resolution.
Laguna (`poolside/Laguna-XS.2`) sizes each layer's attention from
`num_attention_heads_per_layer[layer_idx]`, so `config.num_attention_heads` —
a single global value — is wrong for any layer whose head count differs. The
generic `get_default_hidden_dim` fallback would use that global value and
mis-size the `qkv_proj` / `o_proj` LoRA buffers, which crashes at generation
with `sgemm_lora_a.py: assert x.shape[-1] == K`.
`LagunaModel.get_hidden_dim` overrides just the two attention projections with
per-layer widths and delegates every other module to the shared helper. These
tests exercise that method directly against a minimal fake config — no CUDA,
no server, no real weights — so they stay hermetic and fast.
Usage:
python -m pytest test/registered/unit/lora/test_laguna_hidden_dim_unit.py -v
"""
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
# CPU-only unit test; no CUDA/distributed dependencies.
register_cuda_ci(est_time=6, stage="base-b", runner_config="1-gpu-small")
register_amd_ci(est_time=6, suite="stage-b-test-1-gpu-small-amd")
import unittest
from sglang.srt.configs.laguna import LagunaConfig
from sglang.srt.lora.utils import get_default_hidden_dim
from sglang.srt.models.laguna import LagunaForCausalLM, LagunaModel
def _make_fake_laguna(num_attention_heads_per_layer):
"""Build a `LagunaModel` stand-in around a real `LagunaConfig`, without
running `__init__` (no weights, CPU-only).
`head_dim` (128) is deliberately not `hidden_size // num_attention_heads`,
matching every real Laguna checkpoint — so the hook must read `head_dim`
directly, never derive it. `LagunaConfig` sets the global
`num_attention_heads` from the first full-attention layer.
"""
config = LagunaConfig(
hidden_size=2048,
head_dim=128,
num_key_value_heads=8,
num_hidden_layers=len(num_attention_heads_per_layer),
num_attention_heads_per_layer=list(num_attention_heads_per_layer),
)
model = LagunaModel.__new__(LagunaModel)
model.config = config
return model
class TestLagunaPerLayerAttentionDims(unittest.TestCase):
"""`qkv_proj` / `o_proj` must follow the *layer's own* head count."""
def setUp(self):
# Layer 0: 48 heads (== global default). Layer 1: 64 heads (differs).
self.model = _make_fake_laguna([48, 64])
self.head_dim = 128
self.hidden_size = 2048
self.num_kv_heads = 8
def test_qkv_proj_uses_first_layer_head_count(self):
got = self.model.get_hidden_dim("qkv_proj", layer_idx=0)
expected_out = self.head_dim * (48 + self.num_kv_heads * 2)
self.assertEqual(got, (self.hidden_size, expected_out))
def test_qkv_proj_uses_wider_layer_head_count(self):
# The layer that the global fallback would size incorrectly.
got = self.model.get_hidden_dim("qkv_proj", layer_idx=1)
expected_out = self.head_dim * (64 + self.num_kv_heads * 2)
self.assertEqual(got, (self.hidden_size, expected_out))
def test_o_proj_uses_first_layer_head_count(self):
got = self.model.get_hidden_dim("o_proj", layer_idx=0)
self.assertEqual(got, (self.head_dim * 48, self.hidden_size))
def test_o_proj_uses_wider_layer_head_count(self):
got = self.model.get_hidden_dim("o_proj", layer_idx=1)
self.assertEqual(got, (self.head_dim * 64, self.hidden_size))
def test_wider_layer_differs_from_generic_fallback(self):
"""The regression guard: for the wider layer, the model hook must
return a DIFFERENT (correct) dim than the generic global-head
fallback — otherwise the buffer is mis-sized and generation asserts
in `sgemm_lora_a.py`.
"""
for module_name in ("qkv_proj", "o_proj"):
hook = self.model.get_hidden_dim(module_name, layer_idx=1)
generic = get_default_hidden_dim(module_name, self.model.config, 1)
self.assertNotEqual(
hook,
generic,
f"{module_name}: per-layer hook must diverge from the global "
"fallback on the wider layer",
)
def test_matching_layer_agrees_with_generic_fallback(self):
"""For a layer whose width == the global default, the hook and the
generic fallback must agree (no gratuitous divergence)."""
for module_name in ("qkv_proj", "o_proj"):
hook = self.model.get_hidden_dim(module_name, layer_idx=0)
generic = get_default_hidden_dim(module_name, self.model.config, 0)
self.assertEqual(hook, generic)
def test_missing_head_dim_raises_not_derives(self):
"""Removing the fallback means an absent head_dim fails loudly instead
of silently deriving a wrong hidden_size // num_attention_heads."""
cfg = self.model.config
del cfg.head_dim
with self.assertRaises(AttributeError):
self.model.get_hidden_dim("o_proj", layer_idx=1)
class TestLagunaNonAttentionDelegates(unittest.TestCase):
"""Non-attention modules must delegate to the shared helper so that
`--lora-target-modules all` (MLP / MoE / embed / lm_head) still works
once the model defines `get_hidden_dim`.
"""
def setUp(self):
self.model = _make_fake_laguna([48, 64])
def test_delegates_mlp_and_embedding_modules(self):
for module_name in (
"gate_up_proj",
"down_proj",
"gate_up_proj_moe",
"down_proj_moe",
"embed_tokens",
"lm_head",
):
for layer_idx in (0, 1):
self.assertEqual(
self.model.get_hidden_dim(module_name, layer_idx),
get_default_hidden_dim(module_name, self.model.config, layer_idx),
f"{module_name}@{layer_idx} should match the shared helper",
)
def test_unknown_module_raises(self):
with self.assertRaises(NotImplementedError):
self.model.get_hidden_dim("not_a_module", layer_idx=0)
class TestLagunaForCausalLMDelegation(unittest.TestCase):
"""`LagunaForCausalLM.get_hidden_dim` must forward to the inner model."""
def test_forwards_to_inner_model(self):
inner = _make_fake_laguna([48, 64])
causal = LagunaForCausalLM.__new__(LagunaForCausalLM)
# Bypass nn.Module.__setattr__: __new__ skips __init__, so the module
# registries it expects when assigning a Module-valued attr don't exist.
object.__setattr__(causal, "model", inner)
for module_name in ("qkv_proj", "o_proj", "gate_up_proj"):
for layer_idx in (0, 1):
self.assertEqual(
causal.get_hidden_dim(module_name, layer_idx),
inner.get_hidden_dim(module_name, layer_idx),
)
if __name__ == "__main__":
unittest.main()