nsos-mamba 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.
- nsos_mamba-0.1.0/PKG-INFO +228 -0
- nsos_mamba-0.1.0/README.md +207 -0
- nsos_mamba-0.1.0/nsos_mamba/__init__.py +37 -0
- nsos_mamba-0.1.0/nsos_mamba/core.py +465 -0
- nsos_mamba-0.1.0/nsos_mamba/datasets.py +161 -0
- nsos_mamba-0.1.0/nsos_mamba.egg-info/PKG-INFO +228 -0
- nsos_mamba-0.1.0/nsos_mamba.egg-info/SOURCES.txt +10 -0
- nsos_mamba-0.1.0/nsos_mamba.egg-info/dependency_links.txt +1 -0
- nsos_mamba-0.1.0/nsos_mamba.egg-info/requires.txt +1 -0
- nsos_mamba-0.1.0/nsos_mamba.egg-info/top_level.txt +1 -0
- nsos_mamba-0.1.0/pyproject.toml +35 -0
- nsos_mamba-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nsos-mamba
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Wrapper Python isolado para o módulo Mamba do NSOS com streaming incremental padrão.
|
|
5
|
+
Author: Oxta
|
|
6
|
+
Project-URL: Homepage, https://github.com/vitorGgC569/reimagined
|
|
7
|
+
Project-URL: Documentation, https://github.com/vitorGgC569/reimagined/tree/nsos-gpu-phases12/OXN/nsos/docs
|
|
8
|
+
Project-URL: Source, https://github.com/vitorGgC569/reimagined
|
|
9
|
+
Keywords: mamba,ssm,nsos,edge-ai,streaming
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: numpy>=1.23
|
|
21
|
+
|
|
22
|
+
# NSOS Mamba
|
|
23
|
+
|
|
24
|
+
`nsos-mamba` é um wrapper Python simples para usar apenas o módulo Mamba do
|
|
25
|
+
NSOS. Ele configura automaticamente o modelo em modo Mamba-2 faithful, ativa
|
|
26
|
+
streaming incremental por padrão e oferece utilitários pequenos para treinar
|
|
27
|
+
pares `prompt -> resposta`.
|
|
28
|
+
|
|
29
|
+
Este pacote não instala o runtime nativo `nsos_ext`. Antes de usar, compile ou
|
|
30
|
+
copie o `nsos_ext.so`/`nsos_ext.pyd` e deixe-o acessível via `PYTHONPATH` ou
|
|
31
|
+
pela variável `NSOS_EXT_PATH`.
|
|
32
|
+
|
|
33
|
+
## Instalação
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install nsos-mamba
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Se o `nsos_ext` estiver em uma pasta separada:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export NSOS_EXT_PATH=/caminho/para/nsos_ext
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
No Colab:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import sys, os
|
|
49
|
+
|
|
50
|
+
sys.path.insert(0, "/content/nsos_ext_mambavs")
|
|
51
|
+
# ou:
|
|
52
|
+
os.environ["NSOS_EXT_PATH"] = "/content/nsos_ext_mambavs"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configurações principais
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from nsos_mamba import MambaModuleConfig
|
|
59
|
+
|
|
60
|
+
cfg = MambaModuleConfig(
|
|
61
|
+
vocab_size=128,
|
|
62
|
+
num_layers=12,
|
|
63
|
+
d_model=128,
|
|
64
|
+
device="auto",
|
|
65
|
+
streaming=True,
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Campos mais usados:
|
|
70
|
+
|
|
71
|
+
- `vocab_size`: tamanho do vocabulário.
|
|
72
|
+
- `num_layers`: número de camadas Mamba.
|
|
73
|
+
- `d_model`: largura do modelo.
|
|
74
|
+
- `d_state`: tamanho do estado SSM. Padrão: `64`.
|
|
75
|
+
- `max_context_tokens`: limite de contexto. Padrão: `4096`.
|
|
76
|
+
- `device`: `"auto"`, `"gpu"`, `"cuda"` ou `"cpu"`.
|
|
77
|
+
- `streaming`: ativa decode incremental. Padrão: `True`.
|
|
78
|
+
- `allow_cpu_fallback`: se a GPU falhar, cai para CPU. Padrão: `True`.
|
|
79
|
+
|
|
80
|
+
Variáveis de ambiente equivalentes:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
NSOS_MAMBA_DEVICE=auto
|
|
84
|
+
NSOS_MAMBA_STREAMING=1
|
|
85
|
+
NSOS_MAMBA_LAYERS=12
|
|
86
|
+
NSOS_MAMBA_DMODEL=128
|
|
87
|
+
NSOS_MAMBA_DSTATE=64
|
|
88
|
+
NSOS_MAMBA_CONTEXT=4096
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Exemplo mínimo: "Quem é você?" -> "Oxta"
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from nsos_mamba import CharTokenizer, MambaModuleConfig, NSOSMamba, TextPair
|
|
95
|
+
|
|
96
|
+
pairs = [
|
|
97
|
+
TextPair("Quem é você?", "Oxta"),
|
|
98
|
+
TextPair("quem é você?", "Oxta"),
|
|
99
|
+
TextPair("Quem e voce?", "Oxta"),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
tok = CharTokenizer.from_texts(
|
|
103
|
+
[p.prompt for p in pairs] + [p.answer for p in pairs]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
cfg = MambaModuleConfig(
|
|
107
|
+
vocab_size=tok.vocab_size,
|
|
108
|
+
num_layers=12,
|
|
109
|
+
d_model=128,
|
|
110
|
+
device="auto",
|
|
111
|
+
streaming=True,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
mamba = NSOSMamba(cfg)
|
|
115
|
+
|
|
116
|
+
mamba.fit_text_pairs(
|
|
117
|
+
pairs,
|
|
118
|
+
tok,
|
|
119
|
+
steps=1200,
|
|
120
|
+
batch_size=16,
|
|
121
|
+
learning_rate=3e-3,
|
|
122
|
+
callback=lambda step, loss: print(f"[train] step {step} loss~{loss:.4f}"),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
result = mamba.generate_text("Quem é você?", tok, max_new_tokens=8)
|
|
126
|
+
|
|
127
|
+
print("resposta:", repr(result.text))
|
|
128
|
+
print("tokens/s:", result.decode_tokens_per_sec)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Saída esperada:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
resposta: 'Oxta'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Streaming incremental
|
|
138
|
+
|
|
139
|
+
O streaming incremental é o padrão. O fluxo usado é:
|
|
140
|
+
|
|
141
|
+
1. processa o prompt uma vez (`prefill`);
|
|
142
|
+
2. gera cada token novo passando apenas o último token;
|
|
143
|
+
3. reaproveita o estado interno do modelo.
|
|
144
|
+
|
|
145
|
+
Benchmark:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
prompt_ids = tok.encode("Quem é você?", bos=True)
|
|
149
|
+
bench = mamba.benchmark_decode(prompt_ids, max_new_tokens=1024, repeats=5)
|
|
150
|
+
bench.print()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Métricas:
|
|
154
|
+
|
|
155
|
+
- `prefill_ms`: tempo para processar o prompt.
|
|
156
|
+
- `first_incremental_ms`: primeiro passo incremental após o prefill.
|
|
157
|
+
- `decode_tokens_per_sec`: tokens/s do decode incremental.
|
|
158
|
+
- `end_to_end_tokens_per_sec`: tokens/s incluindo prefill.
|
|
159
|
+
|
|
160
|
+
## Datasets
|
|
161
|
+
|
|
162
|
+
### Lista em memória
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from nsos_mamba import TextPair
|
|
166
|
+
|
|
167
|
+
pairs = [
|
|
168
|
+
TextPair(prompt="Quem é você?", answer="Oxta"),
|
|
169
|
+
TextPair(prompt="Qual seu nome?", answer="Oxta"),
|
|
170
|
+
]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### JSON
|
|
174
|
+
|
|
175
|
+
Arquivo `dataset.json`:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
[
|
|
179
|
+
{"prompt": "Quem é você?", "answer": "Oxta"},
|
|
180
|
+
{"prompt": "Qual seu nome?", "answer": "Oxta"}
|
|
181
|
+
]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Uso:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from nsos_mamba import load_text_pairs_json
|
|
188
|
+
|
|
189
|
+
pairs = load_text_pairs_json("dataset.json")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Também aceita:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{"data": [{"prompt": "...", "answer": "..."}]}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
ou:
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{"examples": [{"prompt": "...", "answer": "..."}]}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### JSONL
|
|
205
|
+
|
|
206
|
+
Arquivo `dataset.jsonl`:
|
|
207
|
+
|
|
208
|
+
```jsonl
|
|
209
|
+
{"prompt": "Quem é você?", "answer": "Oxta"}
|
|
210
|
+
{"prompt": "Qual seu nome?", "answer": "Oxta"}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Uso:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from nsos_mamba import load_text_pairs_jsonl
|
|
217
|
+
|
|
218
|
+
pairs = load_text_pairs_jsonl("dataset.jsonl")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Observações
|
|
222
|
+
|
|
223
|
+
- `CharTokenizer` é para testes, demos e dados sintéticos pequenos.
|
|
224
|
+
- Para treino real, use um tokenizer próprio do projeto.
|
|
225
|
+
- O pacote expõe o Mamba isolado; não ativa Attention, MoE, KAN, TTT, CHRASS,
|
|
226
|
+
MCTS ou memória externa.
|
|
227
|
+
- O pacote é uma camada de uso sobre `nsos_ext`, não uma reimplementação em
|
|
228
|
+
Python.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# NSOS Mamba
|
|
2
|
+
|
|
3
|
+
`nsos-mamba` é um wrapper Python simples para usar apenas o módulo Mamba do
|
|
4
|
+
NSOS. Ele configura automaticamente o modelo em modo Mamba-2 faithful, ativa
|
|
5
|
+
streaming incremental por padrão e oferece utilitários pequenos para treinar
|
|
6
|
+
pares `prompt -> resposta`.
|
|
7
|
+
|
|
8
|
+
Este pacote não instala o runtime nativo `nsos_ext`. Antes de usar, compile ou
|
|
9
|
+
copie o `nsos_ext.so`/`nsos_ext.pyd` e deixe-o acessível via `PYTHONPATH` ou
|
|
10
|
+
pela variável `NSOS_EXT_PATH`.
|
|
11
|
+
|
|
12
|
+
## Instalação
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install nsos-mamba
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Se o `nsos_ext` estiver em uma pasta separada:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export NSOS_EXT_PATH=/caminho/para/nsos_ext
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
No Colab:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import sys, os
|
|
28
|
+
|
|
29
|
+
sys.path.insert(0, "/content/nsos_ext_mambavs")
|
|
30
|
+
# ou:
|
|
31
|
+
os.environ["NSOS_EXT_PATH"] = "/content/nsos_ext_mambavs"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configurações principais
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from nsos_mamba import MambaModuleConfig
|
|
38
|
+
|
|
39
|
+
cfg = MambaModuleConfig(
|
|
40
|
+
vocab_size=128,
|
|
41
|
+
num_layers=12,
|
|
42
|
+
d_model=128,
|
|
43
|
+
device="auto",
|
|
44
|
+
streaming=True,
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Campos mais usados:
|
|
49
|
+
|
|
50
|
+
- `vocab_size`: tamanho do vocabulário.
|
|
51
|
+
- `num_layers`: número de camadas Mamba.
|
|
52
|
+
- `d_model`: largura do modelo.
|
|
53
|
+
- `d_state`: tamanho do estado SSM. Padrão: `64`.
|
|
54
|
+
- `max_context_tokens`: limite de contexto. Padrão: `4096`.
|
|
55
|
+
- `device`: `"auto"`, `"gpu"`, `"cuda"` ou `"cpu"`.
|
|
56
|
+
- `streaming`: ativa decode incremental. Padrão: `True`.
|
|
57
|
+
- `allow_cpu_fallback`: se a GPU falhar, cai para CPU. Padrão: `True`.
|
|
58
|
+
|
|
59
|
+
Variáveis de ambiente equivalentes:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
NSOS_MAMBA_DEVICE=auto
|
|
63
|
+
NSOS_MAMBA_STREAMING=1
|
|
64
|
+
NSOS_MAMBA_LAYERS=12
|
|
65
|
+
NSOS_MAMBA_DMODEL=128
|
|
66
|
+
NSOS_MAMBA_DSTATE=64
|
|
67
|
+
NSOS_MAMBA_CONTEXT=4096
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Exemplo mínimo: "Quem é você?" -> "Oxta"
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from nsos_mamba import CharTokenizer, MambaModuleConfig, NSOSMamba, TextPair
|
|
74
|
+
|
|
75
|
+
pairs = [
|
|
76
|
+
TextPair("Quem é você?", "Oxta"),
|
|
77
|
+
TextPair("quem é você?", "Oxta"),
|
|
78
|
+
TextPair("Quem e voce?", "Oxta"),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
tok = CharTokenizer.from_texts(
|
|
82
|
+
[p.prompt for p in pairs] + [p.answer for p in pairs]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
cfg = MambaModuleConfig(
|
|
86
|
+
vocab_size=tok.vocab_size,
|
|
87
|
+
num_layers=12,
|
|
88
|
+
d_model=128,
|
|
89
|
+
device="auto",
|
|
90
|
+
streaming=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
mamba = NSOSMamba(cfg)
|
|
94
|
+
|
|
95
|
+
mamba.fit_text_pairs(
|
|
96
|
+
pairs,
|
|
97
|
+
tok,
|
|
98
|
+
steps=1200,
|
|
99
|
+
batch_size=16,
|
|
100
|
+
learning_rate=3e-3,
|
|
101
|
+
callback=lambda step, loss: print(f"[train] step {step} loss~{loss:.4f}"),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
result = mamba.generate_text("Quem é você?", tok, max_new_tokens=8)
|
|
105
|
+
|
|
106
|
+
print("resposta:", repr(result.text))
|
|
107
|
+
print("tokens/s:", result.decode_tokens_per_sec)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Saída esperada:
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
resposta: 'Oxta'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Streaming incremental
|
|
117
|
+
|
|
118
|
+
O streaming incremental é o padrão. O fluxo usado é:
|
|
119
|
+
|
|
120
|
+
1. processa o prompt uma vez (`prefill`);
|
|
121
|
+
2. gera cada token novo passando apenas o último token;
|
|
122
|
+
3. reaproveita o estado interno do modelo.
|
|
123
|
+
|
|
124
|
+
Benchmark:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
prompt_ids = tok.encode("Quem é você?", bos=True)
|
|
128
|
+
bench = mamba.benchmark_decode(prompt_ids, max_new_tokens=1024, repeats=5)
|
|
129
|
+
bench.print()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Métricas:
|
|
133
|
+
|
|
134
|
+
- `prefill_ms`: tempo para processar o prompt.
|
|
135
|
+
- `first_incremental_ms`: primeiro passo incremental após o prefill.
|
|
136
|
+
- `decode_tokens_per_sec`: tokens/s do decode incremental.
|
|
137
|
+
- `end_to_end_tokens_per_sec`: tokens/s incluindo prefill.
|
|
138
|
+
|
|
139
|
+
## Datasets
|
|
140
|
+
|
|
141
|
+
### Lista em memória
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from nsos_mamba import TextPair
|
|
145
|
+
|
|
146
|
+
pairs = [
|
|
147
|
+
TextPair(prompt="Quem é você?", answer="Oxta"),
|
|
148
|
+
TextPair(prompt="Qual seu nome?", answer="Oxta"),
|
|
149
|
+
]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### JSON
|
|
153
|
+
|
|
154
|
+
Arquivo `dataset.json`:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
[
|
|
158
|
+
{"prompt": "Quem é você?", "answer": "Oxta"},
|
|
159
|
+
{"prompt": "Qual seu nome?", "answer": "Oxta"}
|
|
160
|
+
]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Uso:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from nsos_mamba import load_text_pairs_json
|
|
167
|
+
|
|
168
|
+
pairs = load_text_pairs_json("dataset.json")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Também aceita:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{"data": [{"prompt": "...", "answer": "..."}]}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
ou:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{"examples": [{"prompt": "...", "answer": "..."}]}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### JSONL
|
|
184
|
+
|
|
185
|
+
Arquivo `dataset.jsonl`:
|
|
186
|
+
|
|
187
|
+
```jsonl
|
|
188
|
+
{"prompt": "Quem é você?", "answer": "Oxta"}
|
|
189
|
+
{"prompt": "Qual seu nome?", "answer": "Oxta"}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Uso:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from nsos_mamba import load_text_pairs_jsonl
|
|
196
|
+
|
|
197
|
+
pairs = load_text_pairs_jsonl("dataset.jsonl")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Observações
|
|
201
|
+
|
|
202
|
+
- `CharTokenizer` é para testes, demos e dados sintéticos pequenos.
|
|
203
|
+
- Para treino real, use um tokenizer próprio do projeto.
|
|
204
|
+
- O pacote expõe o Mamba isolado; não ativa Attention, MoE, KAN, TTT, CHRASS,
|
|
205
|
+
MCTS ou memória externa.
|
|
206
|
+
- O pacote é uma camada de uso sobre `nsos_ext`, não uma reimplementação em
|
|
207
|
+
Python.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Standalone NSOS Mamba Python wrapper.
|
|
2
|
+
|
|
3
|
+
This package is intentionally thin: it does not implement a new architecture.
|
|
4
|
+
It builds a pure faithful-Mamba NSOS ``JambaModel`` on top of ``nsos_ext`` and
|
|
5
|
+
standardizes device selection, streaming incremental decoding and small
|
|
6
|
+
supervised datasets.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .core import (
|
|
10
|
+
DecodeBenchmark,
|
|
11
|
+
GenerationResult,
|
|
12
|
+
MambaModuleConfig,
|
|
13
|
+
NSOSMamba,
|
|
14
|
+
detect_device,
|
|
15
|
+
)
|
|
16
|
+
from .datasets import (
|
|
17
|
+
CharTokenizer,
|
|
18
|
+
TextPair,
|
|
19
|
+
load_text_pairs,
|
|
20
|
+
load_text_pairs_json,
|
|
21
|
+
load_text_pairs_jsonl,
|
|
22
|
+
pairs_to_token_ids,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"CharTokenizer",
|
|
27
|
+
"DecodeBenchmark",
|
|
28
|
+
"GenerationResult",
|
|
29
|
+
"MambaModuleConfig",
|
|
30
|
+
"NSOSMamba",
|
|
31
|
+
"TextPair",
|
|
32
|
+
"detect_device",
|
|
33
|
+
"load_text_pairs",
|
|
34
|
+
"load_text_pairs_json",
|
|
35
|
+
"load_text_pairs_jsonl",
|
|
36
|
+
"pairs_to_token_ids",
|
|
37
|
+
]
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Callable, Iterable, List, Optional, Sequence, Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .datasets import CharTokenizer, TextPair, pairs_to_token_ids
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_nsos_ext():
|
|
15
|
+
ext_path = os.environ.get("NSOS_EXT_PATH")
|
|
16
|
+
if ext_path:
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, ext_path)
|
|
20
|
+
try:
|
|
21
|
+
import nsos_ext as nsos # type: ignore
|
|
22
|
+
except ImportError as exc:
|
|
23
|
+
raise ImportError(
|
|
24
|
+
"nsos_ext is not importable. Add the built nsos_ext directory to "
|
|
25
|
+
"PYTHONPATH/sys.path before importing nsos_mamba."
|
|
26
|
+
) from exc
|
|
27
|
+
return nsos
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _torch_cuda_available() -> bool:
|
|
31
|
+
try:
|
|
32
|
+
import torch # type: ignore
|
|
33
|
+
|
|
34
|
+
return bool(torch.cuda.is_available())
|
|
35
|
+
except Exception:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def detect_device(nsos_ext=None, requested: Optional[str] = None):
|
|
40
|
+
"""Resolve CPU/GPU from a flag or ``NSOS_MAMBA_DEVICE``.
|
|
41
|
+
|
|
42
|
+
Accepted values: ``auto`` (default), ``gpu``/``cuda`` and ``cpu``.
|
|
43
|
+
``auto`` selects GPU only when PyTorch can see CUDA; construction still has
|
|
44
|
+
a CPU fallback in ``NSOSMamba`` in case the native extension cannot allocate
|
|
45
|
+
on the selected device.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
nsos = nsos_ext or _load_nsos_ext()
|
|
49
|
+
mode = (requested or os.environ.get("NSOS_MAMBA_DEVICE", "auto")).strip().lower()
|
|
50
|
+
if mode in ("cpu", "host"):
|
|
51
|
+
return nsos.Device.CPU
|
|
52
|
+
if mode in ("gpu", "cuda"):
|
|
53
|
+
return nsos.Device.GPU
|
|
54
|
+
if mode not in ("", "auto"):
|
|
55
|
+
raise ValueError("device must be one of: auto, gpu, cuda, cpu")
|
|
56
|
+
return nsos.Device.GPU if _torch_cuda_available() else nsos.Device.CPU
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
60
|
+
value = os.environ.get(name)
|
|
61
|
+
if value is None:
|
|
62
|
+
return default
|
|
63
|
+
return value.strip().lower() not in ("0", "false", "no", "off")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _safe_set(obj, name: str, value) -> None:
|
|
67
|
+
try:
|
|
68
|
+
setattr(obj, name, value)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class MambaModuleConfig:
|
|
75
|
+
"""Configuration for the standalone faithful-Mamba wrapper."""
|
|
76
|
+
|
|
77
|
+
vocab_size: int
|
|
78
|
+
num_layers: int = 12
|
|
79
|
+
d_model: int = 128
|
|
80
|
+
d_state: int = 64
|
|
81
|
+
max_context_tokens: int = 4096
|
|
82
|
+
n_heads: int = 4
|
|
83
|
+
n_kv_heads: int = 2
|
|
84
|
+
mamba_expand: int = 2
|
|
85
|
+
mamba_head_dim: int = 64
|
|
86
|
+
mamba_n_groups: int = 1
|
|
87
|
+
tie_word_embeddings: bool = False
|
|
88
|
+
seed: Optional[int] = 42
|
|
89
|
+
device: str = "auto"
|
|
90
|
+
streaming: bool = True
|
|
91
|
+
allow_cpu_fallback: bool = True
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_env(cls, vocab_size: int) -> "MambaModuleConfig":
|
|
95
|
+
return cls(
|
|
96
|
+
vocab_size=vocab_size,
|
|
97
|
+
num_layers=int(os.environ.get("NSOS_MAMBA_LAYERS", "12")),
|
|
98
|
+
d_model=int(os.environ.get("NSOS_MAMBA_DMODEL", "128")),
|
|
99
|
+
d_state=int(os.environ.get("NSOS_MAMBA_DSTATE", "64")),
|
|
100
|
+
max_context_tokens=int(os.environ.get("NSOS_MAMBA_CONTEXT", "4096")),
|
|
101
|
+
device=os.environ.get("NSOS_MAMBA_DEVICE", "auto"),
|
|
102
|
+
streaming=_env_bool("NSOS_MAMBA_STREAMING", True),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class GenerationResult:
|
|
108
|
+
prompt_ids: List[int]
|
|
109
|
+
generated_ids: List[int]
|
|
110
|
+
text: Optional[str]
|
|
111
|
+
prefill_ms: float
|
|
112
|
+
decode_ms: float
|
|
113
|
+
ttft_ms: float
|
|
114
|
+
first_incremental_ms: float
|
|
115
|
+
generated_tokens: int
|
|
116
|
+
decode_tokens_per_sec: float
|
|
117
|
+
end_to_end_tokens_per_sec: float
|
|
118
|
+
streaming: bool
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class DecodeBenchmark:
|
|
123
|
+
runs: List[GenerationResult] = field(default_factory=list)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def avg_decode_tokens_per_sec(self) -> float:
|
|
127
|
+
return float(np.mean([r.decode_tokens_per_sec for r in self.runs])) if self.runs else 0.0
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def avg_end_to_end_tokens_per_sec(self) -> float:
|
|
131
|
+
return float(np.mean([r.end_to_end_tokens_per_sec for r in self.runs])) if self.runs else 0.0
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def avg_prefill_ms(self) -> float:
|
|
135
|
+
return float(np.mean([r.prefill_ms for r in self.runs])) if self.runs else 0.0
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def avg_first_incremental_ms(self) -> float:
|
|
139
|
+
return float(np.mean([r.first_incremental_ms for r in self.runs])) if self.runs else 0.0
|
|
140
|
+
|
|
141
|
+
def print(self) -> None:
|
|
142
|
+
print("\n========== NSOS MAMBA DECODE BENCH ==========")
|
|
143
|
+
for idx, run in enumerate(self.runs):
|
|
144
|
+
print(
|
|
145
|
+
f"run {idx}: {run.generated_tokens} tok | "
|
|
146
|
+
f"decode={run.decode_ms / 1000.0:.4f}s | "
|
|
147
|
+
f"decode_tok/s={run.decode_tokens_per_sec:.2f} | "
|
|
148
|
+
f"e2e_tok/s={run.end_to_end_tokens_per_sec:.2f} | "
|
|
149
|
+
f"prefill={run.prefill_ms:.2f} ms | "
|
|
150
|
+
f"first_incremental={run.first_incremental_ms:.2f} ms"
|
|
151
|
+
)
|
|
152
|
+
print("---------------------------------------------")
|
|
153
|
+
print("avg decode tok/s:", self.avg_decode_tokens_per_sec)
|
|
154
|
+
print("avg e2e tok/s:", self.avg_end_to_end_tokens_per_sec)
|
|
155
|
+
print("avg prefill ms:", self.avg_prefill_ms)
|
|
156
|
+
print("avg first incremental ms:", self.avg_first_incremental_ms)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class NSOSMamba:
|
|
160
|
+
"""Pure faithful-Mamba NSOS module with streaming incremental by default."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, config: MambaModuleConfig, nsos_ext=None):
|
|
163
|
+
self.nsos = nsos_ext or _load_nsos_ext()
|
|
164
|
+
self.config = config
|
|
165
|
+
if config.seed is not None:
|
|
166
|
+
self.nsos.set_seed(int(config.seed))
|
|
167
|
+
|
|
168
|
+
requested_device = detect_device(self.nsos, config.device)
|
|
169
|
+
self.device = requested_device
|
|
170
|
+
try:
|
|
171
|
+
self.model = self.nsos.JambaModel(self._make_native_config(), requested_device)
|
|
172
|
+
self.model.to(requested_device)
|
|
173
|
+
except Exception:
|
|
174
|
+
if not config.allow_cpu_fallback or requested_device == self.nsos.Device.CPU:
|
|
175
|
+
raise
|
|
176
|
+
self.device = self.nsos.Device.CPU
|
|
177
|
+
self.model = self.nsos.JambaModel(self._make_native_config(), self.device)
|
|
178
|
+
self.model.to(self.device)
|
|
179
|
+
|
|
180
|
+
self.set_streaming(config.streaming)
|
|
181
|
+
|
|
182
|
+
def _make_native_config(self):
|
|
183
|
+
cfg = self.nsos.ModelConfig()
|
|
184
|
+
cfg.num_layers = int(self.config.num_layers)
|
|
185
|
+
cfg.d_model = int(self.config.d_model)
|
|
186
|
+
cfg.vocab_size = int(self.config.vocab_size)
|
|
187
|
+
cfg.n_heads = int(self.config.n_heads)
|
|
188
|
+
cfg.n_kv_heads = int(self.config.n_kv_heads)
|
|
189
|
+
cfg.max_context_tokens = int(self.config.max_context_tokens)
|
|
190
|
+
|
|
191
|
+
cfg.use_moe = False
|
|
192
|
+
cfg.use_kan = False
|
|
193
|
+
cfg.use_ttt = False
|
|
194
|
+
cfg.dropout = 0.0
|
|
195
|
+
|
|
196
|
+
cfg.mamba2_faithful = True
|
|
197
|
+
cfg.mamba_expand = int(self.config.mamba_expand)
|
|
198
|
+
cfg.mamba_head_dim = int(self.config.mamba_head_dim)
|
|
199
|
+
cfg.tie_word_embeddings = bool(self.config.tie_word_embeddings)
|
|
200
|
+
|
|
201
|
+
_safe_set(cfg, "mamba_state_expansion", int(self.config.d_state))
|
|
202
|
+
_safe_set(cfg, "mamba_n_groups", int(self.config.mamba_n_groups))
|
|
203
|
+
|
|
204
|
+
# Disable attention by putting the slot outside any real layer schedule.
|
|
205
|
+
cfg.attention_period = 1_000_000
|
|
206
|
+
cfg.attention_slot = 0
|
|
207
|
+
return cfg
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def vocab_size(self) -> int:
|
|
211
|
+
return int(self.config.vocab_size)
|
|
212
|
+
|
|
213
|
+
def set_streaming(self, enabled: bool = True) -> bool:
|
|
214
|
+
try:
|
|
215
|
+
if enabled and not self.model.supports_streaming_inference():
|
|
216
|
+
self.model.set_streaming_inference(False)
|
|
217
|
+
return False
|
|
218
|
+
self.model.set_streaming_inference(bool(enabled))
|
|
219
|
+
return bool(enabled)
|
|
220
|
+
except Exception:
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def _logits_numpy(self, tensor) -> np.ndarray:
|
|
224
|
+
return np.asarray(tensor.cpu().numpy()).reshape(-1, self.vocab_size)
|
|
225
|
+
|
|
226
|
+
def fit_token_pairs(
|
|
227
|
+
self,
|
|
228
|
+
pairs: Sequence[Tuple[Sequence[int], Sequence[int]]],
|
|
229
|
+
*,
|
|
230
|
+
steps: int = 1200,
|
|
231
|
+
batch_size: int = 16,
|
|
232
|
+
learning_rate: float = 3e-3,
|
|
233
|
+
qat: bool = False,
|
|
234
|
+
seed: int = 123,
|
|
235
|
+
progress_every: Optional[int] = None,
|
|
236
|
+
callback: Optional[Callable[[int, float], None]] = None,
|
|
237
|
+
) -> List[float]:
|
|
238
|
+
if not pairs:
|
|
239
|
+
raise ValueError("fit_token_pairs requires at least one pair")
|
|
240
|
+
|
|
241
|
+
trainer = self.nsos.Trainer(self.model, float(learning_rate))
|
|
242
|
+
trainer.warmup_steps = max(1, min(50, steps // 10 if steps > 10 else 1))
|
|
243
|
+
trainer.total_training_steps = int(steps)
|
|
244
|
+
trainer.first_token_loss_scale = 1.0
|
|
245
|
+
trainer.eos_loss_scale = 1.0
|
|
246
|
+
trainer.phase_scheduler.progressive_qat_enabled = bool(qat)
|
|
247
|
+
|
|
248
|
+
rng = random.Random(seed)
|
|
249
|
+
self.model.set_training_mode(True)
|
|
250
|
+
self.set_streaming(False)
|
|
251
|
+
|
|
252
|
+
losses: List[float] = []
|
|
253
|
+
window = progress_every or max(1, steps // 6)
|
|
254
|
+
running = 0.0
|
|
255
|
+
for step in range(int(steps)):
|
|
256
|
+
prompts: List[List[int]] = []
|
|
257
|
+
answers: List[List[int]] = []
|
|
258
|
+
for _ in range(int(batch_size)):
|
|
259
|
+
prompt, answer = rng.choice(pairs)
|
|
260
|
+
prompts.append(list(prompt))
|
|
261
|
+
answers.append(list(answer))
|
|
262
|
+
loss = float(trainer.train_supervised_batch(prompts, answers))
|
|
263
|
+
losses.append(loss)
|
|
264
|
+
running += loss
|
|
265
|
+
if callback and (step + 1) % window == 0:
|
|
266
|
+
callback(step + 1, running / float(window))
|
|
267
|
+
running = 0.0
|
|
268
|
+
|
|
269
|
+
self.model.set_training_mode(False)
|
|
270
|
+
self.set_streaming(self.config.streaming)
|
|
271
|
+
return losses
|
|
272
|
+
|
|
273
|
+
def fit_text_pairs(
|
|
274
|
+
self,
|
|
275
|
+
pairs: Sequence[TextPair],
|
|
276
|
+
tokenizer: CharTokenizer,
|
|
277
|
+
**kwargs,
|
|
278
|
+
) -> List[float]:
|
|
279
|
+
token_pairs = pairs_to_token_ids(pairs, tokenizer)
|
|
280
|
+
return self.fit_token_pairs(token_pairs, **kwargs)
|
|
281
|
+
|
|
282
|
+
def generate_ids(
|
|
283
|
+
self,
|
|
284
|
+
prompt_ids: Sequence[int],
|
|
285
|
+
*,
|
|
286
|
+
max_new_tokens: int = 128,
|
|
287
|
+
eos_token_id: Optional[int] = None,
|
|
288
|
+
stop_on_eos: bool = True,
|
|
289
|
+
streaming: Optional[bool] = None,
|
|
290
|
+
tokenizer: Optional[CharTokenizer] = None,
|
|
291
|
+
) -> GenerationResult:
|
|
292
|
+
if max_new_tokens <= 0:
|
|
293
|
+
return GenerationResult(
|
|
294
|
+
prompt_ids=list(prompt_ids),
|
|
295
|
+
generated_ids=[],
|
|
296
|
+
text="",
|
|
297
|
+
prefill_ms=0.0,
|
|
298
|
+
decode_ms=0.0,
|
|
299
|
+
ttft_ms=0.0,
|
|
300
|
+
first_incremental_ms=0.0,
|
|
301
|
+
generated_tokens=0,
|
|
302
|
+
decode_tokens_per_sec=0.0,
|
|
303
|
+
end_to_end_tokens_per_sec=0.0,
|
|
304
|
+
streaming=bool(streaming),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
use_streaming = self.config.streaming if streaming is None else bool(streaming)
|
|
308
|
+
self.model.set_training_mode(False)
|
|
309
|
+
self.set_streaming(use_streaming)
|
|
310
|
+
|
|
311
|
+
if use_streaming:
|
|
312
|
+
return self._generate_ids_streaming(
|
|
313
|
+
prompt_ids,
|
|
314
|
+
max_new_tokens=max_new_tokens,
|
|
315
|
+
eos_token_id=eos_token_id,
|
|
316
|
+
stop_on_eos=stop_on_eos,
|
|
317
|
+
tokenizer=tokenizer,
|
|
318
|
+
)
|
|
319
|
+
return self._generate_ids_eager(
|
|
320
|
+
prompt_ids,
|
|
321
|
+
max_new_tokens=max_new_tokens,
|
|
322
|
+
eos_token_id=eos_token_id,
|
|
323
|
+
stop_on_eos=stop_on_eos,
|
|
324
|
+
tokenizer=tokenizer,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def _generate_ids_streaming(
|
|
328
|
+
self,
|
|
329
|
+
prompt_ids: Sequence[int],
|
|
330
|
+
*,
|
|
331
|
+
max_new_tokens: int,
|
|
332
|
+
eos_token_id: Optional[int],
|
|
333
|
+
stop_on_eos: bool,
|
|
334
|
+
tokenizer: Optional[CharTokenizer],
|
|
335
|
+
) -> GenerationResult:
|
|
336
|
+
self.model.reset_session()
|
|
337
|
+
prompt = list(prompt_ids)
|
|
338
|
+
|
|
339
|
+
prefill_t0 = time.perf_counter()
|
|
340
|
+
logits = self._logits_numpy(self.model.forward_ids(prompt))
|
|
341
|
+
prefill_s = time.perf_counter() - prefill_t0
|
|
342
|
+
|
|
343
|
+
next_id = int(np.argmax(logits[-1]))
|
|
344
|
+
generated = [next_id]
|
|
345
|
+
if stop_on_eos and eos_token_id is not None and next_id == eos_token_id:
|
|
346
|
+
text = tokenizer.decode(generated) if tokenizer else None
|
|
347
|
+
return self._result(prompt, generated, text, prefill_s, 0.0, 0.0, True)
|
|
348
|
+
|
|
349
|
+
decode_t0 = time.perf_counter()
|
|
350
|
+
first_incremental_s = 0.0
|
|
351
|
+
for idx in range(1, max_new_tokens):
|
|
352
|
+
step_t0 = time.perf_counter()
|
|
353
|
+
logits = self._logits_numpy(self.model.forward_ids([next_id]))
|
|
354
|
+
if idx == 1:
|
|
355
|
+
first_incremental_s = time.perf_counter() - step_t0
|
|
356
|
+
next_id = int(np.argmax(logits[-1]))
|
|
357
|
+
generated.append(next_id)
|
|
358
|
+
if stop_on_eos and eos_token_id is not None and next_id == eos_token_id:
|
|
359
|
+
break
|
|
360
|
+
decode_s = time.perf_counter() - decode_t0
|
|
361
|
+
text = tokenizer.decode(generated) if tokenizer else None
|
|
362
|
+
return self._result(prompt, generated, text, prefill_s, decode_s, first_incremental_s, True)
|
|
363
|
+
|
|
364
|
+
def _generate_ids_eager(
|
|
365
|
+
self,
|
|
366
|
+
prompt_ids: Sequence[int],
|
|
367
|
+
*,
|
|
368
|
+
max_new_tokens: int,
|
|
369
|
+
eos_token_id: Optional[int],
|
|
370
|
+
stop_on_eos: bool,
|
|
371
|
+
tokenizer: Optional[CharTokenizer],
|
|
372
|
+
) -> GenerationResult:
|
|
373
|
+
prompt = list(prompt_ids)
|
|
374
|
+
seq = list(prompt)
|
|
375
|
+
generated: List[int] = []
|
|
376
|
+
t0 = time.perf_counter()
|
|
377
|
+
first_s = 0.0
|
|
378
|
+
for idx in range(max_new_tokens):
|
|
379
|
+
step_t0 = time.perf_counter()
|
|
380
|
+
logits = self._logits_numpy(self.model.forward_ids(seq))
|
|
381
|
+
if idx == 0:
|
|
382
|
+
first_s = time.perf_counter() - step_t0
|
|
383
|
+
next_id = int(np.argmax(logits[-1]))
|
|
384
|
+
generated.append(next_id)
|
|
385
|
+
seq.append(next_id)
|
|
386
|
+
if stop_on_eos and eos_token_id is not None and next_id == eos_token_id:
|
|
387
|
+
break
|
|
388
|
+
total_s = time.perf_counter() - t0
|
|
389
|
+
text = tokenizer.decode(generated) if tokenizer else None
|
|
390
|
+
return self._result(prompt, generated, text, first_s, max(total_s - first_s, 0.0), first_s, False)
|
|
391
|
+
|
|
392
|
+
def _result(
|
|
393
|
+
self,
|
|
394
|
+
prompt: List[int],
|
|
395
|
+
generated: List[int],
|
|
396
|
+
text: Optional[str],
|
|
397
|
+
prefill_s: float,
|
|
398
|
+
decode_s: float,
|
|
399
|
+
first_incremental_s: float,
|
|
400
|
+
streaming: bool,
|
|
401
|
+
) -> GenerationResult:
|
|
402
|
+
# In streaming mode the first generated token comes from prefill; steady
|
|
403
|
+
# decode throughput counts only tokens produced after prefill.
|
|
404
|
+
steady_tokens = max(len(generated) - 1, 0) if streaming else len(generated)
|
|
405
|
+
decode_tps = steady_tokens / max(decode_s, 1e-9)
|
|
406
|
+
e2e_tps = len(generated) / max(prefill_s + decode_s, 1e-9)
|
|
407
|
+
return GenerationResult(
|
|
408
|
+
prompt_ids=prompt,
|
|
409
|
+
generated_ids=generated,
|
|
410
|
+
text=text,
|
|
411
|
+
prefill_ms=prefill_s * 1000.0,
|
|
412
|
+
decode_ms=decode_s * 1000.0,
|
|
413
|
+
ttft_ms=prefill_s * 1000.0,
|
|
414
|
+
first_incremental_ms=first_incremental_s * 1000.0,
|
|
415
|
+
generated_tokens=len(generated),
|
|
416
|
+
decode_tokens_per_sec=decode_tps,
|
|
417
|
+
end_to_end_tokens_per_sec=e2e_tps,
|
|
418
|
+
streaming=streaming,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def generate_text(
|
|
422
|
+
self,
|
|
423
|
+
prompt: str,
|
|
424
|
+
tokenizer: CharTokenizer,
|
|
425
|
+
*,
|
|
426
|
+
max_new_tokens: int = 128,
|
|
427
|
+
stop_on_eos: bool = True,
|
|
428
|
+
) -> GenerationResult:
|
|
429
|
+
prompt_ids = tokenizer.encode(prompt, bos=True, eos=False)
|
|
430
|
+
return self.generate_ids(
|
|
431
|
+
prompt_ids,
|
|
432
|
+
max_new_tokens=max_new_tokens,
|
|
433
|
+
eos_token_id=tokenizer.eos_id,
|
|
434
|
+
stop_on_eos=stop_on_eos,
|
|
435
|
+
tokenizer=tokenizer,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
def benchmark_decode(
|
|
439
|
+
self,
|
|
440
|
+
prompt_ids: Sequence[int],
|
|
441
|
+
*,
|
|
442
|
+
max_new_tokens: int = 256,
|
|
443
|
+
repeats: int = 5,
|
|
444
|
+
warmup_tokens: int = 8,
|
|
445
|
+
eos_token_id: Optional[int] = None,
|
|
446
|
+
) -> DecodeBenchmark:
|
|
447
|
+
# Warmup: one short streaming run, no EOS stop.
|
|
448
|
+
self.generate_ids(
|
|
449
|
+
prompt_ids,
|
|
450
|
+
max_new_tokens=max(1, warmup_tokens),
|
|
451
|
+
eos_token_id=eos_token_id,
|
|
452
|
+
stop_on_eos=False,
|
|
453
|
+
streaming=True,
|
|
454
|
+
)
|
|
455
|
+
runs = [
|
|
456
|
+
self.generate_ids(
|
|
457
|
+
prompt_ids,
|
|
458
|
+
max_new_tokens=max_new_tokens,
|
|
459
|
+
eos_token_id=eos_token_id,
|
|
460
|
+
stop_on_eos=False,
|
|
461
|
+
streaming=True,
|
|
462
|
+
)
|
|
463
|
+
for _ in range(repeats)
|
|
464
|
+
]
|
|
465
|
+
return DecodeBenchmark(runs)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable, List, Mapping, Sequence, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class TextPair:
|
|
11
|
+
"""One supervised prompt -> answer example."""
|
|
12
|
+
|
|
13
|
+
prompt: str
|
|
14
|
+
answer: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CharTokenizer:
|
|
18
|
+
"""Small deterministic char-level tokenizer for demos and probes.
|
|
19
|
+
|
|
20
|
+
It is deliberately simple. Production training should use the project's
|
|
21
|
+
tokenizer pack, but this class is enough for synthetic tasks such as:
|
|
22
|
+
"Quem é você?" -> "Oxta".
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
pad_token = "<PAD>"
|
|
26
|
+
bos_token = "<BOS>"
|
|
27
|
+
eos_token = "<EOS>"
|
|
28
|
+
|
|
29
|
+
def __init__(self, tokens: Sequence[str]):
|
|
30
|
+
unique = list(dict.fromkeys(tokens))
|
|
31
|
+
required = [self.pad_token, self.bos_token, self.eos_token]
|
|
32
|
+
merged = required + [t for t in unique if t not in required]
|
|
33
|
+
self.tokens: List[str] = merged
|
|
34
|
+
self.stoi = {tok: idx for idx, tok in enumerate(self.tokens)}
|
|
35
|
+
self.itos = {idx: tok for tok, idx in self.stoi.items()}
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_texts(cls, texts: Iterable[str]) -> "CharTokenizer":
|
|
39
|
+
chars = sorted({ch for text in texts for ch in text})
|
|
40
|
+
return cls([cls.pad_token, cls.bos_token, cls.eos_token, *chars])
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def pad_id(self) -> int:
|
|
44
|
+
return self.stoi[self.pad_token]
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def bos_id(self) -> int:
|
|
48
|
+
return self.stoi[self.bos_token]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def eos_id(self) -> int:
|
|
52
|
+
return self.stoi[self.eos_token]
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def vocab_size(self) -> int:
|
|
56
|
+
return len(self.tokens)
|
|
57
|
+
|
|
58
|
+
def encode(self, text: str, *, bos: bool = False, eos: bool = False) -> List[int]:
|
|
59
|
+
ids: List[int] = []
|
|
60
|
+
if bos:
|
|
61
|
+
ids.append(self.bos_id)
|
|
62
|
+
for ch in text:
|
|
63
|
+
if ch not in self.stoi:
|
|
64
|
+
raise KeyError(f"character not in tokenizer vocabulary: {ch!r}")
|
|
65
|
+
ids.append(self.stoi[ch])
|
|
66
|
+
if eos:
|
|
67
|
+
ids.append(self.eos_id)
|
|
68
|
+
return ids
|
|
69
|
+
|
|
70
|
+
def decode(self, ids: Iterable[int], *, stop_at_eos: bool = True) -> str:
|
|
71
|
+
out: List[str] = []
|
|
72
|
+
for token_id in ids:
|
|
73
|
+
token_id = int(token_id)
|
|
74
|
+
if token_id == self.eos_id and stop_at_eos:
|
|
75
|
+
break
|
|
76
|
+
if token_id in (self.pad_id, self.bos_id, self.eos_id):
|
|
77
|
+
continue
|
|
78
|
+
out.append(self.itos.get(token_id, ""))
|
|
79
|
+
return "".join(out)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_text_pairs(pairs: Iterable[Tuple[str, str] | Mapping[str, str] | TextPair]) -> List[TextPair]:
|
|
83
|
+
"""Normalize in-memory examples into ``TextPair`` objects."""
|
|
84
|
+
|
|
85
|
+
normalized: List[TextPair] = []
|
|
86
|
+
for item in pairs:
|
|
87
|
+
if isinstance(item, TextPair):
|
|
88
|
+
normalized.append(item)
|
|
89
|
+
elif isinstance(item, Mapping):
|
|
90
|
+
normalized.append(TextPair(str(item["prompt"]), str(item["answer"])))
|
|
91
|
+
else:
|
|
92
|
+
prompt, answer = item
|
|
93
|
+
normalized.append(TextPair(str(prompt), str(answer)))
|
|
94
|
+
return normalized
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_text_pairs_json(
|
|
98
|
+
path: str | Path,
|
|
99
|
+
*,
|
|
100
|
+
prompt_key: str = "prompt",
|
|
101
|
+
answer_key: str = "answer",
|
|
102
|
+
) -> List[TextPair]:
|
|
103
|
+
"""Load examples from a JSON file.
|
|
104
|
+
|
|
105
|
+
Accepted shapes:
|
|
106
|
+
|
|
107
|
+
- ``[{"prompt": "...", "answer": "..."}]``
|
|
108
|
+
- ``{"data": [{"prompt": "...", "answer": "..."}]}``
|
|
109
|
+
- ``{"examples": [{"prompt": "...", "answer": "..."}]}``
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
raw = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
113
|
+
if isinstance(raw, Mapping):
|
|
114
|
+
if "data" in raw:
|
|
115
|
+
raw = raw["data"]
|
|
116
|
+
elif "examples" in raw:
|
|
117
|
+
raw = raw["examples"]
|
|
118
|
+
if not isinstance(raw, list):
|
|
119
|
+
raise ValueError("JSON dataset must be a list or an object with data/examples")
|
|
120
|
+
return [
|
|
121
|
+
TextPair(prompt=str(row[prompt_key]), answer=str(row[answer_key]))
|
|
122
|
+
for row in raw
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_text_pairs_jsonl(
|
|
127
|
+
path: str | Path,
|
|
128
|
+
*,
|
|
129
|
+
prompt_key: str = "prompt",
|
|
130
|
+
answer_key: str = "answer",
|
|
131
|
+
) -> List[TextPair]:
|
|
132
|
+
"""Load one ``{"prompt": "...", "answer": "..."}`` object per line."""
|
|
133
|
+
|
|
134
|
+
examples: List[TextPair] = []
|
|
135
|
+
for line_no, line in enumerate(Path(path).read_text(encoding="utf-8").splitlines(), 1):
|
|
136
|
+
stripped = line.strip()
|
|
137
|
+
if not stripped:
|
|
138
|
+
continue
|
|
139
|
+
row = json.loads(stripped)
|
|
140
|
+
if not isinstance(row, Mapping):
|
|
141
|
+
raise ValueError(f"JSONL line {line_no} is not an object")
|
|
142
|
+
examples.append(TextPair(prompt=str(row[prompt_key]), answer=str(row[answer_key])))
|
|
143
|
+
return examples
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def pairs_to_token_ids(
|
|
147
|
+
pairs: Sequence[TextPair],
|
|
148
|
+
tokenizer: CharTokenizer,
|
|
149
|
+
*,
|
|
150
|
+
add_bos: bool = True,
|
|
151
|
+
add_eos: bool = True,
|
|
152
|
+
) -> List[Tuple[List[int], List[int]]]:
|
|
153
|
+
"""Convert text pairs to NSOS supervised prompt/answer token lists."""
|
|
154
|
+
|
|
155
|
+
return [
|
|
156
|
+
(
|
|
157
|
+
tokenizer.encode(pair.prompt, bos=add_bos, eos=False),
|
|
158
|
+
tokenizer.encode(pair.answer, bos=False, eos=add_eos),
|
|
159
|
+
)
|
|
160
|
+
for pair in pairs
|
|
161
|
+
]
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nsos-mamba
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Wrapper Python isolado para o módulo Mamba do NSOS com streaming incremental padrão.
|
|
5
|
+
Author: Oxta
|
|
6
|
+
Project-URL: Homepage, https://github.com/vitorGgC569/reimagined
|
|
7
|
+
Project-URL: Documentation, https://github.com/vitorGgC569/reimagined/tree/nsos-gpu-phases12/OXN/nsos/docs
|
|
8
|
+
Project-URL: Source, https://github.com/vitorGgC569/reimagined
|
|
9
|
+
Keywords: mamba,ssm,nsos,edge-ai,streaming
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: numpy>=1.23
|
|
21
|
+
|
|
22
|
+
# NSOS Mamba
|
|
23
|
+
|
|
24
|
+
`nsos-mamba` é um wrapper Python simples para usar apenas o módulo Mamba do
|
|
25
|
+
NSOS. Ele configura automaticamente o modelo em modo Mamba-2 faithful, ativa
|
|
26
|
+
streaming incremental por padrão e oferece utilitários pequenos para treinar
|
|
27
|
+
pares `prompt -> resposta`.
|
|
28
|
+
|
|
29
|
+
Este pacote não instala o runtime nativo `nsos_ext`. Antes de usar, compile ou
|
|
30
|
+
copie o `nsos_ext.so`/`nsos_ext.pyd` e deixe-o acessível via `PYTHONPATH` ou
|
|
31
|
+
pela variável `NSOS_EXT_PATH`.
|
|
32
|
+
|
|
33
|
+
## Instalação
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install nsos-mamba
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Se o `nsos_ext` estiver em uma pasta separada:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export NSOS_EXT_PATH=/caminho/para/nsos_ext
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
No Colab:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import sys, os
|
|
49
|
+
|
|
50
|
+
sys.path.insert(0, "/content/nsos_ext_mambavs")
|
|
51
|
+
# ou:
|
|
52
|
+
os.environ["NSOS_EXT_PATH"] = "/content/nsos_ext_mambavs"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configurações principais
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from nsos_mamba import MambaModuleConfig
|
|
59
|
+
|
|
60
|
+
cfg = MambaModuleConfig(
|
|
61
|
+
vocab_size=128,
|
|
62
|
+
num_layers=12,
|
|
63
|
+
d_model=128,
|
|
64
|
+
device="auto",
|
|
65
|
+
streaming=True,
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Campos mais usados:
|
|
70
|
+
|
|
71
|
+
- `vocab_size`: tamanho do vocabulário.
|
|
72
|
+
- `num_layers`: número de camadas Mamba.
|
|
73
|
+
- `d_model`: largura do modelo.
|
|
74
|
+
- `d_state`: tamanho do estado SSM. Padrão: `64`.
|
|
75
|
+
- `max_context_tokens`: limite de contexto. Padrão: `4096`.
|
|
76
|
+
- `device`: `"auto"`, `"gpu"`, `"cuda"` ou `"cpu"`.
|
|
77
|
+
- `streaming`: ativa decode incremental. Padrão: `True`.
|
|
78
|
+
- `allow_cpu_fallback`: se a GPU falhar, cai para CPU. Padrão: `True`.
|
|
79
|
+
|
|
80
|
+
Variáveis de ambiente equivalentes:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
NSOS_MAMBA_DEVICE=auto
|
|
84
|
+
NSOS_MAMBA_STREAMING=1
|
|
85
|
+
NSOS_MAMBA_LAYERS=12
|
|
86
|
+
NSOS_MAMBA_DMODEL=128
|
|
87
|
+
NSOS_MAMBA_DSTATE=64
|
|
88
|
+
NSOS_MAMBA_CONTEXT=4096
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Exemplo mínimo: "Quem é você?" -> "Oxta"
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from nsos_mamba import CharTokenizer, MambaModuleConfig, NSOSMamba, TextPair
|
|
95
|
+
|
|
96
|
+
pairs = [
|
|
97
|
+
TextPair("Quem é você?", "Oxta"),
|
|
98
|
+
TextPair("quem é você?", "Oxta"),
|
|
99
|
+
TextPair("Quem e voce?", "Oxta"),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
tok = CharTokenizer.from_texts(
|
|
103
|
+
[p.prompt for p in pairs] + [p.answer for p in pairs]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
cfg = MambaModuleConfig(
|
|
107
|
+
vocab_size=tok.vocab_size,
|
|
108
|
+
num_layers=12,
|
|
109
|
+
d_model=128,
|
|
110
|
+
device="auto",
|
|
111
|
+
streaming=True,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
mamba = NSOSMamba(cfg)
|
|
115
|
+
|
|
116
|
+
mamba.fit_text_pairs(
|
|
117
|
+
pairs,
|
|
118
|
+
tok,
|
|
119
|
+
steps=1200,
|
|
120
|
+
batch_size=16,
|
|
121
|
+
learning_rate=3e-3,
|
|
122
|
+
callback=lambda step, loss: print(f"[train] step {step} loss~{loss:.4f}"),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
result = mamba.generate_text("Quem é você?", tok, max_new_tokens=8)
|
|
126
|
+
|
|
127
|
+
print("resposta:", repr(result.text))
|
|
128
|
+
print("tokens/s:", result.decode_tokens_per_sec)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Saída esperada:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
resposta: 'Oxta'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Streaming incremental
|
|
138
|
+
|
|
139
|
+
O streaming incremental é o padrão. O fluxo usado é:
|
|
140
|
+
|
|
141
|
+
1. processa o prompt uma vez (`prefill`);
|
|
142
|
+
2. gera cada token novo passando apenas o último token;
|
|
143
|
+
3. reaproveita o estado interno do modelo.
|
|
144
|
+
|
|
145
|
+
Benchmark:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
prompt_ids = tok.encode("Quem é você?", bos=True)
|
|
149
|
+
bench = mamba.benchmark_decode(prompt_ids, max_new_tokens=1024, repeats=5)
|
|
150
|
+
bench.print()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Métricas:
|
|
154
|
+
|
|
155
|
+
- `prefill_ms`: tempo para processar o prompt.
|
|
156
|
+
- `first_incremental_ms`: primeiro passo incremental após o prefill.
|
|
157
|
+
- `decode_tokens_per_sec`: tokens/s do decode incremental.
|
|
158
|
+
- `end_to_end_tokens_per_sec`: tokens/s incluindo prefill.
|
|
159
|
+
|
|
160
|
+
## Datasets
|
|
161
|
+
|
|
162
|
+
### Lista em memória
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from nsos_mamba import TextPair
|
|
166
|
+
|
|
167
|
+
pairs = [
|
|
168
|
+
TextPair(prompt="Quem é você?", answer="Oxta"),
|
|
169
|
+
TextPair(prompt="Qual seu nome?", answer="Oxta"),
|
|
170
|
+
]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### JSON
|
|
174
|
+
|
|
175
|
+
Arquivo `dataset.json`:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
[
|
|
179
|
+
{"prompt": "Quem é você?", "answer": "Oxta"},
|
|
180
|
+
{"prompt": "Qual seu nome?", "answer": "Oxta"}
|
|
181
|
+
]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Uso:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from nsos_mamba import load_text_pairs_json
|
|
188
|
+
|
|
189
|
+
pairs = load_text_pairs_json("dataset.json")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Também aceita:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{"data": [{"prompt": "...", "answer": "..."}]}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
ou:
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{"examples": [{"prompt": "...", "answer": "..."}]}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### JSONL
|
|
205
|
+
|
|
206
|
+
Arquivo `dataset.jsonl`:
|
|
207
|
+
|
|
208
|
+
```jsonl
|
|
209
|
+
{"prompt": "Quem é você?", "answer": "Oxta"}
|
|
210
|
+
{"prompt": "Qual seu nome?", "answer": "Oxta"}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Uso:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from nsos_mamba import load_text_pairs_jsonl
|
|
217
|
+
|
|
218
|
+
pairs = load_text_pairs_jsonl("dataset.jsonl")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Observações
|
|
222
|
+
|
|
223
|
+
- `CharTokenizer` é para testes, demos e dados sintéticos pequenos.
|
|
224
|
+
- Para treino real, use um tokenizer próprio do projeto.
|
|
225
|
+
- O pacote expõe o Mamba isolado; não ativa Attention, MoE, KAN, TTT, CHRASS,
|
|
226
|
+
MCTS ou memória externa.
|
|
227
|
+
- O pacote é uma camada de uso sobre `nsos_ext`, não uma reimplementação em
|
|
228
|
+
Python.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
nsos_mamba/__init__.py
|
|
4
|
+
nsos_mamba/core.py
|
|
5
|
+
nsos_mamba/datasets.py
|
|
6
|
+
nsos_mamba.egg-info/PKG-INFO
|
|
7
|
+
nsos_mamba.egg-info/SOURCES.txt
|
|
8
|
+
nsos_mamba.egg-info/dependency_links.txt
|
|
9
|
+
nsos_mamba.egg-info/requires.txt
|
|
10
|
+
nsos_mamba.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy>=1.23
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nsos_mamba
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nsos-mamba"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Wrapper Python isolado para o módulo Mamba do NSOS com streaming incremental padrão."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Oxta" }
|
|
13
|
+
]
|
|
14
|
+
keywords = ["mamba", "ssm", "nsos", "edge-ai", "streaming"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"numpy>=1.23",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/vitorGgC569/reimagined"
|
|
31
|
+
Documentation = "https://github.com/vitorGgC569/reimagined/tree/nsos-gpu-phases12/OXN/nsos/docs"
|
|
32
|
+
Source = "https://github.com/vitorGgC569/reimagined"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
packages = ["nsos_mamba"]
|