burst-codec 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- burst_codec/Makefile +54 -0
- burst_codec/Makefile.detlm +58 -0
- burst_codec/__init__.py +26 -0
- burst_codec/buckets_v1.json +1 -0
- burst_codec/cli.py +20 -0
- burst_codec/det_codec.py +353 -0
- burst_codec/detcodec.c +174 -0
- burst_codec/detlm.c +692 -0
- burst_codec/detlm.h +53 -0
- burst_codec/detlm_cli.c +162 -0
- burst_codec/detrc.c +267 -0
- burst_codec/detrc.h +55 -0
- burst_codec/detrc_exptab.h +92 -0
- burst_codec/detrc_test.c +131 -0
- burst_codec/gen_exp_table.py +33 -0
- burst_codec/mrc.py +134 -0
- burst_codec/open_codec.py +212 -0
- burst_codec/packs/nws_forecast_style.txt +12 -0
- burst_codec/packs/test/nws_2026-10-28.txt +140 -0
- burst_codec/repro.py +222 -0
- burst_codec/test_det_codec.py +204 -0
- burst_codec-0.1.0.dist-info/METADATA +34 -0
- burst_codec-0.1.0.dist-info/RECORD +27 -0
- burst_codec-0.1.0.dist-info/WHEEL +5 -0
- burst_codec-0.1.0.dist-info/entry_points.txt +2 -0
- burst_codec-0.1.0.dist-info/licenses/LICENSE +216 -0
- burst_codec-0.1.0.dist-info/top_level.txt +1 -0
burst_codec/Makefile
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Makefile — detrc: the shared library for Python (host) and the cross-arch harness (host + aarch64 under qemu).
|
|
2
|
+
# make host only: build/libdetrc.so, build/detrc_test_x86, build/detrc_ctl_x86
|
|
3
|
+
# make arm aarch64 harness builds (needs the Android NDK: ANDROID_HOME, NDK_VERSION as in android/lib/build.gradle.kts)
|
|
4
|
+
# make identity host vs aarch64 (qemu-aarch64) identity gate, with its negative controls
|
|
5
|
+
CFLAGS ?= -O2 -ffp-contract=off -Wall -Wextra -std=c99
|
|
6
|
+
NDK_VERSION ?= 29.0.13113456
|
|
7
|
+
NDK ?= $(ANDROID_HOME)/ndk/$(NDK_VERSION)/toolchains/llvm/prebuilt/linux-x86_64/bin
|
|
8
|
+
ARMCC ?= $(NDK)/aarch64-linux-android31-clang
|
|
9
|
+
QEMU ?= /usr/bin/qemu-aarch64
|
|
10
|
+
B ?= build
|
|
11
|
+
|
|
12
|
+
all: $(B)/libdetrc.so $(B)/detrc_test_x86 $(B)/detrc_ctl_x86
|
|
13
|
+
|
|
14
|
+
arm: $(B)/detrc_test_aarch64 $(B)/detrc_ctl_aarch64
|
|
15
|
+
|
|
16
|
+
$(B):
|
|
17
|
+
mkdir -p $(B)
|
|
18
|
+
|
|
19
|
+
$(B)/libdetrc.so: detrc.c detrc.h detrc_exptab.h | $(B)
|
|
20
|
+
$(CC) $(CFLAGS) -fPIC -shared detrc.c -o $@
|
|
21
|
+
|
|
22
|
+
$(B)/detrc_test_x86: detrc.c detrc_test.c detrc.h detrc_exptab.h | $(B)
|
|
23
|
+
$(CC) $(CFLAGS) detrc.c detrc_test.c -o $@
|
|
24
|
+
|
|
25
|
+
$(B)/detrc_test_aarch64: detrc.c detrc_test.c detrc.h detrc_exptab.h | $(B)
|
|
26
|
+
$(ARMCC) $(CFLAGS) -static detrc.c detrc_test.c -o $@
|
|
27
|
+
|
|
28
|
+
# negative control: a float softmax (libm expf, float sums) with FMA contraction + fast-math ON for the arm build.
|
|
29
|
+
# Measured 10-24: with contraction OFF, glibc and bionic expf AGREED on every synthetic row (that control could not
|
|
30
|
+
# fail); with -O3 -ffast-math -ffp-contract=fast on arm64 the freq tables and bytes DIFFER (byte 24 of the stream).
|
|
31
|
+
# The integer detrc build is identical under the same flags. `make identity` asserts the control's divergence.
|
|
32
|
+
$(B)/detrc_ctl_x86: detrc.c detrc_test.c | $(B)
|
|
33
|
+
$(CC) $(CFLAGS) -DCONTROL_FLOAT detrc.c detrc_test.c -o $@ -lm
|
|
34
|
+
|
|
35
|
+
$(B)/detrc_ctl_aarch64: detrc.c detrc_test.c | $(B)
|
|
36
|
+
$(ARMCC) -O3 -ffast-math -ffp-contract=fast -Wall -std=c99 -DCONTROL_FLOAT -static detrc.c detrc_test.c -o $@ -lm
|
|
37
|
+
|
|
38
|
+
# identity gate: same freq tables + same bytes on both chips; the control build must differ
|
|
39
|
+
identity: $(B)/detrc_test_x86 $(B)/detrc_test_aarch64 $(B)/detrc_ctl_x86 $(B)/detrc_ctl_aarch64
|
|
40
|
+
$(B)/detrc_test_x86 synth $(B)/freq_x86.bin $(B)/rc_x86.bin
|
|
41
|
+
$(QEMU) $(B)/detrc_test_aarch64 synth $(B)/freq_arm.bin $(B)/rc_arm.bin
|
|
42
|
+
cmp $(B)/freq_x86.bin $(B)/freq_arm.bin && cmp $(B)/rc_x86.bin $(B)/rc_arm.bin && echo "detrc: IDENTICAL x86 vs aarch64"
|
|
43
|
+
$(B)/detrc_ctl_x86 synth $(B)/cfreq_x86.bin $(B)/crc_x86.bin
|
|
44
|
+
$(QEMU) $(B)/detrc_ctl_aarch64 synth $(B)/cfreq_arm.bin $(B)/crc_arm.bin
|
|
45
|
+
@if cmp -s $(B)/crc_x86.bin $(B)/crc_arm.bin; then echo "CONTROL DID NOT DIFFER: the gate cannot fail"; exit 1; \
|
|
46
|
+
else echo "control (float softmax, FMA on arm): DIFFERS as it must"; fi
|
|
47
|
+
$(B)/detrc_test_x86 synth1ulp $(B)/freq_x86_1.bin $(B)/rc_x86_1.bin
|
|
48
|
+
@if cmp -s $(B)/rc_x86.bin $(B)/rc_x86_1.bin; then echo "INPUT-CHANGE CONTROL DID NOT DIFFER"; exit 1; \
|
|
49
|
+
else echo "control (one input float changed): DIFFERS as it must"; fi
|
|
50
|
+
|
|
51
|
+
clean:
|
|
52
|
+
rm -rf $(B)
|
|
53
|
+
|
|
54
|
+
.PHONY: all arm identity clean
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# detlm builds (use: make -f Makefile.detlm <target>).
|
|
2
|
+
# all (default): host only — detlm_x86 (CLI), libdetlm.so (for det_codec.py), detcodec_x86
|
|
3
|
+
# arm: static aarch64 builds via the Android NDK (ANDROID_HOME, NDK_VERSION as in android/lib/build.gradle.kts)
|
|
4
|
+
# detlm_x86_reorder / detlm_x86_fma / detlm_arm_fma: negative controls; check-fma: count fused ops in the builds
|
|
5
|
+
# Determinism flags are load-bearing: -ffp-contract=off
|
|
6
|
+
# (gcc and NDK clang both default to contraction), never -ffast-math / -Ofast (crtfastmath flips FTZ/DAZ), no -march=native.
|
|
7
|
+
CC ?= gcc
|
|
8
|
+
NDK_VERSION ?= 29.0.13113456
|
|
9
|
+
NDK ?= $(ANDROID_HOME)/ndk/$(NDK_VERSION)/toolchains/llvm/prebuilt/linux-x86_64/bin
|
|
10
|
+
ARMCC ?= $(NDK)/aarch64-linux-android31-clang
|
|
11
|
+
DETFLAGS = -O2 -ffp-contract=off -fno-math-errno -fno-trapping-math -Wall -Wextra -std=c11
|
|
12
|
+
SRC = detlm.c detlm_cli.c
|
|
13
|
+
|
|
14
|
+
all: detlm_x86 libdetlm.so detcodec_x86
|
|
15
|
+
|
|
16
|
+
arm: detlm_arm detcodec_arm
|
|
17
|
+
|
|
18
|
+
detlm_x86: $(SRC) detlm.h
|
|
19
|
+
$(CC) $(DETFLAGS) -o $@ $(SRC) -lpthread -lm
|
|
20
|
+
|
|
21
|
+
# negative control 1: reversed block accumulation order (must change the hash)
|
|
22
|
+
detlm_x86_reorder: $(SRC) detlm.h
|
|
23
|
+
$(CC) $(DETFLAGS) -DDETLM_REORDER_SUM -o $@ $(SRC) -lpthread -lm
|
|
24
|
+
|
|
25
|
+
# negative control 2: FMA contraction on (x86 needs -mfma to even have the instruction)
|
|
26
|
+
detlm_x86_fma: $(SRC) detlm.h
|
|
27
|
+
$(CC) -O2 -ffp-contract=fast -mfma -fno-math-errno -fno-trapping-math -std=c11 -o $@ $(SRC) -lpthread -lm
|
|
28
|
+
|
|
29
|
+
# static aarch64 (bionic) binary: runs on the phone and under qemu-aarch64
|
|
30
|
+
detlm_arm: $(SRC) detlm.h
|
|
31
|
+
$(ARMCC) $(DETFLAGS) -static -o $@ $(SRC)
|
|
32
|
+
|
|
33
|
+
# negative control 3: aarch64 with contraction on (fmla/fmadd emitted)
|
|
34
|
+
detlm_arm_fma: $(SRC) detlm.h
|
|
35
|
+
$(ARMCC) -O2 -ffp-contract=fast -fno-math-errno -fno-trapping-math -std=c11 -static -o $@ $(SRC)
|
|
36
|
+
|
|
37
|
+
# shared library for ctypes / JNI consumers
|
|
38
|
+
libdetlm.so: detlm.c detlm.h
|
|
39
|
+
$(CC) $(DETFLAGS) -fPIC -shared -o $@ detlm.c -lpthread -lm
|
|
40
|
+
|
|
41
|
+
# detcodec: frame 0x08 on the phone (detlm + detrc, no Python, no tokenizer) — SAME flags as the engine
|
|
42
|
+
CODEC_SRC = detlm.c detrc.c detcodec.c
|
|
43
|
+
detcodec_x86: $(CODEC_SRC) detlm.h detrc.h detrc_exptab.h
|
|
44
|
+
$(CC) $(DETFLAGS) -o $@ $(CODEC_SRC) -lpthread -lm
|
|
45
|
+
|
|
46
|
+
detcodec_arm: $(CODEC_SRC) detlm.h detrc.h detrc_exptab.h
|
|
47
|
+
$(ARMCC) $(DETFLAGS) -static -o $@ $(CODEC_SRC)
|
|
48
|
+
|
|
49
|
+
check-fma: detlm_x86 detlm_arm detlm_arm_fma
|
|
50
|
+
@echo "x86 fused ops (want 0): $$(objdump -d detlm_x86 | grep -cE 'vfmadd|vfmsub|vfnmadd|vfnmsub')"
|
|
51
|
+
@echo "arm fused ops (want 0): $$($(NDK)/llvm-objdump -d detlm_arm | grep -cE '\bfmadd|\bfmsub|\bfnmadd|\bfnmsub|\bfmla|\bfmls')"
|
|
52
|
+
@echo "arm_fma control fused ops (want >0): $$($(NDK)/llvm-objdump -d detlm_arm_fma | grep -cE '\bfmadd|\bfmsub|\bfnmadd|\bfnmsub|\bfmla|\bfmls')"
|
|
53
|
+
@echo "x86 libm transcendentals (want none):"; nm detlm_x86 | grep -E ' U (expf|sinf|cosf|powf|logf|tanhf|exp|sin|cos|pow|log)$$' || echo " none"
|
|
54
|
+
|
|
55
|
+
clean:
|
|
56
|
+
rm -f detlm_x86 detlm_x86_reorder detlm_x86_fma detlm_arm detlm_arm_fma libdetlm.so detcodec_x86 detcodec_arm
|
|
57
|
+
|
|
58
|
+
.PHONY: all arm check-fma clean
|
burst_codec/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""burst_codec — the Burst codec as an installable package (Apache-2.0).
|
|
2
|
+
|
|
3
|
+
The codec modules are flat (det_codec, open_codec, mrc, repro) and import each other by name, exactly as in the
|
|
4
|
+
source tree; importing this package puts them on the path so `import det_codec` works after `pip install`.
|
|
5
|
+
Frame 0x08 = det_codec (deterministic, the default); 0x03 = open_codec (mismatch-resistant, any GGUF).
|
|
6
|
+
Both ends need the same model file: export BURST_GGUF=/path/to/Qwen2.5-0.5B.Q8_0.gguf (sha256 d5163baa…).
|
|
7
|
+
"""
|
|
8
|
+
import os as _os, sys as _sys
|
|
9
|
+
|
|
10
|
+
_HERE = _os.path.dirname(_os.path.abspath(__file__))
|
|
11
|
+
if _HERE not in _sys.path:
|
|
12
|
+
_sys.path.insert(0, _HERE)
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
# libdetlm.so (the bit-identical engine det_codec loads) is built once, here, with the same determinism flags as
|
|
17
|
+
# Makefile.detlm — never -ffast-math, never -march=native, -ffp-contract=off is load-bearing. Needs a C compiler.
|
|
18
|
+
_SO = _os.path.join(_HERE, "libdetlm.so")
|
|
19
|
+
if not _os.path.exists(_SO):
|
|
20
|
+
import subprocess as _sp
|
|
21
|
+
try:
|
|
22
|
+
_sp.run([_os.environ.get("CC", "cc"), "-O2", "-ffp-contract=off", "-fno-math-errno", "-fno-trapping-math", "-std=c11",
|
|
23
|
+
"-fPIC", "-shared", "-o", _SO, _os.path.join(_HERE, "detlm.c"), "-lpthread", "-lm"], check=True,
|
|
24
|
+
capture_output=True)
|
|
25
|
+
except (OSError, _sp.CalledProcessError) as _e:
|
|
26
|
+
_sys.stderr.write(f"burst_codec: could not build libdetlm.so ({_e}); frame 0x08 on the real model needs a C compiler\n")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version": 1, "prime": "The following is a short plain-English message sent between two people over a radio.\n\n", "model": "qwen2.5-0.5b-instruct-q8_0.gguf", "freq": [826.0, 482.0, 474.0, 449.0, 455.0, 340.0, 334.0, 282.0, 289.0, 241.0, 256.0, 212.0, 186.0, 201.0, 159.0, 139.0, 100.0, 76.0, 70.0, 59.0, 46.0, 32.0, 25.0, 18.0, 8.0, 9.0, 14.0, 7.0, 6.0, 6.0, 2.0, 1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]}
|
burst_codec/cli.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""burst-codec command: `burst-codec selftest [--golden]` (coder + refusal tests; --golden needs the model) or
|
|
2
|
+
`burst-codec repro <args>` (the README's reproduction commands, needs the model)."""
|
|
3
|
+
import runpy, sys
|
|
4
|
+
|
|
5
|
+
import burst_codec # noqa: F401 (puts the flat codec modules on the path)
|
|
6
|
+
|
|
7
|
+
CMDS = {"selftest": "test_det_codec", "repro": "repro"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
if len(sys.argv) < 2 or sys.argv[1] not in CMDS:
|
|
12
|
+
print("usage: burst-codec selftest [--golden] | burst-codec repro <args> (see README)")
|
|
13
|
+
sys.exit(2)
|
|
14
|
+
mod = CMDS[sys.argv[1]]
|
|
15
|
+
sys.argv = [mod + ".py", *sys.argv[2:]]
|
|
16
|
+
runpy.run_module(mod, run_name="__main__")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
burst_codec/det_codec.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""det_codec — burst frame 0x08: an exact range coder over a bit-identical model.
|
|
3
|
+
|
|
4
|
+
frame = [0x08][2-byte check = sha256(text)[:2]][varint n_tokens][range-coder bytes]
|
|
5
|
+
|
|
6
|
+
Model and coder are implied by the codec byte: no model id, no c, no tail id. The history window (the learned tail,
|
|
7
|
+
open_codec.prime_for) is found by the receiver trying candidate tails newest-first and letting the 2-byte check pick;
|
|
8
|
+
nothing that fails the check is ever shown. Both ends must compute the SAME logits bit for bit — that is detlm's job
|
|
9
|
+
(detlm.c); detrc (detrc.c via ctypes) turns identical logits into identical bytes.
|
|
10
|
+
|
|
11
|
+
Model protocol (pluggable, so the coder is testable without model weights):
|
|
12
|
+
m.n_vocab int
|
|
13
|
+
m.tokenize(text) -> list[int] m.detokenize(ids) -> str
|
|
14
|
+
m.reset(prime_ids) -> logits float32[V]: next-token logits after the prime
|
|
15
|
+
m.step(tok) -> logits float32[V]: next-token logits after feeding one more token
|
|
16
|
+
m.n_ctx int or None: context size; a decode that would overrun it is refused up front
|
|
17
|
+
Three implementations here: DetlmModel (the real engine), LlamaModel (llama.cpp as a stand-in — exact only encoder-and-decoder-on-the-same-box, same
|
|
18
|
+
eval path) and MockModel (seeded hash model, byte tokens, no weights).
|
|
19
|
+
|
|
20
|
+
API: crunch(text, model, tail=()) -> bytes uncrunch(frame, model, tails=()) -> text (raises DecodeFailed)
|
|
21
|
+
"""
|
|
22
|
+
import ctypes, hashlib, os, subprocess, sys
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
26
|
+
try:
|
|
27
|
+
import open_codec # PRIME, prime_for, TAIL_N — one definition, shared with 0x03
|
|
28
|
+
except ImportError: # source tree: open_codec.py sits one directory up
|
|
29
|
+
sys.path.insert(0, os.path.dirname(HERE))
|
|
30
|
+
import open_codec
|
|
31
|
+
|
|
32
|
+
CODEC_BYTE = 0x08
|
|
33
|
+
MAX_TOKENS = 2048 # a flipped varint byte must not become a million model steps
|
|
34
|
+
MAX_TAILS = 6 # candidate histories tried, newest first
|
|
35
|
+
TOTAL_BITS = 24
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DecodeFailed(ValueError):
|
|
39
|
+
"""The check did not match under any candidate history: refused, never shown."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------------------------------------------------
|
|
43
|
+
# detrc via ctypes
|
|
44
|
+
# ---------------------------------------------------------------------------------------------------------------------
|
|
45
|
+
_lib = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _build_lib():
|
|
49
|
+
so = os.path.join(HERE, "build", "libdetrc.so")
|
|
50
|
+
srcs = [os.path.join(HERE, f) for f in ("detrc.c", "detrc.h", "detrc_exptab.h")]
|
|
51
|
+
if not os.path.exists(so) or any(os.path.getmtime(s) > os.path.getmtime(so) for s in srcs):
|
|
52
|
+
os.makedirs(os.path.dirname(so), exist_ok=True)
|
|
53
|
+
subprocess.run([os.environ.get("CC", "cc"), "-O2", "-ffp-contract=off", "-std=c99", "-fPIC", "-shared",
|
|
54
|
+
srcs[0], "-o", so], check=True)
|
|
55
|
+
return so
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def lib():
|
|
59
|
+
global _lib
|
|
60
|
+
if _lib is None:
|
|
61
|
+
L = ctypes.CDLL(_build_lib())
|
|
62
|
+
u8p, f32p, u32p = ctypes.POINTER(ctypes.c_uint8), ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_uint32)
|
|
63
|
+
L.detrc_version.restype = ctypes.c_int
|
|
64
|
+
L.detrc_freq.argtypes = [f32p, ctypes.c_int32, u32p]; L.detrc_freq.restype = ctypes.c_int
|
|
65
|
+
L.detrc_enc_new.restype = ctypes.c_void_p
|
|
66
|
+
L.detrc_enc_put.argtypes = [ctypes.c_void_p, u32p, ctypes.c_int32, ctypes.c_int32]; L.detrc_enc_put.restype = ctypes.c_int
|
|
67
|
+
L.detrc_enc_put_logits.argtypes = [ctypes.c_void_p, f32p, ctypes.c_int32, ctypes.c_int32]; L.detrc_enc_put_logits.restype = ctypes.c_int
|
|
68
|
+
L.detrc_enc_finish.argtypes = [ctypes.c_void_p]; L.detrc_enc_finish.restype = ctypes.c_int64
|
|
69
|
+
L.detrc_enc_data.argtypes = [ctypes.c_void_p]; L.detrc_enc_data.restype = ctypes.c_void_p
|
|
70
|
+
L.detrc_enc_free.argtypes = [ctypes.c_void_p]
|
|
71
|
+
L.detrc_dec_new.argtypes = [u8p, ctypes.c_int64]; L.detrc_dec_new.restype = ctypes.c_void_p
|
|
72
|
+
L.detrc_dec_get.argtypes = [ctypes.c_void_p, u32p, ctypes.c_int32]; L.detrc_dec_get.restype = ctypes.c_int32
|
|
73
|
+
L.detrc_dec_get_logits.argtypes = [ctypes.c_void_p, f32p, ctypes.c_int32]; L.detrc_dec_get_logits.restype = ctypes.c_int32
|
|
74
|
+
L.detrc_dec_free.argtypes = [ctypes.c_void_p]
|
|
75
|
+
_lib = L
|
|
76
|
+
return _lib
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _f32(a):
|
|
80
|
+
a = np.ascontiguousarray(a, dtype=np.float32)
|
|
81
|
+
return a, a.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def freq_table(logits):
|
|
85
|
+
"""the deterministic integer frequency table (sum 2^24, every entry >= 1) — exposed for tests and the identity gate"""
|
|
86
|
+
a, p = _f32(logits)
|
|
87
|
+
out = np.empty(len(a), np.uint32)
|
|
88
|
+
if lib().detrc_freq(p, len(a), out.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))):
|
|
89
|
+
raise ValueError("detrc_freq failed")
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Encoder:
|
|
94
|
+
def __init__(self):
|
|
95
|
+
self.L = lib(); self.h = self.L.detrc_enc_new()
|
|
96
|
+
|
|
97
|
+
def put_logits(self, logits, sym):
|
|
98
|
+
a, p = _f32(logits)
|
|
99
|
+
if self.L.detrc_enc_put_logits(self.h, p, len(a), int(sym)):
|
|
100
|
+
raise ValueError("detrc_enc_put_logits failed")
|
|
101
|
+
|
|
102
|
+
def put(self, freq, sym):
|
|
103
|
+
f = np.ascontiguousarray(freq, dtype=np.uint32)
|
|
104
|
+
if self.L.detrc_enc_put(self.h, f.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)), len(f), int(sym)):
|
|
105
|
+
raise ValueError("detrc_enc_put failed (bad table?)")
|
|
106
|
+
|
|
107
|
+
def finish(self):
|
|
108
|
+
n = self.L.detrc_enc_finish(self.h)
|
|
109
|
+
if n < 0:
|
|
110
|
+
raise MemoryError("detrc_enc_finish")
|
|
111
|
+
return ctypes.string_at(self.L.detrc_enc_data(self.h), n) if n else b""
|
|
112
|
+
|
|
113
|
+
def __del__(self):
|
|
114
|
+
if getattr(self, "h", None):
|
|
115
|
+
self.L.detrc_enc_free(self.h); self.h = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Decoder:
|
|
119
|
+
def __init__(self, data):
|
|
120
|
+
self.L = lib(); self.buf = (ctypes.c_uint8 * max(1, len(data))).from_buffer_copy(data or b"\0")
|
|
121
|
+
self.h = self.L.detrc_dec_new(self.buf, len(data))
|
|
122
|
+
|
|
123
|
+
def get_logits(self, logits):
|
|
124
|
+
a, p = _f32(logits)
|
|
125
|
+
s = self.L.detrc_dec_get_logits(self.h, p, len(a))
|
|
126
|
+
if s < 0:
|
|
127
|
+
raise ValueError("detrc_dec_get_logits failed")
|
|
128
|
+
return s
|
|
129
|
+
|
|
130
|
+
def get(self, freq):
|
|
131
|
+
f = np.ascontiguousarray(freq, dtype=np.uint32)
|
|
132
|
+
s = self.L.detrc_dec_get(self.h, f.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)), len(f))
|
|
133
|
+
if s < 0:
|
|
134
|
+
raise ValueError("detrc_dec_get failed (bad table?)")
|
|
135
|
+
return s
|
|
136
|
+
|
|
137
|
+
def __del__(self):
|
|
138
|
+
if getattr(self, "h", None):
|
|
139
|
+
self.L.detrc_dec_free(self.h); self.h = None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------------------------------------------------
|
|
143
|
+
# frame
|
|
144
|
+
# ---------------------------------------------------------------------------------------------------------------------
|
|
145
|
+
def _check(text):
|
|
146
|
+
return hashlib.sha256(text.encode()).digest()[:2]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _wv(n, out):
|
|
150
|
+
while True:
|
|
151
|
+
b = n & 0x7F; n >>= 7
|
|
152
|
+
out.append(b | (0x80 if n else 0))
|
|
153
|
+
if not n:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _rv(buf, i, limit=MAX_TOKENS):
|
|
158
|
+
shift = val = 0
|
|
159
|
+
while True:
|
|
160
|
+
if i >= len(buf) or shift > 28:
|
|
161
|
+
raise DecodeFailed("bad token count")
|
|
162
|
+
b = buf[i]; i += 1; val |= (b & 0x7F) << shift
|
|
163
|
+
if not b & 0x80:
|
|
164
|
+
if val > limit:
|
|
165
|
+
raise DecodeFailed(f"token count {val} over the cap {limit}")
|
|
166
|
+
return val, i
|
|
167
|
+
shift += 7
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def crunch(text, model, tail=()):
|
|
171
|
+
"""text -> 0x08 frame. tail = the learned history (the last TAIL_N confirmed messages, oldest first)."""
|
|
172
|
+
tail = list(tail)[-open_codec.TAIL_N:]
|
|
173
|
+
toks = model.tokenize(text)
|
|
174
|
+
if model.detokenize(toks) != text:
|
|
175
|
+
raise ValueError("text does not survive tokenize/detokenize; not sendable as 0x08")
|
|
176
|
+
if len(toks) > MAX_TOKENS:
|
|
177
|
+
raise ValueError(f"{len(toks)} tokens is over the 0x08 cap of {MAX_TOKENS}")
|
|
178
|
+
enc = Encoder()
|
|
179
|
+
lg = model.reset(model.tokenize(open_codec.prime_for(tail)))
|
|
180
|
+
for t in toks:
|
|
181
|
+
enc.put_logits(lg, t)
|
|
182
|
+
lg = model.step(t)
|
|
183
|
+
head = bytearray([CODEC_BYTE]) + _check(text); _wv(len(toks), head)
|
|
184
|
+
return bytes(head) + enc.finish()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _decode_with(body, n, model, tail):
|
|
188
|
+
prime = model.tokenize(open_codec.prime_for(tail))
|
|
189
|
+
n_ctx = getattr(model, "n_ctx", None)
|
|
190
|
+
if n_ctx and len(prime) + n > n_ctx: # cannot be a frame this model encoded under this history
|
|
191
|
+
raise DecodeFailed(f"{len(prime)} prime + {n} message tokens is over the {n_ctx}-token context")
|
|
192
|
+
dec = Decoder(body)
|
|
193
|
+
lg = model.reset(prime)
|
|
194
|
+
toks = []
|
|
195
|
+
for _ in range(n):
|
|
196
|
+
t = dec.get_logits(lg)
|
|
197
|
+
toks.append(t)
|
|
198
|
+
lg = model.step(t)
|
|
199
|
+
return model.detokenize(toks)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def uncrunch(frame, model, tails=(), max_tails=MAX_TAILS):
|
|
203
|
+
"""0x08 frame -> text, or DecodeFailed. tails = candidate histories newest first (each a list of messages, oldest
|
|
204
|
+
first); the base prime (no history) is always tried last. The first candidate whose decode passes the check wins."""
|
|
205
|
+
if not frame or frame[0] != CODEC_BYTE:
|
|
206
|
+
raise DecodeFailed("not a 0x08 frame")
|
|
207
|
+
if len(frame) < 4:
|
|
208
|
+
raise DecodeFailed("frame too short")
|
|
209
|
+
chk = bytes(frame[1:3])
|
|
210
|
+
n, i = _rv(frame, 3)
|
|
211
|
+
body = bytes(frame[i:])
|
|
212
|
+
cands = [list(t)[-open_codec.TAIL_N:] for t in list(tails)[:max_tails]]
|
|
213
|
+
if [] not in cands:
|
|
214
|
+
cands.append([])
|
|
215
|
+
for tail in cands:
|
|
216
|
+
try:
|
|
217
|
+
text = _decode_with(body, n, model, tail)
|
|
218
|
+
except (ValueError, UnicodeError, RuntimeError): # over the context, an engine error, or a stream the
|
|
219
|
+
continue # detokenizer cannot render: this candidate does not match
|
|
220
|
+
if _check(text) == chk:
|
|
221
|
+
return text
|
|
222
|
+
raise DecodeFailed("plaintext check failed under every candidate history: wrong decode, not shown")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------------------------------------------------
|
|
226
|
+
# models
|
|
227
|
+
# ---------------------------------------------------------------------------------------------------------------------
|
|
228
|
+
class LlamaModel:
|
|
229
|
+
"""llama.cpp as the stand-in engine. Exact ONLY when encoder and decoder run the same build on the same box with the
|
|
230
|
+
same eval path: the prime is evaluated as one batch, then every message token one at a time, on both sides
|
|
231
|
+
(measured 10-24: step-vs-step logits bit-identical even at 16 threads; batched-vs-stepped differ by up to 0.34)."""
|
|
232
|
+
|
|
233
|
+
def __init__(self, path=None, n_ctx=1024, n_threads=None):
|
|
234
|
+
from llama_cpp import Llama
|
|
235
|
+
path = path or open_codec.MODEL_PATH
|
|
236
|
+
self.n_ctx = n_ctx
|
|
237
|
+
self.L = Llama(model_path=path, n_ctx=n_ctx, n_threads=n_threads or max(1, (os.cpu_count() or 2) // 2),
|
|
238
|
+
logits_all=False, verbose=False)
|
|
239
|
+
self.n_vocab = self.L.n_vocab()
|
|
240
|
+
|
|
241
|
+
def tokenize(self, text):
|
|
242
|
+
return self.L.tokenize(text.encode(), add_bos=False, special=False)
|
|
243
|
+
|
|
244
|
+
def detokenize(self, ids):
|
|
245
|
+
return open_codec.detok(self.L, ids).decode("utf-8", errors="replace")
|
|
246
|
+
|
|
247
|
+
def _logits(self):
|
|
248
|
+
return np.ctypeslib.as_array(self.L._ctx.get_logits(), shape=(self.n_vocab,)).copy()
|
|
249
|
+
|
|
250
|
+
def reset(self, prime_ids):
|
|
251
|
+
self.L.reset(); self.L.eval(list(prime_ids)); return self._logits()
|
|
252
|
+
|
|
253
|
+
def step(self, tok):
|
|
254
|
+
self.L.eval([int(tok)]); return self._logits()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class DetlmModel:
|
|
258
|
+
"""detlm (detlm.c via ctypes, libdetlm.so): the bit-identical engine — the SAME logits on every CPU it is built
|
|
259
|
+
for, so frames encoded on one machine decode on another exactly. Tokenizer: llama.cpp's, vocab-only load (integer,
|
|
260
|
+
deterministic, no weights). Call pattern, kept identical to the C detcodec on both ends: the prime as ONE batch
|
|
261
|
+
(logits of its last token), then every message token one at a time."""
|
|
262
|
+
|
|
263
|
+
def __init__(self, path=None, n_ctx=2048, n_threads=4, so=None):
|
|
264
|
+
from llama_cpp import Llama
|
|
265
|
+
path = path or open_codec.MODEL_PATH
|
|
266
|
+
self.tok = Llama(model_path=path, vocab_only=True, verbose=False)
|
|
267
|
+
self.L = ctypes.CDLL(so or os.path.join(HERE, "libdetlm.so"))
|
|
268
|
+
L = self.L; i32p, f32p = ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(ctypes.c_float)
|
|
269
|
+
L.detlm_load.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int]; L.detlm_load.restype = ctypes.c_void_p
|
|
270
|
+
L.detlm_free.argtypes = [ctypes.c_void_p]
|
|
271
|
+
L.detlm_reset.argtypes = [ctypes.c_void_p]
|
|
272
|
+
L.detlm_n_vocab.argtypes = [ctypes.c_void_p]; L.detlm_n_vocab.restype = ctypes.c_int
|
|
273
|
+
L.detlm_n_pos.argtypes = [ctypes.c_void_p]; L.detlm_n_pos.restype = ctypes.c_int
|
|
274
|
+
L.detlm_n_ctx.argtypes = [ctypes.c_void_p]; L.detlm_n_ctx.restype = ctypes.c_int
|
|
275
|
+
L.detlm_eval.argtypes = [ctypes.c_void_p, i32p, ctypes.c_int, ctypes.c_int, f32p]; L.detlm_eval.restype = ctypes.c_int
|
|
276
|
+
L.detlm_version.restype = ctypes.c_char_p
|
|
277
|
+
self.h = L.detlm_load(path.encode(), int(n_ctx), int(n_threads))
|
|
278
|
+
if not self.h:
|
|
279
|
+
raise RuntimeError(f"detlm_load failed on {path}")
|
|
280
|
+
self.n_vocab = L.detlm_n_vocab(self.h)
|
|
281
|
+
self.n_ctx = L.detlm_n_ctx(self.h)
|
|
282
|
+
self.version = L.detlm_version().decode()
|
|
283
|
+
|
|
284
|
+
def tokenize(self, text):
|
|
285
|
+
return self.tok.tokenize(text.encode(), add_bos=False, special=False)
|
|
286
|
+
|
|
287
|
+
def detokenize(self, ids):
|
|
288
|
+
return open_codec.detok(self.tok, ids).decode("utf-8", errors="replace")
|
|
289
|
+
|
|
290
|
+
def _eval(self, ids):
|
|
291
|
+
a = np.ascontiguousarray(ids, dtype=np.int32)
|
|
292
|
+
lg = np.empty(self.n_vocab, np.float32)
|
|
293
|
+
if self.L.detlm_eval(self.h, a.ctypes.data_as(ctypes.POINTER(ctypes.c_int32)), len(a), 1,
|
|
294
|
+
lg.ctypes.data_as(ctypes.POINTER(ctypes.c_float))):
|
|
295
|
+
raise RuntimeError("detlm_eval failed (context full or bad token id)")
|
|
296
|
+
return lg
|
|
297
|
+
|
|
298
|
+
def reset(self, prime_ids):
|
|
299
|
+
self.L.detlm_reset(self.h); return self._eval(list(prime_ids))
|
|
300
|
+
|
|
301
|
+
def step(self, tok):
|
|
302
|
+
return self._eval([int(tok)])
|
|
303
|
+
|
|
304
|
+
def close(self):
|
|
305
|
+
if getattr(self, "h", None):
|
|
306
|
+
self.L.detlm_free(self.h); self.h = None
|
|
307
|
+
|
|
308
|
+
def __del__(self):
|
|
309
|
+
self.close()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class MockModel:
|
|
313
|
+
"""A deterministic fake: tokens are UTF-8 bytes (ids 0..255) inside a 151,936 vocabulary; logits come from a seeded
|
|
314
|
+
generator keyed on the WHOLE context (so different histories give different streams, as a real model would), with
|
|
315
|
+
a context-chosen sharpness so flat, peaky and floor cases all occur."""
|
|
316
|
+
n_vocab = 151936
|
|
317
|
+
n_ctx = None
|
|
318
|
+
|
|
319
|
+
def __init__(self, seed=20261024):
|
|
320
|
+
self.seed = seed; self.ctx = []
|
|
321
|
+
|
|
322
|
+
def tokenize(self, text):
|
|
323
|
+
return list(text.encode())
|
|
324
|
+
|
|
325
|
+
def detokenize(self, ids):
|
|
326
|
+
return bytes(i if i < 256 else 0x3F for i in ids).decode("utf-8", errors="replace") # garbage id -> '?'
|
|
327
|
+
|
|
328
|
+
def _logits(self):
|
|
329
|
+
key = hashlib.sha256(repr((self.seed, self.ctx)).encode()).digest()
|
|
330
|
+
rng = np.random.default_rng(int.from_bytes(key[:8], "little"))
|
|
331
|
+
scale = [0.0, 1.0, 4.0, 12.0, 40.0][key[8] % 5]
|
|
332
|
+
lg = (rng.standard_normal(self.n_vocab) * scale).astype(np.float32)
|
|
333
|
+
lg[:256] += 6.0 # bytes are likelier than the unused ids, as a real tokenizer would be
|
|
334
|
+
lg[key[9]] += 20.0 # one strong favourite per context
|
|
335
|
+
return lg
|
|
336
|
+
|
|
337
|
+
def reset(self, prime_ids):
|
|
338
|
+
self.ctx = list(prime_ids); return self._logits()
|
|
339
|
+
|
|
340
|
+
def step(self, tok):
|
|
341
|
+
self.ctx.append(int(tok)); return self._logits()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def ideal_bits(logits, sym):
|
|
345
|
+
"""-log2 softmax(logits)[sym] in float64 — reporting only, never on the coding path"""
|
|
346
|
+
u = np.asarray(logits, np.float64); u = u - u.max(); return float(np.log2(np.exp(u).sum()) - u[sym] / np.log(2))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
m = MockModel()
|
|
351
|
+
for t in ["Checking in. How goes it? Sound off.", "ok", "Line one.\n\nLine two, with a double space.\n"]:
|
|
352
|
+
f = crunch(t, m, tail=["earlier message"]); back = uncrunch(f, m, tails=[["earlier message"]])
|
|
353
|
+
print(f"{len(t):4d} chars -> {len(f):4d} B exact={back == t}")
|