ggufkit 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.
- ggufkit/__init__.py +12 -0
- ggufkit/__main__.py +5 -0
- ggufkit/_util.py +75 -0
- ggufkit/canon.py +200 -0
- ggufkit/cli.py +80 -0
- ggufkit/dequant.py +70 -0
- ggufkit/diff.py +462 -0
- ggufkit/fingerprint.py +263 -0
- ggufkit/manifest.py +556 -0
- ggufkit/model.py +301 -0
- ggufkit/py.typed +0 -0
- ggufkit/stitch.py +163 -0
- ggufkit/verify.py +426 -0
- ggufkit/viz/__init__.py +28 -0
- ggufkit/viz/errmap.py +374 -0
- ggufkit/viz/layers.py +410 -0
- ggufkit/viz/palette.py +71 -0
- ggufkit/viz/render.py +408 -0
- ggufkit-0.1.0.dist-info/METADATA +417 -0
- ggufkit-0.1.0.dist-info/RECORD +23 -0
- ggufkit-0.1.0.dist-info/WHEEL +4 -0
- ggufkit-0.1.0.dist-info/entry_points.txt +2 -0
- ggufkit-0.1.0.dist-info/licenses/LICENSE +22 -0
ggufkit/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""ggufkit: fingerprint, verify and visualize GGUF models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = version("ggufkit")
|
|
9
|
+
except PackageNotFoundError: # pragma: no cover - only when running from a bare checkout
|
|
10
|
+
__version__ = "0.0.0+unknown"
|
|
11
|
+
|
|
12
|
+
__all__ = ["__version__"]
|
ggufkit/__main__.py
ADDED
ggufkit/_util.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Small helpers shared by the commands: safe output files and argument checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def prepare_output(path: Path, *, force: bool) -> None:
|
|
17
|
+
"""Fail early, before any expensive work, if ``path`` cannot or must not be written."""
|
|
18
|
+
if path.exists() and not force:
|
|
19
|
+
msg = f"{path} already exists (use --force to overwrite)"
|
|
20
|
+
raise FileExistsError(msg)
|
|
21
|
+
parent = path.parent if str(path.parent) else Path()
|
|
22
|
+
if not parent.is_dir():
|
|
23
|
+
msg = f"{parent} is not a directory"
|
|
24
|
+
raise FileNotFoundError(msg)
|
|
25
|
+
if not os.access(parent, os.W_OK):
|
|
26
|
+
msg = f"cannot write to {parent}"
|
|
27
|
+
raise PermissionError(msg)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@contextmanager
|
|
31
|
+
def atomic_output(path: Path) -> Iterator[Path]:
|
|
32
|
+
"""Yield a temporary path next to ``path``; move it into place only if the block succeeds.
|
|
33
|
+
|
|
34
|
+
An interrupted or failed write leaves ``path`` untouched instead of half-written.
|
|
35
|
+
"""
|
|
36
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
37
|
+
try:
|
|
38
|
+
yield tmp
|
|
39
|
+
tmp.replace(path)
|
|
40
|
+
finally:
|
|
41
|
+
tmp.unlink(missing_ok=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write_text_atomic(path: Path, text: str) -> None:
|
|
45
|
+
with atomic_output(path) as tmp:
|
|
46
|
+
tmp.write_text(text, encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def positive_int(value: str) -> int:
|
|
50
|
+
n = int(value)
|
|
51
|
+
if n < 1:
|
|
52
|
+
msg = f"must be at least 1, got {value}"
|
|
53
|
+
raise argparse.ArgumentTypeError(msg)
|
|
54
|
+
return n
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def non_negative_int(value: str) -> int:
|
|
58
|
+
n = int(value)
|
|
59
|
+
if n < 0:
|
|
60
|
+
msg = f"must not be negative, got {value}"
|
|
61
|
+
raise argparse.ArgumentTypeError(msg)
|
|
62
|
+
return n
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def positive_float(value: str) -> float:
|
|
66
|
+
x = float(value)
|
|
67
|
+
if not x > 0:
|
|
68
|
+
msg = f"must be greater than 0, got {value}"
|
|
69
|
+
raise argparse.ArgumentTypeError(msg)
|
|
70
|
+
return x
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def to_stderr(message: str) -> None:
|
|
74
|
+
"""Progress and warnings go to stderr, so stdout carries only results."""
|
|
75
|
+
print(message, file=sys.stderr)
|
ggufkit/canon.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Canonical byte encodings behind the ``ggufkit-fp/1`` fingerprint specification.
|
|
2
|
+
|
|
3
|
+
Everything here is deliberately boring: fixed-width little-endian integers, SHA-256, and no
|
|
4
|
+
dependence on where in the file a value happened to be stored. The full specification lives in
|
|
5
|
+
``docs/FINGERPRINT.md``; this module is its reference implementation.
|
|
6
|
+
|
|
7
|
+
Why not just hash the file? Because two GGUF files with identical weights routinely differ in
|
|
8
|
+
bytes: metadata gets reordered or edited by ``gguf-set-metadata``, a model gets split into
|
|
9
|
+
shards, alignment padding changes. A content hash that ignores all of that lets you answer
|
|
10
|
+
"is this the same model?" and "did only the metadata change?" without re-downloading anything.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import struct
|
|
16
|
+
import sys
|
|
17
|
+
from hashlib import sha256
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
from gguf import GGUFEndian, GGUFValueType
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Iterable
|
|
25
|
+
|
|
26
|
+
from gguf.gguf_reader import ReaderField, ReaderTensor
|
|
27
|
+
|
|
28
|
+
SPEC_ID = "ggufkit-fp/1"
|
|
29
|
+
|
|
30
|
+
#: Tensor data is hashed in slices of this many bytes so multi-gigabyte tensors never get copied.
|
|
31
|
+
CHUNK_SIZE = 64 << 20
|
|
32
|
+
|
|
33
|
+
_KV_TAG = b"kv\0"
|
|
34
|
+
_TI_TAG = b"ti\0"
|
|
35
|
+
_WT_TAG = b"wt\0"
|
|
36
|
+
|
|
37
|
+
_SCALAR_TYPES = frozenset(
|
|
38
|
+
{
|
|
39
|
+
GGUFValueType.UINT8,
|
|
40
|
+
GGUFValueType.INT8,
|
|
41
|
+
GGUFValueType.UINT16,
|
|
42
|
+
GGUFValueType.INT16,
|
|
43
|
+
GGUFValueType.UINT32,
|
|
44
|
+
GGUFValueType.INT32,
|
|
45
|
+
GGUFValueType.FLOAT32,
|
|
46
|
+
GGUFValueType.UINT64,
|
|
47
|
+
GGUFValueType.INT64,
|
|
48
|
+
GGUFValueType.FLOAT64,
|
|
49
|
+
GGUFValueType.BOOL,
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class UnsupportedModelError(ValueError):
|
|
55
|
+
"""Raised for files whose canonical form ggufkit cannot compute yet."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _u32(value: int) -> bytes:
|
|
59
|
+
return struct.pack("<I", value)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _u64(value: int) -> bytes:
|
|
63
|
+
return struct.pack("<Q", value)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _little_endian_bytes(arr: np.ndarray) -> bytes:
|
|
67
|
+
"""Raw bytes of ``arr`` with multi-byte values in little-endian order."""
|
|
68
|
+
if arr.dtype.byteorder == ">" or (arr.dtype.byteorder == "=" and sys.byteorder == "big"):
|
|
69
|
+
arr = arr.astype(arr.dtype.newbyteorder("<"))
|
|
70
|
+
return arr.tobytes()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _encode_value(parts: list[np.ndarray], cursor: int, vtype: GGUFValueType) -> tuple[bytes, int]:
|
|
74
|
+
"""Encode the value starting at ``parts[cursor]`` and return it with the next cursor.
|
|
75
|
+
|
|
76
|
+
The reader stores a field's parts in file order, so walking them with a cursor rebuilds
|
|
77
|
+
the exact structure of the value, including nested arrays which the reader otherwise
|
|
78
|
+
flattens.
|
|
79
|
+
"""
|
|
80
|
+
if vtype == GGUFValueType.STRING:
|
|
81
|
+
data = parts[cursor + 1]
|
|
82
|
+
return _u64(int(data.nbytes)) + data.tobytes(), cursor + 2
|
|
83
|
+
if vtype in _SCALAR_TYPES:
|
|
84
|
+
arr = parts[cursor]
|
|
85
|
+
if vtype == GGUFValueType.BOOL:
|
|
86
|
+
return struct.pack("<B", 1 if bool(arr[0]) else 0), cursor + 1
|
|
87
|
+
return _little_endian_bytes(arr), cursor + 1
|
|
88
|
+
if vtype == GGUFValueType.ARRAY:
|
|
89
|
+
item_type = GGUFValueType(int(parts[cursor][0]))
|
|
90
|
+
count = int(parts[cursor + 1][0])
|
|
91
|
+
cursor += 2
|
|
92
|
+
out = bytearray(_u32(int(item_type)) + _u64(count))
|
|
93
|
+
for _ in range(count):
|
|
94
|
+
chunk, cursor = _encode_value(parts, cursor, item_type)
|
|
95
|
+
out += chunk
|
|
96
|
+
return bytes(out), cursor
|
|
97
|
+
msg = f"Unknown GGUF value type {vtype!r}"
|
|
98
|
+
raise UnsupportedModelError(msg)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def encode_kv(field: ReaderField) -> bytes:
|
|
102
|
+
"""Canonical encoding of one metadata entry: tag, name, type, value."""
|
|
103
|
+
if len(field.parts) < 3 or not field.types:
|
|
104
|
+
msg = f"Field {field.name!r} is not a metadata entry"
|
|
105
|
+
raise ValueError(msg)
|
|
106
|
+
name = field.name.encode("utf-8")
|
|
107
|
+
vtype = field.types[0]
|
|
108
|
+
payload, end = _encode_value(field.parts, 3, vtype)
|
|
109
|
+
if end != len(field.parts):
|
|
110
|
+
msg = f"Field {field.name!r}: consumed {end} of {len(field.parts)} parts"
|
|
111
|
+
raise UnsupportedModelError(msg)
|
|
112
|
+
return _KV_TAG + _u64(len(name)) + name + _u32(int(vtype)) + payload
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def encode_tensor_info(tensor: ReaderTensor) -> bytes:
|
|
116
|
+
"""Canonical encoding of a tensor's descriptor: tag, name, dims in GGUF order, type.
|
|
117
|
+
|
|
118
|
+
The file offset is left out on purpose: it changes whenever anything before the tensor
|
|
119
|
+
changes, and it says nothing about the tensor.
|
|
120
|
+
"""
|
|
121
|
+
name = tensor.name.encode("utf-8")
|
|
122
|
+
dims = [int(d) for d in tensor.shape]
|
|
123
|
+
return (
|
|
124
|
+
_TI_TAG
|
|
125
|
+
+ _u64(len(name))
|
|
126
|
+
+ name
|
|
127
|
+
+ _u32(len(dims))
|
|
128
|
+
+ b"".join(_u64(d) for d in dims)
|
|
129
|
+
+ _u32(int(tensor.tensor_type))
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def kv_digest(field: ReaderField) -> bytes:
|
|
134
|
+
return sha256(encode_kv(field)).digest()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def tensor_info_digest(tensor: ReaderTensor) -> bytes:
|
|
138
|
+
return sha256(encode_tensor_info(tensor)).digest()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def is_element_type(tensor: ReaderTensor) -> bool:
|
|
142
|
+
"""Return whether the reader exposes the tensor with a real dtype (F32, F16, F64, I8..I64)."""
|
|
143
|
+
return tensor.data.dtype != np.uint8
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def tensor_data_digest(
|
|
147
|
+
tensor: ReaderTensor,
|
|
148
|
+
file_endianess: GGUFEndian,
|
|
149
|
+
chunk_size: int = CHUNK_SIZE,
|
|
150
|
+
) -> bytes:
|
|
151
|
+
"""SHA-256 of the tensor's stored bytes, normalized to little-endian for element types.
|
|
152
|
+
|
|
153
|
+
Quantized blocks mix scales and packed values, so their byte order cannot be normalized
|
|
154
|
+
without decoding the block layout. Big-endian quantized files are refused rather than
|
|
155
|
+
silently fingerprinted differently; convert them with llama.cpp's ``gguf-convert-endian``.
|
|
156
|
+
"""
|
|
157
|
+
data = tensor.data
|
|
158
|
+
if is_element_type(tensor):
|
|
159
|
+
if data.dtype.byteorder == ">" or (data.dtype.byteorder == "=" and sys.byteorder == "big"):
|
|
160
|
+
data = data.astype(data.dtype.newbyteorder("<"))
|
|
161
|
+
elif file_endianess == GGUFEndian.BIG:
|
|
162
|
+
msg = (
|
|
163
|
+
f"Tensor {tensor.name!r} is {tensor.tensor_type.name} in a big-endian file; "
|
|
164
|
+
"convert the file to little-endian first (gguf-convert-endian)"
|
|
165
|
+
)
|
|
166
|
+
raise UnsupportedModelError(msg)
|
|
167
|
+
|
|
168
|
+
flat = np.ascontiguousarray(data).reshape(-1).view(np.uint8)
|
|
169
|
+
digest = sha256()
|
|
170
|
+
for start in range(0, flat.size, chunk_size):
|
|
171
|
+
digest.update(flat[start : start + chunk_size])
|
|
172
|
+
return digest.digest()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def encode_weight(name: str, data_digest: bytes) -> bytes:
|
|
176
|
+
"""Canonical encoding of one tensor's data under its name: tag, name, data digest.
|
|
177
|
+
|
|
178
|
+
Binding the name to the bytes is what makes swapping the data of two tensors with the
|
|
179
|
+
same shape and type show up in ``weights``.
|
|
180
|
+
"""
|
|
181
|
+
raw = name.encode("utf-8")
|
|
182
|
+
return _WT_TAG + _u64(len(raw)) + raw + data_digest
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def weight_digest(name: str, data_digest: bytes) -> bytes:
|
|
186
|
+
return sha256(encode_weight(name, data_digest)).digest()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def combine(digests: Iterable[bytes]) -> bytes:
|
|
190
|
+
"""Hash a sequence of digests in the order given."""
|
|
191
|
+
return sha256(b"".join(digests)).digest()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def overall_digest(kv: bytes, tensor_info: bytes, weights: bytes) -> bytes:
|
|
195
|
+
return sha256(SPEC_ID.encode("ascii") + b"\0" + kv + tensor_info + weights).digest()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def sorted_by_name(names: Iterable[str]) -> list[str]:
|
|
199
|
+
"""Names in byte-wise UTF-8 order, which is what the spec requires."""
|
|
200
|
+
return sorted(names, key=lambda s: s.encode("utf-8"))
|
ggufkit/cli.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Command-line entry point: ``ggufkit <command> ...``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from ggufkit import __version__
|
|
12
|
+
from ggufkit import diff as diff_cmd
|
|
13
|
+
from ggufkit import fingerprint as fingerprint_cmd
|
|
14
|
+
from ggufkit import manifest as manifest_cmd
|
|
15
|
+
from ggufkit import stitch as stitch_cmd
|
|
16
|
+
from ggufkit import verify as verify_cmd
|
|
17
|
+
from ggufkit import viz as viz_cmd
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
|
|
22
|
+
EXIT_OK = 0
|
|
23
|
+
EXIT_MISMATCH = 1
|
|
24
|
+
EXIT_ERROR = 2
|
|
25
|
+
EXIT_BROKEN_PIPE = 141
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="ggufkit",
|
|
31
|
+
description="Fingerprint, verify and visualize GGUF models.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument("--version", action="version", version=f"ggufkit {__version__}")
|
|
34
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
35
|
+
fingerprint_cmd.add_parser(sub)
|
|
36
|
+
manifest_cmd.add_parser(sub)
|
|
37
|
+
verify_cmd.add_parser(sub)
|
|
38
|
+
diff_cmd.add_parser(sub)
|
|
39
|
+
stitch_cmd.add_parser(sub)
|
|
40
|
+
viz_cmd.add_parser(sub)
|
|
41
|
+
return parser
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
45
|
+
parser = build_parser()
|
|
46
|
+
args = parser.parse_args(argv)
|
|
47
|
+
func = getattr(args, "func", None)
|
|
48
|
+
if func is None:
|
|
49
|
+
parser.print_help()
|
|
50
|
+
return EXIT_ERROR
|
|
51
|
+
try:
|
|
52
|
+
return int(func(args))
|
|
53
|
+
except BrokenPipeError:
|
|
54
|
+
# The reader went away (``ggufkit ... | head``): stop quietly, like other Unix tools.
|
|
55
|
+
return EXIT_BROKEN_PIPE
|
|
56
|
+
except (ValueError, OSError, re.error) as e:
|
|
57
|
+
# ValueError covers ggufkit's own errors (bad models, manifests, arguments).
|
|
58
|
+
print(f"error: {e}", file=sys.stderr)
|
|
59
|
+
return EXIT_ERROR
|
|
60
|
+
except KeyboardInterrupt:
|
|
61
|
+
print("interrupted", file=sys.stderr)
|
|
62
|
+
return 130
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def entry() -> None: # pragma: no cover - exercised by the installed ``ggufkit`` script
|
|
66
|
+
"""Console-script entry point: :func:`main`, plus a quiet exit when stdout is closed."""
|
|
67
|
+
try:
|
|
68
|
+
code = main()
|
|
69
|
+
sys.stdout.flush()
|
|
70
|
+
except BrokenPipeError:
|
|
71
|
+
code = EXIT_BROKEN_PIPE
|
|
72
|
+
if code == EXIT_BROKEN_PIPE:
|
|
73
|
+
# Point stdout at /dev/null so the interpreter's final flush cannot fail again.
|
|
74
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
75
|
+
os.dup2(devnull, sys.stdout.fileno())
|
|
76
|
+
sys.exit(code)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__": # pragma: no cover
|
|
80
|
+
entry()
|
ggufkit/dequant.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Turn any tensor ggufkit can read into float32, reusing llama.cpp's own dequantizers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import cache
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from gguf import GGMLQuantizationType, GGUFEndian, quants
|
|
10
|
+
from gguf.constants import GGML_QUANT_SIZES
|
|
11
|
+
|
|
12
|
+
from ggufkit.canon import UnsupportedModelError, is_element_type
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from gguf.gguf_reader import ReaderTensor
|
|
16
|
+
|
|
17
|
+
ELEMENT_TYPES = frozenset(
|
|
18
|
+
{
|
|
19
|
+
GGMLQuantizationType.F32,
|
|
20
|
+
GGMLQuantizationType.F16,
|
|
21
|
+
GGMLQuantizationType.F64,
|
|
22
|
+
GGMLQuantizationType.I8,
|
|
23
|
+
GGMLQuantizationType.I16,
|
|
24
|
+
GGMLQuantizationType.I32,
|
|
25
|
+
GGMLQuantizationType.I64,
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@cache
|
|
31
|
+
def can_dequantize(qtype: GGMLQuantizationType) -> bool:
|
|
32
|
+
"""Whether the installed ``gguf`` package knows how to decode ``qtype``."""
|
|
33
|
+
if qtype in ELEMENT_TYPES:
|
|
34
|
+
return True
|
|
35
|
+
if qtype not in GGML_QUANT_SIZES:
|
|
36
|
+
return False
|
|
37
|
+
_block, type_size = GGML_QUANT_SIZES[qtype]
|
|
38
|
+
try:
|
|
39
|
+
quants.dequantize(np.zeros((1, type_size), dtype=np.uint8), qtype)
|
|
40
|
+
except NotImplementedError:
|
|
41
|
+
return False
|
|
42
|
+
except Exception: # noqa: BLE001 - zero blocks are not always valid, but the path exists
|
|
43
|
+
return True
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def to_f32(
|
|
48
|
+
tensor: ReaderTensor,
|
|
49
|
+
file_endianess: GGUFEndian = GGUFEndian.LITTLE,
|
|
50
|
+
rows: slice | None = None,
|
|
51
|
+
) -> np.ndarray:
|
|
52
|
+
"""Decode ``tensor`` (or a slice of its leading axis) to a float32 array.
|
|
53
|
+
|
|
54
|
+
The result has numpy shape, which is the reverse of the GGUF ``ne`` order: a
|
|
55
|
+
``[n_embd, n_vocab]`` GGUF tensor comes back as ``(n_vocab, n_embd)``. Slicing by rows
|
|
56
|
+
keeps memory bounded on multi-gigabyte tensors: quantization blocks never span rows.
|
|
57
|
+
"""
|
|
58
|
+
data = tensor.data if rows is None else tensor.data[rows]
|
|
59
|
+
if is_element_type(tensor):
|
|
60
|
+
return data.astype(np.float32, copy=False)
|
|
61
|
+
if file_endianess == GGUFEndian.BIG:
|
|
62
|
+
msg = (
|
|
63
|
+
f"Tensor {tensor.name!r} is {tensor.tensor_type.name} in a big-endian file; "
|
|
64
|
+
"convert the file to little-endian first (gguf-convert-endian)"
|
|
65
|
+
)
|
|
66
|
+
raise UnsupportedModelError(msg)
|
|
67
|
+
if not can_dequantize(tensor.tensor_type):
|
|
68
|
+
msg = f"The installed gguf package cannot dequantize {tensor.tensor_type.name}"
|
|
69
|
+
raise UnsupportedModelError(msg)
|
|
70
|
+
return quants.dequantize(data, tensor.tensor_type)
|