mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 06:15:58 -10:00
quant_formats --hf: the header takes the file's size from the tree listing, so gguf_reader's bounds checks apply (#5075)
Approach from PR #5075. --hf failed on every repository with AttributeError: 'Header' object has no attribute '_size'; a cut or huge-length header is now a ValueError that names the file.
This commit is contained in:
1 parent
3bbf0f399e
commit
f374b5029d
2 files changed
+81
-3
No files matched your search
@@ -61,10 +61,14 @@ class RangeFile(io.RawIOBase):
|
||||
|
||||
|
||||
class Header(G.GGUFFile):
|
||||
def __init__(self, name: str, fh): # noqa: D107 (reuses GGUFFile's parser on any file-like)
|
||||
def __init__(self, name: str, fh, size: int): # noqa: D107 (reuses GGUFFile's parser on any file-like)
|
||||
self.path = pathlib.Path(name)
|
||||
self.metadata, self.tensors, self.version, self.alignment = {}, [], 0, 32
|
||||
self._parse(fh)
|
||||
self._size, self._pos = size, 0 # the parser's bounds checks need the whole file's size (#5075)
|
||||
try:
|
||||
self._parse(fh)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"{self.path.name}: {e}") from None
|
||||
|
||||
|
||||
def hf_headers(repo: str, rev: str, d: str) -> list[Header]:
|
||||
@@ -73,7 +77,7 @@ def hf_headers(repo: str, rev: str, d: str) -> list[Header]:
|
||||
for e in sorted(tree, key=lambda e: e["path"]):
|
||||
if e["type"] == "file" and e["path"].endswith(".gguf"):
|
||||
url = f"https://huggingface.co/{repo}/resolve/{rev}/{e['path']}"
|
||||
out.append(Header(e["path"], RangeFile(url)))
|
||||
out.append(Header(e["path"], RangeFile(url), int(e["size"])))
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""quant_formats.py --hf reads each shard's header over HTTP range requests with gguf_reader's bounds checks (#5075): the
|
||||
header knows the file's size from the Hugging Face tree, so a complete header parses and a cut one is refused with a
|
||||
ValueError that names the file. Over a minimal GGUF v3 header written here (no network, no model).
|
||||
|
||||
python -m unittest tools.test_quant_formats_hf
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import quant_formats as Q # noqa: E402
|
||||
|
||||
|
||||
def gstr(s: bytes) -> bytes:
|
||||
return struct.pack("<Q", len(s)) + s
|
||||
|
||||
|
||||
def shard() -> bytes:
|
||||
"""One Q8_0 [32, 4] tensor `blk.0.ffn_down_exps.weight` (type 8), data at offset 0 after a 32-byte alignment."""
|
||||
head = struct.pack("<IIQQ", 0x46554747, 3, 1, 0) + gstr(b"blk.0.ffn_down_exps.weight") + struct.pack("<I", 2)
|
||||
head += struct.pack("<QQ", 32, 4) + struct.pack("<IQ", 8, 0)
|
||||
head += bytes(-len(head) % 32)
|
||||
return head + bytes(4 * 34)
|
||||
|
||||
|
||||
class Response(io.BytesIO):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
|
||||
def serve(files: dict[str, bytes]):
|
||||
def urlopen(req, timeout=None):
|
||||
url = req if isinstance(req, str) else req.full_url
|
||||
if "/api/models/" in url:
|
||||
return Response(json.dumps([{"type": "file", "path": p, "size": len(b)} for p, b in files.items()]).encode())
|
||||
name = url.rsplit("/", 1)[1]
|
||||
lo, hi = map(int, re.match(r"bytes=([0-9]+)-([0-9]+)", req.get_header("Range")).groups())
|
||||
return Response(files[name][lo:hi + 1])
|
||||
return mock.patch.object(Q.urllib.request, "urlopen", urlopen)
|
||||
|
||||
|
||||
class HfHeaders(unittest.TestCase):
|
||||
def test_a_shard_header_is_read_over_range_requests(self):
|
||||
with serve({"m-00001-of-00001.gguf": shard()}):
|
||||
heads = Q.hf_headers("org/repo", "rev", "dir")
|
||||
self.assertEqual([t.name for t in heads[0].tensors], ["blk.0.ffn_down_exps.weight"])
|
||||
|
||||
def test_a_cut_header_is_refused_with_the_file_name(self):
|
||||
with serve({"m-00001-of-00001.gguf": shard()[:40]}):
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
Q.hf_headers("org/repo", "rev", "dir")
|
||||
self.assertIn("m-00001-of-00001.gguf", str(cm.exception))
|
||||
|
||||
def test_a_huge_name_length_is_refused_not_allocated(self):
|
||||
head = struct.pack("<IIQQ", 0x46554747, 3, 1, 0) + struct.pack("<Q", 2 ** 64 - 16) + bytes(32)
|
||||
with serve({"m.gguf": head}):
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
Q.hf_headers("org/repo", "rev", "dir")
|
||||
self.assertIn("m.gguf", str(cm.exception))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user