Instructions to use RomanCohort/RNAQwen with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use RomanCohort/RNAQwen with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("RomanCohort/RNAQwen", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 7,804 Bytes
68f9568 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | """SOS-LoRA 实现的正确性测试。
测的不是"能跑",是**论文声称的每一条性质是否真的成立**:
1. 同预算:r_tot 相同时参数量与标准 LoRA 一致(论文说"matching the parameter budget")
2. 零初始更新:B 零初始化 => 初始 forward == 原层 forward(论文"zero initial update")
3. 尺度:s_k = (α/r_tot)·γ_k,γ_k 在 [1,2.5] 上线性等距
4. 正交初始化:不同专家的 A 方向两两正交
5. 合并等价:ΔW = Σ s_k A_k B_k 并进 W₀ 后,输出与未合并前一致
6. 梯度分档:每个专家的梯度幅度 ∝ s_k(这是"scale-separated dynamics"的机制本身)
7. 正则:同专家内部不罚、跨专家才罚
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
import torch # noqa: E402
import torch.nn as nn # noqa: E402
from rnaqwen.models.sos_lora import ( # noqa: E402
SOSLoRALayer, fixed_scales, replace_linear_with_sos, sos_lora_penalty,
)
torch.manual_seed(0)
fails: list[str] = []
def check(name: str, ok: bool, detail: str = "") -> None:
print(" [%s] %-46s %s" % ("ok " if ok else "FAIL", name, detail))
if not ok:
fails.append(name)
IN, OUT, R_TOT, K, ALPHA = 64, 128, 8, 4, 16.0
print("=" * 84)
print("1) 同预算:SOS-LoRA 与标准 LoRA 参数量一致")
print("=" * 84)
base = nn.Linear(IN, OUT, bias=False)
sos = SOSLoRALayer(base, r_tot=R_TOT, k=K, alpha=ALPHA)
n_sos = sos.n_trainable()
# 标准 LoRA @ r_tot:A(r,in) + B(out,r)
n_lora = R_TOT * IN + OUT * R_TOT
check("参数量一致", n_sos == n_lora, f"SOS {n_sos:,} LoRA {n_lora:,}")
print(" ", sos.describe())
print()
print("=" * 84)
print("2) 零初始更新:初始时 SOS 层输出 == 原层输出")
print("=" * 84)
x = torch.randn(3, IN)
with torch.no_grad():
d = (sos(x) - base(x)).abs().max().item()
check("初始 ΔW = 0", d == 0.0, f"max|Δ| = {d:.3e}")
print()
print("=" * 84)
print("3) 尺度:s_k = (α/r_tot)·γ_k,γ_k 在 [1, γ_max] 线性等距")
print("=" * 84)
expected = [(ALPHA / R_TOT) * (1.0 + (2.5 - 1.0) / (K - 1) * i) for i in range(K)]
check("s_k 数值", all(abs(a - b) < 1e-9 for a, b in zip(sos.scales, expected)),
f"{['%.3f' % s for s in sos.scales]} vs {['%.3f' % s for s in expected]}")
check("K=1 退化为 α/r", fixed_scales(8, 1, 16.0) == [16.0 / 8], str(fixed_scales(8, 1, 16.0)))
check("γ 最小 = 1(即 s_min = α/r_tot)", abs(sos.scales[0] - ALPHA / R_TOT) < 1e-9)
check("γ 最大 = 2.5", abs(sos.scales[-1] / (ALPHA / R_TOT) - 2.5) < 1e-9,
f"比值 {sos.scales[-1] / (ALPHA / R_TOT):.4f}")
print()
print("=" * 84)
print("4) 正交初始化:不同专家的 A 方向两两正交")
print("=" * 84)
worst = 0.0
for i in range(K):
for j in range(i + 1, K):
a, b = sos.A[i].reshape(-1), sos.A[j].reshape(-1)
cos = abs(float(a @ b) / (float(a.norm()) * float(b.norm()) + 1e-9))
worst = max(worst, cos)
check("跨专家 |cos| ≈ 0", worst < 1e-4, f"最大 |cos| = {worst:.3e}")
print()
print("=" * 84)
print("5) 合并等价:ΔW = Σ s_k A_k B_k")
print("=" * 84)
# ⚠️ 必须先 eval():训练模式下 self.dropout 是开的,sos(x) 用的是被 dropout 过的 x,
# 而 y_merged = x @ w_merged 用的是完整 x —— 两者本来就该不一样。
# 第一版测试漏了这一步,报了个假的 FAIL。
sos.eval()
with torch.no_grad():
for k in range(K): # 给 B 一些非零值,否则合并测不出东西
sos.B[k].normal_(0, 0.02)
w_merged = sos.merged_weight()
with torch.no_grad():
y_sos = sos(x)
y_merged = x @ w_merged.t()
d = (y_sos - y_merged).abs().max().item()
check("合并后输出一致", d < 1e-5, f"max|Δ| = {d:.3e}")
print()
print("=" * 84)
print("6) 梯度分档:每个专家收到的梯度幅度 ∝ s_k")
print("=" * 84)
# ⚠️ ∂L/∂A_k = s_k·Xᵀ(Δ B_kᵀ) —— 梯度**还取决于 B_k 的数值**。
# 要单独测出 s_k 这个乘子,必须让所有专家的 B_k 完全相同,
# 否则测到的是 s_k 与随机 B 的混合(第一版就踩了这个)。
with torch.no_grad():
b_shared = torch.randn(OUT, sos.r) * 0.02
for k in range(K):
sos.B[k].copy_(b_shared)
for p in sos.parameters():
p.grad = None
loss = sos(x).sum()
loss.backward()
gmags = [sos.A[k].grad.norm().item() for k in range(K)]
ratios = [g / max(gmags[0], 1e-12) for g in gmags]
scale_ratios = [s / sos.scales[0] for s in sos.scales]
check("梯度比 == 尺度比", all(abs(a - b) < 1e-6 for a, b in zip(ratios, scale_ratios)),
f"梯度比 {['%.4f' % r for r in ratios]} 尺度比 {['%.4f' % r for r in scale_ratios]}")
print()
print("=" * 84)
print("7) 正则:跨专家才罚,同专家内部不罚")
print("=" * 84)
# 把两个专家的 A 设成同一个方向 -> 惩罚必须 > 0
with torch.no_grad():
sos.A[1].copy_(sos.A[0])
pen_same = float(sos.ortho_penalty())
check("两个专家同方向 -> 惩罚 > 0", pen_same > 0.1, f"penalty = {pen_same:.4f}")
# 重置成正交 -> 惩罚应接近 0
sos.reset_parameters(ortho_init=True)
pen_ortho = float(sos.ortho_penalty())
check("正交时惩罚 ≈ 0", pen_ortho < 1e-6, f"penalty = {pen_ortho:.3e}")
# 单个专家的 A 内部:把同一专家的两行设成相同,惩罚不应变
sos2 = SOSLoRALayer(nn.Linear(IN, OUT, bias=False), r_tot=R_TOT, k=K, alpha=ALPHA)
with torch.no_grad():
sos2.A[0][1].copy_(sos2.A[0][0])
pen_inner = float(sos2.ortho_penalty())
check("同专家内部重复 -> 惩罚不变(不罚内部)", abs(pen_inner - pen_ortho) < 1e-6,
f"{pen_inner:.3e} vs {pen_ortho:.3e}")
print()
print("=" * 84)
print("8) 替换工具:确实替换到了目标层,且名字可核对")
print("=" * 84)
class Blk(nn.Module):
def __init__(self):
super().__init__()
self.q_proj = nn.Linear(IN, OUT, bias=False)
self.k_proj = nn.Linear(IN, OUT, bias=False)
self.mlp = nn.Linear(IN, OUT, bias=False)
self.unrelated = nn.Linear(IN, OUT, bias=False)
class M(nn.Module):
def __init__(self):
super().__init__()
self.layer0 = Blk()
self.layer1 = Blk()
m = M()
rep = replace_linear_with_sos(m, ("q_proj", "k_proj"), R_TOT, K, ALPHA)
check("替换到 4 个层", len(rep) == 4, str(rep))
check("是 SOSLoRALayer", isinstance(m.layer0.q_proj, SOSLoRALayer))
check("mlp 未被替换", isinstance(m.layer0.mlp, nn.Linear))
check("unrelated 未被替换", isinstance(m.layer0.unrelated, nn.Linear))
check("全局正则聚合可用", isinstance(sos_lora_penalty(m), torch.Tensor),
f"penalty = {float(sos_lora_penalty(m)):.4f}")
print()
print("=" * 84)
print("9) dtype 跟随底座(真模型跑 bfloat16,A/B 建错 dtype 会直接崩)")
print("=" * 84)
# 踩过的坑:A/B 默认 float32,Qwen 跑 bfloat16 ->
# "expected mat1 and mat2 to have the same dtype, but got: c10::BFloat16 != float"
# 单测当时用的是默认 dtype,所以没测出来。这里把它测死。
for dt in (torch.float32, torch.bfloat16, torch.float16):
b = nn.Linear(IN, OUT, bias=False).to(dt)
s = SOSLoRALayer(b, r_tot=R_TOT, k=K, alpha=ALPHA)
xd = torch.randn(2, IN).to(dt)
try:
y = s(xd)
ok = (y.dtype == dt) and all(p.dtype == dt for p in list(s.A) + list(s.B))
check(f"{str(dt):<16} 前向不崩且 dtype 一致", ok, f"out={y.dtype}")
except RuntimeError as e:
check(f"{str(dt):<16} 前向不崩且 dtype 一致", False, str(e)[:70])
print()
print("=" * 84)
if fails:
print("VERDICT: FAIL")
for f in fails:
print(" ✗", f)
else:
print("VERDICT: PASS —— 论文声称的 8 条性质全部成立")
print("=" * 84)
raise SystemExit(1 if fails else 0)
|