9was9 0.1.0__tar.gz
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.
- 9was9-0.1.0/9was9.egg-info/PKG-INFO +40 -0
- 9was9-0.1.0/9was9.egg-info/SOURCES.txt +13 -0
- 9was9-0.1.0/9was9.egg-info/dependency_links.txt +1 -0
- 9was9-0.1.0/9was9.egg-info/requires.txt +1 -0
- 9was9-0.1.0/9was9.egg-info/top_level.txt +1 -0
- 9was9-0.1.0/PKG-INFO +40 -0
- 9was9-0.1.0/README.md +22 -0
- 9was9-0.1.0/ninewas9/__init__.py +26 -0
- 9was9-0.1.0/ninewas9/fast_softmax.py +156 -0
- 9was9-0.1.0/ninewas9/fused_linear.py +104 -0
- 9was9-0.1.0/ninewas9/kvcache.py +108 -0
- 9was9-0.1.0/ninewas9/ropecache.py +151 -0
- 9was9-0.1.0/ninewas9/version.py +1 -0
- 9was9-0.1.0/pyproject.toml +30 -0
- 9was9-0.1.0/setup.cfg +4 -0
|
@@ -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,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
9was9.egg-info/PKG-INFO
|
|
4
|
+
9was9.egg-info/SOURCES.txt
|
|
5
|
+
9was9.egg-info/dependency_links.txt
|
|
6
|
+
9was9.egg-info/requires.txt
|
|
7
|
+
9was9.egg-info/top_level.txt
|
|
8
|
+
ninewas9/__init__.py
|
|
9
|
+
ninewas9/fast_softmax.py
|
|
10
|
+
ninewas9/fused_linear.py
|
|
11
|
+
ninewas9/kvcache.py
|
|
12
|
+
ninewas9/ropecache.py
|
|
13
|
+
ninewas9/version.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy>=1.21.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ninewas9
|
9was9-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
|
9was9-0.1.0/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
9was9. Версия 0.1.0. Автор CharmLxt (PyPI: CharmLxt, email: reception.mailspypi@gmail.com). Ссылка на GitHub: https://github.com/CharmLxt/9was9.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
Важно про имя пакета. Установка производится командой pip install 9was9 (так пакет называется на PyPI), но импортировать его нужно как ninewas9, потому что имена Python-модулей не могут начинаться с цифры. Пример: from ninewas9 import KVCache, RoPECache, FusedLinear, fast\_softmax.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
9was9 — это универсальная библиотека-ускоритель для numpy-вычислений, созданная в первую очередь для нейросетевого инференса, но применимая в любом проекте, где важна скорость работы с массивами. В основе лежит несколько идей: предвыделение памяти вместо повторяющихся аллокаций, кэширование часто используемых таблиц, объединение нескольких матричных умножений в одно и численно стабильные реализации классических операций.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Первая версия содержит пять групп функций. KVCache — это менеджер памяти для ключей и значений в трансформерах, который заранее выделяет буфер максимального размера и записывает новые значения в срез, избегая np.concatenate на каждом токене. Это уменьшает сложность вставки с O(n) до O(1) и убирает до 20% накладных расходов на инференс. RoPECache предвычисляет sin/cos таблицы для Rotary Position Embeddings с поддержкой нескольких типов масштабирования (linear, dynamic, llama3) и возвращает view без копирования. Это экономит до 15% времени forward-pass.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
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 фильтры для вероятностного сэмплинга.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Установка: pip install 9was9. Зависимость только одна — numpy. Библиотека не требует torch, tensorflow или других тяжёлых фреймворков. Все функции работают с обычными numpy-массивами и не имеют побочных эффектов. Связь с автором: reception.mailspypi@gmail.com.
|
|
22
|
+
|
|
@@ -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
|
+
]
|
|
@@ -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
|
+
)
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "9was9"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Универсальный ускоритель numpy-вычислений (KV-кэш, RoPE, fused linear)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "CharmLxt", email = "reception.mailspypi@gmail.com" }]
|
|
13
|
+
keywords = ["numpy", "acceleration", "kv-cache", "rope", "llm", "inference"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"numpy>=1.21.0"
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: Scientific/Engineering",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/CharmLxt/9was9"
|
|
27
|
+
Source = "https://github.com/CharmLxt/9was9"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
include = ["ninewas9*"]
|
9was9-0.1.0/setup.cfg
ADDED