wiola 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wiola/__init__.py +47 -0
- wiola/components/__init__.py +21 -0
- wiola/components/atm.py +99 -0
- wiola/components/dsff.py +38 -0
- wiola/components/gcla.py +149 -0
- wiola/components/normalization.py +33 -0
- wiola/components/srpe.py +117 -0
- wiola/configuration_wiola.py +146 -0
- wiola/data/__init__.py +22 -0
- wiola/data/dataset.py +224 -0
- wiola/modeling_wiola.py +427 -0
- wiola/py.typed +0 -0
- wiola/utils.py +70 -0
- wiola-0.1.0.dist-info/METADATA +339 -0
- wiola-0.1.0.dist-info/RECORD +19 -0
- wiola-0.1.0.dist-info/WHEEL +5 -0
- wiola-0.1.0.dist-info/licenses/LICENSE +201 -0
- wiola-0.1.0.dist-info/licenses/NOTICE +7 -0
- wiola-0.1.0.dist-info/top_level.txt +1 -0
wiola/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Wiola: a small language model with SRPE, GCLA, ATM, DSFF and WiolaRMSNorm.
|
|
3
|
+
|
|
4
|
+
Importing this package registers Wiola with the HuggingFace Auto* classes so
|
|
5
|
+
that ``AutoModelForCausalLM.from_pretrained("oscowlai/wiola-360m")`` works once
|
|
6
|
+
weights are published.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .configuration_wiola import WiolaConfig
|
|
10
|
+
from .modeling_wiola import (
|
|
11
|
+
WiolaDecoderLayer,
|
|
12
|
+
WiolaForCausalLM,
|
|
13
|
+
WiolaModel,
|
|
14
|
+
WiolaPreTrainedModel,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"WiolaConfig",
|
|
21
|
+
"WiolaModel",
|
|
22
|
+
"WiolaForCausalLM",
|
|
23
|
+
"WiolaPreTrainedModel",
|
|
24
|
+
"WiolaDecoderLayer",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _register_auto_classes():
|
|
29
|
+
try:
|
|
30
|
+
from transformers import AutoConfig, AutoModel, AutoModelForCausalLM
|
|
31
|
+
except Exception: # transformers not installed
|
|
32
|
+
return
|
|
33
|
+
try:
|
|
34
|
+
AutoConfig.register("wiola", WiolaConfig)
|
|
35
|
+
AutoModel.register(WiolaConfig, WiolaModel)
|
|
36
|
+
AutoModelForCausalLM.register(WiolaConfig, WiolaForCausalLM)
|
|
37
|
+
except Exception:
|
|
38
|
+
# Already registered (e.g. re-import) — safe to ignore.
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Make the config/model loadable from the Hub with trust_remote_code too.
|
|
43
|
+
WiolaConfig.register_for_auto_class("AutoConfig")
|
|
44
|
+
WiolaModel.register_for_auto_class("AutoModel")
|
|
45
|
+
WiolaForCausalLM.register_for_auto_class("AutoModelForCausalLM")
|
|
46
|
+
|
|
47
|
+
_register_auto_classes()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Wiola architectural components."""
|
|
3
|
+
|
|
4
|
+
from .atm import merge_ratio, merge_tokens, unmerge_tokens
|
|
5
|
+
from .dsff import DualStreamFeedForward
|
|
6
|
+
from .gcla import GatedCrossLayerAttention, repeat_kv
|
|
7
|
+
from .normalization import WiolaRMSNorm
|
|
8
|
+
from .srpe import SpiralRotaryEmbedding, apply_srpe, rotate_half
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"WiolaRMSNorm",
|
|
12
|
+
"SpiralRotaryEmbedding",
|
|
13
|
+
"apply_srpe",
|
|
14
|
+
"rotate_half",
|
|
15
|
+
"GatedCrossLayerAttention",
|
|
16
|
+
"repeat_kv",
|
|
17
|
+
"merge_tokens",
|
|
18
|
+
"unmerge_tokens",
|
|
19
|
+
"merge_ratio",
|
|
20
|
+
"DualStreamFeedForward",
|
|
21
|
+
]
|
wiola/components/atm.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Adaptive Token Merging (ATM).
|
|
3
|
+
|
|
4
|
+
Implements Section VIII of the Wiola paper. Adjacent tokens whose cosine
|
|
5
|
+
similarity exceeds ``tau`` are greedily merged (averaged) in a left-to-right,
|
|
6
|
+
non-overlapping scan. A merge map records the source positions so the original
|
|
7
|
+
sequence length can be restored exactly after attention.
|
|
8
|
+
|
|
9
|
+
ATM is **training-only**: it is disabled at inference to keep the KV-cache
|
|
10
|
+
length consistent with the un-merged sequence.
|
|
11
|
+
|
|
12
|
+
Because sequences in a batch may merge by different amounts, merged sequences
|
|
13
|
+
are right-padded to the batch maximum and a boolean ``keep_mask`` marks the real
|
|
14
|
+
(non-padded) merged positions. Unmerge ignores padded slots.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import List, Tuple
|
|
18
|
+
|
|
19
|
+
import torch
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _greedy_merge_one(sim: torch.Tensor, threshold: float) -> List[Tuple[int, ...]]:
|
|
23
|
+
"""Greedy non-overlapping merge decisions for a single sequence.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
sim: cosine similarities between adjacent tokens, shape [T-1].
|
|
27
|
+
threshold: tau.
|
|
28
|
+
Returns:
|
|
29
|
+
A list of groups; each group is a tuple of 1 or 2 source indices.
|
|
30
|
+
"""
|
|
31
|
+
seq_len = sim.shape[0] + 1
|
|
32
|
+
groups: List[Tuple[int, ...]] = []
|
|
33
|
+
i = 0
|
|
34
|
+
while i < seq_len:
|
|
35
|
+
if i < seq_len - 1 and sim[i].item() > threshold:
|
|
36
|
+
groups.append((i, i + 1))
|
|
37
|
+
i += 2
|
|
38
|
+
else:
|
|
39
|
+
groups.append((i,))
|
|
40
|
+
i += 1
|
|
41
|
+
return groups
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def merge_tokens(hidden_states: torch.Tensor, threshold: float):
|
|
45
|
+
"""Merge adjacent redundant tokens.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
hidden_states: [B, T, d].
|
|
49
|
+
threshold: tau (cosine similarity merge threshold).
|
|
50
|
+
Returns:
|
|
51
|
+
merged: [B, T_prime_max, d] (right padded with zeros).
|
|
52
|
+
keep_mask: [B, T_prime_max] bool, True for real merged tokens.
|
|
53
|
+
merge_maps: list (len B) of lists of source-index tuples.
|
|
54
|
+
"""
|
|
55
|
+
bsz, seq_len, dim = hidden_states.shape
|
|
56
|
+
if seq_len < 2:
|
|
57
|
+
keep_mask = torch.ones(bsz, seq_len, dtype=torch.bool, device=hidden_states.device)
|
|
58
|
+
merge_maps = [[(t,) for t in range(seq_len)] for _ in range(bsz)]
|
|
59
|
+
return hidden_states, keep_mask, merge_maps
|
|
60
|
+
|
|
61
|
+
normed = torch.nn.functional.normalize(hidden_states, dim=-1, eps=1e-8)
|
|
62
|
+
# rho_t = <x_hat_t, x_hat_{t+1}> -> [B, T-1]
|
|
63
|
+
sim = (normed[:, :-1] * normed[:, 1:]).sum(-1)
|
|
64
|
+
|
|
65
|
+
merge_maps = [_greedy_merge_one(sim[b], threshold) for b in range(bsz)]
|
|
66
|
+
new_len = max(len(g) for g in merge_maps)
|
|
67
|
+
|
|
68
|
+
merged = hidden_states.new_zeros(bsz, new_len, dim)
|
|
69
|
+
keep_mask = torch.zeros(bsz, new_len, dtype=torch.bool, device=hidden_states.device)
|
|
70
|
+
for b, groups in enumerate(merge_maps):
|
|
71
|
+
for k, grp in enumerate(groups):
|
|
72
|
+
if len(grp) == 2:
|
|
73
|
+
merged[b, k] = 0.5 * (hidden_states[b, grp[0]] + hidden_states[b, grp[1]])
|
|
74
|
+
else:
|
|
75
|
+
merged[b, k] = hidden_states[b, grp[0]]
|
|
76
|
+
keep_mask[b, k] = True
|
|
77
|
+
return merged, keep_mask, merge_maps
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def unmerge_tokens(merged: torch.Tensor,
|
|
81
|
+
merge_maps: List[List[Tuple[int, ...]]],
|
|
82
|
+
original_len: int) -> torch.Tensor:
|
|
83
|
+
"""Restore original sequence length by broadcasting each merged token back
|
|
84
|
+
to its source positions (Eq. 31)."""
|
|
85
|
+
bsz, _, dim = merged.shape
|
|
86
|
+
out = merged.new_zeros(bsz, original_len, dim)
|
|
87
|
+
for b, groups in enumerate(merge_maps):
|
|
88
|
+
for k, grp in enumerate(groups):
|
|
89
|
+
for src in grp:
|
|
90
|
+
out[b, src] = merged[b, k]
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def merge_ratio(merge_maps: List[List[Tuple[int, ...]]], original_len: int) -> float:
|
|
95
|
+
"""Average merge ratio mu = 1 - T'/T across the batch."""
|
|
96
|
+
if original_len == 0:
|
|
97
|
+
return 0.0
|
|
98
|
+
ratios = [1.0 - len(g) / original_len for g in merge_maps]
|
|
99
|
+
return float(sum(ratios) / len(ratios))
|
wiola/components/dsff.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Dual-Stream Feed-Forward (DSFF).
|
|
3
|
+
|
|
4
|
+
Implements Section IX of the Wiola paper. Two parallel *dense* streams of
|
|
5
|
+
different widths and activations are fused by a learned per-dimension gate:
|
|
6
|
+
|
|
7
|
+
Stream A (narrow, SwiGLU): a = D_A( SiLU(G_A x) * (U_A x) )
|
|
8
|
+
Stream B (wide, GELU): b = D_B( GELU(U_B x) )
|
|
9
|
+
gate: alpha = sigmoid(W_f [a; b]) in (0,1)^d
|
|
10
|
+
output: alpha * a + (1 - alpha) * b
|
|
11
|
+
|
|
12
|
+
Setting ``W_f = 0`` yields ``alpha = 0.5``, reducing DSFF to a simple ensemble
|
|
13
|
+
average; DSFF therefore strictly generalises a two-stream ensemble.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import torch
|
|
17
|
+
import torch.nn as nn
|
|
18
|
+
import torch.nn.functional as F
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DualStreamFeedForward(nn.Module):
|
|
22
|
+
def __init__(self, hidden_size: int, narrow_size: int, wide_size: int):
|
|
23
|
+
super().__init__()
|
|
24
|
+
# Stream A: narrow SwiGLU.
|
|
25
|
+
self.gate_a = nn.Linear(hidden_size, narrow_size, bias=False) # G_A
|
|
26
|
+
self.up_a = nn.Linear(hidden_size, narrow_size, bias=False) # U_A
|
|
27
|
+
self.down_a = nn.Linear(narrow_size, hidden_size, bias=False) # D_A
|
|
28
|
+
# Stream B: wide GELU.
|
|
29
|
+
self.up_b = nn.Linear(hidden_size, wide_size, bias=False) # U_B
|
|
30
|
+
self.down_b = nn.Linear(wide_size, hidden_size, bias=False) # D_B
|
|
31
|
+
# Per-dimension fusion gate from concatenated stream outputs.
|
|
32
|
+
self.fusion = nn.Linear(2 * hidden_size, hidden_size, bias=False) # W_f
|
|
33
|
+
|
|
34
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
35
|
+
a = self.down_a(F.silu(self.gate_a(x)) * self.up_a(x))
|
|
36
|
+
b = self.down_b(F.gelu(self.up_b(x)))
|
|
37
|
+
alpha = torch.sigmoid(self.fusion(torch.cat((a, b), dim=-1)))
|
|
38
|
+
return alpha * a + (1.0 - alpha) * b
|
wiola/components/gcla.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Gated Cross-Layer Attention (GCLA).
|
|
3
|
+
|
|
4
|
+
Implements Section VII of the Wiola paper. Each decoder layer performs:
|
|
5
|
+
|
|
6
|
+
* GQA self-attention with SRPE on the local sequence, and
|
|
7
|
+
* cross-attention to compressed summaries of up to ``Lambda`` preceding
|
|
8
|
+
layers (supplied by the model as ``context_summaries``),
|
|
9
|
+
|
|
10
|
+
blended by a scalar gate ``beta = sigmoid(phi)`` and modulated by a sigmoid
|
|
11
|
+
output gate ``G``:
|
|
12
|
+
|
|
13
|
+
O = (1 - beta) * O_self + beta * O_ctx
|
|
14
|
+
A = (G * concat(O)) W_O
|
|
15
|
+
|
|
16
|
+
The context tensor is provided *per query position* as a causal cumulative mean
|
|
17
|
+
of prior-layer outputs (see :class:`WiolaModel`), so a cached incremental decode
|
|
18
|
+
reproduces a full forward pass exactly.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import math
|
|
22
|
+
from typing import Optional, Tuple
|
|
23
|
+
|
|
24
|
+
import torch
|
|
25
|
+
import torch.nn as nn
|
|
26
|
+
import torch.nn.functional as F
|
|
27
|
+
|
|
28
|
+
from .srpe import SpiralRotaryEmbedding, apply_srpe
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def repeat_kv(x: torch.Tensor, n_rep: int) -> torch.Tensor:
|
|
32
|
+
"""Expand [B, H_kv, T, d] -> [B, H_kv*n_rep, T, d] (GQA)."""
|
|
33
|
+
if n_rep == 1:
|
|
34
|
+
return x
|
|
35
|
+
b, kv, t, d = x.shape
|
|
36
|
+
x = x[:, :, None, :, :].expand(b, kv, n_rep, t, d)
|
|
37
|
+
return x.reshape(b, kv * n_rep, t, d)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GatedCrossLayerAttention(nn.Module):
|
|
41
|
+
def __init__(self, config, layer_idx: int):
|
|
42
|
+
super().__init__()
|
|
43
|
+
self.layer_idx = layer_idx
|
|
44
|
+
self.hidden_size = config.hidden_size
|
|
45
|
+
self.num_heads = config.num_attention_heads
|
|
46
|
+
self.num_kv_heads = config.num_key_value_heads
|
|
47
|
+
self.head_dim = config.head_dim
|
|
48
|
+
self.n_rep = self.num_heads // self.num_kv_heads
|
|
49
|
+
self.lookback = config.gcla_lookback
|
|
50
|
+
|
|
51
|
+
q_dim = self.num_heads * self.head_dim
|
|
52
|
+
kv_dim = self.num_kv_heads * self.head_dim
|
|
53
|
+
|
|
54
|
+
self.q_proj = nn.Linear(self.hidden_size, q_dim, bias=False)
|
|
55
|
+
self.k_proj = nn.Linear(self.hidden_size, kv_dim, bias=False)
|
|
56
|
+
self.v_proj = nn.Linear(self.hidden_size, kv_dim, bias=False)
|
|
57
|
+
self.o_proj = nn.Linear(q_dim, self.hidden_size, bias=False)
|
|
58
|
+
|
|
59
|
+
# Cross-layer context projections.
|
|
60
|
+
self.k_ctx_proj = nn.Linear(self.hidden_size, kv_dim, bias=False)
|
|
61
|
+
self.v_ctx_proj = nn.Linear(self.hidden_size, kv_dim, bias=False)
|
|
62
|
+
|
|
63
|
+
# Sigmoid output gate.
|
|
64
|
+
self.gate_proj = nn.Linear(self.hidden_size, q_dim, bias=False)
|
|
65
|
+
|
|
66
|
+
# Scalar blend gate beta = sigmoid(phi).
|
|
67
|
+
self.blend_logit = nn.Parameter(torch.tensor(float(config.gcla_gate_init)))
|
|
68
|
+
|
|
69
|
+
self.srpe = SpiralRotaryEmbedding(
|
|
70
|
+
head_dim=self.head_dim,
|
|
71
|
+
max_position_embeddings=config.max_position_embeddings,
|
|
72
|
+
theta=config.srpe_theta,
|
|
73
|
+
spiral_divisor=config.srpe_spiral_divisor,
|
|
74
|
+
radial_amplitude=config.srpe_radial_amplitude,
|
|
75
|
+
radial_frequency=config.srpe_radial_frequency,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def forward(
|
|
79
|
+
self,
|
|
80
|
+
hidden_states: torch.Tensor, # [B, T, d]
|
|
81
|
+
position_ids: torch.Tensor, # [B, T]
|
|
82
|
+
attention_mask: Optional[torch.Tensor], # [B, 1, T, T_k] additive
|
|
83
|
+
context_summaries: Optional[torch.Tensor] = None, # [B, T, Lambda, d]
|
|
84
|
+
past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
|
85
|
+
use_cache: bool = False,
|
|
86
|
+
):
|
|
87
|
+
bsz, q_len, _ = hidden_states.shape
|
|
88
|
+
|
|
89
|
+
q = self.q_proj(hidden_states)
|
|
90
|
+
k = self.k_proj(hidden_states)
|
|
91
|
+
v = self.v_proj(hidden_states)
|
|
92
|
+
|
|
93
|
+
q = q.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
|
|
94
|
+
k = k.view(bsz, q_len, self.num_kv_heads, self.head_dim).transpose(1, 2)
|
|
95
|
+
v = v.view(bsz, q_len, self.num_kv_heads, self.head_dim).transpose(1, 2)
|
|
96
|
+
|
|
97
|
+
# SRPE rotation on q/k (per head).
|
|
98
|
+
cos, sin = self.srpe(hidden_states, position_ids)
|
|
99
|
+
q, k = apply_srpe(q, k, cos, sin)
|
|
100
|
+
|
|
101
|
+
# Concatenate cached KV (incremental decoding).
|
|
102
|
+
# Concatenate cached KV (incremental decoding).
|
|
103
|
+
if (
|
|
104
|
+
past_key_value is not None
|
|
105
|
+
and past_key_value[0] is not None # ← guard against placeholder None
|
|
106
|
+
and past_key_value[1] is not None
|
|
107
|
+
):
|
|
108
|
+
past_k, past_v = past_key_value
|
|
109
|
+
k = torch.cat([past_k, k], dim=2)
|
|
110
|
+
v = torch.cat([past_v, v], dim=2)
|
|
111
|
+
present = (k, v) if use_cache else None
|
|
112
|
+
|
|
113
|
+
# GQA expansion.
|
|
114
|
+
k_rep = repeat_kv(k, self.n_rep)
|
|
115
|
+
v_rep = repeat_kv(v, self.n_rep)
|
|
116
|
+
|
|
117
|
+
scale = 1.0 / math.sqrt(self.head_dim)
|
|
118
|
+
scores = torch.matmul(q, k_rep.transpose(-1, -2)) * scale # [B,H,T,T_k]
|
|
119
|
+
if attention_mask is not None:
|
|
120
|
+
scores = scores + attention_mask
|
|
121
|
+
attn = F.softmax(scores, dim=-1, dtype=torch.float32).to(q.dtype)
|
|
122
|
+
o_self = torch.matmul(attn, v_rep) # [B,H,T,dh]
|
|
123
|
+
|
|
124
|
+
# Cross-layer context attention.
|
|
125
|
+
if context_summaries is not None and context_summaries.shape[2] > 0:
|
|
126
|
+
lam = context_summaries.shape[2]
|
|
127
|
+
k_ctx = self.k_ctx_proj(context_summaries) # [B,T,Lam,kv_dim]
|
|
128
|
+
v_ctx = self.v_ctx_proj(context_summaries)
|
|
129
|
+
k_ctx = k_ctx.view(bsz, q_len, lam, self.num_kv_heads, self.head_dim)
|
|
130
|
+
v_ctx = v_ctx.view(bsz, q_len, lam, self.num_kv_heads, self.head_dim)
|
|
131
|
+
# Expand kv heads to full head count.
|
|
132
|
+
k_ctx = k_ctx.repeat_interleave(self.n_rep, dim=3) # [B,T,Lam,H,dh]
|
|
133
|
+
v_ctx = v_ctx.repeat_interleave(self.n_rep, dim=3)
|
|
134
|
+
# scores[b,h,t,l] = q[b,h,t,:] . k_ctx[b,t,l,h,:]
|
|
135
|
+
ctx_scores = torch.einsum("bhtd,btlhd->bhtl", q, k_ctx) * scale
|
|
136
|
+
ctx_attn = F.softmax(ctx_scores, dim=-1, dtype=torch.float32).to(q.dtype)
|
|
137
|
+
o_ctx = torch.einsum("bhtl,btlhd->bhtd", ctx_attn, v_ctx)
|
|
138
|
+
beta = torch.sigmoid(self.blend_logit)
|
|
139
|
+
o = (1.0 - beta) * o_self + beta * o_ctx
|
|
140
|
+
else:
|
|
141
|
+
o = o_self
|
|
142
|
+
|
|
143
|
+
# Merge heads.
|
|
144
|
+
o = o.transpose(1, 2).contiguous().view(bsz, q_len, self.num_heads * self.head_dim)
|
|
145
|
+
|
|
146
|
+
# Sigmoid output gate.
|
|
147
|
+
g = torch.sigmoid(self.gate_proj(hidden_states))
|
|
148
|
+
o = self.o_proj(g * o)
|
|
149
|
+
return o, present
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""WiolaRMSNorm: RMS normalisation with a learned per-dimension input offset.
|
|
3
|
+
|
|
4
|
+
Implements Eq. (3) of the Wiola paper:
|
|
5
|
+
|
|
6
|
+
WiolaRMSNorm(x) = gamma * (x + delta) / sqrt(mean((x + delta)^2) + eps)
|
|
7
|
+
|
|
8
|
+
Setting ``delta = 0`` recovers standard RMSNorm exactly, so this strictly
|
|
9
|
+
generalises RMSNorm. The offset shifts the *input before* normalisation,
|
|
10
|
+
changing the normalisation target itself rather than adding a post-norm bias.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import torch
|
|
14
|
+
import torch.nn as nn
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WiolaRMSNorm(nn.Module):
|
|
18
|
+
def __init__(self, hidden_size: int, eps: float = 1e-6):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.weight = nn.Parameter(torch.ones(hidden_size)) # gamma
|
|
21
|
+
self.offset = nn.Parameter(torch.zeros(hidden_size)) # delta
|
|
22
|
+
self.variance_epsilon = eps
|
|
23
|
+
|
|
24
|
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
|
25
|
+
input_dtype = hidden_states.dtype
|
|
26
|
+
# Compute the normalisation in fp32 for numerical stability.
|
|
27
|
+
z = hidden_states.to(torch.float32) + self.offset.to(torch.float32)
|
|
28
|
+
variance = z.pow(2).mean(-1, keepdim=True)
|
|
29
|
+
z = z * torch.rsqrt(variance + self.variance_epsilon)
|
|
30
|
+
return (self.weight.to(torch.float32) * z).to(input_dtype)
|
|
31
|
+
|
|
32
|
+
def extra_repr(self) -> str:
|
|
33
|
+
return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}"
|
wiola/components/srpe.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Spiral Rotary Positional Encoding (SRPE).
|
|
3
|
+
|
|
4
|
+
Implements Section VI of the Wiola paper. For position ``p`` and dimension-pair
|
|
5
|
+
index ``j in [d_h/2]``:
|
|
6
|
+
|
|
7
|
+
omega_j = theta_0 ** (-2j / d_h)
|
|
8
|
+
Theta_j(p)= p * omega_j * (1 + 1/k_s)
|
|
9
|
+
r_j(p) = 1 + a_s * sin(p * f_s * omega_j)
|
|
10
|
+
c_j(p) = r_j(p) * cos(Theta_j(p))
|
|
11
|
+
s_j(p) = r_j(p) * sin(Theta_j(p))
|
|
12
|
+
|
|
13
|
+
and the rotation is applied to q/k with the standard rotate-half trick, which
|
|
14
|
+
is algebraically identical to Eqs. (12)-(13):
|
|
15
|
+
|
|
16
|
+
out_lo = q_lo * c - q_hi * s
|
|
17
|
+
out_hi = q_hi * c + q_lo * s
|
|
18
|
+
|
|
19
|
+
Unlike RoPE, ``Theta`` carries a second winding angle (via ``1 + 1/k_s``) and
|
|
20
|
+
the magnitude is modulated by a radial term ``r_j(p)``, embedding positions on a
|
|
21
|
+
3D helical manifold with no learned parameters.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import torch
|
|
25
|
+
import torch.nn as nn
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def rotate_half(x: torch.Tensor) -> torch.Tensor:
|
|
29
|
+
"""Rotate the two halves of the last dimension: [x1, x2] -> [-x2, x1]."""
|
|
30
|
+
half = x.shape[-1] // 2
|
|
31
|
+
x1 = x[..., :half]
|
|
32
|
+
x2 = x[..., half:]
|
|
33
|
+
return torch.cat((-x2, x1), dim=-1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SpiralRotaryEmbedding(nn.Module):
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
head_dim: int,
|
|
40
|
+
max_position_embeddings: int = 2048,
|
|
41
|
+
theta: float = 10000.0,
|
|
42
|
+
spiral_divisor: int = 8,
|
|
43
|
+
radial_amplitude: float = 0.1,
|
|
44
|
+
radial_frequency: float = 0.01,
|
|
45
|
+
):
|
|
46
|
+
super().__init__()
|
|
47
|
+
if head_dim % 2 != 0:
|
|
48
|
+
raise ValueError(f"head_dim must be even, got {head_dim}.")
|
|
49
|
+
self.head_dim = head_dim
|
|
50
|
+
self.max_position_embeddings = max_position_embeddings
|
|
51
|
+
self.theta = theta
|
|
52
|
+
self.k_s = spiral_divisor
|
|
53
|
+
self.a_s = radial_amplitude
|
|
54
|
+
self.f_s = radial_frequency
|
|
55
|
+
|
|
56
|
+
# omega_j = theta ** (-2j / d_h), j in [0, d_h/2).
|
|
57
|
+
j = torch.arange(0, head_dim, 2, dtype=torch.float32)
|
|
58
|
+
inv_freq = theta ** (-(j / head_dim)) # shape [d_h/2]
|
|
59
|
+
self.register_buffer("inv_freq", inv_freq, persistent=False)
|
|
60
|
+
|
|
61
|
+
self._cached_len = 0
|
|
62
|
+
self._build_cache(max_position_embeddings)
|
|
63
|
+
|
|
64
|
+
def _build_cache(self, seq_len: int, device=None, dtype=torch.float32):
|
|
65
|
+
positions = torch.arange(seq_len, dtype=torch.float32,
|
|
66
|
+
device=device if device is not None else self.inv_freq.device)
|
|
67
|
+
inv_freq = self.inv_freq.to(positions.device)
|
|
68
|
+
# Combined winding angle Theta_j(p) = p * omega_j * (1 + 1/k_s).
|
|
69
|
+
combined = 1.0 + 1.0 / self.k_s
|
|
70
|
+
angle = torch.outer(positions, inv_freq) * combined # [T, d_h/2]
|
|
71
|
+
# Radial modulation r_j(p) = 1 + a_s * sin(p * f_s * omega_j).
|
|
72
|
+
radial = 1.0 + self.a_s * torch.sin(
|
|
73
|
+
torch.outer(positions, inv_freq) * self.f_s
|
|
74
|
+
)
|
|
75
|
+
cos = radial * torch.cos(angle)
|
|
76
|
+
sin = radial * torch.sin(angle)
|
|
77
|
+
# Duplicate to full head_dim so it lines up with rotate_half.
|
|
78
|
+
cos = torch.cat((cos, cos), dim=-1) # [T, d_h]
|
|
79
|
+
sin = torch.cat((sin, sin), dim=-1)
|
|
80
|
+
self.register_buffer("cos_cached", cos.to(dtype), persistent=False)
|
|
81
|
+
self.register_buffer("sin_cached", sin.to(dtype), persistent=False)
|
|
82
|
+
self._cached_len = seq_len
|
|
83
|
+
|
|
84
|
+
@torch.no_grad()
|
|
85
|
+
def _maybe_extend(self, seq_len: int, device, dtype):
|
|
86
|
+
if seq_len > self._cached_len or self.cos_cached.device != device:
|
|
87
|
+
self._build_cache(max(seq_len, self._cached_len), device=device, dtype=dtype)
|
|
88
|
+
|
|
89
|
+
def forward(self, x: torch.Tensor, position_ids: torch.Tensor):
|
|
90
|
+
"""Return (cos, sin) gathered for ``position_ids``.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
x: any tensor only used to read dtype/device.
|
|
94
|
+
position_ids: LongTensor of shape [batch, seq_len].
|
|
95
|
+
Returns:
|
|
96
|
+
cos, sin each of shape [batch, seq_len, head_dim].
|
|
97
|
+
"""
|
|
98
|
+
max_pos = int(position_ids.max().item()) + 1
|
|
99
|
+
self._maybe_extend(max_pos, x.device, torch.float32)
|
|
100
|
+
cos = self.cos_cached[position_ids] # [B, S, d_h]
|
|
101
|
+
sin = self.sin_cached[position_ids]
|
|
102
|
+
return cos.to(x.dtype), sin.to(x.dtype)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def apply_srpe(q: torch.Tensor, k: torch.Tensor,
|
|
106
|
+
cos: torch.Tensor, sin: torch.Tensor):
|
|
107
|
+
"""Apply SRPE rotation to query and key tensors.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
q, k: shape [batch, num_heads, seq_len, head_dim].
|
|
111
|
+
cos, sin: shape [batch, seq_len, head_dim].
|
|
112
|
+
"""
|
|
113
|
+
cos = cos.unsqueeze(1) # [B, 1, S, d_h] -> broadcast over heads
|
|
114
|
+
sin = sin.unsqueeze(1)
|
|
115
|
+
q_rot = (q * cos) + (rotate_half(q) * sin)
|
|
116
|
+
k_rot = (k * cos) + (rotate_half(k) * sin)
|
|
117
|
+
return q_rot, k_rot
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
# Copyright 2025 The Wiola / OSCOWL-AI authors.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
4
|
+
"""Wiola model configuration."""
|
|
5
|
+
|
|
6
|
+
from transformers.configuration_utils import PretrainedConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WiolaConfig(PretrainedConfig):
|
|
10
|
+
r"""
|
|
11
|
+
Configuration class for a :class:`WiolaForCausalLM` model.
|
|
12
|
+
|
|
13
|
+
This stores every hyper-parameter described in the Wiola paper. Defaults
|
|
14
|
+
correspond to the **wiola-360m** variant. The four published sizes are
|
|
15
|
+
available as YAML files under ``configs/`` and as named presets via
|
|
16
|
+
:meth:`WiolaConfig.from_preset`.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
vocab_size (int): Vocabulary size of the BPE tokenizer.
|
|
20
|
+
hidden_size (int): Model hidden dimension ``d``.
|
|
21
|
+
num_hidden_layers (int): Number of decoder layers ``L``.
|
|
22
|
+
num_attention_heads (int): Number of query heads ``H``.
|
|
23
|
+
num_key_value_heads (int): Number of key/value heads ``H_kv`` (GQA).
|
|
24
|
+
max_position_embeddings (int): Maximum context length ``T``.
|
|
25
|
+
dsff_narrow_size (int): DSFF Stream A width ``d_A``.
|
|
26
|
+
dsff_wide_size (int): DSFF Stream B width ``d_B``.
|
|
27
|
+
srpe_theta (float): SRPE base theta ``theta_0``.
|
|
28
|
+
srpe_spiral_divisor (int): SRPE spiral divisor ``k_s``.
|
|
29
|
+
srpe_radial_amplitude (float): SRPE radial amplitude ``a_s``.
|
|
30
|
+
srpe_radial_frequency (float): SRPE radial frequency ``f_s``.
|
|
31
|
+
atm_threshold (float): ATM cosine-similarity merge threshold ``tau``.
|
|
32
|
+
atm_enabled (bool): Master switch for Adaptive Token Merging in training.
|
|
33
|
+
gcla_lookback (int): GCLA lookback depth ``Lambda``.
|
|
34
|
+
gcla_gate_init (float): Logit ``phi`` used to initialise the blend gate
|
|
35
|
+
``beta = sigmoid(phi)``.
|
|
36
|
+
rms_norm_eps (float): Epsilon for WiolaRMSNorm.
|
|
37
|
+
initializer_range (float): Stddev for truncated-normal init.
|
|
38
|
+
tie_word_embeddings (bool): Tie input embedding and LM head.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
model_type = "wiola"
|
|
42
|
+
keys_to_ignore_at_inference = ["past_key_values"]
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
vocab_size: int = 32000,
|
|
47
|
+
hidden_size: int = 1024,
|
|
48
|
+
num_hidden_layers: int = 16,
|
|
49
|
+
num_attention_heads: int = 16,
|
|
50
|
+
num_key_value_heads: int = 4,
|
|
51
|
+
max_position_embeddings: int = 2048,
|
|
52
|
+
dsff_narrow_size: int = 1024,
|
|
53
|
+
dsff_wide_size: int = 4096,
|
|
54
|
+
srpe_theta: float = 10000.0,
|
|
55
|
+
srpe_spiral_divisor: int = 8,
|
|
56
|
+
srpe_radial_amplitude: float = 0.1,
|
|
57
|
+
srpe_radial_frequency: float = 0.01,
|
|
58
|
+
atm_threshold: float = 0.92,
|
|
59
|
+
atm_enabled: bool = True,
|
|
60
|
+
gcla_lookback: int = 2,
|
|
61
|
+
gcla_gate_init: float = -3.0,
|
|
62
|
+
rms_norm_eps: float = 1e-6,
|
|
63
|
+
initializer_range: float = 0.02,
|
|
64
|
+
use_cache: bool = True,
|
|
65
|
+
pad_token_id: int = 0,
|
|
66
|
+
bos_token_id: int = 1,
|
|
67
|
+
eos_token_id: int = 2,
|
|
68
|
+
tie_word_embeddings: bool = True,
|
|
69
|
+
**kwargs,
|
|
70
|
+
):
|
|
71
|
+
self.vocab_size = vocab_size
|
|
72
|
+
self.hidden_size = hidden_size
|
|
73
|
+
self.num_hidden_layers = num_hidden_layers
|
|
74
|
+
self.num_attention_heads = num_attention_heads
|
|
75
|
+
# GQA: default num_key_value_heads to num_attention_heads (MHA) when unset.
|
|
76
|
+
if num_key_value_heads is None:
|
|
77
|
+
num_key_value_heads = num_attention_heads
|
|
78
|
+
self.num_key_value_heads = num_key_value_heads
|
|
79
|
+
self.max_position_embeddings = max_position_embeddings
|
|
80
|
+
|
|
81
|
+
self.dsff_narrow_size = dsff_narrow_size
|
|
82
|
+
self.dsff_wide_size = dsff_wide_size
|
|
83
|
+
|
|
84
|
+
self.srpe_theta = srpe_theta
|
|
85
|
+
self.srpe_spiral_divisor = srpe_spiral_divisor
|
|
86
|
+
self.srpe_radial_amplitude = srpe_radial_amplitude
|
|
87
|
+
self.srpe_radial_frequency = srpe_radial_frequency
|
|
88
|
+
|
|
89
|
+
self.atm_threshold = atm_threshold
|
|
90
|
+
self.atm_enabled = atm_enabled
|
|
91
|
+
|
|
92
|
+
self.gcla_lookback = gcla_lookback
|
|
93
|
+
self.gcla_gate_init = gcla_gate_init
|
|
94
|
+
|
|
95
|
+
self.rms_norm_eps = rms_norm_eps
|
|
96
|
+
self.initializer_range = initializer_range
|
|
97
|
+
self.use_cache = use_cache
|
|
98
|
+
|
|
99
|
+
if hidden_size % num_attention_heads != 0:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"hidden_size ({hidden_size}) must be divisible by "
|
|
102
|
+
f"num_attention_heads ({num_attention_heads})."
|
|
103
|
+
)
|
|
104
|
+
if num_attention_heads % num_key_value_heads != 0:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"num_attention_heads ({num_attention_heads}) must be divisible "
|
|
107
|
+
f"by num_key_value_heads ({num_key_value_heads})."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
super().__init__(
|
|
111
|
+
pad_token_id=pad_token_id,
|
|
112
|
+
bos_token_id=bos_token_id,
|
|
113
|
+
eos_token_id=eos_token_id,
|
|
114
|
+
tie_word_embeddings=tie_word_embeddings,
|
|
115
|
+
**kwargs,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def head_dim(self) -> int:
|
|
120
|
+
return self.hidden_size // self.num_attention_heads
|
|
121
|
+
|
|
122
|
+
# Convenience presets matching the paper's model family. -----------------
|
|
123
|
+
_PRESETS = {
|
|
124
|
+
"wiola-120m": dict(hidden_size=768, num_hidden_layers=12,
|
|
125
|
+
num_attention_heads=12, num_key_value_heads=4,
|
|
126
|
+
dsff_narrow_size=768, dsff_wide_size=3072),
|
|
127
|
+
"wiola-360m": dict(hidden_size=1024, num_hidden_layers=16,
|
|
128
|
+
num_attention_heads=16, num_key_value_heads=4,
|
|
129
|
+
dsff_narrow_size=1024, dsff_wide_size=4096),
|
|
130
|
+
"wiola-700m": dict(hidden_size=1536, num_hidden_layers=24,
|
|
131
|
+
num_attention_heads=16, num_key_value_heads=8,
|
|
132
|
+
dsff_narrow_size=1536, dsff_wide_size=6144),
|
|
133
|
+
"wiola-1.5b": dict(hidden_size=2048, num_hidden_layers=28,
|
|
134
|
+
num_attention_heads=16, num_key_value_heads=8,
|
|
135
|
+
dsff_narrow_size=2048, dsff_wide_size=8192),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_preset(cls, name: str, **overrides) -> "WiolaConfig":
|
|
140
|
+
if name not in cls._PRESETS:
|
|
141
|
+
raise KeyError(
|
|
142
|
+
f"Unknown preset '{name}'. Choose from {list(cls._PRESETS)}."
|
|
143
|
+
)
|
|
144
|
+
params = dict(cls._PRESETS[name])
|
|
145
|
+
params.update(overrides)
|
|
146
|
+
return cls(**params)
|
wiola/data/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Wiola data pipeline."""
|
|
3
|
+
|
|
4
|
+
from .dataset import (
|
|
5
|
+
SPECIAL_TOKENS,
|
|
6
|
+
PackedDatasetConfig,
|
|
7
|
+
PackedTextDataset,
|
|
8
|
+
TextSource,
|
|
9
|
+
build_tokenizer,
|
|
10
|
+
causal_collate,
|
|
11
|
+
load_or_build_tokenizer,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"TextSource",
|
|
16
|
+
"PackedTextDataset",
|
|
17
|
+
"PackedDatasetConfig",
|
|
18
|
+
"causal_collate",
|
|
19
|
+
"build_tokenizer",
|
|
20
|
+
"load_or_build_tokenizer",
|
|
21
|
+
"SPECIAL_TOKENS",
|
|
22
|
+
]
|