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 +159 -0
- atomicng/backends/__init__.py +9 -0
- atomicng/backends/hf.py +248 -0
- atomicng/backends/tiny.py +112 -0
- atomicng/bench.py +97 -0
- atomicng/benchmarks.py +215 -0
- atomicng/cli.py +348 -0
- atomicng/evidence.py +98 -0
- atomicng/exceptions.py +33 -0
- atomicng/exporters.py +88 -0
- atomicng/guards.py +58 -0
- atomicng/journal.py +68 -0
- atomicng/memory.py +249 -0
- atomicng/metrics.py +121 -0
- atomicng/monitor_html.py +146 -0
- atomicng/native.py +141 -0
- atomicng/paper.py +47 -0
- atomicng/publishing.py +93 -0
- atomicng/py.typed +0 -0
- atomicng/rules/__init__.py +5 -0
- atomicng/rules/d1.py +140 -0
- atomicng/signing.py +63 -0
- atomicng/specs.py +57 -0
- atomicng/testing.py +37 -0
- atomicng/verify.py +57 -0
- atomicng/vllm_bridge.py +116 -0
- atomicng/writers/__init__.py +28 -0
- atomicng/writers/hf_training.py +175 -0
- atomicng/writers/io.py +96 -0
- atomicng/writers/mlp.py +92 -0
- atomicng/writers/training.py +131 -0
- atomicng-1.0.0.dist-info/METADATA +155 -0
- atomicng-1.0.0.dist-info/RECORD +37 -0
- atomicng-1.0.0.dist-info/WHEEL +5 -0
- atomicng-1.0.0.dist-info/entry_points.txt +5 -0
- atomicng-1.0.0.dist-info/licenses/LICENSE +202 -0
- atomicng-1.0.0.dist-info/top_level.txt +1 -0
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()
|
atomicng/backends/hf.py
ADDED
|
@@ -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
|