File size: 9,474 Bytes
9e551a1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# Tensor Roll — Recursive CUDA-Q Model Quantizer
# Copyright (C) 2026 SnapKitty Collective
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Representation search: 30B -> 8B (parameterized).

Pipeline:
  1. Per-tensor TensorRoll analysis (core.roll_tree) -> measured options.
  2. Global greedy selection under the hard parameter budget.
  3. Student *architecture* search over (d, heads, dff, layers): each
     candidate is scored by projected retained energy, where every teacher
     tensor is mapped to the candidate shape by truncated-SVD projection
     and its retained singular energy is measured. No hard-coded answer —
     the argmax under the budget wins.
  4. Student weight init from the teacher via the winning projections
     (Xavier where no projection exists), with per-tensor provenance.

The budget is P_target <= 8.5B for the real 30B path. At small scale the
same ratio applies: budget = teacher_params * (8.5/30).
"""

from __future__ import annotations

import itertools

import numpy as np

from . import core as C
from .model import Config, count_params, init_params
from . import qsim as Q

BUDGET_RATIO = 8.5 / 30.0  # hard architectural budget, same at every scale
ABS_BUDGET_CAP = 8_500_000_000


def _svd_cache(params: dict) -> dict:
    cache = {}
    for k, v in params.items():
        v = np.asarray(v, dtype=np.float64)
        if v.ndim == 2 and min(v.shape) > 1:
            U, s, Vt = np.linalg.svd(v, full_matrices=False)
            cache[k] = (U, s, Vt)
    return cache


def project_matrix(W: np.ndarray, out_shape: tuple, svd=None):
    """Truncated-SVD projection of W to out_shape (both dims <= input dims).
    Returns (W_proj, retained_energy_fraction). Optimal rank-k approximation,
    cropped — the classical baseline every exotic option is measured against.
    """
    W = np.asarray(W, dtype=np.float64)
    mo, no = out_shape
    m, n = W.shape
    assert mo <= m and no <= n, f"projection only shrinks: {W.shape} -> {out_shape}"
    if svd is None:
        U, s, Vt = np.linalg.svd(W, full_matrices=False)
    else:
        U, s, Vt = svd
    k = min(mo, no)
    Wp = (U[:mo, :k] * s[:k]) @ Vt[:k, :no]
    total = float((s ** 2).sum())
    energy = float((s[:k] ** 2).sum() / total) if total > 0 else 1.0
    return Wp.astype(np.float32), energy


def _candidate_configs(teacher_cfg: Config):
    """Small grid of student architectures to search (real 30B path uses a
    wider grid over the same code)."""
    ds = sorted({teacher_cfg.d // 2, max(8, teacher_cfg.d * 3 // 4)})
    hs = sorted({max(1, teacher_cfg.heads - 1), teacher_cfg.heads})
    dffs = sorted({teacher_cfg.dff // 2, max(16, teacher_cfg.dff * 3 // 4)})
    ls = sorted({max(1, teacher_cfg.layers - 1), teacher_cfg.layers})
    for d, h, dff, l in itertools.product(ds, hs, dffs, ls):
        if d % h == 0:
            yield Config(vocab=teacher_cfg.vocab, d=d, heads=h,
                         layers=l, dff=dff, seq=teacher_cfg.seq)


def _config_param_count(cfg: Config) -> int:
    # mirrors init_params structure without allocating
    n = cfg.vocab * cfg.d
    per_layer = (4 * cfg.d * cfg.d + 4 * cfg.d      # q,k,v,o + biases
                 + 4 * cfg.d                        # two layernorms
                 + 2 * cfg.d * cfg.dff + cfg.dff + cfg.d)  # mlp
    n += cfg.layers * per_layer
    n += 2 * cfg.d + cfg.d * cfg.vocab + cfg.vocab
    return n


def search_student_config(teacher_params: dict, teacher_cfg: Config,
                          budget_params: float):
    """Returns (best_cfg, scored_list). scored_list entries are
    (cfg, est_energy, param_count), sorted by energy desc."""
    svd = _svd_cache(teacher_params)
    scored = []
    for cfg in _candidate_configs(teacher_cfg):
        pc = _config_param_count(cfg)
        if pc > budget_params:
            continue
        energies, weights = [], []
        for k, v in teacher_params.items():
            v = np.asarray(v)
            if v.ndim != 2 or k not in svd:
                continue
            # map teacher key -> student key shape
            sk = _student_key(k, cfg, teacher_cfg)
            if sk is None:
                continue
            sshape = _student_shape(k, v.shape, cfg, teacher_cfg)
            if sshape is None or sshape[0] > v.shape[0] or sshape[1] > v.shape[1]:
                continue
            _, e = project_matrix(v, sshape, svd[k])
            energies.append(e)
            weights.append(v.size)
        est = float(np.average(energies, weights=weights)) if energies else 0.0
        scored.append({"cfg": cfg.to_dict(), "est_energy": est, "params": pc})
    scored.sort(key=lambda s: -s["est_energy"])
    if not scored:
        raise RuntimeError("no student config fits the budget; cannot proceed honestly")
    best = Config.from_dict(scored[0]["cfg"])
    return best, scored


def _student_key(k: str, cfg: Config, tcfg: Config):
    # student uses identical key names; layer count may shrink
    if k.startswith("L"):
        l = int(k[1:].split(".")[0])
        if l >= cfg.layers:
            return None
    return k


def _student_shape(k: str, tshape: tuple, cfg: Config, tcfg: Config):
    d, td = cfg.d, tcfg.d
    if k == "emb":
        return (cfg.vocab, d)
    if k in ("lnfg", "lnfb"):
        return (d,)
    if k == "Whead":
        return (d, cfg.vocab)
    if k == "bhead":
        return (cfg.vocab,)
    if k.startswith("L"):
        rest = k.split(".", 1)[1]
        if rest in ("ln1g", "ln1b", "ln2g", "ln2b"):
            return (d,)
        if rest in ("Wq", "Wk", "Wv", "Wo"):
            return (d, d)
        if rest in ("bq", "bk", "bv", "bo"):
            return (d,)
        if rest == "W1":
            return (d, cfg.dff)
        if rest == "b1":
            return (cfg.dff,)
        if rest == "W2":
            return (cfg.dff, d)
        if rest == "b2":
            return (d,)
    return None


def init_student_from_teacher(teacher_params: dict, teacher_cfg: Config,
                              student_cfg: Config, rng: np.random.Generator):
    """Project teacher weights into the student shapes; Xavier elsewhere.
    Returns (student_params, provenance, achieved_energies)."""
    student = init_params(rng, student_cfg)  # Xavier baseline everywhere
    svd = _svd_cache(teacher_params)
    provenance, energies = {}, {}
    for k, v in teacher_params.items():
        sk = _student_key(k, student_cfg, teacher_cfg)
        if sk is None or sk not in student:
            provenance[k] = "dropped-by-architecture-search"
            continue
        sv = student[sk]
        v = np.asarray(v)
        if (v.ndim == 2 and sv.ndim == 2 and k in svd
                and sv.shape[0] <= v.shape[0] and sv.shape[1] <= v.shape[1]):
            proj, e = project_matrix(v, sv.shape, svd[k])
            student[sk] = proj
            provenance[k] = f"truncated-svd-projection energy={e:.4f}"
            energies[k] = e
        elif v.shape == sv.shape:
            student[sk] = v.astype(np.float32)
            provenance[k] = "copied-exact"
            energies[k] = 1.0
        else:
            provenance[k] = "xavier-init(shape-mismatch)"
            energies[k] = 0.0
    return student, provenance, energies


def head_consolidation_report(teacher_params: dict, teacher_cfg: Config,
                              student_cfg: Config) -> dict:
    """Which attention heads survived consolidation, by measured spectral
    energy of each head's QK block."""
    report = {}
    for l in range(teacher_cfg.layers):
        if l >= student_cfg.layers:
            continue
        Wq = np.asarray(teacher_params[f"L{l}.Wq"], dtype=np.float64)
        dh = teacher_cfg.dh
        energies = []
        for h in range(teacher_cfg.heads):
            blk = Wq[:, h * dh:(h + 1) * dh]
            s = np.linalg.svd(blk, compute_uv=False)
            energies.append(float((s ** 2).sum()))
        order = sorted(range(len(energies)), key=lambda h: -energies[h])
        kept = sorted(order[:student_cfg.heads])
        report[f"layer_{l}"] = {
            "head_energies": [round(e, 4) for e in energies],
            "kept_heads": kept,
            "dropped_heads": sorted(order[student_cfg.heads:]),
        }
    return report


def run_roll_analysis(teacher_params: dict, *, depth: int = 3,
                      quantum_policy: str = "off", rank: int = 0,
                      stats: dict = None):
    """Run the first-class TensorRoll(T, axis, depth, rank, quantum_policy)
    operator over every teacher tensor. Returns (trees, leaves).

    `stats`: optional {tensor_name: {"act_rms", "grad_norm", "grad_rel"}}
    from model.measure_tensor_stats; attached to each leaf's metrics so the
    search sees measured activation/gradient contributions.
    """
    trees, leaves = {}, []
    for k in sorted(teacher_params.keys()):
        W = np.asarray(teacher_params[k])
        res = C.TensorRoll(W, 0, depth, rank, quantum_policy,
                           qsim_encode=Q.qsim_encode_for_roll
                           if quantum_policy != "off" else None,
                           name=k)
        trees[k] = res["tree"]
        for leaf in C.iter_leaves(res["tree"]):
            if stats and k in stats:
                leaf.metrics["act_rms"] = stats[k]["act_rms"]
                leaf.metrics["grad_norm"] = stats[k]["grad_norm"]
                leaf.metrics["grad_rel"] = stats[k]["grad_rel"]
            leaves.append(leaf)
    return trees, leaves