File size: 7,445 Bytes
0e5fb97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Read individual embedding rows without loading an entire LLM into RAM."""
import json
import struct
from pathlib import Path

import numpy as np


def embedding_name(name):
    return any(s in name for s in ('token_embd.weight', 'embed_tokens.weight',
                                  'word_embeddings.weight', 'wte.weight', 'shared.weight',
                                  'tok_embeddings.weight', 'token_embedding.weight'))


class Model:
    def __init__(self, location):
        self.path = Path(location).expanduser().resolve()
        self.tensors = {}
        self.vocab = None
        self.reader = None
        if self.path.suffix.lower() == '.gguf':
            from gguf import GGUFReader
            self.reader = GGUFReader(str(self.path))
            for t in self.reader.tensors:
                if len(t.shape) == 2:
                    self.tensors[t.name] = ('gguf', t, tuple(int(v) for v in reversed(t.shape)))
            field = self.reader.fields.get('tokenizer.ggml.tokens')
            if field is not None:
                self.vocab = field.contents()
        else:
            files = sorted(self.path.rglob('*.safetensors')) if self.path.is_dir() else [self.path]
            if not files:
                raise ValueError('No safetensors files found in that folder.')
            for file in files:
                if file.suffix != '.safetensors':
                    raise ValueError('Choose a GGUF file, safetensors file, or model folder.')
                with file.open('rb') as f:
                    raw = f.read(8)
                    if len(raw) != 8:
                        raise ValueError('Invalid safetensors header.')
                    size = struct.unpack('<Q', raw)[0]
                    if size > min(file.stat().st_size - 8, 100_000_000):
                        raise ValueError('Invalid safetensors header size.')
                    header = json.loads(f.read(size))
                for name, info in header.items():
                    if name != '__metadata__' and len(info['shape']) == 2:
                        self.tensors[name] = ('safe', (file, 8 + size, info), tuple(info['shape']))
        if not self.tensors:
            raise ValueError('No two-dimensional weight tensors found.')

    def close(self):
        """Release the GGUF mapping when this session unloads its model."""
        reader = self.reader
        self.reader = None
        self.tensors.clear()
        self.vocab = None
        if reader is not None:
            reader.tensors.clear()
            reader.fields.clear()
            reader.data._mmap.close()

    def row(self, name, token_id):
        return self.rows(name, token_id, token_id + 1)[0]

    def rows(self, name, start, stop):
        """Read a bounded contiguous batch, dequantizing only these rows."""
        kind, source, shape = self.tensors[name]
        if not 0 <= start < stop <= shape[0]:
            raise ValueError(f'Token range must be within 0 to {shape[0] - 1}.')
        if kind == 'gguf':
            from gguf.quants import dequantize
            values = dequantize(source.data[start:stop], source.tensor_type)
            return np.asarray(values, dtype=np.float32).reshape(stop - start, shape[1]).copy()
        file, base, info = source
        types = {'F64': '<f8', 'F32': '<f4', 'F16': '<f2', 'BF16': '<u2'}
        if info['dtype'] not in types:
            raise ValueError(f"Unsupported safetensors dtype: {info['dtype']}. Packed quantized safetensors need conversion first.")
        dtype = np.dtype(types[info['dtype']])
        offset = base + info['data_offsets'][0] + start * shape[1] * dtype.itemsize
        end = offset + (stop - start) * shape[1] * dtype.itemsize
        if end > base + info['data_offsets'][1] or end > file.stat().st_size:
            raise ValueError('Tensor data is truncated or its shape is invalid.')
        values = np.memmap(file, mode='r', dtype=dtype, offset=offset, shape=(stop - start, shape[1]))
        if info['dtype'] == 'BF16':
            return (values.astype(np.uint32) << 16).view(np.float32)
        return np.array(values, dtype=np.float32)


def nearest_tokens(read_rows, row_count, token_id, count=10, batch_size=2048, progress=None):
    """Exact cosine search over all rows; skip the query, zero and nonfinite rows.

    Memory is one embedding batch plus one scalar score per vocabulary row.
    The callback receives (rows_scanned, total_rows).
    """
    if not 0 <= token_id < row_count:
        raise ValueError('Source token ID is outside the vocabulary.')
    query = read_rows(token_id, token_id + 1)[0]
    return search_embeddings(read_rows, row_count, query, count=count, exclude_ids=[token_id],
                             batch_size=batch_size, progress=progress)['neighbours']


def search_embeddings(read_rows, row_count, query, count=10, exclude_ids=(), report_id=None,
                      batch_size=2048, progress=None):
    """Search a supplied vector, optionally reporting a target's full-vocabulary rank."""
    if count < 1 or batch_size < 1:
        raise ValueError('Neighbour count and batch size must be positive.')
    query = np.asarray(query, dtype=np.float64)
    if query.ndim != 1:
        raise ValueError('The query must be a one-dimensional vector.')
    norm = np.linalg.norm(query)
    if not np.isfinite(norm) or norm == 0:
        raise ValueError('The query has a zero or nonfinite embedding, so cosine similarity is undefined. Choose another vector.')
    query = query / norm
    scores = np.full(row_count, -np.inf, dtype=np.float64)
    for start in range(0, row_count, batch_size):
        stop = min(start + batch_size, row_count)
        values = np.asarray(read_rows(start, stop))
        if values.shape != (stop - start, len(query)):
            raise ValueError('Query dimensions do not match the embedding tensor.')
        with np.errstate(invalid='ignore', over='ignore', divide='ignore'):
            norms = np.sqrt(np.einsum('ij,ij->i', values, values, dtype=np.float64))
            valid = np.isfinite(norms) & (norms > 0)
            dot = np.einsum('ij,j->i', values, query, dtype=np.float64)
            batch_scores = np.divide(dot, norms, out=np.full(stop - start, -np.inf), where=valid)
            scores[start:stop] = np.where(valid & np.isfinite(batch_scores), np.clip(batch_scores, -1, 1), -np.inf)
        if progress is not None:
            progress(stop, row_count)
    for i in exclude_ids:
        if not isinstance(i, (int, np.integer)) or not 0 <= i < row_count:
            raise ValueError('An excluded token ID is outside the vocabulary.')
        scores[i] = -np.inf
    candidates = np.flatnonzero(np.isfinite(scores))
    # Stable tie-breaking by ID makes duplicate-vector results reproducible.
    ordered = candidates[np.lexsort((candidates, -scores[candidates]))]
    rank = None
    if report_id is not None:
        positions = np.flatnonzero(ordered == report_id)
        rank = int(positions[0]) + 1 if len(positions) else None
    return {'neighbours': [(int(i), float(scores[i])) for i in ordered[:count]],
            'expected_rank': rank,
            'expected_score': float(scores[report_id]) if rank is not None else None,
            'valid_count': len(ordered)}


def load_tokenizer(path):
    from tokenizers import Tokenizer
    p = Path(path).expanduser()
    if p.is_dir():
        p = p / 'tokenizer.json'
    return Tokenizer.from_file(str(p))