dfrokido commited on
Commit
7b71a1a
·
verified ·
1 Parent(s): aa27d87

Upload e8_utils.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. e8_utils.py +173 -0
e8_utils.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ e8_utils.py — fully self-contained E8 lattice utilities for the LatticeMemory HF Space.
3
+
4
+ No imports from liora_core. All math is inlined.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from itertools import combinations
10
+
11
+ import torch
12
+ import torch.nn.functional as F
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Core E8 lattice math
17
+ # ---------------------------------------------------------------------------
18
+
19
+ def _decode_d8(x: torch.Tensor) -> torch.Tensor:
20
+ """Round x to the nearest D8 lattice point (even-sum integer coordinates)."""
21
+ z = torch.round(x)
22
+ parity = z.sum(dim=-1) % 2
23
+ diff = x - z
24
+ worst = diff.abs().argmax(dim=-1)
25
+ adj = torch.sign(diff)
26
+ adj = torch.where(adj == 0, torch.ones_like(adj), adj)
27
+ mask = torch.zeros_like(x)
28
+ mask.scatter_(-1, worst.unsqueeze(-1), 1.0)
29
+ z_fixed = z + adj * mask
30
+ return torch.where(parity.unsqueeze(-1) == 1, z_fixed, z)
31
+
32
+
33
+ def _e8_nearest(x: torch.Tensor) -> torch.Tensor:
34
+ """Return the nearest E8 lattice point to x (batched, last dim = 8)."""
35
+ z0 = _decode_d8(x)
36
+ z1 = _decode_d8(x - 0.5) + 0.5
37
+ d0 = (x - z0).pow(2).sum(dim=-1, keepdim=True)
38
+ d1 = (x - z1).pow(2).sum(dim=-1, keepdim=True)
39
+ return torch.where(d0 <= d1, z0, z1)
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Shell-1 codebook (240 vectors, shape [240, 8])
44
+ # ---------------------------------------------------------------------------
45
+
46
+ def _build_shell1_codebook(device: torch.device = torch.device("cpu")) -> torch.Tensor:
47
+ """Build the 240-vector E8 shell-1 codebook.
48
+
49
+ E8 shell-1 consists of:
50
+ - 112 vectors of the form (±1, ±1, 0, 0, 0, 0, 0, 0) in all permutations
51
+ - 128 vectors of the form (±½, ±½, …, ±½) with an even number of minus signs
52
+ Total: 112 + 128 = 240 vectors.
53
+ """
54
+ vecs: list[list[float]] = []
55
+ # ±1 in two coordinates, rest 0
56
+ for i, j in combinations(range(8), 2):
57
+ for si in (1.0, -1.0):
58
+ for sj in (1.0, -1.0):
59
+ v = [0.0] * 8
60
+ v[i] = si
61
+ v[j] = sj
62
+ vecs.append(v)
63
+ # (±½)^8 with even number of minus signs
64
+ for mask in range(256):
65
+ signs = [1.0 if (mask >> bit) & 1 == 0 else -1.0 for bit in range(8)]
66
+ if signs.count(-1.0) % 2 == 0:
67
+ vecs.append([s * 0.5 for s in signs])
68
+ codebook = torch.tensor(vecs, dtype=torch.float32, device=device)
69
+ if codebook.shape != (240, 8):
70
+ raise RuntimeError(
71
+ f"expected E8 shell-1 codebook shape (240, 8), got {tuple(codebook.shape)}"
72
+ )
73
+ return codebook
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # RF-Snap: batch snap embeddings to E8
78
+ # ---------------------------------------------------------------------------
79
+
80
+ @torch.no_grad()
81
+ def nestquant_snap(embeddings: torch.Tensor) -> torch.Tensor:
82
+ """Snap a batch of embeddings [B, D] (D divisible by 8) to E8 lattice points.
83
+
84
+ Each 8-dim block is independently scaled, snapped to the nearest E8 point,
85
+ and rescaled back. The operation is a no-op in the limit of small beta.
86
+
87
+ Args:
88
+ embeddings: float32 tensor of shape [B, D].
89
+
90
+ Returns:
91
+ Snapped embeddings of shape [B, D].
92
+ """
93
+ if embeddings.dim() != 2:
94
+ raise ValueError(f"nestquant_snap expects [B, D], got {tuple(embeddings.shape)}")
95
+ B, D = embeddings.shape
96
+ if D % 8 != 0:
97
+ raise ValueError(f"D={D} must be divisible by 8")
98
+
99
+ blocks = embeddings.float().reshape(B, D // 8, 8) # [B, n_blocks, 8]
100
+ beta = blocks.norm(p=2, dim=-1).clamp_min(1e-8) / math.sqrt(2.0) # [B, n_blocks]
101
+ snapped = _e8_nearest(blocks / beta.unsqueeze(-1)) # [B, n_blocks, 8]
102
+ return (snapped * beta.unsqueeze(-1)).reshape(B, D)
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # E8 address: encode a single embedding as a hex string
107
+ # ---------------------------------------------------------------------------
108
+
109
+ @torch.no_grad()
110
+ def embedding_to_e8_address(embedding: torch.Tensor, device: torch.device = torch.device("cpu")) -> str:
111
+ """Convert a single embedding vector to its E8 lattice address (hex string).
112
+
113
+ Each 8-dim block is mapped to an index in [0, 239] (the closest shell-1
114
+ codebook vector after unit-normalisation), then packed as a byte. The
115
+ resulting byte string is returned as a lowercase hex string.
116
+
117
+ Expected address length: (D // 8) * 2 hex chars. For D=1024 → 256 chars.
118
+
119
+ Args:
120
+ embedding: 1-D float tensor of shape [D].
121
+ device: torch device for codebook (CPU by default).
122
+
123
+ Returns:
124
+ Lowercase hex string of length D // 4.
125
+ """
126
+ if embedding.dim() != 1:
127
+ raise ValueError(f"embedding_to_e8_address expects 1-D tensor, got {tuple(embedding.shape)}")
128
+ D = embedding.numel()
129
+ if D % 8 != 0:
130
+ raise ValueError(f"D={D} must be divisible by 8")
131
+
132
+ codebook = _build_shell1_codebook(device) # [240, 8]
133
+
134
+ vector = embedding.float().to(device)
135
+ blocks = vector.reshape(-1, 8) # [n_blocks, 8]
136
+ beta = blocks.norm(p=2, dim=-1).clamp_min(1e-8) / math.sqrt(2.0) # [n_blocks]
137
+ snapped = _e8_nearest(blocks / beta.unsqueeze(-1)) # [n_blocks, 8]
138
+
139
+ # Unit-normalise snapped blocks and project onto codebook
140
+ snapped_unit = snapped / snapped.norm(dim=-1, keepdim=True).clamp_min(1e-8)
141
+ dots = snapped_unit @ codebook.T / math.sqrt(2.0) # [n_blocks, 240]
142
+ indices = dots.argmax(dim=-1) # [n_blocks], values in [0,239]
143
+
144
+ return bytes(indices.tolist()).hex()
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Index size comparison
149
+ # ---------------------------------------------------------------------------
150
+
151
+ def index_size_bytes(n_docs: int, d_model: int) -> dict[str, int]:
152
+ """Return byte counts for different index formats.
153
+
154
+ Keys:
155
+ float32 — raw fp32 storage (4 bytes/float)
156
+ int4 — 4-bit quantization (0.5 bytes/float)
157
+ rfsnap — RF-Snap E8 (3 bytes per 8-dim block: 1 byte index + 2 bytes fp16 scale)
158
+
159
+ Args:
160
+ n_docs: number of document vectors.
161
+ d_model: embedding dimension (must be divisible by 8).
162
+
163
+ Returns:
164
+ Dict with keys 'float32', 'int4', 'rfsnap' and integer byte values.
165
+ """
166
+ if d_model % 8 != 0:
167
+ raise ValueError(f"d_model={d_model} must be divisible by 8")
168
+ n_blocks = d_model // 8
169
+ return {
170
+ "float32": n_docs * d_model * 4,
171
+ "int4": n_docs * d_model // 2,
172
+ "rfsnap": n_docs * n_blocks * 3,
173
+ }