jejunepixels commited on
Commit
0e5fb97
·
verified ·
1 Parent(s): 26b18fe

Create embeddings.py

Browse files
Files changed (1) hide show
  1. embeddings.py +150 -0
embeddings.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Read individual embedding rows without loading an entire LLM into RAM."""
2
+ import json
3
+ import struct
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+
8
+
9
+ def embedding_name(name):
10
+ return any(s in name for s in ('token_embd.weight', 'embed_tokens.weight',
11
+ 'word_embeddings.weight', 'wte.weight', 'shared.weight',
12
+ 'tok_embeddings.weight', 'token_embedding.weight'))
13
+
14
+
15
+ class Model:
16
+ def __init__(self, location):
17
+ self.path = Path(location).expanduser().resolve()
18
+ self.tensors = {}
19
+ self.vocab = None
20
+ self.reader = None
21
+ if self.path.suffix.lower() == '.gguf':
22
+ from gguf import GGUFReader
23
+ self.reader = GGUFReader(str(self.path))
24
+ for t in self.reader.tensors:
25
+ if len(t.shape) == 2:
26
+ self.tensors[t.name] = ('gguf', t, tuple(int(v) for v in reversed(t.shape)))
27
+ field = self.reader.fields.get('tokenizer.ggml.tokens')
28
+ if field is not None:
29
+ self.vocab = field.contents()
30
+ else:
31
+ files = sorted(self.path.rglob('*.safetensors')) if self.path.is_dir() else [self.path]
32
+ if not files:
33
+ raise ValueError('No safetensors files found in that folder.')
34
+ for file in files:
35
+ if file.suffix != '.safetensors':
36
+ raise ValueError('Choose a GGUF file, safetensors file, or model folder.')
37
+ with file.open('rb') as f:
38
+ raw = f.read(8)
39
+ if len(raw) != 8:
40
+ raise ValueError('Invalid safetensors header.')
41
+ size = struct.unpack('<Q', raw)[0]
42
+ if size > min(file.stat().st_size - 8, 100_000_000):
43
+ raise ValueError('Invalid safetensors header size.')
44
+ header = json.loads(f.read(size))
45
+ for name, info in header.items():
46
+ if name != '__metadata__' and len(info['shape']) == 2:
47
+ self.tensors[name] = ('safe', (file, 8 + size, info), tuple(info['shape']))
48
+ if not self.tensors:
49
+ raise ValueError('No two-dimensional weight tensors found.')
50
+
51
+ def close(self):
52
+ """Release the GGUF mapping when this session unloads its model."""
53
+ reader = self.reader
54
+ self.reader = None
55
+ self.tensors.clear()
56
+ self.vocab = None
57
+ if reader is not None:
58
+ reader.tensors.clear()
59
+ reader.fields.clear()
60
+ reader.data._mmap.close()
61
+
62
+ def row(self, name, token_id):
63
+ return self.rows(name, token_id, token_id + 1)[0]
64
+
65
+ def rows(self, name, start, stop):
66
+ """Read a bounded contiguous batch, dequantizing only these rows."""
67
+ kind, source, shape = self.tensors[name]
68
+ if not 0 <= start < stop <= shape[0]:
69
+ raise ValueError(f'Token range must be within 0 to {shape[0] - 1}.')
70
+ if kind == 'gguf':
71
+ from gguf.quants import dequantize
72
+ values = dequantize(source.data[start:stop], source.tensor_type)
73
+ return np.asarray(values, dtype=np.float32).reshape(stop - start, shape[1]).copy()
74
+ file, base, info = source
75
+ types = {'F64': '<f8', 'F32': '<f4', 'F16': '<f2', 'BF16': '<u2'}
76
+ if info['dtype'] not in types:
77
+ raise ValueError(f"Unsupported safetensors dtype: {info['dtype']}. Packed quantized safetensors need conversion first.")
78
+ dtype = np.dtype(types[info['dtype']])
79
+ offset = base + info['data_offsets'][0] + start * shape[1] * dtype.itemsize
80
+ end = offset + (stop - start) * shape[1] * dtype.itemsize
81
+ if end > base + info['data_offsets'][1] or end > file.stat().st_size:
82
+ raise ValueError('Tensor data is truncated or its shape is invalid.')
83
+ values = np.memmap(file, mode='r', dtype=dtype, offset=offset, shape=(stop - start, shape[1]))
84
+ if info['dtype'] == 'BF16':
85
+ return (values.astype(np.uint32) << 16).view(np.float32)
86
+ return np.array(values, dtype=np.float32)
87
+
88
+
89
+ def nearest_tokens(read_rows, row_count, token_id, count=10, batch_size=2048, progress=None):
90
+ """Exact cosine search over all rows; skip the query, zero and nonfinite rows.
91
+
92
+ Memory is one embedding batch plus one scalar score per vocabulary row.
93
+ The callback receives (rows_scanned, total_rows).
94
+ """
95
+ if not 0 <= token_id < row_count:
96
+ raise ValueError('Source token ID is outside the vocabulary.')
97
+ query = read_rows(token_id, token_id + 1)[0]
98
+ return search_embeddings(read_rows, row_count, query, count=count, exclude_ids=[token_id],
99
+ batch_size=batch_size, progress=progress)['neighbours']
100
+
101
+
102
+ def search_embeddings(read_rows, row_count, query, count=10, exclude_ids=(), report_id=None,
103
+ batch_size=2048, progress=None):
104
+ """Search a supplied vector, optionally reporting a target's full-vocabulary rank."""
105
+ if count < 1 or batch_size < 1:
106
+ raise ValueError('Neighbour count and batch size must be positive.')
107
+ query = np.asarray(query, dtype=np.float64)
108
+ if query.ndim != 1:
109
+ raise ValueError('The query must be a one-dimensional vector.')
110
+ norm = np.linalg.norm(query)
111
+ if not np.isfinite(norm) or norm == 0:
112
+ raise ValueError('The query has a zero or nonfinite embedding, so cosine similarity is undefined. Choose another vector.')
113
+ query = query / norm
114
+ scores = np.full(row_count, -np.inf, dtype=np.float64)
115
+ for start in range(0, row_count, batch_size):
116
+ stop = min(start + batch_size, row_count)
117
+ values = np.asarray(read_rows(start, stop))
118
+ if values.shape != (stop - start, len(query)):
119
+ raise ValueError('Query dimensions do not match the embedding tensor.')
120
+ with np.errstate(invalid='ignore', over='ignore', divide='ignore'):
121
+ norms = np.sqrt(np.einsum('ij,ij->i', values, values, dtype=np.float64))
122
+ valid = np.isfinite(norms) & (norms > 0)
123
+ dot = np.einsum('ij,j->i', values, query, dtype=np.float64)
124
+ batch_scores = np.divide(dot, norms, out=np.full(stop - start, -np.inf), where=valid)
125
+ scores[start:stop] = np.where(valid & np.isfinite(batch_scores), np.clip(batch_scores, -1, 1), -np.inf)
126
+ if progress is not None:
127
+ progress(stop, row_count)
128
+ for i in exclude_ids:
129
+ if not isinstance(i, (int, np.integer)) or not 0 <= i < row_count:
130
+ raise ValueError('An excluded token ID is outside the vocabulary.')
131
+ scores[i] = -np.inf
132
+ candidates = np.flatnonzero(np.isfinite(scores))
133
+ # Stable tie-breaking by ID makes duplicate-vector results reproducible.
134
+ ordered = candidates[np.lexsort((candidates, -scores[candidates]))]
135
+ rank = None
136
+ if report_id is not None:
137
+ positions = np.flatnonzero(ordered == report_id)
138
+ rank = int(positions[0]) + 1 if len(positions) else None
139
+ return {'neighbours': [(int(i), float(scores[i])) for i in ordered[:count]],
140
+ 'expected_rank': rank,
141
+ 'expected_score': float(scores[report_id]) if rank is not None else None,
142
+ 'valid_count': len(ordered)}
143
+
144
+
145
+ def load_tokenizer(path):
146
+ from tokenizers import Tokenizer
147
+ p = Path(path).expanduser()
148
+ if p.is_dir():
149
+ p = p / 'tokenizer.json'
150
+ return Tokenizer.from_file(str(p))