atomicng 1.0.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.
atomicng/__init__.py ADDED
@@ -0,0 +1,159 @@
1
+ """atomicng — forward-only continual writing into a language model's native n-gram memory.
2
+
3
+ Метод статьи arXiv:2610.31125: T = T⁰ + Δ, запись только forward-проходами,
4
+ очистка Δ побитово возвращает исходную модель.
5
+
6
+ Публичный API v0.2a (Core — чистый NumPy; torch-слой импортируется лениво):
7
+ model = atomicng.TinyModel() # бэкенд для CI; HF — через atomicng.backends.hf
8
+ report = atomicng.write(model, evidence, policies=..., metrics=...)
9
+ model.memory.save_pretrained("out.atomicng")
10
+ atomicng.publish_check(model) # safety-gate перед публикацией
11
+ """
12
+ import time
13
+
14
+ from . import specs
15
+ from .backends.tiny import TinyModel
16
+ from .bench import check_regression, run_bench, save_baseline
17
+ from .benchmarks import acquisition, chain, distraction, run_suite
18
+ from .evidence import Block, ByteTokenizer, build_blocks, build_blocks_from_texts
19
+ from .exceptions import (
20
+ AtomicNGError,
21
+ BackendError,
22
+ BudgetExceeded,
23
+ GuardViolation,
24
+ OverlayError,
25
+ PublishRejected,
26
+ SpecError,
27
+ )
28
+ from .exporters import export_ple, load_ple
29
+ from .guards import WritePolicies
30
+ from .journal import AuditLog, UndoJournal
31
+ from .memory import MemoryPatch
32
+ from .metrics import JsonlExporter, MetricsRegistry
33
+ from .monitor_html import build_report, load_metrics_jsonl, write_report
34
+ from .publishing import PublishConfig, publish_check
35
+ from .rules.d1 import D1Rule, Event, WriteReport
36
+ from .signing import generate_keypair, sign_file, verify_file
37
+ from .specs import SIDECAR_SMALL, TableSpec
38
+ from .testing import HashWriter
39
+ from .verify import VerifyReport, verify_deletion
40
+ from .vllm_bridge import export_for_vllm, merged_table, vllm_general_plugins
41
+
42
+ __version__ = "1.0.0"
43
+
44
+ __all__ = [
45
+ "AuditLog",
46
+ "AtomicNGError",
47
+ "BackendError",
48
+ "Block",
49
+ "BudgetExceeded",
50
+ "ByteTokenizer",
51
+ "D1Rule",
52
+ "Event",
53
+ "GuardViolation",
54
+ "HashWriter",
55
+ "JsonlExporter",
56
+ "MemoryPatch",
57
+ "MetricsRegistry",
58
+ "OverlayError",
59
+ "PublishConfig",
60
+ "PublishRejected",
61
+ "SIDECAR_SMALL",
62
+ "SpecError",
63
+ "TableSpec",
64
+ "TinyModel",
65
+ "UndoJournal",
66
+ "VerifyReport",
67
+ "WritePolicies",
68
+ "WriteReport",
69
+ "acquisition",
70
+ "attach_memory",
71
+ "build_blocks",
72
+ "build_blocks_from_texts",
73
+ "build_report",
74
+ "export_for_vllm",
75
+ "chain",
76
+ "check_regression",
77
+ "clear_memory",
78
+ "distraction",
79
+ "export_ple",
80
+ "generate_keypair",
81
+ "load_metrics_jsonl",
82
+ "load_ple",
83
+ "merged_table",
84
+ "publish_check",
85
+ "revert_last",
86
+ "run_bench",
87
+ "run_suite",
88
+ "save_baseline",
89
+ "sign_file",
90
+ "specs",
91
+ "verify_deletion",
92
+ "verify_file",
93
+ "vllm_general_plugins",
94
+ "write",
95
+ "write_report",
96
+ "__version__",
97
+ ]
98
+
99
+
100
+ def attach_memory(model, spec: TableSpec | None = None):
101
+ """Подключает разреженный патч Δ к модели (у TinyModel она уже есть)."""
102
+ if getattr(model, "memory", None) is None:
103
+ model.memory = MemoryPatch(spec or model.spec)
104
+ return model
105
+
106
+
107
+ def write(
108
+ model,
109
+ evidence,
110
+ writer: HashWriter | None = None,
111
+ rule: D1Rule | None = None,
112
+ policies: WritePolicies | None = None,
113
+ audit: AuditLog | None = None,
114
+ undo: UndoJournal | None = None,
115
+ metrics: MetricsRegistry | None = None,
116
+ source: str | None = None,
117
+ ) -> WriteReport:
118
+ """Записывает свидетельство (str или список строк) в память. Только forward-вычисления.
119
+
120
+ policies: guards (бюджеты/капы); audit: JSONL-журнал (S4);
121
+ undo: pre-image журнал для точного отката; metrics: реестр мониторинга;
122
+ source: идентификатор источника в provenance оверлея.
123
+ """
124
+ texts = [evidence] if isinstance(evidence, str) else list(evidence)
125
+ if not texts:
126
+ raise ValueError("пустое свидетельство")
127
+ writer = writer or HashWriter(model.memory.spec)
128
+ rule = rule or D1Rule()
129
+ events = writer.events(texts)
130
+ if undo is not None:
131
+ undo.record(model.memory, {e.row_id for e in events})
132
+ t0 = time.perf_counter()
133
+ report = rule.apply(model.memory, events, policies)
134
+ elapsed_ms = (time.perf_counter() - t0) * 1000.0
135
+ if source:
136
+ model.memory.sources.append(source)
137
+ if metrics is not None:
138
+ metrics.record_write(report, model.memory.stats(), elapsed_ms)
139
+ if audit is not None:
140
+ audit.append(
141
+ "write",
142
+ {
143
+ **report.to_dict(),
144
+ "source": source,
145
+ "patch_checksum": model.memory.checksum(),
146
+ "duration_ms": round(elapsed_ms, 3),
147
+ },
148
+ )
149
+ return report
150
+
151
+
152
+ def revert_last(model, undo: UndoJournal) -> list[int]:
153
+ """Точный откат последнего блока записи (pre-image journal)."""
154
+ return undo.revert_last(model.memory)
155
+
156
+
157
+ def clear_memory(model) -> None:
158
+ """Kill-switch: Δ=0, модель структурно равна базовой."""
159
+ model.memory.clear()
@@ -0,0 +1,9 @@
1
+ """Engine-адаптеры (слой чтения таблицы моделью).
2
+
3
+ v0.1: tiny — детерминированная CPU-модель для CI.
4
+ v0.2: torch/HF attach. v0.4: vLLM-ридер, llama.cpp PLE-экспорт.
5
+ """
6
+
7
+ from .tiny import TinyModel
8
+
9
+ __all__ = ["TinyModel"]
@@ -0,0 +1,248 @@
1
+ """Sidecar-attach к HF-моделям (torch): инжект чтения T⁰+Δ в residual stream.
2
+
3
+ Поддерживаются causal-LM структуры Llama/Qwen/Mistral-стиля: `model.model.layers[i]`,
4
+ `model.model.norm` перед lm_head. Хук на входе слоя `inject_layer` добавляет проекцию
5
+ прочитанных строк таблицы; хук на `model.model.norm` захватывает Ĥ (вход lm_head)
6
+ для признаков writer'а. Δ хранится в ядре (MemoryPatch, NumPy) — запись only-forward,
7
+ в градиенты модели она не вмешивается.
8
+
9
+ Ограничение v0.2a: sidecar-T⁰ детерминирована от seed (seed сохраняется в provenance
10
+ оверлея), перенос оверлея между машинами требует того же seed. Нативные заводские
11
+ таблицы — Track B (v1.0).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import contextlib
16
+ from types import SimpleNamespace
17
+
18
+ import numpy as np
19
+
20
+ from ..exceptions import BackendError
21
+ from ..memory import MemoryPatch
22
+ from ..rules.d1 import D1Rule
23
+ from ..specs import SIDECAR_SMALL, TableSpec
24
+
25
+
26
+ def _require_torch():
27
+ try:
28
+ import torch # noqa: PLC0415
29
+ return torch
30
+ except ImportError as exc:
31
+ raise BackendError("backends.hf требует torch: pip install 'atomicng[torch]'") from exc
32
+
33
+
34
+ def bigram_rows(input_ids: list[int], n_rows: int) -> list[int]:
35
+ from .tiny import TinyModel # noqa: PLC0415
36
+
37
+ return [TinyModel.row_hash(input_ids[i - 1], input_ids[i], n_rows) for i in range(1, len(input_ids))]
38
+
39
+
40
+ class SidecarTable:
41
+ """T⁰ (детерминированная от seed, заморожена) + проекция в hidden_size модели."""
42
+
43
+ def __init__(self, spec: TableSpec, hidden_size: int, seed: int = 17) -> None:
44
+ torch = _require_torch()
45
+ self.spec = spec
46
+ self.hidden_size = hidden_size
47
+ self.seed = seed
48
+ gen = torch.Generator().manual_seed(seed)
49
+ self.t0 = 0.02 * torch.randn(spec.n_rows, spec.row_width, generator=gen) # T⁰, иммутабельна
50
+ self.proj = torch.nn.Linear(spec.row_width * spec.n_heads, hidden_size, bias=False)
51
+ with torch.no_grad():
52
+ self.proj.weight.mul_(0.02) # стартовое влияние памяти мало (ρ-статья)
53
+
54
+ def to(self, device: str) -> SidecarTable:
55
+ """Переносит T⁰ и проекцию на устройство (для тренировки на GPU/TPU-VM)."""
56
+ torch = _require_torch()
57
+ self.t0 = self.t0.to(device)
58
+ self.proj = self.proj.to(torch.device(device))
59
+ return self
60
+
61
+ def read(self, input_ids: list[int], memory: MemoryPatch) -> np.ndarray:
62
+ """(T, d): строки T⁰+Δ для биграмм последовательности; позиция 0 — нули."""
63
+ t = len(input_ids)
64
+ out = np.zeros((t, self.spec.row_width), dtype=np.float32)
65
+ for i, row in enumerate(bigram_rows(input_ids, self.spec.n_rows), start=1):
66
+ t0_row = self.t0[row]
67
+ if t0_row.is_cuda:
68
+ t0_row = t0_row.detach().cpu()
69
+ out[i] = t0_row.numpy() + memory.get(row)
70
+ return out
71
+
72
+ def inject(self, input_ids: list[int], memory: MemoryPatch):
73
+ """→ тензор (T, hidden) для сложения с hidden states слоя (на устройстве proj)."""
74
+ torch = _require_torch()
75
+ read = torch.from_numpy(self.read(input_ids, memory))
76
+ return self.proj(read.to(self.proj.weight.device))
77
+
78
+
79
+ class SidecarHandle:
80
+ """Хуки чтения + запись свидетельств только forward-проходами."""
81
+
82
+ def __init__(self, model, spec: TableSpec | None = None, inject_layer: int = 2, seed: int = 17) -> None:
83
+ torch = _require_torch()
84
+ self.torch = torch
85
+ self.spec = spec or SIDECAR_SMALL
86
+ self.inject_layer = inject_layer
87
+ try:
88
+ self.inner = model.model # LlamaForCausalLM-стиль
89
+ self.layers = model.model.layers
90
+ self.norm = model.model.norm
91
+ except AttributeError as exc:
92
+ raise BackendError(
93
+ "attach_sidecar: не нашёл model.model.layers — поддержаны causal-LM "
94
+ "Llama/Qwen/Mistral-стиля; для своей архитектуры реализуйте хуки вручную"
95
+ ) from exc
96
+ hidden_size = int(model.config.hidden_size)
97
+ self.table = SidecarTable(self.spec, hidden_size, seed=seed)
98
+ self.memory = MemoryPatch(self.spec, created_by=f"hf-sidecar:seed={seed}")
99
+ self.model_ref = model
100
+ self._ids: list[int] | None = None
101
+ self.last_hidden: np.ndarray | None = None
102
+ self._hooks = [
103
+ self.layers[inject_layer].register_forward_pre_hook(self._inject_hook, with_kwargs=True),
104
+ self.norm.register_forward_pre_hook(self._capture_hook, with_kwargs=True),
105
+ ]
106
+
107
+ # ------------------------------------------------------------------ хуки
108
+
109
+ def _inject_hook(self, module, args, kwargs):
110
+ h = args[0]
111
+ if self._ids is None:
112
+ return args, kwargs
113
+ mem = self.table.inject(self._ids, self.memory).to(h.dtype).to(h.device)
114
+ if h.dim() == 3: # (B, T, Hd): одна сессия — батч 1
115
+ mem = mem.unsqueeze(0)
116
+ return (h + mem,) + tuple(args[1:]), kwargs
117
+
118
+ def _capture_hook(self, module, args, kwargs):
119
+ h = args[0]
120
+ self.last_hidden = h.detach().to("cpu").float().numpy()
121
+
122
+ # --------------------------------------------------------------- сессии
123
+
124
+ @contextlib.contextmanager
125
+ def write_session(self, input_ids: list[int]):
126
+ """Задаёт последовательность для чтения таблицы на время одного forward."""
127
+ prev, self._ids = self._ids, list(input_ids)
128
+ try:
129
+ yield self
130
+ finally:
131
+ self._ids = prev
132
+
133
+ def forward_hidden(self, input_ids: list[int]):
134
+ """Forward с чтением памяти; возвращает Ĥ (T, Hd) — вход lm_head."""
135
+ torch = self.torch
136
+ was_training = self.model_ref.training
137
+ self.model_ref.eval()
138
+ dev = next(self.model_ref.parameters()).device
139
+ with torch.no_grad():
140
+ with self.write_session(input_ids):
141
+ outputs = self.model_ref(input_ids=torch.tensor([input_ids], dtype=torch.long, device=dev))
142
+ if was_training:
143
+ self.model_ref.train()
144
+ if outputs.hidden_states is not None:
145
+ return outputs.hidden_states[-1][0].numpy()
146
+ if self.last_hidden is None:
147
+ raise BackendError("модель не вернула hidden_states и хук Ĥ не сработал")
148
+ return self.last_hidden[0]
149
+
150
+ # ---------------------------------------------------------------- запись
151
+
152
+ def write(self, input_ids: list[int], writer, rule: D1Rule | None = None,
153
+ memory_aware: bool = True) -> dict:
154
+ """Запись свидетельства: Ĥ → writer → цели/гейты → D1 в ядро. Только forward.
155
+
156
+ memory_aware=True — delta-вариант статьи (кодирование читает текущую Δ);
157
+ memory_aware=False — plain-вариант: кодирование на T⁰-only модели
158
+ (writer видит нулевые Δ и счётчики), запись в память при этом обычная.
159
+ """
160
+ from ..rules.d1 import Event # noqa: PLC0415
161
+
162
+ hidden = self.forward_hidden(input_ids) # (T, Hd) — ПОЛНАЯ последовательность
163
+ rows = bigram_rows(input_ids, self.spec.n_rows) # (T-1,)
164
+ th = self.torch.from_numpy(hidden[None]) # (1, T, Hd): writer сам срежет позицию 0
165
+ t_rows = self.torch.tensor([rows], dtype=self.torch.long)
166
+ if memory_aware:
167
+ delta_rows = self.torch.stack(
168
+ [self.torch.from_numpy(self.memory.get(r)) for r in rows]
169
+ )[None]
170
+ counts = self.torch.tensor(
171
+ [[float(self.memory.count(r)) for r in rows]], dtype=self.torch.float32
172
+ )
173
+ else:
174
+ zero = self.torch.zeros(1, len(rows), self.spec.row_width)
175
+ delta_rows, counts = zero, self.torch.zeros(1, len(rows))
176
+ with self.torch.no_grad(): # запись only-forward: writer не обучается здесь
177
+ targets, gates, _ = writer(th, t_rows, delta_rows, counts)
178
+ events = [
179
+ Event(row_id=rows[i], target=targets[0, i].numpy(), gate=float(gates[0, i]))
180
+ for i in range(len(rows))
181
+ ]
182
+ report = (rule or D1Rule()).apply(self.memory, events)
183
+ self.memory.sources.append(f"hf-write:{len(input_ids)}tok")
184
+ return report.to_dict()
185
+
186
+ def nll(self, input_ids_list: list[list[int]]) -> float:
187
+ """Средний NLL (nats/token) последовательностей при текущей памяти T⁰+Δ."""
188
+ total, count = 0.0, 0
189
+ for ids in input_ids_list:
190
+ if len(ids) < 2:
191
+ continue
192
+ with self.write_session(ids), self.torch.no_grad():
193
+ dev = next(self.model_ref.parameters()).device
194
+ out = self.model_ref(input_ids=self.torch.tensor([ids], dtype=self.torch.long, device=dev))
195
+ logits = out.logits[0].float()
196
+ logprobs = self.torch.log_softmax(logits[:-1], dim=-1)
197
+ targets = self.torch.tensor(ids[1:], dtype=self.torch.long, device=logits.device)
198
+ total -= float(logprobs[self.torch.arange(len(ids) - 1, device=logits.device), targets].sum())
199
+ count += len(ids) - 1
200
+ return total / max(count, 1)
201
+
202
+ # --------------------------------------------------------------- оверлеи
203
+
204
+ def save_overlay(self, path: str) -> None:
205
+ self.memory.save_pretrained(path)
206
+
207
+ def detach(self) -> None:
208
+ for h in self._hooks:
209
+ h.remove()
210
+ self._hooks.clear()
211
+
212
+
213
+ def attach_sidecar(model, spec: TableSpec | None = None, inject_layer: int = 2, seed: int = 17) -> SidecarHandle:
214
+ """Подключает sidecar-таблицу к HF-модели (или совместимому стабу) и возвращает handle."""
215
+ return SidecarHandle(model, spec=spec, inject_layer=inject_layer, seed=seed)
216
+
217
+
218
+ def stub_causal_lm(hidden_size: int = 32, vocab_size: int = 64, n_layers: int = 4, seed: int = 3):
219
+ """Мини-стуб causal-LM структуры HF (для тестов attach без скачивания весов)."""
220
+ torch = _require_torch()
221
+ gen = torch.Generator().manual_seed(seed)
222
+
223
+ class StubLayer(torch.nn.Module):
224
+ def forward(self, h):
225
+ return h + 0.1 * torch.tanh(h)
226
+
227
+ class StubInner(torch.nn.Module):
228
+ def __init__(self) -> None:
229
+ super().__init__()
230
+ self.layers = torch.nn.ModuleList(StubLayer() for _ in range(n_layers))
231
+ self.norm = torch.nn.RMSNorm(hidden_size)
232
+
233
+ class StubModel(torch.nn.Module):
234
+ def __init__(self) -> None:
235
+ super().__init__()
236
+ self.config = SimpleNamespace(hidden_size=hidden_size)
237
+ self.embed = torch.nn.Parameter(0.1 * torch.randn(vocab_size, hidden_size, generator=gen))
238
+ self.model = StubInner()
239
+ self.lm_head = torch.nn.Linear(hidden_size, vocab_size, bias=False)
240
+
241
+ def forward(self, input_ids, **_):
242
+ h = self.embed[input_ids]
243
+ for layer in self.model.layers:
244
+ h = layer(h)
245
+ h = self.model.norm(h)
246
+ return SimpleNamespace(logits=self.lm_head(h), hidden_states=None)
247
+
248
+ return StubModel()
@@ -0,0 +1,112 @@
1
+ """Детерминированная CPU-модель для тестов и CI (аналог smoke-конфига официального репо).
2
+
3
+ НЕ модель статьи: крошечный псевдо-LLM, который читает таблицу T⁰+Δ так же, как нативный
4
+ движок (gather строк по хэшу биграммы), — этого достаточно, чтобы гонять весь цикл
5
+ write → read → clear → verify на любом CPU без torch.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+
11
+ import numpy as np
12
+
13
+ from ..memory import MemoryPatch
14
+ from ..specs import SIDECAR_SMALL, TableSpec
15
+
16
+
17
+ def _logsumexp(x: np.ndarray, axis: int = -1, keepdims: bool = False) -> np.ndarray:
18
+ m = np.max(x, axis=axis, keepdims=True)
19
+ out = m + np.log(np.sum(np.exp(x - m), axis=axis, keepdims=True))
20
+ return out if keepdims else np.squeeze(out, axis=axis)
21
+
22
+
23
+ class TinyModel:
24
+ """Псевдо-LLM: эмбеддинги токенов + чтение n-gram таблицы перед «выходным слоем».
25
+
26
+ Логиты для последовательности токенов: пул эмбеддингов + пул прочитанных строк
27
+ таблицы, спроецированные фиксированной матрицей. Всё детерминировано (blake2b-хэш
28
+ строк таблицы, seeded RNG весов).
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ spec: TableSpec | None = None,
34
+ vocab_size: int = 512,
35
+ out_dim: int = 32,
36
+ seed: int = 17,
37
+ ) -> None:
38
+ self.spec = spec or SIDECAR_SMALL
39
+ self.vocab_size = vocab_size
40
+ rng = np.random.default_rng(seed)
41
+ self.t0 = (0.02 * rng.standard_normal((self.spec.n_rows, self.spec.row_width))).astype(np.float32)
42
+ self.embed = (0.10 * rng.standard_normal((vocab_size, self.spec.row_width))).astype(np.float32)
43
+ self.w_out = (0.05 * rng.standard_normal((self.spec.row_width, out_dim))).astype(np.float32)
44
+ self.w_vocab = (0.05 * rng.standard_normal((self.spec.row_width, vocab_size))).astype(np.float32)
45
+ self.memory = MemoryPatch(self.spec)
46
+
47
+ @staticmethod
48
+ def row_hash(prev_token: int, token: int, n_rows: int) -> int:
49
+ """Детерминированный кросс-платформенный хэш биграммы → номер строки."""
50
+ digest = hashlib.blake2b(f"{int(prev_token)}:{int(token)}".encode(), digest_size=8).digest()
51
+ return int.from_bytes(digest, "little") % n_rows
52
+
53
+ def read(self, tokens: list[int]) -> np.ndarray:
54
+ """Чтение таблицы по биграммам: (len, d), позиция 0 — нули (нет предшественника)."""
55
+ out = np.zeros((len(tokens), self.spec.row_width), dtype=np.float32)
56
+ for i in range(1, len(tokens)):
57
+ row = self.row_hash(tokens[i - 1], tokens[i], self.spec.n_rows)
58
+ out[i] = self.t0[row] + self.memory.get(row)
59
+ return out
60
+
61
+ def logits(self, tokens: list[int]) -> np.ndarray:
62
+ """(out_dim,) — «выход модели» для последовательности."""
63
+ h = self.embed[tokens].mean(axis=0) + self.read(tokens).mean(axis=0)
64
+ return h @ self.w_out
65
+
66
+ def probe(self, n: int = 8, seq_len: int = 12, seed: int = 7) -> np.ndarray:
67
+ """Детерминированный набор выходов: (n, out_dim). Не гарантирует нейтральности."""
68
+ rng = np.random.default_rng(seed)
69
+ tokens = rng.integers(0, self.vocab_size, size=(n, seq_len))
70
+ return np.stack([self.logits([int(t) for t in seq]) for seq in tokens])
71
+
72
+ def neutral_token_seqs(self, n: int = 8, seq_len: int = 12, seed: int = 7, max_tries: int = 500) -> list[list[int]]:
73
+ """Последовательности, гарантированно НЕ адресующие строки текущего патча.
74
+
75
+ Генерируются один раз и переиспользуются до и после очистки памяти — так
76
+ verify/publish сравнивают выходы на одних и тех же входах.
77
+ """
78
+ written = set(self.memory.row_ids()) if self.memory is not None else set()
79
+ rng = np.random.default_rng(seed)
80
+ seqs: list[list[int]] = []
81
+ tries = 0
82
+ while len(seqs) < n and tries < max_tries:
83
+ tries += 1
84
+ tokens = [int(x) for x in rng.integers(0, self.vocab_size, size=seq_len)]
85
+ rows = {self.row_hash(tokens[i - 1], tokens[i], self.spec.n_rows) for i in range(1, seq_len)}
86
+ if rows & written:
87
+ continue
88
+ seqs.append(tokens)
89
+ if len(seqs) < n:
90
+ raise RuntimeError(f"не удалось набрать {n} нейтральных последовательностей (патч слишком плотный)")
91
+ return seqs
92
+
93
+ def run_seqs(self, seqs: list[list[int]]) -> np.ndarray:
94
+ return np.stack([self.logits(seq) for seq in seqs])
95
+
96
+ def token_logits(self, tokens: list[int]) -> np.ndarray:
97
+ """Логиты следующего токена (словарные, для NLL): (T-1, V)."""
98
+ h = self.embed[tokens[:-1]] + self.read(tokens[:-1])
99
+ return h @ self.w_vocab
100
+
101
+ def nll(self, seqs: list[list[int]]) -> float:
102
+ """Средний NLL (nats/token) предсказания t+1 при текущей памяти T⁰+Δ."""
103
+ total, count = 0.0, 0
104
+ for seq in seqs:
105
+ if len(seq) < 2:
106
+ continue
107
+ logits = self.token_logits(seq)
108
+ logprobs = logits - _logsumexp(logits, axis=-1, keepdims=True)
109
+ targets = np.asarray(seq[1:], dtype=np.int64)
110
+ total -= float(np.sum(logprobs[np.arange(len(targets)), targets]))
111
+ count += len(targets)
112
+ return total / max(count, 1)
atomicng/bench.py ADDED
@@ -0,0 +1,97 @@
1
+ """atomicng bench (P1): скорость записи, оверхед чтения, время verify + регресс-гейт.
2
+
3
+ Модель-зависимая часть передаётся фабрикой; для tiny-бэкенда есть готовый раннер.
4
+ Выход — JSON-отчёт; `check_regression` сравнивает с baseline с допуском и возвращает
5
+ список нарушений (пустой = зелёный) — используется в CI как блокирующий гейт.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import statistics
11
+ import time
12
+ from collections.abc import Callable
13
+ from pathlib import Path
14
+
15
+ import numpy as np
16
+
17
+
18
+ def run_bench(
19
+ model_factory: Callable[[], object],
20
+ *,
21
+ n_docs: int = 200,
22
+ n_probes: int = 64,
23
+ repeats: int = 5,
24
+ seed: int = 17,
25
+ ) -> dict:
26
+ """Замеряет: write throughput (документы/события в сек), read overhead %, verify время."""
27
+ import atomicng # локально: избегаем цикла в __init__
28
+
29
+ model = model_factory()
30
+ writer = atomicng.HashWriter(model.spec)
31
+ rng = np.random.default_rng(seed)
32
+ docs = [
33
+ " ".join(f"token{int(x)}" for x in rng.integers(0, model.vocab_size, size=48)) for _ in range(n_docs)
34
+ ]
35
+
36
+ # warmup
37
+ atomicng.write(model, docs[0], writer=writer)
38
+
39
+ t0 = time.perf_counter()
40
+ reports = [atomicng.write(model, doc, writer=writer) for doc in docs]
41
+ write_s = time.perf_counter() - t0
42
+ n_events = sum(r.n_events for r in reports)
43
+
44
+ # read overhead: чтение нейтральных проб с полным патчем vs с очищенным
45
+ probe_fn = model.probe
46
+ full_times: list[float] = []
47
+ empty_times: list[float] = []
48
+ snap = model.memory.snapshot()
49
+ checksum_before = model.memory.checksum()
50
+ for _ in range(repeats):
51
+ t = time.perf_counter()
52
+ probe_fn(n_probes)
53
+ full_times.append(time.perf_counter() - t)
54
+ model.memory.clear()
55
+ for _ in range(repeats):
56
+ t = time.perf_counter()
57
+ probe_fn(n_probes)
58
+ empty_times.append(time.perf_counter() - t)
59
+ model.memory.restore(snap)
60
+ assert model.memory.checksum() == checksum_before, "bench исказил патч"
61
+
62
+ full = statistics.median(full_times)
63
+ empty = max(statistics.median(empty_times), 1e-9)
64
+
65
+ t0 = time.perf_counter()
66
+ atomicng.verify_deletion(model, n_probes)
67
+ verify_s = time.perf_counter() - t0
68
+
69
+ return {
70
+ "n_docs": n_docs,
71
+ "n_events": n_events,
72
+ "write_docs_per_s": round(n_docs / write_s, 2),
73
+ "write_events_per_s": round(n_events / write_s, 1),
74
+ "read_time_full_ms": round(full * 1000, 3),
75
+ "read_time_empty_ms": round(empty * 1000, 3),
76
+ "read_overhead_pct": round((full - empty) / empty * 100.0, 2),
77
+ "verify_ms": round(verify_s * 1000, 3),
78
+ }
79
+
80
+
81
+ def save_baseline(report: dict, path: str) -> None:
82
+ Path(path).write_text(json.dumps(report, indent=2), encoding="utf-8")
83
+
84
+
85
+ def check_regression(current: dict, baseline: dict, tolerance: float = 1.25) -> list[str]:
86
+ """Регресс-гейт: write медленнее baseline×tol или оверхед чтения вырос сверх baseline×tol → нарушение."""
87
+ failures: list[str] = []
88
+ for key in ("write_events_per_s", "write_docs_per_s"):
89
+ limit = baseline[key] / tolerance
90
+ if current[key] < limit:
91
+ failures.append(f"{key}: {current[key]} < {limit:.2f} (baseline {baseline[key]}, допуск x{tolerance})")
92
+ limit_overhead = max(baseline["read_overhead_pct"], 1.0) * tolerance
93
+ if current["read_overhead_pct"] > limit_overhead:
94
+ failures.append(
95
+ f"read_overhead_pct: {current['read_overhead_pct']} > {limit_overhead:.2f} (baseline {baseline['read_overhead_pct']})"
96
+ )
97
+ return failures