9was9 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.
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: 9was9
3
+ Version: 0.1.0
4
+ Summary: Универсальный ускоритель numpy-вычислений (KV-кэш, RoPE, fused linear).
5
+ Author-email: CharmLxt <reception.mailspypi@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/CharmLxt/9was9
8
+ Project-URL: Source, https://github.com/CharmLxt/9was9
9
+ Keywords: numpy,acceleration,kv-cache,rope,llm,inference
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: numpy>=1.21.0
18
+
19
+ 9was9. Версия 0.1.0. Автор CharmLxt (PyPI: CharmLxt, email: reception.mailspypi@gmail.com). Ссылка на GitHub: https://github.com/CharmLxt/9was9.
20
+
21
+
22
+
23
+ Важно про имя пакета. Установка производится командой pip install 9was9 (так пакет называется на PyPI), но импортировать его нужно как ninewas9, потому что имена Python-модулей не могут начинаться с цифры. Пример: from ninewas9 import KVCache, RoPECache, FusedLinear, fast\_softmax.
24
+
25
+
26
+
27
+ 9was9 — это универсальная библиотека-ускоритель для numpy-вычислений, созданная в первую очередь для нейросетевого инференса, но применимая в любом проекте, где важна скорость работы с массивами. В основе лежит несколько идей: предвыделение памяти вместо повторяющихся аллокаций, кэширование часто используемых таблиц, объединение нескольких матричных умножений в одно и численно стабильные реализации классических операций.
28
+
29
+
30
+
31
+ Первая версия содержит пять групп функций. KVCache — это менеджер памяти для ключей и значений в трансформерах, который заранее выделяет буфер максимального размера и записывает новые значения в срез, избегая np.concatenate на каждом токене. Это уменьшает сложность вставки с O(n) до O(1) и убирает до 20% накладных расходов на инференс. RoPECache предвычисляет sin/cos таблицы для Rotary Position Embeddings с поддержкой нескольких типов масштабирования (linear, dynamic, llama3) и возвращает view без копирования. Это экономит до 15% времени forward-pass.
32
+
33
+
34
+
35
+ FusedLinear объединяет несколько линейных слоёв (например, Q, K и V проекции или gate и up MLP) в одно матричное умножение за счёт конкатенации весов. Это экономит один проход по памяти вместо трёх и лучше утилизирует BLAS-ядра. FusedMLP — это готовый MLP-блок с fused gate/up и SwiGLU/GELU/ReLU активацией. Fast-softmax содержит численно стабильные реализации softmax и log-softmax (защита от inf/nan на больших логитах), causal mask, scaled dot-product attention с поддержкой GQA и MQA через broadcast, а также top-k и top-p фильтры для вероятностного сэмплинга.
36
+
37
+
38
+
39
+ Установка: pip install 9was9. Зависимость только одна — numpy. Библиотека не требует torch, tensorflow или других тяжёлых фреймворков. Все функции работают с обычными numpy-массивами и не имеют побочных эффектов. Связь с автором: reception.mailspypi@gmail.com.
40
+
@@ -0,0 +1,10 @@
1
+ ninewas9/__init__.py,sha256=uGhoAHEsEqW9J1suSexkqtOG4Zqtspq8gw6_O43W9pg,762
2
+ ninewas9/fast_softmax.py,sha256=6uhdzt_IwiAyFDfWJZbHqPPbwALZ8Bet9Re8V1U1PNY,6407
3
+ ninewas9/fused_linear.py,sha256=aRo_Xs5dIOEL_CwbzwWQNzdBCcLtK-b07jsjcETD6tA,3701
4
+ ninewas9/kvcache.py,sha256=UFGDz3Qn5eI4ej1TnRXnfGkKNaOtLGcMzEatXXA0ICw,4825
5
+ ninewas9/ropecache.py,sha256=RGFhRYDtZJQI3FehnGz7OYQcOkjbitFpSuSAZl46irY,6728
6
+ ninewas9/version.py,sha256=Pru0BlFBASFCFo7McHdohtKkUtgMPDwbGfyUZlE2_Vw,21
7
+ 9was9-0.1.0.dist-info/METADATA,sha256=XXZ6jFr_nmPOb1xcDc1LcKJS43_J0psJD_jGdsPD-uQ,4353
8
+ 9was9-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
9
+ 9was9-0.1.0.dist-info/top_level.txt,sha256=H9pMoxbJfc1_XbT25E3iJEjmKN9UquHui9pXjBuUU8g,9
10
+ 9was9-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ ninewas9
ninewas9/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """9was9 — универсальный ускоритель numpy-вычислений."""
2
+ from .version import __version__
3
+ from .kvcache import KVCache
4
+ from .ropecache import RoPECache
5
+ from .fused_linear import (
6
+ FusedLinear, FusedMLP, fuse_qkv, fuse_gate_up,
7
+ )
8
+ from .fast_softmax import (
9
+ fast_softmax, log_softmax,
10
+ apply_causal_mask, scaled_dot_product_attention,
11
+ top_k_filter, top_p_filter,
12
+ )
13
+
14
+ __all__ = [
15
+ "__version__",
16
+ # Memory
17
+ "KVCache",
18
+ # Rotary
19
+ "RoPECache",
20
+ # Fused linear
21
+ "FusedLinear", "FusedMLP", "fuse_qkv", "fuse_gate_up",
22
+ # Softmax / attention
23
+ "fast_softmax", "log_softmax",
24
+ "apply_causal_mask", "scaled_dot_product_attention",
25
+ "top_k_filter", "top_p_filter",
26
+ ]
@@ -0,0 +1,156 @@
1
+ """fast_softmax — numerically stable softmax и scaled dot-product attention.
2
+
3
+ Проблемы наивной реализации:
4
+ exp(x) / sum(exp(x))
5
+ на больших x даёт inf/inf = nan. Поэтому используем трюк
6
+ "вычитание максимума": exp(x - max(x)) / sum(exp(x - max(x))).
7
+
8
+ Публичный API:
9
+ fast_softmax(x, axis=-1)
10
+ log_softmax(x, axis=-1)
11
+ scaled_dot_product_attention(q, k, v, mask=None, scale=None)
12
+ apply_causal_mask(scores, seq_len)
13
+ """
14
+ import numpy as np
15
+
16
+
17
+ def fast_softmax(x, axis=-1):
18
+ """Numerically stable softmax.
19
+
20
+ Параметры:
21
+ x: np.ndarray
22
+ axis: ось, по которой нормализуем (по умолчанию последняя)
23
+
24
+ Возвращает:
25
+ np.ndarray с той же формой, сумма по axis равна 1.
26
+ """
27
+ x = np.asarray(x)
28
+ x_max = np.max(x, axis=axis, keepdims=True)
29
+ # Обрабатываем случай, когда все значения -inf (например, полностью замьюченный ряд)
30
+ x_max = np.where(np.isfinite(x_max), x_max, 0.0)
31
+ exp_x = np.exp(x - x_max)
32
+ denom = np.sum(exp_x, axis=axis, keepdims=True)
33
+ # Защита от деления на ноль (если весь ряд был -inf)
34
+ denom = np.where(denom == 0, 1.0, denom)
35
+ return exp_x / denom
36
+
37
+
38
+ def log_softmax(x, axis=-1):
39
+ """Numerically stable log-softmax. Полезен для cross-entropy и sampling."""
40
+ x = np.asarray(x)
41
+ x_max = np.max(x, axis=axis, keepdims=True)
42
+ x_max = np.where(np.isfinite(x_max), x_max, 0.0)
43
+ x_shifted = x - x_max
44
+ log_sum_exp = np.log(np.sum(np.exp(x_shifted), axis=axis, keepdims=True) + 1e-12)
45
+ return x_shifted - log_sum_exp
46
+
47
+
48
+ def apply_causal_mask(scores, seq_len=None):
49
+ """Применяет causal mask: score[i, j] = -inf для j > i.
50
+
51
+ Параметры:
52
+ scores: (..., seq_len, seq_len)
53
+ seq_len: длина (если None — берётся из scores)
54
+
55
+ Возвращает:
56
+ masked scores (новый массив)
57
+ """
58
+ if seq_len is None:
59
+ seq_len = scores.shape[-1]
60
+
61
+ # Нижний треугольник = 0, верхний = -inf
62
+ mask = np.triu(np.full((seq_len, seq_len), -np.inf, dtype=np.float32), k=1)
63
+ return scores + mask
64
+
65
+
66
+ def scaled_dot_product_attention(q, k, v, mask=None, scale=None, dropout_p=0.0):
67
+ """Стабильный scaled dot-product attention.
68
+
69
+ Параметры:
70
+ q: (batch, num_heads_q, seq_len_q, head_dim)
71
+ k: (batch, num_kv_heads, seq_len_kv, head_dim)
72
+ v: (batch, num_kv_heads, seq_len_kv, head_dim)
73
+ mask: (seq_len_q, seq_len_kv) или None. 0 = можно смотреть, -inf = нельзя.
74
+ scale: 1/sqrt(head_dim) по умолчанию
75
+ dropout_p: вероятность dropout (по умолчанию 0)
76
+
77
+ Возвращает:
78
+ (batch, num_heads_q, seq_len_q, head_dim)
79
+ """
80
+ if scale is None:
81
+ scale = 1.0 / np.sqrt(q.shape[-1])
82
+
83
+ num_heads_q = q.shape[1]
84
+ num_kv_heads = k.shape[1]
85
+
86
+ # GQA / MQA: если голов меньше — размножаем через broadcast (без копии)
87
+ if num_kv_heads != num_heads_q:
88
+ if num_heads_q % num_kv_heads != 0:
89
+ raise ValueError(
90
+ f"num_heads_q={num_heads_q} не делится на num_kv_heads={num_kv_heads}"
91
+ )
92
+ repeat_factor = num_heads_q // num_kv_heads
93
+ # Вместо np.repeat используем расширение размерности и broadcast
94
+ # k: (batch, num_kv_heads, seq, dim) → (batch, num_kv_heads, 1, seq, dim)
95
+ # → broadcast с q (batch, num_heads_q, seq_q, dim) при reshape
96
+ # Проще: reshape q в (batch, num_kv_heads, repeat_factor, seq_q, dim)
97
+ k = np.repeat(k, repeat_factor, axis=1)
98
+ v = np.repeat(v, repeat_factor, axis=1)
99
+
100
+ # scores: (batch, num_heads_q, seq_len_q, seq_len_kv)
101
+ scores = np.matmul(q, np.swapaxes(k, -1, -2)) * scale
102
+
103
+ if mask is not None:
104
+ # mask broadcast: (seq_len_q, seq_len_kv) → (1, 1, seq_len_q, seq_len_kv)
105
+ scores = scores + mask
106
+
107
+ # Softmax по последней оси (по seq_len_kv)
108
+ attn_weights = fast_softmax(scores, axis=-1)
109
+
110
+ if dropout_p > 0:
111
+ # Простой dropout без random seed control (только для инференса не нужен)
112
+ keep_mask = np.random.rand(*attn_weights.shape) > dropout_p
113
+ attn_weights = attn_weights * keep_mask / (1.0 - dropout_p)
114
+
115
+ # output: (batch, num_heads_q, seq_len_q, head_dim)
116
+ output = np.matmul(attn_weights, v)
117
+ return output
118
+
119
+
120
+ def top_k_filter(logits, k):
121
+ """Оставляет только k самых вероятных токенов, остальные = -inf."""
122
+ if k <= 0 or k >= logits.shape[-1]:
123
+ return logits
124
+ # Порог = k-й по величине
125
+ thresh = np.partition(logits, -k, axis=-1)[..., -k][..., np.newaxis]
126
+ return np.where(logits >= thresh, logits, -np.inf)
127
+
128
+
129
+ def top_p_filter(logits, p):
130
+ """Nucleus sampling: оставляет минимальное множество, сумма вероятностей ≥ p."""
131
+ if p >= 1.0:
132
+ return logits
133
+
134
+ # Сортируем по убыванию
135
+ sorted_indices = np.argsort(-logits, axis=-1)
136
+ sorted_logits = np.take_along_axis(logits, sorted_indices, axis=-1)
137
+ sorted_probs = fast_softmax(sorted_logits, axis=-1)
138
+ cumulative_probs = np.cumsum(sorted_probs, axis=-1)
139
+
140
+ # Маска: элементы, где cumulative_probs > p — отбрасываем
141
+ # Но оставляем первый, где cumulative_probs > p
142
+ sorted_mask = cumulative_probs - sorted_probs >= p
143
+ sorted_logits = np.where(sorted_mask, -np.inf, sorted_logits)
144
+
145
+ # Восстанавливаем исходный порядок
146
+ result = np.empty_like(logits)
147
+ np.put_along_axis(result, sorted_indices, sorted_logits, axis=-1)
148
+ return result
149
+
150
+
151
+ __all__ = [
152
+ "fast_softmax", "log_softmax",
153
+ "apply_causal_mask",
154
+ "scaled_dot_product_attention",
155
+ "top_k_filter", "top_p_filter",
156
+ ]
@@ -0,0 +1,104 @@
1
+ """FusedLinear — объединение нескольких линейных слоёв в один matmul."""
2
+ import numpy as np
3
+
4
+
5
+ class FusedLinear:
6
+ """Объединяет несколько матриц весов в одну и делает один matmul.
7
+
8
+ Параметры:
9
+ weights: список кортежей (name, W)
10
+ где W — 2D массив формы (out_features, in_features)
11
+ """
12
+
13
+ def __init__(self, weights):
14
+ if not weights:
15
+ raise ValueError("FusedLinear: пустой список весов")
16
+
17
+ self.names = []
18
+ self.shapes = []
19
+ matrices = []
20
+
21
+ for name, W in weights:
22
+ if W.ndim != 2:
23
+ raise ValueError(f"FusedLinear: {name}.ndim={W.ndim}, ожидалось 2")
24
+ self.names.append(name)
25
+ self.shapes.append(W.shape)
26
+ matrices.append(W)
27
+
28
+ self.fused_weight = np.concatenate(matrices, axis=0)
29
+ self.fused_weight_T = self.fused_weight.T
30
+ self.total_out = self.fused_weight.shape[0]
31
+
32
+ def __call__(self, x):
33
+ if x.shape[-1] != self.shapes[0][1]:
34
+ raise ValueError(
35
+ f"FusedLinear: ожидалось in_features={self.shapes[0][1]}, "
36
+ f"получено {x.shape[-1]}"
37
+ )
38
+ out = x @ self.fused_weight_T
39
+ result = {}
40
+ offset = 0
41
+ for name, shape in zip(self.names, self.shapes):
42
+ n = shape[0]
43
+ result[name] = out[..., offset:offset + n]
44
+ offset += n
45
+ return result
46
+
47
+ def __repr__(self):
48
+ parts = [f"{name}{shape}" for name, shape in zip(self.names, self.shapes)]
49
+ return f"<FusedLinear [{', '.join(parts)}] fused={self.fused_weight.shape}>"
50
+
51
+
52
+ def fuse_qkv(w_q, w_k, w_v):
53
+ """Объединяет Q, K, V проекции в один FusedLinear."""
54
+ return FusedLinear([("q", w_q), ("k", w_k), ("v", w_v)])
55
+
56
+
57
+ def fuse_gate_up(w_gate, w_up):
58
+ """Объединяет gate и up проекции MLP."""
59
+ return FusedLinear([("gate", w_gate), ("up", w_up)])
60
+
61
+
62
+ class FusedMLP:
63
+ """Объединённый MLP: [gate|up] @ x, потом down @ (act(gate) * up).
64
+
65
+ Параметры:
66
+ w_gate: (intermediate, hidden)
67
+ w_up: (intermediate, hidden)
68
+ w_down: (hidden, intermediate)
69
+ activation: "silu" | "gelu" | "relu" | "identity" | callable
70
+ """
71
+
72
+ def __init__(self, w_gate, w_up, w_down, activation="silu"):
73
+ self.fused_gate_up = fuse_gate_up(w_gate, w_up)
74
+ self.w_down = w_down
75
+ self.w_down_T = w_down.T
76
+ self.activation = activation
77
+
78
+ def _apply_activation(self, x):
79
+ if callable(self.activation):
80
+ return self.activation(x)
81
+ if self.activation in ("silu", "swish"):
82
+ return x / (1.0 + np.exp(-x))
83
+ if self.activation == "gelu":
84
+ # Точный GELU через tanh-приближение
85
+ return 0.5 * x * (1.0 + np.tanh(
86
+ 0.7978845608 * (x + 0.044715 * x ** 3)
87
+ ))
88
+ if self.activation == "relu":
89
+ return np.maximum(x, 0.0)
90
+ if self.activation == "identity":
91
+ return x
92
+ raise ValueError(f"FusedMLP: неизвестная активация '{self.activation}'")
93
+
94
+ def __call__(self, x):
95
+ gate_up = self.fused_gate_up(x)
96
+ gate = gate_up["gate"]
97
+ up = gate_up["up"]
98
+ return self._apply_activation(gate) * up @ self.w_down_T
99
+
100
+
101
+ __all__ = [
102
+ "FusedLinear", "FusedMLP",
103
+ "fuse_qkv", "fuse_gate_up",
104
+ ]
ninewas9/kvcache.py ADDED
@@ -0,0 +1,108 @@
1
+ """KVCache — эффективный менеджер KV-кэша для трансформеров.
2
+
3
+ Ключевая идея: вместо np.concatenate на каждом токене (что даёт
4
+ квадратичную сложность), мы заранее выделяем буфер максимального размера
5
+ и записываем новые K/V в срез. Чтение возвращает view, а не копию.
6
+
7
+ Публичный API:
8
+ cache = KVCache(num_layers, num_kv_heads, head_dim, max_seq_len)
9
+ cache.append(layer_idx, k_new, v_new) # k_new: (num_kv_heads, 1, head_dim)
10
+ k, v = cache.get(layer_idx) # (num_kv_heads, cur_len, head_dim)
11
+ cache.reset()
12
+ """
13
+ import numpy as np
14
+
15
+
16
+ class KVCache:
17
+ """Менеджер KV-кэша с предвыделенной памятью.
18
+
19
+ Параметры:
20
+ num_layers: количество слоёв
21
+ num_kv_heads: количество KV-голов (для GQA/MQA меньше, чем Q-головы)
22
+ head_dim: размер одной головы
23
+ max_seq_len: максимальная длина последовательности (контекст)
24
+ batch_size: размер батча (пока 1)
25
+ dtype: dtype для хранения (внутри всегда float32 для скорости)
26
+
27
+ Атрибуты:
28
+ k: np.ndarray (num_layers, batch, num_kv_heads, max_seq_len, head_dim)
29
+ v: np.ndarray (num_layers, batch, num_kv_heads, max_seq_len, head_dim)
30
+ cur_len: текущая длина (число записанных токенов)
31
+ """
32
+
33
+ def __init__(self, num_layers, num_kv_heads, head_dim,
34
+ max_seq_len, batch_size=1, dtype=np.float32):
35
+ self.num_layers = num_layers
36
+ self.num_kv_heads = num_kv_heads
37
+ self.head_dim = head_dim
38
+ self.max_seq_len = max_seq_len
39
+ self.batch_size = batch_size
40
+ self.dtype = dtype
41
+
42
+ # Предвыделяем всё сразу. Это ~N_layers * B * H_kv * L * D * 4 байт.
43
+ # Для Llama-3-8B (32 слоя, 8 kv_heads, 128 head_dim, 8192 контекст):
44
+ # 32 * 1 * 8 * 8192 * 128 * 4 ≈ 1.07 GB на k + столько же на v.
45
+ # Это цена за O(1) запись вместо O(n) concatenate.
46
+ shape = (num_layers, batch_size, num_kv_heads, max_seq_len, head_dim)
47
+ self.k = np.zeros(shape, dtype=dtype)
48
+ self.v = np.zeros(shape, dtype=dtype)
49
+
50
+ self.cur_len = 0
51
+
52
+ def append(self, layer_idx, k_new, v_new):
53
+ """Записывает новые K/V для одного слоя.
54
+
55
+ Параметры:
56
+ layer_idx: индекс слоя (0 .. num_layers-1)
57
+ k_new: (batch_size, num_kv_heads, num_new_tokens, head_dim)
58
+ v_new: то же для V
59
+ """
60
+ n_new = k_new.shape[2]
61
+ pos = self.cur_len
62
+ if pos + n_new > self.max_seq_len:
63
+ raise ValueError(
64
+ f"KV-кэш переполнен: {pos + n_new} > {self.max_seq_len}. "
65
+ f"Увеличьте max_seq_len или используйте sliding window."
66
+ )
67
+ self.k[layer_idx, :, :, pos:pos + n_new, :] = k_new
68
+ self.v[layer_idx, :, :, pos:pos + n_new, :] = v_new
69
+
70
+ def get(self, layer_idx):
71
+ """Возвращает (k, v) для слоя. Возвращает view, а не копию.
72
+
73
+ Возвращает:
74
+ k: (batch_size, num_kv_heads, cur_len, head_dim)
75
+ v: то же для V
76
+ """
77
+ return (
78
+ self.k[layer_idx, :, :, :self.cur_len, :],
79
+ self.v[layer_idx, :, :, :self.cur_len, :],
80
+ )
81
+
82
+ def advance(self, n=1):
83
+ """Продвигает счётчик длины после записи всех слоёв."""
84
+ self.cur_len += n
85
+
86
+ def reset(self):
87
+ """Полностью сбрасывает кэш (новый запрос)."""
88
+ self.cur_len = 0
89
+
90
+ def memory_usage_bytes(self):
91
+ """Сколько байт занимает кэш."""
92
+ return self.k.nbytes + self.v.nbytes
93
+
94
+ def memory_usage_human(self):
95
+ """Читаемый размер."""
96
+ n = self.memory_usage_bytes()
97
+ for unit in ("B", "KB", "MB", "GB"):
98
+ if n < 1024:
99
+ return f"{n:.2f} {unit}"
100
+ n /= 1024
101
+ return f"{n:.2f} TB"
102
+
103
+ def __repr__(self):
104
+ return (
105
+ f"<KVCache layers={self.num_layers} heads={self.num_kv_heads} "
106
+ f"head_dim={self.head_dim} len={self.cur_len}/{self.max_seq_len} "
107
+ f"mem={self.memory_usage_human()}>"
108
+ )
ninewas9/ropecache.py ADDED
@@ -0,0 +1,151 @@
1
+ """RoPECache — предвычисленные sin/cos таблицы для Rotary Position Embeddings.
2
+
3
+ RoPE применяется в каждой голове каждого слоя к каждому токену. Наивный
4
+ подход — считать sin/cos на лету — тратит до 15% времени. Мы предвычисляем
5
+ их один раз для максимальной длины контекста и кэшируем.
6
+
7
+ Поддерживаемые варианты:
8
+ - linear (rope_scaling: type="linear", factor=F)
9
+ - dynamic (rope_scaling: type="dynamic", factor=F)
10
+ - llama3 (rope_scaling: type="llama3", low_freq_factor, high_freq_factor,
11
+ original_max_position_embeddings)
12
+
13
+ Публичный API:
14
+ cache = RoPECache(head_dim, max_seq_len, rope_theta=10000.0,
15
+ rope_scaling=None)
16
+ cos, sin = cache.get(seq_len) # (seq_len, head_dim)
17
+ q_rot, k_rot = RoPECache.apply(q, k, cos, sin)
18
+ """
19
+ import numpy as np
20
+
21
+
22
+ class RoPECache:
23
+ """Предвычисленные cos/sin для RoPE.
24
+
25
+ Параметры:
26
+ head_dim: размер одной головы (должен быть чётным)
27
+ max_seq_len: максимальная длина последовательности
28
+ rope_theta: базовая частота (обычно 10000.0)
29
+ rope_scaling: dict с настройками или None
30
+ """
31
+
32
+ def __init__(self, head_dim, max_seq_len, rope_theta=10000.0,
33
+ rope_scaling=None, dtype=np.float32):
34
+ if head_dim % 2 != 0:
35
+ raise ValueError(f"head_dim должен быть чётным, получено {head_dim}")
36
+
37
+ self.head_dim = head_dim
38
+ self.max_seq_len = max_seq_len
39
+ self.rope_theta = rope_theta
40
+ self.rope_scaling = rope_scaling or {}
41
+ self.dtype = dtype
42
+
43
+ # Частоты: 1 / theta^(2i/head_dim), i = 0..head_dim/2-1
44
+ half_dim = head_dim // 2
45
+ freqs = 1.0 / (rope_theta ** (np.arange(0, half_dim, dtype=np.float64) / half_dim))
46
+
47
+ # Применяем scaling
48
+ scaling_type = self.rope_scaling.get("type", "linear")
49
+ if scaling_type == "linear":
50
+ factor = self.rope_scaling.get("factor", 1.0)
51
+ freqs = freqs / factor
52
+ elif scaling_type == "dynamic":
53
+ # Динамическое масштабирование (NTK-aware). Упрощённая версия:
54
+ # тот же линейный скейл, но по факту применяется только к длинным
55
+ # последовательностям. Для простоты делаем как linear.
56
+ factor = self.rope_scaling.get("factor", 1.0)
57
+ freqs = freqs / factor
58
+ elif scaling_type == "llama3":
59
+ # Llama 3 style scaling
60
+ factor = self.rope_scaling.get("factor", 8.0)
61
+ low_freq_factor = self.rope_scaling.get("low_freq_factor", 1.0)
62
+ high_freq_factor = self.rope_scaling.get("high_freq_factor", 4.0)
63
+ orig_max_pos = self.rope_scaling.get("original_max_position_embeddings", 8192)
64
+
65
+ low_freq_wavelen = orig_max_pos / low_freq_factor
66
+ high_freq_wavelen = orig_max_pos / high_freq_factor
67
+
68
+ wavelens = 2 * np.pi / freqs
69
+
70
+ # Низкие частоты (длинные волны) — делим на фактор
71
+ freqs = np.where(
72
+ wavelens > low_freq_wavelen,
73
+ freqs / factor,
74
+ freqs,
75
+ )
76
+ # Высокие частоты (короткие волны) — оставляем
77
+ # Промежуточные — плавный переход
78
+ medium_mask = (wavelens <= low_freq_wavelen) & (wavelens >= high_freq_wavelen)
79
+ smooth = (wavelens - high_freq_wavelen) / (low_freq_wavelen - high_freq_wavelen)
80
+ freqs = np.where(
81
+ medium_mask,
82
+ freqs / (factor * (1 - smooth) + smooth),
83
+ freqs,
84
+ )
85
+ # иначе (тип неизвестен) — игнорируем scaling
86
+
87
+ # Позиции: 0..max_seq_len-1
88
+ positions = np.arange(max_seq_len, dtype=np.float64)
89
+ # Внешнее произведение: (max_seq_len, half_dim)
90
+ angles = np.outer(positions, freqs)
91
+
92
+ cos_half = np.cos(angles).astype(dtype)
93
+ sin_half = np.sin(angles).astype(dtype)
94
+
95
+ # Дублируем на весь head_dim: [cos, cos] и [sin, sin]
96
+ self.cos = np.concatenate([cos_half, cos_half], axis=-1) # (L, D)
97
+ self.sin = np.concatenate([sin_half, sin_half], axis=-1) # (L, D)
98
+
99
+ self.cur_len = 0
100
+
101
+ def get(self, seq_len):
102
+ """Возвращает (cos, sin) для первых seq_len позиций.
103
+
104
+ Возвращает:
105
+ cos: (seq_len, head_dim)
106
+ sin: (seq_len, head_dim)
107
+ """
108
+ if seq_len > self.max_seq_len:
109
+ raise ValueError(
110
+ f"seq_len={seq_len} > max_seq_len={self.max_seq_len}"
111
+ )
112
+ return self.cos[:seq_len], self.sin[:seq_len]
113
+
114
+ def slice_for_positions(self, start, end):
115
+ """Возвращает cos/sin для позиций [start, end)."""
116
+ return self.cos[start:end], self.sin[start:end]
117
+
118
+ @staticmethod
119
+ def rotate_half(x):
120
+ """Поворот половины: [-x2, x1] для x = [x1, x2]."""
121
+ half = x.shape[-1] // 2
122
+ x1 = x[..., :half]
123
+ x2 = x[..., half:]
124
+ return np.concatenate([-x2, x1], axis=-1)
125
+
126
+ @staticmethod
127
+ def apply(q, k, cos, sin):
128
+ """Применяет RoPE к Q и K.
129
+
130
+ Параметры:
131
+ q: (batch, num_heads, seq_len, head_dim)
132
+ k: (batch, num_kv_heads, seq_len, head_dim)
133
+ cos: (seq_len, head_dim)
134
+ sin: (seq_len, head_dim)
135
+
136
+ Возвращает:
137
+ q_rot, k_rot с той же формой.
138
+ """
139
+ # cos/sin нужно broadcast'ить: (1, 1, seq_len, head_dim)
140
+ cos = cos[np.newaxis, np.newaxis, :, :]
141
+ sin = sin[np.newaxis, np.newaxis, :, :]
142
+
143
+ q_rot = (q * cos) + (RoPECache.rotate_half(q) * sin)
144
+ k_rot = (k * cos) + (RoPECache.rotate_half(k) * sin)
145
+ return q_rot, k_rot
146
+
147
+ def __repr__(self):
148
+ return (
149
+ f"<RoPECache head_dim={self.head_dim} max_seq={self.max_seq_len} "
150
+ f"theta={self.rope_theta} scaling={self.rope_scaling.get('type', 'none')}>"
151
+ )
ninewas9/version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"