Upload vlib/della.py with huggingface_hub
Browse files- vlib/della.py +176 -0
vlib/della.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DELLA magnitude pruning + task-vector combine over voice head tensors.
|
| 2 |
+
|
| 3 |
+
Clean-room port of the DELLA spec (arXiv:2406.11617) in numpy. No mergekit
|
| 4 |
+
dependency, no vendored code: mergekit is LGPL-3.0 and the authors' snapshot
|
| 5 |
+
carries no license grant, so both served as read-only references only.
|
| 6 |
+
|
| 7 |
+
One deliberate fork from upstream mergekit's ``della_magprune``, recorded
|
| 8 |
+
here rather than collapsed: upstream rescales L1 against the whole-tensor
|
| 9 |
+
sum, while ``magprune`` rescales each row to its own pre-prune ``sum|x|``.
|
| 10 |
+
Per-row keeps every output neuron's scale (each head row is one token's
|
| 11 |
+
direction), stays bounded at aggressive sparsity, and keeps row-blocking
|
| 12 |
+
exactly correct (a global rescale would need the full tensor at once).
|
| 13 |
+
Equivalence spot-checks against mergekit therefore expect "close", not
|
| 14 |
+
bit-exact: same family, different rescale locality.
|
| 15 |
+
|
| 16 |
+
Determinism: ``seed`` spawns one child stream per voice
|
| 17 |
+
(``SeedSequence(seed)``), so a seeded run is bit-identical regardless of
|
| 18 |
+
``block_rows`` — each voice's Bernoulli draws are consumed in row order on
|
| 19 |
+
its own stream. ``seed=None`` gives fresh entropy per voice
|
| 20 |
+
(nondeterministic, as upstream).
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
VALID_RESCALES = ("l1", "inv_p")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _validate_density_epsilon(density, epsilon):
|
| 27 |
+
if density + epsilon >= 1 or density - epsilon <= 0:
|
| 28 |
+
raise ValueError(
|
| 29 |
+
"della density±epsilon must stay in (0,1): "
|
| 30 |
+
f"got density={density} epsilon={epsilon}"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _check_rescale(rescale):
|
| 35 |
+
if rescale not in VALID_RESCALES:
|
| 36 |
+
raise ValueError(f"della rescale must be 'l1' or 'inv_p', got {rescale!r}")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def magprune(delta, density=0.5, epsilon=0.1, rescale="l1", rng=None):
|
| 40 |
+
"""Rank-weighted Bernoulli prune of one delta, row-wise. Always F32 out.
|
| 41 |
+
|
| 42 |
+
Keep-probability ramps linearly with within-row magnitude rank, from
|
| 43 |
+
``density-epsilon`` (smallest) to ``density+epsilon`` (largest), then a
|
| 44 |
+
seeded Bernoulli draw decides survivors. ``rescale="l1"`` restores each
|
| 45 |
+
surviving row's ``sum|x|``; ``rescale="inv_p"`` divides each survivor by
|
| 46 |
+
its own keep-prob (paper-faithful, heavy-tailed).
|
| 47 |
+
"""
|
| 48 |
+
import numpy as np
|
| 49 |
+
F32 = np.float32
|
| 50 |
+
_check_rescale(rescale)
|
| 51 |
+
a = np.asarray(delta, dtype=F32)
|
| 52 |
+
if density >= 1:
|
| 53 |
+
return a
|
| 54 |
+
if density <= 0:
|
| 55 |
+
return np.zeros_like(a)
|
| 56 |
+
_validate_density_epsilon(density, epsilon)
|
| 57 |
+
orig_shape = a.shape
|
| 58 |
+
w = np.atleast_2d(a)
|
| 59 |
+
order = np.argsort(np.abs(w), axis=-1, kind="stable")
|
| 60 |
+
ranks = np.argsort(order, axis=-1).astype(F32) + F32(1.0)
|
| 61 |
+
rmin = ranks.min(axis=-1, keepdims=True)
|
| 62 |
+
rmax = ranks.max(axis=-1, keepdims=True)
|
| 63 |
+
denom = rmax - rmin
|
| 64 |
+
# Single-column rows have no ordering; fall back to flat density.
|
| 65 |
+
rank_norm = np.divide(ranks - rmin, denom,
|
| 66 |
+
out=np.full_like(ranks, F32(0.5)),
|
| 67 |
+
where=denom > 0)
|
| 68 |
+
rank_norm = np.clip(rank_norm, F32(0.0), F32(1.0))
|
| 69 |
+
d = F32(density)
|
| 70 |
+
e = F32(epsilon)
|
| 71 |
+
probs = (d - e) + rank_norm * (F32(2.0) * e)
|
| 72 |
+
if rng is None:
|
| 73 |
+
rng = np.random.default_rng()
|
| 74 |
+
mask = rng.binomial(1, probs).astype(F32)
|
| 75 |
+
masked = w * mask
|
| 76 |
+
if rescale == "l1":
|
| 77 |
+
before = np.abs(w).sum(axis=-1, keepdims=True)
|
| 78 |
+
after = np.abs(masked).sum(axis=-1, keepdims=True)
|
| 79 |
+
ok = (before >= 1e-7) & (after >= 1e-7)
|
| 80 |
+
scale = np.where(ok, before / np.maximum(after, F32(1e-30)), F32(1.0))
|
| 81 |
+
out = masked * scale
|
| 82 |
+
else: # inv_p
|
| 83 |
+
out = masked / probs
|
| 84 |
+
if not np.all(np.isfinite(out)):
|
| 85 |
+
raise ValueError("della inv_p rescale produced NaN/Inf "
|
| 86 |
+
"- refusing to write a corrupt voice")
|
| 87 |
+
return np.asarray(out.reshape(orig_shape), dtype=F32)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def combine(base, deltas, weights, lambda_=1.0, normalize=True):
|
| 91 |
+
"""TIES-sum combine of pruned deltas onto base. F32 in, F32 out.
|
| 92 |
+
|
| 93 |
+
Weight-scale, elect the majority sign per position (ties go positive),
|
| 94 |
+
average the elected (``normalize``) or sum them, scale by ``lambda_``,
|
| 95 |
+
add base. Positions every voice pruned divide by 1, not 0.
|
| 96 |
+
"""
|
| 97 |
+
import numpy as np
|
| 98 |
+
F32 = np.float32
|
| 99 |
+
b = np.asarray(base, dtype=F32)
|
| 100 |
+
ds = [np.asarray(d, dtype=F32) for d in deltas]
|
| 101 |
+
if not ds:
|
| 102 |
+
return b
|
| 103 |
+
if len(weights) != len(ds):
|
| 104 |
+
raise ValueError(f"della weights ({len(weights)}) != deltas ({len(ds)})")
|
| 105 |
+
for i, d in enumerate(ds):
|
| 106 |
+
if d.shape != b.shape:
|
| 107 |
+
raise ValueError(f"della delta {i} shape {d.shape} != base {b.shape}")
|
| 108 |
+
wcol = np.asarray(list(weights), dtype=F32).reshape((-1,) + (1,) * b.ndim)
|
| 109 |
+
W = np.stack(ds).astype(F32, copy=False) * wcol
|
| 110 |
+
majority = np.where(W.sum(axis=0) >= 0, F32(1.0), F32(-1.0))
|
| 111 |
+
elected = np.sign(W) == majority
|
| 112 |
+
mixed = np.where(elected, W, F32(0.0)).sum(axis=0)
|
| 113 |
+
divisor = np.where(elected, wcol, F32(0.0)).sum(axis=0)
|
| 114 |
+
divisor = np.where(divisor == 0, F32(1.0), divisor)
|
| 115 |
+
if normalize:
|
| 116 |
+
mixed = mixed / divisor
|
| 117 |
+
if lambda_ != 1:
|
| 118 |
+
mixed = mixed * F32(lambda_)
|
| 119 |
+
return b + mixed
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def merge(base, voices, weights=None, density=0.5, epsilon=0.1, lambda_=1.0,
|
| 123 |
+
rescale="l1", normalize=True, seed=None, block_rows=4096):
|
| 124 |
+
"""Stream one DELLA merge: per-block (voice-base) prune, then combine.
|
| 125 |
+
|
| 126 |
+
Only ``block_rows`` rows per voice are pruned/combined at once, so the
|
| 127 |
+
full ``n_voices x rows x cols`` delta stack is never materialized; peak
|
| 128 |
+
besides the inputs/output is ``O(block_rows x cols x n_voices)``.
|
| 129 |
+
Blocking is exactly correct: pruning ranks within rows and election
|
| 130 |
+
votes per position, neither crosses a block boundary. Each voice draws
|
| 131 |
+
on its own ``SeedSequence(seed)`` child stream, so repartitioning blocks
|
| 132 |
+
never changes the result. Output is cast back to the base input's dtype.
|
| 133 |
+
"""
|
| 134 |
+
import numpy as np
|
| 135 |
+
F32 = np.float32
|
| 136 |
+
_check_rescale(rescale)
|
| 137 |
+
b = np.asarray(base)
|
| 138 |
+
out_dtype = b.dtype
|
| 139 |
+
bw = np.asarray(b, dtype=F32)
|
| 140 |
+
vs = [np.asarray(v) for v in voices]
|
| 141 |
+
if not vs:
|
| 142 |
+
return np.asarray(b, dtype=out_dtype)
|
| 143 |
+
wts = [1.0] * len(vs) if weights is None else list(weights)
|
| 144 |
+
if len(wts) != len(vs):
|
| 145 |
+
raise ValueError(f"della weights ({len(wts)}) != voices ({len(vs)})")
|
| 146 |
+
for i, v in enumerate(vs):
|
| 147 |
+
if v.shape != bw.shape:
|
| 148 |
+
raise ValueError(f"della voice {i} shape {v.shape} != base {bw.shape}")
|
| 149 |
+
if not (density >= 1 or density <= 0):
|
| 150 |
+
_validate_density_epsilon(density, epsilon)
|
| 151 |
+
squeeze = False
|
| 152 |
+
if bw.ndim == 1:
|
| 153 |
+
squeeze = True
|
| 154 |
+
bw = bw[None, :]
|
| 155 |
+
vs = [v[None, :] for v in vs]
|
| 156 |
+
if bw.ndim < 1:
|
| 157 |
+
raise ValueError("della merge needs at least 1-D tensors")
|
| 158 |
+
if seed is None:
|
| 159 |
+
rngs = [np.random.default_rng() for _ in vs]
|
| 160 |
+
else:
|
| 161 |
+
rngs = [np.random.default_rng(s)
|
| 162 |
+
for s in np.random.SeedSequence(seed).spawn(len(vs))]
|
| 163 |
+
vw = [np.asarray(v, dtype=F32) for v in vs]
|
| 164 |
+
out = np.empty_like(bw)
|
| 165 |
+
for start in range(0, bw.shape[0], block_rows):
|
| 166 |
+
sl = slice(start, min(start + block_rows, bw.shape[0]))
|
| 167 |
+
bb = bw[sl]
|
| 168 |
+
pruned = [magprune(v[sl] - bb, density=density, epsilon=epsilon,
|
| 169 |
+
rescale=rescale, rng=rngs[vi])
|
| 170 |
+
for vi, v in enumerate(vw)]
|
| 171 |
+
out[sl] = combine(bb, pruned, wts, lambda_=lambda_, normalize=normalize)
|
| 172 |
+
if squeeze:
|
| 173 |
+
out = out.reshape(b.shape)
|
| 174 |
+
if out_dtype != F32:
|
| 175 |
+
return out.astype(out_dtype, copy=False)
|
| 176 |
+
return out
|