CoolFace
Modelpublic

Premchan369/Q-TensorFormer

sourceHugging Faceapache-2.0updated 9d agoView on Hugging Face
2likes185downloads
q_tensor_former.py568 linesDownload Raw Back to root
1#!/usr/bin/env python32"""3Q-TensorFormer: Quantum-Enhanced Tensor Network LLM Compression Engine4=======================================================================5Hybrid quantum-tensor transformer with:6  - Pure PyTorch Tensor-Train FFN layers (no compiled deps)7  - PennyLane quantum angle encoding with TorchLayer8  - Entanglement-guided adaptive rank scheduling9  - Selective quantum routing (only "hard" tokens)10  - Full benchmark against identical-architecture baseline11"""12 13import torch14import torch.nn as nn15import torch.nn.functional as F16import math, os17from typing import Optional, Tuple18from dataclasses import dataclass19 20try:21    import pennylane as qml22    HAS_PENNYLANE = True23except ImportError:24    qml = None25    HAS_PENNYLANE = False26 27print("=" * 65)28print(" Q-TENSORFORMER: Quantum-Tensor LLM Compressor")29print("=" * 65)30_qml_ver = qml.__version__ if HAS_PENNYLANE else "Unavailable (Classical Fallback)"31print(f" PyTorch {torch.__version__}  |  PennyLane {_qml_ver}")32print()33 34# ═════════════════════════════════════════════════════════════════════35# CONFIG36# ═════════════════════════════════════════════════════════════════════37 38@dataclass39class CFG:40    d_model: int = 6441    n_heads: int = 442    n_layers: int = 243    ff_multiplier: int = 444    max_seq_len: int = 6445    vocab_size: int = 100046    tt_rank: int = 847    tt_min_rank: int = 248    n_qubits: int = 449    n_quantum_layers: int = 250    quantum_sparsity: float = 0.351    dropout: float = 0.152    lr: float = 3e-453    rank_alpha: float = 2.054    rank_smoothing: float = 0.955 56    # Backward compatibility properties57    @property58    def vocab(self) -> int:59        return self.vocab_size60    @vocab.setter61    def vocab(self, v: int):62        self.vocab_size = v63 64    @property65    def max_seq(self) -> int:66        return self.max_seq_len67    @max_seq.setter68    def max_seq(self, v: int):69        self.max_seq_len = v70 71    @property72    def ff_mult(self) -> int:73        return self.ff_multiplier74    @ff_mult.setter75    def ff_mult(self, v: int):76        self.ff_multiplier = v77 78    @property79    def min_rank(self) -> int:80        return self.tt_min_rank81    @min_rank.setter82    def min_rank(self, v: int):83        self.tt_min_rank = v84 85    @property86    def q_qubits(self) -> int:87        return self.n_qubits88    @q_qubits.setter89    def q_qubits(self, v: int):90        self.n_qubits = v91 92    @property93    def q_layers(self) -> int:94        return self.n_quantum_layers95    @q_layers.setter96    def q_layers(self, v: int):97        self.n_quantum_layers = v98 99    @property100    def q_sparsity(self) -> float:101        return self.quantum_sparsity102    @q_sparsity.setter103    def q_sparsity(self, v: float):104        self.quantum_sparsity = v105 106# ═════════════════════════════════════════════════════════════════════107# 1. PURE PYTORCH TENSOR-TRAIN LINEAR LAYER108# ═════════════════════════════════════════════════════════════════════109 110def auto_factor(n, max_f=4):111    if n <= 1: return (1, 1)112    f, r = [], n113    for p in [2,2,2,2,2,3,3,5,7]:114        while r % p == 0 and len(f) < max_f:115            f.append(p); r //= p116    if r > 1:117        if len(f) < max_f: f.append(r)118        else: f[-1] *= r119    while len(f) < 2: f.insert(0, 1)120    return tuple(f[:max_f])121 122class TTLinear(nn.Module):123    """Tensor-Train decomposed linear layer. Pure PyTorch, zero compiled deps."""124    def __init__(self, in_shape, out_shape, rank=8, bias=True):125        super().__init__()126        in_shape = tuple(in_shape)127        out_shape = tuple(out_shape)128        max_d = max(len(in_shape), len(out_shape))129        in_shape = (1,) * (max_d - len(in_shape)) + in_shape130        out_shape = (1,) * (max_d - len(out_shape)) + out_shape131        assert len(in_shape) == len(out_shape)132        self.in_shape, self.out_shape = in_shape, out_shape133        self.rank, self.ndim = rank, len(in_shape)134        self.in_feat = math.prod(in_shape)135        self.out_feat = math.prod(out_shape)136        self.cores = nn.ParameterList()137        for k in range(self.ndim):138            rl = 1 if k == 0 else rank139            rr = 1 if k == self.ndim - 1 else rank140            c = torch.empty(rl, out_shape[k], in_shape[k], rr)141            bnd = math.sqrt(6.0 / max(1, rl*in_shape[k] + rr*out_shape[k]))142            nn.init.uniform_(c, -bnd, bnd)143            self.cores.append(c)144        self.bias = nn.Parameter(torch.zeros(self.out_feat)) if bias else None145        tp = sum(c.numel() for c in self.cores) + (self.bias.numel() if bias else 0)146        self.compr = (self.in_feat * self.out_feat) / max(tp, 1)147 148    def forward(self, x):149        bs = x.shape[:-1]150        B = math.prod(bs)151        x = x.reshape(B, self.in_feat)152        state = x.reshape(B, *self.in_shape)153 154        for k in range(self.ndim):155            core = self.cores[k]156            r_k, o_k, i_k, r_kp1 = core.shape157 158            if k == 0:159                rest = math.prod(self.in_shape[1:])160                s = state.reshape(B, i_k, rest)161                cm = core.squeeze(0).permute(1, 0, 2).reshape(i_k, o_k * r_kp1)162                s = torch.bmm(s.transpose(1, 2), cm.unsqueeze(0).expand(B, -1, -1))163                s = s.reshape(B, rest, o_k, r_kp1).permute(0, 3, 2, 1)164                state = s.reshape(B, r_kp1, -1)165 166            elif k == self.ndim - 1:167                prev_os = math.prod(self.out_shape[:k])168                s = state.reshape(B, r_k, prev_os, i_k)169                cm = core.squeeze(-1)170                s = torch.einsum('brpi,roi->bpo', s, cm)171                state = s.reshape(B, prev_os * o_k)172 173            else:174                prev_os = math.prod(self.out_shape[:k])175                rest_in = math.prod(self.in_shape[k+1:])176                s = state.reshape(B, r_k, prev_os * i_k * rest_in)177                s = s.reshape(B, r_k, prev_os, i_k, rest_in)178                s = torch.einsum('brpix,roiq->bpoqx', s, core)179                s = s.permute(0, 3, 1, 2, 4)180                state = s.reshape(B, r_kp1, prev_os * o_k * rest_in)181 182        out = state.reshape(B, self.out_feat)183        if self.bias is not None: out = out + self.bias184        return out.reshape(*bs, self.out_feat)185 186    def set_rank(self, nr):187        for i, c in enumerate(self.cores):188            s = [slice(None)]*4189            if i > 0: s[0] = slice(None, nr)190            if i < self.ndim - 1: s[3] = slice(None, nr)191            self.cores[i] = nn.Parameter(c[tuple(s)].clone())192 193# ═════════════════════════════════════════════════════════════════════194# 2. QUANTUM ANGLE EMBEDDING (PennyLane)195# ═════════════════════════════════════════════════════════════════════196 197class QuantumEmbed(nn.Module):198    """Angle embedding → variational circuit → PauliZ expectations."""199    def __init__(self, n_q=4, layers=2, n_out=None):200        super().__init__()201        self.n_q, self.layers = n_q, layers202        n_out = n_out or n_q203        if not HAS_PENNYLANE:204            self.qlayer = nn.Sequential(205                nn.Linear(n_q, layers * n_q),206                nn.Tanh(),207                nn.Linear(layers * n_q, n_out),208            )209        else:210            dev = qml.device("default.qubit", wires=n_q)211 212            @qml.qnode(dev, interface="torch", diff_method="backprop")213            def circ(inputs, w):214                for i in range(n_q): qml.RX(inputs[..., i], wires=i)215                for L in range(layers):216                    for i in range(n_q): qml.RY(w[L, i], wires=i)217                    for i in range(n_q-1): qml.CNOT(wires=[i, i+1])218                    if n_q > 2: qml.CNOT(wires=[n_q-1, 0])219                return [qml.expval(qml.PauliZ(i)) for i in range(n_out)]220 221            self.qlayer = qml.qnn.TorchLayer(circ, {"w": (layers, n_q)})222 223    def forward(self, x): return self.qlayer(x)224 225# ═════════════════════════════════════════════════════════════════════226# 3. TT FEED-FORWARD227# ═════════════════════════════════════════════════════════════════════228 229class TTFFN(nn.Module):230    def __init__(self, D, ff_mult=4, rank=8):231        super().__init__()232        E = D * ff_mult233        self.up = TTLinear(auto_factor(D), auto_factor(E), rank, True)234        self.down = TTLinear(auto_factor(E), auto_factor(D), rank, True)235    def forward(self, x): return self.down(F.gelu(self.up(x)))236    def set_rank(self, r): self.up.set_rank(r); self.down.set_rank(r)237 238# ═════════════════════════════════════════════════════════════════════239# 4. RANK SCHEDULER240# ═════════════════════════════════════════════════════════════════════241 242class RankScheduler(nn.Module):243    """rank = r_min + alpha * entropy (EMA-smoothed)"""244    def __init__(self, mn=2, mx=16, a=2.0, sm=0.9):245        super().__init__()246        self.mn, self.mx = mn, mx247        self.alpha = nn.Parameter(torch.tensor(a))248        self.sm = sm249        self.register_buffer('ema', torch.tensor(0.5))250        self.register_buffer('cur', torch.tensor(float(mx)))251    def forward(self, ent):252        s = ent.mean().detach() if ent.numel()>1 else ent.detach()253        self.ema = self.sm*self.ema + (1-self.sm)*s254        raw = self.mn + self.alpha*self.ema255        r = int(torch.clamp(raw, self.mn, self.mx).round().item())256        if self.training: self.cur.fill_(r)257        return r258    @property259    def current(self): return int(self.cur.item())260 261# ═════════════════════════════════════════════════════════════════════262# 5. QUANTUM ROUTER263# ═════════════════════════════════════════════════════════════════════264 265class QuantumRouter(nn.Module):266    """Learned gate: routes only hard tokens through quantum circuit."""267    def __init__(self, D, qmod, thr=0.5):268        super().__init__()269        self.qmod = qmod270        self.thr = thr271        self.gate = nn.Sequential(272            nn.Linear(D, D//4), nn.ReLU(), nn.Linear(D//4,1), nn.Sigmoid())273        self.register_buffer('tot', torch.tensor(0.0))274        self.register_buffer('qtok', torch.tensor(0.0))275    def forward(self, x):276        B,S,D = x.shape277        g = self.gate(x.reshape(-1,D)).squeeze(-1).reshape(B,S)278        m = (g > self.thr).float()279        if self.training:280            m = m.detach() + g - g.detach()281        xf = x.reshape(-1,D); mf = m.reshape(-1)282        sel = xf[mf > 0.5]; out = xf.clone()283        if sel.shape[0]>0:284            qo = self.qmod(sel)285            if qo.shape[-1]!=D:286                if not hasattr(self,'_proj'):287                    self._proj = nn.Linear(qo.shape[-1],D).to(x.device)288                qo = self._proj(qo)289            out[mf > 0.5] = qo.to(out.dtype)290        self.tot += B*S; self.qtok += m.sum()291        return out.reshape(B,S,D), g292    def sparsity(self):293        if self.tot>0: return 1.0-(self.qtok/self.tot).item()294        return 1.0295 296# ═════════════════════════════════════════════════════════════════════297# 6. ATTENTION298# ═════════════════════════════════════════════════════════════════════299 300class MHA(nn.Module):301    def __init__(self, D, heads=4, drop=0.1):302        super().__init__()303        assert D%heads==0304        self.h, self.hd = heads, D//heads305        self.scale = self.hd**-0.5306        self.qkv = nn.Linear(D, 3*D, bias=False)307        self.out = nn.Linear(D, D)308        self.drop = nn.Dropout(drop)309    def forward(self, x, mask=None):310        B,S,D = x.shape311        qkv = self.qkv(x).reshape(B,S,3,self.h,self.hd).permute(2,0,3,1,4)312        q,k,v = qkv[0], qkv[1], qkv[2]313        a = (q@k.transpose(-2,-1))*self.scale314        if mask is not None:315            a = a.masked_fill(mask[:,None,None,:]==0, float('-inf'))316        aw = F.softmax(a, dim=-1); aw = self.drop(aw)317        o = (aw@v).transpose(1,2).reshape(B,S,D)318        return self.out(o), aw319 320# ═════════════════════════════════════════════════════════════════════321# 7. HYBRID BLOCK322# ═════════════════════════════════════════════════════════════════════323 324class HybridBlock(nn.Module):325    def __init__(self, cfg):326        super().__init__()327        D = cfg.d_model328        self.a_norm = nn.LayerNorm(D)329        self.attn = MHA(D, cfg.n_heads, cfg.dropout)330        self.f_norm = nn.LayerNorm(D)331        ff_multiplier = getattr(cfg, "ff_multiplier", getattr(cfg, "ff_mult", 4))332        self.ffn = TTFFN(D, ff_multiplier, cfg.tt_rank)333        self.qrouter = None334        n_qubits = getattr(cfg, "n_qubits", getattr(cfg, "q_qubits", 4))335        n_quantum_layers = getattr(cfg, "n_quantum_layers", getattr(cfg, "q_layers", 2))336        if n_qubits:337            qc = QuantumEmbed(n_qubits, n_quantum_layers, n_qubits)338            qw = nn.Sequential(nn.Linear(D, n_qubits), qc)339            self.qrouter = QuantumRouter(D, qw)340        tt_min_rank = getattr(cfg, "tt_min_rank", getattr(cfg, "min_rank", 2))341        self.rs = RankScheduler(tt_min_rank, cfg.tt_rank, cfg.rank_alpha, cfg.rank_smoothing)342        self.drop = nn.Dropout(cfg.dropout)343    def forward(self, x, mask=None, adapt=True):344        ao, aw = self.attn(self.a_norm(x), mask)345        x = x + self.drop(ao)346        eps=1e-8347        ent = -torch.sum(aw*torch.log(aw+eps), dim=-1).mean(dim=-1).mean()348        tr = self.rs(ent) if adapt else self.rs.mx349        if adapt: self.ffn.set_rank(tr)350        n = self.f_norm(x)351        qs = 1.0352        if self.qrouter is not None:353            qo, _ = self.qrouter(n)354            n = n + self.drop(qo - n.detach() + n)355            qs = self.qrouter.sparsity()356        x = x + self.drop(self.ffn(n))357        return {'out':x, 'aw':aw, 'entropy':ent, 'rank':tr, 'qsparse':qs}358 359# ═════════════════════════════════════════════════════════════════════360# 8. Q-TENSORFORMER MODEL361# ═════════════════════════════════════════════════════════════════════362 363class QTensorFormer(nn.Module):364    def __init__(self, cfg):365        super().__init__()366        self.cfg = cfg367        vocab_size = getattr(cfg, "vocab_size", getattr(cfg, "vocab", 1000))368        max_seq_len = getattr(cfg, "max_seq_len", getattr(cfg, "max_seq", 64))369        self.tok = nn.Embedding(vocab_size, cfg.d_model)370        self.pos = nn.Parameter(torch.randn(1, max_seq_len, cfg.d_model)*0.02)371        self.layers = nn.ModuleList([HybridBlock(cfg) for _ in range(cfg.n_layers)])372        self.norm = nn.LayerNorm(cfg.d_model)373        self.head = nn.Linear(cfg.d_model, vocab_size, bias=False)374        self.head.weight = self.tok.weight375        self._init()376    def _init(self):377        for p in self.parameters():378            if p.dim()>=2: nn.init.xavier_uniform_(p)379    def forward(self, ids, mask=None, adapt=True):380        B,S = ids.shape381        x = self.tok(ids) + self.pos[:,:S,:]382        if mask is not None: mask = mask[:,None,None,:]383        bos = []384        for l in self.layers:385            o = l(x, mask, adapt); x=o['out']; bos.append(o)386        x = self.norm(x); logits = self.head(x)387        ent = torch.stack([b['entropy'] for b in bos]).mean()388        rk = sum(b['rank'] for b in bos)/len(bos)389        qs = sum(b['qsparse'] for b in bos)/len(bos)390        return {'logits':logits,'entropy':ent,'rank':rk,'qsparse':qs}391    def loss(self, ids, mask=None, labels=None):392        if labels is None: labels=ids.clone()393        out = self(ids, mask)394        sl = out['logits'][:,:-1].contiguous()395        ll = labels[:,1:].contiguous()396        vocab_size = getattr(self.cfg, "vocab_size", getattr(self.cfg, "vocab", 1000))397        l = F.cross_entropy(sl.reshape(-1, vocab_size), ll.reshape(-1), ignore_index=-100)398        return {'loss':l,'ppl':torch.exp(l),'entropy':out['entropy'],'rank':out['rank'],'qsparse':out['qsparse']}399    def nparams(self):400        t = sum(p.numel() for p in self.parameters())401        tr = sum(p.numel() for p in self.parameters() if p.requires_grad)402        return {'total':t,'trainable':tr}403 404# ═════════════════════════════════════════════════════════════════════405# 9. BASELINE (identical architecture, dense FFN)406# ═════════════════════════════════════════════════════════════════════407 408class Baseline(nn.Module):409    def __init__(self, cfg):410        super().__init__()411        self.cfg = cfg412        vocab_size = getattr(cfg, "vocab_size", getattr(cfg, "vocab", 1000))413        max_seq_len = getattr(cfg, "max_seq_len", getattr(cfg, "max_seq", 64))414        ff_multiplier = getattr(cfg, "ff_multiplier", getattr(cfg, "ff_mult", 4))415        self.tok = nn.Embedding(vocab_size, cfg.d_model)416        self.pos = nn.Parameter(torch.randn(1, max_seq_len, cfg.d_model)*0.02)417        self.drop = nn.Dropout(cfg.dropout)418        self.layers = nn.ModuleList()419        for _ in range(cfg.n_layers):420            self.layers.append(nn.ModuleDict({421                'a_n': nn.LayerNorm(cfg.d_model),422                'a': MHA(cfg.d_model, cfg.n_heads, cfg.dropout),423                'f_n': nn.LayerNorm(cfg.d_model),424                'ff': nn.Sequential(425                    nn.Linear(cfg.d_model, cfg.d_model*ff_multiplier),426                    nn.GELU(), nn.Dropout(cfg.dropout),427                    nn.Linear(cfg.d_model*ff_multiplier, cfg.d_model)),428            }))429        self.norm = nn.LayerNorm(cfg.d_model)430        self.head = nn.Linear(cfg.d_model, vocab_size, bias=False)431        self.head.weight = self.tok.weight432        self._init()433    def _init(self):434        for p in self.parameters():435            if p.dim()>=2: nn.init.xavier_uniform_(p)436    def forward(self, ids, mask=None):437        B,S = ids.shape438        x = self.tok(ids)+self.pos[:,:S,:]; x=self.drop(x)439        m = mask[:,None,None,:] if mask is not None else None440        for l in self.layers:441            ao,_ = l['a'](l['a_n'](x),m); x=x+self.drop(ao)442            x = x+self.drop(l['ff'](l['f_n'](x)))443        return {'logits':self.head(self.norm(x))}444    def loss(self, ids, mask=None, labels=None):445        if labels is None: labels=ids.clone()446        out = self(ids, mask)447        sl = out['logits'][:,:-1].contiguous()448        ll = labels[:,1:].contiguous()449        vocab_size = getattr(self.cfg, "vocab_size", getattr(self.cfg, "vocab", 1000))450        l = F.cross_entropy(sl.reshape(-1, vocab_size), ll.reshape(-1), ignore_index=-100)451        return {'loss':l,'ppl':torch.exp(l)}452    def nparams(self):453        t = sum(p.numel() for p in self.parameters())454        tr = sum(p.numel() for p in self.parameters() if p.requires_grad)455        return {'total':t,'trainable':tr}456 457# ═════════════════════════════════════════════════════════════════════458# 10. TRAINING UTILITIES459# ═════════════════════════════════════════════════════════════════════460 461def make_data(vocab=1000, seq=64, n=500, bs=16):462    d = torch.randint(1, vocab, (n, seq))463    ds = torch.utils.data.TensorDataset(d)464    return torch.utils.data.DataLoader(ds, batch_size=bs, shuffle=True,465        collate_fn=lambda batch: {'input_ids': torch.stack([item[0] for item in batch])})466 467def train_epoch(model, dl, opt, sched, e, tag="M"):468    model.train(); tl,tp,nb = 0.0,0.0,0; ex={}469    for b in dl:470        ids = b['input_ids']; m = b.get('attention_mask')471        opt.zero_grad()472        out = model.loss(ids, m); out['loss'].backward()473        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)474        opt.step()475        if sched: sched.step()476        tl += out['loss'].item(); tp += out['ppl'].item(); nb += 1477        for k in ['entropy','rank','qsparse']:478            if k in out: ex[k]=ex.get(k,0.0)+(out[k].item() if isinstance(out[k],torch.Tensor) else out[k])479    al,ap = tl/max(nb,1), tp/max(nb,1)480    s = f"[{tag}] E{e:2d}  loss={al:.4f}  ppl={ap:6.1f}"481    for k,v in ex.items(): s+=f"  {k}={v/max(nb,1):.3f}"482    print(s); return al,ap483 484@torch.no_grad()485def evaluate(model, dl):486    model.eval(); tl,tp,nb=0.0,0.0,0487    for b in dl:488        ids=b['input_ids']; m=b.get('attention_mask')489        out=model.loss(ids,m); tl+=out['loss'].item(); tp+=out['ppl'].item(); nb+=1490    return tl/max(nb,1), tp/max(nb,1)491 492# ═════════════════════════════════════════════════════════════════════493# 11. MAIN BENCHMARK494# ═════════════════════════════════════════════════════════════════════495 496def main():497    torch.manual_seed(42)498    cfg = CFG(d_model=64, n_layers=2, n_heads=4, tt_rank=8,499              q_qubits=4, q_sparsity=0.3, vocab=1000, max_seq=64)500 501    print(f"Config: d={cfg.d_model} layers={cfg.n_layers} heads={cfg.n_heads} rank={cfg.tt_rank}")502    print(f"Quantum: qubits={cfg.q_qubits} sparsity={cfg.q_sparsity}")503    print(f"Tensor FFN: ON\n")504 505    qt = QTensorFormer(cfg)506    bl = Baseline(cfg)507 508    pq = qt.nparams(); pb = bl.nparams()509    print(f"Q-TensorFormer params: {pq['trainable']:>10,}")510    print(f"Baseline params:       {pb['trainable']:>10,}")511    print(f"Compression ratio:     {pb['trainable']/max(pq['trainable'],1):>10.1f}x\n")512 513    train_dl = make_data(cfg.vocab, cfg.max_seq, 500, 16)514    val_dl   = make_data(cfg.vocab, cfg.max_seq, 100, 16)515    E = 8516 517    print("=" * 50)518    print(" TRAINING Q-TENSORFORMER")519    print("=" * 50)520    oq = torch.optim.AdamW(qt.parameters(), lr=cfg.lr)521    sq = torch.optim.lr_scheduler.CosineAnnealingLR(oq, E*len(train_dl))522    for e in range(1, E+1): train_epoch(qt, train_dl, oq, sq, e, "Q-TF")523 524    print("\n" + "=" * 50)525    print(" TRAINING BASELINE")526    print("=" * 50)527    ob = torch.optim.AdamW(bl.parameters(), lr=cfg.lr)528    sb = torch.optim.lr_scheduler.CosineAnnealingLR(ob, E*len(train_dl))529    for e in range(1, E+1): train_epoch(bl, train_dl, ob, sb, e, "BSL")530 531    ql,qp = evaluate(qt, val_dl)532    bl_val,bp = evaluate(bl, val_dl)533 534    torch.save(qt.state_dict(), '/tmp/qt.pt')535    torch.save(bl.state_dict(), '/tmp/bl.pt')536    qsz = os.path.getsize('/tmp/qt.pt')/(1024*1024)537    bsz = os.path.getsize('/tmp/bl.pt')/(1024*1024)538 539    print("\n" + "=" * 65)540    print(" RESULTS")541    print("=" * 65)542    print(f"{'Metric':<30} {'Q-TensorFormer':>15} {'Baseline':>15}")543    print("-" * 60)544    print(f"{'Parameters':<30} {pq['trainable']:>13,}  {pb['trainable']:>13,}")545    print(f"{'Val Loss':<30} {ql:>15.4f} {bl_val:>15.4f}")546    print(f"{'Val Perplexity':<30} {qp:>15.2f} {bp:>15.2f}")547    print(f"{'Model Size (MB)':<30} {qsz:>15.1f} {bsz:>15.1f}")548 549    ps = (1-pq['trainable']/pb['trainable'])*100550    ss = (1-qsz/bsz)*100551    pr = qp/bp552    print(f"\nParameter reduction: {ps:.1f}%")553    print(f"Size reduction:      {ss:.1f}%")554    print(f"PPL ratio (Q-TF/BL): {pr:.2f}x")555 556    if pr < 1.1:557        print(f"\n  >> VERDICT: Significant compression with minimal quality loss! <<")558    elif pr < 1.3:559        print(f"\n  >> VERDICT: Moderate trade-off — compression worth the cost <<")560    else:561        print(f"\n  >> VERDICT: Quality gap too large, needs tuning <<")562 563    print("\nDone!")564    return {'params_q':pq['trainable'],'params_b':pb['trainable'],'qloss':ql,'qppl':qp,'bloss':bl_val,'bppl':bp,'qsz':qsz,'bsz':bsz,'comp':ps,'sred':ss,'ppl_ratio':pr}565 566if __name__ == '__main__':567    results = main()568