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 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
+ ]
@@ -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))
@@ -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
@@ -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}"
@@ -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
+ ]