AQIT 0.1.0
Loading...
Searching...
No Matches
weight_rank.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2# This file is part of the Aquin Engine. Unauthorized copying, modification,
3# distribution, or use of this file, via any medium, is strictly prohibited.
4# Proprietary and confidential. See LICENSE for terms.
5
6"""Stable rank analysis for transformer weight matrices."""
7from __future__ import annotations
8
9import re
10
11import torch
12
13
14_LAYER_RE = re.compile(r"(?:^|\.)(?:layers?|layer|block)\.?(\d+)(?:\.|$)", re.I)
15_EXPERT_WEIGHT_RE = re.compile(r"\.experts?\.\d+", re.I)
16
17
18def _is_heavy_weight_model(model) -> bool:
19 """Large / MoE models fill VRAM — weight analysis must run on CPU and skip experts."""
20 cfg = getattr(model, "cfg", None)
21 d_model = int(getattr(cfg, "d_model", 0) or 0)
22 if d_model >= 4096:
23 return True
24 n_layers = int(getattr(cfg, "n_layers", 0) or 0)
25 if d_model >= 2048 and n_layers >= 24:
26 return True
27 # Avoid iterating every MoE expert parameter — that can hang or exhaust host RAM.
28 return False
29
30
31def _should_skip_weight_param(name: str, *, heavy: bool) -> bool:
32 """Skip tensors that are redundant or too large for in-GPU analysis."""
33 if not heavy:
34 return False
35 kl = name.lower()
36 if _EXPERT_WEIGHT_RE.search(name):
37 return True
38 if "word_embeddings" in kl or kl.endswith("lm_head.weight"):
39 return True
40 if "router" in kl or "gate" in kl and "mlp" in kl:
41 return False # keep MoE router/gate — small and informative
42 return False
43
44
45def _layer_from_key(key: str) -> int:
46 m = _LAYER_RE.search(key.replace("_", "."))
47 return int(m.group(1)) if m else -1
48
50def _hf_matrix_label(key: str) -> str:
51 kl = key.lower()
52 if "self_attn" in kl or ".attn." in kl:
53 for tag, label in (
54 ("q_proj", "Q"),
55 ("k_proj", "K"),
56 ("v_proj", "V"),
57 ("o_proj", "O"),
58 ("out_proj", "O"),
59 ):
60 if tag in kl:
61 return label
62 if "mlp" in kl or "feed_forward" in kl:
63 if "gate" in kl:
64 return "MLP_gate"
65 if "down" in kl:
66 return "MLP_down"
67 if "up" in kl:
68 return "MLP_up"
69 return "MLP"
70 if "conv" in kl or "shortconv" in kl:
71 if "in_proj" in kl:
72 return "Conv_in"
73 if "out_proj" in kl:
74 return "Conv_out"
75 if re.search(r"\.conv\.|conv\d*\.weight$", kl):
76 return "Conv_dw"
77 for tag, label in (
78 ("q_proj", "Q"),
79 ("k_proj", "K"),
80 ("v_proj", "V"),
81 ("o_proj", "O"),
82 ("out_proj", "O"),
83 ("gate_proj", "MLP_gate"),
84 ("up_proj", "MLP_up"),
85 ("down_proj", "MLP_down"),
86 ("query_key_value", "QKV"),
87 ("c_attn", "Attn"),
88 ("c_fc", "MLP_up"),
89 ("c_proj", "MLP_down"),
90 ):
91 if tag in kl:
92 return label
93 parts = key.split(".")
94 return parts[-2] if len(parts) >= 2 else parts[-1]
95
96
97def _weight_root(model) -> object:
98 if hasattr(model, "hf_model"):
99 return model.hf_model
100 return model
102
103def _is_hf_weight_model(model) -> bool:
104 if hasattr(model, "hf_model"):
105 return True
106 if not hasattr(model, "blocks"):
107 return True
108 try:
109 block = model.blocks[0]
110 return not (hasattr(block, "attn") and hasattr(block.attn, "W_Q"))
111 except (IndexError, TypeError, AttributeError):
112 return True
113
114
115def _scalar(t: torch.Tensor) -> float:
116 return t.detach().float().reshape(-1)[0].item()
117
118
119def _to_2d(w: torch.Tensor) -> torch.Tensor:
120 """HookedTransformer attn weights are often 3D (n_heads, d_in, d_out)."""
121 f = w.detach().float()
122 if f.ndim == 1:
123 return f.unsqueeze(0)
124 if f.ndim == 2:
125 return f
126 return f.reshape(-1, f.shape[-1])
127
128
129def _matrix_ranks(w: torch.Tensor) -> tuple[float, float]:
130 # SVD on CPU — model may already occupy nearly all VRAM (e.g. Sarvam 30B MoE).
131 f = _to_2d(w).detach().float().cpu()
132 if f.numel() < 2:
133 return 1.0, 1.0
134 try:
135 sv = torch.linalg.svdvals(f)
136 except Exception:
137 return 0.0, 0.0
138 if sv.numel() == 0:
139 return 0.0, 0.0
140 s0 = _scalar(sv.reshape(-1)[0])
141 if s0 < 1e-12:
142 return 0.0, 0.0
143 ssum = _scalar(sv.sum())
144 s2sum = _scalar((sv ** 2).sum())
145 stable = (ssum ** 2) / max(s2sum, 1e-12)
146 nuclear_ratio = ssum / (s0 * sv.numel())
147 return round(stable, 4), round(nuclear_ratio, 4)
148
149
150def run_weight_rank_hf(model, collapse_threshold: float = 0.1) -> dict:
151 """Stable rank over HuggingFace parameter names (LFM, Sarvam, etc.)."""
152 root = _weight_root(model)
153 cfg = getattr(model, "cfg", None)
154 n_layers = int(getattr(cfg, "n_layers", 0) or 0)
155 heavy = _is_heavy_weight_model(model)
156 matrices: list[dict] = []
157 seen_layers: set[int] = set()
158
159 for name, param in root.named_parameters():
160 if param.dim() < 2 or param.numel() < 256:
161 continue
162 if _should_skip_weight_param(name, heavy=heavy):
163 continue
164 layer = _layer_from_key(name)
165 if layer < 0:
166 continue
167 seen_layers.add(layer)
168 matrix = _hf_matrix_label(name)
169 stable, nuclear = _matrix_ranks(param)
170 matrices.append({
171 "layer": layer,
172 "matrix": matrix,
173 "module": name,
174 "stable_rank": stable,
175 "nuclear_norm_ratio": nuclear,
176 "collapsed": stable < collapse_threshold,
177 })
178
179 if not n_layers and seen_layers:
180 n_layers = max(seen_layers) + 1
181
182 collapsed_count = sum(1 for m in matrices if m["collapsed"])
183 return {
184 "n_layers": n_layers,
185 "matrices": matrices,
186 "collapse_threshold": collapse_threshold,
187 "collapsed_count": collapsed_count,
188 "backend": "hf",
189 "heavy_model": heavy,
190 }
191
192
193def run_weight_rank_tl(model, collapse_threshold: float = 0.1) -> dict:
194 """Compute stable rank for Q/K/V/O and MLP matrices across all layers."""
195 n_layers = int(getattr(model.cfg, "n_layers", 0) or len(model.blocks))
196 matrices: list[dict] = []
198 for layer in range(n_layers):
199 block = model.blocks[layer]
200 attn = block.attn
201 mlp = block.mlp
202 specs: list[tuple[str, torch.Tensor]] = [
203 ("Q", attn.W_Q),
204 ("K", attn.W_K),
205 ("V", attn.W_V),
206 ("O", attn.W_O),
207 ("MLP_up", mlp.W_in),
208 ("MLP_down", mlp.W_out),
209 ]
210 if hasattr(mlp, "W_gate"):
211 specs.append(("MLP_gate", mlp.W_gate))
212
213 for matrix_name, weight in specs:
214 stable, nuclear = _matrix_ranks(weight.detach())
215 matrices.append({
216 "layer": layer,
217 "matrix": matrix_name,
218 "stable_rank": stable,
219 "nuclear_norm_ratio": nuclear,
220 "collapsed": stable < collapse_threshold,
221 })
222
223 collapsed_count = sum(1 for m in matrices if m["collapsed"])
224 return {
225 "n_layers": n_layers,
226 "matrices": matrices,
227 "collapse_threshold": collapse_threshold,
228 "collapsed_count": collapsed_count,
229 "backend": "transformer_lens",
230 }
231
232
233def run_weight_rank_analysis(model, collapse_threshold: float = 0.1) -> dict:
234 """Compute stable rank for attention/MLP (TL) or HF-native weights."""
235 if _is_hf_weight_model(model):
236 return run_weight_rank_hf(model, collapse_threshold=collapse_threshold)
237 return run_weight_rank_tl(model, collapse_threshold=collapse_threshold)
float _scalar(torch.Tensor t)
dict run_weight_rank_hf(model, float collapse_threshold=0.1)
dict run_weight_rank_analysis(model, float collapse_threshold=0.1)
dict run_weight_rank_tl(model, float collapse_threshold=0.1)
torch.Tensor _to_2d(torch.Tensor w)
int _layer_from_key(str key)
bool _is_heavy_weight_model(model)
tuple[float, float] _matrix_ranks(torch.Tensor w)
str _hf_matrix_label(str key)
bool _should_skip_weight_param(str name, *, bool heavy)
bool _is_hf_weight_model(model)