spark_model/layers/qwen3_ssm/
lora.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Feature-1 MoE LoRA install on the GDN / linear-attention layer.
4//!
5//! Linear-attention layers carry NO attention LoRA (their projections are
6//! rejected at classify), but the MoE FFN exists on every layer, so a real
7//! all-layer MoE adapter installs its router + routed-expert deltas here too —
8//! the same `MoeLayer::set_lora_weights` path the full-attention layer uses.
9
10use anyhow::Result;
11use spark_runtime::gpu::GpuBackend;
12
13use super::Qwen3SsmLayer;
14use crate::layers::FfnComponent;
15use crate::layers::ops::lora_delta::{LoraKernels, LoraPair};
16use crate::lora::ExpertLoraLayer;
17
18impl Qwen3SsmLayer {
19    /// Install this GDN layer's MoE router + routed-expert LoRA onto its
20    /// `FfnComponent::Moe`. Hard-rejects (never silently drops) when the layer's
21    /// FFN is not MoE — an expert/router delta on a dense-FFN GDN layer is a
22    /// loader/adapter mismatch.
23    pub fn set_moe_lora_weights(
24        &mut self,
25        router: Option<LoraPair>,
26        experts: ExpertLoraLayer,
27        kernels: LoraKernels,
28        gpu: &dyn GpuBackend,
29    ) -> Result<()> {
30        if let FfnComponent::Moe(m) = &mut self.ffn {
31            return m.set_lora_weights(router, experts, kernels, gpu);
32        }
33        anyhow::bail!(
34            "LoRA: router/expert deltas installed on a linear-attention layer whose \
35             FFN is not MoE (loader/adapter mismatch)"
36        )
37    }
38}
39
40impl Qwen3SsmLayer {
41    /// Install this linear-attention layer's DENSE-FFN LoRA onto its
42    /// `FfnComponent::Dense`.
43    ///
44    /// The mirror of `set_moe_lora_weights` for dense-FFN hybrids. A
45    /// linear-attention layer carries no attention projections, but on
46    /// Qwen3.8-27B it does carry the SwiGLU FFN — all 64 layers do, only 16 of
47    /// which are full attention — and real adapters for that architecture ship
48    /// gate/up/down for every one of them. Rejecting those rejected three
49    /// quarters of the adapter, and the old message could only suggest
50    /// retraining with `layers_to_transform`.
51    ///
52    /// The component is the same `DenseFfnLayer` the full-attention layers
53    /// hold, so the delta path, its pinned dispatch arms and its refusals are
54    /// identical here — this only hands it the weights.
55    ///
56    /// Hard-rejects a non-dense FFN rather than dropping the pairs: a dense
57    /// delta arriving at a MoE or absent FFN is a loader/adapter mismatch, and
58    /// silently ignoring it would be an adapter that reports success and does
59    /// nothing — the exact failure this whole change removes.
60    pub fn set_ffn_lora_weights(
61        &mut self,
62        ffn: crate::layers::ops::lora_delta::LoraFfnWeights,
63    ) -> Result<()> {
64        match &mut self.ffn {
65            FfnComponent::Dense(d) => d.set_lora_weights(ffn),
66            FfnComponent::Moe(_) => anyhow::bail!(
67                "LoRA: dense-FFN delta on a linear-attention layer whose FFN is MoE — \
68                 routed-expert deltas belong on set_moe_lora_weights"
69            ),
70            FfnComponent::None => {
71                anyhow::bail!("LoRA: dense-FFN delta on a linear-attention layer that has no FFN")
72            }
73        }
74    }
75}
76
77impl Qwen3SsmLayer {
78    /// Install this layer's GDN `out_proj` delta.
79    ///
80    /// Separate from `set_ffn_lora_weights`: that one targets the block's FFN,
81    /// this one the linear-attention block's own output projection.
82    pub fn set_out_proj_lora(&mut self, pair: LoraPair, kernels: LoraKernels) {
83        self.lora_out_proj = Some((pair, kernels));
84    }
85
86    /// `out += scale * (normed_out @ A^T) @ B^T`.
87    ///
88    /// No-op without an adapter, so the base path stays byte-identical.
89    ///
90    /// The caller must invoke this AFTER any TP reduce: `out` is a partial
91    /// row-parallel product until then, and a delta added to a partial would
92    /// be summed once per rank.
93    pub(super) fn apply_lora_out_proj(
94        &self,
95        ctx: &crate::layers::ForwardContext,
96        normed_out: spark_runtime::gpu::DevicePtr,
97        out: spark_runtime::gpu::DevicePtr,
98        m: u32,
99        stream: u64,
100    ) -> Result<()> {
101        if crate::layers::ops::lora_delta::lora_no_ffn() {
102            return Ok(());
103        }
104        let Some((ref pair, ref kernels)) = self.lora_out_proj else {
105            return Ok(());
106        };
107        crate::layers::ops::lora_delta::apply_lora_delta(
108            ctx.gpu,
109            kernels,
110            pair,
111            normed_out,
112            out,
113            m,
114            ctx.buffers.lora_xa(),
115            ctx.buffers.lora_delta(),
116            stream,
117        )
118    }
119}